Skip to content

Commit

Permalink
[Security Soution][Endpoint] Uses RBAC in policy details page for art…
Browse files Browse the repository at this point in the history
…ifacts tabs (#147676)

## Summary

- Hide artifact tabs when no read permissions.
- Hide manage/add artifacts button actions when no write permissions.
- Redirects user to policy details page when missing privileges and add
a toast with error message.
- Remove superuser check for `canCreateArtifactsByPolicy` privielge
- Also updates and adds unit tests
  • Loading branch information
dasansol92 authored Dec 20, 2022
1 parent 6b29787 commit 886289d
Show file tree
Hide file tree
Showing 15 changed files with 337 additions and 192 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ export const calculateEndpointAuthz = (
canReadSecuritySolution,
canAccessFleet: fleetAuthz?.fleet.all ?? userRoles.includes('superuser'),
canAccessEndpointManagement: hasEndpointManagementAccess,
canCreateArtifactsByPolicy: hasEndpointManagementAccess && isPlatinumPlusLicense,
canCreateArtifactsByPolicy: isPlatinumPlusLicense,
canWriteEndpointList,
canReadEndpointList,
canWritePolicyManagement,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,21 @@ interface CommonProps {
policyName: string;
listId: string;
labels: typeof POLICY_ARTIFACT_EMPTY_UNASSIGNED_LABELS;
canWriteArtifact?: boolean;
getPolicyArtifactsPath: (policyId: string) => string;
getArtifactPath: (location?: Partial<ArtifactListPageUrlParams>) => string;
}

export const PolicyArtifactsEmptyUnassigned = memo<CommonProps>(
({ policyId, policyName, listId, labels, getPolicyArtifactsPath, getArtifactPath }) => {
({
policyId,
policyName,
listId,
labels,
canWriteArtifact = false,
getPolicyArtifactsPath,
getArtifactPath,
}) => {
const { canCreateArtifactsByPolicy } = useUserPrivileges().endpointPrivileges;
const { onClickHandler, toRouteUrl } = useGetLinkTo(
policyId,
Expand All @@ -50,24 +59,34 @@ export const PolicyArtifactsEmptyUnassigned = memo<CommonProps>(
iconType="plusInCircle"
data-test-subj="policy-artifacts-empty-unassigned"
title={<h2>{labels.emptyUnassignedTitle}</h2>}
body={labels.emptyUnassignedMessage(policyName)}
body={
canWriteArtifact
? labels.emptyUnassignedMessage(policyName)
: labels.emptyUnassignedNoPrivilegesMessage(policyName)
}
actions={[
...(canCreateArtifactsByPolicy
...(canCreateArtifactsByPolicy && canWriteArtifact
? [
<EuiButton
color="primary"
fill
onClick={onClickPrimaryButtonHandler}
data-test-subj="assign-artifacts-button"
data-test-subj="unassigned-assign-artifacts-button"
>
{labels.emptyUnassignedPrimaryActionButtonTitle}
</EuiButton>,
]
: []),
// eslint-disable-next-line @elastic/eui/href-or-on-click
<EuiLink onClick={onClickHandler} href={toRouteUrl}>
{labels.emptyUnassignedSecondaryActionButtonTitle}
</EuiLink>,
canWriteArtifact ? (
// eslint-disable-next-line @elastic/eui/href-or-on-click
<EuiLink
onClick={onClickHandler}
href={toRouteUrl}
data-test-subj="unassigned-manage-artifacts-button"
>
{labels.emptyUnassignedSecondaryActionButtonTitle}
</EuiLink>
) : null,
]}
/>
</EuiPageTemplate>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,20 @@ interface CommonProps {
policyId: string;
policyName: string;
labels: typeof POLICY_ARTIFACT_EMPTY_UNEXISTING_LABELS;
canWriteArtifact?: boolean;
getPolicyArtifactsPath: (policyId: string) => string;
getArtifactPath: (location?: Partial<ArtifactListPageUrlParams>) => string;
}

export const PolicyArtifactsEmptyUnexisting = memo<CommonProps>(
({ policyId, policyName, labels, getPolicyArtifactsPath, getArtifactPath }) => {
({
policyId,
policyName,
labels,
canWriteArtifact = false,
getPolicyArtifactsPath,
getArtifactPath,
}) => {
const { onClickHandler, toRouteUrl } = useGetLinkTo(
policyId,
policyName,
Expand All @@ -42,10 +50,18 @@ export const PolicyArtifactsEmptyUnexisting = memo<CommonProps>(
title={<h2>{labels.emptyUnexistingTitle}</h2>}
body={labels.emptyUnexistingMessage}
actions={
// eslint-disable-next-line @elastic/eui/href-or-on-click
<EuiButton color="primary" fill onClick={onClickHandler} href={toRouteUrl}>
{labels.emptyUnexistingPrimaryActionButtonTitle}
</EuiButton>
canWriteArtifact ? (
// eslint-disable-next-line @elastic/eui/href-or-on-click
<EuiButton
color="primary"
fill
onClick={onClickHandler}
href={toRouteUrl}
data-test-subj="unexisting-manage-artifacts-button"
>
{labels.emptyUnexistingPrimaryActionButtonTitle}
</EuiButton>
) : null
}
/>
</EuiPageTemplate>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ export const POLICY_ARTIFACT_EMPTY_UNASSIGNED_LABELS = Object.freeze({
defaultMessage: 'Manage artifacts',
}
),
emptyUnassignedNoPrivilegesMessage: (policyName: string): string =>
i18n.translate(
'xpack.securitySolution.endpoint.policy.artifacts.empty.unassigned.noPrivileges.content',
{
defaultMessage: 'There are currently no artifacts assigned to {policyName}.',
values: { policyName },
}
),
});

export const POLICY_ARTIFACT_EMPTY_UNEXISTING_LABELS = Object.freeze({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,26 @@ import type { ImmutableObject, PolicyData } from '../../../../../../../common/en
import { parsePoliciesAndFilterToKql } from '../../../../../common/utils';
import { eventFiltersListQueryHttpMock } from '../../../../event_filters/test_utils';
import { getFoundExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/found_exception_list_item_schema.mock';
import { getEndpointPrivilegesInitialStateMock } from '../../../../../../common/components/user_privileges/endpoint/mocks';
import { POLICY_ARTIFACT_EVENT_FILTERS_LABELS } from '../../tabs/event_filters_translations';
import { EventFiltersApiClient } from '../../../../event_filters/service/api_client';
import { SEARCHABLE_FIELDS as EVENT_FILTERS_SEARCHABLE_FIELDS } from '../../../../event_filters/constants';
import { FormattedMessage } from '@kbn/i18n-react';
import { useUserPrivileges } from '../../../../../../common/components/user_privileges';

let render: (externalPrivileges?: boolean) => Promise<ReturnType<AppContextTestRender['render']>>;
jest.mock('../../../../../../common/components/user_privileges');

interface MockedAPIArgs {
query: { filter: string };
}

let render: (canWriteArtifact?: boolean) => Promise<ReturnType<AppContextTestRender['render']>>;
let mockedContext: AppContextTestRender;
let renderResult: ReturnType<AppContextTestRender['render']>;
let policyItem: ImmutableObject<PolicyData>;
const generator = new EndpointDocGenerator();
let mockedApi: ReturnType<typeof eventFiltersListQueryHttpMock>;
let history: AppContextTestRender['history'];
const useUserPrivilegesMock = useUserPrivileges as jest.Mock;

const getEventFiltersLabels = () => ({
...POLICY_ARTIFACT_EVENT_FILTERS_LABELS,
Expand All @@ -46,17 +53,22 @@ const getEventFiltersLabels = () => ({
});

describe('Policy artifacts layout', () => {
const isFilteredByPolicyQuery = (args?: { query: { filter: string } }) =>
args && args.query.filter === parsePoliciesAndFilterToKql({ policies: [policyItem.id, 'all'] });

beforeEach(() => {
mockedContext = createAppRootMockRenderer();
mockedApi = eventFiltersListQueryHttpMock(mockedContext.coreStart.http);
mockedApi.responseProvider.eventFiltersList.mockClear();
policyItem = generator.generatePolicyPackagePolicy();
({ history } = mockedContext);

getEndpointPrivilegesInitialStateMock({
canCreateArtifactsByPolicy: true,
useUserPrivilegesMock.mockReturnValue({
endpointPrivileges: {
canCreateArtifactsByPolicy: true,
},
});
render = async (externalPrivileges = true) => {
render = async (canWriteArtifact = true) => {
await act(async () => {
renderResult = mockedContext.render(
<PolicyArtifactsLayout
Expand All @@ -68,7 +80,7 @@ describe('Policy artifacts layout', () => {
searchableFields={EVENT_FILTERS_SEARCHABLE_FIELDS}
getArtifactPath={getEventFiltersListPath}
getPolicyArtifactsPath={getPolicyEventFiltersPath}
externalPrivileges={externalPrivileges}
canWriteArtifact={canWriteArtifact}
/>
);
await waitFor(mockedApi.responseProvider.eventFiltersList);
Expand Down Expand Up @@ -104,18 +116,13 @@ describe('Policy artifacts layout', () => {
});

it('should render layout with no assigned artifacts data when there are artifacts', async () => {
mockedApi.responseProvider.eventFiltersList.mockImplementation(
(args?: { query: { filter: string } }) => {
if (
!args ||
args.query.filter !== parsePoliciesAndFilterToKql({ policies: [policyItem.id, 'all'] })
) {
return getFoundExceptionListItemSchemaMock(1);
} else {
return getFoundExceptionListItemSchemaMock(0);
}
mockedApi.responseProvider.eventFiltersList.mockImplementation((args?: MockedAPIArgs) => {
if (!isFilteredByPolicyQuery(args)) {
return getFoundExceptionListItemSchemaMock(1);
} else {
return getFoundExceptionListItemSchemaMock(0);
}
);
});

await render();

Expand All @@ -135,8 +142,10 @@ describe('Policy artifacts layout', () => {
});

it('should hide `Assign artifacts to policy` on empty state with unassigned policies when downgraded to a gold or below license', async () => {
getEndpointPrivilegesInitialStateMock({
canCreateArtifactsByPolicy: false,
useUserPrivilegesMock.mockReturnValue({
endpointPrivileges: {
canCreateArtifactsByPolicy: false,
},
});
mockedApi.responseProvider.eventFiltersList.mockReturnValue(
getFoundExceptionListItemSchemaMock(0)
Expand All @@ -148,8 +157,10 @@ describe('Policy artifacts layout', () => {
});

it('should hide the `Assign artifacts to policy` button license is downgraded to gold or below', async () => {
getEndpointPrivilegesInitialStateMock({
canCreateArtifactsByPolicy: false,
useUserPrivilegesMock.mockReturnValue({
endpointPrivileges: {
canCreateArtifactsByPolicy: false,
},
});
mockedApi.responseProvider.eventFiltersList.mockReturnValue(
getFoundExceptionListItemSchemaMock(5)
Expand All @@ -162,8 +173,10 @@ describe('Policy artifacts layout', () => {
});

it('should hide the `Assign artifacts` flyout when license is downgraded to gold or below', async () => {
getEndpointPrivilegesInitialStateMock({
canCreateArtifactsByPolicy: false,
useUserPrivilegesMock.mockReturnValue({
endpointPrivileges: {
canCreateArtifactsByPolicy: false,
},
});
mockedApi.responseProvider.eventFiltersList.mockReturnValue(
getFoundExceptionListItemSchemaMock(2)
Expand All @@ -185,5 +198,26 @@ describe('Policy artifacts layout', () => {
await render(false);
expect(renderResult.queryByTestId('artifacts-assign-button')).toBeNull();
});
it('should not display assign and manage artifacts buttons on empty state when there are artifacts', async () => {
mockedApi.responseProvider.eventFiltersList.mockImplementation((args?: MockedAPIArgs) => {
if (!isFilteredByPolicyQuery(args)) {
return getFoundExceptionListItemSchemaMock(1);
} else {
return getFoundExceptionListItemSchemaMock(0);
}
});
await render(false);
expect(await renderResult.findByTestId('policy-artifacts-empty-unassigned')).not.toBeNull();
expect(renderResult.queryByTestId('unassigned-assign-artifacts-button')).toBeNull();
expect(renderResult.queryByTestId('unassigned-manage-artifacts-button')).toBeNull();
});
it('should not display manage artifacts button on empty state when there are no artifacts', async () => {
mockedApi.responseProvider.eventFiltersList.mockReturnValue(
getFoundExceptionListItemSchemaMock(0)
);
await render(false);
expect(await renderResult.findByTestId('policy-artifacts-empty-unexisting')).not.toBeNull();
expect(renderResult.queryByTestId('unexisting-manage-artifacts-button')).toBeNull();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ interface PolicyArtifactsLayoutProps {
searchableFields: readonly string[];
getArtifactPath: (location?: Partial<ArtifactListPageUrlParams>) => string;
getPolicyArtifactsPath: (policyId: string) => string;
/** A boolean to check extra privileges for restricted actions, true when it's allowed, false when not */
externalPrivileges?: boolean;
/** A boolean to check if has write artifact privilege or not */
canWriteArtifact?: boolean;
}
export const PolicyArtifactsLayout = React.memo<PolicyArtifactsLayoutProps>(
({
Expand All @@ -53,7 +53,7 @@ export const PolicyArtifactsLayout = React.memo<PolicyArtifactsLayoutProps>(
searchableFields,
getArtifactPath,
getPolicyArtifactsPath,
externalPrivileges = true,
canWriteArtifact = false,
}) => {
const exceptionsListApiClient = useMemo(
() => getExceptionsListApiClient(),
Expand Down Expand Up @@ -161,6 +161,7 @@ export const PolicyArtifactsLayout = React.memo<PolicyArtifactsLayoutProps>(
policyName={policyItem.name}
listId={exceptionsListApiClient.listId}
labels={labels}
canWriteArtifact={canWriteArtifact}
getPolicyArtifactsPath={getPolicyArtifactsPath}
getArtifactPath={getArtifactPath}
/>
Expand All @@ -169,6 +170,7 @@ export const PolicyArtifactsLayout = React.memo<PolicyArtifactsLayoutProps>(
policyId={policyItem.id}
policyName={policyItem.name}
labels={labels}
canWriteArtifact={canWriteArtifact}
getPolicyArtifactsPath={getPolicyArtifactsPath}
getArtifactPath={getArtifactPath}
/>
Expand All @@ -192,10 +194,10 @@ export const PolicyArtifactsLayout = React.memo<PolicyArtifactsLayoutProps>(
</EuiText>
</EuiPageHeaderSection>
<EuiPageHeaderSection>
{canCreateArtifactsByPolicy && externalPrivileges && assignToPolicyButton}
{canCreateArtifactsByPolicy && canWriteArtifact && assignToPolicyButton}
</EuiPageHeaderSection>
</EuiPageHeader>
{canCreateArtifactsByPolicy && externalPrivileges && urlParams.show === 'list' && (
{canCreateArtifactsByPolicy && canWriteArtifact && urlParams.show === 'list' && (
<PolicyArtifactsFlyout
policyItem={policyItem}
apiClient={exceptionsListApiClient}
Expand Down Expand Up @@ -228,7 +230,7 @@ export const PolicyArtifactsLayout = React.memo<PolicyArtifactsLayoutProps>(
searchableFields={[...searchableFields]}
labels={labels}
onDeleteActionCallback={handleOnDeleteActionCallback}
externalPrivileges={externalPrivileges}
canWriteArtifact={canWriteArtifact}
getPolicyArtifactsPath={getPolicyArtifactsPath}
getArtifactPath={getArtifactPath}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const getDefaultQueryParameters = (customFilter: string | undefined = '') => ({
jest.setTimeout(10000);

describe('Policy details artifacts list', () => {
let render: (externalPrivileges?: boolean) => Promise<ReturnType<AppContextTestRender['render']>>;
let render: (canWriteArtifact?: boolean) => Promise<ReturnType<AppContextTestRender['render']>>;
let renderResult: ReturnType<AppContextTestRender['render']>;
let history: AppContextTestRender['history'];
let mockedContext: AppContextTestRender;
Expand All @@ -55,7 +55,7 @@ describe('Policy details artifacts list', () => {
getEndpointPrivilegesInitialStateMock({
canCreateArtifactsByPolicy: true,
});
render = async (externalPrivileges = true) => {
render = async (canWriteArtifact = true) => {
await act(async () => {
renderResult = mockedContext.render(
<PolicyArtifactsList
Expand All @@ -64,7 +64,7 @@ describe('Policy details artifacts list', () => {
searchableFields={[...SEARCHABLE_FIELDS]}
labels={POLICY_ARTIFACT_LIST_LABELS}
onDeleteActionCallback={handleOnDeleteActionCallbackMock}
externalPrivileges={externalPrivileges}
canWriteArtifact={canWriteArtifact}
getPolicyArtifactsPath={getPolicyEventFiltersPath}
getArtifactPath={getEventFiltersListPath}
/>
Expand Down
Loading

0 comments on commit 886289d

Please sign in to comment.