mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution] UI trusted applications RBAC (#145593)
## Summary RBAC UI features for Trusted Applications. To test, enable `endpointRbacEnabled` feature-flag, create a non-superuser user with _Security: ALL_ privilege and (All | Read | None) sub-privilege for _Trusted Applications_. <img width="541" alt="image" src="https://user-images.githubusercontent.com/39014407/203073992-fb71e293-2cd8-4639-8d61-4867e39ef071.png"> The modification should: - hide Trusted Apps from Manage navigation items if privilege is NONE, (note: it is still displayed for non-superusers, if the feature flag is disabled) - disable add/edit/delete for Trusted Applications if privilege is READ. ## ⚠️ Note This PR focuses on _Read_ and _None_. The sub-privilege _All_ does not work perfectly at the moment, because of unauthorised API calls. A follow-up PR will fix this, after this PR is merged: https://github.com/elastic/kibana/pull/145361 ### 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
This commit is contained in:
parent
99340486af
commit
4c6a91555d
4 changed files with 152 additions and 34 deletions
|
@ -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)),
|
||||
});
|
||||
|
||||
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 () => {
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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<AppContextTestRender['render']>;
|
||||
|
@ -24,6 +27,7 @@ describe('When on the trusted applications page', () => {
|
|||
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 });
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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={<TrustedAppsArtifactsDocsLink />}
|
||||
allowCardDeleteAction={canWriteTrustedApplications}
|
||||
allowCardEditAction={canWriteTrustedApplications}
|
||||
allowCardCreateAction={canWriteTrustedApplications}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue