[Security Solution][Endpoint][RBAC V1] Show Host Isolation Page only to superusers for RBAC v1 (#144711)

## Summary

- [x] Updates the logic for showing host isolation pages for RBAC v1.
- [x] should allow `superusers` to delete an artifact if exists even
when the license is below `platinum`


### Checklist

- [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:
Ashokaditya 2022-11-10 12:24:45 +01:00 committed by GitHub
parent 42fdcb6981
commit 8a34465b29
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 151 additions and 33 deletions

View file

@ -60,6 +60,7 @@ describe('links', () => {
(calculateEndpointAuthz as jest.Mock).mockReturnValue({
canIsolateHost: true,
canUnIsolateHost: true,
canAccessEndpointManagement: true,
canReadActionsLogManagement: true,
});
@ -75,6 +76,7 @@ describe('links', () => {
(calculateEndpointAuthz as jest.Mock).mockReturnValue({
canIsolateHost: true,
canUnIsolateHost: true,
canAccessEndpointManagement: true,
canReadActionsLogManagement: false,
});
fakeHttpServices.get.mockResolvedValue({ total: 0 });
@ -95,6 +97,7 @@ describe('links', () => {
(calculateEndpointAuthz as jest.Mock).mockReturnValue({
canIsolateHost: false,
canUnIsolateHost: false,
canAccessEndpointManagement: true,
canReadActionsLogManagement: true,
});
@ -112,6 +115,7 @@ describe('links', () => {
(calculateEndpointAuthz as jest.Mock).mockReturnValue({
canIsolateHost: false,
canUnIsolateHost: true,
canAccessEndpointManagement: true,
canReadActionsLogManagement: true,
});
fakeHttpServices.get.mockResolvedValue({ total: 0 });
@ -126,10 +130,30 @@ describe('links', () => {
});
});
it('should return all but HIE when HAS isolation permission AND has HIE entry but not superuser', async () => {
(calculateEndpointAuthz as jest.Mock).mockReturnValue({
canIsolateHost: false,
canUnIsolateHost: true,
canAccessEndpointManagement: false,
canReadActionsLogManagement: true,
});
fakeHttpServices.get.mockResolvedValue({ total: 1 });
const filteredLinks = await getManagementFilteredLinks(
coreMockStarted,
getPlugins(['superuser'])
);
expect(filteredLinks).toEqual({
...links,
links: links.links?.filter((link) => link.id !== SecurityPageName.hostIsolationExceptions),
});
});
it('should return all when NO isolation permission due to license but HAS at least one host isolation exceptions entry', async () => {
(calculateEndpointAuthz as jest.Mock).mockReturnValue({
canIsolateHost: false,
canUnIsolateHost: true,
canAccessEndpointManagement: true,
canReadActionsLogManagement: true,
});
fakeHttpServices.get.mockResolvedValue({ total: 1 });

View file

@ -244,19 +244,26 @@ export const getManagementFilteredLinks = async (
plugins: StartPlugins
): Promise<LinkItem> => {
const fleetAuthz = plugins.fleet?.authz;
const { endpointRbacEnabled, endpointRbacV1Enabled } = ExperimentalFeaturesService.get();
const endpointPermissions = calculatePermissionsFromCapabilities(core.application.capabilities);
const { endpointRbacV1Enabled } = ExperimentalFeaturesService.get();
const hasPermissionsForSecuritySolution = calculatePermissionsFromCapabilities(
core.application.capabilities
);
const linksToExclude: SecurityPageName[] = [];
try {
const currentUserResponse = await plugins.security.authc.getCurrentUser();
const { canReadActionsLogManagement, canIsolateHost, canUnIsolateHost } = fleetAuthz
const {
canReadActionsLogManagement,
canUnIsolateHost,
canIsolateHost,
canAccessEndpointManagement,
} = fleetAuthz
? calculateEndpointAuthz(
licenseService,
fleetAuthz,
currentUserResponse.roles,
endpointRbacEnabled || endpointRbacV1Enabled,
endpointPermissions
endpointRbacV1Enabled,
hasPermissionsForSecuritySolution
)
: getEndpointAuthzInitialState();
@ -265,18 +272,19 @@ export const getManagementFilteredLinks = async (
}
if (!canIsolateHost && canUnIsolateHost) {
let shouldSeeHIEToBeAbleToDeleteEntries: boolean;
let shouldBeAbleToDeleteEntries: boolean;
try {
const hostExceptionCount = await getHostIsolationExceptionTotal(core.http);
shouldSeeHIEToBeAbleToDeleteEntries = hostExceptionCount !== 0;
// has an HIE entry and is a super user then set to TRUE
shouldBeAbleToDeleteEntries = hostExceptionCount !== 0 && canAccessEndpointManagement;
} catch {
shouldSeeHIEToBeAbleToDeleteEntries = false;
shouldBeAbleToDeleteEntries = false;
}
if (!shouldSeeHIEToBeAbleToDeleteEntries) {
if (!shouldBeAbleToDeleteEntries) {
linksToExclude.push(SecurityPageName.hostIsolationExceptions);
}
} else if (!canIsolateHost) {
} else if (!canIsolateHost || !canAccessEndpointManagement) {
linksToExclude.push(SecurityPageName.hostIsolationExceptions);
}
} catch {

View file

@ -36,16 +36,33 @@ describe('host isolation exceptions hooks', () => {
useEndpointPrivilegesMock.mockReset();
});
it('should return true if has the correct privileges', () => {
useEndpointPrivilegesMock.mockReturnValue({ canIsolateHost: true });
it('should return TRUE if IS superuser AND canIsolateHost', () => {
useEndpointPrivilegesMock.mockReturnValue({
canIsolateHost: true,
canAccessEndpointManagement: true,
});
const { result } = renderHook(() => useCanSeeHostIsolationExceptionsMenu(), {
wrapper: TestProviders,
});
expect(result.current).toBe(true);
});
it('should return false if does not have privileges and there are not existing host isolation items', () => {
useEndpointPrivilegesMock.mockReturnValue({ canIsolateHost: false });
it('should return FALSE if NOT superuser AND canIsolateHost', () => {
useEndpointPrivilegesMock.mockReturnValue({
canIsolateHost: true,
canAccessEndpointManagement: false,
});
const { result } = renderHook(() => useCanSeeHostIsolationExceptionsMenu(), {
wrapper: TestProviders,
});
expect(result.current).toBe(false);
});
it('should return FALSE if IS superuser AND and !canIsolateHost and there are no existing host isolation items', () => {
useEndpointPrivilegesMock.mockReturnValue({
canIsolateHost: false,
canAccessEndpointManagement: true,
});
mockedApis.responseProvider.exceptionsSummary.mockReturnValue({
total: 0,
linux: 0,
@ -58,8 +75,11 @@ describe('host isolation exceptions hooks', () => {
expect(result.current).toBe(false);
});
it('should return true if does not have privileges and there are existing host isolation items', async () => {
useEndpointPrivilegesMock.mockReturnValue({ canIsolateHost: false });
it('should return TRUE if IS superuser AND and !canIsolateHost and there are existing host isolation items', async () => {
useEndpointPrivilegesMock.mockReturnValue({
canIsolateHost: false,
canAccessEndpointManagement: true,
});
const { result, waitForNextUpdate } = renderHook(
() => useCanSeeHostIsolationExceptionsMenu(),
{

View file

@ -30,8 +30,11 @@ export function useCanSeeHostIsolationExceptionsMenu(): boolean {
const { data: summary, isFetching, refetch: checkIfHasExceptions, isFetched } = apiQuery;
const canSeeMenu = useMemo(() => {
return privileges.canIsolateHost || Boolean(summary?.total);
}, [privileges.canIsolateHost, summary?.total]);
return (
privileges.canAccessEndpointManagement &&
(privileges.canIsolateHost || Boolean(summary?.total))
);
}, [privileges.canIsolateHost, privileges.canAccessEndpointManagement, summary?.total]);
useEffect(() => {
if (!privileges.canIsolateHost && !privileges.loading && !isFetched && !isFetching) {

View file

@ -105,4 +105,48 @@ describe('When on the host isolation exceptions page', () => {
expect(getByTestId('hostIsolationExceptionsListPage-card-cardDeleteAction')).toBeTruthy();
expect(queryByTestId('hostIsolationExceptionsListPage-card-cardEditAction')).toBeNull();
});
it('should allow Delete action', async () => {
// Use case: license downgrade scenario, where user still has entries defined, but no longer
// able to create or edit them (only Delete them)
const existingPrivileges = useUserPrivilegesMock();
useUserPrivilegesMock.mockReturnValue({
...existingPrivileges,
endpointPrivileges: {
...existingPrivileges.endpointPrivileges,
canIsolateHost: false,
canUnIsolateHost: true,
},
});
const { findAllByTestId, getByTestId } = await render();
await waitFor(async () => {
await expect(findAllByTestId('hostIsolationExceptionsListPage-card')).resolves.toHaveLength(
10
);
});
await getFirstCard(renderResult, {
showActions: true,
testId: 'hostIsolationExceptionsListPage',
});
const deleteButton = getByTestId('hostIsolationExceptionsListPage-card-cardDeleteAction');
expect(deleteButton).toBeTruthy();
userEvent.click(deleteButton);
const confirmDeleteButton = getByTestId(
'hostIsolationExceptionsListPage-deleteModal-submitButton'
);
userEvent.click(confirmDeleteButton);
await waitFor(() => {
expect(apiMocks.responseProvider.exceptionDelete).toHaveReturnedWith(
expect.objectContaining({
namespace_type: 'agnostic',
os_types: ['windows'],
tags: ['policy:all'],
})
);
});
});
});

View file

@ -31,6 +31,7 @@ import { useUserPrivileges } from '../../common/components/user_privileges';
import { HostIsolationExceptionsContainer } from './host_isolation_exceptions';
import { BlocklistContainer } from './blocklist';
import { ResponseActionsContainer } from './response_actions';
import { useCanSeeHostIsolationExceptionsMenu } from './host_isolation_exceptions/view/hooks';
import { PrivilegedRoute } from '../components/privileged_route';
const EndpointTelemetry = () => (
@ -86,6 +87,8 @@ export const ManagementContainer = memo(() => {
canReadEndpointList,
} = useUserPrivileges().endpointPrivileges;
const canSeeHostIsolationExceptionsPage = useCanSeeHostIsolationExceptionsMenu();
// Lets wait until we can verify permissions
if (loading) {
return <EuiLoadingSpinner />;
@ -113,9 +116,10 @@ export const ManagementContainer = memo(() => {
component={EventFilterTelemetry}
hasPrivilege={canReadEventFilters}
/>
<Route
<PrivilegedRoute
path={MANAGEMENT_ROUTING_HOST_ISOLATION_EXCEPTIONS_PATH}
component={HostIsolationExceptionsTelemetry}
hasPrivilege={canSeeHostIsolationExceptionsPage}
/>
<PrivilegedRoute
path={MANAGEMENT_ROUTING_BLOCKLIST_PATH}

View file

@ -60,18 +60,20 @@ export class HostIsolationExceptionsValidator extends BaseValidator {
return item.listId === ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID;
}
protected async validateHasWritePrivilege(): Promise<void> {
return super.validateHasPrivilege('canWriteHostIsolationExceptions');
}
// TODO: 8.7 rbac
// protected async validateHasWritePrivilege(): Promise<void> {
// return super.validateHasPrivilege('canWriteHostIsolationExceptions');
// }
protected async validateHasReadPrivilege(): Promise<void> {
return super.validateHasPrivilege('canReadHostIsolationExceptions');
}
// TODO: 8.7 rbac
// protected async validateHasReadPrivilege(): Promise<void> {
// return super.validateHasPrivilege('canReadHostIsolationExceptions');
// }
async validatePreCreateItem(
item: CreateExceptionListItemOptions
): Promise<CreateExceptionListItemOptions> {
await this.validateHasWritePrivilege();
// TODO add this to 8.7 rbac await this.validateHasWritePrivilege();
await this.validateCanIsolateHosts();
await this.validateHostIsolationData(item);
await this.validateByPolicyItem(item);
@ -84,7 +86,8 @@ export class HostIsolationExceptionsValidator extends BaseValidator {
): Promise<UpdateExceptionListItemOptions> {
const updatedItem = _updatedItem as ExceptionItemLikeOptions;
await this.validateHasWritePrivilege();
// TODO add this to 8.7 rbac add
// await this.validateHasWritePrivilege();
await this.validateCanIsolateHosts();
await this.validateHostIsolationData(updatedItem);
await this.validateByPolicyItem(updatedItem);
@ -93,27 +96,39 @@ export class HostIsolationExceptionsValidator extends BaseValidator {
}
async validatePreGetOneItem(): Promise<void> {
await this.validateHasReadPrivilege();
// TODO: for 8.7 rbac replace with
// await this.validateHasReadPrivilege();
await this.validateCanManageEndpointArtifacts();
}
async validatePreSummary(): Promise<void> {
await this.validateHasReadPrivilege();
// TODO: for 8.7 rbac replace with
// await this.validateHasReadPrivilege();
await this.validateCanManageEndpointArtifacts();
}
async validatePreDeleteItem(): Promise<void> {
await this.validateHasWritePrivilege();
// TODO: for 8.7 rbac replace with
// await this.validateHasWritePrivilege();
await this.validateCanManageEndpointArtifacts();
}
async validatePreExport(): Promise<void> {
await this.validateHasReadPrivilege();
// TODO: for 8.7 rbac replace with
// await this.validateHasReadPrivilege();
await this.validateCanManageEndpointArtifacts();
}
async validatePreSingleListFind(): Promise<void> {
await this.validateHasReadPrivilege();
// TODO: for 8.7 rbac replace with
// await this.validateHasReadPrivilege();
await this.validateCanManageEndpointArtifacts();
}
async validatePreMultiListFind(): Promise<void> {
await this.validateHasReadPrivilege();
// TODO: for 8.7 rbac replace with
// await this.validateHasReadPrivilege();
await this.validateCanManageEndpointArtifacts();
}
async validatePreImport(): Promise<void> {