[Telemetry] Use header-based versioned APIs instead of path-based (#159839)

This commit is contained in:
Alejandro Fernández Haro 2023-08-12 23:20:06 +02:00 committed by GitHub
parent b336a195e0
commit 0284cc158d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 779 additions and 452 deletions

View file

@ -54,12 +54,12 @@ Once you set up the APM infrastructure, you can enable the APM agent and put {ki
*Prerequisites* {kib} logs are configured to be in {ecs-ref}/ecs-reference.html[ECS JSON] format to include tracing identifiers. *Prerequisites* {kib} logs are configured to be in {ecs-ref}/ecs-reference.html[ECS JSON] format to include tracing identifiers.
Open {kib} Logs and search for an operation you are interested in. Open {kib} Logs and search for an operation you are interested in.
For example, suppose you want to investigate the response times for queries to the `/api/telemetry/v2/clusters/_stats` {kib} endpoint. For example, suppose you want to investigate the response times for queries to the `/internal/telemetry/clusters/_stats` {kib} endpoint.
Open Kibana Logs and search for the HTTP server response for the endpoint. It looks similar to the following (some fields are omitted for brevity). Open Kibana Logs and search for the HTTP server response for the endpoint. It looks similar to the following (some fields are omitted for brevity).
[source,json] [source,json]
---- ----
{ {
"message":"POST /api/telemetry/v2/clusters/_stats 200 1014ms - 43.2KB", "message":"POST /internal/telemetry/clusters/_stats 200 1014ms - 43.2KB",
"log":{"level":"DEBUG","logger":"http.server.response"}, "log":{"level":"DEBUG","logger":"http.server.response"},
"trace":{"id":"9b99131a6f66587971ef085ef97dfd07"}, "trace":{"id":"9b99131a6f66587971ef085ef97dfd07"},
"transaction":{"id":"d0c5bbf14f5febca"} "transaction":{"id":"d0c5bbf14f5febca"}

View file

@ -6,6 +6,10 @@
* Side Public License, v 1. * Side Public License, v 1.
*/ */
import {
ELASTIC_HTTP_VERSION_HEADER,
X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
} from '@kbn/core-http-common';
import { setupIntegrationEnvironment, TestEnvironmentUtils } from '../../test_utils'; import { setupIntegrationEnvironment, TestEnvironmentUtils } from '../../test_utils';
describe('Files usage telemetry', () => { describe('Files usage telemetry', () => {
@ -45,7 +49,9 @@ describe('Files usage telemetry', () => {
]); ]);
const { body } = await request const { body } = await request
.post(root, '/api/telemetry/v2/clusters/_stats') .post(root, '/internal/telemetry/clusters/_stats')
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ unencrypted: true }); .send({ unencrypted: true });
expect(body[0].stats.stack_stats.kibana.plugins.files).toMatchInlineSnapshot(` expect(body[0].stats.stack_stats.kibana.plugins.files).toMatchInlineSnapshot(`

View file

@ -32,6 +32,7 @@
"@kbn/core-elasticsearch-server-mocks", "@kbn/core-elasticsearch-server-mocks",
"@kbn/core-saved-objects-server-mocks", "@kbn/core-saved-objects-server-mocks",
"@kbn/logging", "@kbn/logging",
"@kbn/core-http-common",
], ],
"exclude": [ "exclude": [
"target/**/*", "target/**/*",

View file

@ -6,7 +6,49 @@
* Side Public License, v 1. * Side Public License, v 1.
*/ */
const BASE_INTERNAL_PATH = '/internal/telemetry';
export const INTERNAL_VERSION = { version: '2' };
/** /**
* Fetch Telemetry Config * Fetch Telemetry Config
* @public Kept public and path-based because we know other Elastic products fetch the opt-in status via this endpoint.
*/ */
export const FetchTelemetryConfigRoute = '/api/telemetry/v2/config'; export const FetchTelemetryConfigRoutePathBasedV2 = '/api/telemetry/v2/config';
/**
* Fetch Telemetry Config
* @internal
*/
export const FetchTelemetryConfigRoute = `${BASE_INTERNAL_PATH}/config`;
/**
* GET/PUT Last reported date for Snapshot telemetry
* @internal
*/
export const LastReportedRoute = `${BASE_INTERNAL_PATH}/last_reported`;
/**
* Set user has seen notice
* @internal
*/
export const UserHasSeenNoticeRoute = `${BASE_INTERNAL_PATH}/userHasSeenNotice`;
/**
* Set opt-in/out status
* @internal
*/
export const OptInRoute = `${BASE_INTERNAL_PATH}/optIn`;
/**
* Fetch the Snapshot telemetry report
* @internal
*/
export const FetchSnapshotTelemetry = `${BASE_INTERNAL_PATH}/clusters/_stats`;
/**
* Get Opt-in stats
* @internal
* @deprecated
*/
export const GetOptInStatsRoutePathBasedV2 = '/api/telemetry/v2/clusters/_opt_in_stats';

View file

@ -8,4 +8,5 @@
export * from './latest'; export * from './latest';
export * as v1 from './v2'; // Just so v1 can also be used (but for some reason telemetry endpoints have always been v2 :shrug:)
export * as v2 from './v2'; export * as v2 from './v2';

View file

@ -24,7 +24,7 @@ import type { HomePublicPluginSetup } from '@kbn/home-plugin/public';
import { ElasticV3BrowserShipper } from '@kbn/analytics-shippers-elastic-v3-browser'; import { ElasticV3BrowserShipper } from '@kbn/analytics-shippers-elastic-v3-browser';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { FetchTelemetryConfigRoute } from '../common/routes'; import { FetchTelemetryConfigRoute, INTERNAL_VERSION } from '../common/routes';
import type { v2 } from '../common/types'; import type { v2 } from '../common/types';
import { TelemetrySender, TelemetryService, TelemetryNotifications } from './services'; import { TelemetrySender, TelemetryService, TelemetryNotifications } from './services';
import { renderWelcomeTelemetryNotice } from './render_welcome_telemetry_notice'; import { renderWelcomeTelemetryNotice } from './render_welcome_telemetry_notice';
@ -329,7 +329,7 @@ export class TelemetryPlugin
*/ */
private async fetchUpdatedConfig(http: HttpStart | HttpSetup): Promise<TelemetryPluginConfig> { private async fetchUpdatedConfig(http: HttpStart | HttpSetup): Promise<TelemetryPluginConfig> {
const { allowChangingOptInStatus, optIn, sendUsageFrom, telemetryNotifyUserAboutOptInDefault } = const { allowChangingOptInStatus, optIn, sendUsageFrom, telemetryNotifyUserAboutOptInDefault } =
await http.get<v2.FetchTelemetryConfigResponse>(FetchTelemetryConfigRoute); await http.get<v2.FetchTelemetryConfigResponse>(FetchTelemetryConfigRoute, INTERNAL_VERSION);
return { return {
...this.config, ...this.config,

View file

@ -10,6 +10,12 @@
/* eslint-disable dot-notation */ /* eslint-disable dot-notation */
import { mockTelemetryService } from '../mocks'; import { mockTelemetryService } from '../mocks';
import {
FetchSnapshotTelemetry,
INTERNAL_VERSION,
OptInRoute,
UserHasSeenNoticeRoute,
} from '../../common/routes';
describe('TelemetryService', () => { describe('TelemetryService', () => {
describe('fetchTelemetry', () => { describe('fetchTelemetry', () => {
@ -17,7 +23,8 @@ describe('TelemetryService', () => {
const telemetryService = mockTelemetryService(); const telemetryService = mockTelemetryService();
await telemetryService.fetchTelemetry(); await telemetryService.fetchTelemetry();
expect(telemetryService['http'].post).toBeCalledWith('/api/telemetry/v2/clusters/_stats', { expect(telemetryService['http'].post).toBeCalledWith(FetchSnapshotTelemetry, {
...INTERNAL_VERSION,
body: JSON.stringify({ unencrypted: false, refreshCache: false }), body: JSON.stringify({ unencrypted: false, refreshCache: false }),
}); });
}); });
@ -64,7 +71,8 @@ describe('TelemetryService', () => {
const optedIn = true; const optedIn = true;
await telemetryService.setOptIn(optedIn); await telemetryService.setOptIn(optedIn);
expect(telemetryService['http'].post).toBeCalledWith('/api/telemetry/v2/optIn', { expect(telemetryService['http'].post).toBeCalledWith(OptInRoute, {
...INTERNAL_VERSION,
body: JSON.stringify({ enabled: optedIn }), body: JSON.stringify({ enabled: optedIn }),
}); });
}); });
@ -77,7 +85,8 @@ describe('TelemetryService', () => {
const optedIn = false; const optedIn = false;
await telemetryService.setOptIn(optedIn); await telemetryService.setOptIn(optedIn);
expect(telemetryService['http'].post).toBeCalledWith('/api/telemetry/v2/optIn', { expect(telemetryService['http'].post).toBeCalledWith(OptInRoute, {
...INTERNAL_VERSION,
body: JSON.stringify({ enabled: optedIn }), body: JSON.stringify({ enabled: optedIn }),
}); });
}); });
@ -110,7 +119,7 @@ describe('TelemetryService', () => {
config: { allowChangingOptInStatus: true }, config: { allowChangingOptInStatus: true },
}); });
telemetryService['http'].post = jest.fn().mockImplementation((url: string) => { telemetryService['http'].post = jest.fn().mockImplementation((url: string) => {
if (url === '/api/telemetry/v2/optIn') { if (url === OptInRoute) {
throw Error('failed to update opt in.'); throw Error('failed to update opt in.');
} }
}); });
@ -203,7 +212,7 @@ describe('TelemetryService', () => {
}); });
telemetryService['http'].put = jest.fn().mockImplementation((url: string) => { telemetryService['http'].put = jest.fn().mockImplementation((url: string) => {
if (url === '/api/telemetry/v2/userHasSeenNotice') { if (url === UserHasSeenNoticeRoute) {
throw Error('failed to update opt in.'); throw Error('failed to update opt in.');
} }
}); });

View file

@ -8,11 +8,19 @@
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import type { CoreSetup, CoreStart } from '@kbn/core/public'; import type { CoreSetup, CoreStart } from '@kbn/core/public';
import {
LastReportedRoute,
INTERNAL_VERSION,
OptInRoute,
FetchSnapshotTelemetry,
UserHasSeenNoticeRoute,
} from '../../common/routes';
import type { TelemetryPluginConfig } from '../plugin'; import type { TelemetryPluginConfig } from '../plugin';
import { getTelemetryChannelEndpoint } from '../../common/telemetry_config/get_telemetry_channel_endpoint'; import { getTelemetryChannelEndpoint } from '../../common/telemetry_config';
import type { import type {
UnencryptedTelemetryPayload, UnencryptedTelemetryPayload,
EncryptedTelemetryPayload, EncryptedTelemetryPayload,
FetchLastReportedResponse,
} from '../../common/types/latest'; } from '../../common/types/latest';
import { PAYLOAD_CONTENT_ENCODING } from '../../common/constants'; import { PAYLOAD_CONTENT_ENCODING } from '../../common/constants';
@ -93,8 +101,7 @@ export class TelemetryService {
/** Is the cluster allowed to change the opt-in/out status **/ /** Is the cluster allowed to change the opt-in/out status **/
public getCanChangeOptInStatus = () => { public getCanChangeOptInStatus = () => {
const allowChangingOptInStatus = this.config.allowChangingOptInStatus; return this.config.allowChangingOptInStatus;
return allowChangingOptInStatus;
}; };
/** Retrieve the opt-in/out notification URL **/ /** Retrieve the opt-in/out notification URL **/
@ -156,17 +163,18 @@ export class TelemetryService {
}; };
public fetchLastReported = async (): Promise<number | undefined> => { public fetchLastReported = async (): Promise<number | undefined> => {
const response = await this.http.get<{ lastReported?: number }>( const response = await this.http.get<FetchLastReportedResponse>(
'/api/telemetry/v2/last_reported' LastReportedRoute,
INTERNAL_VERSION
); );
return response?.lastReported; return response?.lastReported;
}; };
public updateLastReported = async (): Promise<number | undefined> => { public updateLastReported = async (): Promise<number | undefined> => {
return this.http.put('/api/telemetry/v2/last_reported'); return this.http.put(LastReportedRoute);
}; };
/** Fetches an unencrypted telemetry payload so we can show it to the user **/ /** Fetches an unencrypted telemetry payload, so we can show it to the user **/
public fetchExample = async (): Promise<UnencryptedTelemetryPayload> => { public fetchExample = async (): Promise<UnencryptedTelemetryPayload> => {
return await this.fetchTelemetry({ unencrypted: true, refreshCache: true }); return await this.fetchTelemetry({ unencrypted: true, refreshCache: true });
}; };
@ -174,12 +182,14 @@ export class TelemetryService {
/** /**
* Fetches telemetry payload * Fetches telemetry payload
* @param unencrypted Default `false`. Whether the returned payload should be encrypted or not. * @param unencrypted Default `false`. Whether the returned payload should be encrypted or not.
* @param refreshCache Default `false`. Set to `true` to force the regeneration of the telemetry report.
*/ */
public fetchTelemetry = async <T = EncryptedTelemetryPayload | UnencryptedTelemetryPayload>({ public fetchTelemetry = async <T = EncryptedTelemetryPayload | UnencryptedTelemetryPayload>({
unencrypted = false, unencrypted = false,
refreshCache = false, refreshCache = false,
} = {}): Promise<T> => { } = {}): Promise<T> => {
return this.http.post('/api/telemetry/v2/clusters/_stats', { return this.http.post(FetchSnapshotTelemetry, {
...INTERNAL_VERSION,
body: JSON.stringify({ unencrypted, refreshCache }), body: JSON.stringify({ unencrypted, refreshCache }),
}); });
}; };
@ -198,12 +208,10 @@ export class TelemetryService {
try { try {
// Report the option to the Kibana server to store the settings. // Report the option to the Kibana server to store the settings.
// It returns the encrypted update to send to the telemetry cluster [{cluster_uuid, opt_in_status}] // It returns the encrypted update to send to the telemetry cluster [{cluster_uuid, opt_in_status}]
const optInStatusPayload = await this.http.post<EncryptedTelemetryPayload>( const optInStatusPayload = await this.http.post<EncryptedTelemetryPayload>(OptInRoute, {
'/api/telemetry/v2/optIn', ...INTERNAL_VERSION,
{ body: JSON.stringify({ enabled: optedIn }),
body: JSON.stringify({ enabled: optedIn }), });
}
);
if (this.reportOptInStatusChange) { if (this.reportOptInStatusChange) {
// Use the response to report about the change to the remote telemetry cluster. // Use the response to report about the change to the remote telemetry cluster.
// If it's opt-out, this will be the last communication to the remote service. // If it's opt-out, this will be the last communication to the remote service.
@ -231,7 +239,7 @@ export class TelemetryService {
*/ */
public setUserHasSeenNotice = async (): Promise<void> => { public setUserHasSeenNotice = async (): Promise<void> => {
try { try {
await this.http.put('/api/telemetry/v2/userHasSeenNotice'); await this.http.put(UserHasSeenNoticeRoute, INTERNAL_VERSION);
this.userHasSeenOptedInNotice = true; this.userHasSeenOptedInNotice = true;
} catch (error) { } catch (error) {
this.notifications.toasts.addError(error, { this.notifications.toasts.addError(error, {
@ -248,7 +256,7 @@ export class TelemetryService {
/** /**
* Pushes the encrypted payload [{cluster_uuid, opt_in_status}] to the remote telemetry service * Pushes the encrypted payload [{cluster_uuid, opt_in_status}] to the remote telemetry service
* @param optInPayload [{cluster_uuid, opt_in_status}] encrypted by the server into an array of strings * @param optInStatusPayload [{cluster_uuid, opt_in_status}] encrypted by the server into an array of strings
*/ */
private reportOptInStatus = async ( private reportOptInStatus = async (
optInStatusPayload: EncryptedTelemetryPayload optInStatusPayload: EncryptedTelemetryPayload

View file

@ -8,9 +8,14 @@
import { type Observable, firstValueFrom } from 'rxjs'; import { type Observable, firstValueFrom } from 'rxjs';
import type { IRouter, SavedObjectsClient } from '@kbn/core/server'; import type { IRouter, SavedObjectsClient } from '@kbn/core/server';
import { schema } from '@kbn/config-schema';
import { RequestHandler } from '@kbn/core-http-server';
import type { TelemetryConfigType } from '../config'; import type { TelemetryConfigType } from '../config';
import { v2 } from '../../common/types'; import { v2 } from '../../common/types';
import { FetchTelemetryConfigRoute } from '../../common/routes'; import {
FetchTelemetryConfigRoutePathBasedV2,
FetchTelemetryConfigRoute,
} from '../../common/routes';
import { getTelemetrySavedObject } from '../saved_objects'; import { getTelemetrySavedObject } from '../saved_objects';
import { import {
getNotifyUserAboutOptInDefault, getNotifyUserAboutOptInDefault,
@ -25,54 +30,74 @@ interface RegisterTelemetryConfigRouteOptions {
currentKibanaVersion: string; currentKibanaVersion: string;
savedObjectsInternalClient$: Observable<SavedObjectsClient>; savedObjectsInternalClient$: Observable<SavedObjectsClient>;
} }
export function registerTelemetryConfigRoutes({ export function registerTelemetryConfigRoutes({
router, router,
config$, config$,
currentKibanaVersion, currentKibanaVersion,
savedObjectsInternalClient$, savedObjectsInternalClient$,
}: RegisterTelemetryConfigRouteOptions) { }: RegisterTelemetryConfigRouteOptions) {
// GET to retrieve const v2Handler: RequestHandler = async (context, req, res) => {
router.get( const config = await firstValueFrom(config$);
{ const savedObjectsInternalClient = await firstValueFrom(savedObjectsInternalClient$);
path: FetchTelemetryConfigRoute, const telemetrySavedObject = await getTelemetrySavedObject(savedObjectsInternalClient);
validate: false, const allowChangingOptInStatus = getTelemetryAllowChangingOptInStatus({
configTelemetryAllowChangingOptInStatus: config.allowChangingOptInStatus,
telemetrySavedObject,
});
const optIn = getTelemetryOptIn({
configTelemetryOptIn: config.optIn,
allowChangingOptInStatus,
telemetrySavedObject,
currentKibanaVersion,
});
const sendUsageFrom = getTelemetrySendUsageFrom({
configTelemetrySendUsageFrom: config.sendUsageFrom,
telemetrySavedObject,
});
const telemetryNotifyUserAboutOptInDefault = getNotifyUserAboutOptInDefault({
telemetrySavedObject,
allowChangingOptInStatus,
configTelemetryOptIn: config.optIn,
telemetryOptedIn: optIn,
});
const body: v2.FetchTelemetryConfigResponse = {
allowChangingOptInStatus,
optIn,
sendUsageFrom,
telemetryNotifyUserAboutOptInDefault,
};
return res.ok({ body });
};
const v2Validations = {
response: {
200: {
body: schema.object({
allowChangingOptInStatus: schema.boolean(),
optIn: schema.oneOf([schema.boolean(), schema.literal(null)]),
sendUsageFrom: schema.oneOf([schema.literal('server'), schema.literal('browser')]),
telemetryNotifyUserAboutOptInDefault: schema.boolean(),
}),
},
}, },
async (context, req, res) => { };
const config = await firstValueFrom(config$);
const savedObjectsInternalClient = await firstValueFrom(savedObjectsInternalClient$);
const telemetrySavedObject = await getTelemetrySavedObject(savedObjectsInternalClient);
const allowChangingOptInStatus = getTelemetryAllowChangingOptInStatus({
configTelemetryAllowChangingOptInStatus: config.allowChangingOptInStatus,
telemetrySavedObject,
});
const optIn = getTelemetryOptIn({ // Register the internal versioned API
configTelemetryOptIn: config.optIn, router.versioned
allowChangingOptInStatus, .get({ access: 'internal', path: FetchTelemetryConfigRoute })
telemetrySavedObject, // Just because it used to be /v2/, we are creating identical v1 and v2.
currentKibanaVersion, .addVersion({ version: '1', validate: v2Validations }, v2Handler)
}); .addVersion({ version: '2', validate: v2Validations }, v2Handler);
const sendUsageFrom = getTelemetrySendUsageFrom({ // Register the deprecated public and path-based for BWC
configTelemetrySendUsageFrom: config.sendUsageFrom, // as we know this one is used by other Elastic products to fetch the opt-in status.
telemetrySavedObject, router.versioned
}); .get({ access: 'public', path: FetchTelemetryConfigRoutePathBasedV2 })
.addVersion({ version: '2023-10-31', validate: v2Validations }, v2Handler);
const telemetryNotifyUserAboutOptInDefault = getNotifyUserAboutOptInDefault({
telemetrySavedObject,
allowChangingOptInStatus,
configTelemetryOptIn: config.optIn,
telemetryOptedIn: optIn,
});
const body: v2.FetchTelemetryConfigResponse = {
allowChangingOptInStatus,
optIn,
sendUsageFrom,
telemetryNotifyUserAboutOptInDefault,
};
return res.ok({ body });
}
);
} }

View file

@ -6,9 +6,12 @@
* Side Public License, v 1. * Side Public License, v 1.
*/ */
import { schema } from '@kbn/config-schema';
import type { IRouter, SavedObjectsClient } from '@kbn/core/server'; import type { IRouter, SavedObjectsClient } from '@kbn/core/server';
import type { Observable } from 'rxjs'; import type { Observable } from 'rxjs';
import { firstValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
import { RequestHandler } from '@kbn/core-http-server';
import { LastReportedRoute } from '../../common/routes';
import { v2 } from '../../common/types'; import { v2 } from '../../common/types';
import { getTelemetrySavedObject, updateTelemetrySavedObject } from '../saved_objects'; import { getTelemetrySavedObject, updateTelemetrySavedObject } from '../saved_objects';
@ -17,38 +20,38 @@ export function registerTelemetryLastReported(
savedObjectsInternalClient$: Observable<SavedObjectsClient> savedObjectsInternalClient$: Observable<SavedObjectsClient>
) { ) {
// GET to retrieve // GET to retrieve
router.get( const v2GetValidations = {
{ response: { 200: { body: schema.object({ lastReported: schema.maybe(schema.number()) }) } },
path: '/api/telemetry/v2/last_reported', };
validate: false,
},
async (context, req, res) => {
const savedObjectsInternalClient = await firstValueFrom(savedObjectsInternalClient$);
const telemetrySavedObject = await getTelemetrySavedObject(savedObjectsInternalClient);
const body: v2.FetchLastReportedResponse = { const v2GetHandler: RequestHandler = async (context, req, res) => {
lastReported: telemetrySavedObject && telemetrySavedObject?.lastReported, const savedObjectsInternalClient = await firstValueFrom(savedObjectsInternalClient$);
}; const telemetrySavedObject = await getTelemetrySavedObject(savedObjectsInternalClient);
return res.ok({ const body: v2.FetchLastReportedResponse = {
body, lastReported: telemetrySavedObject && telemetrySavedObject?.lastReported,
}); };
} return res.ok({ body });
); };
router.versioned
.get({ access: 'internal', path: LastReportedRoute })
// Just because it used to be /v2/, we are creating identical v1 and v2.
.addVersion({ version: '1', validate: v2GetValidations }, v2GetHandler)
.addVersion({ version: '2', validate: v2GetValidations }, v2GetHandler);
// PUT to update // PUT to update
router.put( const v2PutHandler: RequestHandler = async (context, req, res) => {
{ const savedObjectsInternalClient = await firstValueFrom(savedObjectsInternalClient$);
path: '/api/telemetry/v2/last_reported', await updateTelemetrySavedObject(savedObjectsInternalClient, {
validate: false, lastReported: Date.now(),
}, });
async (context, req, res) => { return res.ok();
const savedObjectsInternalClient = await firstValueFrom(savedObjectsInternalClient$); };
await updateTelemetrySavedObject(savedObjectsInternalClient, {
lastReported: Date.now(),
});
return res.ok(); router.versioned
} .put({ access: 'internal', path: LastReportedRoute })
); // Just because it used to be /v2/, we are creating identical v1 and v2.
.addVersion({ version: '1', validate: false }, v2PutHandler)
.addVersion({ version: '2', validate: false }, v2PutHandler);
} }

View file

@ -9,12 +9,14 @@
import { firstValueFrom, type Observable } from 'rxjs'; import { firstValueFrom, type Observable } from 'rxjs';
import { schema } from '@kbn/config-schema'; import { schema } from '@kbn/config-schema';
import type { IRouter, Logger } from '@kbn/core/server'; import type { IRouter, Logger } from '@kbn/core/server';
import { SavedObjectsErrorHelpers } from '@kbn/core/server'; import { RequestHandlerContext, SavedObjectsErrorHelpers } from '@kbn/core/server';
import type { import type {
StatsGetterConfig, StatsGetterConfig,
TelemetryCollectionManagerPluginSetup, TelemetryCollectionManagerPluginSetup,
} from '@kbn/telemetry-collection-manager-plugin/server'; } from '@kbn/telemetry-collection-manager-plugin/server';
import { v2 } from '../../common/types'; import { RequestHandler } from '@kbn/core-http-server';
import { OptInRoute } from '../../common/routes';
import { OptInBody, v2 } from '../../common/types';
import { sendTelemetryOptInStatus } from './telemetry_opt_in_stats'; import { sendTelemetryOptInStatus } from './telemetry_opt_in_stats';
import { import {
getTelemetrySavedObject, getTelemetrySavedObject,
@ -41,78 +43,91 @@ export function registerTelemetryOptInRoutes({
currentKibanaVersion, currentKibanaVersion,
telemetryCollectionManager, telemetryCollectionManager,
}: RegisterOptInRoutesParams) { }: RegisterOptInRoutesParams) {
router.post( const v2Handler: RequestHandler<undefined, undefined, OptInBody, RequestHandlerContext> = async (
{ context,
path: '/api/telemetry/v2/optIn', req,
validate: { res
body: schema.object({ enabled: schema.boolean() }), ) => {
const newOptInStatus = req.body.enabled;
const soClient = (await context.core).savedObjects.getClient({
includedHiddenTypes: [TELEMETRY_SAVED_OBJECT_TYPE],
});
const attributes: TelemetrySavedObject = {
enabled: newOptInStatus,
lastVersionChecked: currentKibanaVersion,
};
const config = await firstValueFrom(config$);
let telemetrySavedObject: TelemetrySavedObject | undefined;
try {
telemetrySavedObject = await getTelemetrySavedObject(soClient);
} catch (err) {
if (SavedObjectsErrorHelpers.isForbiddenError(err)) {
// If we couldn't get the saved object due to lack of permissions,
// we can assume the user won't be able to update it either
return res.forbidden();
}
}
const allowChangingOptInStatus = getTelemetryAllowChangingOptInStatus({
configTelemetryAllowChangingOptInStatus: config.allowChangingOptInStatus,
telemetrySavedObject,
});
if (!allowChangingOptInStatus) {
return res.badRequest({
body: JSON.stringify({ error: 'Not allowed to change Opt-in Status.' }),
});
}
const statsGetterConfig: StatsGetterConfig = {
unencrypted: false,
};
const optInStatus = await telemetryCollectionManager.getOptInStats(
newOptInStatus,
statsGetterConfig
);
if (config.sendUsageFrom === 'server') {
const { appendServerlessChannelsSuffix, sendUsageTo } = config;
sendTelemetryOptInStatus(
telemetryCollectionManager,
{ appendServerlessChannelsSuffix, sendUsageTo, newOptInStatus, currentKibanaVersion },
statsGetterConfig
).catch((err) => {
// The server is likely behind a firewall and can't reach the remote service
logger.warn(
`Failed to notify the telemetry endpoint about the opt-in selection. Possibly blocked by a firewall? - Error: ${err.message}`
);
});
}
try {
await updateTelemetrySavedObject(soClient, attributes);
} catch (e) {
if (SavedObjectsErrorHelpers.isForbiddenError(e)) {
return res.forbidden();
}
}
const body: v2.OptInResponse = optInStatus;
return res.ok({ body });
};
const v2Validations = {
request: { body: schema.object({ enabled: schema.boolean() }) },
response: {
200: {
body: schema.arrayOf(
schema.object({ clusterUuid: schema.string(), stats: schema.string() })
),
}, },
}, },
async (context, req, res) => { };
const newOptInStatus = req.body.enabled;
const soClient = (await context.core).savedObjects.getClient({
includedHiddenTypes: [TELEMETRY_SAVED_OBJECT_TYPE],
});
const attributes: TelemetrySavedObject = {
enabled: newOptInStatus,
lastVersionChecked: currentKibanaVersion,
};
const config = await firstValueFrom(config$);
let telemetrySavedObject: TelemetrySavedObject | undefined; router.versioned
try { .post({ access: 'internal', path: OptInRoute })
telemetrySavedObject = await getTelemetrySavedObject(soClient); // Just because it used to be /v2/, we are creating identical v1 and v2.
} catch (err) { .addVersion({ version: '1', validate: v2Validations }, v2Handler)
if (SavedObjectsErrorHelpers.isForbiddenError(err)) { .addVersion({ version: '2', validate: v2Validations }, v2Handler);
// If we couldn't get the saved object due to lack of permissions,
// we can assume the user won't be able to update it either
return res.forbidden();
}
}
const allowChangingOptInStatus = getTelemetryAllowChangingOptInStatus({
configTelemetryAllowChangingOptInStatus: config.allowChangingOptInStatus,
telemetrySavedObject,
});
if (!allowChangingOptInStatus) {
return res.badRequest({
body: JSON.stringify({ error: 'Not allowed to change Opt-in Status.' }),
});
}
const statsGetterConfig: StatsGetterConfig = {
unencrypted: false,
};
const optInStatus = await telemetryCollectionManager.getOptInStats(
newOptInStatus,
statsGetterConfig
);
if (config.sendUsageFrom === 'server') {
const { appendServerlessChannelsSuffix, sendUsageTo } = config;
sendTelemetryOptInStatus(
telemetryCollectionManager,
{ appendServerlessChannelsSuffix, sendUsageTo, newOptInStatus, currentKibanaVersion },
statsGetterConfig
).catch((err) => {
// The server is likely behind a firewall and can't reach the remote service
logger.warn(
`Failed to notify the telemetry endpoint about the opt-in selection. Possibly blocked by a firewall? - Error: ${err.message}`
);
});
}
try {
await updateTelemetrySavedObject(soClient, attributes);
} catch (e) {
if (SavedObjectsErrorHelpers.isForbiddenError(e)) {
return res.forbidden();
}
}
const body: v2.OptInResponse = optInStatus;
return res.ok({ body });
}
);
} }

View file

@ -14,6 +14,7 @@ import type {
TelemetryCollectionManagerPluginSetup, TelemetryCollectionManagerPluginSetup,
StatsGetterConfig, StatsGetterConfig,
} from '@kbn/telemetry-collection-manager-plugin/server'; } from '@kbn/telemetry-collection-manager-plugin/server';
import { GetOptInStatsRoutePathBasedV2 } from '../../common/routes';
import type { v2 } from '../../common/types'; import type { v2 } from '../../common/types';
import { EncryptedTelemetryPayload, UnencryptedTelemetryPayload } from '../../common/types'; import { EncryptedTelemetryPayload, UnencryptedTelemetryPayload } from '../../common/types';
import { getTelemetryChannelEndpoint } from '../../common/telemetry_config'; import { getTelemetryChannelEndpoint } from '../../common/telemetry_config';
@ -62,43 +63,64 @@ export function registerTelemetryOptInStatsRoutes(
router: IRouter, router: IRouter,
telemetryCollectionManager: TelemetryCollectionManagerPluginSetup telemetryCollectionManager: TelemetryCollectionManagerPluginSetup
) { ) {
router.post( router.versioned
{ .post({
path: '/api/telemetry/v2/clusters/_opt_in_stats', access: 'public', // It's not used across Kibana, and I didn't want to remove it in this PR just in case.
validate: { path: GetOptInStatsRoutePathBasedV2,
body: schema.object({ })
enabled: schema.boolean(), .addVersion(
unencrypted: schema.boolean({ defaultValue: true }), {
}), version: '2023-10-31',
validate: {
request: {
body: schema.object({
enabled: schema.boolean(),
unencrypted: schema.boolean({ defaultValue: true }),
}),
},
response: {
200: {
body: schema.arrayOf(
schema.object({
clusterUuid: schema.string(),
stats: schema.object({
cluster_uuid: schema.string(),
opt_in_status: schema.boolean(),
}),
})
),
},
503: { body: schema.string() },
},
},
}, },
}, async (context, req, res) => {
async (context, req, res) => { try {
try { const newOptInStatus = req.body.enabled;
const newOptInStatus = req.body.enabled; const unencrypted = req.body.unencrypted;
const unencrypted = req.body.unencrypted;
if (!(await telemetryCollectionManager.shouldGetTelemetry())) { if (!(await telemetryCollectionManager.shouldGetTelemetry())) {
// We probably won't reach here because there is a license check in the auth phase of the HTTP requests. // We probably won't reach here because there is a license check in the auth phase of the HTTP requests.
// But let's keep it here should that changes at any point. // But let's keep it here should that changes at any point.
return res.customError({ return res.customError({
statusCode: 503, statusCode: 503,
body: `Can't fetch telemetry at the moment because some services are down. Check the /status page for more details.`, body: `Can't fetch telemetry at the moment because some services are down. Check the /status page for more details.`,
}); });
}
const statsGetterConfig: StatsGetterConfig = {
unencrypted,
};
const optInStatus = await telemetryCollectionManager.getOptInStats(
newOptInStatus,
statsGetterConfig
);
const body: v2.OptInStatsResponse = optInStatus;
return res.ok({ body });
} catch (err) {
return res.ok({ body: [] });
} }
const statsGetterConfig: StatsGetterConfig = {
unencrypted,
};
const optInStatus = await telemetryCollectionManager.getOptInStats(
newOptInStatus,
statsGetterConfig
);
const body: v2.OptInStatsResponse = optInStatus;
return res.ok({ body });
} catch (err) {
return res.ok({ body: [] });
} }
} );
);
} }

View file

@ -16,8 +16,9 @@ async function runRequest(
mockRouter: IRouter<RequestHandlerContext>, mockRouter: IRouter<RequestHandlerContext>,
body?: { unencrypted?: boolean; refreshCache?: boolean } body?: { unencrypted?: boolean; refreshCache?: boolean }
) { ) {
expect(mockRouter.post).toBeCalled(); expect(mockRouter.versioned.post).toBeCalled();
const [, handler] = (mockRouter.post as jest.Mock).mock.calls[0]; const [, handler] = (mockRouter.versioned.post as jest.Mock).mock.results[0].value.addVersion.mock
.calls[0];
const mockResponse = httpServerMock.createResponseFactory(); const mockResponse = httpServerMock.createResponseFactory();
const mockRequest = httpServerMock.createKibanaRequest({ body }); const mockRequest = httpServerMock.createKibanaRequest({ body });
await handler(null, mockRequest, mockResponse); await handler(null, mockRequest, mockResponse);
@ -49,10 +50,10 @@ describe('registerTelemetryUsageStatsRoutes', () => {
describe('clusters/_stats POST route', () => { describe('clusters/_stats POST route', () => {
it('registers _stats POST route and accepts body configs', () => { it('registers _stats POST route and accepts body configs', () => {
registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true, getSecurity); registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true, getSecurity);
expect(mockRouter.post).toBeCalledTimes(1); expect(mockRouter.versioned.post).toBeCalledTimes(1);
const [routeConfig, handler] = (mockRouter.post as jest.Mock).mock.calls[0]; const [routeConfig, handler] = (mockRouter.versioned.post as jest.Mock).mock.results[0].value
expect(routeConfig.path).toMatchInlineSnapshot(`"/api/telemetry/v2/clusters/_stats"`); .addVersion.mock.calls[0];
expect(Object.keys(routeConfig.validate.body.props)).toEqual(['unencrypted', 'refreshCache']); expect(routeConfig.version).toMatchInlineSnapshot(`"1"`);
expect(handler).toBeInstanceOf(Function); expect(handler).toBeInstanceOf(Function);
}); });

View file

@ -13,7 +13,9 @@ import type {
StatsGetterConfig, StatsGetterConfig,
} from '@kbn/telemetry-collection-manager-plugin/server'; } from '@kbn/telemetry-collection-manager-plugin/server';
import type { SecurityPluginStart } from '@kbn/security-plugin/server'; import type { SecurityPluginStart } from '@kbn/security-plugin/server';
import { v2 } from '../../common/types'; import { RequestHandler } from '@kbn/core-http-server';
import { FetchSnapshotTelemetry } from '../../common/routes';
import { UsageStatsBody, v2 } from '../../common/types';
export type SecurityGetter = () => SecurityPluginStart | undefined; export type SecurityGetter = () => SecurityPluginStart | undefined;
@ -23,64 +25,75 @@ export function registerTelemetryUsageStatsRoutes(
isDev: boolean, isDev: boolean,
getSecurity: SecurityGetter getSecurity: SecurityGetter
) { ) {
router.post( const v2Handler: RequestHandler<undefined, undefined, UsageStatsBody> = async (
{ context,
path: '/api/telemetry/v2/clusters/_stats', req,
validate: { res
body: schema.object({ ) => {
unencrypted: schema.boolean({ defaultValue: false }), const { unencrypted, refreshCache } = req.body;
refreshCache: schema.boolean({ defaultValue: false }),
}),
},
},
async (context, req, res) => {
const { unencrypted, refreshCache } = req.body;
if (!(await telemetryCollectionManager.shouldGetTelemetry())) { if (!(await telemetryCollectionManager.shouldGetTelemetry())) {
// We probably won't reach here because there is a license check in the auth phase of the HTTP requests. // We probably won't reach here because there is a license check in the auth phase of the HTTP requests.
// But let's keep it here should that changes at any point. // But let's keep it here should that changes at any point.
return res.customError({ return res.customError({
statusCode: 503, statusCode: 503,
body: `Can't fetch telemetry at the moment because some services are down. Check the /status page for more details.`, body: `Can't fetch telemetry at the moment because some services are down. Check the /status page for more details.`,
}); });
} }
const security = getSecurity(); const security = getSecurity();
// We need to check useRbacForRequest to figure out if ES has security enabled before making the privileges check // We need to check useRbacForRequest to figure out if ES has security enabled before making the privileges check
if (security && unencrypted && security.authz.mode.useRbacForRequest(req)) { if (security && unencrypted && security.authz.mode.useRbacForRequest(req)) {
// Normally we would use `options: { tags: ['access:decryptedTelemetry'] }` in the route definition to check authorization for an // Normally we would use `options: { tags: ['access:decryptedTelemetry'] }` in the route definition to check authorization for an
// API action, however, we want to check this conditionally based on the `unencrypted` parameter. In this case we need to use the // API action, however, we want to check this conditionally based on the `unencrypted` parameter. In this case we need to use the
// security API directly to check privileges for this action. Note that the 'decryptedTelemetry' API privilege string is only // security API directly to check privileges for this action. Note that the 'decryptedTelemetry' API privilege string is only
// granted to users that have "Global All" or "Global Read" privileges in Kibana. // granted to users that have "Global All" or "Global Read" privileges in Kibana.
const { checkPrivilegesWithRequest, actions } = security.authz; const { checkPrivilegesWithRequest, actions } = security.authz;
const privileges = { kibana: actions.api.get('decryptedTelemetry') }; const privileges = { kibana: actions.api.get('decryptedTelemetry') };
const { hasAllRequested } = await checkPrivilegesWithRequest(req).globally(privileges); const { hasAllRequested } = await checkPrivilegesWithRequest(req).globally(privileges);
if (!hasAllRequested) { if (!hasAllRequested) {
return res.forbidden(); return res.forbidden();
}
}
try {
const statsConfig: StatsGetterConfig = {
unencrypted,
refreshCache: unencrypted || refreshCache,
};
const body: v2.UnencryptedTelemetryPayload = await telemetryCollectionManager.getStats(
statsConfig
);
return res.ok({ body });
} catch (err) {
if (isDev) {
// don't ignore errors when running in dev mode
throw err;
}
if (unencrypted && err.status === 403) {
return res.forbidden();
}
// ignore errors and return empty set
return res.ok({ body: [] });
} }
} }
);
try {
const statsConfig: StatsGetterConfig = {
unencrypted,
refreshCache: unencrypted || refreshCache,
};
const body: v2.UnencryptedTelemetryPayload = await telemetryCollectionManager.getStats(
statsConfig
);
return res.ok({ body });
} catch (err) {
if (isDev) {
// don't ignore errors when running in dev mode
throw err;
}
if (unencrypted && err.status === 403) {
return res.forbidden();
}
// ignore errors and return empty set
return res.ok({ body: [] });
}
};
const v2Validations = {
request: {
body: schema.object({
unencrypted: schema.boolean({ defaultValue: false }),
refreshCache: schema.boolean({ defaultValue: false }),
}),
},
};
router.versioned
.post({
access: 'internal',
path: FetchSnapshotTelemetry,
})
// Just because it used to be /v2/, we are creating identical v1 and v2.
.addVersion({ version: '1', validate: v2Validations }, v2Handler)
.addVersion({ version: '2', validate: v2Validations }, v2Handler);
} }

View file

@ -7,6 +7,9 @@
*/ */
import type { IRouter } from '@kbn/core/server'; import type { IRouter } from '@kbn/core/server';
import { RequestHandler } from '@kbn/core-http-server';
import { RequestHandlerContext } from '@kbn/core/server';
import { UserHasSeenNoticeRoute } from '../../common/routes';
import { TELEMETRY_SAVED_OBJECT_TYPE } from '../saved_objects'; import { TELEMETRY_SAVED_OBJECT_TYPE } from '../saved_objects';
import { v2 } from '../../common/types'; import { v2 } from '../../common/types';
import { import {
@ -16,38 +19,42 @@ import {
} from '../saved_objects'; } from '../saved_objects';
export function registerTelemetryUserHasSeenNotice(router: IRouter, currentKibanaVersion: string) { export function registerTelemetryUserHasSeenNotice(router: IRouter, currentKibanaVersion: string) {
router.put( const v2Handler: RequestHandler<undefined, undefined, undefined, RequestHandlerContext> = async (
{ context,
path: '/api/telemetry/v2/userHasSeenNotice', req,
validate: false, res
}, ) => {
async (context, req, res) => { const soClient = (await context.core).savedObjects.getClient({
const soClient = (await context.core).savedObjects.getClient({ includedHiddenTypes: [TELEMETRY_SAVED_OBJECT_TYPE],
includedHiddenTypes: [TELEMETRY_SAVED_OBJECT_TYPE], });
}); const telemetrySavedObject = await getTelemetrySavedObject(soClient);
const telemetrySavedObject = await getTelemetrySavedObject(soClient);
// update the object with a flag stating that the opt-in notice has been seen // update the object with a flag stating that the opt-in notice has been seen
const updatedAttributes: TelemetrySavedObjectAttributes = { const updatedAttributes: TelemetrySavedObjectAttributes = {
...telemetrySavedObject, ...telemetrySavedObject,
userHasSeenNotice: true, userHasSeenNotice: true,
// We need to store that the user was notified in this version. // We need to store that the user was notified in this version.
// Otherwise, it'll continuously show the banner if previously opted-out. // Otherwise, it'll continuously show the banner if previously opted-out.
lastVersionChecked: currentKibanaVersion, lastVersionChecked: currentKibanaVersion,
}; };
await updateTelemetrySavedObject(soClient, updatedAttributes); await updateTelemetrySavedObject(soClient, updatedAttributes);
const body: v2.Telemetry = { const body: v2.Telemetry = {
allowChangingOptInStatus: updatedAttributes.allowChangingOptInStatus, allowChangingOptInStatus: updatedAttributes.allowChangingOptInStatus,
enabled: updatedAttributes.enabled, enabled: updatedAttributes.enabled,
lastReported: updatedAttributes.lastReported, lastReported: updatedAttributes.lastReported,
lastVersionChecked: updatedAttributes.lastVersionChecked, lastVersionChecked: updatedAttributes.lastVersionChecked,
reportFailureCount: updatedAttributes.reportFailureCount, reportFailureCount: updatedAttributes.reportFailureCount,
reportFailureVersion: updatedAttributes.reportFailureVersion, reportFailureVersion: updatedAttributes.reportFailureVersion,
sendUsageFrom: updatedAttributes.sendUsageFrom, sendUsageFrom: updatedAttributes.sendUsageFrom,
userHasSeenNotice: updatedAttributes.userHasSeenNotice, userHasSeenNotice: updatedAttributes.userHasSeenNotice,
}; };
return res.ok({ body }); return res.ok({ body });
} };
);
router.versioned
.put({ access: 'internal', path: UserHasSeenNoticeRoute })
// Just because it used to be /v2/, we are creating identical v1 and v2.
.addVersion({ version: '1', validate: false }, v2Handler)
.addVersion({ version: '2', validate: false }, v2Handler);
} }

View file

@ -34,6 +34,7 @@
"@kbn/std", "@kbn/std",
"@kbn/core-http-browser-mocks", "@kbn/core-http-browser-mocks",
"@kbn/core-http-browser", "@kbn/core-http-browser",
"@kbn/core-http-server",
], ],
"exclude": [ "exclude": [
"target/**/*", "target/**/*",

View file

@ -11,6 +11,10 @@ import expect from '@kbn/expect';
import SuperTest from 'supertest'; import SuperTest from 'supertest';
import type { KbnClient } from '@kbn/test'; import type { KbnClient } from '@kbn/test';
import type { TelemetrySavedObjectAttributes } from '@kbn/telemetry-plugin/server/saved_objects'; import type { TelemetrySavedObjectAttributes } from '@kbn/telemetry-plugin/server/saved_objects';
import {
ELASTIC_HTTP_VERSION_HEADER,
X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
} from '@kbn/core-http-common';
import type { FtrProviderContext } from '../../ftr_provider_context'; import type { FtrProviderContext } from '../../ftr_provider_context';
export default function optInTest({ getService }: FtrProviderContext) { export default function optInTest({ getService }: FtrProviderContext) {
@ -18,7 +22,7 @@ export default function optInTest({ getService }: FtrProviderContext) {
const kibanaServer = getService('kibanaServer'); const kibanaServer = getService('kibanaServer');
const esArchiver = getService('esArchiver'); const esArchiver = getService('esArchiver');
describe('/api/telemetry/v2/optIn API', () => { describe('/internal/telemetry/optIn API', () => {
let defaultAttributes: TelemetrySavedObjectAttributes; let defaultAttributes: TelemetrySavedObjectAttributes;
let kibanaVersion: string; let kibanaVersion: string;
before(async () => { before(async () => {
@ -88,8 +92,10 @@ async function postTelemetryV2OptIn(
statusCode: number statusCode: number
): Promise<any> { ): Promise<any> {
const { body } = await supertest const { body } = await supertest
.post('/api/telemetry/v2/optIn') .post('/internal/telemetry/optIn')
.set('kbn-xsrf', 'xxx') .set('kbn-xsrf', 'xxx')
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ enabled: value }) .send({ enabled: value })
.expect(statusCode); .expect(statusCode);

View file

@ -7,6 +7,10 @@
*/ */
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import {
ELASTIC_HTTP_VERSION_HEADER,
X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
} from '@kbn/core-http-common';
import type { FtrProviderContext } from '../../ftr_provider_context'; import type { FtrProviderContext } from '../../ftr_provider_context';
const TELEMETRY_SO_TYPE = 'telemetry'; const TELEMETRY_SO_TYPE = 'telemetry';
@ -16,110 +20,146 @@ export default function telemetryConfigTest({ getService }: FtrProviderContext)
const kbnClient = getService('kibanaServer'); const kbnClient = getService('kibanaServer');
const supertest = getService('supertest'); const supertest = getService('supertest');
describe('/api/telemetry/v2/config API Telemetry config', () => { describe('API Telemetry config', () => {
before(async () => { ['/api/telemetry/v2/config', '/internal/telemetry/config'].forEach((api) => {
try { describe(`GET ${api}`, () => {
await kbnClient.savedObjects.delete({ type: TELEMETRY_SO_TYPE, id: TELEMETRY_SO_ID }); const apiVersion = api === '/api/telemetry/v2/config' ? '2023-10-31' : '2';
} catch (err) { before(async () => {
const is404Error = err instanceof AxiosError && err.response?.status === 404; try {
if (!is404Error) { await kbnClient.savedObjects.delete({ type: TELEMETRY_SO_TYPE, id: TELEMETRY_SO_ID });
throw err; } catch (err) {
} const is404Error = err instanceof AxiosError && err.response?.status === 404;
} if (!is404Error) {
}); throw err;
}
it('GET should get the default config', async () => { }
await supertest.get('/api/telemetry/v2/config').set('kbn-xsrf', 'xxx').expect(200, {
allowChangingOptInStatus: true,
optIn: null, // the config.js for this FTR sets it to `false`, we are bound to ask again.
sendUsageFrom: 'server',
telemetryNotifyUserAboutOptInDefault: false, // it's not opted-in by default (that's what this flag is about)
});
});
it('GET should get `true` when opted-in', async () => {
// Opt-in
await supertest
.post('/api/telemetry/v2/optIn')
.set('kbn-xsrf', 'xxx')
.send({ enabled: true })
.expect(200);
await supertest.get('/api/telemetry/v2/config').set('kbn-xsrf', 'xxx').expect(200, {
allowChangingOptInStatus: true,
optIn: true,
sendUsageFrom: 'server',
telemetryNotifyUserAboutOptInDefault: false,
});
});
it('GET should get false when opted-out', async () => {
// Opt-in
await supertest
.post('/api/telemetry/v2/optIn')
.set('kbn-xsrf', 'xxx')
.send({ enabled: false })
.expect(200);
await supertest.get('/api/telemetry/v2/config').set('kbn-xsrf', 'xxx').expect(200, {
allowChangingOptInStatus: true,
optIn: false,
sendUsageFrom: 'server',
telemetryNotifyUserAboutOptInDefault: false,
});
});
describe('From a previous version', function () {
this.tags(['skipCloud']);
// Get current values
let attributes: Record<string, unknown>;
let currentVersion: string;
let previousMinor: string;
before(async () => {
[{ attributes }, currentVersion] = await Promise.all([
kbnClient.savedObjects.get({ type: TELEMETRY_SO_TYPE, id: TELEMETRY_SO_ID }),
kbnClient.version.get(),
]);
const [major, minor, patch] = currentVersion.match(/^(\d+)\.(\d+)\.(\d+)/)!.map(parseInt);
previousMinor = `${minor === 0 ? major - 1 : major}.${
minor === 0 ? minor : minor - 1
}.${patch}`;
});
it('GET should get `true` when opted-in in the current version', async () => {
// Opt-in from a previous version
await kbnClient.savedObjects.create({
overwrite: true,
type: TELEMETRY_SO_TYPE,
id: TELEMETRY_SO_ID,
attributes: { ...attributes, enabled: true, lastVersionChecked: previousMinor },
}); });
await supertest.get('/api/telemetry/v2/config').set('kbn-xsrf', 'xxx').expect(200, { it('GET should get the default config', async () => {
allowChangingOptInStatus: true, await supertest
optIn: true, .get(api)
sendUsageFrom: 'server', .set('kbn-xsrf', 'xxx')
telemetryNotifyUserAboutOptInDefault: false, .set(ELASTIC_HTTP_VERSION_HEADER, apiVersion)
}); .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
}); .expect(200, {
allowChangingOptInStatus: true,
it('GET should get `null` when opted-out in a previous version', async () => { optIn: null, // the config.js for this FTR sets it to `false`, we are bound to ask again.
// Opt-out from previous version sendUsageFrom: 'server',
await kbnClient.savedObjects.create({ telemetryNotifyUserAboutOptInDefault: false, // it's not opted-in by default (that's what this flag is about)
overwrite: true, });
type: TELEMETRY_SO_TYPE,
id: TELEMETRY_SO_ID,
attributes: { ...attributes, enabled: false, lastVersionChecked: previousMinor },
}); });
await supertest.get('/api/telemetry/v2/config').set('kbn-xsrf', 'xxx').expect(200, { it('GET should get `true` when opted-in', async () => {
allowChangingOptInStatus: true, // Opt-in
optIn: null, await supertest
sendUsageFrom: 'server', .post('/internal/telemetry/optIn')
telemetryNotifyUserAboutOptInDefault: false, .set('kbn-xsrf', 'xxx')
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ enabled: true })
.expect(200);
await supertest
.get(api)
.set('kbn-xsrf', 'xxx')
.set(ELASTIC_HTTP_VERSION_HEADER, apiVersion)
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.expect(200, {
allowChangingOptInStatus: true,
optIn: true,
sendUsageFrom: 'server',
telemetryNotifyUserAboutOptInDefault: false,
});
});
it('GET should get false when opted-out', async () => {
// Opt-in
await supertest
.post('/internal/telemetry/optIn')
.set('kbn-xsrf', 'xxx')
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ enabled: false })
.expect(200);
await supertest
.get(api)
.set('kbn-xsrf', 'xxx')
.set(ELASTIC_HTTP_VERSION_HEADER, apiVersion)
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.expect(200, {
allowChangingOptInStatus: true,
optIn: false,
sendUsageFrom: 'server',
telemetryNotifyUserAboutOptInDefault: false,
});
});
describe('From a previous version', function () {
this.tags(['skipCloud']);
// Get current values
let attributes: Record<string, unknown>;
let currentVersion: string;
let previousMinor: string;
before(async () => {
[{ attributes }, currentVersion] = await Promise.all([
kbnClient.savedObjects.get({ type: TELEMETRY_SO_TYPE, id: TELEMETRY_SO_ID }),
kbnClient.version.get(),
]);
const [major, minor, patch] = currentVersion
.match(/^(\d+)\.(\d+)\.(\d+)/)!
.map(parseInt);
previousMinor = `${minor === 0 ? major - 1 : major}.${
minor === 0 ? minor : minor - 1
}.${patch}`;
});
it('GET should get `true` when opted-in in the current version', async () => {
// Opt-in from a previous version
await kbnClient.savedObjects.create({
overwrite: true,
type: TELEMETRY_SO_TYPE,
id: TELEMETRY_SO_ID,
attributes: { ...attributes, enabled: true, lastVersionChecked: previousMinor },
});
await supertest
.get(api)
.set('kbn-xsrf', 'xxx')
.set(ELASTIC_HTTP_VERSION_HEADER, apiVersion)
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.expect(200, {
allowChangingOptInStatus: true,
optIn: true,
sendUsageFrom: 'server',
telemetryNotifyUserAboutOptInDefault: false,
});
});
it('GET should get `null` when opted-out in a previous version', async () => {
// Opt-out from previous version
await kbnClient.savedObjects.create({
overwrite: true,
type: TELEMETRY_SO_TYPE,
id: TELEMETRY_SO_ID,
attributes: { ...attributes, enabled: false, lastVersionChecked: previousMinor },
});
await supertest
.get(api)
.set('kbn-xsrf', 'xxx')
.set(ELASTIC_HTTP_VERSION_HEADER, apiVersion)
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.expect(200, {
allowChangingOptInStatus: true,
optIn: null,
sendUsageFrom: 'server',
telemetryNotifyUserAboutOptInDefault: false,
});
});
}); });
}); });
}); });

View file

@ -7,23 +7,37 @@
*/ */
import expect from '@kbn/expect'; import expect from '@kbn/expect';
import {
ELASTIC_HTTP_VERSION_HEADER,
X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
} from '@kbn/core-http-common';
import type { FtrProviderContext } from '../../ftr_provider_context'; import type { FtrProviderContext } from '../../ftr_provider_context';
export default function optInTest({ getService }: FtrProviderContext) { export default function optInTest({ getService }: FtrProviderContext) {
const client = getService('kibanaServer'); const client = getService('kibanaServer');
const supertest = getService('supertest'); const supertest = getService('supertest');
describe('/api/telemetry/v2/last_reported API Telemetry lastReported', () => { describe('/internal/telemetry/last_reported API Telemetry lastReported', () => {
before(async () => { before(async () => {
await client.savedObjects.delete({ type: 'telemetry', id: 'telemetry' }); await client.savedObjects.delete({ type: 'telemetry', id: 'telemetry' });
}); });
it('GET should return undefined when there is no stored telemetry.lastReported value', async () => { it('GET should return undefined when there is no stored telemetry.lastReported value', async () => {
await supertest.get('/api/telemetry/v2/last_reported').set('kbn-xsrf', 'xxx').expect(200, {}); await supertest
.get('/internal/telemetry/last_reported')
.set('kbn-xsrf', 'xxx')
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.expect(200, {});
}); });
it('PUT should update telemetry.lastReported to now', async () => { it('PUT should update telemetry.lastReported to now', async () => {
await supertest.put('/api/telemetry/v2/last_reported').set('kbn-xsrf', 'xxx').expect(200); await supertest
.put('/internal/telemetry/last_reported')
.set('kbn-xsrf', 'xxx')
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.expect(200);
const { const {
attributes: { lastReported }, attributes: { lastReported },
@ -46,8 +60,10 @@ export default function optInTest({ getService }: FtrProviderContext) {
expect(lastReported).to.be.a('number'); expect(lastReported).to.be.a('number');
await supertest await supertest
.get('/api/telemetry/v2/last_reported') .get('/internal/telemetry/last_reported')
.set('kbn-xsrf', 'xxx') .set('kbn-xsrf', 'xxx')
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.expect(200, { lastReported }); .expect(200, { lastReported });
}); });
}); });

View file

@ -7,17 +7,26 @@
*/ */
import expect from '@kbn/expect'; import expect from '@kbn/expect';
import {
ELASTIC_HTTP_VERSION_HEADER,
X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
} from '@kbn/core-http-common';
import type { FtrProviderContext } from '../../ftr_provider_context'; import type { FtrProviderContext } from '../../ftr_provider_context';
export default function optInTest({ getService }: FtrProviderContext) { export default function optInTest({ getService }: FtrProviderContext) {
const client = getService('kibanaServer'); const client = getService('kibanaServer');
const supertest = getService('supertest'); const supertest = getService('supertest');
describe('/api/telemetry/v2/userHasSeenNotice API Telemetry User has seen OptIn Notice', () => { describe('/internal/telemetry/userHasSeenNotice API Telemetry User has seen OptIn Notice', () => {
it('should update telemetry setting field via PUT', async () => { it('should update telemetry setting field via PUT', async () => {
await client.savedObjects.delete({ type: 'telemetry', id: 'telemetry' }); await client.savedObjects.delete({ type: 'telemetry', id: 'telemetry' });
await supertest.put('/api/telemetry/v2/userHasSeenNotice').set('kbn-xsrf', 'xxx').expect(200); await supertest
.put('/internal/telemetry/userHasSeenNotice')
.set('kbn-xsrf', 'xxx')
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.expect(200);
const { const {
attributes: { userHasSeenNotice }, attributes: { userHasSeenNotice },

View file

@ -8,6 +8,10 @@
import expect from '@kbn/expect'; import expect from '@kbn/expect';
import { KBN_SCREENSHOT_MODE_ENABLED_KEY } from '@kbn/screenshot-mode-plugin/public'; import { KBN_SCREENSHOT_MODE_ENABLED_KEY } from '@kbn/screenshot-mode-plugin/public';
import {
ELASTIC_HTTP_VERSION_HEADER,
X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
} from '@kbn/core-http-common';
import { PluginFunctionalProviderContext } from '../../services'; import { PluginFunctionalProviderContext } from '../../services';
const TELEMETRY_SO_TYPE = 'telemetry'; const TELEMETRY_SO_TYPE = 'telemetry';
@ -83,8 +87,10 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide
it('does not show the banner if opted-in', async () => { it('does not show the banner if opted-in', async () => {
await supertest await supertest
.post('/api/telemetry/v2/optIn') .post('/internal/telemetry/optIn')
.set('kbn-xsrf', 'xxx') .set('kbn-xsrf', 'xxx')
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ enabled: true }) .send({ enabled: true })
.expect(200); .expect(200);
@ -95,8 +101,10 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide
it('does not show the banner if opted-out in this version', async () => { it('does not show the banner if opted-out in this version', async () => {
await supertest await supertest
.post('/api/telemetry/v2/optIn') .post('/internal/telemetry/optIn')
.set('kbn-xsrf', 'xxx') .set('kbn-xsrf', 'xxx')
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ enabled: false }) .send({ enabled: false })
.expect(200); .expect(200);

View file

@ -8,6 +8,6 @@
export const name = 'telemetry'; export const name = 'telemetry';
export const description = 'Get the clusters stats from the Kibana server'; export const description = 'Get the clusters stats from the Kibana server';
export const method = 'POST'; export const method = 'POST';
export const path = '/api/telemetry/v2/clusters/_stats'; export const path = '/internal/telemetry/clusters/_stats';
export const body = { unencrypted: true, refreshCache: true }; export const body = { unencrypted: true, refreshCache: true };

View file

@ -8,6 +8,10 @@
import expect from '@kbn/expect'; import expect from '@kbn/expect';
import { SuperTest } from 'supertest'; import { SuperTest } from 'supertest';
import { CSV_QUOTE_VALUES_SETTING } from '@kbn/share-plugin/common/constants'; import { CSV_QUOTE_VALUES_SETTING } from '@kbn/share-plugin/common/constants';
import {
ELASTIC_HTTP_VERSION_HEADER,
X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
} from '@kbn/core-http-common';
import { FtrProviderContext } from '../../../ftr_provider_context'; import { FtrProviderContext } from '../../../ftr_provider_context';
export default function featureControlsTests({ getService }: FtrProviderContext) { export default function featureControlsTests({ getService }: FtrProviderContext) {
@ -64,9 +68,11 @@ export default function featureControlsTests({ getService }: FtrProviderContext)
const basePath = spaceId ? `/s/${spaceId}` : ''; const basePath = spaceId ? `/s/${spaceId}` : '';
return await supertest return await supertest
.post(`${basePath}/api/telemetry/v2/optIn`) .post(`${basePath}/internal/telemetry/optIn`)
.auth(username, password) .auth(username, password)
.set('kbn-xsrf', 'foo') .set('kbn-xsrf', 'foo')
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ enabled: true }) .send({ enabled: true })
.then((response: any) => ({ error: undefined, response })) .then((response: any) => ({ error: undefined, response }))
.catch((error: any) => ({ error, response: undefined })); .catch((error: any) => ({ error, response: undefined }));

View file

@ -7,6 +7,10 @@
import expect from '@kbn/expect'; import expect from '@kbn/expect';
import { estypes } from '@elastic/elasticsearch'; import { estypes } from '@elastic/elasticsearch';
import {
ELASTIC_HTTP_VERSION_HEADER,
X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
} from '@kbn/core-http-common';
import { FtrProviderContext } from '../../ftr_provider_context'; import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) { export default function ({ getService }: FtrProviderContext) {
@ -17,7 +21,9 @@ export default function ({ getService }: FtrProviderContext) {
const { const {
body: [{ stats: apiResponse }], body: [{ stats: apiResponse }],
} = await supertest } = await supertest
.post(`/api/telemetry/v2/clusters/_stats`) .post(`/internal/telemetry/clusters/_stats`)
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.set('kbn-xsrf', 'xxxx') .set('kbn-xsrf', 'xxxx')
.send({ .send({
unencrypted: true, unencrypted: true,

View file

@ -21,6 +21,10 @@ import type {
CacheDetails, CacheDetails,
} from '@kbn/telemetry-collection-manager-plugin/server/types'; } from '@kbn/telemetry-collection-manager-plugin/server/types';
import { assertTelemetryPayload } from '@kbn/telemetry-tools'; 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 basicClusterFixture from './fixtures/basiccluster.json';
import multiClusterFixture from './fixtures/multicluster.json'; import multiClusterFixture from './fixtures/multicluster.json';
import type { SecurityService } from '../../../../../test/common/services/security/security'; import type { SecurityService } from '../../../../../test/common/services/security/security';
@ -97,7 +101,7 @@ export default function ({ getService }: FtrProviderContext) {
const esSupertest = getService('esSupertest'); const esSupertest = getService('esSupertest');
const security = getService('security'); const security = getService('security');
describe('/api/telemetry/v2/clusters/_stats', () => { describe('/internal/telemetry/clusters/_stats', () => {
const timestamp = new Date().toISOString(); const timestamp = new Date().toISOString();
describe('monitoring/multicluster', () => { describe('monitoring/multicluster', () => {
let localXPack: Record<string, unknown>; let localXPack: Record<string, unknown>;
@ -112,8 +116,10 @@ export default function ({ getService }: FtrProviderContext) {
await updateMonitoringDates(esSupertest, fromTimestamp, toTimestamp, timestamp); await updateMonitoringDates(esSupertest, fromTimestamp, toTimestamp, timestamp);
const { body }: { body: UnencryptedTelemetryPayload } = await supertest const { body }: { body: UnencryptedTelemetryPayload } = await supertest
.post('/api/telemetry/v2/clusters/_stats') .post('/internal/telemetry/clusters/_stats')
.set('kbn-xsrf', 'xxx') .set('kbn-xsrf', 'xxx')
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ unencrypted: true, refreshCache: true }) .send({ unencrypted: true, refreshCache: true })
.expect(200); .expect(200);
@ -167,8 +173,10 @@ export default function ({ getService }: FtrProviderContext) {
after(() => esArchiver.unload(archive)); after(() => esArchiver.unload(archive));
it('should load non-expiring basic cluster', async () => { it('should load non-expiring basic cluster', async () => {
const { body }: { body: UnencryptedTelemetryPayload } = await supertest const { body }: { body: UnencryptedTelemetryPayload } = await supertest
.post('/api/telemetry/v2/clusters/_stats') .post('/internal/telemetry/clusters/_stats')
.set('kbn-xsrf', 'xxx') .set('kbn-xsrf', 'xxx')
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ unencrypted: true, refreshCache: true }) .send({ unencrypted: true, refreshCache: true })
.expect(200); .expect(200);
@ -193,8 +201,10 @@ export default function ({ getService }: FtrProviderContext) {
await updateMonitoringDates(esSupertest, fromTimestamp, toTimestamp, timestamp); await updateMonitoringDates(esSupertest, fromTimestamp, toTimestamp, timestamp);
// hit the endpoint to cache results // hit the endpoint to cache results
await supertest await supertest
.post('/api/telemetry/v2/clusters/_stats') .post('/internal/telemetry/clusters/_stats')
.set('kbn-xsrf', 'xxx') .set('kbn-xsrf', 'xxx')
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ unencrypted: true, refreshCache: true }) .send({ unencrypted: true, refreshCache: true })
.expect(200); .expect(200);
}); });
@ -204,8 +214,10 @@ export default function ({ getService }: FtrProviderContext) {
it('returns non-cached results when unencrypted', async () => { it('returns non-cached results when unencrypted', async () => {
const now = Date.now(); const now = Date.now();
const { body }: { body: UnencryptedTelemetryPayload } = await supertest const { body }: { body: UnencryptedTelemetryPayload } = await supertest
.post('/api/telemetry/v2/clusters/_stats') .post('/internal/telemetry/clusters/_stats')
.set('kbn-xsrf', 'xxx') .set('kbn-xsrf', 'xxx')
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ unencrypted: true }) .send({ unencrypted: true })
.expect(200); .expect(200);
@ -224,8 +236,10 @@ export default function ({ getService }: FtrProviderContext) {
it('grabs a fresh copy on refresh', async () => { it('grabs a fresh copy on refresh', async () => {
const now = Date.now(); const now = Date.now();
const { body }: { body: UnencryptedTelemetryPayload } = await supertest const { body }: { body: UnencryptedTelemetryPayload } = await supertest
.post('/api/telemetry/v2/clusters/_stats') .post('/internal/telemetry/clusters/_stats')
.set('kbn-xsrf', 'xxx') .set('kbn-xsrf', 'xxx')
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ unencrypted: true, refreshCache: true }) .send({ unencrypted: true, refreshCache: true })
.expect(200); .expect(200);
@ -243,16 +257,20 @@ export default function ({ getService }: FtrProviderContext) {
describe('superadmin user', () => { describe('superadmin user', () => {
it('should return unencrypted telemetry for the admin user', async () => { it('should return unencrypted telemetry for the admin user', async () => {
await supertest await supertest
.post('/api/telemetry/v2/clusters/_stats') .post('/internal/telemetry/clusters/_stats')
.set('kbn-xsrf', 'xxx') .set('kbn-xsrf', 'xxx')
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ unencrypted: true }) .send({ unencrypted: true })
.expect(200); .expect(200);
}); });
it('should return encrypted telemetry for the admin user', async () => { it('should return encrypted telemetry for the admin user', async () => {
await supertest await supertest
.post('/api/telemetry/v2/clusters/_stats') .post('/internal/telemetry/clusters/_stats')
.set('kbn-xsrf', 'xxx') .set('kbn-xsrf', 'xxx')
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ unencrypted: false }) .send({ unencrypted: false })
.expect(200); .expect(200);
}); });
@ -281,18 +299,22 @@ export default function ({ getService }: FtrProviderContext) {
it('should return encrypted telemetry for the global-read user', async () => { it('should return encrypted telemetry for the global-read user', async () => {
await supertestWithoutAuth await supertestWithoutAuth
.post('/api/telemetry/v2/clusters/_stats') .post('/internal/telemetry/clusters/_stats')
.auth(globalReadOnlyUser, password(globalReadOnlyUser)) .auth(globalReadOnlyUser, password(globalReadOnlyUser))
.set('kbn-xsrf', 'xxx') .set('kbn-xsrf', 'xxx')
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ unencrypted: false }) .send({ unencrypted: false })
.expect(200); .expect(200);
}); });
it('should return unencrypted telemetry for the global-read user', async () => { it('should return unencrypted telemetry for the global-read user', async () => {
await supertestWithoutAuth await supertestWithoutAuth
.post('/api/telemetry/v2/clusters/_stats') .post('/internal/telemetry/clusters/_stats')
.auth(globalReadOnlyUser, password(globalReadOnlyUser)) .auth(globalReadOnlyUser, password(globalReadOnlyUser))
.set('kbn-xsrf', 'xxx') .set('kbn-xsrf', 'xxx')
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ unencrypted: true }) .send({ unencrypted: true })
.expect(200); .expect(200);
}); });
@ -330,18 +352,22 @@ export default function ({ getService }: FtrProviderContext) {
it('should return encrypted telemetry for the read-only user', async () => { it('should return encrypted telemetry for the read-only user', async () => {
await supertestWithoutAuth await supertestWithoutAuth
.post('/api/telemetry/v2/clusters/_stats') .post('/internal/telemetry/clusters/_stats')
.auth(noGlobalUser, password(noGlobalUser)) .auth(noGlobalUser, password(noGlobalUser))
.set('kbn-xsrf', 'xxx') .set('kbn-xsrf', 'xxx')
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ unencrypted: false }) .send({ unencrypted: false })
.expect(200); .expect(200);
}); });
it('should return 403 when the read-only user requests unencrypted telemetry', async () => { it('should return 403 when the read-only user requests unencrypted telemetry', async () => {
await supertestWithoutAuth await supertestWithoutAuth
.post('/api/telemetry/v2/clusters/_stats') .post('/internal/telemetry/clusters/_stats')
.auth(noGlobalUser, password(noGlobalUser)) .auth(noGlobalUser, password(noGlobalUser))
.set('kbn-xsrf', 'xxx') .set('kbn-xsrf', 'xxx')
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ unencrypted: true }) .send({ unencrypted: true })
.expect(403); .expect(403);
}); });

View file

@ -12,6 +12,10 @@ import ossPluginsTelemetrySchema from '@kbn/telemetry-plugin/schema/oss_plugins.
import xpackRootTelemetrySchema from '@kbn/telemetry-collection-xpack-plugin/schema/xpack_root.json'; import xpackRootTelemetrySchema from '@kbn/telemetry-collection-xpack-plugin/schema/xpack_root.json';
import xpackPluginsTelemetrySchema from '@kbn/telemetry-collection-xpack-plugin/schema/xpack_plugins.json'; import xpackPluginsTelemetrySchema from '@kbn/telemetry-collection-xpack-plugin/schema/xpack_plugins.json';
import { assertTelemetryPayload } from '@kbn/telemetry-tools'; import { assertTelemetryPayload } from '@kbn/telemetry-tools';
import {
ELASTIC_HTTP_VERSION_HEADER,
X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
} from '@kbn/core-http-common';
import { flatKeys } from '../../../../../test/api_integration/apis/telemetry/utils'; import { flatKeys } from '../../../../../test/api_integration/apis/telemetry/utils';
import type { FtrProviderContext } from '../../ftr_provider_context'; import type { FtrProviderContext } from '../../ftr_provider_context';
@ -31,7 +35,7 @@ export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest'); const supertest = getService('supertest');
const es = getService('es'); const es = getService('es');
describe('/api/telemetry/v2/clusters/_stats with monitoring disabled', () => { describe('/internal/telemetry/clusters/_stats with monitoring disabled', () => {
let stats: Record<string, any>; let stats: Record<string, any>;
before('disable monitoring and pull local stats', async () => { before('disable monitoring and pull local stats', async () => {
@ -39,8 +43,10 @@ export default function ({ getService }: FtrProviderContext) {
await new Promise((r) => setTimeout(r, 1000)); await new Promise((r) => setTimeout(r, 1000));
const { body } = await supertest const { body } = await supertest
.post('/api/telemetry/v2/clusters/_stats') .post('/internal/telemetry/clusters/_stats')
.set('kbn-xsrf', 'xxx') .set('kbn-xsrf', 'xxx')
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ unencrypted: true, refreshCache: true }) .send({ unencrypted: true, refreshCache: true })
.expect(200); .expect(200);

View file

@ -6,6 +6,10 @@
*/ */
import { UsageStatsPayload } from '@kbn/telemetry-collection-manager-plugin/server'; import { UsageStatsPayload } from '@kbn/telemetry-collection-manager-plugin/server';
import {
ELASTIC_HTTP_VERSION_HEADER,
X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
} from '@kbn/core-http-common';
import { FtrProviderContext } from '../ftr_provider_context'; import { FtrProviderContext } from '../ftr_provider_context';
export interface UsageStatsPayloadTestFriendly extends UsageStatsPayload { export interface UsageStatsPayloadTestFriendly extends UsageStatsPayload {
@ -29,9 +33,10 @@ export function UsageAPIProvider({ getService }: FtrProviderContext) {
refreshCache?: boolean; refreshCache?: boolean;
}): Promise<Array<{ clusterUuid: string; stats: UsageStatsPayloadTestFriendly | string }>> { }): Promise<Array<{ clusterUuid: string; stats: UsageStatsPayloadTestFriendly | string }>> {
const { body } = await supertest const { body } = await supertest
.post('/api/telemetry/v2/clusters/_stats') .post('/internal/telemetry/clusters/_stats')
.set('kbn-xsrf', 'xxx') .set('kbn-xsrf', 'xxx')
.set('x-elastic-internal-origin', 'xxx') .set(ELASTIC_HTTP_VERSION_HEADER, '2')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ refreshCache: true, ...payload }) .send({ refreshCache: true, ...payload })
.expect(200); .expect(200);
return body; return body;

View file

@ -6,7 +6,10 @@
*/ */
import expect from '@kbn/expect'; import expect from '@kbn/expect';
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; import {
ELASTIC_HTTP_VERSION_HEADER,
X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
} from '@kbn/core-http-common';
import { data, MockTelemetryFindings } from './data'; import { data, MockTelemetryFindings } from './data';
import type { FtrProviderContext } from '../ftr_provider_context'; import type { FtrProviderContext } from '../ftr_provider_context';
@ -67,7 +70,9 @@ export default function ({ getService }: FtrProviderContext) {
const { const {
body: [{ stats: apiResponse }], body: [{ stats: apiResponse }],
} = await supertest } = await supertest
.post(`/api/telemetry/v2/clusters/_stats`) .post(`/internal/telemetry/clusters/_stats`)
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.set('kbn-xsrf', 'xxxx') .set('kbn-xsrf', 'xxxx')
.send({ .send({
unencrypted: true, unencrypted: true,
@ -119,8 +124,10 @@ export default function ({ getService }: FtrProviderContext) {
const { const {
body: [{ stats: apiResponse }], body: [{ stats: apiResponse }],
} = await supertest } = await supertest
.post(`/api/telemetry/v2/clusters/_stats`) .post(`/internal/telemetry/clusters/_stats`)
.set('kbn-xsrf', 'xxxx') .set('kbn-xsrf', 'xxxx')
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ .send({
unencrypted: true, unencrypted: true,
refreshCache: true, refreshCache: true,
@ -164,8 +171,10 @@ export default function ({ getService }: FtrProviderContext) {
const { const {
body: [{ stats: apiResponse }], body: [{ stats: apiResponse }],
} = await supertest } = await supertest
.post(`/api/telemetry/v2/clusters/_stats`) .post(`/internal/telemetry/clusters/_stats`)
.set('kbn-xsrf', 'xxxx') .set('kbn-xsrf', 'xxxx')
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ .send({
unencrypted: true, unencrypted: true,
refreshCache: true, refreshCache: true,
@ -240,8 +249,10 @@ export default function ({ getService }: FtrProviderContext) {
const { const {
body: [{ stats: apiResponse }], body: [{ stats: apiResponse }],
} = await supertest } = await supertest
.post(`/api/telemetry/v2/clusters/_stats`) .post(`/internal/telemetry/clusters/_stats`)
.set('kbn-xsrf', 'xxxx') .set('kbn-xsrf', 'xxxx')
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ .send({
unencrypted: true, unencrypted: true,
refreshCache: true, refreshCache: true,
@ -294,8 +305,10 @@ export default function ({ getService }: FtrProviderContext) {
const { const {
body: [{ stats: apiResponse }], body: [{ stats: apiResponse }],
} = await supertest } = await supertest
.post(`/api/telemetry/v2/clusters/_stats`) .post(`/internal/telemetry/clusters/_stats`)
.set('kbn-xsrf', 'xxxx') .set('kbn-xsrf', 'xxxx')
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ .send({
unencrypted: true, unencrypted: true,
refreshCache: true, refreshCache: true,

View file

@ -8,6 +8,10 @@
import type { ToolingLog } from '@kbn/tooling-log'; import type { ToolingLog } from '@kbn/tooling-log';
import type SuperTest from 'supertest'; import type SuperTest from 'supertest';
import type { DetectionMetrics } from '@kbn/security-solution-plugin/server/usage/detections/types'; import type { DetectionMetrics } from '@kbn/security-solution-plugin/server/usage/detections/types';
import {
ELASTIC_HTTP_VERSION_HEADER,
X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
} from '@kbn/core-http-common';
import { getStatsUrl } from './get_stats_url'; import { getStatsUrl } from './get_stats_url';
import { getDetectionMetricsFromBody } from './get_detection_metrics_from_body'; import { getDetectionMetricsFromBody } from './get_detection_metrics_from_body';
@ -24,6 +28,8 @@ export const getStats = async (
const response = await supertest const response = await supertest
.post(getStatsUrl()) .post(getStatsUrl())
.set('kbn-xsrf', 'true') .set('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ unencrypted: true, refreshCache: true }); .send({ unencrypted: true, refreshCache: true });
if (response.status !== 200) { if (response.status !== 200) {
log.error( log.error(

View file

@ -8,4 +8,4 @@
/** /**
* Cluster stats URL. Replace this with any from kibana core if there is ever a constant there for this. * Cluster stats URL. Replace this with any from kibana core if there is ever a constant there for this.
*/ */
export const getStatsUrl = (): string => '/api/telemetry/v2/clusters/_stats'; export const getStatsUrl = (): string => '/internal/telemetry/clusters/_stats';

View file

@ -5,6 +5,10 @@
* 2.0. * 2.0.
*/ */
import {
ELASTIC_HTTP_VERSION_HEADER,
X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
} from '@kbn/core-http-common';
import expect from '@kbn/expect'; import expect from '@kbn/expect';
import { FtrProviderContext } from '../../api_integration/ftr_provider_context'; import { FtrProviderContext } from '../../api_integration/ftr_provider_context';
import { skipIfNoDockerRegistry, generateAgent } from '../helpers'; import { skipIfNoDockerRegistry, generateAgent } from '../helpers';
@ -124,8 +128,10 @@ export default function (providerContext: FtrProviderContext) {
const { const {
body: [{ stats: apiResponse }], body: [{ stats: apiResponse }],
} = await supertest } = await supertest
.post(`/api/telemetry/v2/clusters/_stats`) .post(`/internal/telemetry/clusters/_stats`)
.set('kbn-xsrf', 'xxxx') .set('kbn-xsrf', 'xxxx')
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ .send({
unencrypted: true, unencrypted: true,
refreshCache: true, refreshCache: true,

View file

@ -6,6 +6,10 @@
*/ */
import expect from '@kbn/expect'; import expect from '@kbn/expect';
import {
ELASTIC_HTTP_VERSION_HEADER,
X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
} from '@kbn/core-http-common';
import { DATES } from './constants'; import { DATES } from './constants';
import { FtrProviderContext } from '../../ftr_provider_context'; import { FtrProviderContext } from '../../ftr_provider_context';
@ -133,8 +137,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await logsUi.logStreamPage.getStreamEntries(); await logsUi.logStreamPage.getStreamEntries();
const [{ stats }] = await supertest const [{ stats }] = await supertest
.post(`/api/telemetry/v2/clusters/_stats`) .post(`/internal/telemetry/clusters/_stats`)
.set(COMMON_REQUEST_HEADERS) .set(COMMON_REQUEST_HEADERS)
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.set('Accept', 'application/json') .set('Accept', 'application/json')
.send({ .send({
unencrypted: true, unencrypted: true,

View file

@ -5,6 +5,10 @@
* 2.0. * 2.0.
*/ */
import {
ELASTIC_HTTP_VERSION_HEADER,
X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
} from '@kbn/core-http-common';
import expect from '@kbn/expect'; import expect from '@kbn/expect';
import type { FtrProviderContext } from '../ftr_provider_context'; import type { FtrProviderContext } from '../ftr_provider_context';
import { assertLogContains, isExecutionContextLog, ANY } from '../test_utils'; import { assertLogContains, isExecutionContextLog, ANY } from '../test_utils';
@ -111,8 +115,10 @@ export default function ({ getService }: FtrProviderContext) {
it('propagates context for Telemetry collection', async () => { it('propagates context for Telemetry collection', async () => {
await supertest await supertest
.post('/api/telemetry/v2/clusters/_stats') .post('/internal/telemetry/clusters/_stats')
.set('kbn-xsrf', 'true') .set('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.send({ unencrypted: false }) .send({ unencrypted: false })
.expect(200); .expect(200);

View file

@ -1,5 +1,5 @@
{ {
"journeyName": "POST /api/telemetry/v2/clusters/_stats - 1600 dataviews", "journeyName": "POST /internal/telemetry/clusters/_stats - 1600 dataviews",
"scalabilitySetup": { "scalabilitySetup": {
"warmup": [ "warmup": [
{ {
@ -30,13 +30,15 @@
{ {
"http": { "http": {
"method": "POST", "method": "POST",
"path": "/api/telemetry/v2/clusters/_stats", "path": "/internal/telemetry/clusters/_stats",
"body": "{}", "body": "{}",
"headers": { "headers": {
"Cookie": "", "Cookie": "",
"Kbn-Version": "", "Kbn-Version": "",
"Accept-Encoding": "gzip, deflate, br", "Accept-Encoding": "gzip, deflate, br",
"Content-Type": "application/json" "Content-Type": "application/json",
"elastic-api-version": "2",
"x-elastic-internal-origin": "kibana"
}, },
"statusCode": 200 "statusCode": 200
} }

View file

@ -1,5 +1,5 @@
{ {
"journeyName": "POST /api/telemetry/v2/clusters/_stats", "journeyName": "POST /internal/telemetry/clusters/_stats",
"scalabilitySetup": { "scalabilitySetup": {
"warmup": [ "warmup": [
{ {
@ -28,13 +28,15 @@
{ {
"http": { "http": {
"method": "POST", "method": "POST",
"path": "/api/telemetry/v2/clusters/_stats", "path": "/internal/telemetry/clusters/_stats",
"body": "{}", "body": "{}",
"headers": { "headers": {
"Cookie": "", "Cookie": "",
"Kbn-Version": "", "Kbn-Version": "",
"Accept-Encoding": "gzip, deflate, br", "Accept-Encoding": "gzip, deflate, br",
"Content-Type": "application/json" "Content-Type": "application/json",
"elastic-api-version": "2",
"x-elastic-internal-origin": "kibana"
}, },
"statusCode": 200 "statusCode": 200
} }

View file

@ -1,5 +1,5 @@
{ {
"journeyName": "POST /api/telemetry/v2/clusters/_stats - no cache - 1600 dataviews", "journeyName": "POST /internal/telemetry/clusters/_stats - no cache - 1600 dataviews",
"scalabilitySetup": { "scalabilitySetup": {
"responseTimeThreshold": { "responseTimeThreshold": {
"threshold1": 1000, "threshold1": 1000,
@ -35,13 +35,15 @@
{ {
"http": { "http": {
"method": "POST", "method": "POST",
"path": "/api/telemetry/v2/clusters/_stats", "path": "/internal/telemetry/clusters/_stats",
"body": "{ \"refreshCache\": true }", "body": "{ \"refreshCache\": true }",
"headers": { "headers": {
"Cookie": "", "Cookie": "",
"Kbn-Version": "", "Kbn-Version": "",
"Accept-Encoding": "gzip, deflate, br", "Accept-Encoding": "gzip, deflate, br",
"Content-Type": "application/json" "Content-Type": "application/json",
"elastic-api-version": "2",
"x-elastic-internal-origin": "kibana"
}, },
"timeout": 240000, "timeout": 240000,
"statusCode": 200 "statusCode": 200

View file

@ -1,5 +1,5 @@
{ {
"journeyName": "POST /api/telemetry/v2/clusters/_stats - no cache", "journeyName": "POST /internal/telemetry/clusters/_stats - no cache",
"scalabilitySetup": { "scalabilitySetup": {
"responseTimeThreshold": { "responseTimeThreshold": {
"threshold1": 1000, "threshold1": 1000,
@ -33,13 +33,15 @@
{ {
"http": { "http": {
"method": "POST", "method": "POST",
"path": "/api/telemetry/v2/clusters/_stats", "path": "/internal/telemetry/clusters/_stats",
"body": "{ \"refreshCache\": true }", "body": "{ \"refreshCache\": true }",
"headers": { "headers": {
"Cookie": "", "Cookie": "",
"Kbn-Version": "", "Kbn-Version": "",
"Accept-Encoding": "gzip, deflate, br", "Accept-Encoding": "gzip, deflate, br",
"Content-Type": "application/json" "Content-Type": "application/json",
"elastic-api-version": "2",
"x-elastic-internal-origin": "kibana"
}, },
"statusCode": 200 "statusCode": 200
} }