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 222a89bf3229845..991f8336e702026 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/fixtures/telemetry_collectors/stats_collector.ts b/src/fixtures/telemetry_collectors/stats_collector.ts index c8f513a07253bac..6046973f42e8494 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/src/plugins/dashboard/tsconfig.json b/src/plugins/dashboard/tsconfig.json index 55049447aee5766..862bed9d667a011 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/kibana.json b/src/plugins/home/kibana.json index d8c09ab5e80c6a4..02b33e814e2a1c9 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/home/public/application/components/__snapshots__/home.test.tsx.snap b/src/plugins/home/public/application/components/__snapshots__/home.test.tsx.snap index ab6ad1b6cc0c55c..43d8f935221b36f 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 17f7d2520e8621c..861e0ee895887c9 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 9983afa3d4d6113..f27a286488c2b14 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 fdf04ea5806538f..1fb0b3c790ab7e9 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 62df479ecbfdf6c..a634573aaf21ec3 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.mocks.ts b/src/plugins/home/public/application/components/welcome.test.mocks.ts new file mode 100644 index 000000000000000..fc9854bae31990f --- /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 b042a91e58c9d2a..3400b4bfcdb75f5 100644 --- a/src/plugins/home/public/application/components/welcome.test.tsx +++ b/src/plugins/home/public/application/components/welcome.test.tsx @@ -8,58 +8,11 @@ import React from 'react'; import { shallow } from 'enzyme'; +import './welcome.test.mocks'; import { Welcome } from './welcome'; -import { telemetryPluginMock } from '../../../../telemetry/public/mocks'; -jest.mock('../kibana_services', () => ({ - getServices: () => ({ - addBasePath: (path: string) => `root${path}`, - trackUiMetric: () => {}, - }), -})); - -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 1a6251ebdca1184..9efa6d356d9716c 100644 --- a/src/plugins/home/public/application/components/welcome.tsx +++ b/src/plugins/home/public/application/components/welcome.tsx @@ -12,27 +12,17 @@ * 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'; + interface Props { urlBasePath: string; onSkip: () => void; - telemetry?: TelemetryPluginStart; } /** @@ -47,7 +37,7 @@ export class Welcome extends React.Component { } }; - private redirecToAddData() { + private redirectToAddData() { this.services.application.navigateToApp('integrations', { path: '/browse' }); } @@ -58,68 +48,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 +91,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 fdd325df96ac575..3ccfd9413a88ad5 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/index.ts b/src/plugins/home/public/index.ts index 009382eee0009a2..3450f4f9d2caf9a 100644 --- a/src/plugins/home/public/index.ts +++ b/src/plugins/home/public/index.ts @@ -27,6 +27,8 @@ export type { TutorialVariables, TutorialDirectoryHeaderLinkComponent, TutorialModuleNoticeComponent, + WelcomeRenderTelemetryNotice, + WelcomeServiceSetup, } from './services'; export { INSTRUCTION_VARIANT, getDisplayText } from '../common/instruction_variant'; diff --git a/src/plugins/home/public/mocks.ts b/src/plugins/home/public/mocks.ts index 10c186ee3f4e30f..42e489dea9d2a33 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/home/public/plugin.test.mocks.ts b/src/plugins/home/public/plugin.test.mocks.ts index c3e3c50a2fe0f3b..22d314cbd6d0683 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 990f0dce54a05f6..57a1f5ec112aaf6 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/plugin.ts b/src/plugins/home/public/plugin.ts index 1ece73e71f393f3..af43e56a1d75d30 100644 --- a/src/plugins/home/public/plugin.ts +++ b/src/plugins/home/public/plugin.ts @@ -25,11 +25,12 @@ import { TutorialServiceSetup, AddDataService, AddDataServiceSetup, + WelcomeService, + WelcomeServiceSetup, } from './services'; 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'; @@ -38,7 +39,6 @@ import { SharePluginSetup } from '../../share/public'; 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() }, }; } @@ -159,12 +161,12 @@ export interface HomePublicPluginSetup { tutorials: TutorialServiceSetup; addData: AddDataServiceSetup; featureCatalogue: FeatureCatalogueSetup; + welcomeScreen: WelcomeServiceSetup; /** * 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/index.ts b/src/plugins/home/public/services/index.ts index 2ee68a9eef0c29d..41bc9ee258cebbe 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, WelcomeRenderTelemetryNotice } from './welcome'; 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 000000000000000..371c6044c5dc5c2 --- /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, WelcomeRenderTelemetryNotice } 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 000000000000000..921cb990663276e --- /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 000000000000000..df2f95718c78b57 --- /dev/null +++ b/src/plugins/home/public/services/welcome/welcome_service.test.ts @@ -0,0 +1,86 @@ +/* + * 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 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(); + 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); + + 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 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); + expect(() => welcomeServiceSetup.registerTelemetryNoticeRenderer(renderer2)).toThrowError( + 'Only one renderTelemetryNotice handler can be registered' + ); + + 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 new file mode 100644 index 000000000000000..46cf139adb36a39 --- /dev/null +++ b/src/plugins/home/public/services/welcome/welcome_service.ts @@ -0,0 +1,63 @@ +/* + * 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 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; + /** + * 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?: WelcomeRenderTelemetryNotice; + + public setup = (): WelcomeServiceSetup => { + return { + registerOnRendered: (onRendered) => { + this.onRenderedHandlers.push(onRendered); + }, + registerTelemetryNoticeRenderer: (renderTelemetryNotice) => { + if (this.renderTelemetryNoticeHandler) { + throw new Error('Only one renderTelemetryNotice handler can be registered'); + } + this.renderTelemetryNoticeHandler = renderTelemetryNotice; + }, + }; + }; + + public onRendered = () => { + this.onRenderedHandlers.forEach((onRendered) => { + try { + onRendered(); + } catch (err) { + // eslint-disable-next-line no-console + console.error(err); + } + }); + }; + + public renderTelemetryNotice = () => { + if (this.renderTelemetryNoticeHandler) { + try { + return this.renderTelemetryNoticeHandler(); + } catch (err) { + // eslint-disable-next-line no-console + console.error(err); + } + } + return null; + }; +} diff --git a/src/plugins/home/tsconfig.json b/src/plugins/home/tsconfig.json index fa98b98ff8e1c30..17d0fc7bd91acf6 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 09cc6accb68f4b3..a6796e42f92282c 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": ["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 3072ff67703d787..794183cb8a8f5db 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 000000000000000..6da76db915656d7 --- /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 000000000000000..8ef26fb797d5322 --- /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/server/plugin.ts b/src/plugins/telemetry/server/plugin.ts index 73c61ea1c503869..681a871ba105b6f 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_opt_in_stats.ts b/src/plugins/telemetry/server/routes/telemetry_opt_in_stats.ts index 2a956656621944a..6139eee3e10ca68 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 736367446d3c05c..bc7569585c127bb 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,8 @@ 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 { securityMock } from '../../../../../x-pack/plugins/security/server/mocks'; import { telemetryCollectionManagerPluginMock } from '../../../telemetry_collection_manager/server/mocks'; async function runRequest( @@ -35,13 +36,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 +56,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 +68,99 @@ 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 to request unencrypted telemetry', async () => { + const getSecurityMock = jest.fn().mockImplementation(() => { + const securityStartMock = securityMock.createStart(); + securityStartMock.authz.checkPrivilegesWithRequest.mockReturnValue({ + globally: () => ({ hasAllRequested: false }), + }); + return securityStartMock; + }); + registerTelemetryUsageStatsRoutes( + mockRouter, + telemetryCollectionManager, + true, + getSecurityMock + ); + const { mockResponse } = await runRequest(mockRouter, { + refreshCache: false, + unencrypted: true, + }); + 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().mockImplementation(() => { + const securityStartMock = securityMock.createStart(); + securityStartMock.authz.checkPrivilegesWithRequest.mockReturnValue({ + globally: () => ({ hasAllRequested: false }), + }); + return securityStartMock; + }); + 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 2f72ae818f11211..4647f5afe0760bd 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,22 @@ export function registerTelemetryUsageStatsRoutes( async (context, req, res) => { const { unencrypted, refreshCache } = req.body; + const security = getSecurity(); + if (security && unencrypted) { + // 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(); + } + } + try { const statsConfig: StatsGetterConfig = { - request: req, unencrypted, refreshCache: unencrypted || refreshCache, }; diff --git a/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts b/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts index 83f33a894b9032f..4340eaafd2d8ff4 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.test.ts b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts index 2392ac570ecbc19..fa45438e00fbe3c 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/server/telemetry_collection/get_local_stats.ts b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts index ae2a849ccfa19ad..73de59ae8156aaa 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/telemetry/tsconfig.json b/src/plugins/telemetry/tsconfig.json index d50ccd563fe5ac5..052d484447e428c 100644 --- a/src/plugins/telemetry/tsconfig.json +++ b/src/plugins/telemetry/tsconfig.json @@ -17,10 +17,12 @@ ], "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" }, { "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.test.ts b/src/plugins/telemetry_collection_manager/server/plugin.test.ts index ca932e92d98bdb3..990e237b6b27244 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,19 +217,17 @@ describe('Telemetry Collection Manager', () => { }); }); describe('unencrypted: true', () => { - const mockRequest = httpServerMock.createKibanaRequest(); const config: StatsGetterConfig = { unencrypted: true, - request: mockRequest, }; 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([ @@ -249,7 +247,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 () => { @@ -267,7 +265,6 @@ describe('Telemetry Collection Manager', () => { expect(getStatsCollectionConfig).toReturnWith( expect.objectContaining({ refreshCache: true, - kibanaRequest: mockRequest, }) ); @@ -281,7 +278,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 () => { @@ -296,7 +293,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 () => { @@ -311,7 +308,7 @@ describe('Telemetry Collection Manager', () => { ]); expect( collectionStrategy.clusterDetailsGetter.mock.calls[0][0].soClient - ).not.toBeInstanceOf(TelemetrySavedObjectsClient); + ).toBeInstanceOf(TelemetrySavedObjectsClient); }); }); }); diff --git a/src/plugins/telemetry_collection_manager/server/plugin.ts b/src/plugins/telemetry_collection_manager/server/plugin.ts index fad51ca1dbfde82..cffe736f8eeaf52 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 7ea32844a858cb5..9658c0d68d05dba 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; } diff --git a/src/plugins/usage_collection/README.mdx b/src/plugins/usage_collection/README.mdx index a58f197818bf4e4..03d8f7badb8c2a8 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 74373d44a359b6b..1ff04cf3650c0bf 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 5e0698b286f79be..87e841f3de4c54d 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 49332b0a1826fce..3a7c0a66ac60d18 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 ca240a520ee24a5..e284844b34c344d 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 bf1e9f4644b1b73..8d427d211a191b5 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 15f7cd9c627fcbc..2ed8c2a50dbafd7 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/index.ts b/src/plugins/usage_collection/server/index.ts index 74fa77be9843cb4..907a61a752052ae 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 { diff --git a/src/plugins/usage_collection/server/mocks.ts b/src/plugins/usage_collection/server/mocks.ts index 6f7d4f19cbaf120..ac7ad69ed4bce78 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 f415dd768dc226d..7cde8bad706dd1e 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 8e5382d1631721a..72cbd2e5899ff50 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/src/plugins/visualizations/tsconfig.json b/src/plugins/visualizations/tsconfig.json index 57d21d8719ede35..2bc25cfb3c34633 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/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.ts index 7096647854c1570..76cc9adeb43ecd6 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/kibana_monitoring/collectors/get_usage_collector.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts index cbbfe64f5e3e223..0c952949c56b49e 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/monitoring/server/telemetry_collection/register_monitoring_telemetry_collection.ts b/x-pack/plugins/monitoring/server/telemetry_collection/register_monitoring_telemetry_collection.ts index bce6f57d6f950ac..344b04fb4780d4c 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/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 9b66792efcd9e31..b7a52a7a41bcf66 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); 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 7febebc2a51795b..e1bea8d1aa0e187 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 diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index c35b5dbe6667825..3df1c0a71b09572 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3394,12 +3394,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 f706762740ad8e6..0326413eb0ac870 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3402,12 +3402,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/test/api_integration/apis/telemetry/telemetry.ts b/x-pack/test/api_integration/apis/telemetry/telemetry.ts index 088678a74813b69..4b0137ab5f84274 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); + }); + }); + }); }); }