mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
42fdcb6981
commit
8a34465b29
7 changed files with 151 additions and 33 deletions
|
@ -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 });
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(),
|
||||
{
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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'],
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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> {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue