diff --git a/.eslintrc.js b/.eslintrc.js index f94ad60159bfe..9f57ba4249e84 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1332,6 +1332,55 @@ module.exports = { }, }, + /** + * Platform Security Team overrides + */ + { + files: [ + 'src/plugins/security_oss/**/*.{js,mjs,ts,tsx}', + 'src/plugins/spaces_oss/**/*.{js,mjs,ts,tsx}', + 'x-pack/plugins/encrypted_saved_objects/**/*.{js,mjs,ts,tsx}', + 'x-pack/plugins/security/**/*.{js,mjs,ts,tsx}', + 'x-pack/plugins/spaces/**/*.{js,mjs,ts,tsx}', + ], + rules: { + '@typescript-eslint/consistent-type-imports': 1, + 'import/order': [ + // This rule sorts import declarations + 'error', + { + groups: [ + 'unknown', + ['builtin', 'external'], + 'internal', + ['parent', 'sibling', 'index'], + ], + pathGroups: [ + { + pattern: '{@kbn/**,src/**,kibana{,/**}}', + group: 'internal', + }, + ], + pathGroupsExcludedImportTypes: [], + alphabetize: { + order: 'asc', + caseInsensitive: true, + }, + 'newlines-between': 'always', + }, + ], + 'import/no-duplicates': ['error'], + 'sort-imports': [ + // This rule sorts imports of multiple members (destructured imports) + 'error', + { + ignoreCase: true, + ignoreDeclarationSort: true, + }, + ], + }, + }, + { files: [ // core-team owned code diff --git a/api_docs/actions.json b/api_docs/actions.json index 5d630dec25cbb..f4287abb1f470 100644 --- a/api_docs/actions.json +++ b/api_docs/actions.json @@ -647,7 +647,9 @@ "section": "def-server.ActionResult", "text": "ActionResult" }, - ">>; delete: ({ id }: { id: string; }) => Promise<{}>; create: ({ action: { actionTypeId, name, config, secrets }, }: CreateOptions) => Promise<", + ">>; delete: ({ id }: { id: string; }) => Promise<{}>; create: ({ action: { actionTypeId, name, config, secrets }, }: ", + "CreateOptions", + ") => Promise<", { "pluginId": "actions", "scope": "server", @@ -655,23 +657,15 @@ "section": "def-server.ActionResult", "text": "ActionResult" }, - ">>; update: ({ id, action }: UpdateOptions) => Promise<", + ">>; update: ({ id, action }: ", + "UpdateOptions", + ") => Promise<", { "pluginId": "actions", "scope": "server", "docId": "kibActionsPluginApi", "section": "def-server.ActionResult", "text": "ActionResult" - }, - ">>; execute: ({ actionId, params, source, }: Pick<", - "ExecuteOptions", - ", \"source\" | \"params\" | \"actionId\">) => Promise<", - { - "pluginId": "actions", - "scope": "common", - "docId": "kibActionsPluginApi", - "section": "def-common.ActionTypeExecutorResult", - "text": "ActionTypeExecutorResult" } ], "initialIsOpen": false @@ -1071,8 +1065,8 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 94, - "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L94" + "lineNumber": 86, + "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L86" } } ], @@ -1080,15 +1074,15 @@ "returnComment": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 88, - "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L88" + "lineNumber": 80, + "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L80" } } ], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 87, - "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L87" + "lineNumber": 79, + "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L79" }, "lifecycle": "setup", "initialIsOpen": true @@ -1119,8 +1113,8 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 99, - "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L99" + "lineNumber": 91, + "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L91" } }, { @@ -1138,15 +1132,15 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 99, - "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L99" + "lineNumber": 91, + "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L91" } } ], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 99, - "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L99" + "lineNumber": 91, + "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L91" } } ], @@ -1154,8 +1148,8 @@ "returnComment": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 99, - "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L99" + "lineNumber": 91, + "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L91" } }, { @@ -1177,8 +1171,8 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 101, - "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L101" + "lineNumber": 93, + "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L93" } }, { @@ -1191,8 +1185,8 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 102, - "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L102" + "lineNumber": 94, + "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L94" } }, { @@ -1210,15 +1204,15 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 103, - "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L103" + "lineNumber": 95, + "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L95" } } ], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 103, - "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L103" + "lineNumber": 95, + "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L95" } } ], @@ -1226,8 +1220,8 @@ "returnComment": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 100, - "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L100" + "lineNumber": 92, + "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L92" } }, { @@ -1272,8 +1266,8 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 105, - "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L105" + "lineNumber": 97, + "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L97" } } ], @@ -1281,8 +1275,8 @@ "returnComment": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 105, - "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L105" + "lineNumber": 97, + "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L97" } }, { @@ -1327,8 +1321,8 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 106, - "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L106" + "lineNumber": 98, + "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L98" } } ], @@ -1336,8 +1330,8 @@ "returnComment": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 106, - "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L106" + "lineNumber": 98, + "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L98" } }, { @@ -1348,8 +1342,8 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 107, - "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L107" + "lineNumber": 99, + "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L99" }, "signature": [ { @@ -1381,8 +1375,8 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 109, - "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L109" + "lineNumber": 101, + "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L101" } }, { @@ -1395,8 +1389,8 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 110, - "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L110" + "lineNumber": 102, + "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L102" } }, { @@ -1409,8 +1403,8 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 111, - "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L111" + "lineNumber": 103, + "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L103" } } ], @@ -1418,15 +1412,15 @@ "returnComment": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 108, - "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L108" + "lineNumber": 100, + "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L100" } } ], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 98, - "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L98" + "lineNumber": 90, + "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/actions/server/plugin.ts#L90" }, "lifecycle": "start", "initialIsOpen": true diff --git a/api_docs/dashboard.json b/api_docs/dashboard.json index 0df54e577ede6..778bd8b6da64a 100644 --- a/api_docs/dashboard.json +++ b/api_docs/dashboard.json @@ -1389,15 +1389,15 @@ "description": [], "source": { "path": "src/plugins/dashboard/public/plugin.tsx", - "lineNumber": 89, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/dashboard/public/plugin.tsx#L89" + "lineNumber": 90, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/dashboard/public/plugin.tsx#L90" } } ], "source": { "path": "src/plugins/dashboard/public/plugin.tsx", - "lineNumber": 88, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/dashboard/public/plugin.tsx#L88" + "lineNumber": 89, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/dashboard/public/plugin.tsx#L89" }, "initialIsOpen": false }, @@ -1939,8 +1939,8 @@ "description": [], "source": { "path": "src/plugins/dashboard/public/plugin.tsx", - "lineNumber": 86, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/dashboard/public/plugin.tsx#L86" + "lineNumber": 87, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/dashboard/public/plugin.tsx#L87" }, "signature": [ "UrlGeneratorContract<\"DASHBOARD_APP_URL_GENERATOR\">" @@ -2103,8 +2103,8 @@ "description": [], "source": { "path": "src/plugins/dashboard/public/plugin.tsx", - "lineNumber": 118, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/dashboard/public/plugin.tsx#L118" + "lineNumber": 120, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/dashboard/public/plugin.tsx#L120" }, "signature": [ "void" @@ -2127,8 +2127,8 @@ "description": [], "source": { "path": "src/plugins/dashboard/public/plugin.tsx", - "lineNumber": 121, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/dashboard/public/plugin.tsx#L121" + "lineNumber": 123, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/dashboard/public/plugin.tsx#L123" }, "signature": [ "() => ", @@ -2149,8 +2149,8 @@ "description": [], "source": { "path": "src/plugins/dashboard/public/plugin.tsx", - "lineNumber": 122, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/dashboard/public/plugin.tsx#L122" + "lineNumber": 124, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/dashboard/public/plugin.tsx#L124" }, "signature": [ { @@ -2171,8 +2171,8 @@ "description": [], "source": { "path": "src/plugins/dashboard/public/plugin.tsx", - "lineNumber": 123, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/dashboard/public/plugin.tsx#L123" + "lineNumber": 125, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/dashboard/public/plugin.tsx#L125" }, "signature": [ { @@ -2192,8 +2192,8 @@ "description": [], "source": { "path": "src/plugins/dashboard/public/plugin.tsx", - "lineNumber": 124, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/dashboard/public/plugin.tsx#L124" + "lineNumber": 126, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/dashboard/public/plugin.tsx#L126" }, "signature": [ "React.FC" @@ -2202,8 +2202,8 @@ ], "source": { "path": "src/plugins/dashboard/public/plugin.tsx", - "lineNumber": 120, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/dashboard/public/plugin.tsx#L120" + "lineNumber": 122, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/dashboard/public/plugin.tsx#L122" }, "lifecycle": "start", "initialIsOpen": true diff --git a/api_docs/data.json b/api_docs/data.json index ffcaad34c7261..82ab1accac717 100644 --- a/api_docs/data.json +++ b/api_docs/data.json @@ -5434,8 +5434,8 @@ ], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 259, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L259" + "lineNumber": 263, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L263" } }, { @@ -5461,8 +5461,8 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 290, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L290" + "lineNumber": 294, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L294" } }, { @@ -5482,8 +5482,8 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 301, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L301" + "lineNumber": 305, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L305" } }, { @@ -5509,8 +5509,8 @@ ], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 310, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L310" + "lineNumber": 314, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L314" } }, { @@ -5542,8 +5542,8 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 365, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L365" + "lineNumber": 369, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L369" } }, { @@ -5567,8 +5567,8 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 369, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L369" + "lineNumber": 373, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L373" } }, { @@ -5592,8 +5592,8 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 373, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L373" + "lineNumber": 377, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L377" } }, { @@ -5615,8 +5615,8 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 377, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L377" + "lineNumber": 381, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L381" } } ], @@ -5624,8 +5624,8 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 377, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L377" + "lineNumber": 381, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L381" } }, { @@ -5647,8 +5647,8 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 381, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L381" + "lineNumber": 385, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L385" } }, { @@ -5661,8 +5661,8 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 381, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L381" + "lineNumber": 385, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L385" } } ], @@ -5670,8 +5670,8 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 381, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L381" + "lineNumber": 385, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L385" } }, { @@ -5687,8 +5687,8 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 389, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L389" + "lineNumber": 393, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L393" } }, { @@ -5704,8 +5704,8 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 395, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L395" + "lineNumber": 399, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L399" } }, { @@ -5723,8 +5723,8 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 404, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L404" + "lineNumber": 408, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L408" } }, { @@ -5746,8 +5746,8 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 408, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L408" + "lineNumber": 412, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L412" } } ], @@ -5755,8 +5755,8 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 408, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L408" + "lineNumber": 412, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L412" } }, { @@ -5779,8 +5779,8 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 422, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L422" + "lineNumber": 426, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L426" } }, { @@ -5804,8 +5804,8 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 426, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L426" + "lineNumber": 430, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L430" } }, { @@ -5821,8 +5821,8 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 430, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L430" + "lineNumber": 434, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L434" } }, { @@ -5838,8 +5838,8 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 435, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L435" + "lineNumber": 439, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L439" } }, { @@ -5850,8 +5850,8 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 442, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L442" + "lineNumber": 446, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L446" }, "signature": [ { @@ -5871,8 +5871,8 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 446, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L446" + "lineNumber": 450, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L450" }, "signature": [ { @@ -5917,8 +5917,8 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 476, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L476" + "lineNumber": 480, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L480" } } ], @@ -5926,8 +5926,8 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 476, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L476" + "lineNumber": 480, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L480" } } ], diff --git a/api_docs/data_search.json b/api_docs/data_search.json index 4c0829dd181ce..f060343ecef7c 100644 --- a/api_docs/data_search.json +++ b/api_docs/data_search.json @@ -2876,8 +2876,8 @@ ], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 259, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L259" + "lineNumber": 263, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L263" } }, { @@ -2903,8 +2903,8 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 290, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L290" + "lineNumber": 294, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L294" } }, { @@ -2924,8 +2924,8 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 301, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L301" + "lineNumber": 305, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L305" } }, { @@ -2951,8 +2951,8 @@ ], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 310, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L310" + "lineNumber": 314, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L314" } }, { @@ -2984,8 +2984,8 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 365, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L365" + "lineNumber": 369, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L369" } }, { @@ -3009,8 +3009,8 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 369, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L369" + "lineNumber": 373, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L373" } }, { @@ -3034,8 +3034,8 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 373, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L373" + "lineNumber": 377, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L377" } }, { @@ -3057,8 +3057,8 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 377, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L377" + "lineNumber": 381, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L381" } } ], @@ -3066,8 +3066,8 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 377, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L377" + "lineNumber": 381, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L381" } }, { @@ -3089,8 +3089,8 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 381, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L381" + "lineNumber": 385, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L385" } }, { @@ -3103,8 +3103,8 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 381, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L381" + "lineNumber": 385, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L385" } } ], @@ -3112,8 +3112,8 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 381, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L381" + "lineNumber": 385, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L385" } }, { @@ -3129,8 +3129,8 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 389, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L389" + "lineNumber": 393, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L393" } }, { @@ -3146,8 +3146,8 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 395, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L395" + "lineNumber": 399, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L399" } }, { @@ -3165,8 +3165,8 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 404, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L404" + "lineNumber": 408, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L408" } }, { @@ -3188,8 +3188,8 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 408, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L408" + "lineNumber": 412, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L412" } } ], @@ -3197,8 +3197,8 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 408, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L408" + "lineNumber": 412, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L412" } }, { @@ -3221,8 +3221,8 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 422, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L422" + "lineNumber": 426, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L426" } }, { @@ -3246,8 +3246,8 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 426, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L426" + "lineNumber": 430, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L430" } }, { @@ -3263,8 +3263,8 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 430, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L430" + "lineNumber": 434, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L434" } }, { @@ -3280,8 +3280,8 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 435, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L435" + "lineNumber": 439, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L439" } }, { @@ -3292,8 +3292,8 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 442, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L442" + "lineNumber": 446, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L446" }, "signature": [ { @@ -3313,8 +3313,8 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 446, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L446" + "lineNumber": 450, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L450" }, "signature": [ { @@ -3359,8 +3359,8 @@ "description": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 476, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L476" + "lineNumber": 480, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L480" } } ], @@ -3368,8 +3368,8 @@ "returnComment": [], "source": { "path": "src/plugins/data/common/search/aggs/agg_config.ts", - "lineNumber": 476, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L476" + "lineNumber": 480, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/data/common/search/aggs/agg_config.ts#L480" } } ], diff --git a/api_docs/fleet.json b/api_docs/fleet.json index decb0e48a0170..58143beeeaca4 100644 --- a/api_docs/fleet.json +++ b/api_docs/fleet.json @@ -14114,7 +14114,7 @@ "link": "https://github.com/elastic/kibana/tree/masterx-pack/plugins/fleet/common/types/models/epm.ts#L234" }, "signature": [ - "\"text\" | \"password\" | \"integer\" | \"bool\" | \"yaml\"" + "\"string\" | \"text\" | \"password\" | \"integer\" | \"bool\" | \"yaml\"" ], "initialIsOpen": false }, diff --git a/api_docs/telemetry_collection_manager.json b/api_docs/telemetry_collection_manager.json index 0c2069228fe1f..e05635506568e 100644 --- a/api_docs/telemetry_collection_manager.json +++ b/api_docs/telemetry_collection_manager.json @@ -27,8 +27,8 @@ "description": [], "source": { "path": "src/plugins/telemetry_collection_manager/server/types.ts", - "lineNumber": 58, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L58" + "lineNumber": 57, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L57" }, "signature": [ "Pick<", @@ -50,8 +50,8 @@ "description": [], "source": { "path": "src/plugins/telemetry_collection_manager/server/types.ts", - "lineNumber": 59, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L59" + "lineNumber": 58, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L58" }, "signature": [ { @@ -66,13 +66,13 @@ { "tags": [], "id": "def-server.StatsCollectionConfig.soClient", - "type": "CompoundType", + "type": "Object", "label": "soClient", "description": [], "source": { "path": "src/plugins/telemetry_collection_manager/server/types.ts", - "lineNumber": 60, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L60" + "lineNumber": 59, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L59" }, "signature": [ "Pick<", @@ -83,15 +83,7 @@ "section": "def-server.SavedObjectsClient", "text": "SavedObjectsClient" }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\"> | Pick<", - { - "pluginId": "core", - "scope": "server", - "docId": "kibCoreSavedObjectsPluginApi", - "section": "def-server.SavedObjectsRepository", - "text": "SavedObjectsRepository" - }, - ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"deleteByNamespace\" | \"incrementCounter\">" + ", \"get\" | \"delete\" | \"create\" | \"find\" | \"update\" | \"bulkCreate\" | \"bulkGet\" | \"bulkUpdate\" | \"errors\" | \"checkConflicts\" | \"resolve\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"removeReferencesTo\" | \"openPointInTimeForType\" | \"closePointInTime\">" ] }, { @@ -102,8 +94,8 @@ "description": [], "source": { "path": "src/plugins/telemetry_collection_manager/server/types.ts", - "lineNumber": 61, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L61" + "lineNumber": 60, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L60" }, "signature": [ { @@ -119,8 +111,8 @@ ], "source": { "path": "src/plugins/telemetry_collection_manager/server/types.ts", - "lineNumber": 57, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L57" + "lineNumber": 56, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L56" }, "initialIsOpen": false }, @@ -139,8 +131,8 @@ "description": [], "source": { "path": "src/plugins/telemetry_collection_manager/server/types.ts", - "lineNumber": 79, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L79" + "lineNumber": 78, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L78" }, "signature": [ "Logger", @@ -155,15 +147,15 @@ "description": [], "source": { "path": "src/plugins/telemetry_collection_manager/server/types.ts", - "lineNumber": 80, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L80" + "lineNumber": 79, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L79" } } ], "source": { "path": "src/plugins/telemetry_collection_manager/server/types.ts", - "lineNumber": 78, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L78" + "lineNumber": 77, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L77" }, "initialIsOpen": false }, @@ -182,15 +174,15 @@ "description": [], "source": { "path": "src/plugins/telemetry_collection_manager/server/types.ts", - "lineNumber": 54, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L54" + "lineNumber": 53, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L53" } } ], "source": { "path": "src/plugins/telemetry_collection_manager/server/types.ts", - "lineNumber": 53, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L53" + "lineNumber": 52, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L52" }, "initialIsOpen": false }, @@ -214,15 +206,15 @@ "description": [], "source": { "path": "src/plugins/telemetry_collection_manager/server/types.ts", - "lineNumber": 75, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L75" + "lineNumber": 74, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L74" } } ], "source": { "path": "src/plugins/telemetry_collection_manager/server/types.ts", - "lineNumber": 74, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L74" + "lineNumber": 73, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L73" }, "initialIsOpen": false } @@ -237,8 +229,8 @@ "description": [], "source": { "path": "src/plugins/telemetry_collection_manager/server/types.ts", - "lineNumber": 88, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L88" + "lineNumber": 87, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L87" }, "signature": [ "(clustersDetails: ", @@ -259,8 +251,8 @@ "description": [], "source": { "path": "src/plugins/telemetry_collection_manager/server/types.ts", - "lineNumber": 83, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L83" + "lineNumber": 82, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L82" }, "signature": [ "UnencryptedStatsGetterConfig", @@ -277,8 +269,8 @@ "description": [], "source": { "path": "src/plugins/telemetry_collection_manager/server/types.ts", - "lineNumber": 84, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L84" + "lineNumber": 83, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L83" }, "signature": [ "(config: ", @@ -308,8 +300,8 @@ "description": [], "source": { "path": "src/plugins/telemetry_collection_manager/server/types.ts", - "lineNumber": 20, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L20" + "lineNumber": 19, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L19" }, "signature": [ " Promise" @@ -403,8 +395,8 @@ ], "source": { "path": "src/plugins/telemetry_collection_manager/server/types.ts", - "lineNumber": 19, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L19" + "lineNumber": 18, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L18" }, "lifecycle": "setup", "initialIsOpen": true @@ -424,8 +416,8 @@ "description": [], "source": { "path": "src/plugins/telemetry_collection_manager/server/types.ts", - "lineNumber": 29, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L29" + "lineNumber": 28, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L28" }, "signature": [ "(optInStatus: boolean, config: ", @@ -447,8 +439,8 @@ "description": [], "source": { "path": "src/plugins/telemetry_collection_manager/server/types.ts", - "lineNumber": 30, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L30" + "lineNumber": 29, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L29" }, "signature": [ "(config: ", @@ -478,8 +470,8 @@ "description": [], "source": { "path": "src/plugins/telemetry_collection_manager/server/types.ts", - "lineNumber": 31, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L31" + "lineNumber": 30, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L30" }, "signature": [ "() => Promise" @@ -488,8 +480,8 @@ ], "source": { "path": "src/plugins/telemetry_collection_manager/server/types.ts", - "lineNumber": 28, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L28" + "lineNumber": 27, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/telemetry_collection_manager/server/types.ts#L27" }, "lifecycle": "start", "initialIsOpen": true diff --git a/api_docs/usage_collection.json b/api_docs/usage_collection.json index 113c0776ab1ef..d2af1d5b5fb2c 100644 --- a/api_docs/usage_collection.json +++ b/api_docs/usage_collection.json @@ -182,8 +182,8 @@ "description": [], "source": { "path": "src/plugins/usage_collection/server/collector/collector.ts", - "lineNumber": 145, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/usage_collection/server/collector/collector.ts#L145" + "lineNumber": 144, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/usage_collection/server/collector/collector.ts#L144" }, "signature": [ { @@ -204,8 +204,8 @@ "description": [], "source": { "path": "src/plugins/usage_collection/server/collector/collector.ts", - "lineNumber": 146, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/usage_collection/server/collector/collector.ts#L146" + "lineNumber": 145, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/usage_collection/server/collector/collector.ts#L145" } }, { @@ -216,8 +216,8 @@ "description": [], "source": { "path": "src/plugins/usage_collection/server/collector/collector.ts", - "lineNumber": 147, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/usage_collection/server/collector/collector.ts#L147" + "lineNumber": 146, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/usage_collection/server/collector/collector.ts#L146" }, "signature": [ "Function | undefined" @@ -231,8 +231,8 @@ "description": [], "source": { "path": "src/plugins/usage_collection/server/collector/collector.ts", - "lineNumber": 148, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/usage_collection/server/collector/collector.ts#L148" + "lineNumber": 147, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/usage_collection/server/collector/collector.ts#L147" }, "signature": [ { @@ -253,8 +253,8 @@ "description": [], "source": { "path": "src/plugins/usage_collection/server/collector/collector.ts", - "lineNumber": 149, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/usage_collection/server/collector/collector.ts#L149" + "lineNumber": 148, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/usage_collection/server/collector/collector.ts#L148" }, "signature": [ "() => boolean | Promise" @@ -279,8 +279,8 @@ "description": [], "source": { "path": "src/plugins/usage_collection/server/collector/collector.ts", - "lineNumber": 156, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/usage_collection/server/collector/collector.ts#L156" + "lineNumber": 155, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/usage_collection/server/collector/collector.ts#L155" } }, { @@ -300,8 +300,8 @@ "description": [], "source": { "path": "src/plugins/usage_collection/server/collector/collector.ts", - "lineNumber": 157, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/usage_collection/server/collector/collector.ts#L157" + "lineNumber": 156, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/usage_collection/server/collector/collector.ts#L156" } } ], @@ -313,15 +313,15 @@ "returnComment": [], "source": { "path": "src/plugins/usage_collection/server/collector/collector.ts", - "lineNumber": 155, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/usage_collection/server/collector/collector.ts#L155" + "lineNumber": 154, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/usage_collection/server/collector/collector.ts#L154" } } ], "source": { "path": "src/plugins/usage_collection/server/collector/collector.ts", - "lineNumber": 144, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/usage_collection/server/collector/collector.ts#L144" + "lineNumber": 143, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/usage_collection/server/collector/collector.ts#L143" }, "initialIsOpen": false } @@ -343,15 +343,15 @@ "description": [], "source": { "path": "src/plugins/usage_collection/server/collector/collector.ts", - "lineNumber": 34, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/usage_collection/server/collector/collector.ts#L34" + "lineNumber": 33, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/usage_collection/server/collector/collector.ts#L33" } } ], "source": { "path": "src/plugins/usage_collection/server/collector/collector.ts", - "lineNumber": 33, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/usage_collection/server/collector/collector.ts#L33" + "lineNumber": 32, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/usage_collection/server/collector/collector.ts#L32" }, "initialIsOpen": false } @@ -366,8 +366,8 @@ "description": [], "source": { "path": "src/plugins/usage_collection/server/collector/collector.ts", - "lineNumber": 28, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/usage_collection/server/collector/collector.ts#L28" + "lineNumber": 27, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/usage_collection/server/collector/collector.ts#L27" }, "signature": [ "\"boolean\" | \"date\" | \"text\" | \"keyword\" | \"long\" | \"double\" | \"short\" | \"integer\" | \"byte\" | \"float\"" @@ -382,8 +382,8 @@ "description": [], "source": { "path": "src/plugins/usage_collection/server/collector/collector.ts", - "lineNumber": 51, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/usage_collection/server/collector/collector.ts#L51" + "lineNumber": 50, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/usage_collection/server/collector/collector.ts#L50" }, "signature": [ "{ [Key in keyof Required]: Required[Key] extends (infer U)[] ? { type: 'array'; items: RecursiveMakeSchemaFrom; } : RecursiveMakeSchemaFrom[Key]>; }" @@ -398,8 +398,8 @@ "description": [], "source": { "path": "src/plugins/usage_collection/server/collector/collector.ts", - "lineNumber": 112, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/usage_collection/server/collector/collector.ts#L112" + "lineNumber": 111, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/usage_collection/server/collector/collector.ts#L111" }, "signature": [ "{ type: string; init?: Function | undefined; isReady: () => Promise | boolean; schema?: MakeSchemaFrom | undefined; fetch: CollectorFetchMethod; } & ExtraOptions & (WithKibanaRequest extends true ? { extendFetchContext: CollectorOptionsFetchExtendedContext; } : { extendFetchContext?: CollectorOptionsFetchExtendedContext | undefined; })" @@ -448,11 +448,11 @@ ], "source": { "path": "src/plugins/usage_collection/server/collector/collector.ts", - "lineNumber": 65, - "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/usage_collection/server/collector/collector.ts#L65" + "lineNumber": 64, + "link": "https://github.com/elastic/kibana/tree/mastersrc/plugins/usage_collection/server/collector/collector.ts#L64" }, "signature": [ - "{ esClient: ElasticsearchClient; soClient: SavedObjectsClientContract | ISavedObjectsRepository; } & (WithKibanaRequest extends true ? { kibanaRequest?: KibanaRequest | undefined; } : {})" + "{ esClient: ElasticsearchClient; soClient: SavedObjectsClientContract; } & (WithKibanaRequest extends true ? { kibanaRequest?: KibanaRequest | undefined; } : {})" ], "initialIsOpen": false }, diff --git a/docs/api/actions-and-connectors.asciidoc b/docs/api/actions-and-connectors.asciidoc index 17e7ea1b7672a..5480cdd57f691 100644 --- a/docs/api/actions-and-connectors.asciidoc +++ b/docs/api/actions-and-connectors.asciidoc @@ -3,21 +3,23 @@ Manage Actions and Connectors. -The following action APIs are available: +The following connector APIs are available: -* <> to retrieve a single action by ID +* <> to retrieve a single connector by ID -* <> to retrieve all actions +* <> to retrieve all connectors -* <> to retrieve a list of all action types +* <> to retrieve a list of all connector types -* <> to create actions +* <> to create connectors -* <> to update the attributes for an existing action +* <> to update the attributes for an existing connector -* <> to execute an action by ID +* <> to execute a connector by ID -* <> to delete an action by ID +* <> to delete a connector by ID + +For deprecated APIs, refer to <>. For information about the actions and connectors that {kib} supports, refer to <>. @@ -28,3 +30,11 @@ include::actions-and-connectors/create.asciidoc[] include::actions-and-connectors/update.asciidoc[] include::actions-and-connectors/execute.asciidoc[] include::actions-and-connectors/delete.asciidoc[] +include::actions-and-connectors/legacy/index.asciidoc[] +include::actions-and-connectors/legacy/get.asciidoc[] +include::actions-and-connectors/legacy/get_all.asciidoc[] +include::actions-and-connectors/legacy/list.asciidoc[] +include::actions-and-connectors/legacy/create.asciidoc[] +include::actions-and-connectors/legacy/update.asciidoc[] +include::actions-and-connectors/legacy/execute.asciidoc[] +include::actions-and-connectors/legacy/delete.asciidoc[] diff --git a/docs/api/actions-and-connectors/create.asciidoc b/docs/api/actions-and-connectors/create.asciidoc index 230dad22d3bed..c9a09e890ea6d 100644 --- a/docs/api/actions-and-connectors/create.asciidoc +++ b/docs/api/actions-and-connectors/create.asciidoc @@ -1,17 +1,17 @@ [[actions-and-connectors-api-create]] -=== Create action API +=== Create connector API ++++ -Create action API +Create connector API ++++ -Creates an action. +Creates a connector. [[actions-and-connectors-api-create-request]] ==== Request -`POST :/api/actions/action` +`POST :/api/actions/connector` -`POST :/s//api/actions/action` +`POST :/s//api/actions/connector` [[actions-and-connectors-api-create-path-params]] ==== Path parameters @@ -23,18 +23,18 @@ Creates an action. ==== Request body `name`:: - (Required, string) The display name for the action. + (Required, string) The display name for the connector. -`actionTypeId`:: - (Required, string) The action type ID for the action. +`connector_type_id`:: + (Required, string) The connector type ID for the connector. `config`:: - (Required, object) The configuration for the action. Configuration properties vary depending on - the action type. For information about the configuration properties, refer to <>. + (Required, object) The configuration for the connector. Configuration properties vary depending on + the connector type. For information about the configuration properties, refer to <>. `secrets`:: - (Required, object) The secrets configuration for the action. Secrets configuration properties vary - depending on the action type. For information about the secrets configuration properties, refer to <>. + (Required, object) The secrets configuration for the connector. Secrets configuration properties vary + depending on the connector type. For information about the secrets configuration properties, refer to <>. + WARNING: Remember these values. You must provide them each time you call the <> API. @@ -49,10 +49,10 @@ WARNING: Remember these values. You must provide them each time you call the <Delete action API +Delete connector API ++++ -Deletes an action by ID. +Deletes an connector by ID. -WARNING: When you delete an action, _it cannot be recovered_. +WARNING: When you delete a connector, _it cannot be recovered_. [[actions-and-connectors-api-delete-request]] ==== Request -`DELETE :/api/actions/action/` +`DELETE :/api/actions/connector/` -`DELETE :/s//api/actions/action/` +`DELETE :/s//api/actions/connector/` [[actions-and-connectors-api-delete-path-params]] ==== Path parameters `id`:: - (Required, string) The ID of the action. + (Required, string) The ID of the connector. `space_id`:: (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. @@ -34,6 +34,6 @@ WARNING: When you delete an action, _it cannot be recovered_. [source,sh] -------------------------------------------------- -$ curl -X DELETE api/actions/action/c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad +$ curl -X DELETE api/actions/connector/c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad -------------------------------------------------- // KIBANA diff --git a/docs/api/actions-and-connectors/execute.asciidoc b/docs/api/actions-and-connectors/execute.asciidoc index 05a27988578ff..b87380907f7bb 100644 --- a/docs/api/actions-and-connectors/execute.asciidoc +++ b/docs/api/actions-and-connectors/execute.asciidoc @@ -1,23 +1,23 @@ [[actions-and-connectors-api-execute]] -=== Execute action API +=== Execute connector API ++++ -Execute action API +Execute connector API ++++ -Executes an action by ID. +Executes a connector by ID. [[actions-and-connectors-api-execute-request]] ==== Request -`POST :/api/actions/action//_execute` +`POST :/api/actions/connector//_execute` -`POST :/s//api/actions/action//_execute` +`POST :/s//api/actions/connector//_execute` [[actions-and-connectors-api-execute-params]] ==== Path parameters `id`:: - (Required, string) The ID of the action. + (Required, string) The ID of the connector. `space_id`:: (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. @@ -26,8 +26,8 @@ Executes an action by ID. ==== Request body `params`:: - (Required, object) The parameters of the action. Parameter properties vary depending on - the action type. For information about the parameter properties, refer to <>. + (Required, object) The parameters of the connector. Parameter properties vary depending on + the connector type. For information about the parameter properties, refer to <>. [[actions-and-connectors-api-execute-codes]] ==== Response code @@ -40,7 +40,7 @@ Executes an action by ID. [source,sh] -------------------------------------------------- -$ curl -X POST api/actions/action/c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad/_execute -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -d ' +$ curl -X POST api/actions/connector/c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad/_execute -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -d ' { "params": { "documents": [ @@ -83,6 +83,6 @@ The API returns the following: } ] }, - "actionId": "c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad" + "connector_id": "c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad" } -------------------------------------------------- diff --git a/docs/api/actions-and-connectors/get.asciidoc b/docs/api/actions-and-connectors/get.asciidoc index 51af187257d42..33d37a4add4dd 100644 --- a/docs/api/actions-and-connectors/get.asciidoc +++ b/docs/api/actions-and-connectors/get.asciidoc @@ -1,23 +1,23 @@ [[actions-and-connectors-api-get]] -=== Get action API +=== Get connector API ++++ -Get action API +Get connector API ++++ -Retrieves an action by ID. +Retrieves a connector by ID. [[actions-and-connectors-api-get-request]] ==== Request -`GET :/api/actions/action/` +`GET :/api/actions/connector/` -`GET :/s//api/actions/action/` +`GET :/s//api/actions/connector/` [[actions-and-connectors-api-get-params]] ==== Path parameters `id`:: - (Required, string) The ID of the action. + (Required, string) The ID of the connector. `space_id`:: (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. @@ -33,7 +33,7 @@ Retrieves an action by ID. [source,sh] -------------------------------------------------- -$ curl -X GET api/actions/action/c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad +$ curl -X GET api/actions/connector/c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad -------------------------------------------------- // KIBANA @@ -43,13 +43,13 @@ The API returns the following: -------------------------------------------------- { "id": "c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad", - "actionTypeId": ".index", - "name": "my-action", + "connector_type_id": ".index", + "name": "my-connector", "config": { "index": "test-index", "refresh": false, "executionTimeField": null }, - "isPreconfigured": false + "is_preconfigured": false } -------------------------------------------------- diff --git a/docs/api/actions-and-connectors/get_all.asciidoc b/docs/api/actions-and-connectors/get_all.asciidoc index 7a8025d0d215e..8b4977d61e741 100644 --- a/docs/api/actions-and-connectors/get_all.asciidoc +++ b/docs/api/actions-and-connectors/get_all.asciidoc @@ -4,14 +4,14 @@ Get all actions API ++++ -Retrieves all actions. +Retrieves all connectors. [[actions-and-connectors-api-get-all-request]] ==== Request -`GET :/api/actions` +`GET :/api/actions/connectors` -`GET :/s//api/actions` +`GET :/s//api/actions/connectors` [[actions-and-connectors-api-get-all-path-params]] ==== Path parameters @@ -30,7 +30,7 @@ Retrieves all actions. [source,sh] -------------------------------------------------- -$ curl -X GET api/actions +$ curl -X GET api/actions/connectors -------------------------------------------------- // KIBANA @@ -40,21 +40,23 @@ The API returns the following: -------------------------------------------------- [ { - "id": "preconfigured-mail-action", - "actionTypeId": ".email", - "name": "email: preconfigured-mail-action", - "isPreconfigured": true + "id": "preconfigured-mail-connector", + "connector_type_id": ".email", + "name": "email: preconfigured-mail-connector", + "is_preconfigured": true, + "referenced_by_count": 1 }, { "id": "c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad", - "actionTypeId": ".index", - "name": "my-action", + "connector_type_id": ".index", + "name": "my-connector", "config": { "index": "test-index", "refresh": false, "executionTimeField": null }, - "isPreconfigured": false + "is_preconfigured": false, + "referenced_by_count": 3 } ] -------------------------------------------------- diff --git a/docs/api/actions-and-connectors/legacy/create.asciidoc b/docs/api/actions-and-connectors/legacy/create.asciidoc new file mode 100644 index 0000000000000..faf6227f01947 --- /dev/null +++ b/docs/api/actions-and-connectors/legacy/create.asciidoc @@ -0,0 +1,82 @@ +[[actions-and-connectors-legacy-api-create]] +==== Legacy Create connector API +++++ +Legacy Create connector API +++++ + +deprecated::[7.13.0] + +Please use the <> instead. + +Creates a connector. + +[[actions-and-connectors-legacy-api-create-request]] +===== Request + +`POST :/api/actions/action` + +`POST :/s//api/actions/action` + +[[actions-and-connectors-legacy-api-create-path-params]] +===== Path parameters + +`space_id`:: + (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +[[actions-and-connectors-legacy-api-create-request-body]] +===== Request body + +`name`:: + (Required, string) The display name for the connector. + +`actionTypeId`:: + (Required, string) The connector type ID for the connector. + +`config`:: + (Required, object) The configuration for the connector. Configuration properties vary depending on + the connector type. For information about the configuration properties, refer to <>. + +`secrets`:: + (Required, object) The secrets configuration for the connector. Secrets configuration properties vary + depending on the connector type. For information about the secrets configuration properties, refer to <>. ++ +WARNING: Remember these values. You must provide them each time you call the <> API. + +[[actions-and-connectors-legacy-api-create-request-codes]] +===== Response code + +`200`:: + Indicates a successful call. + +[[actions-and-connectors-legacy-api-create-example]] +===== Example + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/actions/action -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -d ' +{ + "name": "my-connector", + "actionTypeId": ".index", + "config": { + "index": "test-index" + } +}' +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{ + "id": "c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad", + "actionTypeId": ".index", + "name": "my-connector", + "config": { + "index": "test-index", + "refresh": false, + "executionTimeField": null + }, + "isPreconfigured": false +} +-------------------------------------------------- diff --git a/docs/api/actions-and-connectors/legacy/delete.asciidoc b/docs/api/actions-and-connectors/legacy/delete.asciidoc new file mode 100644 index 0000000000000..b02f1011fd9b4 --- /dev/null +++ b/docs/api/actions-and-connectors/legacy/delete.asciidoc @@ -0,0 +1,43 @@ +[[actions-and-connectors-legacy-api-delete]] +==== Legacy Delete connector API +++++ +Legacy Delete connector API +++++ + +deprecated::[7.13.0] + +Please use the <> instead. + +Deletes a connector by ID. + +WARNING: When you delete an connector, _it cannot be recovered_. + +[[actions-and-connectors-legacy-api-delete-request]] +===== Request + +`DELETE :/api/actions/action/` + +`DELETE :/s//api/actions/action/` + +[[actions-and-connectors-legacy-api-delete-path-params]] +===== Path parameters + +`id`:: + (Required, string) The ID of the connector. + +`space_id`:: + (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +[[actions-and-connectors-legacy-api-delete-response-codes]] +===== Response code + +`200`:: + Indicates a successful call. + +===== Example + +[source,sh] +-------------------------------------------------- +$ curl -X DELETE api/actions/action/c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad +-------------------------------------------------- +// KIBANA diff --git a/docs/api/actions-and-connectors/legacy/execute.asciidoc b/docs/api/actions-and-connectors/legacy/execute.asciidoc new file mode 100644 index 0000000000000..30cb18c54aa69 --- /dev/null +++ b/docs/api/actions-and-connectors/legacy/execute.asciidoc @@ -0,0 +1,92 @@ +[[actions-and-connectors-legacy-api-execute]] +==== Legacy Execute connector API +++++ +Legacy Execute connector API +++++ + +deprecated::[7.13.0] + +Please use the <> instead. + +Executes a connector by ID. + +[[actions-and-connectors-legacy-api-execute-request]] +===== Request + +`POST :/api/actions/action//_execute` + +`POST :/s//api/actions/action//_execute` + +[[actions-and-connectors-legacy-api-execute-params]] +===== Path parameters + +`id`:: + (Required, string) The ID of the connector. + +`space_id`:: + (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +[[actions-and-connectors-legacy-api-execute-request-body]] +===== Request body + +`params`:: + (Required, object) The parameters of the connector. Parameter properties vary depending on + the connector type. For information about the parameter properties, refer to <>. + +[[actions-and-connectors-legacy-api-execute-codes]] +===== Response code + +`200`:: + Indicates a successful call. + +[[actions-and-connectors-legacy-api-execute-example]] +===== Example + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/actions/action/c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad/_execute -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -d ' +{ + "params": { + "documents": [ + { + "id": "test_doc_id", + "name": "test_doc_name", + "message": "hello, world" + } + ] + } +}' +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{ + "status": "ok", + "data": { + "took": 197, + "errors": false, + "items": [ + { + "index": { + "_index": "updated-index", + "_id": "iKyijHcBKCsmXNFrQe3T", + "_version": 1, + "result": "created", + "_shards": { + "total": 2, + "successful": 1, + "failed": 0 + }, + "_seq_no": 0, + "_primary_term": 1, + "status": 201 + } + } + ] + }, + "actionId": "c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad" +} +-------------------------------------------------- diff --git a/docs/api/actions-and-connectors/legacy/get.asciidoc b/docs/api/actions-and-connectors/legacy/get.asciidoc new file mode 100644 index 0000000000000..cf8cc1b6b677e --- /dev/null +++ b/docs/api/actions-and-connectors/legacy/get.asciidoc @@ -0,0 +1,59 @@ +[[actions-and-connectors-legacy-api-get]] +==== Legacy Get connector API +++++ +Legacy Get connector API +++++ + +deprecated::[7.13.0] + +Please use the <> instead. + +Retrieves a connector by ID. + +[[actions-and-connectors-legacy-api-get-request]] +===== Request + +`GET :/api/actions/action/` + +`GET :/s//api/actions/action/` + +[[actions-and-connectors-legacy-api-get-params]] +===== Path parameters + +`id`:: + (Required, string) The ID of the action. + +`space_id`:: + (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +[[actions-and-connectors-legacy-api-get-codes]] +===== Response code + +`200`:: + Indicates a successful call. + +[[actions-and-connectors-legacy-api-get-example]] +===== Example + +[source,sh] +-------------------------------------------------- +$ curl -X GET api/actions/action/c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{ + "id": "c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad", + "actionTypeId": ".index", + "name": "my-connector", + "config": { + "index": "test-index", + "refresh": false, + "executionTimeField": null + }, + "isPreconfigured": false +} +-------------------------------------------------- diff --git a/docs/api/actions-and-connectors/legacy/get_all.asciidoc b/docs/api/actions-and-connectors/legacy/get_all.asciidoc new file mode 100644 index 0000000000000..24ad446d95d95 --- /dev/null +++ b/docs/api/actions-and-connectors/legacy/get_all.asciidoc @@ -0,0 +1,64 @@ +[[actions-and-connectors-legacy-api-get-all]] +==== Legacy Get all connector API +++++ +Legacy Get all connector API +++++ + +deprecated::[7.13.0] + +Please use the <> instead. + +Retrieves all connectors. + +[[actions-and-connectors-legacy-api-get-all-request]] +===== Request + +`GET :/api/actions` + +`GET :/s//api/actions` + +[[actions-and-connectors-legacy-api-get-all-path-params]] +===== Path parameters + +`space_id`:: + (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +[[actions-and-connectors-legacy-api-get-all-codes]] +===== Response code + +`200`:: + Indicates a successful call. + +[[actions-and-connectors-legacy-api-get-all-example]] +===== Example + +[source,sh] +-------------------------------------------------- +$ curl -X GET api/actions +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +[ + { + "id": "preconfigured-mail-action", + "actionTypeId": ".email", + "name": "email: preconfigured-mail-action", + "isPreconfigured": true + }, + { + "id": "c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad", + "actionTypeId": ".index", + "name": "my-action", + "config": { + "index": "test-index", + "refresh": false, + "executionTimeField": null + }, + "isPreconfigured": false + } +] +-------------------------------------------------- diff --git a/docs/api/actions-and-connectors/legacy/index.asciidoc b/docs/api/actions-and-connectors/legacy/index.asciidoc new file mode 100644 index 0000000000000..859dd652de984 --- /dev/null +++ b/docs/api/actions-and-connectors/legacy/index.asciidoc @@ -0,0 +1,4 @@ +[[actions-and-connectors-legacy-apis]] +=== Deprecated 7.x APIs + +These APIs are deprecated and will be removed as of 8.0. diff --git a/docs/api/actions-and-connectors/legacy/list.asciidoc b/docs/api/actions-and-connectors/legacy/list.asciidoc new file mode 100644 index 0000000000000..86026f332d917 --- /dev/null +++ b/docs/api/actions-and-connectors/legacy/list.asciidoc @@ -0,0 +1,71 @@ +[[actions-and-connectors-legacy-api-list]] +==== Legacy List connector types API +++++ +Legacy List all connector types API +++++ + +deprecated::[7.13.0] + +Please use the <> instead. + +Retrieves a list of all connector types. + +[[actions-and-connectors-legacy-api-list-request]] +===== Request + +`GET :/api/actions/list_action_types` + +`GET :/s//api/actions/list_action_types` + +[[actions-and-connectors-legacy-api-list-path-params]] +===== Path parameters + +`space_id`:: + (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +[[actions-and-connectors-legacy-api-list-codes]] +===== Response code + +`200`:: + Indicates a successful call. + +[[actions-and-connectors-legacy-api-list-example]] +===== Example + +[source,sh] +-------------------------------------------------- +$ curl -X GET api/actions/list_action_types +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +[ + { + "id": ".email", <1> + "name": "Email", <2> + "minimumLicenseRequired": "gold", <3> + "enabled": false, <4> + "enabledInConfig": true, <5> + "enabledInLicense": false <6> + }, + { + "id": ".index", + "name": "Index", + "minimumLicenseRequired": "basic", + "enabled": true, + "enabledInConfig": true, + "enabledInLicense": true + } +] +-------------------------------------------------- + + +<1> `id` - The unique ID of the connector type. +<2> `name` - The name of the connector type. +<3> `minimumLicenseRequired` - The license required to use the connector type. +<4> `enabled` - Specifies if the connector type is enabled or disabled in {kib}. +<5> `enabledInConfig` - Specifies if the connector type is enabled or enabled in the {kib} .yml file. +<6> `enabledInLicense` - Specifies if the connector type is enabled or disabled in the license. diff --git a/docs/api/actions-and-connectors/legacy/update.asciidoc b/docs/api/actions-and-connectors/legacy/update.asciidoc new file mode 100644 index 0000000000000..c2e841988717a --- /dev/null +++ b/docs/api/actions-and-connectors/legacy/update.asciidoc @@ -0,0 +1,77 @@ +[[actions-and-connectors-legacy-api-update]] +==== Legacy Update connector API +++++ +Legacy Update connector API +++++ + +deprecated::[7.13.0] + +Please use the <> instead. + +Updates the attributes for an existing connector. + +[[actions-and-connectors-legacy-api-update-request]] +===== Request + +`PUT :/api/actions/action/` + +`PUT :/s//api/actions/action/` + +[[actions-and-connectors-legacy-api-update-params]] +===== Path parameters + +`id`:: + (Required, string) The ID of the connector. + +`space_id`:: + (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +[[actions-and-connectors-legacy-api-update-request-body]] +===== Request body + +`name`:: + (Required, string) The new name of the connector. + +`config`:: + (Required, object) The new connector configuration. Configuration properties vary depending on the connector type. For information about the configuration properties, refer to <>. + +`secrets`:: + (Required, object) The updated secrets configuration for the connector. Secrets properties vary depending on the connector type. For information about the secrets configuration properties, refer to <>. + +[[actions-and-connectors-legacy-api-update-codes]] +===== Response code + +`200`:: + Indicates a successful call. + +[[actions-and-connectors-legacy-api-update-example]] +===== Example + +[source,sh] +-------------------------------------------------- +$ curl -X PUT api/actions/action/c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -d ' +{ + "name": "updated-connector", + "config": { + "index": "updated-index" + } +}' +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{ + "id": "c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad", + "actionTypeId": ".index", + "name": "updated-connector", + "config": { + "index": "updated-index", + "refresh": false, + "executionTimeField": null + }, + "isPreconfigured": false +} +-------------------------------------------------- diff --git a/docs/api/actions-and-connectors/list.asciidoc b/docs/api/actions-and-connectors/list.asciidoc index 3647bf06c98e0..941f7b4376e91 100644 --- a/docs/api/actions-and-connectors/list.asciidoc +++ b/docs/api/actions-and-connectors/list.asciidoc @@ -1,17 +1,17 @@ [[actions-and-connectors-api-list]] -=== List action types API +=== List connector types API ++++ -List all action types API +List all connector types API ++++ -Retrieves a list of all action types. +Retrieves a list of all connector types. [[actions-and-connectors-api-list-request]] ==== Request -`GET :/api/actions/list_action_types` +`GET :/api/actions/connector_types` -`GET :/s//api/actions/list_action_types` +`GET :/s//api/actions/connector_types` [[actions-and-connectors-api-list-path-params]] ==== Path parameters @@ -30,7 +30,7 @@ Retrieves a list of all action types. [source,sh] -------------------------------------------------- -$ curl -X GET api/actions/list_action_types +$ curl -X GET api/actions/connector_types -------------------------------------------------- // KIBANA @@ -42,26 +42,26 @@ The API returns the following: { "id": ".email", <1> "name": "Email", <2> - "minimumLicenseRequired": "gold", <3> + "minimum_license_required": "gold", <3> "enabled": false, <4> - "enabledInConfig": true, <5> - "enabledInLicense": false <6> + "enabled_in_config": true, <5> + "enabled_in_license": false <6> }, { "id": ".index", "name": "Index", - "minimumLicenseRequired": "basic", + "minimum_license_required": "basic", "enabled": true, - "enabledInConfig": true, - "enabledInLicense": true + "enabled_in_config": true, + "enabled_in_license": true } ] -------------------------------------------------- -<1> `id` - The unique ID of the action type. -<2> `name` - The name of the action type. -<3> `minimumLicenseRequired` - The license required to use the action type. -<4> `enabled` - Specifies if the action type is enabled or disabled in {kib}. -<5> `enabledInConfig` - Specifies if the action type is enabled or enabled in the {kib} .yml file. -<6> `enabledInLicense` - Specifies if the action type is enabled or disabled in the license. +<1> `id` - The unique ID of the connector type. +<2> `name` - The name of the connector type. +<3> `minimum_license_required` - The license required to use the connector type. +<4> `enabled` - Specifies if the connector type is enabled or disabled in {kib}. +<5> `enabled_in_config` - Specifies if the connector type is enabled or enabled in the {kib} .yml file. +<6> `enabled_in_license` - Specifies if the connector type is enabled or disabled in the license. diff --git a/docs/api/actions-and-connectors/update.asciidoc b/docs/api/actions-and-connectors/update.asciidoc index 46e6d91cf9e97..6c4e6040bdfb5 100644 --- a/docs/api/actions-and-connectors/update.asciidoc +++ b/docs/api/actions-and-connectors/update.asciidoc @@ -1,23 +1,23 @@ [[actions-and-connectors-api-update]] -=== Update action API +=== Update connector API ++++ -Update action API +Update connector API ++++ -Updates the attributes for an existing action. +Updates the attributes for an existing connector. [[actions-and-connectors-api-update-request]] ==== Request -`PUT :/api/actions/action/` +`PUT :/api/actions/connector/` -`PUT :/s//api/actions/action/` +`PUT :/s//api/actions/connector/` [[actions-and-connectors-api-update-params]] ==== Path parameters `id`:: - (Required, string) The ID of the action. + (Required, string) The ID of the connector. `space_id`:: (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. @@ -26,13 +26,13 @@ Updates the attributes for an existing action. ==== Request body `name`:: - (Required, string) The new name of the action. + (Required, string) The new name of the connector. `config`:: - (Required, object) The new action configuration. Configuration properties vary depending on the action type. For information about the configuration properties, refer to <>. + (Required, object) The new connector configuration. Configuration properties vary depending on the connector type. For information about the configuration properties, refer to <>. `secrets`:: - (Required, object) The updated secrets configuration for the action. Secrets properties vary depending on the action type. For information about the secrets configuration properties, refer to <>. + (Required, object) The updated secrets configuration for the connector. Secrets properties vary depending on the connector type. For information about the secrets configuration properties, refer to <>. [[actions-and-connectors-api-update-codes]] ==== Response code @@ -45,9 +45,9 @@ Updates the attributes for an existing action. [source,sh] -------------------------------------------------- -$ curl -X PUT api/actions/action/c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -d ' +$ curl -X PUT api/actions/connector/c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -d ' { - "name": "updated-action", + "name": "updated-connector", "config": { "index": "updated-index" } @@ -61,13 +61,13 @@ The API returns the following: -------------------------------------------------- { "id": "c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad", - "actionTypeId": ".index", - "name": "updated-action", + "connector_type_id": ".index", + "name": "updated-connector", "config": { "index": "updated-index", "refresh": false, "executionTimeField": null }, - "isPreconfigured": false + "is_preconfigured": false } -------------------------------------------------- diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 6e9549c0ca5f9..877026ef313ba 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -367,9 +367,8 @@ security and spaces filtering as well as performing audit logging. |{kib-repo}blob/{branch}/x-pack/plugins/event_log/README.md[eventLog] -|The purpose of this plugin is to provide a way to persist a history of events -occuring in Kibana, initially just for the Make It Action project - alerts -and actions. +|The event log plugin provides a persistent history of alerting and action +actitivies. |{kib-repo}blob/{branch}/x-pack/plugins/features/README.md[features] diff --git a/docs/discover/images/discover-add-filter.png b/docs/discover/images/discover-add-filter.png new file mode 100644 index 0000000000000..e6f4685d31d60 Binary files /dev/null and b/docs/discover/images/discover-add-filter.png differ diff --git a/docs/discover/images/discover-maps.png b/docs/discover/images/discover-maps.png new file mode 100644 index 0000000000000..42a17d6102b5c Binary files /dev/null and b/docs/discover/images/discover-maps.png differ diff --git a/docs/discover/images/discover-save-saved-search.png b/docs/discover/images/discover-save-saved-search.png new file mode 100644 index 0000000000000..bf22408729b08 Binary files /dev/null and b/docs/discover/images/discover-save-saved-search.png differ diff --git a/docs/discover/images/discover-search-field.png b/docs/discover/images/discover-search-field.png new file mode 100644 index 0000000000000..9acbf86067fb4 Binary files /dev/null and b/docs/discover/images/discover-search-field.png differ diff --git a/docs/discover/images/discover-sidebar-available-fields.png b/docs/discover/images/discover-sidebar-available-fields.png new file mode 100644 index 0000000000000..7f3514757eed5 Binary files /dev/null and b/docs/discover/images/discover-sidebar-available-fields.png differ diff --git a/docs/discover/images/discover-visualize.png b/docs/discover/images/discover-visualize.png new file mode 100644 index 0000000000000..f4bcaf8aca028 Binary files /dev/null and b/docs/discover/images/discover-visualize.png differ diff --git a/docs/discover/images/geoip-icon.png b/docs/discover/images/geoip-icon.png new file mode 100644 index 0000000000000..d3317451be3ea Binary files /dev/null and b/docs/discover/images/geoip-icon.png differ diff --git a/docs/user/alerting/images/alert-types-es-query-conditions.png b/docs/user/alerting/images/alert-types-es-query-conditions.png index 3cbba5eb4950e..bfb549f5df20e 100644 Binary files a/docs/user/alerting/images/alert-types-es-query-conditions.png and b/docs/user/alerting/images/alert-types-es-query-conditions.png differ diff --git a/docs/user/alerting/stack-alerts/es-query.asciidoc b/docs/user/alerting/stack-alerts/es-query.asciidoc index d25b7267ed18f..cac53f6600163 100644 --- a/docs/user/alerting/stack-alerts/es-query.asciidoc +++ b/docs/user/alerting/stack-alerts/es-query.asciidoc @@ -1,13 +1,13 @@ [role="xpack"] [[alert-type-es-query]] -=== ES query +=== {es} query -The ES query alert type runs a user-configured {es} query, compares the number of matches to a configured threshold, and schedules actions to run when the threshold condition is met. +The {es} query alert type runs a user-configured {es} query, compares the number of matches to a configured threshold, and schedules actions to run when the threshold condition is met. [float] ==== Create the alert -Fill in the <>, then select *ES query*. +Fill in the <>, then select *{es} query*. [float] ==== Define the conditions @@ -19,7 +19,7 @@ image::user/alerting/images/alert-types-es-query-conditions.png[Five clauses def Index:: This clause requires an *index or index pattern* and a *time field* that will be used for the *time window*. Size:: This clause specifies the number of documents to pass to the configured actions when the the threshold condition is met. -ES query:: This clause specifies the ES DSL query to execute. The number of documents that match this query will be evaulated against the threshold +{es} query:: This clause specifies the ES DSL query to execute. The number of documents that match this query will be evaulated against the threshold condition. Aggregations are not supported at this time. Threshold:: This clause defines a threshold value and a comparison operator (`is above`, `is above or equals`, `is below`, `is below or equals`, or `is between`). The number of documents that match the specified query is compared to this threshold. Time window:: This clause determines how far back to search for documents, using the *time field* set in the *index* clause. Generally this value should be set to a value higher than the *check every* value in the <>, to avoid gaps in detection. @@ -27,7 +27,7 @@ Time window:: This clause determines how far back to search for documents, using [float] ==== Add action variables -<> to run when the alert condition is met. The following variables are specific to the ES query alert. You can also specify <>. +<> to run when the alert condition is met. The following variables are specific to the {es} query alert. You can also specify <>. `context.title`:: A preconstructed title for the alert. Example: `alert term match alert query matched`. `context.message`:: A preconstructed message for the alert. Example: + @@ -55,9 +55,9 @@ Use the *Test query* feature to verify that your query DSL is valid. match the query will be displayed. + [role="screenshot"] -image::user/alerting/images/alert-types-es-query-valid.png[Test ES query returns number of matches when valid] +image::user/alerting/images/alert-types-es-query-valid.png[Test {es} query returns number of matches when valid] * An error message is shown if the query is invalid. + [role="screenshot"] -image::user/alerting/images/alert-types-es-query-invalid.png[Test ES query shows error when invalid] \ No newline at end of file +image::user/alerting/images/alert-types-es-query-invalid.png[Test {es} query shows error when invalid] \ No newline at end of file diff --git a/docs/user/discover.asciidoc b/docs/user/discover.asciidoc index 11abe975374e5..39e3a8e41ea6a 100644 --- a/docs/user/discover.asciidoc +++ b/docs/user/discover.asciidoc @@ -3,9 +3,29 @@ [partintro] -- -**_Tell {kib} where to find your data, then search and filter it for hidden insights and relationships._** +**_Gain insight to your data._** + +*Discover* enables you to quickly search and filter your data, get information +about structure of the fields, and visualize your data with *Lens* and *Maps*. +You can customize and save your searches and place them on a dashboard. + +++++ + + +++++ -You’ve added your data, and now you’re ready to dig in. You have questions about your data. +[float] +=== Explore and query your data + +You have questions about your data. What pages on your website contain a specific word or phrase? What events were logged most recently? What processes take longer than 500 milliseconds to respond? @@ -23,9 +43,6 @@ that summarize the contents of the data. At the end of this tutorial, you’ll be ready to start exploring with your own data in *Discover*. -[role="screenshot"] -image::images/Discover-Start.png[Discover] - [float] === Prerequisites @@ -36,24 +53,6 @@ image::images/Discover-Start.png[Discover] - You should have an understanding of {ref}/documents-indices.html[{es} documents and indices]. -[float] -[[whats-you-goal-in-discover]] -=== Define your goal - -When you explore your data in **Discover**, it's common to start with one or two goals: - -- **Get an overview of what is happening.** -For example, you might look for -information on the overall health and performance of your ecommerce business, -and then share your findings in a report. - -- **Find an answer to a specific question.** You want -to determine your customers' shopping preferences, -and then visualize your findings on a dashboard. - -For this tutorial, your goal is to better manage your product inventory. You want to -know the top-selling products and on what day of the week these products sell the most. - [float] [[find-the-data-you-want-to-use]] === Find your data @@ -90,6 +89,9 @@ which can be overwhelming. You’ll modify this table to display only your field . Scan through the list of **Available fields** to see what’s in your data. You can also search for a field by name. ++ +[role="screenshot"] +image:images/discover-sidebar-available-fields.png[Fields list that displays the top five search results, width=50%] . Find the `manufacturer` field, and then click it to view the five most popular values for that field. + @@ -114,7 +116,13 @@ column header, and then use the move and sort controls. One of the unique capabilities of **Discover** is the ability to combine free text search with filtering based on structured data. -To search all fields, enter a simple string in the **Search** field. To search particular fields and +To search all fields, enter a simple string in the **Search** field. + +[role="screenshot"] +image:images/discover-search-field.png[Search field in Discover] + + +To search particular fields and build more complex queries, use the <>. As you type, KQL prompts you with the fields you can search and the operators you can use to build a structured query. @@ -137,6 +145,9 @@ You can filter results to include or exclude specific fields, filter for a value and more. The **Add filter** popup prompts you with the fields you can filter and the operators you can use. +[role="screenshot"] +image:images/discover-add-filter.png[Add filter dialog in Discover] + Exclude documents where day of week is not Wednesday: . Click **Add filter**. @@ -178,6 +189,9 @@ Saving a search saves the query and the filters. . In the toolbar, click **Save**. . Give your search a title, and then click **Save**. ++ +[role="screenshot"] +image:images/discover-save-saved-search.png[Save saved search in Discover, width=50%] [float] === Visualize your findings @@ -186,6 +200,9 @@ visualize it from **Discover**. . From the **Selected fields** list, click `day_of_week`, and then click **Visualize**. + +[role="screenshot"] +image:images/discover-visualize.png[Discover sidebar field popover with visualize button, width=75%] ++ {kib} creates a visualization best suited for this field. . Drag `manufacturer.keyword` from the field list and drop it on @@ -196,11 +213,26 @@ image:images/visualize-from-discover.png[Visualization that opens from Discover . Save your visualization for use on a dashboard. +If your documents contain geo point fields (image:images/geoip-icon.png[Geo point field icon, width=20px]), you can visualize them in **Maps**. + +. Make sure the index pattern is set to **kibana_sample_data_ecommerce** and the configured time range +contains data. + +. From the **Available fields** list, click `geoip.location`, and then click **Visualize**. ++ +[role="screenshot"] +image:images/discover-maps.png[Map containing documents] + +. Save your map for use on a dashboard. + + [float] === What’s next? * <>. +* <>. + * <> to better meet your needs. In **Advanced Settings**, you can configure the number of documents to show, the table columns that display by default, and more. @@ -209,8 +241,6 @@ the table columns that display by default, and more. * <>. -* <>. - -- include::{kib-repo-dir}/management/index-patterns.asciidoc[] diff --git a/package.json b/package.json index 6dec197051e65..1dd1375ac092b 100644 --- a/package.json +++ b/package.json @@ -114,7 +114,6 @@ "@hapi/hoek": "^9.1.1", "@hapi/inert": "^6.0.3", "@hapi/podium": "^4.1.1", - "@hapi/statehood": "^7.0.3", "@hapi/vision": "^6.0.1", "@hapi/wreck": "^17.1.0", "@kbn/ace": "link:packages/kbn-ace", @@ -300,7 +299,6 @@ "set-value": "^3.0.2", "source-map-support": "^0.5.19", "squel": "^5.13.0", - "statehood": "6.0.6", "stats-lite": "^2.2.0", "strip-ansi": "^6.0.0", "style-it": "^2.1.3", diff --git a/packages/kbn-test/src/functional_tests/lib/run_kibana_server.js b/packages/kbn-test/src/functional_tests/lib/run_kibana_server.js index a43d3a09c7d70..2145be4f492f2 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_kibana_server.js +++ b/packages/kbn-test/src/functional_tests/lib/run_kibana_server.js @@ -38,7 +38,7 @@ export async function runKibanaServer({ procs, config, options }) { ...extendNodeOptions(installDir), }, cwd: installDir || KIBANA_ROOT, - wait: /http server running/, + wait: /Kibana is now available/, }); } diff --git a/packages/kbn-test/src/functional_tests/tasks.js b/packages/kbn-test/src/functional_tests/tasks.js index 02c55b6af91dc..099963545a2dc 100644 --- a/packages/kbn-test/src/functional_tests/tasks.js +++ b/packages/kbn-test/src/functional_tests/tasks.js @@ -95,8 +95,6 @@ export async function runTests(options) { try { es = await runElasticsearch({ config, options: opts }); await runKibanaServer({ procs, config, options: opts }); - // workaround until https://github.com/elastic/kibana/issues/89828 is addressed - await delay(5000); await runFtr({ configPath, options: opts }); } finally { try { @@ -162,7 +160,3 @@ async function silence(log, milliseconds) { ) .toPromise(); } - -async function delay(ms) { - await new Promise((resolve) => setTimeout(resolve, ms)); -} diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 337dfa8824303..45bec58bbae3c 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -269,6 +269,7 @@ export class Server { plugins: mapToObject(pluginsStart.contracts), }); + this.status.start(); await this.http.start(); startTransaction?.end(); diff --git a/src/core/server/status/log_overall_status.test.ts b/src/core/server/status/log_overall_status.test.ts new file mode 100644 index 0000000000000..b656b16a9e00b --- /dev/null +++ b/src/core/server/status/log_overall_status.test.ts @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { TestScheduler } from 'rxjs/testing'; +import { ServiceStatus, ServiceStatusLevels } from './types'; +import { getOverallStatusChanges } from './log_overall_status'; + +const getTestScheduler = () => + new TestScheduler((actual, expected) => { + expect(actual).toEqual(expected); + }); + +const createStatus = (parts: Partial = {}): ServiceStatus => ({ + level: ServiceStatusLevels.available, + summary: 'summary', + ...parts, +}); + +describe('getOverallStatusChanges', () => { + it('emits an initial message after first overall$ emission', () => { + getTestScheduler().run(({ expectObservable, hot }) => { + const overall$ = hot('--a', { + a: createStatus(), + }); + const stop$ = hot(''); + const expected = '--a'; + + expectObservable(getOverallStatusChanges(overall$, stop$)).toBe(expected, { + a: 'Kibana is now available', + }); + }); + }); + + it('emits a new message every time the status level changes', () => { + getTestScheduler().run(({ expectObservable, hot }) => { + const overall$ = hot('--a--b', { + a: createStatus({ + level: ServiceStatusLevels.degraded, + }), + b: createStatus({ + level: ServiceStatusLevels.available, + }), + }); + const stop$ = hot(''); + const expected = '--a--b'; + + expectObservable(getOverallStatusChanges(overall$, stop$)).toBe(expected, { + a: 'Kibana is now degraded', + b: 'Kibana is now available (was degraded)', + }); + }); + }); + + it('does not emit when the status stays the same', () => { + getTestScheduler().run(({ expectObservable, hot }) => { + const overall$ = hot('--a--b--c', { + a: createStatus({ + level: ServiceStatusLevels.degraded, + summary: 'summary 1', + }), + b: createStatus({ + level: ServiceStatusLevels.degraded, + summary: 'summary 2', + }), + c: createStatus({ + level: ServiceStatusLevels.available, + summary: 'summary 2', + }), + }); + const stop$ = hot(''); + const expected = '--a-----b'; + + expectObservable(getOverallStatusChanges(overall$, stop$)).toBe(expected, { + a: 'Kibana is now degraded', + b: 'Kibana is now available (was degraded)', + }); + }); + }); + + it('stops emitting once `stop$` emits', () => { + getTestScheduler().run(({ expectObservable, hot }) => { + const overall$ = hot('--a--b', { + a: createStatus({ + level: ServiceStatusLevels.degraded, + }), + b: createStatus({ + level: ServiceStatusLevels.available, + }), + }); + const stop$ = hot('----(s|)'); + const expected = '--a-|'; + + expectObservable(getOverallStatusChanges(overall$, stop$)).toBe(expected, { + a: 'Kibana is now degraded', + }); + }); + }); +}); diff --git a/src/core/server/status/log_overall_status.ts b/src/core/server/status/log_overall_status.ts new file mode 100644 index 0000000000000..c77f634046be1 --- /dev/null +++ b/src/core/server/status/log_overall_status.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Observable } from 'rxjs'; +import { distinctUntilChanged, pairwise, startWith, takeUntil, map } from 'rxjs/operators'; +import { ServiceStatus } from './types'; + +export const getOverallStatusChanges = ( + overall$: Observable, + stop$: Observable +) => { + return overall$.pipe( + takeUntil(stop$), + distinctUntilChanged((previous, next) => { + return previous.level.toString() === next.level.toString(); + }), + startWith(undefined), + pairwise(), + map(([oldStatus, newStatus]) => { + if (oldStatus) { + return `Kibana is now ${newStatus!.level.toString()} (was ${oldStatus!.level.toString()})`; + } + return `Kibana is now ${newStatus!.level.toString()}`; + }) + ); +}; diff --git a/src/core/server/status/status_service.ts b/src/core/server/status/status_service.ts index 09cf5b92b2b8a..911f09278b460 100644 --- a/src/core/server/status/status_service.ts +++ b/src/core/server/status/status_service.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { Observable, combineLatest, Subscription } from 'rxjs'; +import { Observable, combineLatest, Subscription, Subject } from 'rxjs'; import { map, distinctUntilChanged, shareReplay, take, debounceTime } from 'rxjs/operators'; import { isDeepStrictEqual } from 'util'; @@ -25,6 +25,7 @@ import { config, StatusConfigType } from './status_config'; import { ServiceStatus, CoreStatus, InternalStatusServiceSetup } from './types'; import { getSummaryStatus } from './get_summary_status'; import { PluginsStatusService } from './plugins_status'; +import { getOverallStatusChanges } from './log_overall_status'; interface SetupDeps { elasticsearch: Pick; @@ -38,7 +39,9 @@ interface SetupDeps { export class StatusService implements CoreService { private readonly logger: Logger; private readonly config$: Observable; + private readonly stop$ = new Subject(); + private overall$?: Observable; private pluginsStatus?: PluginsStatusService; private overallSubscription?: Subscription; @@ -59,10 +62,7 @@ export class StatusService implements CoreService { const core$ = this.setupCoreStatus({ elasticsearch, savedObjects }); this.pluginsStatus = new PluginsStatusService({ core$, pluginDependencies }); - const overall$: Observable = combineLatest([ - core$, - this.pluginsStatus.getAll$(), - ]).pipe( + this.overall$ = combineLatest([core$, this.pluginsStatus.getAll$()]).pipe( // Prevent many emissions at once from dependency status resolution from making this too noisy debounceTime(500), map(([coreStatus, pluginsStatus]) => { @@ -78,7 +78,7 @@ export class StatusService implements CoreService { ); // Create an unused subscription to ensure all underlying lazy observables are started. - this.overallSubscription = overall$.subscribe(); + this.overallSubscription = this.overall$.subscribe(); const router = http.createRouter(''); registerStatusRoute({ @@ -91,7 +91,7 @@ export class StatusService implements CoreService { }, metrics, status: { - overall$, + overall$: this.overall$, plugins$: this.pluginsStatus.getAll$(), core$, }, @@ -99,7 +99,7 @@ export class StatusService implements CoreService { return { core$, - overall$, + overall$: this.overall$, plugins: { set: this.pluginsStatus.set.bind(this.pluginsStatus), getDependenciesStatus$: this.pluginsStatus.getDependenciesStatus$.bind(this.pluginsStatus), @@ -109,9 +109,19 @@ export class StatusService implements CoreService { }; } - public start() {} + public start() { + if (!this.overall$) { + throw new Error('cannot call `start` before `setup`'); + } + getOverallStatusChanges(this.overall$, this.stop$).subscribe((message) => { + this.logger.info(message); + }); + } public stop() { + this.stop$.next(); + this.stop$.complete(); + if (this.overallSubscription) { this.overallSubscription.unsubscribe(); this.overallSubscription = undefined; diff --git a/src/dev/code_coverage/shell_scripts/merge_functional.sh b/src/dev/code_coverage/shell_scripts/merge_functional.sh index 8bd65c0d8fedb..68370910e6ad7 100755 --- a/src/dev/code_coverage/shell_scripts/merge_functional.sh +++ b/src/dev/code_coverage/shell_scripts/merge_functional.sh @@ -12,11 +12,8 @@ mkdir -p "$coverageBasePath/kibana" rsync -ahSD --ignore-errors --force --delete --stats ./ "$coverageBasePath/kibana/" cd "$coverageBasePath/kibana" -echo "### bootstrap from x-pack folder" -cd x-pack +echo "### bootstrap" yarn kbn bootstrap -# Return to project root -cd .. echo "### Merge coverage reports" yarn nyc report --nycrc-path src/dev/code_coverage/nyc_config/nyc.functional.config.js diff --git a/src/plugins/dashboard/kibana.json b/src/plugins/dashboard/kibana.json index 8286a4badcbe5..32507cbc5e5f4 100644 --- a/src/plugins/dashboard/kibana.json +++ b/src/plugins/dashboard/kibana.json @@ -15,6 +15,7 @@ ], "optionalPlugins": [ "home", + "spacesOss", "savedObjectsTaggingOss", "usageCollection"], "server": true, diff --git a/src/plugins/dashboard/public/application/dashboard_router.tsx b/src/plugins/dashboard/public/application/dashboard_router.tsx index 5c849aa960a4c..f981b135c4359 100644 --- a/src/plugins/dashboard/public/application/dashboard_router.tsx +++ b/src/plugins/dashboard/public/application/dashboard_router.tsx @@ -13,6 +13,7 @@ import { parse, ParsedQuery } from 'query-string'; import { render, unmountComponentAtNode } from 'react-dom'; import { Switch, Route, RouteComponentProps, HashRouter, Redirect } from 'react-router-dom'; +import { first } from 'rxjs/operators'; import { DashboardListing } from './listing'; import { DashboardApp } from './dashboard_app'; import { addHelpMenuToAppChrome, DashboardPanelStorage } from './lib'; @@ -47,6 +48,7 @@ export const dashboardUrlParams = { export interface DashboardMountProps { appUnMounted: () => void; restorePreviousUrl: () => void; + scopedHistory: ScopedHistory; element: AppMountParameters['element']; initializerContext: PluginInitializerContext; @@ -80,6 +82,9 @@ export async function mountApp({ savedObjectsTaggingOss, } = pluginsStart; + const spacesApi = pluginsStart.spacesOss?.isSpacesAvailable ? pluginsStart.spacesOss : undefined; + const activeSpaceId = spacesApi && (await spacesApi.activeSpace$.pipe(first()).toPromise())?.id; + const dashboardServices: DashboardAppServices = { navigation, onAppLeave, @@ -99,7 +104,10 @@ export async function mountApp({ indexPatterns: dataStart.indexPatterns, savedQueryService: dataStart.query.savedQueries, savedObjectsClient: coreStart.savedObjects.client, - dashboardPanelStorage: new DashboardPanelStorage(core.notifications.toasts), + dashboardPanelStorage: new DashboardPanelStorage( + core.notifications.toasts, + activeSpaceId || 'default' + ), savedDashboards: dashboardStart.getSavedDashboardLoader(), savedObjectsTagging: savedObjectsTaggingOss?.getTaggingApi(), allowByValueEmbeddables: initializerContext.config.get() diff --git a/src/plugins/dashboard/public/application/lib/dashboard_panel_storage.ts b/src/plugins/dashboard/public/application/lib/dashboard_panel_storage.ts index fcd3a09dc6b4e..02890f6aaa790 100644 --- a/src/plugins/dashboard/public/application/lib/dashboard_panel_storage.ts +++ b/src/plugins/dashboard/public/application/lib/dashboard_panel_storage.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { set } from '@elastic/safer-lodash-set'; import { Storage } from '../../services/kibana_utils'; import { NotificationsStart } from '../../services/core'; import { panelStorageErrorStrings } from '../../dashboard_strings'; @@ -17,16 +18,17 @@ const DASHBOARD_PANELS_SESSION_KEY = 'dashboardStateManagerPanels'; export class DashboardPanelStorage { private sessionStorage: Storage; - constructor(private toasts: NotificationsStart['toasts']) { + constructor(private toasts: NotificationsStart['toasts'], private activeSpaceId: string) { this.sessionStorage = new Storage(sessionStorage); } public clearPanels(id = DASHBOARD_PANELS_UNSAVED_ID) { try { - const sessionStoragePanels = this.sessionStorage.get(DASHBOARD_PANELS_SESSION_KEY) || {}; - if (sessionStoragePanels[id]) { - delete sessionStoragePanels[id]; - this.sessionStorage.set(DASHBOARD_PANELS_SESSION_KEY, sessionStoragePanels); + const sessionStorage = this.sessionStorage.get(DASHBOARD_PANELS_SESSION_KEY); + const sessionStorageForSpace = sessionStorage?.[this.activeSpaceId] || {}; + if (sessionStorageForSpace[id]) { + delete sessionStorageForSpace[id]; + this.sessionStorage.set(DASHBOARD_PANELS_SESSION_KEY, sessionStorage); } } catch (e) { this.toasts.addDanger({ @@ -38,7 +40,7 @@ export class DashboardPanelStorage { public getPanels(id = DASHBOARD_PANELS_UNSAVED_ID): SavedDashboardPanel[] | undefined { try { - return this.sessionStorage.get(DASHBOARD_PANELS_SESSION_KEY)?.[id]; + return this.sessionStorage.get(DASHBOARD_PANELS_SESSION_KEY)?.[this.activeSpaceId]?.[id]; } catch (e) { this.toasts.addDanger({ title: panelStorageErrorStrings.getPanelsGetError(e.message), @@ -50,7 +52,7 @@ export class DashboardPanelStorage { public setPanels(id = DASHBOARD_PANELS_UNSAVED_ID, newPanels: SavedDashboardPanel[]) { try { const sessionStoragePanels = this.sessionStorage.get(DASHBOARD_PANELS_SESSION_KEY) || {}; - sessionStoragePanels[id] = newPanels; + set(sessionStoragePanels, [this.activeSpaceId, id], newPanels); this.sessionStorage.set(DASHBOARD_PANELS_SESSION_KEY, sessionStoragePanels); } catch (e) { this.toasts.addDanger({ @@ -62,7 +64,9 @@ export class DashboardPanelStorage { public getDashboardIdsWithUnsavedChanges() { try { - return Object.keys(this.sessionStorage.get(DASHBOARD_PANELS_SESSION_KEY) || {}); + return Object.keys( + this.sessionStorage.get(DASHBOARD_PANELS_SESSION_KEY)?.[this.activeSpaceId] || {} + ); } catch (e) { this.toasts.addDanger({ title: panelStorageErrorStrings.getPanelsGetError(e.message), diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 4385e3e8744c2..ae2d2b5f237c9 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -76,6 +76,7 @@ import { UrlGeneratorState } from '../../share/public'; import { ExportCSVAction } from './application/actions/export_csv_action'; import { dashboardFeatureCatalog } from './dashboard_strings'; import { replaceUrlHashQuery } from '../../kibana_utils/public'; +import { SpacesOssPluginStart } from './services/spaces'; declare module '../../share/public' { export interface UrlGeneratorStateMapping { @@ -113,6 +114,7 @@ export interface DashboardStartDependencies { savedObjects: SavedObjectsStart; presentationUtil: PresentationUtilPluginStart; savedObjectsTaggingOss?: SavedObjectTaggingOssPluginStart; + spacesOss?: SpacesOssPluginStart; } export type DashboardSetup = void; diff --git a/src/plugins/dashboard/public/services/spaces.ts b/src/plugins/dashboard/public/services/spaces.ts new file mode 100644 index 0000000000000..e6d2c6400818f --- /dev/null +++ b/src/plugins/dashboard/public/services/spaces.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { SpacesOssPluginStart } from '../../../spaces_oss/public'; diff --git a/src/plugins/dashboard/tsconfig.json b/src/plugins/dashboard/tsconfig.json index ddda2f81d1f62..dd99119cfb457 100644 --- a/src/plugins/dashboard/tsconfig.json +++ b/src/plugins/dashboard/tsconfig.json @@ -31,5 +31,6 @@ { "path": "../saved_objects_tagging_oss/tsconfig.json" }, { "path": "../saved_objects/tsconfig.json" }, { "path": "../ui_actions/tsconfig.json" }, + { "path": "../spaces_oss/tsconfig.json" }, ] } diff --git a/src/plugins/data/common/search/aggs/agg_config.test.ts b/src/plugins/data/common/search/aggs/agg_config.test.ts index 5e52779ffa218..818255f6c8bcc 100644 --- a/src/plugins/data/common/search/aggs/agg_config.test.ts +++ b/src/plugins/data/common/search/aggs/agg_config.test.ts @@ -141,6 +141,51 @@ describe('AggConfig', () => { expect(dsl.aggs[avgConfig.id]).toHaveProperty('avg'); expect(dsl.aggs[avgConfig.id].avg).toBe(football); }); + + it('merges subAggs from #write() output to the current subaggs', () => { + const configStates = [ + { + enabled: true, + type: 'avg', + schema: 'metric', + params: {}, + }, + { + enabled: true, + type: 'median', + schema: 'metric', + params: {}, + }, + { + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: {}, + }, + ]; + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry }); + + const histoConfig = ac.byName('date_histogram')[0]; + const avgConfig = ac.byName('avg')[0]; + const medianConfig = ac.byName('median')[0]; + const football = {}; + + jest + .spyOn(histoConfig, 'write') + .mockImplementation(() => ({ params: {}, subAggs: [avgConfig] })); + jest.spyOn(avgConfig, 'write').mockImplementation(() => ({ params: football })); + jest.spyOn(medianConfig, 'write').mockImplementation(() => ({ params: football })); + + (histoConfig as any).subAggs = [medianConfig]; + const dsl = histoConfig.toDsl(); + expect(dsl).toHaveProperty('aggs'); + expect(dsl.aggs).toHaveProperty(avgConfig.id); + expect(dsl.aggs[avgConfig.id]).toHaveProperty('avg'); + expect(dsl.aggs[avgConfig.id].avg).toBe(football); + expect(dsl.aggs).toHaveProperty(medianConfig.id); + expect(dsl.aggs[medianConfig.id]).toHaveProperty('percentiles'); + expect(dsl.aggs[medianConfig.id].percentiles).toBe(football); + }); }); describe('::ensureIds', () => { diff --git a/src/plugins/data/common/search/aggs/agg_config.ts b/src/plugins/data/common/search/aggs/agg_config.ts index 6a5bc64cbdc55..f62fedc13b32a 100644 --- a/src/plugins/data/common/search/aggs/agg_config.ts +++ b/src/plugins/data/common/search/aggs/agg_config.ts @@ -235,7 +235,11 @@ export class AggConfig { configDsl[this.type.dslName || this.type.name] = output.params; // if the config requires subAggs, write them to the dsl as well - if (this.subAggs.length && !output.subAggs) output.subAggs = this.subAggs; + if (this.subAggs.length) { + if (!output.subAggs) output.subAggs = this.subAggs; + else output.subAggs.push(...this.subAggs); + } + if (output.subAggs) { const subDslLvl = configDsl.aggs || (configDsl.aggs = {}); output.subAggs.forEach(function nestAdhocSubAggs(subAggConfig: any) { diff --git a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/index.ts b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/index.ts index 0f05bb237a583..37ba9077e3318 100644 --- a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/index.ts +++ b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/index.ts @@ -7,6 +7,7 @@ */ export * from './empty_field'; +export * from './max_length'; export * from './min_length'; export * from './min_selectable_selection'; export * from './url'; diff --git a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/max_length.ts b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/max_length.ts new file mode 100644 index 0000000000000..3ba0a272003c8 --- /dev/null +++ b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/max_length.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ValidationFunc, ValidationError } from '../../hook_form_lib'; +import { hasMaxLengthString } from '../../../validators/string'; +import { hasMaxLengthArray } from '../../../validators/array'; +import { ERROR_CODE } from './types'; + +export const maxLengthField = ({ + length = 0, + message, +}: { + length: number; + message: string | ((err: Partial) => string); +}) => (...args: Parameters): ReturnType> => { + const [{ value }] = args; + + // Validate for Arrays + if (Array.isArray(value)) { + return hasMaxLengthArray(length)(value) + ? undefined + : { + code: 'ERR_MAX_LENGTH', + length, + message: typeof message === 'function' ? message({ length }) : message, + }; + } + + // Validate for Strings + return hasMaxLengthString(length)((value as string).trim()) + ? undefined + : { + code: 'ERR_MAX_LENGTH', + length, + message: typeof message === 'function' ? message({ length }) : message, + }; +}; diff --git a/src/plugins/security_oss/.eslintrc.json b/src/plugins/security_oss/.eslintrc.json deleted file mode 100644 index 2b63a9259d220..0000000000000 --- a/src/plugins/security_oss/.eslintrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "rules": { - "@typescript-eslint/consistent-type-imports": 1 - } -} diff --git a/src/plugins/security_oss/public/insecure_cluster_service/insecure_cluster_service.test.tsx b/src/plugins/security_oss/public/insecure_cluster_service/insecure_cluster_service.test.tsx index 45240527d91ef..25eae0b11c519 100644 --- a/src/plugins/security_oss/public/insecure_cluster_service/insecure_cluster_service.test.tsx +++ b/src/plugins/security_oss/public/insecure_cluster_service/insecure_cluster_service.test.tsx @@ -7,7 +7,6 @@ */ import { nextTick } from '@kbn/test/jest'; - import { coreMock } from 'src/core/public/mocks'; import { mockAppStateService } from '../app_state/app_state_service.mock'; diff --git a/src/plugins/security_oss/public/insecure_cluster_service/insecure_cluster_service.tsx b/src/plugins/security_oss/public/insecure_cluster_service/insecure_cluster_service.tsx index 6ad84b80eaf70..6cb2079cbe954 100644 --- a/src/plugins/security_oss/public/insecure_cluster_service/insecure_cluster_service.tsx +++ b/src/plugins/security_oss/public/insecure_cluster_service/insecure_cluster_service.tsx @@ -11,8 +11,8 @@ import { distinctUntilChanged, map } from 'rxjs/operators'; import type { CoreSetup, CoreStart, MountPoint, Toast } from 'src/core/public'; -import type { ConfigType } from '../config'; import type { AppStateServiceStart } from '../app_state'; +import type { ConfigType } from '../config'; import { defaultAlertText, defaultAlertTitle } from './components'; interface SetupDeps { diff --git a/src/plugins/spaces_oss/.eslintrc.json b/src/plugins/spaces_oss/.eslintrc.json deleted file mode 100644 index 2b63a9259d220..0000000000000 --- a/src/plugins/spaces_oss/.eslintrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "rules": { - "@typescript-eslint/consistent-type-imports": 1 - } -} diff --git a/src/plugins/spaces_oss/public/api.ts b/src/plugins/spaces_oss/public/api.ts index 1d33408d645ea..3f562bcbed8e2 100644 --- a/src/plugins/spaces_oss/public/api.ts +++ b/src/plugins/spaces_oss/public/api.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import type { Observable } from 'rxjs'; import type { ReactElement } from 'react'; +import type { Observable } from 'rxjs'; import type { Space } from '../common'; diff --git a/src/plugins/spaces_oss/public/mocks/index.ts b/src/plugins/spaces_oss/public/mocks/index.ts index 8061faa00b089..dc7b9e34fe822 100644 --- a/src/plugins/spaces_oss/public/mocks/index.ts +++ b/src/plugins/spaces_oss/public/mocks/index.ts @@ -6,9 +6,8 @@ * Side Public License, v 1. */ -import { spacesApiMock } from '../api.mock'; - import type { SpacesOssPluginSetup, SpacesOssPluginStart } from '../'; +import { spacesApiMock } from '../api.mock'; const createSetupContract = (): jest.Mocked => ({ registerSpacesApi: jest.fn(), diff --git a/src/plugins/spaces_oss/public/plugin.ts b/src/plugins/spaces_oss/public/plugin.ts index 52a8fd0d570d2..2531453257e3e 100644 --- a/src/plugins/spaces_oss/public/plugin.ts +++ b/src/plugins/spaces_oss/public/plugin.ts @@ -8,8 +8,8 @@ import type { Plugin } from 'src/core/public'; -import type { SpacesOssPluginSetup, SpacesOssPluginStart } from './types'; import type { SpacesApi } from './api'; +import type { SpacesOssPluginSetup, SpacesOssPluginStart } from './types'; export class SpacesOssPlugin implements Plugin { private api?: SpacesApi; diff --git a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts index 45db15bbdedb0..47c6736ff9aea 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts @@ -14,6 +14,7 @@ import { createCollectorFetchContextMock, } from '../../../usage_collection/server/mocks'; import { elasticsearchServiceMock, httpServerMock } from '../../../../../src/core/server/mocks'; +import { StatsCollectionConfig } from '../../../telemetry_collection_manager/server'; function mockUsageCollection(kibanaUsage = {}) { const usageCollection = usageCollectionPluginMock.createSetupContract(); @@ -69,13 +70,16 @@ function mockGetLocalStats(clusterInfo: any, clusterStats: any) { return esClient; } -function mockStatsCollectionConfig(clusterInfo: any, clusterStats: any, kibana: {}) { +function mockStatsCollectionConfig( + clusterInfo: any, + clusterStats: any, + kibana: {} +): StatsCollectionConfig { return { ...createCollectorFetchContextMock(), esClient: mockGetLocalStats(clusterInfo, clusterStats), usageCollection: mockUsageCollection(kibana), kibanaRequest: httpServerMock.createKibanaRequest(), - timestamp: Date.now(), }; } @@ -227,7 +231,7 @@ describe('get_local_stats', () => { const statsCollectionConfig = mockStatsCollectionConfig(clusterInfo, clusterStats, kibana); const response = await getLocalStats( [{ clusterUuid: 'abc123' }], - { ...statsCollectionConfig }, + statsCollectionConfig, context ); const result = response[0]; @@ -243,7 +247,7 @@ describe('get_local_stats', () => { it('returns an empty array when no cluster uuid is provided', async () => { const statsCollectionConfig = mockStatsCollectionConfig(clusterInfo, clusterStats, kibana); - const response = await getLocalStats([], { ...statsCollectionConfig }, context); + const response = await getLocalStats([], statsCollectionConfig, context); expect(response).toBeDefined(); expect(response.length).toEqual(0); }); diff --git a/src/plugins/telemetry_collection_manager/server/plugin.test.ts b/src/plugins/telemetry_collection_manager/server/plugin.test.ts new file mode 100644 index 0000000000000..ac3904ca58b0f --- /dev/null +++ b/src/plugins/telemetry_collection_manager/server/plugin.test.ts @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { coreMock, httpServerMock } from '../../../core/server/mocks'; +import { usageCollectionPluginMock } from '../../usage_collection/server/mocks'; +import { TelemetryCollectionManagerPlugin } from './plugin'; +import { CollectionStrategyConfig, StatsGetterConfig } from './types'; +import { TelemetrySavedObjectsClient } from './telemetry_saved_objects_client'; + +function createCollectionStrategy(priority: number): jest.Mocked { + return { + title: 'test_collection', + priority, + statsGetter: jest.fn(), + clusterDetailsGetter: jest.fn(), + }; +} + +describe('Telemetry Collection Manager', () => { + const initializerContext = coreMock.createPluginInitializerContext(); + const usageCollection = usageCollectionPluginMock.createSetupContract(); + + describe('everything works when no collection mechanisms are registered', () => { + const telemetryCollectionManager = new TelemetryCollectionManagerPlugin(initializerContext); + const setupApi = telemetryCollectionManager.setup(coreMock.createSetup(), { usageCollection }); + test('All collectors are ready (there are none)', async () => { + await expect(setupApi.areAllCollectorsReady()).resolves.toBe(true); + }); + test('getStats returns empty', async () => { + const config: StatsGetterConfig = { unencrypted: false }; + await expect(setupApi.getStats(config)).resolves.toStrictEqual([]); + }); + test('getOptInStats returns empty', async () => { + const config: StatsGetterConfig = { unencrypted: false }; + await expect(setupApi.getOptInStats(true, config)).resolves.toStrictEqual([]); + }); + }); + + describe('With a registered collection strategy', () => { + const telemetryCollectionManager = new TelemetryCollectionManagerPlugin(initializerContext); + const setupApi = telemetryCollectionManager.setup(coreMock.createSetup(), { usageCollection }); + const collectionStrategy = createCollectionStrategy(1); + + describe('before start', () => { + test('registers a collection strategy', () => { + const zeroCollectionStrategy = createCollectionStrategy(0); + expect(setupApi.setCollectionStrategy(zeroCollectionStrategy)).toBeUndefined(); + // eslint-disable-next-line dot-notation + expect(telemetryCollectionManager['collectionStrategy']).toStrictEqual( + zeroCollectionStrategy + ); + // eslint-disable-next-line dot-notation + expect(telemetryCollectionManager['usageGetterMethodPriority']).toBe(0); + }); + test('register a higher-priority collection strategy', () => { + expect(setupApi.setCollectionStrategy(collectionStrategy)).toBeUndefined(); + // eslint-disable-next-line dot-notation + expect(telemetryCollectionManager['collectionStrategy']).toStrictEqual(collectionStrategy); + // eslint-disable-next-line dot-notation + expect(telemetryCollectionManager['usageGetterMethodPriority']).toBe(1); + }); + test('fails to register the collection strategy with the same priority', () => { + expect(() => setupApi.setCollectionStrategy(createCollectionStrategy(1))).toThrowError( + `A Usage Getter with the same priority is already set.` + ); + }); + test('do not register a collection strategy with lower priority', () => { + expect(setupApi.setCollectionStrategy(createCollectionStrategy(0))).toBeUndefined(); + // eslint-disable-next-line dot-notation + expect(telemetryCollectionManager['collectionStrategy']).toStrictEqual(collectionStrategy); + // eslint-disable-next-line dot-notation + expect(telemetryCollectionManager['usageGetterMethodPriority']).toBe(1); + }); + test('getStats returns empty because ES and SO clients are not initialized yet', async () => { + const config: StatsGetterConfig = { unencrypted: false }; + await expect(setupApi.getStats(config)).resolves.toStrictEqual([]); + }); + test('getOptInStats returns empty because ES and SO clients are not initialized yet', async () => { + const config: StatsGetterConfig = { unencrypted: false }; + await expect(setupApi.getOptInStats(true, config)).resolves.toStrictEqual([]); + }); + }); + + describe(`after start`, () => { + beforeAll(() => { + telemetryCollectionManager.start(coreMock.createStart()); + }); + afterEach(() => { + collectionStrategy.clusterDetailsGetter.mockClear(); + collectionStrategy.statsGetter.mockClear(); + }); + describe('unencrypted: false', () => { + const config: StatsGetterConfig = { unencrypted: false }; + + test('getStats returns empty because clusterDetails returns empty, and the soClient is an instance of the TelemetrySavedObjectsClient', async () => { + collectionStrategy.clusterDetailsGetter.mockResolvedValue([]); + await expect(setupApi.getStats(config)).resolves.toStrictEqual([]); + expect(collectionStrategy.clusterDetailsGetter.mock.calls[0][0].soClient).toBeInstanceOf( + TelemetrySavedObjectsClient + ); + }); + test('getOptInStats returns empty', async () => { + collectionStrategy.clusterDetailsGetter.mockResolvedValue([]); + await expect(setupApi.getOptInStats(true, config)).resolves.toStrictEqual([]); + expect(collectionStrategy.clusterDetailsGetter.mock.calls[0][0].soClient).toBeInstanceOf( + TelemetrySavedObjectsClient + ); + }); + }); + describe('unencrypted: true', () => { + const config: StatsGetterConfig = { + unencrypted: true, + request: httpServerMock.createKibanaRequest(), + }; + + test('getStats returns empty because clusterDetails returns empty, and the soClient is not an instance of the TelemetrySavedObjectsClient', async () => { + collectionStrategy.clusterDetailsGetter.mockResolvedValue([]); + await expect(setupApi.getStats(config)).resolves.toStrictEqual([]); + expect( + collectionStrategy.clusterDetailsGetter.mock.calls[0][0].soClient + ).not.toBeInstanceOf(TelemetrySavedObjectsClient); + }); + test('getOptInStats returns empty', async () => { + collectionStrategy.clusterDetailsGetter.mockResolvedValue([]); + await expect(setupApi.getOptInStats(true, config)).resolves.toStrictEqual([]); + expect( + collectionStrategy.clusterDetailsGetter.mock.calls[0][0].soClient + ).not.toBeInstanceOf(TelemetrySavedObjectsClient); + }); + }); + }); + }); +}); diff --git a/src/plugins/telemetry_collection_manager/server/plugin.ts b/src/plugins/telemetry_collection_manager/server/plugin.ts index f86d3e4773d62..0fec225d5c424 100644 --- a/src/plugins/telemetry_collection_manager/server/plugin.ts +++ b/src/plugins/telemetry_collection_manager/server/plugin.ts @@ -15,6 +15,8 @@ import { Logger, IClusterClient, SavedObjectsServiceStart, + ElasticsearchClient, + SavedObjectsClientContract, } from 'src/core/server'; import { @@ -30,6 +32,7 @@ import { } from './types'; import { isClusterOptedIn } from './util'; import { encryptTelemetry } from './encryption'; +import { TelemetrySavedObjectsClient } from './telemetry_saved_objects_client'; interface TelemetryCollectionPluginsDepsSetup { usageCollection: UsageCollectionSetup; @@ -115,14 +118,8 @@ export class TelemetryCollectionManagerPlugin config: StatsGetterConfig, usageCollection: UsageCollectionSetup ): StatsCollectionConfig | undefined { - // Scope the new elasticsearch Client appropriately and pass to the stats collection config - const esClient = config.unencrypted - ? this.elasticsearchClient?.asScoped(config.request).asCurrentUser - : this.elasticsearchClient?.asInternalUser; - // Scope the saved objects client appropriately and pass to the stats collection config - const soClient = config.unencrypted - ? this.savedObjectsService?.getScopedClient(config.request) - : this.savedObjectsService?.createInternalRepository(); + const esClient = this.getElasticsearchClient(config); + const soClient = this.getSavedObjectsClient(config); // Provide the kibanaRequest so opted-in plugins can scope their custom clients only if the request is not encrypted const kibanaRequest = config.unencrypted ? config.request : void 0; @@ -131,6 +128,39 @@ export class TelemetryCollectionManagerPlugin } } + /** + * Returns the ES client scoped to the requester or Kibana's internal user + * depending on whether the request is encrypted or not: + * If the request is unencrypted, we intentionally scope the results to "what the user can see". + * @param config {@link StatsGetterConfig} + * @private + */ + private getElasticsearchClient(config: StatsGetterConfig): ElasticsearchClient | undefined { + return config.unencrypted + ? this.elasticsearchClient?.asScoped(config.request).asCurrentUser + : this.elasticsearchClient?.asInternalUser; + } + + /** + * Returns the SavedObjects client scoped to the requester or Kibana's internal user + * depending on whether the request is encrypted or not: + * If the request is unencrypted, we intentionally scope the results to "what the user can see" + * @param config {@link StatsGetterConfig} + * @private + */ + private getSavedObjectsClient(config: StatsGetterConfig): SavedObjectsClientContract | undefined { + if (config.unencrypted) { + // Intentionally using the scoped client here to make use of all the security wrappers. + // It also returns spaces-scoped telemetry. + return this.savedObjectsService?.getScopedClient(config.request); + } else if (this.savedObjectsService) { + // Wrapping the internalRepository with the `TelemetrySavedObjectsClient` + // to ensure some best practices when collecting "all the telemetry" + // (i.e.: `.find` requests should query all spaces) + return new TelemetrySavedObjectsClient(this.savedObjectsService.createInternalRepository()); + } + } + private async getOptInStats(optInStatus: boolean, config: StatsGetterConfig) { if (!this.usageCollection) { return []; diff --git a/src/plugins/telemetry_collection_manager/server/telemetry_saved_objects_client.test.ts b/src/plugins/telemetry_collection_manager/server/telemetry_saved_objects_client.test.ts new file mode 100644 index 0000000000000..a1aa9a3070d01 --- /dev/null +++ b/src/plugins/telemetry_collection_manager/server/telemetry_saved_objects_client.test.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { TelemetrySavedObjectsClient } from './telemetry_saved_objects_client'; +import { savedObjectsRepositoryMock } from '../../../core/server/mocks'; + +describe('TelemetrySavedObjectsClient', () => { + test("find requests are extended with `namespaces:['*']`", async () => { + const savedObjectsRepository = savedObjectsRepositoryMock.create(); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsRepository); + await telemetrySavedObjectsClient.find({ type: 'my-test-type' }); + expect(savedObjectsRepository.find).toBeCalledWith({ type: 'my-test-type', namespaces: ['*'] }); + }); + test("allow callers to overwrite the `namespaces:['*']`", async () => { + const savedObjectsRepository = savedObjectsRepositoryMock.create(); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsRepository); + await telemetrySavedObjectsClient.find({ type: 'my-test-type', namespaces: ['some_space'] }); + expect(savedObjectsRepository.find).toBeCalledWith({ + type: 'my-test-type', + namespaces: ['some_space'], + }); + }); +}); diff --git a/src/plugins/telemetry_collection_manager/server/telemetry_saved_objects_client.ts b/src/plugins/telemetry_collection_manager/server/telemetry_saved_objects_client.ts new file mode 100644 index 0000000000000..d639b053565d1 --- /dev/null +++ b/src/plugins/telemetry_collection_manager/server/telemetry_saved_objects_client.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { SavedObjectsFindOptions, SavedObjectsFindResponse } from 'src/core/server'; +import { SavedObjectsClient } from '../../../core/server'; + +/** + * Extends the SavedObjectsClient to fit the telemetry fetching requirements (i.e.: find objects from all namespaces by default) + */ +export class TelemetrySavedObjectsClient extends SavedObjectsClient { + /** + * Find the SavedObjects matching the search query in all the Spaces by default + * @param options + */ + async find(options: SavedObjectsFindOptions): Promise> { + return super.find({ namespaces: ['*'], ...options }); + } +} diff --git a/src/plugins/telemetry_collection_manager/server/types.ts b/src/plugins/telemetry_collection_manager/server/types.ts index c98aff03f5d07..985eff409c1de 100644 --- a/src/plugins/telemetry_collection_manager/server/types.ts +++ b/src/plugins/telemetry_collection_manager/server/types.ts @@ -11,7 +11,6 @@ import { Logger, KibanaRequest, SavedObjectsClientContract, - ISavedObjectsRepository, } from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { TelemetryCollectionManagerPlugin } from './plugin'; @@ -57,7 +56,7 @@ export interface ClusterDetails { export interface StatsCollectionConfig { usageCollection: UsageCollectionSetup; esClient: ElasticsearchClient; - soClient: SavedObjectsClientContract | ISavedObjectsRepository; + soClient: SavedObjectsClientContract; kibanaRequest: KibanaRequest | undefined; // intentionally `| undefined` to enforce providing the parameter } diff --git a/src/plugins/usage_collection/server/collector/collector.ts b/src/plugins/usage_collection/server/collector/collector.ts index 8e114df440cbe..90e873388d22e 100644 --- a/src/plugins/usage_collection/server/collector/collector.ts +++ b/src/plugins/usage_collection/server/collector/collector.ts @@ -6,10 +6,9 @@ * Side Public License, v 1. */ -import { +import type { Logger, ElasticsearchClient, - ISavedObjectsRepository, SavedObjectsClientContract, KibanaRequest, } from 'src/core/server'; @@ -72,7 +71,7 @@ export type CollectorFetchContext { export function createCollectorFetchContextMock(): jest.Mocked> { const collectorFetchClientsMock: jest.Mocked> = { esClient: elasticsearchServiceMock.createClusterClient().asInternalUser, - soClient: savedObjectsRepositoryMock.create(), + soClient: savedObjectsClientMock.create(), }; return collectorFetchClientsMock; } @@ -50,7 +50,7 @@ export function createCollectorFetchContextWithKibanaMock(): jest.Mocked< > { const collectorFetchClientsMock: jest.Mocked> = { esClient: elasticsearchServiceMock.createClusterClient().asInternalUser, - soClient: savedObjectsRepositoryMock.create(), + soClient: savedObjectsClientMock.create(), kibanaRequest: httpServerMock.createKibanaRequest(), }; return collectorFetchClientsMock; diff --git a/test/api_integration/apis/telemetry/telemetry_local.ts b/test/api_integration/apis/telemetry/telemetry_local.ts index 9d3ced5224502..85d94cbf46dda 100644 --- a/test/api_integration/apis/telemetry/telemetry_local.ts +++ b/test/api_integration/apis/telemetry/telemetry_local.ts @@ -256,6 +256,7 @@ export default function ({ getService }: FtrProviderContext) { await es.deleteByQuery({ index: '.kibana', body: { query: { term: { type: 'application_usage_transactional' } } }, + conflicts: 'proceed', }); }); diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index c941bc1fab772..2703cbad32460 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -56,7 +56,7 @@ interface Action extends ActionUpdate { actionTypeId: string; } -interface CreateOptions { +export interface CreateOptions { action: Action; } @@ -73,7 +73,7 @@ interface ConstructorOptions { auditLogger?: AuditLogger; } -interface UpdateOptions { +export interface UpdateOptions { id: string; action: ActionUpdate; } diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index c4159c80e806f..d9d34e99bffea 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -50,15 +50,7 @@ import { import { getActionsConfigurationUtilities } from './actions_config'; -import { - createActionRoute, - deleteActionRoute, - getAllActionRoute, - getActionRoute, - updateActionRoute, - listActionTypesRoute, - executeActionRoute, -} from './routes'; +import { defineRoutes } from './routes'; import { IEventLogger, IEventLogService } from '../../event_log/server'; import { initializeActionsTelemetry, scheduleActionsTelemetry } from './usage/task'; import { @@ -237,14 +229,7 @@ export class ActionsPlugin implements Plugin(); - createActionRoute(router, this.licenseState); - deleteActionRoute(router, this.licenseState); - getActionRoute(router, this.licenseState); - getAllActionRoute(router, this.licenseState); - updateActionRoute(router, this.licenseState); - listActionTypesRoute(router, this.licenseState); - executeActionRoute(router, this.licenseState); + defineRoutes(core.http.createRouter(), this.licenseState); return { registerType: < diff --git a/x-pack/plugins/actions/server/routes/connector_types.test.ts b/x-pack/plugins/actions/server/routes/connector_types.test.ts new file mode 100644 index 0000000000000..ebbdc703556d5 --- /dev/null +++ b/x-pack/plugins/actions/server/routes/connector_types.test.ts @@ -0,0 +1,157 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { connectorTypesRoute } from './connector_types'; +import { httpServiceMock } from 'src/core/server/mocks'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { mockHandlerArguments } from './legacy/_mock_handler_arguments'; +import { LicenseType } from '../../../../plugins/licensing/server'; +import { actionsClientMock } from '../mocks'; +import { verifyAccessAndContext } from './verify_access_and_context'; + +jest.mock('./verify_access_and_context.ts', () => ({ + verifyAccessAndContext: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); + (verifyAccessAndContext as jest.Mock).mockImplementation((license, handler) => handler); +}); + +describe('connectorTypesRoute', () => { + it('lists action types with proper parameters', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + connectorTypesRoute(router, licenseState); + + const [config, handler] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/actions/connector_types"`); + + const listTypes = [ + { + id: '1', + name: 'name', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'gold' as LicenseType, + }, + ]; + + const actionsClient = actionsClientMock.create(); + actionsClient.listTypes.mockResolvedValueOnce(listTypes); + const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok']); + + expect(await handler(context, req, res)).toMatchInlineSnapshot(` + Object { + "body": Array [ + Object { + "enabled": true, + "enabled_in_config": true, + "enabled_in_license": true, + "id": "1", + "minimum_license_required": "gold", + "name": "name", + }, + ], + } + `); + + expect(res.ok).toHaveBeenCalledWith({ + body: [ + { + id: '1', + name: 'name', + enabled: true, + enabled_in_config: true, + enabled_in_license: true, + minimum_license_required: 'gold', + }, + ], + }); + }); + + it('ensures the license allows listing action types', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + connectorTypesRoute(router, licenseState); + + const [config, handler] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/actions/connector_types"`); + + const listTypes = [ + { + id: '1', + name: 'name', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'gold' as LicenseType, + }, + ]; + + const actionsClient = actionsClientMock.create(); + actionsClient.listTypes.mockResolvedValueOnce(listTypes); + + const [context, req, res] = mockHandlerArguments( + { actionsClient }, + { + params: { id: '1' }, + }, + ['ok'] + ); + + await handler(context, req, res); + + expect(verifyAccessAndContext).toHaveBeenCalledWith(licenseState, expect.any(Function)); + }); + + it('ensures the license check prevents listing action types', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + (verifyAccessAndContext as jest.Mock).mockImplementation(() => async () => { + throw new Error('OMG'); + }); + + connectorTypesRoute(router, licenseState); + + const [config, handler] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/actions/connector_types"`); + + const listTypes = [ + { + id: '1', + name: 'name', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'gold' as LicenseType, + }, + ]; + + const actionsClient = actionsClientMock.create(); + actionsClient.listTypes.mockResolvedValueOnce(listTypes); + + const [context, req, res] = mockHandlerArguments( + { actionsClient }, + { + params: { id: '1' }, + }, + ['ok'] + ); + + expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); + + expect(verifyAccessAndContext).toHaveBeenCalledWith(licenseState, expect.any(Function)); + }); +}); diff --git a/x-pack/plugins/actions/server/routes/connector_types.ts b/x-pack/plugins/actions/server/routes/connector_types.ts new file mode 100644 index 0000000000000..d686ddbdaee70 --- /dev/null +++ b/x-pack/plugins/actions/server/routes/connector_types.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IRouter } from 'kibana/server'; +import { ILicenseState } from '../lib'; +import { ActionType, BASE_ACTION_API_PATH } from '../../common'; +import { ActionsRequestHandlerContext } from '../types'; +import { verifyAccessAndContext } from './verify_access_and_context'; +import { RewriteResponseCase } from './rewrite_request_case'; + +const rewriteBodyRes: RewriteResponseCase = (results) => { + return results.map(({ enabledInConfig, enabledInLicense, minimumLicenseRequired, ...res }) => ({ + ...res, + enabled_in_config: enabledInConfig, + enabled_in_license: enabledInLicense, + minimum_license_required: minimumLicenseRequired, + })); +}; + +export const connectorTypesRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.get( + { + path: `${BASE_ACTION_API_PATH}/connector_types`, + validate: {}, + }, + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const actionsClient = context.actions.getActionsClient(); + return res.ok({ + body: rewriteBodyRes(await actionsClient.listTypes()), + }); + }) + ) + ); +}; diff --git a/x-pack/plugins/actions/server/routes/create.test.ts b/x-pack/plugins/actions/server/routes/create.test.ts index b917dbcc23ff2..e5d8e6f5861f3 100644 --- a/x-pack/plugins/actions/server/routes/create.test.ts +++ b/x-pack/plugins/actions/server/routes/create.test.ts @@ -8,16 +8,18 @@ import { createActionRoute } from './create'; import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; -import { verifyApiAccess, ActionTypeDisabledError } from '../lib'; -import { mockHandlerArguments } from './_mock_handler_arguments'; +import { mockHandlerArguments } from './legacy/_mock_handler_arguments'; import { actionsClientMock } from '../actions_client.mock'; +import { verifyAccessAndContext } from './verify_access_and_context'; +import { omit } from 'lodash'; -jest.mock('../lib/verify_api_access.ts', () => ({ - verifyApiAccess: jest.fn(), +jest.mock('./verify_access_and_context.ts', () => ({ + verifyAccessAndContext: jest.fn(), })); beforeEach(() => { jest.resetAllMocks(); + (verifyAccessAndContext as jest.Mock).mockImplementation((license, handler) => handler); }); describe('createActionRoute', () => { @@ -29,7 +31,7 @@ describe('createActionRoute', () => { const [config, handler] = router.post.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/actions/action"`); + expect(config.path).toMatchInlineSnapshot(`"/api/actions/connector"`); const createResult = { id: '1', @@ -39,6 +41,12 @@ describe('createActionRoute', () => { isPreconfigured: false, }; + const createApiResult = { + ...omit(createResult, ['actionTypeId', 'isPreconfigured']), + connector_type_id: createResult.actionTypeId, + is_preconfigured: createResult.isPreconfigured, + }; + const actionsClient = actionsClientMock.create(); actionsClient.create.mockResolvedValueOnce(createResult); @@ -47,7 +55,7 @@ describe('createActionRoute', () => { { body: { name: 'My name', - actionTypeId: 'abc', + connector_type_id: 'abc', config: { foo: true }, secrets: {}, }, @@ -55,7 +63,7 @@ describe('createActionRoute', () => { ['ok'] ); - expect(await handler(context, req, res)).toEqual({ body: createResult }); + expect(await handler(context, req, res)).toEqual({ body: createApiResult }); expect(actionsClient.create).toHaveBeenCalledTimes(1); expect(actionsClient.create.mock.calls[0]).toMatchInlineSnapshot(` @@ -74,7 +82,7 @@ describe('createActionRoute', () => { `); expect(res.ok).toHaveBeenCalledWith({ - body: createResult, + body: createApiResult, }); }); @@ -95,18 +103,28 @@ describe('createActionRoute', () => { isPreconfigured: false, }); - const [context, req, res] = mockHandlerArguments({ actionsClient }, {}); + const [context, req, res] = mockHandlerArguments( + { actionsClient }, + { + body: { + name: 'My name', + connector_type_id: 'abc', + config: { foo: true }, + secrets: {}, + }, + } + ); await handler(context, req, res); - expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + expect(verifyAccessAndContext).toHaveBeenCalledWith(licenseState, expect.any(Function)); }); it('ensures the license check prevents creating actions', async () => { const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); - (verifyApiAccess as jest.Mock).mockImplementation(() => { + (verifyAccessAndContext as jest.Mock).mockImplementation(() => async () => { throw new Error('OMG'); }); @@ -123,28 +141,18 @@ describe('createActionRoute', () => { isPreconfigured: false, }); - const [context, req, res] = mockHandlerArguments({ actionsClient }, {}); + const [context, req, res] = mockHandlerArguments( + { actionsClient }, + { + body: { + name: 'My name', + connector_type_id: 'abc', + config: { foo: true }, + secrets: {}, + }, + } + ); expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); - - expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); - }); - - it('ensures the action type gets validated for the license', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - createActionRoute(router, licenseState); - - const [, handler] = router.post.mock.calls[0]; - - const actionsClient = actionsClientMock.create(); - actionsClient.create.mockRejectedValue(new ActionTypeDisabledError('Fail', 'license_invalid')); - - const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok', 'forbidden']); - - await handler(context, req, res); - - expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); }); }); diff --git a/x-pack/plugins/actions/server/routes/create.ts b/x-pack/plugins/actions/server/routes/create.ts index a64264412fc47..e1717891231db 100644 --- a/x-pack/plugins/actions/server/routes/create.ts +++ b/x-pack/plugins/actions/server/routes/create.ts @@ -8,46 +8,54 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from 'kibana/server'; import { ActionResult, ActionsRequestHandlerContext } from '../types'; -import { ILicenseState, verifyApiAccess, isErrorThatHandlesItsOwnResponse } from '../lib'; +import { ILicenseState } from '../lib'; import { BASE_ACTION_API_PATH } from '../../common'; +import { verifyAccessAndContext } from './verify_access_and_context'; +import { RewriteRequestCase, RewriteResponseCase } from './rewrite_request_case'; +import { CreateOptions } from '../actions_client'; export const bodySchema = schema.object({ name: schema.string(), - actionTypeId: schema.string(), + connector_type_id: schema.string(), config: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), secrets: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), }); +const rewriteBodyReq: RewriteRequestCase = ({ + connector_type_id: actionTypeId, + name, + config, + secrets, +}) => ({ actionTypeId, name, config, secrets }); +const rewriteBodyRes: RewriteResponseCase = ({ + actionTypeId, + isPreconfigured, + ...res +}) => ({ + ...res, + connector_type_id: actionTypeId, + is_preconfigured: isPreconfigured, +}); + export const createActionRoute = ( router: IRouter, licenseState: ILicenseState ) => { router.post( { - path: `${BASE_ACTION_API_PATH}/action`, + path: `${BASE_ACTION_API_PATH}/connector`, validate: { body: bodySchema, }, }, - router.handleLegacyErrors(async function (context, req, res) { - verifyApiAccess(licenseState); - - if (!context.actions) { - return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' }); - } - const actionsClient = context.actions.getActionsClient(); - const action = req.body; - try { - const actionRes: ActionResult = await actionsClient.create({ action }); + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const actionsClient = context.actions.getActionsClient(); + const action = rewriteBodyReq(req.body); return res.ok({ - body: actionRes, + body: rewriteBodyRes(await actionsClient.create({ action })), }); - } catch (e) { - if (isErrorThatHandlesItsOwnResponse(e)) { - return e.sendResponse(res); - } - throw e; - } - }) + }) + ) ); }; diff --git a/x-pack/plugins/actions/server/routes/delete.test.ts b/x-pack/plugins/actions/server/routes/delete.test.ts index fa7777fb508ce..438f366e5cb73 100644 --- a/x-pack/plugins/actions/server/routes/delete.test.ts +++ b/x-pack/plugins/actions/server/routes/delete.test.ts @@ -8,16 +8,17 @@ import { deleteActionRoute } from './delete'; import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; -import { verifyApiAccess } from '../lib'; -import { mockHandlerArguments } from './_mock_handler_arguments'; +import { mockHandlerArguments } from './legacy/_mock_handler_arguments'; import { actionsClientMock } from '../mocks'; +import { verifyAccessAndContext } from './verify_access_and_context'; -jest.mock('../lib/verify_api_access.ts', () => ({ - verifyApiAccess: jest.fn(), +jest.mock('./verify_access_and_context.ts', () => ({ + verifyAccessAndContext: jest.fn(), })); beforeEach(() => { jest.resetAllMocks(); + (verifyAccessAndContext as jest.Mock).mockImplementation((license, handler) => handler); }); describe('deleteActionRoute', () => { @@ -29,7 +30,7 @@ describe('deleteActionRoute', () => { const [config, handler] = router.delete.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/actions/action/{id}"`); + expect(config.path).toMatchInlineSnapshot(`"/api/actions/connector/{id}"`); const actionsClient = actionsClientMock.create(); actionsClient.delete.mockResolvedValueOnce({}); @@ -78,14 +79,14 @@ describe('deleteActionRoute', () => { await handler(context, req, res); - expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + expect(verifyAccessAndContext).toHaveBeenCalledWith(licenseState, expect.any(Function)); }); it('ensures the license check prevents deleting actions', async () => { const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); - (verifyApiAccess as jest.Mock).mockImplementation(() => { + (verifyAccessAndContext as jest.Mock).mockImplementation(() => async () => { throw new Error('OMG'); }); @@ -105,6 +106,6 @@ describe('deleteActionRoute', () => { expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); - expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + expect(verifyAccessAndContext).toHaveBeenCalledWith(licenseState, expect.any(Function)); }); }); diff --git a/x-pack/plugins/actions/server/routes/delete.ts b/x-pack/plugins/actions/server/routes/delete.ts index 84da01dd2bb20..653ce32e69eb0 100644 --- a/x-pack/plugins/actions/server/routes/delete.ts +++ b/x-pack/plugins/actions/server/routes/delete.ts @@ -7,9 +7,10 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from 'kibana/server'; -import { ILicenseState, verifyApiAccess, isErrorThatHandlesItsOwnResponse } from '../lib'; +import { ILicenseState } from '../lib'; import { BASE_ACTION_API_PATH } from '../../common'; import { ActionsRequestHandlerContext } from '../types'; +import { verifyAccessAndContext } from './verify_access_and_context'; const paramSchema = schema.object({ id: schema.string(), @@ -21,27 +22,18 @@ export const deleteActionRoute = ( ) => { router.delete( { - path: `${BASE_ACTION_API_PATH}/action/{id}`, + path: `${BASE_ACTION_API_PATH}/connector/{id}`, validate: { params: paramSchema, }, }, - router.handleLegacyErrors(async function (context, req, res) { - verifyApiAccess(licenseState); - if (!context.actions) { - return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' }); - } - const actionsClient = context.actions.getActionsClient(); - const { id } = req.params; - try { + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const actionsClient = context.actions.getActionsClient(); + const { id } = req.params; await actionsClient.delete({ id }); return res.noContent(); - } catch (e) { - if (isErrorThatHandlesItsOwnResponse(e)) { - return e.sendResponse(res); - } - throw e; - } - }) + }) + ) ); }; diff --git a/x-pack/plugins/actions/server/routes/execute.test.ts b/x-pack/plugins/actions/server/routes/execute.test.ts index e07591f404e2f..4b12bf3111c1f 100644 --- a/x-pack/plugins/actions/server/routes/execute.test.ts +++ b/x-pack/plugins/actions/server/routes/execute.test.ts @@ -8,17 +8,19 @@ import { executeActionRoute } from './execute'; import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; -import { mockHandlerArguments } from './_mock_handler_arguments'; -import { verifyApiAccess, ActionTypeDisabledError, asHttpRequestExecutionSource } from '../lib'; +import { mockHandlerArguments } from './legacy/_mock_handler_arguments'; +import { asHttpRequestExecutionSource } from '../lib'; import { actionsClientMock } from '../actions_client.mock'; import { ActionTypeExecutorResult } from '../types'; +import { verifyAccessAndContext } from './verify_access_and_context'; -jest.mock('../lib/verify_api_access.ts', () => ({ - verifyApiAccess: jest.fn(), +jest.mock('./verify_access_and_context.ts', () => ({ + verifyAccessAndContext: jest.fn(), })); beforeEach(() => { jest.resetAllMocks(); + (verifyAccessAndContext as jest.Mock).mockImplementation((license, handler) => handler); }); describe('executeActionRoute', () => { @@ -45,7 +47,7 @@ describe('executeActionRoute', () => { ); const executeResult = { - actionId: '1', + connector_id: '1', status: 'ok', }; @@ -53,7 +55,7 @@ describe('executeActionRoute', () => { const [config, handler] = router.post.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/actions/action/{id}/_execute"`); + expect(config.path).toMatchInlineSnapshot(`"/api/actions/connector/{id}/_execute"`); expect(await handler(context, req, res)).toEqual({ body: executeResult }); @@ -131,7 +133,7 @@ describe('executeActionRoute', () => { await handler(context, req, res); - expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + expect(verifyAccessAndContext).toHaveBeenCalledWith(licenseState, expect.any(Function)); }); it('ensures the license check prevents action execution', async () => { @@ -144,7 +146,7 @@ describe('executeActionRoute', () => { status: 'ok', }); - (verifyApiAccess as jest.Mock).mockImplementation(() => { + (verifyAccessAndContext as jest.Mock).mockImplementation(() => async () => { throw new Error('OMG'); }); @@ -163,31 +165,6 @@ describe('executeActionRoute', () => { expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); - expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); - }); - - it('ensures the action type gets validated for the license', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - const actionsClient = actionsClientMock.create(); - actionsClient.execute.mockRejectedValue(new ActionTypeDisabledError('Fail', 'license_invalid')); - - const [context, req, res] = mockHandlerArguments( - { actionsClient }, - { - body: {}, - params: {}, - }, - ['ok', 'forbidden'] - ); - - executeActionRoute(router, licenseState); - - const [, handler] = router.post.mock.calls[0]; - - await handler(context, req, res); - - expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); + expect(verifyAccessAndContext).toHaveBeenCalledWith(licenseState, expect.any(Function)); }); }); diff --git a/x-pack/plugins/actions/server/routes/execute.ts b/x-pack/plugins/actions/server/routes/execute.ts index f87f1ee0d0890..0d1bee83ed047 100644 --- a/x-pack/plugins/actions/server/routes/execute.ts +++ b/x-pack/plugins/actions/server/routes/execute.ts @@ -7,11 +7,13 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from 'kibana/server'; -import { ILicenseState, verifyApiAccess, isErrorThatHandlesItsOwnResponse } from '../lib'; +import { ILicenseState } from '../lib'; import { ActionTypeExecutorResult, ActionsRequestHandlerContext } from '../types'; import { BASE_ACTION_API_PATH } from '../../common'; import { asHttpRequestExecutionSource } from '../lib/action_execution_source'; +import { verifyAccessAndContext } from './verify_access_and_context'; +import { RewriteResponseCase } from './rewrite_request_case'; const paramSchema = schema.object({ id: schema.string(), @@ -21,29 +23,33 @@ const bodySchema = schema.object({ params: schema.recordOf(schema.string(), schema.any()), }); +const rewriteBodyRes: RewriteResponseCase> = ({ + actionId, + serviceMessage, + ...res +}) => ({ + ...res, + connector_id: actionId, + ...(serviceMessage ? { service_message: serviceMessage } : {}), +}); + export const executeActionRoute = ( router: IRouter, licenseState: ILicenseState ) => { router.post( { - path: `${BASE_ACTION_API_PATH}/action/{id}/_execute`, + path: `${BASE_ACTION_API_PATH}/connector/{id}/_execute`, validate: { body: bodySchema, params: paramSchema, }, }, - router.handleLegacyErrors(async function (context, req, res) { - verifyApiAccess(licenseState); - - if (!context.actions) { - return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' }); - } - - const actionsClient = context.actions.getActionsClient(); - const { params } = req.body; - const { id } = req.params; - try { + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const actionsClient = context.actions.getActionsClient(); + const { params } = req.body; + const { id } = req.params; const body: ActionTypeExecutorResult = await actionsClient.execute({ params, actionId: id, @@ -51,15 +57,10 @@ export const executeActionRoute = ( }); return body ? res.ok({ - body, + body: rewriteBodyRes(body), }) : res.noContent(); - } catch (e) { - if (isErrorThatHandlesItsOwnResponse(e)) { - return e.sendResponse(res); - } - throw e; - } - }) + }) + ) ); }; diff --git a/x-pack/plugins/actions/server/routes/get.test.ts b/x-pack/plugins/actions/server/routes/get.test.ts index d2625f8f434a5..6a42f3b27370e 100644 --- a/x-pack/plugins/actions/server/routes/get.test.ts +++ b/x-pack/plugins/actions/server/routes/get.test.ts @@ -8,16 +8,17 @@ import { getActionRoute } from './get'; import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; -import { verifyApiAccess } from '../lib'; -import { mockHandlerArguments } from './_mock_handler_arguments'; +import { mockHandlerArguments } from './legacy/_mock_handler_arguments'; import { actionsClientMock } from '../actions_client.mock'; +import { verifyAccessAndContext } from './verify_access_and_context'; -jest.mock('../lib/verify_api_access.ts', () => ({ - verifyApiAccess: jest.fn(), +jest.mock('./verify_access_and_context.ts', () => ({ + verifyAccessAndContext: jest.fn(), })); beforeEach(() => { jest.resetAllMocks(); + (verifyAccessAndContext as jest.Mock).mockImplementation((license, handler) => handler); }); describe('getActionRoute', () => { @@ -29,7 +30,7 @@ describe('getActionRoute', () => { const [config, handler] = router.get.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/actions/action/{id}"`); + expect(config.path).toMatchInlineSnapshot(`"/api/actions/connector/{id}"`); const getResult = { id: '1', @@ -53,10 +54,10 @@ describe('getActionRoute', () => { expect(await handler(context, req, res)).toMatchInlineSnapshot(` Object { "body": Object { - "actionTypeId": "2", "config": Object {}, + "connector_type_id": "2", "id": "1", - "isPreconfigured": false, + "is_preconfigured": false, "name": "action name", }, } @@ -66,7 +67,13 @@ describe('getActionRoute', () => { expect(actionsClient.get.mock.calls[0][0].id).toEqual('1'); expect(res.ok).toHaveBeenCalledWith({ - body: getResult, + body: { + id: '1', + connector_type_id: '2', + name: 'action name', + config: {}, + is_preconfigured: false, + }, }); }); @@ -97,14 +104,14 @@ describe('getActionRoute', () => { await handler(context, req, res); - expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + expect(verifyAccessAndContext).toHaveBeenCalledWith(licenseState, expect.any(Function)); }); it('ensures the license check prevents getting actions', async () => { const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); - (verifyApiAccess as jest.Mock).mockImplementation(() => { + (verifyAccessAndContext as jest.Mock).mockImplementation(() => async () => { throw new Error('OMG'); }); @@ -131,6 +138,6 @@ describe('getActionRoute', () => { expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); - expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + expect(verifyAccessAndContext).toHaveBeenCalledWith(licenseState, expect.any(Function)); }); }); diff --git a/x-pack/plugins/actions/server/routes/get.ts b/x-pack/plugins/actions/server/routes/get.ts index 436b6bed9970f..63f89d6b3ca49 100644 --- a/x-pack/plugins/actions/server/routes/get.ts +++ b/x-pack/plugins/actions/server/routes/get.ts @@ -7,35 +7,45 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from 'kibana/server'; -import { ILicenseState, verifyApiAccess } from '../lib'; +import { ILicenseState } from '../lib'; import { BASE_ACTION_API_PATH } from '../../common'; -import { ActionsRequestHandlerContext } from '../types'; +import { ActionResult, ActionsRequestHandlerContext } from '../types'; +import { verifyAccessAndContext } from './verify_access_and_context'; +import { RewriteResponseCase } from './rewrite_request_case'; const paramSchema = schema.object({ id: schema.string(), }); +const rewriteBodyRes: RewriteResponseCase = ({ + actionTypeId, + isPreconfigured, + ...res +}) => ({ + ...res, + connector_type_id: actionTypeId, + is_preconfigured: isPreconfigured, +}); + export const getActionRoute = ( router: IRouter, licenseState: ILicenseState ) => { router.get( { - path: `${BASE_ACTION_API_PATH}/action/{id}`, + path: `${BASE_ACTION_API_PATH}/connector/{id}`, validate: { params: paramSchema, }, }, - router.handleLegacyErrors(async function (context, req, res) { - verifyApiAccess(licenseState); - if (!context.actions) { - return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' }); - } - const actionsClient = context.actions.getActionsClient(); - const { id } = req.params; - return res.ok({ - body: await actionsClient.get({ id }), - }); - }) + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const actionsClient = context.actions.getActionsClient(); + const { id } = req.params; + return res.ok({ + body: rewriteBodyRes(await actionsClient.get({ id })), + }); + }) + ) ); }; diff --git a/x-pack/plugins/actions/server/routes/get_all.test.ts b/x-pack/plugins/actions/server/routes/get_all.test.ts index c57a2ef6d7526..69684457f3dd8 100644 --- a/x-pack/plugins/actions/server/routes/get_all.test.ts +++ b/x-pack/plugins/actions/server/routes/get_all.test.ts @@ -8,16 +8,17 @@ import { getAllActionRoute } from './get_all'; import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; -import { verifyApiAccess } from '../lib'; -import { mockHandlerArguments } from './_mock_handler_arguments'; +import { mockHandlerArguments } from './legacy/_mock_handler_arguments'; import { actionsClientMock } from '../actions_client.mock'; +import { verifyAccessAndContext } from './verify_access_and_context'; -jest.mock('../lib/verify_api_access.ts', () => ({ - verifyApiAccess: jest.fn(), +jest.mock('./verify_access_and_context.ts', () => ({ + verifyAccessAndContext: jest.fn(), })); beforeEach(() => { jest.resetAllMocks(); + (verifyAccessAndContext as jest.Mock).mockImplementation((license, handler) => handler); }); describe('getAllActionRoute', () => { @@ -29,7 +30,7 @@ describe('getAllActionRoute', () => { const [config, handler] = router.get.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/actions"`); + expect(config.path).toMatchInlineSnapshot(`"/api/actions/connectors"`); const actionsClient = actionsClientMock.create(); actionsClient.getAll.mockResolvedValueOnce([]); @@ -57,7 +58,7 @@ describe('getAllActionRoute', () => { const [config, handler] = router.get.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/actions"`); + expect(config.path).toMatchInlineSnapshot(`"/api/actions/connectors"`); const actionsClient = actionsClientMock.create(); actionsClient.getAll.mockResolvedValueOnce([]); @@ -66,14 +67,14 @@ describe('getAllActionRoute', () => { await handler(context, req, res); - expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + expect(verifyAccessAndContext).toHaveBeenCalledWith(licenseState, expect.any(Function)); }); it('ensures the license check prevents getting all actions', async () => { const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); - (verifyApiAccess as jest.Mock).mockImplementation(() => { + (verifyAccessAndContext as jest.Mock).mockImplementation(() => async () => { throw new Error('OMG'); }); @@ -81,7 +82,7 @@ describe('getAllActionRoute', () => { const [config, handler] = router.get.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/actions"`); + expect(config.path).toMatchInlineSnapshot(`"/api/actions/connectors"`); const actionsClient = actionsClientMock.create(); actionsClient.getAll.mockResolvedValueOnce([]); @@ -90,6 +91,6 @@ describe('getAllActionRoute', () => { expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); - expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + expect(verifyAccessAndContext).toHaveBeenCalledWith(licenseState, expect.any(Function)); }); }); diff --git a/x-pack/plugins/actions/server/routes/get_all.ts b/x-pack/plugins/actions/server/routes/get_all.ts index 825950fa3412c..32f48e32ab278 100644 --- a/x-pack/plugins/actions/server/routes/get_all.ts +++ b/x-pack/plugins/actions/server/routes/get_all.ts @@ -6,9 +6,20 @@ */ import { IRouter } from 'kibana/server'; -import { ILicenseState, verifyApiAccess } from '../lib'; +import { ILicenseState } from '../lib'; import { BASE_ACTION_API_PATH } from '../../common'; -import { ActionsRequestHandlerContext } from '../types'; +import { ActionsRequestHandlerContext, FindActionResult } from '../types'; +import { verifyAccessAndContext } from './verify_access_and_context'; +import { RewriteResponseCase } from './rewrite_request_case'; + +const rewriteBodyRes: RewriteResponseCase = (results) => { + return results.map(({ actionTypeId, isPreconfigured, referencedByCount, ...res }) => ({ + ...res, + connector_type_id: actionTypeId, + is_preconfigured: isPreconfigured, + referenced_by_count: referencedByCount, + })); +}; export const getAllActionRoute = ( router: IRouter, @@ -16,19 +27,17 @@ export const getAllActionRoute = ( ) => { router.get( { - path: `${BASE_ACTION_API_PATH}`, + path: `${BASE_ACTION_API_PATH}/connectors`, validate: {}, }, - router.handleLegacyErrors(async function (context, req, res) { - verifyApiAccess(licenseState); - if (!context.actions) { - return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' }); - } - const actionsClient = context.actions.getActionsClient(); - const result = await actionsClient.getAll(); - return res.ok({ - body: result, - }); - }) + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const actionsClient = context.actions.getActionsClient(); + const result = await actionsClient.getAll(); + return res.ok({ + body: rewriteBodyRes(result), + }); + }) + ) ); }; diff --git a/x-pack/plugins/actions/server/routes/index.ts b/x-pack/plugins/actions/server/routes/index.ts index 14f71269db5ff..a236e514ef78d 100644 --- a/x-pack/plugins/actions/server/routes/index.ts +++ b/x-pack/plugins/actions/server/routes/index.ts @@ -5,10 +5,29 @@ * 2.0. */ -export { createActionRoute } from './create'; -export { deleteActionRoute } from './delete'; -export { getAllActionRoute } from './get_all'; -export { getActionRoute } from './get'; -export { updateActionRoute } from './update'; -export { listActionTypesRoute } from './list_action_types'; -export { executeActionRoute } from './execute'; +import { IRouter } from 'kibana/server'; +import { ILicenseState } from '../lib'; +import { ActionsRequestHandlerContext } from '../types'; +import { createActionRoute } from './create'; +import { deleteActionRoute } from './delete'; +import { executeActionRoute } from './execute'; +import { getActionRoute } from './get'; +import { getAllActionRoute } from './get_all'; +import { connectorTypesRoute } from './connector_types'; +import { updateActionRoute } from './update'; +import { defineLegacyRoutes } from './legacy'; + +export function defineRoutes( + router: IRouter, + licenseState: ILicenseState +) { + defineLegacyRoutes(router, licenseState); + + createActionRoute(router, licenseState); + deleteActionRoute(router, licenseState); + getActionRoute(router, licenseState); + getAllActionRoute(router, licenseState); + updateActionRoute(router, licenseState); + connectorTypesRoute(router, licenseState); + executeActionRoute(router, licenseState); +} diff --git a/x-pack/plugins/actions/server/routes/_mock_handler_arguments.ts b/x-pack/plugins/actions/server/routes/legacy/_mock_handler_arguments.ts similarity index 75% rename from x-pack/plugins/actions/server/routes/_mock_handler_arguments.ts rename to x-pack/plugins/actions/server/routes/legacy/_mock_handler_arguments.ts index c27699ba665dc..2a2452b02cc7d 100644 --- a/x-pack/plugins/actions/server/routes/_mock_handler_arguments.ts +++ b/x-pack/plugins/actions/server/routes/legacy/_mock_handler_arguments.ts @@ -5,12 +5,13 @@ * 2.0. */ -import { RequestHandlerContext, KibanaRequest, KibanaResponseFactory } from 'kibana/server'; +import { KibanaRequest, KibanaResponseFactory } from 'kibana/server'; import { identity } from 'lodash'; import type { MethodKeysOf } from '@kbn/utility-types'; -import { httpServerMock } from '../../../../../src/core/server/mocks'; -import { ActionType } from '../../common'; -import { ActionsClientMock, actionsClientMock } from '../actions_client.mock'; +import { httpServerMock } from '../../../../../../src/core/server/mocks'; +import { ActionType } from '../../../common'; +import { ActionsClientMock, actionsClientMock } from '../../actions_client.mock'; +import { ActionsRequestHandlerContext } from '../../types'; export function mockHandlerArguments( { @@ -19,7 +20,7 @@ export function mockHandlerArguments( }: { actionsClient?: ActionsClientMock; listTypes?: ActionType[] }, req: unknown, res?: Array> -): [RequestHandlerContext, KibanaRequest, KibanaResponseFactory] { +): [ActionsRequestHandlerContext, KibanaRequest, KibanaResponseFactory] { const listTypes = jest.fn(() => listTypesRes); return [ ({ @@ -37,7 +38,7 @@ export function mockHandlerArguments( ); }, }, - } as unknown) as RequestHandlerContext, + } as unknown) as ActionsRequestHandlerContext, req as KibanaRequest, mockResponseFactory(res), ]; diff --git a/x-pack/plugins/actions/server/routes/legacy/create.test.ts b/x-pack/plugins/actions/server/routes/legacy/create.test.ts new file mode 100644 index 0000000000000..3993319d1471f --- /dev/null +++ b/x-pack/plugins/actions/server/routes/legacy/create.test.ts @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createActionRoute } from './create'; +import { httpServiceMock } from 'src/core/server/mocks'; +import { licenseStateMock } from '../../lib/license_state.mock'; +import { mockHandlerArguments } from './_mock_handler_arguments'; +import { actionsClientMock } from '../../actions_client.mock'; +import { verifyAccessAndContext } from '../verify_access_and_context'; + +jest.mock('../verify_access_and_context.ts', () => ({ + verifyAccessAndContext: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); + (verifyAccessAndContext as jest.Mock).mockImplementation((license, handler) => handler); +}); + +describe('createActionRoute', () => { + it('creates an action with proper parameters', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + createActionRoute(router, licenseState); + + const [config, handler] = router.post.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/actions/action"`); + + const createResult = { + id: '1', + name: 'My name', + actionTypeId: 'abc', + config: { foo: true }, + isPreconfigured: false, + }; + + const actionsClient = actionsClientMock.create(); + actionsClient.create.mockResolvedValueOnce(createResult); + + const [context, req, res] = mockHandlerArguments( + { actionsClient }, + { + body: { + name: 'My name', + actionTypeId: 'abc', + config: { foo: true }, + secrets: {}, + }, + }, + ['ok'] + ); + + expect(await handler(context, req, res)).toEqual({ body: createResult }); + + expect(actionsClient.create).toHaveBeenCalledTimes(1); + expect(actionsClient.create.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "action": Object { + "actionTypeId": "abc", + "config": Object { + "foo": true, + }, + "name": "My name", + "secrets": Object {}, + }, + }, + ] + `); + + expect(res.ok).toHaveBeenCalledWith({ + body: createResult, + }); + }); + + it('ensures the license allows creating actions', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + createActionRoute(router, licenseState); + + const [, handler] = router.post.mock.calls[0]; + + const actionsClient = actionsClientMock.create(); + actionsClient.create.mockResolvedValueOnce({ + id: '1', + name: 'My name', + actionTypeId: 'abc', + config: { foo: true }, + isPreconfigured: false, + }); + + const [context, req, res] = mockHandlerArguments({ actionsClient }, {}); + + await handler(context, req, res); + + expect(verifyAccessAndContext).toHaveBeenCalledWith(licenseState, expect.any(Function)); + }); + + it('ensures the license check prevents creating actions', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + (verifyAccessAndContext as jest.Mock).mockImplementation(() => async () => { + throw new Error('OMG'); + }); + + createActionRoute(router, licenseState); + + const [, handler] = router.post.mock.calls[0]; + + const actionsClient = actionsClientMock.create(); + actionsClient.create.mockResolvedValueOnce({ + id: '1', + name: 'My name', + actionTypeId: 'abc', + config: { foo: true }, + isPreconfigured: false, + }); + + const [context, req, res] = mockHandlerArguments({ actionsClient }, {}); + + expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); + }); +}); diff --git a/x-pack/plugins/actions/server/routes/legacy/create.ts b/x-pack/plugins/actions/server/routes/legacy/create.ts new file mode 100644 index 0000000000000..caed699641673 --- /dev/null +++ b/x-pack/plugins/actions/server/routes/legacy/create.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { IRouter } from 'kibana/server'; +import { ActionsRequestHandlerContext } from '../../types'; +import { ILicenseState } from '../../lib'; +import { BASE_ACTION_API_PATH } from '../../../common'; +import { verifyAccessAndContext } from '../verify_access_and_context'; + +export const bodySchema = schema.object({ + name: schema.string(), + actionTypeId: schema.string(), + config: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), + secrets: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), +}); + +export const createActionRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.post( + { + path: `${BASE_ACTION_API_PATH}/action`, + validate: { + body: bodySchema, + }, + }, + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const actionsClient = context.actions.getActionsClient(); + const action = req.body; + return res.ok({ + body: await actionsClient.create({ action }), + }); + }) + ) + ); +}; diff --git a/x-pack/plugins/actions/server/routes/legacy/delete.test.ts b/x-pack/plugins/actions/server/routes/legacy/delete.test.ts new file mode 100644 index 0000000000000..cee78d998d62a --- /dev/null +++ b/x-pack/plugins/actions/server/routes/legacy/delete.test.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { deleteActionRoute } from './delete'; +import { httpServiceMock } from 'src/core/server/mocks'; +import { licenseStateMock } from '../../lib/license_state.mock'; +import { verifyApiAccess } from '../../lib'; +import { mockHandlerArguments } from './_mock_handler_arguments'; +import { actionsClientMock } from '../../mocks'; + +jest.mock('../../lib/verify_api_access.ts', () => ({ + verifyApiAccess: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('deleteActionRoute', () => { + it('deletes an action with proper parameters', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + deleteActionRoute(router, licenseState); + + const [config, handler] = router.delete.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/actions/action/{id}"`); + + const actionsClient = actionsClientMock.create(); + actionsClient.delete.mockResolvedValueOnce({}); + + const [context, req, res] = mockHandlerArguments( + { actionsClient }, + { + params: { + id: '1', + }, + }, + ['noContent'] + ); + + expect(await handler(context, req, res)).toEqual(undefined); + + expect(actionsClient.delete).toHaveBeenCalledTimes(1); + expect(actionsClient.delete.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + }, + ] + `); + + expect(res.noContent).toHaveBeenCalled(); + }); + + it('ensures the license allows deleting actions', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + deleteActionRoute(router, licenseState); + + const [, handler] = router.delete.mock.calls[0]; + + const actionsClient = actionsClientMock.create(); + actionsClient.delete.mockResolvedValueOnce({}); + + const [context, req, res] = mockHandlerArguments( + { actionsClient }, + { + params: { id: '1' }, + } + ); + + await handler(context, req, res); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); + + it('ensures the license check prevents deleting actions', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + (verifyApiAccess as jest.Mock).mockImplementation(() => { + throw new Error('OMG'); + }); + + deleteActionRoute(router, licenseState); + + const [, handler] = router.delete.mock.calls[0]; + + const actionsClient = actionsClientMock.create(); + actionsClient.delete.mockResolvedValueOnce({}); + + const [context, req, res] = mockHandlerArguments( + { actionsClient }, + { + id: '1', + } + ); + + expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); +}); diff --git a/x-pack/plugins/actions/server/routes/legacy/delete.ts b/x-pack/plugins/actions/server/routes/legacy/delete.ts new file mode 100644 index 0000000000000..9b3c449607b4a --- /dev/null +++ b/x-pack/plugins/actions/server/routes/legacy/delete.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { IRouter } from 'kibana/server'; +import { ILicenseState, verifyApiAccess, isErrorThatHandlesItsOwnResponse } from '../../lib'; +import { BASE_ACTION_API_PATH } from '../../../common'; +import { ActionsRequestHandlerContext } from '../../types'; + +const paramSchema = schema.object({ + id: schema.string(), +}); + +export const deleteActionRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.delete( + { + path: `${BASE_ACTION_API_PATH}/action/{id}`, + validate: { + params: paramSchema, + }, + }, + router.handleLegacyErrors(async function (context, req, res) { + verifyApiAccess(licenseState); + if (!context.actions) { + return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' }); + } + const actionsClient = context.actions.getActionsClient(); + const { id } = req.params; + try { + await actionsClient.delete({ id }); + return res.noContent(); + } catch (e) { + if (isErrorThatHandlesItsOwnResponse(e)) { + return e.sendResponse(res); + } + throw e; + } + }) + ); +}; diff --git a/x-pack/plugins/actions/server/routes/legacy/execute.test.ts b/x-pack/plugins/actions/server/routes/legacy/execute.test.ts new file mode 100644 index 0000000000000..2ac53ddaaedf6 --- /dev/null +++ b/x-pack/plugins/actions/server/routes/legacy/execute.test.ts @@ -0,0 +1,193 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { executeActionRoute } from './execute'; +import { httpServiceMock } from 'src/core/server/mocks'; +import { licenseStateMock } from '../../lib/license_state.mock'; +import { mockHandlerArguments } from './_mock_handler_arguments'; +import { verifyApiAccess, ActionTypeDisabledError, asHttpRequestExecutionSource } from '../../lib'; +import { actionsClientMock } from '../../actions_client.mock'; +import { ActionTypeExecutorResult } from '../../types'; + +jest.mock('../../lib/verify_api_access.ts', () => ({ + verifyApiAccess: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('executeActionRoute', () => { + it('executes an action with proper parameters', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + const actionsClient = actionsClientMock.create(); + actionsClient.execute.mockResolvedValueOnce({ status: 'ok', actionId: '1' }); + + const [context, req, res] = mockHandlerArguments( + { actionsClient }, + { + body: { + params: { + someData: 'data', + }, + }, + params: { + id: '1', + }, + }, + ['ok'] + ); + + const executeResult = { + actionId: '1', + status: 'ok', + }; + + executeActionRoute(router, licenseState); + + const [config, handler] = router.post.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/actions/action/{id}/_execute"`); + + expect(await handler(context, req, res)).toEqual({ body: executeResult }); + + expect(actionsClient.execute).toHaveBeenCalledWith({ + actionId: '1', + params: { + someData: 'data', + }, + source: asHttpRequestExecutionSource(req), + }); + + expect(res.ok).toHaveBeenCalled(); + }); + + it('returns a "204 NO CONTENT" when the executor returns a nullish value', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + const actionsClient = actionsClientMock.create(); + actionsClient.execute.mockResolvedValueOnce( + (null as unknown) as ActionTypeExecutorResult + ); + + const [context, req, res] = mockHandlerArguments( + { actionsClient }, + { + body: { + params: {}, + }, + params: { + id: '1', + }, + }, + ['noContent'] + ); + + executeActionRoute(router, licenseState); + + const [, handler] = router.post.mock.calls[0]; + + expect(await handler(context, req, res)).toEqual(undefined); + + expect(actionsClient.execute).toHaveBeenCalledWith({ + actionId: '1', + params: {}, + source: asHttpRequestExecutionSource(req), + }); + + expect(res.ok).not.toHaveBeenCalled(); + expect(res.noContent).toHaveBeenCalled(); + }); + + it('ensures the license allows action execution', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + const actionsClient = actionsClientMock.create(); + actionsClient.execute.mockResolvedValue({ + actionId: '1', + status: 'ok', + }); + + const [context, req, res] = mockHandlerArguments( + { actionsClient }, + { + body: {}, + params: {}, + }, + ['ok'] + ); + + executeActionRoute(router, licenseState); + + const [, handler] = router.post.mock.calls[0]; + + await handler(context, req, res); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); + + it('ensures the license check prevents action execution', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + const actionsClient = actionsClientMock.create(); + actionsClient.execute.mockResolvedValue({ + actionId: '1', + status: 'ok', + }); + + (verifyApiAccess as jest.Mock).mockImplementation(() => { + throw new Error('OMG'); + }); + + const [context, req, res] = mockHandlerArguments( + { actionsClient }, + { + body: {}, + params: {}, + }, + ['ok'] + ); + + executeActionRoute(router, licenseState); + + const [, handler] = router.post.mock.calls[0]; + + expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); + + it('ensures the action type gets validated for the license', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + const actionsClient = actionsClientMock.create(); + actionsClient.execute.mockRejectedValue(new ActionTypeDisabledError('Fail', 'license_invalid')); + + const [context, req, res] = mockHandlerArguments( + { actionsClient }, + { + body: {}, + params: {}, + }, + ['ok', 'forbidden'] + ); + + executeActionRoute(router, licenseState); + + const [, handler] = router.post.mock.calls[0]; + + await handler(context, req, res); + + expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); + }); +}); diff --git a/x-pack/plugins/actions/server/routes/legacy/execute.ts b/x-pack/plugins/actions/server/routes/legacy/execute.ts new file mode 100644 index 0000000000000..f6ddec1d01c20 --- /dev/null +++ b/x-pack/plugins/actions/server/routes/legacy/execute.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { IRouter } from 'kibana/server'; +import { ILicenseState, verifyApiAccess, isErrorThatHandlesItsOwnResponse } from '../../lib'; + +import { ActionTypeExecutorResult, ActionsRequestHandlerContext } from '../../types'; +import { BASE_ACTION_API_PATH } from '../../../common'; +import { asHttpRequestExecutionSource } from '../../lib/action_execution_source'; + +const paramSchema = schema.object({ + id: schema.string(), +}); + +const bodySchema = schema.object({ + params: schema.recordOf(schema.string(), schema.any()), +}); + +export const executeActionRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.post( + { + path: `${BASE_ACTION_API_PATH}/action/{id}/_execute`, + validate: { + body: bodySchema, + params: paramSchema, + }, + }, + router.handleLegacyErrors(async function (context, req, res) { + verifyApiAccess(licenseState); + + if (!context.actions) { + return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' }); + } + + const actionsClient = context.actions.getActionsClient(); + const { params } = req.body; + const { id } = req.params; + try { + const body: ActionTypeExecutorResult = await actionsClient.execute({ + params, + actionId: id, + source: asHttpRequestExecutionSource(req), + }); + return body + ? res.ok({ + body, + }) + : res.noContent(); + } catch (e) { + if (isErrorThatHandlesItsOwnResponse(e)) { + return e.sendResponse(res); + } + throw e; + } + }) + ); +}; diff --git a/x-pack/plugins/actions/server/routes/legacy/get.test.ts b/x-pack/plugins/actions/server/routes/legacy/get.test.ts new file mode 100644 index 0000000000000..4d1265030141f --- /dev/null +++ b/x-pack/plugins/actions/server/routes/legacy/get.test.ts @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getActionRoute } from './get'; +import { httpServiceMock } from 'src/core/server/mocks'; +import { licenseStateMock } from '../../lib/license_state.mock'; +import { verifyApiAccess } from '../../lib'; +import { mockHandlerArguments } from './_mock_handler_arguments'; +import { actionsClientMock } from '../../actions_client.mock'; + +jest.mock('../../lib/verify_api_access.ts', () => ({ + verifyApiAccess: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('getActionRoute', () => { + it('gets an action with proper parameters', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + getActionRoute(router, licenseState); + + const [config, handler] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/actions/action/{id}"`); + + const getResult = { + id: '1', + actionTypeId: '2', + name: 'action name', + config: {}, + isPreconfigured: false, + }; + + const actionsClient = actionsClientMock.create(); + actionsClient.get.mockResolvedValueOnce(getResult); + + const [context, req, res] = mockHandlerArguments( + { actionsClient }, + { + params: { id: '1' }, + }, + ['ok'] + ); + + expect(await handler(context, req, res)).toMatchInlineSnapshot(` + Object { + "body": Object { + "actionTypeId": "2", + "config": Object {}, + "id": "1", + "isPreconfigured": false, + "name": "action name", + }, + } + `); + + expect(actionsClient.get).toHaveBeenCalledTimes(1); + expect(actionsClient.get.mock.calls[0][0].id).toEqual('1'); + + expect(res.ok).toHaveBeenCalledWith({ + body: getResult, + }); + }); + + it('ensures the license allows getting actions', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + getActionRoute(router, licenseState); + + const [, handler] = router.get.mock.calls[0]; + + const actionsClient = actionsClientMock.create(); + actionsClient.get.mockResolvedValueOnce({ + id: '1', + actionTypeId: '2', + name: 'action name', + config: {}, + isPreconfigured: false, + }); + + const [context, req, res] = mockHandlerArguments( + { actionsClient }, + { + params: { id: '1' }, + }, + ['ok'] + ); + + await handler(context, req, res); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); + + it('ensures the license check prevents getting actions', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + (verifyApiAccess as jest.Mock).mockImplementation(() => { + throw new Error('OMG'); + }); + + getActionRoute(router, licenseState); + + const [, handler] = router.get.mock.calls[0]; + + const actionsClient = actionsClientMock.create(); + actionsClient.get.mockResolvedValueOnce({ + id: '1', + actionTypeId: '2', + name: 'action name', + config: {}, + isPreconfigured: false, + }); + + const [context, req, res] = mockHandlerArguments( + { actionsClient }, + { + params: { id: '1' }, + }, + ['ok'] + ); + + expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); +}); diff --git a/x-pack/plugins/actions/server/routes/legacy/get.ts b/x-pack/plugins/actions/server/routes/legacy/get.ts new file mode 100644 index 0000000000000..44780d4f8a14b --- /dev/null +++ b/x-pack/plugins/actions/server/routes/legacy/get.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { IRouter } from 'kibana/server'; +import { ILicenseState, verifyApiAccess } from '../../lib'; +import { BASE_ACTION_API_PATH } from '../../../common'; +import { ActionsRequestHandlerContext } from '../../types'; + +const paramSchema = schema.object({ + id: schema.string(), +}); + +export const getActionRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.get( + { + path: `${BASE_ACTION_API_PATH}/action/{id}`, + validate: { + params: paramSchema, + }, + }, + router.handleLegacyErrors(async function (context, req, res) { + verifyApiAccess(licenseState); + if (!context.actions) { + return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' }); + } + const actionsClient = context.actions.getActionsClient(); + const { id } = req.params; + return res.ok({ + body: await actionsClient.get({ id }), + }); + }) + ); +}; diff --git a/x-pack/plugins/actions/server/routes/legacy/get_all.test.ts b/x-pack/plugins/actions/server/routes/legacy/get_all.test.ts new file mode 100644 index 0000000000000..7f1003e564614 --- /dev/null +++ b/x-pack/plugins/actions/server/routes/legacy/get_all.test.ts @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getAllActionRoute } from './get_all'; +import { httpServiceMock } from 'src/core/server/mocks'; +import { licenseStateMock } from '../../lib/license_state.mock'; +import { verifyApiAccess } from '../../lib'; +import { mockHandlerArguments } from './_mock_handler_arguments'; +import { actionsClientMock } from '../../actions_client.mock'; + +jest.mock('../../lib/verify_api_access.ts', () => ({ + verifyApiAccess: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('getAllActionRoute', () => { + it('get all actions with proper parameters', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + getAllActionRoute(router, licenseState); + + const [config, handler] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/actions"`); + + const actionsClient = actionsClientMock.create(); + actionsClient.getAll.mockResolvedValueOnce([]); + + const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok']); + + expect(await handler(context, req, res)).toMatchInlineSnapshot(` + Object { + "body": Array [], + } + `); + + expect(actionsClient.getAll).toHaveBeenCalledTimes(1); + + expect(res.ok).toHaveBeenCalledWith({ + body: [], + }); + }); + + it('ensures the license allows getting all actions', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + getAllActionRoute(router, licenseState); + + const [config, handler] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/actions"`); + + const actionsClient = actionsClientMock.create(); + actionsClient.getAll.mockResolvedValueOnce([]); + + const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok']); + + await handler(context, req, res); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); + + it('ensures the license check prevents getting all actions', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + (verifyApiAccess as jest.Mock).mockImplementation(() => { + throw new Error('OMG'); + }); + + getAllActionRoute(router, licenseState); + + const [config, handler] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/actions"`); + + const actionsClient = actionsClientMock.create(); + actionsClient.getAll.mockResolvedValueOnce([]); + + const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok']); + + expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); +}); diff --git a/x-pack/plugins/actions/server/routes/legacy/get_all.ts b/x-pack/plugins/actions/server/routes/legacy/get_all.ts new file mode 100644 index 0000000000000..9ea5024d8672b --- /dev/null +++ b/x-pack/plugins/actions/server/routes/legacy/get_all.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IRouter } from 'kibana/server'; +import { ILicenseState, verifyApiAccess } from '../../lib'; +import { BASE_ACTION_API_PATH } from '../../../common'; +import { ActionsRequestHandlerContext } from '../../types'; + +export const getAllActionRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.get( + { + path: `${BASE_ACTION_API_PATH}`, + validate: {}, + }, + router.handleLegacyErrors(async function (context, req, res) { + verifyApiAccess(licenseState); + if (!context.actions) { + return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' }); + } + const actionsClient = context.actions.getActionsClient(); + const result = await actionsClient.getAll(); + return res.ok({ + body: result, + }); + }) + ); +}; diff --git a/x-pack/plugins/actions/server/routes/legacy/index.ts b/x-pack/plugins/actions/server/routes/legacy/index.ts new file mode 100644 index 0000000000000..1a22cd9be5681 --- /dev/null +++ b/x-pack/plugins/actions/server/routes/legacy/index.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IRouter } from 'kibana/server'; +import { ILicenseState } from '../../lib'; +import { ActionsRequestHandlerContext } from '../../types'; +import { createActionRoute } from './create'; +import { deleteActionRoute } from './delete'; +import { getAllActionRoute } from './get_all'; +import { getActionRoute } from './get'; +import { updateActionRoute } from './update'; +import { listActionTypesRoute } from './list_action_types'; +import { executeActionRoute } from './execute'; + +export function defineLegacyRoutes( + router: IRouter, + licenseState: ILicenseState +) { + createActionRoute(router, licenseState); + deleteActionRoute(router, licenseState); + getActionRoute(router, licenseState); + getAllActionRoute(router, licenseState); + updateActionRoute(router, licenseState); + listActionTypesRoute(router, licenseState); + executeActionRoute(router, licenseState); +} diff --git a/x-pack/plugins/actions/server/routes/list_action_types.test.ts b/x-pack/plugins/actions/server/routes/legacy/list_action_types.test.ts similarity index 93% rename from x-pack/plugins/actions/server/routes/list_action_types.test.ts rename to x-pack/plugins/actions/server/routes/legacy/list_action_types.test.ts index 102805e019420..e49dd251136ad 100644 --- a/x-pack/plugins/actions/server/routes/list_action_types.test.ts +++ b/x-pack/plugins/actions/server/routes/legacy/list_action_types.test.ts @@ -7,13 +7,13 @@ import { listActionTypesRoute } from './list_action_types'; import { httpServiceMock } from 'src/core/server/mocks'; -import { licenseStateMock } from '../lib/license_state.mock'; -import { verifyApiAccess } from '../lib'; +import { licenseStateMock } from '../../lib/license_state.mock'; +import { verifyApiAccess } from '../../lib'; import { mockHandlerArguments } from './_mock_handler_arguments'; -import { LicenseType } from '../../../../plugins/licensing/server'; -import { actionsClientMock } from '../mocks'; +import { LicenseType } from '../../../../../plugins/licensing/server'; +import { actionsClientMock } from '../../mocks'; -jest.mock('../lib/verify_api_access.ts', () => ({ +jest.mock('../../lib/verify_api_access.ts', () => ({ verifyApiAccess: jest.fn(), })); diff --git a/x-pack/plugins/actions/server/routes/list_action_types.ts b/x-pack/plugins/actions/server/routes/legacy/list_action_types.ts similarity index 83% rename from x-pack/plugins/actions/server/routes/list_action_types.ts rename to x-pack/plugins/actions/server/routes/legacy/list_action_types.ts index 72aba7b6dbbd2..814f5fffd35ff 100644 --- a/x-pack/plugins/actions/server/routes/list_action_types.ts +++ b/x-pack/plugins/actions/server/routes/legacy/list_action_types.ts @@ -6,9 +6,9 @@ */ import { IRouter } from 'kibana/server'; -import { ILicenseState, verifyApiAccess } from '../lib'; -import { BASE_ACTION_API_PATH } from '../../common'; -import { ActionsRequestHandlerContext } from '../types'; +import { ILicenseState, verifyApiAccess } from '../../lib'; +import { BASE_ACTION_API_PATH } from '../../../common'; +import { ActionsRequestHandlerContext } from '../../types'; export const listActionTypesRoute = ( router: IRouter, diff --git a/x-pack/plugins/actions/server/routes/legacy/update.test.ts b/x-pack/plugins/actions/server/routes/legacy/update.test.ts new file mode 100644 index 0000000000000..0ce49751753b2 --- /dev/null +++ b/x-pack/plugins/actions/server/routes/legacy/update.test.ts @@ -0,0 +1,185 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { updateActionRoute } from './update'; +import { httpServiceMock } from 'src/core/server/mocks'; +import { licenseStateMock } from '../../lib/license_state.mock'; +import { verifyApiAccess, ActionTypeDisabledError } from '../../lib'; +import { mockHandlerArguments } from './_mock_handler_arguments'; +import { actionsClientMock } from '../../actions_client.mock'; + +jest.mock('../../lib/verify_api_access.ts', () => ({ + verifyApiAccess: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('updateActionRoute', () => { + it('updates an action with proper parameters', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + updateActionRoute(router, licenseState); + + const [config, handler] = router.put.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/actions/action/{id}"`); + + const updateResult = { + id: '1', + actionTypeId: 'my-action-type-id', + name: 'My name', + config: { foo: true }, + isPreconfigured: false, + }; + + const actionsClient = actionsClientMock.create(); + actionsClient.update.mockResolvedValueOnce(updateResult); + + const [context, req, res] = mockHandlerArguments( + { actionsClient }, + { + params: { + id: '1', + }, + body: { + name: 'My name', + config: { foo: true }, + secrets: { key: 'i8oh34yf9783y39' }, + }, + }, + ['ok'] + ); + + expect(await handler(context, req, res)).toEqual({ body: updateResult }); + + expect(actionsClient.update).toHaveBeenCalledTimes(1); + expect(actionsClient.update.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "action": Object { + "config": Object { + "foo": true, + }, + "name": "My name", + "secrets": Object { + "key": "i8oh34yf9783y39", + }, + }, + "id": "1", + }, + ] + `); + + expect(res.ok).toHaveBeenCalled(); + }); + + it('ensures the license allows deleting actions', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + updateActionRoute(router, licenseState); + + const [, handler] = router.put.mock.calls[0]; + + const updateResult = { + id: '1', + actionTypeId: 'my-action-type-id', + name: 'My name', + config: { foo: true }, + isPreconfigured: false, + }; + + const actionsClient = actionsClientMock.create(); + actionsClient.update.mockResolvedValueOnce(updateResult); + + const [context, req, res] = mockHandlerArguments( + { actionsClient }, + { + params: { + id: '1', + }, + body: { + name: 'My name', + config: { foo: true }, + secrets: { key: 'i8oh34yf9783y39' }, + }, + }, + ['ok'] + ); + + await handler(context, req, res); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); + + it('ensures the license check prevents deleting actions', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + (verifyApiAccess as jest.Mock).mockImplementation(() => { + throw new Error('OMG'); + }); + + updateActionRoute(router, licenseState); + + const [, handler] = router.put.mock.calls[0]; + + const updateResult = { + id: '1', + actionTypeId: 'my-action-type-id', + name: 'My name', + config: { foo: true }, + isPreconfigured: false, + }; + + const actionsClient = actionsClientMock.create(); + actionsClient.update.mockResolvedValueOnce(updateResult); + + const [context, req, res] = mockHandlerArguments( + { actionsClient }, + { + params: { + id: '1', + }, + body: { + name: 'My name', + config: { foo: true }, + secrets: { key: 'i8oh34yf9783y39' }, + }, + }, + ['ok'] + ); + + expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); + + it('ensures the action type gets validated for the license', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + updateActionRoute(router, licenseState); + + const [, handler] = router.put.mock.calls[0]; + + const actionsClient = actionsClientMock.create(); + actionsClient.update.mockRejectedValue(new ActionTypeDisabledError('Fail', 'license_invalid')); + + const [context, req, res] = mockHandlerArguments({ actionsClient }, { params: {}, body: {} }, [ + 'ok', + 'forbidden', + ]); + + await handler(context, req, res); + + expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); + }); +}); diff --git a/x-pack/plugins/actions/server/routes/legacy/update.ts b/x-pack/plugins/actions/server/routes/legacy/update.ts new file mode 100644 index 0000000000000..6cbcd56af5048 --- /dev/null +++ b/x-pack/plugins/actions/server/routes/legacy/update.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { IRouter } from 'kibana/server'; +import { ILicenseState, verifyApiAccess, isErrorThatHandlesItsOwnResponse } from '../../lib'; +import { BASE_ACTION_API_PATH } from '../../../common'; +import { ActionsRequestHandlerContext } from '../../types'; + +const paramSchema = schema.object({ + id: schema.string(), +}); + +const bodySchema = schema.object({ + name: schema.string(), + config: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), + secrets: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), +}); + +export const updateActionRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.put( + { + path: `${BASE_ACTION_API_PATH}/action/{id}`, + validate: { + body: bodySchema, + params: paramSchema, + }, + }, + router.handleLegacyErrors(async function (context, req, res) { + verifyApiAccess(licenseState); + if (!context.actions) { + return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' }); + } + const actionsClient = context.actions.getActionsClient(); + const { id } = req.params; + const { name, config, secrets } = req.body; + + try { + return res.ok({ + body: await actionsClient.update({ + id, + action: { name, config, secrets }, + }), + }); + } catch (e) { + if (isErrorThatHandlesItsOwnResponse(e)) { + return e.sendResponse(res); + } + throw e; + } + }) + ); +}; diff --git a/x-pack/plugins/actions/server/routes/rewrite_request_case.ts b/x-pack/plugins/actions/server/routes/rewrite_request_case.ts new file mode 100644 index 0000000000000..c11a82f4af5ac --- /dev/null +++ b/x-pack/plugins/actions/server/routes/rewrite_request_case.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +type RenameActionToConnector = K extends `actionTypeId` + ? `connectorTypeId` + : K extends `actionId` + ? `connectorId` + : K; + +export type AsApiContract = { + [K in keyof T as CamelToSnake>>]: T[K]; +}; + +export type RewriteRequestCase = (requested: AsApiContract) => T; +export type RewriteResponseCase = ( + responded: T +) => T extends Array ? Array> : AsApiContract; + +/** + * This type maps Camel Case strings into their Snake Case version. + * This is achieved by checking each character and, if it is an uppercase character, it is mapped to an + * underscore followed by a lowercase one. + * + * The reason there are two ternaries is that, for perfformance reasons, TS limits its + * character parsing to ~15 characters. + * To get around this we use the second turnery to parse 2 characters at a time, which allows us to support + * strings that are 30 characters long. + * + * If you get the TS #2589 error ("Type instantiation is excessively deep and possibly infinite") then most + * likely you have a string that's longer than 30 characters. + * Address this by reducing the length if possible, otherwise, you'll need to add a 3rd ternary which + * parses 3 chars at a time :grimace: + * + * For more details see this PR comment: https://github.com/microsoft/TypeScript/pull/40336#issuecomment-686723087 + */ +type CamelToSnake = string extends T + ? string + : T extends `${infer C0}${infer C1}${infer R}` + ? `${C0 extends Uppercase ? '_' : ''}${Lowercase}${C1 extends Uppercase + ? '_' + : ''}${Lowercase}${CamelToSnake}` + : T extends `${infer C0}${infer R}` + ? `${C0 extends Uppercase ? '_' : ''}${Lowercase}${CamelToSnake}` + : ''; diff --git a/x-pack/plugins/actions/server/routes/update.test.ts b/x-pack/plugins/actions/server/routes/update.test.ts index ed539522c00d5..653fcba75da95 100644 --- a/x-pack/plugins/actions/server/routes/update.test.ts +++ b/x-pack/plugins/actions/server/routes/update.test.ts @@ -8,16 +8,17 @@ import { updateActionRoute } from './update'; import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; -import { verifyApiAccess, ActionTypeDisabledError } from '../lib'; -import { mockHandlerArguments } from './_mock_handler_arguments'; +import { mockHandlerArguments } from './legacy/_mock_handler_arguments'; import { actionsClientMock } from '../actions_client.mock'; +import { verifyAccessAndContext } from './verify_access_and_context'; -jest.mock('../lib/verify_api_access.ts', () => ({ - verifyApiAccess: jest.fn(), +jest.mock('./verify_access_and_context.ts', () => ({ + verifyAccessAndContext: jest.fn(), })); beforeEach(() => { jest.resetAllMocks(); + (verifyAccessAndContext as jest.Mock).mockImplementation((license, handler) => handler); }); describe('updateActionRoute', () => { @@ -29,7 +30,7 @@ describe('updateActionRoute', () => { const [config, handler] = router.put.mock.calls[0]; - expect(config.path).toMatchInlineSnapshot(`"/api/actions/action/{id}"`); + expect(config.path).toMatchInlineSnapshot(`"/api/actions/connector/{id}"`); const updateResult = { id: '1', @@ -57,7 +58,15 @@ describe('updateActionRoute', () => { ['ok'] ); - expect(await handler(context, req, res)).toEqual({ body: updateResult }); + expect(await handler(context, req, res)).toEqual({ + body: { + id: '1', + connector_type_id: 'my-action-type-id', + name: 'My name', + config: { foo: true }, + is_preconfigured: false, + }, + }); expect(actionsClient.update).toHaveBeenCalledTimes(1); expect(actionsClient.update.mock.calls[0]).toMatchInlineSnapshot(` @@ -116,14 +125,14 @@ describe('updateActionRoute', () => { await handler(context, req, res); - expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + expect(verifyAccessAndContext).toHaveBeenCalledWith(licenseState, expect.any(Function)); }); it('ensures the license check prevents deleting actions', async () => { const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); - (verifyApiAccess as jest.Mock).mockImplementation(() => { + (verifyAccessAndContext as jest.Mock).mockImplementation(() => async () => { throw new Error('OMG'); }); @@ -159,27 +168,6 @@ describe('updateActionRoute', () => { expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); - expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); - }); - - it('ensures the action type gets validated for the license', async () => { - const licenseState = licenseStateMock.create(); - const router = httpServiceMock.createRouter(); - - updateActionRoute(router, licenseState); - - const [, handler] = router.put.mock.calls[0]; - - const actionsClient = actionsClientMock.create(); - actionsClient.update.mockRejectedValue(new ActionTypeDisabledError('Fail', 'license_invalid')); - - const [context, req, res] = mockHandlerArguments({ actionsClient }, { params: {}, body: {} }, [ - 'ok', - 'forbidden', - ]); - - await handler(context, req, res); - - expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); + expect(verifyAccessAndContext).toHaveBeenCalledWith(licenseState, expect.any(Function)); }); }); diff --git a/x-pack/plugins/actions/server/routes/update.ts b/x-pack/plugins/actions/server/routes/update.ts index 97cc11a5d0ad4..af55fa32b76ca 100644 --- a/x-pack/plugins/actions/server/routes/update.ts +++ b/x-pack/plugins/actions/server/routes/update.ts @@ -7,9 +7,11 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from 'kibana/server'; -import { ILicenseState, verifyApiAccess, isErrorThatHandlesItsOwnResponse } from '../lib'; +import { ILicenseState } from '../lib'; import { BASE_ACTION_API_PATH } from '../../common'; -import { ActionsRequestHandlerContext } from '../types'; +import { ActionResult, ActionsRequestHandlerContext } from '../types'; +import { verifyAccessAndContext } from './verify_access_and_context'; +import { RewriteResponseCase } from './rewrite_request_case'; const paramSchema = schema.object({ id: schema.string(), @@ -21,40 +23,43 @@ const bodySchema = schema.object({ secrets: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), }); +const rewriteBodyRes: RewriteResponseCase = ({ + actionTypeId, + isPreconfigured, + ...res +}) => ({ + ...res, + connector_type_id: actionTypeId, + is_preconfigured: isPreconfigured, +}); + export const updateActionRoute = ( router: IRouter, licenseState: ILicenseState ) => { router.put( { - path: `${BASE_ACTION_API_PATH}/action/{id}`, + path: `${BASE_ACTION_API_PATH}/connector/{id}`, validate: { body: bodySchema, params: paramSchema, }, }, - router.handleLegacyErrors(async function (context, req, res) { - verifyApiAccess(licenseState); - if (!context.actions) { - return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' }); - } - const actionsClient = context.actions.getActionsClient(); - const { id } = req.params; - const { name, config, secrets } = req.body; + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const actionsClient = context.actions.getActionsClient(); + const { id } = req.params; + const { name, config, secrets } = req.body; - try { return res.ok({ - body: await actionsClient.update({ - id, - action: { name, config, secrets }, - }), + body: rewriteBodyRes( + await actionsClient.update({ + id, + action: { name, config, secrets }, + }) + ), }); - } catch (e) { - if (isErrorThatHandlesItsOwnResponse(e)) { - return e.sendResponse(res); - } - throw e; - } - }) + }) + ) ); }; diff --git a/x-pack/plugins/actions/server/routes/verify_access_and_context.test.ts b/x-pack/plugins/actions/server/routes/verify_access_and_context.test.ts new file mode 100644 index 0000000000000..f13bc7279e346 --- /dev/null +++ b/x-pack/plugins/actions/server/routes/verify_access_and_context.test.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { licenseStateMock } from '../lib/license_state.mock'; +import { verifyApiAccess, ActionTypeDisabledError } from '../lib'; +import { mockHandlerArguments } from './legacy/_mock_handler_arguments'; +import { actionsClientMock } from '../actions_client.mock'; +import { verifyAccessAndContext } from './verify_access_and_context'; + +jest.mock('../lib/verify_api_access.ts', () => ({ + verifyApiAccess: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('verifyAccessAndContext', () => { + it('ensures the license allows creating actions', async () => { + const licenseState = licenseStateMock.create(); + + const handler = jest.fn(); + const verify = verifyAccessAndContext(licenseState, handler); + + const actionsClient = actionsClientMock.create(); + const [context, req, res] = mockHandlerArguments({ actionsClient }, {}); + + await verify(context, req, res); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); + + it('ensures the license check prevents creating actions', async () => { + const licenseState = licenseStateMock.create(); + + (verifyApiAccess as jest.Mock).mockImplementation(() => { + throw new Error('OMG'); + }); + + const handler = jest.fn(); + const verify = verifyAccessAndContext(licenseState, handler); + + const actionsClient = actionsClientMock.create(); + const [context, req, res] = mockHandlerArguments({ actionsClient }, {}); + + await expect(verify(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); + + it('supports error that handle their own response', async () => { + const licenseState = licenseStateMock.create(); + + const handler = jest.fn(); + const verify = verifyAccessAndContext(licenseState, handler); + + const actionsClient = actionsClientMock.create(); + const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok', 'forbidden']); + + handler.mockRejectedValue(new ActionTypeDisabledError('Fail', 'license_invalid')); + + await expect(verify(context, req, res)).resolves.toMatchObject({ body: { message: 'Fail' } }); + expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); + }); +}); diff --git a/x-pack/plugins/actions/server/routes/verify_access_and_context.ts b/x-pack/plugins/actions/server/routes/verify_access_and_context.ts new file mode 100644 index 0000000000000..3f1fe4b1c229f --- /dev/null +++ b/x-pack/plugins/actions/server/routes/verify_access_and_context.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RequestHandler } from 'kibana/server'; +import { ILicenseState, isErrorThatHandlesItsOwnResponse, verifyApiAccess } from '../lib'; +import { ActionsRequestHandlerContext } from '../types'; + +type ActionsRequestHandlerWrapper = ( + licenseState: ILicenseState, + handler: RequestHandler +) => RequestHandler; + +export const verifyAccessAndContext: ActionsRequestHandlerWrapper = (licenseState, handler) => { + return async (context, request, response) => { + verifyApiAccess(licenseState); + + if (!context.actions) { + return response.badRequest({ body: 'RouteHandlerContext is not registered for actions' }); + } + + try { + return await handler(context, request, response); + } catch (e) { + if (isErrorThatHandlesItsOwnResponse(e)) { + return e.sendResponse(response); + } + throw e; + } + }; +}; diff --git a/x-pack/plugins/apm/e2e/run-e2e.sh b/x-pack/plugins/apm/e2e/run-e2e.sh index 85ab67bbf9a10..1b3afb4823426 100755 --- a/x-pack/plugins/apm/e2e/run-e2e.sh +++ b/x-pack/plugins/apm/e2e/run-e2e.sh @@ -147,7 +147,7 @@ $WAIT_ON_BIN -i 500 -w 500 http-get://admin:changeme@localhost:$KIBANA_PORT/api/ ## Workaround to wait for the http server running ## See: https://github.com/elastic/kibana/issues/66326 if [ -e kibana.log ] ; then - grep -m 1 "http server running" <(tail -f -n +1 kibana.log) + grep -m 1 "Kibana is now available" <(tail -f -n +1 kibana.log) echo "✅ Kibana server running..." grep -m 1 "bundles compiled successfully" <(tail -f -n +1 kibana.log) echo "✅ Kibana bundles have been compiled..." diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx index e9050d313fbaf..73e839433c25e 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx @@ -24,7 +24,7 @@ import { CANVAS_EMBEDDABLE_CLASSNAME } from '../../../common/lib'; const { embeddable: strings } = RendererStrings; const embeddablesRegistry: { - [key: string]: IEmbeddable; + [key: string]: IEmbeddable | Promise; } = {}; const renderEmbeddableFactory = (core: CoreStart, plugins: StartDeps) => { @@ -67,7 +67,15 @@ export const embeddableRendererFactory = ( throw new EmbeddableFactoryNotFoundError(embeddableType); } - const embeddableObject = await factory.createFromSavedObject(input.id, input); + const embeddablePromise = factory + .createFromSavedObject(input.id, input) + .then((embeddable) => { + embeddablesRegistry[uniqueId] = embeddable; + return embeddable; + }); + embeddablesRegistry[uniqueId] = embeddablePromise; + + const embeddableObject = await (async () => embeddablePromise)(); const palettes = await plugins.charts.palettes.getPalettes(); @@ -105,8 +113,12 @@ export const embeddableRendererFactory = ( return ReactDOM.unmountComponentAtNode(domNode); }); } else { - embeddablesRegistry[uniqueId].updateInput(input); - embeddablesRegistry[uniqueId].reload(); + const embeddable = embeddablesRegistry[uniqueId]; + + if ('updateInput' in embeddable) { + embeddable.updateInput(input); + embeddable.reload(); + } } }, }); diff --git a/x-pack/plugins/encrypted_saved_objects/.eslintrc.json b/x-pack/plugins/encrypted_saved_objects/.eslintrc.json deleted file mode 100644 index 2b63a9259d220..0000000000000 --- a/x-pack/plugins/encrypted_saved_objects/.eslintrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "rules": { - "@typescript-eslint/consistent-type-imports": 1 - } -} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.test.tsx index 75c22d2ae9473..8a355b97e7b9f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.test.tsx @@ -64,6 +64,20 @@ describe('Boosts', () => { expect(select.prop('options').map((o: any) => o.value)).toEqual(['add-boost', 'value']); }); + it('will not render functional or value options if "type" prop is "geolocation"', () => { + const wrapper = shallow( + + ); + + const select = wrapper.find(EuiSuperSelect); + expect(select.prop('options').map((o: any) => o.value)).toEqual(['add-boost', 'proximity']); + }); + it('will add a boost of the selected type when a selection is made', () => { const wrapper = shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.tsx index d6d43ea7beab0..4268e21110277 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.tsx @@ -13,7 +13,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle, EuiSuperSelect } from '@ import { i18n } from '@kbn/i18n'; -import { TEXT } from '../../../../shared/constants/field_types'; +import { GEOLOCATION, TEXT } from '../../../../shared/constants/field_types'; import { SchemaTypes } from '../../../../shared/types'; import { BoostIcon } from '../boost_icon'; @@ -68,6 +68,8 @@ const BASE_OPTIONS = [ const filterInvalidOptions = (value: BoostType, type: SchemaTypes) => { // Proximity and Functional boost types are not valid for text fields if (type === TEXT && [BoostType.Proximity, BoostType.Functional].includes(value)) return false; + // Value and Functional boost types are not valid for geolocation fields + if (type === GEOLOCATION && [BoostType.Functional, BoostType.Value].includes(value)) return false; return true; }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx index 0ae3c8fd3b5dc..70adc91dd2b30 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx @@ -19,6 +19,8 @@ import { DOCS_PREFIX } from '../../routes'; import { RelevanceTuningForm } from './relevance_tuning_form'; import { RelevanceTuningLayout } from './relevance_tuning_layout'; +import { RelevanceTuningPreview } from './relevance_tuning_preview'; + import { RelevanceTuningLogic } from '.'; interface Props { @@ -81,11 +83,13 @@ export const RelevanceTuning: React.FC = ({ engineBreadcrumb }) => { } return ( - - + + - + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.tsx index 87b9e1615774f..ab72f29a678c9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.tsx @@ -10,8 +10,6 @@ import React from 'react'; import { useActions, useValues } from 'kea'; import { - EuiPageHeader, - EuiPageHeaderSection, EuiTitle, EuiFieldSearch, EuiSpacer, @@ -44,37 +42,36 @@ export const RelevanceTuningForm: React.FC = () => { return (
- - - -

- {i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.manageFields.title', - { - defaultMessage: 'Manage fields', - } - )} -

-
-
-
- {schemaFields.length > FIELD_FILTER_CUTOFF && ( - setFilterValue(e.target.value)} - placeholder={i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.manageFields.filterPlaceholder', + +

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.manageFields.title', { - defaultMessage: 'Filter {schemaFieldsLength} fields...', - values: { - schemaFieldsLength: schemaFields.length, - }, + defaultMessage: 'Manage fields', } )} - fullWidth - /> - )} +

+
+ {schemaFields.length > FIELD_FILTER_CUTOFF && ( + <> + setFilterValue(e.target.value)} + placeholder={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.manageFields.filterPlaceholder', + { + defaultMessage: 'Filter {schemaFieldsLength} fields...', + values: { + schemaFieldsLength: schemaFields.length, + }, + } + )} + fullWidth + /> + + + )} {filteredSchemaFields.map((fieldName) => ( { expect(mockEngineActions.initializeEngine).toHaveBeenCalled(); }); - it('will re-fetch the current engine after settings are updated if there were unconfirmed search fieldds', async () => { + it('will re-fetch the current engine after settings are updated if there were unconfirmed search fields', async () => { mockEngineValues.engine.unsearchedUnconfirmedFields = true; mount({}); http.put.mockReturnValueOnce(Promise.resolve(searchSettings)); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_preview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_preview.test.tsx new file mode 100644 index 0000000000000..ec6458a14b346 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_preview.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { setMockActions, setMockValues } from '../../../__mocks__/kea.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiFieldSearch } from '@elastic/eui'; + +import { Result } from '../result/result'; + +import { RelevanceTuningPreview } from './relevance_tuning_preview'; + +describe('RelevanceTuningPreview', () => { + const result1 = { id: { raw: 1 } }; + const result2 = { id: { raw: 2 } }; + const result3 = { id: { raw: 3 } }; + + const actions = { + updateSearchValue: jest.fn(), + }; + + const values = { + searchResults: [result1, result2, result3], + engineName: 'foo', + isMetaEngine: false, + schema: {}, + }; + + beforeAll(() => { + setMockActions(actions); + setMockValues(values); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiFieldSearch).prop('placeholder')).toBe('Search foo'); + + const results = wrapper.find(Result); + expect(results.length).toBe(3); + expect(results.at(0).prop('result')).toBe(result1); + expect(results.at(0).prop('isMetaEngine')).toBe(false); + expect(results.at(0).prop('showScore')).toBe(true); + expect(results.at(0).prop('schemaForTypeHighlights')).toBe(values.schema); + + expect(results.at(1).prop('result')).toBe(result2); + expect(results.at(2).prop('result')).toBe(result3); + + expect(wrapper.find('[data-test-subj="EmptyQueryPrompt"]').exists()).toBe(false); + expect(wrapper.find('[data-test-subj="NoResultsPrompt"]').exists()).toBe(false); + }); + + it('correctly indicates whether or not this is a meta engine in results', () => { + setMockValues({ + ...values, + isMetaEngine: true, + }); + + const wrapper = shallow(); + + const results = wrapper.find(Result); + expect(results.at(0).prop('isMetaEngine')).toBe(true); + expect(results.at(1).prop('isMetaEngine')).toBe(true); + expect(results.at(2).prop('isMetaEngine')).toBe(true); + }); + + it('renders a search box that will update search results whenever it is changed', () => { + const wrapper = shallow(); + + wrapper.find(EuiFieldSearch).simulate('change', { target: { value: 'some search text' } }); + + expect(actions.updateSearchValue).toHaveBeenCalledWith('some search text'); + }); + + it('will show user a prompt to enter a query if they have not entered one', () => { + setMockValues({ + ...values, + // Since `searchResults` is initialized as undefined, an undefined value indicates + // that no query has been performed, which means they have no yet entered a query + searchResults: undefined, + }); + + const wrapper = shallow(); + + expect(wrapper.find('[data-test-subj="EmptyQueryPrompt"]').exists()).toBe(true); + expect(wrapper.find('[data-test-subj="NoResultsPrompt"]').exists()).toBe(false); + }); + + it('will show user a no results message if their query returns no results', () => { + setMockValues({ + ...values, + searchResults: [], + }); + + const wrapper = shallow(); + + expect(wrapper.find('[data-test-subj="EmptyQueryPrompt"]').exists()).toBe(false); + expect(wrapper.find('[data-test-subj="NoResultsPrompt"]').exists()).toBe(true); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_preview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_preview.tsx new file mode 100644 index 0000000000000..298b692ac7b80 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_preview.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions, useValues } from 'kea'; + +import { EuiEmptyPrompt, EuiFieldSearch, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { EngineLogic } from '../engine'; +import { Result } from '../result/result'; + +import { RelevanceTuningLogic } from '.'; + +const emptyCallout = ( + +); + +const noResultsCallout = ( + +); + +export const RelevanceTuningPreview: React.FC = () => { + const { updateSearchValue } = useActions(RelevanceTuningLogic); + const { searchResults, schema } = useValues(RelevanceTuningLogic); + const { engineName, isMetaEngine } = useValues(EngineLogic); + + return ( + + +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.engine.relevanceTuning.preview.title', { + defaultMessage: 'Preview', + })} +

+
+ + updateSearchValue(e.target.value)} + placeholder={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.preview.searchPlaceholder', + { + defaultMessage: 'Search {engineName}', + values: { + engineName, + }, + } + )} + fullWidth + /> + {!searchResults && emptyCallout} + {searchResults && searchResults.length === 0 && noResultsCallout} + {searchResults && + searchResults.map((result) => { + return ( + + + + + ); + })} +
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/search_settings.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/search_settings.ts index 82b0497cd0946..d329c9b834b08 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/search_settings.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/search_settings.ts @@ -79,7 +79,7 @@ export function registerSearchSettingsRoutes({ engineName: schema.string(), }), body: schema.object({ - boosts, + boosts: schema.maybe(boosts), search_fields: searchFields, }), query: schema.object({ diff --git a/x-pack/plugins/event_log/README.md b/x-pack/plugins/event_log/README.md index 9c806680f68a2..2656718cc15ea 100644 --- a/x-pack/plugins/event_log/README.md +++ b/x-pack/plugins/event_log/README.md @@ -1,188 +1,279 @@ # Event Log +The event log plugin provides a persistent history of alerting and action +actitivies. + ## Overview -The purpose of this plugin is to provide a way to persist a history of events -occuring in Kibana, initially just for the Make It Action project - alerts -and actions. +This plugin provides a persistent log of "events" that can be used by other +plugins to record their processing, for later acccess. Currently it's only +used by the alerts and actions plugins. +The "events" are ECS documents, with some custom properties for Kibana, and +alerting-specific properties within those Kibana properties. The number of +ECS fields is limited today, but can be extended fairly easily. We are being +conservative in adding new fields though, to help prevent indexing explosions. -## Basic Usage - Logging Events +A client API is available for other plugins to: -Follow these steps to use `eventLog` in your plugin: +- register the events they want to write +- write the events, with helpers for `duration` calculation, etc +- query the events -1. Declare `eventLog` as a dependency in `kibana.json`: +HTTP APIs are also available to query the events. -```json +Currently, events are written with references to Saved Objects, and queries +against the event log must include the Saved Object references that the query +should return events for. This is the basic security mechanism to prevent +users from accessing events for Saved Objects that they do not have access to. +The queries ensure that the user can read the referenced Saved Objects before +returning the events relating to them. + +The default index name is `.kibana-event-log-${kibanaVersion}-${ILM-sequence}`. + +The index written to is controlled by ILM. The ILM policy is initially created +by the plugin, but is otherwise never updated by the plugin. This allows +customers to customize it to their environment, without having to worry about +their updates getting overwritten by newer versions of Kibana. +The policy provides some default phases to roll over and delete older +indices. The name of the policy is `kibana-event-log-policy`. + + +## Event Documents + +The structure of the event documents can be seen in the +[mappings](generated/mappings.json) and +[config-schema definitions](generated/schemas.ts). Note these files are +generated via a script when the structure changes. See the +[README.md](generated/README.md) for how to change the document structure. + +Below is an document in the expected structure, with descriptions of the fields: + +```js { - ... - "requiredPlugins": ["eventLog"], - ... + "@timestamp": "ISO date", + tags: ["tags", "here"], + message: "message for humans here", + ecs: { + version: "version of ECS used by the event log", + }, + event: { + provider: "see below", + action: "see below", + start: "ISO date of start time for events that capture a duration", + duration: "duration in nanoseconds for events that capture a duration", + end: "ISO date of end time for events that capture a duration", + outcome: "success | failure, for events that indicate an outcome", + reason: "additional detail on failure outcome", + }, + error: { + message: "an error message, usually associated with outcome: failure", + }, + user: { + name: "name of Kibana user", + }, + kibana: { // custom ECS field + server_uuid: "UUID of kibana server, for diagnosing multi-Kibana scenarios", + alerting: { + instance_id: "alert instance id, for relevant documents", + action_group_id: "alert action group, for relevant documents", + action_subgroup_id: "alert action subgroup, for relevant documents", + status: "overall alert status, after alert execution", + }, + saved_objects: [ + { + rel: "'primary' | undefined; see below", + namespace: "${spaceId} | undefined", + id: "saved object id", + type: " saved object type", + }, + ], + }, } ``` -2. Register provider / actions, and create your plugin's logger, using service -API provided in the `setup` stage: +The `event.provider` and `event.action` fields provide a scoped mechanism for +describing who is generating the event, and what kind of event it is. Plugins +that write events need to register the `provider` and `action` values they +will be using. Generally, each plugin should provide it's own `provider`, +but a plugin could provide multiple providers, or a single provider might be +used by multiple plugins. -```typescript -... -import { IEventLogger, IEventLogService } from '../../event_log/server'; -interface PluginSetupDependencies { - eventLog: IEventLogService; -} -... -public setup(core: CoreSetup, { eventLog }: PluginSetupDependencies) { - ... - eventLog.registerProviderActions('my-plugin', ['action-1, action-2']); - const eventLogger: IEventLogger = eventLog.getLogger({ event: { provider: 'my-plugin' } }); - ... -} -... -``` +The following `provider` / `action` pairs are used by the alerting and actions +plugins: -4. To log an event, call `logEvent()` on the `eventLogger` object you created: +- `provider: actions` + - `action: execute` - generated when an action is executed by the actions client + - `action: execute-via-http` - generated when an action is executed via HTTP request -```typescript -... - eventLogger.logEvent({ event: { action: 'action-1' }, tags: ['fe', 'fi', 'fo'] }); -... -``` +- `provider: alerting` + - `action: execute` - generated when an alert executor runs + - `action: execute-action` - generated when an alert schedules an action to run + - `action: new-instance` - generated when an alert has a new instance id that is active + - `action: recovered-instance` - generated when an alert has a previously active instance id that is no longer active + - `action: active-instance` - generated when an alert determines an instance id is active +For the `saved_objects` array elements, these are references to saved objects +associated with the event. For the `alerting` provider, those are alert saved +ojects and for the `actions` provider those are action saved objects. The +`alerts:execute-action` event includes both the alert and action saved object +references. For that event, only the alert reference has the optional `rel` +property with a `primary` value. This property is used when searching the +event log to indicate which saved objects should be directly searchable via +saved object references. For the `alerts:execute-action` event, searching +only via the alert saved object reference will return the event. -## Testing -### Unit tests +## Event Log index - associated resources -Documentation: https://www.elastic.co/guide/en/kibana/current/development-tests.html#_unit_testing +The index template and ILM policy are defined in the file +[`x-pack/plugins/event_log/server/es/documents.ts`](server/es/documents.ts). -``` -yarn test:jest x-pack/plugins/event_log --watch -``` +See [ILM rollover action docs][] for more information on the `is_write_index` +and `index.lifecycle.*` properties. -### API Integration tests +[ILM rollover action docs]: https://www.elastic.co/guide/en/elasticsearch/reference/current/ilm-rollover.html -None yet! +## Using the Event Log for diagnosing alerting and actions issues -## Background +For ad-hoc diagnostic purposes, your go to tools are Discover and Lens. Your +user will need to have access to the index, which is considered a Kibana +system index due to it's prefix. -For the Make It Action alerting / action plugins, we will need a way to -persist data regarding alerts and actions, for UI and investigative purposes. -We're referring to this persisted data as "events", and will be persisted to -a new elasticsearch index referred to as the "event log". +Add the event log index as an index pattern. The only customization needed is +to set the `event.duration` field to a duration in nanoseconds. You'll +probably want it displayed as milliseconds. -Example events are actions firing, alerts running their scheduled functions, -alerts scheduling actions to run, etc. -This functionality will be provided in a new NP plugin `eventLog`, and will -provide server-side plugin APIs to write to the event log, and run limited -queries against it. For now, access via HTTP will not be available, due to -security concerns and lack of use cases. +## Experimental RESTful API for querying -The current clients for the event log are the actions and alerting plugins, -however the event log currently has nothing specific to them, and is general -purpose, so can be used by any plugin to "log events". +As this plugin is space-aware, prefix any URL below with the usual `/s/{space}` +to target a space other than the default space. -We currently assume that there may be many events logged, and that (some) customers -may not be interested in "old" events, and so to keep the event log from -consuming too much disk space, we'll set it up with ILM and some kind of -reasonable default policy that can be customized by the user. This implies -also the use of rollver, setting a write index alias upon rollover, and -that searches for events will be done via an ES index pattern / alias to search -across event log indices with a wildcard. +Usage of the event log allows you to retrieve the events for a given saved object type by the specified set of IDs. +The following API is experimental and can change or be removed in a future release. -The shape of the documents indexed into the event log index is a subset of ECS -properties with a few Kibana extensions. Over time the subset is of ECS and -Kibana extensions will likely grow. +### `GET /api/event_log/{type}/{id}/_find`: Get events for a given saved object type by the ID -# Basic example +Collects event information from the event log for the selected saved object by type and ID. -When an action is executed, an event should be written to the event log. +Params: -Here's a [`kbn-action` command](https://github.com/pmuellr/kbn-action) to -execute a "server log" action (writes a message to the Kibana log): +|Property|Description|Type| +|---|---|---| +|type|The type of the saved object whose events you're trying to get.|string| +|id|The id of the saved object.|string| -```console -$ kbn-action execute 79b4c37e-ef42-4421-a0b0-b536840f930d '{level:info message:hallo}' -{ - "status": "ok" -} -``` +Query: -Here's the event written to the event log index: +|Property|Description|Type| +|---|---|---| +|page|The page number.|number| +|per_page|The number of events to return per page.|number| +|sort_field|Sorts the response. Could be an event fields returned in the response.|string| +|sort_order|Sort direction, either `asc` or `desc`.|string| +|filter|A KQL string that you filter with an attribute from the event. It should look like `event.action:(execute)`.|string| +|start|The date to start looking for saved object events in the event log. Either an ISO date string, or a duration string that indicates the time since now.|string| +|end|The date to stop looking for saved object events in the event log. Either an ISO date string, or a duration string that indicates the time since now.|string| -```json -{ - "_index": ".kibana-event-log-000001", - "_type": "_doc", - "_id": "d2CXT20BPOpswQ8vgXp5", - "_score": 1, - "_source": { - "event": { - "provider": "actions", - "action": "execute", - "start": "2019-12-09T21:16:43.424Z", - "end": "2019-12-09T21:16:43.425Z", - "duration": 1000000 - }, - "kibana": { - "saved_objects": [ - { - "type": "action", - "id": "79b4c37e-ef42-4421-a0b0-b536840f930d" - } - ] - }, - "message": "action executed successfully: 79b4c37e-ef42-4421-a0b0-b536840f930d - .server-log - server-log", - "@timestamp": "2019-12-09T21:16:43.425Z", - "ecs": { - "version": "1.3.1" - } - } -} -``` +Response body: -The shape of the document written to the index is a subset of [ECS][] with an -extended field of `kibana` with some Kibana-related properties contained within -it. +See `QueryEventsBySavedObjectResult` in the Plugin Client APIs below. -The ES mappings for the ECS data, and the config-schema for the ECS data, are -generated by a script, and available here: +### `POST /api/event_log/{type}/_find`: Retrive events for a given saved object type by the IDs -- [`generated/mappings.json`](generated/mappings.json) -- [`generated/schemas.ts`](generated/schemas.ts) +Collects event information from the event log for the selected saved object by type and by IDs. -It's anticipated that these interfaces will grow over time, hopefully adding -more ECS fields but adding Kibana extensions as required. +Params: -Since there are some security concerns with the data, we are currently -restricting access via known saved object ids. That is, you can only query -history records associated with specific saved object ids. +|Property|Description|Type| +|---|---|---| +|type|The type of the saved object whose events you're trying to get.|string| -[ECS]: https://www.elastic.co/guide/en/ecs/current/index.html +Query: +|Property|Description|Type| +|---|---|---| +|page|The page number.|number| +|per_page|The number of events to return per page.|number| +|sort_field|Sorts the response. Could be an event field returned in the response.|string| +|sort_order|Sort direction, either `asc` or `desc`.|string| +|filter|A KQL string that you filter with an attribute from the event. It should look like `event.action:(execute)`.|string| +|start|The date to start looking for saved object events in the event log. Either an ISO date string, or a duration string that indicates the time since now.|string| +|end|The date to stop looking for saved object events in the event log. Either an ISO date string, or a duration string that indicates the time since now.|string| -## API +Request Body: -Event Log plugin returns a service instance from setup() and client service from start() methods. +|Property|Description|Type| +|---|---|---| +|ids|The array ids of the saved object.|string array| -### Setup -```typescript -// IEvent is a TS type generated from the subset of ECS supported +Response body: + +See `QueryEventsBySavedObjectResult` in the Plugin Client APIs below. + + +## Plugin Client APIs for querying + +```ts +interface EventLogClient { + findEventsBySavedObjectIds( + type: string, + ids: string[], + options?: Partial + ): Promise; +} + +interface FindOptionsType { /* typed version of HTTP query parameters ^^^ */ } + +interface QueryEventsBySavedObjectResult { + page: number; + per_page: number; + total: number; + data: Event[]; +} +``` -export interface IEventLogService { - registerProviderActions(provider: string, actions: string[]): void; - isProviderActionRegistered(provider: string, action: string): boolean; - getProviderActions(): Map>; +## Generating Events + +Follow these steps to use `eventLog` in your plugin: - getLogger(properties: IEvent): IEventLogger; +1. Declare `eventLog` as a dependency in `kibana.json`: + +```json +{ + ... + "requiredPlugins": ["eventLog"], + ... } +``` + +2. Register provider / actions, and create your plugin's logger, using the +service API provided in the `setup` stage: -export interface IEventLogger { - logEvent(properties: IEvent): void; - startTiming(event: IEvent): void; - stopTiming(event: IEvent): void; +```typescript +... +import { IEventLogger, IEventLogService } from '../../event_log/server'; +interface PluginSetupDependencies { + eventLog: IEventLogService; } +... +public setup(core: CoreSetup, { eventLog }: PluginSetupDependencies) { + ... + eventLog.registerProviderActions('my-plugin', ['action-1, action-2']); + const eventLogger: IEventLogger = eventLog.getLogger({ event: { provider: 'my-plugin' } }); + ... +} +... +``` + +3. To log an event, call `logEvent()` on the `eventLogger` object you created: + +```typescript +... + eventLogger.logEvent({ event: { action: 'action-1' }, tags: ['fe', 'fi', 'fo'] }); +... ``` The plugin exposes an `IEventLogService` object to plugins that pre-req it. @@ -211,15 +302,15 @@ that result is validated to ensure it's complete and valid. Errors will be logged to the server log. The `logEvent()` method returns no values, and is itself not asynchronous. -It's a "call and forget" kind of thing. The method itself will arrange -to have the ultimate document written to the index asynchronously. It's designed +The messages are queued written asynchonously in bulk. It's designed this way because it's not clear what a client would do with a result from this method, nor what it would do if the method threw an error. All the error processing involved with getting the data into the index is handled internally, and logged to the server log as appropriate. -The `startTiming()` and `stopTiming()` methods can be used to set the timing -properties `start`, `end`, and `duration` in the event. For example: +There are additional utility methods `startTiming()` and `stopTiming()` which +can be used to set the timing properties `start`, `end`, and `duration` in the +event. For example: ```typescript const loggedEvent: IEvent = { event: { action: 'foo' } }; @@ -258,124 +349,17 @@ export interface IEventLogClient { The plugin exposes an `IEventLogClientService` object to plugins that request it. These plugins must call `getClient(request)` to get the event log client. -## Experimental RESTful API - -Usage of the event log allows you to retrieve the events for a given saved object type by the specified set of IDs. -The following API is experimental and can change or be removed in a future release. - -### `GET /api/event_log/{type}/{id}/_find`: Get events for a given saved object type by the ID - -Collects event information from the event log for the selected saved object by type and ID. - -Params: - -|Property|Description|Type| -|---|---|---| -|type|The type of the saved object whose events you're trying to get.|string| -|id|The id of the saved object.|string| - -Query: - -|Property|Description|Type| -|---|---|---| -|page|The page number.|number| -|per_page|The number of events to return per page.|number| -|sort_field|Sorts the response. Could be an event fields returned in the response.|string| -|sort_order|Sort direction, either `asc` or `desc`.|string| -|filter|A KQL string that you filter with an attribute from the event. It should look like `event.action:(execute)`.|string| -|start|The date to start looking for saved object events in the event log. Either an ISO date string, or a duration string that indicates the time since now.|string| -|end|The date to stop looking for saved object events in the event log. Either an ISO date string, or a duration string that indicates the time since now.|string| - -### `POST /api/event_log/{type}/_find`: Retrive events for a given saved object type by the IDs - -Collects event information from the event log for the selected saved object by type and by IDs. - -Params: - -|Property|Description|Type| -|---|---|---| -|type|The type of the saved object whose events you're trying to get.|string| - -Query: - -|Property|Description|Type| -|---|---|---| -|page|The page number.|number| -|per_page|The number of events to return per page.|number| -|sort_field|Sorts the response. Could be an event field returned in the response.|string| -|sort_order|Sort direction, either `asc` or `desc`.|string| -|filter|A KQL string that you filter with an attribute from the event. It should look like `event.action:(execute)`.|string| -|start|The date to start looking for saved object events in the event log. Either an ISO date string, or a duration string that indicates the time since now.|string| -|end|The date to stop looking for saved object events in the event log. Either an ISO date string, or a duration string that indicates the time since now.|string| - -Body: - -|Property|Description|Type| -|---|---|---| -|ids|The array ids of the saved object.|string array| - -## Stored data - -The elasticsearch index for the event log will have ILM and rollover support, -as customers may decide to only keep recent event documents, wanting indices -with older event documents deleted, turned cold, frozen, etc. We'll supply -some default values, but customers will be able to tweak these. - -The index template, mappings, config-schema types, etc for the index can -be found in the [generated directory](generated). These files are generated -from a script which takes as input the ECS properties to use, and the Kibana -extensions. - -See [ilm rollover action docs][] for more info on the `is_write_index`, and `index.lifecycle.*` properties. - -[ilm rollover action docs]: https://www.elastic.co/guide/en/elasticsearch/reference/current/_actions.html#ilm-rollover-action - -Of particular note in the `mappings`: - -- all "objects" are `dynamic: 'strict'` implies users can't add new fields -- all the `properties` are indexed - -We may change some of that before releasing. - +## Testing -## ILM setup +### Unit tests -We'll want to provide default ILM policy, this seems like a reasonable first -attempt: +Documentation: https://www.elastic.co/guide/en/kibana/current/development-tests.html#_unit_testing ``` -PUT _ilm/policy/event_log_policy -{ - "policy": { - "phases": { - "hot": { - "actions": { - "rollover": { - "max_size": "50GB", - "max_age": "30d" - } - } - }, - "delete": { - "min_age": "90d", - "actions": { - "delete": {} - } - } - } - } -} +yarn test:jest x-pack/plugins/event_log --watch ``` -This means that ILM would "rollover" the current index, say -`.kibana-event-log-8.0.0-000001` by creating a new index `.kibana-event-log-8.0.0-000002`, -which would "inherit" everything from the index template, and then ILM will -set the write index of the the alias to the new index. This would happen -when the original index grew past 50 GB, or was created more than 30 days ago. -After rollover, the indices will be removed after 90 days to avoid disks to fill up. +### API Integration tests -For more relevant information on ILM, see: -[getting started with ILM doc][] and [write index alias behavior][]: +See: [`x-pack/test/plugin_api_integration/test_suites/event_log`](https://github.com/elastic/kibana/tree/master/x-pack/test/plugin_api_integration/test_suites/event_log). -[getting started with ILM doc]: https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-started-index-lifecycle-management.html -[write index alias behavior]: https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-rollover-index.html#indices-rollover-is-write-index diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index ca397f73ee770..a710e51878db4 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -231,7 +231,7 @@ export interface RegistryElasticsearch { 'index_template.mappings'?: object; } -export type RegistryVarType = 'integer' | 'bool' | 'password' | 'text' | 'yaml'; +export type RegistryVarType = 'integer' | 'bool' | 'password' | 'text' | 'yaml' | 'string'; // EPR types this as `[]map[string]interface{}` // which means the official/possible type is Record // but we effectively only see this shape diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/validate_package_policy.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/validate_package_policy.ts index 3773f071ffb4c..1874a458d8541 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/validate_package_policy.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/validate_package_policy.ts @@ -229,6 +229,37 @@ export const validatePackagePolicyConfig = ( }) ); } + if ( + (varDef.type === 'text' || varDef.type === 'string') && + parsedValue && + Array.isArray(parsedValue) + ) { + const invalidStrings = parsedValue.filter((cand) => /^[*&]/.test(cand)); + // only show one error if multiple strings in array are invalid + if (invalidStrings.length > 0) { + errors.push( + i18n.translate('xpack.fleet.packagePolicyValidation.quoteStringErrorMessage', { + defaultMessage: + 'Strings starting with special YAML characters like * or & need to be enclosed in double quotes.', + }) + ); + } + } + } + + if ( + (varDef.type === 'text' || varDef.type === 'string') && + parsedValue && + !Array.isArray(parsedValue) + ) { + if (/^[*&]/.test(parsedValue)) { + errors.push( + i18n.translate('xpack.fleet.packagePolicyValidation.quoteStringErrorMessage', { + defaultMessage: + 'Strings starting with special YAML characters like * or & need to be enclosed in double quotes.', + }) + ); + } } return errors.length ? errors : null; diff --git a/x-pack/plugins/fleet/server/mocks.ts b/x-pack/plugins/fleet/server/mocks/index.ts similarity index 82% rename from x-pack/plugins/fleet/server/mocks.ts rename to x-pack/plugins/fleet/server/mocks/index.ts index ea3ba20c59e9c..cff80f533d5e3 100644 --- a/x-pack/plugins/fleet/server/mocks.ts +++ b/x-pack/plugins/fleet/server/mocks/index.ts @@ -4,22 +4,18 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { elasticsearchServiceMock, loggingSystemMock, savedObjectsServiceMock, -} from 'src/core/server/mocks'; - -import { coreMock } from '../../../../src/core/server/mocks'; -import { licensingMock } from '../../../plugins/licensing/server/mocks'; - -import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks'; -import { securityMock } from '../../security/server/mocks'; - -import type { FleetAppContext } from './plugin'; -import type { PackagePolicyServiceInterface } from './services/package_policy'; -import type { AgentPolicyServiceInterface, AgentService } from './services'; + coreMock, +} from '../../../../../src/core/server/mocks'; +import { licensingMock } from '../../../../plugins/licensing/server/mocks'; +import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks'; +import { securityMock } from '../../../security/server/mocks'; +import type { PackagePolicyServiceInterface } from '../services/package_policy'; +import type { AgentPolicyServiceInterface, AgentService } from '../services'; +import type { FleetAppContext } from '../plugin'; export const createAppContextStartContractMock = (): FleetAppContext => { return { diff --git a/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts b/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts index 4509deee0d00f..a76a8b9672d21 100644 --- a/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts @@ -183,71 +183,36 @@ input: logs it('should escape string values when necessary', () => { const stringTemplate = ` my-package: - opencurly: {{opencurly}} - closecurly: {{closecurly}} - opensquare: {{opensquare}} - closesquare: {{closesquare}} - ampersand: {{ampersand}} - asterisk: {{asterisk}} - question: {{question}} - pipe: {{pipe}} - hyphen: {{hyphen}} - openangle: {{openangle}} - closeangle: {{closeangle}} - equals: {{equals}} - exclamation: {{exclamation}} - percent: {{percent}} - at: {{at}} - colon: {{colon}} + asteriskOnly: {{asteriskOnly}} + startsWithAsterisk: {{startsWithAsterisk}} numeric: {{numeric}} - mixed: {{mixed}}`; + mixed: {{mixed}} + concatenatedEnd: {{a}}{{b}} + concatenatedMiddle: {{c}}{{d}} + mixedMultiline: |- + {{{ search }}} | streamstats`; - // List of special chars that may lead to YAML parsing errors when not quoted. - // See YAML specification section 5.3 Indicator characters - // https://yaml.org/spec/1.2/spec.html#id2772075 - // {,},[,],&,*,?,|,-,<,>,=,!,%,@,: const vars = { - opencurly: { value: '{', type: 'string' }, - closecurly: { value: '}', type: 'string' }, - opensquare: { value: '[', type: 'string' }, - closesquare: { value: ']', type: 'string' }, - comma: { value: ',', type: 'string' }, - ampersand: { value: '&', type: 'string' }, - asterisk: { value: '*', type: 'string' }, - question: { value: '?', type: 'string' }, - pipe: { value: '|', type: 'string' }, - hyphen: { value: '-', type: 'string' }, - openangle: { value: '<', type: 'string' }, - closeangle: { value: '>', type: 'string' }, - equals: { value: '=', type: 'string' }, - exclamation: { value: '!', type: 'string' }, - percent: { value: '%', type: 'string' }, - at: { value: '@', type: 'string' }, - colon: { value: ':', type: 'string' }, + asteriskOnly: { value: '"*"', type: 'string' }, + startsWithAsterisk: { value: '"*lala"', type: 'string' }, numeric: { value: '100', type: 'string' }, mixed: { value: '1s', type: 'string' }, + a: { value: '/opt/package/*', type: 'string' }, + b: { value: '/logs/my.log*', type: 'string' }, + c: { value: '/opt/*/package/', type: 'string' }, + d: { value: 'logs/*my.log', type: 'string' }, + search: { value: 'search sourcetype="access*"', type: 'text' }, }; const targetOutput = { 'my-package': { - opencurly: '{', - closecurly: '}', - opensquare: '[', - closesquare: ']', - ampersand: '&', - asterisk: '*', - question: '?', - pipe: '|', - hyphen: '-', - openangle: '<', - closeangle: '>', - equals: '=', - exclamation: '!', - percent: '%', - at: '@', - colon: ':', + asteriskOnly: '*', + startsWithAsterisk: '*lala', numeric: '100', mixed: '1s', + concatenatedEnd: '/opt/package/*/logs/my.log*', + concatenatedMiddle: '/opt/*/package/logs/*my.log', + mixedMultiline: 'search sourcetype="access*" | streamstats', }, }; diff --git a/x-pack/plugins/fleet/server/services/epm/agent/agent.ts b/x-pack/plugins/fleet/server/services/epm/agent/agent.ts index bbdc99b880e8c..26e1497e93852 100644 --- a/x-pack/plugins/fleet/server/services/epm/agent/agent.ts +++ b/x-pack/plugins/fleet/server/services/epm/agent/agent.ts @@ -59,13 +59,8 @@ function replaceVariablesInYaml(yamlVariables: { [k: string]: any }, yaml: any) } const maybeEscapeString = (value: string) => { - // List of special chars that may lead to YAML parsing errors when not quoted. - // See YAML specification section 5.3 Indicator characters - // https://yaml.org/spec/1.2/spec.html#id2772075 - const yamlSpecialCharsRegex = /[{}\[\],&*?|\-<>=!%@:]/; - - // In addition, numeric strings need to be quoted to stay strings. - if ((value.length && !isNaN(+value)) || yamlSpecialCharsRegex.test(value)) { + // Numeric strings need to be quoted to stay strings. + if (value.length && !isNaN(+value)) { return `"${value}"`; } return value; diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap index 2d2478843c454..65eec939d5850 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`tests loading base.yml: base.yml 1`] = ` +exports[`EPM template tests loading base.yml: base.yml 1`] = ` { "priority": 200, "index_patterns": [ @@ -20,12 +20,12 @@ exports[`tests loading base.yml: base.yml 1`] = ` }, "refresh_interval": "5s", "number_of_shards": "1", + "number_of_routing_shards": "30", "query": { "default_field": [ - "message" + "long.nested.foo" ] - }, - "number_of_routing_shards": "30" + } } }, "mappings": { @@ -109,7 +109,7 @@ exports[`tests loading base.yml: base.yml 1`] = ` } `; -exports[`tests loading coredns.logs.yml: coredns.logs.yml 1`] = ` +exports[`EPM template tests loading coredns.logs.yml: coredns.logs.yml 1`] = ` { "priority": 200, "index_patterns": [ @@ -129,12 +129,17 @@ exports[`tests loading coredns.logs.yml: coredns.logs.yml 1`] = ` }, "refresh_interval": "5s", "number_of_shards": "1", + "number_of_routing_shards": "30", "query": { "default_field": [ - "message" + "coredns.id", + "coredns.query.class", + "coredns.query.name", + "coredns.query.type", + "coredns.response.code", + "coredns.response.flags" ] - }, - "number_of_routing_shards": "30" + } } }, "mappings": { @@ -218,7 +223,7 @@ exports[`tests loading coredns.logs.yml: coredns.logs.yml 1`] = ` } `; -exports[`tests loading system.yml: system.yml 1`] = ` +exports[`EPM template tests loading system.yml: system.yml 1`] = ` { "priority": 200, "index_patterns": [ @@ -238,12 +243,45 @@ exports[`tests loading system.yml: system.yml 1`] = ` }, "refresh_interval": "5s", "number_of_shards": "1", + "number_of_routing_shards": "30", "query": { "default_field": [ - "message" + "system.diskio.name", + "system.diskio.serial_number", + "system.filesystem.device_name", + "system.filesystem.type", + "system.filesystem.mount_point", + "system.network.name", + "system.process.state", + "system.process.cmdline", + "system.process.cgroup.id", + "system.process.cgroup.path", + "system.process.cgroup.cpu.id", + "system.process.cgroup.cpu.path", + "system.process.cgroup.cpuacct.id", + "system.process.cgroup.cpuacct.path", + "system.process.cgroup.memory.id", + "system.process.cgroup.memory.path", + "system.process.cgroup.blkio.id", + "system.process.cgroup.blkio.path", + "system.raid.name", + "system.raid.status", + "system.raid.level", + "system.raid.sync_action", + "system.socket.remote.host", + "system.socket.remote.etld_plus_one", + "system.socket.remote.host_error", + "system.socket.process.cmdline", + "system.users.id", + "system.users.seat", + "system.users.path", + "system.users.type", + "system.users.service", + "system.users.state", + "system.users.scope", + "system.users.remote_host" ] - }, - "number_of_routing_shards": "30" + } } }, "mappings": { diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts index a580f58d4fed1..d68d7715436a3 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts @@ -4,194 +4,206 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { elasticsearchServiceMock } from 'src/core/server/mocks'; +import { createAppContextStartContractMock } from '../../../../mocks'; +import { appContextService } from '../../../../services'; + import type { RegistryDataStream } from '../../../../types'; import type { Field } from '../../fields/field'; import { installTemplate } from './install'; -test('tests installPackage to use correct priority and index_patterns for data stream with dataset_is_prefix not set', async () => { - const callCluster = elasticsearchServiceMock.createLegacyScopedClusterClient().callAsCurrentUser; - callCluster.mockImplementation(async (_, params) => { - if ( - params && - params.method === 'GET' && - params.path === '/_index_template/metrics-package.dataset' - ) { - return { index_templates: [] }; - } +describe('EPM install', () => { + beforeEach(async () => { + appContextService.start(createAppContextStartContractMock()); }); - const fields: Field[] = []; - const dataStreamDatasetIsPrefixUnset = { - type: 'metrics', - dataset: 'package.dataset', - title: 'test data stream', - release: 'experimental', - package: 'package', - path: 'path', - ingest_pipeline: 'default', - } as RegistryDataStream; - const pkg = { - name: 'package', - version: '0.0.1', - }; - const templateIndexPatternDatasetIsPrefixUnset = 'metrics-package.dataset-*'; - const templatePriorityDatasetIsPrefixUnset = 200; - await installTemplate({ - callCluster, - fields, - dataStream: dataStreamDatasetIsPrefixUnset, - packageVersion: pkg.version, - packageName: pkg.name, - }); - // @ts-ignore - const sentTemplate = callCluster.mock.calls[1][1].body; - expect(sentTemplate).toBeDefined(); - expect(sentTemplate.priority).toBe(templatePriorityDatasetIsPrefixUnset); - expect(sentTemplate.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixUnset]); -}); + it('tests installPackage to use correct priority and index_patterns for data stream with dataset_is_prefix not set', async () => { + const callCluster = elasticsearchServiceMock.createLegacyScopedClusterClient() + .callAsCurrentUser; + callCluster.mockImplementation(async (_, params) => { + if ( + params && + params.method === 'GET' && + params.path === '/_index_template/metrics-package.dataset' + ) { + return { index_templates: [] }; + } + }); -test('tests installPackage to use correct priority and index_patterns for data stream with dataset_is_prefix set to false', async () => { - const callCluster = elasticsearchServiceMock.createLegacyScopedClusterClient().callAsCurrentUser; - callCluster.mockImplementation(async (_, params) => { - if ( - params && - params.method === 'GET' && - params.path === '/_index_template/metrics-package.dataset' - ) { - return { index_templates: [] }; - } + const fields: Field[] = []; + const dataStreamDatasetIsPrefixUnset = { + type: 'metrics', + dataset: 'package.dataset', + title: 'test data stream', + release: 'experimental', + package: 'package', + path: 'path', + ingest_pipeline: 'default', + } as RegistryDataStream; + const pkg = { + name: 'package', + version: '0.0.1', + }; + const templateIndexPatternDatasetIsPrefixUnset = 'metrics-package.dataset-*'; + const templatePriorityDatasetIsPrefixUnset = 200; + await installTemplate({ + callCluster, + fields, + dataStream: dataStreamDatasetIsPrefixUnset, + packageVersion: pkg.version, + packageName: pkg.name, + }); + // @ts-ignore + const sentTemplate = callCluster.mock.calls[1][1].body; + expect(sentTemplate).toBeDefined(); + expect(sentTemplate.priority).toBe(templatePriorityDatasetIsPrefixUnset); + expect(sentTemplate.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixUnset]); }); - const fields: Field[] = []; - const dataStreamDatasetIsPrefixFalse = { - type: 'metrics', - dataset: 'package.dataset', - title: 'test data stream', - release: 'experimental', - package: 'package', - path: 'path', - ingest_pipeline: 'default', - dataset_is_prefix: false, - } as RegistryDataStream; - const pkg = { - name: 'package', - version: '0.0.1', - }; - const templateIndexPatternDatasetIsPrefixFalse = 'metrics-package.dataset-*'; - const templatePriorityDatasetIsPrefixFalse = 200; - await installTemplate({ - callCluster, - fields, - dataStream: dataStreamDatasetIsPrefixFalse, - packageVersion: pkg.version, - packageName: pkg.name, - }); - // @ts-ignore - const sentTemplate = callCluster.mock.calls[1][1].body; - expect(sentTemplate).toBeDefined(); - expect(sentTemplate.priority).toBe(templatePriorityDatasetIsPrefixFalse); - expect(sentTemplate.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixFalse]); -}); + it('tests installPackage to use correct priority and index_patterns for data stream with dataset_is_prefix set to false', async () => { + const callCluster = elasticsearchServiceMock.createLegacyScopedClusterClient() + .callAsCurrentUser; + callCluster.mockImplementation(async (_, params) => { + if ( + params && + params.method === 'GET' && + params.path === '/_index_template/metrics-package.dataset' + ) { + return { index_templates: [] }; + } + }); -test('tests installPackage to use correct priority and index_patterns for data stream with dataset_is_prefix set to true', async () => { - const callCluster = elasticsearchServiceMock.createLegacyScopedClusterClient().callAsCurrentUser; - callCluster.mockImplementation(async (_, params) => { - if ( - params && - params.method === 'GET' && - params.path === '/_index_template/metrics-package.dataset' - ) { - return { index_templates: [] }; - } + const fields: Field[] = []; + const dataStreamDatasetIsPrefixFalse = { + type: 'metrics', + dataset: 'package.dataset', + title: 'test data stream', + release: 'experimental', + package: 'package', + path: 'path', + ingest_pipeline: 'default', + dataset_is_prefix: false, + } as RegistryDataStream; + const pkg = { + name: 'package', + version: '0.0.1', + }; + const templateIndexPatternDatasetIsPrefixFalse = 'metrics-package.dataset-*'; + const templatePriorityDatasetIsPrefixFalse = 200; + await installTemplate({ + callCluster, + fields, + dataStream: dataStreamDatasetIsPrefixFalse, + packageVersion: pkg.version, + packageName: pkg.name, + }); + // @ts-ignore + const sentTemplate = callCluster.mock.calls[1][1].body; + expect(sentTemplate).toBeDefined(); + expect(sentTemplate.priority).toBe(templatePriorityDatasetIsPrefixFalse); + expect(sentTemplate.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixFalse]); }); - const fields: Field[] = []; - const dataStreamDatasetIsPrefixTrue = { - type: 'metrics', - dataset: 'package.dataset', - title: 'test data stream', - release: 'experimental', - package: 'package', - path: 'path', - ingest_pipeline: 'default', - dataset_is_prefix: true, - } as RegistryDataStream; - const pkg = { - name: 'package', - version: '0.0.1', - }; - const templateIndexPatternDatasetIsPrefixTrue = 'metrics-package.dataset.*-*'; - const templatePriorityDatasetIsPrefixTrue = 150; - await installTemplate({ - callCluster, - fields, - dataStream: dataStreamDatasetIsPrefixTrue, - packageVersion: pkg.version, - packageName: pkg.name, + it('tests installPackage to use correct priority and index_patterns for data stream with dataset_is_prefix set to true', async () => { + const callCluster = elasticsearchServiceMock.createLegacyScopedClusterClient() + .callAsCurrentUser; + callCluster.mockImplementation(async (_, params) => { + if ( + params && + params.method === 'GET' && + params.path === '/_index_template/metrics-package.dataset' + ) { + return { index_templates: [] }; + } + }); + + const fields: Field[] = []; + const dataStreamDatasetIsPrefixTrue = { + type: 'metrics', + dataset: 'package.dataset', + title: 'test data stream', + release: 'experimental', + package: 'package', + path: 'path', + ingest_pipeline: 'default', + dataset_is_prefix: true, + } as RegistryDataStream; + const pkg = { + name: 'package', + version: '0.0.1', + }; + const templateIndexPatternDatasetIsPrefixTrue = 'metrics-package.dataset.*-*'; + const templatePriorityDatasetIsPrefixTrue = 150; + await installTemplate({ + callCluster, + fields, + dataStream: dataStreamDatasetIsPrefixTrue, + packageVersion: pkg.version, + packageName: pkg.name, + }); + // @ts-ignore + const sentTemplate = callCluster.mock.calls[1][1].body; + expect(sentTemplate).toBeDefined(); + expect(sentTemplate.priority).toBe(templatePriorityDatasetIsPrefixTrue); + expect(sentTemplate.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixTrue]); }); - // @ts-ignore - const sentTemplate = callCluster.mock.calls[1][1].body; - expect(sentTemplate).toBeDefined(); - expect(sentTemplate.priority).toBe(templatePriorityDatasetIsPrefixTrue); - expect(sentTemplate.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixTrue]); -}); -test('tests installPackage remove the aliases property if the property existed', async () => { - const callCluster = elasticsearchServiceMock.createLegacyScopedClusterClient().callAsCurrentUser; - callCluster.mockImplementation(async (_, params) => { - if ( - params && - params.method === 'GET' && - params.path === '/_index_template/metrics-package.dataset' - ) { - return { - index_templates: [ - { - name: 'metrics-package.dataset', - index_template: { - index_patterns: ['metrics-package.dataset-*'], - template: { aliases: {} }, + it('tests installPackage remove the aliases property if the property existed', async () => { + const callCluster = elasticsearchServiceMock.createLegacyScopedClusterClient() + .callAsCurrentUser; + callCluster.mockImplementation(async (_, params) => { + if ( + params && + params.method === 'GET' && + params.path === '/_index_template/metrics-package.dataset' + ) { + return { + index_templates: [ + { + name: 'metrics-package.dataset', + index_template: { + index_patterns: ['metrics-package.dataset-*'], + template: { aliases: {} }, + }, }, - }, - ], - }; - } - }); + ], + }; + } + }); - const fields: Field[] = []; - const dataStreamDatasetIsPrefixUnset = { - type: 'metrics', - dataset: 'package.dataset', - title: 'test data stream', - release: 'experimental', - package: 'package', - path: 'path', - ingest_pipeline: 'default', - } as RegistryDataStream; - const pkg = { - name: 'package', - version: '0.0.1', - }; - const templateIndexPatternDatasetIsPrefixUnset = 'metrics-package.dataset-*'; - const templatePriorityDatasetIsPrefixUnset = 200; - await installTemplate({ - callCluster, - fields, - dataStream: dataStreamDatasetIsPrefixUnset, - packageVersion: pkg.version, - packageName: pkg.name, - }); + const fields: Field[] = []; + const dataStreamDatasetIsPrefixUnset = { + type: 'metrics', + dataset: 'package.dataset', + title: 'test data stream', + release: 'experimental', + package: 'package', + path: 'path', + ingest_pipeline: 'default', + } as RegistryDataStream; + const pkg = { + name: 'package', + version: '0.0.1', + }; + const templateIndexPatternDatasetIsPrefixUnset = 'metrics-package.dataset-*'; + const templatePriorityDatasetIsPrefixUnset = 200; + await installTemplate({ + callCluster, + fields, + dataStream: dataStreamDatasetIsPrefixUnset, + packageVersion: pkg.version, + packageName: pkg.name, + }); - // @ts-ignore - const removeAliases = callCluster.mock.calls[1][1].body; - expect(removeAliases.template.aliases).not.toBeDefined(); - // @ts-ignore - const sentTemplate = callCluster.mock.calls[2][1].body; - expect(sentTemplate).toBeDefined(); - expect(sentTemplate.priority).toBe(templatePriorityDatasetIsPrefixUnset); - expect(sentTemplate.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixUnset]); + // @ts-ignore + const removeAliases = callCluster.mock.calls[1][1].body; + expect(removeAliases.template.aliases).not.toBeDefined(); + // @ts-ignore + const sentTemplate = callCluster.mock.calls[2][1].body; + expect(sentTemplate).toBeDefined(); + expect(sentTemplate.priority).toBe(templatePriorityDatasetIsPrefixUnset); + expect(sentTemplate.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixUnset]); + }); }); diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts index 28cfda3fd4189..2769b97fe48a1 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts @@ -300,7 +300,8 @@ export async function installTemplate({ packageVersion: string; packageName: string; }): Promise { - const mappings = generateMappings(processFields(fields)); + const validFields = processFields(fields); + const mappings = generateMappings(validFields); const templateName = generateTemplateName(dataStream); const templateIndexPattern = generateTemplateIndexPattern(dataStream); const templatePriority = getTemplatePriority(dataStream); @@ -362,6 +363,7 @@ export async function installTemplate({ const template = getTemplate({ type: dataStream.type, templateIndexPattern, + fields: validFields, mappings, pipelineName, packageName, diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts index 0f53d260592e7..df82aa90b5a13 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts @@ -10,6 +10,9 @@ import path from 'path'; import { safeLoad } from 'js-yaml'; +import { createAppContextStartContractMock } from '../../../../mocks'; +import { appContextService } from '../../../../services'; + import type { RegistryDataStream } from '../../../../types'; import { processFields } from '../../fields/field'; import type { Field } from '../../fields/field'; @@ -32,132 +35,145 @@ expect.addSnapshotSerializer({ }, }); -test('get template', () => { - const templateIndexPattern = 'logs-nginx.access-abcd-*'; - - const template = getTemplate({ - type: 'logs', - templateIndexPattern, - packageName: 'nginx', - mappings: { properties: {} }, - composedOfTemplates: [], - templatePriority: 200, +describe('EPM template', () => { + beforeEach(async () => { + appContextService.start(createAppContextStartContractMock()); }); - expect(template.index_patterns).toStrictEqual([templateIndexPattern]); -}); - -test('adds composed_of correctly', () => { - const composedOfTemplates = ['component1', 'component2']; - const template = getTemplate({ - type: 'logs', - templateIndexPattern: 'name-*', - packageName: 'nginx', - mappings: { properties: {} }, - composedOfTemplates, - templatePriority: 200, + it('get template', () => { + const templateIndexPattern = 'logs-nginx.access-abcd-*'; + + const template = getTemplate({ + type: 'logs', + templateIndexPattern, + packageName: 'nginx', + fields: [], + mappings: { properties: {} }, + composedOfTemplates: [], + templatePriority: 200, + }); + expect(template.index_patterns).toStrictEqual([templateIndexPattern]); }); - expect(template.composed_of).toStrictEqual(composedOfTemplates); -}); - -test('adds empty composed_of correctly', () => { - const composedOfTemplates: string[] = []; - const template = getTemplate({ - type: 'logs', - templateIndexPattern: 'name-*', - packageName: 'nginx', - mappings: { properties: {} }, - composedOfTemplates, - templatePriority: 200, + it('adds composed_of correctly', () => { + const composedOfTemplates = ['component1', 'component2']; + + const template = getTemplate({ + type: 'logs', + templateIndexPattern: 'name-*', + packageName: 'nginx', + fields: [], + mappings: { properties: {} }, + composedOfTemplates, + templatePriority: 200, + }); + expect(template.composed_of).toStrictEqual(composedOfTemplates); }); - expect(template.composed_of).toStrictEqual(composedOfTemplates); -}); -test('adds hidden field correctly', () => { - const templateIndexPattern = 'logs-nginx.access-abcd-*'; - - const templateWithHidden = getTemplate({ - type: 'logs', - templateIndexPattern, - packageName: 'nginx', - mappings: { properties: {} }, - composedOfTemplates: [], - templatePriority: 200, - hidden: true, - }); - expect(templateWithHidden.data_stream.hidden).toEqual(true); - - const templateWithoutHidden = getTemplate({ - type: 'logs', - templateIndexPattern, - packageName: 'nginx', - mappings: { properties: {} }, - composedOfTemplates: [], - templatePriority: 200, + it('adds empty composed_of correctly', () => { + const composedOfTemplates: string[] = []; + + const template = getTemplate({ + type: 'logs', + templateIndexPattern: 'name-*', + packageName: 'nginx', + fields: [], + mappings: { properties: {} }, + composedOfTemplates, + templatePriority: 200, + }); + expect(template.composed_of).toStrictEqual(composedOfTemplates); }); - expect(templateWithoutHidden.data_stream.hidden).toEqual(undefined); -}); -test('tests loading base.yml', () => { - const ymlPath = path.join(__dirname, '../../fields/tests/base.yml'); - const fieldsYML = readFileSync(ymlPath, 'utf-8'); - const fields: Field[] = safeLoad(fieldsYML); - - const processedFields = processFields(fields); - const mappings = generateMappings(processedFields); - const template = getTemplate({ - type: 'logs', - templateIndexPattern: 'foo-*', - packageName: 'nginx', - mappings, - composedOfTemplates: [], - templatePriority: 200, + it('adds hidden field correctly', () => { + const templateIndexPattern = 'logs-nginx.access-abcd-*'; + + const templateWithHidden = getTemplate({ + type: 'logs', + templateIndexPattern, + packageName: 'nginx', + fields: [], + mappings: { properties: {} }, + composedOfTemplates: [], + templatePriority: 200, + hidden: true, + }); + expect(templateWithHidden.data_stream.hidden).toEqual(true); + + const templateWithoutHidden = getTemplate({ + type: 'logs', + templateIndexPattern, + packageName: 'nginx', + fields: [], + mappings: { properties: {} }, + composedOfTemplates: [], + templatePriority: 200, + }); + expect(templateWithoutHidden.data_stream.hidden).toEqual(undefined); }); - expect(template).toMatchSnapshot(path.basename(ymlPath)); -}); - -test('tests loading coredns.logs.yml', () => { - const ymlPath = path.join(__dirname, '../../fields/tests/coredns.logs.yml'); - const fieldsYML = readFileSync(ymlPath, 'utf-8'); - const fields: Field[] = safeLoad(fieldsYML); - - const processedFields = processFields(fields); - const mappings = generateMappings(processedFields); - const template = getTemplate({ - type: 'logs', - templateIndexPattern: 'foo-*', - packageName: 'coredns', - mappings, - composedOfTemplates: [], - templatePriority: 200, + it('tests loading base.yml', () => { + const ymlPath = path.join(__dirname, '../../fields/tests/base.yml'); + const fieldsYML = readFileSync(ymlPath, 'utf-8'); + const fields: Field[] = safeLoad(fieldsYML); + + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + const template = getTemplate({ + type: 'logs', + templateIndexPattern: 'foo-*', + packageName: 'nginx', + fields: processedFields, + mappings, + composedOfTemplates: [], + templatePriority: 200, + }); + + expect(template).toMatchSnapshot(path.basename(ymlPath)); }); - expect(template).toMatchSnapshot(path.basename(ymlPath)); -}); - -test('tests loading system.yml', () => { - const ymlPath = path.join(__dirname, '../../fields/tests/system.yml'); - const fieldsYML = readFileSync(ymlPath, 'utf-8'); - const fields: Field[] = safeLoad(fieldsYML); - - const processedFields = processFields(fields); - const mappings = generateMappings(processedFields); - const template = getTemplate({ - type: 'metrics', - templateIndexPattern: 'whatsthis-*', - packageName: 'system', - mappings, - composedOfTemplates: [], - templatePriority: 200, + it('tests loading coredns.logs.yml', () => { + const ymlPath = path.join(__dirname, '../../fields/tests/coredns.logs.yml'); + const fieldsYML = readFileSync(ymlPath, 'utf-8'); + const fields: Field[] = safeLoad(fieldsYML); + + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + const template = getTemplate({ + type: 'logs', + templateIndexPattern: 'foo-*', + packageName: 'coredns', + fields: processedFields, + mappings, + composedOfTemplates: [], + templatePriority: 200, + }); + + expect(template).toMatchSnapshot(path.basename(ymlPath)); }); - expect(template).toMatchSnapshot(path.basename(ymlPath)); -}); + it('tests loading system.yml', () => { + const ymlPath = path.join(__dirname, '../../fields/tests/system.yml'); + const fieldsYML = readFileSync(ymlPath, 'utf-8'); + const fields: Field[] = safeLoad(fieldsYML); + + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + const template = getTemplate({ + type: 'metrics', + templateIndexPattern: 'whatsthis-*', + packageName: 'system', + fields: processedFields, + mappings, + composedOfTemplates: [], + templatePriority: 200, + }); + + expect(template).toMatchSnapshot(path.basename(ymlPath)); + }); -test('tests processing text field with multi fields', () => { - const textWithMultiFieldsLiteralYml = ` + it('tests processing text field with multi fields', () => { + const textWithMultiFieldsLiteralYml = ` - name: textWithMultiFields type: text multi_fields: @@ -166,30 +182,30 @@ test('tests processing text field with multi fields', () => { - name: indexed type: text `; - const textWithMultiFieldsMapping = { - properties: { - textWithMultiFields: { - type: 'text', - fields: { - raw: { - ignore_above: 1024, - type: 'keyword', - }, - indexed: { - type: 'text', + const textWithMultiFieldsMapping = { + properties: { + textWithMultiFields: { + type: 'text', + fields: { + raw: { + ignore_above: 1024, + type: 'keyword', + }, + indexed: { + type: 'text', + }, }, }, }, - }, - }; - const fields: Field[] = safeLoad(textWithMultiFieldsLiteralYml); - const processedFields = processFields(fields); - const mappings = generateMappings(processedFields); - expect(mappings).toEqual(textWithMultiFieldsMapping); -}); + }; + const fields: Field[] = safeLoad(textWithMultiFieldsLiteralYml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(mappings).toEqual(textWithMultiFieldsMapping); + }); -test('tests processing keyword field with multi fields', () => { - const keywordWithMultiFieldsLiteralYml = ` + it('tests processing keyword field with multi fields', () => { + const keywordWithMultiFieldsLiteralYml = ` - name: keywordWithMultiFields type: keyword multi_fields: @@ -199,31 +215,31 @@ test('tests processing keyword field with multi fields', () => { type: text `; - const keywordWithMultiFieldsMapping = { - properties: { - keywordWithMultiFields: { - ignore_above: 1024, - type: 'keyword', - fields: { - raw: { - ignore_above: 1024, - type: 'keyword', - }, - indexed: { - type: 'text', + const keywordWithMultiFieldsMapping = { + properties: { + keywordWithMultiFields: { + ignore_above: 1024, + type: 'keyword', + fields: { + raw: { + ignore_above: 1024, + type: 'keyword', + }, + indexed: { + type: 'text', + }, }, }, }, - }, - }; - const fields: Field[] = safeLoad(keywordWithMultiFieldsLiteralYml); - const processedFields = processFields(fields); - const mappings = generateMappings(processedFields); - expect(mappings).toEqual(keywordWithMultiFieldsMapping); -}); + }; + const fields: Field[] = safeLoad(keywordWithMultiFieldsLiteralYml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(mappings).toEqual(keywordWithMultiFieldsMapping); + }); -test('tests processing keyword field with multi fields with analyzed text field', () => { - const keywordWithAnalyzedMultiFieldsLiteralYml = ` + it('tests processing keyword field with multi fields with analyzed text field', () => { + const keywordWithAnalyzedMultiFieldsLiteralYml = ` - name: keywordWithAnalyzedMultiField type: keyword multi_fields: @@ -233,29 +249,29 @@ test('tests processing keyword field with multi fields with analyzed text field' search_analyzer: standard `; - const keywordWithAnalyzedMultiFieldsMapping = { - properties: { - keywordWithAnalyzedMultiField: { - ignore_above: 1024, - type: 'keyword', - fields: { - analyzed: { - analyzer: 'autocomplete', - search_analyzer: 'standard', - type: 'text', + const keywordWithAnalyzedMultiFieldsMapping = { + properties: { + keywordWithAnalyzedMultiField: { + ignore_above: 1024, + type: 'keyword', + fields: { + analyzed: { + analyzer: 'autocomplete', + search_analyzer: 'standard', + type: 'text', + }, }, }, }, - }, - }; - const fields: Field[] = safeLoad(keywordWithAnalyzedMultiFieldsLiteralYml); - const processedFields = processFields(fields); - const mappings = generateMappings(processedFields); - expect(mappings).toEqual(keywordWithAnalyzedMultiFieldsMapping); -}); + }; + const fields: Field[] = safeLoad(keywordWithAnalyzedMultiFieldsLiteralYml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(mappings).toEqual(keywordWithAnalyzedMultiFieldsMapping); + }); -test('tests processing keyword field with multi fields with normalized keyword field', () => { - const keywordWithNormalizedMultiFieldsLiteralYml = ` + it('tests processing keyword field with multi fields with normalized keyword field', () => { + const keywordWithNormalizedMultiFieldsLiteralYml = ` - name: keywordWithNormalizedMultiField type: keyword multi_fields: @@ -264,235 +280,235 @@ test('tests processing keyword field with multi fields with normalized keyword f normalizer: lowercase `; - const keywordWithNormalizedMultiFieldsMapping = { - properties: { - keywordWithNormalizedMultiField: { - ignore_above: 1024, - type: 'keyword', - fields: { - normalized: { - type: 'keyword', - ignore_above: 1024, - normalizer: 'lowercase', + const keywordWithNormalizedMultiFieldsMapping = { + properties: { + keywordWithNormalizedMultiField: { + ignore_above: 1024, + type: 'keyword', + fields: { + normalized: { + type: 'keyword', + ignore_above: 1024, + normalizer: 'lowercase', + }, }, }, }, - }, - }; - const fields: Field[] = safeLoad(keywordWithNormalizedMultiFieldsLiteralYml); - const processedFields = processFields(fields); - const mappings = generateMappings(processedFields); - expect(mappings).toEqual(keywordWithNormalizedMultiFieldsMapping); -}); + }; + const fields: Field[] = safeLoad(keywordWithNormalizedMultiFieldsLiteralYml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(mappings).toEqual(keywordWithNormalizedMultiFieldsMapping); + }); -test('tests processing object field with no other attributes', () => { - const objectFieldLiteralYml = ` + it('tests processing object field with no other attributes', () => { + const objectFieldLiteralYml = ` - name: objectField type: object `; - const objectFieldMapping = { - properties: { - objectField: { - type: 'object', + const objectFieldMapping = { + properties: { + objectField: { + type: 'object', + }, }, - }, - }; - const fields: Field[] = safeLoad(objectFieldLiteralYml); - const processedFields = processFields(fields); - const mappings = generateMappings(processedFields); - expect(mappings).toEqual(objectFieldMapping); -}); + }; + const fields: Field[] = safeLoad(objectFieldLiteralYml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(mappings).toEqual(objectFieldMapping); + }); -test('tests processing object field with enabled set to false', () => { - const objectFieldEnabledFalseLiteralYml = ` + it('tests processing object field with enabled set to false', () => { + const objectFieldEnabledFalseLiteralYml = ` - name: objectField type: object enabled: false `; - const objectFieldEnabledFalseMapping = { - properties: { - objectField: { - type: 'object', - enabled: false, + const objectFieldEnabledFalseMapping = { + properties: { + objectField: { + type: 'object', + enabled: false, + }, }, - }, - }; - const fields: Field[] = safeLoad(objectFieldEnabledFalseLiteralYml); - const processedFields = processFields(fields); - const mappings = generateMappings(processedFields); - expect(mappings).toEqual(objectFieldEnabledFalseMapping); -}); + }; + const fields: Field[] = safeLoad(objectFieldEnabledFalseLiteralYml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(mappings).toEqual(objectFieldEnabledFalseMapping); + }); -test('tests processing object field with dynamic set to false', () => { - const objectFieldDynamicFalseLiteralYml = ` + it('tests processing object field with dynamic set to false', () => { + const objectFieldDynamicFalseLiteralYml = ` - name: objectField type: object dynamic: false `; - const objectFieldDynamicFalseMapping = { - properties: { - objectField: { - type: 'object', - dynamic: false, + const objectFieldDynamicFalseMapping = { + properties: { + objectField: { + type: 'object', + dynamic: false, + }, }, - }, - }; - const fields: Field[] = safeLoad(objectFieldDynamicFalseLiteralYml); - const processedFields = processFields(fields); - const mappings = generateMappings(processedFields); - expect(mappings).toEqual(objectFieldDynamicFalseMapping); -}); + }; + const fields: Field[] = safeLoad(objectFieldDynamicFalseLiteralYml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(mappings).toEqual(objectFieldDynamicFalseMapping); + }); -test('tests processing object field with dynamic set to true', () => { - const objectFieldDynamicTrueLiteralYml = ` + it('tests processing object field with dynamic set to true', () => { + const objectFieldDynamicTrueLiteralYml = ` - name: objectField type: object dynamic: true `; - const objectFieldDynamicTrueMapping = { - properties: { - objectField: { - type: 'object', - dynamic: true, + const objectFieldDynamicTrueMapping = { + properties: { + objectField: { + type: 'object', + dynamic: true, + }, }, - }, - }; - const fields: Field[] = safeLoad(objectFieldDynamicTrueLiteralYml); - const processedFields = processFields(fields); - const mappings = generateMappings(processedFields); - expect(mappings).toEqual(objectFieldDynamicTrueMapping); -}); + }; + const fields: Field[] = safeLoad(objectFieldDynamicTrueLiteralYml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(mappings).toEqual(objectFieldDynamicTrueMapping); + }); -test('tests processing object field with dynamic set to strict', () => { - const objectFieldDynamicStrictLiteralYml = ` + it('tests processing object field with dynamic set to strict', () => { + const objectFieldDynamicStrictLiteralYml = ` - name: objectField type: object dynamic: strict `; - const objectFieldDynamicStrictMapping = { - properties: { - objectField: { - type: 'object', - dynamic: 'strict', + const objectFieldDynamicStrictMapping = { + properties: { + objectField: { + type: 'object', + dynamic: 'strict', + }, }, - }, - }; - const fields: Field[] = safeLoad(objectFieldDynamicStrictLiteralYml); - const processedFields = processFields(fields); - const mappings = generateMappings(processedFields); - expect(mappings).toEqual(objectFieldDynamicStrictMapping); -}); + }; + const fields: Field[] = safeLoad(objectFieldDynamicStrictLiteralYml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(mappings).toEqual(objectFieldDynamicStrictMapping); + }); -test('tests processing object field with property', () => { - const objectFieldWithPropertyLiteralYml = ` + it('tests processing object field with property', () => { + const objectFieldWithPropertyLiteralYml = ` - name: a type: object - name: a.b type: keyword `; - const objectFieldWithPropertyMapping = { - properties: { - a: { - properties: { - b: { - ignore_above: 1024, - type: 'keyword', + const objectFieldWithPropertyMapping = { + properties: { + a: { + properties: { + b: { + ignore_above: 1024, + type: 'keyword', + }, }, }, }, - }, - }; - const fields: Field[] = safeLoad(objectFieldWithPropertyLiteralYml); - const processedFields = processFields(fields); - const mappings = generateMappings(processedFields); - expect(mappings).toEqual(objectFieldWithPropertyMapping); -}); + }; + const fields: Field[] = safeLoad(objectFieldWithPropertyLiteralYml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(mappings).toEqual(objectFieldWithPropertyMapping); + }); -test('tests processing object field with property, reverse order', () => { - const objectFieldWithPropertyReversedLiteralYml = ` + it('tests processing object field with property, reverse order', () => { + const objectFieldWithPropertyReversedLiteralYml = ` - name: a.b type: keyword - name: a type: object dynamic: false `; - const objectFieldWithPropertyReversedMapping = { - properties: { - a: { - dynamic: false, - properties: { - b: { - ignore_above: 1024, - type: 'keyword', + const objectFieldWithPropertyReversedMapping = { + properties: { + a: { + dynamic: false, + properties: { + b: { + ignore_above: 1024, + type: 'keyword', + }, }, }, }, - }, - }; - const fields: Field[] = safeLoad(objectFieldWithPropertyReversedLiteralYml); - const processedFields = processFields(fields); - const mappings = generateMappings(processedFields); - expect(mappings).toEqual(objectFieldWithPropertyReversedMapping); -}); + }; + const fields: Field[] = safeLoad(objectFieldWithPropertyReversedLiteralYml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(mappings).toEqual(objectFieldWithPropertyReversedMapping); + }); -test('tests processing nested field with property', () => { - const nestedYaml = ` + it('tests processing nested field with property', () => { + const nestedYaml = ` - name: a.b type: keyword - name: a type: nested dynamic: false `; - const expectedMapping = { - properties: { - a: { - dynamic: false, - type: 'nested', - properties: { - b: { - ignore_above: 1024, - type: 'keyword', + const expectedMapping = { + properties: { + a: { + dynamic: false, + type: 'nested', + properties: { + b: { + ignore_above: 1024, + type: 'keyword', + }, }, }, }, - }, - }; - const fields: Field[] = safeLoad(nestedYaml); - const processedFields = processFields(fields); - const mappings = generateMappings(processedFields); - expect(mappings).toEqual(expectedMapping); -}); + }; + const fields: Field[] = safeLoad(nestedYaml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(mappings).toEqual(expectedMapping); + }); -test('tests processing nested field with property, nested field first', () => { - const nestedYaml = ` + it('tests processing nested field with property, nested field first', () => { + const nestedYaml = ` - name: a type: nested include_in_parent: true - name: a.b type: keyword `; - const expectedMapping = { - properties: { - a: { - include_in_parent: true, - type: 'nested', - properties: { - b: { - ignore_above: 1024, - type: 'keyword', + const expectedMapping = { + properties: { + a: { + include_in_parent: true, + type: 'nested', + properties: { + b: { + ignore_above: 1024, + type: 'keyword', + }, }, }, }, - }, - }; - const fields: Field[] = safeLoad(nestedYaml); - const processedFields = processFields(fields); - const mappings = generateMappings(processedFields); - expect(mappings).toEqual(expectedMapping); -}); + }; + const fields: Field[] = safeLoad(nestedYaml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(mappings).toEqual(expectedMapping); + }); -test('tests processing nested leaf field with properties', () => { - const nestedYaml = ` + it('tests processing nested leaf field with properties', () => { + const nestedYaml = ` - name: a type: object dynamic: false @@ -500,98 +516,99 @@ test('tests processing nested leaf field with properties', () => { type: nested enabled: false `; - const expectedMapping = { - properties: { - a: { - dynamic: false, - properties: { - b: { - enabled: false, - type: 'nested', + const expectedMapping = { + properties: { + a: { + dynamic: false, + properties: { + b: { + enabled: false, + type: 'nested', + }, }, }, }, - }, - }; - const fields: Field[] = safeLoad(nestedYaml); - const processedFields = processFields(fields); - const mappings = generateMappings(processedFields); - expect(mappings).toEqual(expectedMapping); -}); + }; + const fields: Field[] = safeLoad(nestedYaml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(mappings).toEqual(expectedMapping); + }); -test('tests constant_keyword field type handling', () => { - const constantKeywordLiteralYaml = ` + it('tests constant_keyword field type handling', () => { + const constantKeywordLiteralYaml = ` - name: constantKeyword type: constant_keyword `; - const constantKeywordMapping = { - properties: { - constantKeyword: { - type: 'constant_keyword', + const constantKeywordMapping = { + properties: { + constantKeyword: { + type: 'constant_keyword', + }, }, - }, - }; - const fields: Field[] = safeLoad(constantKeywordLiteralYaml); - const processedFields = processFields(fields); - const mappings = generateMappings(processedFields); - expect(JSON.stringify(mappings)).toEqual(JSON.stringify(constantKeywordMapping)); -}); + }; + const fields: Field[] = safeLoad(constantKeywordLiteralYaml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(JSON.stringify(mappings)).toEqual(JSON.stringify(constantKeywordMapping)); + }); -test('tests priority and index pattern for data stream without dataset_is_prefix', () => { - const dataStreamDatasetIsPrefixUnset = { - type: 'metrics', - dataset: 'package.dataset', - title: 'test data stream', - release: 'experimental', - package: 'package', - path: 'path', - ingest_pipeline: 'default', - } as RegistryDataStream; - const templateIndexPatternDatasetIsPrefixUnset = 'metrics-package.dataset-*'; - const templatePriorityDatasetIsPrefixUnset = 200; - const templateIndexPattern = generateTemplateIndexPattern(dataStreamDatasetIsPrefixUnset); - const templatePriority = getTemplatePriority(dataStreamDatasetIsPrefixUnset); - - expect(templateIndexPattern).toEqual(templateIndexPatternDatasetIsPrefixUnset); - expect(templatePriority).toEqual(templatePriorityDatasetIsPrefixUnset); -}); + it('tests priority and index pattern for data stream without dataset_is_prefix', () => { + const dataStreamDatasetIsPrefixUnset = { + type: 'metrics', + dataset: 'package.dataset', + title: 'test data stream', + release: 'experimental', + package: 'package', + path: 'path', + ingest_pipeline: 'default', + } as RegistryDataStream; + const templateIndexPatternDatasetIsPrefixUnset = 'metrics-package.dataset-*'; + const templatePriorityDatasetIsPrefixUnset = 200; + const templateIndexPattern = generateTemplateIndexPattern(dataStreamDatasetIsPrefixUnset); + const templatePriority = getTemplatePriority(dataStreamDatasetIsPrefixUnset); + + expect(templateIndexPattern).toEqual(templateIndexPatternDatasetIsPrefixUnset); + expect(templatePriority).toEqual(templatePriorityDatasetIsPrefixUnset); + }); -test('tests priority and index pattern for data stream with dataset_is_prefix set to false', () => { - const dataStreamDatasetIsPrefixFalse = { - type: 'metrics', - dataset: 'package.dataset', - title: 'test data stream', - release: 'experimental', - package: 'package', - path: 'path', - ingest_pipeline: 'default', - dataset_is_prefix: false, - } as RegistryDataStream; - const templateIndexPatternDatasetIsPrefixFalse = 'metrics-package.dataset-*'; - const templatePriorityDatasetIsPrefixFalse = 200; - const templateIndexPattern = generateTemplateIndexPattern(dataStreamDatasetIsPrefixFalse); - const templatePriority = getTemplatePriority(dataStreamDatasetIsPrefixFalse); - - expect(templateIndexPattern).toEqual(templateIndexPatternDatasetIsPrefixFalse); - expect(templatePriority).toEqual(templatePriorityDatasetIsPrefixFalse); -}); + it('tests priority and index pattern for data stream with dataset_is_prefix set to false', () => { + const dataStreamDatasetIsPrefixFalse = { + type: 'metrics', + dataset: 'package.dataset', + title: 'test data stream', + release: 'experimental', + package: 'package', + path: 'path', + ingest_pipeline: 'default', + dataset_is_prefix: false, + } as RegistryDataStream; + const templateIndexPatternDatasetIsPrefixFalse = 'metrics-package.dataset-*'; + const templatePriorityDatasetIsPrefixFalse = 200; + const templateIndexPattern = generateTemplateIndexPattern(dataStreamDatasetIsPrefixFalse); + const templatePriority = getTemplatePriority(dataStreamDatasetIsPrefixFalse); + + expect(templateIndexPattern).toEqual(templateIndexPatternDatasetIsPrefixFalse); + expect(templatePriority).toEqual(templatePriorityDatasetIsPrefixFalse); + }); -test('tests priority and index pattern for data stream with dataset_is_prefix set to true', () => { - const dataStreamDatasetIsPrefixTrue = { - type: 'metrics', - dataset: 'package.dataset', - title: 'test data stream', - release: 'experimental', - package: 'package', - path: 'path', - ingest_pipeline: 'default', - dataset_is_prefix: true, - } as RegistryDataStream; - const templateIndexPatternDatasetIsPrefixTrue = 'metrics-package.dataset.*-*'; - const templatePriorityDatasetIsPrefixTrue = 150; - const templateIndexPattern = generateTemplateIndexPattern(dataStreamDatasetIsPrefixTrue); - const templatePriority = getTemplatePriority(dataStreamDatasetIsPrefixTrue); - - expect(templateIndexPattern).toEqual(templateIndexPatternDatasetIsPrefixTrue); - expect(templatePriority).toEqual(templatePriorityDatasetIsPrefixTrue); + it('tests priority and index pattern for data stream with dataset_is_prefix set to true', () => { + const dataStreamDatasetIsPrefixTrue = { + type: 'metrics', + dataset: 'package.dataset', + title: 'test data stream', + release: 'experimental', + package: 'package', + path: 'path', + ingest_pipeline: 'default', + dataset_is_prefix: true, + } as RegistryDataStream; + const templateIndexPatternDatasetIsPrefixTrue = 'metrics-package.dataset.*-*'; + const templatePriorityDatasetIsPrefixTrue = 150; + const templateIndexPattern = generateTemplateIndexPattern(dataStreamDatasetIsPrefixTrue); + const templatePriority = getTemplatePriority(dataStreamDatasetIsPrefixTrue); + + expect(templateIndexPattern).toEqual(templateIndexPatternDatasetIsPrefixTrue); + expect(templatePriority).toEqual(templatePriorityDatasetIsPrefixTrue); + }); }); diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts index a13ad007663d8..01b9a92045b29 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts @@ -13,6 +13,7 @@ import type { IndexTemplate, IndexTemplateMappings, } from '../../../../types'; +import { appContextService } from '../../../'; import { getRegistryDataStreamAssetBaseName } from '../index'; interface Properties { @@ -37,6 +38,9 @@ const DEFAULT_IGNORE_ABOVE = 1024; const DEFAULT_TEMPLATE_PRIORITY = 200; const DATASET_IS_PREFIX_TEMPLATE_PRIORITY = 150; +const QUERY_DEFAULT_FIELD_TYPES = ['keyword', 'text']; +const QUERY_DEFAULT_FIELD_LIMIT = 1024; + /** * getTemplate retrieves the default template but overwrites the index pattern with the given value. * @@ -45,6 +49,7 @@ const DATASET_IS_PREFIX_TEMPLATE_PRIORITY = 150; export function getTemplate({ type, templateIndexPattern, + fields, mappings, pipelineName, packageName, @@ -55,6 +60,7 @@ export function getTemplate({ }: { type: string; templateIndexPattern: string; + fields: Fields; mappings: IndexTemplateMappings; pipelineName?: string | undefined; packageName: string; @@ -66,6 +72,7 @@ export function getTemplate({ const template = getBaseTemplate( type, templateIndexPattern, + fields, mappings, packageName, composedOfTemplates, @@ -296,9 +303,28 @@ export function generateESIndexPatterns( return patterns; } +const flattenFieldsToNameAndType = ( + fields: Fields, + path: string = '' +): Array> => { + let newFields: Array> = []; + fields.forEach((field) => { + const fieldName = path ? `${path}.${field.name}` : field.name; + newFields.push({ + name: fieldName, + type: field.type, + }); + if (field.fields && field.fields.length) { + newFields = newFields.concat(flattenFieldsToNameAndType(field.fields, fieldName)); + } + }); + return newFields; +}; + function getBaseTemplate( type: string, templateIndexPattern: string, + fields: Fields, mappings: IndexTemplateMappings, packageName: string, composedOfTemplates: string[], @@ -306,6 +332,8 @@ function getBaseTemplate( ilmPolicy?: string | undefined, hidden?: boolean ): IndexTemplate { + const logger = appContextService.getLogger(); + // Meta information to identify Ingest Manager's managed templates and indices const _meta = { package: { @@ -315,6 +343,21 @@ function getBaseTemplate( managed: true, }; + // Find all field names to set `index.query.default_field` to, which will be + // the first 1024 keyword or text fields + const defaultFields = flattenFieldsToNameAndType(fields).filter( + (field) => field.type && QUERY_DEFAULT_FIELD_TYPES.includes(field.type) + ); + if (defaultFields.length > QUERY_DEFAULT_FIELD_LIMIT) { + logger.warn( + `large amount of default fields detected for index template ${templateIndexPattern} in package ${packageName}, applying the first ${QUERY_DEFAULT_FIELD_LIMIT} fields` + ); + } + const defaultFieldNames = (defaultFields.length > QUERY_DEFAULT_FIELD_LIMIT + ? defaultFields.slice(0, QUERY_DEFAULT_FIELD_LIMIT) + : defaultFields + ).map((field) => field.name); + return { priority: templatePriority, // To be completed with the correct index patterns @@ -338,13 +381,18 @@ function getBaseTemplate( refresh_interval: '5s', // Default in the stack now, still good to have it in number_of_shards: '1', - // All the default fields which should be queried have to be added here. - // So far we add all keyword and text fields here. - query: { - default_field: ['message'], - }, // We are setting 30 because it can be devided by several numbers. Useful when shrinking. number_of_routing_shards: '30', + // All the default fields which should be queried have to be added here. + // So far we add all keyword and text fields here if there are any, otherwise + // this setting is skipped. + ...(defaultFieldNames.length + ? { + query: { + default_field: defaultFieldNames, + }, + } + : {}), }, }, mappings: { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/table_content.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/table_content.tsx index 76d2cf666f659..2c653ee5f76f6 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/table_content.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/table_content.tsx @@ -319,7 +319,11 @@ export const TableContent: React.FunctionComponent = ({ const rows = sortedPolicies.map((policy) => { const { name } = policy; - return {renderRowCells(policy)}; + return ( + + {renderRowCells(policy)} + + ); }); const renderAddPolicyToTemplateConfirmModal = (policy: PolicyFromES): ReactElement => { diff --git a/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts b/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts index 56f64b012fa06..59b7d00431459 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_position/log_position_state.ts @@ -21,6 +21,7 @@ interface DateRange { startTimestamp: number; endTimestamp: number; timestampsLastUpdate: number; + lastCompleteDateRangeExpressionUpdate: number; } interface VisiblePositions { @@ -46,6 +47,7 @@ export interface LogPositionStateParams { startTimestamp: number | null; endTimestamp: number | null; timestampsLastUpdate: number; + lastCompleteDateRangeExpressionUpdate: number; } export interface LogPositionCallbacks { @@ -121,6 +123,7 @@ export const useLogPositionState: () => LogPositionStateParams & LogPositionCall startTimestamp: datemathToEpochMillis(DEFAULT_DATE_RANGE.startDateExpression)!, endTimestamp: datemathToEpochMillis(DEFAULT_DATE_RANGE.endDateExpression, 'up')!, timestampsLastUpdate: Date.now(), + lastCompleteDateRangeExpressionUpdate: Date.now(), }); useEffect(() => { @@ -171,12 +174,18 @@ export const useLogPositionState: () => LogPositionStateParams & LogPositionCall jumpToTargetPosition(null); } - setDateRange({ + setDateRange((prevState) => ({ ...newDateRange, startTimestamp: nextStartTimestamp, endTimestamp: nextEndTimestamp, timestampsLastUpdate: Date.now(), - }); + // NOTE: Complete refers to the last time an update was requested with both expressions. These require a full refresh (unless streaming). Timerange expansion + // and pagination however do not. + lastCompleteDateRangeExpressionUpdate: + 'startDateExpression' in newDateRange && 'endDateExpression' in newDateRange + ? Date.now() + : prevState.lastCompleteDateRangeExpressionUpdate, + })); }, [setDateRange, dateRange, targetPosition] ); diff --git a/x-pack/plugins/infra/public/containers/logs/log_stream/use_fetch_log_entries_around.ts b/x-pack/plugins/infra/public/containers/logs/log_stream/use_fetch_log_entries_around.ts index d96cb7f2b713a..40a887686cb54 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_stream/use_fetch_log_entries_around.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_stream/use_fetch_log_entries_around.ts @@ -6,7 +6,7 @@ */ import { useCallback } from 'react'; -import { combineLatest, Observable, Subject } from 'rxjs'; +import { combineLatest, Observable, ReplaySubject } from 'rxjs'; import { last, map, startWith, switchMap } from 'rxjs/operators'; import { LogSourceColumnConfiguration } from '../../../../common/http_api/log_sources'; import { LogEntryCursor } from '../../../../common/log_entry'; @@ -53,7 +53,7 @@ export const useFetchLogEntriesAround = ({ type LogEntriesAfterRequest = NonNullable>; const logEntriesAroundSearchRequests$ = useObservable( - () => new Subject<[LogEntriesBeforeRequest, Observable]>(), + () => new ReplaySubject<[LogEntriesBeforeRequest, Observable]>(), [] ); diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx index 4362f412d5a78..4e1815e754a59 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx @@ -111,10 +111,14 @@ export const LogEntryExampleMessage: React.FunctionComponent = ({ flyoutOptions: encode({ surroundingLogsId: id, }), - logFilter: encode({ - expression: `${partitionField}: ${dataset}`, - kind: 'kuery', - }), + ...(dataset + ? { + logFilter: encode({ + expression: `${partitionField}: ${dataset}`, + kind: 'kuery', + }), + } + : {}), }, }); diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_logs_content.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_logs_content.tsx index e3e576a22e6fb..70279678a3877 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_logs_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_logs_content.tsx @@ -60,6 +60,7 @@ export const LogsPageLogsContent: React.FunctionComponent = () => { startDateExpression, endDateExpression, updateDateRange, + lastCompleteDateRangeExpressionUpdate, } = useContext(LogPositionState.Context); const { filterQuery, applyLogFilterQuery } = useContext(LogFilterState.Context); @@ -81,16 +82,16 @@ export const LogsPageLogsContent: React.FunctionComponent = () => { const prevStartTimestamp = usePrevious(startTimestamp); const prevEndTimestamp = usePrevious(endTimestamp); const prevFilterQuery = usePrevious(filterQuery); + const prevLastCompleteDateRangeExpressionUpdate = usePrevious( + lastCompleteDateRangeExpressionUpdate + ); // Refetch entries if... useEffect(() => { const isFirstLoad = !prevStartTimestamp || !prevEndTimestamp; - const newDateRangeDoesNotOverlap = - (prevStartTimestamp != null && - startTimestamp != null && - prevStartTimestamp < startTimestamp) || - (prevEndTimestamp != null && endTimestamp != null && prevEndTimestamp > endTimestamp); + const completeDateRangeExpressionHasChanged = + lastCompleteDateRangeExpressionUpdate !== prevLastCompleteDateRangeExpressionUpdate; const isCenterPointOutsideLoadedRange = targetPosition != null && @@ -101,7 +102,7 @@ export const LogsPageLogsContent: React.FunctionComponent = () => { if ( isFirstLoad || - newDateRangeDoesNotOverlap || + completeDateRangeExpressionHasChanged || isCenterPointOutsideLoadedRange || hasQueryChanged ) { @@ -124,6 +125,8 @@ export const LogsPageLogsContent: React.FunctionComponent = () => { bottomCursor, filterQuery, prevFilterQuery, + lastCompleteDateRangeExpressionUpdate, + prevLastCompleteDateRangeExpressionUpdate, ]); const { logSummaryHighlights, currentHighlightKey, logEntryHighlightsById } = useContext( diff --git a/x-pack/plugins/licensing/server/plugin.ts b/x-pack/plugins/licensing/server/plugin.ts index a1ec5e9450b28..7c0f0a119d259 100644 --- a/x-pack/plugins/licensing/server/plugin.ts +++ b/x-pack/plugins/licensing/server/plugin.ts @@ -32,6 +32,7 @@ import { FeatureUsageService } from './services'; import { LicenseConfigType } from './licensing_config'; import { createRouteHandlerContext } from './licensing_route_handler_context'; import { createOnPreResponseHandler } from './on_pre_response_handler'; +import { getPluginStatus$ } from './plugin_status'; function normalizeServerLicense(license: RawLicense): PublicLicense { return { @@ -80,7 +81,7 @@ function sign({ * current Kibana instance. */ export class LicensingPlugin implements Plugin { - private stop$ = new Subject(); + private stop$ = new Subject(); private readonly logger: Logger; private readonly config: LicenseConfigType; private loggingSubscription?: Subscription; @@ -127,6 +128,8 @@ export class LicensingPlugin implements Plugin + new TestScheduler((actual, expected) => { + expect(actual).toEqual(expected); + }); + +const degradedStatus = { + level: ServiceStatusLevels.degraded, + summary: expect.any(String), +}; +const availableStatus = { + level: ServiceStatusLevels.available, + summary: expect.any(String), +}; +const unavailableStatus = { + level: ServiceStatusLevels.unavailable, + summary: expect.any(String), +}; + +describe('getPluginStatus$', () => { + it('emits an initial `degraded` status', () => { + getTestScheduler().run(({ expectObservable, hot }) => { + const license$ = hot('|'); + const stop$ = hot(''); + const expected = '(a|)'; + + expectObservable(getPluginStatus$(license$, stop$)).toBe(expected, { + a: degradedStatus, + }); + }); + }); + + it('emits an `available` status once the license emits', () => { + getTestScheduler().run(({ expectObservable, hot }) => { + const license$ = hot('--a', { + a: licenseMock.createLicenseMock(), + }); + const stop$ = hot(''); + const expected = 'a-b'; + + expectObservable(getPluginStatus$(license$, stop$)).toBe(expected, { + a: degradedStatus, + b: availableStatus, + }); + }); + }); + + it('emits an `unavailable` status if the license emits an error', () => { + getTestScheduler().run(({ expectObservable, hot }) => { + const errorLicense = licenseMock.createLicenseMock(); + errorLicense.error = 'some-error'; + + const license$ = hot('--a', { + a: errorLicense, + }); + const stop$ = hot(''); + const expected = 'a-b'; + + expectObservable(getPluginStatus$(license$, stop$)).toBe(expected, { + a: degradedStatus, + b: unavailableStatus, + }); + }); + }); + + it('can emit `available` after `unavailable`', () => { + getTestScheduler().run(({ expectObservable, hot }) => { + const errorLicense = licenseMock.createLicenseMock(); + errorLicense.error = 'some-error'; + const validLicense = licenseMock.createLicenseMock(); + + const license$ = hot('--a--b', { + a: errorLicense, + b: validLicense, + }); + const stop$ = hot(''); + const expected = 'a-b--c'; + + expectObservable(getPluginStatus$(license$, stop$)).toBe(expected, { + a: degradedStatus, + b: unavailableStatus, + c: availableStatus, + }); + }); + }); + + it('closes when `stop$` emits', () => { + getTestScheduler().run(({ expectObservable, hot }) => { + const license$ = hot('--a--b', { + a: licenseMock.createLicenseMock(), + b: licenseMock.createLicenseMock(), + }); + const stop$ = hot('----a', { a: undefined }); + const expected = 'a-b-|'; + + expectObservable(getPluginStatus$(license$, stop$)).toBe(expected, { + a: degradedStatus, + b: availableStatus, + }); + }); + }); +}); diff --git a/x-pack/plugins/licensing/server/plugin_status.ts b/x-pack/plugins/licensing/server/plugin_status.ts new file mode 100644 index 0000000000000..f830bd966b71b --- /dev/null +++ b/x-pack/plugins/licensing/server/plugin_status.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Observable } from 'rxjs'; +import { takeUntil, startWith, map } from 'rxjs/operators'; +import { ServiceStatus, ServiceStatusLevels } from '../../../../src/core/server'; +import { ILicense } from '../common/types'; + +export const getPluginStatus$ = ( + license$: Observable, + stop$: Observable +): Observable => { + return license$.pipe( + startWith(undefined), + takeUntil(stop$), + map((license) => { + if (license) { + if (license.error) { + return { + level: ServiceStatusLevels.unavailable, + summary: 'Error fetching license', + }; + } + return { + level: ServiceStatusLevels.available, + summary: 'License fetched', + }; + } + return { + level: ServiceStatusLevels.degraded, + summary: 'License not fetched yet', + }; + }) + ); +}; diff --git a/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts index 7a21599605b52..7c4746fd2ccb3 100644 --- a/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts @@ -27,6 +27,7 @@ export type LayerDescriptor = { __isPreviewLayer?: boolean; __errorMessage?: string; __trackedLayerDescriptor?: LayerDescriptor; + __areTilesLoaded?: boolean; alpha?: number; id: string; joins?: JoinDescriptor[]; diff --git a/x-pack/plugins/maps/public/actions/layer_actions.ts b/x-pack/plugins/maps/public/actions/layer_actions.ts index d68e4744975f1..fe62e9fe9da51 100644 --- a/x-pack/plugins/maps/public/actions/layer_actions.ts +++ b/x-pack/plugins/maps/public/actions/layer_actions.ts @@ -539,3 +539,12 @@ export function setHiddenLayers(hiddenLayerIds: string[]) { } }; } + +export function setAreTilesLoaded(layerId: string, areTilesLoaded: boolean) { + return { + type: UPDATE_LAYER_PROP, + id: layerId, + propName: '__areTilesLoaded', + newValue: areTilesLoaded, + }; +} diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx index e3d5150c9cd09..a73449b0fa718 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx @@ -400,7 +400,11 @@ export class AbstractLayer implements ILayer { } isLayerLoading(): boolean { - return this._dataRequests.some((dataRequest) => dataRequest.isLoading()); + const areTilesLoading = + typeof this._descriptor.__areTilesLoaded !== 'undefined' + ? !this._descriptor.__areTilesLoaded + : false; + return areTilesLoading || this._dataRequests.some((dataRequest) => dataRequest.isLoading()); } isLoadingBounds() { diff --git a/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.d.ts b/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.d.ts index 3ef8f09189276..e83eff53c57c8 100644 --- a/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.d.ts +++ b/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.d.ts @@ -16,5 +16,6 @@ interface ITileLayerArguments { export class TileLayer extends AbstractLayer { static type: string; + constructor(args: ITileLayerArguments); } diff --git a/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.js b/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.js index d3aaefcd34197..0995d117aaa47 100644 --- a/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.js +++ b/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.js @@ -117,8 +117,4 @@ export class TileLayer extends AbstractLayer { getLayerTypeIconName() { return 'grid'; } - - isLayerLoading() { - return false; - } } diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/index.ts b/x-pack/plugins/maps/public/connected_components/mb_map/index.ts index 352e9ae8382d4..2c92f67bd7410 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/index.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/index.ts @@ -18,6 +18,7 @@ import { clearGoto, setMapInitError, MapExtentState, + setAreTilesLoaded, } from '../../actions'; import { getLayerList, @@ -69,6 +70,9 @@ function mapDispatchToProps(dispatch: ThunkDispatch void; geoFields: GeoFieldWithIndex[]; renderTooltipContent?: RenderToolTipContent; + setAreTilesLoaded: (layerId: string, areTilesLoaded: boolean) => void; } interface State { @@ -86,6 +88,7 @@ export class MBMap extends Component { private _containerRef: HTMLDivElement | null = null; private _prevDisableInteractive?: boolean; private _navigationControl = new mapboxgl.NavigationControl({ showCompass: false }); + private _tileStatusTracker?: TileStatusTracker; state: State = { prevLayerList: undefined, @@ -123,6 +126,9 @@ export class MBMap extends Component { if (this._checker) { this._checker.destroy(); } + if (this._tileStatusTracker) { + this._tileStatusTracker.destroy(); + } if (this.state.mbMap) { this.state.mbMap.remove(); this.state.mbMap = undefined; @@ -199,6 +205,12 @@ export class MBMap extends Component { mbMap.dragRotate.disable(); mbMap.touchZoomRotate.disableRotation(); + this._tileStatusTracker = new TileStatusTracker({ + mbMap, + getCurrentLayerList: () => this.props.layerList, + setAreTilesLoaded: this.props.setAreTilesLoaded, + }); + const tooManyFeaturesImageSrc = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAA7DgAAOw4BzLahgwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAARLSURBVHic7ZnPbxRVAMe/7735sWO3293ZlUItJsivCxEE0oTYRgu1FqTQoFSwKTYx8SAH/wHjj4vRozGGi56sMcW2UfqTEuOhppE0KJc2GIuKQFDY7qzdtrudX88D3YTUdFuQN8+k87ltZt7uZz958/bNLAGwBWsYKltANmEA2QKyCQPIFpBNGEC2gGzCALIFZBMGkC0gmzCAbAHZhAFkC8gmDCBbQDZhANkCslnzARQZH6oDpNs0D5UDSUIInePcOpPLfdfnODNBuwQWIAWwNOABwHZN0x8npE6hNLJ4DPWRyFSf40wE5VOEQPBjcR0g3YlE4ybGmtK+/1NzJtOZA/xSYwZMs3nG962T2ez3It2AANaA/kSidYuivOQBs5WM1fUnk6f0u+GXJUqIuUtVXx00zRbRfkIDfBqL7a1WlIYbjvNtTTr99jXXHVpH6dMjK0R4cXq6c9rzxjcx9sKX8XitSEdhAToMI7VP10/97fsTh7PZrgWAN1lW72KE2vOm2b5chDTgtWQyn93x/bEEIetEOQIC14CxVOr1CkKefH929t0v8vn0vcdGEoljGxXl4C3PGz2YyXy+AHARDqtByAxoUdWKBKV70r4/vvTLA0CjZfX+5nkDGxirKzUTgkBIgNaysh3gnF627R+XO+dQJvP1ddcdrmSsbtA020pF+CAW21qrqmUiXIUEqGRsIwD0FQq/lzqv0bJ6rrvucBVjzwyb5ivLRTiiaW+8VV7eIEBVTAANiIIQd9RxZlc6t9Gyem647vn1jD07ZJonl4sQASoevqmgABzwwHnJzc69PGdZ3X+47sgGxuqHTPPE0ggeVtg5/QeEBMhxPg1Aa1DV2GrHPG9ZXy1G2D+wNALn9jyQEeHKAJgP+033Kgrdqij7AFwZtu3bqx3XWShMHtV1o1pRGo4YxiNd+fyEB2DKdX/4aG5u0hbwcylkBryTy/3scT6zW9Nq7ndso2Wdvea6Q1WUHuiPx1/WAXLBcWZXun94UMRcAoD/p+ddTFK6u8MwUvc7vsmyem+67oVqVT0wkEgcF+FYRNhW+L25uX6f84XThtHxIBudE5bVY/t++jFVrU/dvVSFICzAqG3PX/S8rihj2/61qK1AOUB7ksl2jdLUL7Z9rvgcQQRCFsEi5wqFmw26XnhCUQ63GcZmCly95Lrzpca0G0byk3j8tEnpU1c975tmyxoU5QcE8EAEAM5WVOzfoarHAeC2749dcpzxMwsLv07Ztg0AOzVNf03Ttu/S9T2PMlbjc25fdpyutmx2TLRbIAEA4M1otKo1EjmaoHQn4ZwBgA/kAVAK6MXXdzxv/ONcrq/HcbJBeAUWoEizqsaORaPbKglZrxMSZZyrM76f/ovzWx/m85PFWREUgQf4v7Hm/xcIA8gWkE0YQLaAbMIAsgVkEwaQLSCbMIBsAdmEAWQLyCYMIFtANmEA2QKyCQPIFpDNmg/wD3OFdEybUvJjAAAAAElFTkSuQmCC'; const tooManyFeaturesImage = new Image(); diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker.test.ts b/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker.test.ts new file mode 100644 index 0000000000000..223efae657601 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker.test.ts @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// eslint-disable-next-line max-classes-per-file +import { TileStatusTracker } from './tile_status_tracker'; +import { Map as MbMap } from 'mapbox-gl'; +import { ILayer } from '../../classes/layers/layer'; + +class MockMbMap { + public listeners: Array<{ type: string; callback: (e: unknown) => void }> = []; + + on(type: string, callback: (e: unknown) => void) { + this.listeners.push({ + type, + callback, + }); + } + + emit(type: string, e: unknown) { + this.listeners.forEach((listener) => { + if (listener.type === type) { + listener.callback(e); + } + }); + } + + off(type: string, callback: (e: unknown) => void) { + this.listeners = this.listeners.filter((listener) => { + return !(listener.type === type && listener.callback === callback); + }); + } +} + +class MockLayer { + readonly _id: string; + readonly _mbSourceId: string; + constructor(id: string, mbSourceId: string) { + this._id = id; + this._mbSourceId = mbSourceId; + } + getId() { + return this._id; + } + + ownsMbSourceId(mbSourceId: string) { + return this._mbSourceId === mbSourceId; + } +} + +function createMockLayer(id: string, mbSourceId: string): ILayer { + return (new MockLayer(id, mbSourceId) as unknown) as ILayer; +} + +function createMockMbDataEvent(mbSourceId: string, tileKey: string): unknown { + return { + sourceId: mbSourceId, + dataType: 'source', + tile: { + tileID: { + key: tileKey, + }, + }, + source: { + type: 'vector', + }, + }; +} + +async function sleep(timeout: number) { + return await new Promise((resolve) => { + setTimeout(() => { + resolve(true); + }, timeout); + }); +} + +describe('TileStatusTracker', () => { + test('should add and remove tiles', async () => { + const mockMbMap = new MockMbMap(); + const loadedMap: Map = new Map(); + new TileStatusTracker({ + mbMap: (mockMbMap as unknown) as MbMap, + setAreTilesLoaded: (layerId, areTilesLoaded) => { + loadedMap.set(layerId, areTilesLoaded); + }, + getCurrentLayerList: () => { + return [ + createMockLayer('foo', 'foosource'), + createMockLayer('bar', 'barsource'), + createMockLayer('foobar', 'foobarsource'), + ]; + }, + }); + + mockMbMap.emit('sourcedataloading', createMockMbDataEvent('foosource', 'aa11')); + + const aa11BarTile = createMockMbDataEvent('barsource', 'aa11'); + mockMbMap.emit('sourcedataloading', aa11BarTile); + + mockMbMap.emit('sourcedata', createMockMbDataEvent('foosource', 'aa11')); + + // simulate delay. Cache-checking is debounced. + await sleep(300); + + expect(loadedMap.get('foo')).toBe(true); + expect(loadedMap.get('bar')).toBe(false); // still outstanding tile requests + expect(loadedMap.has('foobar')).toBe(true); // never received tile requests + + (aa11BarTile as { tile: { aborted: boolean } })!.tile.aborted = true; // abort tile + mockMbMap.emit('sourcedataloading', createMockMbDataEvent('barsource', 'af1d')); + mockMbMap.emit('sourcedataloading', createMockMbDataEvent('foosource', 'af1d')); + mockMbMap.emit('error', createMockMbDataEvent('barsource', 'af1d')); + + // simulate delay. Cache-checking is debounced. + await sleep(300); + + expect(loadedMap.get('foo')).toBe(false); // still outstanding tile requests + expect(loadedMap.get('bar')).toBe(true); // tiles were aborted or errored + expect(loadedMap.has('foobar')).toBe(true); // never received tile requests + }); + + test('should cleanup listeners on destroy', async () => { + const mockMbMap = new MockMbMap(); + const tileStatusTracker = new TileStatusTracker({ + mbMap: (mockMbMap as unknown) as MbMap, + setAreTilesLoaded: () => {}, + getCurrentLayerList: () => { + return []; + }, + }); + + expect(mockMbMap.listeners.length).toBe(3); + tileStatusTracker.destroy(); + expect(mockMbMap.listeners.length).toBe(0); + }); +}); diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker.ts b/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker.ts new file mode 100644 index 0000000000000..be946a12fe225 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker.ts @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Map as MapboxMap, MapSourceDataEvent } from 'mapbox-gl'; +import _ from 'lodash'; +import { ILayer } from '../../classes/layers/layer'; +import { SPATIAL_FILTERS_LAYER_ID } from '../../../common/constants'; + +interface MbTile { + // references internal object from mapbox + aborted?: boolean; +} + +interface Tile { + mbKey: string; + mbSourceId: string; + mbTile: MbTile; +} + +export class TileStatusTracker { + private _tileCache: Tile[]; + private readonly _mbMap: MapboxMap; + private readonly _setAreTilesLoaded: (layerId: string, areTilesLoaded: boolean) => void; + private readonly _getCurrentLayerList: () => ILayer[]; + private readonly _onSourceDataLoading = (e: MapSourceDataEvent) => { + if ( + e.sourceId && + e.sourceId !== SPATIAL_FILTERS_LAYER_ID && + e.dataType === 'source' && + e.tile && + (e.source.type === 'vector' || e.source.type === 'raster') + ) { + const tracked = this._tileCache.find((tile) => { + return ( + tile.mbKey === ((e.tile.tileID.key as unknown) as string) && + tile.mbSourceId === e.sourceId + ); + }); + + if (!tracked) { + this._tileCache.push({ + mbKey: (e.tile.tileID.key as unknown) as string, + mbSourceId: e.sourceId, + mbTile: e.tile, + }); + this._updateTileStatus(); + } + } + }; + + private readonly _onError = (e: MapSourceDataEvent) => { + if ( + e.sourceId && + e.sourceId !== SPATIAL_FILTERS_LAYER_ID && + e.tile && + (e.source.type === 'vector' || e.source.type === 'raster') + ) { + this._removeTileFromCache(e.sourceId, (e.tile.tileID.key as unknown) as string); + } + }; + private readonly _onSourceData = (e: MapSourceDataEvent) => { + if ( + e.sourceId && + e.sourceId !== SPATIAL_FILTERS_LAYER_ID && + e.dataType === 'source' && + e.tile && + (e.source.type === 'vector' || e.source.type === 'raster') + ) { + this._removeTileFromCache(e.sourceId, (e.tile.tileID.key as unknown) as string); + } + }; + + constructor({ + mbMap, + setAreTilesLoaded, + getCurrentLayerList, + }: { + mbMap: MapboxMap; + setAreTilesLoaded: (layerId: string, areTilesLoaded: boolean) => void; + getCurrentLayerList: () => ILayer[]; + }) { + this._tileCache = []; + this._setAreTilesLoaded = setAreTilesLoaded; + this._getCurrentLayerList = getCurrentLayerList; + + this._mbMap = mbMap; + this._mbMap.on('sourcedataloading', this._onSourceDataLoading); + this._mbMap.on('error', this._onError); + this._mbMap.on('sourcedata', this._onSourceData); + } + + _updateTileStatus = _.debounce(() => { + this._tileCache = this._tileCache.filter((tile) => { + return typeof tile.mbTile.aborted === 'boolean' ? !tile.mbTile.aborted : true; + }); + const layerList = this._getCurrentLayerList(); + for (let i = 0; i < layerList.length; i++) { + const layer: ILayer = layerList[i]; + let atLeastOnePendingTile = false; + for (let j = 0; j < this._tileCache.length; j++) { + const tile = this._tileCache[j]; + if (layer.ownsMbSourceId(tile.mbSourceId)) { + atLeastOnePendingTile = true; + break; + } + } + this._setAreTilesLoaded(layer.getId(), !atLeastOnePendingTile); + } + }, 100); + + _removeTileFromCache = (mbSourceId: string, mbKey: string) => { + const trackedIndex = this._tileCache.findIndex((tile) => { + return tile.mbKey === ((mbKey as unknown) as string) && tile.mbSourceId === mbSourceId; + }); + + if (trackedIndex >= 0) { + this._tileCache.splice(trackedIndex, 1); + this._updateTileStatus(); + } + }; + + destroy() { + this._mbMap.off('error', this._onError); + this._mbMap.off('sourcedata', this._onSourceData); + this._mbMap.off('sourcedataloading', this._onSourceDataLoading); + this._tileCache.length = 0; + } +} diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/__snapshots__/view.test.js.snap b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/__snapshots__/view.test.js.snap index 0af4eb0793f03..05c2ad69af771 100644 --- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/__snapshots__/view.test.js.snap +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/__snapshots__/view.test.js.snap @@ -109,7 +109,23 @@ exports[`LayerControl isLayerTOCOpen Should render expand button with error icon `; -exports[`LayerControl isLayerTOCOpen Should render expand button with loading icon when layer is loading 1`] = ` +exports[`LayerControl isLayerTOCOpen spinner icon Should not render expand button with loading icon when layer is invisible 1`] = ` + + + +`; + +exports[`LayerControl isLayerTOCOpen spinner icon Should render expand button with loading icon when layer is loading 1`] = ` { /> ); tooltipContent = this.props.layer.getErrors(); - } else if (this.props.layer.isLayerLoading()) { - icon = ; } else if (!this.props.layer.isVisible()) { icon = ; tooltipContent = i18n.translate('xpack.maps.layer.layerHiddenTooltip', { defaultMessage: `Layer is hidden.`, }); + } else if (this.props.layer.isLayerLoading()) { + icon = ; } else if (!this.props.layer.showAtZoomLevel(this.props.zoom)) { const minZoom = this.props.layer.getMinZoom(); const maxZoom = this.props.layer.getMaxZoom(); diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/view.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/view.js index b24dc515c8615..6a859befa18c1 100644 --- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/view.js +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/view.js @@ -65,7 +65,7 @@ export function LayerControl({ return layer.hasErrors(); }); const isLoading = layerList.some((layer) => { - return layer.isLayerLoading(); + return layer.isLayerLoading() && layer.isVisible(); }); return ( diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/view.test.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/view.test.js index 25baabe74e0b6..e4af1ad4f46ca 100644 --- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/view.test.js +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/view.test.js @@ -47,28 +47,44 @@ describe('LayerControl', () => { describe('isLayerTOCOpen', () => { test('Should render expand button', () => { const component = shallow(); - expect(component).toMatchSnapshot(); }); - test('Should render expand button with loading icon when layer is loading', () => { + describe('spinner icon', () => { + const isLayerLoading = true; + let isVisible = true; const mockLayerThatIsLoading = { hasErrors: () => { return false; }, isLayerLoading: () => { - return true; + return isLayerLoading; + }, + isVisible: () => { + return isVisible; }, }; - const component = shallow( - - ); - - expect(component).toMatchSnapshot(); + test('Should render expand button with loading icon when layer is loading', () => { + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); + test('Should not render expand button with loading icon when layer is invisible', () => { + isVisible = false; + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); }); test('Should render expand button with error icon when layer has error', () => { diff --git a/x-pack/plugins/security/.eslintrc.json b/x-pack/plugins/security/.eslintrc.json deleted file mode 100644 index 2b63a9259d220..0000000000000 --- a/x-pack/plugins/security/.eslintrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "rules": { - "@typescript-eslint/consistent-type-imports": 1 - } -} diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx index cf9ac50b0daf0..5d6b4a1b4fdaf 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx @@ -49,11 +49,11 @@ import type { RoleIndexPrivilege, } from '../../../../common/model'; import { - copyRole, - getExtendedRoleDeprecationNotice, isRoleDeprecated as checkIfRoleDeprecated, isRoleReadOnly as checkIfRoleReadOnly, isRoleReserved as checkIfRoleReserved, + copyRole, + getExtendedRoleDeprecationNotice, prepareRoleClone, } from '../../../../common/model'; import type { UserAPIClient } from '../../users'; diff --git a/x-pack/plugins/security/server/authorization/authorization_service.test.ts b/x-pack/plugins/security/server/authorization/authorization_service.test.ts index 62c7dd9f8891c..ff6f510f64b28 100644 --- a/x-pack/plugins/security/server/authorization/authorization_service.test.ts +++ b/x-pack/plugins/security/server/authorization/authorization_service.test.ts @@ -11,6 +11,7 @@ import { nextTick } from '@kbn/test/jest'; import { coreMock, elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks'; // Note: this import must be before other relative imports for the mocks to work as intended. +// eslint-disable-next-line import/order import { mockAuthorizationModeFactory, mockCheckPrivilegesDynamicallyWithRequestFactory, diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.ts index de0db392bdc86..28d3ddefc62b5 100644 --- a/x-pack/plugins/security/server/authorization/privileges/privileges.ts +++ b/x-pack/plugins/security/server/authorization/privileges/privileges.ts @@ -8,8 +8,8 @@ import { uniq } from 'lodash'; import type { - KibanaFeature, PluginSetupContract as FeaturesPluginSetup, + KibanaFeature, } from '../../../../features/server'; import type { SecurityLicense } from '../../../common/licensing'; import type { RawKibanaPrivileges } from '../../../common/model'; diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index 92f96a591ab5d..de2c937e0ae75 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -118,6 +118,8 @@ export const TIMELINE_DATA_PROVIDERS = '[data-test-subj="dataProviders"]'; export const TIMELINE_DATA_PROVIDERS_ACTION_MENU = '[data-test-subj="providerActions"]'; +export const TIMELINE_ADD_FIELD_BUTTON = '[data-test-subj="addField"]'; + export const TIMELINE_DATA_PROVIDERS_EMPTY = '[data-test-subj="dataProviders"] [data-test-subj="empty"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/common.ts b/x-pack/plugins/security_solution/cypress/tasks/common.ts index 293cd8fbeaa85..db8d834171128 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/common.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/common.ts @@ -27,14 +27,14 @@ export const drag = (subject: JQuery) => { clientY: subjectLocation.top, force: true, }) - .wait(300) + .wait(3000) .trigger('mousemove', { button: primaryButton, clientX: subjectLocation.left + dndSloppyClickDetectionThreshold, clientY: subjectLocation.top, force: true, }) - .wait(300); + .wait(3000); }; /** Drags the subject being dragged on the specified drop target, but does not drop it */ diff --git a/x-pack/plugins/security_solution/cypress/tasks/hosts/all_hosts.ts b/x-pack/plugins/security_solution/cypress/tasks/hosts/all_hosts.ts index 98e3d74ad3bc4..6ec5920ebf7c8 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/hosts/all_hosts.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/hosts/all_hosts.ts @@ -6,7 +6,7 @@ */ import { ALL_HOSTS_TABLE, HOSTS_NAMES_DRAGGABLE, HOSTS_NAMES } from '../../screens/hosts/all_hosts'; -import { TIMELINE_DATA_PROVIDERS, TIMELINE_DATA_PROVIDERS_EMPTY } from '../../screens/timeline'; +import { TIMELINE_ADD_FIELD_BUTTON, TIMELINE_DATA_PROVIDERS_EMPTY } from '../../screens/timeline'; import { drag, dragWithoutDrop, drop } from '../../tasks/common'; @@ -14,7 +14,7 @@ export const dragAndDropFirstHostToTimeline = () => { cy.get(HOSTS_NAMES_DRAGGABLE) .first() .then((firstHost) => drag(firstHost)); - cy.get(TIMELINE_DATA_PROVIDERS) + cy.get(TIMELINE_ADD_FIELD_BUTTON) .filter(':visible') .then((dataProvidersDropArea) => drop(dataProvidersDropArea)); }; diff --git a/x-pack/plugins/security_solution/public/common/components/charts/areachart.tsx b/x-pack/plugins/security_solution/public/common/components/charts/areachart.tsx index 53f613821196d..9a00736c4a605 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/areachart.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/areachart.tsx @@ -62,6 +62,15 @@ const checkIfAnyValidSeriesExist = ( ): data is ChartSeriesData[] => Array.isArray(data) && data.some(checkIfAllTheDataInTheSeriesAreValid); +const axisStyle = { + tickLine: { + visible: false, + }, + tickLabel: { + padding: 3, + }, +}; + // https://ela.st/multi-areaseries export const AreaChartBaseComponent = ({ data, @@ -111,23 +120,10 @@ export const AreaChartBaseComponent = ({ position={Position.Bottom} showOverlappingTicks={false} tickFormat={xTickFormatter} - style={{ - tickLine: { - visible: false, - }, - }} + style={axisStyle} /> - + ) : null; diff --git a/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.test.tsx index 6d52ea8c011c5..fe7c74aa9dc7c 100644 --- a/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.test.tsx @@ -79,7 +79,8 @@ const timepickerRanges = [ }, ]; -describe('SIEM Super Date Picker', () => { +// FLAKY: https://github.com/elastic/kibana/issues/93735 +describe.skip('SIEM Super Date Picker', () => { describe('#SuperDatePicker', () => { const state: State = mockGlobalState; const { storage } = createSecuritySolutionStorageMock(); diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts index 5eae3a4d72988..c933afc98856b 100644 --- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts @@ -214,7 +214,7 @@ export const mockGlobalState: State = { description: '', eqlOptions: { eventCategoryField: 'event.category', - tiebreakerField: 'event.sequence', + tiebreakerField: '', timestampField: '@timestamp', }, eventIdToNoteIds: {}, diff --git a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts index 26b30e0d1f89a..a9214eed60b36 100644 --- a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts +++ b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts @@ -2238,7 +2238,7 @@ export const defaultTimelineProps: CreateTimelineProps = { eventCategoryField: 'event.category', query: '', size: 100, - tiebreakerField: 'event.sequence', + tiebreakerField: '', timestampField: '@timestamp', }, eventIdToNoteIds: {}, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index 7d577659d66e2..a8aa42a3a59ff 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -157,7 +157,7 @@ describe('alert actions', () => { eventCategoryField: 'event.category', query: '', size: 100, - tiebreakerField: 'event.sequence', + tiebreakerField: '', timestampField: '@timestamp', }, eventIdToNoteIds: {}, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx index 9f5ab7be8a117..4dd40eb2ddaee 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx @@ -15,7 +15,11 @@ import { i18n } from '@kbn/i18n'; import type { Filter } from '../../../../../../../src/plugins/data/common/es_query/filters'; import { TimelineId, TimelineStatus, TimelineType } from '../../../../common/types/timeline'; import { updateAlertStatus } from '../../containers/detection_engine/alerts/api'; -import { SendAlertToTimelineActionProps, UpdateAlertStatusActionProps } from './types'; +import { + SendAlertToTimelineActionProps, + ThresholdAggregationData, + UpdateAlertStatusActionProps, +} from './types'; import { Ecs } from '../../../../common/ecs'; import { GetOneTimeline, TimelineResult } from '../../../graphql/types'; import { @@ -123,13 +127,13 @@ export const determineToAndFrom = ({ ecs }: { ecs: Ecs[] | Ecs }) => { }; } const ecsData = ecs as Ecs; - const ellapsedTimeRule = moment.duration( + const elapsedTimeRule = moment.duration( moment().diff( dateMath.parse(ecsData?.signal?.rule?.from != null ? ecsData.signal?.rule?.from[0] : 'now-0s') ) ); const from = moment(ecsData?.timestamp ?? new Date()) - .subtract(ellapsedTimeRule) + .subtract(elapsedTimeRule) .toISOString(); const to = moment(ecsData?.timestamp ?? new Date()).toISOString(); @@ -146,83 +150,100 @@ const getFiltersFromRule = (filters: string[]): Filter[] => } }, [] as Filter[]); -export const getThresholdAggregationDataProvider = ( +export const getThresholdAggregationData = ( ecsData: Ecs | Ecs[], nonEcsData: TimelineNonEcsData[] -): DataProvider[] => { +): ThresholdAggregationData => { const thresholdEcsData: Ecs[] = Array.isArray(ecsData) ? ecsData : [ecsData]; - return thresholdEcsData.reduce((outerAcc, thresholdData) => { - const threshold = thresholdData.signal?.rule?.threshold as string[]; - - let aggField: string[] = []; - let thresholdResult: { - terms?: Array<{ - field?: string; - value: string; - }>; - count: number; - }; - - try { - thresholdResult = JSON.parse((thresholdData.signal?.threshold_result as string[])[0]); - aggField = JSON.parse(threshold[0]).field; - } catch (err) { - thresholdResult = { - terms: [ - { - field: (thresholdData.rule?.threshold as { field: string }).field, - value: (thresholdData.signal?.threshold_result as { value: string }).value, - }, - ], - count: (thresholdData.signal?.threshold_result as { count: number }).count, + return thresholdEcsData.reduce( + (outerAcc, thresholdData) => { + const threshold = thresholdData.signal?.rule?.threshold as string[]; + + let aggField: string[] = []; + let thresholdResult: { + terms?: Array<{ + field?: string; + value: string; + }>; + count: number; + from: string; }; - } - - const aggregationFields = Array.isArray(aggField) ? aggField : [aggField]; - - return [ - ...outerAcc, - ...aggregationFields.reduce((acc, aggregationField, i) => { - const aggregationValue = (thresholdResult.terms ?? []).filter( - (term: { field?: string | undefined; value: string }) => term.field === aggregationField - )[0].value; - const dataProviderValue = Array.isArray(aggregationValue) - ? aggregationValue[0] - : aggregationValue; - - if (!dataProviderValue) { - return acc; - } - - const aggregationFieldId = aggregationField.replace('.', '-'); - const dataProviderPartial = { - id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-${aggregationFieldId}-${dataProviderValue}`, - name: aggregationField, - enabled: true, - excluded: false, - kqlQuery: '', - queryMatch: { - field: aggregationField, - value: dataProviderValue, - operator: ':' as QueryOperator, - }, - }; - if (i === 0) { - return [ - ...acc, + try { + thresholdResult = JSON.parse((thresholdData.signal?.threshold_result as string[])[0]); + aggField = JSON.parse(threshold[0]).field; + } catch (err) { + thresholdResult = { + terms: [ { - ...dataProviderPartial, - and: [], + field: (thresholdData.rule?.threshold as { field: string }).field, + value: (thresholdData.signal?.threshold_result as { value: string }).value, }, - ]; - } else { - acc[0].and.push(dataProviderPartial); - return acc; - } - }, []), - ]; - }, []); + ], + count: (thresholdData.signal?.threshold_result as { count: number }).count, + from: (thresholdData.signal?.threshold_result as { from: string }).from, + }; + } + + const originalTime = moment(thresholdData.signal?.original_time![0]); + const now = moment(); + const ruleFrom = dateMath.parse(thresholdData.signal?.rule?.from![0]!); + const ruleInterval = moment.duration(now.diff(ruleFrom)); + const fromOriginalTime = originalTime.clone().subtract(ruleInterval); // This is the default... can overshoot + const aggregationFields = Array.isArray(aggField) ? aggField : [aggField]; + + return { + // Use `threshold_result.from` if available (it will always be available for new signals). Otherwise, use a calculated + // lower bound, which could result in the timeline showing a superset of the events that made up the threshold set. + thresholdFrom: thresholdResult.from ?? fromOriginalTime.toISOString(), + thresholdTo: originalTime.toISOString(), + dataProviders: [ + ...outerAcc.dataProviders, + ...aggregationFields.reduce((acc, aggregationField, i) => { + const aggregationValue = (thresholdResult.terms ?? []).filter( + (term: { field?: string | undefined; value: string }) => + term.field === aggregationField + )[0].value; + const dataProviderValue = Array.isArray(aggregationValue) + ? aggregationValue[0] + : aggregationValue; + + if (!dataProviderValue) { + return acc; + } + + const aggregationFieldId = aggregationField.replace('.', '-'); + const dataProviderPartial = { + id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-${aggregationFieldId}-${dataProviderValue}`, + name: aggregationField, + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: aggregationField, + value: dataProviderValue, + operator: ':' as QueryOperator, + }, + }; + + if (i === 0) { + return [ + ...acc, + { + ...dataProviderPartial, + and: [], + }, + ]; + } else { + acc[0].and.push(dataProviderPartial); + return acc; + } + }, []), + ], + }; + }, + { dataProviders: [], thresholdFrom: '', thresholdTo: '' } as ThresholdAggregationData + ); }; export const isEqlRuleWithGroupId = (ecsData: Ecs) => @@ -446,19 +467,24 @@ export const sendAlertToTimelineAction = async ({ } if (isThresholdRule(ecsData)) { + const { thresholdFrom, thresholdTo, dataProviders } = getThresholdAggregationData( + ecsData, + nonEcsData + ); + return createTimeline({ - from, + from: thresholdFrom, notes: null, timeline: { ...timelineDefaults, description: `_id: ${ecsData._id}`, filters: getFiltersFromRule(ecsData.signal?.rule?.filters as string[]), - dataProviders: getThresholdAggregationDataProvider(ecsData, nonEcsData), + dataProviders, id: TimelineId.active, indexNames: [], dateRange: { - start: from, - end: to, + start: thresholdFrom, + end: thresholdTo, }, eventType: 'all', kqlQuery: { @@ -475,7 +501,7 @@ export const sendAlertToTimelineAction = async ({ }, }, }, - to, + to: thresholdTo, ruleNote: noteContent, }); } else { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts index a0285f1ddb1f3..987372b95a26c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts @@ -11,6 +11,7 @@ import { ISearchStart } from '../../../../../../../src/plugins/data/public'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; import { Ecs } from '../../../../common/ecs'; import { TimelineNonEcsData } from '../../../../common/search_strategy/timeline'; +import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider'; import { NoteResult } from '../../../graphql/types'; import { TimelineModel } from '../../../timelines/store/timeline/model'; import { inputsModel } from '../../../common/store'; @@ -72,3 +73,9 @@ export interface CreateTimelineProps { } export type CreateTimeline = ({ from, timeline, to }: CreateTimelineProps) => void; + +export interface ThresholdAggregationData { + thresholdFrom: string; + thresholdTo: string; + dataProviders: DataProvider[]; +} diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx index af3e427056867..7b9c3f35ef57b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx @@ -461,7 +461,9 @@ export const buildThresholdDescription = (label: string, threshold: Threshold): <> {isEmpty(threshold.field[0]) ? `${i18n.THRESHOLD_RESULTS_ALL} >= ${threshold.value}` - : `${i18n.THRESHOLD_RESULTS_AGGREGATED_BY} ${threshold.field[0]} >= ${threshold.value}`} + : `${i18n.THRESHOLD_RESULTS_AGGREGATED_BY} ${ + Array.isArray(threshold.field) ? threshold.field.join(',') : threshold.field + } >= ${threshold.value}`} ), }, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/footer.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/footer.test.tsx new file mode 100644 index 0000000000000..4de87ed33a87b --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/footer.test.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mount } from 'enzyme'; + +import { TestProviders } from '../../../../common/mock'; +import { EqlQueryBarFooter } from './footer'; + +jest.mock('../../../../common/lib/kibana', () => { + const originalModule = jest.requireActual('../../../../common/lib/kibana'); + return { + ...originalModule, + useKibana: () => ({ + services: { + docLinks: { links: { query: { eql: 'url-eql_doc' } } }, + }, + }), + }; +}); + +describe('EQL footer', () => { + describe('EQL Settings', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('EQL settings button is enable when popover is NOT open', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper.find(`[data-test-subj="eql-settings-trigger"]`).first().prop('isDisabled') + ).toBeFalsy(); + }); + + it('disable EQL settings button when popover is open', () => { + const wrapper = mount( + + + + ); + wrapper.find(`[data-test-subj="eql-settings-trigger"]`).first().simulate('click'); + wrapper.update(); + + expect( + wrapper.find(`[data-test-subj="eql-settings-trigger"]`).first().prop('isDisabled') + ).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/footer.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/footer.tsx index b2b5badac2127..6c31eef221ee1 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/footer.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/footer.tsx @@ -75,8 +75,14 @@ export const EqlQueryBarFooter: FC = ({ const [openEqlSettings, setIsOpenEqlSettings] = useState(false); const [localSize, setLocalSize] = useState(optionsSelected?.size ?? 100); const debounceSize = useRef(); - const openEqlSettingsHandler = useCallback(() => setIsOpenEqlSettings(true), []); - const closeEqlSettingsHandler = useCallback(() => setIsOpenEqlSettings(false), []); + + const openEqlSettingsHandler = useCallback(() => { + setIsOpenEqlSettings(true); + }, []); + const closeEqlSettingsHandler = useCallback(() => { + setIsOpenEqlSettings(false); + }, []); + const handleEventCategoryField = useCallback( (opt: EuiComboBoxOptionOption[]) => { if (onOptionsChange) { @@ -174,13 +180,15 @@ export const EqlQueryBarFooter: FC = ({ } isOpen={openEqlSettings} closePopover={closeEqlSettingsHandler} anchorPosition="downCenter" - ownFocus={true} + ownFocus={false} > {i18n.EQL_SETTINGS_TITLE}
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx index 194584ec8eb87..ace5fd083620c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx @@ -206,6 +206,28 @@ export const schema: FormSchema = { defaultMessage: "Select fields to group by. Fields are joined together with 'AND'", } ), + validations: [ + { + validator: ( + ...args: Parameters + ): ReturnType> | undefined => { + const [{ formData }] = args; + const needsValidation = isThresholdRule(formData.ruleType); + if (!needsValidation) { + return; + } + return fieldValidators.maxLengthField({ + length: 3, + message: i18n.translate( + 'xpack.securitySolution.detectionEngine.validations.thresholdFieldFieldData.arrayLengthGreaterThanMaxErrorMessage', + { + defaultMessage: 'Number of fields must be 3 or less.', + } + ), + })(...args); + }, + }, + ], }, value: { type: FIELD_TYPES.NUMBER, @@ -245,7 +267,7 @@ export const schema: FormSchema = { fieldsToValidateOnChange: ['threshold.cardinality.field', 'threshold.cardinality.value'], type: FIELD_TYPES.COMBO_BOX, label: i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThresholdCardinalityFieldLabel', + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdCardinalityFieldLabel', { defaultMessage: 'Count', } @@ -277,7 +299,7 @@ export const schema: FormSchema = { }, ], helpText: i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThresholdFieldCardinalityFieldHelpText', + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdFieldCardinalityFieldHelpText', { defaultMessage: 'Select a field to check cardinality', } @@ -287,7 +309,7 @@ export const schema: FormSchema = { fieldsToValidateOnChange: ['threshold.cardinality.field', 'threshold.cardinality.value'], type: FIELD_TYPES.NUMBER, label: i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThresholdCardinalityValueFieldLabel', + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdCardinalityValueFieldLabel', { defaultMessage: 'Unique values', } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/antivirus_registration_form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/antivirus_registration_form/index.tsx index 7be2c9870b2f7..51edf1a200c53 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/antivirus_registration_form/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/antivirus_registration_form/index.tsx @@ -54,7 +54,14 @@ export const AntivirusRegistrationForm = memo(() => { ); return ( - + {TRANSLATIONS.description} ( ConfigFormHeading.displayName = 'ConfigFormHeading'; export const ConfigForm: FC = memo( - ({ type, supportedOss, dataTestSubj, rightCorner, children }) => ( + ({ type, supportedOss, osRestriction, dataTestSubj, rightCorner, children }) => ( - + {TITLES.type} {type} {TITLES.os} - {supportedOss.map((os) => OS_TITLES[os]).join(', ')} + + + {supportedOss.map((os) => OS_TITLES[os]).join(', ')} + + {osRestriction && ( + + + + + + + + + + + + + )} + - + {rightCorner} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx index 1ae4144a26835..ff5f410611099 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx @@ -326,7 +326,7 @@ describe('Policy Details', () => { const userNotificationCustomMessageTextArea = policyView.find( 'EuiTextArea[data-test-subj="malwareUserNotificationCustomMessage"]' ); - const tooltip = policyView.find('EuiIconTip'); + const tooltip = policyView.find('EuiIconTip[data-test-subj="malwareTooltip"]'); expect(userNotificationCheckbox).toHaveLength(0); expect(userNotificationCustomMessageTextArea).toHaveLength(0); expect(tooltip).toHaveLength(0); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx index ec0b563421537..a5be095abfc59 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx @@ -211,14 +211,14 @@ export const MalwareProtections = React.memo(() => { - + - + { - + - + { deletedEventIds: [], eqlOptions: { eventCategoryField: 'event.category', - tiebreakerField: 'event.sequence', + tiebreakerField: '', timestampField: '@timestamp', query: '', size: 100, @@ -399,7 +399,7 @@ describe('helpers', () => { deletedEventIds: [], eqlOptions: { eventCategoryField: 'event.category', - tiebreakerField: 'event.sequence', + tiebreakerField: '', timestampField: '@timestamp', query: '', size: 100, @@ -509,7 +509,7 @@ describe('helpers', () => { deletedEventIds: [], eqlOptions: { eventCategoryField: 'event.category', - tiebreakerField: 'event.sequence', + tiebreakerField: '', timestampField: '@timestamp', query: '', size: 100, @@ -617,7 +617,7 @@ describe('helpers', () => { deletedEventIds: [], eqlOptions: { eventCategoryField: 'event.category', - tiebreakerField: 'event.sequence', + tiebreakerField: '', timestampField: '@timestamp', query: '', size: 100, @@ -763,7 +763,7 @@ describe('helpers', () => { deletedEventIds: [], eqlOptions: { eventCategoryField: 'event.category', - tiebreakerField: 'event.sequence', + tiebreakerField: '', timestampField: '@timestamp', query: '', size: 100, @@ -898,7 +898,7 @@ describe('helpers', () => { deletedEventIds: [], eqlOptions: { eventCategoryField: 'event.category', - tiebreakerField: 'event.sequence', + tiebreakerField: '', timestampField: '@timestamp', query: '', size: 100, @@ -1049,7 +1049,7 @@ describe('helpers', () => { deletedEventIds: [], eqlOptions: { eventCategoryField: 'event.category', - tiebreakerField: 'event.sequence', + tiebreakerField: '', timestampField: '@timestamp', query: '', size: 100, @@ -1159,7 +1159,7 @@ describe('helpers', () => { deletedEventIds: [], eqlOptions: { eventCategoryField: 'event.category', - tiebreakerField: 'event.sequence', + tiebreakerField: '', timestampField: '@timestamp', query: '', size: 100, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx index 68461a7234d09..62aa4f0c2456d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx @@ -21,6 +21,7 @@ import { StatefulBody } from '../body'; import { Footer, footerHeight } from '../footer'; import { requiredFieldsForActions } from '../../../../detections/components/alerts_table/default_config'; import { EventDetailsWidthProvider } from '../../../../common/components/events_viewer/event_details_width_context'; +import { sourcererSelectors } from '../../../../common/store/sourcerer'; import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; import { timelineDefaults } from '../../../store/timeline/defaults'; import { useSourcererScope } from '../../../../common/containers/sourcerer'; @@ -30,6 +31,7 @@ import { State } from '../../../../common/store'; import { calculateTotalPages } from '../helpers'; import { TimelineTabs } from '../../../../../common/types/timeline'; import { DetailsPanel } from '../../side_panel'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; const StyledEuiFlyoutBody = styled(EuiFlyoutBody)` overflow-y: hidden; @@ -97,6 +99,12 @@ export const PinnedTabContentComponent: React.FC = ({ SourcererScopeName.timeline ); + const existingIndexNamesSelector = useMemo( + () => sourcererSelectors.getAllExistingIndexNamesSelector(), + [] + ); + const existingIndexNames = useDeepEqualSelector(existingIndexNamesSelector); + const filterQuery = useMemo(() => { if (isEmpty(pinnedEventIds)) { return ''; @@ -159,7 +167,7 @@ export const PinnedTabContentComponent: React.FC = ({ docValueFields, endDate: '', id: `pinned-${timelineId}`, - indexNames: [''], + indexNames: existingIndexNames, fields: timelineQueryFields, limit: itemsPerPage, filterQuery, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/eql/selectors.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/eql/selectors.tsx index 83418d5f2561f..9c3555f2fb710 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/eql/selectors.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/eql/selectors.tsx @@ -16,7 +16,7 @@ export const getEqlOptions = () => eventCategoryField: [{ label: 'event.category' }], tiebreakerField: [ { - label: 'event.sequence', + label: '', }, ], timestampField: [ diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts index 77d5b5d3caed8..5f9e64843573f 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts @@ -25,7 +25,7 @@ export const timelineDefaults: SubsetTimelineModel & description: '', eqlOptions: { eventCategoryField: 'event.category', - tiebreakerField: 'event.sequence', + tiebreakerField: '', timestampField: '@timestamp', query: '', size: 100, diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.test.ts index d9accd5512de6..57fa86f853c8d 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.test.ts @@ -91,7 +91,7 @@ describe('Epic Timeline', () => { description: '', eqlOptions: { eventCategoryField: 'event.category', - tiebreakerField: 'event.sequence', + tiebreakerField: '', timestampField: '@timestamp', }, eventIdToNoteIds: {}, @@ -241,7 +241,7 @@ describe('Epic Timeline', () => { description: '', eqlOptions: { eventCategoryField: 'event.category', - tiebreakerField: 'event.sequence', + tiebreakerField: '', timestampField: '@timestamp', }, eventType: 'all', diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts index e9f2d6c468207..acdf064c2355f 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts @@ -79,7 +79,7 @@ const basicTimeline: TimelineModel = { description: '', eqlOptions: { eventCategoryField: 'event.category', - tiebreakerField: 'event.sequence', + tiebreakerField: '', timestampField: '@timestamp', }, eventIdToNoteIds: {}, diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts index a3d41d35aaa92..58a73d31708a1 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts @@ -30,10 +30,10 @@ import { isAtLeast, LicenseService } from '../../../../common/license/license'; export class PolicyWatcher { private logger: Logger; - private soClient: SavedObjectsClientContract; private esClient: ElasticsearchClient; private policyService: PackagePolicyServiceInterface; private subscription: Subscription | undefined; + private soStart: SavedObjectsServiceStart; constructor( policyService: PackagePolicyServiceInterface, soStart: SavedObjectsServiceStart, @@ -41,9 +41,9 @@ export class PolicyWatcher { logger: Logger ) { this.policyService = policyService; - this.soClient = this.makeInternalSOClient(soStart); this.esClient = esStart.client.asInternalUser; this.logger = logger; + this.soStart = soStart; } /** @@ -89,7 +89,7 @@ export class PolicyWatcher { }; do { try { - response = await this.policyService.list(this.soClient, { + response = await this.policyService.list(this.makeInternalSOClient(this.soStart), { page: page++, perPage: 100, kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: endpoint`, @@ -119,12 +119,17 @@ export class PolicyWatcher { license ); try { - await this.policyService.update(this.soClient, this.esClient, policy.id, updatePolicy); + await this.policyService.update( + this.makeInternalSOClient(this.soStart), + this.esClient, + policy.id, + updatePolicy + ); } catch (e) { // try again for transient issues try { await this.policyService.update( - this.soClient, + this.makeInternalSOClient(this.soStart), this.esClient, policy.id, updatePolicy diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json index 26ff1c501540b..d6a06848592cc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json @@ -391,6 +391,9 @@ }, "threshold_result": { "properties": { + "from": { + "type": "date" + }, "terms": { "properties": { "field": { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts index 2d6e90443c00f..4106d7532f7bb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -380,40 +380,15 @@ export const sampleThresholdSignalHit = (): SignalHit => ({ kind: 'signal', }, signal: { - parents: [ - { - id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', - type: 'event', - index: 'myFakeSignalIndex', - depth: 0, - }, - { - id: '730ddf9e-5a00-4f85-9ddf-5878ca511a87', - type: 'event', - index: 'myFakeSignalIndex', - depth: 0, - }, - ], - ancestors: [ - { - id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', - type: 'event', - index: 'myFakeSignalIndex', - depth: 0, - }, - { - id: '730ddf9e-5a00-4f85-9ddf-5878ca511a87', - type: 'event', - index: 'myFakeSignalIndex', - depth: 0, - }, - ], + parents: [], + ancestors: [], original_time: '2021-02-16T17:37:34.275Z', status: 'open', threshold_result: { count: 72, terms: [{ field: 'host.name', value: 'a hostname' }], cardinality: [{ field: 'process.name', value: 6 }], + from: '2021-02-16T17:31:34.275Z', }, rule: { author: [], @@ -483,6 +458,47 @@ export const sampleLegacyThresholdSignalHit = (): unknown => ({ }, }); +export const sampleThresholdSignalHitWithMitigatedDupes = (): unknown => ({ + ...sampleThresholdHit, + signal: { + ...sampleThresholdHit.signal, + threshold_result: { + ...sampleThresholdHit.signal.threshold_result, + from: '2021-02-16T17:34:34.275Z', + }, + }, +}); + +export const sampleThresholdSignalHitWithEverything = (): unknown => ({ + ...sampleThresholdHit, + signal: { + ...sampleThresholdHit.signal, + rule: { + ...sampleThresholdHit.signal.rule, + threshold: { + field: ['host.name', 'event.category', 'source.ip'], + value: 5, + cardinality: [ + { + field: 'process.name', + value: 2, + }, + ], + }, + }, + threshold_result: { + count: 22, + terms: [ + { field: 'host.name', value: 'a hostname' }, + { field: 'event.category', value: 'network' }, + { field: 'source.ip', value: '192.168.0.1' }, + ], + cardinality: [{ field: 'process.name', value: 3 }], + from: '2021-02-16T17:34:34.275Z', + }, + }, +}); + export const sampleWrappedThresholdSignalHit = (): WrappedSignalHit => { return { _index: 'myFakeSignalIndex', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/threshold_signal_history.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/threshold_signal_history.mock.ts new file mode 100644 index 0000000000000..4daa0de79ac30 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/threshold_signal_history.mock.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ThresholdSignalHistory } from '../types'; +import { getThresholdTermsHash } from '../utils'; + +export const sampleThresholdSignalHistory = (): ThresholdSignalHistory => { + const terms = [ + { + field: 'source.ip', + value: '127.0.0.1', + }, + { + field: 'host.name', + value: 'garden-gnomes', + }, + ]; + return { + [`${getThresholdTermsHash(terms)}`]: { + terms, + lastSignalTimestamp: new Date('2020-12-17T16:28:00Z').getTime(), + }, + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts index bfa452af0f3e9..fe4781d384358 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts @@ -41,10 +41,13 @@ export const signalSchema = schema.object({ threat: schema.nullable(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), threshold: schema.maybe( schema.object({ - // Can be an empty string or empty array - field: schema.nullable(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])), + // Can be an empty string (pre-7.12) or empty array (7.12+) + field: schema.nullable( + schema.oneOf([schema.string(), schema.arrayOf(schema.string(), { maxSize: 3 })]) + ), // Always required value: schema.number(), + // Can be null (pre-7.12) or empty array (7.12+) cardinality: schema.nullable( schema.arrayOf( schema.object({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 45d3986a1c115..c24b10dc09a86 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -56,10 +56,13 @@ import { import { signalParamsSchema } from './signal_params_schema'; import { siemRuleActionGroups } from './siem_rule_action_groups'; import { findMlSignals } from './find_ml_signals'; -import { findThresholdSignals } from './find_threshold_signals'; +import { + bulkCreateThresholdSignals, + getThresholdBucketFilters, + getThresholdSignalHistory, + findThresholdSignals, +} from './threshold'; import { bulkCreateMlSignals } from './bulk_create_ml_signals'; -import { bulkCreateThresholdSignals } from './bulk_create_threshold_signals'; -import { getThresholdBucketFilters } from './threshold_get_bucket_filters'; import { scheduleNotificationActions, NotificationRuleTypeParams, @@ -378,94 +381,103 @@ export const signalRulesAlertType = ({ } const inputIndex = await getInputIndex(services, version, index); - const { - filters: bucketFilters, - searchErrors: previousSearchErrors, - } = await getThresholdBucketFilters({ - indexPattern: [outputIndex], - from, - to, - services, - logger, - ruleId, - bucketByFields: normalizeThresholdField(threshold.field), - timestampOverride, - buildRuleMessage, - }); - - const esFilter = await getFilter({ - type, - filters: filters ? filters.concat(bucketFilters) : bucketFilters, - language, - query, - savedId, - services, - index: inputIndex, - lists: exceptionItems ?? [], - }); + for (const tuple of tuples) { + const { + thresholdSignalHistory, + searchErrors: previousSearchErrors, + } = await getThresholdSignalHistory({ + indexPattern: [outputIndex], + from: tuple.from.toISOString(), + to: tuple.to.toISOString(), + services, + logger, + ruleId, + bucketByFields: normalizeThresholdField(threshold.field), + timestampOverride, + buildRuleMessage, + }); - const { - searchResult: thresholdResults, - searchErrors, - searchDuration: thresholdSearchDuration, - } = await findThresholdSignals({ - inputIndexPattern: inputIndex, - from, - to, - services, - logger, - filter: esFilter, - threshold, - timestampOverride, - buildRuleMessage, - }); + const bucketFilters = await getThresholdBucketFilters({ + thresholdSignalHistory, + timestampOverride, + }); - const { - success, - bulkCreateDuration, - createdItemsCount, - createdItems, - errors, - } = await bulkCreateThresholdSignals({ - actions, - throttle, - someResult: thresholdResults, - ruleParams: params, - filter: esFilter, - services, - logger, - id: alertId, - inputIndexPattern: inputIndex, - signalsIndex: outputIndex, - timestampOverride, - startedAt, - name, - createdBy, - createdAt, - updatedBy, - updatedAt, - interval, - enabled, - refresh, - tags, - buildRuleMessage, - }); + const esFilter = await getFilter({ + type, + filters: filters ? filters.concat(bucketFilters) : bucketFilters, + language, + query, + savedId, + services, + index: inputIndex, + lists: exceptionItems ?? [], + }); - result = mergeReturns([ - result, - createSearchAfterReturnTypeFromResponse({ + const { searchResult: thresholdResults, + searchErrors, + searchDuration: thresholdSearchDuration, + } = await findThresholdSignals({ + inputIndexPattern: inputIndex, + from: tuple.from.toISOString(), + to: tuple.to.toISOString(), + services, + logger, + filter: esFilter, + threshold, timestampOverride, - }), - createSearchAfterReturnType({ + buildRuleMessage, + }); + + const { success, - errors: [...errors, ...previousSearchErrors, ...searchErrors], - createdSignalsCount: createdItemsCount, - createdSignals: createdItems, - bulkCreateTimes: bulkCreateDuration ? [bulkCreateDuration] : [], - searchAfterTimes: [thresholdSearchDuration], - }), - ]); + bulkCreateDuration, + createdItemsCount, + createdItems, + errors, + } = await bulkCreateThresholdSignals({ + actions, + throttle, + someResult: thresholdResults, + ruleParams: params, + filter: esFilter, + services, + logger, + id: alertId, + inputIndexPattern: inputIndex, + signalsIndex: outputIndex, + timestampOverride, + startedAt, + from: tuple.from.toDate(), + name, + createdBy, + createdAt, + updatedBy, + updatedAt, + interval, + enabled, + refresh, + tags, + thresholdSignalHistory, + buildRuleMessage, + }); + + result = mergeReturns([ + result, + createSearchAfterReturnTypeFromResponse({ + searchResult: thresholdResults, + timestampOverride, + }), + createSearchAfterReturnType({ + success, + errors: [...errors, ...previousSearchErrors, ...searchErrors], + createdSignalsCount: createdItemsCount, + createdSignals: createdItems, + bulkCreateTimes: bulkCreateDuration ? [bulkCreateDuration] : [], + searchAfterTimes: [thresholdSearchDuration], + }), + ]); + } } else if (isThreatMatchRule(type)) { if ( threatQuery == null || diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.test.ts similarity index 76% rename from x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.test.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.test.ts index 130077d2fdf2b..3726f66cb0f82 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.test.ts @@ -5,15 +5,16 @@ * 2.0. */ -import { loggingSystemMock } from '../../../../../../../src/core/server/mocks'; -import { sampleDocNoSortId, sampleDocSearchResultsNoSortId } from './__mocks__/es_results'; -import { transformThresholdResultsToEcs } from './bulk_create_threshold_signals'; -import { calculateThresholdSignalUuid } from './utils'; -import { normalizeThresholdField } from '../../../../common/detection_engine/utils'; +import { loggingSystemMock } from '../../../../../../../../src/core/server/mocks'; +import { normalizeThresholdField } from '../../../../../common/detection_engine/utils'; import { Threshold, ThresholdNormalized, -} from '../../../../common/detection_engine/schemas/common/schemas'; +} from '../../../../../common/detection_engine/schemas/common/schemas'; +import { sampleDocNoSortId, sampleDocSearchResultsNoSortId } from '../__mocks__/es_results'; +import { sampleThresholdSignalHistory } from '../__mocks__/threshold_signal_history.mock'; +import { calculateThresholdSignalUuid } from '../utils'; +import { transformThresholdResultsToEcs } from './bulk_create_threshold_signals'; describe('transformThresholdNormalizedResultsToEcs', () => { it('should return transformed threshold results for pre-7.12 rules', () => { @@ -21,6 +22,7 @@ describe('transformThresholdNormalizedResultsToEcs', () => { field: 'source.ip', value: 1, }; + const from = new Date('2020-12-17T16:27:00Z'); const startedAt = new Date('2020-12-17T16:27:00Z'); const transformedResults = transformThresholdResultsToEcs( { @@ -43,6 +45,7 @@ describe('transformThresholdNormalizedResultsToEcs', () => { }, 'test', startedAt, + from, undefined, loggingSystemMock.createLogger(), { @@ -50,7 +53,8 @@ describe('transformThresholdNormalizedResultsToEcs', () => { field: normalizeThresholdField(threshold.field), }, '1234', - undefined + undefined, + sampleThresholdSignalHistory() ); const _id = calculateThresholdSignalUuid('1234', startedAt, ['source.ip'], '127.0.0.1'); expect(transformedResults).toEqual({ @@ -77,6 +81,7 @@ describe('transformThresholdNormalizedResultsToEcs', () => { _source: { '@timestamp': '2020-04-20T21:27:45+0000', threshold_result: { + from: new Date('2020-12-17T16:27:00.000Z'), terms: [ { field: 'source.ip', @@ -98,6 +103,7 @@ describe('transformThresholdNormalizedResultsToEcs', () => { field: '', value: 1, }; + const from = new Date('2020-12-17T16:27:00Z'); const startedAt = new Date('2020-12-17T16:27:00Z'); const transformedResults = transformThresholdResultsToEcs( { @@ -120,6 +126,7 @@ describe('transformThresholdNormalizedResultsToEcs', () => { }, 'test', startedAt, + from, undefined, loggingSystemMock.createLogger(), { @@ -127,7 +134,8 @@ describe('transformThresholdNormalizedResultsToEcs', () => { field: normalizeThresholdField(threshold.field), }, '1234', - undefined + undefined, + sampleThresholdSignalHistory() ); const _id = calculateThresholdSignalUuid('1234', startedAt, [], ''); expect(transformedResults).toEqual({ @@ -154,6 +162,7 @@ describe('transformThresholdNormalizedResultsToEcs', () => { _source: { '@timestamp': '2020-04-20T21:27:45+0000', threshold_result: { + from: new Date('2020-12-17T16:27:00.000Z'), terms: [], cardinality: undefined, count: 15, @@ -176,6 +185,7 @@ describe('transformThresholdNormalizedResultsToEcs', () => { }, ], }; + const from = new Date('2020-12-17T16:27:00Z'); const startedAt = new Date('2020-12-17T16:27:00Z'); const transformedResults = transformThresholdResultsToEcs( { @@ -209,11 +219,13 @@ describe('transformThresholdNormalizedResultsToEcs', () => { }, 'test', startedAt, + from, undefined, loggingSystemMock.createLogger(), threshold, '1234', - undefined + undefined, + sampleThresholdSignalHistory() ); const _id = calculateThresholdSignalUuid( '1234', @@ -245,15 +257,16 @@ describe('transformThresholdNormalizedResultsToEcs', () => { _source: { '@timestamp': '2020-04-20T21:27:45+0000', threshold_result: { + from: new Date('2020-12-17T16:28:00.000Z'), // from threshold signal history terms: [ - { - field: 'source.ip', - value: '127.0.0.1', - }, { field: 'host.name', value: 'garden-gnomes', }, + { + field: 'source.ip', + value: '127.0.0.1', + }, ], cardinality: [ { @@ -270,6 +283,68 @@ describe('transformThresholdNormalizedResultsToEcs', () => { }); }); + it('should return transformed threshold results with empty buckets', () => { + const threshold: ThresholdNormalized = { + field: ['source.ip', 'host.name'], + value: 1, + cardinality: [ + { + field: 'destination.ip', + value: 5, + }, + ], + }; + const from = new Date('2020-12-17T16:27:00Z'); + const startedAt = new Date('2020-12-17T16:27:00Z'); + const transformedResults = transformThresholdResultsToEcs( + { + ...sampleDocSearchResultsNoSortId('abcd'), + aggregations: { + 'threshold_0:source.ip': { + buckets: [ + { + key: '127.0.0.1', + doc_count: 15, + 'threshold_1:host.name': { + buckets: [], + }, + }, + ], + }, + }, + }, + 'test', + startedAt, + from, + undefined, + loggingSystemMock.createLogger(), + threshold, + '1234', + undefined, + sampleThresholdSignalHistory() + ); + expect(transformedResults).toEqual({ + took: 10, + timed_out: false, + _shards: { + total: 10, + successful: 10, + failed: 0, + skipped: 0, + }, + results: { + hits: { + total: 0, + }, + }, + hits: { + total: 100, + max_score: 100, + hits: [], + }, + }); + }); + it('should return transformed threshold results without threshold fields', () => { const threshold: ThresholdNormalized = { field: [], @@ -281,6 +356,7 @@ describe('transformThresholdNormalizedResultsToEcs', () => { }, ], }; + const from = new Date('2020-12-17T16:27:00Z'); const startedAt = new Date('2020-12-17T16:27:00Z'); const transformedResults = transformThresholdResultsToEcs( { @@ -306,11 +382,13 @@ describe('transformThresholdNormalizedResultsToEcs', () => { }, 'test', startedAt, + from, undefined, loggingSystemMock.createLogger(), threshold, '1234', - undefined + undefined, + sampleThresholdSignalHistory() ); const _id = calculateThresholdSignalUuid('1234', startedAt, [], ''); expect(transformedResults).toEqual({ @@ -337,6 +415,7 @@ describe('transformThresholdNormalizedResultsToEcs', () => { _source: { '@timestamp': '2020-04-20T21:27:45+0000', threshold_result: { + from: new Date('2020-12-17T16:27:00.000Z'), terms: [], cardinality: [ { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts similarity index 71% rename from x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts index fd99d04f87b77..c226b63a0b9ac 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts @@ -8,24 +8,33 @@ import { get } from 'lodash/fp'; import set from 'set-value'; -import { normalizeThresholdField } from '../../../../common/detection_engine/utils'; +import { normalizeThresholdField } from '../../../../../common/detection_engine/utils'; import { ThresholdNormalized, TimestampOverrideOrUndefined, -} from '../../../../common/detection_engine/schemas/common/schemas'; -import { Logger } from '../../../../../../../src/core/server'; +} from '../../../../../common/detection_engine/schemas/common/schemas'; +import { Logger } from '../../../../../../../../src/core/server'; import { AlertInstanceContext, AlertInstanceState, AlertServices, -} from '../../../../../alerting/server'; -import { BaseHit, RuleAlertAction } from '../../../../common/detection_engine/types'; -import { RuleTypeParams, RefreshTypes } from '../types'; -import { singleBulkCreate, SingleBulkCreateResponse } from './single_bulk_create'; -import { calculateThresholdSignalUuid, getThresholdAggregationParts } from './utils'; -import { BuildRuleMessage } from './rule_messages'; -import { TermAggregationBucket } from '../../types'; -import { MultiAggBucket, SignalSearchResponse, SignalSource } from './types'; +} from '../../../../../../alerting/server'; +import { BaseHit, RuleAlertAction } from '../../../../../common/detection_engine/types'; +import { TermAggregationBucket } from '../../../types'; +import { RuleTypeParams, RefreshTypes } from '../../types'; +import { singleBulkCreate, SingleBulkCreateResponse } from '../single_bulk_create'; +import { + calculateThresholdSignalUuid, + getThresholdAggregationParts, + getThresholdTermsHash, +} from '../utils'; +import { BuildRuleMessage } from '../rule_messages'; +import { + MultiAggBucket, + SignalSearchResponse, + SignalSource, + ThresholdSignalHistory, +} from '../types'; interface BulkCreateThresholdSignalsParams { actions: RuleAlertAction[]; @@ -49,6 +58,8 @@ interface BulkCreateThresholdSignalsParams { tags: string[]; throttle: string; startedAt: Date; + from: Date; + thresholdSignalHistory: ThresholdSignalHistory; buildRuleMessage: BuildRuleMessage; } @@ -56,11 +67,13 @@ const getTransformedHits = ( results: SignalSearchResponse, inputIndex: string, startedAt: Date, + from: Date, logger: Logger, threshold: ThresholdNormalized, ruleId: string, filter: unknown, - timestampOverride: TimestampOverrideOrUndefined + timestampOverride: TimestampOverrideOrUndefined, + thresholdSignalHistory: ThresholdSignalHistory ) => { const aggParts = threshold.field.length ? results.aggregations && getThresholdAggregationParts(results.aggregations) @@ -126,6 +139,8 @@ const getTransformedHits = ( }, []); }; + // Recurse through the nested buckets and collect each unique combination of terms. Collect the + // cardinality and document count from the leaf buckets and return a signal for each set of terms. return getCombinations(results.aggregations[aggParts.name].buckets, 0, aggParts.field).reduce( (acc: Array>, bucket) => { const hit = bucket.topThresholdHits?.hits.hits[0]; @@ -143,22 +158,23 @@ const getTransformedHits = ( return acc; } + const termsHash = getThresholdTermsHash(bucket.terms); + const signalHit = thresholdSignalHistory[termsHash]; + const source = { '@timestamp': timestamp, threshold_result: { - terms: bucket.terms.map((term) => { - return { - field: term.field, - value: term.value, - }; - }), - cardinality: bucket.cardinality?.map((cardinality) => { - return { - field: cardinality.field, - value: cardinality.value, - }; - }), + terms: bucket.terms, + cardinality: bucket.cardinality, count: bucket.docCount, + // Store `from` in the signal so that we know the lower bound for the + // threshold set in the timeline search. The upper bound will always be + // the `original_time` of the signal (the timestamp of the latest event + // in the set). + from: + signalHit?.lastSignalTimestamp != null + ? new Date(signalHit!.lastSignalTimestamp) + : from, }, }; @@ -168,7 +184,10 @@ const getTransformedHits = ( ruleId, startedAt, threshold.field, - bucket.terms.map((term) => term.value).join(',') + bucket.terms + .map((term) => term.value) + .sort() + .join(',') ), _source: source, }); @@ -183,21 +202,25 @@ export const transformThresholdResultsToEcs = ( results: SignalSearchResponse, inputIndex: string, startedAt: Date, + from: Date, filter: unknown, logger: Logger, threshold: ThresholdNormalized, ruleId: string, - timestampOverride: TimestampOverrideOrUndefined + timestampOverride: TimestampOverrideOrUndefined, + thresholdSignalHistory: ThresholdSignalHistory ): SignalSearchResponse => { const transformedHits = getTransformedHits( results, inputIndex, startedAt, + from, logger, threshold, ruleId, filter, - timestampOverride + timestampOverride, + thresholdSignalHistory ); const thresholdResults = { ...results, @@ -223,6 +246,7 @@ export const bulkCreateThresholdSignals = async ( thresholdResults, params.inputIndexPattern.join(','), params.startedAt, + params.from, params.filter, params.logger, { @@ -230,7 +254,8 @@ export const bulkCreateThresholdSignals = async ( field: normalizeThresholdField(threshold.field), }, params.ruleParams.ruleId, - params.timestampOverride + params.timestampOverride, + params.thresholdSignalHistory ); const buildRuleMessage = params.buildRuleMessage; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_find_previous_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_previous_threshold_signals.ts similarity index 72% rename from x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_find_previous_signals.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_previous_threshold_signals.ts index b0c7d68f5f306..06e718b646ffa 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_find_previous_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_previous_threshold_signals.ts @@ -5,17 +5,16 @@ * 2.0. */ -import { TimestampOverrideOrUndefined } from '../../../../common/detection_engine/schemas/common/schemas'; -import { singleSearchAfter } from './single_search_after'; - +import { TimestampOverrideOrUndefined } from '../../../../../common/detection_engine/schemas/common/schemas'; import { AlertInstanceContext, AlertInstanceState, AlertServices, -} from '../../../../../alerting/server'; -import { Logger } from '../../../../../../../src/core/server'; -import { SignalSearchResponse } from './types'; -import { BuildRuleMessage } from './rule_messages'; +} from '../../../../../../alerting/server'; +import { Logger } from '../../../../../../../../src/core/server'; +import { BuildRuleMessage } from '../rule_messages'; +import { singleSearchAfter } from '../single_search_after'; +import { SignalSearchResponse } from '../types'; interface FindPreviousThresholdSignalsParams { from: string; @@ -52,6 +51,14 @@ export const findPreviousThresholdSignals = async ({ 'signal.rule.rule_id': ruleId, }, }, + // We might find a signal that was generated on the interval for old data... make sure to exclude those. + { + range: { + 'signal.original_time': { + gte: from, + }, + }, + }, ...bucketByFields.map((field) => { return { term: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.test.ts similarity index 97% rename from x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.test.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.test.ts index f8d88100d77a0..622e77309765f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.test.ts @@ -5,12 +5,12 @@ * 2.0. */ -import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; -import { getQueryFilter } from '../../../../common/detection_engine/get_query_filter'; -import { mockLogger } from './__mocks__/es_results'; +import { alertsMock, AlertServicesMock } from '../../../../../../alerting/server/mocks'; +import { getQueryFilter } from '../../../../../common/detection_engine/get_query_filter'; +import { mockLogger } from '../__mocks__/es_results'; +import { buildRuleMessageFactory } from '../rule_messages'; +import * as single_search_after from '../single_search_after'; import { findThresholdSignals } from './find_threshold_signals'; -import { buildRuleMessageFactory } from './rule_messages'; -import * as single_search_after from './single_search_after'; const buildRuleMessage = buildRuleMessageFactory({ id: 'fake id', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.ts similarity index 51% rename from x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.ts index 0a6057ce27c20..7dda263dd9f0a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.ts @@ -10,18 +10,17 @@ import { set } from '@elastic/safer-lodash-set'; import { Threshold, TimestampOverrideOrUndefined, -} from '../../../../common/detection_engine/schemas/common/schemas'; -import { normalizeThresholdField } from '../../../../common/detection_engine/utils'; -import { singleSearchAfter } from './single_search_after'; - +} from '../../../../../common/detection_engine/schemas/common/schemas'; +import { normalizeThresholdField } from '../../../../../common/detection_engine/utils'; import { AlertInstanceContext, AlertInstanceState, AlertServices, -} from '../../../../../alerting/server'; -import { Logger } from '../../../../../../../src/core/server'; -import { SignalSearchResponse } from './types'; -import { BuildRuleMessage } from './rule_messages'; +} from '../../../../../../alerting/server'; +import { Logger } from '../../../../../../../../src/core/server'; +import { BuildRuleMessage } from '../rule_messages'; +import { singleSearchAfter } from '../single_search_after'; +import { SignalSearchResponse } from '../types'; interface FindThresholdSignalsParams { from: string; @@ -50,27 +49,52 @@ export const findThresholdSignals = async ({ searchDuration: string; searchErrors: string[]; }> => { - const topHitsAgg = { - top_hits: { - sort: [ - { - [timestampOverride ?? '@timestamp']: { - order: 'desc', + // Leaf aggregations used below + const leafAggs = { + top_threshold_hits: { + top_hits: { + sort: [ + { + [timestampOverride ?? '@timestamp']: { + order: 'desc', + }, }, - }, - ], - fields: [ - { - field: '*', - include_unmapped: true, - }, - ], - size: 1, + ], + fields: [ + { + field: '*', + include_unmapped: true, + }, + ], + size: 1, + }, }, + ...(threshold.cardinality?.length + ? { + cardinality_count: { + cardinality: { + field: threshold.cardinality[0].field, + }, + }, + cardinality_check: { + bucket_selector: { + buckets_path: { + cardinalityCount: 'cardinality_count', + }, + script: `params.cardinalityCount >= ${threshold.cardinality[0].value}`, // TODO: cardinality operator + }, + }, + } + : {}), }; const thresholdFields = normalizeThresholdField(threshold.field); + // Generate a nested terms aggregation for each threshold grouping field provided, appending leaf + // aggregations to 1) filter out buckets that don't meet the cardinality threshold, if provided, and + // 2) return the latest hit for each bucket so that we can persist the timestamp of the event in the + // `original_time` of the signal. This will be used for dupe mitigation purposes by the detection + // engine. const aggregations = thresholdFields.length ? thresholdFields.reduce((acc, field, i) => { const aggPath = [...Array(i + 1).keys()] @@ -86,60 +110,21 @@ export const findThresholdSignals = async ({ }, }); if (i === (thresholdFields.length ?? 0) - 1) { - if (threshold.cardinality?.length) { - set(acc, `${aggPath}['aggs']`, { - top_threshold_hits: topHitsAgg, - cardinality_count: { - cardinality: { - field: threshold.cardinality[0].field, - }, - }, - cardinality_check: { - bucket_selector: { - buckets_path: { - cardinalityCount: 'cardinality_count', - }, - script: `params.cardinalityCount >= ${threshold.cardinality[0].value}`, // TODO: cardinality operator - }, - }, - }); - } else { - set(acc, `${aggPath}['aggs']`, { - top_threshold_hits: topHitsAgg, - }); - } + set(acc, `${aggPath}['aggs']`, leafAggs); } return acc; }, {}) : { + // No threshold grouping fields provided threshold_0: { terms: { script: { - source: '""', + source: '""', // Group everything in the same bucket lang: 'painless', }, min_doc_count: threshold.value, }, - aggs: { - top_threshold_hits: topHitsAgg, - ...(threshold.cardinality?.length - ? { - cardinality_count: { - cardinality: { - field: threshold.cardinality[0].field, - }, - }, - cardinality_check: { - bucket_selector: { - buckets_path: { - cardinalityCount: 'cardinality_count', - }, - script: `params.cardinalityCount >= ${threshold.cardinality[0].value}`, // TODO: cardinality operator - }, - }, - } - : {}), - }, + aggs: leafAggs, }, }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.test.ts new file mode 100644 index 0000000000000..d621868a0956c --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { sampleThresholdSignalHistory } from '../__mocks__/threshold_signal_history.mock'; +import { getThresholdBucketFilters } from './get_threshold_bucket_filters'; + +describe('getThresholdBucketFilters', () => { + it('should generate filters for threshold signal detection with dupe mitigation', async () => { + const result = await getThresholdBucketFilters({ + thresholdSignalHistory: sampleThresholdSignalHistory(), + timestampOverride: undefined, + }); + expect(result).toEqual([ + { + bool: { + must_not: [ + { + bool: { + filter: [ + { + range: { + '@timestamp': { + lte: '2020-12-17T16:28:00.000Z', + }, + }, + }, + { + term: { + 'host.name': 'garden-gnomes', + }, + }, + { + term: { + 'source.ip': '127.0.0.1', + }, + }, + ], + }, + }, + ], + }, + }, + ]); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.ts new file mode 100644 index 0000000000000..208727944765c --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Filter } from 'src/plugins/data/common'; +import { ESFilter } from '../../../../../../../typings/elasticsearch'; +import { ThresholdSignalHistory, ThresholdSignalHistoryRecord } from '../types'; + +export const getThresholdBucketFilters = async ({ + thresholdSignalHistory, + timestampOverride, +}: { + thresholdSignalHistory: ThresholdSignalHistory; + timestampOverride: string | undefined; +}): Promise => { + const filters = Object.values(thresholdSignalHistory).reduce( + (acc: ESFilter[], bucket: ThresholdSignalHistoryRecord): ESFilter[] => { + const filter = { + bool: { + filter: [ + { + range: { + [timestampOverride ?? '@timestamp']: { + lte: new Date(bucket.lastSignalTimestamp).toISOString(), + }, + }, + }, + ], + }, + } as ESFilter; + + bucket.terms.forEach((term) => { + if (term.field != null) { + (filter.bool.filter as ESFilter[]).push({ + term: { + [term.field]: `${term.value}`, + }, + }); + } + }); + + return [...acc, filter]; + }, + [] as ESFilter[] + ); + + return [ + ({ + bool: { + must_not: filters, + }, + } as unknown) as Filter, + ]; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_signal_history.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_signal_history.ts new file mode 100644 index 0000000000000..b93d8423c0259 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_signal_history.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema'; +import { TimestampOverrideOrUndefined } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { + AlertInstanceContext, + AlertInstanceState, + AlertServices, +} from '../../../../../../alerting/server'; +import { Logger } from '../../../../../../../../src/core/server'; +import { ThresholdSignalHistory } from '../types'; +import { BuildRuleMessage } from '../rule_messages'; +import { findPreviousThresholdSignals } from './find_previous_threshold_signals'; +import { getThresholdTermsHash } from '../utils'; + +interface GetThresholdSignalHistoryParams { + from: string; + to: string; + indexPattern: string[]; + services: AlertServices; + logger: Logger; + ruleId: string; + bucketByFields: string[]; + timestampOverride: TimestampOverrideOrUndefined; + buildRuleMessage: BuildRuleMessage; +} + +export const getThresholdSignalHistory = async ({ + from, + to, + indexPattern, + services, + logger, + ruleId, + bucketByFields, + timestampOverride, + buildRuleMessage, +}: GetThresholdSignalHistoryParams): Promise<{ + thresholdSignalHistory: ThresholdSignalHistory; + searchErrors: string[]; +}> => { + const { searchResult, searchErrors } = await findPreviousThresholdSignals({ + indexPattern, + from, + to, + services, + logger, + ruleId, + bucketByFields, + timestampOverride, + buildRuleMessage, + }); + + const thresholdSignalHistory = searchResult.hits.hits.reduce( + (acc, hit) => { + if (!hit._source) { + return acc; + } + + const terms = + hit._source.signal?.threshold_result?.terms != null + ? hit._source.signal.threshold_result.terms + : [ + // Pre-7.12 signals + { + field: + (((hit._source.signal?.rule as RulesSchema).threshold as unknown) as { + field: string; + }).field ?? '', + value: ((hit._source.signal?.threshold_result as unknown) as { value: string }) + .value, + }, + ]; + + const hash = getThresholdTermsHash(terms); + const existing = acc[hash]; + const originalTime = + hit._source.signal?.original_time != null + ? new Date(hit._source.signal?.original_time).getTime() + : undefined; + + if (existing != null) { + if (originalTime && originalTime > existing.lastSignalTimestamp) { + acc[hash].lastSignalTimestamp = originalTime; + } + } else if (originalTime) { + acc[hash] = { + terms, + lastSignalTimestamp: originalTime, + }; + } + return acc; + }, + {} + ); + + return { + thresholdSignalHistory, + searchErrors, + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/index.ts new file mode 100644 index 0000000000000..8dde6bf2df877 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './bulk_create_threshold_signals'; +export * from './get_threshold_bucket_filters'; +export * from './get_threshold_signal_history'; +export * from './find_threshold_signals'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_get_bucket_filters.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_get_bucket_filters.test.ts deleted file mode 100644 index e1b048cd2fd1c..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_get_bucket_filters.test.ts +++ /dev/null @@ -1,216 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; -import { - mockLogger, - sampleWrappedThresholdSignalHit, - sampleWrappedLegacyThresholdSignalHit, -} from './__mocks__/es_results'; -import { getThresholdBucketFilters } from './threshold_get_bucket_filters'; -import { buildRuleMessageFactory } from './rule_messages'; - -const buildRuleMessage = buildRuleMessageFactory({ - id: 'fake id', - ruleId: 'fake rule id', - index: 'fakeindex', - name: 'fake name', -}); - -describe('thresholdGetBucketFilters', () => { - let mockService: AlertServicesMock; - - beforeEach(() => { - jest.clearAllMocks(); - mockService = alertsMock.createAlertServices(); - }); - - it('should generate filters for threshold signal detection with dupe mitigation', async () => { - mockService.callCluster.mockResolvedValue({ - took: 10, - timed_out: false, - _shards: { - total: 10, - successful: 10, - failed: 0, - skipped: 0, - }, - hits: { - total: 1, - max_score: 100, - hits: [sampleWrappedThresholdSignalHit()], - }, - }); - const result = await getThresholdBucketFilters({ - from: 'now-6m', - to: 'now', - indexPattern: ['*'], - services: mockService, - logger: mockLogger, - ruleId: '7a7065d7-6e8b-4aae-8d20-c93613dec9f9', - bucketByFields: ['host.name'], - timestampOverride: undefined, - buildRuleMessage, - }); - expect(result).toEqual({ - filters: [ - { - bool: { - must_not: [ - { - bool: { - filter: [ - { - range: { - '@timestamp': { - lte: '2021-02-16T17:37:34.275Z', - }, - }, - }, - { - term: { - 'host.name': 'a hostname', - }, - }, - ], - }, - }, - ], - }, - }, - ], - searchErrors: [], - }); - }); - - it('should generate filters for threshold signal detection based on pre-7.12 signals', async () => { - mockService.callCluster.mockResolvedValue({ - took: 10, - timed_out: false, - _shards: { - total: 10, - successful: 10, - failed: 0, - skipped: 0, - }, - hits: { - total: 1, - max_score: 100, - hits: [sampleWrappedLegacyThresholdSignalHit()], - }, - }); - const result = await getThresholdBucketFilters({ - from: 'now-6m', - to: 'now', - indexPattern: ['*'], - services: mockService, - logger: mockLogger, - ruleId: '7a7065d7-6e8b-4aae-8d20-c93613dec9f9', - bucketByFields: ['host.name'], - timestampOverride: undefined, - buildRuleMessage, - }); - expect(result).toEqual({ - filters: [ - { - bool: { - must_not: [ - { - bool: { - filter: [ - { - range: { - '@timestamp': { - lte: '2021-02-16T17:37:34.275Z', - }, - }, - }, - { - term: { - 'host.name': 'a hostname', - }, - }, - ], - }, - }, - ], - }, - }, - ], - searchErrors: [], - }); - }); - - it('should generate filters for threshold signal detection with mixed pre-7.12 and post-7.12 signals', async () => { - const signalHit = sampleWrappedThresholdSignalHit(); - const wrappedSignalHit = { - ...signalHit, - _source: { - ...signalHit._source, - signal: { - ...signalHit._source.signal, - original_time: '2021-02-16T18:37:34.275Z', - }, - }, - }; - mockService.callCluster.mockResolvedValue({ - took: 10, - timed_out: false, - _shards: { - total: 10, - successful: 10, - failed: 0, - skipped: 0, - }, - hits: { - total: 1, - max_score: 100, - hits: [sampleWrappedLegacyThresholdSignalHit(), wrappedSignalHit], - }, - }); - const result = await getThresholdBucketFilters({ - from: 'now-6m', - to: 'now', - indexPattern: ['*'], - services: mockService, - logger: mockLogger, - ruleId: '7a7065d7-6e8b-4aae-8d20-c93613dec9f9', - bucketByFields: ['host.name'], - timestampOverride: undefined, - buildRuleMessage, - }); - expect(result).toEqual({ - filters: [ - { - bool: { - must_not: [ - { - bool: { - filter: [ - { - range: { - '@timestamp': { - lte: '2021-02-16T18:37:34.275Z', - }, - }, - }, - { - term: { - 'host.name': 'a hostname', - }, - }, - ], - }, - }, - ], - }, - }, - ], - searchErrors: [], - }); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_get_bucket_filters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_get_bucket_filters.ts deleted file mode 100644 index 0b1bd70c0df19..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold_get_bucket_filters.ts +++ /dev/null @@ -1,173 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import crypto from 'crypto'; -import { isEmpty } from 'lodash'; - -import { Filter } from 'src/plugins/data/common'; -import { ESFilter } from '../../../../../../typings/elasticsearch'; - -import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; -import { TimestampOverrideOrUndefined } from '../../../../common/detection_engine/schemas/common/schemas'; -import { - AlertInstanceContext, - AlertInstanceState, - AlertServices, -} from '../../../../../alerting/server'; -import { Logger } from '../../../../../../../src/core/server'; -import { ThresholdSignalHistory, ThresholdSignalHistoryRecord } from './types'; -import { BuildRuleMessage } from './rule_messages'; -import { findPreviousThresholdSignals } from './threshold_find_previous_signals'; - -interface GetThresholdBucketFiltersParams { - from: string; - to: string; - indexPattern: string[]; - services: AlertServices; - logger: Logger; - ruleId: string; - bucketByFields: string[]; - timestampOverride: TimestampOverrideOrUndefined; - buildRuleMessage: BuildRuleMessage; -} - -export const getThresholdBucketFilters = async ({ - from, - to, - indexPattern, - services, - logger, - ruleId, - bucketByFields, - timestampOverride, - buildRuleMessage, -}: GetThresholdBucketFiltersParams): Promise<{ - filters: Filter[]; - searchErrors: string[]; -}> => { - const { searchResult, searchErrors } = await findPreviousThresholdSignals({ - indexPattern, - from, - to, - services, - logger, - ruleId, - bucketByFields, - timestampOverride, - buildRuleMessage, - }); - - const thresholdSignalHistory = searchResult.hits.hits.reduce( - (acc, hit) => { - if (!hit._source) { - return acc; - } - - const terms = bucketByFields.map((field) => { - let signalTerms = hit._source.signal?.threshold_result?.terms; - - // Handle pre-7.12 signals - if (signalTerms == null) { - signalTerms = [ - { - field: (((hit._source.signal?.rule as RulesSchema).threshold as unknown) as { - field: string; - }).field, - value: ((hit._source.signal?.threshold_result as unknown) as { value: string }).value, - }, - ]; - } else if (isEmpty(signalTerms)) { - signalTerms = []; - } - - const result = signalTerms.filter((resultField) => { - return resultField.field === field; - }); - - return { - field, - value: result[0].value, - }; - }); - - const hash = crypto - .createHash('sha256') - .update( - terms - .sort((term1, term2) => (term1.field > term2.field ? 1 : -1)) - .map((field) => { - return field.value; - }) - .join(',') - ) - .digest('hex'); - - const existing = acc[hash]; - const originalTime = - hit._source.signal?.original_time != null - ? new Date(hit._source.signal?.original_time).getTime() - : undefined; - - if (existing != null) { - if (originalTime && originalTime > existing.lastSignalTimestamp) { - acc[hash].lastSignalTimestamp = originalTime; - } - } else if (originalTime) { - acc[hash] = { - terms, - lastSignalTimestamp: originalTime, - }; - } - return acc; - }, - {} - ); - - const filters = Object.values(thresholdSignalHistory).reduce( - (acc: ESFilter[], bucket: ThresholdSignalHistoryRecord): ESFilter[] => { - const filter = { - bool: { - filter: [ - { - range: { - [timestampOverride ?? '@timestamp']: { - lte: new Date(bucket.lastSignalTimestamp).toISOString(), - }, - }, - }, - ], - }, - } as ESFilter; - - if (!isEmpty(bucketByFields)) { - bucket.terms.forEach((term) => { - if (term.field != null) { - (filter.bool.filter as ESFilter[]).push({ - term: { - [term.field]: `${term.value}`, - }, - }); - } - }); - } - - return [...acc, filter]; - }, - [] as ESFilter[] - ); - - return { - filters: [ - ({ - bool: { - must_not: filters, - }, - } as unknown) as Filter, - ], - searchErrors, - }; -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index 84ac66ea41e05..013d4a07cbeb7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -51,7 +51,7 @@ export interface SignalsStatusParams { export interface ThresholdResult { terms?: Array<{ - field?: string; + field: string; value: string; }>; cardinality?: Array<{ @@ -59,6 +59,7 @@ export interface ThresholdResult { value: number; }>; count: number; + from: string; } export interface ThresholdSignalHistoryRecord { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts index 1c70b8f288103..3a1dde3f5c28c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts @@ -662,8 +662,8 @@ describe('utils', () => { return; } expect(moment(item.to).diff(moment(item.from), 's')).toEqual(13); - expect(item.to.diff(tuples[index - 1].to, 's')).toEqual(-10); - expect(item.from.diff(tuples[index - 1].from, 's')).toEqual(-10); + expect(item.to.diff(tuples[index - 1].to, 's')).toEqual(10); + expect(item.from.diff(tuples[index - 1].from, 's')).toEqual(10); }); expect(remainingGap.asMilliseconds()).toEqual(12000); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index 57973754c969e..2d98f4f6a8a69 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -475,7 +475,7 @@ export const getRuleRangeTuples = ({ gap.asMilliseconds() - catchup * intervalDuration.asMilliseconds(), 0 ); - return { tuples, remainingGap: moment.duration(remainingGapMilliseconds) }; + return { tuples: tuples.reverse(), remainingGap: moment.duration(remainingGapMilliseconds) }; }; /** @@ -793,3 +793,21 @@ export const getThresholdAggregationParts = ( } } }; + +export const getThresholdTermsHash = ( + terms: Array<{ + field: string; + value: string; + }> +): string => { + return createHash('sha256') + .update( + terms + .sort((term1, term2) => (term1.field > term2.field ? 1 : -1)) + .map((field) => { + return field.value; + }) + .join(',') + ) + .digest('hex'); +}; diff --git a/x-pack/plugins/spaces/.eslintrc.json b/x-pack/plugins/spaces/.eslintrc.json deleted file mode 100644 index 2b63a9259d220..0000000000000 --- a/x-pack/plugins/spaces/.eslintrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "rules": { - "@typescript-eslint/consistent-type-imports": 1 - } -} diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_internal.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_internal.tsx index 9dbeb70d46cfb..8f219b7154def 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_internal.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_internal.tsx @@ -28,11 +28,11 @@ import type { ProcessedImportResponse } from 'src/plugins/saved_objects_manageme import type { Space } from 'src/plugins/spaces_oss/common'; import { processImportResponse } from '../../../../../../src/plugins/saved_objects_management/public'; +import { useSpaces } from '../../spaces_context'; import type { CopyOptions, ImportRetry, SavedObjectTarget } from '../types'; import { CopyToSpaceFlyoutFooter } from './copy_to_space_flyout_footer'; import { CopyToSpaceForm } from './copy_to_space_form'; import { ProcessingCopyToSpace } from './processing_copy_to_space'; -import { useSpaces } from '../../spaces_context'; export interface CopyToSpaceFlyoutProps { onClose: () => void; diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/summarize_copy_result.test.ts b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/summarize_copy_result.test.ts index 43435b322d6b0..5a0ad95b6bead 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/summarize_copy_result.test.ts +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/summarize_copy_result.test.ts @@ -11,8 +11,8 @@ import type { SavedObjectsManagementRecord, } from 'src/plugins/saved_objects_management/public'; -import { summarizeCopyResult } from './summarize_copy_result'; import type { SavedObjectTarget } from '../types'; +import { summarizeCopyResult } from './summarize_copy_result'; // Sample data references: // diff --git a/x-pack/plugins/spaces/public/create_feature_catalogue_entry.ts b/x-pack/plugins/spaces/public/create_feature_catalogue_entry.ts index acaa361bca9fa..86b2f0d3ac7fe 100644 --- a/x-pack/plugins/spaces/public/create_feature_catalogue_entry.ts +++ b/x-pack/plugins/spaces/public/create_feature_catalogue_entry.ts @@ -6,7 +6,6 @@ */ import { i18n } from '@kbn/i18n'; - import type { FeatureCatalogueEntry } from 'src/plugins/home/public'; import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public'; diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_action.tsx b/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_action.tsx index 4e07f6799e9c6..657ff8ac0bc70 100644 --- a/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_action.tsx +++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_action.tsx @@ -9,7 +9,7 @@ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import type { SavedObjectsManagementRecord } from 'src/plugins/saved_objects_management/public'; -import type { SpacesApiUi, ShareToSpaceFlyoutProps } from 'src/plugins/spaces_oss/public'; +import type { ShareToSpaceFlyoutProps, SpacesApiUi } from 'src/plugins/spaces_oss/public'; import { SavedObjectsManagementAction } from '../../../../../src/plugins/saved_objects_management/public'; diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_column.tsx b/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_column.tsx index c9909af16e387..609811cd6b7ce 100644 --- a/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_column.tsx +++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_column.tsx @@ -9,7 +9,7 @@ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import type { SavedObjectsManagementColumn } from 'src/plugins/saved_objects_management/public'; -import type { SpacesApiUi, SpaceListProps } from 'src/plugins/spaces_oss/public'; +import type { SpaceListProps, SpacesApiUi } from 'src/plugins/spaces_oss/public'; interface WrapperProps { spacesApiUi: SpacesApiUi; diff --git a/x-pack/plugins/spaces/public/spaces_context/wrapper_internal.tsx b/x-pack/plugins/spaces/public/spaces_context/wrapper_internal.tsx index 7c33f859a8c3e..dd6408e9550ee 100644 --- a/x-pack/plugins/spaces/public/spaces_context/wrapper_internal.tsx +++ b/x-pack/plugins/spaces/public/spaces_context/wrapper_internal.tsx @@ -14,7 +14,7 @@ import type { SpacesContextProps } from 'src/plugins/spaces_oss/public'; import type { SpacesManager } from '../spaces_manager'; import type { ShareToSpacesData, ShareToSpaceTarget } from '../types'; import { createSpacesReactContext } from './context'; -import type { SpacesReactContext, InternalProps } from './types'; +import type { InternalProps, SpacesReactContext } from './types'; interface Services { application: ApplicationStart; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx index 8bb546bbc301b..09487f1ebe936 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx @@ -270,7 +270,7 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent<
@@ -281,7 +281,7 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent< label={ } isInvalid={errors.esQuery.length > 0} @@ -290,7 +290,7 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent< } @@ -302,7 +302,7 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent< theme="github" data-test-subj="queryJsonEditor" aria-label={i18n.translate('xpack.stackAlerts.esQuery.ui.queryEditor', { - defaultMessage: 'ES query editor', + defaultMessage: 'Elasticsearch query editor', })} value={xJson} onChange={(xjson: string) => { diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/index.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/index.ts index 6d42ee714a222..43f36c3ea44db 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/index.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/index.ts @@ -15,7 +15,7 @@ export function getAlertType(): AlertTypeModel { return { id: '.es-query', description: i18n.translate('xpack.stackAlerts.esQuery.ui.alertType.descriptionText', { - defaultMessage: 'Alert on matches against an ES query.', + defaultMessage: 'Alert on matches against an Elasticsearch query.', }), iconClass: 'logoElastic', documentationUrl: (docLinks) => docLinks.links.alerting.esQuery, @@ -24,7 +24,7 @@ export function getAlertType(): AlertTypeModel { defaultActionMessage: i18n.translate( 'xpack.stackAlerts.esQuery.ui.alertType.defaultActionMessage', { - defaultMessage: `ES query alert '\\{\\{alertName\\}\\}' is active: + defaultMessage: `Elasticsearch query alert '\\{\\{alertName\\}\\}' is active: - Value: \\{\\{context.value\\}\\} - Conditions Met: \\{\\{context.conditions\\}\\} over \\{\\{params.timeWindowSize\\}\\}\\{\\{params.timeWindowUnit\\}\\} diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.ts index e6449dd4a6089..a392563323aa7 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.ts @@ -48,7 +48,7 @@ export const validateExpression = (alertParams: EsQueryAlertParams): ValidationR if (!esQuery) { errors.esQuery.push( i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredQueryText', { - defaultMessage: 'ES query is required.', + defaultMessage: 'Elasticsearch query is required.', }) ); } else { diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts index c38dad5134373..d844651d0b8aa 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts @@ -17,7 +17,7 @@ describe('alertType', () => { it('alert type creation structure is the expected value', async () => { expect(alertType.id).toBe('.es-query'); - expect(alertType.name).toBe('ES query'); + expect(alertType.name).toBe('Elasticsearch query'); expect(alertType.actionGroups).toEqual([{ id: 'query matched', name: 'Query matched' }]); expect(alertType.actionVariables).toMatchInlineSnapshot(` @@ -54,7 +54,7 @@ describe('alertType', () => { "name": "index", }, Object { - "description": "The string representation of the ES query.", + "description": "The string representation of the Elasticsearch query.", "name": "esQuery", }, Object { diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts index e4854b85d178b..bad25a0d1d09c 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts @@ -30,7 +30,7 @@ export function getAlertType( logger: Logger ): AlertType { const alertTypeName = i18n.translate('xpack.stackAlerts.esQuery.alertTypeTitle', { - defaultMessage: 'ES query', + defaultMessage: 'Elasticsearch query', }); const actionGroupName = i18n.translate('xpack.stackAlerts.esQuery.actionGroupThresholdMetTitle', { @@ -82,7 +82,7 @@ export function getAlertType( const actionVariableContextQueryLabel = i18n.translate( 'xpack.stackAlerts.esQuery.actionVariableContextQueryLabel', { - defaultMessage: 'The string representation of the ES query.', + defaultMessage: 'The string representation of the Elasticsearch query.', } ); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/README.md b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/README.md index de5b57dfbffc6..4aebb51a85a89 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/README.md +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/README.md @@ -2,7 +2,7 @@ directory in plugin: `server/alert_types/index_threshold` -The index threshold alert type is designed to run an ES query over indices, +The index threshold alert type is designed to run an Elasticsearch query over indices, aggregating field values from documents, comparing them to threshold values, and scheduling actions to run when the thresholds are met. @@ -91,7 +91,7 @@ scheduled against them independently). The time window is set to 5 seconds. That means, every time the alert runs it's queries (every second, in the example above), it will run it's -ES query over the last 5 seconds. Thus, the queries, over time, will overlap. +Elasticsearch query over the last 5 seconds. Thus, the queries, over time, will overlap. Sometimes that's what you want. Other times, maybe you just want to do sampling, running an alert every hour, with a 5 minute window. Up to the you! diff --git a/x-pack/plugins/stack_alerts/server/plugin.test.ts b/x-pack/plugins/stack_alerts/server/plugin.test.ts index fdb320ad394aa..3560a849e1ec5 100644 --- a/x-pack/plugins/stack_alerts/server/plugin.test.ts +++ b/x-pack/plugins/stack_alerts/server/plugin.test.ts @@ -83,7 +83,7 @@ describe('AlertingBuiltins Plugin', () => { }, ], "id": ".es-query", - "name": "ES query", + "name": "Elasticsearch query", } `); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/solution_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/solution_filter.tsx index 194d511f51945..f49d1ef49f6ed 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/solution_filter.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/solution_filter.tsx @@ -44,7 +44,7 @@ export const SolutionFilter: React.FunctionComponent = ({ > } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/email.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/email.ts index a15bd0a268df5..518bfcef9e0d3 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/email.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/email.ts @@ -18,11 +18,11 @@ export default function emailTest({ getService }: FtrProviderContext) { it('should return 200 when creating an email action successfully', async () => { const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'An email action', - actionTypeId: '.email', + connector_type_id: '.email', config: { service: '__json', from: 'bob@example.com', @@ -38,9 +38,9 @@ export default function emailTest({ getService }: FtrProviderContext) { createdActionId = createdAction.id; expect(createdAction).to.eql({ id: createdActionId, - isPreconfigured: false, + is_preconfigured: false, name: 'An email action', - actionTypeId: '.email', + connector_type_id: '.email', config: { service: '__json', hasAuth: true, @@ -54,14 +54,14 @@ export default function emailTest({ getService }: FtrProviderContext) { expect(typeof createdActionId).to.be('string'); const { body: fetchedAction } = await supertest - .get(`/api/actions/action/${createdActionId}`) + .get(`/api/actions/connector/${createdActionId}`) .expect(200); expect(fetchedAction).to.eql({ id: fetchedAction.id, - isPreconfigured: false, + is_preconfigured: false, name: 'An email action', - actionTypeId: '.email', + connector_type_id: '.email', config: { from: 'bob@example.com', service: '__json', @@ -75,7 +75,7 @@ export default function emailTest({ getService }: FtrProviderContext) { it('should return the message data when firing the __json service', async () => { await supertest - .post(`/api/actions/action/${createdActionId}/_execute`) + .post(`/api/actions/connector/${createdActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -119,7 +119,7 @@ export default function emailTest({ getService }: FtrProviderContext) { it('should render html from markdown', async () => { await supertest - .post(`/api/actions/action/${createdActionId}/_execute`) + .post(`/api/actions/connector/${createdActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -142,7 +142,7 @@ export default function emailTest({ getService }: FtrProviderContext) { it('should allow customizing the kibana footer link', async () => { await supertest - .post(`/api/actions/action/${createdActionId}/_execute`) + .post(`/api/actions/connector/${createdActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -169,11 +169,11 @@ export default function emailTest({ getService }: FtrProviderContext) { it('should respond with a 400 Bad Request when creating an email action with an invalid config', async () => { await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'An email action', - actionTypeId: '.email', + connector_type_id: '.email', config: {}, }) .expect(400) @@ -189,11 +189,11 @@ export default function emailTest({ getService }: FtrProviderContext) { it('should respond with a 400 Bad Request when creating an email action with a server not added to allowedHosts', async () => { await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'An email action', - actionTypeId: '.email', + connector_type_id: '.email', config: { service: 'gmail', // not added to allowedHosts in the config for this test from: 'bob@example.com', @@ -214,11 +214,11 @@ export default function emailTest({ getService }: FtrProviderContext) { }); await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'An email action', - actionTypeId: '.email', + connector_type_id: '.email', config: { host: 'stmp.gmail.com', // not added to allowedHosts in the config for this test port: 666, @@ -242,11 +242,11 @@ export default function emailTest({ getService }: FtrProviderContext) { it('should handle creating an email action with a server added to allowedHosts', async () => { const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'An email action', - actionTypeId: '.email', + connector_type_id: '.email', config: { host: 'some.non.existent.com', // added to allowedHosts in the config for this test port: 666, @@ -263,11 +263,11 @@ export default function emailTest({ getService }: FtrProviderContext) { it('should handle an email action with no auth', async () => { const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'An email action with no auth', - actionTypeId: '.email', + connector_type_id: '.email', config: { service: '__json', from: 'jim@example.com', @@ -276,7 +276,7 @@ export default function emailTest({ getService }: FtrProviderContext) { .expect(200); await supertest - .post(`/api/actions/action/${createdAction.id}/_execute`) + .post(`/api/actions/connector/${createdAction.id}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts index dfc3de5609a18..432598e66bcc4 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts @@ -26,11 +26,11 @@ export default function indexTest({ getService }: FtrProviderContext) { it('should be created successfully', async () => { // create action with no config const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'An index action', - actionTypeId: '.index', + connector_type_id: '.index', config: { index: ES_TEST_INDEX_NAME, }, @@ -40,9 +40,9 @@ export default function indexTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdAction.id, - isPreconfigured: false, + is_preconfigured: false, name: 'An index action', - actionTypeId: '.index', + connector_type_id: '.index', config: { index: ES_TEST_INDEX_NAME, refresh: false, @@ -53,24 +53,24 @@ export default function indexTest({ getService }: FtrProviderContext) { expect(typeof createdActionID).to.be('string'); const { body: fetchedAction } = await supertest - .get(`/api/actions/action/${createdActionID}`) + .get(`/api/actions/connector/${createdActionID}`) .expect(200); expect(fetchedAction).to.eql({ id: fetchedAction.id, - isPreconfigured: false, + is_preconfigured: false, name: 'An index action', - actionTypeId: '.index', + connector_type_id: '.index', config: { index: ES_TEST_INDEX_NAME, refresh: false, executionTimeField: null }, }); // create action with all config props const { body: createdActionWithIndex } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'An index action with index config', - actionTypeId: '.index', + connector_type_id: '.index', config: { index: ES_TEST_INDEX_NAME, refresh: true, @@ -81,9 +81,9 @@ export default function indexTest({ getService }: FtrProviderContext) { expect(createdActionWithIndex).to.eql({ id: createdActionWithIndex.id, - isPreconfigured: false, + is_preconfigured: false, name: 'An index action with index config', - actionTypeId: '.index', + connector_type_id: '.index', config: { index: ES_TEST_INDEX_NAME, refresh: true, @@ -94,14 +94,14 @@ export default function indexTest({ getService }: FtrProviderContext) { expect(typeof createdActionIDWithIndex).to.be('string'); const { body: fetchedActionWithIndex } = await supertest - .get(`/api/actions/action/${createdActionIDWithIndex}`) + .get(`/api/actions/connector/${createdActionIDWithIndex}`) .expect(200); expect(fetchedActionWithIndex).to.eql({ id: fetchedActionWithIndex.id, - isPreconfigured: false, + is_preconfigured: false, name: 'An index action with index config', - actionTypeId: '.index', + connector_type_id: '.index', config: { index: ES_TEST_INDEX_NAME, refresh: true, @@ -112,11 +112,11 @@ export default function indexTest({ getService }: FtrProviderContext) { it('should respond with error when creation unsuccessful', async () => { await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'An index action', - actionTypeId: '.index', + connector_type_id: '.index', config: { index: 666 }, }) .expect(400) @@ -132,11 +132,11 @@ export default function indexTest({ getService }: FtrProviderContext) { it('should execute successly when expected for a single body', async () => { const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'An index action', - actionTypeId: '.index', + connector_type_id: '.index', config: { index: ES_TEST_INDEX_NAME, refresh: true, @@ -145,7 +145,7 @@ export default function indexTest({ getService }: FtrProviderContext) { }) .expect(200); const { body: result } = await supertest - .post(`/api/actions/action/${createdAction.id}/_execute`) + .post(`/api/actions/connector/${createdAction.id}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -162,11 +162,11 @@ export default function indexTest({ getService }: FtrProviderContext) { it('should execute successly when expected for with multiple bodies', async () => { const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'An index action', - actionTypeId: '.index', + connector_type_id: '.index', config: { index: ES_TEST_INDEX_NAME, refresh: true, @@ -175,7 +175,7 @@ export default function indexTest({ getService }: FtrProviderContext) { }) .expect(200); const { body: result } = await supertest - .post(`/api/actions/action/${createdAction.id}/_execute`) + .post(`/api/actions/connector/${createdAction.id}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -206,11 +206,11 @@ export default function indexTest({ getService }: FtrProviderContext) { it('should execute successly with refresh false', async () => { const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'An index action', - actionTypeId: '.index', + connector_type_id: '.index', config: { index: ES_TEST_INDEX_NAME, refresh: false, @@ -220,7 +220,7 @@ export default function indexTest({ getService }: FtrProviderContext) { }) .expect(200); const { body: result } = await supertest - .post(`/api/actions/action/${createdAction.id}/_execute`) + .post(`/api/actions/connector/${createdAction.id}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -235,11 +235,11 @@ export default function indexTest({ getService }: FtrProviderContext) { expect(items.length).to.be.lessThan(2); const { body: createdActionWithRefresh } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'An index action', - actionTypeId: '.index', + connector_type_id: '.index', config: { index: ES_TEST_INDEX_NAME, refresh: true, @@ -248,7 +248,7 @@ export default function indexTest({ getService }: FtrProviderContext) { }) .expect(200); const { body: result2 } = await supertest - .post(`/api/actions/action/${createdActionWithRefresh.id}/_execute`) + .post(`/api/actions/connector/${createdActionWithRefresh.id}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index_preconfigured.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index_preconfigured.ts index 69c1e54486d65..48f042473c14e 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index_preconfigured.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index_preconfigured.ts @@ -24,7 +24,7 @@ export default function indexTest({ getService }: FtrProviderContext) { it('should execute successfully when expected for a single body', async () => { const { body: result } = await supertest - .post(`/api/actions/action/${ACTION_ID}/_execute`) + .post(`/api/actions/connector/${ACTION_ID}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts index 8bd0b8a790d40..d2e2b4be9b94c 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts @@ -60,11 +60,11 @@ export default function jiraTest({ getService }: FtrProviderContext) { describe('Jira - Action Creation', () => { it('should return 200 when creating a jira action successfully', async () => { const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A jira action', - actionTypeId: '.jira', + connector_type_id: '.jira', config: { ...mockJira.config, apiUrl: jiraSimulatorURL, @@ -75,9 +75,9 @@ export default function jiraTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdAction.id, - isPreconfigured: false, + is_preconfigured: false, name: 'A jira action', - actionTypeId: '.jira', + connector_type_id: '.jira', config: { apiUrl: jiraSimulatorURL, projectKey: mockJira.config.projectKey, @@ -85,14 +85,14 @@ export default function jiraTest({ getService }: FtrProviderContext) { }); const { body: fetchedAction } = await supertest - .get(`/api/actions/action/${createdAction.id}`) + .get(`/api/actions/connector/${createdAction.id}`) .expect(200); expect(fetchedAction).to.eql({ id: fetchedAction.id, - isPreconfigured: false, + is_preconfigured: false, name: 'A jira action', - actionTypeId: '.jira', + connector_type_id: '.jira', config: { apiUrl: jiraSimulatorURL, projectKey: mockJira.config.projectKey, @@ -102,11 +102,11 @@ export default function jiraTest({ getService }: FtrProviderContext) { it('should respond with a 400 Bad Request when creating a jira action with no apiUrl', async () => { await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A jira action', - actionTypeId: '.jira', + connector_type_id: '.jira', config: { projectKey: 'CK' }, }) .expect(400) @@ -122,11 +122,11 @@ export default function jiraTest({ getService }: FtrProviderContext) { it('should respond with a 400 Bad Request when creating a jira action with no projectKey', async () => { await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A jira action', - actionTypeId: '.jira', + connector_type_id: '.jira', config: { apiUrl: jiraSimulatorURL }, }) .expect(400) @@ -142,11 +142,11 @@ export default function jiraTest({ getService }: FtrProviderContext) { it('should respond with a 400 Bad Request when creating a jira action with a not present in allowedHosts apiUrl', async () => { await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A jira action', - actionTypeId: '.jira', + connector_type_id: '.jira', config: { apiUrl: 'http://jira.mynonexistent.com', projectKey: mockJira.config.projectKey, @@ -166,11 +166,11 @@ export default function jiraTest({ getService }: FtrProviderContext) { it('should respond with a 400 Bad Request when creating a jira action without secrets', async () => { await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A jira action', - actionTypeId: '.jira', + connector_type_id: '.jira', config: { apiUrl: jiraSimulatorURL, projectKey: mockJira.config.projectKey, @@ -195,11 +195,11 @@ export default function jiraTest({ getService }: FtrProviderContext) { before(async () => { const { body } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A jira simulator', - actionTypeId: '.jira', + connector_type_id: '.jira', config: { apiUrl: jiraSimulatorURL, projectKey: mockJira.config.projectKey, @@ -220,14 +220,14 @@ export default function jiraTest({ getService }: FtrProviderContext) { describe('Validation', () => { it('should handle failing with a simulated success without action', async () => { await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) + .post(`/api/actions/connector/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: {}, }) .then((resp: any) => { - expect(Object.keys(resp.body)).to.eql(['status', 'actionId', 'message', 'retry']); - expect(resp.body.actionId).to.eql(simulatedActionId); + expect(Object.keys(resp.body)).to.eql(['status', 'message', 'retry', 'connector_id']); + expect(resp.body.connector_id).to.eql(simulatedActionId); expect(resp.body.status).to.eql('error'); expect(resp.body.retry).to.eql(false); // Node.js 12 oddity: @@ -261,14 +261,14 @@ export default function jiraTest({ getService }: FtrProviderContext) { it('should handle failing with a simulated success without unsupported action', async () => { await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) + .post(`/api/actions/connector/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { subAction: 'non-supported' }, }) .then((resp: any) => { expect(resp.body).to.eql({ - actionId: simulatedActionId, + connector_id: simulatedActionId, status: 'error', retry: false, message: @@ -279,14 +279,14 @@ export default function jiraTest({ getService }: FtrProviderContext) { it('should handle failing with a simulated success without subActionParams', async () => { await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) + .post(`/api/actions/connector/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { subAction: 'pushToService' }, }) .then((resp: any) => { expect(resp.body).to.eql({ - actionId: simulatedActionId, + connector_id: simulatedActionId, status: 'error', retry: false, message: @@ -297,7 +297,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { it('should handle failing with a simulated success without title', async () => { await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) + .post(`/api/actions/connector/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -312,7 +312,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { }) .then((resp: any) => { expect(resp.body).to.eql({ - actionId: simulatedActionId, + connector_id: simulatedActionId, status: 'error', retry: false, message: @@ -323,7 +323,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { it('should handle failing with a simulated success without commentId', async () => { await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) + .post(`/api/actions/connector/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -340,7 +340,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { }) .then((resp: any) => { expect(resp.body).to.eql({ - actionId: simulatedActionId, + connector_id: simulatedActionId, status: 'error', retry: false, message: @@ -351,7 +351,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { it('should handle failing with a simulated success without comment message', async () => { await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) + .post(`/api/actions/connector/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -367,7 +367,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { }) .then((resp: any) => { expect(resp.body).to.eql({ - actionId: simulatedActionId, + connector_id: simulatedActionId, status: 'error', retry: false, message: @@ -378,7 +378,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { it('should handle failing with a simulated success when labels containing a space', async () => { await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) + .post(`/api/actions/connector/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -395,7 +395,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { }) .then((resp: any) => { expect(resp.body).to.eql({ - actionId: simulatedActionId, + connector_id: simulatedActionId, status: 'error', retry: false, message: @@ -408,7 +408,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { describe('Execution', () => { it('should handle creating an incident without comments', async () => { const { body } = await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) + .post(`/api/actions/connector/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -427,7 +427,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { expect(proxyHaveBeenCalled).to.equal(true); expect(body).to.eql({ status: 'ok', - actionId: simulatedActionId, + connector_id: simulatedActionId, data: { id: '123', title: 'CK-1', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts index 405085c4f3ffb..dcdd8e86ec177 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts @@ -45,11 +45,11 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) { it('should return successfully when passed valid create parameters', async () => { const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A pagerduty action', - actionTypeId: '.pagerduty', + connector_type_id: '.pagerduty', config: { apiUrl: pagerdutySimulatorURL, }, @@ -61,9 +61,9 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdAction.id, - isPreconfigured: false, + is_preconfigured: false, name: 'A pagerduty action', - actionTypeId: '.pagerduty', + connector_type_id: '.pagerduty', config: { apiUrl: pagerdutySimulatorURL, }, @@ -72,14 +72,14 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) { expect(typeof createdAction.id).to.be('string'); const { body: fetchedAction } = await supertest - .get(`/api/actions/action/${createdAction.id}`) + .get(`/api/actions/connector/${createdAction.id}`) .expect(200); expect(fetchedAction).to.eql({ id: fetchedAction.id, - isPreconfigured: false, + is_preconfigured: false, name: 'A pagerduty action', - actionTypeId: '.pagerduty', + connector_type_id: '.pagerduty', config: { apiUrl: pagerdutySimulatorURL, }, @@ -88,11 +88,11 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) { it('should return unsuccessfully when passed invalid create parameters', async () => { await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A pagerduty action', - actionTypeId: '.pagerduty', + connector_type_id: '.pagerduty', config: { apiUrl: pagerdutySimulatorURL, }, @@ -111,11 +111,11 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) { it('should return unsuccessfully when default pagerduty url is not present in allowedHosts', async () => { await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A pagerduty action', - actionTypeId: '.pagerduty', + connector_type_id: '.pagerduty', secrets: {}, }) .expect(400) @@ -131,11 +131,11 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) { it('should create pagerduty simulator action successfully', async () => { const { body: createdSimulatedAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A pagerduty simulator', - actionTypeId: '.pagerduty', + connector_type_id: '.pagerduty', config: { apiUrl: pagerdutySimulatorURL, }, @@ -150,7 +150,7 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) { it('should handle executing with a simulated success', async () => { const { body: result } = await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) + .post(`/api/actions/connector/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -162,7 +162,7 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) { expect(proxyHaveBeenCalled).to.equal(true); expect(result).to.eql({ status: 'ok', - actionId: simulatedActionId, + connector_id: simulatedActionId, data: { message: 'Event processed', status: 'success', @@ -172,7 +172,7 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) { it('should handle a 40x pagerduty error', async () => { const { body: result } = await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) + .post(`/api/actions/connector/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -186,7 +186,7 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) { it('should handle a 429 pagerduty error', async () => { const { body: result } = await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) + .post(`/api/actions/connector/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -204,7 +204,7 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) { it('should handle a 500 pagerduty error', async () => { const { body: result } = await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) + .post(`/api/actions/connector/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts index 682714758c0a0..18f082c688907 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts @@ -63,11 +63,11 @@ export default function resilientTest({ getService }: FtrProviderContext) { describe('IBM Resilient - Action Creation', () => { it('should return 200 when creating a ibm resilient action successfully', async () => { const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'An IBM Resilient action', - actionTypeId: '.resilient', + connector_type_id: '.resilient', config: { ...mockResilient.config, apiUrl: resilientSimulatorURL, @@ -78,9 +78,9 @@ export default function resilientTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdAction.id, - isPreconfigured: false, + is_preconfigured: false, name: 'An IBM Resilient action', - actionTypeId: '.resilient', + connector_type_id: '.resilient', config: { apiUrl: resilientSimulatorURL, orgId: mockResilient.config.orgId, @@ -88,14 +88,14 @@ export default function resilientTest({ getService }: FtrProviderContext) { }); const { body: fetchedAction } = await supertest - .get(`/api/actions/action/${createdAction.id}`) + .get(`/api/actions/connector/${createdAction.id}`) .expect(200); expect(fetchedAction).to.eql({ id: fetchedAction.id, - isPreconfigured: false, + is_preconfigured: false, name: 'An IBM Resilient action', - actionTypeId: '.resilient', + connector_type_id: '.resilient', config: { apiUrl: resilientSimulatorURL, orgId: mockResilient.config.orgId, @@ -105,11 +105,11 @@ export default function resilientTest({ getService }: FtrProviderContext) { it('should respond with a 400 Bad Request when creating a ibm resilient action with no apiUrl', async () => { await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'An IBM Resilient', - actionTypeId: '.resilient', + connector_type_id: '.resilient', config: { orgId: '201' }, }) .expect(400) @@ -125,11 +125,11 @@ export default function resilientTest({ getService }: FtrProviderContext) { it('should respond with a 400 Bad Request when creating a ibm resilient action with no orgId', async () => { await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'An IBM Resilient', - actionTypeId: '.resilient', + connector_type_id: '.resilient', config: { apiUrl: resilientSimulatorURL }, }) .expect(400) @@ -145,11 +145,11 @@ export default function resilientTest({ getService }: FtrProviderContext) { it('should respond with a 400 Bad Request when creating a ibm resilient action with a not present in allowedHosts apiUrl', async () => { await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'An IBM Resilient', - actionTypeId: '.resilient', + connector_type_id: '.resilient', config: { apiUrl: 'http://resilient.mynonexistent.com', orgId: mockResilient.config.orgId, @@ -169,11 +169,11 @@ export default function resilientTest({ getService }: FtrProviderContext) { it('should respond with a 400 Bad Request when creating a ibm resilient action without secrets', async () => { await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'An IBM Resilient', - actionTypeId: '.resilient', + connector_type_id: '.resilient', config: { apiUrl: resilientSimulatorURL, orgId: mockResilient.config.orgId, @@ -197,11 +197,11 @@ export default function resilientTest({ getService }: FtrProviderContext) { let proxyHaveBeenCalled = false; before(async () => { const { body } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A ibm resilient simulator', - actionTypeId: '.resilient', + connector_type_id: '.resilient', config: { apiUrl: resilientSimulatorURL, orgId: mockResilient.config.orgId, @@ -222,14 +222,14 @@ export default function resilientTest({ getService }: FtrProviderContext) { describe('Validation', () => { it('should handle failing with a simulated success without action', async () => { await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) + .post(`/api/actions/connector/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: {}, }) .then((resp: any) => { - expect(Object.keys(resp.body)).to.eql(['status', 'actionId', 'message', 'retry']); - expect(resp.body.actionId).to.eql(simulatedActionId); + expect(Object.keys(resp.body)).to.eql(['status', 'message', 'retry', 'connector_id']); + expect(resp.body.connector_id).to.eql(simulatedActionId); expect(resp.body.status).to.eql('error'); expect(resp.body.retry).to.eql(false); // Node.js 12 oddity: @@ -263,14 +263,14 @@ export default function resilientTest({ getService }: FtrProviderContext) { it('should handle failing with a simulated success without unsupported action', async () => { await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) + .post(`/api/actions/connector/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { subAction: 'non-supported' }, }) .then((resp: any) => { expect(resp.body).to.eql({ - actionId: simulatedActionId, + connector_id: simulatedActionId, status: 'error', retry: false, message: @@ -281,14 +281,14 @@ export default function resilientTest({ getService }: FtrProviderContext) { it('should handle failing with a simulated success without subActionParams', async () => { await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) + .post(`/api/actions/connector/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { subAction: 'pushToService' }, }) .then((resp: any) => { expect(resp.body).to.eql({ - actionId: simulatedActionId, + connector_id: simulatedActionId, status: 'error', retry: false, message: @@ -299,7 +299,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { it('should handle failing with a simulated success without title', async () => { await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) + .post(`/api/actions/connector/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -314,7 +314,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { }) .then((resp: any) => { expect(resp.body).to.eql({ - actionId: simulatedActionId, + connector_id: simulatedActionId, status: 'error', retry: false, message: @@ -325,7 +325,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { it('should handle failing with a simulated success without commentId', async () => { await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) + .post(`/api/actions/connector/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -341,7 +341,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { }) .then((resp: any) => { expect(resp.body).to.eql({ - actionId: simulatedActionId, + connector_id: simulatedActionId, status: 'error', retry: false, message: @@ -352,7 +352,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { it('should handle failing with a simulated success without comment message', async () => { await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) + .post(`/api/actions/connector/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -368,7 +368,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { }) .then((resp: any) => { expect(resp.body).to.eql({ - actionId: simulatedActionId, + connector_id: simulatedActionId, status: 'error', retry: false, message: @@ -381,7 +381,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { describe('Execution', () => { it('should handle creating an incident without comments', async () => { const { body } = await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) + .post(`/api/actions/connector/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -397,7 +397,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { expect(proxyHaveBeenCalled).to.equal(true); expect(body).to.eql({ status: 'ok', - actionId: simulatedActionId, + connector_id: simulatedActionId, data: { id: '123', title: '123', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/server_log.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/server_log.ts index 5357c94d7e0f2..3e8b11ee62162 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/server_log.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/server_log.ts @@ -18,41 +18,41 @@ export default function serverLogTest({ getService }: FtrProviderContext) { it('should return 200 when creating a builtin server-log action', async () => { const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A server.log action', - actionTypeId: '.server-log', + connector_type_id: '.server-log', }) .expect(200); serverLogActionId = createdAction.id; expect(createdAction).to.eql({ id: createdAction.id, - isPreconfigured: false, + is_preconfigured: false, name: 'A server.log action', - actionTypeId: '.server-log', + connector_type_id: '.server-log', config: {}, }); expect(typeof createdAction.id).to.be('string'); const { body: fetchedAction } = await supertest - .get(`/api/actions/action/${createdAction.id}`) + .get(`/api/actions/connector/${createdAction.id}`) .expect(200); expect(fetchedAction).to.eql({ id: fetchedAction.id, - isPreconfigured: false, + is_preconfigured: false, name: 'A server.log action', - actionTypeId: '.server-log', + connector_type_id: '.server-log', config: {}, }); }); it('should handle firing the action', async () => { const { body: result } = await supertest - .post(`/api/actions/action/${serverLogActionId}/_execute`) + .post(`/api/actions/connector/${serverLogActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts index 2d584f764e5e4..264e9cf42d97e 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts @@ -65,11 +65,11 @@ export default function servicenowTest({ getService }: FtrProviderContext) { describe('ServiceNow - Action Creation', () => { it('should return 200 when creating a servicenow action successfully', async () => { const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A servicenow action', - actionTypeId: '.servicenow', + connector_type_id: '.servicenow', config: { apiUrl: servicenowSimulatorURL, }, @@ -79,23 +79,23 @@ export default function servicenowTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdAction.id, - isPreconfigured: false, + is_preconfigured: false, name: 'A servicenow action', - actionTypeId: '.servicenow', + connector_type_id: '.servicenow', config: { apiUrl: servicenowSimulatorURL, }, }); const { body: fetchedAction } = await supertest - .get(`/api/actions/action/${createdAction.id}`) + .get(`/api/actions/connector/${createdAction.id}`) .expect(200); expect(fetchedAction).to.eql({ id: fetchedAction.id, - isPreconfigured: false, + is_preconfigured: false, name: 'A servicenow action', - actionTypeId: '.servicenow', + connector_type_id: '.servicenow', config: { apiUrl: servicenowSimulatorURL, }, @@ -104,11 +104,11 @@ export default function servicenowTest({ getService }: FtrProviderContext) { it('should respond with a 400 Bad Request when creating a servicenow action with no apiUrl', async () => { await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A servicenow action', - actionTypeId: '.servicenow', + connector_type_id: '.servicenow', config: {}, }) .expect(400) @@ -124,11 +124,11 @@ export default function servicenowTest({ getService }: FtrProviderContext) { it('should respond with a 400 Bad Request when creating a servicenow action with a not present in allowedHosts apiUrl', async () => { await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A servicenow action', - actionTypeId: '.servicenow', + connector_type_id: '.servicenow', config: { apiUrl: 'http://servicenow.mynonexistent.com', }, @@ -147,11 +147,11 @@ export default function servicenowTest({ getService }: FtrProviderContext) { it('should respond with a 400 Bad Request when creating a servicenow action without secrets', async () => { await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A servicenow action', - actionTypeId: '.servicenow', + connector_type_id: '.servicenow', config: { apiUrl: servicenowSimulatorURL, }, @@ -174,11 +174,11 @@ export default function servicenowTest({ getService }: FtrProviderContext) { let proxyHaveBeenCalled = false; before(async () => { const { body } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A servicenow simulator', - actionTypeId: '.servicenow', + connector_type_id: '.servicenow', config: { apiUrl: servicenowSimulatorURL, }, @@ -198,14 +198,14 @@ export default function servicenowTest({ getService }: FtrProviderContext) { describe('Validation', () => { it('should handle failing with a simulated success without action', async () => { await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) + .post(`/api/actions/connector/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: {}, }) .then((resp: any) => { - expect(Object.keys(resp.body)).to.eql(['status', 'actionId', 'message', 'retry']); - expect(resp.body.actionId).to.eql(simulatedActionId); + expect(Object.keys(resp.body)).to.eql(['status', 'message', 'retry', 'connector_id']); + expect(resp.body.connector_id).to.eql(simulatedActionId); expect(resp.body.status).to.eql('error'); expect(resp.body.retry).to.eql(false); // Node.js 12 oddity: @@ -239,14 +239,14 @@ export default function servicenowTest({ getService }: FtrProviderContext) { it('should handle failing with a simulated success without unsupported action', async () => { await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) + .post(`/api/actions/connector/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { subAction: 'non-supported' }, }) .then((resp: any) => { expect(resp.body).to.eql({ - actionId: simulatedActionId, + connector_id: simulatedActionId, status: 'error', retry: false, message: @@ -257,14 +257,14 @@ export default function servicenowTest({ getService }: FtrProviderContext) { it('should handle failing with a simulated success without subActionParams', async () => { await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) + .post(`/api/actions/connector/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { subAction: 'pushToService' }, }) .then((resp: any) => { expect(resp.body).to.eql({ - actionId: simulatedActionId, + connector_id: simulatedActionId, status: 'error', retry: false, message: @@ -275,7 +275,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { it('should handle failing with a simulated success without title', async () => { await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) + .post(`/api/actions/connector/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -287,7 +287,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }) .then((resp: any) => { expect(resp.body).to.eql({ - actionId: simulatedActionId, + connector_id: simulatedActionId, status: 'error', retry: false, message: @@ -298,7 +298,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { it('should handle failing with a simulated success without commentId', async () => { await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) + .post(`/api/actions/connector/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -314,7 +314,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }) .then((resp: any) => { expect(resp.body).to.eql({ - actionId: simulatedActionId, + connector_id: simulatedActionId, status: 'error', retry: false, message: @@ -325,7 +325,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { it('should handle failing with a simulated success without comment message', async () => { await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) + .post(`/api/actions/connector/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -341,7 +341,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }) .then((resp: any) => { expect(resp.body).to.eql({ - actionId: simulatedActionId, + connector_id: simulatedActionId, status: 'error', retry: false, message: @@ -353,7 +353,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { describe('getChoices', () => { it('should fail when field is not provided', async () => { await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) + .post(`/api/actions/connector/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -363,7 +363,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }) .then((resp: any) => { expect(resp.body).to.eql({ - actionId: simulatedActionId, + connector_id: simulatedActionId, status: 'error', retry: false, message: @@ -377,7 +377,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { describe('Execution', () => { it('should handle creating an incident without comments', async () => { const { body: result } = await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) + .post(`/api/actions/connector/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -393,7 +393,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { expect(proxyHaveBeenCalled).to.equal(true); expect(result).to.eql({ status: 'ok', - actionId: simulatedActionId, + connector_id: simulatedActionId, data: { id: '123', title: 'INC01', @@ -406,7 +406,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { describe('getChoices', () => { it('should get choices', async () => { const { body: result } = await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) + .post(`/api/actions/connector/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -419,7 +419,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { expect(proxyHaveBeenCalled).to.equal(true); expect(result).to.eql({ status: 'ok', - actionId: simulatedActionId, + connector_id: simulatedActionId, data: [ { dependent_value: '', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts index 42cc93d50ea11..85c46ff98acd1 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts @@ -45,11 +45,11 @@ export default function slackTest({ getService }: FtrProviderContext) { it('should return 200 when creating a slack action successfully', async () => { const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A slack action', - actionTypeId: '.slack', + connector_type_id: '.slack', secrets: { webhookUrl: slackSimulatorURL, }, @@ -58,34 +58,34 @@ export default function slackTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdAction.id, - isPreconfigured: false, + is_preconfigured: false, name: 'A slack action', - actionTypeId: '.slack', + connector_type_id: '.slack', config: {}, }); expect(typeof createdAction.id).to.be('string'); const { body: fetchedAction } = await supertest - .get(`/api/actions/action/${createdAction.id}`) + .get(`/api/actions/connector/${createdAction.id}`) .expect(200); expect(fetchedAction).to.eql({ id: fetchedAction.id, - isPreconfigured: false, + is_preconfigured: false, name: 'A slack action', - actionTypeId: '.slack', + connector_type_id: '.slack', config: {}, }); }); it('should respond with a 400 Bad Request when creating a slack action with no webhookUrl', async () => { await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A slack action', - actionTypeId: '.slack', + connector_type_id: '.slack', secrets: {}, }) .expect(400) @@ -101,11 +101,11 @@ export default function slackTest({ getService }: FtrProviderContext) { it('should respond with a 400 Bad Request when creating a slack action with not present in allowedHosts webhookUrl', async () => { await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A slack action', - actionTypeId: '.slack', + connector_type_id: '.slack', secrets: { webhookUrl: 'http://slack.mynonexistent.com/other/stuff/in/the/path', }, @@ -122,11 +122,11 @@ export default function slackTest({ getService }: FtrProviderContext) { it('should respond with a 400 Bad Request when creating a slack action with a webhookUrl with no hostname', async () => { await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A slack action', - actionTypeId: '.slack', + connector_type_id: '.slack', secrets: { webhookUrl: 'fee-fi-fo-fum', }, @@ -144,11 +144,11 @@ export default function slackTest({ getService }: FtrProviderContext) { it('should create our slack simulator action successfully', async () => { const { body: createdSimulatedAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A slack simulator', - actionTypeId: '.slack', + connector_type_id: '.slack', secrets: { webhookUrl: slackSimulatorURL, }, @@ -160,7 +160,7 @@ export default function slackTest({ getService }: FtrProviderContext) { it('should handle firing with a simulated success', async () => { const { body: result } = await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) + .post(`/api/actions/connector/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -174,7 +174,7 @@ export default function slackTest({ getService }: FtrProviderContext) { it('should handle an empty message error', async () => { const { body: result } = await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) + .post(`/api/actions/connector/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -188,7 +188,7 @@ export default function slackTest({ getService }: FtrProviderContext) { it('should handle a 40x slack error', async () => { const { body: result } = await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) + .post(`/api/actions/connector/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -203,7 +203,7 @@ export default function slackTest({ getService }: FtrProviderContext) { it('should handle a 429 slack error', async () => { const dateStart = new Date().getTime(); const { body: result } = await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) + .post(`/api/actions/connector/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -221,7 +221,7 @@ export default function slackTest({ getService }: FtrProviderContext) { it('should handle a 500 slack error', async () => { const { body: result } = await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) + .post(`/api/actions/connector/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts index 61def8b6536ee..d48fb99088d61 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts @@ -54,11 +54,11 @@ export default function webhookTest({ getService }: FtrProviderContext) { }; const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'test') .send({ name: 'A generic Webhook action', - actionTypeId: '.webhook', + connector_type_id: '.webhook', secrets: { user, password, @@ -99,11 +99,11 @@ export default function webhookTest({ getService }: FtrProviderContext) { it('should return 200 when creating a webhook action successfully', async () => { const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'test') .send({ name: 'A generic Webhook action', - actionTypeId: '.webhook', + connector_type_id: '.webhook', secrets: { user: 'username', password: 'mypassphrase', @@ -116,9 +116,9 @@ export default function webhookTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdAction.id, - isPreconfigured: false, + is_preconfigured: false, name: 'A generic Webhook action', - actionTypeId: '.webhook', + connector_type_id: '.webhook', config: { ...defaultValues, url: webhookSimulatorURL, @@ -128,14 +128,14 @@ export default function webhookTest({ getService }: FtrProviderContext) { expect(typeof createdAction.id).to.be('string'); const { body: fetchedAction } = await supertest - .get(`/api/actions/action/${createdAction.id}`) + .get(`/api/actions/connector/${createdAction.id}`) .expect(200); expect(fetchedAction).to.eql({ id: fetchedAction.id, - isPreconfigured: false, + is_preconfigured: false, name: 'A generic Webhook action', - actionTypeId: '.webhook', + connector_type_id: '.webhook', config: { ...defaultValues, url: webhookSimulatorURL, @@ -145,11 +145,11 @@ export default function webhookTest({ getService }: FtrProviderContext) { it('should remove headers when a webhook is updated', async () => { const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'test') .send({ name: 'A generic Webhook action', - actionTypeId: '.webhook', + connector_type_id: '.webhook', secrets: { user: 'username', password: 'mypassphrase', @@ -165,9 +165,9 @@ export default function webhookTest({ getService }: FtrProviderContext) { expect(createdAction).to.eql({ id: createdAction.id, - isPreconfigured: false, + is_preconfigured: false, name: 'A generic Webhook action', - actionTypeId: '.webhook', + connector_type_id: '.webhook', config: { ...defaultValues, url: webhookSimulatorURL, @@ -178,7 +178,7 @@ export default function webhookTest({ getService }: FtrProviderContext) { }); await supertest - .put(`/api/actions/action/${createdAction.id}`) + .put(`/api/actions/connector/${createdAction.id}`) .set('kbn-xsrf', 'foo') .send({ name: 'A generic Webhook action', @@ -196,14 +196,14 @@ export default function webhookTest({ getService }: FtrProviderContext) { .expect(200); const { body: fetchedAction } = await supertest - .get(`/api/actions/action/${createdAction.id}`) + .get(`/api/actions/connector/${createdAction.id}`) .expect(200); expect(fetchedAction).to.eql({ id: fetchedAction.id, - isPreconfigured: false, + is_preconfigured: false, name: 'A generic Webhook action', - actionTypeId: '.webhook', + connector_type_id: '.webhook', config: { ...defaultValues, url: webhookSimulatorURL, @@ -217,7 +217,7 @@ export default function webhookTest({ getService }: FtrProviderContext) { it('should send authentication to the webhook target', async () => { const webhookActionId = await createWebhookAction(webhookSimulatorURL, {}, kibanaURL); const { body: result } = await supertest - .post(`/api/actions/action/${webhookActionId}/_execute`) + .post(`/api/actions/connector/${webhookActionId}/_execute`) .set('kbn-xsrf', 'test') .send({ params: { @@ -236,7 +236,7 @@ export default function webhookTest({ getService }: FtrProviderContext) { kibanaURL ); const { body: result } = await supertest - .post(`/api/actions/action/${webhookActionId}/_execute`) + .post(`/api/actions/connector/${webhookActionId}/_execute`) .set('kbn-xsrf', 'test') .send({ params: { @@ -255,7 +255,7 @@ export default function webhookTest({ getService }: FtrProviderContext) { kibanaURL ); const { body: result } = await supertest - .post(`/api/actions/action/${webhookActionId}/_execute`) + .post(`/api/actions/connector/${webhookActionId}/_execute`) .set('kbn-xsrf', 'test') .send({ params: { @@ -270,11 +270,11 @@ export default function webhookTest({ getService }: FtrProviderContext) { it('should handle target webhooks that are not added to allowedHosts', async () => { const { body: result } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'test') .send({ name: 'A generic Webhook action', - actionTypeId: '.webhook', + connector_type_id: '.webhook', secrets: { user: 'username', password: 'mypassphrase', @@ -296,7 +296,7 @@ export default function webhookTest({ getService }: FtrProviderContext) { kibanaURL ); const { body: result } = await supertest - .post(`/api/actions/action/${webhookActionId}/_execute`) + .post(`/api/actions/connector/${webhookActionId}/_execute`) .set('kbn-xsrf', 'test') .send({ params: { @@ -312,7 +312,7 @@ export default function webhookTest({ getService }: FtrProviderContext) { it('should handle failing webhook targets', async () => { const webhookActionId = await createWebhookAction(webhookSimulatorURL, {}, kibanaURL); const { body: result } = await supertest - .post(`/api/actions/action/${webhookActionId}/_execute`) + .post(`/api/actions/connector/${webhookActionId}/_execute`) .set('kbn-xsrf', 'test') .send({ params: { @@ -323,7 +323,7 @@ export default function webhookTest({ getService }: FtrProviderContext) { expect(result.status).to.eql('error'); expect(result.message).to.match(/error calling webhook, retry later/); - expect(result.serviceMessage).to.eql('[500] Internal Server Error'); + expect(result.service_message).to.eql('[500] Internal Server Error'); }); after(() => { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/list_action_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/connector_types.ts similarity index 96% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/list_action_types.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/connector_types.ts index 6625af3147450..a76654415c16d 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/list_action_types.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/connector_types.ts @@ -20,7 +20,7 @@ export default function listActionTypesTests({ getService }: FtrProviderContext) describe(scenario.id, () => { it('should return 200 with list of action types containing defaults', async () => { const response = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/actions/list_action_types`) + .get(`${getUrlPrefix(space.id)}/api/actions/connector_types`) .auth(user.username, user.password); function createActionTypeMatcher(id: string, name: string) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/create.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/create.ts index 103811ae98c56..5e83ce7821d74 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/create.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/create.ts @@ -25,12 +25,12 @@ export default function createActionTests({ getService }: FtrProviderContext) { describe(scenario.id, () => { it('should handle create action request appropriately', async () => { const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/actions/action`) + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) .auth(user.username, user.password) .set('kbn-xsrf', 'foo') .send({ name: 'My action', - actionTypeId: 'test.index-record', + connector_type_id: 'test.index-record', config: { unencrypted: `This value shouldn't get encrypted`, }, @@ -58,9 +58,9 @@ export default function createActionTests({ getService }: FtrProviderContext) { objectRemover.add(space.id, response.body.id, 'action', 'actions'); expect(response.body).to.eql({ id: response.body.id, - isPreconfigured: false, + is_preconfigured: false, name: 'My action', - actionTypeId: 'test.index-record', + connector_type_id: 'test.index-record', config: { unencrypted: `This value shouldn't get encrypted`, }, @@ -81,12 +81,12 @@ export default function createActionTests({ getService }: FtrProviderContext) { it(`should handle create action request appropriately when action type isn't registered`, async () => { const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/actions/action`) + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send({ name: 'My action', - actionTypeId: 'test.unregistered-action-type', + connector_type_id: 'test.unregistered-action-type', config: {}, }); @@ -119,7 +119,7 @@ export default function createActionTests({ getService }: FtrProviderContext) { it('should handle create action request appropriately when payload is empty and invalid', async () => { const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/actions/action`) + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send({}); @@ -146,12 +146,12 @@ export default function createActionTests({ getService }: FtrProviderContext) { it(`should handle create action request appropriately when config isn't valid`, async () => { const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/actions/action`) + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send({ name: 'my name', - actionTypeId: 'test.index-record', + connector_type_id: 'test.index-record', config: { unencrypted: 'my unencrypted text', }, @@ -187,12 +187,12 @@ export default function createActionTests({ getService }: FtrProviderContext) { it(`should handle create action requests for action types that are not enabled`, async () => { const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/actions/action`) + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send({ name: 'my name', - actionTypeId: 'test.not-enabled', + connector_type_id: 'test.not-enabled', }); switch (scenario.id) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/delete.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/delete.ts index 404e5e21a2d83..a0aae0a1bd64d 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/delete.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/delete.ts @@ -26,11 +26,11 @@ export default function deleteActionTests({ getService }: FtrProviderContext) { describe(scenario.id, () => { it('should handle delete action request appropriately', async () => { const { body: createdAction } = await supertest - .post(`${getUrlPrefix(space.id)}/api/actions/action`) + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .send({ name: 'My action', - actionTypeId: 'test.index-record', + connector_type_id: 'test.index-record', config: { unencrypted: `This value shouldn't get encrypted`, }, @@ -41,7 +41,7 @@ export default function deleteActionTests({ getService }: FtrProviderContext) { .expect(200); const response = await supertestWithoutAuth - .delete(`${getUrlPrefix(space.id)}/api/actions/action/${createdAction.id}`) + .delete(`${getUrlPrefix(space.id)}/api/actions/connector/${createdAction.id}`) .auth(user.username, user.password) .set('kbn-xsrf', 'foo'); @@ -71,11 +71,11 @@ export default function deleteActionTests({ getService }: FtrProviderContext) { it(`shouldn't delete action from another space`, async () => { const { body: createdAction } = await supertest - .post(`${getUrlPrefix(space.id)}/api/actions/action`) + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .send({ name: 'My action', - actionTypeId: 'test.index-record', + connector_type_id: 'test.index-record', config: { unencrypted: `This value shouldn't get encrypted`, }, @@ -87,7 +87,7 @@ export default function deleteActionTests({ getService }: FtrProviderContext) { objectRemover.add(space.id, createdAction.id, 'action', 'actions'); const response = await supertestWithoutAuth - .delete(`${getUrlPrefix('other')}/api/actions/action/${createdAction.id}`) + .delete(`${getUrlPrefix('other')}/api/actions/connector/${createdAction.id}`) .auth(user.username, user.password) .set('kbn-xsrf', 'foo'); @@ -120,7 +120,7 @@ export default function deleteActionTests({ getService }: FtrProviderContext) { it(`should handle delete request appropriately when action doesn't exist`, async () => { const response = await supertestWithoutAuth - .delete(`${getUrlPrefix(space.id)}/api/actions/action/2`) + .delete(`${getUrlPrefix(space.id)}/api/actions/connector/2`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password); @@ -148,7 +148,7 @@ export default function deleteActionTests({ getService }: FtrProviderContext) { it(`shouldn't delete action from preconfigured list`, async () => { const response = await supertestWithoutAuth - .delete(`${getUrlPrefix(space.id)}/api/actions/action/my-slack1`) + .delete(`${getUrlPrefix(space.id)}/api/actions/connector/my-slack1`) .auth(user.username, user.password) .set('kbn-xsrf', 'foo'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts index 5e438eb9506ed..f3c3ee74551c0 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts @@ -48,11 +48,11 @@ export default function ({ getService }: FtrProviderContext) { describe(scenario.id, () => { it('should handle execute request appropriately', async () => { const { body: createdAction } = await supertest - .post(`${getUrlPrefix(space.id)}/api/actions/action`) + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .send({ name: 'My action', - actionTypeId: 'test.index-record', + connector_type_id: 'test.index-record', config: { unencrypted: `This value shouldn't get encrypted`, }, @@ -65,7 +65,7 @@ export default function ({ getService }: FtrProviderContext) { const reference = `actions-execute-1:${user.username}`; const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/actions/action/${createdAction.id}/_execute`) + .post(`${getUrlPrefix(space.id)}/api/actions/connector/${createdAction.id}/_execute`) .auth(user.username, user.password) .set('kbn-xsrf', 'foo') .send({ @@ -117,7 +117,7 @@ export default function ({ getService }: FtrProviderContext) { await validateEventLog({ spaceId: space.id, - actionId: createdAction.id, + connectorId: createdAction.id, outcome: 'success', message: `action executed: test.index-record:${createdAction.id}: My action`, }); @@ -129,11 +129,11 @@ export default function ({ getService }: FtrProviderContext) { it(`shouldn't execute an action from another space`, async () => { const { body: createdAction } = await supertest - .post(`${getUrlPrefix(space.id)}/api/actions/action`) + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .send({ name: 'My action', - actionTypeId: 'test.index-record', + connector_type_id: 'test.index-record', config: { unencrypted: `This value shouldn't get encrypted`, }, @@ -146,7 +146,7 @@ export default function ({ getService }: FtrProviderContext) { const reference = `actions-execute-4:${user.username}`; const response = await supertestWithoutAuth - .post(`${getUrlPrefix('other')}/api/actions/action/${createdAction.id}/_execute`) + .post(`${getUrlPrefix('other')}/api/actions/connector/${createdAction.id}/_execute`) .auth(user.username, user.password) .set('kbn-xsrf', 'foo') .send({ @@ -186,11 +186,11 @@ export default function ({ getService }: FtrProviderContext) { it('should handle execute request appropriately after action is updated', async () => { const { body: createdAction } = await supertest - .post(`${getUrlPrefix(space.id)}/api/actions/action`) + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .send({ name: 'My action', - actionTypeId: 'test.index-record', + connector_type_id: 'test.index-record', config: { unencrypted: `This value shouldn't get encrypted`, }, @@ -202,7 +202,7 @@ export default function ({ getService }: FtrProviderContext) { objectRemover.add(space.id, createdAction.id, 'action', 'actions'); await supertest - .put(`${getUrlPrefix(space.id)}/api/actions/action/${createdAction.id}`) + .put(`${getUrlPrefix(space.id)}/api/actions/connector/${createdAction.id}`) .set('kbn-xsrf', 'foo') .send({ name: 'My action updated', @@ -217,7 +217,7 @@ export default function ({ getService }: FtrProviderContext) { const reference = `actions-execute-2:${user.username}`; const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/actions/action/${createdAction.id}/_execute`) + .post(`${getUrlPrefix(space.id)}/api/actions/connector/${createdAction.id}/_execute`) .auth(user.username, user.password) .set('kbn-xsrf', 'foo') .send({ @@ -274,7 +274,7 @@ export default function ({ getService }: FtrProviderContext) { it(`should handle execute request appropriately when action doesn't exist`, async () => { const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/actions/action/1/_execute`) + .post(`${getUrlPrefix(space.id)}/api/actions/connector/1/_execute`) .auth(user.username, user.password) .set('kbn-xsrf', 'foo') .send({ @@ -310,7 +310,7 @@ export default function ({ getService }: FtrProviderContext) { it('should handle execute request appropriately when payload is empty and invalid', async () => { const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/actions/action/1/_execute`) + .post(`${getUrlPrefix(space.id)}/api/actions/connector/1/_execute`) .auth(user.username, user.password) .set('kbn-xsrf', 'foo') .send({}); @@ -338,11 +338,11 @@ export default function ({ getService }: FtrProviderContext) { it('should handle execute request appropriately after changing config properties', async () => { const { body: createdAction } = await supertest - .post(`${getUrlPrefix(space.id)}/api/actions/action`) + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .send({ name: 'test email action', - actionTypeId: '.email', + connector_type_id: '.email', config: { from: 'email-from-1@example.com', // this host is specifically added to allowedHosts in: @@ -359,7 +359,7 @@ export default function ({ getService }: FtrProviderContext) { objectRemover.add(space.id, createdAction.id, 'action', 'actions'); await supertest - .put(`${getUrlPrefix(space.id)}/api/actions/action/${createdAction.id}`) + .put(`${getUrlPrefix(space.id)}/api/actions/connector/${createdAction.id}`) .set('kbn-xsrf', 'foo') .send({ name: 'a test email action 2', @@ -375,7 +375,7 @@ export default function ({ getService }: FtrProviderContext) { .expect(200); const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/actions/action/${createdAction.id}/_execute`) + .post(`${getUrlPrefix(space.id)}/api/actions/connector/${createdAction.id}/_execute`) .auth(user.username, user.password) .set('kbn-xsrf', 'foo') .send({ @@ -413,17 +413,17 @@ export default function ({ getService }: FtrProviderContext) { let searchResult: any; const reference = `actions-execute-3:${user.username}`; const { body: createdAction } = await supertest - .post(`${getUrlPrefix(space.id)}/api/actions/action`) + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .send({ name: 'My action', - actionTypeId: 'test.authorization', + connector_type_id: 'test.authorization', }) .expect(200); objectRemover.add(space.id, createdAction.id, 'action', 'actions'); const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/actions/action/${createdAction.id}/_execute`) + .post(`${getUrlPrefix(space.id)}/api/actions/connector/${createdAction.id}/_execute`) .auth(user.username, user.password) .set('kbn-xsrf', 'foo') .send({ @@ -503,21 +503,21 @@ export default function ({ getService }: FtrProviderContext) { interface ValidateEventLogParams { spaceId: string; - actionId: string; + connectorId: string; outcome: string; message: string; errorMessage?: string; } async function validateEventLog(params: ValidateEventLogParams): Promise { - const { spaceId, actionId, outcome, message, errorMessage } = params; + const { spaceId, connectorId, outcome, message, errorMessage } = params; const events: IValidatedEvent[] = await retry.try(async () => { return await getEventLog({ getService, spaceId, type: 'action', - id: actionId, + id: connectorId, provider: 'actions', actions: new Map([['execute', { equal: 1 }]]), filter: 'event.action:(execute)', @@ -550,7 +550,7 @@ export default function ({ getService }: FtrProviderContext) { { rel: 'primary', type: 'action', - id: actionId, + id: connectorId, namespace: spaceId, }, ]); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts index 5cbda5f15d839..0a2b2c7520e36 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts @@ -25,11 +25,11 @@ export default function getActionTests({ getService }: FtrProviderContext) { describe(scenario.id, () => { it('should handle get action request appropriately', async () => { const { body: createdAction } = await supertest - .post(`${getUrlPrefix(space.id)}/api/actions/action`) + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .send({ name: 'My action', - actionTypeId: 'test.index-record', + connector_type_id: 'test.index-record', config: { unencrypted: `This value shouldn't get encrypted`, }, @@ -41,7 +41,7 @@ export default function getActionTests({ getService }: FtrProviderContext) { objectRemover.add(space.id, createdAction.id, 'action', 'actions'); const response = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/actions/action/${createdAction.id}`) + .get(`${getUrlPrefix(space.id)}/api/actions/connector/${createdAction.id}`) .auth(user.username, user.password); switch (scenario.id) { @@ -62,8 +62,8 @@ export default function getActionTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(200); expect(response.body).to.eql({ id: createdAction.id, - isPreconfigured: false, - actionTypeId: 'test.index-record', + is_preconfigured: false, + connector_type_id: 'test.index-record', name: 'My action', config: { unencrypted: `This value shouldn't get encrypted`, @@ -77,11 +77,11 @@ export default function getActionTests({ getService }: FtrProviderContext) { it(`action shouldn't be acessible from another space`, async () => { const { body: createdAction } = await supertest - .post(`${getUrlPrefix(space.id)}/api/actions/action`) + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .send({ name: 'My action', - actionTypeId: 'test.index-record', + connector_type_id: 'test.index-record', config: { unencrypted: `This value shouldn't get encrypted`, }, @@ -93,7 +93,7 @@ export default function getActionTests({ getService }: FtrProviderContext) { objectRemover.add(space.id, createdAction.id, 'action', 'actions'); const response = await supertestWithoutAuth - .get(`${getUrlPrefix('other')}/api/actions/action/${createdAction.id}`) + .get(`${getUrlPrefix('other')}/api/actions/connector/${createdAction.id}`) .auth(user.username, user.password); switch (scenario.id) { @@ -125,7 +125,7 @@ export default function getActionTests({ getService }: FtrProviderContext) { it('should handle get preconfigured action request appropriately', async () => { const response = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/actions/action/my-slack1`) + .get(`${getUrlPrefix(space.id)}/api/actions/connector/my-slack1`) .auth(user.username, user.password); switch (scenario.id) { @@ -146,9 +146,9 @@ export default function getActionTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(200); expect(response.body).to.eql({ id: 'my-slack1', - actionTypeId: '.slack', + connector_type_id: '.slack', name: 'Slack#xyz', - isPreconfigured: true, + is_preconfigured: true, }); break; default: diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts index 1a460dd630e01..2142520894669 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts @@ -25,11 +25,11 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { describe(scenario.id, () => { it('should handle get all action request appropriately', async () => { const { body: createdAction } = await supertest - .post(`${getUrlPrefix(space.id)}/api/actions/action`) + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .send({ name: 'My action', - actionTypeId: 'test.index-record', + connector_type_id: 'test.index-record', config: { unencrypted: `This value shouldn't get encrypted`, }, @@ -41,7 +41,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { objectRemover.add(space.id, createdAction.id, 'action', 'actions'); const response = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/actions`) + .get(`${getUrlPrefix(space.id)}/api/actions/connectors`) .auth(user.username, user.password); switch (scenario.id) { @@ -63,41 +63,41 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { expect(response.body).to.eql([ { id: createdAction.id, - isPreconfigured: false, + is_preconfigured: false, name: 'My action', - actionTypeId: 'test.index-record', + connector_type_id: 'test.index-record', config: { unencrypted: `This value shouldn't get encrypted`, }, - referencedByCount: 0, + referenced_by_count: 0, }, { id: 'preconfigured-es-index-action', - isPreconfigured: true, - actionTypeId: '.index', + is_preconfigured: true, + connector_type_id: '.index', name: 'preconfigured_es_index_action', - referencedByCount: 0, + referenced_by_count: 0, }, { id: 'my-slack1', - isPreconfigured: true, - actionTypeId: '.slack', + is_preconfigured: true, + connector_type_id: '.slack', name: 'Slack#xyz', - referencedByCount: 0, + referenced_by_count: 0, }, { id: 'custom-system-abc-connector', - isPreconfigured: true, - actionTypeId: 'system-abc-action-type', + is_preconfigured: true, + connector_type_id: 'system-abc-action-type', name: 'SystemABC', - referencedByCount: 0, + referenced_by_count: 0, }, { id: 'preconfigured.test.index-record', - isPreconfigured: true, - actionTypeId: 'test.index-record', + is_preconfigured: true, + connector_type_id: 'test.index-record', name: 'Test:_Preconfigured_Index_Record', - referencedByCount: 0, + referenced_by_count: 0, }, ]); break; @@ -106,13 +106,13 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { } }); - it('should handle get all request appropriately with proper referencedByCount', async () => { + it('should handle get all request appropriately with proper referenced_by_count', async () => { const { body: createdAction } = await supertest - .post(`${getUrlPrefix(space.id)}/api/actions/action`) + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .send({ name: 'My action', - actionTypeId: 'test.index-record', + connector_type_id: 'test.index-record', config: { unencrypted: `This value shouldn't get encrypted`, }, @@ -148,7 +148,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const response = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/actions`) + .get(`${getUrlPrefix(space.id)}/api/actions/connectors`) .auth(user.username, user.password); switch (scenario.id) { @@ -170,41 +170,41 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { expect(response.body).to.eql([ { id: createdAction.id, - isPreconfigured: false, + is_preconfigured: false, name: 'My action', - actionTypeId: 'test.index-record', + connector_type_id: 'test.index-record', config: { unencrypted: `This value shouldn't get encrypted`, }, - referencedByCount: 1, + referenced_by_count: 1, }, { id: 'preconfigured-es-index-action', - isPreconfigured: true, - actionTypeId: '.index', + is_preconfigured: true, + connector_type_id: '.index', name: 'preconfigured_es_index_action', - referencedByCount: 0, + referenced_by_count: 0, }, { id: 'my-slack1', - isPreconfigured: true, - actionTypeId: '.slack', + is_preconfigured: true, + connector_type_id: '.slack', name: 'Slack#xyz', - referencedByCount: 1, + referenced_by_count: 1, }, { id: 'custom-system-abc-connector', - isPreconfigured: true, - actionTypeId: 'system-abc-action-type', + is_preconfigured: true, + connector_type_id: 'system-abc-action-type', name: 'SystemABC', - referencedByCount: 0, + referenced_by_count: 0, }, { id: 'preconfigured.test.index-record', - isPreconfigured: true, - actionTypeId: 'test.index-record', + is_preconfigured: true, + connector_type_id: 'test.index-record', name: 'Test:_Preconfigured_Index_Record', - referencedByCount: 0, + referenced_by_count: 0, }, ]); break; @@ -215,11 +215,11 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { it(`shouldn't get actions from another space`, async () => { const { body: createdAction } = await supertest - .post(`${getUrlPrefix(space.id)}/api/actions/action`) + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .send({ name: 'My action', - actionTypeId: 'test.index-record', + connector_type_id: 'test.index-record', config: { unencrypted: `This value shouldn't get encrypted`, }, @@ -231,7 +231,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { objectRemover.add(space.id, createdAction.id, 'action', 'actions'); const response = await supertestWithoutAuth - .get(`${getUrlPrefix('other')}/api/actions`) + .get(`${getUrlPrefix('other')}/api/actions/connectors`) .auth(user.username, user.password); switch (scenario.id) { @@ -253,31 +253,31 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { expect(response.body).to.eql([ { id: 'preconfigured-es-index-action', - isPreconfigured: true, - actionTypeId: '.index', + is_preconfigured: true, + connector_type_id: '.index', name: 'preconfigured_es_index_action', - referencedByCount: 0, + referenced_by_count: 0, }, { id: 'my-slack1', - isPreconfigured: true, - actionTypeId: '.slack', + is_preconfigured: true, + connector_type_id: '.slack', name: 'Slack#xyz', - referencedByCount: 0, + referenced_by_count: 0, }, { id: 'custom-system-abc-connector', - isPreconfigured: true, - actionTypeId: 'system-abc-action-type', + is_preconfigured: true, + connector_type_id: 'system-abc-action-type', name: 'SystemABC', - referencedByCount: 0, + referenced_by_count: 0, }, { id: 'preconfigured.test.index-record', - isPreconfigured: true, - actionTypeId: 'test.index-record', + is_preconfigured: true, + connector_type_id: 'test.index-record', name: 'Test:_Preconfigured_Index_Record', - referencedByCount: 0, + referenced_by_count: 0, }, ]); break; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts index 9af6c9ad0109c..b5ff287ac58f6 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts @@ -34,7 +34,7 @@ export default function actionsTests({ loadTestFile, getService }: FtrProviderCo loadTestFile(require.resolve('./execute')); loadTestFile(require.resolve('./get_all')); loadTestFile(require.resolve('./get')); - loadTestFile(require.resolve('./list_action_types')); + loadTestFile(require.resolve('./connector_types')); loadTestFile(require.resolve('./update')); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/manual/pr_40694.js b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/manual/pr_40694.js index e6ee275103041..5b422bcd0614d 100755 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/manual/pr_40694.js +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/manual/pr_40694.js @@ -16,8 +16,8 @@ if (require.main === module) main(); async function main() { let response; - response = await httpPost('api/actions/action', { - actionTypeId: '.email', + response = await httpPost('api/actions/connector', { + connector_type_id: '.email', name: 'an email action', config: { from: 'patrick.mueller@elastic.co', @@ -32,12 +32,12 @@ async function main() { }); console.log(`result of create: ${JSON.stringify(response, null, 4)}`); - const actionId = response.id; + const connectorId = response.id; - response = await httpGet(`api/actions/${actionId}`); + response = await httpGet(`api/actions/${connectorId}`); console.log(`action after create: ${JSON.stringify(response, null, 4)}`); - response = await httpPut(`api/actions/action/${actionId}`, { + response = await httpPut(`api/actions/connector/${connectorId}`, { name: 'an email action', config: { from: 'patrick.mueller@elastic.co', @@ -51,10 +51,10 @@ async function main() { console.log(`response from update: ${JSON.stringify(response, null, 4)}`); - response = await httpGet(`api/actions/${actionId}`); + response = await httpGet(`api/actions/${connectorId}`); console.log(`action after update: ${JSON.stringify(response, null, 4)}`); - response = await httpPost(`api/actions/action/${actionId}/_execute`, { + response = await httpPost(`api/actions/connector/${connectorId}/_execute`, { params: { to: ['patrick.mueller@elastic.co'], subject: 'the email subject', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/update.ts index ded02f1982be7..15cf409970184 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/update.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/update.ts @@ -25,11 +25,11 @@ export default function updateActionTests({ getService }: FtrProviderContext) { describe(scenario.id, () => { it('should handle update action request appropriately', async () => { const { body: createdAction } = await supertest - .post(`${getUrlPrefix(space.id)}/api/actions/action`) + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .send({ name: 'My action', - actionTypeId: 'test.index-record', + connector_type_id: 'test.index-record', config: { unencrypted: `This value shouldn't get encrypted`, }, @@ -41,7 +41,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) { objectRemover.add(space.id, createdAction.id, 'action', 'actions'); const response = await supertestWithoutAuth - .put(`${getUrlPrefix(space.id)}/api/actions/action/${createdAction.id}`) + .put(`${getUrlPrefix(space.id)}/api/actions/connector/${createdAction.id}`) .auth(user.username, user.password) .set('kbn-xsrf', 'foo') .send({ @@ -72,8 +72,8 @@ export default function updateActionTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(200); expect(response.body).to.eql({ id: createdAction.id, - isPreconfigured: false, - actionTypeId: 'test.index-record', + is_preconfigured: false, + connector_type_id: 'test.index-record', name: 'My action updated', config: { unencrypted: `This value shouldn't get encrypted`, @@ -94,11 +94,11 @@ export default function updateActionTests({ getService }: FtrProviderContext) { it(`shouldn't update action from another space`, async () => { const { body: createdAction } = await supertest - .post(`${getUrlPrefix(space.id)}/api/actions/action`) + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .send({ name: 'My action', - actionTypeId: 'test.index-record', + connector_type_id: 'test.index-record', config: { unencrypted: `This value shouldn't get encrypted`, }, @@ -110,7 +110,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) { objectRemover.add(space.id, createdAction.id, 'action', 'actions'); const response = await supertestWithoutAuth - .put(`${getUrlPrefix('other')}/api/actions/action/${createdAction.id}`) + .put(`${getUrlPrefix('other')}/api/actions/connector/${createdAction.id}`) .auth(user.username, user.password) .set('kbn-xsrf', 'foo') .send({ @@ -152,7 +152,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) { it('should handle update action request appropriately when passing a null config', async () => { const response = await supertestWithoutAuth - .put(`${getUrlPrefix(space.id)}/api/actions/action/1`) + .put(`${getUrlPrefix(space.id)}/api/actions/connector/1`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send({ @@ -182,7 +182,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) { it(`should handle update action request appropriately when action doesn't exist`, async () => { const response = await supertestWithoutAuth - .put(`${getUrlPrefix(space.id)}/api/actions/action/1`) + .put(`${getUrlPrefix(space.id)}/api/actions/connector/1`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send({ @@ -224,7 +224,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) { it('should handle update action request appropriately when payload is empty and invalid', async () => { const response = await supertestWithoutAuth - .put(`${getUrlPrefix(space.id)}/api/actions/action/1`) + .put(`${getUrlPrefix(space.id)}/api/actions/connector/1`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send({}); @@ -252,11 +252,11 @@ export default function updateActionTests({ getService }: FtrProviderContext) { it('should handle update action request appropriately when secrets are not valid', async () => { const { body: createdAction } = await supertest - .post(`${getUrlPrefix(space.id)}/api/actions/action`) + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .send({ name: 'My action', - actionTypeId: 'test.index-record', + connector_type_id: 'test.index-record', config: { unencrypted: `This value shouldn't get encrypted`, }, @@ -268,7 +268,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) { objectRemover.add(space.id, createdAction.id, 'action', 'actions'); const response = await supertestWithoutAuth - .put(`${getUrlPrefix(space.id)}/api/actions/action/${createdAction.id}`) + .put(`${getUrlPrefix(space.id)}/api/actions/connector/${createdAction.id}`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send({ @@ -311,7 +311,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) { it(`shouldn't update action from preconfigured list`, async () => { const response = await supertestWithoutAuth - .put(`${getUrlPrefix(space.id)}/api/actions/action/custom-system-abc-connector`) + .put(`${getUrlPrefix(space.id)}/api/actions/connector/custom-system-abc-connector`) .auth(user.username, user.password) .set('kbn-xsrf', 'foo') .send({ diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/list_action_types.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/connector_types.ts similarity index 55% rename from x-pack/test/alerting_api_integration/spaces_only/tests/actions/list_action_types.ts rename to x-pack/test/alerting_api_integration/spaces_only/tests/actions/connector_types.ts index c999def5ead27..bb7fbbf0802d2 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/list_action_types.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/connector_types.ts @@ -14,10 +14,10 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function listActionTypesTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - describe('list_action_types', () => { - it('should return 200 with list of action types containing defaults', async () => { + describe('connector_types', () => { + it('should return 200 with list of connector types containing defaults', async () => { const response = await supertest.get( - `${getUrlPrefix(Spaces.space1.id)}/api/actions/list_action_types` + `${getUrlPrefix(Spaces.space1.id)}/api/actions/connector_types` ); function createActionTypeMatcher(id: string, name: string) { @@ -33,5 +33,26 @@ export default function listActionTypesTests({ getService }: FtrProviderContext) response.body.some(createActionTypeMatcher('test.index-record', 'Test: Index Record')) ).to.be(true); }); + + describe('legacy', () => { + it('should return 200 with list of action types containing defaults', async () => { + const response = await supertest.get( + `${getUrlPrefix(Spaces.space1.id)}/api/actions/list_action_types` + ); + + function createActionTypeMatcher(id: string, name: string) { + return (actionType: { id: string; name: string }) => { + return actionType.id === id && actionType.name === name; + }; + } + + expect(response.status).to.eql(200); + // Check for values explicitly in order to avoid this test failing each time plugins register + // a new action type + expect( + response.body.some(createActionTypeMatcher('test.index-record', 'Test: Index Record')) + ).to.be(true); + }); + }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/create.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/create.ts index de06f3fc990b6..c91c05bda606e 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/create.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/create.ts @@ -19,13 +19,13 @@ export default function createActionTests({ getService }: FtrProviderContext) { after(() => objectRemover.removeAll()); - it('should handle create action request appropriately', async () => { + it('should handle create connector request appropriately', async () => { const response = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .send({ name: 'My action', - actionTypeId: 'test.index-record', + connector_type_id: 'test.index-record', config: { unencrypted: `This value shouldn't get encrypted`, }, @@ -38,9 +38,9 @@ export default function createActionTests({ getService }: FtrProviderContext) { objectRemover.add(Spaces.space1.id, response.body.id, 'action', 'actions'); expect(response.body).to.eql({ id: response.body.id, - isPreconfigured: false, + is_preconfigured: false, name: 'My action', - actionTypeId: 'test.index-record', + connector_type_id: 'test.index-record', config: { unencrypted: `This value shouldn't get encrypted`, }, @@ -56,6 +56,45 @@ export default function createActionTests({ getService }: FtrProviderContext) { }); }); + describe('legacy', () => { + it('should handle create action request appropriately', async () => { + const response = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My action', + actionTypeId: 'test.index-record', + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + secrets: { + encrypted: 'This value should be encrypted', + }, + }); + + expect(response.status).to.eql(200); + objectRemover.add(Spaces.space1.id, response.body.id, 'action', 'actions'); + expect(response.body).to.eql({ + id: response.body.id, + isPreconfigured: false, + name: 'My action', + actionTypeId: 'test.index-record', + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + }); + expect(typeof response.body.id).to.be('string'); + + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: Spaces.space1.id, + type: 'action', + id: response.body.id, + }); + }); + }); + it('should notify feature usage when creating a gold action type', async () => { const testStart = new Date(); const response = await supertest diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/delete.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/delete.ts index 5ba5083bd9245..66ae047e5151b 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/delete.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/delete.ts @@ -20,11 +20,11 @@ export default function deleteActionTests({ getService }: FtrProviderContext) { it('should handle delete action request appropriately', async () => { const { body: createdAction } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .send({ name: 'My action', - actionTypeId: 'test.index-record', + connector_type_id: 'test.index-record', config: { unencrypted: `This value shouldn't get encrypted`, }, @@ -35,18 +35,18 @@ export default function deleteActionTests({ getService }: FtrProviderContext) { .expect(200); await supertest - .delete(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action/${createdAction.id}`) + .delete(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector/${createdAction.id}`) .set('kbn-xsrf', 'foo') .expect(204, ''); }); it(`shouldn't delete action from another space`, async () => { const { body: createdAction } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .send({ name: 'My action', - actionTypeId: 'test.index-record', + connector_type_id: 'test.index-record', config: { unencrypted: `This value shouldn't get encrypted`, }, @@ -58,7 +58,7 @@ export default function deleteActionTests({ getService }: FtrProviderContext) { objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions'); await supertest - .delete(`${getUrlPrefix(Spaces.other.id)}/api/actions/action/${createdAction.id}`) + .delete(`${getUrlPrefix(Spaces.other.id)}/api/actions/connector/${createdAction.id}`) .set('kbn-xsrf', 'foo') .expect(404, { statusCode: 404, @@ -69,7 +69,7 @@ export default function deleteActionTests({ getService }: FtrProviderContext) { it(`should handle delete request appropriately when action doesn't exist`, async () => { await supertest - .delete(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action/2`) + .delete(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector/2`) .set('kbn-xsrf', 'foo') .expect(404, { statusCode: 404, @@ -80,7 +80,7 @@ export default function deleteActionTests({ getService }: FtrProviderContext) { it(`shouldn't delete action from preconfigured list`, async () => { await supertest - .delete(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action/my-slack1`) + .delete(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector/my-slack1`) .set('kbn-xsrf', 'foo') .expect(400, { statusCode: 400, @@ -88,5 +88,78 @@ export default function deleteActionTests({ getService }: FtrProviderContext) { message: `Preconfigured action my-slack1 is not allowed to delete.`, }); }); + + describe('legacy', () => { + it('should handle delete action request appropriately', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My action', + actionTypeId: 'test.index-record', + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + secrets: { + encrypted: 'This value should be encrypted', + }, + }) + .expect(200); + + await supertest + .delete(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action/${createdAction.id}`) + .set('kbn-xsrf', 'foo') + .expect(204, ''); + }); + + it(`shouldn't delete action from another space`, async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My action', + actionTypeId: 'test.index-record', + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + secrets: { + encrypted: 'This value should be encrypted', + }, + }) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions'); + + await supertest + .delete(`${getUrlPrefix(Spaces.other.id)}/api/actions/action/${createdAction.id}`) + .set('kbn-xsrf', 'foo') + .expect(404, { + statusCode: 404, + error: 'Not Found', + message: `Saved object [action/${createdAction.id}] not found`, + }); + }); + + it(`should handle delete request appropriately when action doesn't exist`, async () => { + await supertest + .delete(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action/2`) + .set('kbn-xsrf', 'foo') + .expect(404, { + statusCode: 404, + error: 'Not Found', + message: 'Saved object [action/2] not found', + }); + }); + + it(`shouldn't delete action from preconfigured list`, async () => { + await supertest + .delete(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action/my-slack1`) + .set('kbn-xsrf', 'foo') + .expect(400, { + statusCode: 400, + error: 'Bad Request', + message: `Preconfigured action my-slack1 is not allowed to delete.`, + }); + }); + }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts index 0e3e25ad9c99a..fbdde2104dd61 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts @@ -44,11 +44,11 @@ export default function ({ getService }: FtrProviderContext) { it('should handle execute request appropriately', async () => { const { body: createdAction } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .send({ name: 'My action', - actionTypeId: 'test.index-record', + connector_type_id: 'test.index-record', config: { unencrypted: `This value shouldn't get encrypted`, }, @@ -59,9 +59,11 @@ export default function ({ getService }: FtrProviderContext) { .expect(200); objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions'); - const reference = `actions-execute-1:${Spaces.space1.id}`; + const reference = `actions-execute-1:${Spaces.space1.id}:${createdAction.id}`; const response = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action/${createdAction.id}/_execute`) + .post( + `${getUrlPrefix(Spaces.space1.id)}/api/actions/connector/${createdAction.id}/_execute` + ) .set('kbn-xsrf', 'foo') .send({ params: { @@ -102,18 +104,20 @@ export default function ({ getService }: FtrProviderContext) { it('should handle failed executions', async () => { const { body: createdAction } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .send({ name: 'failing action', - actionTypeId: 'test.failing', + connector_type_id: 'test.failing', }) .expect(200); objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions'); const reference = `actions-failure-1:${Spaces.space1.id}`; const response = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action/${createdAction.id}/_execute`) + .post( + `${getUrlPrefix(Spaces.space1.id)}/api/actions/connector/${createdAction.id}/_execute` + ) .set('kbn-xsrf', 'foo') .send({ params: { @@ -124,10 +128,10 @@ export default function ({ getService }: FtrProviderContext) { expect(response.status).to.eql(200); expect(response.body).to.eql({ - actionId: createdAction.id, + connector_id: createdAction.id, status: 'error', message: 'an error occurred while running the action executor', - serviceMessage: `expected failure for ${ES_TEST_INDEX_NAME} ${reference}`, + service_message: `expected failure for ${ES_TEST_INDEX_NAME} ${reference}`, retry: false, }); @@ -142,11 +146,11 @@ export default function ({ getService }: FtrProviderContext) { it(`shouldn't execute an action from another space`, async () => { const { body: createdAction } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .send({ name: 'My action', - actionTypeId: 'test.index-record', + connector_type_id: 'test.index-record', config: { unencrypted: `This value shouldn't get encrypted`, }, @@ -178,17 +182,19 @@ export default function ({ getService }: FtrProviderContext) { it('should handle execute request appropriately and have proper callCluster and savedObjectsClient authorization', async () => { const reference = `actions-execute-3:${Spaces.space1.id}`; const { body: createdAction } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .send({ name: 'My action', - actionTypeId: 'test.authorization', + connector_type_id: 'test.authorization', }) .expect(200); objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions'); const response = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action/${createdAction.id}/_execute`) + .post( + `${getUrlPrefix(Spaces.space1.id)}/api/actions/connector/${createdAction.id}/_execute` + ) .set('kbn-xsrf', 'foo') .send({ params: { @@ -220,11 +226,11 @@ export default function ({ getService }: FtrProviderContext) { it('should notify feature usage when executing a gold action type', async () => { const { body: createdAction } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .send({ name: 'Noop action type', - actionTypeId: 'test.noop', + connector_type_id: 'test.noop', secrets: {}, config: {}, }) @@ -233,7 +239,9 @@ export default function ({ getService }: FtrProviderContext) { const executionStart = new Date(); await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action/${createdAction.id}/_execute`) + .post( + `${getUrlPrefix(Spaces.space1.id)}/api/actions/connector/${createdAction.id}/_execute` + ) .set('kbn-xsrf', 'foo') .send({ params: {}, @@ -251,6 +259,72 @@ export default function ({ getService }: FtrProviderContext) { expect(noopFeature.last_used).to.be.a('string'); expect(new Date(noopFeature.last_used).getTime()).to.be.greaterThan(executionStart.getTime()); }); + + describe('legacy', () => { + it('should handle execute request appropriately', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My action', + actionTypeId: 'test.index-record', + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + secrets: { + encrypted: 'This value should be encrypted', + }, + }) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions'); + + const reference = `actions-execute-1:${Spaces.space1.id}:${createdAction.id}`; + const response = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action/${createdAction.id}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + reference, + index: ES_TEST_INDEX_NAME, + message: 'Testing 123', + }, + }); + + expect(response.status).to.eql(200); + }); + + it('should handle failed executions', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'failing action', + actionTypeId: 'test.failing', + }) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions'); + + const reference = `actions-failure-1:${Spaces.space1.id}:${createdAction.id}`; + const response = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action/${createdAction.id}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + reference, + index: ES_TEST_INDEX_NAME, + }, + }); + + expect(response.status).to.eql(200); + expect(response.body).to.eql({ + actionId: createdAction.id, + status: 'error', + message: 'an error occurred while running the action executor', + serviceMessage: `expected failure for ${ES_TEST_INDEX_NAME} ${reference}`, + retry: false, + }); + }); + }); }); interface ValidateEventLogParams { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts index f10755ae31d26..2219e9dda17d6 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts @@ -20,11 +20,11 @@ export default function getActionTests({ getService }: FtrProviderContext) { it('should handle get action request appropriately', async () => { const { body: createdAction } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .send({ name: 'My action', - actionTypeId: 'test.index-record', + connector_type_id: 'test.index-record', config: { unencrypted: `This value shouldn't get encrypted`, }, @@ -36,11 +36,11 @@ export default function getActionTests({ getService }: FtrProviderContext) { objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions'); await supertest - .get(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action/${createdAction.id}`) + .get(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector/${createdAction.id}`) .expect(200, { id: createdAction.id, - isPreconfigured: false, - actionTypeId: 'test.index-record', + is_preconfigured: false, + connector_type_id: 'test.index-record', name: 'My action', config: { unencrypted: `This value shouldn't get encrypted`, @@ -50,11 +50,11 @@ export default function getActionTests({ getService }: FtrProviderContext) { it(`action should't be acessible from another space`, async () => { const { body: createdAction } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .send({ name: 'My action', - actionTypeId: 'test.index-record', + connector_type_id: 'test.index-record', config: { unencrypted: `This value shouldn't get encrypted`, }, @@ -66,7 +66,7 @@ export default function getActionTests({ getService }: FtrProviderContext) { objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions'); await supertest - .get(`${getUrlPrefix(Spaces.other.id)}/api/actions/action/${createdAction.id}`) + .get(`${getUrlPrefix(Spaces.other.id)}/api/actions/connector/${createdAction.id}`) .expect(404, { statusCode: 404, error: 'Not Found', @@ -76,13 +76,82 @@ export default function getActionTests({ getService }: FtrProviderContext) { it('should handle get action request from preconfigured list', async () => { await supertest - .get(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action/my-slack1`) + .get(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector/my-slack1`) .expect(200, { id: 'my-slack1', - isPreconfigured: true, - actionTypeId: '.slack', + is_preconfigured: true, + connector_type_id: '.slack', name: 'Slack#xyz', }); }); + + describe('legacy', () => { + it('should handle get action request appropriately', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My action', + actionTypeId: 'test.index-record', + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + secrets: { + encrypted: 'This value should be encrypted', + }, + }) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions'); + + await supertest + .get(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action/${createdAction.id}`) + .expect(200, { + id: createdAction.id, + isPreconfigured: false, + actionTypeId: 'test.index-record', + name: 'My action', + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + }); + }); + + it(`action should't be acessible from another space`, async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My action', + actionTypeId: 'test.index-record', + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + secrets: { + encrypted: 'This value should be encrypted', + }, + }) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions'); + + await supertest + .get(`${getUrlPrefix(Spaces.other.id)}/api/actions/action/${createdAction.id}`) + .expect(404, { + statusCode: 404, + error: 'Not Found', + message: `Saved object [action/${createdAction.id}] not found`, + }); + }); + + it('should handle get action request from preconfigured list', async () => { + await supertest + .get(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action/my-slack1`) + .expect(200, { + id: 'my-slack1', + isPreconfigured: true, + actionTypeId: '.slack', + name: 'Slack#xyz', + }); + }); + }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts index 49befae793445..531df9d4ed19f 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts @@ -35,44 +35,44 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { .expect(200); objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions'); - await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/actions`).expect(200, [ + await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connectors`).expect(200, [ { id: createdAction.id, - isPreconfigured: false, + is_preconfigured: false, name: 'My action', - actionTypeId: 'test.index-record', + connector_type_id: 'test.index-record', config: { unencrypted: `This value shouldn't get encrypted`, }, - referencedByCount: 0, + referenced_by_count: 0, }, { id: 'preconfigured-es-index-action', - isPreconfigured: true, - actionTypeId: '.index', + is_preconfigured: true, + connector_type_id: '.index', name: 'preconfigured_es_index_action', - referencedByCount: 0, + referenced_by_count: 0, }, { id: 'my-slack1', - isPreconfigured: true, - actionTypeId: '.slack', + is_preconfigured: true, + connector_type_id: '.slack', name: 'Slack#xyz', - referencedByCount: 0, + referenced_by_count: 0, }, { id: 'custom-system-abc-connector', - isPreconfigured: true, - actionTypeId: 'system-abc-action-type', + is_preconfigured: true, + connector_type_id: 'system-abc-action-type', name: 'SystemABC', - referencedByCount: 0, + referenced_by_count: 0, }, { id: 'preconfigured.test.index-record', - isPreconfigured: true, - actionTypeId: 'test.index-record', + is_preconfigured: true, + connector_type_id: 'test.index-record', name: 'Test:_Preconfigured_Index_Record', - referencedByCount: 0, + referenced_by_count: 0, }, ]); }); @@ -94,36 +94,97 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { .expect(200); objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions'); - await supertest.get(`${getUrlPrefix(Spaces.other.id)}/api/actions`).expect(200, [ + await supertest.get(`${getUrlPrefix(Spaces.other.id)}/api/actions/connectors`).expect(200, [ { id: 'preconfigured-es-index-action', - isPreconfigured: true, - actionTypeId: '.index', + is_preconfigured: true, + connector_type_id: '.index', name: 'preconfigured_es_index_action', - referencedByCount: 0, + referenced_by_count: 0, }, { id: 'my-slack1', - isPreconfigured: true, - actionTypeId: '.slack', + is_preconfigured: true, + connector_type_id: '.slack', name: 'Slack#xyz', - referencedByCount: 0, + referenced_by_count: 0, }, { id: 'custom-system-abc-connector', - isPreconfigured: true, - actionTypeId: 'system-abc-action-type', + is_preconfigured: true, + connector_type_id: 'system-abc-action-type', name: 'SystemABC', - referencedByCount: 0, + referenced_by_count: 0, }, { id: 'preconfigured.test.index-record', - isPreconfigured: true, - actionTypeId: 'test.index-record', + is_preconfigured: true, + connector_type_id: 'test.index-record', name: 'Test:_Preconfigured_Index_Record', - referencedByCount: 0, + referenced_by_count: 0, }, ]); }); + + describe('legacy', () => { + it('should handle get all action request appropriately', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My action', + actionTypeId: 'test.index-record', + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + secrets: { + encrypted: 'This value should be encrypted', + }, + }) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions'); + + await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/actions`).expect(200, [ + { + id: createdAction.id, + isPreconfigured: false, + name: 'My action', + actionTypeId: 'test.index-record', + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + referencedByCount: 0, + }, + { + id: 'preconfigured-es-index-action', + isPreconfigured: true, + actionTypeId: '.index', + name: 'preconfigured_es_index_action', + referencedByCount: 0, + }, + { + id: 'my-slack1', + isPreconfigured: true, + actionTypeId: '.slack', + name: 'Slack#xyz', + referencedByCount: 0, + }, + { + id: 'custom-system-abc-connector', + isPreconfigured: true, + actionTypeId: 'system-abc-action-type', + name: 'SystemABC', + referencedByCount: 0, + }, + { + id: 'preconfigured.test.index-record', + isPreconfigured: true, + actionTypeId: 'test.index-record', + name: 'Test:_Preconfigured_Index_Record', + referencedByCount: 0, + }, + ]); + }); + }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts index 74e4cbb75f01c..d5056508e5de9 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts @@ -18,7 +18,7 @@ export default function actionsTests({ loadTestFile, getService }: FtrProviderCo loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./get_all')); loadTestFile(require.resolve('./get')); - loadTestFile(require.resolve('./list_action_types')); + loadTestFile(require.resolve('./connector_types')); loadTestFile(require.resolve('./update')); loadTestFile(require.resolve('./execute')); loadTestFile(require.resolve('./builtin_action_types/es_index')); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/update.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/update.ts index 80bdba539689d..4e0b4eac8da32 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/update.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/update.ts @@ -21,11 +21,11 @@ export default function updateActionTests({ getService }: FtrProviderContext) { it('should handle update action request appropriately', async () => { const { body: createdAction } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .send({ name: 'My action', - actionTypeId: 'test.index-record', + connector_type_id: 'test.index-record', config: { unencrypted: `This value shouldn't get encrypted`, }, @@ -37,7 +37,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) { objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions'); await supertest - .put(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action/${createdAction.id}`) + .put(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector/${createdAction.id}`) .set('kbn-xsrf', 'foo') .send({ name: 'My action updated', @@ -50,8 +50,8 @@ export default function updateActionTests({ getService }: FtrProviderContext) { }) .expect(200, { id: createdAction.id, - isPreconfigured: false, - actionTypeId: 'test.index-record', + is_preconfigured: false, + connector_type_id: 'test.index-record', name: 'My action updated', config: { unencrypted: `This value shouldn't get encrypted`, @@ -69,11 +69,11 @@ export default function updateActionTests({ getService }: FtrProviderContext) { it(`shouldn't update action from another space`, async () => { const { body: createdAction } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .send({ name: 'My action', - actionTypeId: 'test.index-record', + connector_type_id: 'test.index-record', config: { unencrypted: `This value shouldn't get encrypted`, }, @@ -105,7 +105,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) { it(`shouldn't update action from preconfigured list`, async () => { await supertest - .put(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action/custom-system-abc-connector`) + .put(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector/custom-system-abc-connector`) .set('kbn-xsrf', 'foo') .send({ name: 'My action updated', @@ -125,11 +125,11 @@ export default function updateActionTests({ getService }: FtrProviderContext) { it('should notify feature usage when editing a gold action type', async () => { const { body: createdAction } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`) + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .send({ name: 'Noop action type', - actionTypeId: 'test.noop', + connector_type_id: 'test.noop', secrets: {}, config: {}, }) @@ -138,7 +138,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) { const updateStart = new Date(); await supertest - .put(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action/${createdAction.id}`) + .put(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector/${createdAction.id}`) .set('kbn-xsrf', 'foo') .send({ name: 'Noop action type updated', @@ -158,5 +158,147 @@ export default function updateActionTests({ getService }: FtrProviderContext) { expect(noopFeature.last_used).to.be.a('string'); expect(new Date(noopFeature.last_used).getTime()).to.be.greaterThan(updateStart.getTime()); }); + + describe('legacy', () => { + it('should handle update action request appropriately', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My action', + actionTypeId: 'test.index-record', + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + secrets: { + encrypted: 'This value should be encrypted', + }, + }) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions'); + + await supertest + .put(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action/${createdAction.id}`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My action updated', + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + secrets: { + encrypted: 'This value should be encrypted', + }, + }) + .expect(200, { + id: createdAction.id, + isPreconfigured: false, + actionTypeId: 'test.index-record', + name: 'My action updated', + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + }); + + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: Spaces.space1.id, + type: 'action', + id: createdAction.id, + }); + }); + + it(`shouldn't update action from another space`, async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My action', + actionTypeId: 'test.index-record', + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + secrets: { + encrypted: 'This value should be encrypted', + }, + }) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions'); + + await supertest + .put(`${getUrlPrefix(Spaces.other.id)}/api/actions/action/${createdAction.id}`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My action updated', + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + secrets: { + encrypted: 'This value should be encrypted', + }, + }) + .expect(404, { + statusCode: 404, + error: 'Not Found', + message: `Saved object [action/${createdAction.id}] not found`, + }); + }); + + it(`shouldn't update action from preconfigured list`, async () => { + await supertest + .put(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action/custom-system-abc-connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My action updated', + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + secrets: { + encrypted: 'This value should be encrypted', + }, + }) + .expect(400, { + statusCode: 400, + error: 'Bad Request', + message: `Preconfigured action custom-system-abc-connector is not allowed to update.`, + }); + }); + + it('should notify feature usage when editing a gold action type', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'Noop action type', + actionTypeId: 'test.noop', + secrets: {}, + config: {}, + }) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions'); + + const updateStart = new Date(); + await supertest + .put(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action/${createdAction.id}`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'Noop action type updated', + secrets: {}, + config: {}, + }) + .expect(200); + + const { + body: { features }, + } = await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/licensing/feature_usage`); + expect(features).to.be.an(Array); + const noopFeature = features.find( + (feature: { name: string }) => feature.name === 'Connector: Test: Noop' + ); + expect(noopFeature).to.be.ok(); + expect(noopFeature.last_used).to.be.a('string'); + expect(new Date(noopFeature.last_used).getTime()).to.be.greaterThan(updateStart.getTime()); + }); + }); }); } diff --git a/x-pack/test/fleet_api_integration/apis/epm/template.ts b/x-pack/test/fleet_api_integration/apis/epm/template.ts index d79452ca0eb6f..517d2c77d430d 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/template.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/template.ts @@ -7,6 +7,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { appContextService } from '../../../../plugins/fleet/server/services'; import { getTemplate } from '../../../../plugins/fleet/server/services/epm/elasticsearch/template/template'; export default function ({ getService }: FtrProviderContext) { @@ -20,12 +21,31 @@ export default function ({ getService }: FtrProviderContext) { }, }, }; + const fields = [ + { + name: 'foo', + type: 'keyword', + }, + ]; + // This test was inspired by https://github.com/elastic/kibana/blob/master/x-pack/test/api_integration/apis/monitoring/common/mappings_exist.js describe('EPM - template', async () => { + beforeEach(async () => { + appContextService.start({ + // @ts-ignore + elasticsearch: { client: {} }, + // @ts-ignore + logger: { + warn: () => {}, + }, + }); + }); + it('can be loaded', async () => { const template = getTemplate({ type: 'logs', templateIndexPattern, + fields, mappings, packageName: 'system', composedOfTemplates: [], diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_panel_action.ts b/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_panel_action.ts index 4b9700747dd84..c78c716364c4b 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_panel_action.ts +++ b/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_panel_action.ts @@ -24,9 +24,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const kibanaServer = getService('kibanaServer'); - // FLAKY: https://github.com/elastic/kibana/issues/84011 - // FLAKY: https://github.com/elastic/kibana/issues/84012 - describe.skip('Explore underlying data - panel action', function () { + describe('Explore underlying data - panel action', function () { before( 'change default index pattern to verify action navigates to correct index pattern', async () => { diff --git a/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts b/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts index 5c1ec2f39d663..44cd2cda7e1af 100644 --- a/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts +++ b/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts @@ -11,6 +11,8 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'indexLifecycleManagement']); const log = getService('log'); + const retry = getService('retry'); + const testSubjects = getService('testSubjects'); describe('Home page', function () { before(async () => { @@ -25,5 +27,26 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const createPolicyButton = await pageObjects.indexLifecycleManagement.createPolicyButton(); expect(await createPolicyButton.isDisplayed()).to.be(true); }); + + it('Create new policy with Warm and Cold Phases', async () => { + const policyName = 'testPolicy1'; + await pageObjects.indexLifecycleManagement.createNewPolicyAndSave( + policyName, + true, + true, + false + ); + + await retry.waitFor('navigation back to home page.', async () => { + return (await testSubjects.getVisibleText('sectionHeading')) === 'Index Lifecycle Policies'; + }); + + const allPolicies = await pageObjects.indexLifecycleManagement.getPolicyList(); + const filteredPolicies = allPolicies.filter(function (policy) { + return policy.name === policyName; + }); + + expect(filteredPolicies.length).to.be(1); + }); }); }; diff --git a/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js b/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js index 8af2e45b59838..9847923c1bf5b 100644 --- a/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js +++ b/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js @@ -11,7 +11,8 @@ export default function ({ getPageObjects, getService }) { const PageObjects = getPageObjects(['maps']); const security = getService('security'); - describe('auto fit map to bounds', () => { + // FLAKY: https://github.com/elastic/kibana/issues/93737 + describe.skip('auto fit map to bounds', () => { describe('initial location', () => { before(async () => { await security.testUser.setRoles(['global_maps_all', 'test_logstash_reader']); diff --git a/x-pack/test/functional/apps/maps/documents_source/search_hits.js b/x-pack/test/functional/apps/maps/documents_source/search_hits.js index 4da36a44cff08..2663242406a75 100644 --- a/x-pack/test/functional/apps/maps/documents_source/search_hits.js +++ b/x-pack/test/functional/apps/maps/documents_source/search_hits.js @@ -84,7 +84,8 @@ export default function ({ getPageObjects, getService }) { expect(beforeQueryRefreshTimestamp).not.to.equal(afterQueryRefreshTimestamp); }); - it('should apply query to fit to bounds', async () => { + // https://github.com/elastic/kibana/issues/93718 + it.skip('should apply query to fit to bounds', async () => { // Set view to other side of world so no matching results await PageObjects.maps.setView(-15, -100, 6); await PageObjects.maps.clickFitToBounds('logstash'); diff --git a/x-pack/test/functional/page_objects/index_lifecycle_management_page.ts b/x-pack/test/functional/page_objects/index_lifecycle_management_page.ts index ddf46926f122a..f47e79260e61c 100644 --- a/x-pack/test/functional/page_objects/index_lifecycle_management_page.ts +++ b/x-pack/test/functional/page_objects/index_lifecycle_management_page.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { map as mapAsync } from 'bluebird'; import { FtrProviderContext } from '../ftr_provider_context'; export function IndexLifecycleManagementPageProvider({ getService }: FtrProviderContext) { @@ -50,8 +50,34 @@ export function IndexLifecycleManagementPageProvider({ getService }: FtrProvider coldEnabled: boolean = false, deletePhaseEnabled: boolean = false ) { + await testSubjects.click('createPolicyButton'); await this.fillNewPolicyForm(policyName, warmEnabled, coldEnabled, deletePhaseEnabled); await this.saveNewPolicy(); }, + + async getPolicyList() { + const policies = await testSubjects.findAll('policyTableRow'); + return mapAsync(policies, async (policy) => { + const policyNameElement = await policy.findByTestSubject('policyTableCell-name'); + const policyLinkedIndicesElement = await policy.findByTestSubject( + 'policyTableCell-linkedIndices' + ); + const policyVersionElement = await policy.findByTestSubject('policyTableCell-version'); + const policyModifiedDateElement = await policy.findByTestSubject( + 'policyTableCell-modified_date' + ); + const policyActionsButtonElement = await policy.findByTestSubject( + 'policyActionsContextMenuButton' + ); + + return { + name: await policyNameElement.getVisibleText(), + linkedIndices: await policyLinkedIndicesElement.getVisibleText(), + version: await policyVersionElement.getVisibleText(), + modifiedDate: await policyModifiedDateElement.getVisibleText(), + actionsButton: policyActionsButtonElement, + }; + }); + }, }; } diff --git a/x-pack/test/upgrade/apps/canvas/canvas_smoke_tests.ts b/x-pack/test/upgrade/apps/canvas/canvas_smoke_tests.ts new file mode 100644 index 0000000000000..c7db9127e01bd --- /dev/null +++ b/x-pack/test/upgrade/apps/canvas/canvas_smoke_tests.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const retry = getService('retry'); + const PageObjects = getPageObjects(['common', 'header']); + const testSubjects = getService('testSubjects'); + + describe('canvas smoke tests', function describeIndexTests() { + const spaces = [ + { space: 'default', basePath: '' }, + { space: 'automation', basePath: 's/automation' }, + ]; + + const canvasTests = [ + { + name: 'flights', + id: 'workpad-a474e74b-aedc-47c3-894a-db77e62c41e0/page/1', + numElements: 35, + }, + { name: 'logs', id: 'workpad-5563cc40-5760-4afe-bf33-9da72fac53b7/page/1', numElements: 57 }, + { + name: 'ecommerce', + id: 'workpad-e08b9bdb-ec14-4339-94c4-063bddfd610e/page/1', + numElements: 16, + }, + { + name: 'ecommerce', + id: 'workpad-e08b9bdb-ec14-4339-94c4-063bddfd610e/page/2', + numElements: 9, + }, + ]; + + spaces.forEach(({ space, basePath }) => { + canvasTests.forEach(({ name, id, numElements }) => { + describe('space ' + space + ' name ' + name, () => { + beforeEach(async () => { + await PageObjects.common.navigateToActualUrl('canvas', 'workpad/' + id, { + basePath, + }); + await PageObjects.header.waitUntilLoadingHasFinished(); + }); + it('renders elements on workpad', async () => { + await retry.try(async () => { + const elements = await testSubjects.findAll( + 'canvasWorkpadPage > canvasWorkpadPageElementContent' + ); + expect(elements).to.have.length(numElements); + }); + }); + }); + }); + }); + }); +} diff --git a/x-pack/test/upgrade/apps/canvas/index.js b/x-pack/test/upgrade/apps/canvas/index.js new file mode 100644 index 0000000000000..0ecc2e98ea67a --- /dev/null +++ b/x-pack/test/upgrade/apps/canvas/index.js @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export default ({ loadTestFile }) => { + describe('upgrade', function () { + loadTestFile(require.resolve('./canvas_smoke_tests')); + }); +}; diff --git a/x-pack/test/upgrade/apps/dashboard/dashboard_smoke_tests.ts b/x-pack/test/upgrade/apps/dashboard/dashboard_smoke_tests.ts new file mode 100644 index 0000000000000..9efc9224b2438 --- /dev/null +++ b/x-pack/test/upgrade/apps/dashboard/dashboard_smoke_tests.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import moment from 'moment'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const find = getService('find'); + const log = getService('log'); + const pieChart = getService('pieChart'); + const renderable = getService('renderable'); + const dashboardExpect = getService('dashboardExpect'); + const PageObjects = getPageObjects(['common', 'header', 'home', 'dashboard', 'timePicker']); + + describe('dashboard smoke tests', function describeIndexTests() { + const spaces = [ + { space: 'default', basePath: '' }, + { space: 'automation', basePath: 's/automation' }, + ]; + + const dashboardTests = [ + { name: 'flights', numPanels: 19 }, + { name: 'logs', numPanels: 11 }, + { name: 'ecommerce', numPanels: 12 }, + ]; + + spaces.forEach(({ space, basePath }) => { + describe('space ' + space, () => { + beforeEach(async () => { + await PageObjects.common.navigateToActualUrl('home', '/tutorial_directory/sampleData', { + basePath, + }); + await PageObjects.header.waitUntilLoadingHasFinished(); + }); + dashboardTests.forEach(({ name, numPanels }) => { + it('should launch sample ' + name + ' data set dashboard', async () => { + await PageObjects.home.launchSampleDashboard(name); + await PageObjects.header.waitUntilLoadingHasFinished(); + await renderable.waitForRender(); + const todayYearMonthDay = moment().format('MMM D, YYYY'); + const fromTime = `${todayYearMonthDay} @ 00:00:00.000`; + const toTime = `${todayYearMonthDay} @ 23:59:59.999`; + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.be(numPanels); + }); + }); + it('should render visualizations', async () => { + await PageObjects.home.launchSampleDashboard('flights'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await renderable.waitForRender(); + log.debug('Checking pie charts rendered'); + await pieChart.expectPieSliceCount(4); + // https://github.com/elastic/kibana/issues/92887 + // log.debug('Checking area, bar and heatmap charts rendered'); + // await dashboardExpect.seriesElementCount(15); + log.debug('Checking saved searches rendered'); + await dashboardExpect.savedSearchRowCount(50); + log.debug('Checking input controls rendered'); + await dashboardExpect.inputControlItemCount(3); + log.debug('Checking tag cloud rendered'); + await dashboardExpect.tagCloudWithValuesFound([ + 'Sunny', + 'Rain', + 'Clear', + 'Cloudy', + 'Hail', + ]); + log.debug('Checking vega chart rendered'); + const tsvb = await find.existsByCssSelector('.vgaVis__view'); + expect(tsvb).to.be(true); + }); + }); + }); + }); +} diff --git a/x-pack/test/upgrade/apps/dashboard/index.js b/x-pack/test/upgrade/apps/dashboard/index.js new file mode 100644 index 0000000000000..b12d1270f79f9 --- /dev/null +++ b/x-pack/test/upgrade/apps/dashboard/index.js @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export default ({ loadTestFile }) => { + describe('upgrade', function () { + loadTestFile(require.resolve('./dashboard_smoke_tests')); + }); +}; diff --git a/x-pack/test/upgrade/apps/maps/index.js b/x-pack/test/upgrade/apps/maps/index.js new file mode 100644 index 0000000000000..57d175a62ceb3 --- /dev/null +++ b/x-pack/test/upgrade/apps/maps/index.js @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export default ({ loadTestFile }) => { + describe('upgrade', function () { + loadTestFile(require.resolve('./maps_smoke_tests')); + }); +}; diff --git a/x-pack/test/upgrade/apps/maps/maps_smoke_tests.ts b/x-pack/test/upgrade/apps/maps/maps_smoke_tests.ts new file mode 100644 index 0000000000000..7ec83aad26641 --- /dev/null +++ b/x-pack/test/upgrade/apps/maps/maps_smoke_tests.ts @@ -0,0 +1,174 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; +import { UI_SETTINGS } from '../../../../../src/plugins/data/common'; + +export default function ({ + getPageObjects, + getService, + updateBaselines, +}: FtrProviderContext & { updateBaselines: boolean }) { + const PageObjects = getPageObjects(['common', 'maps', 'header', 'home', 'timePicker']); + const screenshot = getService('screenshots'); + const testSubjects = getService('testSubjects'); + const kibanaServer = getService('kibanaServer'); + const SAMPLE_DATA_RANGE = `[ + { + "from": "now-30d", + "to": "now+40d", + "display": "sample data range" + }, + { + "from": "now/d", + "to": "now/d", + "display": "Today" + }, + { + "from": "now/w", + "to": "now/w", + "display": "This week" + }, + { + "from": "now-15m", + "to": "now", + "display": "Last 15 minutes" + }, + { + "from": "now-30m", + "to": "now", + "display": "Last 30 minutes" + }, + { + "from": "now-1h", + "to": "now", + "display": "Last 1 hour" + }, + { + "from": "now-24h", + "to": "now", + "display": "Last 24 hours" + }, + { + "from": "now-7d", + "to": "now", + "display": "Last 7 days" + }, + { + "from": "now-30d", + "to": "now", + "display": "Last 30 days" + }, + { + "from": "now-90d", + "to": "now", + "display": "Last 90 days" + }, + { + "from": "now-1y", + "to": "now", + "display": "Last 1 year" + } + ]`; + + // Only update the baseline images from Jenkins session images after comparing them + // These tests might fail locally because of scaling factors and resolution. + + describe('maps smoke tests', function describeIndexTests() { + const spaces = [ + { space: 'default', basePath: '' }, + { space: 'automation', basePath: 's/automation' }, + ]; + + before(async () => { + await kibanaServer.uiSettings.update({ + [UI_SETTINGS.TIMEPICKER_QUICK_RANGES]: SAMPLE_DATA_RANGE, + }); + }); + + spaces.forEach(({ space, basePath }) => { + describe('space ' + space + ' ecommerce', () => { + before(async () => { + await PageObjects.common.navigateToActualUrl('home', '/tutorial_directory/sampleData', { + basePath, + }); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.home.addSampleDataSet('ecommerce'); + await PageObjects.maps.loadSavedMap('[eCommerce] Orders by Country'); + await PageObjects.maps.toggleLayerVisibility('Road map'); + await PageObjects.maps.toggleLayerVisibility('United Kingdom'); + await PageObjects.maps.toggleLayerVisibility('France'); + await PageObjects.maps.toggleLayerVisibility('United States'); + await PageObjects.maps.toggleLayerVisibility('World Countries'); + await PageObjects.timePicker.setCommonlyUsedTime('sample_data range'); + await PageObjects.maps.enterFullScreen(); + await PageObjects.maps.closeLegend(); + const mapContainerElement = await testSubjects.find('mapContainer'); + await mapContainerElement.moveMouseTo({ xOffset: 0, yOffset: 0 }); + }); + it('should load layers', async () => { + const percentDifference = await screenshot.compareAgainstBaseline( + 'ecommerce_map', + updateBaselines + ); + expect(percentDifference).to.be.lessThan(0.02); + }); + }); + + describe('space ' + space + ' flights', () => { + before(async () => { + await PageObjects.common.navigateToActualUrl('home', '/tutorial_directory/sampleData', { + basePath, + }); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.home.addSampleDataSet('flights'); + await PageObjects.maps.loadSavedMap('[Flights] Origin and Destination Flight Time'); + await PageObjects.maps.toggleLayerVisibility('Road map'); + await PageObjects.timePicker.setCommonlyUsedTime('sample_data range'); + await PageObjects.maps.enterFullScreen(); + await PageObjects.maps.closeLegend(); + const mapContainerElement = await testSubjects.find('mapContainer'); + await mapContainerElement.moveMouseTo({ xOffset: 0, yOffset: 0 }); + }); + it('should load saved object and display layers', async () => { + const percentDifference = await screenshot.compareAgainstBaseline( + 'flights_map', + updateBaselines + ); + expect(percentDifference).to.be.lessThan(0.02); + }); + }); + + describe('space ' + space + ' web logs', () => { + before(async () => { + await PageObjects.common.navigateToActualUrl('home', '/tutorial_directory/sampleData', { + basePath, + }); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.home.addSampleDataSet('logs'); + await PageObjects.maps.loadSavedMap('[Logs] Total Requests and Bytes'); + await PageObjects.maps.toggleLayerVisibility('Road map'); + await PageObjects.maps.toggleLayerVisibility('Total Requests by Country'); + await PageObjects.timePicker.setCommonlyUsedTime('sample_data range'); + await PageObjects.maps.enterFullScreen(); + await PageObjects.maps.closeLegend(); + const mapContainerElement = await testSubjects.find('mapContainer'); + await mapContainerElement.moveMouseTo({ xOffset: 0, yOffset: 0 }); + }); + it('should load saved object and display layers', async () => { + const percentDifference = await screenshot.compareAgainstBaseline( + 'web_logs_map', + updateBaselines + ); + expect(percentDifference).to.be.lessThan(0.02); + }); + }); + }); + }); +} diff --git a/x-pack/test/upgrade/apps/reporting/index.js b/x-pack/test/upgrade/apps/reporting/index.js new file mode 100644 index 0000000000000..5bd5081a3568c --- /dev/null +++ b/x-pack/test/upgrade/apps/reporting/index.js @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export default ({ loadTestFile }) => { + describe('upgrade', function () { + loadTestFile(require.resolve('./reporting_smoke_tests')); + }); +}; diff --git a/x-pack/test/upgrade/apps/reporting/reporting_smoke_tests.ts b/x-pack/test/upgrade/apps/reporting/reporting_smoke_tests.ts new file mode 100644 index 0000000000000..41b5aab35c84d --- /dev/null +++ b/x-pack/test/upgrade/apps/reporting/reporting_smoke_tests.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { parse } from 'url'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { ReportingUsageStats } from '../../reporting_services'; + +interface UsageStats { + reporting: ReportingUsageStats; +} + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const reportingAPI = getService('reportingAPI'); + const usageAPI = getService('usageAPI'); + const find = getService('find'); + const browser = getService('browser'); + const PageObjects = getPageObjects(['common', 'header', 'home', 'dashboard', 'share']); + const testSubjects = getService('testSubjects'); + + const spaces = [ + { space: 'default', basePath: '' }, + { space: 'automation', basePath: 's/automation' }, + ]; + + const reportingTests = [ + { name: 'flights', type: 'pdf', link: 'PDF Reports' }, + { name: 'flights', type: 'pdf_optimize', link: 'PDF Reports' }, + { name: 'flights', type: 'png', link: 'PNG Reports' }, + { name: 'logs', type: 'pdf', link: 'PDF Reports' }, + { name: 'logs', type: 'pdf_optimize', link: 'PDF Reports' }, + { name: 'logs', type: 'png', link: 'PNG Reports' }, + { name: 'ecommerce', type: 'pdf', link: 'PDF Reports' }, + { name: 'ecommerce', type: 'pdf_optimize', link: 'PDF Reports' }, + { name: 'ecommerce', type: 'png', link: 'PNG Reports' }, + ]; + + describe('reporting smoke tests', () => { + let completedReportCount: number; + let usage: UsageStats; + describe('initial state', () => { + before(async () => { + usage = (await usageAPI.getUsageStats()) as UsageStats; + }); + + it('shows reporting as available and enabled', async () => { + expect(usage.reporting.available).to.be(true); + expect(usage.reporting.enabled).to.be(true); + }); + }); + spaces.forEach(({ space, basePath }) => { + describe('generate report space ' + space, () => { + before(async () => { + usage = (await usageAPI.getUsageStats()) as UsageStats; + completedReportCount = reportingAPI.getCompletedReportCount(usage); + }); + beforeEach(async () => { + await PageObjects.common.navigateToActualUrl('home', '/tutorial_directory/sampleData', { + basePath, + }); + await PageObjects.header.waitUntilLoadingHasFinished(); + }); + reportingTests.forEach(({ name, type, link }) => { + it('name ' + name + ' type ' + type, async () => { + await PageObjects.home.launchSampleDashboard(name); + await PageObjects.share.openShareMenuItem(link); + if (type === 'pdf_optimize') { + await testSubjects.click('usePrintLayout'); + } + const postUrl = await find.byXPath(`//button[descendant::*[text()='Copy POST URL']]`); + await postUrl.click(); + const url = await browser.getClipboardValue(); + await reportingAPI.expectAllJobsToFinishSuccessfully( + await Promise.all([ + reportingAPI.postJob(parse(url).pathname + '?' + parse(url).query), + ]) + ); + usage = (await usageAPI.getUsageStats()) as UsageStats; + reportingAPI.expectCompletedReportCount(usage, completedReportCount + 1); + }); + }); + }); + }); + }); +} diff --git a/x-pack/test/upgrade/config.ts b/x-pack/test/upgrade/config.ts new file mode 100644 index 0000000000000..86555335ba47b --- /dev/null +++ b/x-pack/test/upgrade/config.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { pageObjects } from './page_objects'; +import { ReportingAPIProvider } from './reporting_services'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const apiConfig = await readConfigFile(require.resolve('../api_integration/config')); + const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + + return { + ...functionalConfig.getAll(), + + testFiles: [ + require.resolve('./apps/canvas'), + require.resolve('./apps/dashboard'), + require.resolve('./apps/maps'), + require.resolve('./apps/reporting'), + ], + + pageObjects, + + services: { + ...apiConfig.get('services'), + ...functionalConfig.get('services'), + reportingAPI: ReportingAPIProvider, + }, + + junit: { + reportName: 'Upgrade Tests', + }, + + security: { + disableTestUser: true, + }, + }; +} diff --git a/x-pack/test/upgrade/ftr_provider_context.d.ts b/x-pack/test/upgrade/ftr_provider_context.d.ts new file mode 100644 index 0000000000000..ec28c00e72e47 --- /dev/null +++ b/x-pack/test/upgrade/ftr_provider_context.d.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; + +import { pageObjects } from './page_objects'; +import { services } from './services'; + +export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/upgrade/page_objects.ts b/x-pack/test/upgrade/page_objects.ts new file mode 100644 index 0000000000000..c8b0c9050dbb9 --- /dev/null +++ b/x-pack/test/upgrade/page_objects.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { pageObjects } from '../functional/page_objects'; + +export { pageObjects }; diff --git a/x-pack/test/upgrade/reporting_services.ts b/x-pack/test/upgrade/reporting_services.ts new file mode 100644 index 0000000000000..13186cb9b2a75 --- /dev/null +++ b/x-pack/test/upgrade/reporting_services.ts @@ -0,0 +1,213 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { indexTimestamp } from '../../plugins/reporting/server/lib/store/index_timestamp'; +import { services as xpackServices } from '../functional/services'; +import { services as apiIntegrationServices } from '../api_integration/services'; +import { FtrProviderContext } from './ftr_provider_context'; + +interface PDFAppCounts { + app: { + [appName: string]: number; + }; + layout: { + [layoutType: string]: number; + }; +} + +export interface ReportingUsageStats { + available: boolean; + enabled: boolean; + total: number; + last_7_days: { + total: number; + printable_pdf: PDFAppCounts; + [jobType: string]: any; + }; + printable_pdf: PDFAppCounts; + status: any; + [jobType: string]: any; +} + +interface UsageStats { + reporting: ReportingUsageStats; +} + +function removeWhitespace(str: string) { + return str.replace(/\s/g, ''); +} + +export function ReportingAPIProvider({ getService }: FtrProviderContext) { + const log = getService('log'); + const supertest = getService('supertest'); + const esSupertest = getService('esSupertest'); + const retry = getService('retry'); + + return { + async waitForJobToFinish(downloadReportPath: string) { + log.debug(`Waiting for job to finish: ${downloadReportPath}`); + const JOB_IS_PENDING_CODE = 503; + + const statusCode = await new Promise((resolve) => { + const intervalId = setInterval(async () => { + const response = (await supertest + .get(downloadReportPath) + .responseType('blob') + .set('kbn-xsrf', 'xxx')) as any; + if (response.statusCode === 503) { + log.debug(`Report at path ${downloadReportPath} is pending`); + } else if (response.statusCode === 200) { + log.debug(`Report at path ${downloadReportPath} is complete`); + } else { + log.debug(`Report at path ${downloadReportPath} returned code ${response.statusCode}`); + } + if (response.statusCode !== JOB_IS_PENDING_CODE) { + clearInterval(intervalId); + resolve(response.statusCode); + } + }, 1500); + }); + + expect(statusCode).to.be(200); + }, + + async expectAllJobsToFinishSuccessfully(jobPaths: string[]) { + await Promise.all( + jobPaths.map(async (path) => { + await this.waitForJobToFinish(path); + }) + ); + }, + + async postJob(apiPath: string): Promise { + log.debug(`ReportingAPI.postJob(${apiPath})`); + const { body } = await supertest + .post(removeWhitespace(apiPath)) + .set('kbn-xsrf', 'xxx') + .expect(200); + return body.path; + }, + + async postJobJSON(apiPath: string, jobJSON: object = {}): Promise { + log.debug(`ReportingAPI.postJobJSON((${apiPath}): ${JSON.stringify(jobJSON)})`); + const { body } = await supertest.post(apiPath).set('kbn-xsrf', 'xxx').send(jobJSON); + return body.path; + }, + + /** + * + * @return {Promise} A function to call to clean up the index alias that was added. + */ + async coerceReportsIntoExistingIndex(indexName: string) { + log.debug(`ReportingAPI.coerceReportsIntoExistingIndex(${indexName})`); + + // Adding an index alias coerces the report to be generated on an existing index which means any new + // index schema won't be applied. This is important if a point release updated the schema. Reports may still + // be inserted into an existing index before the new schema is applied. + const timestampForIndex = indexTimestamp('week', '.'); + await esSupertest + .post('/_aliases') + .send({ + actions: [ + { + add: { index: indexName, alias: `.reporting-${timestampForIndex}` }, + }, + ], + }) + .expect(200); + + return async () => { + await esSupertest + .post('/_aliases') + .send({ + actions: [ + { + remove: { index: indexName, alias: `.reporting-${timestampForIndex}` }, + }, + ], + }) + .expect(200); + }; + }, + + async deleteAllReports() { + log.debug('ReportingAPI.deleteAllReports'); + + // ignores 409 errs and keeps retrying + await retry.tryForTime(5000, async () => { + await esSupertest + .post('/.reporting*/_delete_by_query') + .send({ query: { match_all: {} } }) + .expect(200); + }); + }, + + expectRecentPdfAppStats(stats: UsageStats, app: string, count: number) { + expect(stats.reporting.last_7_days.printable_pdf.app[app]).to.be(count); + }, + + expectAllTimePdfAppStats(stats: UsageStats, app: string, count: number) { + expect(stats.reporting.printable_pdf.app[app]).to.be(count); + }, + + expectRecentPdfLayoutStats(stats: UsageStats, layout: string, count: number) { + expect(stats.reporting.last_7_days.printable_pdf.layout[layout]).to.be(count); + }, + + expectAllTimePdfLayoutStats(stats: UsageStats, layout: string, count: number) { + expect(stats.reporting.printable_pdf.layout[layout]).to.be(count); + }, + + expectRecentJobTypeTotalStats(stats: UsageStats, jobType: string, count: number) { + expect(stats.reporting.last_7_days[jobType].total).to.be(count); + }, + + expectAllTimeJobTypeTotalStats(stats: UsageStats, jobType: string, count: number) { + expect(stats.reporting[jobType].total).to.be(count); + }, + + getCompletedReportCount(stats: UsageStats) { + return stats.reporting.status.completed; + }, + + expectCompletedReportCount(stats: UsageStats, count: number) { + expect(this.getCompletedReportCount(stats)).to.be(count); + }, + + getRecentPdfAppStats(stats: UsageStats, app: string) { + return stats.reporting.last_7_days.printable_pdf.app[app]; + }, + + getAllTimePdfAppStats(stats: UsageStats, app: string) { + return stats.reporting.printable_pdf.app[app]; + }, + + getRecentPdfLayoutStats(stats: UsageStats, layout: string) { + return stats.reporting.last_7_days.printable_pdf.layout[layout]; + }, + + getAllTimePdfLayoutStats(stats: UsageStats, layout: string) { + return stats.reporting.printable_pdf.layout[layout]; + }, + + getRecentJobTypeTotalStats(stats: UsageStats, jobType: string) { + return stats.reporting.last_7_days[jobType].total; + }, + + getAllTimeJobTypeTotalStats(stats: UsageStats, jobType: string) { + return stats.reporting[jobType].total; + }, + }; +} + +export const services = { + ...xpackServices, + supertestWithoutAuth: apiIntegrationServices.supertestWithoutAuth, + usageAPI: apiIntegrationServices.usageAPI, + reportingAPI: ReportingAPIProvider, +}; diff --git a/x-pack/test/upgrade/services.ts b/x-pack/test/upgrade/services.ts new file mode 100644 index 0000000000000..cb49abe5e2011 --- /dev/null +++ b/x-pack/test/upgrade/services.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { services as functionalServices } from '../functional/services'; +import { services as reportingServices } from './reporting_services'; + +export const services = { + ...functionalServices, + ...reportingServices, +}; diff --git a/yarn.lock b/yarn.lock index efe77edbaed81..466853582e230 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7722,13 +7722,6 @@ axobject-query@^2.0.2: dependencies: ast-types-flow "0.0.7" -b64@4.x.x: - version "4.1.2" - resolved "https://registry.yarnpkg.com/b64/-/b64-4.1.2.tgz#7015372ba8101f7fb18da070717a93c28c8580d8" - integrity sha512-+GUspBxlH3CJaxMUGUE1EBoWM6RKgWiYwUDal0qdf8m3ArnXNN1KzKVo5HOnE/FSq4HHyWf3TlHLsZI8PKQgrQ== - dependencies: - hoek "6.x.x" - babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" @@ -8493,14 +8486,6 @@ bottleneck@^2.15.3: resolved "https://registry.yarnpkg.com/bottleneck/-/bottleneck-2.18.0.tgz#41fa63ae185b65435d789d1700334bc48222dacf" integrity sha512-U1xiBRaokw4yEguzikOl0VrnZp6uekjpmfrh6rKtr1D+/jFjYCL6J83ZXlGtlBDwVdTmJJ+4Lg5FpB3xmLSiyA== -bounce@1.x.x: - version "1.2.3" - resolved "https://registry.yarnpkg.com/bounce/-/bounce-1.2.3.tgz#2b286d36eb21d5f08fe672dd8cd37a109baad121" - integrity sha512-3G7B8CyBnip5EahCZJjnvQ1HLyArC6P5e+xcolo13BVI9ogFaDOsNMAE7FIWliHtIkYI8/nTRCvCY9tZa3Mu4g== - dependencies: - boom "7.x.x" - hoek "6.x.x" - bowser@^1.7.3: version "1.9.4" resolved "https://registry.yarnpkg.com/bowser/-/bowser-1.9.4.tgz#890c58a2813a9d3243704334fa81b96a5c150c9a" @@ -10591,13 +10576,6 @@ crypt@~0.0.1: resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs= -cryptiles@4.x.x: - version "4.1.3" - resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-4.1.3.tgz#2461d3390ea0b82c643a6ba79f0ed491b0934c25" - integrity sha512-gT9nyTMSUC1JnziQpPbxKGBbUg8VL7Zn2NB4E1cJYvuXdElHrwxrV9bmltZGDzet45zSDGyYceueke1TjynGzw== - dependencies: - boom "7.x.x" - crypto-browserify@^3.0.0, crypto-browserify@^3.11.0: version "3.12.0" resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" @@ -16600,16 +16578,6 @@ ipaddr.js@2.0.0: resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.0.0.tgz#77ccccc8063ae71ab65c55f21b090698e763fc6e" integrity sha512-S54H9mIj0rbxRIyrDMEuuER86LdlgUg9FSeZ8duQb6CUG2iRrA36MYVQBSprTF/ZeAwvyQ5mDGuNvIPM0BIl3w== -iron@5.x.x: - version "5.0.6" - resolved "https://registry.yarnpkg.com/iron/-/iron-5.0.6.tgz#7121d4a6e3ac2f65e4d02971646fea1995434744" - integrity sha512-zYUMOSkEXGBdwlV/AXF9zJC0aLuTJUKHkGeYS5I2g225M5i6SrxQyGJGhPgOR8BK1omL6N5i6TcwfsXbP8/Exw== - dependencies: - b64 "4.x.x" - boom "7.x.x" - cryptiles "4.x.x" - hoek "6.x.x" - irregular-plurals@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/irregular-plurals/-/irregular-plurals-3.2.0.tgz#b19c490a0723798db51b235d7e39add44dab0822" @@ -18039,7 +18007,7 @@ jju@~1.4.0: resolved "https://registry.yarnpkg.com/jju/-/jju-1.4.0.tgz#a3abe2718af241a2b2904f84a625970f389ae32a" integrity sha1-o6vicYryQaKykE+EpiWXDzia4yo= -joi@13.x.x, joi@^13.5.2: +joi@^13.5.2: version "13.7.0" resolved "https://registry.yarnpkg.com/joi/-/joi-13.7.0.tgz#cfd85ebfe67e8a1900432400b4d03bbd93fb879f" integrity sha512-xuY5VkHfeOYK3Hdi91ulocfuFopwgbSORmIwzcwHKESQhC7w1kD5jaVSPnqDxS2I8t3RZ9omCKAxNwXN5zG1/Q== @@ -26128,18 +26096,6 @@ state-toggle@^1.0.0: resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.0.tgz#d20f9a616bb4f0c3b98b91922d25b640aa2bc425" integrity sha1-0g+aYWu08MO5i5GSLSW2QKorxCU= -statehood@6.0.6: - version "6.0.6" - resolved "https://registry.yarnpkg.com/statehood/-/statehood-6.0.6.tgz#0dbd7c50774d3f61a24e42b0673093bbc81fa5f0" - integrity sha512-jR45n5ZMAkasw0xoE9j9TuLmJv4Sa3AkXe+6yIFT6a07kXYHgSbuD2OVGECdZGFxTmvNqLwL1iRIgvq6O6rq+A== - dependencies: - boom "7.x.x" - bounce "1.x.x" - cryptiles "4.x.x" - hoek "5.x.x" - iron "5.x.x" - joi "13.x.x" - static-eval@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/static-eval/-/static-eval-2.0.5.tgz#f0782e66999c4b3651cda99d9ce59c507d188f71"