[Security Solution][Endpoint][Response Actions] Gate response actions history page based on license (#142470)

* Gate response actions history page based on license

fixes elastic/security-team/issues/5118

* useUserPrivileges isntead

review changes (@paul-tavares)

* don't register the route if no access

review changes (@paul-tavares)

* reset mocked privilege

review changes (@paul-tavares)
This commit is contained in:
Ashokaditya 2022-10-04 12:52:11 +02:00 committed by GitHub
parent cd91e866ae
commit 93229592d7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 79 additions and 23 deletions

View file

@ -121,6 +121,7 @@ describe('Endpoint Authz service', () => {
canSuspendProcess: false,
canGetRunningProcesses: false,
canAccessResponseConsole: false,
canAccessResponseActionsHistory: false,
});
});
});

View file

@ -38,6 +38,7 @@ export const calculateEndpointAuthz = (
canSuspendProcess: hasEndpointManagementAccess && isEnterpriseLicense,
canGetRunningProcesses: hasEndpointManagementAccess && isEnterpriseLicense,
canAccessResponseConsole: hasEndpointManagementAccess && isEnterpriseLicense,
canAccessResponseActionsHistory: hasEndpointManagementAccess && isPlatinumPlusLicense,
};
};
@ -52,5 +53,6 @@ export const getEndpointAuthzInitialState = (): EndpointAuthz => {
canSuspendProcess: false,
canGetRunningProcesses: false,
canAccessResponseConsole: false,
canAccessResponseActionsHistory: false,
};
};

View file

@ -28,6 +28,8 @@ export interface EndpointAuthz {
canGetRunningProcesses: boolean;
/** If user has permissions to use the Response Actions Console */
canAccessResponseConsole: boolean;
/** If user has permissions to access the Response Actions History page */
canAccessResponseActionsHistory: boolean;
}
export type EndpointAuthzKeyList = Array<keyof EndpointAuthz>;

View file

@ -68,7 +68,6 @@ export interface NavTab {
}
export const securityNavKeys = [
SecurityPageName.alerts,
SecurityPageName.responseActionsHistory,
SecurityPageName.blocklist,
SecurityPageName.detectionAndResponse,
SecurityPageName.case,
@ -81,6 +80,7 @@ export const securityNavKeys = [
SecurityPageName.hosts,
SecurityPageName.network,
SecurityPageName.overview,
SecurityPageName.responseActionsHistory,
SecurityPageName.rules,
SecurityPageName.timelines,
SecurityPageName.trustedApps,

View file

@ -250,16 +250,6 @@ Object {
"name": "Blocklist",
"onClick": [Function],
},
Object {
"data-href": "securitySolutionUI/response_actions_history",
"data-test-subj": "navigation-response_actions_history",
"disabled": false,
"href": "securitySolutionUI/response_actions_history",
"id": "response_actions_history",
"isSelected": false,
"name": "Response actions history",
"onClick": [Function],
},
Object {
"data-href": "securitySolutionUI/cloud_security_posture-benchmarks",
"data-test-subj": "navigation-cloud_security_posture-benchmarks",

View file

@ -17,6 +17,7 @@ import { TestProviders } from '../../../mock';
import { CASES_FEATURE_ID } from '../../../../../common/constants';
import { useCanSeeHostIsolationExceptionsMenu } from '../../../../management/pages/host_isolation_exceptions/view/hooks';
import { useTourContext } from '../../guided_onboarding';
import { useUserPrivileges } from '../../user_privileges';
import {
noCasesPermissions,
readCasesCapabilities,
@ -38,6 +39,9 @@ jest.mock('../../../hooks/use_experimental_features');
jest.mock('../../../utils/route/use_route_spy');
jest.mock('../../../../management/pages/host_isolation_exceptions/view/hooks');
jest.mock('../../guided_onboarding');
jest.mock('../../user_privileges');
const mockUseUserPrivileges = useUserPrivileges as jest.Mock;
describe('useSecuritySolutionNavigation', () => {
const mockRouteSpy = [
@ -56,6 +60,9 @@ describe('useSecuritySolutionNavigation', () => {
(useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(false);
(useRouteSpy as jest.Mock).mockReturnValue(mockRouteSpy);
(useCanSeeHostIsolationExceptionsMenu as jest.Mock).mockReturnValue(true);
mockUseUserPrivileges.mockImplementation(() => ({
endpointPrivileges: { canReadActionsLogManagement: true },
}));
(useTourContext as jest.Mock).mockReturnValue({ isTourShown: false });
const cases = mockCasesContract();
@ -83,6 +90,10 @@ describe('useSecuritySolutionNavigation', () => {
});
});
afterEach(() => {
mockUseUserPrivileges.mockReset();
});
it('should create navigation config', async () => {
const { result } = renderHook<{}, KibanaPageTemplateProps['solutionNav']>(
() => useSecuritySolutionNavigation(),
@ -117,6 +128,23 @@ describe('useSecuritySolutionNavigation', () => {
).toBeUndefined();
});
it('should omit response actions history if hook reports false', () => {
mockUseUserPrivileges.mockImplementation(() => ({
endpointPrivileges: { canReadActionsLogManagement: false },
}));
const { result } = renderHook<{}, KibanaPageTemplateProps['solutionNav']>(
() => useSecuritySolutionNavigation(),
{ wrapper: TestProviders }
);
const items = result.current?.items;
expect(items).toBeDefined();
expect(
items!
.find((item) => item.id === 'manage')
?.items?.find((item) => item.id === 'response_actions_history')
).toBeUndefined();
});
describe('Permission gated routes', () => {
describe('cases', () => {
it('should display the cases navigation item when the user has read permissions', () => {

View file

@ -21,6 +21,7 @@ import { SecurityPageName } from '../../../../../common/constants';
import { useCanSeeHostIsolationExceptionsMenu } from '../../../../management/pages/host_isolation_exceptions/view/hooks';
import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features';
import { useGlobalQueryString } from '../../../utils/global_query_string';
import { useUserPrivileges } from '../../user_privileges';
export const usePrimaryNavigationItems = ({
navTabs,
@ -71,6 +72,8 @@ export const usePrimaryNavigationItems = ({
function usePrimaryNavigationItemsToDisplay(navTabs: Record<string, NavTab>) {
const hasCasesReadPermissions = useGetUserCasesPermissions().read;
const canSeeHostIsolationExceptions = useCanSeeHostIsolationExceptionsMenu();
const canSeeResponseActionsHistory =
useUserPrivileges().endpointPrivileges.canAccessResponseActionsHistory;
const isPolicyListEnabled = useIsExperimentalFeatureEnabled('policyListEnabled');
const uiCapabilities = useKibana().services.application.capabilities;
@ -138,7 +141,9 @@ function usePrimaryNavigationItemsToDisplay(navTabs: Record<string, NavTab>) {
? [navTabs[SecurityPageName.hostIsolationExceptions]]
: []),
navTabs[SecurityPageName.blocklist],
navTabs[SecurityPageName.responseActionsHistory],
...(canSeeResponseActionsHistory
? [navTabs[SecurityPageName.responseActionsHistory]]
: []),
navTabs[SecurityPageName.cloudSecurityPostureBenchmarks],
],
},
@ -156,6 +161,7 @@ function usePrimaryNavigationItemsToDisplay(navTabs: Record<string, NavTab>) {
navTabs,
hasCasesReadPermissions,
canSeeHostIsolationExceptions,
canSeeResponseActionsHistory,
isPolicyListEnabled,
]
);

View file

@ -64,13 +64,30 @@ describe('links', () => {
expect(filteredLinks).toEqual(links);
});
it('it returns all but response actions history when no access privilege to either response actions history or HIE but have at least one HIE entry', async () => {
fakeHttpServices.get.mockResolvedValue({ total: 1 });
const filteredLinks = await getManagementFilteredLinks(
coreMockStarted,
getPlugins(['superuser'])
);
(licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(false);
expect(filteredLinks).toEqual({
...links,
links: links.links?.filter((link) => link.id !== SecurityPageName.responseActionsHistory),
});
});
it('it returns filtered links when not having isolation permissions and no host isolation exceptions entry', async () => {
fakeHttpServices.get.mockResolvedValue({ total: 0 });
(licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(false);
const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins([]));
expect(filteredLinks).toEqual({
...links,
links: links.links?.filter((link) => link.id !== SecurityPageName.hostIsolationExceptions),
links: links.links?.filter(
(link) =>
link.id !== SecurityPageName.hostIsolationExceptions &&
link.id !== SecurityPageName.responseActionsHistory
),
});
});
});

View file

@ -221,7 +221,7 @@ export const links: LinkItem = {
],
};
const getFilteredLinks = (linkIds: SecurityPageName[]) => ({
const excludeLinks = (linkIds: SecurityPageName[]) => ({
...links,
links: links.links?.filter((link) => !linkIds.includes(link.id)),
});
@ -238,19 +238,26 @@ export const getManagementFilteredLinks = async (
currentUserResponse.roles
);
if (!privileges.canAccessEndpointManagement) {
return getFilteredLinks([SecurityPageName.hostIsolationExceptions]);
return excludeLinks([
SecurityPageName.hostIsolationExceptions,
SecurityPageName.responseActionsHistory,
]);
}
if (!privileges.canIsolateHost) {
if (!privileges.canIsolateHost || !privileges.canAccessResponseActionsHistory) {
const hostIsolationExceptionsApiClientInstance = HostIsolationExceptionsApiClient.getInstance(
core.http
);
const summaryResponse = await hostIsolationExceptionsApiClientInstance.summary();
if (!summaryResponse.total) {
return getFilteredLinks([SecurityPageName.hostIsolationExceptions]);
return excludeLinks([
SecurityPageName.hostIsolationExceptions,
SecurityPageName.responseActionsHistory,
]);
}
return excludeLinks([SecurityPageName.responseActionsHistory]);
}
} catch {
return getFilteredLinks([SecurityPageName.hostIsolationExceptions]);
return excludeLinks([SecurityPageName.hostIsolationExceptions]);
}
return links;

View file

@ -76,7 +76,8 @@ const ResponseActionsTelemetry = () => (
);
export const ManagementContainer = memo(() => {
const { loading, canAccessEndpointManagement } = useUserPrivileges().endpointPrivileges;
const { loading, canAccessEndpointManagement, canAccessResponseActionsHistory } =
useUserPrivileges().endpointPrivileges;
// Lets wait until we can verify permissions
if (loading) {
@ -103,10 +104,12 @@ export const ManagementContainer = memo(() => {
component={HostIsolationExceptionsTelemetry}
/>
<Route path={MANAGEMENT_ROUTING_BLOCKLIST_PATH} component={BlocklistContainer} />
<Route
path={MANAGEMENT_ROUTING_RESPONSE_ACTIONS_HISTORY_PATH}
component={ResponseActionsTelemetry}
/>
{canAccessResponseActionsHistory && (
<Route
path={MANAGEMENT_ROUTING_RESPONSE_ACTIONS_HISTORY_PATH}
component={ResponseActionsTelemetry}
/>
)}
<Route path={MANAGEMENT_PATH} exact>
<Redirect to={getEndpointListPath({ name: 'endpointList' })} />
</Route>