From ca98c07d43e04a44577048816839fec1d448cd90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Wed, 23 Feb 2022 12:35:31 +0100 Subject: [PATCH 01/21] [Telemetry] Include `security` dependency --- src/plugins/home/kibana.json | 2 +- src/plugins/telemetry/kibana.json | 1 + src/plugins/telemetry/server/plugin.ts | 13 ++++++++++++- src/plugins/telemetry/server/routes/index.ts | 8 +++++--- .../server/routes/telemetry_usage_stats.ts | 17 +++++++++++++++-- src/plugins/telemetry/tsconfig.json | 3 ++- .../server/plugin.ts | 13 +++---------- .../server/types.ts | 14 +++----------- 8 files changed, 42 insertions(+), 29 deletions(-) diff --git a/src/plugins/home/kibana.json b/src/plugins/home/kibana.json index d8c09ab5e80c6a..02b33e814e2a1c 100644 --- a/src/plugins/home/kibana.json +++ b/src/plugins/home/kibana.json @@ -8,6 +8,6 @@ "server": true, "ui": true, "requiredPlugins": ["dataViews", "share", "urlForwarding"], - "optionalPlugins": ["usageCollection", "telemetry", "customIntegrations"], + "optionalPlugins": ["usageCollection", "customIntegrations"], "requiredBundles": ["kibanaReact"] } diff --git a/src/plugins/telemetry/kibana.json b/src/plugins/telemetry/kibana.json index 09cc6accb68f4b..2206072d86947c 100644 --- a/src/plugins/telemetry/kibana.json +++ b/src/plugins/telemetry/kibana.json @@ -8,6 +8,7 @@ "server": true, "ui": true, "requiredPlugins": ["telemetryCollectionManager", "usageCollection", "screenshotMode"], + "optionalPlugins": ["security"], "extraPublicDirs": ["common/constants"], "requiredBundles": ["kibanaUtils", "kibanaReact"] } diff --git a/src/plugins/telemetry/server/plugin.ts b/src/plugins/telemetry/server/plugin.ts index 73c61ea1c50386..681a871ba105b6 100644 --- a/src/plugins/telemetry/server/plugin.ts +++ b/src/plugins/telemetry/server/plugin.ts @@ -23,6 +23,7 @@ import type { Plugin, Logger, } from 'src/core/server'; +import type { SecurityPluginStart } from '../../../../x-pack/plugins/security/server'; import { SavedObjectsClient } from '../../../core/server'; import { registerRoutes } from './routes'; import { registerCollection } from './telemetry_collection'; @@ -42,6 +43,7 @@ interface TelemetryPluginsDepsSetup { interface TelemetryPluginsDepsStart { telemetryCollectionManager: TelemetryCollectionManagerPluginStart; + security?: SecurityPluginStart; } /** @@ -90,6 +92,8 @@ export class TelemetryPlugin implements Plugin(1); + private security?: SecurityPluginStart; + constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); this.isDev = initializerContext.env.mode.dev; @@ -119,6 +123,7 @@ export class TelemetryPlugin implements Plugin this.security, }); this.registerMappings((opts) => savedObjects.registerType(opts)); @@ -137,11 +142,17 @@ export class TelemetryPlugin implements Plugin; + getSecurity: SecurityGetter; } export function registerRoutes(options: RegisterRoutesParams) { - const { isDev, telemetryCollectionManager, router, savedObjectsInternalClient$ } = options; + const { isDev, telemetryCollectionManager, router, savedObjectsInternalClient$, getSecurity } = + options; registerTelemetryOptInRoutes(options); - registerTelemetryUsageStatsRoutes(router, telemetryCollectionManager, isDev); + registerTelemetryUsageStatsRoutes(router, telemetryCollectionManager, isDev, getSecurity); registerTelemetryOptInStatsRoutes(router, telemetryCollectionManager); registerTelemetryUserHasSeenNotice(router); registerTelemetryLastReported(router, savedObjectsInternalClient$); diff --git a/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts b/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts index 2f72ae818f1121..c94f3c1f8d1335 100644 --- a/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts +++ b/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts @@ -12,11 +12,15 @@ import { TelemetryCollectionManagerPluginSetup, StatsGetterConfig, } from 'src/plugins/telemetry_collection_manager/server'; +import type { SecurityPluginStart } from '../../../../../x-pack/plugins/security/server'; + +export type SecurityGetter = () => SecurityPluginStart | undefined; export function registerTelemetryUsageStatsRoutes( router: IRouter, telemetryCollectionManager: TelemetryCollectionManagerPluginSetup, - isDev: boolean + isDev: boolean, + getSecurity: SecurityGetter ) { router.post( { @@ -31,9 +35,18 @@ export function registerTelemetryUsageStatsRoutes( async (context, req, res) => { const { unencrypted, refreshCache } = req.body; + const security = getSecurity(); + if (security) { + const hasEnoughPrivileges = await security.authz + .checkPrivilegesWithRequest(req) + .globally({ kibana: 'decryptedTelemetry' }); + if (!hasEnoughPrivileges) { + return res.forbidden(); + } + } + try { const statsConfig: StatsGetterConfig = { - request: req, unencrypted, refreshCache: unencrypted || refreshCache, }; diff --git a/src/plugins/telemetry/tsconfig.json b/src/plugins/telemetry/tsconfig.json index d50ccd563fe5ac..194cc1161063be 100644 --- a/src/plugins/telemetry/tsconfig.json +++ b/src/plugins/telemetry/tsconfig.json @@ -21,6 +21,7 @@ { "path": "../../plugins/kibana_utils/tsconfig.json" }, { "path": "../../plugins/screenshot_mode/tsconfig.json" }, { "path": "../../plugins/telemetry_collection_manager/tsconfig.json" }, - { "path": "../../plugins/usage_collection/tsconfig.json" } + { "path": "../../plugins/usage_collection/tsconfig.json" }, + { "path": "../../../x-pack/plugins/security/tsconfig.json" } ] } diff --git a/src/plugins/telemetry_collection_manager/server/plugin.ts b/src/plugins/telemetry_collection_manager/server/plugin.ts index fad51ca1dbfde8..cffe736f8eeaf5 100644 --- a/src/plugins/telemetry_collection_manager/server/plugin.ts +++ b/src/plugins/telemetry_collection_manager/server/plugin.ts @@ -126,11 +126,10 @@ export class TelemetryCollectionManagerPlugin const esClient = this.getElasticsearchClient(config); const soClient = this.getSavedObjectsClient(config); // Provide the kibanaRequest so opted-in plugins can scope their custom clients only if the request is not encrypted - const kibanaRequest = config.unencrypted ? config.request : void 0; const refreshCache = config.unencrypted ? true : !!config.refreshCache; if (esClient && soClient) { - return { usageCollection, esClient, soClient, kibanaRequest, refreshCache }; + return { usageCollection, esClient, soClient, refreshCache }; } } @@ -142,9 +141,7 @@ export class TelemetryCollectionManagerPlugin * @private */ private getElasticsearchClient(config: StatsGetterConfig): ElasticsearchClient | undefined { - return config.unencrypted - ? this.elasticsearchClient?.asScoped(config.request).asCurrentUser - : this.elasticsearchClient?.asInternalUser; + return this.elasticsearchClient?.asInternalUser; } /** @@ -155,11 +152,7 @@ export class TelemetryCollectionManagerPlugin * @private */ private getSavedObjectsClient(config: StatsGetterConfig): SavedObjectsClientContract | undefined { - if (config.unencrypted) { - // Intentionally using the scoped client here to make use of all the security wrappers. - // It also returns spaces-scoped telemetry. - return this.savedObjectsService?.getScopedClient(config.request); - } else if (this.savedObjectsService) { + if (this.savedObjectsService) { // Wrapping the internalRepository with the `TelemetrySavedObjectsClient` // to ensure some best practices when collecting "all the telemetry" // (i.e.: `.find` requests should query all spaces) diff --git a/src/plugins/telemetry_collection_manager/server/types.ts b/src/plugins/telemetry_collection_manager/server/types.ts index 7ea32844a858cb..9658c0d68d05db 100644 --- a/src/plugins/telemetry_collection_manager/server/types.ts +++ b/src/plugins/telemetry_collection_manager/server/types.ts @@ -6,14 +6,9 @@ * Side Public License, v 1. */ -import { - ElasticsearchClient, - Logger, - KibanaRequest, - SavedObjectsClientContract, -} from 'src/core/server'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { TelemetryCollectionManagerPlugin } from './plugin'; +import type { ElasticsearchClient, Logger, SavedObjectsClientContract } from 'src/core/server'; +import type { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import type { TelemetryCollectionManagerPlugin } from './plugin'; export interface TelemetryCollectionManagerPluginSetup { setCollectionStrategy: ( @@ -36,7 +31,6 @@ export interface TelemetryOptInStats { export interface BaseStatsGetterConfig { unencrypted: boolean; refreshCache?: boolean; - request?: KibanaRequest; } export interface EncryptedStatsGetterConfig extends BaseStatsGetterConfig { @@ -45,7 +39,6 @@ export interface EncryptedStatsGetterConfig extends BaseStatsGetterConfig { export interface UnencryptedStatsGetterConfig extends BaseStatsGetterConfig { unencrypted: true; - request: KibanaRequest; } export interface ClusterDetails { @@ -56,7 +49,6 @@ export interface StatsCollectionConfig { usageCollection: UsageCollectionSetup; esClient: ElasticsearchClient; soClient: SavedObjectsClientContract; - kibanaRequest: KibanaRequest | undefined; // intentionally `| undefined` to enforce providing the parameter refreshCache: boolean; } From da5c75609e8a1fa4637669391cf450d5a088f127 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Wed, 23 Feb 2022 13:43:25 +0100 Subject: [PATCH 02/21] Clean usage collection types (removal of KibanaRequest context extension) --- .../imported_interface_from_export/index.ts | 2 +- .../server/telemetry_collection/get_kibana.ts | 12 +- .../telemetry_collection/get_local_stats.ts | 4 +- src/plugins/usage_collection/README.mdx | 3 +- .../server/collector/collector.ts | 25 +- .../server/collector/collector_set.test.ts | 262 +----------------- .../server/collector/collector_set.ts | 37 +-- .../server/collector/index.ts | 1 - .../server/collector/types.ts | 77 +---- .../server/collector/usage_collector.ts | 12 +- src/plugins/usage_collection/server/mocks.ts | 16 +- src/plugins/usage_collection/server/plugin.ts | 18 +- .../server/routes/stats/stats.ts | 8 +- .../collectors/get_usage_collector.ts | 11 +- .../task_manager_usage_collector.test.ts | 6 +- 15 files changed, 54 insertions(+), 440 deletions(-) diff --git a/src/fixtures/telemetry_collectors/imported_interface_from_export/index.ts b/src/fixtures/telemetry_collectors/imported_interface_from_export/index.ts index 222a89bf322984..991f8336e70202 100644 --- a/src/fixtures/telemetry_collectors/imported_interface_from_export/index.ts +++ b/src/fixtures/telemetry_collectors/imported_interface_from_export/index.ts @@ -11,7 +11,7 @@ import { createUsageCollectionSetupMock } from '../../../plugins/usage_collectio const { makeUsageCollector } = createUsageCollectionSetupMock(); -export const myCollector = makeUsageCollector({ +export const myCollector = makeUsageCollector({ type: 'importing_from_export_collector', isReady: () => true, fetch() { diff --git a/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts b/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts index 83f33a894b9032..4340eaafd2d8ff 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts @@ -7,10 +7,9 @@ */ import { omit } from 'lodash'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; -import { StatsCollectionContext } from 'src/plugins/telemetry_collection_manager/server'; -import { ElasticsearchClient } from 'src/core/server'; +import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; +import type { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import type { StatsCollectionContext } from 'src/plugins/telemetry_collection_manager/server'; export interface KibanaUsageStats { kibana: { @@ -71,9 +70,8 @@ export function handleKibanaStats( export async function getKibana( usageCollection: UsageCollectionSetup, asInternalUser: ElasticsearchClient, - soClient: SavedObjectsClientContract, - kibanaRequest: KibanaRequest | undefined // intentionally `| undefined` to enforce providing the parameter + soClient: SavedObjectsClientContract ): Promise { - const usage = await usageCollection.bulkFetch(asInternalUser, soClient, kibanaRequest); + const usage = await usageCollection.bulkFetch(asInternalUser, soClient); return usageCollection.toObject(usage); } diff --git a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts index ae2a849ccfa19a..73de59ae8156aa 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts @@ -65,7 +65,7 @@ export const getLocalStats: StatsGetter = async ( config, context ) => { - const { usageCollection, esClient, soClient, kibanaRequest } = config; + const { usageCollection, esClient, soClient } = config; return await Promise.all( clustersDetails.map(async (clustersDetail) => { @@ -73,7 +73,7 @@ export const getLocalStats: StatsGetter = async ( getClusterInfo(esClient), // cluster info getClusterStats(esClient), // cluster stats (not to be confused with cluster _state_) getNodesUsage(esClient), // nodes_usage info - getKibana(usageCollection, esClient, soClient, kibanaRequest), + getKibana(usageCollection, esClient, soClient), getDataTelemetry(esClient), ]); return handleLocalStats( diff --git a/src/plugins/usage_collection/README.mdx b/src/plugins/usage_collection/README.mdx index a58f197818bf4e..03d8f7badb8c2a 100644 --- a/src/plugins/usage_collection/README.mdx +++ b/src/plugins/usage_collection/README.mdx @@ -297,8 +297,7 @@ Some background: - `isReady` (added in v7.2.0 and v6.8.4) is a way for a usage collector to announce that some async process must finish first before it can return data in the `fetch` method (e.g. a client needs to ne initialized, or the task manager needs to run a task first). If any collector reports that it is not ready when we call its `fetch` method, we reset a flag to try again and, after a set amount of time, collect data from those collectors that are ready and skip any that are not. This means that if a collector returns `true` for `isReady` and it actually isn't ready to return data, there won't be telemetry data from that collector in that telemetry report (usually once per day). You should consider what it means if your collector doesn't return data in the first few documents when Kibana starts or, if we should wait for any other reason (e.g. the task manager needs to run your task first). If you need to tell telemetry collection to wait, you should implement this function with custom logic. If your `fetch` method can run without the need of any previous dependencies, then you can return true for `isReady` as shown in the example below. -- The `fetch` method needs to support multiple contexts in which it is called. For example, when a user requests the example of what we collect in the **Kibana>Advanced Settings>Usage data** section, the clients provided in the context of the function (`CollectorFetchContext`) are scoped to that user's privileges. The reason is to avoid exposing via telemetry any data that user should not have access to (i.e.: if the user does not have access to certain indices, they shouldn't be allowed to see the number of documents that exists in it). In this case, the `fetch` method receives the clients `esClient` and `soClient` scoped to the user who performed the HTTP API request. Alternatively, when requesting the usage data to be reported to the Remote Telemetry Service, the clients are scoped to the internal Kibana user (`kibana_system`). Please, mind it might have lower-level access than the default super-admin `elastic` test user. -In some scenarios, your collector might need to maintain its own client. An example of that is the `monitoring` plugin, that maintains a connection to the Remote Monitoring Cluster to push its monitoring data. If that's the case, your plugin can opt-in to receive the additional `kibanaRequest` parameter by adding `extendFetchContext.kibanaRequest: true` to the collector's config: it will be appended to the context of the `fetch` method only if the request needs to be scoped to a user other than Kibana Internal, so beware that your collector will need to work for both scenarios (especially for the scenario when `kibanaRequest` is missing). +- The clients provided to the `fetch` method are scoped to the internal Kibana user (`kibana_system`). Note: there will be many cases where you won't need to use the `esClient` or `soClient` function that gets passed in to your `fetch` method at all. Your feature might have an accumulating value in server memory, or read something from the OS. diff --git a/src/plugins/usage_collection/server/collector/collector.ts b/src/plugins/usage_collection/server/collector/collector.ts index 74373d44a359b6..1ff04cf3650c0b 100644 --- a/src/plugins/usage_collection/server/collector/collector.ts +++ b/src/plugins/usage_collection/server/collector/collector.ts @@ -7,20 +7,14 @@ */ import type { Logger } from 'src/core/server'; -import type { - CollectorFetchMethod, - CollectorOptions, - CollectorOptionsFetchExtendedContext, - ICollector, -} from './types'; +import type { CollectorFetchMethod, CollectorOptions, ICollector } from './types'; export class Collector implements ICollector { - public readonly extendFetchContext: CollectorOptionsFetchExtendedContext; - public readonly type: CollectorOptions['type']; - public readonly fetch: CollectorFetchMethod; - public readonly isReady: CollectorOptions['isReady']; + public readonly type: CollectorOptions['type']; + public readonly fetch: CollectorFetchMethod; + public readonly isReady: CollectorOptions['isReady']; /** * @private Constructor of a Collector. It should be called via the CollectorSet factory methods: `makeStatsCollector` and `makeUsageCollector` * @param log {@link Logger} @@ -28,15 +22,7 @@ export class Collector */ constructor( public readonly log: Logger, - { - type, - fetch, - isReady, - extendFetchContext = {}, - ...options - }: // Any does not affect here, but needs to be set so it doesn't affect anything else down the line - // eslint-disable-next-line @typescript-eslint/no-explicit-any - CollectorOptions + { type, fetch, isReady, ...options }: CollectorOptions ) { if (type === undefined) { throw new Error('Collector must be instantiated with a options.type string property'); @@ -50,6 +36,5 @@ export class Collector this.type = type; this.fetch = fetch; this.isReady = typeof isReady === 'function' ? isReady : () => true; - this.extendFetchContext = extendFetchContext; } } diff --git a/src/plugins/usage_collection/server/collector/collector_set.test.ts b/src/plugins/usage_collection/server/collector/collector_set.test.ts index 5e0698b286f79b..87e841f3de4c54 100644 --- a/src/plugins/usage_collection/server/collector/collector_set.test.ts +++ b/src/plugins/usage_collection/server/collector/collector_set.test.ts @@ -15,7 +15,6 @@ import { elasticsearchServiceMock, loggingSystemMock, savedObjectsClientMock, - httpServerMock, executionContextServiceMock, } from '../../../../core/server/mocks'; import type { ExecutionContextSetup, Logger } from 'src/core/server'; @@ -39,7 +38,6 @@ describe('CollectorSet', () => { }); const mockEsClient = elasticsearchServiceMock.createClusterClient().asInternalUser; const mockSoClient = savedObjectsClientMock.create(); - const req = void 0; // No need to instantiate any KibanaRequest in these tests it('should throw an error if non-Collector type of object is registered', () => { const collectors = new CollectorSet(collectorSetConfig); @@ -88,7 +86,7 @@ describe('CollectorSet', () => { }) ); - const result = await collectors.bulkFetch(mockEsClient, mockSoClient, req); + const result = await collectors.bulkFetch(mockEsClient, mockSoClient); expect(logger.debug).toHaveBeenCalledTimes(2); expect(logger.debug).toHaveBeenCalledWith('Getting ready collectors'); expect(logger.debug).toHaveBeenCalledWith('Fetching data from MY_TEST_COLLECTOR collector'); @@ -121,7 +119,7 @@ describe('CollectorSet', () => { let result; try { - result = await collectors.bulkFetch(mockEsClient, mockSoClient, req); + result = await collectors.bulkFetch(mockEsClient, mockSoClient); } catch (err) { // Do nothing } @@ -150,7 +148,7 @@ describe('CollectorSet', () => { }) ); - const result = await collectors.bulkFetch(mockEsClient, mockSoClient, req); + const result = await collectors.bulkFetch(mockEsClient, mockSoClient); expect(result).toStrictEqual([ { type: 'MY_TEST_COLLECTOR', @@ -178,7 +176,7 @@ describe('CollectorSet', () => { }) ); - const result = await collectors.bulkFetch(mockEsClient, mockSoClient, req); + const result = await collectors.bulkFetch(mockEsClient, mockSoClient); expect(result).toStrictEqual([ { type: 'MY_TEST_COLLECTOR', @@ -269,50 +267,6 @@ describe('CollectorSet', () => { collectorSet = new CollectorSet(collectorSetConfig); }); - test('TS should hide kibanaRequest when not opted-in', () => { - collectorSet.makeStatsCollector({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - // @ts-expect-error - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - }); - }); - - test('TS should hide kibanaRequest when not opted-in (explicit false)', () => { - collectorSet.makeStatsCollector({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - // @ts-expect-error - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - extendFetchContext: { - kibanaRequest: false, - }, - }); - }); - - test('TS should allow using kibanaRequest when opted-in (explicit true)', () => { - collectorSet.makeStatsCollector({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - extendFetchContext: { - kibanaRequest: true, - }, - }); - }); - test('fetch can use the logger (TS allows it)', () => { const collector = collectorSet.makeStatsCollector({ type: 'MY_TEST_COLLECTOR', @@ -339,188 +293,6 @@ describe('CollectorSet', () => { collectorSet = new CollectorSet(collectorSetConfig); }); - describe('TS validations', () => { - describe('when types are inferred', () => { - test('TS should hide kibanaRequest when not opted-in', () => { - collectorSet.makeUsageCollector({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - // @ts-expect-error - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - }); - }); - - test('TS should hide kibanaRequest when not opted-in (explicit false)', () => { - collectorSet.makeUsageCollector({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - // @ts-expect-error - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - extendFetchContext: { - kibanaRequest: false, - }, - }); - }); - - test('TS should allow using kibanaRequest when opted-in (explicit true)', () => { - collectorSet.makeUsageCollector({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - extendFetchContext: { - kibanaRequest: true, - }, - }); - }); - }); - - describe('when types are explicit', () => { - test('TS should hide `kibanaRequest` from ctx when undefined or false', () => { - collectorSet.makeUsageCollector<{ test: number }>({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - // @ts-expect-error - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - }); - collectorSet.makeUsageCollector<{ test: number }, false>({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - // @ts-expect-error - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - extendFetchContext: { - kibanaRequest: false, - }, - }); - collectorSet.makeUsageCollector<{ test: number }, false>({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - // @ts-expect-error - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - }); - }); - test('TS should not allow `true` when types declare false', () => { - // false is the default when at least 1 type is specified - collectorSet.makeUsageCollector<{ test: number }>({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - // @ts-expect-error - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - extendFetchContext: { - // @ts-expect-error - kibanaRequest: true, - }, - }); - collectorSet.makeUsageCollector<{ test: number }, false>({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - // @ts-expect-error - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - extendFetchContext: { - // @ts-expect-error - kibanaRequest: true, - }, - }); - }); - - test('TS should allow `true` when types explicitly declare `true` and do not allow `false` or undefined', () => { - // false is the default when at least 1 type is specified - collectorSet.makeUsageCollector<{ test: number }, true>({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - extendFetchContext: { - kibanaRequest: true, - }, - }); - collectorSet.makeUsageCollector<{ test: number }, true>({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - extendFetchContext: { - // @ts-expect-error - kibanaRequest: false, - }, - }); - collectorSet.makeUsageCollector<{ test: number }, true>({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - extendFetchContext: { - // @ts-expect-error - kibanaRequest: undefined, - }, - }); - collectorSet.makeUsageCollector<{ test: number }, true>({ - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - // @ts-expect-error - extendFetchContext: {}, - }); - collectorSet.makeUsageCollector<{ test: number }, true>( - // @ts-expect-error - { - type: 'MY_TEST_COLLECTOR', - isReady: () => true, - schema: { test: { type: 'long' } }, - fetch: (ctx) => { - const { kibanaRequest } = ctx; - return { test: kibanaRequest ? 1 : 0 }; - }, - } - ); - }); - }); - }); - test('fetch can use the logger (TS allows it)', () => { const collector = collectorSet.makeUsageCollector({ type: 'MY_TEST_COLLECTOR', @@ -777,31 +549,5 @@ describe('CollectorSet', () => { expect.any(Function) ); }); - - it('adds extra context to collectors with extendFetchContext config', async () => { - const mockReadyFetch = jest.fn().mockResolvedValue({}); - collectorSet.registerCollector( - collectorSet.makeUsageCollector({ - type: 'ready_col', - isReady: () => true, - schema: {}, - fetch: mockReadyFetch, - extendFetchContext: { kibanaRequest: true }, - }) - ); - - const mockEsClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - const mockSoClient = savedObjectsClientMock.create(); - const request = httpServerMock.createKibanaRequest(); - const results = await collectorSet.bulkFetch(mockEsClient, mockSoClient, request); - - expect(mockReadyFetch).toBeCalledTimes(1); - expect(mockReadyFetch).toBeCalledWith({ - esClient: mockEsClient, - soClient: mockSoClient, - kibanaRequest: request, - }); - expect(results).toHaveLength(2); - }); }); }); diff --git a/src/plugins/usage_collection/server/collector/collector_set.ts b/src/plugins/usage_collection/server/collector/collector_set.ts index 49332b0a1826fc..3a7c0a66ac60d1 100644 --- a/src/plugins/usage_collection/server/collector/collector_set.ts +++ b/src/plugins/usage_collection/server/collector/collector_set.ts @@ -11,7 +11,6 @@ import type { Logger, ElasticsearchClient, SavedObjectsClientContract, - KibanaRequest, KibanaExecutionContext, ExecutionContextSetup, } from 'src/core/server'; @@ -64,12 +63,8 @@ export class CollectorSet { * Instantiates a stats collector with the definition provided in the options * @param options Definition of the collector {@link CollectorOptions} */ - public makeStatsCollector = < - TFetchReturn, - WithKibanaRequest extends boolean, - ExtraOptions extends object = {} - >( - options: CollectorOptions + public makeStatsCollector = ( + options: CollectorOptions ) => { return new Collector(this.logger, options); }; @@ -78,15 +73,8 @@ export class CollectorSet { * Instantiates an usage collector with the definition provided in the options * @param options Definition of the collector {@link CollectorOptions} */ - public makeUsageCollector = < - TFetchReturn, - // TODO: Right now, users will need to explicitly claim `true` for TS to allow `kibanaRequest` usage. - // If we improve `telemetry-check-tools` so plugins do not need to specify TFetchReturn, - // we'll be able to remove the type defaults and TS will successfully infer the config value as provided in JS. - WithKibanaRequest extends boolean = false, - ExtraOptions extends object = {} - >( - options: UsageCollectorOptions + public makeUsageCollector = ( + options: UsageCollectorOptions ) => { return new UsageCollector(this.logger, options); }; @@ -191,7 +179,6 @@ export class CollectorSet { public bulkFetch = async ( esClient: ElasticsearchClient, soClient: SavedObjectsClientContract, - kibanaRequest: KibanaRequest | undefined, // intentionally `| undefined` to enforce providing the parameter collectors: Map = this.collectors ) => { this.logger.debug(`Getting ready collectors`); @@ -209,11 +196,7 @@ export class CollectorSet { readyCollectors.map(async (collector) => { this.logger.debug(`Fetching data from ${collector.type} collector`); try { - const context = { - esClient, - soClient, - ...(collector.extendFetchContext.kibanaRequest && { kibanaRequest }), - }; + const context = { esClient, soClient }; const executionContext: KibanaExecutionContext = { type: 'usage_collection', name: 'collector.fetch', @@ -254,16 +237,10 @@ export class CollectorSet { public bulkFetchUsage = async ( esClient: ElasticsearchClient, - savedObjectsClient: SavedObjectsClientContract, - kibanaRequest: KibanaRequest | undefined // intentionally `| undefined` to enforce providing the parameter + savedObjectsClient: SavedObjectsClientContract ) => { const usageCollectors = this.getFilteredCollectorSet((c) => c instanceof UsageCollector); - return await this.bulkFetch( - esClient, - savedObjectsClient, - kibanaRequest, - usageCollectors.collectors - ); + return await this.bulkFetch(esClient, savedObjectsClient, usageCollectors.collectors); }; /** diff --git a/src/plugins/usage_collection/server/collector/index.ts b/src/plugins/usage_collection/server/collector/index.ts index ca240a520ee24a..e284844b34c344 100644 --- a/src/plugins/usage_collection/server/collector/index.ts +++ b/src/plugins/usage_collection/server/collector/index.ts @@ -17,7 +17,6 @@ export type { CollectorOptions, CollectorFetchContext, CollectorFetchMethod, - CollectorOptionsFetchExtendedContext, ICollector as Collector, } from './types'; export type { UsageCollectorOptions } from './usage_collector'; diff --git a/src/plugins/usage_collection/server/collector/types.ts b/src/plugins/usage_collection/server/collector/types.ts index bf1e9f4644b1b7..8d427d211a191b 100644 --- a/src/plugins/usage_collection/server/collector/types.ts +++ b/src/plugins/usage_collection/server/collector/types.ts @@ -6,12 +6,7 @@ * Side Public License, v 1. */ -import type { - ElasticsearchClient, - KibanaRequest, - SavedObjectsClientContract, - Logger, -} from 'src/core/server'; +import type { ElasticsearchClient, SavedObjectsClientContract, Logger } from 'src/core/server'; /** Types matching number values **/ export type AllowedSchemaNumberTypes = @@ -73,7 +68,7 @@ export type MakeSchemaFrom = { * * @remark Bear in mind when testing your collector that your user has the same privileges as the Kibana Internal user to ensure the expected data is sent to the remote cluster. */ -export type CollectorFetchContext = { +export interface CollectorFetchContext { /** * Request-scoped Elasticsearch client * @remark Bear in mind when testing your collector that your user has the same privileges as the Kibana Internal user to ensure the expected data is sent to the remote cluster (more info: {@link CollectorFetchContext}) @@ -84,58 +79,22 @@ export type CollectorFetchContext = ( +export type CollectorFetchMethod = ( this: ICollector & ExtraOptions, // Specify the context of `this` for this.log and others to become available - context: CollectorFetchContext + context: CollectorFetchContext ) => Promise | TReturn; -export interface ICollectorOptionsFetchExtendedContext { - /** - * Set to `true` if your `fetch` method requires the `KibanaRequest` object to be added in its context {@link CollectorFetchContextWithRequest}. - * @remark You should fully acknowledge that by using the `KibanaRequest` in your collector, you need to ensure it should specially work without it because it won't be provided when building the telemetry payload actually sent to the remote telemetry service. - */ - kibanaRequest?: WithKibanaRequest; -} - -/** - * The options to extend the context provided to the `fetch` method. - * @remark Only to be used in very rare scenarios when this is really needed. - */ -export type CollectorOptionsFetchExtendedContext = - ICollectorOptionsFetchExtendedContext & - (WithKibanaRequest extends true // If enforced to true via Types, the config must be expected - ? Required, 'kibanaRequest'>> - : {}); - /** * Options to instantiate a collector */ -export type CollectorOptions< - TFetchReturn = unknown, - WithKibanaRequest extends boolean = boolean, - ExtraOptions extends object = {} -> = { +export type CollectorOptions = { /** * Unique string identifier for the collector */ @@ -152,17 +111,8 @@ export type CollectorOptions< * The method that will collect and return the data in the final format. * @param collectorFetchContext {@link CollectorFetchContext} */ - fetch: CollectorFetchMethod; -} & ExtraOptions & - (WithKibanaRequest extends true // If enforced to true via Types, the config must be enforced - ? { - /** {@link CollectorOptionsFetchExtendedContext} **/ - extendFetchContext: CollectorOptionsFetchExtendedContext; - } - : { - /** {@link CollectorOptionsFetchExtendedContext} **/ - extendFetchContext?: CollectorOptionsFetchExtendedContext; - }); + fetch: CollectorFetchMethod; +} & ExtraOptions; /** * Common interface for Usage and Stats Collectors @@ -170,13 +120,8 @@ export type CollectorOptions< export interface ICollector { /** Logger **/ readonly log: Logger; - /** - * The options to extend the context provided to the `fetch` method: {@link CollectorOptionsFetchExtendedContext}. - * @remark Only to be used in very rare scenarios when this is really needed. - */ - readonly extendFetchContext: CollectorOptionsFetchExtendedContext; /** The registered type (aka name) of the collector **/ - readonly type: CollectorOptions['type']; + readonly type: CollectorOptions['type']; /** * The actual logic that reports the Usage collection. * It will be called on every collection request. @@ -188,9 +133,9 @@ export interface ICollector { * [type]: await fetch(context) * } */ - readonly fetch: CollectorFetchMethod; + readonly fetch: CollectorFetchMethod; /** * Should return `true` when it's safe to call the `fetch` method. */ - readonly isReady: CollectorOptions['isReady']; + readonly isReady: CollectorOptions['isReady']; } diff --git a/src/plugins/usage_collection/server/collector/usage_collector.ts b/src/plugins/usage_collection/server/collector/usage_collector.ts index 15f7cd9c627fcb..2ed8c2a50dbafd 100644 --- a/src/plugins/usage_collection/server/collector/usage_collector.ts +++ b/src/plugins/usage_collection/server/collector/usage_collector.ts @@ -15,10 +15,9 @@ import { Collector } from './collector'; */ export type UsageCollectorOptions< TFetchReturn = unknown, - WithKibanaRequest extends boolean = false, ExtraOptions extends object = {} -> = CollectorOptions & - Required, 'schema'>>; +> = CollectorOptions & + Required, 'schema'>>; /** * @private Only used in fixtures as a type @@ -27,12 +26,7 @@ export class UsageCollector exte TFetchReturn, ExtraOptions > { - constructor( - log: Logger, - // Needed because it doesn't affect on anything here but being explicit creates a lot of pain down the line - // eslint-disable-next-line @typescript-eslint/no-explicit-any - collectorOptions: UsageCollectorOptions - ) { + constructor(log: Logger, collectorOptions: UsageCollectorOptions) { super(log, collectorOptions); } } diff --git a/src/plugins/usage_collection/server/mocks.ts b/src/plugins/usage_collection/server/mocks.ts index 6f7d4f19cbaf12..ac7ad69ed4bce7 100644 --- a/src/plugins/usage_collection/server/mocks.ts +++ b/src/plugins/usage_collection/server/mocks.ts @@ -9,7 +9,6 @@ import { elasticsearchServiceMock, executionContextServiceMock, - httpServerMock, loggingSystemMock, savedObjectsClientMock, } from '../../../../src/core/server/mocks'; @@ -45,25 +44,14 @@ export const createUsageCollectionSetupMock = () => { return usageCollectionSetupMock; }; -export function createCollectorFetchContextMock(): jest.Mocked> { - const collectorFetchClientsMock: jest.Mocked> = { +export function createCollectorFetchContextMock(): jest.Mocked { + const collectorFetchClientsMock: jest.Mocked = { esClient: elasticsearchServiceMock.createClusterClient().asInternalUser, soClient: savedObjectsClientMock.create(), }; return collectorFetchClientsMock; } -export function createCollectorFetchContextWithKibanaMock(): jest.Mocked< - CollectorFetchContext -> { - const collectorFetchClientsMock: jest.Mocked> = { - esClient: elasticsearchServiceMock.createClusterClient().asInternalUser, - soClient: savedObjectsClientMock.create(), - kibanaRequest: httpServerMock.createKibanaRequest(), - }; - return collectorFetchClientsMock; -} - export const usageCollectionPluginMock = { createSetupContract: createUsageCollectionSetupMock, }; diff --git a/src/plugins/usage_collection/server/plugin.ts b/src/plugins/usage_collection/server/plugin.ts index f415dd768dc226..7cde8bad706dd1 100644 --- a/src/plugins/usage_collection/server/plugin.ts +++ b/src/plugins/usage_collection/server/plugin.ts @@ -15,7 +15,6 @@ import type { Plugin, ElasticsearchClient, SavedObjectsClientContract, - KibanaRequest, } from 'src/core/server'; import type { ConfigType } from './config'; import { CollectorSet } from './collector'; @@ -39,12 +38,8 @@ export interface UsageCollectionSetup { * Creates a usage collector to collect plugin telemetry data. * registerCollector must be called to connect the created collector with the service. */ - makeUsageCollector: < - TFetchReturn, - WithKibanaRequest extends boolean = false, - ExtraOptions extends object = {} - >( - options: UsageCollectorOptions + makeUsageCollector: ( + options: UsageCollectorOptions ) => Collector; /** * Register a usage collector or a stats collector. @@ -66,7 +61,6 @@ export interface UsageCollectionSetup { bulkFetch: ( esClient: ElasticsearchClient, soClient: SavedObjectsClientContract, - kibanaRequest: KibanaRequest | undefined, // intentionally `| undefined` to enforce providing the parameter collectors?: Map> ) => Promise>; /** @@ -88,12 +82,8 @@ export interface UsageCollectionSetup { * registerCollector must be called to connect the created collector with the service. * @internal: telemetry and monitoring use */ - makeStatsCollector: < - TFetchReturn, - WithKibanaRequest extends boolean, - ExtraOptions extends object = {} - >( - options: CollectorOptions + makeStatsCollector: ( + options: CollectorOptions ) => Collector; } diff --git a/src/plugins/usage_collection/server/routes/stats/stats.ts b/src/plugins/usage_collection/server/routes/stats/stats.ts index 8e5382d1631721..72cbd2e5899ff5 100644 --- a/src/plugins/usage_collection/server/routes/stats/stats.ts +++ b/src/plugins/usage_collection/server/routes/stats/stats.ts @@ -15,7 +15,6 @@ import { first } from 'rxjs/operators'; import { ElasticsearchClient, IRouter, - KibanaRequest, MetricsServiceSetup, SavedObjectsClientContract, ServiceStatus, @@ -55,10 +54,9 @@ export function registerStatsRoute({ }) { const getUsage = async ( esClient: ElasticsearchClient, - savedObjectsClient: SavedObjectsClientContract, - kibanaRequest: KibanaRequest + savedObjectsClient: SavedObjectsClientContract ): Promise => { - const usage = await collectorSet.bulkFetchUsage(esClient, savedObjectsClient, kibanaRequest); + const usage = await collectorSet.bulkFetchUsage(esClient, savedObjectsClient); return collectorSet.toObject(usage); }; @@ -97,7 +95,7 @@ export function registerStatsRoute({ const [usage, clusterUuid] = await Promise.all([ shouldGetUsage - ? getUsage(asCurrentUser, savedObjectsClient, req) + ? getUsage(asCurrentUser, savedObjectsClient) : Promise.resolve({}), getClusterUuid(asCurrentUser), ]); diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts index cbbfe64f5e3e22..0c952949c56b49 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts @@ -18,7 +18,7 @@ export function getMonitoringUsageCollector( config: MonitoringConfig, getClient: () => IClusterClient ) { - return usageCollection.makeUsageCollector({ + return usageCollection.makeUsageCollector({ type: 'monitoring', isReady: () => true, schema: { @@ -95,13 +95,8 @@ export function getMonitoringUsageCollector( }, }, }, - extendFetchContext: { - kibanaRequest: true, - }, - fetch: async ({ kibanaRequest }) => { - const callCluster = kibanaRequest - ? getClient().asScoped(kibanaRequest).asCurrentUser - : getClient().asInternalUser; + fetch: async () => { + const callCluster = getClient().asInternalUser; const usageClusters: MonitoringClusterStackProductUsage[] = []; const availableCcs = config.ui.ccs.enabled; const clusters = await fetchClusters(callCluster); diff --git a/x-pack/plugins/task_manager/server/usage/task_manager_usage_collector.test.ts b/x-pack/plugins/task_manager/server/usage/task_manager_usage_collector.test.ts index 9b66792efcd9e3..b7a52a7a41bcf6 100644 --- a/x-pack/plugins/task_manager/server/usage/task_manager_usage_collector.test.ts +++ b/x-pack/plugins/task_manager/server/usage/task_manager_usage_collector.test.ts @@ -9,7 +9,7 @@ import { merge } from 'lodash'; import { loggingSystemMock } from 'src/core/server/mocks'; import { Collector, - createCollectorFetchContextWithKibanaMock, + createCollectorFetchContextMock, createUsageCollectionSetupMock, } from 'src/plugins/usage_collection/server/mocks'; import { HealthStatus } from '../monitoring'; @@ -26,7 +26,7 @@ describe('registerTaskManagerUsageCollector', () => { it('should report telemetry on the ephemeral queue', async () => { const monitoringStats$ = new Subject(); const usageCollectionMock = createUsageCollectionSetupMock(); - const fetchContext = createCollectorFetchContextWithKibanaMock(); + const fetchContext = createCollectorFetchContextMock(); usageCollectionMock.makeUsageCollector.mockImplementation((config) => { collector = new Collector(logger, config); return createUsageCollectionSetupMock().makeUsageCollector(config); @@ -53,7 +53,7 @@ describe('registerTaskManagerUsageCollector', () => { it('should report telemetry on the excluded task types', async () => { const monitoringStats$ = new Subject(); const usageCollectionMock = createUsageCollectionSetupMock(); - const fetchContext = createCollectorFetchContextWithKibanaMock(); + const fetchContext = createCollectorFetchContextMock(); usageCollectionMock.makeUsageCollector.mockImplementation((config) => { collector = new Collector(logger, config); return createUsageCollectionSetupMock().makeUsageCollector(config); From de282e20ee7f41f3188abae5248ba73cec961350 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Wed, 23 Feb 2022 15:57:20 +0100 Subject: [PATCH 03/21] Fix premissions usage --- src/plugins/telemetry/server/routes/telemetry_usage_stats.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts b/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts index c94f3c1f8d1335..839f7e25db1433 100644 --- a/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts +++ b/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts @@ -37,10 +37,10 @@ export function registerTelemetryUsageStatsRoutes( const security = getSecurity(); if (security) { - const hasEnoughPrivileges = await security.authz + const { hasAllRequested } = await security.authz .checkPrivilegesWithRequest(req) .globally({ kibana: 'decryptedTelemetry' }); - if (!hasEnoughPrivileges) { + if (!hasAllRequested) { return res.forbidden(); } } From 7d4aa84e3fcfe4e8c5384ccefdbd653827a39c05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Wed, 23 Feb 2022 22:55:17 +0100 Subject: [PATCH 04/21] Remove `home` dependency on `telemetry` --- .../__snapshots__/home.test.tsx.snap | 196 --------- .../__snapshots__/welcome.test.tsx.snap | 384 +----------------- .../application/components/home.test.tsx | 2 - .../public/application/components/home.tsx | 10 +- .../public/application/components/home_app.js | 2 - .../application/components/welcome.test.tsx | 48 +-- .../public/application/components/welcome.tsx | 98 +---- .../public/application/kibana_services.ts | 4 +- src/plugins/home/public/plugin.ts | 15 +- .../home/public/services/welcome/index.ts | 10 + .../services/welcome/welcome_service.mocks.ts | 36 ++ .../services/welcome/welcome_service.test.ts | 54 +++ .../services/welcome/welcome_service.ts | 44 ++ src/plugins/home/tsconfig.json | 3 +- src/plugins/telemetry/kibana.json | 2 +- src/plugins/telemetry/public/plugin.ts | 15 +- .../render_welcome_telemetry_notice.test.ts | 32 ++ .../render_welcome_telemetry_notice.tsx | 80 ++++ src/plugins/telemetry/tsconfig.json | 1 + 19 files changed, 305 insertions(+), 731 deletions(-) create mode 100644 src/plugins/home/public/services/welcome/index.ts create mode 100644 src/plugins/home/public/services/welcome/welcome_service.mocks.ts create mode 100644 src/plugins/home/public/services/welcome/welcome_service.test.ts create mode 100644 src/plugins/home/public/services/welcome/welcome_service.ts create mode 100644 src/plugins/telemetry/public/render_welcome_telemetry_notice.test.ts create mode 100644 src/plugins/telemetry/public/render_welcome_telemetry_notice.tsx diff --git a/src/plugins/home/public/application/components/__snapshots__/home.test.tsx.snap b/src/plugins/home/public/application/components/__snapshots__/home.test.tsx.snap index ab6ad1b6cc0c55..43d8f935221b36 100644 --- a/src/plugins/home/public/application/components/__snapshots__/home.test.tsx.snap +++ b/src/plugins/home/public/application/components/__snapshots__/home.test.tsx.snap @@ -374,202 +374,6 @@ exports[`home isNewKibanaInstance should set isNewKibanaInstance to false when t exports[`home isNewKibanaInstance should set isNewKibanaInstance to true when there are no index patterns 1`] = ` `; diff --git a/src/plugins/home/public/application/components/__snapshots__/welcome.test.tsx.snap b/src/plugins/home/public/application/components/__snapshots__/welcome.test.tsx.snap index 17f7d2520e8621..861e0ee895887c 100644 --- a/src/plugins/home/public/application/components/__snapshots__/welcome.test.tsx.snap +++ b/src/plugins/home/public/application/components/__snapshots__/welcome.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should render a Welcome screen with no telemetry disclaimer 1`] = ` +exports[`should render a Welcome screen 1`] = `
`; - -exports[`should render a Welcome screen with the telemetry disclaimer 1`] = ` - -
-
-
- - - - - -

- -

-
- -
-
-
- - - - - - - - - - - - - - - - - -
-
-
-`; - -exports[`should render a Welcome screen with the telemetry disclaimer when optIn is false 1`] = ` - -
-
-
- - - - - -

- -

-
- -
-
-
- - - - - - - - - - - - - - - - - -
-
-
-`; - -exports[`should render a Welcome screen with the telemetry disclaimer when optIn is true 1`] = ` - -
-
-
- - - - - -

- -

-
- -
-
-
- - - - - - - - - - - - - - - - - -
-
-
-`; - -exports[`should render a Welcome screen without the opt in/out link when user cannot change optIn status 1`] = ` - -
-
-
- - - - - -

- -

-
- -
-
-
- - - - - - - - - - - - - -
-
-
-`; diff --git a/src/plugins/home/public/application/components/home.test.tsx b/src/plugins/home/public/application/components/home.test.tsx index 9983afa3d4d611..f27a286488c2b1 100644 --- a/src/plugins/home/public/application/components/home.test.tsx +++ b/src/plugins/home/public/application/components/home.test.tsx @@ -12,7 +12,6 @@ import type { HomeProps } from './home'; import { Home } from './home'; import { FeatureCatalogueCategory } from '../../services'; -import { telemetryPluginMock } from '../../../../telemetry/public/mocks'; import { Welcome } from './welcome'; let mockHasIntegrationsPermission = true; @@ -57,7 +56,6 @@ describe('home', () => { setItem: jest.fn(), }, urlBasePath: 'goober', - telemetry: telemetryPluginMock.createStartContract(), addBasePath(url) { return `base_path/${url}`; }, diff --git a/src/plugins/home/public/application/components/home.tsx b/src/plugins/home/public/application/components/home.tsx index fdf04ea5806538..1fb0b3c790ab7e 100644 --- a/src/plugins/home/public/application/components/home.tsx +++ b/src/plugins/home/public/application/components/home.tsx @@ -10,7 +10,6 @@ import React, { Component } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { METRIC_TYPE } from '@kbn/analytics'; import { i18n } from '@kbn/i18n'; -import type { TelemetryPluginStart } from 'src/plugins/telemetry/public'; import { KibanaPageTemplate, OverviewPageFooter } from '../../../../kibana_react/public'; import { HOME_APP_BASE_PATH } from '../../../common/constants'; import type { FeatureCatalogueEntry, FeatureCatalogueSolution } from '../../services'; @@ -29,7 +28,6 @@ export interface HomeProps { solutions: FeatureCatalogueSolution[]; localStorage: Storage; urlBasePath: string; - telemetry: TelemetryPluginStart; hasUserDataView: () => Promise; } @@ -175,13 +173,7 @@ export class Home extends Component { } private renderWelcome() { - return ( - this.skipWelcome()} - urlBasePath={this.props.urlBasePath} - telemetry={this.props.telemetry} - /> - ); + return this.skipWelcome()} urlBasePath={this.props.urlBasePath} />; } public render() { diff --git a/src/plugins/home/public/application/components/home_app.js b/src/plugins/home/public/application/components/home_app.js index 62df479ecbfdf6..a634573aaf21ec 100644 --- a/src/plugins/home/public/application/components/home_app.js +++ b/src/plugins/home/public/application/components/home_app.js @@ -26,7 +26,6 @@ export function HomeApp({ directories, solutions }) { getBasePath, addBasePath, environmentService, - telemetry, dataViewsService, } = getServices(); const environment = environmentService.getEnvironment(); @@ -75,7 +74,6 @@ export function HomeApp({ directories, solutions }) { solutions={solutions} localStorage={localStorage} urlBasePath={getBasePath()} - telemetry={telemetry} hasUserDataView={() => dataViewsService.hasUserDataView()} /> diff --git a/src/plugins/home/public/application/components/welcome.test.tsx b/src/plugins/home/public/application/components/welcome.test.tsx index b042a91e58c9d2..623f816efaebf5 100644 --- a/src/plugins/home/public/application/components/welcome.test.tsx +++ b/src/plugins/home/public/application/components/welcome.test.tsx @@ -9,57 +9,21 @@ import React from 'react'; import { shallow } from 'enzyme'; import { Welcome } from './welcome'; -import { telemetryPluginMock } from '../../../../telemetry/public/mocks'; jest.mock('../kibana_services', () => ({ getServices: () => ({ addBasePath: (path: string) => `root${path}`, trackUiMetric: () => {}, + // can't use welcomeServiceMock because jest.mock does not allow importing out-of-scope variables + welcomeService: { + onRendered: jest.fn(), + renderTelemetryNotice: jest.fn(), + }, }), })); -test('should render a Welcome screen with the telemetry disclaimer', () => { - const telemetry = telemetryPluginMock.createStartContract(); - const component = shallow( {}} telemetry={telemetry} />); - - expect(component).toMatchSnapshot(); -}); - -test('should render a Welcome screen with the telemetry disclaimer when optIn is true', () => { - const telemetry = telemetryPluginMock.createStartContract(); - telemetry.telemetryService.getIsOptedIn = jest.fn().mockReturnValue(true); - const component = shallow( {}} telemetry={telemetry} />); - - expect(component).toMatchSnapshot(); -}); - -test('should render a Welcome screen with the telemetry disclaimer when optIn is false', () => { - const telemetry = telemetryPluginMock.createStartContract(); - telemetry.telemetryService.getIsOptedIn = jest.fn().mockReturnValue(false); - const component = shallow( {}} telemetry={telemetry} />); - - expect(component).toMatchSnapshot(); -}); - -test('should render a Welcome screen with no telemetry disclaimer', () => { +test('should render a Welcome screen', () => { const component = shallow( {}} />); expect(component).toMatchSnapshot(); }); - -test('should render a Welcome screen without the opt in/out link when user cannot change optIn status', () => { - const telemetry = telemetryPluginMock.createStartContract(); - telemetry.telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(false); - const component = shallow( {}} telemetry={telemetry} />); - - expect(component).toMatchSnapshot(); -}); - -test('fires opt-in seen when mounted', () => { - const telemetry = telemetryPluginMock.createStartContract(); - const mockSetOptedInNoticeSeen = jest.fn(); - telemetry.telemetryNotifications.setOptedInNoticeSeen = mockSetOptedInNoticeSeen; - shallow( {}} telemetry={telemetry} />); - - expect(mockSetOptedInNoticeSeen).toHaveBeenCalled(); -}); diff --git a/src/plugins/home/public/application/components/welcome.tsx b/src/plugins/home/public/application/components/welcome.tsx index 1a6251ebdca118..a536c88bc8ee50 100644 --- a/src/plugins/home/public/application/components/welcome.tsx +++ b/src/plugins/home/public/application/components/welcome.tsx @@ -12,27 +12,19 @@ * in Elasticsearch. */ -import React, { Fragment } from 'react'; -import { - EuiLink, - EuiTextColor, - EuiTitle, - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiPortal, -} from '@elastic/eui'; +import React from 'react'; +import { EuiTitle, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiPortal } from '@elastic/eui'; import { METRIC_TYPE } from '@kbn/analytics'; import { FormattedMessage } from '@kbn/i18n-react'; import { getServices } from '../kibana_services'; -import { TelemetryPluginStart } from '../../../../telemetry/public'; import { SampleDataCard } from './sample_data'; + +export type HomeWelcomeRenderTelemetryNotice = () => null | JSX.Element; + interface Props { urlBasePath: string; onSkip: () => void; - telemetry?: TelemetryPluginStart; } /** @@ -47,7 +39,7 @@ export class Welcome extends React.Component { } }; - private redirecToAddData() { + private redirectToAddData() { this.services.application.navigateToApp('integrations', { path: '/browse' }); } @@ -58,68 +50,23 @@ export class Welcome extends React.Component { private onSampleDataConfirm = () => { this.services.trackUiMetric(METRIC_TYPE.CLICK, 'sampleDataConfirm'); - this.redirecToAddData(); + this.redirectToAddData(); }; componentDidMount() { - const { telemetry } = this.props; + const { welcomeService } = this.services; this.services.trackUiMetric(METRIC_TYPE.LOADED, 'welcomeScreenMount'); - if (telemetry?.telemetryService.userCanChangeSettings) { - telemetry.telemetryNotifications.setOptedInNoticeSeen(); - } document.addEventListener('keydown', this.hideOnEsc); + welcomeService.onRendered(); } componentWillUnmount() { document.removeEventListener('keydown', this.hideOnEsc); } - private renderTelemetryEnabledOrDisabledText = () => { - const { telemetry } = this.props; - if ( - !telemetry || - !telemetry.telemetryService.userCanChangeSettings || - !telemetry.telemetryService.getCanChangeOptInStatus() - ) { - return null; - } - - const isOptedIn = telemetry.telemetryService.getIsOptedIn(); - if (isOptedIn) { - return ( - - - - - - - ); - } else { - return ( - - - - - - - ); - } - }; - render() { - const { urlBasePath, telemetry } = this.props; + const { urlBasePath } = this.props; + const { welcomeService } = this.services; return (
@@ -146,28 +93,7 @@ export class Welcome extends React.Component { onDecline={this.onSampleDataDecline} /> - {!!telemetry && ( - - - - - - - {this.renderTelemetryEnabledOrDisabledText()} - - - - )} + {welcomeService.renderTelemetryNotice()}
diff --git a/src/plugins/home/public/application/kibana_services.ts b/src/plugins/home/public/application/kibana_services.ts index fdd325df96ac57..3ccfd9413a88ad 100644 --- a/src/plugins/home/public/application/kibana_services.ts +++ b/src/plugins/home/public/application/kibana_services.ts @@ -17,7 +17,6 @@ import { ApplicationStart, } from 'kibana/public'; import { UiCounterMetricType } from '@kbn/analytics'; -import { TelemetryPluginStart } from '../../../telemetry/public'; import { UrlForwardingStart } from '../../../url_forwarding/public'; import { DataViewsContract } from '../../../data_views/public'; import { TutorialService } from '../services/tutorials'; @@ -26,6 +25,7 @@ import { FeatureCatalogueRegistry } from '../services/feature_catalogue'; import { EnvironmentService } from '../services/environment'; import { ConfigSchema } from '../../config'; import { SharePluginSetup } from '../../../share/public'; +import type { WelcomeService } from '../services/welcome'; export interface HomeKibanaServices { dataViewsService: DataViewsContract; @@ -46,9 +46,9 @@ export interface HomeKibanaServices { docLinks: DocLinksStart; addBasePath: (url: string) => string; environmentService: EnvironmentService; - telemetry?: TelemetryPluginStart; tutorialService: TutorialService; addDataService: AddDataService; + welcomeService: WelcomeService; } let services: HomeKibanaServices | null = null; diff --git a/src/plugins/home/public/plugin.ts b/src/plugins/home/public/plugin.ts index 1ece73e71f393f..ecdcadaeeab44f 100644 --- a/src/plugins/home/public/plugin.ts +++ b/src/plugins/home/public/plugin.ts @@ -29,16 +29,16 @@ import { import { ConfigSchema } from '../config'; import { setServices } from './application/kibana_services'; import { DataViewsPublicPluginStart } from '../../data_views/public'; -import { TelemetryPluginStart } from '../../telemetry/public'; import { UsageCollectionSetup } from '../../usage_collection/public'; import { UrlForwardingSetup, UrlForwardingStart } from '../../url_forwarding/public'; import { AppNavLinkStatus } from '../../../core/public'; import { PLUGIN_ID, HOME_APP_BASE_PATH } from '../common/constants'; import { SharePluginSetup } from '../../share/public'; +import type { WelcomeServiceSetup } from './services/welcome'; +import { WelcomeService } from './services/welcome'; export interface HomePluginStartDependencies { dataViews: DataViewsPublicPluginStart; - telemetry?: TelemetryPluginStart; urlForwarding: UrlForwardingStart; } @@ -61,6 +61,7 @@ export class HomePublicPlugin private readonly environmentService = new EnvironmentService(); private readonly tutorialService = new TutorialService(); private readonly addDataService = new AddDataService(); + private readonly welcomeService = new WelcomeService(); constructor(private readonly initializerContext: PluginInitializerContext) {} @@ -76,7 +77,7 @@ export class HomePublicPlugin const trackUiMetric = usageCollection ? usageCollection.reportUiCounter.bind(usageCollection, 'Kibana_home') : () => {}; - const [coreStart, { telemetry, dataViews, urlForwarding: urlForwardingStart }] = + const [coreStart, { dataViews, urlForwarding: urlForwardingStart }] = await core.getStartServices(); setServices({ share, @@ -89,7 +90,6 @@ export class HomePublicPlugin savedObjectsClient: coreStart.savedObjects.client, chrome: coreStart.chrome, application: coreStart.application, - telemetry, uiSettings: core.uiSettings, addBasePath: core.http.basePath.prepend, getBasePath: core.http.basePath.get, @@ -100,6 +100,7 @@ export class HomePublicPlugin tutorialService: this.tutorialService, addDataService: this.addDataService, featureCatalogue: this.featuresCatalogueRegistry, + welcomeService: this.welcomeService, }); coreStart.chrome.docTitle.change( i18n.translate('home.pageTitle', { defaultMessage: 'Home' }) @@ -132,6 +133,7 @@ export class HomePublicPlugin environment: { ...this.environmentService.setup() }, tutorials: { ...this.tutorialService.setup() }, addData: { ...this.addDataService.setup() }, + welcomeScreen: { ...this.welcomeService.setup() }, }; } @@ -154,17 +156,20 @@ export type TutorialSetup = TutorialServiceSetup; /** @public */ export type AddDataSetup = AddDataServiceSetup; +/** @public */ +export type WelcomeSetup = WelcomeServiceSetup; + /** @public */ export interface HomePublicPluginSetup { tutorials: TutorialServiceSetup; addData: AddDataServiceSetup; featureCatalogue: FeatureCatalogueSetup; + welcomeScreen: WelcomeSetup; /** * The environment service is only available for a transition period and will * be replaced by display specific extension points. * @deprecated */ - environment: EnvironmentSetup; } diff --git a/src/plugins/home/public/services/welcome/index.ts b/src/plugins/home/public/services/welcome/index.ts new file mode 100644 index 00000000000000..50a53f11604bd8 --- /dev/null +++ b/src/plugins/home/public/services/welcome/index.ts @@ -0,0 +1,10 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export type { WelcomeServiceSetup } from './welcome_service'; +export { WelcomeService } from './welcome_service'; diff --git a/src/plugins/home/public/services/welcome/welcome_service.mocks.ts b/src/plugins/home/public/services/welcome/welcome_service.mocks.ts new file mode 100644 index 00000000000000..921cb990663276 --- /dev/null +++ b/src/plugins/home/public/services/welcome/welcome_service.mocks.ts @@ -0,0 +1,36 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PublicMethodsOf } from '@kbn/utility-types'; +import { WelcomeService, WelcomeServiceSetup } from './welcome_service'; + +const createSetupMock = (): jest.Mocked => { + const welcomeService = new WelcomeService(); + const welcomeServiceSetup = welcomeService.setup(); + return { + registerTelemetryNoticeRenderer: jest + .fn() + .mockImplementation(welcomeServiceSetup.registerTelemetryNoticeRenderer), + registerOnRendered: jest.fn().mockImplementation(welcomeServiceSetup.registerOnRendered), + }; +}; + +const createMock = (): jest.Mocked> => { + const welcomeService = new WelcomeService(); + + return { + setup: jest.fn().mockImplementation(welcomeService.setup), + onRendered: jest.fn().mockImplementation(welcomeService.onRendered), + renderTelemetryNotice: jest.fn().mockImplementation(welcomeService.renderTelemetryNotice), + }; +}; + +export const welcomeServiceMock = { + createSetup: createSetupMock, + create: createMock, +}; diff --git a/src/plugins/home/public/services/welcome/welcome_service.test.ts b/src/plugins/home/public/services/welcome/welcome_service.test.ts new file mode 100644 index 00000000000000..a7c96aa4b41c63 --- /dev/null +++ b/src/plugins/home/public/services/welcome/welcome_service.test.ts @@ -0,0 +1,54 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { WelcomeService, WelcomeServiceSetup } from './welcome_service'; + +describe('WelcomeService', () => { + let welcomeService: WelcomeService; + let welcomeServiceSetup: WelcomeServiceSetup; + + beforeEach(() => { + welcomeService = new WelcomeService(); + welcomeServiceSetup = welcomeService.setup(); + }); + describe('onRendered', () => { + test('it should register an onRendered listener', () => { + const onRendered = jest.fn(); + welcomeServiceSetup.registerOnRendered(onRendered); + + welcomeService.onRendered(); + expect(onRendered).toHaveBeenCalledTimes(1); + }); + + test('it should allow registering multiple onRendered listeners', () => { + const onRendered = jest.fn(); + welcomeServiceSetup.registerOnRendered(onRendered); + welcomeServiceSetup.registerOnRendered(onRendered); + + welcomeService.onRendered(); + expect(onRendered).toHaveBeenCalledTimes(2); + }); + }); + describe('renderTelemetryNotice', () => { + test('it should register a renderer', () => { + const renderer = jest.fn().mockReturnValue('rendered text'); + welcomeServiceSetup.registerTelemetryNoticeRenderer(renderer); + + expect(welcomeService.renderTelemetryNotice()).toEqual('rendered text'); + }); + + test('it should use the last registered renderer', () => { + const renderer = jest.fn().mockReturnValue('rendered text'); + const renderer2 = jest.fn().mockReturnValue('other text'); + welcomeServiceSetup.registerTelemetryNoticeRenderer(renderer); + welcomeServiceSetup.registerTelemetryNoticeRenderer(renderer2); + + expect(welcomeService.renderTelemetryNotice()).toEqual('other text'); + }); + }); +}); diff --git a/src/plugins/home/public/services/welcome/welcome_service.ts b/src/plugins/home/public/services/welcome/welcome_service.ts new file mode 100644 index 00000000000000..6f1edc04d6e081 --- /dev/null +++ b/src/plugins/home/public/services/welcome/welcome_service.ts @@ -0,0 +1,44 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { HomeWelcomeRenderTelemetryNotice } from '../../application/components/welcome'; + +export interface WelcomeServiceSetup { + registerOnRendered: (onRendered: () => void) => void; + registerTelemetryNoticeRenderer: ( + renderTelemetryNotice: HomeWelcomeRenderTelemetryNotice + ) => void; +} + +export class WelcomeService { + private readonly onRenderedHandlers: Array<() => void> = []; + private renderTelemetryNoticeHandler?: HomeWelcomeRenderTelemetryNotice; + + public setup(): WelcomeServiceSetup { + return { + registerOnRendered: (onRendered) => { + this.onRenderedHandlers.push(onRendered); + }, + registerTelemetryNoticeRenderer: (renderTelemetryNotice) => { + this.renderTelemetryNoticeHandler = renderTelemetryNotice; + }, + }; + } + + public onRendered() { + this.onRenderedHandlers.forEach((onRendered) => onRendered()); + } + + public renderTelemetryNotice() { + if (this.renderTelemetryNoticeHandler) { + return this.renderTelemetryNoticeHandler(); + } else { + return null; + } + } +} diff --git a/src/plugins/home/tsconfig.json b/src/plugins/home/tsconfig.json index fa98b98ff8e1c3..17d0fc7bd91acf 100644 --- a/src/plugins/home/tsconfig.json +++ b/src/plugins/home/tsconfig.json @@ -15,7 +15,6 @@ { "path": "../kibana_react/tsconfig.json" }, { "path": "../share/tsconfig.json" }, { "path": "../url_forwarding/tsconfig.json" }, - { "path": "../usage_collection/tsconfig.json" }, - { "path": "../telemetry/tsconfig.json" } + { "path": "../usage_collection/tsconfig.json" } ] } diff --git a/src/plugins/telemetry/kibana.json b/src/plugins/telemetry/kibana.json index 2206072d86947c..10c680a5c61cff 100644 --- a/src/plugins/telemetry/kibana.json +++ b/src/plugins/telemetry/kibana.json @@ -7,7 +7,7 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["telemetryCollectionManager", "usageCollection", "screenshotMode"], + "requiredPlugins": ["home", "telemetryCollectionManager", "usageCollection", "screenshotMode"], "optionalPlugins": ["security"], "extraPublicDirs": ["common/constants"], "requiredBundles": ["kibanaUtils", "kibanaReact"] diff --git a/src/plugins/telemetry/public/plugin.ts b/src/plugins/telemetry/public/plugin.ts index 3072ff67703d78..ab28d77505368f 100644 --- a/src/plugins/telemetry/public/plugin.ts +++ b/src/plugins/telemetry/public/plugin.ts @@ -31,6 +31,8 @@ import { } from '../common/telemetry_config'; import { getNotifyUserAboutOptInDefault } from '../common/telemetry_config/get_telemetry_notify_user_about_optin_default'; import { PRIVACY_STATEMENT_URL } from '../common/constants'; +import { HomePublicPluginSetup } from '../../home/public'; +import { renderWelcomeTelemetryNotice } from './render_welcome_telemetry_notice'; /** * Publicly exposed APIs from the Telemetry Service @@ -82,6 +84,7 @@ export interface TelemetryPluginStart { interface TelemetryPluginSetupDependencies { screenshotMode: ScreenshotModePluginSetup; + home: HomePublicPluginSetup; } /** @@ -121,7 +124,7 @@ export class TelemetryPlugin implements Plugin { + if (this.telemetryService?.userCanChangeSettings) { + this.telemetryNotifications?.setOptedInNoticeSeen(); + } + }); + + home.welcomeScreen.registerTelemetryNoticeRenderer(() => + renderWelcomeTelemetryNotice(this.telemetryService!, http.basePath.prepend) + ); + return { telemetryService: this.getTelemetryServicePublicApis(), }; diff --git a/src/plugins/telemetry/public/render_welcome_telemetry_notice.test.ts b/src/plugins/telemetry/public/render_welcome_telemetry_notice.test.ts new file mode 100644 index 00000000000000..6da76db915656d --- /dev/null +++ b/src/plugins/telemetry/public/render_welcome_telemetry_notice.test.ts @@ -0,0 +1,32 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { renderWelcomeTelemetryNotice } from './render_welcome_telemetry_notice'; +import { mockTelemetryService } from './mocks'; + +describe('renderWelcomeTelemetryNotice', () => { + test('it should show the opt-out message', () => { + const telemetryService = mockTelemetryService(); + const component = mountWithIntl(renderWelcomeTelemetryNotice(telemetryService, (url) => url)); + expect(component.exists('[id="telemetry.dataManagementDisableCollectionLink"]')).toBe(true); + }); + + test('it should show the opt-in message', () => { + const telemetryService = mockTelemetryService({ config: { optIn: false } }); + const component = mountWithIntl(renderWelcomeTelemetryNotice(telemetryService, (url) => url)); + expect(component.exists('[id="telemetry.dataManagementEnableCollectionLink"]')).toBe(true); + }); + + test('it should not show opt-in/out options if user cannot change the settings', () => { + const telemetryService = mockTelemetryService({ config: { allowChangingOptInStatus: false } }); + const component = mountWithIntl(renderWelcomeTelemetryNotice(telemetryService, (url) => url)); + expect(component.exists('[id="telemetry.dataManagementDisableCollectionLink"]')).toBe(false); + expect(component.exists('[id="telemetry.dataManagementEnableCollectionLink"]')).toBe(false); + }); +}); diff --git a/src/plugins/telemetry/public/render_welcome_telemetry_notice.tsx b/src/plugins/telemetry/public/render_welcome_telemetry_notice.tsx new file mode 100644 index 00000000000000..8ef26fb797d532 --- /dev/null +++ b/src/plugins/telemetry/public/render_welcome_telemetry_notice.tsx @@ -0,0 +1,80 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiLink, EuiSpacer, EuiTextColor } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { TelemetryService } from './services'; +import { PRIVACY_STATEMENT_URL } from '../common/constants'; + +export function renderWelcomeTelemetryNotice( + telemetryService: TelemetryService, + addBasePath: (url: string) => string +) { + return ( + <> + + + + + + {renderTelemetryEnabledOrDisabledText(telemetryService, addBasePath)} + + + + ); +} + +function renderTelemetryEnabledOrDisabledText( + telemetryService: TelemetryService, + addBasePath: (url: string) => string +) { + if (!telemetryService.userCanChangeSettings || !telemetryService.getCanChangeOptInStatus()) { + return null; + } + + const isOptedIn = telemetryService.getIsOptedIn(); + + if (isOptedIn) { + return ( + <> + + + + + + ); + } else { + return ( + <> + + + + + + ); + } +} diff --git a/src/plugins/telemetry/tsconfig.json b/src/plugins/telemetry/tsconfig.json index 194cc1161063be..052d484447e428 100644 --- a/src/plugins/telemetry/tsconfig.json +++ b/src/plugins/telemetry/tsconfig.json @@ -17,6 +17,7 @@ ], "references": [ { "path": "../../core/tsconfig.json" }, + { "path": "../../plugins/home/tsconfig.json" }, { "path": "../../plugins/kibana_react/tsconfig.json" }, { "path": "../../plugins/kibana_utils/tsconfig.json" }, { "path": "../../plugins/screenshot_mode/tsconfig.json" }, From 64f6fd3edb0f926dd1a015e457083e26f30dd670 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Wed, 23 Feb 2022 23:33:57 +0100 Subject: [PATCH 05/21] Fix types --- src/plugins/home/public/mocks.ts | 7 +-- .../server/routes/telemetry_opt_in_stats.ts | 1 - .../routes/telemetry_usage_stats.test.ts | 52 +++++++++++++------ .../get_local_stats.test.ts | 3 +- .../server/plugin.test.ts | 1 - src/plugins/usage_collection/server/index.ts | 1 - 6 files changed, 42 insertions(+), 23 deletions(-) diff --git a/src/plugins/home/public/mocks.ts b/src/plugins/home/public/mocks.ts index 10c186ee3f4e30..42e489dea9d2a3 100644 --- a/src/plugins/home/public/mocks.ts +++ b/src/plugins/home/public/mocks.ts @@ -8,16 +8,17 @@ import { featureCatalogueRegistryMock } from './services/feature_catalogue/feature_catalogue_registry.mock'; import { environmentServiceMock } from './services/environment/environment.mock'; -import { configSchema } from '../config'; import { tutorialServiceMock } from './services/tutorials/tutorial_service.mock'; import { addDataServiceMock } from './services/add_data/add_data_service.mock'; +import { HomePublicPluginSetup } from './plugin'; +import { welcomeServiceMock } from './services/welcome/welcome_service.mocks'; -const createSetupContract = () => ({ +const createSetupContract = (): jest.Mocked => ({ featureCatalogue: featureCatalogueRegistryMock.createSetup(), environment: environmentServiceMock.createSetup(), tutorials: tutorialServiceMock.createSetup(), addData: addDataServiceMock.createSetup(), - config: configSchema.validate({}), + welcomeScreen: welcomeServiceMock.createSetup(), }); export const homePluginMock = { diff --git a/src/plugins/telemetry/server/routes/telemetry_opt_in_stats.ts b/src/plugins/telemetry/server/routes/telemetry_opt_in_stats.ts index 2a956656621944..6139eee3e10ca6 100644 --- a/src/plugins/telemetry/server/routes/telemetry_opt_in_stats.ts +++ b/src/plugins/telemetry/server/routes/telemetry_opt_in_stats.ts @@ -75,7 +75,6 @@ export function registerTelemetryOptInStatsRoutes( const statsGetterConfig: StatsGetterConfig = { unencrypted, - request: req, }; const optInStatus = await telemetryCollectionManager.getOptInStats( diff --git a/src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts b/src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts index 736367446d3c05..624f779381beba 100644 --- a/src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts +++ b/src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts @@ -8,7 +8,7 @@ import { registerTelemetryUsageStatsRoutes } from './telemetry_usage_stats'; import { coreMock, httpServerMock } from 'src/core/server/mocks'; -import type { RequestHandlerContext, IRouter } from 'kibana/server'; +import type { RequestHandlerContext, IRouter } from 'src/core/server'; import { telemetryCollectionManagerPluginMock } from '../../../telemetry_collection_manager/server/mocks'; async function runRequest( @@ -35,13 +35,18 @@ describe('registerTelemetryUsageStatsRoutes', () => { }; const telemetryCollectionManager = telemetryCollectionManagerPluginMock.createSetupContract(); const mockCoreSetup = coreMock.createSetup(); - const mockRouter = mockCoreSetup.http.createRouter(); const mockStats = [{ clusterUuid: 'text', stats: 'enc_str' }]; telemetryCollectionManager.getStats.mockResolvedValue(mockStats); + const getSecurity = jest.fn(); + + let mockRouter: IRouter; + beforeEach(() => { + mockRouter = mockCoreSetup.http.createRouter(); + }); describe('clusters/_stats POST route', () => { it('registers _stats POST route and accepts body configs', () => { - registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true); + registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true, getSecurity); expect(mockRouter.post).toBeCalledTimes(1); const [routeConfig, handler] = (mockRouter.post as jest.Mock).mock.calls[0]; expect(routeConfig.path).toMatchInlineSnapshot(`"/api/telemetry/v2/clusters/_stats"`); @@ -50,11 +55,10 @@ describe('registerTelemetryUsageStatsRoutes', () => { }); it('responds with encrypted stats with no cache refresh by default', async () => { - registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true); + registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true, getSecurity); - const { mockRequest, mockResponse } = await runRequest(mockRouter); + const { mockResponse } = await runRequest(mockRouter); expect(telemetryCollectionManager.getStats).toBeCalledWith({ - request: mockRequest, unencrypted: undefined, refreshCache: undefined, }); @@ -63,39 +67,57 @@ describe('registerTelemetryUsageStatsRoutes', () => { }); it('when unencrypted is set getStats is called with unencrypted and refreshCache', async () => { - registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true); + registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true, getSecurity); - const { mockRequest } = await runRequest(mockRouter, { unencrypted: true }); + await runRequest(mockRouter, { unencrypted: true }); expect(telemetryCollectionManager.getStats).toBeCalledWith({ - request: mockRequest, unencrypted: true, refreshCache: true, }); }); it('calls getStats with refreshCache when set in body', async () => { - registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true); - const { mockRequest } = await runRequest(mockRouter, { refreshCache: true }); + registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true, getSecurity); + await runRequest(mockRouter, { refreshCache: true }); expect(telemetryCollectionManager.getStats).toBeCalledWith({ - request: mockRequest, unencrypted: undefined, refreshCache: true, }); }); it('calls getStats with refreshCache:true even if set to false in body when unencrypted is set to true', async () => { - registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true); - const { mockRequest } = await runRequest(mockRouter, { + registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true, getSecurity); + await runRequest(mockRouter, { refreshCache: false, unencrypted: true, }); expect(telemetryCollectionManager.getStats).toBeCalledWith({ - request: mockRequest, unencrypted: true, refreshCache: true, }); }); + it('returns 403 when the user does not have enough permissions', async () => { + const getSecurityMock = jest.fn().mockReturnValue({ + authz: { + checkPrivilegesWithRequest: () => ({ + globally: () => ({ hasAllRequested: false }), + }), + }, + }); + registerTelemetryUsageStatsRoutes( + mockRouter, + telemetryCollectionManager, + true, + getSecurityMock + ); + const { mockResponse } = await runRequest(mockRouter, { + refreshCache: false, + unencrypted: true, + }); + expect(mockResponse.forbidden).toBeCalled(); + }); + it.todo('always returns an empty array on errors on encrypted payload'); it.todo('returns the actual request error object when in development mode'); it.todo('returns forbidden on unencrypted and ES returns 403 in getStats'); diff --git a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts index 2392ac570ecbc1..fa45438e00fbe3 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts @@ -14,7 +14,7 @@ import { usageCollectionPluginMock, createCollectorFetchContextMock, } from '../../../usage_collection/server/mocks'; -import { elasticsearchServiceMock, httpServerMock } from '../../../../../src/core/server/mocks'; +import { elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; import { StatsCollectionConfig } from '../../../telemetry_collection_manager/server'; function mockUsageCollection(kibanaUsage = {}) { @@ -74,7 +74,6 @@ function mockStatsCollectionConfig( ...createCollectorFetchContextMock(), esClient: mockGetLocalStats(clusterInfo, clusterStats), usageCollection: mockUsageCollection(kibana), - kibanaRequest: httpServerMock.createKibanaRequest(), refreshCache: false, }; } diff --git a/src/plugins/telemetry_collection_manager/server/plugin.test.ts b/src/plugins/telemetry_collection_manager/server/plugin.test.ts index ca932e92d98bdb..9ef85d3a30b3a9 100644 --- a/src/plugins/telemetry_collection_manager/server/plugin.test.ts +++ b/src/plugins/telemetry_collection_manager/server/plugin.test.ts @@ -220,7 +220,6 @@ describe('Telemetry Collection Manager', () => { const mockRequest = httpServerMock.createKibanaRequest(); const config: StatsGetterConfig = { unencrypted: true, - request: mockRequest, }; describe('getStats', () => { diff --git a/src/plugins/usage_collection/server/index.ts b/src/plugins/usage_collection/server/index.ts index 74fa77be9843cb..907a61a752052a 100644 --- a/src/plugins/usage_collection/server/index.ts +++ b/src/plugins/usage_collection/server/index.ts @@ -17,7 +17,6 @@ export type { UsageCollectorOptions, CollectorFetchContext, CollectorFetchMethod, - CollectorOptionsFetchExtendedContext, } from './collector'; export type { From eea162b770348ab7f62d6bff1e298de3c7dca0e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Wed, 23 Feb 2022 23:50:20 +0100 Subject: [PATCH 06/21] Fix more types and tests --- src/fixtures/telemetry_collectors/stats_collector.ts | 2 +- .../collectors/get_settings_collector.ts | 1 - .../register_monitoring_telemetry_collection.ts | 10 +++------- .../telemetry_collection/get_stats_with_xpack.test.ts | 3 --- 4 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/fixtures/telemetry_collectors/stats_collector.ts b/src/fixtures/telemetry_collectors/stats_collector.ts index c8f513a07253ba..6046973f42e849 100644 --- a/src/fixtures/telemetry_collectors/stats_collector.ts +++ b/src/fixtures/telemetry_collectors/stats_collector.ts @@ -19,7 +19,7 @@ interface Usage { * We should collect them when the schema is defined. */ -export const myCollectorWithSchema = makeStatsCollector({ +export const myCollectorWithSchema = makeStatsCollector({ type: 'my_stats_collector_with_schema', isReady: () => true, fetch() { diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.ts index 7096647854c157..76cc9adeb43ecd 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.ts @@ -90,7 +90,6 @@ export function getSettingsCollector( ) { return usageCollection.makeStatsCollector< EmailSettingData | undefined, - false, KibanaSettingsCollectorExtraOptions >({ type: 'kibana_settings', diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/register_monitoring_telemetry_collection.ts b/x-pack/plugins/monitoring/server/telemetry_collection/register_monitoring_telemetry_collection.ts index bce6f57d6f950a..344b04fb4780d4 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/register_monitoring_telemetry_collection.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/register_monitoring_telemetry_collection.ts @@ -34,13 +34,9 @@ export function registerMonitoringTelemetryCollection( getClient: () => IClusterClient, maxBucketSize: number ) { - const monitoringStatsCollector = usageCollection.makeStatsCollector< - MonitoringTelemetryUsage, - true - >({ + const monitoringStatsCollector = usageCollection.makeStatsCollector({ type: 'monitoringTelemetry', isReady: () => true, - extendFetchContext: { kibanaRequest: true }, schema: { stats: { type: 'array', @@ -137,13 +133,13 @@ export function registerMonitoringTelemetryCollection( }, }, }, - fetch: async ({ kibanaRequest, esClient }) => { + fetch: async () => { const timestamp = Date.now(); // Collect the telemetry from the monitoring indices for this moment. // NOTE: Usually, the monitoring indices index stats for each product every 10s (by default). // However, some data may be delayed up-to 24h because monitoring only collects extended Kibana stats in that interval // to avoid overloading of the system when retrieving data from the collectors (that delay is dealt with in the Kibana Stats getter inside the `getAllStats` method). // By 8.x, we expect to stop collecting the Kibana extended stats and keep only the monitoring-related metrics. - const callCluster = kibanaRequest ? esClient : getClient().asInternalUser; + const callCluster = getClient().asInternalUser; const clusterDetails = await getClusterUuids(callCluster, timestamp, maxBucketSize); const [licenses, stats] = await Promise.all([ getLicenses(clusterDetails, callCluster, maxBucketSize), diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts index 7febebc2a51795..e1bea8d1aa0e18 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts @@ -112,7 +112,6 @@ describe('Telemetry Collection: Get Aggregated Stats', () => { esClient, usageCollection, soClient, - kibanaRequest: undefined, refreshCache: false, }, context @@ -135,7 +134,6 @@ describe('Telemetry Collection: Get Aggregated Stats', () => { esClient, usageCollection, soClient, - kibanaRequest: undefined, refreshCache: false, }, context @@ -163,7 +161,6 @@ describe('Telemetry Collection: Get Aggregated Stats', () => { esClient, usageCollection, soClient, - kibanaRequest: undefined, refreshCache: false, }, context From 93aaf415d2f9b309e7b23ea1c0a64cf4afb290a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Thu, 24 Feb 2022 00:11:11 +0100 Subject: [PATCH 07/21] Fix TS refs (no idea why it failed in this PR) --- src/plugins/dashboard/tsconfig.json | 1 + src/plugins/home/public/index.ts | 1 + src/plugins/home/public/plugin.ts | 4 ++-- src/plugins/home/public/services/index.ts | 3 +++ 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/plugins/dashboard/tsconfig.json b/src/plugins/dashboard/tsconfig.json index 55049447aee576..862bed9d667a01 100644 --- a/src/plugins/dashboard/tsconfig.json +++ b/src/plugins/dashboard/tsconfig.json @@ -24,6 +24,7 @@ { "path": "../navigation/tsconfig.json" }, { "path": "../saved_objects_tagging_oss/tsconfig.json" }, { "path": "../saved_objects/tsconfig.json" }, + { "path": "../screenshot_mode/tsconfig.json" }, { "path": "../ui_actions/tsconfig.json" }, { "path": "../charts/tsconfig.json" }, { "path": "../discover/tsconfig.json" }, diff --git a/src/plugins/home/public/index.ts b/src/plugins/home/public/index.ts index 009382eee0009a..25c22606e68156 100644 --- a/src/plugins/home/public/index.ts +++ b/src/plugins/home/public/index.ts @@ -12,6 +12,7 @@ export type { FeatureCatalogueSetup, EnvironmentSetup, TutorialSetup, + WelcomeSetup, HomePublicPluginSetup, HomePublicPluginStart, } from './plugin'; diff --git a/src/plugins/home/public/plugin.ts b/src/plugins/home/public/plugin.ts index ecdcadaeeab44f..2df2407891dea0 100644 --- a/src/plugins/home/public/plugin.ts +++ b/src/plugins/home/public/plugin.ts @@ -25,6 +25,8 @@ import { TutorialServiceSetup, AddDataService, AddDataServiceSetup, + WelcomeService, + WelcomeServiceSetup, } from './services'; import { ConfigSchema } from '../config'; import { setServices } from './application/kibana_services'; @@ -34,8 +36,6 @@ import { UrlForwardingSetup, UrlForwardingStart } from '../../url_forwarding/pub import { AppNavLinkStatus } from '../../../core/public'; import { PLUGIN_ID, HOME_APP_BASE_PATH } from '../common/constants'; import { SharePluginSetup } from '../../share/public'; -import type { WelcomeServiceSetup } from './services/welcome'; -import { WelcomeService } from './services/welcome'; export interface HomePluginStartDependencies { dataViews: DataViewsPublicPluginStart; diff --git a/src/plugins/home/public/services/index.ts b/src/plugins/home/public/services/index.ts index 2ee68a9eef0c29..a585b464ef01c0 100644 --- a/src/plugins/home/public/services/index.ts +++ b/src/plugins/home/public/services/index.ts @@ -28,3 +28,6 @@ export type { export { AddDataService } from './add_data'; export type { AddDataServiceSetup, AddDataTab } from './add_data'; + +export { WelcomeService } from './welcome'; +export type { WelcomeServiceSetup } from './welcome'; From 543b7128551a439ae487c9637d5691769cab31ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Thu, 24 Feb 2022 10:30:52 +0100 Subject: [PATCH 08/21] Fix broken tests --- .../public/application/components/welcome.tsx | 2 -- src/plugins/home/public/index.ts | 1 + src/plugins/home/public/plugin.test.mocks.ts | 3 +++ src/plugins/home/public/plugin.test.ts | 13 +++++++++++++ src/plugins/home/public/services/index.ts | 2 +- src/plugins/home/public/services/welcome/index.ts | 2 +- .../public/services/welcome/welcome_service.ts | 15 ++++++++++----- .../server/plugin.test.ts | 13 ++++++------- 8 files changed, 35 insertions(+), 16 deletions(-) diff --git a/src/plugins/home/public/application/components/welcome.tsx b/src/plugins/home/public/application/components/welcome.tsx index a536c88bc8ee50..9efa6d356d9716 100644 --- a/src/plugins/home/public/application/components/welcome.tsx +++ b/src/plugins/home/public/application/components/welcome.tsx @@ -20,8 +20,6 @@ import { getServices } from '../kibana_services'; import { SampleDataCard } from './sample_data'; -export type HomeWelcomeRenderTelemetryNotice = () => null | JSX.Element; - interface Props { urlBasePath: string; onSkip: () => void; diff --git a/src/plugins/home/public/index.ts b/src/plugins/home/public/index.ts index 25c22606e68156..421d9d5f093d49 100644 --- a/src/plugins/home/public/index.ts +++ b/src/plugins/home/public/index.ts @@ -28,6 +28,7 @@ export type { TutorialVariables, TutorialDirectoryHeaderLinkComponent, TutorialModuleNoticeComponent, + WelcomeRenderTelemetryNotice, } from './services'; export { INSTRUCTION_VARIANT, getDisplayText } from '../common/instruction_variant'; diff --git a/src/plugins/home/public/plugin.test.mocks.ts b/src/plugins/home/public/plugin.test.mocks.ts index c3e3c50a2fe0f3..22d314cbd6d068 100644 --- a/src/plugins/home/public/plugin.test.mocks.ts +++ b/src/plugins/home/public/plugin.test.mocks.ts @@ -10,14 +10,17 @@ import { featureCatalogueRegistryMock } from './services/feature_catalogue/featu import { environmentServiceMock } from './services/environment/environment.mock'; import { tutorialServiceMock } from './services/tutorials/tutorial_service.mock'; import { addDataServiceMock } from './services/add_data/add_data_service.mock'; +import { welcomeServiceMock } from './services/welcome/welcome_service.mocks'; export const registryMock = featureCatalogueRegistryMock.create(); export const environmentMock = environmentServiceMock.create(); export const tutorialMock = tutorialServiceMock.create(); export const addDataMock = addDataServiceMock.create(); +export const welcomeMock = welcomeServiceMock.create(); jest.doMock('./services', () => ({ FeatureCatalogueRegistry: jest.fn(() => registryMock), EnvironmentService: jest.fn(() => environmentMock), TutorialService: jest.fn(() => tutorialMock), AddDataService: jest.fn(() => addDataMock), + WelcomeService: jest.fn(() => welcomeMock), })); diff --git a/src/plugins/home/public/plugin.test.ts b/src/plugins/home/public/plugin.test.ts index 990f0dce54a05f..57a1f5ec112aaf 100644 --- a/src/plugins/home/public/plugin.test.ts +++ b/src/plugins/home/public/plugin.test.ts @@ -79,5 +79,18 @@ describe('HomePublicPlugin', () => { expect(setup).toHaveProperty('tutorials'); expect(setup.tutorials).toHaveProperty('setVariable'); }); + + test('wires up and returns welcome service', async () => { + const setup = await new HomePublicPlugin(mockInitializerContext).setup( + coreMock.createSetup() as any, + { + share: mockShare, + urlForwarding: urlForwardingPluginMock.createSetupContract(), + } + ); + expect(setup).toHaveProperty('welcomeScreen'); + expect(setup.welcomeScreen).toHaveProperty('registerOnRendered'); + expect(setup.welcomeScreen).toHaveProperty('registerTelemetryNoticeRenderer'); + }); }); }); diff --git a/src/plugins/home/public/services/index.ts b/src/plugins/home/public/services/index.ts index a585b464ef01c0..41bc9ee258cebb 100644 --- a/src/plugins/home/public/services/index.ts +++ b/src/plugins/home/public/services/index.ts @@ -30,4 +30,4 @@ export { AddDataService } from './add_data'; export type { AddDataServiceSetup, AddDataTab } from './add_data'; export { WelcomeService } from './welcome'; -export type { WelcomeServiceSetup } from './welcome'; +export type { WelcomeServiceSetup, WelcomeRenderTelemetryNotice } from './welcome'; diff --git a/src/plugins/home/public/services/welcome/index.ts b/src/plugins/home/public/services/welcome/index.ts index 50a53f11604bd8..371c6044c5dc5c 100644 --- a/src/plugins/home/public/services/welcome/index.ts +++ b/src/plugins/home/public/services/welcome/index.ts @@ -6,5 +6,5 @@ * Side Public License, v 1. */ -export type { WelcomeServiceSetup } from './welcome_service'; +export type { WelcomeServiceSetup, WelcomeRenderTelemetryNotice } from './welcome_service'; export { WelcomeService } from './welcome_service'; diff --git a/src/plugins/home/public/services/welcome/welcome_service.ts b/src/plugins/home/public/services/welcome/welcome_service.ts index 6f1edc04d6e081..c2f285f8157f23 100644 --- a/src/plugins/home/public/services/welcome/welcome_service.ts +++ b/src/plugins/home/public/services/welcome/welcome_service.ts @@ -6,18 +6,23 @@ * Side Public License, v 1. */ -import type { HomeWelcomeRenderTelemetryNotice } from '../../application/components/welcome'; +export type WelcomeRenderTelemetryNotice = () => null | JSX.Element; export interface WelcomeServiceSetup { + /** + * Register listeners to be called when the Welcome component is mounted. + * It can be called multiple times to register multiple listeners. + */ registerOnRendered: (onRendered: () => void) => void; - registerTelemetryNoticeRenderer: ( - renderTelemetryNotice: HomeWelcomeRenderTelemetryNotice - ) => void; + /** + * Register a renderer of the telemetry notice to be shown below the Welcome page. + */ + registerTelemetryNoticeRenderer: (renderTelemetryNotice: WelcomeRenderTelemetryNotice) => void; } export class WelcomeService { private readonly onRenderedHandlers: Array<() => void> = []; - private renderTelemetryNoticeHandler?: HomeWelcomeRenderTelemetryNotice; + private renderTelemetryNoticeHandler?: WelcomeRenderTelemetryNotice; public setup(): WelcomeServiceSetup { return { diff --git a/src/plugins/telemetry_collection_manager/server/plugin.test.ts b/src/plugins/telemetry_collection_manager/server/plugin.test.ts index 9ef85d3a30b3a9..0c7b4a50b73c8d 100644 --- a/src/plugins/telemetry_collection_manager/server/plugin.test.ts +++ b/src/plugins/telemetry_collection_manager/server/plugin.test.ts @@ -223,12 +223,12 @@ describe('Telemetry Collection Manager', () => { }; describe('getStats', () => { - test('getStats returns empty because clusterDetails returns empty, and the soClient is not an instance of the TelemetrySavedObjectsClient', async () => { + test('getStats returns empty because clusterDetails returns empty, and the soClient is an instance of the TelemetrySavedObjectsClient', async () => { collectionStrategy.clusterDetailsGetter.mockResolvedValue([]); await expect(setupApi.getStats(config)).resolves.toStrictEqual([]); expect( collectionStrategy.clusterDetailsGetter.mock.calls[0][0].soClient - ).not.toBeInstanceOf(TelemetrySavedObjectsClient); + ).toBeInstanceOf(TelemetrySavedObjectsClient); }); test('returns encrypted payload (assumes opted-in when no explicitly opted-out)', async () => { collectionStrategy.clusterDetailsGetter.mockResolvedValue([ @@ -248,7 +248,7 @@ describe('Telemetry Collection Manager', () => { expect( collectionStrategy.clusterDetailsGetter.mock.calls[0][0].soClient - ).not.toBeInstanceOf(TelemetrySavedObjectsClient); + ).toBeInstanceOf(TelemetrySavedObjectsClient); }); it('calls getStats with config { refreshCache: true } even if set to false', async () => { @@ -266,7 +266,6 @@ describe('Telemetry Collection Manager', () => { expect(getStatsCollectionConfig).toReturnWith( expect.objectContaining({ refreshCache: true, - kibanaRequest: mockRequest, }) ); @@ -280,7 +279,7 @@ describe('Telemetry Collection Manager', () => { await expect(setupApi.getOptInStats(true, config)).resolves.toStrictEqual([]); expect( collectionStrategy.clusterDetailsGetter.mock.calls[0][0].soClient - ).not.toBeInstanceOf(TelemetrySavedObjectsClient); + ).toBeInstanceOf(TelemetrySavedObjectsClient); }); test('returns results for opt-in true', async () => { @@ -295,7 +294,7 @@ describe('Telemetry Collection Manager', () => { ]); expect( collectionStrategy.clusterDetailsGetter.mock.calls[0][0].soClient - ).not.toBeInstanceOf(TelemetrySavedObjectsClient); + ).toBeInstanceOf(TelemetrySavedObjectsClient); }); test('returns results for opt-in false', async () => { @@ -310,7 +309,7 @@ describe('Telemetry Collection Manager', () => { ]); expect( collectionStrategy.clusterDetailsGetter.mock.calls[0][0].soClient - ).not.toBeInstanceOf(TelemetrySavedObjectsClient); + ).toBeInstanceOf(TelemetrySavedObjectsClient); }); }); }); From 260aa2a2a076bb7ba3558d9bb3c9a7f2938924e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Thu, 24 Feb 2022 10:33:30 +0100 Subject: [PATCH 09/21] Fix translation files --- x-pack/plugins/translations/translations/ja-JP.json | 6 ------ x-pack/plugins/translations/translations/zh-CN.json | 6 ------ 2 files changed, 12 deletions(-) diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 60ce151101e5bf..bbbff26238841a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3403,12 +3403,6 @@ "home.addData.uploadFileButtonLabel": "ファイルをアップロード", "home.breadcrumbs.homeTitle": "ホーム", "home.breadcrumbs.integrationsAppTitle": "統合", - "home.dataManagementDisableCollection": " 収集を停止するには、", - "home.dataManagementDisableCollectionLink": "ここで使用状況データを無効にします。", - "home.dataManagementDisclaimerPrivacy": "使用状況データがどのように製品とサービスの管理と改善につながるのかに関する詳細については ", - "home.dataManagementDisclaimerPrivacyLink": "プライバシーポリシーをご覧ください。", - "home.dataManagementEnableCollection": " 収集を開始するには、", - "home.dataManagementEnableCollectionLink": "ここで使用状況データを有効にします。", "home.exploreButtonLabel": "独りで閲覧", "home.exploreYourDataDescription": "すべてのステップを終えたら、データ閲覧準備の完了です。", "home.header.title": "ようこそホーム", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 5150bd19a604c2..78bd6ec0949b9d 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3411,12 +3411,6 @@ "home.addData.uploadFileButtonLabel": "上传文件", "home.breadcrumbs.homeTitle": "主页", "home.breadcrumbs.integrationsAppTitle": "集成", - "home.dataManagementDisableCollection": " 要停止收集,", - "home.dataManagementDisableCollectionLink": "请在此禁用使用情况数据。", - "home.dataManagementDisclaimerPrivacy": "要了解使用情况数据如何帮助我们管理和改善产品和服务,请参阅我们的 ", - "home.dataManagementDisclaimerPrivacyLink": "隐私声明。", - "home.dataManagementEnableCollection": " 要启动收集,", - "home.dataManagementEnableCollectionLink": "请在此处启用使用情况数据。", "home.exploreButtonLabel": "自己浏览", "home.exploreYourDataDescription": "完成所有步骤后,您便可以随时浏览自己的数据。", "home.header.title": "欢迎归来", From 35dd344f33714b90951770e2aaf4d0d75fe5af64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Thu, 24 Feb 2022 10:34:08 +0100 Subject: [PATCH 10/21] Remove unused variable --- src/plugins/telemetry_collection_manager/server/plugin.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/plugins/telemetry_collection_manager/server/plugin.test.ts b/src/plugins/telemetry_collection_manager/server/plugin.test.ts index 0c7b4a50b73c8d..990e237b6b2724 100644 --- a/src/plugins/telemetry_collection_manager/server/plugin.test.ts +++ b/src/plugins/telemetry_collection_manager/server/plugin.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { coreMock, httpServerMock } from '../../../core/server/mocks'; +import { coreMock } from '../../../core/server/mocks'; import { usageCollectionPluginMock } from '../../usage_collection/server/mocks'; import { TelemetryCollectionManagerPlugin } from './plugin'; import type { BasicStatsPayload, CollectionStrategyConfig, StatsGetterConfig } from './types'; @@ -217,7 +217,6 @@ describe('Telemetry Collection Manager', () => { }); }); describe('unencrypted: true', () => { - const mockRequest = httpServerMock.createKibanaRequest(); const config: StatsGetterConfig = { unencrypted: true, }; From 2a5497b48ad37f8145a13460fea6ef449b5f2709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Thu, 24 Feb 2022 10:43:34 +0100 Subject: [PATCH 11/21] Fix `home` public API missing export --- src/plugins/home/public/index.ts | 2 +- src/plugins/home/public/plugin.ts | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/plugins/home/public/index.ts b/src/plugins/home/public/index.ts index 421d9d5f093d49..3450f4f9d2caf9 100644 --- a/src/plugins/home/public/index.ts +++ b/src/plugins/home/public/index.ts @@ -12,7 +12,6 @@ export type { FeatureCatalogueSetup, EnvironmentSetup, TutorialSetup, - WelcomeSetup, HomePublicPluginSetup, HomePublicPluginStart, } from './plugin'; @@ -29,6 +28,7 @@ export type { TutorialDirectoryHeaderLinkComponent, TutorialModuleNoticeComponent, WelcomeRenderTelemetryNotice, + WelcomeServiceSetup, } from './services'; export { INSTRUCTION_VARIANT, getDisplayText } from '../common/instruction_variant'; diff --git a/src/plugins/home/public/plugin.ts b/src/plugins/home/public/plugin.ts index 2df2407891dea0..af43e56a1d75d3 100644 --- a/src/plugins/home/public/plugin.ts +++ b/src/plugins/home/public/plugin.ts @@ -156,15 +156,12 @@ export type TutorialSetup = TutorialServiceSetup; /** @public */ export type AddDataSetup = AddDataServiceSetup; -/** @public */ -export type WelcomeSetup = WelcomeServiceSetup; - /** @public */ export interface HomePublicPluginSetup { tutorials: TutorialServiceSetup; addData: AddDataServiceSetup; featureCatalogue: FeatureCatalogueSetup; - welcomeScreen: WelcomeSetup; + welcomeScreen: WelcomeServiceSetup; /** * The environment service is only available for a transition period and will * be replaced by display specific extension points. From 6b200bf3c5ee865d685feb243a0adf986b8ed5e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Thu, 24 Feb 2022 12:26:34 +0100 Subject: [PATCH 12/21] Only validate the permissions on unencrypted requests --- .../routes/telemetry_usage_stats.test.ts | 23 ++++++++++++++++++- .../server/routes/telemetry_usage_stats.ts | 2 +- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts b/src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts index 624f779381beba..06b6679cb9bd6a 100644 --- a/src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts +++ b/src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts @@ -97,7 +97,7 @@ describe('registerTelemetryUsageStatsRoutes', () => { }); }); - it('returns 403 when the user does not have enough permissions', async () => { + it('returns 403 when the user does not have enough permissions to request unencrypted telemetry', async () => { const getSecurityMock = jest.fn().mockReturnValue({ authz: { checkPrivilegesWithRequest: () => ({ @@ -118,6 +118,27 @@ describe('registerTelemetryUsageStatsRoutes', () => { expect(mockResponse.forbidden).toBeCalled(); }); + it('returns 200 when the user does not have enough permissions to request unencrypted telemetry but it requests encrypted', async () => { + const getSecurityMock = jest.fn().mockReturnValue({ + authz: { + checkPrivilegesWithRequest: () => ({ + globally: () => ({ hasAllRequested: false }), + }), + }, + }); + registerTelemetryUsageStatsRoutes( + mockRouter, + telemetryCollectionManager, + true, + getSecurityMock + ); + const { mockResponse } = await runRequest(mockRouter, { + refreshCache: false, + unencrypted: false, + }); + expect(mockResponse.ok).toBeCalled(); + }); + it.todo('always returns an empty array on errors on encrypted payload'); it.todo('returns the actual request error object when in development mode'); it.todo('returns forbidden on unencrypted and ES returns 403 in getStats'); diff --git a/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts b/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts index 839f7e25db1433..821080051fe0ac 100644 --- a/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts +++ b/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts @@ -36,7 +36,7 @@ export function registerTelemetryUsageStatsRoutes( const { unencrypted, refreshCache } = req.body; const security = getSecurity(); - if (security) { + if (security && unencrypted) { const { hasAllRequested } = await security.authz .checkPrivilegesWithRequest(req) .globally({ kibana: 'decryptedTelemetry' }); From 5b48f6ea3f4bd9273b42465b728f5f6c770561cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Thu, 24 Feb 2022 15:56:37 +0100 Subject: [PATCH 13/21] @jportner is our saviour --- .../server/routes/telemetry_usage_stats.test.ts | 10 ++++++++++ .../telemetry/server/routes/telemetry_usage_stats.ts | 10 +++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts b/src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts index 06b6679cb9bd6a..6cbe9bec2bf53d 100644 --- a/src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts +++ b/src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts @@ -100,6 +100,11 @@ describe('registerTelemetryUsageStatsRoutes', () => { it('returns 403 when the user does not have enough permissions to request unencrypted telemetry', async () => { const getSecurityMock = jest.fn().mockReturnValue({ authz: { + actions: { + api: { + get: jest.fn(), + }, + }, checkPrivilegesWithRequest: () => ({ globally: () => ({ hasAllRequested: false }), }), @@ -121,6 +126,11 @@ describe('registerTelemetryUsageStatsRoutes', () => { it('returns 200 when the user does not have enough permissions to request unencrypted telemetry but it requests encrypted', async () => { const getSecurityMock = jest.fn().mockReturnValue({ authz: { + actions: { + api: { + get: jest.fn(), + }, + }, checkPrivilegesWithRequest: () => ({ globally: () => ({ hasAllRequested: false }), }), diff --git a/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts b/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts index 821080051fe0ac..4647f5afe0760b 100644 --- a/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts +++ b/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts @@ -37,9 +37,13 @@ export function registerTelemetryUsageStatsRoutes( const security = getSecurity(); if (security && unencrypted) { - const { hasAllRequested } = await security.authz - .checkPrivilegesWithRequest(req) - .globally({ kibana: 'decryptedTelemetry' }); + // 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 + // 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. + const { checkPrivilegesWithRequest, actions } = security.authz; + const privileges = { kibana: actions.api.get('decryptedTelemetry') }; + const { hasAllRequested } = await checkPrivilegesWithRequest(req).globally(privileges); if (!hasAllRequested) { return res.forbidden(); } From 6788d6260a45775aea63c93c7824d0f322efdddd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Fri, 4 Mar 2022 11:18:48 +0100 Subject: [PATCH 14/21] nit: test 2 different onRendered handlers --- .../public/services/welcome/welcome_service.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/plugins/home/public/services/welcome/welcome_service.test.ts b/src/plugins/home/public/services/welcome/welcome_service.test.ts index a7c96aa4b41c63..1fe67fba5617fb 100644 --- a/src/plugins/home/public/services/welcome/welcome_service.test.ts +++ b/src/plugins/home/public/services/welcome/welcome_service.test.ts @@ -26,6 +26,17 @@ describe('WelcomeService', () => { }); test('it should allow registering multiple onRendered listeners', () => { + const onRendered = jest.fn(); + const onRendered2 = jest.fn(); + welcomeServiceSetup.registerOnRendered(onRendered); + welcomeServiceSetup.registerOnRendered(onRendered2); + + welcomeService.onRendered(); + expect(onRendered).toHaveBeenCalledTimes(1); + expect(onRendered2).toHaveBeenCalledTimes(1); + }); + + test('if the same handler is registered twice, it is called twice', () => { const onRendered = jest.fn(); welcomeServiceSetup.registerOnRendered(onRendered); welcomeServiceSetup.registerOnRendered(onRendered); From 65d7835a4c34c3ea79b6e13750406cc9607535b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Fri, 4 Mar 2022 12:10:52 +0100 Subject: [PATCH 15/21] nit: Use `file.test.mocks.ts` + `jest.doMock` --- .../components/welcome.test.mocks.ts | 17 +++++++++++++++++ .../application/components/welcome.test.tsx | 13 +------------ .../public/services/welcome/welcome_service.ts | 12 ++++++------ 3 files changed, 24 insertions(+), 18 deletions(-) create mode 100644 src/plugins/home/public/application/components/welcome.test.mocks.ts diff --git a/src/plugins/home/public/application/components/welcome.test.mocks.ts b/src/plugins/home/public/application/components/welcome.test.mocks.ts new file mode 100644 index 00000000000000..fc9854bae31990 --- /dev/null +++ b/src/plugins/home/public/application/components/welcome.test.mocks.ts @@ -0,0 +1,17 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { welcomeServiceMock } from '../../services/welcome/welcome_service.mocks'; + +jest.doMock('../kibana_services', () => ({ + getServices: () => ({ + addBasePath: (path: string) => `root${path}`, + trackUiMetric: () => {}, + welcomeService: welcomeServiceMock.create(), + }), +})); diff --git a/src/plugins/home/public/application/components/welcome.test.tsx b/src/plugins/home/public/application/components/welcome.test.tsx index 623f816efaebf5..3400b4bfcdb75f 100644 --- a/src/plugins/home/public/application/components/welcome.test.tsx +++ b/src/plugins/home/public/application/components/welcome.test.tsx @@ -8,20 +8,9 @@ import React from 'react'; import { shallow } from 'enzyme'; +import './welcome.test.mocks'; import { Welcome } from './welcome'; -jest.mock('../kibana_services', () => ({ - getServices: () => ({ - addBasePath: (path: string) => `root${path}`, - trackUiMetric: () => {}, - // can't use welcomeServiceMock because jest.mock does not allow importing out-of-scope variables - welcomeService: { - onRendered: jest.fn(), - renderTelemetryNotice: jest.fn(), - }, - }), -})); - test('should render a Welcome screen', () => { const component = shallow( {}} />); diff --git a/src/plugins/home/public/services/welcome/welcome_service.ts b/src/plugins/home/public/services/welcome/welcome_service.ts index c2f285f8157f23..a05da56a8c81e4 100644 --- a/src/plugins/home/public/services/welcome/welcome_service.ts +++ b/src/plugins/home/public/services/welcome/welcome_service.ts @@ -24,7 +24,7 @@ export class WelcomeService { private readonly onRenderedHandlers: Array<() => void> = []; private renderTelemetryNoticeHandler?: WelcomeRenderTelemetryNotice; - public setup(): WelcomeServiceSetup { + public setup = (): WelcomeServiceSetup => { return { registerOnRendered: (onRendered) => { this.onRenderedHandlers.push(onRendered); @@ -33,17 +33,17 @@ export class WelcomeService { this.renderTelemetryNoticeHandler = renderTelemetryNotice; }, }; - } + }; - public onRendered() { + public onRendered = () => { this.onRenderedHandlers.forEach((onRendered) => onRendered()); - } + }; - public renderTelemetryNotice() { + public renderTelemetryNotice = () => { if (this.renderTelemetryNoticeHandler) { return this.renderTelemetryNoticeHandler(); } else { return null; } - } + }; } From 6f7e64850f745da3fe0481c0339fcb09195aff96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Fri, 4 Mar 2022 12:15:21 +0100 Subject: [PATCH 16/21] Only allow one telemetry notice renderer --- .../home/public/services/welcome/welcome_service.test.ts | 8 +++++--- .../home/public/services/welcome/welcome_service.ts | 3 +++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/plugins/home/public/services/welcome/welcome_service.test.ts b/src/plugins/home/public/services/welcome/welcome_service.test.ts index 1fe67fba5617fb..2b1f74b18b5343 100644 --- a/src/plugins/home/public/services/welcome/welcome_service.test.ts +++ b/src/plugins/home/public/services/welcome/welcome_service.test.ts @@ -53,13 +53,15 @@ describe('WelcomeService', () => { expect(welcomeService.renderTelemetryNotice()).toEqual('rendered text'); }); - test('it should use the last registered renderer', () => { + test('it should fail to register a 2nd renderer and still use the first registered renderer', () => { const renderer = jest.fn().mockReturnValue('rendered text'); const renderer2 = jest.fn().mockReturnValue('other text'); welcomeServiceSetup.registerTelemetryNoticeRenderer(renderer); - welcomeServiceSetup.registerTelemetryNoticeRenderer(renderer2); + expect(() => welcomeServiceSetup.registerTelemetryNoticeRenderer(renderer2)).toThrowError( + 'Only one renderTelemetryNotice handler can be registered' + ); - expect(welcomeService.renderTelemetryNotice()).toEqual('other text'); + expect(welcomeService.renderTelemetryNotice()).toEqual('rendered text'); }); }); }); diff --git a/src/plugins/home/public/services/welcome/welcome_service.ts b/src/plugins/home/public/services/welcome/welcome_service.ts index a05da56a8c81e4..d07e05f4b0af29 100644 --- a/src/plugins/home/public/services/welcome/welcome_service.ts +++ b/src/plugins/home/public/services/welcome/welcome_service.ts @@ -30,6 +30,9 @@ export class WelcomeService { this.onRenderedHandlers.push(onRendered); }, registerTelemetryNoticeRenderer: (renderTelemetryNotice) => { + if (this.renderTelemetryNoticeHandler) { + throw new Error('Only one renderTelemetryNotice handler can be registered'); + } this.renderTelemetryNoticeHandler = renderTelemetryNotice; }, }; From 40578d1e156e3e2558f6665c63e954306dc95ecc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Fri, 4 Mar 2022 12:20:00 +0100 Subject: [PATCH 17/21] Make `home` dependency optional --- src/plugins/telemetry/kibana.json | 4 ++-- src/plugins/telemetry/public/plugin.ts | 20 +++++++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/plugins/telemetry/kibana.json b/src/plugins/telemetry/kibana.json index 10c680a5c61cff..a6796e42f92282 100644 --- a/src/plugins/telemetry/kibana.json +++ b/src/plugins/telemetry/kibana.json @@ -7,8 +7,8 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["home", "telemetryCollectionManager", "usageCollection", "screenshotMode"], - "optionalPlugins": ["security"], + "requiredPlugins": ["telemetryCollectionManager", "usageCollection", "screenshotMode"], + "optionalPlugins": ["home", "security"], "extraPublicDirs": ["common/constants"], "requiredBundles": ["kibanaUtils", "kibanaReact"] } diff --git a/src/plugins/telemetry/public/plugin.ts b/src/plugins/telemetry/public/plugin.ts index ab28d77505368f..794183cb8a8f5d 100644 --- a/src/plugins/telemetry/public/plugin.ts +++ b/src/plugins/telemetry/public/plugin.ts @@ -84,7 +84,7 @@ export interface TelemetryPluginStart { interface TelemetryPluginSetupDependencies { screenshotMode: ScreenshotModePluginSetup; - home: HomePublicPluginSetup; + home?: HomePublicPluginSetup; } /** @@ -138,15 +138,17 @@ export class TelemetryPlugin implements Plugin { - if (this.telemetryService?.userCanChangeSettings) { - this.telemetryNotifications?.setOptedInNoticeSeen(); - } - }); + if (home) { + home.welcomeScreen.registerOnRendered(() => { + if (this.telemetryService?.userCanChangeSettings) { + this.telemetryNotifications?.setOptedInNoticeSeen(); + } + }); - home.welcomeScreen.registerTelemetryNoticeRenderer(() => - renderWelcomeTelemetryNotice(this.telemetryService!, http.basePath.prepend) - ); + home.welcomeScreen.registerTelemetryNoticeRenderer(() => + renderWelcomeTelemetryNotice(this.telemetryService!, http.basePath.prepend) + ); + } return { telemetryService: this.getTelemetryServicePublicApis(), From 8639532e4170269e3114820d3ee7874816074702 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Fri, 4 Mar 2022 12:29:09 +0100 Subject: [PATCH 18/21] Use security mock instead of creating our own --- .../routes/telemetry_usage_stats.test.ts | 56 +++++++++++-------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts b/src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts index 6cbe9bec2bf53d..bc7569585c127b 100644 --- a/src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts +++ b/src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts @@ -9,6 +9,7 @@ import { registerTelemetryUsageStatsRoutes } from './telemetry_usage_stats'; import { coreMock, httpServerMock } from 'src/core/server/mocks'; import type { RequestHandlerContext, IRouter } from 'src/core/server'; +import { securityMock } from '../../../../../x-pack/plugins/security/server/mocks'; import { telemetryCollectionManagerPluginMock } from '../../../telemetry_collection_manager/server/mocks'; async function runRequest( @@ -98,17 +99,12 @@ describe('registerTelemetryUsageStatsRoutes', () => { }); it('returns 403 when the user does not have enough permissions to request unencrypted telemetry', async () => { - const getSecurityMock = jest.fn().mockReturnValue({ - authz: { - actions: { - api: { - get: jest.fn(), - }, - }, - checkPrivilegesWithRequest: () => ({ - globally: () => ({ hasAllRequested: false }), - }), - }, + const getSecurityMock = jest.fn().mockImplementation(() => { + const securityStartMock = securityMock.createStart(); + securityStartMock.authz.checkPrivilegesWithRequest.mockReturnValue({ + globally: () => ({ hasAllRequested: false }), + }); + return securityStartMock; }); registerTelemetryUsageStatsRoutes( mockRouter, @@ -123,18 +119,34 @@ describe('registerTelemetryUsageStatsRoutes', () => { expect(mockResponse.forbidden).toBeCalled(); }); + it('returns 200 when the user has enough permissions to request unencrypted telemetry', async () => { + const getSecurityMock = jest.fn().mockImplementation(() => { + const securityStartMock = securityMock.createStart(); + securityStartMock.authz.checkPrivilegesWithRequest.mockReturnValue({ + globally: () => ({ hasAllRequested: true }), + }); + return securityStartMock; + }); + registerTelemetryUsageStatsRoutes( + mockRouter, + telemetryCollectionManager, + true, + getSecurityMock + ); + const { mockResponse } = await runRequest(mockRouter, { + refreshCache: false, + unencrypted: true, + }); + expect(mockResponse.ok).toBeCalled(); + }); + it('returns 200 when the user does not have enough permissions to request unencrypted telemetry but it requests encrypted', async () => { - const getSecurityMock = jest.fn().mockReturnValue({ - authz: { - actions: { - api: { - get: jest.fn(), - }, - }, - checkPrivilegesWithRequest: () => ({ - globally: () => ({ hasAllRequested: false }), - }), - }, + const getSecurityMock = jest.fn().mockImplementation(() => { + const securityStartMock = securityMock.createStart(); + securityStartMock.authz.checkPrivilegesWithRequest.mockReturnValue({ + globally: () => ({ hasAllRequested: false }), + }); + return securityStartMock; }); registerTelemetryUsageStatsRoutes( mockRouter, From b50da6bb14b3d5b7b1d6e6a9eee986f28a8b00f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Fri, 4 Mar 2022 14:28:07 +0100 Subject: [PATCH 19/21] Add functional tests --- .../apis/telemetry/telemetry.ts | 131 ++++++++++++++++++ ...t_auth.js => es_supertest_without_auth.ts} | 3 +- x-pack/test/api_integration/services/index.ts | 2 - ...hout_auth.js => supertest_without_auth.ts} | 3 +- 4 files changed, 135 insertions(+), 4 deletions(-) rename x-pack/test/api_integration/services/{es_supertest_without_auth.js => es_supertest_without_auth.ts} (81%) rename x-pack/test/api_integration/services/{supertest_without_auth.js => supertest_without_auth.ts} (82%) diff --git a/x-pack/test/api_integration/apis/telemetry/telemetry.ts b/x-pack/test/api_integration/apis/telemetry/telemetry.ts index 088678a74813b6..4b0137ab5f8427 100644 --- a/x-pack/test/api_integration/apis/telemetry/telemetry.ts +++ b/x-pack/test/api_integration/apis/telemetry/telemetry.ts @@ -10,6 +10,7 @@ import moment from 'moment'; import type SuperTest from 'supertest'; import deepmerge from 'deepmerge'; import type { FtrProviderContext } from '../../ftr_provider_context'; +import type { SecurityService } from '../../../../../test/common/services/security/security'; import multiClusterFixture from './fixtures/multicluster.json'; import basicClusterFixture from './fixtures/basiccluster.json'; @@ -90,10 +91,31 @@ function updateMonitoringDates( ]); } +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('/api/telemetry/v2/clusters/_stats', () => { const timestamp = new Date().toISOString(); @@ -236,5 +258,114 @@ export default function ({ getService }: FtrProviderContext) { 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('/api/telemetry/v2/clusters/_stats') + .set('kbn-xsrf', 'xxx') + .send({ unencrypted: true }) + .expect(200); + }); + + it('should return encrypted telemetry for the admin user', async () => { + await supertest + .post('/api/telemetry/v2/clusters/_stats') + .set('kbn-xsrf', 'xxx') + .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('/api/telemetry/v2/clusters/_stats') + .auth(globalReadOnlyUser, password(globalReadOnlyUser)) + .set('kbn-xsrf', 'xxx') + .send({ unencrypted: false }) + .expect(200); + }); + + it('should return unencrypted telemetry for the global-read user', async () => { + await supertestWithoutAuth + .post('/api/telemetry/v2/clusters/_stats') + .auth(globalReadOnlyUser, password(globalReadOnlyUser)) + .set('kbn-xsrf', 'xxx') + .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('/api/telemetry/v2/clusters/_stats') + .auth(noGlobalUser, password(noGlobalUser)) + .set('kbn-xsrf', 'xxx') + .send({ unencrypted: false }) + .expect(200); + }); + + it('should return 403 when the read-only user requests unencrypted telemetry', async () => { + await supertestWithoutAuth + .post('/api/telemetry/v2/clusters/_stats') + .auth(noGlobalUser, password(noGlobalUser)) + .set('kbn-xsrf', 'xxx') + .send({ unencrypted: true }) + .expect(403); + }); + }); + }); }); } diff --git a/x-pack/test/api_integration/services/es_supertest_without_auth.js b/x-pack/test/api_integration/services/es_supertest_without_auth.ts similarity index 81% rename from x-pack/test/api_integration/services/es_supertest_without_auth.js rename to x-pack/test/api_integration/services/es_supertest_without_auth.ts index 71ec058be46ab7..034dbd2bb766d5 100644 --- a/x-pack/test/api_integration/services/es_supertest_without_auth.js +++ b/x-pack/test/api_integration/services/es_supertest_without_auth.ts @@ -8,12 +8,13 @@ import { format as formatUrl } from 'url'; import supertest from 'supertest'; +import type { FtrProviderContext } from '../ftr_provider_context'; /** * Supertest provider that doesn't include user credentials into base URL that is passed * to the supertest. */ -export function EsSupertestWithoutAuthProvider({ getService }) { +export function EsSupertestWithoutAuthProvider({ getService }: FtrProviderContext) { const config = getService('config'); const elasticsearchServerConfig = config.get('servers.elasticsearch'); diff --git a/x-pack/test/api_integration/services/index.ts b/x-pack/test/api_integration/services/index.ts index cf439eb7cd5a80..88e360b2722f8c 100644 --- a/x-pack/test/api_integration/services/index.ts +++ b/x-pack/test/api_integration/services/index.ts @@ -8,9 +8,7 @@ import { services as kibanaApiIntegrationServices } from '../../../../test/api_integration/services'; import { services as commonServices } from '../../common/services'; -// @ts-ignore not ts yet import { EsSupertestWithoutAuthProvider } from './es_supertest_without_auth'; -// @ts-ignore not ts yet import { SupertestWithoutAuthProvider } from './supertest_without_auth'; import { UsageAPIProvider } from './usage_api'; diff --git a/x-pack/test/api_integration/services/supertest_without_auth.js b/x-pack/test/api_integration/services/supertest_without_auth.ts similarity index 82% rename from x-pack/test/api_integration/services/supertest_without_auth.js rename to x-pack/test/api_integration/services/supertest_without_auth.ts index ea4a5bdf08a940..2d1ed38827d4d7 100644 --- a/x-pack/test/api_integration/services/supertest_without_auth.js +++ b/x-pack/test/api_integration/services/supertest_without_auth.ts @@ -8,12 +8,13 @@ import { format as formatUrl } from 'url'; import supertest from 'supertest'; +import type { FtrProviderContext } from '../ftr_provider_context'; /** * supertest provider that doesn't include user credentials into base URL that is passed * to the supertest. It's used to test API behaviour for not yet authenticated user. */ -export function SupertestWithoutAuthProvider({ getService }) { +export function SupertestWithoutAuthProvider({ getService }: FtrProviderContext) { const config = getService('config'); const kibanaServerConfig = config.get('servers.kibana'); From 8802cd8d0e8bcaf672ecb226ea37606a27545fcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Fri, 4 Mar 2022 14:49:50 +0100 Subject: [PATCH 20/21] try/catch externally registered handlers --- .../services/welcome/welcome_service.test.ts | 19 +++++++++++++++++++ .../services/welcome/welcome_service.ts | 19 +++++++++++++++---- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/plugins/home/public/services/welcome/welcome_service.test.ts b/src/plugins/home/public/services/welcome/welcome_service.test.ts index 2b1f74b18b5343..df2f95718c78b5 100644 --- a/src/plugins/home/public/services/welcome/welcome_service.test.ts +++ b/src/plugins/home/public/services/welcome/welcome_service.test.ts @@ -25,6 +25,16 @@ describe('WelcomeService', () => { expect(onRendered).toHaveBeenCalledTimes(1); }); + test('it should handle onRendered errors', () => { + const onRendered = jest.fn().mockImplementation(() => { + throw new Error('Something went terribly wrong'); + }); + welcomeServiceSetup.registerOnRendered(onRendered); + + expect(() => welcomeService.onRendered()).not.toThrow(); + expect(onRendered).toHaveBeenCalledTimes(1); + }); + test('it should allow registering multiple onRendered listeners', () => { const onRendered = jest.fn(); const onRendered2 = jest.fn(); @@ -63,5 +73,14 @@ describe('WelcomeService', () => { expect(welcomeService.renderTelemetryNotice()).toEqual('rendered text'); }); + + test('it should handle errors in the renderer', () => { + const renderer = jest.fn().mockImplementation(() => { + throw new Error('Something went terribly wrong'); + }); + welcomeServiceSetup.registerTelemetryNoticeRenderer(renderer); + + expect(welcomeService.renderTelemetryNotice()).toEqual(null); + }); }); }); diff --git a/src/plugins/home/public/services/welcome/welcome_service.ts b/src/plugins/home/public/services/welcome/welcome_service.ts index d07e05f4b0af29..46cf139adb36a3 100644 --- a/src/plugins/home/public/services/welcome/welcome_service.ts +++ b/src/plugins/home/public/services/welcome/welcome_service.ts @@ -39,14 +39,25 @@ export class WelcomeService { }; public onRendered = () => { - this.onRenderedHandlers.forEach((onRendered) => onRendered()); + this.onRenderedHandlers.forEach((onRendered) => { + try { + onRendered(); + } catch (err) { + // eslint-disable-next-line no-console + console.error(err); + } + }); }; public renderTelemetryNotice = () => { if (this.renderTelemetryNoticeHandler) { - return this.renderTelemetryNoticeHandler(); - } else { - return null; + try { + return this.renderTelemetryNoticeHandler(); + } catch (err) { + // eslint-disable-next-line no-console + console.error(err); + } } + return null; }; } From 51b02f07ed210527dfddcee259723c4aeae1a1a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Fri, 4 Mar 2022 20:00:53 +0100 Subject: [PATCH 21/21] Fix type check --- src/plugins/visualizations/tsconfig.json | 1 + ..._supertest_without_auth.ts => es_supertest_without_auth.js} | 3 +-- x-pack/test/api_integration/services/index.ts | 2 ++ .../{supertest_without_auth.ts => supertest_without_auth.js} | 3 +-- 4 files changed, 5 insertions(+), 4 deletions(-) rename x-pack/test/api_integration/services/{es_supertest_without_auth.ts => es_supertest_without_auth.js} (81%) rename x-pack/test/api_integration/services/{supertest_without_auth.ts => supertest_without_auth.js} (82%) diff --git a/src/plugins/visualizations/tsconfig.json b/src/plugins/visualizations/tsconfig.json index 57d21d8719ede3..2bc25cfb3c3463 100644 --- a/src/plugins/visualizations/tsconfig.json +++ b/src/plugins/visualizations/tsconfig.json @@ -30,6 +30,7 @@ { "path": "../home/tsconfig.json" }, { "path": "../share/tsconfig.json" }, { "path": "../presentation_util/tsconfig.json" }, + { "path": "../screenshot_mode/tsconfig.json" }, { "path": "../../../x-pack/plugins/spaces/tsconfig.json" } ] } diff --git a/x-pack/test/api_integration/services/es_supertest_without_auth.ts b/x-pack/test/api_integration/services/es_supertest_without_auth.js similarity index 81% rename from x-pack/test/api_integration/services/es_supertest_without_auth.ts rename to x-pack/test/api_integration/services/es_supertest_without_auth.js index 034dbd2bb766d5..71ec058be46ab7 100644 --- a/x-pack/test/api_integration/services/es_supertest_without_auth.ts +++ b/x-pack/test/api_integration/services/es_supertest_without_auth.js @@ -8,13 +8,12 @@ import { format as formatUrl } from 'url'; import supertest from 'supertest'; -import type { FtrProviderContext } from '../ftr_provider_context'; /** * Supertest provider that doesn't include user credentials into base URL that is passed * to the supertest. */ -export function EsSupertestWithoutAuthProvider({ getService }: FtrProviderContext) { +export function EsSupertestWithoutAuthProvider({ getService }) { const config = getService('config'); const elasticsearchServerConfig = config.get('servers.elasticsearch'); diff --git a/x-pack/test/api_integration/services/index.ts b/x-pack/test/api_integration/services/index.ts index 88e360b2722f8c..cf439eb7cd5a80 100644 --- a/x-pack/test/api_integration/services/index.ts +++ b/x-pack/test/api_integration/services/index.ts @@ -8,7 +8,9 @@ import { services as kibanaApiIntegrationServices } from '../../../../test/api_integration/services'; import { services as commonServices } from '../../common/services'; +// @ts-ignore not ts yet import { EsSupertestWithoutAuthProvider } from './es_supertest_without_auth'; +// @ts-ignore not ts yet import { SupertestWithoutAuthProvider } from './supertest_without_auth'; import { UsageAPIProvider } from './usage_api'; diff --git a/x-pack/test/api_integration/services/supertest_without_auth.ts b/x-pack/test/api_integration/services/supertest_without_auth.js similarity index 82% rename from x-pack/test/api_integration/services/supertest_without_auth.ts rename to x-pack/test/api_integration/services/supertest_without_auth.js index 2d1ed38827d4d7..ea4a5bdf08a940 100644 --- a/x-pack/test/api_integration/services/supertest_without_auth.ts +++ b/x-pack/test/api_integration/services/supertest_without_auth.js @@ -8,13 +8,12 @@ import { format as formatUrl } from 'url'; import supertest from 'supertest'; -import type { FtrProviderContext } from '../ftr_provider_context'; /** * supertest provider that doesn't include user credentials into base URL that is passed * to the supertest. It's used to test API behaviour for not yet authenticated user. */ -export function SupertestWithoutAuthProvider({ getService }: FtrProviderContext) { +export function SupertestWithoutAuthProvider({ getService }) { const config = getService('config'); const kibanaServerConfig = config.get('servers.kibana');