Skip to content

Commit

Permalink
[ResponseOps] Granular connector RBAC followup (#205818)
Browse files Browse the repository at this point in the history
## Summary

This PR is followup to, #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
  • Loading branch information
doakalexi authored Jan 21, 2025
1 parent b8f9778 commit 12998a8
Show file tree
Hide file tree
Showing 8 changed files with 106 additions and 3 deletions.
2 changes: 1 addition & 1 deletion x-pack/platform/plugins/shared/actions/server/feature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,6 @@ export function getConnectorType(): ConnectorTypeModel<
},
actionConnectorFields: lazy(() => import('./crowdstrike_connector')),
actionParamsFields: lazy(() => import('./crowdstrike_params')),
subFeature: 'endpointSecurity',
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,6 @@ export function getConnectorType(): ConnectorTypeModel<
},
actionConnectorFields: lazy(() => import('./microsoft_defender_endpoint_connector')),
actionParamsFields: lazy(() => import('./microsoft_defender_endpoint_params')),
subFeature: 'endpointSecurity',
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,6 @@ export function getConnectorType(): ConnectorTypeModel<
},
actionConnectorFields: lazy(() => import('./sentinelone_connector')),
actionParamsFields: lazy(() => import('./sentinelone_params')),
subFeature: 'endpointSecurity',
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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}</>,
Expand All @@ -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({
Expand Down Expand Up @@ -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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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'));

Expand All @@ -45,6 +46,7 @@ export const ActionsConnectorsHome: React.FunctionComponent<RouteComponentProps<
actionTypeRegistry,
http,
notifications: { toasts },
application: { capabilities },
} = useKibana().services;

const location = useLocation();
Expand Down Expand Up @@ -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];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.`
);
}
}
});
});
}

0 comments on commit 12998a8

Please sign in to comment.