Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution] UI Event Filters RBAC #146111

Merged
87 changes: 35 additions & 52 deletions x-pack/plugins/security_solution/public/management/links.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand All @@ -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({
Expand All @@ -102,6 +100,8 @@ describe('links', () => {
canAccessEndpointManagement: true,
canReadActionsLogManagement: true,
canReadEndpointList: true,
canReadTrustedApplications: true,
canReadEventFilters: true,
});

const filteredLinks = await getManagementFilteredLinks(
Expand All @@ -118,6 +118,8 @@ describe('links', () => {
canAccessEndpointManagement: true,
canReadActionsLogManagement: true,
canReadEndpointList: true,
canReadTrustedApplications: true,
canReadEventFilters: true,
});
fakeHttpServices.get.mockResolvedValue({ total: 0 });

Expand All @@ -135,6 +137,8 @@ describe('links', () => {
canAccessEndpointManagement: false,
canReadActionsLogManagement: true,
canReadEndpointList: true,
canReadTrustedApplications: true,
canReadEventFilters: true,
});
fakeHttpServices.get.mockResolvedValue({ total: 1 });

Expand Down Expand Up @@ -166,6 +170,8 @@ describe('links', () => {
canUnIsolateHost: true,
canReadActionsLogManagement: true,
canReadEndpointList: true,
canReadTrustedApplications: true,
canReadEventFilters: true,
});
fakeHttpServices.get.mockRejectedValue(new Error());

Expand All @@ -182,6 +188,8 @@ describe('links', () => {
canUnIsolateHost: true,
canReadActionsLogManagement: false,
canReadEndpointList: true,
canReadTrustedApplications: true,
canReadEventFilters: true,
});
fakeHttpServices.get.mockRejectedValue(new Error());

Expand All @@ -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('shows Trusted Applications for non-superuser, too', async () => {
(calculateEndpointAuthz as jest.Mock).mockReturnValue(getEndpointAuthzInitialStateMock());
it('should hide Trusted Applications for user without privilege', async () => {
(calculateEndpointAuthz as jest.Mock).mockReturnValue(
getEndpointAuthzInitialStateMock({
canReadTrustedApplications: false,
})
);

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('hides Trusted Applications for user without privilege', async () => {
(calculateEndpointAuthz as jest.Mock).mockReturnValue(
getEndpointAuthzInitialStateMock({
canReadTrustedApplications: false,
canReadHostIsolationExceptions: true,
})
);

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());
it('should hide Event Filters for user without privilege', async () => {
(calculateEndpointAuthz as jest.Mock).mockReturnValue(
getEndpointAuthzInitialStateMock({
canReadEventFilters: false,
})
);

const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins([]));
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(
Expand All @@ -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));
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ export const getManagementFilteredLinks = async (
canReadHostIsolationExceptions,
canReadEndpointList,
canReadTrustedApplications,
canReadEventFilters,
} = fleetAuthz
? calculateEndpointAuthz(
licenseService,
Expand All @@ -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);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);

Expand All @@ -144,6 +146,9 @@ export const EventFiltersList = memo(() => {
data-test-subj="EventFiltersListPage"
searchableFields={SEARCHABLE_FIELDS}
flyoutSize="l"
allowCardCreateAction={canWriteEventFilters}
allowCardEditAction={canWriteEventFilters}
allowCardDeleteAction={canWriteEventFilters}
/>
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,19 @@ 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']>;
let renderResult: ReturnType<typeof render>;
let history: AppContextTestRender['history'];
let mockedContext: AppContextTestRender;
let apiMocks: ReturnType<typeof exceptionsListAllHttpMocks>;
let mockedEndpointPrivileges: Partial<EndpointPrivileges>;

beforeEach(() => {
mockedContext = createAppRootMockRenderer();
Expand All @@ -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 () => {
Expand All @@ -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();
});
});
});
});