diff --git a/x-pack/plugins/security_solution/public/management/links.test.ts b/x-pack/plugins/security_solution/public/management/links.test.ts index ea27d4fdd28e2..f17fa4bc46c23 100644 --- a/x-pack/plugins/security_solution/public/management/links.test.ts +++ b/x-pack/plugins/security_solution/public/management/links.test.ts @@ -32,6 +32,11 @@ describe('links', () => { let getPlugins: (roles: string[]) => StartPlugins; let fakeHttpServices: jest.Mocked; + const getLinksWithout = (...excludedLinks: SecurityPageName[]) => ({ + ...links, + links: links.links?.filter((link) => !excludedLinks.includes(link.id)), + }); + beforeAll(() => { ExperimentalFeaturesService.init({ experimentalFeatures: { ...allowedExperimentalValues }, @@ -103,10 +108,7 @@ describe('links', () => { coreMockStarted, getPlugins(['superuser']) ); - expect(filteredLinks).toEqual({ - ...links, - links: links.links?.filter((link) => link.id !== SecurityPageName.hostIsolationExceptions), - }); + expect(filteredLinks).toEqual(getLinksWithout(SecurityPageName.hostIsolationExceptions)); }); it('should return all but HIE when NO isolation permission due to license and NO host isolation exceptions entry', async () => { @@ -123,10 +125,7 @@ describe('links', () => { coreMockStarted, getPlugins(['superuser']) ); - expect(filteredLinks).toEqual({ - ...links, - links: links.links?.filter((link) => link.id !== SecurityPageName.hostIsolationExceptions), - }); + expect(filteredLinks).toEqual(getLinksWithout(SecurityPageName.hostIsolationExceptions)); }); it('should return all but HIE when HAS isolation permission AND has HIE entry but not superuser', async () => { @@ -143,10 +142,7 @@ describe('links', () => { coreMockStarted, getPlugins(['superuser']) ); - expect(filteredLinks).toEqual({ - ...links, - links: links.links?.filter((link) => link.id !== SecurityPageName.hostIsolationExceptions), - }); + expect(filteredLinks).toEqual(getLinksWithout(SecurityPageName.hostIsolationExceptions)); }); it('should return all when NO isolation permission due to license but HAS at least one host isolation exceptions entry', async () => { @@ -177,10 +173,7 @@ describe('links', () => { coreMockStarted, getPlugins(['superuser']) ); - expect(filteredLinks).toEqual({ - ...links, - links: links.links?.filter((link) => link.id !== SecurityPageName.hostIsolationExceptions), - }); + expect(filteredLinks).toEqual(getLinksWithout(SecurityPageName.hostIsolationExceptions)); }); it('should not affect hiding Action Log if getting from HIE API throws error', async () => { @@ -196,15 +189,60 @@ describe('links', () => { coreMockStarted, getPlugins(['superuser']) ); - expect(filteredLinks).toEqual({ - ...links, - links: links.links?.filter( - (link) => - link.id !== SecurityPageName.hostIsolationExceptions && - link.id !== SecurityPageName.responseActionsHistory - ), + expect(filteredLinks).toEqual( + getLinksWithout( + SecurityPageName.hostIsolationExceptions, + SecurityPageName.responseActionsHistory + ) + ); + }); + }); + + // 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()); + + const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins([])); + + expect(filteredLinks).toEqual(links); + }); + }); + + // 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()); + + const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins([])); + + expect(filteredLinks).toEqual(links); + }); }); describe('Endpoint List', () => { it('should return all but endpoints link when no Endpoint List READ access', async () => { diff --git a/x-pack/plugins/security_solution/public/management/links.ts b/x-pack/plugins/security_solution/public/management/links.ts index 75e0c0bdc7383..fa564cc67a338 100644 --- a/x-pack/plugins/security_solution/public/management/links.ts +++ b/x-pack/plugins/security_solution/public/management/links.ts @@ -273,17 +273,21 @@ export const getManagementFilteredLinks = async ( ); } - const { canReadActionsLogManagement, canReadHostIsolationExceptions, canReadEndpointList } = - fleetAuthz - ? calculateEndpointAuthz( - licenseService, - fleetAuthz, - currentUser.roles, - isEndpointRbacEnabled, - endpointPermissions, - hasHostIsolationExceptions - ) - : getEndpointAuthzInitialState(); + const { + canReadActionsLogManagement, + canReadHostIsolationExceptions, + canReadEndpointList, + canReadTrustedApplications, + } = fleetAuthz + ? calculateEndpointAuthz( + licenseService, + fleetAuthz, + currentUser.roles, + isEndpointRbacEnabled, + endpointPermissions, + hasHostIsolationExceptions + ) + : getEndpointAuthzInitialState(); if (!canReadEndpointList) { linksToExclude.push(SecurityPageName.endpoints); @@ -297,5 +301,9 @@ export const getManagementFilteredLinks = async ( linksToExclude.push(SecurityPageName.hostIsolationExceptions); } + if (endpointRbacEnabled && !canReadTrustedApplications) { + linksToExclude.push(SecurityPageName.trustedApps); + } + return excludeLinks(linksToExclude); }; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.test.tsx index 55992f5ffac03..6548dd8b0d590 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.test.tsx @@ -15,8 +15,11 @@ import { TrustedAppsList } from './trusted_apps_list'; import { exceptionsListAllHttpMocks } from '../../../mocks/exceptions_list_http_mocks'; import { SEARCHABLE_FIELDS } from '../constants'; import { parseQueryFilterToKQL } from '../../../common/utils'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; +import type { EndpointPrivileges } from '../../../../../common/endpoint/types'; jest.mock('../../../../common/components/user_privileges'); +const mockUserPrivileges = useUserPrivileges as jest.Mock; describe('When on the trusted applications page', () => { let render: () => ReturnType; @@ -24,6 +27,7 @@ describe('When on the trusted applications page', () => { let history: AppContextTestRender['history']; let mockedContext: AppContextTestRender; let apiMocks: ReturnType; + let mockedEndpointPrivileges: Partial; beforeEach(() => { mockedContext = createAppRootMockRenderer(); @@ -35,6 +39,13 @@ describe('When on the trusted applications page', () => { act(() => { history.push(TRUSTED_APPS_PATH); }); + + mockedEndpointPrivileges = { canWriteTrustedApplications: true }; + mockUserPrivileges.mockReturnValue({ endpointPrivileges: mockedEndpointPrivileges }); + }); + + afterEach(() => { + mockUserPrivileges.mockReset(); }); it('should search using expected exception item fields', async () => { @@ -59,4 +70,60 @@ describe('When on the trusted applications page', () => { }) ); }); + + describe('RBAC Trusted Applications', () => { + describe('ALL privilege', () => { + beforeEach(() => { + mockedEndpointPrivileges.canWriteTrustedApplications = true; + }); + + it('should enable adding entries', async () => { + render(); + + await waitFor(() => + expect(renderResult.queryByTestId('trustedAppsListPage-pageAddButton')).toBeTruthy() + ); + }); + + it('should enable modifying/deleting entries', async () => { + render(); + + const actionsButton = await waitFor( + () => renderResult.getAllByTestId('trustedAppsListPage-card-header-actions-button')[0] + ); + userEvent.click(actionsButton); + + expect(renderResult.getByTestId('trustedAppsListPage-card-cardEditAction')).toBeTruthy(); + expect(renderResult.getByTestId('trustedAppsListPage-card-cardDeleteAction')).toBeTruthy(); + }); + }); + + describe('READ privilege', () => { + beforeEach(() => { + mockedEndpointPrivileges.canWriteTrustedApplications = false; + }); + + it('should disable adding entries', async () => { + render(); + + await waitFor(() => + expect(renderResult.queryByTestId('trustedAppsListPage-container')).toBeTruthy() + ); + + expect(renderResult.queryByTestId('trustedAppsListPage-pageAddButton')).toBeNull(); + }); + + it('should disable modifying/deleting entries', async () => { + render(); + + await waitFor(() => + expect(renderResult.queryByTestId('trustedAppsListPage-container')).toBeTruthy() + ); + + expect( + renderResult.queryByTestId('trustedAppsListPage-card-header-actions-button') + ).toBeNull(); + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx index 33912a5b795c4..4695f938249e9 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx @@ -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'; @@ -108,6 +109,7 @@ const TRUSTED_APPS_PAGE_LABELS: ArtifactListPageProps['labels'] = { }; export const TrustedAppsList = memo(() => { + const { canWriteTrustedApplications } = useUserPrivileges().endpointPrivileges; const http = useHttp(); const trustedAppsApiClient = TrustedAppsApiClient.getInstance(http); @@ -119,6 +121,9 @@ export const TrustedAppsList = memo(() => { data-test-subj="trustedAppsListPage" searchableFields={SEARCHABLE_FIELDS} secondaryPageInfo={} + allowCardDeleteAction={canWriteTrustedApplications} + allowCardEditAction={canWriteTrustedApplications} + allowCardCreateAction={canWriteTrustedApplications} /> ); });