[Security Solution][Endpoint] Remove checks for superuser role and instead look at fleet kibana privileges (#120027) (#120380)

* Change endpoint privileges to use fleet authz instead of checking for superuser
* split user privileges react context component from hook in order to better support mocking
* remove `isPlatinumPlus` from endpoint privileges and refactor to use `useUserPrivileges()` hook instead
* add `endpointAuthz` to the Server API route handler context
* moved fleet's `createFleetAuthzMock` to `fleet/common`

# Conflicts:
#	x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx
This commit is contained in:
Paul Tavares 2021-12-03 13:41:51 -05:00 committed by GitHub
parent a7b09bd991
commit f89346697b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 496 additions and 297 deletions

View file

@ -13,3 +13,4 @@ export * from './services';
export * from './types';
export type { FleetAuthz } from './authz';
export { calculateAuthz } from './authz';
export { createFleetAuthzMock } from './mocks';

View file

@ -5,7 +5,8 @@
* 2.0.
*/
import type { NewPackagePolicy, PackagePolicy, DeletePackagePoliciesResponse } from './types';
import type { DeletePackagePoliciesResponse, NewPackagePolicy, PackagePolicy } from './types';
import type { FleetAuthz } from './authz';
export const createNewPackagePolicyMock = (): NewPackagePolicy => {
return {
@ -56,3 +57,27 @@ export const deletePackagePolicyMock = (): DeletePackagePoliciesResponse => {
},
];
};
/**
* Creates mock `authz` object
*/
export const createFleetAuthzMock = (): FleetAuthz => {
return {
fleet: {
all: true,
setup: true,
readEnrollmentTokens: true,
},
integrations: {
readPackageInfo: true,
readInstalledPackages: true,
installPackages: true,
upgradePackages: true,
removePackages: true,
readPackageSettings: true,
writePackageSettings: true,
readIntegrationPolicies: true,
writeIntegrationPolicies: true,
},
};
};

View file

@ -7,11 +7,11 @@
import { of } from 'rxjs';
import {
coreMock,
elasticsearchServiceMock,
loggingSystemMock,
savedObjectsServiceMock,
coreMock,
savedObjectsClientMock,
savedObjectsServiceMock,
} from '../../../../../src/core/server/mocks';
import { dataPluginMock } from '../../../../../src/plugins/data/server/mocks';
import { licensingMock } from '../../../../plugins/licensing/server/mocks';
@ -21,7 +21,7 @@ import type { PackagePolicyServiceInterface } from '../services/package_policy';
import type { AgentPolicyServiceInterface, PackageService } from '../services';
import type { FleetAppContext } from '../plugin';
import { createMockTelemetryEventsSender } from '../telemetry/__mocks__';
import type { FleetAuthz } from '../../common';
import { createFleetAuthzMock } from '../../common';
import { agentServiceMock } from '../services/agents/agent_service.mock';
import type { FleetRequestHandlerContext } from '../types';
@ -145,27 +145,3 @@ export const createMockPackageService = (): PackageService => {
ensureInstalledPackage: jest.fn(),
};
};
/**
* Creates mock `authz` object
*/
export const createFleetAuthzMock = (): FleetAuthz => {
return {
fleet: {
all: true,
setup: true,
readEnrollmentTokens: true,
},
integrations: {
readPackageInfo: true,
readInstalledPackages: true,
installPackages: true,
upgradePackages: true,
removePackages: true,
readPackageSettings: true,
writePackageSettings: true,
readIntegrationPolicies: true,
writeIntegrationPolicies: true,
},
};
};

View file

@ -9,12 +9,14 @@ import { httpServerMock, savedObjectsClientMock } from 'src/core/server/mocks';
import type { PostFleetSetupResponse } from '../../../common';
import { RegistryError } from '../../errors';
import { createAppContextStartContractMock, xpackMocks, createFleetAuthzMock } from '../../mocks';
import { createAppContextStartContractMock, xpackMocks } from '../../mocks';
import { agentServiceMock } from '../../services/agents/agent_service.mock';
import { appContextService } from '../../services/app_context';
import { setupFleet } from '../../services/setup';
import type { FleetRequestHandlerContext } from '../../types';
import { createFleetAuthzMock } from '../../../common';
import { fleetSetupHandler } from './handlers';
jest.mock('../../services/setup', () => {

View file

@ -0,0 +1,75 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { calculateEndpointAuthz, getEndpointAuthzInitialState } from './authz';
import { createFleetAuthzMock, FleetAuthz } from '../../../../../fleet/common';
import { createLicenseServiceMock } from '../../../license/mocks';
import type { EndpointAuthz } from '../../types/authz';
describe('Endpoint Authz service', () => {
let licenseService: ReturnType<typeof createLicenseServiceMock>;
let fleetAuthz: FleetAuthz;
beforeEach(() => {
licenseService = createLicenseServiceMock();
fleetAuthz = createFleetAuthzMock();
});
describe('calculateEndpointAuthz()', () => {
describe('and `fleet.all` access is true', () => {
it.each<Array<keyof EndpointAuthz>>([
['canAccessFleet'],
['canAccessEndpointManagement'],
['canIsolateHost'],
])('should set `%s` to `true`', (authProperty) => {
expect(calculateEndpointAuthz(licenseService, fleetAuthz)[authProperty]).toBe(true);
});
it('should set `canIsolateHost` to false if not proper license', () => {
licenseService.isPlatinumPlus.mockReturnValue(false);
expect(calculateEndpointAuthz(licenseService, fleetAuthz).canIsolateHost).toBe(false);
});
it('should set `canUnIsolateHost` to true even if not proper license', () => {
licenseService.isPlatinumPlus.mockReturnValue(false);
expect(calculateEndpointAuthz(licenseService, fleetAuthz).canUnIsolateHost).toBe(true);
});
});
describe('and `fleet.all` access is false', () => {
beforeEach(() => (fleetAuthz.fleet.all = false));
it.each<Array<keyof EndpointAuthz>>([
['canAccessFleet'],
['canAccessEndpointManagement'],
['canIsolateHost'],
])('should set `%s` to `false`', (authProperty) => {
expect(calculateEndpointAuthz(licenseService, fleetAuthz)[authProperty]).toBe(false);
});
it('should set `canUnIsolateHost` to true even if not proper license', () => {
licenseService.isPlatinumPlus.mockReturnValue(false);
expect(calculateEndpointAuthz(licenseService, fleetAuthz).canUnIsolateHost).toBe(true);
});
});
});
describe('getEndpointAuthzInitialState()', () => {
it('returns expected initial state', () => {
expect(getEndpointAuthzInitialState()).toEqual({
canAccessFleet: false,
canAccessEndpointManagement: false,
canIsolateHost: false,
canUnIsolateHost: true,
canCreateArtifactsByPolicy: false,
});
});
});
});

View file

@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { LicenseService } from '../../../license';
import { FleetAuthz } from '../../../../../fleet/common';
import { EndpointAuthz } from '../../types/authz';
/**
* Used by both the server and the UI to generate the Authorization for access to Endpoint related
* functionality
*
* @param licenseService
* @param fleetAuthz
*/
export const calculateEndpointAuthz = (
licenseService: LicenseService,
fleetAuthz: FleetAuthz
): EndpointAuthz => {
const isPlatinumPlusLicense = licenseService.isPlatinumPlus();
const hasAllAccessToFleet = fleetAuthz.fleet.all;
return {
canAccessFleet: hasAllAccessToFleet,
canAccessEndpointManagement: hasAllAccessToFleet,
canCreateArtifactsByPolicy: isPlatinumPlusLicense,
canIsolateHost: isPlatinumPlusLicense && hasAllAccessToFleet,
canUnIsolateHost: true,
};
};
export const getEndpointAuthzInitialState = (): EndpointAuthz => {
return {
canAccessFleet: false,
canAccessEndpointManagement: false,
canCreateArtifactsByPolicy: false,
canIsolateHost: false,
canUnIsolateHost: true,
};
};

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { getEndpointAuthzInitialState, calculateEndpointAuthz } from './authz';
export { getEndpointAuthzInitialStateMock } from './mocks';

View file

@ -0,0 +1,29 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EndpointAuthz } from '../../types/authz';
import { getEndpointAuthzInitialState } from './authz';
export const getEndpointAuthzInitialStateMock = (
overrides: Partial<EndpointAuthz> = {}
): EndpointAuthz => {
const authz: EndpointAuthz = {
...(
Object.entries(getEndpointAuthzInitialState()) as Array<[keyof EndpointAuthz, boolean]>
).reduce((mockPrivileges, [key, value]) => {
// Invert the initial values (from `false` to `true`) so that everything is authorized
mockPrivileges[key] = !value;
return mockPrivileges;
}, {} as EndpointAuthz),
// this one is currently treated special in that everyone can un-isolate
canUnIsolateHost: true,
...overrides,
};
return authz;
};

View file

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
/**
* Set of Endpoint Specific privileges that control application authorization. This interface is
* used both on the client and server for consistency
*/
export interface EndpointAuthz {
/** If user has permissions to access Fleet */
canAccessFleet: boolean;
/** If user has permissions to access Endpoint management (includes check to ensure they also have access to fleet) */
canAccessEndpointManagement: boolean;
/** if user has permissions to create Artifacts by Policy */
canCreateArtifactsByPolicy: boolean;
/** If user has permissions to isolate hosts */
canIsolateHost: boolean;
/** If user has permissions to un-isolate (release) hosts */
canUnIsolateHost: boolean;
}
export interface EndpointPrivileges extends EndpointAuthz {
loading: boolean;
}

View file

@ -1246,3 +1246,5 @@ interface BaseListResponse<D = unknown> {
* Returned by the server via GET /api/endpoint/metadata
*/
export type MetadataListResponse = BaseListResponse<HostInfo>;
export type { EndpointPrivileges } from './authz';

View file

@ -25,7 +25,7 @@ import { State } from '../common/store';
import { StartServices } from '../types';
import { PageRouter } from './routes';
import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common';
import { UserPrivilegesProvider } from '../common/components/user_privileges';
import { UserPrivilegesProvider } from '../common/components/user_privileges/user_privileges_context';
interface StartAppComponent {
children: React.ReactNode;

View file

@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { initialUserPrivilegesState, UserPrivilegesState } from '../user_privileges_context';
import { getEndpointPrivilegesInitialStateMock } from '../endpoint/mocks';
export const useUserPrivileges = jest.fn(() => {
const mockedPrivileges: UserPrivilegesState = {
...initialUserPrivilegesState(),
endpointPrivileges: getEndpointPrivilegesInitialStateMock(),
};
return mockedPrivileges;
});

View file

@ -5,5 +5,5 @@
* 2.0.
*/
export * from './use_endpoint_privileges';
export { useEndpointPrivileges } from './use_endpoint_privileges';
export { getEndpointPrivilegesInitialState } from './utils';

View file

@ -5,24 +5,16 @@
* 2.0.
*/
import type { EndpointPrivileges } from './use_endpoint_privileges';
import { getEndpointPrivilegesInitialState } from './utils';
import { EndpointPrivileges } from '../../../../../common/endpoint/types';
import { getEndpointAuthzInitialStateMock } from '../../../../../common/endpoint/service/authz/mocks';
export const getEndpointPrivilegesInitialStateMock = (
overrides: Partial<EndpointPrivileges> = {}
): EndpointPrivileges => {
// Get the initial state and set all permissions to `true` (enabled) for testing
export const getEndpointPrivilegesInitialStateMock = ({
loading = false,
...overrides
}: Partial<EndpointPrivileges> = {}): EndpointPrivileges => {
const endpointPrivilegesMock: EndpointPrivileges = {
...(
Object.entries(getEndpointPrivilegesInitialState()) as Array<
[keyof EndpointPrivileges, boolean]
>
).reduce((mockPrivileges, [key, value]) => {
mockPrivileges[key] = !value;
return mockPrivileges;
}, {} as EndpointPrivileges),
...overrides,
...getEndpointAuthzInitialStateMock(overrides),
loading,
};
return endpointPrivilegesMock;

View file

@ -6,14 +6,14 @@
*/
import { act, renderHook, RenderHookResult, RenderResult } from '@testing-library/react-hooks';
import { useHttp, useCurrentUser } from '../../../lib/kibana';
import { EndpointPrivileges, useEndpointPrivileges } from './use_endpoint_privileges';
import { useCurrentUser, useKibana } from '../../../lib/kibana';
import { useEndpointPrivileges } from './use_endpoint_privileges';
import { securityMock } from '../../../../../../security/public/mocks';
import { appRoutesService } from '../../../../../../fleet/common';
import { AuthenticatedUser } from '../../../../../../security/common';
import { licenseService } from '../../../hooks/use_license';
import { fleetGetCheckPermissionsHttpMock } from '../../../../management/pages/mocks';
import { getEndpointPrivilegesInitialStateMock } from './mocks';
import { EndpointPrivileges } from '../../../../../common/endpoint/types';
import { getEndpointPrivilegesInitialState } from './utils';
jest.mock('../../../lib/kibana');
jest.mock('../../../hooks/use_license', () => {
@ -32,10 +32,9 @@ const licenseServiceMock = licenseService as jest.Mocked<typeof licenseService>;
describe('When using useEndpointPrivileges hook', () => {
let authenticatedUser: AuthenticatedUser;
let fleetApiMock: ReturnType<typeof fleetGetCheckPermissionsHttpMock>;
let result: RenderResult<EndpointPrivileges>;
let unmount: ReturnType<typeof renderHook>['unmount'];
let waitForNextUpdate: ReturnType<typeof renderHook>['waitForNextUpdate'];
let releaseFleetAuthz: () => void;
let render: () => RenderHookResult<void, EndpointPrivileges>;
beforeEach(() => {
@ -45,14 +44,19 @@ describe('When using useEndpointPrivileges hook', () => {
(useCurrentUser as jest.Mock).mockReturnValue(authenticatedUser);
fleetApiMock = fleetGetCheckPermissionsHttpMock(
useHttp() as Parameters<typeof fleetGetCheckPermissionsHttpMock>[0]
);
licenseServiceMock.isPlatinumPlus.mockReturnValue(true);
// Add a daly to fleet service that provides authz information
const fleetAuthz = useKibana().services.fleet!.authz;
// Add a delay to the fleet Authz promise to test out the `loading` property
useKibana().services.fleet!.authz = new Promise((resolve) => {
releaseFleetAuthz = () => resolve(fleetAuthz);
});
render = () => {
const hookRenderResponse = renderHook(() => useEndpointPrivileges());
({ result, unmount, waitForNextUpdate } = hookRenderResponse);
({ result, unmount } = hookRenderResponse);
return hookRenderResponse;
};
});
@ -62,88 +66,22 @@ describe('When using useEndpointPrivileges hook', () => {
});
it('should return `loading: true` while retrieving privileges', async () => {
// Add a daly to the API response that we can control from the test
let releaseApiResponse: () => void;
fleetApiMock.responseProvider.checkPermissions.mockDelay.mockReturnValue(
new Promise<void>((resolve) => {
releaseApiResponse = () => resolve();
})
);
(useCurrentUser as jest.Mock).mockReturnValue(null);
const { rerender } = render();
expect(result.current).toEqual(
getEndpointPrivilegesInitialStateMock({
canAccessEndpointManagement: false,
canAccessFleet: false,
loading: true,
})
);
expect(result.current).toEqual(getEndpointPrivilegesInitialState());
// Make user service available
(useCurrentUser as jest.Mock).mockReturnValue(authenticatedUser);
rerender();
expect(result.current).toEqual(
getEndpointPrivilegesInitialStateMock({
canAccessEndpointManagement: false,
canAccessFleet: false,
loading: true,
})
);
expect(result.current).toEqual(getEndpointPrivilegesInitialState());
// Release the API response
await act(async () => {
fleetApiMock.waitForApi();
releaseApiResponse!();
releaseFleetAuthz();
await useKibana().services.fleet!.authz;
});
expect(result.current).toEqual(getEndpointPrivilegesInitialStateMock());
});
it('should call Fleet permissions api to determine user privilege to fleet', async () => {
render();
await waitForNextUpdate();
await fleetApiMock.waitForApi();
expect(useHttp().get as jest.Mock).toHaveBeenCalledWith(
appRoutesService.getCheckPermissionsPath()
);
});
it('should set privileges to false if user does not have superuser role', async () => {
authenticatedUser.roles = [];
render();
await waitForNextUpdate();
await fleetApiMock.waitForApi();
expect(result.current).toEqual(
getEndpointPrivilegesInitialStateMock({
canAccessEndpointManagement: false,
})
);
});
it('should set privileges to false if fleet api check returns failure', async () => {
fleetApiMock.responseProvider.checkPermissions.mockReturnValue({
error: 'MISSING_SECURITY',
success: false,
});
render();
await waitForNextUpdate();
await fleetApiMock.waitForApi();
expect(result.current).toEqual(
getEndpointPrivilegesInitialStateMock({
canAccessEndpointManagement: false,
canAccessFleet: false,
})
);
});
it.each([['canIsolateHost'], ['canCreateArtifactsByPolicy']])(
'should set %s to false if license is not PlatinumPlus',
async (privilege) => {
licenseServiceMock.isPlatinumPlus.mockReturnValue(false);
render();
await waitForNextUpdate();
expect(result.current).toEqual(expect.objectContaining({ [privilege]: false }));
}
);
});

View file

@ -6,24 +6,14 @@
*/
import { useEffect, useMemo, useRef, useState } from 'react';
import { useCurrentUser, useHttp } from '../../../lib/kibana';
import { appRoutesService, CheckPermissionsResponse } from '../../../../../../fleet/common';
import { useCurrentUser, useKibana } from '../../../lib/kibana';
import { useLicense } from '../../../hooks/use_license';
import { Immutable } from '../../../../../common/endpoint/types';
export interface EndpointPrivileges {
loading: boolean;
/** If user has permissions to access Fleet */
canAccessFleet: boolean;
/** If user has permissions to access Endpoint management (includes check to ensure they also have access to fleet) */
canAccessEndpointManagement: boolean;
/** if user has permissions to create Artifacts by Policy */
canCreateArtifactsByPolicy: boolean;
/** If user has permissions to use the Host isolation feature */
canIsolateHost: boolean;
/** @deprecated do not use. instead, use one of the other privileges defined */
isPlatinumPlus: boolean;
}
import { EndpointPrivileges, Immutable } from '../../../../../common/endpoint/types';
import {
calculateEndpointAuthz,
getEndpointAuthzInitialState,
} from '../../../../../common/endpoint/service/authz';
import { FleetAuthz } from '../../../../../../fleet/common';
/**
* Retrieve the endpoint privileges for the current user.
@ -32,23 +22,39 @@ export interface EndpointPrivileges {
* to keep API calls to a minimum.
*/
export const useEndpointPrivileges = (): Immutable<EndpointPrivileges> => {
const http = useHttp();
const user = useCurrentUser();
const fleetServices = useKibana().services.fleet;
const isMounted = useRef<boolean>(true);
const isPlatinumPlusLicense = useLicense().isPlatinumPlus();
const [canAccessFleet, setCanAccessFleet] = useState<boolean>(false);
const licenseService = useLicense();
const [fleetCheckDone, setFleetCheckDone] = useState<boolean>(false);
const [fleetAuthz, setFleetAuthz] = useState<FleetAuthz | null>(null);
const privileges = useMemo(() => {
const privilegeList: EndpointPrivileges = Object.freeze({
loading: !fleetCheckDone || !user,
...(fleetAuthz
? calculateEndpointAuthz(licenseService, fleetAuthz)
: getEndpointAuthzInitialState()),
});
return privilegeList;
}, [fleetCheckDone, user, fleetAuthz, licenseService]);
// Check if user can access fleet
useEffect(() => {
if (!fleetServices) {
setFleetCheckDone(true);
return;
}
setFleetCheckDone(false);
(async () => {
try {
const fleetPermissionsResponse = await http.get<CheckPermissionsResponse>(
appRoutesService.getCheckPermissionsPath()
);
const fleetAuthzForCurrentUser = await fleetServices.authz;
if (isMounted.current) {
setCanAccessFleet(fleetPermissionsResponse.success);
setFleetAuthz(fleetAuthzForCurrentUser);
}
} finally {
if (isMounted.current) {
@ -56,30 +62,7 @@ export const useEndpointPrivileges = (): Immutable<EndpointPrivileges> => {
}
}
})();
}, [http]);
// Check if user has `superuser` role
const isSuperUser = useMemo(() => {
if (user?.roles) {
return user.roles.includes('superuser');
}
return false;
}, [user?.roles]);
const privileges = useMemo(() => {
const privilegeList: EndpointPrivileges = Object.freeze({
loading: !fleetCheckDone || !user,
canAccessFleet,
canAccessEndpointManagement: canAccessFleet && isSuperUser,
canCreateArtifactsByPolicy: isPlatinumPlusLicense,
canIsolateHost: isPlatinumPlusLicense,
// FIXME: Remove usages of the property below
/** @deprecated */
isPlatinumPlus: isPlatinumPlusLicense,
});
return privilegeList;
}, [canAccessFleet, fleetCheckDone, isSuperUser, user, isPlatinumPlusLicense]);
}, [fleetServices]);
// Capture if component is unmounted
useEffect(

View file

@ -5,15 +5,12 @@
* 2.0.
*/
import { EndpointPrivileges } from './use_endpoint_privileges';
import { EndpointPrivileges } from '../../../../../common/endpoint/types';
import { getEndpointAuthzInitialState } from '../../../../../common/endpoint/service/authz';
export const getEndpointPrivilegesInitialState = (): EndpointPrivileges => {
return {
loading: true,
canAccessFleet: false,
canAccessEndpointManagement: false,
canIsolateHost: false,
canCreateArtifactsByPolicy: false,
isPlatinumPlus: false,
...getEndpointAuthzInitialState(),
};
};

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useContext } from 'react';
import { DeepReadonly } from 'utility-types';
import { UserPrivilegesContext, UserPrivilegesState } from './user_privileges_context';
export const useUserPrivileges = (): DeepReadonly<UserPrivilegesState> =>
useContext(UserPrivilegesContext);

View file

@ -5,16 +5,14 @@
* 2.0.
*/
import React, { createContext, useContext, useEffect, useState } from 'react';
import { DeepReadonly } from 'utility-types';
import { Capabilities } from '../../../../../../../src/core/public';
import { useFetchDetectionEnginePrivileges } from '../../../detections/components/user_privileges/use_fetch_detection_engine_privileges';
import { useFetchListPrivileges } from '../../../detections/components/user_privileges/use_fetch_list_privileges';
import { EndpointPrivileges, useEndpointPrivileges } from './endpoint';
import React, { createContext, useEffect, useState } from 'react';
import { Capabilities } from '../../../../../../../src/core/types';
import { SERVER_APP_ID } from '../../../../common/constants';
import { getEndpointPrivilegesInitialState } from './endpoint/utils';
import { useFetchListPrivileges } from '../../../detections/components/user_privileges/use_fetch_list_privileges';
import { useFetchDetectionEnginePrivileges } from '../../../detections/components/user_privileges/use_fetch_detection_engine_privileges';
import { getEndpointPrivilegesInitialState, useEndpointPrivileges } from './endpoint';
import { EndpointPrivileges } from '../../../../common/endpoint/types';
export interface UserPrivilegesState {
listPrivileges: ReturnType<typeof useFetchListPrivileges>;
detectionEnginePrivileges: ReturnType<typeof useFetchDetectionEnginePrivileges>;
@ -28,8 +26,9 @@ export const initialUserPrivilegesState = (): UserPrivilegesState => ({
endpointPrivileges: getEndpointPrivilegesInitialState(),
kibanaSecuritySolutionsPrivileges: { crud: false, read: false },
});
const UserPrivilegesContext = createContext<UserPrivilegesState>(initialUserPrivilegesState());
export const UserPrivilegesContext = createContext<UserPrivilegesState>(
initialUserPrivilegesState()
);
interface UserPrivilegesProviderProps {
kibanaCapabilities: Capabilities;
@ -73,6 +72,3 @@ export const UserPrivilegesProvider = ({
</UserPrivilegesContext.Provider>
);
};
export const useUserPrivileges = (): DeepReadonly<UserPrivilegesState> =>
useContext(UserPrivilegesContext);

View file

@ -25,8 +25,8 @@ import {
import { FieldHook } from '../../shared_imports';
import { SUB_PLUGINS_REDUCER } from './utils';
import { createSecuritySolutionStorageMock, localStorageMock } from './mock_local_storage';
import { UserPrivilegesProvider } from '../components/user_privileges';
import { CASES_FEATURE_ID } from '../../../common/constants';
import { UserPrivilegesProvider } from '../components/user_privileges/user_privileges_context';
const state: State = mockGlobalState;

View file

@ -13,7 +13,7 @@ import { Capabilities } from 'src/core/public';
import { useKibana } from '../../../common/lib/kibana';
import * as api from '../../containers/detection_engine/alerts/api';
import { TestProviders } from '../../../common/mock/test_providers';
import { UserPrivilegesProvider } from '../../../common/components/user_privileges';
import { UserPrivilegesProvider } from '../../../common/components/user_privileges/user_privileges_context';
jest.mock('../../../common/lib/kibana');
jest.mock('../../containers/detection_engine/alerts/api');

View file

@ -8,18 +8,21 @@
import React from 'react';
import { act, fireEvent } from '@testing-library/react';
import { AppContextTestRender, createAppRootMockRenderer } from '../../../common/mock/endpoint';
import {
EndpointPrivileges,
useEndpointPrivileges,
} from '../../../common/components/user_privileges/endpoint/use_endpoint_privileges';
import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data';
import { useUserPrivileges } from '../../../common/components/user_privileges';
import { SearchExceptions, SearchExceptionsProps } from '.';
import { getEndpointPrivilegesInitialStateMock } from '../../../common/components/user_privileges/endpoint/mocks';
jest.mock('../../../common/components/user_privileges/endpoint/use_endpoint_privileges');
import {
initialUserPrivilegesState,
UserPrivilegesState,
} from '../../../common/components/user_privileges/user_privileges_context';
import { EndpointPrivileges } from '../../../../common/endpoint/types';
jest.mock('../../../common/components/user_privileges');
let onSearchMock: jest.Mock;
const mockUseEndpointPrivileges = useEndpointPrivileges as jest.Mock;
const mockUseUserPrivileges = useUserPrivileges as jest.Mock;
describe('Search exceptions', () => {
let appTestContext: AppContextTestRender;
@ -28,13 +31,16 @@ describe('Search exceptions', () => {
props?: Partial<SearchExceptionsProps>
) => ReturnType<AppContextTestRender['render']>;
const loadedUserEndpointPrivilegesState = (
const loadedUserPrivilegesState = (
endpointOverrides: Partial<EndpointPrivileges> = {}
): EndpointPrivileges =>
getEndpointPrivilegesInitialStateMock({
isPlatinumPlus: false,
...endpointOverrides,
});
): UserPrivilegesState => {
return {
...initialUserPrivilegesState(),
endpointPrivileges: getEndpointPrivilegesInitialStateMock({
...endpointOverrides,
}),
};
};
beforeEach(() => {
onSearchMock = jest.fn();
@ -51,11 +57,11 @@ describe('Search exceptions', () => {
return renderResult;
};
mockUseEndpointPrivileges.mockReturnValue(loadedUserEndpointPrivilegesState());
mockUseUserPrivileges.mockReturnValue(loadedUserPrivilegesState());
});
afterAll(() => {
mockUseEndpointPrivileges.mockReset();
mockUseUserPrivileges.mockReset();
});
it('should have a default value', () => {
@ -102,8 +108,8 @@ describe('Search exceptions', () => {
it('should hide policies selector when no license', () => {
const generator = new EndpointDocGenerator('policy-list');
const policy = generator.generatePolicyPackagePolicy();
mockUseEndpointPrivileges.mockReturnValue(
loadedUserEndpointPrivilegesState({ isPlatinumPlus: false })
mockUseUserPrivileges.mockReturnValue(
loadedUserPrivilegesState({ canCreateArtifactsByPolicy: false })
);
const element = render({ policyList: [policy], hasPolicyFilter: true });
@ -113,8 +119,8 @@ describe('Search exceptions', () => {
it('should display policies selector when right license', () => {
const generator = new EndpointDocGenerator('policy-list');
const policy = generator.generatePolicyPackagePolicy();
mockUseEndpointPrivileges.mockReturnValue(
loadedUserEndpointPrivilegesState({ isPlatinumPlus: true })
mockUseUserPrivileges.mockReturnValue(
loadedUserPrivilegesState({ canCreateArtifactsByPolicy: true })
);
const element = render({ policyList: [policy], hasPolicyFilter: true });

View file

@ -10,7 +10,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiFieldSearch, EuiButton } from '@elastic/e
import { i18n } from '@kbn/i18n';
import { PolicySelectionItem, PoliciesSelector } from '../policies_selector';
import { ImmutableArray, PolicyData } from '../../../../common/endpoint/types';
import { useEndpointPrivileges } from '../../../common/components/user_privileges/endpoint/use_endpoint_privileges';
import { useUserPrivileges } from '../../../common/components/user_privileges';
export interface SearchExceptionsProps {
defaultValue?: string;
@ -34,7 +34,7 @@ export const SearchExceptions = memo<SearchExceptionsProps>(
defaultExcludedPolicies,
hideRefreshButton = false,
}) => {
const { isPlatinumPlus } = useEndpointPrivileges();
const { canCreateArtifactsByPolicy } = useUserPrivileges().endpointPrivileges;
const [query, setQuery] = useState<string>(defaultValue);
const [includedPolicies, setIncludedPolicies] = useState<string>(defaultIncludedPolicies || '');
const [excludedPolicies, setExcludedPolicies] = useState<string>(defaultExcludedPolicies || '');
@ -92,7 +92,7 @@ export const SearchExceptions = memo<SearchExceptionsProps>(
data-test-subj="searchField"
/>
</EuiFlexItem>
{isPlatinumPlus && hasPolicyFilter && policyList ? (
{canCreateArtifactsByPolicy && hasPolicyFilter && policyList ? (
<EuiFlexItem grow={false}>
<PoliciesSelector
policies={policyList}

View file

@ -13,11 +13,12 @@ import { HOST_ISOLATION_EXCEPTIONS_PATH } from '../../../../../common/constants'
import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint';
import { getHostIsolationExceptionItems } from '../service';
import { HostIsolationExceptionsList } from './host_isolation_exceptions_list';
import { useEndpointPrivileges } from '../../../../common/components/user_privileges/endpoint';
import { useUserPrivileges as _useUserPrivileges } from '../../../../common/components/user_privileges';
import { EndpointPrivileges } from '../../../../../common/endpoint/types';
jest.mock('../service');
jest.mock('../../../../common/hooks/use_license');
jest.mock('../../../../common/components/user_privileges/endpoint/use_endpoint_privileges');
jest.mock('../../../../common/components/user_privileges');
const getHostIsolationExceptionItemsMock = getHostIsolationExceptionItems as jest.Mock;
@ -27,7 +28,20 @@ describe('When on the host isolation exceptions page', () => {
let history: AppContextTestRender['history'];
let mockedContext: AppContextTestRender;
const useEndpointPrivilegesMock = useEndpointPrivileges as jest.Mock;
const useUserPrivilegesMock = _useUserPrivileges as jest.Mock;
const setEndpointPrivileges = (overrides: Partial<EndpointPrivileges> = {}) => {
const newPrivileges = _useUserPrivileges();
useUserPrivilegesMock.mockReturnValue({
...newPrivileges,
endpointPrivileges: {
...newPrivileges.endpointPrivileges,
...overrides,
},
});
};
const waitForApiCall = () => {
return waitFor(() => expect(getHostIsolationExceptionItemsMock).toHaveBeenCalled());
};
@ -162,7 +176,7 @@ describe('When on the host isolation exceptions page', () => {
describe('has canIsolateHost privileges', () => {
beforeEach(async () => {
useEndpointPrivilegesMock.mockReturnValue({ canIsolateHost: true });
setEndpointPrivileges({ canIsolateHost: true });
getHostIsolationExceptionItemsMock.mockImplementation(getFoundExceptionListItemSchemaMock);
});
@ -185,7 +199,7 @@ describe('When on the host isolation exceptions page', () => {
describe('does not have canIsolateHost privileges', () => {
beforeEach(() => {
useEndpointPrivilegesMock.mockReturnValue({ canIsolateHost: false });
setEndpointPrivileges({ canIsolateHost: false });
});
it('should not show the create flyout if the user navigates to the create url', () => {

View file

@ -31,11 +31,11 @@ import {
EDIT_HOST_ISOLATION_EXCEPTION_LABEL,
} from './components/translations';
import { getEndpointListPath } from '../../../common/routing';
import { useEndpointPrivileges } from '../../../../common/components/user_privileges/endpoint';
import {
MANAGEMENT_DEFAULT_PAGE_SIZE,
MANAGEMENT_PAGE_SIZE_OPTIONS,
} from '../../../common/constants';
import { useUserPrivileges } from '../../../../common/components/user_privileges';
type HostIsolationExceptionPaginatedContent = PaginatedContentProps<
Immutable<ExceptionListItemSchema>,
@ -44,7 +44,7 @@ type HostIsolationExceptionPaginatedContent = PaginatedContentProps<
export const HostIsolationExceptionsList = () => {
const history = useHistory();
const privileges = useEndpointPrivileges();
const privileges = useUserPrivileges().endpointPrivileges;
const location = useHostIsolationExceptionsSelector(getCurrentLocation);
const navigateCallback = useHostIsolationExceptionsNavigateCallback();

View file

@ -10,7 +10,7 @@ import { EuiEmptyPrompt, EuiButton, EuiPageTemplate, EuiLink } from '@elastic/eu
import { FormattedMessage } from '@kbn/i18n-react';
import { usePolicyDetailsNavigateCallback } from '../../policy_hooks';
import { useGetLinkTo } from './use_policy_trusted_apps_empty_hooks';
import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges';
import { useUserPrivileges } from '../../../../../../common/components/user_privileges';
interface CommonProps {
policyId: string;
@ -18,7 +18,7 @@ interface CommonProps {
}
export const PolicyTrustedAppsEmptyUnassigned = memo<CommonProps>(({ policyId, policyName }) => {
const { isPlatinumPlus } = useEndpointPrivileges();
const { canCreateArtifactsByPolicy } = useUserPrivileges().endpointPrivileges;
const navigateCallback = usePolicyDetailsNavigateCallback();
const { onClickHandler, toRouteUrl } = useGetLinkTo(policyId, policyName);
const onClickPrimaryButtonHandler = useCallback(
@ -49,7 +49,7 @@ export const PolicyTrustedAppsEmptyUnassigned = memo<CommonProps>(({ policyId, p
/>
}
actions={[
...(isPlatinumPlus
...(canCreateArtifactsByPolicy
? [
<EuiButton
color="primary"

View file

@ -128,7 +128,7 @@ describe('Policy trusted apps layout', () => {
it('should hide assign button on empty state with unassigned policies when downgraded to a gold or below license', async () => {
mockUseEndpointPrivileges.mockReturnValue(
getEndpointPrivilegesInitialStateMock({
isPlatinumPlus: false,
canCreateArtifactsByPolicy: false,
})
);
const component = render();
@ -146,7 +146,7 @@ describe('Policy trusted apps layout', () => {
it('should hide the `Assign trusted applications` button when there is data and the license is downgraded to gold or below', async () => {
mockUseEndpointPrivileges.mockReturnValue(
getEndpointPrivilegesInitialStateMock({
isPlatinumPlus: false,
canCreateArtifactsByPolicy: false,
})
);
const component = render();

View file

@ -30,10 +30,10 @@ import {
import { usePolicyDetailsNavigateCallback, usePolicyDetailsSelector } from '../../policy_hooks';
import { PolicyTrustedAppsFlyout } from '../flyout';
import { PolicyTrustedAppsList } from '../list/policy_trusted_apps_list';
import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges';
import { useAppUrl } from '../../../../../../common/lib/kibana';
import { APP_UI_ID } from '../../../../../../../common/constants';
import { getTrustedAppsListPath } from '../../../../../common/routing';
import { useUserPrivileges } from '../../../../../../common/components/user_privileges';
export const PolicyTrustedAppsLayout = React.memo(() => {
const { getAppUrl } = useAppUrl();
@ -43,7 +43,7 @@ export const PolicyTrustedAppsLayout = React.memo(() => {
const policyItem = usePolicyDetailsSelector(policyDetails);
const navigateCallback = usePolicyDetailsNavigateCallback();
const hasAssignedTrustedApps = usePolicyDetailsSelector(doesPolicyHaveTrustedApps);
const { isPlatinumPlus } = useEndpointPrivileges();
const { canCreateArtifactsByPolicy } = useUserPrivileges().endpointPrivileges;
const totalAssignedCount = usePolicyDetailsSelector(
getPolicyTrustedAppsListPagination
).totalItemCount;
@ -139,7 +139,9 @@ export const PolicyTrustedAppsLayout = React.memo(() => {
</EuiText>
</EuiPageHeaderSection>
<EuiPageHeaderSection>{isPlatinumPlus && assignTrustedAppButton}</EuiPageHeaderSection>
<EuiPageHeaderSection>
{canCreateArtifactsByPolicy && assignTrustedAppButton}
</EuiPageHeaderSection>
</EuiPageHeader>
<EuiSpacer size="m" />
@ -168,7 +170,7 @@ export const PolicyTrustedAppsLayout = React.memo(() => {
<PolicyTrustedAppsList hideTotalShowingLabel={true} />
)}
</EuiPageContent>
{isPlatinumPlus && showListFlyout ? <PolicyTrustedAppsFlyout /> : null}
{canCreateArtifactsByPolicy && showListFlyout ? <PolicyTrustedAppsFlyout /> : null}
</div>
) : null;
});

View file

@ -20,14 +20,12 @@ import {
} from '../../../../../state';
import { fireEvent, within, act, waitFor } from '@testing-library/react';
import { APP_UI_ID } from '../../../../../../../common/constants';
import {
EndpointPrivileges,
useEndpointPrivileges,
} from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges';
import { useUserPrivileges } from '../../../../../../common/components/user_privileges';
import { getEndpointPrivilegesInitialStateMock } from '../../../../../../common/components/user_privileges/endpoint/mocks';
import { EndpointPrivileges } from '../../../../../../../common/endpoint/types';
jest.mock('../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges');
const mockUseEndpointPrivileges = useEndpointPrivileges as jest.Mock;
jest.mock('../../../../../../common/components/user_privileges');
const mockUseUserPrivileges = useUserPrivileges as jest.Mock;
describe('when rendering the PolicyTrustedAppsList', () => {
// The index (zero based) of the card created by the generator that is policy specific
@ -82,11 +80,14 @@ describe('when rendering the PolicyTrustedAppsList', () => {
};
afterAll(() => {
mockUseEndpointPrivileges.mockReset();
mockUseUserPrivileges.mockReset();
});
beforeEach(() => {
appTestContext = createAppRootMockRenderer();
mockUseEndpointPrivileges.mockReturnValue(loadedUserEndpointPrivilegesState());
mockUseUserPrivileges.mockReturnValue({
...mockUseUserPrivileges(),
endpointPrivileges: loadedUserEndpointPrivilegesState(),
});
mockedApis = policyDetailsPageAllApiHttpMocks(appTestContext.coreStart.http);
appTestContext.setExperimentalFlag({ trustedAppsByPolicyEnabled: true });
@ -324,11 +325,12 @@ describe('when rendering the PolicyTrustedAppsList', () => {
});
it('does not show remove option in actions menu if license is downgraded to gold or below', async () => {
mockUseEndpointPrivileges.mockReturnValue(
loadedUserEndpointPrivilegesState({
isPlatinumPlus: false,
})
);
mockUseUserPrivileges.mockReturnValue({
...mockUseUserPrivileges(),
endpointPrivileges: loadedUserEndpointPrivilegesState({
canCreateArtifactsByPolicy: false,
}),
});
await render();
await toggleCardActionMenu(POLICY_SPECIFIC_CARD_INDEX);

View file

@ -38,7 +38,7 @@ import { ContextMenuItemNavByRouterProps } from '../../../../../components/conte
import { ArtifactEntryCollapsibleCardProps } from '../../../../../components/artifact_entry_card';
import { useTestIdGenerator } from '../../../../../components/hooks/use_test_id_generator';
import { RemoveTrustedAppFromPolicyModal } from './remove_trusted_app_from_policy_modal';
import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges';
import { useUserPrivileges } from '../../../../../../common/components/user_privileges';
const DATA_TEST_SUBJ = 'policyTrustedAppsGrid';
@ -52,7 +52,7 @@ export const PolicyTrustedAppsList = memo<PolicyTrustedAppsListProps>(
const toasts = useToasts();
const history = useHistory();
const { getAppUrl } = useAppUrl();
const { isPlatinumPlus } = useEndpointPrivileges();
const { canCreateArtifactsByPolicy } = useUserPrivileges().endpointPrivileges;
const policyId = usePolicyDetailsSelector(policyIdFromParams);
const hasTrustedApps = usePolicyDetailsSelector(doesPolicyHaveTrustedApps);
const isLoading = usePolicyDetailsSelector(isPolicyTrustedAppListLoading);
@ -158,7 +158,7 @@ export const PolicyTrustedAppsList = memo<PolicyTrustedAppsListProps>(
];
const thisTrustedAppCardProps: ArtifactCardGridCardComponentProps = {
expanded: Boolean(isCardExpanded[trustedApp.id]),
actions: isPlatinumPlus
actions: canCreateArtifactsByPolicy
? [
...fullDetailsAction,
{
@ -194,7 +194,14 @@ export const PolicyTrustedAppsList = memo<PolicyTrustedAppsListProps>(
}
return newCardProps;
}, [allPoliciesById, getAppUrl, getTestId, isCardExpanded, trustedAppItems, isPlatinumPlus]);
}, [
allPoliciesById,
getAppUrl,
getTestId,
isCardExpanded,
trustedAppItems,
canCreateArtifactsByPolicy,
]);
const provideCardProps = useCallback<Required<ArtifactCardGridProps>['cardComponentProps']>(
(item) => {

View file

@ -17,10 +17,7 @@ import {
UseMessagesStorage,
} from '../../common/containers/local_storage/use_messages_storage';
import { Overview } from './index';
import {
initialUserPrivilegesState,
useUserPrivileges,
} from '../../common/components/user_privileges';
import { useUserPrivileges } from '../../common/components/user_privileges';
import { useSourcererDataView } from '../../common/containers/sourcerer';
import { useFetchIndex } from '../../common/containers/source';
import { useIsThreatIntelModuleEnabled } from '../containers/overview_cti_links/use_is_threat_intel_module_enabled';
@ -30,9 +27,10 @@ import {
mockCtiLinksResponse,
} from '../components/overview_cti_links/mock';
import { useCtiDashboardLinks } from '../containers/overview_cti_links';
import { EndpointPrivileges } from '../../common/components/user_privileges/endpoint/use_endpoint_privileges';
import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features';
import { useHostsRiskScore } from '../containers/overview_risky_host_links/use_hosts_risk_score';
import { initialUserPrivilegesState } from '../../common/components/user_privileges/user_privileges_context';
import { EndpointPrivileges } from '../../../common/endpoint/types';
jest.mock('../../common/lib/kibana');
jest.mock('../../common/containers/source');

View file

@ -17,9 +17,8 @@ import {
createMockAgentPolicyService,
createMockAgentService,
createArtifactsClientMock,
createFleetAuthzMock,
} from '../../../fleet/server/mocks';
import { createMockConfig } from '../lib/detection_engine/routes/__mocks__';
import { createMockConfig, requestContextMock } from '../lib/detection_engine/routes/__mocks__';
import {
EndpointAppContextService,
EndpointAppContextServiceSetupContract,
@ -40,6 +39,7 @@ import { parseExperimentalConfigValue } from '../../common/experimental_features
import { createCasesClientMock } from '../../../cases/server/client/mocks';
import { requestContextFactoryMock } from '../request_context_factory.mock';
import { EndpointMetadataService } from './services/metadata';
import { createFleetAuthzMock } from '../../../fleet/common';
/**
* Creates a mocked EndpointAppContext.
@ -183,8 +183,7 @@ export function createRouteHandlerContext(
dataClient: jest.Mocked<IScopedClusterClient>,
savedObjectsClient: jest.Mocked<SavedObjectsClientContract>
) {
const context =
xpackMocks.createRequestHandlerContext() as unknown as jest.Mocked<SecuritySolutionRequestHandlerContext>;
const context = requestContextMock.create() as jest.Mocked<SecuritySolutionRequestHandlerContext>;
context.core.elasticsearch.client = dataClient;
context.core.savedObjects.client = savedObjectsClient;
return context;

View file

@ -48,6 +48,7 @@ import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'
import { legacyMetadataSearchResponseMock } from '../metadata/support/test_support';
import { AGENT_ACTIONS_INDEX, ElasticsearchAssetType } from '../../../../../fleet/common';
import { CasesClientMock } from '../../../../../cases/server/client/mocks';
import { EndpointAuthz } from '../../../../common/endpoint/types/authz';
interface CallRouteInterface {
body?: HostIsolationRequestBody;
@ -55,6 +56,7 @@ interface CallRouteInterface {
searchResponse?: HostMetadata;
mockUser?: any;
license?: License;
authz?: Partial<EndpointAuthz>;
}
const Platinum = licenseMock.createLicense({ license: { type: 'platinum', mode: 'platinum' } });
@ -182,7 +184,7 @@ describe('Host Isolation', () => {
// it returns the requestContext mock used in the call, to assert internal calls (e.g. the indexed document)
callRoute = async (
routePrefix: string,
{ body, idxResponse, searchResponse, mockUser, license }: CallRouteInterface,
{ body, idxResponse, searchResponse, mockUser, license, authz = {} }: CallRouteInterface,
indexExists?: { endpointDsExists: boolean }
): Promise<jest.Mocked<SecuritySolutionRequestHandlerContext>> => {
const asUser = mockUser ? mockUser : superUser;
@ -191,6 +193,12 @@ describe('Host Isolation', () => {
);
const ctx = createRouteHandlerContext(mockScopedClient, mockSavedObjectClient);
ctx.securitySolution.endpointAuthz = {
...ctx.securitySolution.endpointAuthz,
...authz,
};
// mock _index_template
ctx.core.elasticsearch.client.asInternalUser.indices.existsIndexTemplate = jest
.fn()
@ -206,6 +214,7 @@ describe('Host Isolation', () => {
statusCode: 404,
});
});
const withIdxResp = idxResponse ? idxResponse : { statusCode: 201 };
const mockIndexResponse = jest.fn().mockImplementation(() => Promise.resolve(withIdxResp));
const mockSearchResponse = jest
@ -213,19 +222,25 @@ describe('Host Isolation', () => {
.mockImplementation(() =>
Promise.resolve({ body: legacyMetadataSearchResponseMock(searchResponse) })
);
if (indexExists) {
ctx.core.elasticsearch.client.asInternalUser.index = mockIndexResponse;
}
ctx.core.elasticsearch.client.asCurrentUser.index = mockIndexResponse;
ctx.core.elasticsearch.client.asCurrentUser.search = mockSearchResponse;
const withLicense = license ? license : Platinum;
licenseEmitter.next(withLicense);
const mockRequest = httpServerMock.createKibanaRequest({ body });
const [, routeHandler]: [
RouteConfig<any, any, any, any>,
RequestHandler<any, any, any, any>
] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith(routePrefix))!;
await routeHandler(ctx, mockRequest, mockResponse);
return ctx as unknown as jest.Mocked<SecuritySolutionRequestHandlerContext>;
};
});
@ -424,14 +439,17 @@ describe('Host Isolation', () => {
});
expect(mockResponse.ok).toBeCalled();
});
it('prohibits license levels less than platinum from isolating hosts', async () => {
licenseEmitter.next(Gold);
it('prohibits isolating hosts if no authz for it', async () => {
await callRoute(ISOLATE_HOST_ROUTE, {
body: { endpoint_ids: ['XYZ'] },
authz: { canIsolateHost: false },
license: Gold,
});
expect(mockResponse.forbidden).toBeCalled();
});
it('allows any license level to unisolate', async () => {
licenseEmitter.next(Gold);
await callRoute(UNISOLATE_HOST_ROUTE, {
@ -442,37 +460,33 @@ describe('Host Isolation', () => {
});
});
describe('User Level', () => {
it('allows superuser to perform isolation', async () => {
const superU = { username: 'foo', roles: ['superuser'] };
describe('User Authorization Level', () => {
it('allows user to perform isolation when canIsolateHost is true', async () => {
await callRoute(ISOLATE_HOST_ROUTE, {
body: { endpoint_ids: ['XYZ'] },
mockUser: superU,
});
expect(mockResponse.ok).toBeCalled();
});
it('allows superuser to perform unisolation', async () => {
const superU = { username: 'foo', roles: ['superuser'] };
await callRoute(UNISOLATE_HOST_ROUTE, {
body: { endpoint_ids: ['XYZ'] },
mockUser: superU,
});
expect(mockResponse.ok).toBeCalled();
});
it('prohibits non-admin user from performing isolation', async () => {
const superU = { username: 'foo', roles: ['user'] };
it('allows user to perform unisolation when canUnIsolateHost is true', async () => {
await callRoute(UNISOLATE_HOST_ROUTE, {
body: { endpoint_ids: ['XYZ'] },
});
expect(mockResponse.ok).toBeCalled();
});
it('prohibits user from performing isolation if canIsolateHost is false', async () => {
await callRoute(ISOLATE_HOST_ROUTE, {
body: { endpoint_ids: ['XYZ'] },
mockUser: superU,
authz: { canIsolateHost: false },
});
expect(mockResponse.forbidden).toBeCalled();
});
it('prohibits non-admin user from performing unisolation', async () => {
const superU = { username: 'foo', roles: ['user'] };
it('prohibits user from performing un-isolation if canUnIsolateHost is false', async () => {
await callRoute(UNISOLATE_HOST_ROUTE, {
body: { endpoint_ids: ['XYZ'] },
mockUser: superU,
authz: { canUnIsolateHost: false },
});
expect(mockResponse.forbidden).toBeCalled();
});

View file

@ -33,7 +33,6 @@ import {
import { getMetadataForEndpoints } from '../../services';
import { EndpointAppContext } from '../../types';
import { APP_ID } from '../../../../common/constants';
import { userCanIsolate } from '../../../../common/endpoint/actions';
import { doLogsEndpointActionDsExists } from '../../utils';
/**
@ -100,24 +99,19 @@ export const isolationRequestHandler = function (
SecuritySolutionRequestHandlerContext
> {
return async (context, req, res) => {
// only allow admin users
const user = endpointContext.service.security?.authc.getCurrentUser(req);
if (!userCanIsolate(user?.roles)) {
const { canIsolateHost, canUnIsolateHost } = context.securitySolution.endpointAuthz;
// Ensure user has authorization to use this api
if ((!canIsolateHost && isolate) || (!canUnIsolateHost && !isolate)) {
return res.forbidden({
body: {
message: 'You do not have permission to perform this action',
message:
'You do not have permission to perform this action or license level does not allow for this action',
},
});
}
// isolation requires plat+
if (isolate && !endpointContext.service.getLicenseService()?.isPlatinumPlus()) {
return res.forbidden({
body: {
message: 'Your license level does not allow for this action',
},
});
}
const user = endpointContext.service.security?.authc.getCurrentUser(req);
// fetch the Agent IDs to send the commands to
const endpointIDs = [...new Set(req.body.endpoint_ids)]; // dedupe

View file

@ -30,6 +30,7 @@ import type {
SecuritySolutionApiRequestHandlerContext,
SecuritySolutionRequestHandlerContext,
} from '../../../../types';
import { getEndpointAuthzInitialStateMock } from '../../../../../common/endpoint/service/authz';
const createMockClients = () => {
const core = coreMock.createRequestHandlerContext();
@ -93,6 +94,7 @@ const createSecuritySolutionRequestContextMock = (
return {
core,
endpointAuthz: getEndpointAuthzInitialStateMock(),
getConfig: jest.fn(() => clients.config),
getFrameworkRequest: jest.fn(() => {
return {

View file

@ -17,7 +17,18 @@ import {
SecuritySolutionPluginCoreSetupDependencies,
SecuritySolutionPluginSetupDependencies,
} from './plugin_contract';
import { SecuritySolutionApiRequestHandlerContext } from './types';
import {
SecuritySolutionApiRequestHandlerContext,
SecuritySolutionRequestHandlerContext,
} from './types';
import { Immutable } from '../common/endpoint/types';
import { EndpointAuthz } from '../common/endpoint/types/authz';
import {
calculateEndpointAuthz,
getEndpointAuthzInitialState,
} from '../common/endpoint/service/authz';
import { licenseService } from './lib/license';
import { FleetAuthz } from '../../fleet/common';
export interface IRequestContextFactory {
create(
@ -41,7 +52,7 @@ export class RequestContextFactory implements IRequestContextFactory {
}
public async create(
context: RequestHandlerContext,
context: Omit<SecuritySolutionRequestHandlerContext, 'securitySolution'>,
request: KibanaRequest
): Promise<SecuritySolutionApiRequestHandlerContext> {
const { options, appClientFactory } = this;
@ -55,9 +66,31 @@ export class RequestContextFactory implements IRequestContextFactory {
config,
});
let endpointAuthz: Immutable<EndpointAuthz>;
let fleetAuthz: FleetAuthz;
// If Fleet is enabled, then get its Authz
if (startPlugins.fleet) {
fleetAuthz = context.fleet?.authz ?? (await startPlugins.fleet?.authz.fromRequest(request));
}
return {
core: context.core,
get endpointAuthz(): Immutable<EndpointAuthz> {
// Lazy getter of endpoint Authz. No point in defining it if it is never used.
if (!endpointAuthz) {
// If no fleet (fleet plugin is optional in the configuration), then just turn off all permissions
if (!startPlugins.fleet) {
endpointAuthz = getEndpointAuthzInitialState();
} else {
endpointAuthz = calculateEndpointAuthz(licenseService, fleetAuthz);
}
}
return endpointAuthz;
},
getConfig: () => config,
getFrameworkRequest: () => frameworkRequest,

View file

@ -17,10 +17,12 @@ import { AppClient } from './client';
import { ConfigType } from './config';
import { IRuleExecutionLogClient } from './lib/detection_engine/rule_execution_log/types';
import { FrameworkRequest } from './lib/framework';
import { EndpointAuthz } from '../common/endpoint/types/authz';
export { AppClient };
export interface SecuritySolutionApiRequestHandlerContext extends RequestHandlerContext {
endpointAuthz: EndpointAuthz;
getConfig: () => ConfigType;
getFrameworkRequest: () => FrameworkRequest;
getAppClient: () => AppClient;