diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml index ad3ec6d0853bb..003dbe3fd22f6 100644 --- a/.buildkite/ftr_configs.yml +++ b/.buildkite/ftr_configs.yml @@ -158,7 +158,9 @@ enabled: - test/server_integration/http/ssl_with_p12/config.js - test/server_integration/http/ssl/config.js - test/ui_capabilities/newsfeed_err/config.ts - - x-pack/test/accessibility/config.ts + - x-pack/test/accessibility/apps/group1/config.ts + - x-pack/test/accessibility/apps/group2/config.ts + - x-pack/test/accessibility/apps/group3/config.ts - x-pack/test/localization/config.ja_jp.ts - x-pack/test/localization/config.fr_fr.ts - x-pack/test/localization/config.zh_cn.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a380211d9f652..9944de64b186d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -55,6 +55,7 @@ packages/kbn-bazel-runner @elastic/kibana-operations examples/bfetch_explorer @elastic/appex-sharedux src/plugins/bfetch @elastic/appex-sharedux packages/kbn-calculate-auto @elastic/obs-ux-management-team +packages/kbn-calculate-width-from-char-count @elastic/kibana-visualizations x-pack/plugins/canvas @elastic/kibana-presentation x-pack/test/cases_api_integration/common/plugins/cases @elastic/response-ops packages/kbn-cases-components @elastic/response-ops @@ -871,7 +872,7 @@ packages/kbn-zod-helpers @elastic/security-detection-rule-management /test/functional/apps/management/ccs_compatibility/_data_views_ccs.ts @elastic/kibana-data-discovery /test/functional/apps/management/data_views @elastic/kibana-data-discovery /test/plugin_functional/test_suites/data_plugin @elastic/kibana-data-discovery -/x-pack/test/accessibility/apps/search_sessions.ts @elastic/kibana-data-discovery +/x-pack/test/accessibility/apps/group3/search_sessions.ts @elastic/kibana-data-discovery /x-pack/test/api_integration/apis/management/rollup/index_patterns_extensions.js @elastic/kibana-data-discovery /x-pack/test/api_integration/apis/search @elastic/kibana-data-discovery /x-pack/test/examples/search_examples @elastic/kibana-data-discovery @@ -1074,8 +1075,8 @@ x-pack/plugins/infra/server/lib/alerting @elastic/obs-ux-management-team #CC# /src/plugins/kibana_react/public/code_editor/ @elastic/kibana-presentation # Machine Learning -/x-pack/test/accessibility/apps/ml.ts @elastic/ml-ui -/x-pack/test/accessibility/apps/ml_embeddables_in_dashboard.ts @elastic/ml-ui +/x-pack/test/accessibility/apps/group2/ml.ts @elastic/ml-ui +/x-pack/test/accessibility/apps/group3/ml_embeddables_in_dashboard.ts @elastic/ml-ui /x-pack/test/api_integration/apis/ml/ @elastic/ml-ui /x-pack/test/api_integration_basic/apis/ml/ @elastic/ml-ui /x-pack/test/functional/apps/ml/ @elastic/ml-ui @@ -1089,7 +1090,7 @@ x-pack/plugins/infra/server/lib/alerting @elastic/obs-ux-management-team /x-pack/test/screenshot_creation/services/ml_screenshots.ts @elastic/ml-ui # Additional plugins and packages maintained by the ML team. -/x-pack/test/accessibility/apps/transform.ts @elastic/ml-ui +/x-pack/test/accessibility/apps/group2/transform.ts @elastic/ml-ui /x-pack/test/api_integration/apis/aiops/ @elastic/ml-ui /x-pack/test/api_integration/apis/transform/ @elastic/ml-ui /x-pack/test/api_integration_basic/apis/transform/ @elastic/ml-ui @@ -1176,10 +1177,10 @@ x-pack/plugins/cloud_integrations/cloud_full_story/server/config.ts @elastic/kib /test/interactive_setup_api_integration/ @elastic/kibana-security /test/interactive_setup_functional/ @elastic/kibana-security /test/plugin_functional/test_suites/core_plugins/rendering.ts @elastic/kibana-security -/x-pack/test/accessibility/apps/login_page.ts @elastic/kibana-security -/x-pack/test/accessibility/apps/roles.ts @elastic/kibana-security -/x-pack/test/accessibility/apps/spaces.ts @elastic/kibana-security -/x-pack/test/accessibility/apps/users.ts @elastic/kibana-security +/x-pack/test/accessibility/apps/group1/login_page.ts @elastic/kibana-security +/x-pack/test/accessibility/apps/group1/roles.ts @elastic/kibana-security +/x-pack/test/accessibility/apps/group1/spaces.ts @elastic/kibana-security +/x-pack/test/accessibility/apps/group1/users.ts @elastic/kibana-security /x-pack/test/api_integration/apis/security/ @elastic/kibana-security /x-pack/test/api_integration/apis/spaces/ @elastic/kibana-security /x-pack/test/ui_capabilities/ @elastic/kibana-security diff --git a/docs/developer/contributing/development-accessibility-tests.asciidoc b/docs/developer/contributing/development-accessibility-tests.asciidoc index 2fe2682a3e365..491c16b8a82db 100644 --- a/docs/developer/contributing/development-accessibility-tests.asciidoc +++ b/docs/developer/contributing/development-accessibility-tests.asciidoc @@ -68,7 +68,7 @@ node scripts/functional_test_runner.js --config test/accessibility/config.ts ----------- To run the x-pack tests, swap the config file out for -`x-pack/test/accessibility/config.ts`. +`x-pack/test/accessibility/apps/{group1,group2,group3}/config.ts`. The testing is done using https://github.com/dequelabs/axe-core[axe]. You can run the same thing that runs CI using browser plugins: diff --git a/docs/developer/contributing/interpreting-ci-failures.asciidoc b/docs/developer/contributing/interpreting-ci-failures.asciidoc index 7708c866c3a81..976b3aded3653 100644 --- a/docs/developer/contributing/interpreting-ci-failures.asciidoc +++ b/docs/developer/contributing/interpreting-ci-failures.asciidoc @@ -33,7 +33,7 @@ image::images/test_results.png[Buildkite build screenshot] Looking at the failure, we first look at the Error and stack trace. In the example below, this test failed to find an element within the timeout; `Error: retry.try timeout: TimeoutError: Waiting for element to be located By(css selector, [data-test-subj="createSpace"])` -We know the test file from the stack trace was on line 50 of `test/accessibility/apps/spaces.ts` (this test and the stack trace context is kibana/x-pack/ so the file is https://github.com/elastic/kibana/blob/main/x-pack/test/accessibility/apps/spaces.ts#L50). +We know the test file from the stack trace was on line 50 of `test/accessibility/apps/spaces.ts` (this test and the stack trace context is kibana/x-pack/ so the file is https://github.com/elastic/kibana/blob/main/x-pack/test/accessibility/apps/group1/spaces.ts#L50). The function to click on the element was called from a page object method in `test/functional/page_objects/space_selector_page.ts` https://github.com/elastic/kibana/blob/main/x-pack/test/functional/page_objects/space_selector_page.ts#L58 diff --git a/docs/setup/configuring-reporting.asciidoc b/docs/setup/configuring-reporting.asciidoc index 903f6fdfb5afd..d8ff9e1b202b6 100644 --- a/docs/setup/configuring-reporting.asciidoc +++ b/docs/setup/configuring-reporting.asciidoc @@ -113,7 +113,7 @@ Granting the privilege to generate reports also grants the user the privilege to ==== Grant access with the role API With <> enabled in Reporting, you can also use the {ref}/security-api-put-role.html[role API] to grant access to the {report-features}, using *All* privileges, or sub-feature privileges. -NOTE: this [API request](https://www.elastic.co/guide/en/kibana/current/role-management-api-put.html) needs to be executed against the Kibana API endpoint +NOTE: This link:https://www.elastic.co/guide/en/kibana/current/role-management-api-put.html[API request] needs to be executed against the link:https://www.elastic.co/guide/en/kibana/current/api.html[Kibana API endpoint]. [source, sh] --------------------------------------------------------------- POST :/api/_security/role/custom_reporting_user @@ -229,3 +229,5 @@ For more information, see {ref}/notification-settings.html#ssl-notification-sett . Add one or more users who have access to the {report-features}. + Once you've enabled SSL for {kib}, all requests to the reporting endpoints must include valid credentials. + +For more information on sharing reports, direct links, and more, refer to <>. diff --git a/docs/user/reporting/index.asciidoc b/docs/user/reporting/index.asciidoc index 8d53018dec572..676fb430b9c66 100644 --- a/docs/user/reporting/index.asciidoc +++ b/docs/user/reporting/index.asciidoc @@ -31,6 +31,8 @@ You access the options from the *Share* menu in the toolbar. The sharing options NOTE: For Elastic Cloud deployments, {kib} instances require a minimum of 2GB RAM to generate PDF or PNG reports. To change {kib} sizing, {ess-console}[edit the deployment]. +For more information on how to configure reporting in {kib}, refer to <> + [float] [[manually-generate-reports]] == Create reports diff --git a/package.json b/package.json index da2855826068a..92a0ae4f2be74 100644 --- a/package.json +++ b/package.json @@ -167,6 +167,7 @@ "@kbn/bfetch-explorer-plugin": "link:examples/bfetch_explorer", "@kbn/bfetch-plugin": "link:src/plugins/bfetch", "@kbn/calculate-auto": "link:packages/kbn-calculate-auto", + "@kbn/calculate-width-from-char-count": "link:packages/kbn-calculate-width-from-char-count", "@kbn/canvas-plugin": "link:x-pack/plugins/canvas", "@kbn/cases-api-integration-test-plugin": "link:x-pack/test/cases_api_integration/common/plugins/cases", "@kbn/cases-components": "link:packages/kbn-cases-components", diff --git a/packages/kbn-calculate-width-from-char-count/.storybook/main.js b/packages/kbn-calculate-width-from-char-count/.storybook/main.js new file mode 100644 index 0000000000000..8dc3c5d1518f4 --- /dev/null +++ b/packages/kbn-calculate-width-from-char-count/.storybook/main.js @@ -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. + */ + +module.exports = require('@kbn/storybook').defaultConfig; diff --git a/packages/kbn-calculate-width-from-char-count/README.md b/packages/kbn-calculate-width-from-char-count/README.md new file mode 100644 index 0000000000000..13581e81bd9e6 --- /dev/null +++ b/packages/kbn-calculate-width-from-char-count/README.md @@ -0,0 +1,3 @@ +# @kbn/calculate-width-from-char-count + +This package contains a function that calculates the approximate width of the component from a text length. diff --git a/packages/kbn-calculate-width-from-char-count/index.ts b/packages/kbn-calculate-width-from-char-count/index.ts new file mode 100644 index 0000000000000..de0577ee3ed83 --- /dev/null +++ b/packages/kbn-calculate-width-from-char-count/index.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 * from './src'; diff --git a/packages/kbn-calculate-width-from-char-count/jest.config.js b/packages/kbn-calculate-width-from-char-count/jest.config.js new file mode 100644 index 0000000000000..0538847bfc820 --- /dev/null +++ b/packages/kbn-calculate-width-from-char-count/jest.config.js @@ -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 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. + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../..', + roots: ['/packages/kbn-calculate-width-from-char-count'], +}; diff --git a/packages/kbn-calculate-width-from-char-count/kibana.jsonc b/packages/kbn-calculate-width-from-char-count/kibana.jsonc new file mode 100644 index 0000000000000..216b12ddeac89 --- /dev/null +++ b/packages/kbn-calculate-width-from-char-count/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-common", + "id": "@kbn/calculate-width-from-char-count", + "owner": "@elastic/kibana-visualizations" +} diff --git a/packages/kbn-calculate-width-from-char-count/package.json b/packages/kbn-calculate-width-from-char-count/package.json new file mode 100644 index 0000000000000..dd8182452f0ee --- /dev/null +++ b/packages/kbn-calculate-width-from-char-count/package.json @@ -0,0 +1,7 @@ +{ + "name": "@kbn/calculate-width-from-char-count", + "private": true, + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0", + "sideEffects": false +} \ No newline at end of file diff --git a/packages/kbn-calculate-width-from-char-count/src/calculate_width_from_char_count.test.ts b/packages/kbn-calculate-width-from-char-count/src/calculate_width_from_char_count.test.ts new file mode 100644 index 0000000000000..1dbe25306b639 --- /dev/null +++ b/packages/kbn-calculate-width-from-char-count/src/calculate_width_from_char_count.test.ts @@ -0,0 +1,21 @@ +/* + * 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 { calculateWidthFromCharCount, MAX_WIDTH } from './calculate_width_from_char_count'; + +describe('calculateWidthFromCharCount', () => { + it('should return minimum width if char count is smaller than minWidth', () => { + expect(calculateWidthFromCharCount(10, { minWidth: 300 })).toBe(300); + }); + it('should return calculated width', () => { + expect(calculateWidthFromCharCount(30)).toBe(30 * 7 + 116); + }); + it('should return maximum width if char count is bigger than maxWidth', () => { + expect(calculateWidthFromCharCount(1000)).toBe(MAX_WIDTH); + }); +}); diff --git a/packages/kbn-calculate-width-from-char-count/src/calculate_width_from_char_count.ts b/packages/kbn-calculate-width-from-char-count/src/calculate_width_from_char_count.ts new file mode 100644 index 0000000000000..c79307473c7e8 --- /dev/null +++ b/packages/kbn-calculate-width-from-char-count/src/calculate_width_from_char_count.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 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 interface LIMITS { + paddingsWidth: number; + minWidth?: number; + avCharWidth: number; + maxWidth: number; +} + +export const MAX_WIDTH = 550; +const PADDINGS_WIDTH = 116; +const AVERAGE_CHAR_WIDTH = 7; + +const defaultPanelWidths: LIMITS = { + maxWidth: MAX_WIDTH, + avCharWidth: AVERAGE_CHAR_WIDTH, + paddingsWidth: PADDINGS_WIDTH, +}; + +export function calculateWidthFromCharCount( + labelLength: number, + overridesPanelWidths?: Partial +) { + const { maxWidth, avCharWidth, paddingsWidth, minWidth } = { + ...defaultPanelWidths, + ...overridesPanelWidths, + }; + const widthForCharCount = paddingsWidth + labelLength * avCharWidth; + + if (minWidth && widthForCharCount < minWidth) { + return minWidth; + } + + return Math.min(widthForCharCount, maxWidth); +} diff --git a/packages/kbn-calculate-width-from-char-count/src/calculate_width_from_entries.test.ts b/packages/kbn-calculate-width-from-char-count/src/calculate_width_from_entries.test.ts new file mode 100644 index 0000000000000..6e740defdce92 --- /dev/null +++ b/packages/kbn-calculate-width-from-char-count/src/calculate_width_from_entries.test.ts @@ -0,0 +1,53 @@ +/* + * 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 { calculateWidthFromEntries } from './calculate_width_from_entries'; +import { MAX_WIDTH } from './calculate_width_from_char_count'; +import faker from 'faker'; + +const generateLabel = (length: number) => faker.random.alpha({ count: length }); + +const generateObjectWithLabelOfLength = (length: number, propOverrides?: Record) => ({ + label: generateLabel(length), + ...propOverrides, +}); + +describe('calculateWidthFromEntries', () => { + it('calculates width for array of strings', () => { + const shortLabels = [10, 20].map(generateLabel); + expect(calculateWidthFromEntries(shortLabels)).toBe(256); + + const mediumLabels = [50, 55, 10, 20].map(generateLabel); + expect(calculateWidthFromEntries(mediumLabels)).toBe(501); + + const longLabels = [80, 90, 10].map(generateLabel); + expect(calculateWidthFromEntries(longLabels)).toBe(MAX_WIDTH); + }); + + it('calculates width for array of objects with keys', () => { + const shortLabels = [10, 20].map((v) => generateObjectWithLabelOfLength(v)); + expect(calculateWidthFromEntries(shortLabels, ['label'])).toBe(256); + + const mediumLabels = [50, 55, 10, 20].map((v) => generateObjectWithLabelOfLength(v)); + expect(calculateWidthFromEntries(mediumLabels, ['label'])).toBe(501); + + const longLabels = [80, 90, 10].map((v) => generateObjectWithLabelOfLength(v)); + expect(calculateWidthFromEntries(longLabels, ['label'])).toBe(MAX_WIDTH); + }); + it('calculates width for array of objects for fallback keys', () => { + const shortLabels = [10, 20].map((v) => + generateObjectWithLabelOfLength(v, { label: undefined, name: generateLabel(v) }) + ); + expect(calculateWidthFromEntries(shortLabels, ['id', 'label', 'name'])).toBe(256); + + const mediumLabels = [50, 55, 10, 20].map((v) => + generateObjectWithLabelOfLength(v, { label: undefined, name: generateLabel(v) }) + ); + expect(calculateWidthFromEntries(mediumLabels, ['id', 'label', 'name'])).toBe(501); + }); +}); diff --git a/packages/kbn-calculate-width-from-char-count/src/calculate_width_from_entries.ts b/packages/kbn-calculate-width-from-char-count/src/calculate_width_from_entries.ts new file mode 100644 index 0000000000000..4a6795c8ea077 --- /dev/null +++ b/packages/kbn-calculate-width-from-char-count/src/calculate_width_from_entries.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 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 { LIMITS, calculateWidthFromCharCount } from './calculate_width_from_char_count'; + +type GenericObject> = T; + +const getMaxLabelLengthForObjects = ( + entries: GenericObject[], + labelKeys: Array +) => + entries.reduce((acc, curr) => { + const labelKey = labelKeys.find((key) => curr[key]); + if (!labelKey) { + return acc; + } + const labelLength = curr[labelKey].length; + return acc > labelLength ? acc : labelLength; + }, 0); + +const getMaxLabelLengthForStrings = (arr: string[]) => + arr.reduce((acc, curr) => (acc > curr.length ? acc : curr.length), 0); + +export function calculateWidthFromEntries( + entries: GenericObject[] | string[], + labelKeys?: Array, + overridesPanelWidths?: Partial +) { + const maxLabelLength = labelKeys + ? getMaxLabelLengthForObjects(entries as GenericObject[], labelKeys) + : getMaxLabelLengthForStrings(entries as string[]); + + return calculateWidthFromCharCount(maxLabelLength, overridesPanelWidths); +} diff --git a/packages/kbn-calculate-width-from-char-count/src/index.ts b/packages/kbn-calculate-width-from-char-count/src/index.ts new file mode 100644 index 0000000000000..33fcddecf7403 --- /dev/null +++ b/packages/kbn-calculate-width-from-char-count/src/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 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 { calculateWidthFromCharCount } from './calculate_width_from_char_count'; + +export { calculateWidthFromEntries } from './calculate_width_from_entries'; diff --git a/packages/kbn-calculate-width-from-char-count/tsconfig.json b/packages/kbn-calculate-width-from-char-count/tsconfig.json new file mode 100644 index 0000000000000..ea0a30fa75171 --- /dev/null +++ b/packages/kbn-calculate-width-from-char-count/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node", + "react", + ], + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ], + "kbn_references": [], + "exclude": [ + "target/**/*", + ] +} diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index 362f3b391ad01..8234daa4b4454 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -458,6 +458,9 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { privileges: `${SECURITY_SOLUTION_DOCS}endpoint-management-req.html`, manageDetectionRules: `${SECURITY_SOLUTION_DOCS}rules-ui-management.html`, createEsqlRuleType: `${SECURITY_SOLUTION_DOCS}rules-ui-create.html#create-esql-rule`, + entityAnalytics: { + riskScorePrerequisites: `${SECURITY_SOLUTION_DOCS}ers-requirements.html`, + }, }, query: { eql: `${ELASTICSEARCH_DOCS}eql.html`, diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index 1426cab0b3341..b2298eecd3e17 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -349,6 +349,9 @@ export interface DocLinks { readonly privileges: string; readonly manageDetectionRules: string; readonly createEsqlRuleType: string; + readonly entityAnalytics: { + readonly riskScorePrerequisites: string; + }; }; readonly query: { readonly eql: string; diff --git a/packages/kbn-visualization-ui-components/components/field_picker/field_picker.test.tsx b/packages/kbn-visualization-ui-components/components/field_picker/field_picker.test.tsx new file mode 100644 index 0000000000000..1b821dd44bc93 --- /dev/null +++ b/packages/kbn-visualization-ui-components/components/field_picker/field_picker.test.tsx @@ -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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { FieldPicker, FieldPickerProps } from './field_picker'; +import { render, screen } from '@testing-library/react'; +import faker from 'faker'; +import userEvent from '@testing-library/user-event'; +import { DataType, FieldOptionValue } from './types'; + +const generateFieldWithLabelOfLength = (length: number) => ({ + label: faker.random.alpha({ count: length }), + value: { + type: 'field' as const, + field: faker.random.alpha({ count: length }), + dataType: 'date' as DataType, + operationType: 'count', + }, + exists: true, + compatible: 1, +}); + +const generateProps = (customField = generateFieldWithLabelOfLength(20)) => + ({ + selectedOptions: [ + { + label: 'Category', + value: { + type: 'field' as const, + field: 'category.keyword', + dataType: 'keyword' as DataType, + operationType: 'count', + }, + }, + ], + options: [ + { + label: 'nested options', + exists: true, + compatible: 1, + value: generateFieldWithLabelOfLength(20), + options: [ + generateFieldWithLabelOfLength(20), + customField, + generateFieldWithLabelOfLength(20), + ], + }, + ], + onChoose: jest.fn(), + fieldIsInvalid: false, + } as unknown as FieldPickerProps); + +describe('field picker', () => { + const renderFieldPicker = (customField = generateFieldWithLabelOfLength(20)) => { + const props = generateProps(customField); + const rtlRender = render(); + return { + openCombobox: () => userEvent.click(screen.getByLabelText(/open list of options/i)), + ...rtlRender, + }; + }; + + it('should render minimum width dropdown list if all labels are short', async () => { + const { openCombobox } = renderFieldPicker(); + openCombobox(); + const popover = screen.getByRole('dialog'); + expect(popover).toHaveStyle('inline-size: 256px'); + }); + + it('should render calculated width dropdown list if the longest label is longer than min width', async () => { + const { openCombobox } = renderFieldPicker(generateFieldWithLabelOfLength(50)); + openCombobox(); + + const popover = screen.getByRole('dialog'); + expect(popover).toHaveStyle('inline-size: 466px'); + }); + + it('should render maximum width dropdown list if the longest label is longer than max width', async () => { + const { openCombobox } = renderFieldPicker(generateFieldWithLabelOfLength(80)); + openCombobox(); + const popover = screen.getByRole('dialog'); + expect(popover).toHaveStyle('inline-size: 550px'); + }); +}); diff --git a/packages/kbn-visualization-ui-components/components/field_picker/field_picker.tsx b/packages/kbn-visualization-ui-components/components/field_picker/field_picker.tsx index 5b6022d5cb454..237b7c85cd8fd 100644 --- a/packages/kbn-visualization-ui-components/components/field_picker/field_picker.tsx +++ b/packages/kbn-visualization-ui-components/components/field_picker/field_picker.tsx @@ -9,9 +9,10 @@ import './field_picker.scss'; import React from 'react'; import { i18n } from '@kbn/i18n'; +import classNames from 'classnames'; import { EuiComboBox, EuiComboBoxProps } from '@elastic/eui'; import { FieldIcon } from '@kbn/field-utils/src/components/field_icon'; -import classNames from 'classnames'; +import { calculateWidthFromCharCount } from '@kbn/calculate-width-from-char-count'; import type { FieldOptionValue, FieldOption } from './types'; export interface FieldPickerProps @@ -27,23 +28,26 @@ export interface FieldPickerProps const MIDDLE_TRUNCATION_PROPS = { truncation: 'middle' as const }; const SINGLE_SELECTION_AS_TEXT_PROPS = { asPlainText: true }; -export function FieldPicker({ - selectedOptions, - options, - onChoose, - onDelete, - fieldIsInvalid, - ['data-test-subj']: dataTestSub, - ...rest -}: FieldPickerProps) { - let theLongestLabel = ''; +export function FieldPicker( + props: FieldPickerProps +) { + const { + selectedOptions, + options, + onChoose, + onDelete, + fieldIsInvalid, + ['data-test-subj']: dataTestSub, + ...rest + } = props; + let maxLabelLength = 0; const styledOptions = options?.map(({ compatible, exists, ...otherAttr }) => { if (otherAttr.options) { return { ...otherAttr, options: otherAttr.options.map(({ exists: fieldOptionExists, ...fieldOption }) => { - if (fieldOption.label.length > theLongestLabel.length) { - theLongestLabel = fieldOption.label; + if (fieldOption.label.length > maxLabelLength) { + maxLabelLength = fieldOption.label.length; } return { ...fieldOption, @@ -75,7 +79,6 @@ export function FieldPicker({ }; }); - const panelMinWidth = getPanelMinWidth(theLongestLabel.length); return ( ({ selectedOptions={selectedOptions} singleSelection={SINGLE_SELECTION_AS_TEXT_PROPS} truncationProps={MIDDLE_TRUNCATION_PROPS} - inputPopoverProps={{ panelMinWidth }} + inputPopoverProps={{ + panelMinWidth: calculateWidthFromCharCount(maxLabelLength), + anchorPosition: 'downRight', + }} onChange={(choices) => { if (choices.length === 0) { onDelete?.(); @@ -102,20 +108,3 @@ export function FieldPicker({ /> ); } - -const MINIMUM_POPOVER_WIDTH = 300; -const MINIMUM_POPOVER_WIDTH_CHAR_COUNT = 28; -const AVERAGE_CHAR_WIDTH = 7; -const MAXIMUM_POPOVER_WIDTH_CHAR_COUNT = 60; -const MAXIMUM_POPOVER_WIDTH = 550; // fitting 60 characters - -function getPanelMinWidth(labelLength: number) { - if (labelLength > MAXIMUM_POPOVER_WIDTH_CHAR_COUNT) { - return MAXIMUM_POPOVER_WIDTH; - } - if (labelLength > MINIMUM_POPOVER_WIDTH_CHAR_COUNT) { - const overflownChars = labelLength - MINIMUM_POPOVER_WIDTH_CHAR_COUNT; - return MINIMUM_POPOVER_WIDTH + overflownChars * AVERAGE_CHAR_WIDTH; - } - return MINIMUM_POPOVER_WIDTH; -} diff --git a/packages/kbn-visualization-ui-components/tsconfig.json b/packages/kbn-visualization-ui-components/tsconfig.json index 78f0b8a4b111f..a9d6627828dc7 100644 --- a/packages/kbn-visualization-ui-components/tsconfig.json +++ b/packages/kbn-visualization-ui-components/tsconfig.json @@ -31,5 +31,6 @@ "@kbn/coloring", "@kbn/field-formats-plugin", "@kbn/field-utils", + "@kbn/calculate-width-from-char-count" ], } diff --git a/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.tsx b/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.tsx index 1804a2fcf2046..8e1c7fbc74b99 100644 --- a/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.tsx +++ b/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.tsx @@ -9,6 +9,7 @@ import React, { useState } from 'react'; import { EuiSelectable, EuiInputPopover, EuiSelectableProps } from '@elastic/eui'; import { DataViewListItem } from '@kbn/data-views-plugin/common'; +import { calculateWidthFromEntries } from '@kbn/calculate-width-from-char-count'; import { ToolbarButton, ToolbarButtonProps } from '@kbn/shared-ux-button-toolbar'; @@ -67,6 +68,7 @@ export function DataViewPicker({ isOpen={isPopoverOpen} input={createTrigger()} closePopover={() => setPopoverIsOpen(false)} + panelMinWidth={calculateWidthFromEntries(dataViews, ['name', 'id'])} panelProps={{ 'data-test-subj': 'data-view-picker-popover', }} diff --git a/src/plugins/presentation_util/tsconfig.json b/src/plugins/presentation_util/tsconfig.json index e17fd6cc5a754..4076319587e17 100644 --- a/src/plugins/presentation_util/tsconfig.json +++ b/src/plugins/presentation_util/tsconfig.json @@ -31,7 +31,8 @@ "@kbn/ui-actions-plugin", "@kbn/saved-objects-finder-plugin", "@kbn/content-management-plugin", - "@kbn/shared-ux-button-toolbar" + "@kbn/shared-ux-button-toolbar", + "@kbn/calculate-width-from-char-count" ], "exclude": ["target/**/*"] } diff --git a/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx index e3e4059ad3cf5..77e00e157d62b 100644 --- a/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx +++ b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx @@ -9,6 +9,7 @@ import { EuiComboBox, EuiComboBoxOptionOption, EuiToolTip, useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; import { DataView, DataViewField } from '@kbn/data-views-plugin/common'; +import { calculateWidthFromEntries } from '@kbn/calculate-width-from-char-count'; import { i18n } from '@kbn/i18n'; import React, { useCallback, useState } from 'react'; import { UnifiedHistogramBreakdownContext } from '../types'; @@ -59,11 +60,10 @@ export const BreakdownFieldSelector = ({ const breakdownCss = css` width: 100%; max-width: ${euiTheme.base * 22}px; - &:focus-within { - max-width: ${euiTheme.base * 30}px; - } `; + const panelMinWidth = calculateWidthFromEntries(fieldOptions, ['label']); + return ( { +const MIN_WIDTH = 300; + +export const changeDataViewStyles = ({ + fullWidth, + dataViewsList, +}: { + fullWidth?: boolean; + dataViewsList: DataViewListItemEnhanced[]; +}) => { return { trigger: { - maxWidth: fullWidth ? undefined : DATA_VIEW_POPOVER_CONTENT_WIDTH, + maxWidth: fullWidth ? undefined : MIN_WIDTH, }, popoverContent: { - width: DATA_VIEW_POPOVER_CONTENT_WIDTH, + width: calculateWidthFromEntries(dataViewsList, ['name', 'id'], { minWidth: MIN_WIDTH }), }, }; }; diff --git a/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx b/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx index 8c35ed21568bb..1398483fa0a1a 100644 --- a/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx +++ b/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx @@ -96,7 +96,9 @@ export function ChangeDataView({ const { application, data, storage, dataViews, dataViewEditor, appName, usageCollection } = kibana.services; const reportUiCounter = usageCollection?.reportUiCounter.bind(usageCollection, appName); - const styles = changeDataViewStyles({ fullWidth: trigger.fullWidth }); + + const styles = changeDataViewStyles({ fullWidth: trigger.fullWidth, dataViewsList }); + const [isTextLangTransitionModalDismissed, setIsTextLangTransitionModalDismissed] = useState(() => Boolean(storage.get(TEXT_LANG_TRANSITION_MODAL_KEY)) ); diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_value_input.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_value_input.tsx index adb80df6cf543..9328ecfa66c50 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_value_input.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_value_input.tsx @@ -10,6 +10,7 @@ import { InjectedIntl, injectI18n } from '@kbn/i18n-react'; import { uniq } from 'lodash'; import React from 'react'; import { withKibana } from '@kbn/kibana-react-plugin/public'; +import { calculateWidthFromEntries } from '@kbn/calculate-width-from-char-count'; import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box'; import { PhraseSuggestorUI, PhraseSuggestorProps } from './phrase_suggestor'; import { ValueInputType } from './value_input_type'; @@ -26,7 +27,6 @@ interface PhraseValueInputProps extends PhraseSuggestorProps { } class PhraseValueInputUI extends PhraseSuggestorUI { - comboBoxWrapperRef = React.createRef(); inputRef: HTMLInputElement | null = null; public render() { @@ -59,43 +59,39 @@ class PhraseValueInputUI extends PhraseSuggestorUI { // there are cases when the value is a number, this would cause an exception const valueAsStr = String(value); const options = value ? uniq([valueAsStr, ...suggestions]) : suggestions; + const panelMinWidth = calculateWidthFromEntries(options); return ( -
- { - this.inputRef = ref; - }} - isDisabled={this.props.disabled} - fullWidth={fullWidth} - compressed={this.props.compressed} - placeholder={intl.formatMessage({ - id: 'unifiedSearch.filter.filterEditor.valueSelectPlaceholder', - defaultMessage: 'Select a value', - })} - aria-label={intl.formatMessage({ - id: 'unifiedSearch.filter.filterEditor.valueSelectPlaceholder', - defaultMessage: 'Select a value', - })} - options={options} - getLabel={(option) => option} - selectedOptions={value ? [valueAsStr] : []} - onChange={([newValue = '']) => { - onChange(newValue); - setTimeout(() => { - // Note: requires a tick skip to correctly blur element focus - this.inputRef?.blur(); - }); - }} - onSearchChange={this.onSearchChange} - onCreateOption={onChange} - isClearable={false} - data-test-subj="filterParamsComboBox phraseParamsComboxBox" - singleSelection={SINGLE_SELECTION_AS_TEXT_PROPS} - truncationProps={MIDDLE_TRUNCATION_PROPS} - /> -
+ { + this.inputRef = ref; + }} + isDisabled={this.props.disabled} + fullWidth={fullWidth} + compressed={this.props.compressed} + placeholder={intl.formatMessage({ + id: 'unifiedSearch.filter.filterEditor.valueSelectPlaceholder', + defaultMessage: 'Select a value', + })} + aria-label={intl.formatMessage({ + id: 'unifiedSearch.filter.filterEditor.valueSelectPlaceholder', + defaultMessage: 'Select a value', + })} + options={options} + getLabel={(option) => option} + selectedOptions={value ? [valueAsStr] : []} + onChange={([newValue = '']) => { + onChange(newValue); + }} + onSearchChange={this.onSearchChange} + onCreateOption={onChange} + isClearable={false} + data-test-subj="filterParamsComboBox phraseParamsComboxBox" + singleSelection={SINGLE_SELECTION_AS_TEXT_PROPS} + truncationProps={MIDDLE_TRUNCATION_PROPS} + inputPopoverProps={{ panelMinWidth, anchorPosition: 'downRight' }} + /> ); } } diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/phrases_values_input.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/phrases_values_input.tsx index 500b875f42667..30fd03fb3d9c2 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/phrases_values_input.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/phrases_values_input.tsx @@ -11,6 +11,7 @@ import { uniq } from 'lodash'; import React from 'react'; import { withKibana } from '@kbn/kibana-react-plugin/public'; import { withEuiTheme, WithEuiThemeProps } from '@elastic/eui'; +import { calculateWidthFromEntries } from '@kbn/calculate-width-from-char-count'; import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box'; import { PhraseSuggestorUI, PhraseSuggestorProps } from './phrase_suggestor'; import { phrasesValuesComboboxCss } from './phrases_values_input.styles'; @@ -28,45 +29,42 @@ interface Props { export type PhrasesValuesInputProps = Props & PhraseSuggestorProps & WithEuiThemeProps; class PhrasesValuesInputUI extends PhraseSuggestorUI { - comboBoxWrapperRef = React.createRef(); - public render() { const { suggestions, isLoading } = this.state; const { values, intl, onChange, fullWidth, onParamsUpdate, compressed, disabled } = this.props; const options = values ? uniq([...values, ...suggestions]) : suggestions; - + const panelMinWidth = calculateWidthFromEntries(options); return ( -
- option} - selectedOptions={values || []} - onSearchChange={this.onSearchChange} - onCreateOption={(option: string) => { - onParamsUpdate(option.trim()); - }} - className={phrasesValuesComboboxCss(this.props.theme)} - onChange={onChange} - isClearable={false} - data-test-subj="filterParamsComboBox phrasesParamsComboxBox" - isDisabled={disabled} - truncationProps={MIDDLE_TRUNCATION_PROPS} - /> -
+ option} + selectedOptions={values || []} + onSearchChange={this.onSearchChange} + onCreateOption={(option: string) => { + onParamsUpdate(option.trim()); + }} + className={phrasesValuesComboboxCss(this.props.theme)} + onChange={onChange} + isClearable={false} + data-test-subj="filterParamsComboBox phrasesParamsComboxBox" + isDisabled={disabled} + truncationProps={MIDDLE_TRUNCATION_PROPS} + inputPopoverProps={{ panelMinWidth, anchorPosition: 'downRight' }} + /> ); } } diff --git a/src/plugins/unified_search/public/filters_builder/filter_item/field_input.tsx b/src/plugins/unified_search/public/filters_builder/filter_item/field_input.tsx index 540226caef525..cc87c3de78936 100644 --- a/src/plugins/unified_search/public/filters_builder/filter_item/field_input.tsx +++ b/src/plugins/unified_search/public/filters_builder/filter_item/field_input.tsx @@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n'; import { FieldIcon } from '@kbn/react-field'; import { KBN_FIELD_TYPES } from '@kbn/field-types'; import type { DataView, DataViewField } from '@kbn/data-views-plugin/common'; +import { calculateWidthFromEntries } from '@kbn/calculate-width-from-char-count'; import { useGeneratedHtmlId, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { getFilterableFields } from '../../filter_bar/filter_editor'; import { FiltersBuilderContextType } from '../context'; @@ -36,7 +37,6 @@ export function FieldInput({ field, dataView, onHandleField }: FieldInputProps) const { disabled, suggestionsAbstraction } = useContext(FiltersBuilderContextType); const fields = dataView ? getFilterableFields(dataView) : []; const id = useGeneratedHtmlId({ prefix: 'fieldInput' }); - const comboBoxWrapperRef = useRef(null); const inputRef = useRef(null); const onFieldChange = useCallback( @@ -72,40 +72,30 @@ export function FieldInput({ field, dataView, onHandleField }: FieldInputProps) ({ label }) => fields[optionFields.findIndex((optionField) => optionField.label === label)] ); onFieldChange(newValues); - - setTimeout(() => { - // Note: requires a tick skip to correctly blur element focus - inputRef?.current?.blur(); - }); }; - const handleFocus: React.FocusEventHandler = () => { - // Force focus on input due to https://github.com/elastic/eui/issues/7170 - inputRef?.current?.focus(); - }; + const panelMinWidth = calculateWidthFromEntries(euiOptions, ['label']); return ( -
- { - inputRef.current = ref; - }} - options={euiOptions} - selectedOptions={selectedEuiOptions} - onChange={onComboBoxChange} - isDisabled={disabled} - placeholder={strings.getFieldSelectPlaceholderLabel()} - sortMatchesBy="startsWith" - aria-label={strings.getFieldSelectPlaceholderLabel()} - isClearable={false} - compressed - fullWidth - onFocus={handleFocus} - data-test-subj="filterFieldSuggestionList" - singleSelection={SINGLE_SELECTION_AS_TEXT_PROPS} - truncationProps={MIDDLE_TRUNCATION_PROPS} - /> -
+ { + inputRef.current = ref; + }} + options={euiOptions} + selectedOptions={selectedEuiOptions} + onChange={onComboBoxChange} + isDisabled={disabled} + placeholder={strings.getFieldSelectPlaceholderLabel()} + sortMatchesBy="startsWith" + aria-label={strings.getFieldSelectPlaceholderLabel()} + isClearable={false} + compressed + fullWidth + data-test-subj="filterFieldSuggestionList" + singleSelection={SINGLE_SELECTION_AS_TEXT_PROPS} + truncationProps={MIDDLE_TRUNCATION_PROPS} + inputPopoverProps={{ panelMinWidth }} + /> ); } diff --git a/src/plugins/unified_search/public/filters_builder/filter_item/filter_item.styles.ts b/src/plugins/unified_search/public/filters_builder/filter_item/filter_item.styles.ts index 6ec0ac9ab7058..78c4952aa69b0 100644 --- a/src/plugins/unified_search/public/filters_builder/filter_item/filter_item.styles.ts +++ b/src/plugins/unified_search/public/filters_builder/filter_item/filter_item.styles.ts @@ -26,9 +26,6 @@ export const fieldAndParamCss = (euiTheme: EuiThemeComputed) => css` .euiFormRow { max-width: 800px; } - &:focus-within { - flex-grow: 4; - } `; export const operationCss = (euiTheme: EuiThemeComputed) => css` diff --git a/src/plugins/unified_search/public/index_pattern_select/index_pattern_select.tsx b/src/plugins/unified_search/public/index_pattern_select/index_pattern_select.tsx index f7148db93ce19..d8517eedba4ed 100644 --- a/src/plugins/unified_search/public/index_pattern_select/index_pattern_select.tsx +++ b/src/plugins/unified_search/public/index_pattern_select/index_pattern_select.tsx @@ -11,7 +11,9 @@ import React, { Component } from 'react'; import { Required } from '@kbn/utility-types'; import { EuiComboBox, EuiComboBoxProps } from '@elastic/eui'; +import { calculateWidthFromEntries } from '@kbn/calculate-width-from-char-count'; import type { DataViewsContract } from '@kbn/data-views-plugin/public'; +import { MIDDLE_TRUNCATION_PROPS } from '../filter_bar/filter_editor/lib/helpers'; export type IndexPatternSelectProps = Required< Omit, 'onSearchChange' | 'options' | 'selectedOptions' | 'onChange'>, @@ -28,7 +30,7 @@ export type IndexPatternSelectInternalProps = IndexPatternSelectProps & { interface IndexPatternSelectState { isLoading: boolean; - options: []; + options: Array<{ value: string; label: string }>; selectedIndexPattern: { value: string; label: string } | undefined; searchValue: string | undefined; } @@ -147,6 +149,8 @@ export default class IndexPatternSelect extends Component ); } diff --git a/src/plugins/unified_search/tsconfig.json b/src/plugins/unified_search/tsconfig.json index f83de4ff80fc7..0412bbc4c8c98 100644 --- a/src/plugins/unified_search/tsconfig.json +++ b/src/plugins/unified_search/tsconfig.json @@ -42,6 +42,7 @@ "@kbn/core-doc-links-browser", "@kbn/core-lifecycle-browser", "@kbn/ml-string-hash", + "@kbn/calculate-width-from-char-count" ], "exclude": [ "target/**/*", diff --git a/tsconfig.base.json b/tsconfig.base.json index 0f4d5c25aba60..3c4a87242841b 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -104,6 +104,8 @@ "@kbn/bfetch-plugin/*": ["src/plugins/bfetch/*"], "@kbn/calculate-auto": ["packages/kbn-calculate-auto"], "@kbn/calculate-auto/*": ["packages/kbn-calculate-auto/*"], + "@kbn/calculate-width-from-char-count": ["packages/kbn-calculate-width-from-char-count"], + "@kbn/calculate-width-from-char-count/*": ["packages/kbn-calculate-width-from-char-count/*"], "@kbn/canvas-plugin": ["x-pack/plugins/canvas"], "@kbn/canvas-plugin/*": ["x-pack/plugins/canvas/*"], "@kbn/cases-api-integration-test-plugin": ["x-pack/test/cases_api_integration/common/plugins/cases"], diff --git a/x-pack/packages/security-solution/features/src/app_features_keys.ts b/x-pack/packages/security-solution/features/src/app_features_keys.ts index ab54c64cf8992..ed8923cdb229a 100644 --- a/x-pack/packages/security-solution/features/src/app_features_keys.ts +++ b/x-pack/packages/security-solution/features/src/app_features_keys.ts @@ -99,6 +99,7 @@ export enum SecuritySubFeatureId { /** Sub-features IDs for Cases */ export enum CasesSubFeatureId { deleteCases = 'deleteCasesSubFeature', + casesSettings = 'casesSettingsSubFeature', } /** Sub-features IDs for Security Assistant */ diff --git a/x-pack/packages/security-solution/features/src/cases/kibana_sub_features.ts b/x-pack/packages/security-solution/features/src/cases/kibana_sub_features.ts index 3cbdb3f0e9123..1f49d01f979da 100644 --- a/x-pack/packages/security-solution/features/src/cases/kibana_sub_features.ts +++ b/x-pack/packages/security-solution/features/src/cases/kibana_sub_features.ts @@ -17,6 +17,7 @@ import type { CasesFeatureParams } from './types'; */ export const getCasesBaseKibanaSubFeatureIds = (): CasesSubFeatureId[] => [ CasesSubFeatureId.deleteCases, + CasesSubFeatureId.casesSettings, ]; /** @@ -60,7 +61,42 @@ export const getCasesSubFeaturesMap = ({ ], }; + const casesSettingsCasesSubFeature: SubFeatureConfig = { + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.casesSettingsSubFeatureName', + { + defaultMessage: 'Case Settings', + } + ), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'cases_settings', + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.casesSettingsSubFeatureDetails', + { + defaultMessage: 'Edit Case Settings', + } + ), + includeIn: 'all', + savedObject: { + all: [...savedObjects.files], + read: [...savedObjects.files], + }, + cases: { + settings: [APP_ID], + }, + ui: uiCapabilities.settings, + }, + ], + }, + ], + }; + return new Map([ [CasesSubFeatureId.deleteCases, deleteCasesSubFeature], + [CasesSubFeatureId.casesSettings, casesSettingsCasesSubFeature], ]); }; diff --git a/x-pack/plugins/cases/common/constants/index.ts b/x-pack/plugins/cases/common/constants/index.ts index 5a540b610135c..d4b6839e1dc37 100644 --- a/x-pack/plugins/cases/common/constants/index.ts +++ b/x-pack/plugins/cases/common/constants/index.ts @@ -162,6 +162,7 @@ export const READ_CASES_CAPABILITY = 'read_cases' as const; export const UPDATE_CASES_CAPABILITY = 'update_cases' as const; export const DELETE_CASES_CAPABILITY = 'delete_cases' as const; export const PUSH_CASES_CAPABILITY = 'push_cases' as const; +export const CASES_SETTINGS_CAPABILITY = 'cases_settings' as const; export const CASES_CONNECTORS_CAPABILITY = 'cases_connectors' as const; /** diff --git a/x-pack/plugins/cases/common/index.ts b/x-pack/plugins/cases/common/index.ts index 4283adf4c081a..520b5ca079b63 100644 --- a/x-pack/plugins/cases/common/index.ts +++ b/x-pack/plugins/cases/common/index.ts @@ -29,6 +29,7 @@ export type { Ecs, CaseViewRefreshPropInterface, CasesPermissions, + CasesCapabilities, CasesStatus, } from './ui/types'; @@ -52,6 +53,7 @@ export { CASE_COMMENT_SAVED_OBJECT, CASES_CONNECTORS_CAPABILITY, GET_CONNECTORS_CONFIGURE_API_TAG, + CASES_SETTINGS_CAPABILITY, } from './constants'; export type { AttachmentAttributes } from './types/domain'; diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index 2a76e56a59fe0..01d006a0dcd7d 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -12,7 +12,11 @@ import type { READ_CASES_CAPABILITY, UPDATE_CASES_CAPABILITY, } from '..'; -import type { CASES_CONNECTORS_CAPABILITY, PUSH_CASES_CAPABILITY } from '../constants'; +import type { + CASES_CONNECTORS_CAPABILITY, + CASES_SETTINGS_CAPABILITY, + PUSH_CASES_CAPABILITY, +} from '../constants'; import type { SnakeToCamelCase } from '../types'; import type { CaseSeverity, @@ -299,6 +303,7 @@ export interface CasesPermissions { delete: boolean; push: boolean; connectors: boolean; + settings: boolean; } export interface CasesCapabilities { @@ -308,4 +313,5 @@ export interface CasesCapabilities { [DELETE_CASES_CAPABILITY]: boolean; [PUSH_CASES_CAPABILITY]: boolean; [CASES_CONNECTORS_CAPABILITY]: boolean; + [CASES_SETTINGS_CAPABILITY]: boolean; } diff --git a/x-pack/plugins/cases/common/utils/capabilities.test.tsx b/x-pack/plugins/cases/common/utils/capabilities.test.tsx new file mode 100644 index 0000000000000..07b82ea0d0e8f --- /dev/null +++ b/x-pack/plugins/cases/common/utils/capabilities.test.tsx @@ -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 { createUICapabilities } from './capabilities'; + +describe('createUICapabilities', () => { + it('returns the UI capabilities correctly', () => { + expect(createUICapabilities()).toMatchInlineSnapshot(` + Object { + "all": Array [ + "create_cases", + "read_cases", + "update_cases", + "push_cases", + "cases_connectors", + ], + "delete": Array [ + "delete_cases", + ], + "read": Array [ + "read_cases", + "cases_connectors", + ], + "settings": Array [ + "cases_settings", + ], + } + `); + }); +}); diff --git a/x-pack/plugins/cases/common/utils/capabilities.ts b/x-pack/plugins/cases/common/utils/capabilities.ts index 28b3fa00f9272..6b33dd8c8dceb 100644 --- a/x-pack/plugins/cases/common/utils/capabilities.ts +++ b/x-pack/plugins/cases/common/utils/capabilities.ts @@ -12,12 +12,14 @@ import { PUSH_CASES_CAPABILITY, READ_CASES_CAPABILITY, UPDATE_CASES_CAPABILITY, + CASES_SETTINGS_CAPABILITY, } from '../constants'; export interface CasesUiCapabilities { all: readonly string[]; read: readonly string[]; delete: readonly string[]; + settings: readonly string[]; } /** * Return the UI capabilities for each type of operation. These strings must match the values defined in the UI @@ -33,4 +35,5 @@ export const createUICapabilities = (): CasesUiCapabilities => ({ ] as const, read: [READ_CASES_CAPABILITY, CASES_CONNECTORS_CAPABILITY] as const, delete: [DELETE_CASES_CAPABILITY] as const, + settings: [CASES_SETTINGS_CAPABILITY] as const, }); diff --git a/x-pack/plugins/cases/public/client/helpers/can_use_cases.ts b/x-pack/plugins/cases/public/client/helpers/can_use_cases.ts index 1cc22c0799702..90b0d3b18908f 100644 --- a/x-pack/plugins/cases/public/client/helpers/can_use_cases.ts +++ b/x-pack/plugins/cases/public/client/helpers/can_use_cases.ts @@ -40,10 +40,19 @@ export const canUseCases = acc.update = acc.update || userCapabilitiesForOwner.update; acc.delete = acc.delete || userCapabilitiesForOwner.delete; acc.push = acc.push || userCapabilitiesForOwner.push; + acc.connectors = acc.connectors || userCapabilitiesForOwner.connectors; + acc.settings = acc.settings || userCapabilitiesForOwner.settings; + const allFromAcc = - acc.create && acc.read && acc.update && acc.delete && acc.push && acc.connectors; + acc.create && + acc.read && + acc.update && + acc.delete && + acc.push && + acc.connectors && + acc.settings; + acc.all = acc.all || userCapabilitiesForOwner.all || allFromAcc; - acc.connectors = acc.connectors || userCapabilitiesForOwner.connectors; return acc; }, @@ -55,6 +64,7 @@ export const canUseCases = delete: false, push: false, connectors: false, + settings: false, } ); diff --git a/x-pack/plugins/cases/public/client/helpers/capabilities.test.ts b/x-pack/plugins/cases/public/client/helpers/capabilities.test.ts index a3f741f373032..ce374243b10b2 100644 --- a/x-pack/plugins/cases/public/client/helpers/capabilities.test.ts +++ b/x-pack/plugins/cases/public/client/helpers/capabilities.test.ts @@ -17,6 +17,7 @@ describe('getUICapabilities', () => { "delete": false, "push": false, "read": false, + "settings": false, "update": false, } `); @@ -31,6 +32,7 @@ describe('getUICapabilities', () => { "delete": false, "push": false, "read": false, + "settings": false, "update": false, } `); @@ -45,6 +47,7 @@ describe('getUICapabilities', () => { "delete": false, "push": false, "read": false, + "settings": false, "update": false, } `); @@ -68,6 +71,7 @@ describe('getUICapabilities', () => { "delete": false, "push": false, "read": false, + "settings": false, "update": false, } `); @@ -82,6 +86,7 @@ describe('getUICapabilities', () => { "delete": false, "push": false, "read": false, + "settings": false, "update": false, } `); @@ -105,6 +110,7 @@ describe('getUICapabilities', () => { "delete": true, "push": true, "read": true, + "settings": false, "update": true, } `); @@ -113,23 +119,65 @@ describe('getUICapabilities', () => { it('returns false for the all field when cases_connectors is false', () => { expect( getUICapabilities({ - create_cases: false, + create_cases: true, read_cases: true, update_cases: true, delete_cases: true, push_cases: true, cases_connectors: false, + cases_settings: true, }) ).toMatchInlineSnapshot(` Object { "all": false, "connectors": false, - "create": false, + "create": true, + "delete": true, + "push": true, + "read": true, + "settings": true, + "update": true, + } + `); + }); + + it('returns false for the all field when cases_settings is false', () => { + expect( + getUICapabilities({ + create_cases: true, + read_cases: true, + update_cases: true, + delete_cases: true, + push_cases: true, + cases_connectors: true, + cases_settings: false, + }) + ).toMatchInlineSnapshot(` + Object { + "all": false, + "connectors": true, + "create": true, "delete": true, "push": true, "read": true, + "settings": false, "update": true, } `); }); + + it('returns true for cases_settings when it is set to true in the ui capabilities', () => { + expect(getUICapabilities({ cases_settings: true })).toMatchInlineSnapshot(` + Object { + "all": false, + "connectors": false, + "create": false, + "delete": false, + "push": false, + "read": false, + "settings": true, + "update": false, + } + `); + }); }); diff --git a/x-pack/plugins/cases/public/client/helpers/capabilities.ts b/x-pack/plugins/cases/public/client/helpers/capabilities.ts index 278512fef623c..9be5b5f05f646 100644 --- a/x-pack/plugins/cases/public/client/helpers/capabilities.ts +++ b/x-pack/plugins/cases/public/client/helpers/capabilities.ts @@ -8,6 +8,7 @@ import type { CasesPermissions } from '../../../common'; import { CASES_CONNECTORS_CAPABILITY, + CASES_SETTINGS_CAPABILITY, CREATE_CASES_CAPABILITY, DELETE_CASES_CAPABILITY, PUSH_CASES_CAPABILITY, @@ -24,7 +25,9 @@ export const getUICapabilities = ( const deletePriv = !!featureCapabilities?.[DELETE_CASES_CAPABILITY]; const push = !!featureCapabilities?.[PUSH_CASES_CAPABILITY]; const connectors = !!featureCapabilities?.[CASES_CONNECTORS_CAPABILITY]; - const all = create && read && update && deletePriv && push && connectors; + const settings = !!featureCapabilities?.[CASES_SETTINGS_CAPABILITY]; + + const all = create && read && update && deletePriv && push && connectors && settings; return { all, @@ -34,5 +37,6 @@ export const getUICapabilities = ( delete: deletePriv, push, connectors, + settings, }; }; diff --git a/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts b/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts index fdb5a22e66985..39b4d3d1edc76 100644 --- a/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts +++ b/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts @@ -194,6 +194,7 @@ export const useApplicationCapabilities = (): UseApplicationCapabilities => { delete: permissions.delete, push: permissions.push, connectors: permissions.connectors, + settings: permissions.settings, }, visualize: { crud: !!capabilities.visualize?.save, read: !!capabilities.visualize?.show }, dashboard: { @@ -215,6 +216,7 @@ export const useApplicationCapabilities = (): UseApplicationCapabilities => { permissions.delete, permissions.push, permissions.connectors, + permissions.settings, ] ); }; diff --git a/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.tsx b/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.tsx index 31ea452874c28..195c1f433a8e7 100644 --- a/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.tsx +++ b/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.tsx @@ -75,6 +75,7 @@ export const createStartServicesMock = ({ license }: StartServiceArgs = {}): Sta delete_cases: true, push_cases: true, cases_connectors: true, + cases_settings: true, }, visualize: { save: true, show: true }, dashboard: { show: true, createNew: true }, diff --git a/x-pack/plugins/cases/public/common/mock/permissions.ts b/x-pack/plugins/cases/public/common/mock/permissions.ts index 4d68e9d36c776..fce274cd7f338 100644 --- a/x-pack/plugins/cases/public/common/mock/permissions.ts +++ b/x-pack/plugins/cases/public/common/mock/permissions.ts @@ -16,7 +16,9 @@ export const noCasesPermissions = () => delete: false, push: false, connectors: false, + settings: false, }); + export const readCasesPermissions = () => buildCasesPermissions({ read: true, @@ -25,6 +27,7 @@ export const readCasesPermissions = () => delete: false, push: false, connectors: true, + settings: false, }); export const noCreateCasesPermissions = () => buildCasesPermissions({ create: false }); export const noUpdateCasesPermissions = () => buildCasesPermissions({ update: false }); @@ -34,6 +37,7 @@ export const writeCasesPermissions = () => buildCasesPermissions({ read: false } export const onlyDeleteCasesPermission = () => buildCasesPermissions({ read: false, create: false, update: false, delete: true, push: false }); export const noConnectorsCasePermission = () => buildCasesPermissions({ connectors: false }); +export const noCasesSettingsPermission = () => buildCasesPermissions({ settings: false }); export const buildCasesPermissions = (overrides: Partial> = {}) => { const create = overrides.create ?? true; @@ -42,7 +46,8 @@ export const buildCasesPermissions = (overrides: Partial delete_cases: false, push_cases: false, cases_connectors: false, + cases_settings: false, }); export const readCasesCapabilities = () => buildCasesCapabilities({ @@ -71,6 +78,7 @@ export const readCasesCapabilities = () => update_cases: false, delete_cases: false, push_cases: false, + cases_settings: false, }); export const writeCasesCapabilities = () => { return buildCasesCapabilities({ @@ -86,5 +94,6 @@ export const buildCasesCapabilities = (overrides?: Partial) = delete_cases: overrides?.delete_cases ?? true, push_cases: overrides?.push_cases ?? true, cases_connectors: overrides?.cases_connectors ?? true, + cases_settings: overrides?.cases_settings ?? true, }; }; diff --git a/x-pack/plugins/cases/public/components/all_cases/header.test.tsx b/x-pack/plugins/cases/public/components/all_cases/header.test.tsx index 08bd228b32e11..333f330394442 100644 --- a/x-pack/plugins/cases/public/components/all_cases/header.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/header.test.tsx @@ -46,9 +46,9 @@ describe('CasesTableHeader', () => { expect(result.getByTestId('configure-case-button')).toBeInTheDocument(); }); - it('does not display the configure button when the user does not have update privileges', () => { + it('does not display the configure button when the user does not have settings privileges', () => { appMockRender = createAppMockRenderer({ - permissions: buildCasesPermissions({ update: false }), + permissions: buildCasesPermissions({ settings: false }), }); const result = appMockRender.render(); diff --git a/x-pack/plugins/cases/public/components/all_cases/nav_buttons.test.tsx b/x-pack/plugins/cases/public/components/all_cases/nav_buttons.test.tsx new file mode 100644 index 0000000000000..b825f5c27f2eb --- /dev/null +++ b/x-pack/plugins/cases/public/components/all_cases/nav_buttons.test.tsx @@ -0,0 +1,55 @@ +/* + * 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 { screen } from '@testing-library/react'; +import type { AppMockRenderer } from '../../common/mock'; +import { + createAppMockRenderer, + noCasesSettingsPermission, + noCreateCasesPermissions, + buildCasesPermissions, +} from '../../common/mock'; +import { NavButtons } from './nav_buttons'; + +describe('NavButtons', () => { + let appMockRenderer: AppMockRenderer; + + beforeEach(() => { + appMockRenderer = createAppMockRenderer(); + }); + + it('shows the configure case button', () => { + appMockRenderer.render(); + + expect(screen.getByTestId('configure-case-button')).toBeInTheDocument(); + }); + + it('does not render the case create button with no create permissions', () => { + appMockRenderer = createAppMockRenderer({ permissions: noCreateCasesPermissions() }); + appMockRenderer.render(); + + expect(screen.queryByTestId('createNewCaseBtn')).not.toBeInTheDocument(); + }); + + it('does not render the case configure button with no settings permissions', () => { + appMockRenderer = createAppMockRenderer({ permissions: noCasesSettingsPermission() }); + appMockRenderer.render(); + + expect(screen.queryByTestId('configure-case-button')).not.toBeInTheDocument(); + }); + + it('does not render any button with no create and no settings permissions', () => { + appMockRenderer = createAppMockRenderer({ + permissions: buildCasesPermissions({ create: false, settings: false }), + }); + appMockRenderer.render(); + + expect(screen.queryByTestId('createNewCaseBtn')).not.toBeInTheDocument(); + expect(screen.queryByTestId('configure-case-button')).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/all_cases/nav_buttons.tsx b/x-pack/plugins/cases/public/components/all_cases/nav_buttons.tsx index aafb287eed869..05febf94f431f 100644 --- a/x-pack/plugins/cases/public/components/all_cases/nav_buttons.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/nav_buttons.tsx @@ -43,14 +43,14 @@ export const NavButtons: FunctionComponent = ({ actionsErrors }) => { [navigateToCreateCase] ); - if (!permissions.create && !permissions.update) { + if (!permissions.create && !permissions.settings) { return null; } return ( - {permissions.update && ( + {permissions.settings && ( { }); }); - describe('Configure cases', () => { - it('navigates to the configure cases page', () => { + describe('Cases settings', () => { + it('navigates to the cases settings page', () => { renderWithRouter(['/cases/configure']); expect(screen.getByText('Settings')).toBeInTheDocument(); }); - it('shows the no privileges page if the user does not have update privileges', () => { - renderWithRouter(['/cases/configure'], noUpdateCasesPermissions()); + it('shows the no privileges page if the user does not have settings privileges', () => { + renderWithRouter(['/cases/configure'], noCasesSettingsPermission()); expect(screen.getByText('Privileges required')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/cases/public/components/app/routes.tsx b/x-pack/plugins/cases/public/components/app/routes.tsx index 7f4e35fc4ac81..27bf536b11ab8 100644 --- a/x-pack/plugins/cases/public/components/app/routes.tsx +++ b/x-pack/plugins/cases/public/components/app/routes.tsx @@ -71,7 +71,7 @@ const CasesRoutesComponent: React.FC = ({ - {permissions.update ? ( + {permissions.settings ? ( ) : ( diff --git a/x-pack/plugins/cases/public/components/use_push_to_service/callout/callout.test.tsx b/x-pack/plugins/cases/public/components/use_push_to_service/callout/callout.test.tsx index 868e9be03ff6f..2dfd0d188f5bb 100644 --- a/x-pack/plugins/cases/public/components/use_push_to_service/callout/callout.test.tsx +++ b/x-pack/plugins/cases/public/components/use_push_to_service/callout/callout.test.tsx @@ -6,14 +6,18 @@ */ import React from 'react'; -import { mount } from 'enzyme'; +import { screen } from '@testing-library/react'; import type { CallOutProps } from './callout'; import { CallOut } from './callout'; import { CLOSED_CASE_PUSH_ERROR_ID } from './types'; -import { TestProviders } from '../../../common/mock'; +import type { AppMockRenderer } from '../../../common/mock'; +import { noCasesSettingsPermission, createAppMockRenderer } from '../../../common/mock'; +import userEvent from '@testing-library/user-event'; describe('Callout', () => { + let appMockRenderer: AppMockRenderer; + const handleButtonClick = jest.fn(); const defaultProps: CallOutProps = { id: 'md5-hex', @@ -31,50 +35,19 @@ describe('Callout', () => { beforeEach(() => { jest.clearAllMocks(); + appMockRenderer = createAppMockRenderer(); }); it('It renders the callout', () => { - const wrapper = mount(); - expect(wrapper.find(`[data-test-subj="case-callout-md5-hex"]`).exists()).toBeTruthy(); - expect(wrapper.find(`[data-test-subj="callout-messages-md5-hex"]`).exists()).toBeTruthy(); - expect(wrapper.find(`[data-test-subj="callout-onclick-md5-hex"]`).exists()).toBeTruthy(); + appMockRenderer.render(); + expect(screen.getByTestId('case-callout-md5-hex')).toBeInTheDocument(); + expect(screen.getByTestId('callout-messages-md5-hex')).toBeInTheDocument(); + expect(screen.getByTestId('callout-onclick-md5-hex')).toBeInTheDocument(); }); it('does not shows any messages when the list is empty', () => { - const wrapper = mount(); - expect(wrapper.find(`[data-test-subj="callout-messages-md5-hex"]`).exists()).toBeFalsy(); - }); - - it('transform the button color correctly - primary', () => { - const wrapper = mount(); - const className = - wrapper.find(`button[data-test-subj="callout-onclick-md5-hex"]`).first().prop('className') ?? - ''; - expect(className.includes('primary')).toBeTruthy(); - }); - - it('transform the button color correctly - success', () => { - const wrapper = mount(); - const className = - wrapper.find(`button[data-test-subj="callout-onclick-md5-hex"]`).first().prop('className') ?? - ''; - expect(className.includes('success')).toBeTruthy(); - }); - - it('transform the button color correctly - warning', () => { - const wrapper = mount(); - const className = - wrapper.find(`button[data-test-subj="callout-onclick-md5-hex"]`).first().prop('className') ?? - ''; - expect(className.includes('warning')).toBeTruthy(); - }); - - it('transform the button color correctly - danger', () => { - const wrapper = mount(); - const className = - wrapper.find(`button[data-test-subj="callout-onclick-md5-hex"]`).first().prop('className') ?? - ''; - expect(className.includes('danger')).toBeTruthy(); + appMockRenderer.render(); + expect(screen.queryByTestId('callout-messages-md5-hex')).not.toBeInTheDocument(); }); it('does not show the button when case is closed error is present', () => { @@ -89,15 +62,9 @@ describe('Callout', () => { ], }; - const wrapper = mount( - - - - ); + appMockRenderer.render(); - expect(wrapper.find(`button[data-test-subj="callout-onclick-md5-hex"]`).exists()).toEqual( - false - ); + expect(screen.queryByTestId('callout-onclick-md5-hex')).not.toBeInTheDocument(); }); it('does not show the button when license error is present', () => { @@ -106,22 +73,27 @@ describe('Callout', () => { hasLicenseError: true, }; - const wrapper = mount( - - - - ); + appMockRenderer.render(); + + expect(screen.queryByTestId('callout-onclick-md5-hex')).not.toBeInTheDocument(); + }); + + it('does not show the button with no settings permissions', () => { + appMockRenderer = createAppMockRenderer({ permissions: noCasesSettingsPermission() }); - expect(wrapper.find(`button[data-test-subj="callout-onclick-md5-hex"]`).exists()).toEqual( - false - ); + appMockRenderer.render(); + + expect(screen.queryByTestId('callout-onclick-md5-hex')).not.toBeInTheDocument(); }); // use this for storage if we ever want to bring that back it('onClick passes id and type', () => { - const wrapper = mount(); - expect(wrapper.find(`[data-test-subj="callout-onclick-md5-hex"]`).exists()).toBeTruthy(); - wrapper.find(`button[data-test-subj="callout-onclick-md5-hex"]`).simulate('click'); + appMockRenderer.render(); + + expect(screen.getByTestId('callout-onclick-md5-hex')).toBeInTheDocument(); + + userEvent.click(screen.getByTestId('callout-onclick-md5-hex')); + expect(handleButtonClick.mock.calls[0][1]).toEqual('md5-hex'); expect(handleButtonClick.mock.calls[0][2]).toEqual('primary'); }); diff --git a/x-pack/plugins/cases/public/components/use_push_to_service/callout/callout.tsx b/x-pack/plugins/cases/public/components/use_push_to_service/callout/callout.tsx index ffd19f8366252..c94fbb826df48 100644 --- a/x-pack/plugins/cases/public/components/use_push_to_service/callout/callout.tsx +++ b/x-pack/plugins/cases/public/components/use_push_to_service/callout/callout.tsx @@ -12,6 +12,7 @@ import React, { memo, useCallback, useMemo } from 'react'; import type { ErrorMessage } from './types'; import { CLOSED_CASE_PUSH_ERROR_ID } from './types'; import * as i18n from './translations'; +import { useCasesContext } from '../../cases_context/use_cases_context'; export interface CallOutProps { handleButtonClick: ( @@ -32,6 +33,8 @@ const CallOutComponent = ({ type, hasLicenseError, }: CallOutProps) => { + const { permissions } = useCasesContext(); + const handleCallOut = useCallback( (e) => handleButtonClick(e, id, type), [handleButtonClick, id, type] @@ -57,7 +60,7 @@ const CallOutComponent = ({ size="s" > - {!isCaseClosed && !hasLicenseError && ( + {!isCaseClosed && !hasLicenseError && permissions.settings && ( = { update: false, delete: false, push: false, + connectors: false, + settings: false, }), getRuleIdFromEvent: jest.fn(), groupAlertsByRule: jest.fn(), diff --git a/x-pack/plugins/cases/server/features.ts b/x-pack/plugins/cases/server/features.ts index b44c3589ecd08..62276ad4fcc30 100644 --- a/x-pack/plugins/cases/server/features.ts +++ b/x-pack/plugins/cases/server/features.ts @@ -100,6 +100,33 @@ export const getCasesKibanaFeature = (): KibanaFeatureConfig => { }, ], }, + { + name: i18n.translate('xpack.cases.features.casesSettingsSubFeatureName', { + defaultMessage: 'Case Settings', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'cases_settings', + name: i18n.translate('xpack.cases.features.casesSettingsSubFeatureDetails', { + defaultMessage: 'Edit Case Settings', + }), + includeIn: 'all', + savedObject: { + all: [...filesSavedObjectTypes], + read: [...filesSavedObjectTypes], + }, + cases: { + settings: [APP_ID], + }, + ui: capabilities.settings, + }, + ], + }, + ], + }, ], }; }; diff --git a/x-pack/plugins/enterprise_search/README.md b/x-pack/plugins/enterprise_search/README.md index af0cdd43d97b8..bc49c47fe6880 100644 --- a/x-pack/plugins/enterprise_search/README.md +++ b/x-pack/plugins/enterprise_search/README.md @@ -137,4 +137,4 @@ To track what Cypress is doing while running tests, you can pass in `--config vi See [our functional test runner README](../../test/functional_enterprise_search). -Our automated accessibility tests can be found in [x-pack/test/accessibility/apps](../../test/accessibility/apps/enterprise_search.ts). \ No newline at end of file +Our automated accessibility tests can be found in [x-pack/test/accessibility/apps](../../test/accessibility/apps/group3/enterprise_search.ts). \ No newline at end of file diff --git a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/components/action_menu/action_menu.test.tsx b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/components/action_menu/action_menu.test.tsx index 4e81ca840199f..5d43f5bd1e6df 100644 --- a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/components/action_menu/action_menu.test.tsx +++ b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/components/action_menu/action_menu.test.tsx @@ -12,8 +12,6 @@ import { sampleAttribute } from '../../configurations/test_data/sample_attribute import * as pluginHook from '../../../../../hooks/use_plugin_context'; import { TypedLensByValueInput } from '@kbn/lens-plugin/public'; import { ExpViewActionMenuContent } from './action_menu'; -import { noCasesPermissions as mockUseGetCasesPermissions } from '@kbn/observability-shared-plugin/public'; -import * as obsHooks from '@kbn/observability-shared-plugin/public/hooks/use_get_user_cases_permissions'; jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({ appMountParameters: { @@ -21,13 +19,6 @@ jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({ }, } as any); -jest.spyOn(obsHooks, 'useGetUserCasesPermissions').mockImplementation( - () => - ({ - useGetUserCasesPermissions: jest.fn(() => mockUseGetCasesPermissions()), - } as any) -); - describe('Action Menu', function () { afterAll(() => { jest.clearAllMocks(); diff --git a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/exploratory_view.test.tsx b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/exploratory_view.test.tsx index 7b4e0cb5cc57f..83cae3e8b4ebb 100644 --- a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/exploratory_view.test.tsx +++ b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/exploratory_view.test.tsx @@ -12,8 +12,6 @@ import { ExploratoryView } from './exploratory_view'; import * as obsvDataViews from '../../../utils/observability_data_views/observability_data_views'; import * as pluginHook from '../../../hooks/use_plugin_context'; import { createStubIndexPattern } from '@kbn/data-plugin/common/stubs'; -import { noCasesPermissions as mockUseGetCasesPermissions } from '@kbn/observability-shared-plugin/public/utils/cases_permissions'; -import * as obsHooks from '@kbn/observability-shared-plugin/public/hooks/use_get_user_cases_permissions'; jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({ appMountParameters: { @@ -21,13 +19,6 @@ jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({ }, } as any); -jest.spyOn(obsHooks, 'useGetUserCasesPermissions').mockImplementation( - () => - ({ - useGetUserCasesPermissions: jest.fn(() => mockUseGetCasesPermissions()), - } as any) -); - describe('ExploratoryView', () => { mockAppDataView(); diff --git a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/header/add_to_case_action.test.tsx b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/header/add_to_case_action.test.tsx index ffccfdf6db3f2..1d42716bf405d 100644 --- a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/header/add_to_case_action.test.tsx +++ b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/header/add_to_case_action.test.tsx @@ -13,10 +13,15 @@ import * as useCaseHook from '../hooks/use_add_to_case'; import * as datePicker from '../components/date_range_picker'; import moment from 'moment'; import { noCasesPermissions as mockUseGetCasesPermissions } from '@kbn/observability-shared-plugin/public'; -import * as obsHooks from '@kbn/observability-shared-plugin/public/hooks/use_get_user_cases_permissions'; -jest.spyOn(obsHooks, 'useGetUserCasesPermissions').mockReturnValue(mockUseGetCasesPermissions()); describe('AddToCaseAction', function () { + const coreRenderProps = { + cases: { + ui: { getAllCasesSelectorModal: jest.fn() }, + helpers: { canUseCases: () => mockUseGetCasesPermissions() }, + }, + }; + beforeEach(() => { jest.spyOn(datePicker, 'parseRelativeDate').mockRestore(); }); @@ -26,7 +31,8 @@ describe('AddToCaseAction', function () { + />, + { core: coreRenderProps } ); expect(await findByText('Add to case')).toBeInTheDocument(); }); @@ -39,7 +45,8 @@ describe('AddToCaseAction', function () { + />, + { core: coreRenderProps } ); expect(await findByText('Add to case')).toBeInTheDocument(); @@ -60,7 +67,8 @@ describe('AddToCaseAction', function () { const useAddToCaseHook = jest.spyOn(useCaseHook, 'useAddToCase'); const { getByText } = render( - + , + { core: coreRenderProps } ); expect(await forNearestButton(getByText)('Add to case')).toBeDisabled(); @@ -95,7 +103,7 @@ describe('AddToCaseAction', function () { lensAttributes={{ title: 'Performance distribution' } as any} timeRange={{ to: 'now', from: 'now-5m' }} />, - { initSeries } + { initSeries, core: coreRenderProps } ); fireEvent.click(await findByText('Add to case')); @@ -111,6 +119,7 @@ describe('AddToCaseAction', function () { delete: false, push: false, connectors: false, + settings: false, }, }) ); diff --git a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/header/add_to_case_action.tsx b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/header/add_to_case_action.tsx index 1cbb904f6500d..590451eaea6ef 100644 --- a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/header/add_to_case_action.tsx +++ b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/header/add_to_case_action.tsx @@ -16,7 +16,6 @@ import { } from '@kbn/cases-plugin/public'; import { TypedLensByValueInput } from '@kbn/lens-plugin/public'; import { observabilityFeatureId } from '@kbn/observability-shared-plugin/public'; -import { useGetUserCasesPermissions } from '@kbn/observability-shared-plugin/public'; import { ObservabilityAppServices } from '../../../../application/types'; import { useAddToCase } from '../hooks/use_add_to_case'; import { parseRelativeDate } from '../components/date_range_picker'; @@ -37,7 +36,7 @@ export function AddToCaseAction({ timeRange, }: AddToCaseProps) { const kServices = useKibana().services; - const userCasesPermissions = useGetUserCasesPermissions(); + const userCasesPermissions = kServices.cases.helpers.canUseCases([observabilityFeatureId]); const { cases, diff --git a/x-pack/plugins/features/common/feature_kibana_privileges.ts b/x-pack/plugins/features/common/feature_kibana_privileges.ts index 3a56b080bb91f..49c001c890b69 100644 --- a/x-pack/plugins/features/common/feature_kibana_privileges.ts +++ b/x-pack/plugins/features/common/feature_kibana_privileges.ts @@ -204,6 +204,16 @@ export interface FeatureKibanaPrivileges { * ``` */ delete?: readonly string[]; + /** + * List of case owners which users should have settings access to when granted this privilege. + * @example + * ```ts + * { + * settings: ['securitySolution'] + * } + * ``` + */ + settings?: readonly string[]; }; /** diff --git a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap index 62e868e77e520..7247c1d23cb0c 100644 --- a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap +++ b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap @@ -558,6 +558,7 @@ Array [ "delete": Array [], "push": Array [], "read": Array [], + "settings": Array [], "update": Array [], }, "catalogue": Array [ @@ -710,6 +711,7 @@ Array [ "delete": Array [], "push": Array [], "read": Array [], + "settings": Array [], "update": Array [], }, "catalogue": Array [ @@ -1032,6 +1034,7 @@ Array [ "delete": Array [], "push": Array [], "read": Array [], + "settings": Array [], "update": Array [], }, "catalogue": Array [ @@ -1169,6 +1172,7 @@ Array [ "delete": Array [], "push": Array [], "read": Array [], + "settings": Array [], "update": Array [], }, "catalogue": Array [ @@ -1321,6 +1325,7 @@ Array [ "delete": Array [], "push": Array [], "read": Array [], + "settings": Array [], "update": Array [], }, "catalogue": Array [ @@ -1643,6 +1648,7 @@ Array [ "delete": Array [], "push": Array [], "read": Array [], + "settings": Array [], "update": Array [], }, "catalogue": Array [ diff --git a/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.test.ts b/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.test.ts index f2f6ed1071f81..58a39c85bf9e9 100644 --- a/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.test.ts +++ b/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.test.ts @@ -77,6 +77,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-type'], delete: ['cases-delete-type'], push: ['cases-push-type'], + settings: ['cases-settings-type'], }, ui: ['ui-action'], }, @@ -146,6 +147,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-type'], delete: ['cases-delete-type'], push: ['cases-push-type'], + settings: ['cases-settings-type'], }, ui: ['ui-action'], }, @@ -214,6 +216,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-type'], delete: ['cases-delete-type'], push: ['cases-push-type'], + settings: ['cases-settings-type'], }, ui: ['ui-action'], }, @@ -284,6 +287,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-type'], delete: ['cases-delete-type'], push: ['cases-push-type'], + settings: ['cases-settings-type'], }, ui: ['ui-action'], }, @@ -324,6 +328,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-type'], delete: ['cases-delete-type'], push: ['cases-push-type'], + settings: ['cases-settings-type'], }, ui: ['ui-action'], }, @@ -385,6 +390,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-sub-type'], delete: ['cases-delete-sub-type'], push: ['cases-push-sub-type'], + settings: ['cases-settings-sub-type'], }, ui: ['ui-sub-type'], }, @@ -431,6 +437,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-type'], delete: ['cases-delete-type'], push: ['cases-push-type'], + settings: ['cases-settings-type'], }, ui: ['ui-action'], }, @@ -498,6 +505,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-type'], delete: ['cases-delete-type'], push: ['cases-push-type'], + settings: ['cases-settings-type'], }, ui: ['ui-action'], }, @@ -559,6 +567,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-sub-type'], delete: ['cases-delete-sub-type'], push: ['cases-push-sub-type'], + settings: ['cases-settings-sub-type'], }, ui: ['ui-sub-type'], }, @@ -605,6 +614,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-type'], delete: ['cases-delete-type'], push: ['cases-push-type'], + settings: ['cases-settings-type'], }, ui: ['ui-action'], }, @@ -672,6 +682,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-type'], delete: ['cases-delete-type'], push: ['cases-push-type'], + settings: ['cases-settings-type'], }, ui: ['ui-action'], }, @@ -734,6 +745,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-sub-type'], delete: ['cases-delete-sub-type'], push: ['cases-push-sub-type'], + settings: ['cases-settings-sub-type'], }, ui: ['ui-sub-type'], }, @@ -783,6 +795,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-type', 'cases-update-sub-type'], delete: ['cases-delete-type', 'cases-delete-sub-type'], push: ['cases-push-type', 'cases-push-sub-type'], + settings: ['cases-settings-type', 'cases-settings-sub-type'], }, ui: ['ui-action', 'ui-sub-type'], }, @@ -818,6 +831,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-sub-type'], delete: ['cases-delete-sub-type'], push: ['cases-push-sub-type'], + settings: ['cases-settings-sub-type'], }, ui: ['ui-action', 'ui-sub-type'], }, @@ -860,6 +874,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-type'], delete: ['cases-delete-type'], push: ['cases-push-type'], + settings: ['cases-settings-type'], }, ui: ['ui-action'], }, @@ -964,6 +979,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-type'], delete: ['cases-delete-type'], push: ['cases-push-type'], + settings: ['cases-settings-type'], }, ui: ['ui-action'], }, @@ -998,6 +1014,7 @@ describe('featurePrivilegeIterator', () => { update: [], delete: [], push: [], + settings: [], }, ui: ['ui-action'], }, @@ -1038,6 +1055,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-type'], delete: ['cases-delete-type'], push: ['cases-push-type'], + settings: ['cases-settings-type'], }, ui: ['ui-action'], }, @@ -1100,6 +1118,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-sub-type'], delete: ['cases-delete-sub-type'], push: ['cases-push-sub-type'], + settings: ['cases-settings-sub-type'], }, ui: ['ui-sub-type'], }, @@ -1149,6 +1168,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-type', 'cases-update-sub-type'], delete: ['cases-delete-type', 'cases-delete-sub-type'], push: ['cases-push-type', 'cases-push-sub-type'], + settings: ['cases-settings-type', 'cases-settings-sub-type'], }, ui: ['ui-action', 'ui-sub-type'], }, @@ -1341,6 +1361,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-sub-type'], delete: ['cases-delete-sub-type'], push: ['cases-push-sub-type'], + settings: ['cases-settings-sub-type'], }, ui: ['ui-sub-type'], }, @@ -1390,6 +1411,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-sub-type'], delete: ['cases-delete-sub-type'], push: ['cases-push-sub-type'], + settings: ['cases-settings-sub-type'], }, ui: ['ui-sub-type'], }, @@ -1425,6 +1447,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-sub-type'], delete: ['cases-delete-sub-type'], push: ['cases-push-sub-type'], + settings: ['cases-settings-sub-type'], }, ui: ['ui-sub-type'], }, @@ -1465,6 +1488,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-type'], delete: ['cases-delete-type'], push: ['cases-push-type'], + settings: ['cases-settings-type'], }, ui: ['ui-action'], }, @@ -1555,6 +1579,7 @@ describe('featurePrivilegeIterator', () => { update: ['cases-update-type'], delete: ['cases-delete-type'], push: ['cases-push-type'], + settings: ['cases-settings-type'], }, ui: ['ui-action'], }, @@ -1589,6 +1614,7 @@ describe('featurePrivilegeIterator', () => { update: [], delete: [], push: [], + settings: [], }, ui: ['ui-action'], }, diff --git a/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.ts b/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.ts index 9392b3b3fee33..0d1dc8e3ab788 100644 --- a/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.ts +++ b/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.ts @@ -147,6 +147,10 @@ function mergeWithSubFeatures( subFeaturePrivilege.cases?.delete ?? [] ), push: mergeArrays(mergedConfig.cases?.push ?? [], subFeaturePrivilege.cases?.push ?? []), + settings: mergeArrays( + mergedConfig.cases?.settings ?? [], + subFeaturePrivilege.cases?.settings ?? [] + ), }; } return mergedConfig; diff --git a/x-pack/plugins/features/server/feature_schema.ts b/x-pack/plugins/features/server/feature_schema.ts index 416a9bf534b3a..b332ea355dcc0 100644 --- a/x-pack/plugins/features/server/feature_schema.ts +++ b/x-pack/plugins/features/server/feature_schema.ts @@ -82,6 +82,7 @@ const casesSchemaObject = schema.maybe( update: schema.maybe(casesSchema), delete: schema.maybe(casesSchema), push: schema.maybe(casesSchema), + settings: schema.maybe(casesSchema), }) ); diff --git a/x-pack/plugins/fleet/server/mocks/index.ts b/x-pack/plugins/fleet/server/mocks/index.ts index 2716fd82b6811..ec8ada164623d 100644 --- a/x-pack/plugins/fleet/server/mocks/index.ts +++ b/x-pack/plugins/fleet/server/mocks/index.ts @@ -201,5 +201,7 @@ export function createUninstallTokenServiceMock(): UninstallTokenServiceInterfac generateTokensForPolicyIds: jest.fn(), generateTokensForAllPolicies: jest.fn(), encryptTokens: jest.fn(), + checkTokenValidityForAllPolicies: jest.fn(), + checkTokenValidityForPolicy: jest.fn(), }; } diff --git a/x-pack/plugins/fleet/server/services/agent_policy.test.ts b/x-pack/plugins/fleet/server/services/agent_policy.test.ts index 710ac46b94592..b6950ba672817 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.test.ts @@ -32,6 +32,7 @@ import { getFullAgentPolicy } from './agent_policies'; import * as outputsHelpers from './agent_policies/outputs_helpers'; import { auditLoggingService } from './audit_logging'; import { licenseService } from './license'; +import type { UninstallTokenServiceInterface } from './security/uninstall_token_service'; function getSavedObjectMock(agentPolicyAttributes: any) { const mock = savedObjectsClientMock.create(); @@ -182,13 +183,13 @@ describe('agent policy', () => { }); }); - it('should throw FleetUnauthorizedError if is_protected=true with insufficient license', () => { + it('should throw FleetUnauthorizedError if is_protected=true with insufficient license', async () => { jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(false); const soClient = getAgentPolicyCreateMock(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - expect( + await expect( agentPolicyService.create(soClient, esClient, { name: 'test', namespace: 'default', @@ -199,13 +200,13 @@ describe('agent policy', () => { ); }); - it('should not throw FleetUnauthorizedError if is_protected=false with insufficient license', () => { + it('should not throw FleetUnauthorizedError if is_protected=false with insufficient license', async () => { jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(false); const soClient = getAgentPolicyCreateMock(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - expect( + await expect( agentPolicyService.create(soClient, esClient, { name: 'test', namespace: 'default', @@ -619,7 +620,7 @@ describe('agent policy', () => { }); }); - it('should throw FleetUnauthorizedError if is_protected=true with insufficient license', () => { + it('should throw FleetUnauthorizedError if is_protected=true with insufficient license', async () => { jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(false); const soClient = getAgentPolicyCreateMock(); @@ -632,7 +633,7 @@ describe('agent policy', () => { references: [], }); - expect( + await expect( agentPolicyService.update(soClient, esClient, 'test-id', { name: 'test', namespace: 'default', @@ -643,7 +644,7 @@ describe('agent policy', () => { ); }); - it('should not throw FleetUnauthorizedError if is_protected=false with insufficient license', () => { + it('should not throw FleetUnauthorizedError if is_protected=false with insufficient license', async () => { jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(false); const soClient = getAgentPolicyCreateMock(); @@ -656,7 +657,7 @@ describe('agent policy', () => { references: [], }); - expect( + await expect( agentPolicyService.update(soClient, esClient, 'test-id', { name: 'test', namespace: 'default', @@ -665,6 +666,32 @@ describe('agent policy', () => { new FleetUnauthorizedError('Tamper protection requires Platinum license') ); }); + + it('should throw Error if is_protected=true with invalid uninstall token', async () => { + jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true); + + mockedAppContextService.getUninstallTokenService.mockReturnValueOnce({ + checkTokenValidityForPolicy: jest.fn().mockRejectedValueOnce(new Error('reason')), + } as unknown as UninstallTokenServiceInterface); + + const soClient = getAgentPolicyCreateMock(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + + soClient.get.mockResolvedValue({ + attributes: {}, + id: 'test-id', + type: 'mocked', + references: [], + }); + + await expect( + agentPolicyService.update(soClient, esClient, 'test-id', { + name: 'test', + namespace: 'default', + is_protected: true, + }) + ).rejects.toThrowError(new Error('Cannot enable Agent Tamper Protection: reason')); + }); }); describe('deployPolicy', () => { diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 8673bd6ad91a9..5e8c897d5611a 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -512,6 +512,7 @@ class AgentPolicyService { } this.checkTamperProtectionLicense(agentPolicy); + await this.checkForValidUninstallToken(agentPolicy, id); const logger = appContextService.getLogger(); @@ -1212,6 +1213,20 @@ class AgentPolicyService { throw new FleetUnauthorizedError('Tamper protection requires Platinum license'); } } + private async checkForValidUninstallToken( + agentPolicy: { is_protected?: boolean }, + policyId: string + ): Promise { + if (agentPolicy?.is_protected) { + const uninstallTokenService = appContextService.getUninstallTokenService(); + + try { + await uninstallTokenService?.checkTokenValidityForPolicy(policyId); + } catch (e) { + throw new Error(`Cannot enable Agent Tamper Protection: ${e.message}`); + } + } + } } export const agentPolicyService = new AgentPolicyService(); diff --git a/x-pack/plugins/fleet/server/services/security/uninstall_token_service/index.test.ts b/x-pack/plugins/fleet/server/services/security/uninstall_token_service/index.test.ts index 4cf657b7255c5..4b3be1de81632 100644 --- a/x-pack/plugins/fleet/server/services/security/uninstall_token_service/index.test.ts +++ b/x-pack/plugins/fleet/server/services/security/uninstall_token_service/index.test.ts @@ -499,5 +499,80 @@ describe('UninstallTokenService', () => { }); }); }); + + describe('check validity of tokens', () => { + const okaySO = getDefaultSO(canEncrypt); + + const errorWithDecryptionSO2 = { + ...getDefaultSO2(canEncrypt), + error: new Error('error reason'), + }; + const missingTokenSO2 = { + ...getDefaultSO2(canEncrypt), + attributes: { + ...getDefaultSO2(canEncrypt).attributes, + token: undefined, + token_plain: undefined, + }, + }; + + describe('checkTokenValidityForAllPolicies', () => { + it('resolves if all of the tokens are available', async () => { + mockCreatePointInTimeFinderAsInternalUser(); + + await expect( + uninstallTokenService.checkTokenValidityForAllPolicies() + ).resolves.not.toThrowError(); + }); + + it('rejects if any of the tokens is missing', async () => { + mockCreatePointInTimeFinderAsInternalUser([okaySO, missingTokenSO2]); + + await expect( + uninstallTokenService.checkTokenValidityForAllPolicies() + ).rejects.toThrowError( + 'Invalid uninstall token: Saved object is missing the `token` attribute.' + ); + }); + + it('rejects if token decryption gives error', async () => { + mockCreatePointInTimeFinderAsInternalUser([okaySO, errorWithDecryptionSO2]); + + await expect( + uninstallTokenService.checkTokenValidityForAllPolicies() + ).rejects.toThrowError('Error when reading Uninstall Token: error reason'); + }); + }); + + describe('checkTokenValidityForPolicy', () => { + it('resolves if token is available', async () => { + mockCreatePointInTimeFinderAsInternalUser(); + + await expect( + uninstallTokenService.checkTokenValidityForPolicy(okaySO.attributes.policy_id) + ).resolves.not.toThrowError(); + }); + + it('rejects if token is missing', async () => { + mockCreatePointInTimeFinderAsInternalUser([okaySO, missingTokenSO2]); + + await expect( + uninstallTokenService.checkTokenValidityForPolicy(missingTokenSO2.attributes.policy_id) + ).rejects.toThrowError( + 'Invalid uninstall token: Saved object is missing the `token` attribute.' + ); + }); + + it('rejects if token decryption gives error', async () => { + mockCreatePointInTimeFinderAsInternalUser([okaySO, errorWithDecryptionSO2]); + + await expect( + uninstallTokenService.checkTokenValidityForPolicy( + errorWithDecryptionSO2.attributes.policy_id + ) + ).rejects.toThrowError('Error when reading Uninstall Token: error reason'); + }); + }); + }); }); }); diff --git a/x-pack/plugins/fleet/server/services/security/uninstall_token_service/index.ts b/x-pack/plugins/fleet/server/services/security/uninstall_token_service/index.ts index 8309910be6f53..9e03e7869c584 100644 --- a/x-pack/plugins/fleet/server/services/security/uninstall_token_service/index.ts +++ b/x-pack/plugins/fleet/server/services/security/uninstall_token_service/index.ts @@ -109,7 +109,7 @@ export interface UninstallTokenServiceInterface { * @param force generate a new token even if one already exists * @returns hashedToken */ - generateTokenForPolicyId(policyId: string, force?: boolean): Promise; + generateTokenForPolicyId(policyId: string, force?: boolean): Promise; /** * Generate uninstall tokens for given policy ids @@ -119,7 +119,7 @@ export interface UninstallTokenServiceInterface { * @param force generate a new token even if one already exists * @returns Record */ - generateTokensForPolicyIds(policyIds: string[], force?: boolean): Promise>; + generateTokensForPolicyIds(policyIds: string[], force?: boolean): Promise; /** * Generate uninstall tokens all policies @@ -128,12 +128,26 @@ export interface UninstallTokenServiceInterface { * @param force generate a new token even if one already exists * @returns Record */ - generateTokensForAllPolicies(force?: boolean): Promise>; + generateTokensForAllPolicies(force?: boolean): Promise; /** * If encryption is available, checks for any plain text uninstall tokens and encrypts them */ encryptTokens(): Promise; + + /** + * Check whether the selected policy has a valid uninstall token. Rejects returning promise if not. + * + * @param policyId policy Id to check + */ + checkTokenValidityForPolicy(policyId: string): Promise; + + /** + * Check whether all policies have a valid uninstall token. Rejects returning promise if not. + * + * @param policyId policy Id to check + */ + checkTokenValidityForAllPolicies(): Promise; } export class UninstallTokenService implements UninstallTokenServiceInterface { @@ -210,7 +224,11 @@ export class UninstallTokenService implements UninstallTokenServiceInterface { tokensFinder.close(); const uninstallTokens: UninstallToken[] = tokenObject.map( - ({ id: _id, attributes, created_at: createdAt }) => { + ({ id: _id, attributes, created_at: createdAt, error }) => { + if (error) { + throw new UninstallTokenError(`Error when reading Uninstall Token: ${error.message}`); + } + this.assertPolicyId(attributes); this.assertToken(attributes); this.assertCreatedAt(createdAt); @@ -304,32 +322,30 @@ export class UninstallTokenService implements UninstallTokenServiceInterface { return this.getHashedTokensForPolicyIds(policyIds); } - public async generateTokenForPolicyId(policyId: string, force: boolean = false): Promise { - return (await this.generateTokensForPolicyIds([policyId], force))[policyId]; + public generateTokenForPolicyId(policyId: string, force: boolean = false): Promise { + return this.generateTokensForPolicyIds([policyId], force); } public async generateTokensForPolicyIds( policyIds: string[], force: boolean = false - ): Promise> { + ): Promise { const { agentTamperProtectionEnabled } = appContextService.getExperimentalFeatures(); if (!agentTamperProtectionEnabled || !policyIds.length) { - return {}; + return; } - const existingTokens = force - ? {} - : (await this.getDecryptedTokensForPolicyIds(policyIds)).reduce( - (acc, { policy_id: policyId, token }) => { - acc[policyId] = token; - return acc; - }, - {} as Record - ); + const existingTokens = new Set(); + + if (!force) { + (await this.getTokenObjectsByIncludeFilter(policyIds)).forEach((tokenObject) => { + existingTokens.add(tokenObject._source[UNINSTALL_TOKENS_SAVED_OBJECT_TYPE].policy_id); + }); + } const missingTokenPolicyIds = force ? policyIds - : policyIds.filter((policyId) => !existingTokens[policyId]); + : policyIds.filter((policyId) => !existingTokens.has(policyId)); const newTokensMap = missingTokenPolicyIds.reduce((acc, policyId) => { const token = this.generateToken(); @@ -338,7 +354,6 @@ export class UninstallTokenService implements UninstallTokenServiceInterface { [policyId]: token, }; }, {} as Record); - await this.persistTokens(missingTokenPolicyIds, newTokensMap); if (force) { const config = appContextService.getConfig(); @@ -349,21 +364,9 @@ export class UninstallTokenService implements UninstallTokenServiceInterface { await agentPolicyService.deployPolicies(this.soClient, policyIdsBatch) ); } - - const tokensMap = { - ...existingTokens, - ...newTokensMap, - }; - - return Object.entries(tokensMap).reduce((acc, [policyId, token]) => { - acc[policyId] = this.hashToken(token); - return acc; - }, {} as Record); } - public async generateTokensForAllPolicies( - force: boolean = false - ): Promise> { + public async generateTokensForAllPolicies(force: boolean = false): Promise { const policyIds = await this.getAllPolicyIds(); return this.generateTokensForPolicyIds(policyIds, force); } @@ -486,6 +489,15 @@ export class UninstallTokenService implements UninstallTokenServiceInterface { return this._soClient; } + public async checkTokenValidityForPolicy(policyId: string): Promise { + await this.getDecryptedTokensForPolicyIds([policyId]); + } + + public async checkTokenValidityForAllPolicies(): Promise { + const policyIds = await this.getAllPolicyIds(); + await this.getDecryptedTokensForPolicyIds(policyIds); + } + private get isEncryptionAvailable(): boolean { return appContextService.getEncryptedSavedObjectsSetup()?.canEncrypt ?? false; } @@ -498,7 +510,9 @@ export class UninstallTokenService implements UninstallTokenServiceInterface { private assertToken(attributes: UninstallTokenSOAttributes | undefined) { if (!attributes?.token && !attributes?.token_plain) { - throw new UninstallTokenError('Uninstall Token is missing the token.'); + throw new UninstallTokenError( + 'Invalid uninstall token: Saved object is missing the `token` attribute.' + ); } } diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index 178499011bc61..60ce6460d0ac2 100644 --- a/x-pack/plugins/fleet/server/services/setup.ts +++ b/x-pack/plugins/fleet/server/services/setup.ts @@ -6,6 +6,7 @@ */ import fs from 'fs/promises'; + import apm from 'elastic-apm-node'; import { compact } from 'lodash'; @@ -13,6 +14,8 @@ import pMap from 'p-map'; import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants'; +import type { UninstallTokenError } from '../../common/errors'; + import { AUTO_UPDATE_PACKAGES } from '../../common/constants'; import type { PreconfigurationError } from '../../common/constants'; import type { DefaultPackagesInstallationError } from '../../common/types'; @@ -54,7 +57,10 @@ import { cleanUpOldFileIndices } from './setup/clean_old_fleet_indices'; export interface SetupStatus { isInitialized: boolean; nonFatalErrors: Array< - PreconfigurationError | DefaultPackagesInstallationError | UpgradeManagedPackagePoliciesResult + | PreconfigurationError + | DefaultPackagesInstallationError + | UpgradeManagedPackagePoliciesResult + | { error: UninstallTokenError } >; } @@ -196,9 +202,17 @@ async function createSetupSideEffects( logger.debug('Checking for and encrypting plain text uninstall tokens'); await appContextService.getUninstallTokenService()?.encryptTokens(); } + + logger.debug('Checking validity of Uninstall Tokens'); + try { + await appContextService.getUninstallTokenService()?.checkTokenValidityForAllPolicies(); + } catch (error) { + nonFatalErrors.push({ error }); + } stepSpan?.end(); stepSpan = apm.startSpan('Upgrade agent policy schema', 'preconfiguration'); + logger.debug('Upgrade Agent policy schema version'); await upgradeAgentPolicySchemaVersion(soClient); stepSpan?.end(); diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/create_condition_script.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/create_condition_script.ts index 37a39d215eddd..a62f5d92dac06 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/create_condition_script.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/create_condition_script.ts @@ -25,7 +25,8 @@ export const createConditionScript = ( } if (comparator === Comparator.OUTSIDE_RANGE && threshold.length === 2) { return { - source: `params.value < params.threshold0 && params.value > params.threshold1 ? 1 : 0`, + // OUTSIDE_RANGE/NOT BETWEEN is the opposite of BETWEEN. Use the BETWEEN condition and switch the 1 and 0 + source: `params.value > params.threshold0 && params.value < params.threshold1 ? 0 : 1`, params: { threshold0: threshold[0], threshold1: threshold[1], diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/create_condition_script.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/create_condition_script.ts index b4285863dbccb..1320607685a87 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/create_condition_script.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/create_condition_script.ts @@ -18,7 +18,8 @@ export const createConditionScript = (threshold: number[], comparator: Comparato } if (comparator === Comparator.OUTSIDE_RANGE && threshold.length === 2) { return { - source: `params.value < params.threshold0 && params.value > params.threshold1 ? 1 : 0`, + // OUTSIDE_RANGE/NOT BETWEEN is the opposite of BETWEEN. Use the BETWEEN condition and switch the 1 and 0 + source: `params.value > params.threshold0 && params.value < params.threshold1 ? 0 : 1`, params: { threshold0: threshold[0], threshold1: threshold[1], diff --git a/x-pack/plugins/lens/public/shared_components/dataview_picker/dataview_picker.tsx b/x-pack/plugins/lens/public/shared_components/dataview_picker/dataview_picker.tsx index 9d55284cc36c8..c4efd626d4772 100644 --- a/x-pack/plugins/lens/public/shared_components/dataview_picker/dataview_picker.tsx +++ b/x-pack/plugins/lens/public/shared_components/dataview_picker/dataview_picker.tsx @@ -6,9 +6,11 @@ */ import { i18n } from '@kbn/i18n'; +import { calculateWidthFromEntries } from '@kbn/calculate-width-from-char-count'; import React, { useState } from 'react'; import { EuiPopover, EuiPopoverTitle, EuiSelectableProps } from '@elastic/eui'; import { DataViewsList } from '@kbn/unified-search-plugin/public'; +import { css } from '@emotion/react'; import { type IndexPatternRef } from '../../types'; import { type ChangeIndexPatternTriggerProps, TriggerButton } from './trigger'; @@ -30,43 +32,47 @@ export function ChangeIndexPattern({ const [isPopoverOpen, setPopoverIsOpen] = useState(false); return ( - <> - setPopoverIsOpen(!isPopoverOpen)} - /> - } - panelProps={{ - ['data-test-subj']: 'lnsChangeIndexPatternPopover', - }} - isOpen={isPopoverOpen} - closePopover={() => setPopoverIsOpen(false)} - display="block" - panelPaddingSize="none" - ownFocus + setPopoverIsOpen(!isPopoverOpen)} + /> + } + panelProps={{ + ['data-test-subj']: 'lnsChangeIndexPatternPopover', + }} + isOpen={isPopoverOpen} + closePopover={() => setPopoverIsOpen(false)} + display="block" + panelPaddingSize="none" + ownFocus + > +
-
- - {i18n.translate('xpack.lens.indexPattern.changeDataViewTitle', { - defaultMessage: 'Data view', - })} - + + {i18n.translate('xpack.lens.indexPattern.changeDataViewTitle', { + defaultMessage: 'Data view', + })} + - { - onChangeIndexPattern(newId); - setPopoverIsOpen(false); - }} - currentDataViewId={indexPatternId} - selectableProps={selectableProps} - /> -
- - + { + onChangeIndexPattern(newId); + setPopoverIsOpen(false); + }} + currentDataViewId={indexPatternId} + selectableProps={selectableProps} + /> +
+
); } diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/layer_header.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/layer_header.tsx index 99c7b2bec30d4..30ffc3a32b1f7 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/layer_header.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/layer_header.tsx @@ -170,7 +170,6 @@ function DataLayerHeader(props: VisualizationLayerWidgetProps) { return ( setPopoverIsOpen(!isPopoverOpen)} @@ -188,7 +187,11 @@ function DataLayerHeader(props: VisualizationLayerWidgetProps) { defaultMessage: 'Layer visualization type', })} -
+
- - - + diff --git a/x-pack/plugins/observability/public/context/has_data_context/has_data_context.tsx b/x-pack/plugins/observability/public/context/has_data_context/has_data_context.tsx index 40c54e7d0b2ec..9d3b22a27704a 100644 --- a/x-pack/plugins/observability/public/context/has_data_context/has_data_context.tsx +++ b/x-pack/plugins/observability/public/context/has_data_context/has_data_context.tsx @@ -7,7 +7,6 @@ import { isEmpty, uniqueId } from 'lodash'; import React, { createContext, useEffect, useState } from 'react'; -import { useRouteMatch } from 'react-router-dom'; import { asyncForEach } from '@kbn/std'; import { FETCH_STATUS } from '@kbn/observability-shared-plugin/public'; import { useKibana } from '../../utils/kibana_react'; @@ -65,84 +64,81 @@ export function HasDataContextProvider({ children }: { children: React.ReactNode const [hasDataMap, setHasDataMap] = useState({}); - const isExploratoryView = useRouteMatch('/exploratory-view'); - useEffect( () => { - if (!isExploratoryView) - asyncForEach(apps, async (app) => { - try { - const updateState = ({ - hasData, - indices, - serviceName, - }: { - hasData?: boolean; - serviceName?: string; - indices?: string | ApmIndicesConfig; - }) => { - setHasDataMap((prevState) => ({ - ...prevState, - [app]: { - hasData, - ...(serviceName ? { serviceName } : {}), - ...(indices ? { indices } : {}), - status: FETCH_STATUS.SUCCESS, - }, - })); - }; - switch (app) { - case UX_APP: - const params = { absoluteTime: { start: absoluteStart!, end: absoluteEnd! } }; - const resultUx = await getDataHandler(app)?.hasData(params); - updateState({ - hasData: resultUx?.hasData, - indices: resultUx?.indices, - serviceName: resultUx?.serviceName as string, - }); - break; - case UPTIME_APP: - const resultSy = await getDataHandler(app)?.hasData(); - updateState({ hasData: resultSy?.hasData, indices: resultSy?.indices }); - - break; - case APM_APP: - const resultApm = await getDataHandler(app)?.hasData(); - updateState({ hasData: resultApm?.hasData, indices: resultApm?.indices }); - - break; - case INFRA_LOGS_APP: - const resultInfraLogs = await getDataHandler(app)?.hasData(); - updateState({ - hasData: resultInfraLogs?.hasData, - indices: resultInfraLogs?.indices, - }); - break; - case INFRA_METRICS_APP: - const resultInfraMetrics = await getDataHandler(app)?.hasData(); - updateState({ - hasData: resultInfraMetrics?.hasData, - indices: resultInfraMetrics?.indices, - }); - break; - case UNIVERSAL_PROFILING_APP: - // Profiling only shows the empty section for now - updateState({ hasData: false }); - break; - } - } catch (e) { + asyncForEach(apps, async (app) => { + try { + const updateState = ({ + hasData, + indices, + serviceName, + }: { + hasData?: boolean; + serviceName?: string; + indices?: string | ApmIndicesConfig; + }) => { setHasDataMap((prevState) => ({ ...prevState, [app]: { - hasData: undefined, - status: FETCH_STATUS.FAILURE, + hasData, + ...(serviceName ? { serviceName } : {}), + ...(indices ? { indices } : {}), + status: FETCH_STATUS.SUCCESS, }, })); + }; + switch (app) { + case UX_APP: + const params = { absoluteTime: { start: absoluteStart!, end: absoluteEnd! } }; + const resultUx = await getDataHandler(app)?.hasData(params); + updateState({ + hasData: resultUx?.hasData, + indices: resultUx?.indices, + serviceName: resultUx?.serviceName as string, + }); + break; + case UPTIME_APP: + const resultSy = await getDataHandler(app)?.hasData(); + updateState({ hasData: resultSy?.hasData, indices: resultSy?.indices }); + + break; + case APM_APP: + const resultApm = await getDataHandler(app)?.hasData(); + updateState({ hasData: resultApm?.hasData, indices: resultApm?.indices }); + + break; + case INFRA_LOGS_APP: + const resultInfraLogs = await getDataHandler(app)?.hasData(); + updateState({ + hasData: resultInfraLogs?.hasData, + indices: resultInfraLogs?.indices, + }); + break; + case INFRA_METRICS_APP: + const resultInfraMetrics = await getDataHandler(app)?.hasData(); + updateState({ + hasData: resultInfraMetrics?.hasData, + indices: resultInfraMetrics?.indices, + }); + break; + case UNIVERSAL_PROFILING_APP: + // Profiling only shows the empty section for now + updateState({ hasData: false }); + break; } - }); + } catch (e) { + setHasDataMap((prevState) => ({ + ...prevState, + [app]: { + hasData: undefined, + status: FETCH_STATUS.FAILURE, + }, + })); + } + }); }, // eslint-disable-next-line react-hooks/exhaustive-deps - [isExploratoryView] + [] ); useEffect(() => { diff --git a/x-pack/plugins/observability/public/hooks/use_get_user_cases_permissions.tsx b/x-pack/plugins/observability/public/hooks/use_get_user_cases_permissions.tsx deleted file mode 100644 index ea80fc8f8cc1c..0000000000000 --- a/x-pack/plugins/observability/public/hooks/use_get_user_cases_permissions.tsx +++ /dev/null @@ -1,50 +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 { useEffect, useState } from 'react'; -import { CasesPermissions } from '@kbn/cases-plugin/common'; -import { useKibana } from '../utils/kibana_react'; -import { casesFeatureId } from '../../common'; - -export function useGetUserCasesPermissions() { - const [casesPermissions, setCasesPermissions] = useState({ - all: false, - read: false, - create: false, - update: false, - delete: false, - push: false, - connectors: false, - }); - const uiCapabilities = useKibana().services.application.capabilities; - - const casesCapabilities = useKibana().services.cases.helpers.getUICapabilities( - uiCapabilities[casesFeatureId] - ); - - useEffect(() => { - setCasesPermissions({ - all: casesCapabilities.all, - create: casesCapabilities.create, - read: casesCapabilities.read, - update: casesCapabilities.update, - delete: casesCapabilities.delete, - push: casesCapabilities.push, - connectors: casesCapabilities.connectors, - }); - }, [ - casesCapabilities.all, - casesCapabilities.create, - casesCapabilities.read, - casesCapabilities.update, - casesCapabilities.delete, - casesCapabilities.push, - casesCapabilities.connectors, - ]); - - return casesPermissions; -} diff --git a/x-pack/plugins/observability/public/pages/alert_details/alert_details.test.tsx b/x-pack/plugins/observability/public/pages/alert_details/alert_details.test.tsx index dfb1f571b6d43..756f90ecd97cf 100644 --- a/x-pack/plugins/observability/public/pages/alert_details/alert_details.test.tsx +++ b/x-pack/plugins/observability/public/pages/alert_details/alert_details.test.tsx @@ -76,16 +76,6 @@ jest.mock('../../hooks/use_fetch_rule', () => { }; }); jest.mock('@kbn/observability-shared-plugin/public'); -jest.mock('../../hooks/use_get_user_cases_permissions', () => ({ - useGetUserCasesPermissions: () => ({ - all: true, - create: true, - delete: true, - push: true, - read: true, - update: true, - }), -})); const useFetchAlertDetailMock = useFetchAlertDetail as jest.Mock; const useParamsMock = useParams as jest.Mock; diff --git a/x-pack/plugins/observability/public/pages/alert_details/alert_details.tsx b/x-pack/plugins/observability/public/pages/alert_details/alert_details.tsx index a7661ba43fddb..06da477896c5d 100644 --- a/x-pack/plugins/observability/public/pages/alert_details/alert_details.tsx +++ b/x-pack/plugins/observability/public/pages/alert_details/alert_details.tsx @@ -58,7 +58,7 @@ export function AlertDetails() { const [isLoading, alert] = useFetchAlertDetail(alertId); const [ruleTypeModel, setRuleTypeModel] = useState(null); const CasesContext = getCasesContext(); - const userCasesPermissions = canUseCases(); + const userCasesPermissions = canUseCases([observabilityFeatureId]); const { rule } = useFetchRule({ ruleId: alert?.fields[ALERT_RULE_UUID], }); diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts.tsx index 9e80d01ed82e4..c1b1493daa3d9 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts.tsx @@ -19,12 +19,10 @@ import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; import { rulesLocatorID } from '../../../common'; import { RulesParams } from '../../locators/rules'; import { useKibana } from '../../utils/kibana_react'; -import { useHasData } from '../../hooks/use_has_data'; import { usePluginContext } from '../../hooks/use_plugin_context'; import { useTimeBuckets } from '../../hooks/use_time_buckets'; import { useGetFilteredRuleTypes } from '../../hooks/use_get_filtered_rule_types'; import { useToasts } from '../../hooks/use_toast'; -import { LoadingObservability } from '../../components/loading_observability'; import { renderRuleStats, RuleStatsState } from './components/rule_stats'; import { ObservabilityAlertSearchBar } from '../../components/alert_search_bar/alert_search_bar'; import { @@ -94,7 +92,6 @@ function InternalAlertsPage() { error: 0, snoozed: 0, }); - const { hasAnyData, isAllRequestsComplete } = useHasData(); const [esQuery, setEsQuery] = useState<{ bool: BoolQuery }>(); const timeBuckets = useTimeBuckets(); const bucketSize = useMemo( @@ -173,10 +170,6 @@ function InternalAlertsPage() { const manageRulesHref = http.basePath.prepend('/app/observability/alerts/rules'); - if (!hasAnyData && !isAllRequestsComplete) { - return ; - } - return ( ({ __esModule: true, useKibana: jest.fn(() => mockUseKibanaReturnValue), })); -jest.mock('../../../hooks/use_get_user_cases_permissions', () => ({ - useGetUserCasesPermissions: jest.fn(() => ({ create: true, read: true })), -})); - jest.mock('@kbn/triggers-actions-ui-plugin/public/common/lib/kibana/kibana_react', () => ({ useKibana: jest.fn(() => ({ services: { notifications: { toasts: { addDanger: jest.fn(), addSuccess: jest.fn() } } }, @@ -175,4 +174,18 @@ describe('ObservabilityActions component', () => { expect(refresh).toHaveBeenCalled(); }); + + it('should hide the case actions without permissions', async () => { + mockUseKibanaReturnValue.services.cases.helpers.canUseCases.mockReturnValue( + noCasesPermissions() + ); + + const wrapper = await setup('nothing'); + wrapper.find('[data-test-subj="alertsTableRowActionMore"]').hostNodes().simulate('click'); + + expect(wrapper.find('[data-test-subj="add-to-new-case-action"]').hostNodes().length).toBe(0); + expect(wrapper.find('[data-test-subj="add-to-existing-case-action"]').hostNodes().length).toBe( + 0 + ); + }); }); diff --git a/x-pack/plugins/observability/public/pages/alerts/components/alert_actions.tsx b/x-pack/plugins/observability/public/pages/alerts/components/alert_actions.tsx index 7799655908907..9a3bce0a9a326 100644 --- a/x-pack/plugins/observability/public/pages/alerts/components/alert_actions.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/components/alert_actions.tsx @@ -30,12 +30,11 @@ import { } from '@kbn/rule-data-utils'; import { useBulkUntrackAlerts } from '@kbn/triggers-actions-ui-plugin/public'; import { useKibana } from '../../../utils/kibana_react'; -import { useGetUserCasesPermissions } from '../../../hooks/use_get_user_cases_permissions'; import { isAlertDetailsEnabledPerApp } from '../../../utils/is_alert_details_enabled'; import { parseAlert } from '../helpers/parse_alert'; import { paths } from '../../../../common/locators/paths'; import { RULE_DETAILS_PAGE_ID } from '../../rule_details/constants'; -import type { ObservabilityRuleTypeRegistry } from '../../..'; +import { observabilityFeatureId, ObservabilityRuleTypeRegistry } from '../../..'; import type { ConfigSchema } from '../../../plugin'; import type { TopAlert } from '../../../typings/alerts'; @@ -62,15 +61,15 @@ export function AlertActions({ }: Props) { const { cases: { - helpers: { getRuleIdFromEvent }, + helpers: { getRuleIdFromEvent, canUseCases }, hooks: { useCasesAddToNewCaseFlyout, useCasesAddToExistingCaseModal }, }, http: { basePath: { prepend }, }, } = useKibana().services; - const userCasesPermissions = useGetUserCasesPermissions(); const { mutateAsync: untrackAlerts } = useBulkUntrackAlerts(); + const userCasesPermissions = canUseCases([observabilityFeatureId]); const parseObservabilityAlert = useMemo( () => parseAlert(observabilityRuleTypeRegistry), diff --git a/x-pack/plugins/observability/public/pages/cases/cases.tsx b/x-pack/plugins/observability/public/pages/cases/cases.tsx index 5f746bcc4d490..13fc73e909601 100644 --- a/x-pack/plugins/observability/public/pages/cases/cases.tsx +++ b/x-pack/plugins/observability/public/pages/cases/cases.tsx @@ -7,23 +7,17 @@ import React from 'react'; -import { useGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions'; +import { observabilityFeatureId } from '../../../common'; import { usePluginContext } from '../../hooks/use_plugin_context'; -import { useHasData } from '../../hooks/use_has_data'; import { Cases } from './components/cases'; -import { LoadingObservability } from '../../components/loading_observability'; import { CaseFeatureNoPermissions } from './components/feature_no_permissions'; import { HeaderMenu } from '../overview/components/header_menu/header_menu'; +import { useKibana } from '../../utils/kibana_react'; export function CasesPage() { - const userCasesPermissions = useGetUserCasesPermissions(); const { ObservabilityPageTemplate } = usePluginContext(); - - const { hasAnyData, isAllRequestsComplete } = useHasData(); - - if (!hasAnyData && !isAllRequestsComplete) { - return ; - } + const { canUseCases } = useKibana().services.cases.helpers; + const userCasesPermissions = canUseCases([observabilityFeatureId]); return userCasesPermissions.read ? ( diff --git a/x-pack/plugins/observability/public/pages/cases/components/cases.stories.tsx b/x-pack/plugins/observability/public/pages/cases/components/cases.stories.tsx index d0fc1d01734f2..695165d05e1bd 100644 --- a/x-pack/plugins/observability/public/pages/cases/components/cases.stories.tsx +++ b/x-pack/plugins/observability/public/pages/cases/components/cases.stories.tsx @@ -27,6 +27,7 @@ const defaultProps: CasesProps = { push: true, update: true, connectors: true, + settings: true, }, }; @@ -43,5 +44,6 @@ CasesPageWithNoPermissions.args = { push: false, update: false, connectors: false, + settings: false, }, }; diff --git a/x-pack/plugins/observability/public/pages/slos/slos.tsx b/x-pack/plugins/observability/public/pages/slos/slos.tsx index 579b1bb8d19ae..e010df224ce1c 100644 --- a/x-pack/plugins/observability/public/pages/slos/slos.tsx +++ b/x-pack/plugins/observability/public/pages/slos/slos.tsx @@ -32,7 +32,7 @@ export function SlosPage() { const { hasWriteCapabilities } = useCapabilities(); const { hasAtLeast } = useLicense(); - const { isInitialLoading, isLoading, isError, data: sloList } = useFetchSloList(); + const { isLoading, isError, data: sloList } = useFetchSloList(); const { total } = sloList ?? { total: 0 }; const { storeAutoRefreshState, getAutoRefreshState } = useAutoRefreshStorage(); @@ -63,10 +63,6 @@ export function SlosPage() { storeAutoRefreshState(!isAutoRefreshing); }; - if (isInitialLoading) { - return null; - } - return ( component was not working here // so I've recreated this simple version for this purpose. @@ -65,7 +66,11 @@ export const routes = { }, [LANDING_PATH]: { handler: () => { - return ; + return ( + + + + ); }, params: {}, exact: true, @@ -73,9 +78,11 @@ export const routes = { [OVERVIEW_PATH]: { handler: () => { return ( - - - + + + + + ); }, params: {}, diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/create_condition_script.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/create_condition_script.ts index ad4aaa980aa63..2e5eda9fa32b4 100644 --- a/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/create_condition_script.ts +++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/create_condition_script.ts @@ -19,7 +19,8 @@ export const createConditionScript = (threshold: number[], comparator: Comparato } if (comparator === Comparator.OUTSIDE_RANGE && threshold.length === 2) { return { - source: `params.value < params.threshold0 && params.value > params.threshold1 ? 1 : 0`, + // OUTSIDE_RANGE/NOT BETWEEN is the opposite of BETWEEN. Use the BETWEEN condition and switch the 1 and 0 + source: `params.value > params.threshold0 && params.value < params.threshold1 ? 0 : 1`, params: { threshold0: threshold[0], threshold1: threshold[1], diff --git a/x-pack/plugins/observability/server/plugin.ts b/x-pack/plugins/observability/server/plugin.ts index 17476d335f48f..d1ca0e45b74a2 100644 --- a/x-pack/plugins/observability/server/plugin.ts +++ b/x-pack/plugins/observability/server/plugin.ts @@ -175,6 +175,36 @@ export class ObservabilityPlugin implements Plugin { }, ], }, + { + name: i18n.translate('xpack.observability.featureRegistry.casesSettingsSubFeatureName', { + defaultMessage: 'Case Settings', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'cases_settings', + name: i18n.translate( + 'xpack.observability.featureRegistry.casesSettingsSubFeatureDetails', + { + defaultMessage: 'Edit Case Settings', + } + ), + includeIn: 'all', + savedObject: { + all: [...filesSavedObjectTypes], + read: [...filesSavedObjectTypes], + }, + cases: { + settings: [observabilityFeatureId], + }, + ui: casesCapabilities.settings, + }, + ], + }, + ], + }, ], }); diff --git a/x-pack/plugins/observability_shared/public/hooks/use_get_user_cases_permissions.tsx b/x-pack/plugins/observability_shared/public/hooks/use_get_user_cases_permissions.tsx deleted file mode 100644 index 21c6a08815b76..0000000000000 --- a/x-pack/plugins/observability_shared/public/hooks/use_get_user_cases_permissions.tsx +++ /dev/null @@ -1,52 +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 { useEffect, useState } from 'react'; -import { CasesPermissions } from '@kbn/cases-plugin/common'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { casesFeatureId } from '../../common'; -import { ObservabilitySharedStart } from '../plugin'; - -export function useGetUserCasesPermissions() { - const [casesPermissions, setCasesPermissions] = useState({ - all: false, - read: false, - create: false, - update: false, - delete: false, - push: false, - connectors: false, - }); - const uiCapabilities = useKibana().services.application!.capabilities; - - const casesCapabilities = - useKibana().services.cases.helpers.getUICapabilities( - uiCapabilities[casesFeatureId] - ); - - useEffect(() => { - setCasesPermissions({ - all: casesCapabilities.all, - create: casesCapabilities.create, - read: casesCapabilities.read, - update: casesCapabilities.update, - delete: casesCapabilities.delete, - push: casesCapabilities.push, - connectors: casesCapabilities.connectors, - }); - }, [ - casesCapabilities.all, - casesCapabilities.create, - casesCapabilities.read, - casesCapabilities.update, - casesCapabilities.delete, - casesCapabilities.push, - casesCapabilities.connectors, - ]); - - return casesPermissions; -} diff --git a/x-pack/plugins/observability_shared/public/index.ts b/x-pack/plugins/observability_shared/public/index.ts index cbb59c3d2debe..8d8556e509e25 100644 --- a/x-pack/plugins/observability_shared/public/index.ts +++ b/x-pack/plugins/observability_shared/public/index.ts @@ -57,7 +57,6 @@ export { } from './hooks/use_track_metric'; export type { TrackEvent } from './hooks/use_track_metric'; export { useQuickTimeRanges } from './hooks/use_quick_time_ranges'; -export { useGetUserCasesPermissions } from './hooks/use_get_user_cases_permissions'; export { useTimeZone } from './hooks/use_time_zone'; export { useChartTheme } from './hooks/use_chart_theme'; export { useLinkProps, shouldHandleLinkEvent } from './hooks/use_link_props'; @@ -66,7 +65,7 @@ export { NavigationWarningPromptProvider, Prompt } from './components/navigation export type { ApmIndicesConfig, UXMetrics } from './types'; -export { noCasesPermissions } from './utils/cases_permissions'; +export { noCasesPermissions, allCasesPermissions } from './utils/cases_permissions'; export { type ObservabilityActionContextMenuItemProps, diff --git a/x-pack/plugins/observability_shared/public/utils/cases_permissions.ts b/x-pack/plugins/observability_shared/public/utils/cases_permissions.ts index a0b6a8aed95b0..0ceea46ad0d38 100644 --- a/x-pack/plugins/observability_shared/public/utils/cases_permissions.ts +++ b/x-pack/plugins/observability_shared/public/utils/cases_permissions.ts @@ -13,4 +13,16 @@ export const noCasesPermissions = () => ({ delete: false, push: false, connectors: false, + settings: false, +}); + +export const allCasesPermissions = () => ({ + all: true, + create: true, + read: true, + update: true, + delete: true, + push: true, + connectors: true, + settings: true, }); diff --git a/x-pack/plugins/reporting/public/management/report_listing_table.tsx b/x-pack/plugins/reporting/public/management/report_listing_table.tsx index 61b9e7d541928..c8dcbf77aba30 100644 --- a/x-pack/plugins/reporting/public/management/report_listing_table.tsx +++ b/x-pack/plugins/reporting/public/management/report_listing_table.tsx @@ -397,12 +397,12 @@ export class ReportListingTable extends Component { return ( {this.state.selectedJobs.length > 0 && ( - +
{this.renderDeleteButton()} - +
)} { return jobManagementPreRouting(reporting, res, docId, user, counters, async (doc) => { const docIndex = doc.index; const stream = await getContentStream(reporting, { id: docId, index: docIndex }); - /** @note Overwriting existing content with an empty buffer to remove all the chunks. */ - await promisify(stream.end.bind(stream, '', 'utf8'))(); + await new Promise((resolve) => { + stream.end('', 'utf8', () => { + resolve(); + }); + }); await jobsQuery.delete(docIndex, docId); return res.ok({ diff --git a/x-pack/plugins/reporting/server/routes/common/jobs/jobs_query.ts b/x-pack/plugins/reporting/server/routes/common/jobs/jobs_query.ts index 9efe74a0c3aac..b87c6040e46b4 100644 --- a/x-pack/plugins/reporting/server/routes/common/jobs/jobs_query.ts +++ b/x-pack/plugins/reporting/server/routes/common/jobs/jobs_query.ts @@ -206,11 +206,7 @@ export function jobsQueryFactory(reportingCore: ReportingCore): JobsQueryFactory async delete(deleteIndex, id) { try { const { asInternalUser: elasticsearchClient } = await reportingCore.getEsClient(); - - // Using `wait_for` helps avoid users seeing recently-deleted reports temporarily flashing back in the - // job listing. - const query = { id, index: deleteIndex, refresh: 'wait_for' as const }; - + const query = { id, index: deleteIndex }; return await elasticsearchClient.delete(query, { meta: true }); } catch (error) { throw new Error( diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/__snapshots__/cases.test.ts.snap b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/__snapshots__/cases.test.ts.snap index fc31927e6cfb5..1874a17515e19 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/__snapshots__/cases.test.ts.snap +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/__snapshots__/cases.test.ts.snap @@ -5,7 +5,6 @@ Array [ "cases:observability/pushCase", "cases:observability/createCase", "cases:observability/createComment", - "cases:observability/createConfiguration", "cases:observability/getCase", "cases:observability/getComment", "cases:observability/getTags", @@ -14,9 +13,10 @@ Array [ "cases:observability/findConfigurations", "cases:observability/updateCase", "cases:observability/updateComment", - "cases:observability/updateConfiguration", "cases:observability/deleteCase", "cases:observability/deleteComment", + "cases:observability/createConfiguration", + "cases:observability/updateConfiguration", ] `; @@ -24,7 +24,6 @@ exports[`cases feature_privilege_builder within feature grants create privileges Array [ "cases:securitySolution/createCase", "cases:securitySolution/createComment", - "cases:securitySolution/createConfiguration", ] `; @@ -52,10 +51,16 @@ Array [ ] `; +exports[`cases feature_privilege_builder within feature grants settings privileges under feature with id observability 1`] = ` +Array [ + "cases:observability/createConfiguration", + "cases:observability/updateConfiguration", +] +`; + exports[`cases feature_privilege_builder within feature grants update privileges under feature with id observability 1`] = ` Array [ "cases:observability/updateCase", "cases:observability/updateComment", - "cases:observability/updateConfiguration", ] `; diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts index d4d49a5334f1d..ad0563ef7a827 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts @@ -47,6 +47,7 @@ describe(`cases`, () => { ['read', 'observability'], ['update', 'observability'], ['delete', 'securitySolution'], + ['settings', 'observability'], ])('grants %s privileges under feature with id %s', (operation, featureID) => { const actions = new Actions(); const casesFeaturePrivilege = new FeaturePrivilegeCasesBuilder(actions); @@ -55,7 +56,6 @@ describe(`cases`, () => { cases: { [operation]: [featureID], }, - savedObject: { all: [], read: [], @@ -88,8 +88,8 @@ describe(`cases`, () => { update: ['obs'], delete: ['security'], read: ['obs'], + settings: ['security'], }, - savedObject: { all: [], read: [], @@ -113,7 +113,6 @@ describe(`cases`, () => { "cases:security/pushCase", "cases:security/createCase", "cases:security/createComment", - "cases:security/createConfiguration", "cases:security/getCase", "cases:security/getComment", "cases:security/getTags", @@ -122,9 +121,10 @@ describe(`cases`, () => { "cases:security/findConfigurations", "cases:security/updateCase", "cases:security/updateComment", - "cases:security/updateConfiguration", "cases:security/deleteCase", "cases:security/deleteComment", + "cases:security/createConfiguration", + "cases:security/updateConfiguration", "cases:obs/getCase", "cases:obs/getComment", "cases:obs/getTags", @@ -133,7 +133,6 @@ describe(`cases`, () => { "cases:obs/findConfigurations", "cases:obs/updateCase", "cases:obs/updateComment", - "cases:obs/updateConfiguration", ] `); }); @@ -147,7 +146,6 @@ describe(`cases`, () => { all: ['security', 'other-security'], read: ['obs', 'other-obs'], }, - savedObject: { all: [], read: [], @@ -171,7 +169,6 @@ describe(`cases`, () => { "cases:security/pushCase", "cases:security/createCase", "cases:security/createComment", - "cases:security/createConfiguration", "cases:security/getCase", "cases:security/getComment", "cases:security/getTags", @@ -180,13 +177,13 @@ describe(`cases`, () => { "cases:security/findConfigurations", "cases:security/updateCase", "cases:security/updateComment", - "cases:security/updateConfiguration", "cases:security/deleteCase", "cases:security/deleteComment", + "cases:security/createConfiguration", + "cases:security/updateConfiguration", "cases:other-security/pushCase", "cases:other-security/createCase", "cases:other-security/createComment", - "cases:other-security/createConfiguration", "cases:other-security/getCase", "cases:other-security/getComment", "cases:other-security/getTags", @@ -195,9 +192,10 @@ describe(`cases`, () => { "cases:other-security/findConfigurations", "cases:other-security/updateCase", "cases:other-security/updateComment", - "cases:other-security/updateConfiguration", "cases:other-security/deleteCase", "cases:other-security/deleteComment", + "cases:other-security/createConfiguration", + "cases:other-security/updateConfiguration", "cases:obs/getCase", "cases:obs/getComment", "cases:obs/getTags", diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts index 0f442c9d871e1..b54ba77777dd8 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts @@ -13,11 +13,16 @@ import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; export type CasesSupportedOperations = typeof allOperations[number]; -// if you add a value here you'll likely also need to make changes here: -// x-pack/plugins/cases/server/authorization/index.ts +/** + * If you add a new operation type (all, push, update, etc) you should also + * extend the mapping here x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.ts + * + * Also if you add a new operation (createCase, updateCase, etc) here you'll likely also need to make changes here: + * x-pack/plugins/cases/server/authorization/index.ts + */ const pushOperations = ['pushCase'] as const; -const createOperations = ['createCase', 'createComment', 'createConfiguration'] as const; +const createOperations = ['createCase', 'createComment'] as const; const readOperations = [ 'getCase', 'getComment', @@ -26,14 +31,16 @@ const readOperations = [ 'getUserActions', 'findConfigurations', ] as const; -const updateOperations = ['updateCase', 'updateComment', 'updateConfiguration'] as const; +const updateOperations = ['updateCase', 'updateComment'] as const; const deleteOperations = ['deleteCase', 'deleteComment'] as const; +const settingsOperations = ['createConfiguration', 'updateConfiguration'] as const; const allOperations = [ ...pushOperations, ...createOperations, ...readOperations, ...updateOperations, ...deleteOperations, + ...settingsOperations, ] as const; export class FeaturePrivilegeCasesBuilder extends BaseFeaturePrivilegeBuilder { @@ -57,6 +64,7 @@ export class FeaturePrivilegeCasesBuilder extends BaseFeaturePrivilegeBuilder { ...getCasesPrivilege(readOperations, privilegeDefinition.cases?.read), ...getCasesPrivilege(updateOperations, privilegeDefinition.cases?.update), ...getCasesPrivilege(deleteOperations, privilegeDefinition.cases?.delete), + ...getCasesPrivilege(settingsOperations, privilegeDefinition.cases?.settings), ]); } } diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 75eda07fa185e..c5870f94bf168 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -259,6 +259,7 @@ export const RISK_ENGINE_STATUS_URL = `${RISK_ENGINE_URL}/status`; export const RISK_ENGINE_INIT_URL = `${RISK_ENGINE_URL}/init`; export const RISK_ENGINE_ENABLE_URL = `${RISK_ENGINE_URL}/enable`; export const RISK_ENGINE_DISABLE_URL = `${RISK_ENGINE_URL}/disable`; +export const RISK_ENGINE_PRIVILEGES_URL = `${RISK_ENGINE_URL}/privileges`; /** * Public Risk Score routes diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 2ada33b7426f1..b6ad057bbeffe 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -114,6 +114,18 @@ export const allowedExperimentalValues = Object.freeze({ * Enables Protection Updates tab in the Endpoint Policy Details page */ protectionUpdatesEnabled: true, + + /** + * Disables the timeline save tour. + * This flag is used to disable the tour in cypress tests. + */ + disableTimelineSaveTour: false, + + /** + * Enables the risk engine privileges route + * and associated callout in the UI + */ + riskEnginePrivilegesRouteEnabled: false, }); type ExperimentalConfigKeys = Array; diff --git a/x-pack/plugins/security_solution/common/risk_engine/constants.ts b/x-pack/plugins/security_solution/common/risk_engine/constants.ts index 2d4d208559894..46a5a99a7e21a 100644 --- a/x-pack/plugins/security_solution/common/risk_engine/constants.ts +++ b/x-pack/plugins/security_solution/common/risk_engine/constants.ts @@ -6,3 +6,12 @@ */ export const MAX_SPACES_COUNT = 1; + +export const RISK_ENGINE_REQUIRED_ES_CLUSTER_PRIVILEGES = [ + 'manage_index_templates', + 'manage_transform', +]; + +export const RISK_ENGINE_REQUIRED_ES_INDEX_PRIVILEGES = Object.freeze({ + 'risk-score.risk-score-*': ['read', 'write'], +}); diff --git a/x-pack/plugins/security_solution/common/test/ess_roles.json b/x-pack/plugins/security_solution/common/test/ess_roles.json index d21fe90e2de02..9bf9e1b64aee3 100644 --- a/x-pack/plugins/security_solution/common/test/ess_roles.json +++ b/x-pack/plugins/security_solution/common/test/ess_roles.json @@ -132,5 +132,22 @@ "base": [] } ] + }, + "no_risk_engine_privileges": { + "name": "no_risk_engine_privileges", + "elasticsearch": { + "cluster": [], + "indices": [], + "run_as": [] + }, + "kibana": [ + { + "feature": { + "siem": ["read"] + }, + "spaces": ["*"], + "base": [] + } + ] } } diff --git a/x-pack/plugins/security_solution/common/test/index.ts b/x-pack/plugins/security_solution/common/test/index.ts index ac2fd661320ce..277f54c78e6c5 100644 --- a/x-pack/plugins/security_solution/common/test/index.ts +++ b/x-pack/plugins/security_solution/common/test/index.ts @@ -29,6 +29,7 @@ export enum ROLES { reader = 'reader', hunter = 'hunter', hunter_no_actions = 'hunter_no_actions', + no_risk_engine_privileges = 'no_risk_engine_privileges', } /** diff --git a/x-pack/plugins/security_solution/public/app/routes.tsx b/x-pack/plugins/security_solution/public/app/routes.tsx index 73fe2615b0e5a..307d8f4c32376 100644 --- a/x-pack/plugins/security_solution/public/app/routes.tsx +++ b/x-pack/plugins/security_solution/public/app/routes.tsx @@ -14,7 +14,7 @@ import type { AppLeaveHandler } from '@kbn/core/public'; import { APP_ID } from '../../common/constants'; import { RouteCapture } from '../common/components/endpoint/route_capture'; -import { useGetUserCasesPermissions, useKibana } from '../common/lib/kibana'; +import { useKibana } from '../common/lib/kibana'; import type { AppAction } from '../common/store/actions'; import { ManageRoutesSpy } from '../common/utils/route/manage_spy_routes'; import { NotFoundPage } from './404'; @@ -29,7 +29,7 @@ interface RouterProps { const PageRouterComponent: FC = ({ children, history, onAppLeave }) => { const { cases } = useKibana().services; const CasesContext = cases.ui.getCasesContext(); - const userCasesPermissions = useGetUserCasesPermissions(); + const userCasesPermissions = cases.helpers.canUseCases([APP_ID]); const dispatch = useDispatch<(action: AppAction) => void>(); useEffect(() => { return () => { diff --git a/x-pack/plugins/security_solution/public/cases/links.ts b/x-pack/plugins/security_solution/public/cases/links.ts index 3017fa28816e3..2d2f6d94b351a 100644 --- a/x-pack/plugins/security_solution/public/cases/links.ts +++ b/x-pack/plugins/security_solution/public/cases/links.ts @@ -8,7 +8,7 @@ import { CREATE_CASES_CAPABILITY, READ_CASES_CAPABILITY, - UPDATE_CASES_CAPABILITY, + CASES_SETTINGS_CAPABILITY, } from '@kbn/cases-plugin/common'; import { getCasesDeepLinks } from '@kbn/cases-plugin/public'; import { CASES_FEATURE_ID, CASES_PATH, SecurityPageName } from '../../common/constants'; @@ -22,7 +22,7 @@ const casesLinks = getCasesDeepLinks({ capabilities: [`${CASES_FEATURE_ID}.${READ_CASES_CAPABILITY}`], }, [SecurityPageName.caseConfigure]: { - capabilities: [`${CASES_FEATURE_ID}.${UPDATE_CASES_CAPABILITY}`], + capabilities: [`${CASES_FEATURE_ID}.${CASES_SETTINGS_CAPABILITY}`], sideNavDisabled: true, }, [SecurityPageName.caseCreate]: { diff --git a/x-pack/plugins/security_solution/public/cases/pages/index.tsx b/x-pack/plugins/security_solution/public/cases/pages/index.tsx index dd639862e2812..ab5b170e423c0 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/index.tsx @@ -21,7 +21,7 @@ import { TimelineId } from '../../../common/types/timeline'; import { getRuleDetailsUrl, useFormatUrl } from '../../common/components/link_to'; -import { useGetUserCasesPermissions, useKibana, useNavigation } from '../../common/lib/kibana'; +import { useKibana, useNavigation } from '../../common/lib/kibana'; import { APP_ID, CASES_PATH, @@ -56,7 +56,7 @@ const TimelineDetailsPanel = () => { const CaseContainerComponent: React.FC = () => { const { cases } = useKibana().services; const { getAppUrl, navigateTo } = useNavigation(); - const userCasesPermissions = useGetUserCasesPermissions(); + const userCasesPermissions = cases.helpers.canUseCases([APP_ID]); const dispatch = useDispatch(); const { formatUrl: detectionsFormatUrl, search: detectionsUrlSearch } = useFormatUrl( SecurityPageName.rules diff --git a/x-pack/plugins/security_solution/public/cases_test_utils.ts b/x-pack/plugins/security_solution/public/cases_test_utils.ts index d177934cb02ee..dc70dcab33eaa 100644 --- a/x-pack/plugins/security_solution/public/cases_test_utils.ts +++ b/x-pack/plugins/security_solution/public/cases_test_utils.ts @@ -5,34 +5,39 @@ * 2.0. */ -export const noCasesCapabilities = () => ({ +import type { CasesPermissions, CasesCapabilities } from '@kbn/cases-plugin/common'; + +export const noCasesCapabilities = (): CasesCapabilities => ({ create_cases: false, read_cases: false, update_cases: false, delete_cases: false, push_cases: false, - cases_connector: false, + cases_connectors: false, + cases_settings: false, }); -export const readCasesCapabilities = () => ({ +export const readCasesCapabilities = (): CasesCapabilities => ({ create_cases: false, read_cases: true, update_cases: false, delete_cases: false, push_cases: false, - cases_connector: true, + cases_connectors: true, + cases_settings: false, }); -export const allCasesCapabilities = () => ({ +export const allCasesCapabilities = (): CasesCapabilities => ({ create_cases: true, read_cases: true, update_cases: true, delete_cases: true, push_cases: true, - cases_connector: true, + cases_connectors: true, + cases_settings: true, }); -export const noCasesPermissions = () => ({ +export const noCasesPermissions = (): CasesPermissions => ({ all: false, create: false, read: false, @@ -40,9 +45,10 @@ export const noCasesPermissions = () => ({ delete: false, push: false, connectors: false, + settings: false, }); -export const readCasesPermissions = () => ({ +export const readCasesPermissions = (): CasesPermissions => ({ all: false, create: false, read: true, @@ -50,9 +56,10 @@ export const readCasesPermissions = () => ({ delete: false, push: false, connectors: true, + settings: false, }); -export const writeCasesPermissions = () => ({ +export const writeCasesPermissions = (): CasesPermissions => ({ all: false, create: true, read: false, @@ -60,9 +67,10 @@ export const writeCasesPermissions = () => ({ delete: true, push: true, connectors: true, + settings: true, }); -export const allCasesPermissions = () => ({ +export const allCasesPermissions = (): CasesPermissions => ({ all: true, create: true, read: true, @@ -70,4 +78,5 @@ export const allCasesPermissions = () => ({ delete: true, push: true, connectors: true, + settings: true, }); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx index edc72e92ff153..2718c3adf2012 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx @@ -26,7 +26,7 @@ import { mockAlertDetailsData } from './__mocks__'; import type { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; import { TimelineTabs } from '../../../../common/types/timeline'; import { useInvestigationTimeEnrichment } from '../../containers/cti/event_enrichment'; -import { useGetUserCasesPermissions, useKibana } from '../../lib/kibana'; +import { useKibana } from '../../lib/kibana'; import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; @@ -44,14 +44,8 @@ jest.mock('../../../timelines/components/timeline/body/renderers', () => { }); jest.mock('../../lib/kibana'); -const originalKibanaLib = jest.requireActual('../../lib/kibana'); const useKibanaMock = useKibana as jest.Mocked; -// Restore the useGetUserCasesPermissions so the calling functions can receive a valid permissions object -// The returned permissions object will indicate that the user does not have permissions by default -const mockUseGetUserCasesPermissions = useGetUserCasesPermissions as jest.Mock; -mockUseGetUserCasesPermissions.mockImplementation(originalKibanaLib.useGetUserCasesPermissions); - jest.mock('../../containers/cti/event_enrichment'); jest.mock('../../../detection_engine/rule_management/logic/use_rule_with_fallback', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/insights/insights.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/insights/insights.test.tsx index 2ef1277884c10..b9b132763d6f7 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/insights/insights.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/insights/insights.test.tsx @@ -12,7 +12,6 @@ import { TestProviders } from '../../../mock'; import type { TimelineEventsDetailsItem } from '../../../../../common/search_strategy/timeline'; import { useKibana as mockUseKibana } from '../../../lib/kibana/__mocks__'; -import { useGetUserCasesPermissions } from '../../../lib/kibana'; import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features'; import { licenseService } from '../../../hooks/use_license'; import { noCasesPermissions, readCasesPermissions } from '../../../../cases_test_utils'; @@ -20,12 +19,13 @@ import { Insights } from './insights'; import * as i18n from './translations'; const mockedUseKibana = mockUseKibana(); +const mockCanUseCases = jest.fn(); + jest.mock('../../../lib/kibana', () => { const original = jest.requireActual('../../../lib/kibana'); return { ...original, - useGetUserCasesPermissions: jest.fn(), useToasts: jest.fn().mockReturnValue({ addWarning: jest.fn() }), useKibana: () => ({ ...mockedUseKibana, @@ -35,12 +35,12 @@ jest.mock('../../../lib/kibana', () => { api: { getRelatedCases: jest.fn(), }, + helpers: { canUseCases: mockCanUseCases }, }, }, }), }; }); -const mockUseGetUserCasesPermissions = useGetUserCasesPermissions as jest.Mock; jest.mock('../../../hooks/use_license', () => { const licenseServiceInstance = { @@ -94,7 +94,7 @@ const data: TimelineEventsDetailsItem[] = [ describe('Insights', () => { beforeEach(() => { - mockUseGetUserCasesPermissions.mockReturnValue(noCasesPermissions()); + mockCanUseCases.mockReturnValue(noCasesPermissions()); }); it('does not render when there is no content to show', () => { @@ -116,7 +116,7 @@ describe('Insights', () => { // It will show for all users that are able to read case data. // Enabling that permission, will show the case insight module which // is necessary to pass this test. - mockUseGetUserCasesPermissions.mockReturnValue(readCasesPermissions()); + mockCanUseCases.mockReturnValue(readCasesPermissions()); render( diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/insights/insights.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/insights/insights.tsx index e4e4f317467e2..60c89438aa12d 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/insights/insights.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/insights/insights.tsx @@ -11,12 +11,12 @@ import { euiStyled } from '@kbn/kibana-react-plugin/common'; import { ALERT_SUPPRESSION_DOCS_COUNT } from '@kbn/rule-data-utils'; import { find } from 'lodash/fp'; +import { APP_ID } from '../../../../../common'; import * as i18n from './translations'; import type { BrowserFields } from '../../../containers/source'; import type { TimelineEventsDetailsItem } from '../../../../../common/search_strategy/timeline'; import { hasData } from './helpers'; -import { useGetUserCasesPermissions } from '../../../lib/kibana'; import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features'; import { useLicense } from '../../../hooks/use_license'; import { RelatedAlertsByProcessAncestry } from './related_alerts_by_process_ancestry'; @@ -24,6 +24,7 @@ import { RelatedCases } from './related_cases'; import { RelatedAlertsBySourceEvent } from './related_alerts_by_source_event'; import { RelatedAlertsBySession } from './related_alerts_by_session'; import { RelatedAlertsUpsell } from './related_alerts_upsell'; +import { useKibana } from '../../../lib/kibana'; const StyledInsightItem = euiStyled(EuiFlexItem)` border: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; @@ -45,6 +46,7 @@ interface Props { */ export const Insights = React.memo( ({ browserFields, eventId, data, isReadOnly, scopeId }) => { + const { cases } = useKibana().services; const isRelatedAlertsByProcessAncestryEnabled = useIsExperimentalFeatureEnabled( 'insightsRelatedAlertsByProcessAncestry' ); @@ -83,7 +85,7 @@ export const Insights = React.memo( ); const hasAlertSuppressionField = hasData(alertSuppressionField); - const userCasesPermissions = useGetUserCasesPermissions(); + const userCasesPermissions = cases.helpers.canUseCases([APP_ID]); const hasCasesReadPermissions = userCasesPermissions.read; // Make sure that the alert has at least one of the associated fields diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_cases.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_cases.test.tsx index 8e6bc304e1a38..52a6d5eb1eb42 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_cases.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_cases.test.tsx @@ -10,7 +10,6 @@ import React from 'react'; import { TestProviders } from '../../../mock'; import { useKibana as mockUseKibana } from '../../../lib/kibana/__mocks__'; -import { useGetUserCasesPermissions } from '../../../lib/kibana'; import { RelatedCases } from './related_cases'; import { noCasesPermissions, readCasesPermissions } from '../../../../cases_test_utils'; import { CASES_LOADING, CASES_COUNT } from './translations'; @@ -19,13 +18,14 @@ import { AlertsCasesTourSteps } from '../../guided_onboarding_tour/tour_config'; const mockedUseKibana = mockUseKibana(); const mockGetRelatedCases = jest.fn(); +const mockCanUseCases = jest.fn(); + jest.mock('../../guided_onboarding_tour'); jest.mock('../../../lib/kibana', () => { const original = jest.requireActual('../../../lib/kibana'); return { ...original, - useGetUserCasesPermissions: jest.fn(), useToasts: jest.fn().mockReturnValue({ addWarning: jest.fn() }), useKibana: () => ({ ...mockedUseKibana, @@ -35,6 +35,7 @@ jest.mock('../../../lib/kibana', () => { api: { getRelatedCases: mockGetRelatedCases, }, + helpers: { canUseCases: mockCanUseCases }, }, }, }), @@ -47,7 +48,7 @@ window.HTMLElement.prototype.scrollIntoView = scrollToMock; describe('Related Cases', () => { beforeEach(() => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue(readCasesPermissions()); + mockCanUseCases.mockReturnValue(readCasesPermissions()); (useTourContext as jest.Mock).mockReturnValue({ activeStep: AlertsCasesTourSteps.viewCase, incrementStep: () => null, @@ -58,7 +59,7 @@ describe('Related Cases', () => { }); describe('When user does not have cases read permissions', () => { beforeEach(() => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue(noCasesPermissions()); + mockCanUseCases.mockReturnValue(noCasesPermissions()); }); test('should not show related cases when user does not have permissions', async () => { await act(async () => { diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx index cef3117cf28c5..903f1f7c548c9 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx @@ -22,7 +22,6 @@ import { useTimelineEvents } from './use_timelines_events'; import { getDefaultControlColumn } from '../../../timelines/components/timeline/body/control_columns'; import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; import type { UseFieldBrowserOptionsProps } from '../../../timelines/components/fields_browser'; -import { useGetUserCasesPermissions } from '../../lib/kibana'; import { TableId } from '@kbn/securitysolution-data-table'; import { mount } from 'enzyme'; @@ -38,13 +37,6 @@ jest.mock('react-redux', () => { }; }); -const originalKibanaLib = jest.requireActual('../../lib/kibana'); - -// Restore the useGetUserCasesPermissions so the calling functions can receive a valid permissions object -// The returned permissions object will indicate that the user does not have permissions by default -const mockUseGetUserCasesPermissions = useGetUserCasesPermissions as jest.Mock; -mockUseGetUserCasesPermissions.mockImplementation(originalKibanaLib.useGetUserCasesPermissions); - jest.mock('./use_timelines_events'); jest.mock('../../utils/normalize_time_range'); diff --git a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.test.tsx index 7765c4dc0f79d..afe1d0f208765 100644 --- a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.test.tsx @@ -11,21 +11,12 @@ import { TestProviders } from '../../mock'; import { TEST_ID, SessionsView, defaultSessionsFilter } from '.'; import type { EntityType } from '@kbn/timelines-plugin/common'; import type { SessionsComponentsProps } from './types'; -import { useGetUserCasesPermissions } from '../../lib/kibana'; import { TableId } from '@kbn/securitysolution-data-table'; import { licenseService } from '../../hooks/use_license'; import { mount } from 'enzyme'; import type { EventsViewerProps } from '../events_viewer'; jest.mock('../../lib/kibana'); - -const originalKibanaLib = jest.requireActual('../../lib/kibana'); - -// Restore the useGetUserCasesPermissions so the calling functions can receive a valid permissions object -// The returned permissions object will indicate that the user does not have permissions by default -const mockUseGetUserCasesPermissions = useGetUserCasesPermissions as jest.Mock; -mockUseGetUserCasesPermissions.mockImplementation(originalKibanaLib.useGetUserCasesPermissions); - jest.mock('../../utils/normalize_time_range'); const startDate = '2022-03-22T22:10:56.794Z'; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/actions.test.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/actions.test.tsx index aa168343cdb90..924b1158593a7 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/actions.test.tsx @@ -101,6 +101,7 @@ describe('VisualizationActions', () => { .fn() .mockReturnValue({ open: mockGetCreateCaseFlyoutOpen }), }, + helpers: { canUseCases: jest.fn().mockReturnValue(allCasesPermissions()) }, }, application: { capabilities: { [CASES_FEATURE_ID]: allCasesCapabilities() }, diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_existing_case.test.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_existing_case.test.tsx index 6118bc4441420..cc03f80daf95b 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_existing_case.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_existing_case.test.tsx @@ -8,7 +8,6 @@ import { renderHook } from '@testing-library/react-hooks'; import { useKibana as mockUseKibana } from '../../lib/kibana/__mocks__'; import { kpiHostMetricLensAttributes } from './lens_attributes/hosts/kpi_host_metric'; import { useAddToExistingCase } from './use_add_to_existing_case'; -import { useGetUserCasesPermissions } from '../../lib/kibana'; import { allCasesPermissions, readCasesPermissions, @@ -18,13 +17,13 @@ import { AttachmentType } from '@kbn/cases-plugin/common'; const mockedUseKibana = mockUseKibana(); const mockGetUseCasesAddToExistingCaseModal = jest.fn(); +const mockCanUseCases = jest.fn(); jest.mock('../../lib/kibana', () => { const original = jest.requireActual('../../lib/kibana'); return { ...original, - useGetUserCasesPermissions: jest.fn(), useKibana: () => ({ ...mockedUseKibana, services: { @@ -33,6 +32,7 @@ jest.mock('../../lib/kibana', () => { hooks: { useCasesAddToExistingCaseModal: mockGetUseCasesAddToExistingCaseModal, }, + helpers: { canUseCases: mockCanUseCases }, }, }, }), @@ -47,7 +47,7 @@ describe('useAddToExistingCase', () => { }; beforeEach(() => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue(allCasesPermissions()); + mockCanUseCases.mockReturnValue(allCasesPermissions()); }); it('useCasesAddToExistingCaseModal with attachments', () => { @@ -68,7 +68,7 @@ describe('useAddToExistingCase', () => { }); it("disables the button if the user can't create but can read", () => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue(readCasesPermissions()); + mockCanUseCases.mockReturnValue(readCasesPermissions()); const { result } = renderHook(() => useAddToExistingCase({ @@ -81,7 +81,7 @@ describe('useAddToExistingCase', () => { }); it("disables the button if the user can't read but can create", () => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue(writeCasesPermissions()); + mockCanUseCases.mockReturnValue(writeCasesPermissions()); const { result } = renderHook(() => useAddToExistingCase({ diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_existing_case.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_existing_case.tsx index 9a9c239c65e91..8f28e9534e6a7 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_existing_case.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_existing_case.tsx @@ -8,7 +8,8 @@ import { useCallback, useMemo } from 'react'; import { AttachmentType, LENS_ATTACHMENT_TYPE } from '@kbn/cases-plugin/common'; import type { CaseAttachmentsWithoutOwner } from '@kbn/cases-plugin/public'; -import { useKibana, useGetUserCasesPermissions } from '../../lib/kibana'; +import { APP_ID } from '../../../../common'; +import { useKibana } from '../../lib/kibana'; import { ADD_TO_CASE_SUCCESS } from './translations'; import type { LensAttributes } from './types'; @@ -21,8 +22,8 @@ export const useAddToExistingCase = ({ lensAttributes: LensAttributes | null; timeRange: { from: string; to: string } | null; }) => { - const userCasesPermissions = useGetUserCasesPermissions(); const { cases } = useKibana().services; + const userCasesPermissions = cases.helpers.canUseCases([APP_ID]); const attachments = useMemo(() => { return [ { diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_new_case.test.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_new_case.test.tsx index 29969d489a038..91347dc9fe073 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_new_case.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_new_case.test.tsx @@ -8,7 +8,6 @@ import { renderHook } from '@testing-library/react-hooks'; import { useKibana as mockUseKibana } from '../../lib/kibana/__mocks__'; import { kpiHostMetricLensAttributes } from './lens_attributes/hosts/kpi_host_metric'; import { useAddToNewCase } from './use_add_to_new_case'; -import { useGetUserCasesPermissions } from '../../lib/kibana'; import { allCasesPermissions, readCasesPermissions, @@ -20,13 +19,13 @@ jest.mock('../../lib/kibana/kibana_react'); const mockedUseKibana = mockUseKibana(); const mockGetUseCasesAddToNewCaseFlyout = jest.fn(); +const mockCanUseCases = jest.fn(); jest.mock('../../lib/kibana', () => { const original = jest.requireActual('../../lib/kibana'); return { ...original, - useGetUserCasesPermissions: jest.fn(), useKibana: () => ({ ...mockedUseKibana, services: { @@ -35,6 +34,7 @@ jest.mock('../../lib/kibana', () => { hooks: { useCasesAddToNewCaseFlyout: mockGetUseCasesAddToNewCaseFlyout, }, + helpers: { canUseCases: mockCanUseCases }, }, }, }), @@ -47,7 +47,7 @@ describe('useAddToNewCase', () => { to: '2022-03-07T15:59:59.999Z', }; beforeEach(() => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue(allCasesPermissions()); + mockCanUseCases.mockReturnValue(allCasesPermissions()); }); it('useCasesAddToNewCaseFlyout with attachments', () => { @@ -64,7 +64,7 @@ describe('useAddToNewCase', () => { }); it("disables the button if the user can't create but can read", () => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue(readCasesPermissions()); + mockCanUseCases.mockReturnValue(readCasesPermissions()); const { result } = renderHook(() => useAddToNewCase({ @@ -76,7 +76,7 @@ describe('useAddToNewCase', () => { }); it("disables the button if the user can't read but can create", () => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue(writeCasesPermissions()); + mockCanUseCases.mockReturnValue(writeCasesPermissions()); const { result } = renderHook(() => useAddToNewCase({ diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_new_case.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_new_case.tsx index 6a395af34b445..68f730b376dcf 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_new_case.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_new_case.tsx @@ -8,7 +8,8 @@ import { useCallback, useMemo } from 'react'; import { AttachmentType, LENS_ATTACHMENT_TYPE } from '@kbn/cases-plugin/common'; import type { CaseAttachmentsWithoutOwner } from '@kbn/cases-plugin/public'; -import { useKibana, useGetUserCasesPermissions } from '../../lib/kibana'; +import { APP_ID } from '../../../../common'; +import { useKibana } from '../../lib/kibana'; import { ADD_TO_CASE_SUCCESS } from './translations'; import type { LensAttributes } from './types'; @@ -20,8 +21,9 @@ export interface UseAddToNewCaseProps { } export const useAddToNewCase = ({ onClick, timeRange, lensAttributes }: UseAddToNewCaseProps) => { - const userCasesPermissions = useGetUserCasesPermissions(); const { cases } = useKibana().services; + const userCasesPermissions = cases.helpers.canUseCases([APP_ID]); + const attachments = useMemo(() => { return [ { diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts index 954f4fd0b74bc..a0f59fb18f3f8 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts @@ -95,7 +95,6 @@ export const useToasts = jest export const useCurrentUser = jest.fn(); export const withKibana = jest.fn(createWithKibanaMock()); export const KibanaContextProvider = jest.fn(createKibanaContextProviderMock()); -export const useGetUserCasesPermissions = jest.fn(); export const useAppUrl = jest.fn().mockReturnValue({ getAppUrl: jest .fn() diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts index c1e48a8a9ba98..714049872ee5d 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts @@ -14,7 +14,6 @@ import { camelCase, isArray, isObject } from 'lodash'; import { set } from '@kbn/safer-lodash-set'; import type { AuthenticatedUser } from '@kbn/security-plugin/common'; import type { Capabilities } from '@kbn/core/public'; -import type { CasesPermissions } from '@kbn/cases-plugin/common'; import { useGetAppUrl, useNavigateTo, @@ -22,11 +21,7 @@ import { type GetAppUrl, type NavigateTo, } from '@kbn/security-solution-navigation'; -import { - CASES_FEATURE_ID, - DEFAULT_DATE_FORMAT, - DEFAULT_DATE_FORMAT_TZ, -} from '../../../../common/constants'; +import { DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ } from '../../../../common/constants'; import { errorToToaster, useStateToaster } from '../../components/toasters'; import type { StartServices } from '../../../types'; import { useUiSetting, useKibana } from './kibana_react'; @@ -153,44 +148,6 @@ export const useCurrentUser = (): AuthenticatedElasticUser | null => { return user; }; -export const useGetUserCasesPermissions = () => { - const [casesPermissions, setCasesPermissions] = useState({ - all: false, - create: false, - read: false, - update: false, - delete: false, - push: false, - connectors: false, - }); - const uiCapabilities = useKibana().services.application.capabilities; - const casesCapabilities = useKibana().services.cases.helpers.getUICapabilities( - uiCapabilities[CASES_FEATURE_ID] - ); - - useEffect(() => { - setCasesPermissions({ - all: casesCapabilities.all, - create: casesCapabilities.create, - read: casesCapabilities.read, - update: casesCapabilities.update, - delete: casesCapabilities.delete, - push: casesCapabilities.push, - connectors: casesCapabilities.connectors, - }); - }, [ - casesCapabilities.all, - casesCapabilities.create, - casesCapabilities.read, - casesCapabilities.update, - casesCapabilities.delete, - casesCapabilities.push, - casesCapabilities.connectors, - ]); - - return casesPermissions; -}; - export const useAppUrl = useGetAppUrl; export { useNavigateTo, useNavigation }; export type { GetAppUrl, NavigateTo }; diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts index 3d6d1a7034d28..ffbd26b97028a 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts @@ -117,7 +117,7 @@ export const createStartServicesMock = ( const discover = discoverPluginMock.createStartContract(); const cases = mockCasesContract(); const dataViewServiceMock = dataViewPluginMocks.createStartContract(); - cases.helpers.getUICapabilities.mockReturnValue(noCasesPermissions()); + cases.helpers.canUseCases.mockReturnValue(noCasesPermissions()); const triggersActionsUi = triggersActionsUiMock.createStart(); const cloudExperiments = cloudExperimentsMock.createStartMock(); const guidedOnboarding = guidedOnboardingMock.createStart(); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx index 2b887808696bd..a8e13bb8c5c27 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx @@ -74,17 +74,22 @@ jest.mock('../../../../common/lib/kibana', () => { application: { capabilities: { siem: { crud_alerts: true, read_alerts: true } }, }, - cases: mockCasesContract(), + cases: { + ...mockCasesContract(), + helpers: { + canUseCases: jest.fn().mockReturnValue({ + all: true, + create: true, + read: true, + update: true, + delete: true, + push: true, + }), + getRuleIdFromEvent: jest.fn(), + }, + }, }, }), - useGetUserCasesPermissions: jest.fn().mockReturnValue({ - all: true, - create: true, - read: true, - update: true, - delete: true, - push: true, - }), }; }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.test.tsx index de33379f48aba..cbe56a62c4574 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.test.tsx @@ -12,7 +12,7 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { useAddToCaseActions } from './use_add_to_case_actions'; import { TestProviders } from '../../../../common/mock'; -import { useGetUserCasesPermissions, useKibana } from '../../../../common/lib/kibana'; +import { useKibana } from '../../../../common/lib/kibana'; import { useTourContext } from '../../../../common/components/guided_onboarding_tour'; import { AlertsCasesTourSteps, @@ -20,6 +20,7 @@ import { } from '../../../../common/components/guided_onboarding_tour/tour_config'; import { CasesTourSteps } from '../../../../common/components/guided_onboarding_tour/cases_tour_steps'; import type { AlertTableContextMenuItem } from '../types'; +import { allCasesPermissions } from '../../../../cases_test_utils'; jest.mock('../../../../common/components/guided_onboarding_tour'); jest.mock('../../../../common/lib/kibana'); @@ -76,15 +77,6 @@ describe('useAddToCaseActions', () => { isTourShown: () => false, }); - (useGetUserCasesPermissions as jest.Mock).mockReturnValue({ - all: true, - create: true, - read: true, - update: true, - delete: true, - push: true, - }); - useKibanaMock.mockReturnValue({ services: { cases: { @@ -94,6 +86,7 @@ describe('useAddToCaseActions', () => { }, helpers: { getRuleIdFromEvent: () => null, + canUseCases: jest.fn().mockReturnValue(allCasesPermissions()), }, }, }, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.tsx index 821a638e893c2..de3c8782722fa 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.tsx @@ -9,6 +9,7 @@ import React, { useCallback, useMemo } from 'react'; import { AttachmentType } from '@kbn/cases-plugin/common'; import type { CaseAttachmentsWithoutOwner } from '@kbn/cases-plugin/public'; import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; +import { APP_ID } from '../../../../../common'; import { CasesTourSteps } from '../../../../common/components/guided_onboarding_tour/cases_tour_steps'; import { AlertsCasesTourSteps, @@ -16,7 +17,7 @@ import { SecurityStepId, } from '../../../../common/components/guided_onboarding_tour/tour_config'; import { useTourContext } from '../../../../common/components/guided_onboarding_tour'; -import { useGetUserCasesPermissions, useKibana } from '../../../../common/lib/kibana'; +import { useKibana } from '../../../../common/lib/kibana'; import type { TimelineNonEcsData } from '../../../../../common/search_strategy'; import { ADD_TO_EXISTING_CASE, ADD_TO_NEW_CASE } from '../translations'; import type { AlertTableContextMenuItem } from '../types'; @@ -43,7 +44,7 @@ export const useAddToCaseActions = ({ refetch, }: UseAddToCaseActions) => { const { cases: casesUi } = useKibana().services; - const userCasesPermissions = useGetUserCasesPermissions(); + const userCasesPermissions = casesUi.helpers.canUseCases([APP_ID]); const isAlert = useMemo(() => { return ecsData?.event?.kind?.includes('signal'); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.test.tsx index 3bee45e94712c..9a490bec1ce25 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_preview/preview_histogram.test.tsx @@ -23,7 +23,6 @@ import { usePreviewHistogram } from './use_preview_histogram'; import { PreviewHistogram } from './preview_histogram'; import { ALL_VALUES_ZEROS_TITLE } from '../../../../common/components/charts/translation'; -import { useGetUserCasesPermissions } from '../../../../common/lib/kibana'; import { useTimelineEvents } from '../../../../common/components/events_viewer/use_timelines_events'; import { TableId } from '@kbn/securitysolution-data-table'; import { createStore } from '../../../../common/store'; @@ -58,12 +57,7 @@ const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as j const getMockUseIsExperimentalFeatureEnabled = (mockMapping?: Partial) => (flag: keyof typeof allowedExperimentalValues) => mockMapping ? mockMapping?.[flag] : allowedExperimentalValues?.[flag]; -const originalKibanaLib = jest.requireActual('../../../../common/lib/kibana'); -// Restore the useGetUserCasesPermissions so the calling functions can receive a valid permissions object -// The returned permissions object will indicate that the user does not have permissions by default -const mockUseGetUserCasesPermissions = useGetUserCasesPermissions as jest.Mock; -mockUseGetUserCasesPermissions.mockImplementation(originalKibanaLib.useGetUserCasesPermissions); const mockUseFieldBrowserOptions = jest.fn(); jest.mock('../../../../timelines/components/fields_browser', () => ({ useFieldBrowserOptions: (props: UseFieldBrowserOptionsProps) => mockUseFieldBrowserOptions(props), diff --git a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx index 356938a6d54e3..cf67bf45fd360 100644 --- a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx @@ -18,7 +18,7 @@ import { TimelineId } from '../../../../common/types/timeline'; import { TestProviders } from '../../../common/mock'; import { mockTimelines } from '../../../common/mock/mock_timelines_plugin'; import { createStartServicesMock } from '../../../common/lib/kibana/kibana_react.mock'; -import { useKibana, useGetUserCasesPermissions, useHttp } from '../../../common/lib/kibana'; +import { useKibana, useHttp } from '../../../common/lib/kibana'; import { mockCasesContract } from '@kbn/cases-plugin/public/mocks'; import { initialUserPrivilegesState as mockInitialUserPrivilegesState } from '../../../common/components/user_privileges/user_privileges_context'; import { useUserPrivileges } from '../../../common/components/user_privileges'; @@ -46,7 +46,6 @@ jest.mock('../user_info', () => ({ })); jest.mock('../../../common/lib/kibana'); -(useGetUserCasesPermissions as jest.Mock).mockReturnValue(allCasesPermissions()); jest.mock('../../containers/detection_engine/alerts/use_alerts_privileges', () => ({ useAlertsPrivileges: jest.fn().mockReturnValue({ hasIndexWrite: true, hasKibanaCRUD: true }), @@ -119,7 +118,13 @@ describe('take action dropdown', () => { services: { ...mockStartServicesMock, timelines: { ...mockTimelines }, - cases: mockCasesContract(), + cases: { + ...mockCasesContract(), + helpers: { + canUseCases: jest.fn().mockReturnValue(allCasesPermissions()), + getRuleIdFromEvent: () => null, + }, + }, osquery: { isOsqueryAvailable: jest.fn().mockReturnValue(true), }, diff --git a/x-pack/plugins/security_solution/public/entity_analytics/api/api.ts b/x-pack/plugins/security_solution/public/entity_analytics/api/api.ts index 6b92583b1ddde..a3e324a58dd3b 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/api/api.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/api/api.ts @@ -11,6 +11,7 @@ import { RISK_ENGINE_ENABLE_URL, RISK_ENGINE_DISABLE_URL, RISK_ENGINE_INIT_URL, + RISK_ENGINE_PRIVILEGES_URL, } from '../../../common/constants'; import { KibanaServices } from '../../common/lib/kibana'; @@ -20,6 +21,7 @@ import type { GetRiskEngineStatusResponse, InitRiskEngineResponse, DisableRiskEngineResponse, + RiskEnginePrivilegesResponse, } from '../../../server/lib/entity_analytics/risk_engine/types'; import type { RiskScorePreviewRequestSchema } from '../../../common/risk_engine/risk_score_preview/request_schema'; @@ -85,3 +87,13 @@ export const disableRiskEngine = async (): Promise => method: 'POST', }); }; + +/** + * Get risk engine privileges + */ +export const fetchRiskEnginePrivileges = async (): Promise => { + return KibanaServices.get().http.fetch(RISK_ENGINE_PRIVILEGES_URL, { + version: '1', + method: 'GET', + }); +}; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_risk_engine_privileges.ts b/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_risk_engine_privileges.ts new file mode 100644 index 0000000000000..2a3ffa40856cb --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_risk_engine_privileges.ts @@ -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. + */ +import { useQuery } from '@tanstack/react-query'; +import { fetchRiskEnginePrivileges } from '../api'; + +export const useRiskEnginePrivileges = () => { + return useQuery(['GET', 'FETCH_RISK_ENGINE_PRIVILEGES'], fetchRiskEnginePrivileges); +}; diff --git a/x-pack/plugins/observability/public/utils/cases_permissions.ts b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_engine_privileges_callout/index.tsx similarity index 64% rename from x-pack/plugins/observability/public/utils/cases_permissions.ts rename to x-pack/plugins/security_solution/public/entity_analytics/components/risk_engine_privileges_callout/index.tsx index 2b3ff9cfbaf54..f26814431b364 100644 --- a/x-pack/plugins/observability/public/utils/cases_permissions.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_engine_privileges_callout/index.tsx @@ -5,11 +5,4 @@ * 2.0. */ -export const noCasesPermissions = () => ({ - all: false, - create: false, - read: false, - update: false, - delete: false, - push: false, -}); +export { RiskEnginePrivilegesCallOut } from './risk_engine_privileges_callout'; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_engine_privileges_callout/risk_engine_privileges_callout.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_engine_privileges_callout/risk_engine_privileges_callout.tsx new file mode 100644 index 0000000000000..edb6bc11f7217 --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_engine_privileges_callout/risk_engine_privileges_callout.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { CallOutMessage } from '../../../common/components/callouts'; +import { CallOutSwitcher } from '../../../common/components/callouts'; +import { MissingPrivilegesCallOutBody, MISSING_PRIVILEGES_CALLOUT_TITLE } from './translations'; +import { useMissingPrivileges } from './use_missing_risk_engine_privileges'; + +export const RiskEnginePrivilegesCallOut = () => { + const privileges = useMissingPrivileges(); + + if (privileges.isLoading || privileges.hasAllRequiredPrivileges) { + return null; + } + + const message: CallOutMessage = { + type: 'primary', + id: `missing-risk-engine-privileges`, + title: MISSING_PRIVILEGES_CALLOUT_TITLE, + description: , + }; + + return ( + message && + ); +}; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_engine_privileges_callout/translations.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_engine_privileges_callout/translations.tsx new file mode 100644 index 0000000000000..ed58f50f28279 --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_engine_privileges_callout/translations.tsx @@ -0,0 +1,97 @@ +/* + * 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 { EuiCode, EuiLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React from 'react'; +import { useKibana } from '../../../common/lib/kibana'; +import { CommaSeparatedValues } from '../../../detections/components/callouts/missing_privileges_callout/comma_separated_values'; +import type { MissingPrivileges } from './use_missing_risk_engine_privileges'; + +export const MISSING_PRIVILEGES_CALLOUT_TITLE = i18n.translate( + 'xpack.securitySolution.riskEngine.missingPrivilegesCallOut.messageTitle', + { + defaultMessage: 'Insufficient privileges', + } +); + +export const MissingPrivilegesCallOutBody: React.FC = ({ + indexPrivileges, + clusterPrivileges, +}) => { + const { docLinks } = useKibana().services; + + return ( + + + + + ), + }} + /> +

+ ), + indexPrivileges: + indexPrivileges.length > 0 ? ( + <> + +
    + {indexPrivileges.map(([index, missingPrivileges]) => ( +
  • {missingIndexPrivileges(index, missingPrivileges)}
  • + ))} +
+ + ) : null, + clusterPrivileges: + clusterPrivileges.length > 0 ? ( + <> + +
    + {clusterPrivileges.map((privilege) => ( +
  • {privilege}
  • + ))} +
+ + ) : null, + }} + /> + ); +}; + +const missingIndexPrivileges = (index: string, privileges: string[]) => ( + , + index: {index}, + }} + /> +); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_engine_privileges_callout/use_missing_risk_engine_privileges.ts b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_engine_privileges_callout/use_missing_risk_engine_privileges.ts new file mode 100644 index 0000000000000..ec41d7445e578 --- /dev/null +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_engine_privileges_callout/use_missing_risk_engine_privileges.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 { useMemo } from 'react'; +import type { RiskEnginePrivilegesResponse } from '../../../../server/lib/entity_analytics/risk_engine/types'; +import { useRiskEnginePrivileges } from '../../api/hooks/use_risk_engine_privileges'; +import { + RISK_ENGINE_REQUIRED_ES_CLUSTER_PRIVILEGES, + RISK_ENGINE_REQUIRED_ES_INDEX_PRIVILEGES, +} from '../../../../common/risk_engine'; + +const getMissingIndexPrivileges = ( + privileges: RiskEnginePrivilegesResponse['privileges']['elasticsearch']['index'] +): MissingIndexPrivileges => { + const missingIndexPrivileges: MissingIndexPrivileges = []; + + for (const [indexName, requiredPrivileges] of Object.entries( + RISK_ENGINE_REQUIRED_ES_INDEX_PRIVILEGES + )) { + const missingPrivileges = requiredPrivileges.filter( + (privilege) => !privileges[indexName][privilege] + ); + + if (missingPrivileges.length) { + missingIndexPrivileges.push([indexName, missingPrivileges]); + } + } + + return missingIndexPrivileges; +}; + +export type MissingClusterPrivileges = string[]; +export type MissingIndexPrivileges = Array<[indexName: string, privileges: string[]]>; + +export interface MissingPrivileges { + clusterPrivileges: MissingClusterPrivileges; + indexPrivileges: MissingIndexPrivileges; +} + +export type MissingPrivilegesResponse = + | { isLoading: true } + | { isLoading: false; hasAllRequiredPrivileges: true } + | { isLoading: false; missingPrivileges: MissingPrivileges; hasAllRequiredPrivileges: false }; + +export const useMissingPrivileges = (): MissingPrivilegesResponse => { + const { data: privilegesResponse, isLoading } = useRiskEnginePrivileges(); + + return useMemo(() => { + if (isLoading || !privilegesResponse) { + return { + isLoading: true, + }; + } + + if (privilegesResponse.has_all_required) { + return { + isLoading: false, + hasAllRequiredPrivileges: true, + }; + } + + const { privileges } = privilegesResponse; + const missinIndexPrivileges = getMissingIndexPrivileges(privileges.elasticsearch.index); + const missingClusterPrivileges = RISK_ENGINE_REQUIRED_ES_CLUSTER_PRIVILEGES.filter( + (privilege) => !privileges.elasticsearch.cluster[privilege] + ); + + return { + isLoading: false, + hasAllRequiredPrivileges: false, + missingPrivileges: { + indexPrivileges: missinIndexPrivileges, + clusterPrivileges: missingClusterPrivileges, + }, + }; + }, [isLoading, privilegesResponse]); +}; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_enable_section.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_enable_section.tsx index 703ed94a8c617..b5ccc1b8daa63 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_enable_section.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_enable_section.tsx @@ -296,7 +296,11 @@ export const RiskScoreEnableSection = () => { )} {!isUpdateAvailable && ( - {isLoading && } + + {isLoading && ( + + )} + { + const privilegesCalloutEnabled = useIsExperimentalFeatureEnabled( + 'riskEnginePrivilegesRouteEnabled' + ); return ( <> + {privilegesCalloutEnabled && } { + const original = jest.requireActual('../../../../common/lib/kibana/kibana_react'); + + return { + ...original, + useKibana: () => ({ + ...mockedUseKibana, + services: { + ...mockedUseKibana.services, + cases: { + helpers: { canUseCases: mockCanUseCases }, + }, + }, + }), + }; +}); describe('useShowRelatedCases', () => { it(`should return false if user doesn't have cases read privilege`, () => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue({ + mockCanUseCases.mockReturnValue({ all: false, create: false, read: false, @@ -28,7 +47,7 @@ describe('useShowRelatedCases', () => { }); it('should return true if user has cases read privilege', () => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue({ + mockCanUseCases.mockReturnValue({ all: false, create: false, read: true, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_cases.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_cases.ts index e469cc2ef155c..7bc3429cd0585 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_cases.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_show_related_cases.ts @@ -5,12 +5,15 @@ * 2.0. */ -import { useGetUserCasesPermissions } from '../../../../common/lib/kibana'; +import { APP_ID } from '../../../../../common'; +import { useKibana } from '../../../../common/lib/kibana/kibana_react'; /** * Returns true if the user has read privileges for cases, false otherwise */ export const useShowRelatedCases = (): boolean => { - const userCasesPermissions = useGetUserCasesPermissions(); + const { cases } = useKibana().services; + const userCasesPermissions = cases.helpers.canUseCases([APP_ID]); + return userCasesPermissions.read; }; diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_cases/index.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_cases/index.tsx index 5e75ed3643a71..134fac9ac7295 100644 --- a/x-pack/plugins/security_solution/public/overview/components/recent_cases/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/recent_cases/index.tsx @@ -7,14 +7,14 @@ import React from 'react'; -import { useGetUserCasesPermissions, useKibana } from '../../../common/lib/kibana'; +import { useKibana } from '../../../common/lib/kibana'; import { APP_ID } from '../../../../common/constants'; const MAX_CASES_TO_SHOW = 3; const RecentCasesComponent = () => { const { cases } = useKibana().services; - const userCasesPermissions = useGetUserCasesPermissions(); + const userCasesPermissions = cases.helpers.canUseCases([APP_ID]); return cases.ui.getRecentCases({ permissions: userCasesPermissions, diff --git a/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.test.tsx b/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.test.tsx index 692ffd0b44ec8..8835fc0e48f27 100644 --- a/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.test.tsx @@ -10,7 +10,7 @@ import { mount } from 'enzyme'; import { waitFor } from '@testing-library/react'; import { TestProviders } from '../../../common/mock'; import { Sidebar } from './sidebar'; -import { useGetUserCasesPermissions, useKibana } from '../../../common/lib/kibana'; +import { useKibana } from '../../../common/lib/kibana'; import type { CaseUiClientMock } from '@kbn/cases-plugin/public/mocks'; import { casesPluginMock } from '@kbn/cases-plugin/public/mocks'; import { noCasesPermissions, readCasesPermissions } from '../../../cases_test_utils'; @@ -38,7 +38,7 @@ describe('Sidebar', () => { }); it('does not render the recently created cases section when the user does not have read permissions', async () => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue(noCasesPermissions()); + casesMock.helpers.canUseCases.mockReturnValue(noCasesPermissions()); await waitFor(() => mount( @@ -52,7 +52,7 @@ describe('Sidebar', () => { }); it('does render the recently created cases section when the user has read permissions', async () => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue(readCasesPermissions()); + casesMock.helpers.canUseCases.mockReturnValue(readCasesPermissions()); await waitFor(() => mount( diff --git a/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.tsx b/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.tsx index 4f87ec1d86605..501e03a65cb02 100644 --- a/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/sidebar/sidebar.tsx @@ -8,7 +8,12 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import React, { useMemo } from 'react'; -import { ENABLE_NEWS_FEED_SETTING, NEWS_FEED_URL_SETTING } from '../../../../common/constants'; +import { useKibana } from '../../../common/lib/kibana/kibana_react'; +import { + APP_ID, + ENABLE_NEWS_FEED_SETTING, + NEWS_FEED_URL_SETTING, +} from '../../../../common/constants'; import { Filters as RecentTimelinesFilters } from '../recent_timelines/filters'; import { StatefulRecentTimelines } from '../recent_timelines'; import { StatefulNewsFeed } from '../../../common/components/news_feed'; @@ -17,7 +22,6 @@ import { SidebarHeader } from '../../../common/components/sidebar_header'; import * as i18n from '../../pages/translations'; import { RecentCases } from '../recent_cases'; -import { useGetUserCasesPermissions } from '../../../common/lib/kibana'; const SidebarSpacerComponent = () => ( @@ -30,6 +34,7 @@ export const Sidebar = React.memo<{ recentTimelinesFilterBy: RecentTimelinesFilterMode; setRecentTimelinesFilterBy: (filterBy: RecentTimelinesFilterMode) => void; }>(({ recentTimelinesFilterBy, setRecentTimelinesFilterBy }) => { + const { cases } = useKibana().services; const recentTimelinesFilters = useMemo( () => ( diff --git a/x-pack/plugins/security_solution/public/overview/pages/data_quality.test.tsx b/x-pack/plugins/security_solution/public/overview/pages/data_quality.test.tsx index 5fbe5d80f99ce..3e075d27ac76b 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/data_quality.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/data_quality.test.tsx @@ -30,14 +30,6 @@ jest.mock('../../common/lib/kibana', () => { return { ...original, KibanaServices: mockKibanaServices, - useGetUserCasesPermissions: () => ({ - all: false, - create: false, - read: true, - update: false, - delete: false, - push: false, - }), useKibana: jest.fn(), useUiSetting$: () => ['0,0.[000]'], }; @@ -80,6 +72,16 @@ describe('DataQuality', () => { hooks: { useCasesAddToNewCaseFlyout: jest.fn(), }, + helpers: { + canUseCases: jest.fn().mockReturnValue({ + all: false, + create: false, + read: true, + update: false, + delete: false, + push: false, + }), + }, }, configSettings: { ILMEnabled: true }, }, @@ -307,6 +309,16 @@ describe('DataQuality', () => { hooks: { useCasesAddToNewCaseFlyout: jest.fn(), }, + helpers: { + canUseCases: jest.fn().mockReturnValue({ + all: false, + create: false, + read: true, + update: false, + delete: false, + push: false, + }), + }, }, configSettings: { ILMEnabled: false }, }, diff --git a/x-pack/plugins/security_solution/public/overview/pages/data_quality.tsx b/x-pack/plugins/security_solution/public/overview/pages/data_quality.tsx index 0ef421977a402..0c318b38a4660 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/data_quality.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/data_quality.tsx @@ -38,15 +38,9 @@ import { HeaderPage } from '../../common/components/header_page'; import { LandingPageComponent } from '../../common/components/landing_page'; import { useLocalStorage } from '../../common/components/local_storage'; import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper'; -import { DEFAULT_BYTES_FORMAT, DEFAULT_NUMBER_FORMAT } from '../../../common/constants'; +import { APP_ID, DEFAULT_BYTES_FORMAT, DEFAULT_NUMBER_FORMAT } from '../../../common/constants'; import { useSourcererDataView } from '../../common/containers/sourcerer'; -import { - KibanaServices, - useGetUserCasesPermissions, - useKibana, - useToasts, - useUiSetting$, -} from '../../common/lib/kibana'; +import { KibanaServices, useKibana, useToasts, useUiSetting$ } from '../../common/lib/kibana'; import { SpyRoute } from '../../common/utils/route/spy_routes'; import { useSignalIndex } from '../../detections/containers/detection_engine/alerts/use_signal_index'; import * as i18n from './translations'; @@ -141,9 +135,7 @@ const DataQualityComponent: React.FC = () => { const httpFetch = KibanaServices.get().http.fetch; const { baseTheme, theme } = useThemes(); const toasts = useToasts(); - const { - services: { telemetry }, - } = useKibana(); + const addSuccessToast = useCallback( (toast: { title: string }) => { toasts.addSuccess(toast); @@ -156,7 +148,7 @@ const DataQualityComponent: React.FC = () => { const [selectedOptions, setSelectedOptions] = useState(defaultOptions); const { indicesExist, loading: isSourcererLoading, selectedPatterns } = useSourcererDataView(); const { signalIndexName, loading: isSignalIndexNameLoading } = useSignalIndex(); - const { configSettings, cases } = useKibana().services; + const { configSettings, cases, telemetry } = useKibana().services; const isILMAvailable = configSettings.ILMEnabled; const [startDate, setStartTime] = useState(); @@ -210,7 +202,7 @@ const DataQualityComponent: React.FC = () => { key: LOCAL_STORAGE_KEY, }); - const userCasesPermissions = useGetUserCasesPermissions(); + const userCasesPermissions = cases.helpers.canUseCases([APP_ID]); const canUserCreateAndReadCases = useCallback( () => userCasesPermissions.create && userCasesPermissions.read, [userCasesPermissions.create, userCasesPermissions.read] diff --git a/x-pack/plugins/security_solution/public/overview/pages/detection_response.test.tsx b/x-pack/plugins/security_solution/public/overview/pages/detection_response.test.tsx index 60ee2d02e6b25..2b5561f58cb5c 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/detection_response.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/detection_response.test.tsx @@ -11,6 +11,7 @@ import { render } from '@testing-library/react'; import { DetectionResponse } from './detection_response'; import { TestProviders } from '../../common/mock'; import { noCasesPermissions, readCasesPermissions } from '../../cases_test_utils'; +import { useKibana as mockUseKibana } from '../../common/lib/kibana/__mocks__'; jest.mock('../components/detection_response/alerts_by_status', () => ({ AlertsByStatus: () =>
, @@ -75,12 +76,24 @@ jest.mock('../../detections/containers/detection_engine/alerts/use_alerts_privil })); const defaultUseCasesPermissionsReturn = readCasesPermissions(); -const mockUseCasesPermissions = jest.fn(() => defaultUseCasesPermissionsReturn); -jest.mock('../../common/lib/kibana/hooks', () => { - const original = jest.requireActual('../../common/lib/kibana/hooks'); + +const mockedUseKibana = mockUseKibana(); +const mockCanUseCases = jest.fn(); + +jest.mock('../../common/lib/kibana', () => { + const original = jest.requireActual('../../common/lib/kibana'); + return { ...original, - useGetUserCasesPermissions: () => mockUseCasesPermissions(), + useKibana: () => ({ + ...mockedUseKibana, + services: { + ...mockedUseKibana.services, + cases: { + helpers: { canUseCases: mockCanUseCases }, + }, + }, + }), }; }); @@ -90,7 +103,7 @@ describe('DetectionResponse', () => { mockUseSourcererDataView.mockReturnValue(defaultUseSourcererReturn); mockUseAlertsPrivileges.mockReturnValue(defaultUseAlertsPrivilegesReturn); mockUseSignalIndex.mockReturnValue(defaultUseSignalIndexReturn); - mockUseCasesPermissions.mockReturnValue(defaultUseCasesPermissionsReturn); + mockCanUseCases.mockReturnValue(defaultUseCasesPermissionsReturn); }); it('should render default page', () => { @@ -197,7 +210,7 @@ describe('DetectionResponse', () => { }); it('should not render cases data sections if the user does not have cases read permission', () => { - mockUseCasesPermissions.mockReturnValue(noCasesPermissions()); + mockCanUseCases.mockReturnValue(noCasesPermissions()); const result = render( @@ -218,7 +231,7 @@ describe('DetectionResponse', () => { }); it('should render page permissions message if the user does not have read permission', () => { - mockUseCasesPermissions.mockReturnValue(noCasesPermissions()); + mockCanUseCases.mockReturnValue(noCasesPermissions()); mockUseAlertsPrivileges.mockReturnValue({ hasKibanaREAD: true, hasIndexRead: false, diff --git a/x-pack/plugins/security_solution/public/overview/pages/detection_response.tsx b/x-pack/plugins/security_solution/public/overview/pages/detection_response.tsx index 8bdc95fc69aab..77bbdbe8816ed 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/detection_response.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/detection_response.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; import type { DocLinks } from '@kbn/doc-links'; +import { APP_ID } from '../../../common'; import { InputsModelId } from '../../common/store/inputs/constants'; import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; import { SocTrends } from '../components/detection_response/soc_trends'; @@ -18,7 +19,6 @@ import { useSourcererDataView } from '../../common/containers/sourcerer'; import { useSignalIndex } from '../../detections/containers/detection_engine/alerts/use_signal_index'; import { useAlertsPrivileges } from '../../detections/containers/detection_engine/alerts/use_alerts_privileges'; import { HeaderPage } from '../../common/components/header_page'; -import { useGetUserCasesPermissions } from '../../common/lib/kibana'; import { LandingPageComponent } from '../../common/components/landing_page'; import { AlertsByStatus } from '../components/detection_response/alerts_by_status'; @@ -31,13 +31,16 @@ import { CasesByStatus } from '../components/detection_response/cases_by_status' import { NoPrivileges } from '../../common/components/no_privileges'; import { FiltersGlobal } from '../../common/components/filters_global'; import { useGlobalFilterQuery } from '../../common/hooks/use_global_filter_query'; +import { useKibana } from '../../common/lib/kibana'; const DetectionResponseComponent = () => { + const { cases } = useKibana().services; const { filterQuery } = useGlobalFilterQuery(); const { indicesExist, loading: isSourcererLoading, sourcererDataView } = useSourcererDataView(); const { signalIndexName } = useSignalIndex(); const { hasKibanaREAD, hasIndexRead } = useAlertsPrivileges(); - const canReadCases = useGetUserCasesPermissions().read; + const userCasesPermissions = cases.helpers.canUseCases([APP_ID]); + const canReadCases = userCasesPermissions.read; const canReadAlerts = hasKibanaREAD && hasIndexRead; const isSocTrendsEnabled = useIsExperimentalFeatureEnabled('socTrendsEnabled'); if (!canReadAlerts && !canReadCases) { diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.test.tsx index 39dc27c540d56..7203e74fe02c1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.test.tsx @@ -7,19 +7,46 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; -import { useKibana, useGetUserCasesPermissions } from '../../../../common/lib/kibana'; import { TestProviders, mockIndexNames, mockIndexPattern } from '../../../../common/mock'; import { useSourcererDataView } from '../../../../common/containers/sourcerer'; import { allCasesPermissions, readCasesPermissions } from '../../../../cases_test_utils'; import { mockBrowserFields } from '../../../../common/containers/source/mock'; import { TimelineActionMenu } from '.'; import { TimelineId, TimelineTabs } from '../../../../../common/types'; +import { useKibana as mockUseKibana } from '../../../../common/lib/kibana/__mocks__'; const mockUseSourcererDataView: jest.Mock = useSourcererDataView as jest.Mock; +const mockedUseKibana = mockUseKibana(); +const mockCanUseCases = jest.fn(); + jest.mock('../../../../common/containers/sourcerer'); -const useKibanaMock = useKibana as jest.Mocked; -jest.mock('../../../../common/lib/kibana'); +jest.mock('../../../../common/lib/kibana/kibana_react', () => { + const original = jest.requireActual('../../../../common/lib/kibana/kibana_react'); + + return { + ...original, + useKibana: () => ({ + ...mockedUseKibana, + services: { + ...mockedUseKibana.services, + cases: { + ...mockedUseKibana.services.cases, + helpers: { canUseCases: mockCanUseCases }, + }, + }, + application: { + capabilities: { + navLinks: {}, + management: {}, + catalogue: {}, + actions: { show: true, crud: true }, + }, + }, + }), + }; +}); + jest.mock('@kbn/i18n-react', () => { const originalModule = jest.requireActual('@kbn/i18n-react'); const FormattedRelative = jest.fn().mockImplementation(() => '20 hours ago'); @@ -41,20 +68,15 @@ describe('Action menu', () => { beforeEach(() => { // Mocking these services is required for the header component to render. mockUseSourcererDataView.mockImplementation(() => sourcererDefaultValue); - useKibanaMock().services.application.capabilities = { - navLinks: {}, - management: {}, - catalogue: {}, - actions: { show: true, crud: true }, - }; }); afterEach(() => { jest.clearAllMocks(); }); + describe('AddToCaseButton', () => { it('renders the button when the user has create and read permissions', () => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue(allCasesPermissions()); + mockCanUseCases.mockReturnValue(allCasesPermissions()); render( @@ -70,7 +92,7 @@ describe('Action menu', () => { }); it('does not render the button when the user does not have create permissions', () => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue(readCasesPermissions()); + mockCanUseCases.mockReturnValue(readCasesPermissions()); render( diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.tsx index 850078e134129..04f7b2eb0a31e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/action_menu/index.tsx @@ -7,7 +7,8 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; -import { useGetUserCasesPermissions } from '../../../../common/lib/kibana'; +import { useKibana } from '../../../../common/lib/kibana/kibana_react'; +import { APP_ID } from '../../../../../common'; import type { TimelineTabs } from '../../../../../common/types'; import { InspectButton } from '../../../../common/components/inspect'; import { InputsModelId } from '../../../../common/store/inputs/constants'; @@ -29,7 +30,9 @@ const TimelineActionMenuComponent = ({ activeTab, isInspectButtonDisabled, }: TimelineActionMenuProps) => { - const userCasesPermissions = useGetUserCasesPermissions(); + const { cases } = useKibana().services; + const userCasesPermissions = cases.helpers.canUseCases([APP_ID]); + return ( { }); jest.mock('../../../../common/lib/kibana'); -const originalKibanaLib = jest.requireActual('../../../../common/lib/kibana'); - -// Restore the useGetUserCasesPermissions so the calling functions can receive a valid permissions object -// The returned permissions object will indicate that the user does not have permissions by default -const mockUseGetUserCasesPermissions = useGetUserCasesPermissions as jest.Mock; -mockUseGetUserCasesPermissions.mockImplementation(originalKibanaLib.useGetUserCasesPermissions); - jest.mock('../../../../common/hooks/use_selector'); const useKibanaMock = useKibana as jest.Mocked; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx index 56b7bafe58ea2..81e520368da6c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx @@ -15,7 +15,7 @@ import { APP_ID, APP_UI_ID } from '../../../../../common/constants'; import { timelineSelectors } from '../../../store/timeline'; import { setInsertTimeline, showTimeline } from '../../../store/timeline/actions'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; -import { useGetUserCasesPermissions, useKibana } from '../../../../common/lib/kibana'; +import { useKibana } from '../../../../common/lib/kibana'; import { TimelineId } from '../../../../../common/types/timeline'; import { TimelineStatus, TimelineType } from '../../../../../common/api/timeline'; import { getCreateCaseUrl, getCaseDetailsUrl } from '../../../../common/components/link_to'; @@ -68,7 +68,7 @@ const AddToCaseButtonComponent: React.FC = ({ timelineId }) => { [dispatch, graphEventId, navigateToApp, savedObjectId, timelineId, timelineTitle] ); - const userCasesPermissions = useGetUserCasesPermissions(); + const userCasesPermissions = cases.helpers.canUseCases([APP_ID]); const handleButtonClick = useCallback(() => { setPopover((currentIsOpen) => !currentIsOpen); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/flyout/footer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/flyout/footer.test.tsx index 91a2904287ffc..5362af38c2413 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/flyout/footer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/flyout/footer.test.tsx @@ -13,11 +13,7 @@ import { TimelineId } from '../../../../../../common/types/timeline'; import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; import { mockAlertDetailsData } from '../../../../../common/components/event_details/__mocks__'; import type { TimelineEventsDetailsItem } from '../../../../../../common/search_strategy'; -import { - KibanaServices, - useGetUserCasesPermissions, - useKibana, -} from '../../../../../common/lib/kibana'; +import { KibanaServices, useKibana } from '../../../../../common/lib/kibana'; import { coreMock } from '@kbn/core/public/mocks'; import { mockCasesContract } from '@kbn/cases-plugin/public/mocks'; @@ -70,12 +66,6 @@ jest.mock('../../../../../detections/components/user_info', () => ({ })); jest.mock('../../../../../common/lib/kibana'); -const originalKibanaLib = jest.requireActual('../../../../../common/lib/kibana'); - -// Restore the useGetUserCasesPermissions so the calling functions can receive a valid permissions object -// The returned permissions object will indicate that the user does not have permissions by default -const mockUseGetUserCasesPermissions = useGetUserCasesPermissions as jest.Mock; -mockUseGetUserCasesPermissions.mockImplementation(originalKibanaLib.useGetUserCasesPermissions); jest.mock( '../../../../../detections/containers/detection_engine/alerts/use_alerts_privileges', diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.test.tsx index 10a536f69c8d0..508a5caa590f4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.test.tsx @@ -11,11 +11,7 @@ import '../../../../common/mock/match_media'; import { TestProviders } from '../../../../common/mock'; import { TimelineId, TimelineTabs } from '../../../../../common/types/timeline'; import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; -import { - KibanaServices, - useKibana, - useGetUserCasesPermissions, -} from '../../../../common/lib/kibana'; +import { KibanaServices, useKibana } from '../../../../common/lib/kibana'; import { mockBrowserFields, mockRuntimeMappings } from '../../../../common/containers/source/mock'; import { coreMock } from '@kbn/core/public/mocks'; import { mockCasesContext } from '@kbn/cases-plugin/public/mocks/mock_cases_context'; @@ -156,6 +152,9 @@ describe('event details panel component', () => { ui: { getCasesContext: () => mockCasesContext, }, + cases: { + helpers: { canUseCases: jest.fn().mockReturnValue(allCasesPermissions()) }, + }, }, timelines: { getHoverActions: jest.fn().mockReturnValue({ @@ -168,11 +167,12 @@ describe('event details panel component', () => { }, }, }); - (useGetUserCasesPermissions as jest.Mock).mockReturnValue(allCasesPermissions()); }); + afterEach(() => { jest.clearAllMocks(); }); + test('it renders the take action dropdown in the timeline version', () => { const wrapper = render( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx index ce03812e58fc8..7928a93dc4885 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx @@ -32,7 +32,6 @@ import { defaultRowRenderers } from './body/renderers'; import { useSourcererDataView } from '../../../common/containers/sourcerer'; import { createStore } from '../../../common/store'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; -import { useGetUserCasesPermissions } from '../../../common/lib/kibana'; jest.mock('../../containers', () => ({ useTimelineEvents: jest.fn(), @@ -43,12 +42,6 @@ jest.mock('./tabs_content', () => ({ })); jest.mock('../../../common/lib/kibana'); -const originalKibanaLib = jest.requireActual('../../../common/lib/kibana'); - -// Restore the useGetUserCasesPermissions so the calling functions can receive a valid permissions object -// The returned permissions object will indicate that the user does not have permissions by default -const mockUseGetUserCasesPermissions = useGetUserCasesPermissions as jest.Mock; -mockUseGetUserCasesPermissions.mockImplementation(originalKibanaLib.useGetUserCasesPermissions); jest.mock('../../../common/utils/normalize_time_range'); jest.mock('@kbn/i18n-react', () => { diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/get_user_risk_engine_privileges.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/get_user_risk_engine_privileges.ts new file mode 100644 index 0000000000000..503d671c3c0e2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/get_user_risk_engine_privileges.ts @@ -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 type { KibanaRequest } from '@kbn/core/server'; +import type { SecurityPluginStart } from '@kbn/security-plugin/server'; +import type { RiskEnginePrivilegesResponse } from './types'; +import { + RISK_ENGINE_REQUIRED_ES_CLUSTER_PRIVILEGES, + RISK_ENGINE_REQUIRED_ES_INDEX_PRIVILEGES, +} from '../../../../common/risk_engine'; + +const groupPrivilegesByName = ( + privileges: Array<{ + privilege: PrivilegeName; + authorized: boolean; + }> +): Record => { + return privileges.reduce>((acc, { privilege, authorized }) => { + acc[privilege] = authorized; + return acc; + }, {}); +}; + +export async function getUserRiskEnginePrivileges( + request: KibanaRequest, + security: SecurityPluginStart +): Promise { + const checkPrivileges = security.authz.checkPrivilegesDynamicallyWithRequest(request); + const { privileges, hasAllRequested } = await checkPrivileges({ + elasticsearch: { + cluster: RISK_ENGINE_REQUIRED_ES_CLUSTER_PRIVILEGES, + index: RISK_ENGINE_REQUIRED_ES_INDEX_PRIVILEGES, + }, + }); + + const clusterPrivilegesByPrivilege = groupPrivilegesByName(privileges.elasticsearch.cluster); + + const indexPrivilegesByIndex = Object.entries(privileges.elasticsearch.index).reduce< + Record> + >((acc, [index, indexPrivileges]) => { + acc[index] = groupPrivilegesByName(indexPrivileges); + return acc; + }, {}); + + return { + privileges: { + elasticsearch: { + cluster: clusterPrivilegesByPrivilege, + index: indexPrivilegesByIndex, + }, + }, + has_all_required: hasAllRequested, + }; +} diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/index.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/index.ts index 1c37efc508f05..fc2c420dcb645 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/index.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/index.ts @@ -10,3 +10,4 @@ export { riskEngineInitRoute } from './risk_engine_init_route'; export { riskEngineEnableRoute } from './risk_engine_enable_route'; export { riskEngineDisableRoute } from './risk_engine_disable_route'; export { riskEngineStatusRoute } from './risk_engine_status_route'; +export { riskEnginePrivilegesRoute } from './risk_engine_privileges_route'; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/risk_engine_privileges_route.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/risk_engine_privileges_route.ts new file mode 100644 index 0000000000000..d035119ec0f1e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/routes/risk_engine_privileges_route.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. + */ + +import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; +import type { StartServicesAccessor } from '@kbn/core/server'; +import { RISK_ENGINE_PRIVILEGES_URL, APP_ID } from '../../../../../common/constants'; + +import type { StartPlugins } from '../../../../plugin'; +import type { SecuritySolutionPluginRouter } from '../../../../types'; +import { getUserRiskEnginePrivileges } from '../get_user_risk_engine_privileges'; + +export const riskEnginePrivilegesRoute = ( + router: SecuritySolutionPluginRouter, + getStartServices: StartServicesAccessor +) => { + router.versioned + .get({ + access: 'internal', + path: RISK_ENGINE_PRIVILEGES_URL, + options: { + tags: ['access:securitySolution', `access:${APP_ID}-entity-analytics`], + }, + }) + .addVersion({ version: '1', validate: false }, async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + + const [_, { security }] = await getStartServices(); + const body = await getUserRiskEnginePrivileges(request, security); + + try { + return response.ok({ + body, + }); + } catch (e) { + const error = transformError(e); + + return siemResponse.error({ + statusCode: error.statusCode, + body: error.message, + }); + } + }); +}; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/schema/risk_score_apis.yml b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/schema/risk_score_apis.yml index c51bf19ebd2b1..d9840840ea2f6 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/schema/risk_score_apis.yml +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/schema/risk_score_apis.yml @@ -93,6 +93,16 @@ paths: application/json: schema: $ref: '#/components/schemas/RiskEngineDisableResponse' + /engine/privileges: + get: + summary: Check if the user has access to the risk engine + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/RiskEnginePrivilegesResponse' components: @@ -436,4 +446,25 @@ components: type: boolean error: type: string - \ No newline at end of file + RiskEnginePrivilegesResponse: + type: object + properties: + privileges: + type: object + properties: + elasticsearch: + type: object + properties: + cluster: + type: object + additionalProperties: + type: boolean + index: + type: object + additionalProperties: + type: object + additionalProperties: + type: boolean + has_all_required: + description: If true then the user has full access to the risk engine + type: boolean diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/types.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/types.ts index f5aeaf4f56428..4315b2638fbd8 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/types.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_engine/types.ts @@ -98,6 +98,16 @@ export interface DisableRiskEngineResponse { success: boolean; } +export interface RiskEnginePrivilegesResponse { + privileges: { + elasticsearch: { + cluster: Record; + index: Record>; + }; + }; + has_all_required: boolean; +} + export interface CalculateRiskScoreAggregations { user?: { after_key: AfterKey; diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/index.ts b/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/index.ts index 3260a3e188242..4af8d3a9e435d 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/index.ts @@ -82,10 +82,12 @@ export const createMockTelemetryReceiver = ( fetchLicenseInfo: jest.fn().mockReturnValue(stubLicenseInfo), copyLicenseFields: jest.fn(), fetchFleetAgents: jest.fn(), + openPointInTime: jest.fn(), + getAlertsIndex: jest.fn().mockReturnValue('alerts-*'), fetchDiagnosticAlerts: jest.fn().mockReturnValue(diagnosticsAlert ?? jest.fn()), fetchEndpointMetrics: jest.fn().mockReturnValue(stubEndpointMetricsResponse), fetchEndpointPolicyResponses: jest.fn(), - fetchPrebuiltRuleAlerts: jest.fn().mockReturnValue(prebuiltRuleAlertsResponse), + fetchPrebuiltRuleAlertsBatch: jest.fn().mockReturnValue(prebuiltRuleAlertsResponse), fetchDetectionRulesPackageVersion: jest.fn(), fetchTrustedApplications: jest.fn(), fetchEndpointList: jest.fn(), @@ -95,7 +97,6 @@ export const createMockTelemetryReceiver = ( buildProcessTree: jest.fn().mockReturnValue(processTreeResponse), fetchTimelineEvents: jest.fn().mockReturnValue(Promise.resolve(stubFetchTimelineEvents())), fetchValueListMetaData: jest.fn(), - getAlertsIndex: jest.fn().mockReturnValue('test-alerts-index'), } as unknown as jest.Mocked; }; diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts b/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts index a2360617c48f3..80b90deba6ef5 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts @@ -85,6 +85,14 @@ export interface ITelemetryReceiver { getClusterInfo(): ESClusterInfo | undefined; + fetchClusterInfo(): Promise; + + fetchLicenseInfo(): Promise; + + openPointInTime(indexPattern: string): Promise; + + closePointInTime(pitId: string): Promise; + fetchDetectionRulesPackageVersion(): Promise; fetchFleetAgents(): Promise< @@ -151,9 +159,15 @@ export interface ITelemetryReceiver { per_page: number; }>; - fetchClusterInfo(): Promise; - - fetchLicenseInfo(): Promise; + fetchPrebuiltRuleAlertsBatch( + pitId: string, + searchAfterValue: SortResults | undefined + ): Promise<{ + moreToFetch: boolean; + newPitId: string; + searchAfter: SortResults | undefined; + alerts: TelemetryEvent[]; + }>; copyLicenseFields(lic: ESLicense): { issuer?: string | undefined; @@ -163,8 +177,6 @@ export interface ITelemetryReceiver { type: string; }; - fetchPrebuiltRuleAlerts(): Promise<{ events: TelemetryEvent[]; count: number }>; - fetchTimelineAlerts( index: string, rangeFrom: string, @@ -582,138 +594,188 @@ export class TelemetryReceiver implements ITelemetryReceiver { }; } - /** - * Fetch an overview of detection rule alerts over the last 3 hours. - * Filters out custom rules and endpoint rules. - * @returns total of alerts by rules - */ - public async fetchPrebuiltRuleAlerts() { + public async fetchPrebuiltRuleAlertsBatch( + pitId: string, + searchAfterValue: SortResults | undefined + ) { if (this.esClient === undefined || this.esClient === null) { - throw Error('elasticsearch client is unavailable: cannot retrieve pre-built rule alerts'); + throw Error('es client is unavailable: cannot retrieve pre-built rule alert batches'); } - const query: SearchRequest = { - expand_wildcards: ['open' as const, 'hidden' as const], - index: `${this.alertsIndex}*`, - ignore_unavailable: true, - body: { - size: 1_000, - _source: { - exclude: ['message', 'kibana.alert.rule.note', 'kibana.alert.rule.parameters.note'], - }, - query: { - bool: { - filter: [ - { - bool: { - should: [ - { - bool: { - must_not: { - bool: { - should: [ - { - match_phrase: { - 'kibana.alert.rule.name': 'Malware Prevention Alert', - }, + let newPitId = pitId; + let fetchMore = true; + let searchAfter: SortResults | undefined = searchAfterValue; + const query: ESSearchRequest = { + query: { + bool: { + filter: [ + { + bool: { + should: [ + { + bool: { + must_not: { + bool: { + should: [ + { + match_phrase: { + 'kibana.alert.rule.name': 'Malware Prevention Alert', }, - ], - }, + }, + ], }, }, }, - { - bool: { - must_not: { - bool: { - should: [ - { - match_phrase: { - 'kibana.alert.rule.name': 'Malware Detection Alert', - }, + }, + { + bool: { + must_not: { + bool: { + should: [ + { + match_phrase: { + 'kibana.alert.rule.name': 'Malware Detection Alert', }, - ], - }, + }, + ], }, }, }, - { - bool: { - must_not: { - bool: { - should: [ - { - match_phrase: { - 'kibana.alert.rule.name': 'Ransomware Prevention Alert', - }, + }, + { + bool: { + must_not: { + bool: { + should: [ + { + match_phrase: { + 'kibana.alert.rule.name': 'Ransomware Prevention Alert', }, - ], - }, + }, + ], }, }, }, - { - bool: { - must_not: { - bool: { - should: [ - { - match_phrase: { - 'kibana.alert.rule.name': 'Ransomware Detection Alert', - }, + }, + { + bool: { + must_not: { + bool: { + should: [ + { + match_phrase: { + 'kibana.alert.rule.name': 'Ransomware Detection Alert', }, - ], - }, + }, + ], }, }, }, - ], - }, + }, + ], }, - { - bool: { - should: [ - { - match_phrase: { - 'kibana.alert.rule.parameters.immutable': 'true', - }, + }, + { + bool: { + should: [ + { + match_phrase: { + 'kibana.alert.rule.parameters.immutable': 'true', }, - ], - }, - }, - { - range: { - '@timestamp': { - gte: 'now-1h', - lte: 'now', }, + ], + }, + }, + { + range: { + '@timestamp': { + gte: 'now-1h', + lte: 'now', }, }, - ], - }, - }, - aggs: { - prebuilt_rule_alert_count: { - cardinality: { - field: 'event.id', }, - }, + ], }, }, + track_total_hits: false, + sort: [ + { '@timestamp': { order: 'asc', format: 'strict_date_optional_time_nanos' } }, + { _shard_doc: 'desc' }, + ] as unknown as string[], + pit: { id: pitId }, + search_after: searchAfter, + size: 1_000, }; - const response = await this.esClient.search(query, { meta: true }); - tlog(this.logger, `received prebuilt alerts: (${response.body.hits.hits.length})`); + let response = null; + try { + response = await this.esClient.search(query); + const numOfHits = response?.hits.hits.length; + + if (numOfHits > 0) { + const lastHit = response?.hits.hits[numOfHits - 1]; + searchAfter = lastHit?.sort; + } + + fetchMore = numOfHits > 0 && numOfHits < 1_000; + } catch (e) { + tlog(this.logger, e); + fetchMore = false; + } + + if (response == null) { + return { + moreToFetch: false, + newPitId: pitId, + searchAfter, + alerts: [] as TelemetryEvent[], + }; + } - const telemetryEvents: TelemetryEvent[] = response.body.hits.hits.flatMap((h) => + const alerts: TelemetryEvent[] = response.hits.hits.flatMap((h) => h._source != null ? ([h._source] as TelemetryEvent[]) : [] ); - const aggregations = response.body?.aggregations as unknown as { - prebuilt_rule_alert_count: { value: number }; + if (response?.pit_id != null) { + newPitId = response?.pit_id; + } + + tlog(this.logger, `Prebuilt rule alerts to return: ${alerts.length}`); + + return { + moreToFetch: fetchMore, + newPitId, + searchAfter, + alerts, }; + } + + public async openPointInTime(indexPattern: string) { + if (this.esClient === undefined || this.esClient === null) { + throw Error('es client is unavailable: cannot retrieve pre-built rule alert batches'); + } - return { events: telemetryEvents, count: aggregations?.prebuilt_rule_alert_count.value ?? 0 }; + const keepAlive = '5m'; + const pitId: OpenPointInTimeResponse['id'] = ( + await this.esClient.openPointInTime({ + index: `${indexPattern}*`, + keep_alive: keepAlive, + }) + ).id; + + return pitId; + } + + public async closePointInTime(pitId: string) { + if (this.esClient === undefined || this.esClient === null) { + throw Error('es client is unavailable: cannot retrieve pre-built rule alert batches'); + } + + try { + await this.esClient.closePointInTime({ id: pitId }); + } catch (error) { + tlog(this.logger, `Error trying to close point in time: "${pitId}". Error is: "${error}"`); + } } async fetchTimelineAlerts(index: string, rangeFrom: string, rangeTo: string) { diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.test.ts index de5219a7f1fa2..479ceabd65a3b 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.test.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.test.ts @@ -42,11 +42,7 @@ describe('security telemetry - detection rule alerts task test', () => { testTaskExecutionPeriod ); expect(mockTelemetryReceiver.fetchDetectionRulesPackageVersion).toHaveBeenCalled(); - expect(mockTelemetryReceiver.fetchPrebuiltRuleAlerts).toHaveBeenCalled(); - expect(mockTelemetryEventsSender.getTelemetryUsageCluster).toHaveBeenCalled(); - expect(mockTelemetryEventsSender.getTelemetryUsageCluster()?.incrementCounter).toBeCalledTimes( - 1 - ); + expect(mockTelemetryReceiver.fetchPrebuiltRuleAlertsBatch).toHaveBeenCalled(); expect(mockTelemetryEventsSender.sendOnDemand).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.ts b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.ts index 0fdc6cf32a69c..0765d1d5bbdae 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.ts @@ -6,6 +6,7 @@ */ import type { Logger } from '@kbn/core/server'; +import type { SortResults } from '@elastic/elasticsearch/lib/api/types'; import type { ITelemetryEventsSender } from '../sender'; import type { ITelemetryReceiver } from '../receiver'; import type { ESClusterInfo, ESLicense, TelemetryEvent } from '../types'; @@ -15,12 +16,14 @@ import { batchTelemetryRecords, createTaskMetric, processK8sUsernames, tlog } fr import { copyAllowlistedFields, filterList } from '../filterlists'; export function createTelemetryPrebuiltRuleAlertsTaskConfig(maxTelemetryBatch: number) { + const taskVersion = '1.2.0'; + return { type: 'security:telemetry-prebuilt-rule-alerts', title: 'Security Solution - Prebuilt Rule and Elastic ML Alerts Telemetry', interval: '1h', - timeout: '5m', - version: '1.0.0', + timeout: '15m', + version: taskVersion, runTask: async ( taskId: string, logger: Logger, @@ -47,53 +50,62 @@ export function createTelemetryPrebuiltRuleAlertsTaskConfig(maxTelemetryBatch: n : ({} as ESLicense | undefined); const packageInfo = packageVersion.status === 'fulfilled' ? packageVersion.value : undefined; + const index = receiver.getAlertsIndex(); - const { events: telemetryEvents, count: totalPrebuiltAlertCount } = - await receiver.fetchPrebuiltRuleAlerts(); - - sender.getTelemetryUsageCluster()?.incrementCounter({ - counterName: 'telemetry_prebuilt_rule_alerts', - counterType: 'prebuilt_alert_count', - incrementBy: totalPrebuiltAlertCount, - }); - - if (telemetryEvents.length === 0) { - tlog(logger, 'no prebuilt rule alerts retrieved'); - await sender.sendOnDemand(TASK_METRICS_CHANNEL, [ - createTaskMetric(taskName, true, startTime), - ]); + if (index === undefined) { + tlog(logger, `alerts index is not ready yet, skipping telemetry task`); return 0; } - const processedAlerts = telemetryEvents.map( - (event: TelemetryEvent): TelemetryEvent => - copyAllowlistedFields(filterList.prebuiltRulesAlerts, event) - ); - - const sanitizedAlerts = processedAlerts.map( - (event: TelemetryEvent): TelemetryEvent => - processK8sUsernames(clusterInfo?.cluster_uuid, event) - ); - - const enrichedAlerts = sanitizedAlerts.map( - (event: TelemetryEvent): TelemetryEvent => ({ - ...event, - licence_id: licenseInfo?.uid, - cluster_uuid: clusterInfo?.cluster_uuid, - cluster_name: clusterInfo?.cluster_name, - package_version: packageInfo?.version, - }) - ); - - tlog(logger, `sending ${enrichedAlerts.length} elastic prebuilt alerts`); - const batches = batchTelemetryRecords(enrichedAlerts, maxTelemetryBatch); - for (const batch of batches) { - await sender.sendOnDemand(TELEMETRY_CHANNEL_DETECTION_ALERTS, batch); + let fetchMore = true; + let searchAfterValue: SortResults | undefined; + let pitId = await receiver.openPointInTime(index); + + while (fetchMore) { + const { moreToFetch, newPitId, searchAfter, alerts } = + await receiver.fetchPrebuiltRuleAlertsBatch(pitId, searchAfterValue); + + if (alerts.length === 0) { + return 0; + } + + fetchMore = moreToFetch; + searchAfterValue = searchAfter; + pitId = newPitId; + + const processedAlerts = alerts.map( + (event: TelemetryEvent): TelemetryEvent => + copyAllowlistedFields(filterList.prebuiltRulesAlerts, event) + ); + + const sanitizedAlerts = processedAlerts.map( + (event: TelemetryEvent): TelemetryEvent => + processK8sUsernames(clusterInfo?.cluster_uuid, event) + ); + + const enrichedAlerts = sanitizedAlerts.map( + (event: TelemetryEvent): TelemetryEvent => ({ + ...event, + licence_id: licenseInfo?.uid, + cluster_uuid: clusterInfo?.cluster_uuid, + cluster_name: clusterInfo?.cluster_name, + package_version: packageInfo?.version, + task_version: taskVersion, + }) + ); + + tlog(logger, `sending ${enrichedAlerts.length} elastic prebuilt alerts`); + const batches = batchTelemetryRecords(enrichedAlerts, maxTelemetryBatch); + + const promises = batches.map(async (batch) => { + sender.sendOnDemand(TELEMETRY_CHANNEL_DETECTION_ALERTS, batch); + }); + + await Promise.all(promises); } - await sender.sendOnDemand(TASK_METRICS_CHANNEL, [ - createTaskMetric(taskName, true, startTime), - ]); - return enrichedAlerts.length; + + await receiver.closePointInTime(pitId); + return 0; } catch (err) { logger.error('could not complete prebuilt alerts telemetry task'); await sender.sendOnDemand(TASK_METRICS_CHANNEL, [ diff --git a/x-pack/plugins/security_solution/server/routes/index.ts b/x-pack/plugins/security_solution/server/routes/index.ts index b5b6a5c205e95..d220851c48588 100644 --- a/x-pack/plugins/security_solution/server/routes/index.ts +++ b/x-pack/plugins/security_solution/server/routes/index.ts @@ -80,6 +80,7 @@ import { riskEngineInitRoute, riskEngineEnableRoute, riskEngineStatusRoute, + riskEnginePrivilegesRoute, } from '../lib/entity_analytics/risk_engine/routes'; import { riskScoreCalculationRoute } from '../lib/entity_analytics/risk_engine/routes/risk_score_calculation_route'; @@ -187,5 +188,8 @@ export const initRoutes = ( riskEngineInitRoute(router, getStartServices); riskEngineEnableRoute(router, getStartServices); riskEngineDisableRoute(router, getStartServices); + if (config.experimentalFeatures.riskEnginePrivilegesRouteEnabled) { + riskEnginePrivilegesRoute(router, getStartServices); + } } }; diff --git a/x-pack/plugins/transform/readme.md b/x-pack/plugins/transform/readme.md index 8c25f2ddd8ac4..e86d92340bf0c 100644 --- a/x-pack/plugins/transform/readme.md +++ b/x-pack/plugins/transform/readme.md @@ -151,4 +151,4 @@ With PATH_TO_CONFIG and other options as follows. node scripts/functional_tests_server --config test/accessibility/config.ts node scripts/functional_test_runner.js --config test/accessibility/config.ts --grep=transform - Transform accessibility tests are located in `x-pack/test/accessibility/apps`. + Transform accessibility tests are located in `x-pack/test/accessibility/apps/group2`. diff --git a/x-pack/test/accessibility/apps/advanced_settings.ts b/x-pack/test/accessibility/apps/group1/advanced_settings.ts similarity index 97% rename from x-pack/test/accessibility/apps/advanced_settings.ts rename to x-pack/test/accessibility/apps/group1/advanced_settings.ts index 6c931f0a0e5a1..44899932302ba 100644 --- a/x-pack/test/accessibility/apps/advanced_settings.ts +++ b/x-pack/test/accessibility/apps/group1/advanced_settings.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'settings', 'header']); diff --git a/x-pack/test/accessibility/apps/group1/config.ts b/x-pack/test/accessibility/apps/group1/config.ts new file mode 100644 index 0000000000000..8e5510141abf9 --- /dev/null +++ b/x-pack/test/accessibility/apps/group1/config.ts @@ -0,0 +1,29 @@ +/* + * 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'; +import { services } from '../../services'; +import { pageObjects } from '../../page_objects'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile( + require.resolve('../../../functional/config.base.js') + ); + + return { + ...functionalConfig.getAll(), + + testFiles: [require.resolve('.')], + + pageObjects, + services, + + junit: { + reportName: 'X-Pack Accessibility Tests - Group 1', + }, + }; +} diff --git a/x-pack/test/accessibility/apps/dashboard_controls.ts b/x-pack/test/accessibility/apps/group1/dashboard_controls.ts similarity index 98% rename from x-pack/test/accessibility/apps/dashboard_controls.ts rename to x-pack/test/accessibility/apps/group1/dashboard_controls.ts index 74f7288ccce9e..dd45b68674343 100644 --- a/x-pack/test/accessibility/apps/dashboard_controls.ts +++ b/x-pack/test/accessibility/apps/group1/dashboard_controls.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); diff --git a/x-pack/test/accessibility/apps/dashboard_panel_options.ts b/x-pack/test/accessibility/apps/group1/dashboard_panel_options.ts similarity index 97% rename from x-pack/test/accessibility/apps/dashboard_panel_options.ts rename to x-pack/test/accessibility/apps/group1/dashboard_panel_options.ts index 4e4dc3b218d79..5f12f3600c29a 100644 --- a/x-pack/test/accessibility/apps/dashboard_panel_options.ts +++ b/x-pack/test/accessibility/apps/group1/dashboard_panel_options.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; -import { WebElementWrapper } from '../../../../test/functional/services/lib/web_element_wrapper'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import type { WebElementWrapper } from '../../../../../test/functional/services/lib/web_element_wrapper'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); diff --git a/x-pack/test/accessibility/apps/grok_debugger.ts b/x-pack/test/accessibility/apps/group1/grok_debugger.ts similarity index 94% rename from x-pack/test/accessibility/apps/grok_debugger.ts rename to x-pack/test/accessibility/apps/group1/grok_debugger.ts index 8ee9114c7da0a..da630c6bed7b3 100644 --- a/x-pack/test/accessibility/apps/grok_debugger.ts +++ b/x-pack/test/accessibility/apps/group1/grok_debugger.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'security']); diff --git a/x-pack/test/accessibility/apps/helpers.ts b/x-pack/test/accessibility/apps/group1/helpers.ts similarity index 100% rename from x-pack/test/accessibility/apps/helpers.ts rename to x-pack/test/accessibility/apps/group1/helpers.ts diff --git a/x-pack/test/accessibility/apps/home.ts b/x-pack/test/accessibility/apps/group1/home.ts similarity index 97% rename from x-pack/test/accessibility/apps/home.ts rename to x-pack/test/accessibility/apps/group1/home.ts index 544a32843f7f3..800312bb4de5f 100644 --- a/x-pack/test/accessibility/apps/home.ts +++ b/x-pack/test/accessibility/apps/group1/home.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const { common, home } = getPageObjects(['common', 'home']); diff --git a/x-pack/test/accessibility/apps/group1/index.ts b/x-pack/test/accessibility/apps/group1/index.ts new file mode 100644 index 0000000000000..d8cd2e76c42dd --- /dev/null +++ b/x-pack/test/accessibility/apps/group1/index.ts @@ -0,0 +1,29 @@ +/* + * 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 { FtrProviderContext } from '../../../common/ftr_provider_context'; + +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('X-Pack Accessibility Tests - Group 1', function () { + loadTestFile(require.resolve('./login_page')); + loadTestFile(require.resolve('./kibana_overview')); + loadTestFile(require.resolve('./home')); + loadTestFile(require.resolve('./management')); + loadTestFile(require.resolve('./grok_debugger')); + loadTestFile(require.resolve('./search_profiler')); + loadTestFile(require.resolve('./painless_lab')); + loadTestFile(require.resolve('./uptime')); + loadTestFile(require.resolve('./spaces')); + loadTestFile(require.resolve('./advanced_settings')); + loadTestFile(require.resolve('./dashboard_panel_options')); + loadTestFile(require.resolve('./dashboard_controls')); + loadTestFile(require.resolve('./users')); + loadTestFile(require.resolve('./roles')); + loadTestFile(require.resolve('./ingest_node_pipelines')); + loadTestFile(require.resolve('./index_lifecycle_management')); + }); +}; diff --git a/x-pack/test/accessibility/apps/index_lifecycle_management.ts b/x-pack/test/accessibility/apps/group1/index_lifecycle_management.ts similarity index 98% rename from x-pack/test/accessibility/apps/index_lifecycle_management.ts rename to x-pack/test/accessibility/apps/group1/index_lifecycle_management.ts index fc3ec1ff5cf81..b994c4193e49f 100644 --- a/x-pack/test/accessibility/apps/index_lifecycle_management.ts +++ b/x-pack/test/accessibility/apps/group1/index_lifecycle_management.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; const REPO_NAME = 'test'; const POLICY_NAME = 'ilm-a11y-test'; diff --git a/x-pack/test/accessibility/apps/ingest_node_pipelines.ts b/x-pack/test/accessibility/apps/group1/ingest_node_pipelines.ts similarity index 100% rename from x-pack/test/accessibility/apps/ingest_node_pipelines.ts rename to x-pack/test/accessibility/apps/group1/ingest_node_pipelines.ts diff --git a/x-pack/test/accessibility/apps/kibana_overview.ts b/x-pack/test/accessibility/apps/group1/kibana_overview.ts similarity index 93% rename from x-pack/test/accessibility/apps/kibana_overview.ts rename to x-pack/test/accessibility/apps/group1/kibana_overview.ts index 373044c4bffc3..fe1bc55cd4c00 100644 --- a/x-pack/test/accessibility/apps/kibana_overview.ts +++ b/x-pack/test/accessibility/apps/group1/kibana_overview.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'home']); diff --git a/x-pack/test/accessibility/apps/login_page.ts b/x-pack/test/accessibility/apps/group1/login_page.ts similarity index 97% rename from x-pack/test/accessibility/apps/login_page.ts rename to x-pack/test/accessibility/apps/group1/login_page.ts index 3993d9ffcd72e..32cb825f86b33 100644 --- a/x-pack/test/accessibility/apps/login_page.ts +++ b/x-pack/test/accessibility/apps/group1/login_page.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); diff --git a/x-pack/test/accessibility/apps/management.ts b/x-pack/test/accessibility/apps/group1/management.ts similarity index 97% rename from x-pack/test/accessibility/apps/management.ts rename to x-pack/test/accessibility/apps/group1/management.ts index 2021642c2aa27..82c7baf8e830d 100644 --- a/x-pack/test/accessibility/apps/management.ts +++ b/x-pack/test/accessibility/apps/group1/management.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects([ diff --git a/x-pack/test/accessibility/apps/painless_lab.ts b/x-pack/test/accessibility/apps/group1/painless_lab.ts similarity index 97% rename from x-pack/test/accessibility/apps/painless_lab.ts rename to x-pack/test/accessibility/apps/group1/painless_lab.ts index a0a4712dbe4e3..522ffa9c7b238 100644 --- a/x-pack/test/accessibility/apps/painless_lab.ts +++ b/x-pack/test/accessibility/apps/group1/painless_lab.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'security']); diff --git a/x-pack/test/accessibility/apps/roles.ts b/x-pack/test/accessibility/apps/group1/roles.ts similarity index 98% rename from x-pack/test/accessibility/apps/roles.ts rename to x-pack/test/accessibility/apps/group1/roles.ts index 5369dced427fa..cf798bcb853f5 100644 --- a/x-pack/test/accessibility/apps/roles.ts +++ b/x-pack/test/accessibility/apps/group1/roles.ts @@ -7,7 +7,7 @@ // a11y tests for spaces, space selection and spacce creation and feature controls -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['security', 'settings']); diff --git a/x-pack/test/accessibility/apps/search_profiler.ts b/x-pack/test/accessibility/apps/group1/search_profiler.ts similarity index 97% rename from x-pack/test/accessibility/apps/search_profiler.ts rename to x-pack/test/accessibility/apps/group1/search_profiler.ts index 30043f8f4157f..522c5e4cf730e 100644 --- a/x-pack/test/accessibility/apps/search_profiler.ts +++ b/x-pack/test/accessibility/apps/group1/search_profiler.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'security']); diff --git a/x-pack/test/accessibility/apps/spaces.ts b/x-pack/test/accessibility/apps/group1/spaces.ts similarity index 98% rename from x-pack/test/accessibility/apps/spaces.ts rename to x-pack/test/accessibility/apps/group1/spaces.ts index 622b1b3cefd64..33616c3576b1d 100644 --- a/x-pack/test/accessibility/apps/spaces.ts +++ b/x-pack/test/accessibility/apps/group1/spaces.ts @@ -7,7 +7,7 @@ // a11y tests for spaces, space selection and space creation and feature controls -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'spaceSelector', 'home', 'header', 'security']); diff --git a/x-pack/test/accessibility/apps/uptime.ts b/x-pack/test/accessibility/apps/group1/uptime.ts similarity index 91% rename from x-pack/test/accessibility/apps/uptime.ts rename to x-pack/test/accessibility/apps/group1/uptime.ts index 49243c37fe730..3818ddb1061a2 100644 --- a/x-pack/test/accessibility/apps/uptime.ts +++ b/x-pack/test/accessibility/apps/group1/uptime.ts @@ -6,8 +6,8 @@ */ import moment from 'moment'; -import { FtrProviderContext } from '../ftr_provider_context'; -import { makeChecks } from '../../api_integration/apis/uptime/rest/helper/make_checks'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { makeChecks } from '../../../api_integration/apis/uptime/rest/helper/make_checks'; const A11Y_TEST_MONITOR_ID = 'a11yTestMonitor'; @@ -19,7 +19,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const es = getService('es'); const toasts = getService('toasts'); - describe('uptime Accessibility', () => { + // github.com/elastic/kibana/issues/153601 + describe.skip('uptime Accessibility', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/uptime/blank'); await makeChecks(es, A11Y_TEST_MONITOR_ID, 150, 1, 1000, { diff --git a/x-pack/test/accessibility/apps/users.ts b/x-pack/test/accessibility/apps/group1/users.ts similarity index 98% rename from x-pack/test/accessibility/apps/users.ts rename to x-pack/test/accessibility/apps/group1/users.ts index 6057b4d45bb09..e26e6a6f6a54f 100644 --- a/x-pack/test/accessibility/apps/users.ts +++ b/x-pack/test/accessibility/apps/group1/users.ts @@ -7,7 +7,7 @@ // a11y tests for spaces, space selection and spacce creation and feature controls -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['security', 'settings']); diff --git a/x-pack/test/accessibility/apps/group2/config.ts b/x-pack/test/accessibility/apps/group2/config.ts new file mode 100644 index 0000000000000..27cf620bc05c8 --- /dev/null +++ b/x-pack/test/accessibility/apps/group2/config.ts @@ -0,0 +1,29 @@ +/* + * 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'; +import { services } from '../../services'; +import { pageObjects } from '../../page_objects'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile( + require.resolve('../../../functional/config.base.js') + ); + + return { + ...functionalConfig.getAll(), + + testFiles: [require.resolve('.')], + + pageObjects, + services, + + junit: { + reportName: 'X-Pack Accessibility Tests - Group 2', + }, + }; +} diff --git a/x-pack/test/accessibility/apps/group2/index.ts b/x-pack/test/accessibility/apps/group2/index.ts new file mode 100644 index 0000000000000..2c6bf4e58a08b --- /dev/null +++ b/x-pack/test/accessibility/apps/group2/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('X-Pack Accessibility Tests - Group 2', function () { + loadTestFile(require.resolve('./ml')); + loadTestFile(require.resolve('./ml_anomaly_detection')); + loadTestFile(require.resolve('./transform')); + loadTestFile(require.resolve('./lens')); + }); +}; diff --git a/x-pack/test/accessibility/apps/lens.ts b/x-pack/test/accessibility/apps/group2/lens.ts similarity index 98% rename from x-pack/test/accessibility/apps/lens.ts rename to x-pack/test/accessibility/apps/group2/lens.ts index 1153d61d1fc68..860138fc77701 100644 --- a/x-pack/test/accessibility/apps/lens.ts +++ b/x-pack/test/accessibility/apps/group2/lens.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'visualize', 'timePicker', 'home', 'lens']); @@ -25,11 +25,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); after(async () => { - await PageObjects.common.navigateToApp('visualize'); - await listingTable.searchForItemWithName(lensChartName); - await listingTable.checkListingSelectAllCheckbox(); - await listingTable.clickDeleteSelected(); - await PageObjects.common.clickConfirmOnModal(); await esArchiver.unload('x-pack/test/functional/es_archives/logstash_functional'); await kibanaServer.importExport.unload( 'x-pack/test/functional/fixtures/kbn_archiver/lens/lens_basic.json' @@ -173,6 +168,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('saves lens chart', async () => { await PageObjects.lens.save(lensChartName); await a11y.testAppSnapshot(); + // delete newly created Lens + await PageObjects.common.navigateToApp('visualize'); + await listingTable.searchForItemWithName(lensChartName); + await listingTable.checkListingSelectAllCheckbox(); + await listingTable.clickDeleteSelected(); + await PageObjects.common.clickConfirmOnModal(); }); }); } diff --git a/x-pack/test/accessibility/apps/ml.ts b/x-pack/test/accessibility/apps/group2/ml.ts similarity index 98% rename from x-pack/test/accessibility/apps/ml.ts rename to x-pack/test/accessibility/apps/group2/ml.ts index 2a6021b7be78e..af9664b5e258a 100644 --- a/x-pack/test/accessibility/apps/ml.ts +++ b/x-pack/test/accessibility/apps/group2/ml.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const a11y = getService('a11y'); @@ -68,7 +68,7 @@ export default function ({ getService }: FtrProviderContext) { const dfaClassificationJobTrainingPercent = 30; const uploadFilePath = require.resolve( - '../../functional/apps/ml/data_visualizer/files_to_import/artificial_server_log' + '../../../functional/apps/ml/data_visualizer/files_to_import/artificial_server_log' ); before(async () => { diff --git a/x-pack/test/accessibility/apps/ml_anomaly_detection.ts b/x-pack/test/accessibility/apps/group2/ml_anomaly_detection.ts similarity index 99% rename from x-pack/test/accessibility/apps/ml_anomaly_detection.ts rename to x-pack/test/accessibility/apps/group2/ml_anomaly_detection.ts index 39860093871e5..33c7db9eb0b20 100644 --- a/x-pack/test/accessibility/apps/ml_anomaly_detection.ts +++ b/x-pack/test/accessibility/apps/group2/ml_anomaly_detection.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; interface Detector { identifier: string; diff --git a/x-pack/test/accessibility/apps/transform.ts b/x-pack/test/accessibility/apps/group2/transform.ts similarity index 99% rename from x-pack/test/accessibility/apps/transform.ts rename to x-pack/test/accessibility/apps/group2/transform.ts index b1228b6c7155a..2e28a7e75fb2a 100644 --- a/x-pack/test/accessibility/apps/transform.ts +++ b/x-pack/test/accessibility/apps/group2/transform.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const a11y = getService('a11y'); diff --git a/x-pack/test/accessibility/apps/canvas.ts b/x-pack/test/accessibility/apps/group3/canvas.ts similarity index 96% rename from x-pack/test/accessibility/apps/canvas.ts rename to x-pack/test/accessibility/apps/group3/canvas.ts index f3dfa9305fb95..fb6cc672e2ffa 100644 --- a/x-pack/test/accessibility/apps/canvas.ts +++ b/x-pack/test/accessibility/apps/group3/canvas.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); diff --git a/x-pack/test/accessibility/apps/group3/config.ts b/x-pack/test/accessibility/apps/group3/config.ts new file mode 100644 index 0000000000000..94f6c862b5396 --- /dev/null +++ b/x-pack/test/accessibility/apps/group3/config.ts @@ -0,0 +1,29 @@ +/* + * 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'; +import { services } from '../../services'; +import { pageObjects } from '../../page_objects'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile( + require.resolve('../../../functional/config.base.js') + ); + + return { + ...functionalConfig.getAll(), + + testFiles: [require.resolve('.')], + + pageObjects, + services, + + junit: { + reportName: 'X-Pack Accessibility Tests - Group 3', + }, + }; +} diff --git a/x-pack/test/accessibility/apps/cross_cluster_replication.ts b/x-pack/test/accessibility/apps/group3/cross_cluster_replication.ts similarity index 95% rename from x-pack/test/accessibility/apps/cross_cluster_replication.ts rename to x-pack/test/accessibility/apps/group3/cross_cluster_replication.ts index bc81770de9f4b..db5d70ac26d04 100644 --- a/x-pack/test/accessibility/apps/cross_cluster_replication.ts +++ b/x-pack/test/accessibility/apps/group3/cross_cluster_replication.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects([ @@ -20,7 +20,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const es = getService('es'); const retry = getService('retry'); - describe('cross cluster replication - a11y tests', async () => { + // github.com/elastic/kibana/issues/153599 + describe.skip('cross cluster replication - a11y tests', async () => { before(async () => { await PageObjects.common.navigateToApp('crossClusterReplication'); }); diff --git a/x-pack/test/accessibility/apps/enterprise_search.ts b/x-pack/test/accessibility/apps/group3/enterprise_search.ts similarity index 98% rename from x-pack/test/accessibility/apps/enterprise_search.ts rename to x-pack/test/accessibility/apps/group3/enterprise_search.ts index b610130d99e0e..a7b7724e43181 100644 --- a/x-pack/test/accessibility/apps/enterprise_search.ts +++ b/x-pack/test/accessibility/apps/group3/enterprise_search.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); diff --git a/x-pack/test/accessibility/apps/graph.ts b/x-pack/test/accessibility/apps/group3/graph.ts similarity index 98% rename from x-pack/test/accessibility/apps/graph.ts rename to x-pack/test/accessibility/apps/group3/graph.ts index 03ca3b2afbfe4..839ea50ce2779 100644 --- a/x-pack/test/accessibility/apps/graph.ts +++ b/x-pack/test/accessibility/apps/group3/graph.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); diff --git a/x-pack/test/accessibility/apps/group3/grok_debugger.ts b/x-pack/test/accessibility/apps/group3/grok_debugger.ts new file mode 100644 index 0000000000000..da630c6bed7b3 --- /dev/null +++ b/x-pack/test/accessibility/apps/group3/grok_debugger.ts @@ -0,0 +1,37 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'security']); + const a11y = getService('a11y'); + const grokDebugger = getService('grokDebugger'); + + // Fixes:https://github.com/elastic/kibana/issues/62102 + describe('Dev tools grok debugger', () => { + before(async () => { + await PageObjects.common.navigateToApp('grokDebugger'); + await grokDebugger.assertExists(); + }); + + it('Dev tools grok debugger set input', async () => { + await grokDebugger.setEventInput('SegerCommaBob'); + await a11y.testAppSnapshot(); + }); + + it('Dev tools grok debugger set pattern', async () => { + await grokDebugger.setPatternInput('%{USERNAME:u}'); + await a11y.testAppSnapshot(); + }); + + it('Dev tools grok debugger simulate', async () => { + await grokDebugger.clickSimulate(); + await a11y.testAppSnapshot(); + }); + }); +} diff --git a/x-pack/test/accessibility/apps/group3/index.ts b/x-pack/test/accessibility/apps/group3/index.ts new file mode 100644 index 0000000000000..d295c2a17a4f0 --- /dev/null +++ b/x-pack/test/accessibility/apps/group3/index.ts @@ -0,0 +1,35 @@ +/* + * 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 { FtrProviderContext } from '../../../common/ftr_provider_context'; + +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('X-Pack Accessibility Tests - Group 3', function () { + loadTestFile(require.resolve('./upgrade_assistant')); + loadTestFile(require.resolve('./canvas')); + loadTestFile(require.resolve('./maps')); + loadTestFile(require.resolve('./graph')); + loadTestFile(require.resolve('./security_solution')); + loadTestFile(require.resolve('./ml_embeddables_in_dashboard')); + loadTestFile(require.resolve('./rules_connectors')); + // Please make sure that the remote clusters, snapshot and restore and + // CCR tests stay in that order. Their execution fails if rearranged. + loadTestFile(require.resolve('./remote_clusters')); + loadTestFile(require.resolve('./snapshot_and_restore')); + loadTestFile(require.resolve('./cross_cluster_replication')); + loadTestFile(require.resolve('./reporting')); + loadTestFile(require.resolve('./enterprise_search')); + + // loadTestFile(require.resolve('./license_management')); + // loadTestFile(require.resolve('./tags')); + // loadTestFile(require.resolve('./search_sessions')); + // loadTestFile(require.resolve('./stack_monitoring')); + // loadTestFile(require.resolve('./watcher')); + // loadTestFile(require.resolve('./rollup_jobs')); + // loadTestFile(require.resolve('./observability')); + }); +}; diff --git a/x-pack/test/accessibility/apps/license_management.ts b/x-pack/test/accessibility/apps/group3/license_management.ts similarity index 95% rename from x-pack/test/accessibility/apps/license_management.ts rename to x-pack/test/accessibility/apps/group3/license_management.ts index 7693ebb197ff1..a71ac90f54ce8 100644 --- a/x-pack/test/accessibility/apps/license_management.ts +++ b/x-pack/test/accessibility/apps/group3/license_management.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['licenseManagement', 'common']); diff --git a/x-pack/test/accessibility/apps/maps.ts b/x-pack/test/accessibility/apps/group3/maps.ts similarity index 98% rename from x-pack/test/accessibility/apps/maps.ts rename to x-pack/test/accessibility/apps/group3/maps.ts index af74466fb8f24..2f69696b5824f 100644 --- a/x-pack/test/accessibility/apps/maps.ts +++ b/x-pack/test/accessibility/apps/group3/maps.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); diff --git a/x-pack/test/accessibility/apps/ml_embeddables_in_dashboard.ts b/x-pack/test/accessibility/apps/group3/ml_embeddables_in_dashboard.ts similarity index 98% rename from x-pack/test/accessibility/apps/ml_embeddables_in_dashboard.ts rename to x-pack/test/accessibility/apps/group3/ml_embeddables_in_dashboard.ts index 71933514de001..c3278b39096c7 100644 --- a/x-pack/test/accessibility/apps/ml_embeddables_in_dashboard.ts +++ b/x-pack/test/accessibility/apps/group3/ml_embeddables_in_dashboard.ts @@ -6,7 +6,7 @@ */ import { Datafeed, Job } from '@kbn/ml-plugin/common/types/anomaly_detection_jobs'; -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; // @ts-expect-error not full interface const JOB_CONFIG: Job = { diff --git a/x-pack/test/accessibility/apps/observability.ts b/x-pack/test/accessibility/apps/group3/observability.ts similarity index 95% rename from x-pack/test/accessibility/apps/observability.ts rename to x-pack/test/accessibility/apps/group3/observability.ts index ead89c913d1e1..1d24c1c17be24 100644 --- a/x-pack/test/accessibility/apps/observability.ts +++ b/x-pack/test/accessibility/apps/group3/observability.ts @@ -6,7 +6,7 @@ */ // a11y tests for spaces, space selection and space creation and feature controls -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'infraHome']); diff --git a/x-pack/test/accessibility/apps/remote_clusters.ts b/x-pack/test/accessibility/apps/group3/remote_clusters.ts similarity index 99% rename from x-pack/test/accessibility/apps/remote_clusters.ts rename to x-pack/test/accessibility/apps/group3/remote_clusters.ts index deb0e4a090b8c..4b509c88f0525 100644 --- a/x-pack/test/accessibility/apps/remote_clusters.ts +++ b/x-pack/test/accessibility/apps/group3/remote_clusters.ts @@ -6,7 +6,7 @@ */ import { ClusterPayloadEs } from '@kbn/remote-clusters-plugin/common/lib'; -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; const emptyPrompt = 'remoteClusterListEmptyPrompt'; const createButton = 'remoteClusterEmptyPromptCreateButton'; diff --git a/x-pack/test/accessibility/apps/reporting.ts b/x-pack/test/accessibility/apps/group3/reporting.ts similarity index 97% rename from x-pack/test/accessibility/apps/reporting.ts rename to x-pack/test/accessibility/apps/group3/reporting.ts index f1ac0770c9587..45959e42b383a 100644 --- a/x-pack/test/accessibility/apps/reporting.ts +++ b/x-pack/test/accessibility/apps/group3/reporting.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const { common } = getPageObjects(['common']); diff --git a/x-pack/test/accessibility/apps/rollup_jobs.ts b/x-pack/test/accessibility/apps/group3/rollup_jobs.ts similarity index 98% rename from x-pack/test/accessibility/apps/rollup_jobs.ts rename to x-pack/test/accessibility/apps/group3/rollup_jobs.ts index 2acf48d5f049f..5581a11955e18 100644 --- a/x-pack/test/accessibility/apps/rollup_jobs.ts +++ b/x-pack/test/accessibility/apps/group3/rollup_jobs.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'settings', 'header', 'rollup']); diff --git a/x-pack/test/accessibility/apps/rules_connectors.ts b/x-pack/test/accessibility/apps/group3/rules_connectors.ts similarity index 98% rename from x-pack/test/accessibility/apps/rules_connectors.ts rename to x-pack/test/accessibility/apps/group3/rules_connectors.ts index bc17793717282..bf46735a84fde 100644 --- a/x-pack/test/accessibility/apps/rules_connectors.ts +++ b/x-pack/test/accessibility/apps/group3/rules_connectors.ts @@ -7,7 +7,7 @@ // a11y tests for rules, logs and connectors page -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['settings', 'common']); diff --git a/x-pack/test/accessibility/apps/search_sessions.ts b/x-pack/test/accessibility/apps/group3/search_sessions.ts similarity index 98% rename from x-pack/test/accessibility/apps/search_sessions.ts rename to x-pack/test/accessibility/apps/group3/search_sessions.ts index 5a4ca433e3d21..c400d17263221 100644 --- a/x-pack/test/accessibility/apps/search_sessions.ts +++ b/x-pack/test/accessibility/apps/group3/search_sessions.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['searchSessionsManagement']); diff --git a/x-pack/test/accessibility/apps/security_solution.ts b/x-pack/test/accessibility/apps/group3/security_solution.ts similarity index 98% rename from x-pack/test/accessibility/apps/security_solution.ts rename to x-pack/test/accessibility/apps/group3/security_solution.ts index ba7d22fd2d39d..7257f88be8a74 100644 --- a/x-pack/test/accessibility/apps/security_solution.ts +++ b/x-pack/test/accessibility/apps/group3/security_solution.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); diff --git a/x-pack/test/accessibility/apps/snapshot_and_restore.ts b/x-pack/test/accessibility/apps/group3/snapshot_and_restore.ts similarity index 98% rename from x-pack/test/accessibility/apps/snapshot_and_restore.ts rename to x-pack/test/accessibility/apps/group3/snapshot_and_restore.ts index c5f0f52c9c9fe..4c3e5121f9830 100644 --- a/x-pack/test/accessibility/apps/snapshot_and_restore.ts +++ b/x-pack/test/accessibility/apps/group3/snapshot_and_restore.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'settings', 'header', 'snapshotRestore']); diff --git a/x-pack/test/accessibility/apps/stack_monitoring.ts b/x-pack/test/accessibility/apps/group3/stack_monitoring.ts similarity index 97% rename from x-pack/test/accessibility/apps/stack_monitoring.ts rename to x-pack/test/accessibility/apps/group3/stack_monitoring.ts index 87bd4d64d1fb8..64eaa652e702e 100644 --- a/x-pack/test/accessibility/apps/stack_monitoring.ts +++ b/x-pack/test/accessibility/apps/group3/stack_monitoring.ts @@ -7,7 +7,7 @@ // a11y tests for stack monitoring import expect from '@kbn/expect'; -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'spaceSelector', 'home', 'header', 'security']); diff --git a/x-pack/test/accessibility/apps/tags.ts b/x-pack/test/accessibility/apps/group3/tags.ts similarity index 98% rename from x-pack/test/accessibility/apps/tags.ts rename to x-pack/test/accessibility/apps/group3/tags.ts index 0c0f836cfc894..f885e8ba77c07 100644 --- a/x-pack/test/accessibility/apps/tags.ts +++ b/x-pack/test/accessibility/apps/group3/tags.ts @@ -7,7 +7,7 @@ // a11y tests for spaces, space selection and space creation and feature controls -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'settings', 'header', 'home', 'tagManagement']); diff --git a/x-pack/test/accessibility/apps/upgrade_assistant.ts b/x-pack/test/accessibility/apps/group3/upgrade_assistant.ts similarity index 99% rename from x-pack/test/accessibility/apps/upgrade_assistant.ts rename to x-pack/test/accessibility/apps/group3/upgrade_assistant.ts index 47582893e771f..02f823ef6a674 100644 --- a/x-pack/test/accessibility/apps/upgrade_assistant.ts +++ b/x-pack/test/accessibility/apps/group3/upgrade_assistant.ts @@ -11,7 +11,7 @@ */ import type { IndicesCreateRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; const translogSettingsIndexDeprecation: IndicesCreateRequest = { index: 'deprecated_settings', diff --git a/x-pack/test/accessibility/apps/watcher.ts b/x-pack/test/accessibility/apps/group3/watcher.ts similarity index 95% rename from x-pack/test/accessibility/apps/watcher.ts rename to x-pack/test/accessibility/apps/group3/watcher.ts index 85a11db0122ab..72b4e87e50660 100644 --- a/x-pack/test/accessibility/apps/watcher.ts +++ b/x-pack/test/accessibility/apps/group3/watcher.ts @@ -7,7 +7,7 @@ // a11y tests for spaces, space selection and space creation and feature controls -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'home', 'header', 'watcher', 'security']); diff --git a/x-pack/test/accessibility/config.ts b/x-pack/test/accessibility/config.ts deleted file mode 100644 index 1475b3aeff8af..0000000000000 --- a/x-pack/test/accessibility/config.ts +++ /dev/null @@ -1,70 +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 { FtrConfigProviderContext } from '@kbn/test'; -import { services } from './services'; -import { pageObjects } from './page_objects'; - -export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); - - return { - ...functionalConfig.getAll(), - - testFiles: [ - require.resolve('./apps/login_page'), - require.resolve('./apps/kibana_overview'), - require.resolve('./apps/home'), - require.resolve('./apps/management'), - require.resolve('./apps/grok_debugger'), - require.resolve('./apps/search_profiler'), - require.resolve('./apps/painless_lab'), - // https://github.com/elastic/kibana/issues/153601 - // require.resolve('./apps/uptime'), - require.resolve('./apps/spaces'), - require.resolve('./apps/advanced_settings'), - require.resolve('./apps/dashboard_panel_options'), - require.resolve('./apps/dashboard_controls'), - require.resolve('./apps/users'), - require.resolve('./apps/roles'), - require.resolve('./apps/ingest_node_pipelines'), - require.resolve('./apps/index_lifecycle_management'), - require.resolve('./apps/ml'), - require.resolve('./apps/ml_anomaly_detection'), - require.resolve('./apps/transform'), - require.resolve('./apps/lens'), - require.resolve('./apps/upgrade_assistant'), - require.resolve('./apps/canvas'), - require.resolve('./apps/maps'), - require.resolve('./apps/graph'), - require.resolve('./apps/security_solution'), - require.resolve('./apps/ml_embeddables_in_dashboard'), - require.resolve('./apps/rules_connectors'), - // Please make sure that the remote clusters, snapshot and restore and - // CCR tests stay in that order. Their execution fails if rearranged. - require.resolve('./apps/remote_clusters'), - require.resolve('./apps/snapshot_and_restore'), - require.resolve('./apps/cross_cluster_replication'), - require.resolve('./apps/reporting'), - require.resolve('./apps/enterprise_search'), - // require.resolve('./apps/license_management'), - // require.resolve('./apps/tags'), - // require.resolve('./apps/search_sessions'), - // require.resolve('./apps/stack_monitoring'), - // require.resolve('./apps/watcher'), - // require.resolve('./apps/rollup_jobs'), - // require.resolve('./apps/observability'), - ], - - pageObjects, - services, - - junit: { - reportName: 'X-Pack Accessibility Tests', - }, - }; -} diff --git a/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/documents_count_fired.ts b/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/documents_count_fired.ts index a602dc9012850..a1057e4ed6f74 100644 --- a/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/documents_count_fired.ts +++ b/x-pack/test/alerting_api_integration/observability/custom_threshold_rule/documents_count_fired.ts @@ -91,8 +91,8 @@ export default function ({ getService }: FtrProviderContext) { params: { criteria: [ { - comparator: Comparator.GT, - threshold: [2], + comparator: Comparator.OUTSIDE_RANGE, + threshold: [1, 2], timeSize: 1, timeUnit: 'm', metrics: [{ name: 'A', filter: '', aggType: Aggregators.COUNT }], @@ -186,8 +186,8 @@ export default function ({ getService }: FtrProviderContext) { .eql({ criteria: [ { - comparator: '>', - threshold: [2], + comparator: Comparator.OUTSIDE_RANGE, + threshold: [1, 2], timeSize: 1, timeUnit: 'm', metrics: [{ name: 'A', filter: '', aggType: 'count' }], @@ -211,7 +211,7 @@ export default function ({ getService }: FtrProviderContext) { `https://localhost:5601/app/observability/alerts?_a=(kuery:%27kibana.alert.uuid:%20%22${alertId}%22%27%2CrangeFrom:%27${rangeFrom}%27%2CrangeTo:now%2Cstatus:all)` ); expect(resp.hits.hits[0]._source?.reason).eql( - `Document count is 3, above the threshold of 2. (duration: 1 min, data view: ${DATE_VIEW_NAME})` + `Document count is 3, not between the threshold of 1 and 2. (duration: 1 min, data view: ${DATE_VIEW_NAME})` ); expect(resp.hits.hits[0]._source?.value).eql('3'); }); diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index 81cceb6561bd6..04a4177485348 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -22,8 +22,22 @@ export default function ({ getService }: FtrProviderContext) { savedObjectsTagging: ['all', 'read', 'minimal_all', 'minimal_read'], canvas: ['all', 'read', 'minimal_all', 'minimal_read'], maps: ['all', 'read', 'minimal_all', 'minimal_read'], - generalCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'], - observabilityCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'], + generalCases: [ + 'all', + 'read', + 'minimal_all', + 'minimal_read', + 'cases_delete', + 'cases_settings', + ], + observabilityCases: [ + 'all', + 'read', + 'minimal_all', + 'minimal_read', + 'cases_delete', + 'cases_settings', + ], observabilityAIAssistant: ['all', 'read', 'minimal_all', 'minimal_read'], slo: ['all', 'read', 'minimal_all', 'minimal_read'], fleetv2: ['all', 'read', 'minimal_all', 'minimal_read'], @@ -57,7 +71,14 @@ export default function ({ getService }: FtrProviderContext) { ], uptime: ['all', 'read', 'minimal_all', 'minimal_read', 'elastic_managed_locations_enabled'], securitySolutionAssistant: ['all', 'read', 'minimal_all', 'minimal_read'], - securitySolutionCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'], + securitySolutionCases: [ + 'all', + 'read', + 'minimal_all', + 'minimal_read', + 'cases_delete', + 'cases_settings', + ], infrastructure: ['all', 'read', 'minimal_all', 'minimal_read'], logs: ['all', 'read', 'minimal_all', 'minimal_read'], apm: ['all', 'read', 'minimal_all', 'minimal_read'], diff --git a/x-pack/test/api_integration/apis/security/privileges_basic.ts b/x-pack/test/api_integration/apis/security/privileges_basic.ts index 174ac2a3c8f66..2773adfe070e8 100644 --- a/x-pack/test/api_integration/apis/security/privileges_basic.ts +++ b/x-pack/test/api_integration/apis/security/privileges_basic.ts @@ -98,8 +98,22 @@ export default function ({ getService }: FtrProviderContext) { savedObjectsTagging: ['all', 'read', 'minimal_all', 'minimal_read'], canvas: ['all', 'read', 'minimal_all', 'minimal_read'], maps: ['all', 'read', 'minimal_all', 'minimal_read'], - generalCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'], - observabilityCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'], + generalCases: [ + 'all', + 'read', + 'minimal_all', + 'minimal_read', + 'cases_delete', + 'cases_settings', + ], + observabilityCases: [ + 'all', + 'read', + 'minimal_all', + 'minimal_read', + 'cases_delete', + 'cases_settings', + ], observabilityAIAssistant: ['all', 'read', 'minimal_all', 'minimal_read'], slo: ['all', 'read', 'minimal_all', 'minimal_read'], fleetv2: ['all', 'read', 'minimal_all', 'minimal_read'], @@ -139,7 +153,14 @@ export default function ({ getService }: FtrProviderContext) { 'minimal_read', ], securitySolutionAssistant: ['all', 'read', 'minimal_all', 'minimal_read'], - securitySolutionCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'], + securitySolutionCases: [ + 'all', + 'read', + 'minimal_all', + 'minimal_read', + 'cases_delete', + 'cases_settings', + ], infrastructure: ['all', 'read', 'minimal_all', 'minimal_read'], logs: ['all', 'read', 'minimal_all', 'minimal_read'], apm: ['all', 'read', 'minimal_all', 'minimal_read'], diff --git a/x-pack/test/cases_api_integration/common/plugins/security_solution/server/plugin.ts b/x-pack/test/cases_api_integration/common/plugins/security_solution/server/plugin.ts index 9648e89827568..3f46aa016811c 100644 --- a/x-pack/test/cases_api_integration/common/plugins/security_solution/server/plugin.ts +++ b/x-pack/test/cases_api_integration/common/plugins/security_solution/server/plugin.ts @@ -68,13 +68,13 @@ export class FixturePlugin implements Plugin hours * 60 * 60 * 1000; @@ -77,7 +77,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await findings.index.add(dataOldKspm); await findings.navigateToLatestFindingsPage(); - pageObjects.header.waitUntilLoadingHasFinished(); + await pageObjects.header.waitUntilLoadingHasFinished(); expect(await findings.isLatestFindingsTableThere()).to.be(false); }); it('returns no Findings CSPM', async () => { @@ -86,7 +86,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await findings.index.add(dataOldCspm); await findings.navigateToLatestFindingsPage(); - pageObjects.header.waitUntilLoadingHasFinished(); + await pageObjects.header.waitUntilLoadingHasFinished(); expect(await findings.isLatestFindingsTableThere()).to.be(false); }); }); diff --git a/x-pack/test/functional_with_es_ssl/plugins/cases/public/application.tsx b/x-pack/test/functional_with_es_ssl/plugins/cases/public/application.tsx index 851134d346d9a..af4fb983124e8 100644 --- a/x-pack/test/functional_with_es_ssl/plugins/cases/public/application.tsx +++ b/x-pack/test/functional_with_es_ssl/plugins/cases/public/application.tsx @@ -43,6 +43,7 @@ const permissions = { delete: true, push: true, connectors: true, + settings: true, }; const attachments = [{ type: AttachmentType.user as const, comment: 'test' }]; diff --git a/x-pack/test/security_solution_api_integration/config/ess/config.base.ts b/x-pack/test/security_solution_api_integration/config/ess/config.base.ts index b4fbdba6de4c4..185b55e491def 100644 --- a/x-pack/test/security_solution_api_integration/config/ess/config.base.ts +++ b/x-pack/test/security_solution_api_integration/config/ess/config.base.ts @@ -81,6 +81,7 @@ export function createTestConfig(options: CreateTestConfigOptions, testFiles?: s 'previewTelemetryUrlEnabled', 'riskScoringPersistence', 'riskScoringRoutesEnabled', + 'riskEnginePrivilegesRouteEnabled', ])}`, '--xpack.task_manager.poll_interval=1000', `--xpack.actions.preconfigured=${JSON.stringify({ diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/default_license/risk_engine/index.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/default_license/risk_engine/index.ts index 878725cd32f9a..5106151cb8c18 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/default_license/risk_engine/index.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/default_license/risk_engine/index.ts @@ -15,5 +15,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./risk_scoring_task/task_execution')); loadTestFile(require.resolve('./risk_scoring_task/task_execution_nondefault_spaces')); loadTestFile(require.resolve('./telemetry_usage')); + loadTestFile(require.resolve('./risk_engine_privileges')); }); } diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/default_license/risk_engine/risk_engine_privileges.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/default_license/risk_engine/risk_engine_privileges.ts new file mode 100644 index 0000000000000..d0cc241b559f5 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/default_license/risk_engine/risk_engine_privileges.ts @@ -0,0 +1,234 @@ +/* + * 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 type { SecurityService } from '../../../../../../../test/common/services/security/security'; +import { riskEngineRouteHelpersFactoryNoAuth } from '../../utils'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +const USER_PASSWORD = 'changeme'; +const ROLES = [ + { + name: 'security_feature_read', + privileges: { + kibana: [ + { + feature: { + siem: ['read'], + }, + spaces: ['default'], + }, + ], + }, + }, + { + name: 'cluster_manage_index_templates', + privileges: { + elasticsearch: { + cluster: ['manage_index_templates'], + }, + }, + }, + { + name: 'cluster_manage_transform', + privileges: { + elasticsearch: { + cluster: ['manage_transform'], + }, + }, + }, + { + name: 'risk_score_index_read', + privileges: { + elasticsearch: { + indices: [ + { + names: ['risk-score.risk-score-*'], + privileges: ['read'], + }, + ], + }, + }, + }, + { + name: 'risk_score_index_write', + privileges: { + elasticsearch: { + indices: [ + { + names: ['risk-score.risk-score-*'], + privileges: ['write'], + }, + ], + }, + }, + }, +]; + +const ALL_ROLE_NAMES = ROLES.map((role) => role.name); + +const allRolesExcept = (role: string) => ALL_ROLE_NAMES.filter((r) => r !== role); + +const USERNAME_TO_ROLES = { + no_cluster_manage_index_templates: allRolesExcept('cluster_manage_index_templates'), + no_cluster_manage_transform: allRolesExcept('cluster_manage_transform'), + no_risk_score_index_read: allRolesExcept('risk_score_index_read'), + no_risk_score_index_write: allRolesExcept('risk_score_index_write'), + all: ALL_ROLE_NAMES, +}; + +export default ({ getService }: FtrProviderContext) => { + describe('@ess privileges_apis', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const riskEngineRoutesNoAuth = riskEngineRouteHelpersFactoryNoAuth(supertestWithoutAuth); + const logger = getService('log'); + let security: SecurityService; + try { + security = getService('security'); + } catch (e) { + // even though this test doesn't have the @serverless tag I cannot get it to stop running + // with serverless config. This is a hack to skip the test if security service is not available + logger.info( + 'Skipping privileges test as security service not available (likely run with serverless config)' + ); + return; + } + + const createRole = async ({ name, privileges }: { name: string; privileges: any }) => { + return await security.role.create(name, privileges); + }; + + const createUser = async ({ + username, + password, + roles, + }: { + username: string; + password: string; + roles: string[]; + }) => { + return await security.user.create(username, { + password, + roles, + full_name: username.replace('_', ' '), + email: `${username}@elastic.co`, + }); + }; + + async function createPrivilegeTestUsers() { + const rolePromises = ROLES.map((role) => createRole(role)); + + await Promise.all(rolePromises); + const userPromises = Object.entries(USERNAME_TO_ROLES).map(([username, roles]) => + createUser({ username, roles, password: USER_PASSWORD }) + ); + + return Promise.all(userPromises); + } + + const getPrivilegesForUsername = async (username: string) => + riskEngineRoutesNoAuth.privilegesForUser({ + username, + password: USER_PASSWORD, + }); + before(async () => { + await createPrivilegeTestUsers(); + }); + + describe('Risk engine privileges API', () => { + it('should return has_all_required true for user with all risk engine privileges', async () => { + const { body } = await getPrivilegesForUsername('all'); + expect(body.has_all_required).to.eql(true); + expect(body.privileges).to.eql({ + elasticsearch: { + cluster: { + manage_index_templates: true, + manage_transform: true, + }, + index: { + 'risk-score.risk-score-*': { + read: true, + write: true, + }, + }, + }, + }); + }); + it('should return has_all_required false for user with no write access to risk indices', async () => { + const { body } = await getPrivilegesForUsername('no_risk_score_index_write'); + expect(body.has_all_required).to.eql(false); + expect(body.privileges).to.eql({ + elasticsearch: { + cluster: { + manage_index_templates: true, + manage_transform: true, + }, + index: { + 'risk-score.risk-score-*': { + read: true, + write: false, + }, + }, + }, + }); + }); + it('should return has_all_required false for user with no read access to risk indices', async () => { + const { body } = await getPrivilegesForUsername('no_risk_score_index_read'); + expect(body.has_all_required).to.eql(false); + expect(body.privileges).to.eql({ + elasticsearch: { + cluster: { + manage_index_templates: true, + manage_transform: true, + }, + index: { + 'risk-score.risk-score-*': { + read: false, + write: true, + }, + }, + }, + }); + }); + it('should return has_all_required false for user with no cluster manage transform privilege', async () => { + const { body } = await getPrivilegesForUsername('no_cluster_manage_transform'); + expect(body.has_all_required).to.eql(false); + expect(body.privileges).to.eql({ + elasticsearch: { + cluster: { + manage_index_templates: true, + manage_transform: false, + }, + index: { + 'risk-score.risk-score-*': { + read: true, + write: true, + }, + }, + }, + }); + }); + it('should return has_all_required false for user with no cluster manage index templates privilege', async () => { + const { body } = await getPrivilegesForUsername('no_cluster_manage_index_templates'); + expect(body.has_all_required).to.eql(false); + expect(body.privileges).to.eql({ + elasticsearch: { + cluster: { + manage_index_templates: false, + manage_transform: true, + }, + index: { + 'risk-score.risk-score-*': { + read: true, + write: true, + }, + }, + }, + }); + }); + }); + }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/risk_engine.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/risk_engine.ts index 48c7763d7a9d6..103577482a771 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/risk_engine.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/risk_engine.ts @@ -21,6 +21,7 @@ import { RISK_ENGINE_DISABLE_URL, RISK_ENGINE_ENABLE_URL, RISK_ENGINE_STATUS_URL, + RISK_ENGINE_PRIVILEGES_URL, } from '@kbn/security-solution-plugin/common/constants'; import { createRule, @@ -504,6 +505,20 @@ export const riskEngineRouteHelpersFactory = ( .expect(200), }); +export const riskEngineRouteHelpersFactoryNoAuth = ( + supertestWithoutAuth: SuperTest.SuperTest, + namespace?: string +) => ({ + privilegesForUser: async ({ username, password }: { username: string; password: string }) => + await supertestWithoutAuth + .get(RISK_ENGINE_PRIVILEGES_URL) + .auth(username, password) + .set('elastic-api-version', '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send() + .expect(200), +}); + export const installLegacyRiskScore = async ({ supertest, }: { diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/entity_analytics_management_page_privileges_callout.ts b/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/entity_analytics_management_page_privileges_callout.ts new file mode 100644 index 0000000000000..a2d2b095d1b38 --- /dev/null +++ b/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/entity_analytics_management_page_privileges_callout.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 { ROLES } from '@kbn/security-solution-plugin/common/test'; +import { RISK_ENGINE_PRIVILEGES_URL } from '@kbn/security-solution-plugin/common/constants'; +import { + RISK_SCORE_PRIVILEGES_CALLOUT, + RISK_SCORE_STATUS_LOADING, +} from '../../screens/entity_analytics_management'; + +import { login } from '../../tasks/login'; +import { visit } from '../../tasks/navigation'; +import { ENTITY_ANALYTICS_MANAGEMENT_URL } from '../../urls/navigation'; + +const loadPageAsUserWithNoPrivileges = () => { + login(ROLES.no_risk_engine_privileges); + visit(ENTITY_ANALYTICS_MANAGEMENT_URL, { role: ROLES.no_risk_engine_privileges }); +}; + +// this test suite doesn't run on serverless because it requires a custom role +describe( + 'Entity analytics management page - Risk Engine Privileges Callout', + { + tags: ['@ess'], + env: { + ftrConfig: { enableExperimental: ['riskEnginePrivilegesRouteEnabled'] }, + }, + }, + () => { + it('should not show the callout for superuser', () => { + cy.intercept(RISK_ENGINE_PRIVILEGES_URL).as('getPrivileges'); + login(); + visit(ENTITY_ANALYTICS_MANAGEMENT_URL); + cy.wait('@getPrivileges', { timeout: 15000 }); + cy.get(RISK_SCORE_STATUS_LOADING).should('not.exist'); + cy.get(RISK_SCORE_PRIVILEGES_CALLOUT).should('not.exist'); + }); + + it('should show the callout for user without risk engine privileges', () => { + cy.intercept(RISK_ENGINE_PRIVILEGES_URL).as('getPrivileges'); + loadPageAsUserWithNoPrivileges(); + cy.get(RISK_SCORE_STATUS_LOADING).should('not.exist'); + cy.wait('@getPrivileges', { timeout: 15000 }); + cy.get(RISK_SCORE_PRIVILEGES_CALLOUT); + cy.get(RISK_SCORE_PRIVILEGES_CALLOUT).should( + 'contain', + 'Missing read, write privileges for the risk-score.risk-score-* index.' + ); + cy.get(RISK_SCORE_PRIVILEGES_CALLOUT).should('contain', 'manage_index_templates'); + cy.get(RISK_SCORE_PRIVILEGES_CALLOUT).should('contain', 'manage_transform'); + }); + } +); diff --git a/x-pack/test/security_solution_cypress/cypress/screens/entity_analytics_management.ts b/x-pack/test/security_solution_cypress/cypress/screens/entity_analytics_management.ts index ebdabcb67bb1e..2c025efcbe79a 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/entity_analytics_management.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/entity_analytics_management.ts @@ -33,4 +33,9 @@ export const RISK_SCORE_UPDATE_BUTTON = '[data-test-subj="risk-score-update-butt export const RISK_SCORE_STATUS = '[data-test-subj="risk-score-status"]'; +export const RISK_SCORE_STATUS_LOADING = '[data-test-subj="risk-score-status-loading"]'; + +export const RISK_SCORE_PRIVILEGES_CALLOUT = + '[data-test-subj="callout-missing-risk-engine-privileges"]'; + export const RISK_SCORE_SWITCH = '[data-test-subj="risk-score-switch"]'; diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/documents_count_fired.ts b/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/documents_count_fired.ts index 01f04b12f0c50..453da41b81196 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/documents_count_fired.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/custom_threshold_rule/documents_count_fired.ts @@ -84,8 +84,8 @@ export default function ({ getService }: FtrProviderContext) { params: { criteria: [ { - comparator: Comparator.GT, - threshold: [2], + comparator: Comparator.OUTSIDE_RANGE, + threshold: [1, 2], timeSize: 1, timeUnit: 'm', metrics: [{ name: 'A', filter: '', aggType: Aggregators.COUNT }], @@ -176,8 +176,8 @@ export default function ({ getService }: FtrProviderContext) { .eql({ criteria: [ { - comparator: '>', - threshold: [2], + comparator: Comparator.OUTSIDE_RANGE, + threshold: [1, 2], timeSize: 1, timeUnit: 'm', metrics: [{ name: 'A', filter: '', aggType: 'count' }], diff --git a/yarn.lock b/yarn.lock index 5cec3b14b8c11..2bc0c40e3a415 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3145,6 +3145,10 @@ version "0.0.0" uid "" +"@kbn/calculate-width-from-char-count@link:packages/kbn-calculate-width-from-char-count": + version "0.0.0" + uid "" + "@kbn/canvas-plugin@link:x-pack/plugins/canvas": version "0.0.0" uid ""