[Security Solution] use endpoint rbac for isolate/unisolate host (#141810)

This commit is contained in:
Joey F. Poon 2022-09-27 17:34:49 -05:00 committed by GitHub
parent 27483d5aa9
commit e4080d5b64
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 137 additions and 42 deletions

View file

@ -15,7 +15,10 @@ import { ENDPOINT_PRIVILEGES } from './constants';
const SECURITY_SOLUTION_ID = DEFAULT_APP_CATEGORIES.security.id;
function generateActions(privileges: string[] = [], overrides: Record<string, boolean> = {}) {
function generateActions(
privileges: typeof ENDPOINT_PRIVILEGES,
overrides: Record<string, boolean> = {}
) {
return privileges.reduce((acc, privilege) => {
const executePackageAction = overrides[privilege] || false;

View file

@ -23,4 +23,4 @@ export const ENDPOINT_PRIVILEGES = [
'writeHostIsolation',
'writeProcessOperations',
'writeFileOperations',
];
] as const;

View file

@ -7,6 +7,7 @@
import type { DeletePackagePoliciesResponse, NewPackagePolicy, PackagePolicy } from './types';
import type { FleetAuthz } from './authz';
import { ENDPOINT_PRIVILEGES } from './constants';
export const createNewPackagePolicyMock = (): NewPackagePolicy => {
return {
@ -61,6 +62,15 @@ export const deletePackagePolicyMock = (): DeletePackagePoliciesResponse => {
* Creates mock `authz` object
*/
export const createFleetAuthzMock = (): FleetAuthz => {
const endpointActions = ENDPOINT_PRIVILEGES.reduce((acc, privilege) => {
return {
...acc,
[privilege]: {
executePackageAction: true,
},
};
}, {});
return {
fleet: {
all: true,
@ -80,5 +90,10 @@ export const createFleetAuthzMock = (): FleetAuthz => {
readIntegrationPolicies: true,
writeIntegrationPolicies: true,
},
packagePrivileges: {
endpoint: {
actions: endpointActions,
},
},
};
};

View file

@ -107,6 +107,40 @@ describe('Endpoint Authz service', () => {
);
});
});
describe('endpoint rbac is enabled', () => {
describe('canIsolateHost', () => {
it('should be true if packagePrivilege.writeHostIsolation is true', () => {
fleetAuthz.packagePrivileges!.endpoint.actions.writeHostIsolation.executePackageAction =
true;
const authz = calculateEndpointAuthz(licenseService, fleetAuthz, userRoles, true);
expect(authz.canIsolateHost).toBe(true);
});
it('should be false if packagePrivilege.writeHostIsolation is false', () => {
fleetAuthz.packagePrivileges!.endpoint.actions.writeHostIsolation.executePackageAction =
false;
const authz = calculateEndpointAuthz(licenseService, fleetAuthz, userRoles, true);
expect(authz.canIsolateHost).toBe(false);
});
});
describe('canUnIsolateHost', () => {
it('should be true if packagePrivilege.writeHostIsolation is true', () => {
fleetAuthz.packagePrivileges!.endpoint.actions.writeHostIsolation.executePackageAction =
true;
const authz = calculateEndpointAuthz(licenseService, fleetAuthz, userRoles, true);
expect(authz.canUnIsolateHost).toBe(true);
});
it('should be false if packagePrivilege.writeHostIsolation is false', () => {
fleetAuthz.packagePrivileges!.endpoint.actions.writeHostIsolation.executePackageAction =
false;
const authz = calculateEndpointAuthz(licenseService, fleetAuthz, userRoles, true);
expect(authz.canUnIsolateHost).toBe(false);
});
});
});
});
describe('getEndpointAuthzInitialState()', () => {

View file

@ -28,14 +28,18 @@ export const calculateEndpointAuthz = (
const isPlatinumPlusLicense = licenseService.isPlatinumPlus();
const isEnterpriseLicense = licenseService.isEnterprise();
const hasEndpointManagementAccess = userRoles.includes('superuser');
const canIsolateHost = isEndpointRbacEnabled
? fleetAuthz.packagePrivileges?.endpoint?.actions?.writeHostIsolation?.executePackageAction ||
false
: hasEndpointManagementAccess;
return {
canAccessFleet: fleetAuthz?.fleet.all ?? userRoles.includes('superuser'),
canAccessEndpointManagement: hasEndpointManagementAccess,
canCreateArtifactsByPolicy: hasEndpointManagementAccess && isPlatinumPlusLicense,
// Response Actions
canIsolateHost: isPlatinumPlusLicense && hasEndpointManagementAccess,
canUnIsolateHost: hasEndpointManagementAccess,
canIsolateHost: isPlatinumPlusLicense && canIsolateHost,
canUnIsolateHost: canIsolateHost,
canKillProcess: hasEndpointManagementAccess && isEnterpriseLicense,
canSuspendProcess: hasEndpointManagementAccess && isEnterpriseLicense,
canGetRunningProcesses: hasEndpointManagementAccess && isEnterpriseLicense,

View file

@ -7,15 +7,19 @@
import type { RenderHookResult, RenderResult } from '@testing-library/react-hooks';
import { act, renderHook } from '@testing-library/react-hooks';
import { useCurrentUser, useKibana } from '../../../lib/kibana';
import { useEndpointPrivileges } from './use_endpoint_privileges';
import { securityMock } from '@kbn/security-plugin/public/mocks';
import type { AuthenticatedUser } from '@kbn/security-plugin/common';
import { licenseService } from '../../../hooks/use_license';
import { getEndpointPrivilegesInitialStateMock } from './mocks';
import { createFleetAuthzMock } from '@kbn/fleet-plugin/common';
import type { EndpointPrivileges } from '../../../../../common/endpoint/types';
import { useCurrentUser, useKibana } from '../../../lib/kibana';
import { licenseService } from '../../../hooks/use_license';
import { useEndpointPrivileges } from './use_endpoint_privileges';
import { getEndpointPrivilegesInitialStateMock } from './mocks';
import { getEndpointPrivilegesInitialState } from './utils';
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
jest.mock('../../../lib/kibana');
jest.mock('../../../hooks/use_license', () => {
const licenseServiceInstance = {
@ -47,6 +51,7 @@ describe('When using useEndpointPrivileges hook', () => {
});
(useCurrentUser as jest.Mock).mockReturnValue(authenticatedUser);
useKibanaMock().services.fleet!.authz = createFleetAuthzMock();
licenseServiceMock.isPlatinumPlus.mockReturnValue(true);

View file

@ -14,6 +14,9 @@ import { act } from '@testing-library/react';
import { endpointPageHttpMock } from '../../../mocks';
import { fireEvent } from '@testing-library/dom';
import { licenseService } from '../../../../../../common/hooks/use_license';
import { useUserPrivileges } from '../../../../../../common/components/user_privileges';
import { initialUserPrivilegesState } from '../../../../../../common/components/user_privileges/user_privileges_context';
import { getUserPrivilegesMockDefaultValue } from '../../../../../../common/components/user_privileges/__mocks__';
jest.mock('../../../../../../common/lib/kibana/kibana_react', () => {
const originalModule = jest.requireActual('../../../../../../common/lib/kibana/kibana_react');
@ -31,6 +34,7 @@ jest.mock('../../../../../../common/lib/kibana/kibana_react', () => {
};
});
jest.mock('../../../../../../common/hooks/use_license');
jest.mock('../../../../../../common/components/user_privileges');
describe('When using the Endpoint Details Actions Menu', () => {
let render: () => Promise<ReturnType<AppContextTestRender['render']>>;
@ -59,6 +63,8 @@ describe('When using the Endpoint Details Actions Menu', () => {
waitForAction = mockedContext.middlewareSpy.waitForAction;
httpMocks = endpointPageHttpMock(mockedContext.coreStart.http);
(useUserPrivileges as jest.Mock).mockReturnValue(getUserPrivilegesMockDefaultValue());
act(() => {
mockedContext.history.push(
'/administration/endpoints?selected_endpoint=5fe11314-678c-413e-87a2-b4a3461878ee'
@ -80,6 +86,10 @@ describe('When using the Endpoint Details Actions Menu', () => {
};
});
afterEach(() => {
(useUserPrivileges as jest.Mock).mockClear();
});
it('should not show the response actions history link', async () => {
await render();
expect(renderResult.queryByTestId('actionsLink')).toBeNull();
@ -121,18 +131,38 @@ describe('When using the Endpoint Details Actions Menu', () => {
describe('and endpoint host is isolated', () => {
beforeEach(() => setEndpointMetadataResponse(true));
it('should display Unisolate action', async () => {
await render();
expect(renderResult.getByTestId('unIsolateLink')).not.toBeNull();
});
it('should navigate via router when unisolate is clicked', async () => {
await render();
act(() => {
fireEvent.click(renderResult.getByTestId('unIsolateLink'));
describe('and user has unisolate privilege', () => {
it('should display Unisolate action', async () => {
await render();
expect(renderResult.getByTestId('unIsolateLink')).not.toBeNull();
});
expect(coreStart.application.navigateToApp).toHaveBeenCalled();
it('should navigate via router when unisolate is clicked', async () => {
await render();
act(() => {
fireEvent.click(renderResult.getByTestId('unIsolateLink'));
});
expect(coreStart.application.navigateToApp).toHaveBeenCalled();
});
});
describe('and user does not have unisolate privilege', () => {
beforeEach(() => {
(useUserPrivileges as jest.Mock).mockReturnValue({
...initialUserPrivilegesState(),
endpointPrivileges: {
...initialUserPrivilegesState().endpointPrivileges,
canIsolateHost: false,
canUnIsolateHost: false,
},
});
});
it('should not display unisolate action', async () => {
await render();
expect(renderResult.queryByTestId('unIsolateLink')).toBeNull();
});
});
});
@ -143,12 +173,6 @@ describe('When using the Endpoint Details Actions Menu', () => {
afterEach(() => licenseServiceMock.isPlatinumPlus.mockReturnValue(true));
it('should not show the `isolate` action', async () => {
setEndpointMetadataResponse();
await render();
expect(renderResult.queryByTestId('isolateLink')).toBeNull();
});
it('should still show `unisolate` action for endpoints that are currently isolated', async () => {
setEndpointMetadataResponse(true);
await render();

View file

@ -19,7 +19,6 @@ import { agentPolicies, uiQueryParams } from '../../store/selectors';
import { useAppUrl } from '../../../../../common/lib/kibana/hooks';
import type { ContextMenuItemNavByRouterProps } from '../../../../components/context_menu_with_router_support/context_menu_item_nav_by_router';
import { isEndpointHostIsolated } from '../../../../../common/utils/validators';
import { useLicense } from '../../../../../common/hooks/use_license';
import { isIsolationSupported } from '../../../../../../common/endpoint/service/host_isolation/utils';
import { useDoesEndpointSupportResponder } from '../../../../../common/hooks/endpoint/use_does_endpoint_support_responder';
import { UPGRADE_ENDPOINT_FOR_RESPONDER } from '../../../../../common/translations';
@ -36,7 +35,6 @@ export const useEndpointActionItems = (
endpointMetadata: MaybeImmutable<HostMetadata> | undefined,
options?: Options
): ContextMenuItemNavByRouterProps[] => {
const isPlatinumPlus = useLicense().isPlatinumPlus();
const { getAppUrl } = useAppUrl();
const fleetAgentPolicies = useEndpointSelector(agentPolicies);
const allCurrentUrlParams = useEndpointSelector(uiQueryParams);
@ -44,7 +42,8 @@ export const useEndpointActionItems = (
const isResponseActionsConsoleEnabled = useIsExperimentalFeatureEnabled(
'responseActionsConsoleEnabled'
);
const canAccessResponseConsole = useUserPrivileges().endpointPrivileges.canAccessResponseConsole;
const { canAccessResponseConsole, canIsolateHost, canUnIsolateHost } =
useUserPrivileges().endpointPrivileges;
const isResponderCapabilitiesEnabled = useDoesEndpointSupportResponder(endpointMetadata);
return useMemo<ContextMenuItemNavByRouterProps[]>(() => {
@ -82,8 +81,8 @@ export const useEndpointActionItems = (
const isolationActions = [];
if (isIsolated) {
// Un-isolate is always available to users regardless of license level
if (isIsolated && canUnIsolateHost) {
// Un-isolate is available to users regardless of license level if they have unisolate permissions
isolationActions.push({
'data-test-subj': 'unIsolateLink',
icon: 'lockOpen',
@ -100,7 +99,7 @@ export const useEndpointActionItems = (
/>
),
});
} else if (isPlatinumPlus && isolationSupported) {
} else if (isolationSupported && canIsolateHost) {
// For Platinum++ licenses, users also have ability to isolate
isolationActions.push({
'data-test-subj': 'isolateLink',
@ -260,10 +259,11 @@ export const useEndpointActionItems = (
endpointMetadata,
fleetAgentPolicies,
getAppUrl,
isPlatinumPlus,
isResponseActionsConsoleEnabled,
showEndpointResponseActionsConsole,
options?.isEndpointList,
isResponderCapabilitiesEnabled,
canIsolateHost,
canUnIsolateHost,
]);
};

View file

@ -1007,6 +1007,7 @@ describe('when on the endpoint list page', () => {
let agentId: string;
let agentPolicyId: string;
let renderResult: ReturnType<AppContextTestRender['render']>;
let endpointActionsButton: HTMLElement;
// 2nd endpoint only has isolation capabilities
const mockEndpointListApi = () => {
@ -1081,13 +1082,7 @@ describe('when on the endpoint list page', () => {
beforeEach(async () => {
mockEndpointListApi();
(useUserPrivileges as jest.Mock).mockReturnValue({
...mockInitialUserPrivilegesState(),
endpointPrivileges: {
...mockInitialUserPrivilegesState().endpointPrivileges,
canAccessResponseConsole: true,
},
});
(useUserPrivileges as jest.Mock).mockReturnValue(getUserPrivilegesMockDefaultValue());
reactTestingLibrary.act(() => {
history.push(`${MANAGEMENT_PATH}/endpoints`);
@ -1097,9 +1092,7 @@ describe('when on the endpoint list page', () => {
await middlewareSpy.waitForAction('serverReturnedEndpointList');
await middlewareSpy.waitForAction('serverReturnedEndpointAgentPolicies');
const endpointActionsButton = (
await renderResult.findAllByTestId('endpointTableRowActions')
)[0];
endpointActionsButton = (await renderResult.findAllByTestId('endpointTableRowActions'))[0];
reactTestingLibrary.act(() => {
reactTestingLibrary.fireEvent.click(endpointActionsButton);
@ -1108,7 +1101,6 @@ describe('when on the endpoint list page', () => {
afterEach(() => {
jest.clearAllMocks();
(useUserPrivileges as jest.Mock).mockReturnValue(getUserPrivilegesMockDefaultValue());
});
it('shows the Responder option when all 3 processes capabilities are present in the endpoint', async () => {
@ -1141,6 +1133,24 @@ describe('when on the endpoint list page', () => {
);
});
it('hides isolate host option if canIsolateHost is false', () => {
(useUserPrivileges as jest.Mock).mockReturnValue({
...mockInitialUserPrivilegesState(),
endpointPrivileges: {
...mockInitialUserPrivilegesState().endpointPrivileges,
canIsolateHost: false,
},
});
reactTestingLibrary.act(() => {
reactTestingLibrary.fireEvent.click(endpointActionsButton);
});
reactTestingLibrary.act(() => {
reactTestingLibrary.fireEvent.click(endpointActionsButton);
});
const isolateLink = screen.queryByTestId('isolateLink');
expect(isolateLink).toBeNull();
});
it('navigates to the Security Solution Host Details page', async () => {
const hostLink = await renderResult.findByTestId('hostLink');
expect(hostLink.getAttribute('href')).toEqual(