[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
This commit is contained in:
Alexi Doak 2025-01-21 13:33:54 -08:00 committed by GitHub
parent b8f9778575
commit 12998a8fe1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 106 additions and 3 deletions

View file

@ -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: [

View file

@ -61,5 +61,6 @@ export function getConnectorType(): ConnectorTypeModel<
},
actionConnectorFields: lazy(() => import('./crowdstrike_connector')),
actionParamsFields: lazy(() => import('./crowdstrike_params')),
subFeature: 'endpointSecurity',
};
}

View file

@ -61,5 +61,6 @@ export function getConnectorType(): ConnectorTypeModel<
},
actionConnectorFields: lazy(() => import('./microsoft_defender_endpoint_connector')),
actionParamsFields: lazy(() => import('./microsoft_defender_endpoint_params')),
subFeature: 'endpointSecurity',
};
}

View file

@ -61,5 +61,6 @@ export function getConnectorType(): ConnectorTypeModel<
},
actionConnectorFields: lazy(() => import('./sentinelone_connector')),
actionParamsFields: lazy(() => import('./sentinelone_params')),
subFeature: 'endpointSecurity',
};
}

View file

@ -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<MatchParams> = {
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<MatchParams> = {
history: createMemoryHistory({
initialEntries: ['/connectors'],
}),
location: createLocation('/connectors'),
match: {
isExact: true,
path: '/connectors',
url: '',
params: {
section: 'connectors',
},
},
};
render(
<IntlProvider locale="en">
<Router history={props.history}>
<QueryClientProvider client={queryClient}>
<ActionsConnectorsHome {...props} />
</QueryClientProvider>
</Router>
</IntlProvider>
);
expect(screen.queryByRole('button', { name: 'Create connector' })).not.toBeInTheDocument();
const documentationButton = await screen.findByRole('link', { name: 'Documentation' });
expect(documentationButton).toBeEnabled();
});
});

View file

@ -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<RouteComponentProps<
actionTypeRegistry,
http,
notifications: { toasts },
application: { capabilities },
} = useKibana().services;
const location = useLocation();
@ -187,7 +189,12 @@ export const ActionsConnectorsHome: React.FunctionComponent<RouteComponentProps<
}) ||
matchPath(location.pathname, { path: routeToConnectorEdit, exact: true })
) {
topRightSideButtons = [createConnectorButton, documentationButton];
topRightSideButtons = [];
const canSave = hasSaveActionsCapability(capabilities);
if (canSave) {
topRightSideButtons.push(createConnectorButton);
}
topRightSideButtons.push(documentationButton);
} else if (matchPath(location.pathname, { path: routeToLogs, exact: true })) {
topRightSideButtons = [documentationButton];
}

View file

@ -57,6 +57,7 @@ export default function connectorsTests({ loadTestFile, getService }: FtrProvide
loadTestFile(require.resolve('./connector_types_system'));
loadTestFile(require.resolve('./update'));
loadTestFile(require.resolve('./bulk_enqueue'));
loadTestFile(require.resolve('./sub_feature_descriptions'));
/**
* Sub action framework

View file

@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
const SUB_FEATURE_DESC_PREFIX = 'Includes: ';
// eslint-disable-next-line import/no-default-export
export default function subFeatureDescriptionsTest({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
describe('sub feature descriptions', () => {
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.`
);
}
}
});
});
}