From 34307c8d1306d4935666ac97d0c9dd51f6d80bbb Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Wed, 24 Jun 2020 12:36:24 -0400 Subject: [PATCH 1/7] [IM] Move common step containers to shared (#69713) --- .../components/shared/components/index.ts | 7 +++++- .../shared/components/wizard_steps/index.ts | 8 ++++--- .../wizard_steps/step_aliases_container.tsx | 22 ++++++++++++++++++ .../wizard_steps}/step_mappings_container.tsx | 17 ++++++++------ .../wizard_steps/step_settings_container.tsx | 22 ++++++++++++++++++ .../shared/components/wizard_steps/types.ts | 13 +++++++++++ .../application/components/shared/index.ts | 7 +++--- .../components/template_form/steps/index.ts | 3 --- .../steps/step_aliases_container.tsx | 23 ------------------- .../steps/step_settings_container.tsx | 23 ------------------- .../template_form/template_form.tsx | 18 +++++++-------- 11 files changed, 90 insertions(+), 73 deletions(-) create mode 100644 x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_aliases_container.tsx rename x-pack/plugins/index_management/public/application/components/{template_form/steps => shared/components/wizard_steps}/step_mappings_container.tsx (57%) create mode 100644 x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings_container.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/types.ts delete mode 100644 x-pack/plugins/index_management/public/application/components/template_form/steps/step_aliases_container.tsx delete mode 100644 x-pack/plugins/index_management/public/application/components/template_form/steps/step_settings_container.tsx diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/index.ts b/x-pack/plugins/index_management/public/application/components/shared/components/index.ts index e1700ad6a632d2..b67a9c355e723d 100644 --- a/x-pack/plugins/index_management/public/application/components/shared/components/index.ts +++ b/x-pack/plugins/index_management/public/application/components/shared/components/index.ts @@ -6,4 +6,9 @@ export { TabAliases, TabMappings, TabSettings } from './details_panel'; -export { StepAliases, StepMappings, StepSettings } from './wizard_steps'; +export { + StepAliasesContainer, + StepMappingsContainer, + StepSettingsContainer, + CommonWizardSteps, +} from './wizard_steps'; diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/index.ts b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/index.ts index 90ce6227c09c88..ea554ca269d8bd 100644 --- a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/index.ts +++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/index.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export { StepAliases } from './step_aliases'; -export { StepMappings } from './step_mappings'; -export { StepSettings } from './step_settings'; +export { StepAliasesContainer } from './step_aliases_container'; +export { StepMappingsContainer } from './step_mappings_container'; +export { StepSettingsContainer } from './step_settings_container'; + +export { CommonWizardSteps } from './types'; diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_aliases_container.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_aliases_container.tsx new file mode 100644 index 00000000000000..a5953ea00a1063 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_aliases_container.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; + +import { Forms } from '../../../../../shared_imports'; +import { CommonWizardSteps } from './types'; +import { StepAliases } from './step_aliases'; + +interface Props { + esDocsBase: string; +} + +export const StepAliasesContainer: React.FunctionComponent = ({ esDocsBase }) => { + const { defaultValue, updateContent } = Forms.useContent('aliases'); + + return ( + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings_container.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings_container.tsx similarity index 57% rename from x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings_container.tsx rename to x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings_container.tsx index 80c0d1d4df4890..34e05d88c651d7 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings_container.tsx +++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings_container.tsx @@ -5,20 +5,23 @@ */ import React from 'react'; -import { Forms } from '../../../../shared_imports'; -import { documentationService } from '../../../services/documentation'; -import { StepMappings } from '../../shared'; -import { WizardContent } from '../template_form'; +import { Forms } from '../../../../../shared_imports'; +import { CommonWizardSteps } from './types'; +import { StepMappings } from './step_mappings'; -export const StepMappingsContainer = () => { - const { defaultValue, updateContent, getData } = Forms.useContent('mappings'); +interface Props { + esDocsBase: string; +} + +export const StepMappingsContainer: React.FunctionComponent = ({ esDocsBase }) => { + const { defaultValue, updateContent, getData } = Forms.useContent('mappings'); return ( ); }; diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings_container.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings_container.tsx new file mode 100644 index 00000000000000..c540ddceb95c2f --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings_container.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; + +import { Forms } from '../../../../../shared_imports'; +import { CommonWizardSteps } from './types'; +import { StepSettings } from './step_settings'; + +interface Props { + esDocsBase: string; +} + +export const StepSettingsContainer = React.memo(({ esDocsBase }: Props) => { + const { defaultValue, updateContent } = Forms.useContent('settings'); + + return ( + + ); +}); diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/types.ts b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/types.ts new file mode 100644 index 00000000000000..f8088e2b6e058b --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/types.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Mappings, IndexSettings, Aliases } from '../../../../../../common'; + +export interface CommonWizardSteps { + settings?: IndexSettings; + mappings?: Mappings; + aliases?: Aliases; +} diff --git a/x-pack/plugins/index_management/public/application/components/shared/index.ts b/x-pack/plugins/index_management/public/application/components/shared/index.ts index 5ec1f717102709..897e86c99eca0b 100644 --- a/x-pack/plugins/index_management/public/application/components/shared/index.ts +++ b/x-pack/plugins/index_management/public/application/components/shared/index.ts @@ -8,7 +8,8 @@ export { TabAliases, TabMappings, TabSettings, - StepAliases, - StepMappings, - StepSettings, + StepAliasesContainer, + StepMappingsContainer, + StepSettingsContainer, + CommonWizardSteps, } from './components'; diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/index.ts b/x-pack/plugins/index_management/public/application/components/template_form/steps/index.ts index 95d1222ad2cc9a..b7e3e36e61814d 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/index.ts +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/index.ts @@ -5,7 +5,4 @@ */ export { StepLogisticsContainer } from './step_logistics_container'; -export { StepAliasesContainer } from './step_aliases_container'; -export { StepMappingsContainer } from './step_mappings_container'; -export { StepSettingsContainer } from './step_settings_container'; export { StepReviewContainer } from './step_review_container'; diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_aliases_container.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_aliases_container.tsx deleted file mode 100644 index a0e0c59be6622e..00000000000000 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_aliases_container.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React from 'react'; - -import { Forms } from '../../../../shared_imports'; -import { documentationService } from '../../../services/documentation'; -import { StepAliases } from '../../shared'; -import { WizardContent } from '../template_form'; - -export const StepAliasesContainer = () => { - const { defaultValue, updateContent } = Forms.useContent('aliases'); - - return ( - - ); -}; diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_settings_container.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_settings_container.tsx deleted file mode 100644 index b79c6804d382b3..00000000000000 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_settings_container.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React from 'react'; - -import { Forms } from '../../../../shared_imports'; -import { documentationService } from '../../../services/documentation'; -import { StepSettings } from '../../shared'; -import { WizardContent } from '../template_form'; - -export const StepSettingsContainer = React.memo(() => { - const { defaultValue, updateContent } = Forms.useContent('settings'); - - return ( - - ); -}); diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx index 9e6d49faac5630..8a2c991aea8d07 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx @@ -11,13 +11,14 @@ import { EuiSpacer } from '@elastic/eui'; import { TemplateDeserialized, CREATE_LEGACY_TEMPLATE_BY_DEFAULT } from '../../../../common'; import { serializers, Forms } from '../../../shared_imports'; import { SectionError } from '../section_error'; +import { StepLogisticsContainer, StepReviewContainer } from './steps'; import { - StepLogisticsContainer, + CommonWizardSteps, StepSettingsContainer, StepMappingsContainer, StepAliasesContainer, - StepReviewContainer, -} from './steps'; +} from '../shared'; +import { documentationService } from '../../services/documentation'; const { stripEmptyFields } = serializers; const { FormWizard, FormWizardStep } = Forms; @@ -31,11 +32,8 @@ interface Props { isEditing?: boolean; } -export interface WizardContent { +export interface WizardContent extends CommonWizardSteps { logistics: Omit; - settings: TemplateDeserialized['template']['settings']; - mappings: TemplateDeserialized['template']['mappings']; - aliases: TemplateDeserialized['template']['aliases']; } export type WizardSection = keyof WizardContent | 'review'; @@ -183,15 +181,15 @@ export const TemplateForm = ({ - + - + - + From a89fa3c1f88059cb2288e3a89f3a992dd02def90 Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Wed, 24 Jun 2020 12:14:55 -0600 Subject: [PATCH 2/7] [Maps] New mappings: maps-telemetry -> maps (#69816) --- x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts b/x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts index 2512bf3094bcfb..ad0b17af36ddab 100644 --- a/x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts +++ b/x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts @@ -6,7 +6,7 @@ import { SavedObjectsType } from 'src/core/server'; export const mapsTelemetrySavedObjects: SavedObjectsType = { - name: 'maps-telemetry', + name: 'maps', hidden: false, namespaceType: 'agnostic', mappings: { From ba9aed4b4e460ae52cd1bd2c484cffab0219a35c Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Wed, 24 Jun 2020 11:19:13 -0700 Subject: [PATCH 3/7] Don't set a min-length on encryption key for reportin (#69827) --- x-pack/plugins/reporting/server/config/schema.test.ts | 2 ++ x-pack/plugins/reporting/server/config/schema.ts | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/reporting/server/config/schema.test.ts b/x-pack/plugins/reporting/server/config/schema.test.ts index 41285c2bfa133a..ddd5491b661bc4 100644 --- a/x-pack/plugins/reporting/server/config/schema.test.ts +++ b/x-pack/plugins/reporting/server/config/schema.test.ts @@ -112,6 +112,8 @@ describe('Reporting Config Schema', () => { .encryptionKey ).toBe('qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq'); + expect(ConfigSchema.validate({ encryptionKey: 'weaksauce' }).encryptionKey).toBe('weaksauce'); + // disableSandbox expect( ConfigSchema.validate({ capture: { browser: { chromium: { disableSandbox: true } } } }) diff --git a/x-pack/plugins/reporting/server/config/schema.ts b/x-pack/plugins/reporting/server/config/schema.ts index b1234a6ddf0b66..2f77aff0020d53 100644 --- a/x-pack/plugins/reporting/server/config/schema.ts +++ b/x-pack/plugins/reporting/server/config/schema.ts @@ -136,8 +136,8 @@ const CsvSchema = schema.object({ const EncryptionKeySchema = schema.conditional( schema.contextRef('dist'), true, - schema.maybe(schema.string({ minLength: 32 })), // default value is dynamic in createConfig$ - schema.string({ minLength: 32, defaultValue: 'a'.repeat(32) }) + schema.maybe(schema.string()), // default value is dynamic in createConfig$ + schema.string({ defaultValue: 'a'.repeat(32) }) ); const RolesSchema = schema.object({ From 94321ccdd0d6354de818c8d465eb2c23129ca7d5 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Wed, 24 Jun 2020 14:36:37 -0400 Subject: [PATCH 4/7] [ML] DF Analytics Creation: add progress indicator (#69583) * add progress indicator to creation wizard page * only show progress bar if job is started immediately * add title and switch to timeout * fix progress check * clean up interval on unmount * fix types * clear interval if stats undefined. show progress if job created --- .../components/create_step/create_step.tsx | 5 + .../components/create_step/progress_stats.tsx | 110 ++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/progress_stats.tsx diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/create_step.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/create_step.tsx index 8d51848a25f500..0d1690cf179463 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/create_step.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/create_step.tsx @@ -19,6 +19,7 @@ import { CreateAnalyticsFormProps } from '../../../analytics_management/hooks/us import { Messages } from '../shared'; import { ANALYTICS_STEPS } from '../../page'; import { BackToListPanel } from '../back_to_list_panel'; +import { ProgressStats } from './progress_stats'; interface Props extends CreateAnalyticsFormProps { step: ANALYTICS_STEPS; @@ -27,8 +28,10 @@ interface Props extends CreateAnalyticsFormProps { export const CreateStep: FC = ({ actions, state, step }) => { const { createAnalyticsJob, startAnalyticsJob } = actions; const { isAdvancedEditorValidJson, isJobCreated, isJobStarted, isValid, requestMessages } = state; + const { jobId } = state.form; const [checked, setChecked] = useState(true); + const [showProgress, setShowProgress] = useState(false); if (step !== ANALYTICS_STEPS.CREATE) return null; @@ -36,6 +39,7 @@ export const CreateStep: FC = ({ actions, state, step }) => { await createAnalyticsJob(); if (checked) { + setShowProgress(true); startAnalyticsJob(); } }; @@ -82,6 +86,7 @@ export const CreateStep: FC = ({ actions, state, step }) => { )} + {isJobCreated === true && showProgress && } {isJobCreated === true && } ); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/progress_stats.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/progress_stats.tsx new file mode 100644 index 00000000000000..8cee63d3c4c841 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/progress_stats.tsx @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useState, useEffect } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSpacer, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useMlKibana } from '../../../../../contexts/kibana'; +import { getDataFrameAnalyticsProgressPhase } from '../../../analytics_management/components/analytics_list/common'; +import { isGetDataFrameAnalyticsStatsResponseOk } from '../../../analytics_management/services/analytics_service/get_analytics'; +import { ml } from '../../../../../services/ml_api_service'; +import { DataFrameAnalyticsId } from '../../../../common/analytics'; + +export const PROGRESS_REFRESH_INTERVAL_MS = 1000; + +export const ProgressStats: FC<{ jobId: DataFrameAnalyticsId }> = ({ jobId }) => { + const [initialized, setInitialized] = useState(false); + const [currentProgress, setCurrentProgress] = useState< + | { + currentPhase: number; + progress: number; + totalPhases: number; + } + | undefined + >(undefined); + + const { + services: { notifications }, + } = useMlKibana(); + + useEffect(() => { + setInitialized(true); + }, []); + + useEffect(() => { + const interval = setInterval(async () => { + try { + const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId); + const jobStats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats) + ? analyticsStats.data_frame_analytics[0] + : undefined; + + if (jobStats !== undefined) { + const progressStats = getDataFrameAnalyticsProgressPhase(jobStats); + setCurrentProgress(progressStats); + if ( + progressStats.currentPhase === progressStats.totalPhases && + progressStats.progress === 100 + ) { + clearInterval(interval); + } + } else { + clearInterval(interval); + } + } catch (e) { + notifications.toasts.addDanger( + i18n.translate('xpack.ml.dataframe.analytics.create.analyticsProgressErrorMessage', { + defaultMessage: 'An error occurred getting progress stats for analytics job {jobId}', + values: { jobId }, + }) + ); + clearInterval(interval); + } + }, PROGRESS_REFRESH_INTERVAL_MS); + + return () => clearInterval(interval); + }, [initialized]); + + if (currentProgress === undefined) return null; + + return ( + <> + + + + {i18n.translate('xpack.ml.dataframe.analytics.create.analyticsProgressTitle', { + defaultMessage: 'Progress', + })} + + + + + + + + {i18n.translate('xpack.ml.dataframe.analytics.create.analyticsProgressPhaseTitle', { + defaultMessage: 'Phase', + })}{' '} + {currentProgress.currentPhase}/{currentProgress.totalPhases} + + + + + + + + {`${currentProgress.progress}%`} + + + + ); +}; From 904be0249b1e8eb1bcf7cb781913f60dd1fab69c Mon Sep 17 00:00:00 2001 From: Shahzad Date: Wed, 24 Jun 2020 20:45:20 +0200 Subject: [PATCH 5/7] [Uptime] Fix charts dark theme (#69748) --- .../components/common/charts/duration_chart.tsx | 6 +++++- .../common/charts/monitor_bar_series.tsx | 2 ++ .../components/common/charts/ping_histogram.tsx | 2 ++ .../public/contexts/uptime_theme_context.tsx | 16 +++++++++++++++- 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/uptime/public/components/common/charts/duration_chart.tsx b/x-pack/plugins/uptime/public/components/common/charts/duration_chart.tsx index a6bb097de45ad7..a1e23ab8b38a72 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/duration_chart.tsx +++ b/x-pack/plugins/uptime/public/components/common/charts/duration_chart.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState } from 'react'; +import React, { useContext, useState } from 'react'; import { i18n } from '@kbn/i18n'; import moment from 'moment'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -26,6 +26,7 @@ import { getTickFormat } from './get_tick_format'; import { ChartEmptyState } from './chart_empty_state'; import { DurationAnomaliesBar } from './duration_line_bar_list'; import { AnomalyRecords } from '../../../state/actions'; +import { UptimeThemeContext } from '../../../contexts'; interface DurationChartProps { /** @@ -59,6 +60,8 @@ export const DurationChartComponent = ({ const [hiddenLegends, setHiddenLegends] = useState([]); + const { chartTheme } = useContext(UptimeThemeContext); + const onBrushEnd: BrushEndListener = ({ x }) => { if (!x) { return; @@ -93,6 +96,7 @@ export const DurationChartComponent = ({ legendPosition={Position.Bottom} onBrushEnd={onBrushEnd} onLegendItemClick={legendToggleVisibility} + {...chartTheme} /> { const { colors: { danger }, + chartTheme, } = useContext(UptimeThemeContext); const [getUrlParams, updateUrlParams] = useUrlParams(); const { absoluteDateRangeStart, absoluteDateRangeEnd } = getUrlParams(); @@ -62,6 +63,7 @@ export const MonitorBarSeries = ({ histogramSeries }: MonitorBarSeriesProps) => = ({ }) => { const { colors: { danger, gray }, + chartTheme, } = useContext(UptimeThemeContext); const [, updateUrlParams] = useUrlParams(); @@ -128,6 +129,7 @@ export const PingHistogramComponent: React.FC = ({ }} showLegend={false} onBrushEnd={onBrushEnd} + {...chartTheme} /> = ({ darkMo const value = useMemo(() => { return { colors, + chartTheme: { + baseTheme: darkMode ? DARK_THEME : LIGHT_THEME, + theme: darkMode ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme, + }, }; - }, [colors]); + }, [colors, darkMode]); return ; }; From a104e5ab0ee939b4d7f9cc4511a18dcb822c9dbf Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Wed, 24 Jun 2020 15:16:15 -0400 Subject: [PATCH 6/7] [Ingest Manager] Support registration of server side callbacks for Create Datasource API (#69428) * Ingest: Expose `registerExternalCallback()` method out of Ingest server `start` lifecycle * Ingest: Add support for External Callbacks on REST `createDatasourceHandler()` * Ingest: expose DatasourceServices to Plugin start interface * Endpoint: Added Endpoint Ingest handler for Create Datasources - Also moved the temporary logic from the middleware to the handler (still temporary) Co-authored-by: Elastic Machine --- x-pack/plugins/ingest_manager/server/index.ts | 3 + x-pack/plugins/ingest_manager/server/mocks.ts | 36 ++ .../plugins/ingest_manager/server/plugin.ts | 26 +- .../datasource/datasource_handlers.test.ts | 332 ++++++++++++++++++ .../server/routes/datasource/handlers.ts | 41 ++- .../server/services/app_context.ts | 20 +- .../server/services/datasource.ts | 1 + .../policy/store/policy_details/middleware.ts | 18 - .../endpoint/alerts/handlers/alerts.test.ts | 6 +- .../endpoint/endpoint_app_context_services.ts | 13 +- .../server/endpoint/ingest_integration.ts | 49 +++ .../server/endpoint/mocks.ts | 25 +- .../endpoint/routes/metadata/metadata.test.ts | 17 +- .../endpoint/routes/policy/handlers.test.ts | 12 +- .../security_solution/server/plugin.ts | 2 + 15 files changed, 553 insertions(+), 48 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/server/mocks.ts create mode 100644 x-pack/plugins/ingest_manager/server/routes/datasource/datasource_handlers.test.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts diff --git a/x-pack/plugins/ingest_manager/server/index.ts b/x-pack/plugins/ingest_manager/server/index.ts index f6b2d7ccc6d480..1e9011c9dfe4f7 100644 --- a/x-pack/plugins/ingest_manager/server/index.ts +++ b/x-pack/plugins/ingest_manager/server/index.ts @@ -11,6 +11,7 @@ export { IngestManagerSetupContract, IngestManagerSetupDeps, IngestManagerStartContract, + ExternalCallback, } from './plugin'; export const config = { @@ -42,6 +43,8 @@ export const config = { export type IngestManagerConfigType = TypeOf; +export { DatasourceServiceInterface } from './services/datasource'; + export const plugin = (initializerContext: PluginInitializerContext) => { return new IngestManagerPlugin(initializerContext); }; diff --git a/x-pack/plugins/ingest_manager/server/mocks.ts b/x-pack/plugins/ingest_manager/server/mocks.ts new file mode 100644 index 00000000000000..3bdef14dc85a01 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mocks'; +import { IngestManagerAppContext } from './plugin'; +import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks'; +import { securityMock } from '../../security/server/mocks'; +import { DatasourceServiceInterface } from './services/datasource'; + +export const createAppContextStartContractMock = (): IngestManagerAppContext => { + return { + encryptedSavedObjectsStart: encryptedSavedObjectsMock.createStart(), + savedObjects: savedObjectsServiceMock.createStartContract(), + security: securityMock.createSetup(), + logger: loggingSystemMock.create().get(), + isProductionMode: true, + kibanaVersion: '8.0.0', + }; +}; + +export const createDatasourceServiceMock = () => { + return { + assignPackageStream: jest.fn(), + buildDatasourceFromPackage: jest.fn(), + bulkCreate: jest.fn(), + create: jest.fn(), + delete: jest.fn(), + get: jest.fn(), + getByIDs: jest.fn(), + list: jest.fn(), + update: jest.fn(), + } as jest.Mocked; +}; diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts index fb1c218e1545b4..e060eb5e8068ba 100644 --- a/x-pack/plugins/ingest_manager/server/plugin.ts +++ b/x-pack/plugins/ingest_manager/server/plugin.ts @@ -45,13 +45,14 @@ import { registerSettingsRoutes, registerAppRoutes, } from './routes'; -import { IngestManagerConfigType } from '../common'; +import { IngestManagerConfigType, NewDatasource } from '../common'; import { appContextService, licenseService, ESIndexPatternSavedObjectService, ESIndexPatternService, AgentService, + datasourceService, } from './services'; import { getAgentStatusById } from './services/agents'; import { CloudSetup } from '../../cloud/server'; @@ -92,12 +93,31 @@ const allSavedObjectTypes = [ ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, ]; +/** + * Callbacks supported by the Ingest plugin + */ +export type ExternalCallback = [ + 'datasourceCreate', + (newDatasource: NewDatasource) => Promise +]; + +export type ExternalCallbacksStorage = Map>; + /** * Describes public IngestManager plugin contract returned at the `startup` stage. */ export interface IngestManagerStartContract { esIndexPatternService: ESIndexPatternService; agentService: AgentService; + /** + * Services for Ingest's Datasources + */ + datasourceService: typeof datasourceService; + /** + * Register callbacks for inclusion in ingest API processing + * @param args + */ + registerExternalCallback: (...args: ExternalCallback) => void; } export class IngestManagerPlugin @@ -237,6 +257,10 @@ export class IngestManagerPlugin agentService: { getAgentStatusById, }, + datasourceService, + registerExternalCallback: (...args: ExternalCallback) => { + return appContextService.addExternalCallback(...args); + }, }; } diff --git a/x-pack/plugins/ingest_manager/server/routes/datasource/datasource_handlers.test.ts b/x-pack/plugins/ingest_manager/server/routes/datasource/datasource_handlers.test.ts new file mode 100644 index 00000000000000..07cbeb8b2cec56 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/datasource/datasource_handlers.test.ts @@ -0,0 +1,332 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { httpServerMock, httpServiceMock } from 'src/core/server/mocks'; +import { IRouter, KibanaRequest, Logger, RequestHandler, RouteConfig } from 'kibana/server'; +import { registerRoutes } from './index'; +import { DATASOURCE_API_ROUTES } from '../../../common/constants'; +import { xpackMocks } from '../../../../../mocks'; +import { appContextService } from '../../services'; +import { createAppContextStartContractMock } from '../../mocks'; +import { DatasourceServiceInterface, ExternalCallback } from '../..'; +import { CreateDatasourceRequestSchema } from '../../types/rest_spec'; +import { datasourceService } from '../../services'; + +const datasourceServiceMock = datasourceService as jest.Mocked; + +jest.mock('../../services/datasource', (): { + datasourceService: jest.Mocked; +} => { + return { + datasourceService: { + assignPackageStream: jest.fn((packageInfo, dataInputs) => Promise.resolve(dataInputs)), + buildDatasourceFromPackage: jest.fn(), + bulkCreate: jest.fn(), + create: jest.fn((soClient, newData) => + Promise.resolve({ + ...newData, + id: '1', + revision: 1, + updated_at: new Date().toISOString(), + updated_by: 'elastic', + created_at: new Date().toISOString(), + created_by: 'elastic', + }) + ), + delete: jest.fn(), + get: jest.fn(), + getByIDs: jest.fn(), + list: jest.fn(), + update: jest.fn(), + }, + }; +}); + +jest.mock('../../services/epm/packages', () => { + return { + ensureInstalledPackage: jest.fn(() => Promise.resolve()), + getPackageInfo: jest.fn(() => Promise.resolve()), + }; +}); + +describe('When calling datasource', () => { + let routerMock: jest.Mocked; + let routeHandler: RequestHandler; + let routeConfig: RouteConfig; + let context: ReturnType; + let response: ReturnType; + + beforeAll(() => { + routerMock = httpServiceMock.createRouter(); + registerRoutes(routerMock); + }); + + beforeEach(() => { + appContextService.start(createAppContextStartContractMock()); + context = xpackMocks.createRequestHandlerContext(); + response = httpServerMock.createResponseFactory(); + }); + + afterEach(() => { + jest.clearAllMocks(); + appContextService.stop(); + }); + + describe('create api handler', () => { + const getCreateKibanaRequest = ( + newData?: typeof CreateDatasourceRequestSchema.body + ): KibanaRequest => { + return httpServerMock.createKibanaRequest< + undefined, + undefined, + typeof CreateDatasourceRequestSchema.body + >({ + path: routeConfig.path, + method: 'post', + body: newData || { + name: 'endpoint-1', + description: '', + config_id: 'a5ca00c0-b30c-11ea-9732-1bb05811278c', + enabled: true, + output_id: '', + inputs: [], + namespace: 'default', + package: { name: 'endpoint', title: 'Elastic Endpoint', version: '0.5.0' }, + }, + }); + }; + + // Set the routeConfig and routeHandler to the Create API + beforeAll(() => { + [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => + path.startsWith(DATASOURCE_API_ROUTES.CREATE_PATTERN) + )!; + }); + + describe('and external callbacks are registered', () => { + const callbackCallingOrder: string[] = []; + + // Callback one adds an input that includes a `config` property + const callbackOne: ExternalCallback[1] = jest.fn(async (ds) => { + callbackCallingOrder.push('one'); + const newDs = { + ...ds, + inputs: [ + { + type: 'endpoint', + enabled: true, + streams: [], + config: { + one: { + value: 'inserted by callbackOne', + }, + }, + }, + ], + }; + return newDs; + }); + + // Callback two adds an additional `input[0].config` property + const callbackTwo: ExternalCallback[1] = jest.fn(async (ds) => { + callbackCallingOrder.push('two'); + const newDs = { + ...ds, + inputs: [ + { + ...ds.inputs[0], + config: { + ...ds.inputs[0].config, + two: { + value: 'inserted by callbackTwo', + }, + }, + }, + ], + }; + return newDs; + }); + + beforeEach(() => { + appContextService.addExternalCallback('datasourceCreate', callbackOne); + appContextService.addExternalCallback('datasourceCreate', callbackTwo); + }); + + afterEach(() => (callbackCallingOrder.length = 0)); + + it('should call external callbacks in expected order', async () => { + const request = getCreateKibanaRequest(); + await routeHandler(context, request, response); + expect(response.ok).toHaveBeenCalled(); + expect(callbackCallingOrder).toEqual(['one', 'two']); + }); + + it('should feed datasource returned by last callback', async () => { + const request = getCreateKibanaRequest(); + await routeHandler(context, request, response); + expect(response.ok).toHaveBeenCalled(); + expect(callbackOne).toHaveBeenCalledWith({ + config_id: 'a5ca00c0-b30c-11ea-9732-1bb05811278c', + description: '', + enabled: true, + inputs: [], + name: 'endpoint-1', + namespace: 'default', + output_id: '', + package: { + name: 'endpoint', + title: 'Elastic Endpoint', + version: '0.5.0', + }, + }); + expect(callbackTwo).toHaveBeenCalledWith({ + config_id: 'a5ca00c0-b30c-11ea-9732-1bb05811278c', + description: '', + enabled: true, + inputs: [ + { + type: 'endpoint', + enabled: true, + streams: [], + config: { + one: { + value: 'inserted by callbackOne', + }, + }, + }, + ], + name: 'endpoint-1', + namespace: 'default', + output_id: '', + package: { + name: 'endpoint', + title: 'Elastic Endpoint', + version: '0.5.0', + }, + }); + }); + + it('should create with data from callback', async () => { + const request = getCreateKibanaRequest(); + await routeHandler(context, request, response); + expect(response.ok).toHaveBeenCalled(); + expect(datasourceServiceMock.create.mock.calls[0][1]).toEqual({ + config_id: 'a5ca00c0-b30c-11ea-9732-1bb05811278c', + description: '', + enabled: true, + inputs: [ + { + config: { + one: { + value: 'inserted by callbackOne', + }, + two: { + value: 'inserted by callbackTwo', + }, + }, + enabled: true, + streams: [], + type: 'endpoint', + }, + ], + name: 'endpoint-1', + namespace: 'default', + output_id: '', + package: { + name: 'endpoint', + title: 'Elastic Endpoint', + version: '0.5.0', + }, + }); + }); + + describe('and a callback throws an exception', () => { + const callbackThree: ExternalCallback[1] = jest.fn(async (ds) => { + callbackCallingOrder.push('three'); + throw new Error('callbackThree threw error on purpose'); + }); + + const callbackFour: ExternalCallback[1] = jest.fn(async (ds) => { + callbackCallingOrder.push('four'); + return { + ...ds, + inputs: [ + { + ...ds.inputs[0], + config: { + ...ds.inputs[0].config, + four: { + value: 'inserted by callbackFour', + }, + }, + }, + ], + }; + }); + + beforeEach(() => { + appContextService.addExternalCallback('datasourceCreate', callbackThree); + appContextService.addExternalCallback('datasourceCreate', callbackFour); + }); + + it('should skip over callback exceptions and still execute other callbacks', async () => { + const request = getCreateKibanaRequest(); + await routeHandler(context, request, response); + expect(response.ok).toHaveBeenCalled(); + expect(callbackCallingOrder).toEqual(['one', 'two', 'three', 'four']); + }); + + it('should log errors', async () => { + const errorLogger = (appContextService.getLogger() as jest.Mocked).error; + const request = getCreateKibanaRequest(); + await routeHandler(context, request, response); + expect(response.ok).toHaveBeenCalled(); + expect(errorLogger.mock.calls).toEqual([ + ['An external registered [datasourceCreate] callback failed when executed'], + [new Error('callbackThree threw error on purpose')], + ]); + }); + + it('should create datasource with last successful returned datasource', async () => { + const request = getCreateKibanaRequest(); + await routeHandler(context, request, response); + expect(response.ok).toHaveBeenCalled(); + expect(datasourceServiceMock.create.mock.calls[0][1]).toEqual({ + config_id: 'a5ca00c0-b30c-11ea-9732-1bb05811278c', + description: '', + enabled: true, + inputs: [ + { + config: { + one: { + value: 'inserted by callbackOne', + }, + two: { + value: 'inserted by callbackTwo', + }, + four: { + value: 'inserted by callbackFour', + }, + }, + enabled: true, + streams: [], + type: 'endpoint', + }, + ], + name: 'endpoint-1', + namespace: 'default', + output_id: '', + package: { + name: 'endpoint', + title: 'Elastic Endpoint', + version: '0.5.0', + }, + }); + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts index 09daec3370400c..4f83d24a846ea7 100644 --- a/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts @@ -14,6 +14,7 @@ import { CreateDatasourceRequestSchema, UpdateDatasourceRequestSchema, DeleteDatasourcesRequestSchema, + NewDatasource, } from '../../types'; import { CreateDatasourceResponse, DeleteDatasourcesResponse } from '../../../common'; @@ -76,23 +77,50 @@ export const createDatasourceHandler: RequestHandler< const soClient = context.core.savedObjects.client; const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; const user = (await appContextService.getSecurity()?.authc.getCurrentUser(request)) || undefined; - const newData = { ...request.body }; + const logger = appContextService.getLogger(); + let newData = { ...request.body }; try { + // If we have external callbacks, then process those now before creating the actual datasource + const externalCallbacks = appContextService.getExternalCallbacks('datasourceCreate'); + if (externalCallbacks && externalCallbacks.size > 0) { + let updatedNewData: NewDatasource = newData; + + for (const callback of externalCallbacks) { + try { + // ensure that the returned value by the callback passes schema validation + updatedNewData = CreateDatasourceRequestSchema.body.validate( + await callback(updatedNewData) + ); + } catch (error) { + // Log the error, but keep going and process the other callbacks + logger.error('An external registered [datasourceCreate] callback failed when executed'); + logger.error(error); + } + } + + // The type `NewDatasource` and the `DatasourceBaseSchema` are incompatible. + // `NewDatasrouce` defines `namespace` as optional string, which means that `undefined` is a + // valid value, however, the schema defines it as string with a minimum length of 1. + // Here, we need to cast the value back to the schema type and ignore the TS error. + // @ts-ignore + newData = updatedNewData as typeof CreateDatasourceRequestSchema.body; + } + // Make sure the datasource package is installed - if (request.body.package?.name) { + if (newData.package?.name) { await ensureInstalledPackage({ savedObjectsClient: soClient, - pkgName: request.body.package.name, + pkgName: newData.package.name, callCluster, }); const pkgInfo = await getPackageInfo({ savedObjectsClient: soClient, - pkgName: request.body.package.name, - pkgVersion: request.body.package.version, + pkgName: newData.package.name, + pkgVersion: newData.package.version, }); newData.inputs = (await datasourceService.assignPackageStream( pkgInfo, - request.body.inputs + newData.inputs )) as TypeOf['inputs']; } @@ -103,6 +131,7 @@ export const createDatasourceHandler: RequestHandler< body, }); } catch (e) { + logger.error(e); return response.customError({ statusCode: 500, body: { message: e.message }, diff --git a/x-pack/plugins/ingest_manager/server/services/app_context.ts b/x-pack/plugins/ingest_manager/server/services/app_context.ts index 5ed6f7c5e54d18..4d109b73d12d90 100644 --- a/x-pack/plugins/ingest_manager/server/services/app_context.ts +++ b/x-pack/plugins/ingest_manager/server/services/app_context.ts @@ -12,7 +12,7 @@ import { } from '../../../encrypted_saved_objects/server'; import { SecurityPluginSetup } from '../../../security/server'; import { IngestManagerConfigType } from '../../common'; -import { IngestManagerAppContext } from '../plugin'; +import { ExternalCallback, ExternalCallbacksStorage, IngestManagerAppContext } from '../plugin'; import { CloudSetup } from '../../../cloud/server'; class AppContextService { @@ -27,6 +27,7 @@ class AppContextService { private cloud?: CloudSetup; private logger: Logger | undefined; private httpSetup?: HttpServiceSetup; + private externalCallbacks: ExternalCallbacksStorage = new Map(); public async start(appContext: IngestManagerAppContext) { this.encryptedSavedObjects = appContext.encryptedSavedObjectsStart?.getClient(); @@ -47,7 +48,9 @@ class AppContextService { } } - public stop() {} + public stop() { + this.externalCallbacks.clear(); + } public getEncryptedSavedObjects() { if (!this.encryptedSavedObjects) { @@ -121,6 +124,19 @@ class AppContextService { } return this.kibanaVersion; } + + public addExternalCallback(type: ExternalCallback[0], callback: ExternalCallback[1]) { + if (!this.externalCallbacks.has(type)) { + this.externalCallbacks.set(type, new Set()); + } + this.externalCallbacks.get(type)!.add(callback); + } + + public getExternalCallbacks(type: ExternalCallback[0]) { + if (this.externalCallbacks) { + return this.externalCallbacks.get(type); + } + } } export const appContextService = new AppContextService(); diff --git a/x-pack/plugins/ingest_manager/server/services/datasource.ts b/x-pack/plugins/ingest_manager/server/services/datasource.ts index 3ad94ea8191d4d..f3f460d2a74206 100644 --- a/x-pack/plugins/ingest_manager/server/services/datasource.ts +++ b/x-pack/plugins/ingest_manager/server/services/datasource.ts @@ -307,4 +307,5 @@ async function _assignPackageStreamToStream( return { ...stream }; } +export type DatasourceServiceInterface = DatasourceService; export const datasourceService = new DatasourceService(); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts index ec0c526482b453..899f85ecdea306 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts @@ -17,7 +17,6 @@ import { sendPutDatasource, } from '../policy_list/services/ingest'; import { NewPolicyData, PolicyData } from '../../../../../../common/endpoint/types'; -import { factory as policyConfigFactory } from '../../../../../../common/endpoint/models/policy_config'; import { ImmutableMiddlewareFactory } from '../../../../../common/store'; export const policyDetailsMiddlewareFactory: ImmutableMiddlewareFactory = ( @@ -43,23 +42,6 @@ export const policyDetailsMiddlewareFactory: ImmutableMiddlewareFactory { routerMock = httpServiceMock.createRouter(); endpointAppContextService = new EndpointAppContextService(); - endpointAppContextService.start({ - agentService: createMockAgentService(), - }); + endpointAppContextService.start(createMockEndpointAppContextServiceStartContract()); registerAlertRoutes(routerMock, { logFactory: loggingSystemMock.create(), diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts index cb8c913a73b8e8..7b8a368b6c9757 100644 --- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts +++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts @@ -3,7 +3,15 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { AgentService } from '../../../ingest_manager/server'; +import { AgentService, IngestManagerStartContract } from '../../../ingest_manager/server'; +import { handleDatasourceCreate } from './ingest_integration'; + +export type EndpointAppContextServiceStartContract = Pick< + IngestManagerStartContract, + 'agentService' +> & { + registerIngestCallback: IngestManagerStartContract['registerExternalCallback']; +}; /** * A singleton that holds shared services that are initialized during the start up phase @@ -12,8 +20,9 @@ import { AgentService } from '../../../ingest_manager/server'; export class EndpointAppContextService { private agentService: AgentService | undefined; - public start(dependencies: { agentService: AgentService }) { + public start(dependencies: EndpointAppContextServiceStartContract) { this.agentService = dependencies.agentService; + dependencies.registerIngestCallback('datasourceCreate', handleDatasourceCreate); } public stop() {} diff --git a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts new file mode 100644 index 00000000000000..6ff0949311587f --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { factory as policyConfigFactory } from '../../common/endpoint/models/policy_config'; +import { NewPolicyData } from '../../common/endpoint/types'; +import { NewDatasource } from '../../../ingest_manager/common/types/models'; + +/** + * Callback to handle creation of Datasources in Ingest Manager + * @param newDatasource + */ +export const handleDatasourceCreate = async ( + newDatasource: NewDatasource +): Promise => { + // We only care about Endpoint datasources + if (newDatasource.package?.name !== 'endpoint') { + return newDatasource; + } + + // We cast the type here so that any changes to the Endpoint specific data + // follow the types/schema expected + let updatedDatasource = newDatasource as NewPolicyData; + + // Until we get the Default Policy Configuration in the Endpoint package, + // we will add it here manually at creation time. + // @ts-ignore + if (newDatasource.inputs.length === 0) { + updatedDatasource = { + ...newDatasource, + inputs: [ + { + type: 'endpoint', + enabled: true, + streams: [], + config: { + policy: { + value: policyConfigFactory(), + }, + }, + }, + ], + }; + } + + return updatedDatasource; +}; diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts index b10e9e4dc90e77..5435eff4ef1507 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts @@ -6,7 +6,28 @@ import { IScopedClusterClient, SavedObjectsClientContract } from 'kibana/server'; import { xpackMocks } from '../../../../mocks'; -import { AgentService, IngestManagerStartContract } from '../../../ingest_manager/server'; +import { + AgentService, + IngestManagerStartContract, + ExternalCallback, +} from '../../../ingest_manager/server'; +import { EndpointAppContextServiceStartContract } from './endpoint_app_context_services'; +import { createDatasourceServiceMock } from '../../../ingest_manager/server/mocks'; + +/** + * Crates a mocked input contract for the `EndpointAppContextService#start()` method + */ +export const createMockEndpointAppContextServiceStartContract = (): jest.Mocked< + EndpointAppContextServiceStartContract +> => { + return { + agentService: createMockAgentService(), + registerIngestCallback: jest.fn< + ReturnType, + Parameters + >(), + }; +}; /** * Creates a mock AgentService @@ -32,6 +53,8 @@ export const createMockIngestManagerStartContract = ( getESIndexPattern: jest.fn().mockResolvedValue(indexPattern), }, agentService: createMockAgentService(), + registerExternalCallback: jest.fn((...args: ExternalCallback) => {}), + datasourceService: createDatasourceServiceMock(), }; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts index ba51a3b6aa92ee..c04975fa8b28e0 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts @@ -27,8 +27,10 @@ import { } from '../../../../common/endpoint/types'; import { SearchResponse } from 'elasticsearch'; import { registerEndpointRoutes } from './index'; -import { createMockAgentService, createRouteHandlerContext } from '../../mocks'; -import { AgentService } from '../../../../../ingest_manager/server'; +import { + createMockEndpointAppContextServiceStartContract, + createRouteHandlerContext, +} from '../../mocks'; import Boom from 'boom'; import { EndpointAppContextService } from '../../endpoint_app_context_services'; import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; @@ -44,7 +46,9 @@ describe('test endpoint route', () => { let routeHandler: RequestHandler; // eslint-disable-next-line @typescript-eslint/no-explicit-any let routeConfig: RouteConfig; - let mockAgentService: jest.Mocked; + let mockAgentService: ReturnType< + typeof createMockEndpointAppContextServiceStartContract + >['agentService']; let endpointAppContextService: EndpointAppContextService; beforeEach(() => { @@ -56,11 +60,10 @@ describe('test endpoint route', () => { mockClusterClient.asScoped.mockReturnValue(mockScopedClient); routerMock = httpServiceMock.createRouter(); mockResponse = httpServerMock.createResponseFactory(); - mockAgentService = createMockAgentService(); endpointAppContextService = new EndpointAppContextService(); - endpointAppContextService.start({ - agentService: mockAgentService, - }); + const startContract = createMockEndpointAppContextServiceStartContract(); + endpointAppContextService.start(startContract); + mockAgentService = startContract.agentService; registerEndpointRoutes(routerMock, { logFactory: loggingSystemMock.create(), diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts index 6c1f0a206ffaa8..16af3a95bc72da 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts @@ -4,7 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ import { EndpointAppContextService } from '../../endpoint_app_context_services'; -import { createMockAgentService, createRouteHandlerContext } from '../../mocks'; +import { + createMockEndpointAppContextServiceStartContract, + createRouteHandlerContext, +} from '../../mocks'; import { getHostPolicyResponseHandler } from './handlers'; import { IScopedClusterClient, @@ -17,7 +20,6 @@ import { loggingSystemMock, savedObjectsClientMock, } from '../../../../../../../src/core/server/mocks'; -import { AgentService } from '../../../../../ingest_manager/server/services'; import { SearchResponse } from 'elasticsearch'; import { GetHostPolicyResponse, HostPolicyResponse } from '../../../../common/endpoint/types'; import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; @@ -28,17 +30,13 @@ describe('test policy response handler', () => { let mockScopedClient: jest.Mocked; let mockSavedObjectClient: jest.Mocked; let mockResponse: jest.Mocked; - let mockAgentService: jest.Mocked; beforeEach(() => { mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); mockSavedObjectClient = savedObjectsClientMock.create(); mockResponse = httpServerMock.createResponseFactory(); endpointAppContextService = new EndpointAppContextService(); - mockAgentService = createMockAgentService(); - endpointAppContextService.start({ - agentService: mockAgentService, - }); + endpointAppContextService.start(createMockEndpointAppContextServiceStartContract()); }); afterEach(() => endpointAppContextService.stop()); diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 9fe7307e8cb6da..879c132ddec54d 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -219,7 +219,9 @@ export class Plugin implements IPlugin Date: Wed, 24 Jun 2020 15:53:38 -0400 Subject: [PATCH 7/7] [IngestManager] Expose agent authentication using access key (#69650) * [IngestManager] Expose agent authentication using access key * Add unit tests to authenticateAgentWithAccessToken service --- .../plugins/ingest_manager/server/plugin.ts | 3 +- .../server/routes/agent/acks_handlers.test.ts | 2 +- .../server/routes/agent/acks_handlers.ts | 4 +- .../server/routes/agent/handlers.ts | 3 +- .../server/routes/agent/index.ts | 2 +- .../server/services/agents/acks.ts | 4 +- .../services/agents/authenticate.test.ts | 154 ++++++++++++++++++ .../server/services/agents/authenticate.ts | 30 ++++ .../server/services/agents/index.ts | 1 + 9 files changed, 193 insertions(+), 10 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/server/services/agents/authenticate.test.ts create mode 100644 x-pack/plugins/ingest_manager/server/services/agents/authenticate.ts diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts index e060eb5e8068ba..fcdb6387fed3ae 100644 --- a/x-pack/plugins/ingest_manager/server/plugin.ts +++ b/x-pack/plugins/ingest_manager/server/plugin.ts @@ -54,7 +54,7 @@ import { AgentService, datasourceService, } from './services'; -import { getAgentStatusById } from './services/agents'; +import { getAgentStatusById, authenticateAgentWithAccessToken } from './services/agents'; import { CloudSetup } from '../../cloud/server'; import { agentCheckinState } from './services/agents/checkin/state'; @@ -256,6 +256,7 @@ export class IngestManagerPlugin esIndexPatternService: new ESIndexPatternSavedObjectService(), agentService: { getAgentStatusById, + authenticateAgentWithAccessToken, }, datasourceService, registerExternalCallback: (...args: ExternalCallback) => { diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.test.ts b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.test.ts index 84923d5c336642..aaed189ae3dddf 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.test.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.test.ts @@ -77,7 +77,7 @@ describe('test acks handlers', () => { id: 'action1', }, ]), - getAgentByAccessAPIKeyId: jest.fn().mockReturnValueOnce({ + authenticateAgentWithAccessToken: jest.fn().mockReturnValueOnce({ id: 'agent', }), getSavedObjectsClientContract: jest.fn().mockReturnValueOnce(mockSavedObjectsClient), diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts index 83d894295c3126..0b719d8a67df74 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts @@ -9,7 +9,6 @@ import { RequestHandler } from 'kibana/server'; import { TypeOf } from '@kbn/config-schema'; import { PostAgentAcksRequestSchema } from '../../types/rest_spec'; -import * as APIKeyService from '../../services/api_keys'; import { AcksService } from '../../services/agents'; import { AgentEvent } from '../../../common/types/models'; import { PostAgentAcksResponse } from '../../../common/types/rest_spec'; @@ -24,8 +23,7 @@ export const postAgentAcksHandlerBuilder = function ( return async (context, request, response) => { try { const soClient = ackService.getSavedObjectsClientContract(request); - const res = APIKeyService.parseApiKeyFromHeaders(request.headers); - const agent = await ackService.getAgentByAccessAPIKeyId(soClient, res.apiKeyId as string); + const agent = await ackService.authenticateAgentWithAccessToken(soClient, request); const agentEvents = request.body.events as AgentEvent[]; // validate that all events are for the authorized agent obtained from the api key diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts index 0d1c77b8d697fd..d31498599a2b65 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts @@ -171,8 +171,7 @@ export const postAgentCheckinHandler: RequestHandler< > = async (context, request, response) => { try { const soClient = appContextService.getInternalUserSOClient(request); - const res = APIKeyService.parseApiKeyFromHeaders(request.headers); - const agent = await AgentService.getAgentByAccessAPIKeyId(soClient, res.apiKeyId); + const agent = await AgentService.authenticateAgentWithAccessToken(soClient, request); const abortController = new AbortController(); request.events.aborted$.subscribe(() => { abortController.abort(); diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts index 87eee4622c80b6..eaab46c7b455c3 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts @@ -109,7 +109,7 @@ export const registerRoutes = (router: IRouter) => { }, postAgentAcksHandlerBuilder({ acknowledgeAgentActions: AgentService.acknowledgeAgentActions, - getAgentByAccessAPIKeyId: AgentService.getAgentByAccessAPIKeyId, + authenticateAgentWithAccessToken: AgentService.authenticateAgentWithAccessToken, getSavedObjectsClientContract: appContextService.getInternalUserSOClient.bind( appContextService ), diff --git a/x-pack/plugins/ingest_manager/server/services/agents/acks.ts b/x-pack/plugins/ingest_manager/server/services/agents/acks.ts index 81ba9754e8aa48..a1b48a879bb890 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/acks.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/acks.ts @@ -140,9 +140,9 @@ export interface AcksService { actionIds: AgentEvent[] ) => Promise; - getAgentByAccessAPIKeyId: ( + authenticateAgentWithAccessToken: ( soClient: SavedObjectsClientContract, - accessAPIKeyId: string + request: KibanaRequest ) => Promise; getSavedObjectsClientContract: (kibanaRequest: KibanaRequest) => SavedObjectsClientContract; diff --git a/x-pack/plugins/ingest_manager/server/services/agents/authenticate.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/authenticate.test.ts new file mode 100644 index 00000000000000..b56ca4ca8cc177 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/agents/authenticate.test.ts @@ -0,0 +1,154 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaRequest } from 'kibana/server'; +import { savedObjectsClientMock } from 'src/core/server/mocks'; + +import { authenticateAgentWithAccessToken } from './authenticate'; + +describe('test agent autenticate services', () => { + it('should succeed with a valid API key and an active agent', async () => { + const mockSavedObjectsClient = savedObjectsClientMock.create(); + mockSavedObjectsClient.find.mockReturnValue( + Promise.resolve({ + page: 1, + per_page: 100, + total: 1, + saved_objects: [ + { + id: 'agent1', + type: 'agent', + references: [], + score: 0, + attributes: { + active: true, + access_api_key_id: 'pedTuHIBTEDt93wW0Fhr', + }, + }, + ], + }) + ); + await authenticateAgentWithAccessToken(mockSavedObjectsClient, { + auth: { isAuthenticated: true }, + headers: { + authorization: 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw==', + }, + } as KibanaRequest); + }); + + it('should throw if the request is not authenticated', async () => { + const mockSavedObjectsClient = savedObjectsClientMock.create(); + mockSavedObjectsClient.find.mockReturnValue( + Promise.resolve({ + page: 1, + per_page: 100, + total: 1, + saved_objects: [ + { + id: 'agent1', + type: 'agent', + references: [], + score: 0, + attributes: { + active: true, + access_api_key_id: 'pedTuHIBTEDt93wW0Fhr', + }, + }, + ], + }) + ); + expect( + authenticateAgentWithAccessToken(mockSavedObjectsClient, { + auth: { isAuthenticated: false }, + headers: { + authorization: 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw==', + }, + } as KibanaRequest) + ).rejects.toThrow(/Request not authenticated/); + }); + + it('should throw if the ApiKey headers is malformed', async () => { + const mockSavedObjectsClient = savedObjectsClientMock.create(); + mockSavedObjectsClient.find.mockReturnValue( + Promise.resolve({ + page: 1, + per_page: 100, + total: 1, + saved_objects: [ + { + id: 'agent1', + type: 'agent', + references: [], + score: 0, + attributes: { + active: false, + access_api_key_id: 'pedTuHIBTEDt93wW0Fhr', + }, + }, + ], + }) + ); + expect( + authenticateAgentWithAccessToken(mockSavedObjectsClient, { + auth: { isAuthenticated: true }, + headers: { + authorization: 'aaaa', + }, + } as KibanaRequest) + ).rejects.toThrow(/Authorization header is malformed/); + }); + + it('should throw if the agent is not active', async () => { + const mockSavedObjectsClient = savedObjectsClientMock.create(); + mockSavedObjectsClient.find.mockReturnValue( + Promise.resolve({ + page: 1, + per_page: 100, + total: 1, + saved_objects: [ + { + id: 'agent1', + type: 'agent', + references: [], + score: 0, + attributes: { + active: false, + access_api_key_id: 'pedTuHIBTEDt93wW0Fhr', + }, + }, + ], + }) + ); + expect( + authenticateAgentWithAccessToken(mockSavedObjectsClient, { + auth: { isAuthenticated: true }, + headers: { + authorization: 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw==', + }, + } as KibanaRequest) + ).rejects.toThrow(/Agent inactive/); + }); + + it('should throw if there is no agent matching the API key', async () => { + const mockSavedObjectsClient = savedObjectsClientMock.create(); + mockSavedObjectsClient.find.mockReturnValue( + Promise.resolve({ + page: 1, + per_page: 100, + total: 1, + saved_objects: [], + }) + ); + expect( + authenticateAgentWithAccessToken(mockSavedObjectsClient, { + auth: { isAuthenticated: true }, + headers: { + authorization: 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw==', + }, + } as KibanaRequest) + ).rejects.toThrow(/Agent not found/); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/agents/authenticate.ts b/x-pack/plugins/ingest_manager/server/services/agents/authenticate.ts new file mode 100644 index 00000000000000..2515a02da4e781 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/agents/authenticate.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { KibanaRequest, SavedObjectsClientContract } from 'src/core/server'; +import { Agent } from '../../types'; +import * as APIKeyService from '../api_keys'; +import { getAgentByAccessAPIKeyId } from './crud'; + +export async function authenticateAgentWithAccessToken( + soClient: SavedObjectsClientContract, + request: KibanaRequest +): Promise { + if (!request.auth.isAuthenticated) { + throw Boom.unauthorized('Request not authenticated'); + } + let res: { apiKey: string; apiKeyId: string }; + try { + res = APIKeyService.parseApiKeyFromHeaders(request.headers); + } catch (err) { + throw Boom.unauthorized(err.message); + } + + const agent = await getAgentByAccessAPIKeyId(soClient, res.apiKeyId); + + return agent; +} diff --git a/x-pack/plugins/ingest_manager/server/services/agents/index.ts b/x-pack/plugins/ingest_manager/server/services/agents/index.ts index 257091af0ebd0a..400c099af4e936 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/index.ts @@ -14,3 +14,4 @@ export * from './crud'; export * from './update'; export * from './actions'; export * from './reassign'; +export * from './authenticate';