From 2c1f134a7e7afe25daed59d7bf39c4e909c2b529 Mon Sep 17 00:00:00 2001 From: Lee Drengenberg Date: Thu, 7 May 2020 20:53:43 +0000 Subject: [PATCH 1/5] [7.8] remove navigate from createIndexPattern and pass insertTimstamp to browser.get() (#65507) (#65733) --- test/functional/apps/discover/_discover_histogram.js | 2 +- test/functional/apps/getting_started/_shakespeare.js | 1 + test/functional/page_objects/common_page.ts | 4 ++-- test/functional/page_objects/settings_page.ts | 1 - x-pack/test/functional/apps/graph/graph.ts | 1 + .../test/functional/apps/security/doc_level_security_roles.js | 1 + 6 files changed, 6 insertions(+), 4 deletions(-) diff --git a/test/functional/apps/discover/_discover_histogram.js b/test/functional/apps/discover/_discover_histogram.js index 20e69ef8345c6f..0f63510dce431c 100644 --- a/test/functional/apps/discover/_discover_histogram.js +++ b/test/functional/apps/discover/_discover_histogram.js @@ -35,7 +35,7 @@ export default function({ getService, getPageObjects }) { describe('discover histogram', function describeIndexTests() { before(async function() { log.debug('load kibana index with default index pattern'); - await PageObjects.common.navigateToApp('home'); + await PageObjects.common.navigateToApp('settings'); await security.testUser.setRoles([ 'kibana_admin', 'test_logstash_reader', diff --git a/test/functional/apps/getting_started/_shakespeare.js b/test/functional/apps/getting_started/_shakespeare.js index 3a3d6b93e166bf..b0a572d9a54f99 100644 --- a/test/functional/apps/getting_started/_shakespeare.js +++ b/test/functional/apps/getting_started/_shakespeare.js @@ -58,6 +58,7 @@ export default function({ getService, getPageObjects }) { }); it('should create shakespeare index pattern', async function() { + await PageObjects.common.navigateToApp('settings'); log.debug('Create shakespeare index pattern'); await PageObjects.settings.createIndexPattern('shakespeare', null); const patternName = await PageObjects.settings.getIndexPageHeading(); diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index 33b84c31e76991..75de13f1cd4d00 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -111,7 +111,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo await browser.get(appUrl); } else { log.debug(`navigateToUrl ${appUrl}`); - await browser.get(appUrl); + await browser.get(appUrl, insertTimestamp); // accept alert if it pops up const alert = await browser.getAlert(); await alert?.accept(); @@ -242,7 +242,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo let lastUrl = await retry.try(async () => { // since we're using hash URLs, always reload first to force re-render log.debug('navigate to: ' + appUrl); - await browser.get(appUrl); + await browser.get(appUrl, insertTimestamp); // accept alert if it pops up const alert = await browser.getAlert(); await alert?.accept(); diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index b7a6e10efd7dc1..8175361ffc8420 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -324,7 +324,6 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider isStandardIndexPattern = true ) { await retry.try(async () => { - await this.navigateTo(); await PageObjects.header.waitUntilLoadingHasFinished(); await this.clickKibanaIndexPatterns(); await PageObjects.header.waitUntilLoadingHasFinished(); diff --git a/x-pack/test/functional/apps/graph/graph.ts b/x-pack/test/functional/apps/graph/graph.ts index fcf7298c5577ae..d8214dc5ffefd8 100644 --- a/x-pack/test/functional/apps/graph/graph.ts +++ b/x-pack/test/functional/apps/graph/graph.ts @@ -19,6 +19,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { log.debug('load graph/secrepo data'); await esArchiver.loadIfNeeded('graph/secrepo'); await esArchiver.load('empty_kibana'); + await PageObjects.common.navigateToApp('settings'); log.debug('create secrepo index pattern'); await PageObjects.settings.createIndexPattern('secrepo', '@timestamp'); log.debug('navigateTo graph'); diff --git a/x-pack/test/functional/apps/security/doc_level_security_roles.js b/x-pack/test/functional/apps/security/doc_level_security_roles.js index 09b133bab0d5af..8bc8ac24d0a7a1 100644 --- a/x-pack/test/functional/apps/security/doc_level_security_roles.js +++ b/x-pack/test/functional/apps/security/doc_level_security_roles.js @@ -21,6 +21,7 @@ export default function({ getService, getPageObjects }) { await esArchiver.loadIfNeeded('security/dlstest'); await browser.setWindowSize(1600, 1000); + await PageObjects.common.navigateToApp('settings'); await PageObjects.settings.createIndexPattern('dlstest', null); await PageObjects.settings.navigateTo(); From 09ee9df456b008907131123bcfae46d0030dbd49 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Thu, 7 May 2020 17:55:57 -0400 Subject: [PATCH 2/5] [Uptime] Add TLS alert functional test (#65303) (#65572) * Add alert option for flyout selection helper function. * Add functional test for TLS alerts. Co-authored-by: Elastic Machine --- .../test/functional/services/uptime/alerts.ts | 8 +- .../apps/uptime/alert_flyout.ts | 283 ++++++++++++------ 2 files changed, 190 insertions(+), 101 deletions(-) diff --git a/x-pack/test/functional/services/uptime/alerts.ts b/x-pack/test/functional/services/uptime/alerts.ts index dc10fcccaa6cef..c4f75b843d7816 100644 --- a/x-pack/test/functional/services/uptime/alerts.ts +++ b/x-pack/test/functional/services/uptime/alerts.ts @@ -11,10 +11,14 @@ export function UptimeAlertsProvider({ getService }: FtrProviderContext) { const browser = getService('browser'); return { - async openFlyout() { + async openFlyout(alertType: 'monitorStatus' | 'tls') { await testSubjects.click('xpack.uptime.alertsPopover.toggleButton', 5000); await testSubjects.click('xpack.uptime.openAlertContextPanel', 5000); - await testSubjects.click('xpack.uptime.toggleAlertFlyout', 5000); + if (alertType === 'monitorStatus') { + await testSubjects.click('xpack.uptime.toggleAlertFlyout', 5000); + } else if (alertType === 'tls') { + await testSubjects.click('xpack.uptime.toggleTlsAlertFlyout'); + } }, async openMonitorStatusAlertType(alertType: string) { return testSubjects.click(`xpack.uptime.alerts.${alertType}-SelectOption`, 5000); diff --git a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts index fb4f34d65f9b00..f1883733b02c9c 100644 --- a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts @@ -8,123 +8,208 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ getPageObjects, getService }: FtrProviderContext) => { - describe('overview page alert flyout controls', function() { - const DEFAULT_DATE_START = 'Sep 10, 2019 @ 12:40:08.078'; - const DEFAULT_DATE_END = 'Sep 11, 2019 @ 19:40:08.078'; + describe('uptime alerts', () => { const pageObjects = getPageObjects(['common', 'uptime']); const supertest = getService('supertest'); const retry = getService('retry'); - let alerts: any; - before(async () => { - alerts = getService('uptime').alerts; - }); + describe('overview page alert flyout controls', function() { + const DEFAULT_DATE_START = 'Sep 10, 2019 @ 12:40:08.078'; + const DEFAULT_DATE_END = 'Sep 11, 2019 @ 19:40:08.078'; + let alerts: any; - it('can open alert flyout', async () => { - await pageObjects.uptime.goToUptimeOverviewAndLoadData(DEFAULT_DATE_START, DEFAULT_DATE_END); - await alerts.openFlyout(); - }); + before(async () => { + alerts = getService('uptime').alerts; + }); - it('can set alert name', async () => { - await alerts.setAlertName('uptime-test'); - }); + it('can open alert flyout', async () => { + await pageObjects.uptime.goToUptimeOverviewAndLoadData( + DEFAULT_DATE_START, + DEFAULT_DATE_END + ); + await alerts.openFlyout('monitorStatus'); + }); - it('can set alert tags', async () => { - await alerts.setAlertTags(['uptime', 'another']); - }); + it('can set alert name', async () => { + await alerts.setAlertName('uptime-test'); + }); - it('can set alert interval', async () => { - await alerts.setAlertInterval('11'); - }); + it('can set alert tags', async () => { + await alerts.setAlertTags(['uptime', 'another']); + }); - it('can set alert throttle interval', async () => { - await alerts.setAlertThrottleInterval('30'); - }); + it('can set alert interval', async () => { + await alerts.setAlertInterval('11'); + }); - it('can set alert status number of time', async () => { - await alerts.setAlertStatusNumTimes('3'); - }); - it('can set alert time range', async () => { - await alerts.setAlertTimerangeSelection('1'); - }); - it('can set monitor hours', async () => { - await alerts.setMonitorStatusSelectableToHours(); - }); + it('can set alert throttle interval', async () => { + await alerts.setAlertThrottleInterval('30'); + }); - it('can set kuery bar filters', async () => { - await pageObjects.uptime.setAlertKueryBarText('monitor.id: "0001-up"'); - }); + it('can set alert status number of time', async () => { + await alerts.setAlertStatusNumTimes('3'); + }); - it('can select location filter', async () => { - await alerts.clickAddFilterLocation(); - await alerts.clickLocationExpression('mpls'); - }); + it('can set alert time range', async () => { + await alerts.setAlertTimerangeSelection('1'); + }); - it('can select port filter', async () => { - await alerts.clickAddFilterPort(); - await alerts.clickPortExpression('5678'); - }); + it('can set monitor hours', async () => { + await alerts.setMonitorStatusSelectableToHours(); + }); - it('can select type/scheme filter', async () => { - await alerts.clickAddFilterType(); - await alerts.clickTypeExpression('http'); - }); + it('can set kuery bar filters', async () => { + await pageObjects.uptime.setAlertKueryBarText('monitor.id: "0001-up"'); + }); + + it('can select location filter', async () => { + await alerts.clickAddFilterLocation(); + await alerts.clickLocationExpression('mpls'); + }); + + it('can select port filter', async () => { + await alerts.clickAddFilterPort(); + await alerts.clickPortExpression('5678'); + }); + + it('can select type/scheme filter', async () => { + await alerts.clickAddFilterType(); + await alerts.clickTypeExpression('http'); + }); + + it('can save alert', async () => { + await alerts.clickSaveAlertButton(); + }); - it('can save alert', async () => { - await alerts.clickSaveAlertButton(); + it('posts an alert, verifies its presence, and deletes the alert', async () => { + // The creation of the alert could take some time, so the first few times we query after + // the previous line resolves, the API may not be done creating the alert yet, so we + // put the fetch code in a retry block with a timeout. + let alert: any; + await retry.tryForTime(15000, async () => { + const apiResponse = await supertest.get('/api/alert/_find?search=uptime-test'); + const alertsFromThisTest = apiResponse.body.data.filter( + ({ name }: { name: string }) => name === 'uptime-test' + ); + expect(alertsFromThisTest).to.have.length(1); + alert = alertsFromThisTest[0]; + }); + + // Ensure the parameters and other stateful data + // on the alert match up with the values we provided + // for our test helper to input into the flyout. + const { + actions, + alertTypeId, + consumer, + id, + params: { numTimes, timerange, locations, filters }, + schedule: { interval }, + tags, + } = alert; + + try { + // we're not testing the flyout's ability to associate alerts with action connectors + expect(actions).to.eql([]); + + expect(alertTypeId).to.eql('xpack.uptime.alerts.monitorStatus'); + expect(consumer).to.eql('uptime'); + expect(interval).to.eql('11m'); + expect(tags).to.eql(['uptime', 'another']); + expect(numTimes).to.be(3); + expect(timerange.from).to.be('now-1h'); + expect(timerange.to).to.be('now'); + expect(locations).to.eql(['mpls']); + expect(filters).to.eql( + '{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"monitor.id":"0001-up"}}],' + + '"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match":{"observer.geo.name":"mpls"}}],' + + '"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match":{"url.port":5678}}],' + + '"minimum_should_match":1}},{"bool":{"should":[{"match":{"monitor.type":"http"}}],"minimum_should_match":1}}]}}]}}]}}' + ); + } finally { + await supertest + .delete(`/api/alert/${id}`) + .set('kbn-xsrf', 'true') + .expect(204); + } + }); }); - it('posts an alert, verifies its presence, and deletes the alert', async () => { - // The creation of the alert could take some time, so the first few times we query after - // the previous line resolves, the API may not be done creating the alert yet, so we - // put the fetch code in a retry block with a timeout. - let alert: any; - await retry.tryForTime(15000, async () => { - const apiResponse = await supertest.get('/api/alert/_find?search=uptime-test'); - const alertsFromThisTest = apiResponse.body.data.filter( - ({ name }: { name: string }) => name === 'uptime-test' - ); - expect(alertsFromThisTest).to.have.length(1); - alert = alertsFromThisTest[0]; - }); - - // Ensure the parameters and other stateful data - // on the alert match up with the values we provided - // for our test helper to input into the flyout. - const { - actions, - alertTypeId, - consumer, - id, - params: { numTimes, timerange, locations, filters }, - schedule: { interval }, - tags, - } = alert; - - try { - // we're not testing the flyout's ability to associate alerts with action connectors - expect(actions).to.eql([]); - - expect(alertTypeId).to.eql('xpack.uptime.alerts.monitorStatus'); - expect(consumer).to.eql('uptime'); - expect(interval).to.eql('11m'); - expect(tags).to.eql(['uptime', 'another']); - expect(numTimes).to.be(3); - expect(timerange.from).to.be('now-1h'); - expect(timerange.to).to.be('now'); - expect(locations).to.eql(['mpls']); - expect(filters).to.eql( - '{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"monitor.id":"0001-up"}}],' + - '"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match":{"observer.geo.name":"mpls"}}],' + - '"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match":{"url.port":5678}}],' + - '"minimum_should_match":1}},{"bool":{"should":[{"match":{"monitor.type":"http"}}],"minimum_should_match":1}}]}}]}}]}}' + describe('tls alert', function() { + const DEFAULT_DATE_START = 'Sep 10, 2019 @ 12:40:08.078'; + const DEFAULT_DATE_END = 'Sep 11, 2019 @ 19:40:08.078'; + let alerts: any; + const alertId = 'uptime-tls'; + + before(async () => { + alerts = getService('uptime').alerts; + }); + + it('can open alert flyout', async () => { + await pageObjects.uptime.goToUptimeOverviewAndLoadData( + DEFAULT_DATE_START, + DEFAULT_DATE_END ); - } finally { - await supertest - .delete(`/api/alert/${id}`) - .set('kbn-xsrf', 'true') - .expect(204); - } + await alerts.openFlyout('tls'); + }); + + it('can set alert name', async () => { + await alerts.setAlertName(alertId); + }); + + it('can set alert tags', async () => { + await alerts.setAlertTags(['uptime', 'certs']); + }); + + it('can set alert interval', async () => { + await alerts.setAlertInterval('11'); + }); + + it('can set alert throttle interval', async () => { + await alerts.setAlertThrottleInterval('30'); + }); + + it('can save alert', async () => { + await alerts.clickSaveAlertButton(); + }); + + it('has created a valid alert with expected parameters', async () => { + let alert: any; + await retry.tryForTime(15000, async () => { + const apiResponse = await supertest.get(`/api/alert/_find?search=${alertId}`); + const alertsFromThisTest = apiResponse.body.data.filter( + ({ name }: { name: string }) => name === alertId + ); + expect(alertsFromThisTest).to.have.length(1); + alert = alertsFromThisTest[0]; + }); + + // Ensure the parameters and other stateful data + // on the alert match up with the values we provided + // for our test helper to input into the flyout. + const { + actions, + alertTypeId, + consumer, + id, + params, + schedule: { interval }, + tags, + } = alert; + try { + expect(actions).to.eql([]); + expect(alertTypeId).to.eql('xpack.uptime.alerts.tls'); + expect(consumer).to.eql('uptime'); + expect(tags).to.eql(['uptime', 'certs']); + expect(params).to.eql({}); + expect(interval).to.eql('11m'); + } finally { + await supertest + .delete(`/api/alert/${id}`) + .set('kbn-xsrf', 'true') + .expect(204); + } + }); }); }); }; From 088d5384df53c5f4cea9a72c6d0d4f571f7a83f5 Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Thu, 7 May 2020 14:59:26 -0700 Subject: [PATCH 3/5] [Metrics UI] Fix isAbove to only display when threshold set (#65540) (#65760) --- .../alerting/metric_threshold/components/expression_chart.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx index a600d59865cccd..82e751627a05bd 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx @@ -224,7 +224,7 @@ export const ExpressionChart: React.FC = ({ /> ) : null} - {isAbove ? ( + {isAbove && first(expression.threshold) != null ? ( Date: Thu, 7 May 2020 15:20:42 -0700 Subject: [PATCH 4/5] [DOCS] Adds missing tagged sections in breaking changes (#65591) (#65784) --- docs/migration/migrate_7_2.asciidoc | 7 +++++++ docs/migration/migrate_7_5.asciidoc | 1 + docs/migration/migrate_7_6.asciidoc | 5 +++++ docs/migration/migrate_7_7.asciidoc | 12 ++++-------- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/docs/migration/migrate_7_2.asciidoc b/docs/migration/migrate_7_2.asciidoc index acb2275afabd3e..ac877f8514e64c 100644 --- a/docs/migration/migrate_7_2.asciidoc +++ b/docs/migration/migrate_7_2.asciidoc @@ -9,6 +9,13 @@ your application to Kibana 7.2. See also <> and <>. +//NOTE: The notable-breaking-changes tagged regions are re-used in the +//Installation and Upgrade Guide + +//tag::notable-breaking-changes[] + +// end::notable-breaking-changes[] + [float] [[breaking_72_index_pattern_changes]] diff --git a/docs/migration/migrate_7_5.asciidoc b/docs/migration/migrate_7_5.asciidoc index f01c352c69ee50..04e8de26297c59 100644 --- a/docs/migration/migrate_7_5.asciidoc +++ b/docs/migration/migrate_7_5.asciidoc @@ -45,3 +45,4 @@ Any installs that previously enabled the Code app will now log a warning when Kibana starts up. It's safe to remove all configurations starting with `xpack.code.`. Starting in 8.0, these warnings will become errors that prevent Kibana from starting up. +// end::notable-breaking-changes[] \ No newline at end of file diff --git a/docs/migration/migrate_7_6.asciidoc b/docs/migration/migrate_7_6.asciidoc index a44a3ca5418a37..00fb4074274cad 100644 --- a/docs/migration/migrate_7_6.asciidoc +++ b/docs/migration/migrate_7_6.asciidoc @@ -10,6 +10,11 @@ your application to Kibana 7.6. * <> * <> +// The following section is re-used in the Installation and Upgrade Guide +//tag::notable-breaking-changes[] + +// end::notable-breaking-changes[] + [float] [[user-facing-changes]] === Breaking changes for users diff --git a/docs/migration/migrate_7_7.asciidoc b/docs/migration/migrate_7_7.asciidoc index 0098e07e29a595..4cf52b3535c0b2 100644 --- a/docs/migration/migrate_7_7.asciidoc +++ b/docs/migration/migrate_7_7.asciidoc @@ -7,14 +7,10 @@ This page discusses the breaking changes that you need to be aware of when migrating your application to Kibana 7.7. -//NOTE: The notable-breaking-changes tagged regions are re-used in the -//Installation and Upgrade Guide - -//// -The following section is re-used in the Installation and Upgrade Guide -[[breaking_70_notable]] -=== Notable breaking changes -//// +// The following section is re-used in the Installation and Upgrade Guide +// tag::notable-breaking-changes[] + +// end::notable-breaking-changes[] [float] === Breaking changes for users From 667406a268f8f659cc0f55d51bcee6fca39d720c Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Thu, 7 May 2020 18:23:06 -0400 Subject: [PATCH 5/5] [Alerting] changes preconfigured actions config from array to object (#65397) (#65759) resolves https://github.com/elastic/kibana/issues/63171 Previously, preconfigured actions were specified as an array of action properties. This ended up being problematic when using the kibana keystore for secrets, as you'd have to reference specific actions via index. This changes preconfigured actions to be specified as an object, where the property key is the id, and the body is the remainder of the action properties. As access to preconfigured actions has leaked across the code base, it's probably time to consider changing the internal representation from an array to a Map, to provide easier access by action id. For a future PR. # Conflicts: # docs/user/alerting/action-types/email.asciidoc # docs/user/alerting/action-types/index.asciidoc # docs/user/alerting/action-types/pagerduty.asciidoc # docs/user/alerting/action-types/server-log.asciidoc # docs/user/alerting/action-types/slack.asciidoc # docs/user/alerting/action-types/webhook.asciidoc # docs/user/alerting/pre-configured-connectors.asciidoc --- .../pre-configured-connectors.asciidoc | 6 +- x-pack/plugins/actions/README.md | 2 +- x-pack/plugins/actions/server/config.test.ts | 53 ++++++++++++++--- x-pack/plugins/actions/server/config.ts | 39 +++++++++---- x-pack/plugins/actions/server/plugin.test.ts | 57 +++++++++---------- x-pack/plugins/actions/server/plugin.ts | 14 +++-- .../alerting_api_integration/common/config.ts | 16 ++---- x-pack/test/functional_with_es_ssl/config.ts | 10 ++-- 8 files changed, 121 insertions(+), 76 deletions(-) diff --git a/docs/user/alerting/pre-configured-connectors.asciidoc b/docs/user/alerting/pre-configured-connectors.asciidoc index 4c408da92f5791..fa54c0e25e0871 100644 --- a/docs/user/alerting/pre-configured-connectors.asciidoc +++ b/docs/user/alerting/pre-configured-connectors.asciidoc @@ -22,12 +22,12 @@ The following example shows a valid configuration 2 out-of-the box connector. ```js xpack.actions.preconfigured: - - id: 'my-slack1' <1> + my-slack1: <1> actionTypeId: .slack <2> name: 'Slack #xyz' <3> config: <4> webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz' - - id: 'webhook-service' + webhook-service: actionTypeId: .webhook name: 'Email service' config: @@ -41,7 +41,7 @@ The following example shows a valid configuration 2 out-of-the box connector. password: changeme ``` -<1> `id` is the action connector identifier. +<1> the key is the action connector identifier, eg `my-slack1` in this example. <2> `actionTypeId` is the action type identifier. <3> `name` is the name of the preconfigured connector. <4> `config` is the action type specific to the configuration. diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 4c8cc3aa503e6e..54624b94e0de35 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -98,7 +98,7 @@ Built-In-Actions are configured using the _xpack.actions_ namespoace under _kiba | _xpack.actions._**enabled** | Feature toggle which enabled Actions in Kibana. | boolean | | _xpack.actions._**whitelistedHosts** | Which _hostnames_ are whitelisted for the Built-In-Action? This list should contain hostnames of every external service you wish to interact with using Webhooks, Email or any other built in Action. Note that you may use the string "\*" in place of a specific hostname to enable Kibana to target any URL, but keep in mind the potential use of such a feature to execute [SSRF](https://www.owasp.org/index.php/Server_Side_Request_Forgery) attacks from your server. | Array | | _xpack.actions._**enabledActionTypes** | A list of _actionTypes_ id's that are enabled. A "\*" may be used as an element to indicate all registered actionTypes should be enabled. The actionTypes registered for Kibana are `.server-log`, `.slack`, `.email`, `.index`, `.pagerduty`, `.webhook`. Default: `["*"]` | Array | -| _xpack.actions._**preconfigured** | A list of preconfigured actions. Default: `[]` | Array | +| _xpack.actions._**preconfigured** | A object of action id / preconfigured actions. Default: `{}` | Array | #### Whitelisting Built-in Action Types diff --git a/x-pack/plugins/actions/server/config.test.ts b/x-pack/plugins/actions/server/config.test.ts index 161a6c31d4e599..e86f2d7832828e 100644 --- a/x-pack/plugins/actions/server/config.test.ts +++ b/x-pack/plugins/actions/server/config.test.ts @@ -14,7 +14,7 @@ describe('config validation', () => { "enabledActionTypes": Array [ "*", ], - "preconfigured": Array [], + "preconfigured": Object {}, "whitelistedHosts": Array [ "*", ], @@ -24,16 +24,15 @@ describe('config validation', () => { test('action with preconfigured actions', () => { const config: Record = { - preconfigured: [ - { - id: 'my-slack1', + preconfigured: { + mySlack1: { actionTypeId: '.slack', name: 'Slack #xyz', config: { webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', }, }, - ], + }, }; expect(configSchema.validate(config)).toMatchInlineSnapshot(` Object { @@ -41,21 +40,57 @@ describe('config validation', () => { "enabledActionTypes": Array [ "*", ], - "preconfigured": Array [ - Object { + "preconfigured": Object { + "mySlack1": Object { "actionTypeId": ".slack", "config": Object { "webhookUrl": "https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz", }, - "id": "my-slack1", "name": "Slack #xyz", "secrets": Object {}, }, - ], + }, "whitelistedHosts": Array [ "*", ], } `); }); + + test('validates preconfigured action ids', () => { + expect(() => + configSchema.validate(preConfiguredActionConfig('')) + ).toThrowErrorMatchingInlineSnapshot( + `"[preconfigured]: invalid preconfigured action id \\"\\""` + ); + + expect(() => + configSchema.validate(preConfiguredActionConfig('constructor')) + ).toThrowErrorMatchingInlineSnapshot( + `"[preconfigured]: invalid preconfigured action id \\"constructor\\""` + ); + + expect(() => + configSchema.validate(preConfiguredActionConfig('__proto__')) + ).toThrowErrorMatchingInlineSnapshot( + `"[preconfigured]: invalid preconfigured action id \\"__proto__\\""` + ); + }); }); + +// object creator that ensures we can create a property named __proto__ on an +// object, via JSON.parse() +function preConfiguredActionConfig(id: string) { + return JSON.parse(`{ + "preconfigured": { + ${JSON.stringify(id)}: { + "actionTypeId": ".server-log", + "name": "server log 1" + }, + "serverLog": { + "actionTypeId": ".server-log", + "name": "server log 2" + } + } + }`); +} diff --git a/x-pack/plugins/actions/server/config.ts b/x-pack/plugins/actions/server/config.ts index 1f04efd1941b4b..b2f3fa2680a9cc 100644 --- a/x-pack/plugins/actions/server/config.ts +++ b/x-pack/plugins/actions/server/config.ts @@ -7,6 +7,13 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { WhitelistedHosts, EnabledActionTypes } from './actions_config'; +const preconfiguredActionSchema = schema.object({ + name: schema.string({ minLength: 1 }), + actionTypeId: schema.string({ minLength: 1 }), + config: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), + secrets: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), +}); + export const configSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), whitelistedHosts: schema.arrayOf( @@ -21,18 +28,26 @@ export const configSchema = schema.object({ defaultValue: [WhitelistedHosts.Any], } ), - preconfigured: schema.arrayOf( - schema.object({ - id: schema.string({ minLength: 1 }), - name: schema.string(), - actionTypeId: schema.string({ minLength: 1 }), - config: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), - secrets: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), - }), - { - defaultValue: [], - } - ), + preconfigured: schema.recordOf(schema.string(), preconfiguredActionSchema, { + defaultValue: {}, + validate: validatePreconfigured, + }), }); export type ActionsConfig = TypeOf; + +const invalidActionIds = new Set(['', '__proto__', 'constructor']); + +function validatePreconfigured(preconfigured: Record): string | undefined { + // check for ids that should not be used + for (const id of Object.keys(preconfigured)) { + if (invalidActionIds.has(id)) { + return `invalid preconfigured action id "${id}"`; + } + } + + // in case __proto__ was used as a preconfigured action id ... + if (Object.getPrototypeOf(preconfigured) !== Object.getPrototypeOf({})) { + return `invalid preconfigured action id "__proto__"`; + } +} diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts index 2b334953063d18..8673d992ada983 100644 --- a/x-pack/plugins/actions/server/plugin.test.ts +++ b/x-pack/plugins/actions/server/plugin.test.ts @@ -12,6 +12,7 @@ import { taskManagerMock } from '../../task_manager/server/mocks'; import { eventLogMock } from '../../event_log/server/mocks'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { ActionType } from './types'; +import { ActionsConfig } from './config'; import { ActionsPlugin, ActionsPluginsSetup, @@ -31,33 +32,11 @@ describe('Actions Plugin', () => { let pluginsSetup: jest.Mocked; beforeEach(() => { - context = coreMock.createPluginInitializerContext({ - preconfigured: [ - { - id: 'my-slack1', - actionTypeId: '.slack', - name: 'Slack #xyz', - description: 'Send a message to the #xyz channel', - config: { - webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', - }, - }, - { - id: 'custom-system-abc-connector', - actionTypeId: 'system-abc-action-type', - description: 'Send a notification to system ABC', - name: 'System ABC', - config: { - xyzConfig1: 'value1', - xyzConfig2: 'value2', - listOfThings: ['a', 'b', 'c', 'd'], - }, - secrets: { - xyzSecret1: 'credential1', - xyzSecret2: 'credential2', - }, - }, - ], + context = coreMock.createPluginInitializerContext({ + enabled: true, + enabledActionTypes: ['*'], + whitelistedHosts: ['*'], + preconfigured: {}, }); plugin = new ActionsPlugin(context); coreSetup = coreMock.createSetup(); @@ -192,6 +171,7 @@ describe('Actions Plugin', () => { }); }); }); + describe('start()', () => { let plugin: ActionsPlugin; let coreSetup: ReturnType; @@ -200,8 +180,18 @@ describe('Actions Plugin', () => { let pluginsStart: jest.Mocked; beforeEach(() => { - const context = coreMock.createPluginInitializerContext({ - preconfigured: [], + const context = coreMock.createPluginInitializerContext({ + enabled: true, + enabledActionTypes: ['*'], + whitelistedHosts: ['*'], + preconfigured: { + preconfiguredServerLog: { + actionTypeId: '.server-log', + name: 'preconfigured-server-log', + config: {}, + secrets: {}, + }, + }, }); plugin = new ActionsPlugin(context); coreSetup = coreMock.createSetup(); @@ -220,6 +210,15 @@ describe('Actions Plugin', () => { }); describe('getActionsClientWithRequest()', () => { + it('should handle preconfigured actions', async () => { + // coreMock.createSetup doesn't support Plugin generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await plugin.setup(coreSetup as any, pluginsSetup); + const pluginStart = plugin.start(coreStart, pluginsStart); + + expect(pluginStart.isActionExecutable('preconfiguredServerLog', '.server-log')).toBe(true); + }); + it('should not throw error when ESO plugin not using a generated key', async () => { // coreMock.createSetup doesn't support Plugin generics // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index a6cc1fb5463bbb..b891249485a6df 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -150,12 +150,14 @@ export class ActionsPlugin implements Plugin, Plugi const actionsConfig = (await this.config) as ActionsConfig; const actionsConfigUtils = getActionsConfigurationUtilities(actionsConfig); - this.preconfiguredActions.push( - ...actionsConfig.preconfigured.map( - preconfiguredAction => - ({ ...preconfiguredAction, isPreconfigured: true } as PreConfiguredAction) - ) - ); + for (const preconfiguredId of Object.keys(actionsConfig.preconfigured)) { + this.preconfiguredActions.push({ + ...actionsConfig.preconfigured[preconfiguredId], + id: preconfiguredId, + isPreconfigured: true, + }); + } + const actionTypeRegistry = new ActionTypeRegistry({ taskRunnerFactory, taskManager: plugins.taskManager, diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 72a2774e672f1b..1e0860f44e68fa 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -77,17 +77,15 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) ])}`, `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, '--xpack.eventLog.logEntries=true', - `--xpack.actions.preconfigured=${JSON.stringify([ - { - id: 'my-slack1', + `--xpack.actions.preconfigured=${JSON.stringify({ + 'my-slack1': { actionTypeId: '.slack', name: 'Slack#xyz', config: { webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', }, }, - { - id: 'custom-system-abc-connector', + 'custom-system-abc-connector': { actionTypeId: 'system-abc-action-type', name: 'SystemABC', config: { @@ -100,8 +98,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) xyzSecret2: 'credential2', }, }, - { - id: 'preconfigured-es-index-action', + 'preconfigured-es-index-action': { actionTypeId: '.index', name: 'preconfigured_es_index_action', config: { @@ -110,8 +107,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) executionTimeField: 'timestamp', }, }, - { - id: 'preconfigured.test.index-record', + 'preconfigured.test.index-record': { actionTypeId: 'test.index-record', name: 'Test:_Preconfigured_Index_Record', config: { @@ -121,7 +117,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) encrypted: 'this-is-also-ignored-and-also-required', }, }, - ])}`, + })}`, ...disabledPlugins.map(key => `--xpack.${key}.enabled=false`), `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'alerts')}`, `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'actions_simulators')}`, diff --git a/x-pack/test/functional_with_es_ssl/config.ts b/x-pack/test/functional_with_es_ssl/config.ts index ef2270fb97745d..50de76d67e06b5 100644 --- a/x-pack/test/functional_with_es_ssl/config.ts +++ b/x-pack/test/functional_with_es_ssl/config.ts @@ -66,21 +66,19 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, `--plugin-path=${join(__dirname, 'fixtures', 'plugins', 'alerts')}`, `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, - `--xpack.actions.preconfigured=${JSON.stringify([ - { - id: 'my-slack1', + `--xpack.actions.preconfigured=${JSON.stringify({ + 'my-slack1': { actionTypeId: '.slack', name: 'Slack#xyztest', config: { webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', }, }, - { - id: 'my-server-log', + 'my-server-log': { actionTypeId: '.server-log', name: 'Serverlog#xyz', }, - ])}`, + })}`, ], }, };