From 12998a8fe1823c3ba672ad8ff68a1292ccc72d8e Mon Sep 17 00:00:00 2001 From: Alexi Doak <109488926+doakalexi@users.noreply.github.com> Date: Tue, 21 Jan 2025 13:33:54 -0800 Subject: [PATCH] [ResponseOps] Granular connector RBAC followup (#205818) ## Summary This PR is followup to, https://github.com/elastic/kibana/pull/203503. This PR adds a test to make sure that sub-feature description remains accurate, and changes to hide the connector edit test tab and create connector button when a user only has read access. ### Checklist - [ ] [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 ### To verify 1. Create a new read only role and disable EDR connectors under the Actions and Connectors privilege 2. Create a new user and assign that role to user 3. Create a Sentinel One connector (It doesn't need to work, you can use fake values for the url and token) 4. Login as the new user and go to the connector page in stack management 5. Verify that the "Create connector" button is not visible 6. Click on the connector you created, verify that you can't see the test tab --- .../plugins/shared/actions/server/feature.ts | 2 +- .../crowdstrike/crowdstrike.ts | 1 + .../microsoft_defender_endpoint.ts | 1 + .../sentinelone/sentinelone.ts | 1 + .../actions_connectors_home.test.tsx | 43 +++++++++++++++- .../components/actions_connectors_home.tsx | 9 +++- .../group2/tests/actions/index.ts | 1 + .../tests/actions/sub_feature_descriptions.ts | 51 +++++++++++++++++++ 8 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/sub_feature_descriptions.ts diff --git a/x-pack/platform/plugins/shared/actions/server/feature.ts b/x-pack/platform/plugins/shared/actions/server/feature.ts index b9997ce64365f..e0ee3b905af0f 100644 --- a/x-pack/platform/plugins/shared/actions/server/feature.ts +++ b/x-pack/platform/plugins/shared/actions/server/feature.ts @@ -77,7 +77,7 @@ export const ACTIONS_FEATURE: KibanaFeatureConfig = { description: i18n.translate( 'xpack.actions.featureRegistry.endpointSecuritySubFeatureDescription', { - defaultMessage: 'Includes: Sentinel One, Crowdstrike', + defaultMessage: 'Includes: Sentinel One, CrowdStrike, Microsoft Defender for Endpoint', } ), privilegeGroups: [ diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/crowdstrike/crowdstrike.ts b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/crowdstrike/crowdstrike.ts index 2a928ed7d64de..07f0ba0891b21 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/crowdstrike/crowdstrike.ts +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/crowdstrike/crowdstrike.ts @@ -61,5 +61,6 @@ export function getConnectorType(): ConnectorTypeModel< }, actionConnectorFields: lazy(() => import('./crowdstrike_connector')), actionParamsFields: lazy(() => import('./crowdstrike_params')), + subFeature: 'endpointSecurity', }; } diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/microsoft_defender_endpoint/microsoft_defender_endpoint.ts b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/microsoft_defender_endpoint/microsoft_defender_endpoint.ts index 0e1c1f08b3bc5..3d6a52f731068 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/microsoft_defender_endpoint/microsoft_defender_endpoint.ts +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/microsoft_defender_endpoint/microsoft_defender_endpoint.ts @@ -61,5 +61,6 @@ export function getConnectorType(): ConnectorTypeModel< }, actionConnectorFields: lazy(() => import('./microsoft_defender_endpoint_connector')), actionParamsFields: lazy(() => import('./microsoft_defender_endpoint_params')), + subFeature: 'endpointSecurity', }; } diff --git a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/sentinelone/sentinelone.ts b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/sentinelone/sentinelone.ts index a65fc6a4f011c..a8b01f214478f 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/sentinelone/sentinelone.ts +++ b/x-pack/platform/plugins/shared/stack_connectors/public/connector_types/sentinelone/sentinelone.ts @@ -61,5 +61,6 @@ export function getConnectorType(): ConnectorTypeModel< }, actionConnectorFields: lazy(() => import('./sentinelone_connector')), actionParamsFields: lazy(() => import('./sentinelone_params')), + subFeature: 'endpointSecurity', }; } diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_home.test.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_home.test.tsx index 7a51b74699562..493e3d4ebf41e 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_home.test.tsx +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_home.test.tsx @@ -21,7 +21,10 @@ jest.mock('../../../lib/action_connector_api', () => ({ })); const { loadAllActions } = jest.requireMock('../../../lib/action_connector_api'); jest.mock('../../../../common/lib/kibana'); -jest.mock('../../../lib/capabilities'); +jest.mock('../../../lib/capabilities', () => ({ + hasSaveActionsCapability: jest.fn(), +})); +const { hasSaveActionsCapability } = jest.requireMock('../../../lib/capabilities'); jest.mock('../../../../common/get_experimental_features'); jest.mock('../../../components/health_check', () => ({ HealthCheck: ({ children }: { children: React.ReactNode }) => <>{children}, @@ -48,6 +51,11 @@ jest.mock('./actions_connectors_event_log_list_table', () => { const queryClient = new QueryClient(); describe('ActionsConnectorsHome', () => { + beforeEach(() => { + jest.clearAllMocks(); + hasSaveActionsCapability.mockReturnValue(true); + }); + it('renders Actions connectors list component', async () => { const props: RouteComponentProps = { history: createMemoryHistory({ @@ -240,4 +248,37 @@ describe('ActionsConnectorsHome', () => { }); expect(selectConnectorFlyout).toBeInTheDocument(); }); + + it('hide "Create connector" button when the user only has read access', async () => { + hasSaveActionsCapability.mockReturnValue(false); + const props: RouteComponentProps = { + history: createMemoryHistory({ + initialEntries: ['/connectors'], + }), + location: createLocation('/connectors'), + match: { + isExact: true, + path: '/connectors', + url: '', + params: { + section: 'connectors', + }, + }, + }; + + render( + + + + + + + + ); + + expect(screen.queryByRole('button', { name: 'Create connector' })).not.toBeInTheDocument(); + + const documentationButton = await screen.findByRole('link', { name: 'Documentation' }); + expect(documentationButton).toBeEnabled(); + }); }); diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_home.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_home.tsx index e2671ae7246b8..35e457a907dfc 100644 --- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_home.tsx +++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_home.tsx @@ -25,6 +25,7 @@ import { CreateConnectorFlyout } from '../../action_connector_form/create_connec import { EditConnectorFlyout } from '../../action_connector_form/edit_connector_flyout'; import { EditConnectorProps } from './types'; import { loadAllActions } from '../../../lib/action_connector_api'; +import { hasSaveActionsCapability } from '../../../lib/capabilities'; const ConnectorsList = lazy(() => import('./actions_connectors_list')); @@ -45,6 +46,7 @@ export const ActionsConnectorsHome: React.FunctionComponent { + it('should have each connector in a sub feature description', async () => { + const { body: features } = await supertest.get('/api/features').expect(200); + expect(Array.isArray(features)).to.be(true); + const actionsFeature = features.find((o: any) => o.id === 'actions'); + expect(!!actionsFeature).to.be(true); + + const connectorTitles = []; + for (const subFeature of actionsFeature.subFeatures) { + expect(subFeature.description.indexOf(SUB_FEATURE_DESC_PREFIX)).to.be(0); + connectorTitles.push( + ...subFeature.description.substring(SUB_FEATURE_DESC_PREFIX.length).split(', ') + ); + } + + const { body: connectorTypes } = await supertest + .get('/api/actions/connector_types') + .expect(200); + for (const connectorType of connectorTypes) { + if (connectorType.sub_feature && !connectorTitles.includes(connectorType.name)) { + throw new Error( + `Connector type "${connectorType.name}" is not included in any of the "Actions & Connectors" sub-feature descriptions. Each new connector type must be manually added to the relevant sub-features. Please update the sub-feature descriptions in "x-pack/plugins/actions/server/feature.ts" to include "${connectorType.name}" to make this test pass.` + ); + } + } + for (const connectorTitle of connectorTitles) { + if (!connectorTypes.find((o: any) => o.name === connectorTitle)) { + throw new Error( + `Connector type "${connectorTitle}" is included in the "Actions & Connectors" sub-feature descriptions but not registered as a connector type. Please update the sub-feature descriptions in "x-pack/plugins/actions/server/feature.ts" to remove "${connectorTitle}" to make this test pass.` + ); + } + } + }); + }); +}