From e6e30c20215ce7cbb8bd25d6646edc5d0a8bc33e Mon Sep 17 00:00:00 2001 From: "Eyo O. Eyo" <7893459+eokoneyo@users.noreply.github.com> Date: Wed, 16 Oct 2024 11:02:51 +0100 Subject: [PATCH 01/18] [Spaces] Read Security license to infer eligibility for sub feature customization (#195389) ## Summary Closes https://github.com/elastic/kibana/issues/195549 This PR adds implementation such that eligibility to allow for the toggling of the switch for customization of sub features whilst defining privileges that would be assigned to a space is determined from security license. ### Before ![ScreenRecording2024-10-09at10 09 33-ezgif com-video-to-gif-converter](https://github.com/user-attachments/assets/c80761c9-a45e-4784-835e-e6895d2fbed5) ### After ![ScreenRecording2024-10-09at10 05 53-ezgif com-video-to-gif-converter](https://github.com/user-attachments/assets/4e7d5724-42b0-4495-8fae-b47e7a97957c) --- .../security/plugin_types_public/index.ts | 1 + .../plugin_types_public/src/license/index.ts | 10 ++ .../edit_space_content_tab.test.tsx | 8 +- .../edit_space_general_tab.test.tsx | 8 +- .../management/edit_space/edit_space_page.tsx | 8 +- .../edit_space/edit_space_roles_tab.test.tsx | 8 +- .../edit_space/edit_space_roles_tab.tsx | 5 +- .../provider/edit_space_provider.test.tsx | 12 +- .../provider/edit_space_provider.tsx | 73 +++++++-- .../management/edit_space/provider/index.ts | 9 +- .../space_assign_role_privilege_form.test.tsx | 147 ++++++++++++++++-- .../space_assign_role_privilege_form.tsx | 38 ++--- .../management/management_service.test.ts | 4 + .../public/management/management_service.tsx | 27 +--- .../management/security_license.mock.ts | 49 ++++++ .../management/spaces_management_app.test.tsx | 2 + .../management/spaces_management_app.tsx | 6 +- x-pack/plugins/spaces/public/plugin.tsx | 15 +- 18 files changed, 334 insertions(+), 96 deletions(-) create mode 100644 x-pack/packages/security/plugin_types_public/src/license/index.ts create mode 100644 x-pack/plugins/spaces/public/management/security_license.mock.ts diff --git a/x-pack/packages/security/plugin_types_public/index.ts b/x-pack/packages/security/plugin_types_public/index.ts index a48511441382a..fc8829ad8a5f8 100644 --- a/x-pack/packages/security/plugin_types_public/index.ts +++ b/x-pack/packages/security/plugin_types_public/index.ts @@ -24,3 +24,4 @@ export type { } from './src/roles'; export { PrivilegesAPIClientPublicContract } from './src/privileges'; export type { PrivilegesAPIClientGetAllArgs } from './src/privileges'; +export type { SecurityLicense } from './src/license'; diff --git a/x-pack/packages/security/plugin_types_public/src/license/index.ts b/x-pack/packages/security/plugin_types_public/src/license/index.ts new file mode 100644 index 0000000000000..0c1ec0431c10a --- /dev/null +++ b/x-pack/packages/security/plugin_types_public/src/license/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SecurityPluginSetup } from '../plugin'; + +export type SecurityLicense = SecurityPluginSetup['license']; diff --git a/x-pack/plugins/spaces/public/management/edit_space/edit_space_content_tab.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/edit_space_content_tab.test.tsx index bb55cea5cd50f..f586f0d7f035e 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/edit_space_content_tab.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/edit_space_content_tab.test.tsx @@ -19,12 +19,13 @@ import { import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { EditSpaceContentTab } from './edit_space_content_tab'; -import { EditSpaceProvider } from './provider'; +import { EditSpaceProviderRoot } from './provider'; import type { Space } from '../../../common'; import { spacesManagerMock } from '../../spaces_manager/spaces_manager.mock'; import type { SpaceContentTypeSummaryItem } from '../../types'; import { getPrivilegeAPIClientMock } from '../privilege_api_client.mock'; import { getRolesAPIClientMock } from '../roles_api_client.mock'; +import { getSecurityLicenseMock } from '../security_license.mock'; const getUrlForApp = (appId: string) => appId; const navigateToUrl = jest.fn(); @@ -42,7 +43,7 @@ const logger = loggingSystemMock.createLogger(); const TestComponent: React.FC = ({ children }) => { return ( - = ({ children }) => { notifications={notifications} overlays={overlays} getPrivilegesAPIClient={getPrivilegeAPIClient} + getSecurityLicense={getSecurityLicenseMock} theme={theme} i18n={i18n} logger={logger} > {children} - + ); }; diff --git a/x-pack/plugins/spaces/public/management/edit_space/edit_space_general_tab.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/edit_space_general_tab.test.tsx index 1c32b97f777c0..9a35572254340 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/edit_space_general_tab.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/edit_space_general_tab.test.tsx @@ -23,12 +23,13 @@ import { KibanaFeature } from '@kbn/features-plugin/common'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { EditSpaceSettingsTab } from './edit_space_general_tab'; -import { EditSpaceProvider } from './provider/edit_space_provider'; +import { EditSpaceProviderRoot } from './provider/edit_space_provider'; import type { SolutionView } from '../../../common'; import { SOLUTION_VIEW_CLASSIC } from '../../../common/constants'; import { spacesManagerMock } from '../../spaces_manager/spaces_manager.mock'; import { getPrivilegeAPIClientMock } from '../privilege_api_client.mock'; import { getRolesAPIClientMock } from '../roles_api_client.mock'; +import { getSecurityLicenseMock } from '../security_license.mock'; const space = { id: 'default', name: 'Default', disabledFeatures: [], _reserved: true }; const history = scopedHistoryMock.create(); @@ -64,7 +65,7 @@ describe('EditSpaceSettings', () => { const TestComponent: React.FC = ({ children }) => { return ( - { notifications={notifications} overlays={overlays} getPrivilegesAPIClient={getPrivilegeAPIClient} + getSecurityLicense={getSecurityLicenseMock} theme={theme} i18n={i18n} logger={logger} > {children} - + ); }; diff --git a/x-pack/plugins/spaces/public/management/edit_space/edit_space_page.tsx b/x-pack/plugins/spaces/public/management/edit_space/edit_space_page.tsx index 882301d36459a..bf59f00b5490d 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/edit_space_page.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/edit_space_page.tsx @@ -9,9 +9,9 @@ import React from 'react'; import type { ComponentProps, PropsWithChildren } from 'react'; import { EditSpace } from './edit_space'; -import { EditSpaceProvider, type EditSpaceProviderProps } from './provider'; +import { EditSpaceProviderRoot, type EditSpaceProviderRootProps } from './provider'; -type EditSpacePageProps = ComponentProps & EditSpaceProviderProps; +type EditSpacePageProps = ComponentProps & EditSpaceProviderRootProps; export function EditSpacePage({ spaceId, @@ -25,7 +25,7 @@ export function EditSpacePage({ ...editSpaceServicesProps }: PropsWithChildren) { return ( - + - + ); } diff --git a/x-pack/plugins/spaces/public/management/edit_space/edit_space_roles_tab.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/edit_space_roles_tab.test.tsx index fccd999eb7941..1959d1d8465ac 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/edit_space_roles_tab.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/edit_space_roles_tab.test.tsx @@ -19,10 +19,11 @@ import { import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { EditSpaceAssignedRolesTab } from './edit_space_roles_tab'; -import { EditSpaceProvider } from './provider'; +import { EditSpaceProviderRoot } from './provider'; import { spacesManagerMock } from '../../spaces_manager/spaces_manager.mock'; import { getPrivilegeAPIClientMock } from '../privilege_api_client.mock'; import { getRolesAPIClientMock } from '../roles_api_client.mock'; +import { getSecurityLicenseMock } from '../security_license.mock'; const getUrlForApp = (appId: string) => appId; const navigateToUrl = jest.fn(); @@ -51,7 +52,7 @@ describe('EditSpaceAssignedRolesTab', () => { const TestComponent: React.FC = ({ children }) => { return ( - { notifications={notifications} overlays={overlays} getPrivilegesAPIClient={getPrivilegeAPIClient} + getSecurityLicense={getSecurityLicenseMock} theme={theme} i18n={i18n} logger={logger} > {children} - + ); }; diff --git a/x-pack/plugins/spaces/public/management/edit_space/edit_space_roles_tab.tsx b/x-pack/plugins/spaces/public/management/edit_space/edit_space_roles_tab.tsx index 2733790d8de8b..2e3d40527dbd7 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/edit_space_roles_tab.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/edit_space_roles_tab.tsx @@ -62,7 +62,7 @@ export const EditSpaceAssignedRolesTab: FC = ({ space, features, isReadOn (defaultSelected?: Role[]) => { const overlayRef = overlays.openFlyout( toMountPoint( - + = ({ space, features, isReadOn [ overlays, services, + dispatch, + state, space, features, - dispatch, invokeClient, getUrlForApp, theme, diff --git a/x-pack/plugins/spaces/public/management/edit_space/provider/edit_space_provider.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/provider/edit_space_provider.test.tsx index bfd7d7b6059e8..a236b9bc05e1d 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/provider/edit_space_provider.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/provider/edit_space_provider.test.tsx @@ -20,10 +20,15 @@ import { import type { ApplicationStart } from '@kbn/core-application-browser'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; -import { EditSpaceProvider, useEditSpaceServices, useEditSpaceStore } from './edit_space_provider'; +import { + EditSpaceProviderRoot, + useEditSpaceServices, + useEditSpaceStore, +} from './edit_space_provider'; import { spacesManagerMock } from '../../../spaces_manager/spaces_manager.mock'; import { getPrivilegeAPIClientMock } from '../../privilege_api_client.mock'; import { getRolesAPIClientMock } from '../../roles_api_client.mock'; +import { getSecurityLicenseMock } from '../../security_license.mock'; const http = httpServiceMock.createStartContract(); const notifications = notificationServiceMock.createStartContract(); @@ -45,7 +50,7 @@ const SUTProvider = ({ }: PropsWithChildren>>) => { return ( - _, getRolesAPIClient: getRolesAPIClientMock, getPrivilegesAPIClient: getPrivilegeAPIClientMock, + getSecurityLicense: getSecurityLicenseMock, navigateToUrl: jest.fn(), capabilities, }} > {children} - + ); }; diff --git a/x-pack/plugins/spaces/public/management/edit_space/provider/edit_space_provider.tsx b/x-pack/plugins/spaces/public/management/edit_space/provider/edit_space_provider.tsx index 75af2beea2108..374d90d19ace1 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/provider/edit_space_provider.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/provider/edit_space_provider.tsx @@ -23,6 +23,7 @@ import type { Logger } from '@kbn/logging'; import type { PrivilegesAPIClientPublicContract, RolesAPIClient, + SecurityLicense, } from '@kbn/security-plugin-types-public'; import { @@ -32,7 +33,7 @@ import { } from './reducers'; import type { SpacesManager } from '../../../spaces_manager'; -export interface EditSpaceProviderProps +export interface EditSpaceProviderRootProps extends Pick { logger: Logger; capabilities: ApplicationStart['capabilities']; @@ -42,10 +43,7 @@ export interface EditSpaceProviderProps spacesManager: SpacesManager; getRolesAPIClient: () => Promise; getPrivilegesAPIClient: () => Promise; -} - -export interface EditSpaceServices extends EditSpaceProviderProps { - invokeClient(arg: (clients: EditSpaceClients) => Promise): Promise; + getSecurityLicense: () => Promise; } interface EditSpaceClients { @@ -54,6 +52,15 @@ interface EditSpaceClients { privilegesClient: PrivilegesAPIClientPublicContract; } +export interface EditSpaceServices + extends Omit< + EditSpaceProviderRootProps, + 'getRolesAPIClient' | 'getPrivilegesAPIClient' | 'getSecurityLicense' + > { + invokeClient(arg: (clients: EditSpaceClients) => Promise): Promise; + license?: SecurityLicense; +} + export interface EditSpaceStore { state: IEditSpaceStoreState; dispatch: Dispatch; @@ -63,16 +70,43 @@ const createSpaceRolesContext = once(() => createContext( const createEditSpaceServicesContext = once(() => createContext(null)); +/** + * + * @description EditSpaceProvider is a provider component that wraps the children components with the necessary context providers for the Edit Space feature. It provides the necessary services and state management for the feature, + * this is provided as an export for use with out of band renders within the spaces app + */ export const EditSpaceProvider = ({ children, + state, + dispatch, ...services -}: PropsWithChildren) => { +}: PropsWithChildren) => { const EditSpaceStoreContext = createSpaceRolesContext(); const EditSpaceServicesContext = createEditSpaceServicesContext(); - const clients = useRef( - Promise.all([services.getRolesAPIClient(), services.getPrivilegesAPIClient()]) + return ( + + + {children} + + ); +}; + +/** + * @description EditSpaceProviderRoot is the root provider for the Edit Space feature. It instantiates the necessary services and state management for the feature. It ideally + * should only be rendered once + */ +export const EditSpaceProviderRoot = ({ + children, + ...services +}: PropsWithChildren) => { + const { logger, getRolesAPIClient, getPrivilegesAPIClient, getSecurityLicense } = services; + + const clients = useRef(Promise.all([getRolesAPIClient(), getPrivilegesAPIClient()])); + const license = useRef(getSecurityLicense); + + const licenseRef = useRef(); const rolesAPIClientRef = useRef(); const privilegesClientRef = useRef(); @@ -81,7 +115,14 @@ export const EditSpaceProvider = ({ fetchRolesError: false, }); - const { logger } = services; + const resolveSecurityLicense = useCallback(async () => { + try { + licenseRef.current = await license.current(); + } catch (err) { + logger.error('Could not resolve Security License!', err); + } + }, [logger]); + const resolveAPIClients = useCallback(async () => { try { [rolesAPIClientRef.current, privilegesClientRef.current] = await clients.current; @@ -94,6 +135,10 @@ export const EditSpaceProvider = ({ resolveAPIClients(); }, [resolveAPIClients]); + useEffect(() => { + resolveSecurityLicense(); + }, [resolveSecurityLicense]); + const createInitialState = useCallback((state: IEditSpaceStoreState) => { return state; }, []); @@ -118,11 +163,11 @@ export const EditSpaceProvider = ({ ); return ( - - - {children} - - + + {children} + ); }; diff --git a/x-pack/plugins/spaces/public/management/edit_space/provider/index.ts b/x-pack/plugins/spaces/public/management/edit_space/provider/index.ts index 7ae7301cd2c60..405f59c44a6f8 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/provider/index.ts +++ b/x-pack/plugins/spaces/public/management/edit_space/provider/index.ts @@ -5,9 +5,14 @@ * 2.0. */ -export { EditSpaceProvider, useEditSpaceServices, useEditSpaceStore } from './edit_space_provider'; +export { + EditSpaceProviderRoot, + EditSpaceProvider, + useEditSpaceServices, + useEditSpaceStore, +} from './edit_space_provider'; export type { - EditSpaceProviderProps, + EditSpaceProviderRootProps, EditSpaceServices, EditSpaceStore, } from './edit_space_provider'; diff --git a/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.test.tsx index 3595cefd1220c..7f99202e23791 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { render, screen, waitFor } from '@testing-library/react'; +import { render, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import crypto from 'crypto'; import React from 'react'; @@ -19,7 +19,7 @@ import { themeServiceMock, } from '@kbn/core/public/mocks'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; -import type { Role } from '@kbn/security-plugin-types-common'; +import type { Role, SecurityLicense } from '@kbn/security-plugin-types-common'; import { createRawKibanaPrivileges, kibanaFeatures, @@ -33,11 +33,8 @@ import { FEATURE_PRIVILEGES_READ, } from '../../../../../common/constants'; import { spacesManagerMock } from '../../../../spaces_manager/spaces_manager.mock'; -import { - createPrivilegeAPIClientMock, - getPrivilegeAPIClientMock, -} from '../../../privilege_api_client.mock'; -import { createRolesAPIClientMock, getRolesAPIClientMock } from '../../../roles_api_client.mock'; +import { createPrivilegeAPIClientMock } from '../../../privilege_api_client.mock'; +import { createRolesAPIClientMock } from '../../../roles_api_client.mock'; import { EditSpaceProvider } from '../../provider'; const rolesAPIClient = createRolesAPIClientMock(); @@ -74,6 +71,9 @@ const spacesClientsInvocatorMock = jest.fn((fn) => const dispatchMock = jest.fn(); const onSaveCompleted = jest.fn(); const closeFlyout = jest.fn(); +const licenseMock = { + getFeatures: jest.fn(() => ({})), +} as unknown as SecurityLicense; const renderPrivilegeRolesForm = ({ preSelectedRoles, @@ -93,15 +93,20 @@ const renderPrivilegeRolesForm = ({ spacesManager, serverBasePath: '', getUrlForApp: jest.fn((_) => _), - getRolesAPIClient: getRolesAPIClientMock, - getPrivilegesAPIClient: getPrivilegeAPIClientMock, navigateToUrl: jest.fn(), + license: licenseMock, capabilities: { navLinks: {}, management: {}, catalogue: {}, spaces: { manage: true }, }, + dispatch: dispatchMock, + state: { + roles: new Map(), + fetchRolesError: false, + }, + invokeClient: spacesClientsInvocatorMock, }} > _), }} /> @@ -358,11 +360,11 @@ describe('PrivilegesRolesForm', () => { preSelectedRoles: roles, }); - await waitFor(() => null); - - expect(screen.getByTestId(`${FEATURE_PRIVILEGES_READ}-privilege-button`)).toHaveAttribute( - 'aria-pressed', - String(true) + await waitFor(() => + expect(screen.getByTestId(`${FEATURE_PRIVILEGES_READ}-privilege-button`)).toHaveAttribute( + 'aria-pressed', + String(true) + ) ); await user.click(screen.getByTestId('custom-privilege-button')); @@ -408,5 +410,116 @@ describe('PrivilegesRolesForm', () => { String(true) ); }); + + it('prevents customization up to sub privilege level by default', async () => { + const user = userEvent.setup(); + + const roles: Role[] = [ + createRole('test_role_1', [ + { base: [FEATURE_PRIVILEGES_READ], feature: {}, spaces: [space.id] }, + ]), + ]; + + getRolesSpy.mockResolvedValue([]); + getAllKibanaPrivilegeSpy.mockResolvedValue(createRawKibanaPrivileges(kibanaFeatures)); + + const featuresWithSubFeatures = kibanaFeatures.filter((kibanaFeature) => + Boolean(kibanaFeature.subFeatures.length) + ); + + renderPrivilegeRolesForm({ + preSelectedRoles: roles, + }); + + await user.click(screen.getByTestId('custom-privilege-button')); + + expect( + screen.getByTestId('space-assign-role-privilege-customization-form') + ).toBeInTheDocument(); + + const featureUT = featuresWithSubFeatures[0]; + + // change a single feature with sub features to read from default privilege "none" + await user.click(screen.getByTestId(`${featureUT.id}_${FEATURE_PRIVILEGES_READ}`)); + + // click on the accordion toggle to show sub features + await user.click( + screen.getByTestId( + `featurePrivilegeControls_${featureUT.category.id}_${featureUT.id}_accordionToggle` + ) + ); + + // sub feature table renders + expect( + screen.getByTestId(`${featureUT.category.id}_${featureUT.id}_subFeaturesTable`) + ).toBeInTheDocument(); + + // assert switch to customize sub feature can toggled + expect( + within( + screen.getByTestId( + `${featureUT.category.id}_${featureUT.id}_customizeSubFeaturesSwitchContainer` + ) + ).getByTestId('customizeSubFeaturePrivileges') + ).toBeDisabled(); + }); + + it('supports customization up to sub privilege level only when security license allows', async () => { + const user = userEvent.setup(); + + const roles: Role[] = [ + createRole('test_role_1', [ + { base: [FEATURE_PRIVILEGES_READ], feature: {}, spaces: [space.id] }, + ]), + ]; + + // enable sub feature privileges + (licenseMock.getFeatures as jest.Mock).mockReturnValue({ + allowSubFeaturePrivileges: true, + }); + + getRolesSpy.mockResolvedValue([]); + getAllKibanaPrivilegeSpy.mockResolvedValue(createRawKibanaPrivileges(kibanaFeatures)); + + const featuresWithSubFeatures = kibanaFeatures.filter((kibanaFeature) => + Boolean(kibanaFeature.subFeatures.length) + ); + + renderPrivilegeRolesForm({ + preSelectedRoles: roles, + }); + + await user.click(screen.getByTestId('custom-privilege-button')); + + expect( + screen.getByTestId('space-assign-role-privilege-customization-form') + ).toBeInTheDocument(); + + const featureUT = featuresWithSubFeatures[0]; + + // change a single feature with sub features to read from default privilege "none" + await user.click(screen.getByTestId(`${featureUT.id}_${FEATURE_PRIVILEGES_READ}`)); + + // click on the accordion toggle to show sub features + await user.click( + screen.getByTestId( + `featurePrivilegeControls_${featureUT.category.id}_${featureUT.id}_accordionToggle` + ) + ); + + // sub feature table renders + expect( + screen.getByTestId(`${featureUT.category.id}_${featureUT.id}_subFeaturesTable`) + ).toBeInTheDocument(); + + // assert switch to customize sub feature can toggled + expect( + within( + screen.getByTestId( + `${featureUT.category.id}_${featureUT.id}_customizeSubFeaturesSwitchContainer` + ) + ).getByTestId('customizeSubFeaturePrivileges') + ).not.toBeDisabled(); + }); }); }); diff --git a/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.tsx b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.tsx index f33c2cba25268..e0f3e8f3714c6 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.tsx @@ -46,7 +46,7 @@ import { FEATURE_PRIVILEGES_CUSTOM, FEATURE_PRIVILEGES_READ, } from '../../../../../common/constants'; -import { type EditSpaceServices, type EditSpaceStore, useEditSpaceServices } from '../../provider'; +import { useEditSpaceServices, useEditSpaceStore } from '../../provider'; type KibanaRolePrivilege = | keyof NonNullable @@ -62,9 +62,6 @@ interface PrivilegesRolesFormProps { * this is useful when the form is opened in edit mode */ defaultSelected?: Role[]; - storeDispatch: EditSpaceStore['dispatch']; - spacesClientsInvocator: EditSpaceServices['invokeClient']; - getUrlForApp: EditSpaceServices['getUrlForApp']; } const createRolesComboBoxOptions = (roles: Role[]): Array> => @@ -74,17 +71,9 @@ const createRolesComboBoxOptions = (roles: Role[]): Array = (props) => { - const { - space, - onSaveCompleted, - closeFlyout, - features, - defaultSelected = [], - spacesClientsInvocator, - storeDispatch, - getUrlForApp, - } = props; - const { logger, notifications } = useEditSpaceServices(); + const { space, onSaveCompleted, closeFlyout, features, defaultSelected = [] } = props; + const { logger, notifications, license, invokeClient, getUrlForApp } = useEditSpaceServices(); + const { dispatch: storeDispatch } = useEditSpaceStore(); const [assigningToRole, setAssigningToRole] = useState(false); const [fetchingDataDeps, setFetchingDataDeps] = useState(false); const [kibanaPrivileges, setKibanaPrivileges] = useState(null); @@ -98,7 +87,7 @@ export const PrivilegesRolesForm: FC = (props) => { async function fetchRequiredData(spaceId: string) { setFetchingDataDeps(true); - const [systemRoles, _kibanaPrivileges] = await spacesClientsInvocator((clients) => + const [systemRoles, _kibanaPrivileges] = await invokeClient((clients) => Promise.all([ clients.rolesClient.getRoles(), clients.privilegesClient.getAll({ includeActions: true, respectLicenseLevel: false }), @@ -123,7 +112,7 @@ export const PrivilegesRolesForm: FC = (props) => { } fetchRequiredData(space.id!).finally(() => setFetchingDataDeps(false)); - }, [space.id, spacesClientsInvocator]); + }, [invokeClient, space.id]); const selectedRolesCombinedPrivileges = useMemo(() => { const combinedPrivilege = new Set( @@ -315,7 +304,7 @@ export const PrivilegesRolesForm: FC = (props) => { return selectedRole.value!; }); - await spacesClientsInvocator((clients) => + await invokeClient((clients) => clients.rolesClient.bulkUpdateRoles({ rolesUpdate: updatedRoles }).then((response) => { setAssigningToRole(false); onSaveCompleted(response); @@ -338,13 +327,14 @@ export const PrivilegesRolesForm: FC = (props) => { }); } }, [ + roleSpacePrivilege, + roleCustomizationAnchor.value?.kibana, + roleCustomizationAnchor.privilegeIndex, selectedRoles, - spacesClientsInvocator, + invokeClient, storeDispatch, - onSaveCompleted, space.id, - roleSpacePrivilege, - roleCustomizationAnchor, + onSaveCompleted, logger, notifications.toasts, ]); @@ -571,7 +561,9 @@ export const PrivilegesRolesForm: FC = (props) => { ) } allSpacesSelected={false} - canCustomizeSubFeaturePrivileges={false} + canCustomizeSubFeaturePrivileges={ + license?.getFeatures().allowSubFeaturePrivileges ?? false + } /> )} diff --git a/x-pack/plugins/spaces/public/management/management_service.test.ts b/x-pack/plugins/spaces/public/management/management_service.test.ts index d1d7fe8d160a9..8a08334b0a76d 100644 --- a/x-pack/plugins/spaces/public/management/management_service.test.ts +++ b/x-pack/plugins/spaces/public/management/management_service.test.ts @@ -13,6 +13,7 @@ import { managementPluginMock } from '@kbn/management-plugin/public/mocks'; import { ManagementService } from './management_service'; import { getRolesAPIClientMock } from './roles_api_client.mock'; +import { getSecurityLicenseMock } from './security_license.mock'; import { EventTracker } from '../analytics'; import type { ConfigType } from '../config'; import type { PluginsStart } from '../plugin'; @@ -49,6 +50,7 @@ describe('ManagementService', () => { logger, getRolesAPIClient: getRolesAPIClientMock, getPrivilegesAPIClient: jest.fn(), + getSecurityLicense: getSecurityLicenseMock, eventTracker, isServerless: false, }); @@ -73,6 +75,7 @@ describe('ManagementService', () => { logger, getRolesAPIClient: getRolesAPIClientMock, getPrivilegesAPIClient: jest.fn(), + getSecurityLicense: getSecurityLicenseMock, eventTracker, isServerless: false, }); @@ -98,6 +101,7 @@ describe('ManagementService', () => { logger, getRolesAPIClient: jest.fn(), getPrivilegesAPIClient: jest.fn(), + getSecurityLicense: getSecurityLicenseMock, eventTracker, isServerless: false, }); diff --git a/x-pack/plugins/spaces/public/management/management_service.tsx b/x-pack/plugins/spaces/public/management/management_service.tsx index ba66229323bc8..317e091839534 100644 --- a/x-pack/plugins/spaces/public/management/management_service.tsx +++ b/x-pack/plugins/spaces/public/management/management_service.tsx @@ -5,30 +5,15 @@ * 2.0. */ -import type { StartServicesAccessor } from '@kbn/core/public'; -import type { Logger } from '@kbn/logging'; import type { ManagementApp, ManagementSetup } from '@kbn/management-plugin/public'; -import type { - PrivilegesAPIClientPublicContract, - RolesAPIClient, -} from '@kbn/security-plugin-types-public'; -import { spacesManagementApp } from './spaces_management_app'; -import type { EventTracker } from '../analytics'; -import type { ConfigType } from '../config'; -import type { PluginsStart } from '../plugin'; -import type { SpacesManager } from '../spaces_manager'; +import { + spacesManagementApp, + type CreateParams as SpacesManagementAppCreateParams, +} from './spaces_management_app'; -interface SetupDeps { +interface SetupDeps extends SpacesManagementAppCreateParams { management: ManagementSetup; - getStartServices: StartServicesAccessor; - spacesManager: SpacesManager; - config: ConfigType; - getRolesAPIClient: () => Promise; - eventTracker: EventTracker; - getPrivilegesAPIClient: () => Promise; - logger: Logger; - isServerless: boolean; } export class ManagementService { @@ -44,6 +29,7 @@ export class ManagementService { eventTracker, getPrivilegesAPIClient, isServerless, + getSecurityLicense, }: SetupDeps) { this.registeredSpacesManagementApp = management.sections.section.kibana.registerApp( spacesManagementApp.create({ @@ -55,6 +41,7 @@ export class ManagementService { eventTracker, getPrivilegesAPIClient, isServerless, + getSecurityLicense, }) ); } diff --git a/x-pack/plugins/spaces/public/management/security_license.mock.ts b/x-pack/plugins/spaces/public/management/security_license.mock.ts new file mode 100644 index 0000000000000..d5d6e73d03db4 --- /dev/null +++ b/x-pack/plugins/spaces/public/management/security_license.mock.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { BehaviorSubject, type Observable } from 'rxjs'; + +import type { SecurityLicense } from '@kbn/security-plugin-types-public'; + +type SecurityLicenseFeatures = SecurityLicense['features$'] extends Observable ? P : never; + +export const createSecurityLicenseMock = ({ + securityFeaturesConfig, +}: { + securityFeaturesConfig: SecurityLicenseFeatures; +}): SecurityLicense => { + return { + isLicenseAvailable: jest.fn(), + isEnabled: jest.fn(), + getFeatures: jest.fn(), + getUnavailableReason: jest.fn(), + hasAtLeast: jest.fn(), + getLicenseType: jest.fn(), + features$: new BehaviorSubject(securityFeaturesConfig), + }; +}; + +export const getSecurityLicenseMock = jest.fn().mockResolvedValue( + createSecurityLicenseMock({ + securityFeaturesConfig: { + showLinks: true, + showLogin: true, + allowLogin: true, + allowRbac: true, + allowFips: true, + showRoleMappingsManagement: true, + allowAccessAgreement: true, + allowAuditLogging: true, + allowSubFeaturePrivileges: true, + allowRoleFieldLevelSecurity: true, + allowRoleDocumentLevelSecurity: true, + allowRoleRemoteIndexPrivileges: true, + allowRemoteClusterPrivileges: true, + allowUserProfileCollaboration: true, + }, + }) +); diff --git a/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx b/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx index ffafe432a5a3b..9c7056214d8fd 100644 --- a/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx @@ -77,6 +77,7 @@ async function mountApp(basePath: string, pathname: string, spaceId?: string) { logger, getRolesAPIClient: jest.fn(), getPrivilegesAPIClient: jest.fn(), + getSecurityLicense: jest.fn(), eventTracker, isServerless: false, }) @@ -102,6 +103,7 @@ describe('spacesManagementApp', () => { logger, getRolesAPIClient: jest.fn(), getPrivilegesAPIClient: jest.fn(), + getSecurityLicense: jest.fn(), eventTracker, isServerless: false, }) diff --git a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx index 7ad85d0ef7c52..a7111b72e563e 100644 --- a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx @@ -18,6 +18,7 @@ import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import type { PrivilegesAPIClientPublicContract, RolesAPIClient, + SecurityLicense, } from '@kbn/security-plugin-types-public'; import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app'; import { Route, Router, Routes } from '@kbn/shared-ux-router'; @@ -28,7 +29,7 @@ import type { ConfigType } from '../config'; import type { PluginsStart } from '../plugin'; import type { SpacesManager } from '../spaces_manager'; -interface CreateParams { +export interface CreateParams { getStartServices: StartServicesAccessor; spacesManager: SpacesManager; config: ConfigType; @@ -37,6 +38,7 @@ interface CreateParams { eventTracker: EventTracker; getPrivilegesAPIClient: () => Promise; isServerless: boolean; + getSecurityLicense: () => Promise; } export const spacesManagementApp = Object.freeze({ @@ -50,6 +52,7 @@ export const spacesManagementApp = Object.freeze({ getRolesAPIClient, getPrivilegesAPIClient, isServerless, + getSecurityLicense, }: CreateParams) { const title = i18n.translate('xpack.spaces.displayName', { defaultMessage: 'Spaces', @@ -149,6 +152,7 @@ export const spacesManagementApp = Object.freeze({ capabilities={application.capabilities} getUrlForApp={application.getUrlForApp} navigateToUrl={application.navigateToUrl} + getSecurityLicense={getSecurityLicense} serverBasePath={http.basePath.serverBasePath} getFeatures={features.getFeatures} http={http} diff --git a/x-pack/plugins/spaces/public/plugin.tsx b/x-pack/plugins/spaces/public/plugin.tsx index 8450aaff32657..107d3fab57652 100644 --- a/x-pack/plugins/spaces/public/plugin.tsx +++ b/x-pack/plugins/spaces/public/plugin.tsx @@ -10,7 +10,7 @@ import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kb import type { FeaturesPluginStart } from '@kbn/features-plugin/public'; import type { HomePublicPluginSetup } from '@kbn/home-plugin/public'; import type { ManagementSetup, ManagementStart } from '@kbn/management-plugin/public'; -import type { SecurityPluginStart } from '@kbn/security-plugin-types-public'; +import type { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin-types-public'; import { EventTracker, registerAnalyticsContext, registerSpacesEventTypes } from './analytics'; import type { ConfigType } from './config'; @@ -114,6 +114,18 @@ export class SpacesPlugin implements Plugin { + const { security } = await core.plugins.onSetup<{ security: SecurityPluginSetup }>( + 'security' + ); + + if (!security.found) { + throw new Error('Security plugin is not available as runtime dependency.'); + } + + return security.contract.license; + }; + if (plugins.home) { plugins.home.featureCatalogue.register(createSpacesFeatureCatalogueEntry()); } @@ -130,6 +142,7 @@ export class SpacesPlugin implements Plugin Date: Wed, 16 Oct 2024 12:14:46 +0200 Subject: [PATCH 02/18] [Cases] Adapt breadcrumbs to new stateful navigation (#196494) ## Summary Continuation of: https://github.com/elastic/kibana/pull/196169 Adapted the Cases breadcrumbs to the new navigation for stateful (ESS) environments. Using the same `chrome.setBreadcrumbs` API, the case title breadcrumb now needs to be passed separately, inside the second _param_ object with `{ project: { value }}`. ### Screenshots Before before After after --- .../components/use_breadcrumbs/index.test.tsx | 47 +++++++------ .../components/use_breadcrumbs/index.ts | 67 +++++++++++-------- 2 files changed, 68 insertions(+), 46 deletions(-) diff --git a/x-pack/plugins/cases/public/components/use_breadcrumbs/index.test.tsx b/x-pack/plugins/cases/public/components/use_breadcrumbs/index.test.tsx index d34e3c112d9fb..9f48783fde24d 100644 --- a/x-pack/plugins/cases/public/components/use_breadcrumbs/index.test.tsx +++ b/x-pack/plugins/cases/public/components/use_breadcrumbs/index.test.tsx @@ -54,10 +54,10 @@ describe('useCasesBreadcrumbs', () => { describe('set all_cases breadcrumbs', () => { it('call setBreadcrumbs with all items', async () => { renderHook(() => useCasesBreadcrumbs(CasesDeepLinkId.cases), { wrapper }); - expect(mockSetBreadcrumbs).toHaveBeenCalledWith([ - { href: '/test', onClick: expect.any(Function), text: 'Test' }, - { text: 'Cases' }, - ]); + expect(mockSetBreadcrumbs).toHaveBeenCalledWith( + [{ href: '/test', onClick: expect.any(Function), text: 'Test' }, { text: 'Cases' }], + { project: { value: [] } } + ); expect(mockSetServerlessBreadcrumbs).toHaveBeenCalledWith([]); }); @@ -76,11 +76,14 @@ describe('useCasesBreadcrumbs', () => { describe('set create_case breadcrumbs', () => { it('call setBreadcrumbs with all items', () => { renderHook(() => useCasesBreadcrumbs(CasesDeepLinkId.casesCreate), { wrapper }); - expect(mockSetBreadcrumbs).toHaveBeenCalledWith([ - { href: '/test', onClick: expect.any(Function), text: 'Test' }, - { href: CasesDeepLinkId.cases, onClick: expect.any(Function), text: 'Cases' }, - { text: 'Create' }, - ]); + expect(mockSetBreadcrumbs).toHaveBeenCalledWith( + [ + { href: '/test', onClick: expect.any(Function), text: 'Test' }, + { href: CasesDeepLinkId.cases, onClick: expect.any(Function), text: 'Cases' }, + { text: 'Create' }, + ], + { project: { value: [] } } + ); expect(mockSetServerlessBreadcrumbs).toHaveBeenCalledWith([]); }); @@ -100,11 +103,14 @@ describe('useCasesBreadcrumbs', () => { const title = 'Fake Title'; it('call setBreadcrumbs with title', () => { renderHook(() => useCasesTitleBreadcrumbs(title), { wrapper }); - expect(mockSetBreadcrumbs).toHaveBeenCalledWith([ - { href: '/test', onClick: expect.any(Function), text: 'Test' }, - { href: CasesDeepLinkId.cases, onClick: expect.any(Function), text: 'Cases' }, - { text: title }, - ]); + expect(mockSetBreadcrumbs).toHaveBeenCalledWith( + [ + { href: '/test', onClick: expect.any(Function), text: 'Test' }, + { href: CasesDeepLinkId.cases, onClick: expect.any(Function), text: 'Cases' }, + { text: title }, + ], + { project: { value: [{ text: title }] } } + ); expect(mockSetServerlessBreadcrumbs).toHaveBeenCalledWith([{ text: title }]); }); @@ -123,11 +129,14 @@ describe('useCasesBreadcrumbs', () => { describe('set settings breadcrumbs', () => { it('call setBreadcrumbs with all items', () => { renderHook(() => useCasesBreadcrumbs(CasesDeepLinkId.casesConfigure), { wrapper }); - expect(mockSetBreadcrumbs).toHaveBeenCalledWith([ - { href: '/test', onClick: expect.any(Function), text: 'Test' }, - { href: CasesDeepLinkId.cases, onClick: expect.any(Function), text: 'Cases' }, - { text: 'Settings' }, - ]); + expect(mockSetBreadcrumbs).toHaveBeenCalledWith( + [ + { href: '/test', onClick: expect.any(Function), text: 'Test' }, + { href: CasesDeepLinkId.cases, onClick: expect.any(Function), text: 'Cases' }, + { text: 'Settings' }, + ], + { project: { value: [] } } + ); expect(mockSetServerlessBreadcrumbs).toHaveBeenCalledWith([]); }); diff --git a/x-pack/plugins/cases/public/components/use_breadcrumbs/index.ts b/x-pack/plugins/cases/public/components/use_breadcrumbs/index.ts index 6312918842ba3..1750cd54e7d53 100644 --- a/x-pack/plugins/cases/public/components/use_breadcrumbs/index.ts +++ b/x-pack/plugins/cases/public/components/use_breadcrumbs/index.ts @@ -29,34 +29,48 @@ function getTitleFromBreadcrumbs(breadcrumbs: ChromeBreadcrumb[]): string[] { return breadcrumbs.map(({ text }) => text?.toString() ?? '').reverse(); } +const useGetBreadcrumbsWithNavigation = () => { + const { navigateToUrl } = useKibana().services.application; + + return useCallback( + (breadcrumbs: ChromeBreadcrumb[]): ChromeBreadcrumb[] => { + return breadcrumbs.map((breadcrumb) => { + const { href, onClick } = breadcrumb; + if (!href || onClick) { + return breadcrumb; + } + return { + ...breadcrumb, + onClick: (event) => { + if (event) { + event.preventDefault(); + } + navigateToUrl(href); + }, + }; + }); + }, + [navigateToUrl] + ); +}; + const useApplyBreadcrumbs = () => { - const { - chrome: { docTitle, setBreadcrumbs }, - application: { navigateToUrl }, - } = useKibana().services; + const { docTitle, setBreadcrumbs } = useKibana().services.chrome; + const getBreadcrumbsWithNavigation = useGetBreadcrumbsWithNavigation(); + return useCallback( - (breadcrumbs: ChromeBreadcrumb[]) => { - docTitle.change(getTitleFromBreadcrumbs(breadcrumbs)); - setBreadcrumbs( - breadcrumbs.map((breadcrumb) => { - const { href, onClick } = breadcrumb; - return { - ...breadcrumb, - ...(href && !onClick - ? { - onClick: (event) => { - if (event) { - event.preventDefault(); - } - navigateToUrl(href); - }, - } - : {}), - }; - }) - ); + ( + leadingRawBreadcrumbs: ChromeBreadcrumb[], + trailingRawBreadcrumbs: ChromeBreadcrumb[] = [] + ) => { + const leadingBreadcrumbs = getBreadcrumbsWithNavigation(leadingRawBreadcrumbs); + const trailingBreadcrumbs = getBreadcrumbsWithNavigation(trailingRawBreadcrumbs); + const allBreadcrumbs = [...leadingBreadcrumbs, ...trailingBreadcrumbs]; + + docTitle.change(getTitleFromBreadcrumbs(allBreadcrumbs)); + setBreadcrumbs(allBreadcrumbs, { project: { value: trailingBreadcrumbs } }); }, - [docTitle, setBreadcrumbs, navigateToUrl] + [docTitle, setBreadcrumbs, getBreadcrumbsWithNavigation] ); }; @@ -103,9 +117,8 @@ export const useCasesTitleBreadcrumbs = (caseTitle: string) => { text: casesBreadcrumbTitle[CasesDeepLinkId.cases], href: getAppUrl({ deepLinkId: CasesDeepLinkId.cases }), }, - titleBreadcrumb, ]; - applyBreadcrumbs(casesBreadcrumbs); + applyBreadcrumbs(casesBreadcrumbs, [titleBreadcrumb]); KibanaServices.get().serverless?.setBreadcrumbs([titleBreadcrumb]); }, [caseTitle, appTitle, getAppUrl, applyBreadcrumbs]); }; From 1de6fb5b639ccfe044bf388cb301e99c19890f03 Mon Sep 17 00:00:00 2001 From: Pablo Machado Date: Wed, 16 Oct 2024 12:29:26 +0200 Subject: [PATCH 03/18] [SecuritySolution] Fix schedule risk engine callout when engine is installed but disabled (#196496) ## Summary Fix the visibility of the Schedule Risk Engine Callout inside the asset criticality bulk upload when the risk engine is disabled. The callout should not be displayed when the risk engine is installed but disabled. ### How to test it? * Open Kibana - Security Solution * Navigate to the risk engine page and install the risk engine * On the same page, disable the risk engine * Navigate to the asset criticality page * Successfully upload a document * You should see a "success" but no risk engine callout. ![Screenshot 2024-10-16 at 10 41 39](https://github.com/user-attachments/assets/5c8f078e-4588-434e-8c76-03d72b1c5a16) ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../schedule_risk_engine_callout.test.tsx | 28 +++++++++++++++---- .../schedule_risk_engine_callout.tsx | 6 +++- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/schedule_risk_engine_callout.test.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/schedule_risk_engine_callout.test.tsx index 0ce75c714f89f..fa22892f21db8 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/schedule_risk_engine_callout.test.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/schedule_risk_engine_callout.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { render, fireEvent, waitFor } from '@testing-library/react'; import { TestProviders } from '../../../../common/mock'; import { ScheduleRiskEngineCallout } from './schedule_risk_engine_callout'; +import { RiskEngineStatusEnum } from '../../../../../common/api/entity_analytics'; const oneHourFromNow = () => { const date = new Date(); @@ -48,7 +49,7 @@ describe('ScheduleRiskEngineCallout', () => { mockUseRiskEngineStatus.mockReturnValue({ data: { isNewRiskScoreModuleInstalled: true, - + risk_engine_status: RiskEngineStatusEnum.ENABLED, risk_engine_task_status: { status: 'idle', runAt: oneHourFromNow().toISOString(), @@ -70,7 +71,7 @@ describe('ScheduleRiskEngineCallout', () => { mockUseRiskEngineStatus.mockReturnValue({ data: { isNewRiskScoreModuleInstalled: true, - + risk_engine_status: RiskEngineStatusEnum.ENABLED, risk_engine_task_status: { status: 'running', runAt: oneHourFromNow().toISOString(), @@ -89,7 +90,7 @@ describe('ScheduleRiskEngineCallout', () => { mockUseRiskEngineStatus.mockReturnValue({ data: { isNewRiskScoreModuleInstalled: true, - + risk_engine_status: RiskEngineStatusEnum.ENABLED, risk_engine_task_status: { status: 'idle', runAt: new Date().toISOString(), // past date @@ -110,7 +111,7 @@ describe('ScheduleRiskEngineCallout', () => { mockUseRiskEngineStatus.mockReturnValueOnce({ data: { isNewRiskScoreModuleInstalled: true, - + risk_engine_status: RiskEngineStatusEnum.ENABLED, risk_engine_task_status: { status: 'idle', runAt: oneHourFromNow(), @@ -127,7 +128,7 @@ describe('ScheduleRiskEngineCallout', () => { mockUseRiskEngineStatus.mockReturnValueOnce({ data: { isNewRiskScoreModuleInstalled: true, - + risk_engine_status: RiskEngineStatusEnum.ENABLED, risk_engine_task_status: { status: 'idle', runAt: thirtyMinutesFromNow(), @@ -143,7 +144,7 @@ describe('ScheduleRiskEngineCallout', () => { mockUseRiskEngineStatus.mockReturnValue({ data: { isNewRiskScoreModuleInstalled: true, - + risk_engine_status: RiskEngineStatusEnum.ENABLED, risk_engine_task_status: { status: 'idle', runAt: new Date().toISOString(), // past date @@ -173,4 +174,19 @@ describe('ScheduleRiskEngineCallout', () => { expect(queryByTestId('risk-engine-callout')).toBeNull(); }); + + it('should not show the callout if the risk engine is disabled', () => { + mockUseRiskEngineStatus.mockReturnValue({ + data: { + isNewRiskScoreModuleInstalled: true, + risk_engine_status: RiskEngineStatusEnum.DISABLED, + }, + }); + + const { queryByTestId } = render(, { + wrapper: TestProviders, + }); + + expect(queryByTestId('risk-engine-callout')).toBeNull(); + }); }); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/schedule_risk_engine_callout.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/schedule_risk_engine_callout.tsx index 2e8722cd20005..432e03c231a4d 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/schedule_risk_engine_callout.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/schedule_risk_engine_callout.tsx @@ -16,6 +16,7 @@ import React, { useCallback, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; +import { RiskEngineStatusEnum } from '../../../../../common/api/entity_analytics'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { formatTimeFromNow } from '../helpers'; import { useScheduleNowRiskEngineMutation } from '../../../api/hooks/use_schedule_now_risk_engine_mutation'; @@ -76,7 +77,10 @@ export const ScheduleRiskEngineCallout: React.FC = () => { scheduleRiskEngineMutation(); }, [scheduleRiskEngineMutation]); - if (!riskEngineStatus?.isNewRiskScoreModuleInstalled) { + if ( + !riskEngineStatus?.isNewRiskScoreModuleInstalled || + riskEngineStatus?.risk_engine_status !== RiskEngineStatusEnum.ENABLED + ) { return null; } From d3fc354cbbd1985e7da95cbed132e05af856b740 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Wed, 16 Oct 2024 05:44:34 -0500 Subject: [PATCH 04/18] upgrade request-converter for 8.16 (#196193) ## Summary https://github.com/elastic/request-converter needs to be updated regularly to maintain support for the latest ES apis. --- yarn.lock | 58 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/yarn.lock b/yarn.lock index ec4c8f0e0837f..ed8af28c675f4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1844,9 +1844,9 @@ "@elastic/search-ui" "1.20.2" "@elastic/request-converter@^8.15.4": - version "8.15.4" - resolved "https://registry.yarnpkg.com/@elastic/request-converter/-/request-converter-8.15.4.tgz#332dc6266841b5a578c92e1655eee46b82c47ed2" - integrity sha512-iZDQpZpygV+AVOweaDzTsMJBfa2hwwduPXNNzk/yTXgC9qtjmns/AjehtLStKXs274+u3fg+BFxVt6NcMwUAAg== + version "8.16.0" + resolved "https://registry.yarnpkg.com/@elastic/request-converter/-/request-converter-8.16.0.tgz#e607d06d898ec290c7a9412104d7fa67d5fb9c8c" + integrity sha512-tSwCJMoX3/hme1HXi7ewfP5E+BLFV2LcItt3EB4eucWPKEtG3SqJ3iuK3ygWm1PodPz8Mww/DMaxTJFERb/usg== dependencies: child-process-promise "^2.2.1" commander "^12.1.0" @@ -21081,7 +21081,7 @@ isbinaryfile@4.0.2: isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== isexe@^3.1.1: version "3.1.1" @@ -26411,7 +26411,7 @@ prr@~1.0.1: pseudomap@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" - integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= + integrity sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ== psl@^1.1.33: version "1.4.0" @@ -29690,7 +29690,7 @@ string-replace-loader@^2.2.0: loader-utils "^1.2.3" schema-utils "^1.0.0" -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -29708,6 +29708,15 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -29818,7 +29827,7 @@ stringify-object@^3.2.1: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -29832,6 +29841,13 @@ strip-ansi@^3.0.0, strip-ansi@^3.0.1: dependencies: ansi-regex "^2.0.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -31140,9 +31156,9 @@ uc.micro@^2.0.0, uc.micro@^2.1.0: integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A== uglify-js@^3.1.4: - version "3.17.4" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c" - integrity sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g== + version "3.19.3" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.19.3.tgz#82315e9bbc6f2b25888858acd1fff8441035b77f" + integrity sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ== unbox-primitive@^1.0.2: version "1.0.2" @@ -32733,7 +32749,7 @@ word-wrap@~1.2.3: wordwrap@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" - integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= + integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== worker-farm@^1.7.0: version "1.7.0" @@ -32754,7 +32770,7 @@ workerpool@6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -32780,6 +32796,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" @@ -32892,7 +32917,7 @@ xpath@^0.0.33: resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.33.tgz#5136b6094227c5df92002e7c3a13516a5074eb07" integrity sha512-NNXnzrkDrAzalLhIUc01jO2mOzXGXh1JwPgkihcLLzw98c0WgYDmmjSh1Kl3wzaxSVWMuA+fe0WTWOBDWCBmNA== -"xstate5@npm:xstate@^5.18.1", xstate@^5.18.1: +"xstate5@npm:xstate@^5.18.1": version "5.18.1" resolved "https://registry.yarnpkg.com/xstate/-/xstate-5.18.1.tgz#c4d43ceaba6e6c31705d36bd96e285de4be4f7f4" integrity sha512-m02IqcCQbaE/kBQLunwub/5i8epvkD2mFutnL17Oeg1eXTShe1sRF4D5mhv1dlaFO4vbW5gRGRhraeAD5c938g== @@ -32902,6 +32927,11 @@ xstate@^4.38.2: resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.38.2.tgz#1b74544fc9c8c6c713ba77f81c6017e65aa89804" integrity sha512-Fba/DwEPDLneHT3tbJ9F3zafbQXszOlyCJyQqqdzmtlY/cwE2th462KK48yaANf98jHlP6lJvxfNtN0LFKXPQg== +xstate@^5.18.1: + version "5.18.1" + resolved "https://registry.yarnpkg.com/xstate/-/xstate-5.18.1.tgz#c4d43ceaba6e6c31705d36bd96e285de4be4f7f4" + integrity sha512-m02IqcCQbaE/kBQLunwub/5i8epvkD2mFutnL17Oeg1eXTShe1sRF4D5mhv1dlaFO4vbW5gRGRhraeAD5c938g== + "xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@^4.0.1, xtend@^4.0.2, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" @@ -32937,7 +32967,7 @@ y18n@^5.0.5: yallist@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" - integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= + integrity sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A== yallist@^3.0.2: version "3.1.1" From 1f56f2102fd2108dc4469710e1a2765f918451b6 Mon Sep 17 00:00:00 2001 From: Vadim Kibana <82822460+vadimkibana@users.noreply.github.com> Date: Wed, 16 Oct 2024 14:04:04 +0200 Subject: [PATCH 05/18] [ES|QL] Catch internal parsing errors (#196249) ## Summary Addresses this comment https://github.com/elastic/kibana/issues/191683#issuecomment-2338181503 Catches parser internal errors, and reports them the same as the regular parsing errors. --- .../src/parser/__tests__/columns.test.ts | 2 +- .../src/parser/__tests__/commands.test.ts | 2 +- .../src/parser/__tests__/from.test.ts | 2 +- .../src/parser/__tests__/function.test.ts | 2 +- .../src/parser/__tests__/inlinecast.test.ts | 2 +- .../src/parser/__tests__/literal.test.ts | 2 +- .../src/parser/__tests__/metrics.test.ts | 2 +- .../src/parser/__tests__/params.test.ts | 2 +- .../src/parser/__tests__/rename.test.ts | 2 +- .../src/parser/__tests__/sort.test.ts | 2 +- .../src/parser/__tests__/where.test.ts | 2 +- packages/kbn-esql-ast/src/parser/parser.ts | 85 +++++++++++++------ 12 files changed, 70 insertions(+), 37 deletions(-) diff --git a/packages/kbn-esql-ast/src/parser/__tests__/columns.test.ts b/packages/kbn-esql-ast/src/parser/__tests__/columns.test.ts index e79c418eeb3cc..38e98104d41bd 100644 --- a/packages/kbn-esql-ast/src/parser/__tests__/columns.test.ts +++ b/packages/kbn-esql-ast/src/parser/__tests__/columns.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { getAstAndSyntaxErrors as parse } from '..'; +import { parse } from '..'; describe('Column Identifier Expressions', () => { it('can parse un-quoted identifiers', () => { diff --git a/packages/kbn-esql-ast/src/parser/__tests__/commands.test.ts b/packages/kbn-esql-ast/src/parser/__tests__/commands.test.ts index 30d44d447387e..6fb176c9624f7 100644 --- a/packages/kbn-esql-ast/src/parser/__tests__/commands.test.ts +++ b/packages/kbn-esql-ast/src/parser/__tests__/commands.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { getAstAndSyntaxErrors as parse } from '..'; +import { parse } from '..'; describe('commands', () => { describe('correctly formatted, basic usage', () => { diff --git a/packages/kbn-esql-ast/src/parser/__tests__/from.test.ts b/packages/kbn-esql-ast/src/parser/__tests__/from.test.ts index 101661973a692..f2f0fded57ca5 100644 --- a/packages/kbn-esql-ast/src/parser/__tests__/from.test.ts +++ b/packages/kbn-esql-ast/src/parser/__tests__/from.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { getAstAndSyntaxErrors as parse } from '..'; +import { parse } from '..'; describe('FROM', () => { describe('correctly formatted', () => { diff --git a/packages/kbn-esql-ast/src/parser/__tests__/function.test.ts b/packages/kbn-esql-ast/src/parser/__tests__/function.test.ts index 8ec533816a56e..9d822f78f9333 100644 --- a/packages/kbn-esql-ast/src/parser/__tests__/function.test.ts +++ b/packages/kbn-esql-ast/src/parser/__tests__/function.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { getAstAndSyntaxErrors as parse } from '..'; +import { parse } from '..'; import { Walker } from '../../walker'; describe('function AST nodes', () => { diff --git a/packages/kbn-esql-ast/src/parser/__tests__/inlinecast.test.ts b/packages/kbn-esql-ast/src/parser/__tests__/inlinecast.test.ts index d0650ab3f3213..889ca2a2ecf3d 100644 --- a/packages/kbn-esql-ast/src/parser/__tests__/inlinecast.test.ts +++ b/packages/kbn-esql-ast/src/parser/__tests__/inlinecast.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { getAstAndSyntaxErrors as parse } from '..'; +import { parse } from '..'; import { ESQLFunction, ESQLInlineCast, ESQLSingleAstItem } from '../../types'; describe('Inline cast (::)', () => { diff --git a/packages/kbn-esql-ast/src/parser/__tests__/literal.test.ts b/packages/kbn-esql-ast/src/parser/__tests__/literal.test.ts index 514d769d5c45e..7f50198c96047 100644 --- a/packages/kbn-esql-ast/src/parser/__tests__/literal.test.ts +++ b/packages/kbn-esql-ast/src/parser/__tests__/literal.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { getAstAndSyntaxErrors as parse } from '..'; +import { parse } from '..'; import { ESQLLiteral } from '../../types'; describe('literal expression', () => { diff --git a/packages/kbn-esql-ast/src/parser/__tests__/metrics.test.ts b/packages/kbn-esql-ast/src/parser/__tests__/metrics.test.ts index 54ddc49c5d048..d33c94e8903ac 100644 --- a/packages/kbn-esql-ast/src/parser/__tests__/metrics.test.ts +++ b/packages/kbn-esql-ast/src/parser/__tests__/metrics.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { getAstAndSyntaxErrors as parse } from '..'; +import { parse } from '..'; describe('METRICS', () => { describe('correctly formatted', () => { diff --git a/packages/kbn-esql-ast/src/parser/__tests__/params.test.ts b/packages/kbn-esql-ast/src/parser/__tests__/params.test.ts index 8586236eeb2f9..e4b1a892d32d9 100644 --- a/packages/kbn-esql-ast/src/parser/__tests__/params.test.ts +++ b/packages/kbn-esql-ast/src/parser/__tests__/params.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { getAstAndSyntaxErrors as parse } from '..'; +import { parse } from '..'; import { Walker } from '../../walker'; /** diff --git a/packages/kbn-esql-ast/src/parser/__tests__/rename.test.ts b/packages/kbn-esql-ast/src/parser/__tests__/rename.test.ts index 4acad891150b2..214e5c1d36882 100644 --- a/packages/kbn-esql-ast/src/parser/__tests__/rename.test.ts +++ b/packages/kbn-esql-ast/src/parser/__tests__/rename.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { getAstAndSyntaxErrors as parse } from '..'; +import { parse } from '..'; describe('RENAME', () => { /** diff --git a/packages/kbn-esql-ast/src/parser/__tests__/sort.test.ts b/packages/kbn-esql-ast/src/parser/__tests__/sort.test.ts index cfaec0a6e39e9..981eac40b68ae 100644 --- a/packages/kbn-esql-ast/src/parser/__tests__/sort.test.ts +++ b/packages/kbn-esql-ast/src/parser/__tests__/sort.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { getAstAndSyntaxErrors as parse } from '..'; +import { parse } from '..'; describe('SORT', () => { describe('correctly formatted', () => { diff --git a/packages/kbn-esql-ast/src/parser/__tests__/where.test.ts b/packages/kbn-esql-ast/src/parser/__tests__/where.test.ts index d507b559fd407..f3f6aeb886ec0 100644 --- a/packages/kbn-esql-ast/src/parser/__tests__/where.test.ts +++ b/packages/kbn-esql-ast/src/parser/__tests__/where.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { getAstAndSyntaxErrors as parse } from '..'; +import { parse } from '..'; describe('WHERE', () => { describe('correctly formatted', () => { diff --git a/packages/kbn-esql-ast/src/parser/parser.ts b/packages/kbn-esql-ast/src/parser/parser.ts index ad263a49ebd00..f99e00e92d1e0 100644 --- a/packages/kbn-esql-ast/src/parser/parser.ts +++ b/packages/kbn-esql-ast/src/parser/parser.ts @@ -100,33 +100,66 @@ export interface ParseResult { } export const parse = (text: string | undefined, options: ParseOptions = {}): ParseResult => { - if (text == null) { - const commands: ESQLAstQueryExpression['commands'] = []; - return { ast: commands, root: Builder.expression.query(commands), errors: [], tokens: [] }; + try { + if (text == null) { + const commands: ESQLAstQueryExpression['commands'] = []; + return { ast: commands, root: Builder.expression.query(commands), errors: [], tokens: [] }; + } + const errorListener = new ESQLErrorListener(); + const parseListener = new ESQLAstBuilderListener(); + const { tokens, parser } = getParser( + CharStreams.fromString(text), + errorListener, + parseListener + ); + + parser[GRAMMAR_ROOT_RULE](); + + const errors = errorListener.getErrors().filter((error) => { + return !SYNTAX_ERRORS_TO_IGNORE.includes(error.message); + }); + const { ast: commands } = parseListener.getAst(); + const root = Builder.expression.query(commands, { + location: { + min: 0, + max: text.length - 1, + }, + }); + + if (options.withFormatting) { + const decorations = collectDecorations(tokens); + attachDecorations(root, tokens.tokens, decorations.lines); + } + + return { root, ast: commands, errors, tokens: tokens.tokens }; + } catch (error) { + /** + * Parsing should never fail, meaning this branch should never execute. But + * if it does fail, we want to log the error message for easier debugging. + */ + // eslint-disable-next-line no-console + console.error(error); + + const root = Builder.expression.query(); + + return { + root, + ast: root.commands, + errors: [ + { + startLineNumber: 0, + endLineNumber: 0, + startColumn: 0, + endColumn: 0, + message: + 'Parsing internal error: ' + + (!!error && typeof error === 'object' ? String(error.message) : String(error)), + severity: 'error', + }, + ], + tokens: [], + }; } - const errorListener = new ESQLErrorListener(); - const parseListener = new ESQLAstBuilderListener(); - const { tokens, parser } = getParser(CharStreams.fromString(text), errorListener, parseListener); - - parser[GRAMMAR_ROOT_RULE](); - - const errors = errorListener.getErrors().filter((error) => { - return !SYNTAX_ERRORS_TO_IGNORE.includes(error.message); - }); - const { ast: commands } = parseListener.getAst(); - const root = Builder.expression.query(commands, { - location: { - min: 0, - max: text.length - 1, - }, - }); - - if (options.withFormatting) { - const decorations = collectDecorations(tokens); - attachDecorations(root, tokens.tokens, decorations.lines); - } - - return { root, ast: commands, errors, tokens: tokens.tokens }; }; export const parseErrors = (text: string) => { From 4ab58325a93d2870ec1dbf17c6f02bfeb8dfc24e Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 16 Oct 2024 23:04:49 +1100 Subject: [PATCH 06/18] skip failing test suite (#196526) --- .../entity_store/trial_license_complete_tier/engine.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine.ts index c10144aec0342..a7d32767f50ce 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine.ts @@ -14,7 +14,8 @@ export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); const utils = EntityStoreUtils(getService); - describe('@ess @skipInServerlessMKI Entity Store Engine APIs', () => { + // Failing: See https://github.com/elastic/kibana/issues/196526 + describe.skip('@ess @skipInServerlessMKI Entity Store Engine APIs', () => { const dataView = dataViewRouteHelpersFactory(supertest); before(async () => { From 4edb91bb70f2e86bb756723549d5b90daee9f84c Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 16 Oct 2024 23:11:16 +1100 Subject: [PATCH 07/18] skip failing test suite (#196470) --- .../trial_license_complete_tier/perform_bulk_action_ess.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action_ess.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action_ess.ts index 3e3eb594cc0bc..303425fca9b09 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action_ess.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action_ess.ts @@ -47,7 +47,8 @@ export default ({ getService }: FtrProviderContext): void => { const createWebHookConnector = () => createConnector(getWebHookAction()); // Failing: See https://github.com/elastic/kibana/issues/173804 - describe('@ess perform_bulk_action - ESS specific logic', () => { + // Failing: See https://github.com/elastic/kibana/issues/196470 + describe.skip('@ess perform_bulk_action - ESS specific logic', () => { beforeEach(async () => { await deleteAllRules(supertest, log); }); From 3e86a7c7d3d6acf5116245b939d4ae6ea8006051 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 16 Oct 2024 23:11:56 +1100 Subject: [PATCH 08/18] skip failing test suite (#196492) --- .../saved_objects/trial_license_complete_tier/notes.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/security_solution_api_integration/test_suites/investigation/saved_objects/trial_license_complete_tier/notes.ts b/x-pack/test/security_solution_api_integration/test_suites/investigation/saved_objects/trial_license_complete_tier/notes.ts index 5d1fefadb2f65..027c0a20262a8 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/investigation/saved_objects/trial_license_complete_tier/notes.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/investigation/saved_objects/trial_license_complete_tier/notes.ts @@ -15,7 +15,8 @@ export default function ({ getService }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const supertest = getService('supertest'); - describe('Note - Saved Objects', () => { + // Failing: See https://github.com/elastic/kibana/issues/196492 + describe.skip('Note - Saved Objects', () => { const es = getService('es'); before(() => kibanaServer.savedObjects.cleanStandardList()); From 9bca8b744af8bf2b2b5e5ff74680c494ae7d1824 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 16 Oct 2024 23:12:22 +1100 Subject: [PATCH 09/18] skip failing test suite (#196462) --- .../trial_license_complete_tier/perform_bulk_action_ess.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action_ess.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action_ess.ts index 303425fca9b09..0fb8644f5e93c 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action_ess.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action_ess.ts @@ -48,6 +48,7 @@ export default ({ getService }: FtrProviderContext): void => { // Failing: See https://github.com/elastic/kibana/issues/173804 // Failing: See https://github.com/elastic/kibana/issues/196470 + // Failing: See https://github.com/elastic/kibana/issues/196462 describe.skip('@ess perform_bulk_action - ESS specific logic', () => { beforeEach(async () => { await deleteAllRules(supertest, log); From 501874f2a8097e056b629c1fb14b246e8d4b4102 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 16 Oct 2024 23:12:58 +1100 Subject: [PATCH 10/18] skip failing test suite (#196120) --- .../apps/discover/group2_data_grid1/_data_grid_context.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/discover/group2_data_grid1/_data_grid_context.ts b/test/functional/apps/discover/group2_data_grid1/_data_grid_context.ts index 0757304199d8a..6a7a3fc80343e 100644 --- a/test/functional/apps/discover/group2_data_grid1/_data_grid_context.ts +++ b/test/functional/apps/discover/group2_data_grid1/_data_grid_context.ts @@ -40,7 +40,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); const security = getService('security'); - describe('discover data grid context tests', () => { + // Failing: See https://github.com/elastic/kibana/issues/196120 + describe.skip('discover data grid context tests', () => { before(async () => { await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']); await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); From 4ac575b3b291743af5fcd70f795ca43ad805fbe7 Mon Sep 17 00:00:00 2001 From: Katerina Date: Wed, 16 Oct 2024 15:16:05 +0300 Subject: [PATCH 11/18] [Inventory] Add Technical preview icon and link to docs (#196353) ## Summary closes https://github.com/elastic/kibana/issues/196264 https://github.com/user-attachments/assets/a8fdf896-5d55-4060-a274-d2a4188c62b2 --- .../enable_entity_model_button.tsx | 3 +++ .../inventory_page_template/no_data_config.tsx | 14 +++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entity_enablement/enable_entity_model_button.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entity_enablement/enable_entity_model_button.tsx index 6f13c33585bca..7941881ff2c51 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entity_enablement/enable_entity_model_button.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entity_enablement/enable_entity_model_button.tsx @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; import { EntityManagerUnauthorizedError } from '@kbn/entityManager-plugin/public'; import type { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public'; +import { TechnicalPreviewBadge } from '@kbn/observability-shared-plugin/public'; import { useKibana } from '../../hooks/use_kibana'; import { Unauthorized } from './unauthorized_modal'; @@ -57,6 +58,8 @@ export function EnableEntityModelButton({ onSuccess }: { onSuccess: () => void } data-test-subj="inventoryInventoryPageTemplateFilledButton" fill onClick={handleEnablement} + iconType={() => } + iconSide="right" > {i18n.translate('xpack.inventory.noData.card.button', { defaultMessage: 'Enable', diff --git a/x-pack/plugins/observability_solution/inventory/public/components/inventory_page_template/no_data_config.tsx b/x-pack/plugins/observability_solution/inventory/public/components/inventory_page_template/no_data_config.tsx index 3b12e11d2ba7c..79db53f39c346 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/inventory_page_template/no_data_config.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/inventory_page_template/no_data_config.tsx @@ -33,8 +33,20 @@ export function getEntityManagerEnablement({ description: ( + {i18n.translate('xpack.inventory.noData.card.description.inventory', { + defaultMessage: 'Inventory', + })} + + ), link: ( Date: Wed, 16 Oct 2024 14:29:03 +0200 Subject: [PATCH 12/18] [Global Search] Add multiword type handling in global search (#196087) ## Summary This PR improves the UX of global search by allowing users to search for types that consist of multiple words without having to turn them into phrases (wrapping them in quotes). For example: The following query: ``` hello type:canvas workpad type:enterprise search world tag:new ``` Will get mapped to: ``` hello type:"canvas workpad" type:"enterprise search" world tag:new ``` Which will result in following `Query` object: ```json { "term": "hello world", "filters": { "tags": ["new"] "types": ["canvas workpad", "enterprise search"], }, } ``` Fixes: #176877 ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../public/components/search_bar.tsx | 2 +- .../search_syntax/parse_search_params.test.ts | 86 +++++++++++++++++-- .../search_syntax/parse_search_params.ts | 48 ++++++++++- 3 files changed, 124 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/global_search_bar/public/components/search_bar.tsx b/x-pack/plugins/global_search_bar/public/components/search_bar.tsx index b1473c56b37ed..efc564089fb43 100644 --- a/x-pack/plugins/global_search_bar/public/components/search_bar.tsx +++ b/x-pack/plugins/global_search_bar/public/components/search_bar.tsx @@ -173,7 +173,7 @@ export const SearchBar: FC = (opts) => { reportEvent.searchRequest(); } - const rawParams = parseSearchParams(searchValue.toLowerCase()); + const rawParams = parseSearchParams(searchValue.toLowerCase(), searchableTypes); let tagIds: string[] | undefined; if (taggingApi && rawParams.filters.tags) { tagIds = rawParams.filters.tags.map( diff --git a/x-pack/plugins/global_search_bar/public/search_syntax/parse_search_params.test.ts b/x-pack/plugins/global_search_bar/public/search_syntax/parse_search_params.test.ts index c6df745be847f..8e24f599ce1d2 100644 --- a/x-pack/plugins/global_search_bar/public/search_syntax/parse_search_params.test.ts +++ b/x-pack/plugins/global_search_bar/public/search_syntax/parse_search_params.test.ts @@ -9,12 +9,12 @@ import { parseSearchParams } from './parse_search_params'; describe('parseSearchParams', () => { it('returns the correct term', () => { - const searchParams = parseSearchParams('tag:(my-tag OR other-tag) hello'); + const searchParams = parseSearchParams('tag:(my-tag OR other-tag) hello', []); expect(searchParams.term).toEqual('hello'); }); it('returns the raw query as `term` in case of parsing error', () => { - const searchParams = parseSearchParams('tag:((()^invalid'); + const searchParams = parseSearchParams('tag:((()^invalid', []); expect(searchParams).toEqual({ term: 'tag:((()^invalid', filters: {}, @@ -22,12 +22,12 @@ describe('parseSearchParams', () => { }); it('returns `undefined` term if query only contains field clauses', () => { - const searchParams = parseSearchParams('tag:(my-tag OR other-tag)'); + const searchParams = parseSearchParams('tag:(my-tag OR other-tag)', []); expect(searchParams.term).toBeUndefined(); }); it('returns correct filters when no field clause is defined', () => { - const searchParams = parseSearchParams('hello'); + const searchParams = parseSearchParams('hello', []); expect(searchParams.filters).toEqual({ tags: undefined, types: undefined, @@ -35,7 +35,7 @@ describe('parseSearchParams', () => { }); it('returns correct filters when field clauses are present', () => { - const searchParams = parseSearchParams('tag:foo type:bar hello tag:dolly'); + const searchParams = parseSearchParams('tag:foo type:bar hello tag:dolly', []); expect(searchParams).toEqual({ term: 'hello', filters: { @@ -46,7 +46,7 @@ describe('parseSearchParams', () => { }); it('considers unknown field clauses to be part of the raw search term', () => { - const searchParams = parseSearchParams('tag:foo unknown:bar hello'); + const searchParams = parseSearchParams('tag:foo unknown:bar hello', []); expect(searchParams).toEqual({ term: 'unknown:bar hello', filters: { @@ -56,7 +56,7 @@ describe('parseSearchParams', () => { }); it('handles aliases field clauses', () => { - const searchParams = parseSearchParams('tag:foo tags:bar type:dash types:board hello'); + const searchParams = parseSearchParams('tag:foo tags:bar type:dash types:board hello', []); expect(searchParams).toEqual({ term: 'hello', filters: { @@ -67,7 +67,7 @@ describe('parseSearchParams', () => { }); it('converts boolean and number values to string for known filters', () => { - const searchParams = parseSearchParams('tag:42 tags:true type:69 types:false hello'); + const searchParams = parseSearchParams('tag:42 tags:true type:69 types:false hello', []); expect(searchParams).toEqual({ term: 'hello', filters: { @@ -76,4 +76,74 @@ describe('parseSearchParams', () => { }, }); }); + + it('converts multiword searchable types to phrases so they get picked up as types', () => { + const mockSearchableMultiwordTypes = ['canvas-workpad', 'enterprise search']; + const searchParams = parseSearchParams( + 'type:canvas workpad types:canvas-workpad hello type:enterprise search type:not multiword', + mockSearchableMultiwordTypes + ); + expect(searchParams).toEqual({ + term: 'hello multiword', + filters: { + types: ['canvas workpad', 'enterprise search', 'not'], + }, + }); + }); + + it('parses correctly when multiword types are already quoted', () => { + const mockSearchableMultiwordTypes = ['canvas-workpad']; + const searchParams = parseSearchParams( + `type:"canvas workpad" hello type:"dashboard"`, + mockSearchableMultiwordTypes + ); + expect(searchParams).toEqual({ + term: 'hello', + filters: { + types: ['canvas workpad', 'dashboard'], + }, + }); + }); + + it('parses correctly when there is whitespace between type keyword and value', () => { + const mockSearchableMultiwordTypes = ['canvas-workpad']; + const searchParams = parseSearchParams( + 'type: canvas workpad hello type: dashboard', + mockSearchableMultiwordTypes + ); + expect(searchParams).toEqual({ + term: 'hello', + filters: { + types: ['canvas workpad', 'dashboard'], + }, + }); + }); + + it('dedupes duplicate types', () => { + const mockSearchableMultiwordTypes = ['canvas-workpad']; + const searchParams = parseSearchParams( + 'type:canvas workpad hello type:dashboard type:canvas-workpad type:canvas workpad type:dashboard', + mockSearchableMultiwordTypes + ); + expect(searchParams).toEqual({ + term: 'hello', + filters: { + types: ['canvas workpad', 'dashboard'], + }, + }); + }); + + it('handles whitespace removal even if there are no multiword types', () => { + const mockSearchableMultiwordTypes: string[] = []; + const searchParams = parseSearchParams( + 'hello type: dashboard', + mockSearchableMultiwordTypes + ); + expect(searchParams).toEqual({ + term: 'hello', + filters: { + types: ['dashboard'], + }, + }); + }); }); diff --git a/x-pack/plugins/global_search_bar/public/search_syntax/parse_search_params.ts b/x-pack/plugins/global_search_bar/public/search_syntax/parse_search_params.ts index 1df6c1123a328..90ba36cce5fcb 100644 --- a/x-pack/plugins/global_search_bar/public/search_syntax/parse_search_params.ts +++ b/x-pack/plugins/global_search_bar/public/search_syntax/parse_search_params.ts @@ -16,12 +16,54 @@ const aliasMap = { type: ['types'], }; -export const parseSearchParams = (term: string): ParsedSearchParams => { +// Converts multiword types to phrases by wrapping them in quotes and trimming whitespace after type keyword. Example: type: canvas workpad -> type:"canvas workpad". If the type is already wrapped in quotes or is a single word, it will only trim whitespace after type keyword. +const convertMultiwordTypesToPhrasesAndTrimWhitespace = ( + term: string, + multiWordTypes: string[] +): string => { + if (!multiWordTypes.length) { + return term.replace( + /(type:|types:)\s*([^"']*?)\b([^"'\s]+)/gi, + (_, typeKeyword, whitespace, typeValue) => `${typeKeyword}${whitespace.trim()}${typeValue}` + ); + } + + const typesPattern = multiWordTypes.join('|'); + const termReplaceRegex = new RegExp( + `(type:|types:)\\s*([^"']*?)\\b((${typesPattern})\\b|[^\\s"']+)`, + 'gi' + ); + + return term.replace(termReplaceRegex, (_, typeKeyword, whitespace, typeValue) => { + const trimmedTypeKeyword = `${typeKeyword}${whitespace.trim()}`; + + // If the type value is already wrapped in quotes, leave it as is + return /['"]/.test(typeValue) + ? `${trimmedTypeKeyword}${typeValue}` + : `${trimmedTypeKeyword}"${typeValue}"`; + }); +}; + +const dedupeTypes = (types: FilterValues): FilterValues => [ + ...new Set(types.map((item) => item.replace(/[-\s]+/g, ' ').trim())), +]; + +export const parseSearchParams = (term: string, searchableTypes: string[]): ParsedSearchParams => { const recognizedFields = knownFilters.concat(...Object.values(aliasMap)); let query: Query; + // Finds all multiword types that are separated by whitespace or hyphens + const multiWordSearchableTypesWhitespaceSeperated = searchableTypes + .filter((item) => /[ -]/.test(item)) + .map((item) => item.replace(/-/g, ' ')); + + const modifiedTerm = convertMultiwordTypesToPhrasesAndTrimWhitespace( + term, + multiWordSearchableTypesWhitespaceSeperated + ); + try { - query = Query.parse(term, { + query = Query.parse(modifiedTerm, { schema: { recognizedFields }, }); } catch (e) { @@ -42,7 +84,7 @@ export const parseSearchParams = (term: string): ParsedSearchParams => { term: searchTerm, filters: { tags: tags ? valuesToString(tags) : undefined, - types: types ? valuesToString(types) : undefined, + types: types ? dedupeTypes(valuesToString(types)) : undefined, }, }; }; From 8d77cd49996281e746a0a7138c7624867c047053 Mon Sep 17 00:00:00 2001 From: Jeramy Soucy Date: Wed, 16 Oct 2024 15:11:25 +0200 Subject: [PATCH 13/18] Sets explicit access for public platform security endpoints (#195099) Related issue: #189833 ## Summary This PR explicitly sets the access level for platform security HTTP API endpoints. This is to address restriction of internal endpoints in v9. For details, see https://github.com/elastic/kibana/issues/189833. Additionally, this PR sets the `excludeFromOAS` option where applicable, in order to refrain from generating documentation for endpoints which are public but should either remain undocumented, or should be documented as part of a specific topic (e.g. external authentication flow). Note: the invalidate sessions API has been changed to internal in serverless Endpoints excluded from OAS: - GET /api/security/logout - GET /api/security/v1/logout - /api/security/oidc/implicit - /api/security/v1/oidc/implicit - /internal/security/oidc/implicit.js - GET /api/security/oidc/callback - GET /api/security/v1/oidc - POST /api/security/oidc/initiate_login - POST /api/security/v1/oidc - GET /api/security/oidc/initiate_login - POST /api/security/saml/callback - /internal/security/reset_session_page.js - /security/access_agreement - /security/account - /internal/security/capture-url - /security/logged_out - /login - /logout - /security/overwritten_session - /spaces/space_selector --- .../server/routes/index.mock.ts | 4 +- .../server/routes/key_rotation.test.ts | 20 ++++++++ .../server/routes/key_rotation.ts | 2 +- .../routes/authentication/common.test.ts | 3 +- .../server/routes/authentication/common.ts | 6 ++- .../server/routes/authentication/oidc.ts | 15 ++++-- .../server/routes/authentication/saml.test.ts | 1 + .../server/routes/authentication/saml.ts | 1 + .../routes/authorization/privileges/get.ts | 1 + .../authorization/reset_session_page.ts | 2 +- .../server/routes/session_management/index.ts | 9 +--- .../routes/session_management/invalidate.ts | 13 ++++- .../routes/views/access_agreement.test.ts | 2 +- .../server/routes/views/access_agreement.ts | 2 +- .../server/routes/views/account_management.ts | 5 +- .../server/routes/views/capture_url.test.ts | 2 +- .../server/routes/views/capture_url.ts | 2 +- .../server/routes/views/logged_out.test.ts | 2 +- .../server/routes/views/logged_out.ts | 2 +- .../server/routes/views/login.test.ts | 2 +- .../security/server/routes/views/login.ts | 2 +- .../security/server/routes/views/logout.ts | 2 +- .../routes/views/overwritten_session.ts | 2 +- .../spaces/server/routes/views/index.test.ts | 2 +- .../spaces/server/routes/views/index.ts | 3 +- .../common/platform_security/sessions.ts | 50 +++++++++++++++---- 26 files changed, 112 insertions(+), 45 deletions(-) diff --git a/x-pack/plugins/encrypted_saved_objects/server/routes/index.mock.ts b/x-pack/plugins/encrypted_saved_objects/server/routes/index.mock.ts index 4d453f64b6954..4f486d3337632 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/routes/index.mock.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/routes/index.mock.ts @@ -13,11 +13,11 @@ import { ConfigSchema } from '../config'; import { encryptionKeyRotationServiceMock } from '../crypto/index.mock'; export const routeDefinitionParamsMock = { - create: (config: Record = {}) => ({ + create: (config: Record = {}, buildFlavor: BuildFlavor = 'traditional') => ({ router: httpServiceMock.createRouter(), logger: loggingSystemMock.create().get(), config: ConfigSchema.validate(config) as ConfigType, encryptionKeyRotationService: encryptionKeyRotationServiceMock.create(), - buildFlavor: 'traditional' as BuildFlavor, + buildFlavor, }), }; diff --git a/x-pack/plugins/encrypted_saved_objects/server/routes/key_rotation.test.ts b/x-pack/plugins/encrypted_saved_objects/server/routes/key_rotation.test.ts index 9b9dba6108ff3..f387e94e80990 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/routes/key_rotation.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/routes/key_rotation.test.ts @@ -44,6 +44,7 @@ describe('Key rotation routes', () => { it('correctly defines route.', () => { expect(routeConfig.options).toEqual({ + access: 'public', tags: ['access:rotateEncryptionKey', 'oas-tag:saved objects'], summary: `Rotate a key for encrypted saved objects`, description: `If a saved object cannot be decrypted using the primary encryption key, Kibana attempts to decrypt it using the specified decryption-only keys. In most of the cases this overhead is negligible, but if you're dealing with a large number of saved objects and experiencing performance issues, you may want to rotate the encryption key. @@ -83,6 +84,25 @@ describe('Key rotation routes', () => { ); }); + it('defines route as internal when build flavor is serverless', () => { + const routeParamsMock = routeDefinitionParamsMock.create( + { keyRotation: { decryptionOnlyKeys: ['b'.repeat(32)] } }, + 'serverless' + ); + defineKeyRotationRoutes(routeParamsMock); + const [config] = routeParamsMock.router.post.mock.calls.find( + ([{ path }]) => path === '/api/encrypted_saved_objects/_rotate_key' + )!; + + expect(config.options).toEqual({ + access: 'internal', + tags: ['access:rotateEncryptionKey', 'oas-tag:saved objects'], + summary: `Rotate a key for encrypted saved objects`, + description: `If a saved object cannot be decrypted using the primary encryption key, Kibana attempts to decrypt it using the specified decryption-only keys. In most of the cases this overhead is negligible, but if you're dealing with a large number of saved objects and experiencing performance issues, you may want to rotate the encryption key. + NOTE: Bulk key rotation can consume a considerable amount of resources and hence only user with a superuser role can trigger it.`, + }); + }); + it('returns 400 if decryption only keys are not specified.', async () => { const routeParamsMock = routeDefinitionParamsMock.create(); defineKeyRotationRoutes(routeParamsMock); diff --git a/x-pack/plugins/encrypted_saved_objects/server/routes/key_rotation.ts b/x-pack/plugins/encrypted_saved_objects/server/routes/key_rotation.ts index 80907497010da..272e74c3a69cb 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/routes/key_rotation.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/routes/key_rotation.ts @@ -41,7 +41,7 @@ export function defineKeyRotationRoutes({ }, options: { tags: ['access:rotateEncryptionKey', 'oas-tag:saved objects'], - access: buildFlavor === 'serverless' ? 'internal' : undefined, + access: buildFlavor === 'serverless' ? 'internal' : 'public', summary: `Rotate a key for encrypted saved objects`, description: `If a saved object cannot be decrypted using the primary encryption key, Kibana attempts to decrypt it using the specified decryption-only keys. In most of the cases this overhead is negligible, but if you're dealing with a large number of saved objects and experiencing performance issues, you may want to rotate the encryption key. NOTE: Bulk key rotation can consume a considerable amount of resources and hence only user with a superuser role can trigger it.`, diff --git a/x-pack/plugins/security/server/routes/authentication/common.test.ts b/x-pack/plugins/security/server/routes/authentication/common.test.ts index c494389eb7c13..0fd2c54a1e6ca 100644 --- a/x-pack/plugins/security/server/routes/authentication/common.test.ts +++ b/x-pack/plugins/security/server/routes/authentication/common.test.ts @@ -69,6 +69,7 @@ describe('Common authentication routes', () => { access: 'public', authRequired: false, tags: [ROUTE_TAG_CAN_REDIRECT, ROUTE_TAG_AUTH_FLOW], + excludeFromOAS: true, }); expect(routeConfig.validate).toEqual({ body: undefined, @@ -170,7 +171,7 @@ describe('Common authentication routes', () => { }); it('correctly defines route.', async () => { - expect(routeConfig.options).toBeUndefined(); + expect(routeConfig.options).toEqual({ access: 'internal' }); expect(routeConfig.validate).toBe(false); }); diff --git a/x-pack/plugins/security/server/routes/authentication/common.ts b/x-pack/plugins/security/server/routes/authentication/common.ts index e54d6e35f1669..b519171fd4fe6 100644 --- a/x-pack/plugins/security/server/routes/authentication/common.ts +++ b/x-pack/plugins/security/server/routes/authentication/common.ts @@ -48,6 +48,7 @@ export function defineCommonRoutes({ validate: { query: schema.object({}, { unknowns: 'allow' }) }, options: { access: 'public', + excludeFromOAS: true, authRequired: false, tags: [ROUTE_TAG_CAN_REDIRECT, ROUTE_TAG_AUTH_FLOW], }, @@ -89,10 +90,11 @@ export function defineCommonRoutes({ '/internal/security/me', ...(buildFlavor !== 'serverless' ? ['/api/security/v1/me'] : []), ]) { + const deprecated = path === '/api/security/v1/me'; router.get( - { path, validate: false }, + { path, validate: false, options: { access: deprecated ? 'public' : 'internal' } }, createLicensedRouteHandler(async (context, request, response) => { - if (path === '/api/security/v1/me') { + if (deprecated) { logger.warn( `The "${basePath.serverBasePath}${path}" endpoint is deprecated and will be removed in the next major version.`, { tags: ['deprecation'] } diff --git a/x-pack/plugins/security/server/routes/authentication/oidc.ts b/x-pack/plugins/security/server/routes/authentication/oidc.ts index 2c4ab9de1491b..69c3ce1700671 100644 --- a/x-pack/plugins/security/server/routes/authentication/oidc.ts +++ b/x-pack/plugins/security/server/routes/authentication/oidc.ts @@ -37,7 +37,7 @@ export function defineOIDCRoutes({ { path, validate: false, - options: { authRequired: false }, + options: { authRequired: false, excludeFromOAS: true }, }, (context, request, response) => { const serverBasePath = basePath.serverBasePath; @@ -68,7 +68,7 @@ export function defineOIDCRoutes({ { path: '/internal/security/oidc/implicit.js', validate: false, - options: { authRequired: false }, + options: { authRequired: false, excludeFromOAS: true }, }, (context, request, response) => { const serverBasePath = basePath.serverBasePath; @@ -106,7 +106,12 @@ export function defineOIDCRoutes({ { unknowns: 'allow' } ), }, - options: { authRequired: false, tags: [ROUTE_TAG_CAN_REDIRECT, ROUTE_TAG_AUTH_FLOW] }, + options: { + access: 'public', + excludeFromOAS: true, + authRequired: false, + tags: [ROUTE_TAG_CAN_REDIRECT, ROUTE_TAG_AUTH_FLOW], + }, }, createLicensedRouteHandler(async (context, request, response) => { const serverBasePath = basePath.serverBasePath; @@ -184,6 +189,8 @@ export function defineOIDCRoutes({ ), }, options: { + access: 'public', + excludeFromOAS: true, authRequired: false, xsrfRequired: false, tags: [ROUTE_TAG_CAN_REDIRECT, ROUTE_TAG_AUTH_FLOW], @@ -227,6 +234,8 @@ export function defineOIDCRoutes({ ), }, options: { + access: 'public', + excludeFromOAS: true, authRequired: false, tags: [ROUTE_TAG_CAN_REDIRECT, ROUTE_TAG_AUTH_FLOW], }, diff --git a/x-pack/plugins/security/server/routes/authentication/saml.test.ts b/x-pack/plugins/security/server/routes/authentication/saml.test.ts index e952d98a38649..f693d20354e89 100644 --- a/x-pack/plugins/security/server/routes/authentication/saml.test.ts +++ b/x-pack/plugins/security/server/routes/authentication/saml.test.ts @@ -56,6 +56,7 @@ describe('SAML authentication routes', () => { expect(routeConfig.options).toEqual({ access: 'public', authRequired: false, + excludeFromOAS: true, xsrfRequired: false, tags: [ROUTE_TAG_CAN_REDIRECT, ROUTE_TAG_AUTH_FLOW], }); diff --git a/x-pack/plugins/security/server/routes/authentication/saml.ts b/x-pack/plugins/security/server/routes/authentication/saml.ts index ddc31fbc88b89..3c72fd908e6c4 100644 --- a/x-pack/plugins/security/server/routes/authentication/saml.ts +++ b/x-pack/plugins/security/server/routes/authentication/saml.ts @@ -38,6 +38,7 @@ export function defineSAMLRoutes({ }, options: { access: 'public', + excludeFromOAS: true, authRequired: false, xsrfRequired: false, tags: [ROUTE_TAG_CAN_REDIRECT, ROUTE_TAG_AUTH_FLOW], diff --git a/x-pack/plugins/security/server/routes/authorization/privileges/get.ts b/x-pack/plugins/security/server/routes/authorization/privileges/get.ts index 1d278aa676ac3..b7204faaa7ca4 100644 --- a/x-pack/plugins/security/server/routes/authorization/privileges/get.ts +++ b/x-pack/plugins/security/server/routes/authorization/privileges/get.ts @@ -26,6 +26,7 @@ export function defineGetPrivilegesRoutes({ router, authz }: RouteDefinitionPara ), }), }, + options: { access: 'public' }, }, createLicensedRouteHandler((context, request, response) => { const respectLicenseLevel = request.query.respectLicenseLevel !== 'false'; // if undefined resolve to true by default diff --git a/x-pack/plugins/security/server/routes/authorization/reset_session_page.ts b/x-pack/plugins/security/server/routes/authorization/reset_session_page.ts index 67254735b9a16..0af24ad8d8397 100644 --- a/x-pack/plugins/security/server/routes/authorization/reset_session_page.ts +++ b/x-pack/plugins/security/server/routes/authorization/reset_session_page.ts @@ -12,7 +12,7 @@ export function resetSessionPageRoutes({ httpResources }: RouteDefinitionParams) { path: '/internal/security/reset_session_page.js', validate: false, - options: { authRequired: false }, + options: { authRequired: false, excludeFromOAS: true }, }, (context, request, response) => { return response.renderJs({ diff --git a/x-pack/plugins/security/server/routes/session_management/index.ts b/x-pack/plugins/security/server/routes/session_management/index.ts index c095a77409975..041feea8a62fd 100644 --- a/x-pack/plugins/security/server/routes/session_management/index.ts +++ b/x-pack/plugins/security/server/routes/session_management/index.ts @@ -13,12 +13,5 @@ import type { RouteDefinitionParams } from '..'; export function defineSessionManagementRoutes(params: RouteDefinitionParams) { defineSessionInfoRoutes(params); defineSessionExtendRoutes(params); - - // The invalidate session API was introduced to address situations where the session index - // could grow rapidly - when session timeouts are disabled, or with anonymous access. - // In the serverless environment, sessions timeouts are always be enabled, and there is no - // anonymous access. This eliminates the need for an invalidate session HTTP API. - if (params.buildFlavor !== 'serverless') { - defineInvalidateSessionsRoutes(params); - } + defineInvalidateSessionsRoutes(params); } diff --git a/x-pack/plugins/security/server/routes/session_management/invalidate.ts b/x-pack/plugins/security/server/routes/session_management/invalidate.ts index c7d27b835edf2..a45d8f00c1ca4 100644 --- a/x-pack/plugins/security/server/routes/session_management/invalidate.ts +++ b/x-pack/plugins/security/server/routes/session_management/invalidate.ts @@ -12,7 +12,11 @@ import type { RouteDefinitionParams } from '..'; /** * Defines routes required for session invalidation. */ -export function defineInvalidateSessionsRoutes({ router, getSession }: RouteDefinitionParams) { +export function defineInvalidateSessionsRoutes({ + router, + getSession, + buildFlavor, +}: RouteDefinitionParams) { router.post( { path: '/api/security/session/_invalidate', @@ -34,7 +38,12 @@ export function defineInvalidateSessionsRoutes({ router, getSession }: RouteDefi }), }, options: { - access: 'public', + // The invalidate session API was introduced to address situations where the session index + // could grow rapidly - when session timeouts are disabled, or with anonymous access. + // In the serverless environment, sessions timeouts are always be enabled, and there is no + // anonymous access. However, keeping this endpoint available internally in serverless would + // be useful in situations where we need to batch-invalidate user sessions. + access: buildFlavor === 'serverless' ? 'internal' : 'public', tags: ['access:sessionManagement'], summary: `Invalidate user sessions`, }, diff --git a/x-pack/plugins/security/server/routes/views/access_agreement.test.ts b/x-pack/plugins/security/server/routes/views/access_agreement.test.ts index ef588ae1cfcfc..74eee1e129e2c 100644 --- a/x-pack/plugins/security/server/routes/views/access_agreement.test.ts +++ b/x-pack/plugins/security/server/routes/views/access_agreement.test.ts @@ -71,7 +71,7 @@ describe('Access agreement view routes', () => { }); it('correctly defines route.', () => { - expect(routeConfig.options).toBeUndefined(); + expect(routeConfig.options).toEqual({ excludeFromOAS: true }); expect(routeConfig.validate).toBe(false); }); diff --git a/x-pack/plugins/security/server/routes/views/access_agreement.ts b/x-pack/plugins/security/server/routes/views/access_agreement.ts index 3724892edd6df..823fbb0286f33 100644 --- a/x-pack/plugins/security/server/routes/views/access_agreement.ts +++ b/x-pack/plugins/security/server/routes/views/access_agreement.ts @@ -24,7 +24,7 @@ export function defineAccessAgreementRoutes({ const canHandleRequest = () => license.getFeatures().allowAccessAgreement; httpResources.register( - { path: '/security/access_agreement', validate: false }, + { path: '/security/access_agreement', validate: false, options: { excludeFromOAS: true } }, createLicensedRouteHandler(async (context, request, response) => canHandleRequest() ? response.renderCoreApp() diff --git a/x-pack/plugins/security/server/routes/views/account_management.ts b/x-pack/plugins/security/server/routes/views/account_management.ts index af49f325a25d2..4b3fbb78fed90 100644 --- a/x-pack/plugins/security/server/routes/views/account_management.ts +++ b/x-pack/plugins/security/server/routes/views/account_management.ts @@ -11,7 +11,8 @@ import type { RouteDefinitionParams } from '..'; * Defines routes required for the Account Management view. */ export function defineAccountManagementRoutes({ httpResources }: RouteDefinitionParams) { - httpResources.register({ path: '/security/account', validate: false }, (context, req, res) => - res.renderCoreApp() + httpResources.register( + { path: '/security/account', validate: false, options: { excludeFromOAS: true } }, + (context, req, res) => res.renderCoreApp() ); } diff --git a/x-pack/plugins/security/server/routes/views/capture_url.test.ts b/x-pack/plugins/security/server/routes/views/capture_url.test.ts index 1893ad6c9cb5f..4496ab341b085 100644 --- a/x-pack/plugins/security/server/routes/views/capture_url.test.ts +++ b/x-pack/plugins/security/server/routes/views/capture_url.test.ts @@ -34,7 +34,7 @@ describe('Capture URL view routes', () => { }); it('correctly defines route.', () => { - expect(routeConfig.options).toEqual({ authRequired: false }); + expect(routeConfig.options).toEqual({ authRequired: false, excludeFromOAS: true }); expect(routeConfig.validate).toEqual({ body: undefined, diff --git a/x-pack/plugins/security/server/routes/views/capture_url.ts b/x-pack/plugins/security/server/routes/views/capture_url.ts index 8eff92d78999d..394b799ca1f9d 100644 --- a/x-pack/plugins/security/server/routes/views/capture_url.ts +++ b/x-pack/plugins/security/server/routes/views/capture_url.ts @@ -19,7 +19,7 @@ export function defineCaptureURLRoutes({ httpResources }: RouteDefinitionParams) validate: { query: schema.object({ next: schema.maybe(schema.string()) }, { unknowns: 'ignore' }), }, - options: { authRequired: false }, + options: { authRequired: false, excludeFromOAS: true }, }, (context, request, response) => response.renderAnonymousCoreApp() ); diff --git a/x-pack/plugins/security/server/routes/views/logged_out.test.ts b/x-pack/plugins/security/server/routes/views/logged_out.test.ts index 850a533e3d93a..9aecb39750b1b 100644 --- a/x-pack/plugins/security/server/routes/views/logged_out.test.ts +++ b/x-pack/plugins/security/server/routes/views/logged_out.test.ts @@ -35,7 +35,7 @@ describe('LoggedOut view routes', () => { }); it('correctly defines route.', () => { - expect(routeConfig.options).toEqual({ authRequired: false }); + expect(routeConfig.options).toEqual({ authRequired: false, excludeFromOAS: true }); expect(routeConfig.validate).toBe(false); }); diff --git a/x-pack/plugins/security/server/routes/views/logged_out.ts b/x-pack/plugins/security/server/routes/views/logged_out.ts index 360c0fb2c9b7c..66581f574def8 100644 --- a/x-pack/plugins/security/server/routes/views/logged_out.ts +++ b/x-pack/plugins/security/server/routes/views/logged_out.ts @@ -20,7 +20,7 @@ export function defineLoggedOutRoutes({ { path: '/security/logged_out', validate: false, - options: { authRequired: false }, + options: { authRequired: false, excludeFromOAS: true }, }, async (context, request, response) => { // Authentication flow isn't triggered automatically for this route, so we should explicitly diff --git a/x-pack/plugins/security/server/routes/views/login.test.ts b/x-pack/plugins/security/server/routes/views/login.test.ts index b19ef41ca9098..11797e20523e9 100644 --- a/x-pack/plugins/security/server/routes/views/login.test.ts +++ b/x-pack/plugins/security/server/routes/views/login.test.ts @@ -52,7 +52,7 @@ describe('Login view routes', () => { }); it('correctly defines route.', () => { - expect(routeConfig.options).toEqual({ authRequired: 'optional' }); + expect(routeConfig.options).toEqual({ authRequired: 'optional', excludeFromOAS: true }); expect(routeConfig.validate).toEqual({ body: undefined, diff --git a/x-pack/plugins/security/server/routes/views/login.ts b/x-pack/plugins/security/server/routes/views/login.ts index 5d4468fcbba57..8cf8459d523b8 100644 --- a/x-pack/plugins/security/server/routes/views/login.ts +++ b/x-pack/plugins/security/server/routes/views/login.ts @@ -39,7 +39,7 @@ export function defineLoginRoutes({ { unknowns: 'allow' } ), }, - options: { authRequired: 'optional' }, + options: { authRequired: 'optional', excludeFromOAS: true }, }, async (context, request, response) => { // Default to true if license isn't available or it can't be resolved for some reason. diff --git a/x-pack/plugins/security/server/routes/views/logout.ts b/x-pack/plugins/security/server/routes/views/logout.ts index 3fb905ee10d37..d61f4e83083d2 100644 --- a/x-pack/plugins/security/server/routes/views/logout.ts +++ b/x-pack/plugins/security/server/routes/views/logout.ts @@ -12,7 +12,7 @@ import type { RouteDefinitionParams } from '..'; */ export function defineLogoutRoutes({ httpResources }: RouteDefinitionParams) { httpResources.register( - { path: '/logout', validate: false, options: { authRequired: false } }, + { path: '/logout', validate: false, options: { authRequired: false, excludeFromOAS: true } }, (context, request, response) => response.renderAnonymousCoreApp() ); } diff --git a/x-pack/plugins/security/server/routes/views/overwritten_session.ts b/x-pack/plugins/security/server/routes/views/overwritten_session.ts index 115f7ea0a093f..4ab57f2cc9e72 100644 --- a/x-pack/plugins/security/server/routes/views/overwritten_session.ts +++ b/x-pack/plugins/security/server/routes/views/overwritten_session.ts @@ -12,7 +12,7 @@ import type { RouteDefinitionParams } from '..'; */ export function defineOverwrittenSessionRoutes({ httpResources }: RouteDefinitionParams) { httpResources.register( - { path: '/security/overwritten_session', validate: false }, + { path: '/security/overwritten_session', validate: false, options: { excludeFromOAS: true } }, (context, req, res) => res.renderCoreApp() ); } diff --git a/x-pack/plugins/spaces/server/routes/views/index.test.ts b/x-pack/plugins/spaces/server/routes/views/index.test.ts index b87bfe86c022a..e42f2dcf42eaf 100644 --- a/x-pack/plugins/spaces/server/routes/views/index.test.ts +++ b/x-pack/plugins/spaces/server/routes/views/index.test.ts @@ -59,7 +59,7 @@ describe('Space Selector view routes', () => { }); it('correctly defines route.', () => { - expect(routeConfig.options).toBeUndefined(); + expect(routeConfig.options).toEqual({ excludeFromOAS: true }); expect(routeConfig.validate).toBe(false); }); diff --git a/x-pack/plugins/spaces/server/routes/views/index.ts b/x-pack/plugins/spaces/server/routes/views/index.ts index ab06b17374f13..f21a665e35525 100644 --- a/x-pack/plugins/spaces/server/routes/views/index.ts +++ b/x-pack/plugins/spaces/server/routes/views/index.ts @@ -20,7 +20,7 @@ export interface ViewRouteDeps { export function initSpacesViewsRoutes(deps: ViewRouteDeps) { deps.httpResources.register( - { path: '/spaces/space_selector', validate: false }, + { path: '/spaces/space_selector', validate: false, options: { excludeFromOAS: true } }, (context, request, response) => response.renderCoreApp() ); @@ -32,6 +32,7 @@ export function initSpacesViewsRoutes(deps: ViewRouteDeps) { schema.object({ next: schema.maybe(schema.string()) }, { unknowns: 'ignore' }) ), }, + options: { excludeFromOAS: true }, }, async (context, request, response) => { try { diff --git a/x-pack/test_serverless/api_integration/test_suites/common/platform_security/sessions.ts b/x-pack/test_serverless/api_integration/test_suites/common/platform_security/sessions.ts index c76ccb81f8ce2..c102f502f9489 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/platform_security/sessions.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/platform_security/sessions.ts @@ -10,7 +10,6 @@ import { SupertestWithRoleScopeType } from '@kbn/test-suites-xpack/api_integrati import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { - const svlCommonApi = getService('svlCommonApi'); const samlAuth = getService('samlAuth'); const roleScopedSupertest = getService('roleScopedSupertest'); let supertestViewerWithCookieCredentials: SupertestWithRoleScopeType; @@ -26,16 +25,6 @@ export default function ({ getService }: FtrProviderContext) { }); describe('route access', () => { - describe('disabled', () => { - it('invalidate', async () => { - const { body, status } = await supertestViewerWithCookieCredentials - .post('/api/security/session/_invalidate') - .set(samlAuth.getInternalRequestHeader()) - .send({ match: 'all' }); - svlCommonApi.assertApiNotFound(body, status); - }); - }); - describe('internal', () => { it('get session info', async () => { let body: any; @@ -84,6 +73,45 @@ export default function ({ getService }: FtrProviderContext) { // expect redirect expect(status).toBe(302); }); + + it('invalidate', async () => { + const supertestAdmin = await roleScopedSupertest.getSupertestWithRoleScope('admin', { + useCookieHeader: true, + }); + + let body: any; + let status: number; + + ({ body, status } = await supertestViewerWithCookieCredentials + .post('/api/security/session/_invalidate') + .set(samlAuth.getCommonRequestHeader())); + // expect a rejection because we're not using the internal header + expect(body).toEqual({ + statusCode: 400, + error: 'Bad Request', + message: expect.stringContaining( + 'method [post] exists but is not available with the current configuration' + ), + }); + expect(status).toBe(400); + + ({ body, status } = await supertestViewerWithCookieCredentials + .post('/api/security/session/_invalidate') + .set(samlAuth.getInternalRequestHeader())); + // expect forbidden because the viewer does not have privilege to invalidate a session + expect(status).toBe(403); + + ({ body, status } = await supertestAdmin + .post('/api/security/session/_invalidate') + .set(samlAuth.getInternalRequestHeader())); + // expect 400 due to no body, admin has privilege, but the request body is missing + expect(status).toBe(400); + expect(body).toEqual({ + error: 'Bad Request', + message: '[request body]: expected a plain object value, but found [null] instead.', + statusCode: 400, + }); + }); }); }); }); From dd0af8d2852d750f463dc2cbeb8dda9d75c95363 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 17 Oct 2024 00:42:34 +1100 Subject: [PATCH 14/18] skip failing test suite (#196546) --- .../trial_license_complete_tier/engine_nondefault_spaces.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine_nondefault_spaces.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine_nondefault_spaces.ts index de949730d3d10..481f7aa4056f6 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine_nondefault_spaces.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine_nondefault_spaces.ts @@ -18,7 +18,8 @@ export default ({ getService }: FtrProviderContextWithSpaces) => { const supertest = getService('supertest'); const utils = EntityStoreUtils(getService, namespace); - describe('@ess Entity Store Engine APIs in non-default space', () => { + // Failing: See https://github.com/elastic/kibana/issues/196546 + describe.skip('@ess Entity Store Engine APIs in non-default space', () => { const dataView = dataViewRouteHelpersFactory(supertest, namespace); before(async () => { From e6569f0b2503968c4b8eadd9227dca96d69fd4e9 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 17 Oct 2024 00:43:08 +1100 Subject: [PATCH 15/18] skip failing test suite (#194305) --- .../functional/test_suites/common/discover/esql/_esql_view.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/esql/_esql_view.ts b/x-pack/test_serverless/functional/test_suites/common/discover/esql/_esql_view.ts index 1bedd0acd0cc4..89edce106f64e 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/esql/_esql_view.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/esql/_esql_view.ts @@ -35,7 +35,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { defaultIndex: 'logstash-*', }; - describe('discover esql view', function () { + // Failing: See https://github.com/elastic/kibana/issues/194305 + describe.skip('discover esql view', function () { // see details: https://github.com/elastic/kibana/issues/188816 this.tags(['failsOnMKI']); From 661458f9e9196b3618db30a68bbd6785606f1b59 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 17 Oct 2024 01:17:59 +1100 Subject: [PATCH 16/18] skip failing test suite (#196319) --- .../trial_license_complete_tier/init_and_status_apis.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/init_and_status_apis.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/init_and_status_apis.ts index 3224caa24d5e2..dd1fe34cd050a 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/init_and_status_apis.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/init_and_status_apis.ts @@ -30,7 +30,8 @@ export default ({ getService }: FtrProviderContext) => { const riskEngineRoutes = riskEngineRouteHelpersFactory(supertest); const log = getService('log'); - describe('@ess @serverless @serverlessQA init_and_status_apis', () => { + // Failing: See https://github.com/elastic/kibana/issues/196319 + describe.skip('@ess @serverless @serverlessQA init_and_status_apis', () => { before(async () => { await riskEngineRoutes.cleanUp(); }); From 50cf1b322e59419b54e19351c217c0b77af3aa0a Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 17 Oct 2024 01:20:48 +1100 Subject: [PATCH 17/18] skip failing test suite (#196153) --- .../apps/triggers_actions_ui/alert_create_flyout.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts index 3c39bd235bf97..121bb753e434b 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts @@ -94,7 +94,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await rules.common.cancelRuleCreation(); } - describe('create alert', function () { + // Failing: See https://github.com/elastic/kibana/issues/196153 + describe.skip('create alert', function () { let apmSynthtraceEsClient: ApmSynthtraceEsClient; before(async () => { await esArchiver.load( From 241a05aa68cbaa737f5ccd92c1e6f91252a3cc9b Mon Sep 17 00:00:00 2001 From: Alexi Doak <109488926+doakalexi@users.noreply.github.com> Date: Wed, 16 Oct 2024 07:25:41 -0700 Subject: [PATCH 18/18] [ResponseOps] Prepare the connector `execute` HTTP API for versioning (#194481) Towards https://github.com/elastic/response-ops-team/issues/125 ## Summary Preparing the `POST ${BASE_ACTION_API_PATH}/connector/{id}/_execute` HTTP API for versioning --------- Co-authored-by: Ying Mao --- .../routes/connector/apis/execute/index.ts | 21 +++++ .../connector/apis/execute/schemas/latest.ts | 8 ++ .../connector/apis/execute/schemas/v1.ts | 20 +++++ .../connector/apis/execute/types/latest.ts | 8 ++ .../routes/connector/apis/execute/types/v1.ts | 12 +++ .../common/routes/connector/response/index.ts | 9 +- .../connector/response/schemas/latest.ts | 1 + .../routes/connector/response/schemas/v1.ts | 52 +++++++++++ .../routes/connector/response/types/v1.ts | 12 +++ .../server/actions_client/actions_client.ts | 87 +++---------------- .../connector/methods/execute/execute.ts | 73 ++++++++++++++++ .../connector/methods/execute/index.ts | 8 ++ .../connector/methods/execute/types/index.ts | 8 ++ .../connector/methods/execute/types/types.ts | 10 +++ .../get_system_action_kibana_privileges.ts | 28 ++++++ .../actions/server/lib/is_preconfigured.ts | 14 +++ .../actions/server/lib/is_system_action.ts | 14 +++ .../{ => connector/execute}/execute.test.ts | 30 +++---- .../routes/{ => connector/execute}/execute.ts | 52 ++++------- .../server/routes/connector/execute/index.ts | 8 ++ .../connector/execute/transforms/index.ts | 10 +++ .../transform_connector_response/latest.ts | 8 ++ .../transform_connector_response/v1.ts | 21 +++++ x-pack/plugins/actions/server/routes/index.ts | 4 +- .../actions/connector_types/cases_webhook.ts | 8 +- .../tests/actions/connector_types/jira.ts | 8 +- .../tests/actions/connector_types/opsgenie.ts | 8 +- .../actions/connector_types/resilient.ts | 8 +- .../connector_types/servicenow_itom.ts | 8 +- .../connector_types/servicenow_itsm.ts | 8 +- .../actions/connector_types/servicenow_sir.ts | 8 +- .../tests/actions/connector_types/swimlane.ts | 8 +- .../tests/actions/connector_types/tines.ts | 8 +- 33 files changed, 426 insertions(+), 164 deletions(-) create mode 100644 x-pack/plugins/actions/common/routes/connector/apis/execute/index.ts create mode 100644 x-pack/plugins/actions/common/routes/connector/apis/execute/schemas/latest.ts create mode 100644 x-pack/plugins/actions/common/routes/connector/apis/execute/schemas/v1.ts create mode 100644 x-pack/plugins/actions/common/routes/connector/apis/execute/types/latest.ts create mode 100644 x-pack/plugins/actions/common/routes/connector/apis/execute/types/v1.ts create mode 100644 x-pack/plugins/actions/server/application/connector/methods/execute/execute.ts create mode 100644 x-pack/plugins/actions/server/application/connector/methods/execute/index.ts create mode 100644 x-pack/plugins/actions/server/application/connector/methods/execute/types/index.ts create mode 100644 x-pack/plugins/actions/server/application/connector/methods/execute/types/types.ts create mode 100644 x-pack/plugins/actions/server/lib/get_system_action_kibana_privileges.ts create mode 100644 x-pack/plugins/actions/server/lib/is_preconfigured.ts create mode 100644 x-pack/plugins/actions/server/lib/is_system_action.ts rename x-pack/plugins/actions/server/routes/{ => connector/execute}/execute.test.ts (85%) rename x-pack/plugins/actions/server/routes/{ => connector/execute}/execute.ts (61%) create mode 100644 x-pack/plugins/actions/server/routes/connector/execute/index.ts create mode 100644 x-pack/plugins/actions/server/routes/connector/execute/transforms/index.ts create mode 100644 x-pack/plugins/actions/server/routes/connector/execute/transforms/transform_connector_response/latest.ts create mode 100644 x-pack/plugins/actions/server/routes/connector/execute/transforms/transform_connector_response/v1.ts diff --git a/x-pack/plugins/actions/common/routes/connector/apis/execute/index.ts b/x-pack/plugins/actions/common/routes/connector/apis/execute/index.ts new file mode 100644 index 0000000000000..448428839336d --- /dev/null +++ b/x-pack/plugins/actions/common/routes/connector/apis/execute/index.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { + executeConnectorRequestParamsSchema, + executeConnectorRequestBodySchema, +} from './schemas/latest'; +export type { ExecuteConnectorRequestParams, ExecuteConnectorRequestBody } from './types/latest'; + +export { + executeConnectorRequestParamsSchema as executeConnectorRequestParamsSchemaV1, + executeConnectorRequestBodySchema as executeConnectorRequestBodySchemaV1, +} from './schemas/v1'; +export type { + ExecuteConnectorRequestParams as ExecuteConnectorRequestParamsV1, + ExecuteConnectorRequestBody as ExecuteConnectorRequestBodyV1, +} from './types/v1'; diff --git a/x-pack/plugins/actions/common/routes/connector/apis/execute/schemas/latest.ts b/x-pack/plugins/actions/common/routes/connector/apis/execute/schemas/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/plugins/actions/common/routes/connector/apis/execute/schemas/latest.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './v1'; diff --git a/x-pack/plugins/actions/common/routes/connector/apis/execute/schemas/v1.ts b/x-pack/plugins/actions/common/routes/connector/apis/execute/schemas/v1.ts new file mode 100644 index 0000000000000..1f41763a004a2 --- /dev/null +++ b/x-pack/plugins/actions/common/routes/connector/apis/execute/schemas/v1.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; + +export const executeConnectorRequestParamsSchema = schema.object({ + id: schema.string({ + meta: { + description: 'An identifier for the connector.', + }, + }), +}); + +export const executeConnectorRequestBodySchema = schema.object({ + params: schema.recordOf(schema.string(), schema.any()), +}); diff --git a/x-pack/plugins/actions/common/routes/connector/apis/execute/types/latest.ts b/x-pack/plugins/actions/common/routes/connector/apis/execute/types/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/plugins/actions/common/routes/connector/apis/execute/types/latest.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './v1'; diff --git a/x-pack/plugins/actions/common/routes/connector/apis/execute/types/v1.ts b/x-pack/plugins/actions/common/routes/connector/apis/execute/types/v1.ts new file mode 100644 index 0000000000000..cc1b6e4cdc196 --- /dev/null +++ b/x-pack/plugins/actions/common/routes/connector/apis/execute/types/v1.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 type { TypeOf } from '@kbn/config-schema'; +import { executeConnectorRequestParamsSchemaV1, executeConnectorRequestBodySchemaV1 } from '..'; + +export type ExecuteConnectorRequestParams = TypeOf; +export type ExecuteConnectorRequestBody = TypeOf; diff --git a/x-pack/plugins/actions/common/routes/connector/response/index.ts b/x-pack/plugins/actions/common/routes/connector/response/index.ts index c870698329052..3a58325a542ed 100644 --- a/x-pack/plugins/actions/common/routes/connector/response/index.ts +++ b/x-pack/plugins/actions/common/routes/connector/response/index.ts @@ -6,11 +6,16 @@ */ // Latest -export type { ConnectorResponse, AllConnectorsResponse } from './types/latest'; +export type { + ConnectorResponse, + AllConnectorsResponse, + ConnectorExecuteResponse, +} from './types/latest'; export { connectorResponseSchema, allConnectorsResponseSchema, connectorTypesResponseSchema, + connectorExecuteResponseSchema, } from './schemas/latest'; // v1 @@ -18,9 +23,11 @@ export type { ConnectorResponse as ConnectorResponseV1, AllConnectorsResponse as AllConnectorsResponseV1, ConnectorTypesResponse as ConnectorTypesResponseV1, + ConnectorExecuteResponse as ConnectorExecuteResponseV1, } from './types/v1'; export { connectorResponseSchema as connectorResponseSchemaV1, allConnectorsResponseSchema as connectorWithExtraFindDataSchemaV1, connectorTypesResponseSchema as connectorTypesResponseSchemaV1, + connectorExecuteResponseSchema as connectorExecuteResponseSchemaV1, } from './schemas/v1'; diff --git a/x-pack/plugins/actions/common/routes/connector/response/schemas/latest.ts b/x-pack/plugins/actions/common/routes/connector/response/schemas/latest.ts index bc4edc5be46d0..c89efd04bc485 100644 --- a/x-pack/plugins/actions/common/routes/connector/response/schemas/latest.ts +++ b/x-pack/plugins/actions/common/routes/connector/response/schemas/latest.ts @@ -8,3 +8,4 @@ export { connectorResponseSchema } from './v1'; export { allConnectorsResponseSchema } from './v1'; export { connectorTypesResponseSchema } from './v1'; +export { connectorExecuteResponseSchema } from './v1'; diff --git a/x-pack/plugins/actions/common/routes/connector/response/schemas/v1.ts b/x-pack/plugins/actions/common/routes/connector/response/schemas/v1.ts index 5c9b95ca8fc7b..096e2f2943d80 100644 --- a/x-pack/plugins/actions/common/routes/connector/response/schemas/v1.ts +++ b/x-pack/plugins/actions/common/routes/connector/response/schemas/v1.ts @@ -98,3 +98,55 @@ export const connectorTypesResponseSchema = schema.object({ meta: { description: 'Indicates whether the action is a system action.' }, }), }); + +export const connectorExecuteResponseSchema = schema.object({ + connector_id: schema.string({ + meta: { + description: 'The identifier for the connector.', + }, + }), + status: schema.oneOf([schema.literal('ok'), schema.literal('error')], { + meta: { + description: 'The outcome of the connector execution.', + }, + }), + message: schema.maybe( + schema.string({ + meta: { + description: 'The connector execution error message.', + }, + }) + ), + service_message: schema.maybe( + schema.string({ + meta: { + description: 'An error message that contains additional details.', + }, + }) + ), + data: schema.maybe( + schema.any({ + meta: { + description: 'The connector execution data.', + }, + }) + ), + retry: schema.maybe( + schema.nullable( + schema.oneOf([schema.boolean(), schema.string()], { + meta: { + description: + 'When the status is error, identifies whether the connector execution will retry .', + }, + }) + ) + ), + errorSource: schema.maybe( + schema.oneOf([schema.literal('user'), schema.literal('framework')], { + meta: { + description: + 'When the status is error, identifies whether the error is a framework error or a user error.', + }, + }) + ), +}); diff --git a/x-pack/plugins/actions/common/routes/connector/response/types/v1.ts b/x-pack/plugins/actions/common/routes/connector/response/types/v1.ts index 3bf7401d2d0e0..499cc2ec21d48 100644 --- a/x-pack/plugins/actions/common/routes/connector/response/types/v1.ts +++ b/x-pack/plugins/actions/common/routes/connector/response/types/v1.ts @@ -10,6 +10,7 @@ import { connectorResponseSchemaV1, connectorTypesResponseSchemaV1, allConnectorsResponseSchema, + connectorExecuteResponseSchema, } from '..'; type ConnectorResponseSchemaType = TypeOf; @@ -41,3 +42,14 @@ export interface ConnectorTypesResponse { supported_feature_ids: ConnectorTypesResponseSchemaType['supported_feature_ids']; is_system_action_type: ConnectorTypesResponseSchemaType['is_system_action_type']; } + +type ConnectorExecuteResponseSchemaType = TypeOf; +export interface ConnectorExecuteResponse { + connector_id: ConnectorExecuteResponseSchemaType['connector_id']; + status: ConnectorExecuteResponseSchemaType['status']; + message?: ConnectorExecuteResponseSchemaType['message']; + service_message?: ConnectorExecuteResponseSchemaType['service_message']; + data?: ConnectorExecuteResponseSchemaType['data']; + retry?: ConnectorExecuteResponseSchemaType['retry']; + errorSource?: ConnectorExecuteResponseSchemaType['errorSource']; +} diff --git a/x-pack/plugins/actions/server/actions_client/actions_client.ts b/x-pack/plugins/actions/server/actions_client/actions_client.ts index f485d82b2f120..edad072acbca6 100644 --- a/x-pack/plugins/actions/server/actions_client/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client/actions_client.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { v4 as uuidv4 } from 'uuid'; import Boom from '@hapi/boom'; import url from 'url'; import { UsageCounter } from '@kbn/usage-collection-plugin/server'; @@ -30,6 +29,7 @@ import { get } from '../application/connector/methods/get'; import { getAll, getAllSystemConnectors } from '../application/connector/methods/get_all'; import { update } from '../application/connector/methods/update'; import { listTypes } from '../application/connector/methods/list_types'; +import { execute } from '../application/connector/methods/execute'; import { GetGlobalExecutionKPIParams, GetGlobalExecutionLogParams, @@ -54,7 +54,6 @@ import { HookServices, } from '../types'; import { PreconfiguredActionDisabledModificationError } from '../lib/errors/preconfigured_action_disabled_modification'; -import { ExecuteOptions } from '../lib/action_executor'; import { ExecutionEnqueuer, ExecuteOptions as EnqueueExecutionOptions, @@ -96,6 +95,9 @@ import { connectorFromSavedObject, isConnectorDeprecated } from '../application/ import { ListTypesParams } from '../application/connector/methods/list_types/types'; import { ConnectorUpdateParams } from '../application/connector/methods/update/types'; import { ConnectorUpdate } from '../application/connector/methods/update/types/types'; +import { isPreconfigured } from '../lib/is_preconfigured'; +import { isSystemAction } from '../lib/is_system_action'; +import { ConnectorExecuteParams } from '../application/connector/methods/execute/types'; interface Action extends ConnectorUpdate { actionTypeId: string; @@ -649,75 +651,10 @@ export class ActionsClient { return result; } - private getSystemActionKibanaPrivileges(connectorId: string, params?: ExecuteOptions['params']) { - const inMemoryConnector = this.context.inMemoryConnectors.find( - (connector) => connector.id === connectorId - ); - - const additionalPrivileges = inMemoryConnector?.isSystemAction - ? this.context.actionTypeRegistry.getSystemActionKibanaPrivileges( - inMemoryConnector.actionTypeId, - params - ) - : []; - - return additionalPrivileges; - } - - public async execute({ - actionId, - params, - source, - relatedSavedObjects, - }: Omit): Promise< - ActionTypeExecutorResult - > { - const log = this.context.logger; - - if ( - (await getAuthorizationModeBySource(this.context.unsecuredSavedObjectsClient, source)) === - AuthorizationMode.RBAC - ) { - const additionalPrivileges = this.getSystemActionKibanaPrivileges(actionId, params); - let actionTypeId: string | undefined; - - try { - if (this.isPreconfigured(actionId) || this.isSystemAction(actionId)) { - const connector = this.context.inMemoryConnectors.find( - (inMemoryConnector) => inMemoryConnector.id === actionId - ); - - actionTypeId = connector?.actionTypeId; - } else { - // TODO: Optimize so we don't do another get on top of getAuthorizationModeBySource and within the actionExecutor.execute - const { attributes } = await this.context.unsecuredSavedObjectsClient.get( - 'action', - actionId - ); - - actionTypeId = attributes.actionTypeId; - } - } catch (err) { - log.debug(`Failed to retrieve actionTypeId for action [${actionId}]`, err); - } - - await this.context.authorization.ensureAuthorized({ - operation: 'execute', - additionalPrivileges, - actionTypeId, - }); - } else { - trackLegacyRBACExemption('execute', this.context.usageCounter); - } - - return this.context.actionExecutor.execute({ - actionId, - params, - source, - request: this.context.request, - relatedSavedObjects, - actionExecutionId: uuidv4(), - }); + public async execute( + connectorExecuteParams: ConnectorExecuteParams + ): Promise> { + return execute(this.context, connectorExecuteParams); } public async bulkEnqueueExecution( @@ -789,15 +726,11 @@ export class ActionsClient { } public isPreconfigured(connectorId: string): boolean { - return !!this.context.inMemoryConnectors.find( - (connector) => connector.isPreconfigured && connector.id === connectorId - ); + return isPreconfigured(this.context, connectorId); } public isSystemAction(connectorId: string): boolean { - return !!this.context.inMemoryConnectors.find( - (connector) => connector.isSystemAction && connector.id === connectorId - ); + return isSystemAction(this.context, connectorId); } public async getGlobalExecutionLogWithAuth({ diff --git a/x-pack/plugins/actions/server/application/connector/methods/execute/execute.ts b/x-pack/plugins/actions/server/application/connector/methods/execute/execute.ts new file mode 100644 index 0000000000000..f9922e0b61a8d --- /dev/null +++ b/x-pack/plugins/actions/server/application/connector/methods/execute/execute.ts @@ -0,0 +1,73 @@ +/* + * 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 { v4 as uuidv4 } from 'uuid'; +import { RawAction, ActionTypeExecutorResult } from '../../../../types'; +import { getSystemActionKibanaPrivileges } from '../../../../lib/get_system_action_kibana_privileges'; +import { isPreconfigured } from '../../../../lib/is_preconfigured'; +import { isSystemAction } from '../../../../lib/is_system_action'; +import { + getAuthorizationModeBySource, + AuthorizationMode, +} from '../../../../authorization/get_authorization_mode_by_source'; +import { trackLegacyRBACExemption } from '../../../../lib/track_legacy_rbac_exemption'; +import { ConnectorExecuteParams } from './types'; +import { ACTION_SAVED_OBJECT_TYPE } from '../../../../constants/saved_objects'; +import { ActionsClientContext } from '../../../../actions_client'; + +export async function execute( + context: ActionsClientContext, + connectorExecuteParams: ConnectorExecuteParams +): Promise> { + const log = context.logger; + const { actionId, params, source, relatedSavedObjects } = connectorExecuteParams; + + if ( + (await getAuthorizationModeBySource(context.unsecuredSavedObjectsClient, source)) === + AuthorizationMode.RBAC + ) { + const additionalPrivileges = getSystemActionKibanaPrivileges(context, actionId, params); + let actionTypeId: string | undefined; + + try { + if (isPreconfigured(context, actionId) || isSystemAction(context, actionId)) { + const connector = context.inMemoryConnectors.find( + (inMemoryConnector) => inMemoryConnector.id === actionId + ); + + actionTypeId = connector?.actionTypeId; + } else { + // TODO: Optimize so we don't do another get on top of getAuthorizationModeBySource and within the actionExecutor.execute + const { attributes } = await context.unsecuredSavedObjectsClient.get( + ACTION_SAVED_OBJECT_TYPE, + actionId + ); + + actionTypeId = attributes.actionTypeId; + } + } catch (err) { + log.debug(`Failed to retrieve actionTypeId for action [${actionId}]`, err); + } + + await context.authorization.ensureAuthorized({ + operation: 'execute', + additionalPrivileges, + actionTypeId, + }); + } else { + trackLegacyRBACExemption('execute', context.usageCounter); + } + + return context.actionExecutor.execute({ + actionId, + params, + source, + request: context.request, + relatedSavedObjects, + actionExecutionId: uuidv4(), + }); +} diff --git a/x-pack/plugins/actions/server/application/connector/methods/execute/index.ts b/x-pack/plugins/actions/server/application/connector/methods/execute/index.ts new file mode 100644 index 0000000000000..21598e68a047c --- /dev/null +++ b/x-pack/plugins/actions/server/application/connector/methods/execute/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { execute } from './execute'; diff --git a/x-pack/plugins/actions/server/application/connector/methods/execute/types/index.ts b/x-pack/plugins/actions/server/application/connector/methods/execute/types/index.ts new file mode 100644 index 0000000000000..ff2bc6be97a80 --- /dev/null +++ b/x-pack/plugins/actions/server/application/connector/methods/execute/types/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type { ConnectorExecuteParams } from './types'; diff --git a/x-pack/plugins/actions/server/application/connector/methods/execute/types/types.ts b/x-pack/plugins/actions/server/application/connector/methods/execute/types/types.ts new file mode 100644 index 0000000000000..22aa019de599f --- /dev/null +++ b/x-pack/plugins/actions/server/application/connector/methods/execute/types/types.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ExecuteOptions } from '../../../../../lib/action_executor'; + +export type ConnectorExecuteParams = Omit; diff --git a/x-pack/plugins/actions/server/lib/get_system_action_kibana_privileges.ts b/x-pack/plugins/actions/server/lib/get_system_action_kibana_privileges.ts new file mode 100644 index 0000000000000..ef3b8ff853d17 --- /dev/null +++ b/x-pack/plugins/actions/server/lib/get_system_action_kibana_privileges.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ActionsClientContext } from '../actions_client'; +import { ExecuteOptions } from './action_executor'; + +export function getSystemActionKibanaPrivileges( + context: ActionsClientContext, + connectorId: string, + params?: ExecuteOptions['params'] +) { + const inMemoryConnector = context.inMemoryConnectors.find( + (connector) => connector.id === connectorId + ); + + const additionalPrivileges = inMemoryConnector?.isSystemAction + ? context.actionTypeRegistry.getSystemActionKibanaPrivileges( + inMemoryConnector.actionTypeId, + params + ) + : []; + + return additionalPrivileges; +} diff --git a/x-pack/plugins/actions/server/lib/is_preconfigured.ts b/x-pack/plugins/actions/server/lib/is_preconfigured.ts new file mode 100644 index 0000000000000..9f42c496d7cb2 --- /dev/null +++ b/x-pack/plugins/actions/server/lib/is_preconfigured.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ActionsClientContext } from '../actions_client'; + +export function isPreconfigured(context: ActionsClientContext, connectorId: string): boolean { + return !!context.inMemoryConnectors.find( + (connector) => connector.isPreconfigured && connector.id === connectorId + ); +} diff --git a/x-pack/plugins/actions/server/lib/is_system_action.ts b/x-pack/plugins/actions/server/lib/is_system_action.ts new file mode 100644 index 0000000000000..e21e1ee480df8 --- /dev/null +++ b/x-pack/plugins/actions/server/lib/is_system_action.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ActionsClientContext } from '../actions_client'; + +export function isSystemAction(context: ActionsClientContext, connectorId: string): boolean { + return !!context.inMemoryConnectors.find( + (connector) => connector.isSystemAction && connector.id === connectorId + ); +} diff --git a/x-pack/plugins/actions/server/routes/execute.test.ts b/x-pack/plugins/actions/server/routes/connector/execute/execute.test.ts similarity index 85% rename from x-pack/plugins/actions/server/routes/execute.test.ts rename to x-pack/plugins/actions/server/routes/connector/execute/execute.test.ts index 39319ff1dabf2..a9ae5e881f141 100644 --- a/x-pack/plugins/actions/server/routes/execute.test.ts +++ b/x-pack/plugins/actions/server/routes/connector/execute/execute.test.ts @@ -5,16 +5,16 @@ * 2.0. */ -import { executeActionRoute } from './execute'; +import { executeConnectorRoute } from './execute'; import { httpServiceMock } from '@kbn/core/server/mocks'; -import { licenseStateMock } from '../lib/license_state.mock'; -import { mockHandlerArguments } from './legacy/_mock_handler_arguments'; -import { asHttpRequestExecutionSource } from '../lib'; -import { actionsClientMock } from '../actions_client/actions_client.mock'; -import { ActionTypeExecutorResult } from '../types'; -import { verifyAccessAndContext } from './verify_access_and_context'; - -jest.mock('./verify_access_and_context', () => ({ +import { licenseStateMock } from '../../../lib/license_state.mock'; +import { mockHandlerArguments } from '../../legacy/_mock_handler_arguments'; +import { asHttpRequestExecutionSource } from '../../../lib'; +import { actionsClientMock } from '../../../actions_client/actions_client.mock'; +import { ActionTypeExecutorResult } from '../../../types'; +import { verifyAccessAndContext } from '../../verify_access_and_context'; + +jest.mock('../../verify_access_and_context', () => ({ verifyAccessAndContext: jest.fn(), })); @@ -23,7 +23,7 @@ beforeEach(() => { (verifyAccessAndContext as jest.Mock).mockImplementation((license, handler) => handler); }); -describe('executeActionRoute', () => { +describe('executeConnectorRoute', () => { beforeEach(() => { jest.clearAllMocks(); }); @@ -55,7 +55,7 @@ describe('executeActionRoute', () => { status: 'ok', }; - executeActionRoute(router, licenseState); + executeConnectorRoute(router, licenseState); const [config, handler] = router.post.mock.calls[0]; @@ -95,7 +95,7 @@ describe('executeActionRoute', () => { ['noContent'] ); - executeActionRoute(router, licenseState); + executeConnectorRoute(router, licenseState); const [, handler] = router.post.mock.calls[0]; @@ -131,7 +131,7 @@ describe('executeActionRoute', () => { ['ok'] ); - executeActionRoute(router, licenseState); + executeConnectorRoute(router, licenseState); const [, handler] = router.post.mock.calls[0]; @@ -163,7 +163,7 @@ describe('executeActionRoute', () => { ['ok'] ); - executeActionRoute(router, licenseState); + executeConnectorRoute(router, licenseState); const [, handler] = router.post.mock.calls[0]; @@ -192,7 +192,7 @@ describe('executeActionRoute', () => { ['ok'] ); - executeActionRoute(router, licenseState); + executeConnectorRoute(router, licenseState); const [_, handler] = router.post.mock.calls[0]; diff --git a/x-pack/plugins/actions/server/routes/execute.ts b/x-pack/plugins/actions/server/routes/connector/execute/execute.ts similarity index 61% rename from x-pack/plugins/actions/server/routes/execute.ts rename to x-pack/plugins/actions/server/routes/connector/execute/execute.ts index 74813a73474ac..ab5ed25ff5f78 100644 --- a/x-pack/plugins/actions/server/routes/execute.ts +++ b/x-pack/plugins/actions/server/routes/connector/execute/execute.ts @@ -5,37 +5,23 @@ * 2.0. */ -import { schema } from '@kbn/config-schema'; import { IRouter } from '@kbn/core/server'; -import { ILicenseState } from '../lib'; +import { ILicenseState } from '../../../lib'; -import { ActionTypeExecutorResult, ActionsRequestHandlerContext } from '../types'; -import { BASE_ACTION_API_PATH, RewriteResponseCase } from '../../common'; -import { asHttpRequestExecutionSource } from '../lib/action_execution_source'; -import { verifyAccessAndContext } from './verify_access_and_context'; -import { connectorResponseSchemaV1 } from '../../common/routes/connector/response'; +import { ActionTypeExecutorResult, ActionsRequestHandlerContext } from '../../../types'; +import { BASE_ACTION_API_PATH } from '../../../../common'; +import { asHttpRequestExecutionSource } from '../../../lib/action_execution_source'; +import { verifyAccessAndContext } from '../../verify_access_and_context'; +import { connectorResponseSchemaV1 } from '../../../../common/routes/connector/response'; +import { + executeConnectorRequestBodySchemaV1, + ExecuteConnectorRequestBodyV1, + executeConnectorRequestParamsSchemaV1, + ExecuteConnectorRequestParamsV1, +} from '../../../../common/routes/connector/apis/execute'; +import { transformExecuteConnectorResponseV1 } from './transforms'; -const paramSchema = schema.object({ - id: schema.string({ - meta: { description: 'An identifier for the connector.' }, - }), -}); - -const bodySchema = schema.object({ - params: schema.recordOf(schema.string(), schema.any()), -}); - -const rewriteBodyRes: RewriteResponseCase> = ({ - actionId, - serviceMessage, - ...res -}) => ({ - ...res, - connector_id: actionId, - ...(serviceMessage ? { service_message: serviceMessage } : {}), -}); - -export const executeActionRoute = ( +export const executeConnectorRoute = ( router: IRouter, licenseState: ILicenseState ) => { @@ -51,8 +37,8 @@ export const executeActionRoute = ( }, validate: { request: { - body: bodySchema, - params: paramSchema, + body: executeConnectorRequestBodySchemaV1, + params: executeConnectorRequestParamsSchemaV1, }, response: { 200: { @@ -65,8 +51,8 @@ export const executeActionRoute = ( router.handleLegacyErrors( verifyAccessAndContext(licenseState, async function (context, req, res) { const actionsClient = (await context.actions).getActionsClient(); - const { params } = req.body; - const { id } = req.params; + const { params }: ExecuteConnectorRequestBodyV1 = req.body; + const { id }: ExecuteConnectorRequestParamsV1 = req.params; if (actionsClient.isSystemAction(id)) { return res.badRequest({ body: 'Execution of system action is not allowed' }); @@ -81,7 +67,7 @@ export const executeActionRoute = ( return body ? res.ok({ - body: rewriteBodyRes(body), + body: transformExecuteConnectorResponseV1(body), }) : res.noContent(); }) diff --git a/x-pack/plugins/actions/server/routes/connector/execute/index.ts b/x-pack/plugins/actions/server/routes/connector/execute/index.ts new file mode 100644 index 0000000000000..6f5cb866722b7 --- /dev/null +++ b/x-pack/plugins/actions/server/routes/connector/execute/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { executeConnectorRoute } from './execute'; diff --git a/x-pack/plugins/actions/server/routes/connector/execute/transforms/index.ts b/x-pack/plugins/actions/server/routes/connector/execute/transforms/index.ts new file mode 100644 index 0000000000000..5c245970ec914 --- /dev/null +++ b/x-pack/plugins/actions/server/routes/connector/execute/transforms/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { transformExecuteConnectorResponse } from './transform_connector_response/latest'; + +export { transformExecuteConnectorResponse as transformExecuteConnectorResponseV1 } from './transform_connector_response/v1'; diff --git a/x-pack/plugins/actions/server/routes/connector/execute/transforms/transform_connector_response/latest.ts b/x-pack/plugins/actions/server/routes/connector/execute/transforms/transform_connector_response/latest.ts new file mode 100644 index 0000000000000..900d86f842fc6 --- /dev/null +++ b/x-pack/plugins/actions/server/routes/connector/execute/transforms/transform_connector_response/latest.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { transformExecuteConnectorResponse } from './v1'; diff --git a/x-pack/plugins/actions/server/routes/connector/execute/transforms/transform_connector_response/v1.ts b/x-pack/plugins/actions/server/routes/connector/execute/transforms/transform_connector_response/v1.ts new file mode 100644 index 0000000000000..bc001cd9f9103 --- /dev/null +++ b/x-pack/plugins/actions/server/routes/connector/execute/transforms/transform_connector_response/v1.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ConnectorExecuteResponseV1 } from '../../../../../../common/routes/connector/response'; +import { ActionTypeExecutorResult } from '../../../../../types'; + +export const transformExecuteConnectorResponse = ({ + actionId, + retry, + serviceMessage, + ...res +}: ActionTypeExecutorResult): ConnectorExecuteResponseV1 => ({ + ...res, + connector_id: actionId, + ...(retry && retry instanceof Date ? { retry: retry.toISOString() } : { retry }), + ...(serviceMessage ? { service_message: serviceMessage } : {}), +}); diff --git a/x-pack/plugins/actions/server/routes/index.ts b/x-pack/plugins/actions/server/routes/index.ts index cccca87d849e2..5ea804d1ce47e 100644 --- a/x-pack/plugins/actions/server/routes/index.ts +++ b/x-pack/plugins/actions/server/routes/index.ts @@ -15,7 +15,7 @@ import { ILicenseState } from '../lib'; import { ActionsRequestHandlerContext } from '../types'; import { createActionRoute } from './create'; import { deleteConnectorRoute } from './connector/delete'; -import { executeActionRoute } from './execute'; +import { executeConnectorRoute } from './connector/execute'; import { getConnectorRoute } from './connector/get'; import { updateConnectorRoute } from './connector/update'; import { getOAuthAccessToken } from './get_oauth_access_token'; @@ -42,7 +42,7 @@ export function defineRoutes(opts: RouteOptions) { getAllConnectorsRoute(router, licenseState); updateConnectorRoute(router, licenseState); listTypesRoute(router, licenseState); - executeActionRoute(router, licenseState); + executeConnectorRoute(router, licenseState); getGlobalExecutionLogRoute(router, licenseState); getGlobalExecutionKPIRoute(router, licenseState); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/cases_webhook.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/cases_webhook.ts index 1ef7b170a4f0d..fcf0f2d84e755 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/cases_webhook.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/cases_webhook.ts @@ -246,12 +246,12 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) { params: {}, }) .then((resp: any) => { - expect(Object.keys(resp.body)).to.eql([ - 'status', + expect(Object.keys(resp.body).sort()).to.eql([ + 'connector_id', + 'errorSource', 'message', 'retry', - 'errorSource', - 'connector_id', + 'status', ]); expect(resp.body.connector_id).to.eql(simulatedActionId); expect(resp.body.status).to.eql('error'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/jira.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/jira.ts index 2268e379f441a..d41f8f1fcad71 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/jira.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/jira.ts @@ -236,12 +236,12 @@ export default function jiraTest({ getService }: FtrProviderContext) { params: {}, }) .then((resp: any) => { - expect(Object.keys(resp.body)).to.eql([ - 'status', + expect(Object.keys(resp.body).sort()).to.eql([ + 'connector_id', + 'errorSource', 'message', 'retry', - 'errorSource', - 'connector_id', + 'status', ]); expect(resp.body.connector_id).to.eql(simulatedActionId); expect(resp.body.status).to.eql('error'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/opsgenie.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/opsgenie.ts index bd315edfb0459..0c5f52862b9de 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/opsgenie.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/opsgenie.ts @@ -169,12 +169,12 @@ export default function opsgenieTest({ getService }: FtrProviderContext) { }); expect(200); - expect(Object.keys(body)).to.eql([ - 'status', + expect(Object.keys(body).sort()).to.eql([ + 'connector_id', + 'errorSource', 'message', 'retry', - 'errorSource', - 'connector_id', + 'status', ]); expect(body.connector_id).to.eql(opsgenieActionId); expect(body.status).to.eql('error'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/resilient.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/resilient.ts index 6dfb420463e9f..232668c24749c 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/resilient.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/resilient.ts @@ -230,12 +230,12 @@ export default function resilientTest({ getService }: FtrProviderContext) { params: {}, }) .then((resp: any) => { - expect(Object.keys(resp.body)).to.eql([ - 'status', + expect(Object.keys(resp.body).sort()).to.eql([ + 'connector_id', + 'errorSource', 'message', 'retry', - 'errorSource', - 'connector_id', + 'status', ]); expect(resp.body.connector_id).to.eql(resilientActionId); expect(resp.body.status).to.eql('error'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/servicenow_itom.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/servicenow_itom.ts index 0f1748db4f5ef..c189580951495 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/servicenow_itom.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/servicenow_itom.ts @@ -416,12 +416,12 @@ export default function serviceNowITOMTest({ getService }: FtrProviderContext) { params: {}, }) .then((resp: any) => { - expect(Object.keys(resp.body)).to.eql([ - 'status', + expect(Object.keys(resp.body).sort()).to.eql([ + 'connector_id', + 'errorSource', 'message', 'retry', - 'errorSource', - 'connector_id', + 'status', ]); expect(resp.body.connector_id).to.eql(simulatedActionId); expect(resp.body.status).to.eql('error'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/servicenow_itsm.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/servicenow_itsm.ts index bc0f48f15caf5..1f4f01db068d9 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/servicenow_itsm.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/servicenow_itsm.ts @@ -452,12 +452,12 @@ export default function serviceNowITSMTest({ getService }: FtrProviderContext) { params: {}, }) .then((resp: any) => { - expect(Object.keys(resp.body)).to.eql([ - 'status', + expect(Object.keys(resp.body).sort()).to.eql([ + 'connector_id', + 'errorSource', 'message', 'retry', - 'errorSource', - 'connector_id', + 'status', ]); expect(resp.body.connector_id).to.eql(simulatedActionId); expect(resp.body.status).to.eql('error'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/servicenow_sir.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/servicenow_sir.ts index 717a44a406712..527ea53bbd1d5 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/servicenow_sir.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/servicenow_sir.ts @@ -465,12 +465,12 @@ export default function serviceNowSIRTest({ getService }: FtrProviderContext) { params: {}, }) .then((resp: any) => { - expect(Object.keys(resp.body)).to.eql([ - 'status', + expect(Object.keys(resp.body).sort()).to.eql([ + 'connector_id', + 'errorSource', 'message', 'retry', - 'errorSource', - 'connector_id', + 'status', ]); expect(resp.body.connector_id).to.eql(simulatedActionId); expect(resp.body.status).to.eql('error'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/swimlane.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/swimlane.ts index 4d91fdddf80dd..93c2e4bc973af 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/swimlane.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/swimlane.ts @@ -327,12 +327,12 @@ export default function swimlaneTest({ getService }: FtrProviderContext) { params: {}, }) .then((resp: any) => { - expect(Object.keys(resp.body)).to.eql([ - 'status', + expect(Object.keys(resp.body).sort()).to.eql([ + 'connector_id', + 'errorSource', 'message', 'retry', - 'errorSource', - 'connector_id', + 'status', ]); expect(resp.body.connector_id).to.eql(simulatedActionId); expect(resp.body.status).to.eql('error'); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/tines.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/tines.ts index 04971990f879e..25b3b4b35cc76 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/tines.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types/tines.ts @@ -188,12 +188,12 @@ export default function tinesTest({ getService }: FtrProviderContext) { }); expect(200); - expect(Object.keys(body)).to.eql([ - 'status', + expect(Object.keys(body).sort()).to.eql([ + 'connector_id', + 'errorSource', 'message', 'retry', - 'errorSource', - 'connector_id', + 'status', ]); expect(body.connector_id).to.eql(tinesActionId); expect(body.status).to.eql('error');