[Security Solution] UI Event Filters RBAC (#146111)

## Summary

Similarly to https://github.com/elastic/kibana/pull/145593, this PR
handles the _None_ and _Read_ privileges for the Event Filters
sub-feature. The _All_ privilege should not need any UI modification,
but will need API modification.
<img width="554" alt="image"
src="https://user-images.githubusercontent.com/39014407/203514418-b016a47b-819c-4057-a86e-d7b4a3d8e5c5.png">

The modification should:
- hide Event Filters from Manage navigation items if privilege is NONE,
~(note: it is still displayed for non-superusers, if the feature flag is
disabled)~ update: it is hidden for non-superusers if the feature flag
is disabled
- disable add/edit/delete for Event Filters if privilege is READ.



#### Checked:
- the Event Filters form still works from the "Hosts > Events" side of
the app 
<img width="1354" alt="image"
src="https://user-images.githubusercontent.com/39014407/204316619-85121106-9d28-4165-9675-522890e39dfe.png">
<img width="1323" alt="image"
src="https://user-images.githubusercontent.com/39014407/204326904-6917c8fe-a364-4a40-8bdc-e8240115fa1d.png">



### Checklist

Delete any items that are not applicable to this PR.

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

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Gergő Ábrahám 2022-12-01 12:32:20 +01:00 committed by GitHub
parent bbd229f96b
commit 5f3ac5d938
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 115 additions and 53 deletions

View file

@ -64,13 +64,10 @@ describe('links', () => {
} as unknown as StartPlugins);
});
it('should return all links without filtering when having isolate permission', async () => {
it('should return all links for user with all sub-feature privileges', async () => {
(calculateEndpointAuthz as jest.Mock).mockReturnValue(getEndpointAuthzInitialStateMock());
const filteredLinks = await getManagementFilteredLinks(
coreMockStarted,
getPlugins(['superuser'])
);
const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins([]));
expect(filteredLinks).toEqual(links);
});
@ -87,13 +84,14 @@ describe('links', () => {
coreMockStarted,
getPlugins(['superuser'])
);
expect(filteredLinks).toEqual({
...links,
links: links.links?.filter((link) => link.id !== SecurityPageName.responseActionsHistory),
});
expect(filteredLinks).toEqual(getLinksWithout(SecurityPageName.responseActionsHistory));
});
});
// todo: these tests should be updated, because in the end, showing/hiding HIE depends on nothing
// else but the mock return of `calculateEndpointAuthz`.
// These tests should check what is the value of `hasHostIsolationExceptions` which is passed to
// `calculateEndpointAuthz`.
describe('Host Isolation Exception', () => {
it('should return all but HIE when NO isolation permission due to privilege', async () => {
(calculateEndpointAuthz as jest.Mock).mockReturnValue({
@ -102,6 +100,8 @@ describe('links', () => {
canAccessEndpointManagement: true,
canReadActionsLogManagement: true,
canReadEndpointList: true,
canReadTrustedApplications: true,
canReadEventFilters: true,
});
const filteredLinks = await getManagementFilteredLinks(
@ -118,6 +118,8 @@ describe('links', () => {
canAccessEndpointManagement: true,
canReadActionsLogManagement: true,
canReadEndpointList: true,
canReadTrustedApplications: true,
canReadEventFilters: true,
});
fakeHttpServices.get.mockResolvedValue({ total: 0 });
@ -135,6 +137,8 @@ describe('links', () => {
canAccessEndpointManagement: false,
canReadActionsLogManagement: true,
canReadEndpointList: true,
canReadTrustedApplications: true,
canReadEventFilters: true,
});
fakeHttpServices.get.mockResolvedValue({ total: 1 });
@ -166,6 +170,8 @@ describe('links', () => {
canUnIsolateHost: true,
canReadActionsLogManagement: true,
canReadEndpointList: true,
canReadTrustedApplications: true,
canReadEventFilters: true,
});
fakeHttpServices.get.mockRejectedValue(new Error());
@ -182,6 +188,8 @@ describe('links', () => {
canUnIsolateHost: true,
canReadActionsLogManagement: false,
canReadEndpointList: true,
canReadTrustedApplications: true,
canReadEventFilters: true,
});
fakeHttpServices.get.mockRejectedValue(new Error());
@ -198,52 +206,30 @@ describe('links', () => {
});
});
// this can be removed after removing endpointRbacEnabled feature flag
describe('without endpointRbacEnabled', () => {
beforeAll(() => {
ExperimentalFeaturesService.init({
experimentalFeatures: { ...allowedExperimentalValues, endpointRbacEnabled: false },
});
});
it('should hide Trusted Applications for user without privilege', async () => {
(calculateEndpointAuthz as jest.Mock).mockReturnValue(
getEndpointAuthzInitialStateMock({
canReadTrustedApplications: false,
})
);
it('shows Trusted Applications for non-superuser, too', async () => {
(calculateEndpointAuthz as jest.Mock).mockReturnValue(getEndpointAuthzInitialStateMock());
const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins([]));
const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins([]));
expect(filteredLinks).toEqual(links);
});
expect(filteredLinks).toEqual(getLinksWithout(SecurityPageName.trustedApps));
});
// this can be the default after removing endpointRbacEnabled feature flag
describe('with endpointRbacEnabled', () => {
beforeAll(() => {
ExperimentalFeaturesService.init({
experimentalFeatures: { ...allowedExperimentalValues, endpointRbacEnabled: true },
});
});
it('should hide Event Filters for user without privilege', async () => {
(calculateEndpointAuthz as jest.Mock).mockReturnValue(
getEndpointAuthzInitialStateMock({
canReadEventFilters: false,
})
);
it('hides Trusted Applications for user without privilege', async () => {
(calculateEndpointAuthz as jest.Mock).mockReturnValue(
getEndpointAuthzInitialStateMock({
canReadTrustedApplications: false,
canReadHostIsolationExceptions: true,
})
);
const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins([]));
const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins([]));
expect(filteredLinks).toEqual(getLinksWithout(SecurityPageName.trustedApps));
});
it('shows Trusted Applications for user with privilege', async () => {
(calculateEndpointAuthz as jest.Mock).mockReturnValue(getEndpointAuthzInitialStateMock());
const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins([]));
expect(filteredLinks).toEqual(links);
});
expect(filteredLinks).toEqual(getLinksWithout(SecurityPageName.eventFilters));
});
describe('Endpoint List', () => {
it('should return all but endpoints link when no Endpoint List READ access', async () => {
(calculateEndpointAuthz as jest.Mock).mockReturnValue(
@ -255,10 +241,7 @@ describe('links', () => {
coreMockStarted,
getPlugins(['superuser'])
);
expect(filteredLinks).toEqual({
...links,
links: links.links?.filter((link) => link.id !== SecurityPageName.endpoints),
});
expect(filteredLinks).toEqual(getLinksWithout(SecurityPageName.endpoints));
});
});
});

View file

@ -278,6 +278,7 @@ export const getManagementFilteredLinks = async (
canReadHostIsolationExceptions,
canReadEndpointList,
canReadTrustedApplications,
canReadEventFilters,
} = fleetAuthz
? calculateEndpointAuthz(
licenseService,
@ -301,9 +302,13 @@ export const getManagementFilteredLinks = async (
linksToExclude.push(SecurityPageName.hostIsolationExceptions);
}
if (endpointRbacEnabled && !canReadTrustedApplications) {
if (!canReadTrustedApplications) {
linksToExclude.push(SecurityPageName.trustedApps);
}
if (!canReadEventFilters) {
linksToExclude.push(SecurityPageName.eventFilters);
}
return excludeLinks(linksToExclude);
};

View file

@ -11,6 +11,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
import type { DocLinks } from '@kbn/doc-links';
import { EuiLink } from '@elastic/eui';
import { useUserPrivileges } from '../../../../common/components/user_privileges';
import { useHttp } from '../../../../common/lib/kibana';
import type { ArtifactListPageProps } from '../../../components/artifact_list_page';
import { ArtifactListPage } from '../../../components/artifact_list_page';
@ -133,6 +134,7 @@ const EVENT_FILTERS_PAGE_LABELS: ArtifactListPageProps['labels'] = {
};
export const EventFiltersList = memo(() => {
const { canWriteEventFilters } = useUserPrivileges().endpointPrivileges;
const http = useHttp();
const eventFiltersApiClient = EventFiltersApiClient.getInstance(http);
@ -144,6 +146,9 @@ export const EventFiltersList = memo(() => {
data-test-subj="EventFiltersListPage"
searchableFields={SEARCHABLE_FIELDS}
flyoutSize="l"
allowCardCreateAction={canWriteEventFilters}
allowCardEditAction={canWriteEventFilters}
allowCardDeleteAction={canWriteEventFilters}
/>
);
});

View file

@ -15,6 +15,11 @@ import { EventFiltersList } from '../event_filters_list';
import { exceptionsListAllHttpMocks } from '../../../../mocks/exceptions_list_http_mocks';
import { SEARCHABLE_FIELDS } from '../../constants';
import { parseQueryFilterToKQL } from '../../../../common/utils';
import type { EndpointPrivileges } from '../../../../../../common/endpoint/types';
import { useUserPrivileges } from '../../../../../common/components/user_privileges';
jest.mock('../../../../../common/components/user_privileges');
const mockUserPrivileges = useUserPrivileges as jest.Mock;
describe('When on the Event Filters list page', () => {
let render: () => ReturnType<AppContextTestRender['render']>;
@ -22,6 +27,7 @@ describe('When on the Event Filters list page', () => {
let history: AppContextTestRender['history'];
let mockedContext: AppContextTestRender;
let apiMocks: ReturnType<typeof exceptionsListAllHttpMocks>;
let mockedEndpointPrivileges: Partial<EndpointPrivileges>;
beforeEach(() => {
mockedContext = createAppRootMockRenderer();
@ -31,6 +37,13 @@ describe('When on the Event Filters list page', () => {
act(() => {
history.push(EVENT_FILTERS_PATH);
});
mockedEndpointPrivileges = { canWriteTrustedApplications: true };
mockUserPrivileges.mockReturnValue({ endpointPrivileges: mockedEndpointPrivileges });
});
afterEach(() => {
mockUserPrivileges.mockReset();
});
it('should search using expected exception item fields', async () => {
@ -55,4 +68,60 @@ describe('When on the Event Filters list page', () => {
})
);
});
describe('RBAC Event Filters', () => {
describe('ALL privilege', () => {
beforeEach(() => {
mockedEndpointPrivileges.canWriteEventFilters = true;
});
it('should enable adding entries', async () => {
render();
await waitFor(() =>
expect(renderResult.queryByTestId('EventFiltersListPage-pageAddButton')).toBeTruthy()
);
});
it('should enable modifying/deleting entries', async () => {
render();
const actionsButton = await waitFor(
() => renderResult.getAllByTestId('EventFiltersListPage-card-header-actions-button')[0]
);
userEvent.click(actionsButton);
expect(renderResult.getByTestId('EventFiltersListPage-card-cardEditAction')).toBeTruthy();
expect(renderResult.getByTestId('EventFiltersListPage-card-cardDeleteAction')).toBeTruthy();
});
});
describe('READ privilege', () => {
beforeEach(() => {
mockedEndpointPrivileges.canWriteEventFilters = false;
});
it('should disable adding entries', async () => {
render();
await waitFor(() =>
expect(renderResult.queryByTestId('EventFiltersListPage-container')).toBeTruthy()
);
expect(renderResult.queryByTestId('EventFiltersListPage-pageAddButton')).toBeNull();
});
it('should disable modifying/deleting entries', async () => {
render();
await waitFor(() =>
expect(renderResult.queryByTestId('EventFiltersListPage-container')).toBeTruthy()
);
expect(
renderResult.queryByTestId('EventFiltersListPage-card-header-actions-button')
).toBeNull();
});
});
});
});