-
Notifications
You must be signed in to change notification settings - Fork 8.6k
[Security Solution] UI trusted applications RBAC #145593
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
Changes from all commits
4ed8187
c20c08e
763893a
4814c22
0c09f1a
dd367cc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,6 +32,11 @@ describe('links', () => { | |
| let getPlugins: (roles: string[]) => StartPlugins; | ||
| let fakeHttpServices: jest.Mocked<HttpSetup>; | ||
|
|
||
| const getLinksWithout = (...excludedLinks: SecurityPageName[]) => ({ | ||
| ...links, | ||
| links: links.links?.filter((link) => !excludedLinks.includes(link.id)), | ||
| }); | ||
|
|
||
|
Comment on lines
+35
to
+39
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice 🚀 |
||
| 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 () => { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is a slight difference in the operation with and without the feature flag:
So this condition here is to ensure that the operation stays exactly the same if the feature flag is disabled.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @gergoabraham ,
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @paul-tavares
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's here: 8ecd2ba |
||
| linksToExclude.push(SecurityPageName.trustedApps); | ||
| } | ||
|
|
||
| return excludeLinks(linksToExclude); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,15 +15,19 @@ 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<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(); | ||
|
|
@@ -35,6 +39,13 @@ describe('When on the trusted applications page', () => { | |
| act(() => { | ||
| history.push(TRUSTED_APPS_PATH); | ||
| }); | ||
|
|
||
| mockedEndpointPrivileges = { canWriteTrustedApplications: true }; | ||
| mockUserPrivileges.mockReturnValue({ endpointPrivileges: mockedEndpointPrivileges }); | ||
|
Comment on lines
+43
to
+44
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider resetting mockUserPrivileges after each test
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice catch, thanks!
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| }); | ||
|
|
||
| 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(); | ||
| }); | ||
| }); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be worth using the
withoutfunction fromlodash?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wasn't familiar with
withoutso thanks for the suggestion! However, it looks like thatwithoutuses equality comparison on objects, so I don't think it's possible to filter out objects based on their attributes,link.idhere.