[Security Solution][Lists] - Hide exception list delete icon if Kibana read only (#126710)

Addresses bug #126313

Even if user is given index privileges to lists, UI should follow Kibana privileges. Checks if user is a read only Kibana user and hides the delete icon from exception list view if true.
This commit is contained in:
Yara Tercero 2022-03-04 15:33:14 -08:00 committed by GitHub
parent f8586a87d0
commit 7af9c37016
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 67 additions and 15 deletions

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { ROLES } from '../../../common/test';
import { getExceptionList, expectedExportedExceptionList } from '../../objects/exception';
import { getNewRule } from '../../objects/rule';
@ -25,6 +26,7 @@ import {
clearSearchSelection,
} from '../../tasks/exceptions_table';
import {
EXCEPTIONS_TABLE_DELETE_BTN,
EXCEPTIONS_TABLE_LIST_NAME,
EXCEPTIONS_TABLE_SHOWING_LISTS,
} from '../../screens/exceptions';
@ -168,3 +170,22 @@ describe('Exceptions Table', () => {
cy.contains(EXCEPTIONS_TABLE_SHOWING_LISTS, '1');
});
});
describe('Exceptions Table - read only', () => {
before(() => {
// First we login as a privileged user to create exception list
cleanKibana();
loginAndWaitForPageWithoutDateRange(EXCEPTIONS_URL, ROLES.platform_engineer);
createExceptionList(getExceptionList(), getExceptionList().list_id);
// Then we login as read-only user to test.
loginAndWaitForPageWithoutDateRange(EXCEPTIONS_URL, ROLES.reader);
waitForExceptionsTableToBeLoaded();
cy.get(EXCEPTIONS_TABLE_SHOWING_LISTS).should('have.text', `Showing 1 list`);
});
it('Delete icon is not shown', () => {
cy.get(EXCEPTIONS_TABLE_DELETE_BTN).should('not.exist');
});
});

View file

@ -27,7 +27,8 @@ export const getAllExceptionListsColumns = (
onExport: (arg: { id: string; listId: string; namespaceType: NamespaceType }) => () => void,
onDelete: (arg: { id: string; listId: string; namespaceType: NamespaceType }) => () => void,
formatUrl: FormatUrl,
navigateToUrl: (url: string) => Promise<void>
navigateToUrl: (url: string) => Promise<void>,
isKibanaReadOnly: boolean
): AllExceptionListsColumns[] => [
{
align: 'left',
@ -155,7 +156,7 @@ export const getAllExceptionListsColumns = (
},
{
render: ({ id, list_id: listId, namespace_type: namespaceType }: ExceptionListInfo) => {
return listId === 'endpoint_list' ? (
return listId === 'endpoint_list' || isKibanaReadOnly ? (
<></>
) : (
<EuiButtonIcon

View file

@ -11,12 +11,14 @@ import { mount } from 'enzyme';
import { TestProviders } from '../../../../../../common/mock';
import { mockHistory } from '../../../../../../common/utils/route/index.test';
import { getExceptionListSchemaMock } from '../../../../../../../../lists/common/schemas/response/exception_list_schema.mock';
import { useUserData } from '../../../../../components/user_info';
import { ExceptionListsTable } from './exceptions_table';
import { useApi, useExceptionLists } from '@kbn/securitysolution-list-hooks';
import { useAllExceptionLists } from './use_all_exception_lists';
import { useHistory } from 'react-router-dom';
jest.mock('../../../../../components/user_info');
jest.mock('../../../../../../common/lib/kibana');
jest.mock('./use_all_exception_lists');
jest.mock('@kbn/securitysolution-list-hooks');
@ -41,15 +43,6 @@ jest.mock('../../../../../containers/detection_engine/lists/use_lists_config', (
useListsConfig: jest.fn().mockReturnValue({ loading: false }),
}));
jest.mock('../../../../../components/user_info', () => ({
useUserData: jest.fn().mockReturnValue([
{
loading: false,
canUserCRUD: false,
},
]),
}));
describe('ExceptionListsTable', () => {
const exceptionList1 = getExceptionListSchemaMock();
const exceptionList2 = { ...getExceptionListSchemaMock(), list_id: 'not_endpoint_list', id: '2' };
@ -86,9 +79,17 @@ describe('ExceptionListsTable', () => {
endpoint_list: exceptionList1,
},
]);
(useUserData as jest.Mock).mockReturnValue([
{
loading: false,
canUserCRUD: false,
canUserREAD: false,
},
]);
});
it('does not render delete option disabled if list is "endpoint_list"', async () => {
it('does not render delete option if list is "endpoint_list"', async () => {
const wrapper = mount(
<TestProviders>
<ExceptionListsTable />
@ -106,4 +107,25 @@ describe('ExceptionListsTable', () => {
wrapper.find('[data-test-subj="exceptionsTableDeleteButton"] button').at(0).prop('disabled')
).toBeFalsy();
});
it('does not render delete option if user is read only', async () => {
(useUserData as jest.Mock).mockReturnValue([
{
loading: false,
canUserCRUD: false,
canUserREAD: true,
},
]);
const wrapper = mount(
<TestProviders>
<ExceptionListsTable />
</TestProviders>
);
expect(wrapper.find('[data-test-subj="exceptionsTableListId"]').at(1).text()).toEqual(
'not_endpoint_list'
);
expect(wrapper.find('[data-test-subj="exceptionsTableDeleteButton"] button')).toHaveLength(0);
});
});

View file

@ -60,7 +60,7 @@ const exceptionReferenceModalInitialState: ReferenceModalState = {
export const ExceptionListsTable = React.memo(() => {
const { formatUrl } = useFormatUrl(SecurityPageName.rules);
const [{ loading: userInfoLoading, canUserCRUD }] = useUserData();
const [{ loading: userInfoLoading, canUserCRUD, canUserREAD }] = useUserData();
const hasPermissions = userHasPermissions(canUserCRUD);
const { loading: listsConfigLoading } = useListsConfig();
@ -193,8 +193,16 @@ export const ExceptionListsTable = React.memo(() => {
);
const exceptionsColumns = useMemo((): AllExceptionListsColumns[] => {
return getAllExceptionListsColumns(handleExport, handleDelete, formatUrl, navigateToUrl);
}, [handleExport, handleDelete, formatUrl, navigateToUrl]);
// Defaulting to true to default to the lower privilege first
const isKibanaReadOnly = (canUserREAD && !canUserCRUD) ?? true;
return getAllExceptionListsColumns(
handleExport,
handleDelete,
formatUrl,
navigateToUrl,
isKibanaReadOnly
);
}, [handleExport, handleDelete, formatUrl, navigateToUrl, canUserREAD, canUserCRUD]);
const handleRefresh = useCallback((): void => {
if (refreshExceptions != null) {