[TIP] use cases plugin permission api (#146738)

elastic/security-team#5474
This commit is contained in:
Philippe Oberti 2022-12-07 10:46:55 -06:00 committed by GitHub
parent a30d225421
commit cf2bbcd957
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 476 additions and 48 deletions

View file

@ -70,6 +70,12 @@ const defaultServices = {
getUseCasesAddToNewCaseFlyout: () => {},
getUseCasesAddToExistingCaseModal: () => {},
},
helpers: {
canUseCases: () => ({
create: true,
update: true,
}),
},
},
} as unknown as CoreStart;

View file

@ -13,20 +13,12 @@ import { IntegrationsGuard } from './integrations_guard/integrations_guard';
import { SecuritySolutionPluginTemplateWrapper } from './security_solution_plugin_template_wrapper';
import { useKibana } from '../hooks';
// export const APP_ID = 'threatIntdelligence';
export const APP_ID = 'securitySolution';
export const IndicatorsPageWrapper: VFC = () => {
const { cases } = useKibana().services;
const CasesContext = cases.ui.getCasesContext();
const permissions: CasesPermissions = {
all: true,
create: true,
read: true,
update: true,
delete: true,
push: true,
};
const permissions: CasesPermissions = cases.helpers.canUseCases();
const queryClient = new QueryClient();

View file

@ -91,7 +91,100 @@ Object {
}
`;
exports[`AddToExistingCase should render the EuiContextMenuItem disabled 1`] = `
exports[`AddToExistingCase should render the EuiContextMenuItem disabled if indicator is missing name 1`] = `
Object {
"asFragment": [Function],
"baseElement": <body>
<div>
<button
class="euiContextMenuItem euiContextMenuItem-isDisabled"
disabled=""
type="button"
>
<span
class="euiContextMenu__itemLayout"
>
<span
class="euiContextMenuItem__text"
>
Add to existing case
</span>
</span>
</button>
</div>
</body>,
"container": <div>
<button
class="euiContextMenuItem euiContextMenuItem-isDisabled"
disabled=""
type="button"
>
<span
class="euiContextMenu__itemLayout"
>
<span
class="euiContextMenuItem__text"
>
Add to existing case
</span>
</span>
</button>
</div>,
"debug": [Function],
"findAllByAltText": [Function],
"findAllByDisplayValue": [Function],
"findAllByLabelText": [Function],
"findAllByPlaceholderText": [Function],
"findAllByRole": [Function],
"findAllByTestId": [Function],
"findAllByText": [Function],
"findAllByTitle": [Function],
"findByAltText": [Function],
"findByDisplayValue": [Function],
"findByLabelText": [Function],
"findByPlaceholderText": [Function],
"findByRole": [Function],
"findByTestId": [Function],
"findByText": [Function],
"findByTitle": [Function],
"getAllByAltText": [Function],
"getAllByDisplayValue": [Function],
"getAllByLabelText": [Function],
"getAllByPlaceholderText": [Function],
"getAllByRole": [Function],
"getAllByTestId": [Function],
"getAllByText": [Function],
"getAllByTitle": [Function],
"getByAltText": [Function],
"getByDisplayValue": [Function],
"getByLabelText": [Function],
"getByPlaceholderText": [Function],
"getByRole": [Function],
"getByTestId": [Function],
"getByText": [Function],
"getByTitle": [Function],
"queryAllByAltText": [Function],
"queryAllByDisplayValue": [Function],
"queryAllByLabelText": [Function],
"queryAllByPlaceholderText": [Function],
"queryAllByRole": [Function],
"queryAllByTestId": [Function],
"queryAllByText": [Function],
"queryAllByTitle": [Function],
"queryByAltText": [Function],
"queryByDisplayValue": [Function],
"queryByLabelText": [Function],
"queryByPlaceholderText": [Function],
"queryByRole": [Function],
"queryByTestId": [Function],
"queryByText": [Function],
"queryByTitle": [Function],
"rerender": [Function],
"unmount": [Function],
}
`;
exports[`AddToExistingCase should render the EuiContextMenuItem disabled if user has no update permission 1`] = `
Object {
"asFragment": [Function],
"baseElement": <body>

View file

@ -10,31 +10,87 @@ import { render } from '@testing-library/react';
import { AddToExistingCase } from './add_to_existing_case';
import { TestProvidersComponent } from '../../../../common/mocks/test_providers';
import { generateMockFileIndicator, Indicator } from '../../../../../common/types/indicator';
import { casesPluginMock } from '@kbn/cases-plugin/public/mocks';
import { KibanaContext } from '../../../../hooks';
const indicator: Indicator = generateMockFileIndicator();
const onClick = () => window.alert('clicked');
const casesServiceMock = casesPluginMock.createStartContract();
describe('AddToExistingCase', () => {
it('should render an EuiContextMenuItem', () => {
const indicator: Indicator = generateMockFileIndicator();
const onClick = () => window.alert('clicked');
const mockedServices = {
cases: {
...casesServiceMock,
helpers: {
...casesServiceMock.helpers,
canUseCases: () => ({
create: true,
update: true,
}),
},
},
};
const component = render(
<TestProvidersComponent>
<AddToExistingCase indicator={indicator} onClick={onClick} />
<KibanaContext.Provider value={{ services: mockedServices } as any}>
<AddToExistingCase indicator={indicator} onClick={onClick} />
</KibanaContext.Provider>
</TestProvidersComponent>
);
expect(component).toMatchSnapshot();
});
it('should render the EuiContextMenuItem disabled', () => {
const indicator: Indicator = generateMockFileIndicator();
it('should render the EuiContextMenuItem disabled if indicator is missing name', () => {
const mockedServices = {
cases: {
...casesServiceMock,
helpers: {
...casesServiceMock.helpers,
canUseCases: () => ({
create: true,
update: true,
}),
},
},
};
const fields = { ...indicator.fields };
delete fields['threat.indicator.name'];
const indicatorMissingName = {
_id: indicator._id,
fields,
};
const onClick = () => window.alert('clicked');
const component = render(
<TestProvidersComponent>
<AddToExistingCase indicator={indicatorMissingName} onClick={onClick} />
<KibanaContext.Provider value={{ services: mockedServices } as any}>
<AddToExistingCase indicator={indicatorMissingName} onClick={onClick} />
</KibanaContext.Provider>
</TestProvidersComponent>
);
expect(component).toMatchSnapshot();
});
it('should render the EuiContextMenuItem disabled if user has no update permission', () => {
const mockedServices = {
cases: {
...casesServiceMock,
helpers: {
...casesServiceMock.helpers,
canUseCases: () => ({
create: false,
update: false,
}),
},
},
};
const component = render(
<TestProvidersComponent>
<KibanaContext.Provider value={{ services: mockedServices } as any}>
<AddToExistingCase indicator={indicator} onClick={onClick} />
</KibanaContext.Provider>
</TestProvidersComponent>
);
expect(component).toMatchSnapshot();

View file

@ -9,7 +9,7 @@ import React, { VFC } from 'react';
import { EuiContextMenuItem } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { CaseAttachmentsWithoutOwner } from '@kbn/cases-plugin/public';
import { EMPTY_VALUE } from '../../../../common/constants';
import { useCaseDisabled } from '../../hooks/use_case_permission';
import {
AttachmentMetadata,
generateAttachmentsMetadata,
@ -52,20 +52,18 @@ export const AddToExistingCase: VFC<AddToExistingCaseProps> = ({
const id: string = indicator._id as string;
const attachmentMetadata: AttachmentMetadata = generateAttachmentsMetadata(indicator);
const attachments: CaseAttachmentsWithoutOwner = generateAttachmentsWithoutOwner(
id,
attachmentMetadata
);
// disable the item if there isn't an indicator name
// in the case's attachment, the indicator name is the link to open the flyout
const disabled: boolean = attachmentMetadata.indicatorName === EMPTY_VALUE;
const menuItemClicked = () => {
onClick();
selectCaseModal.open({ attachments });
};
const disabled: boolean = useCaseDisabled(attachmentMetadata.indicatorName);
return (
<EuiContextMenuItem
key="attachmentsExistingCase"

View file

@ -91,7 +91,100 @@ Object {
}
`;
exports[`AddToNewCase should render the EuiContextMenuItem disabled 1`] = `
exports[`AddToNewCase should render the EuiContextMenuItem disabled if indicator is missing name 1`] = `
Object {
"asFragment": [Function],
"baseElement": <body>
<div>
<button
class="euiContextMenuItem euiContextMenuItem-isDisabled"
disabled=""
type="button"
>
<span
class="euiContextMenu__itemLayout"
>
<span
class="euiContextMenuItem__text"
>
Add to new case
</span>
</span>
</button>
</div>
</body>,
"container": <div>
<button
class="euiContextMenuItem euiContextMenuItem-isDisabled"
disabled=""
type="button"
>
<span
class="euiContextMenu__itemLayout"
>
<span
class="euiContextMenuItem__text"
>
Add to new case
</span>
</span>
</button>
</div>,
"debug": [Function],
"findAllByAltText": [Function],
"findAllByDisplayValue": [Function],
"findAllByLabelText": [Function],
"findAllByPlaceholderText": [Function],
"findAllByRole": [Function],
"findAllByTestId": [Function],
"findAllByText": [Function],
"findAllByTitle": [Function],
"findByAltText": [Function],
"findByDisplayValue": [Function],
"findByLabelText": [Function],
"findByPlaceholderText": [Function],
"findByRole": [Function],
"findByTestId": [Function],
"findByText": [Function],
"findByTitle": [Function],
"getAllByAltText": [Function],
"getAllByDisplayValue": [Function],
"getAllByLabelText": [Function],
"getAllByPlaceholderText": [Function],
"getAllByRole": [Function],
"getAllByTestId": [Function],
"getAllByText": [Function],
"getAllByTitle": [Function],
"getByAltText": [Function],
"getByDisplayValue": [Function],
"getByLabelText": [Function],
"getByPlaceholderText": [Function],
"getByRole": [Function],
"getByTestId": [Function],
"getByText": [Function],
"getByTitle": [Function],
"queryAllByAltText": [Function],
"queryAllByDisplayValue": [Function],
"queryAllByLabelText": [Function],
"queryAllByPlaceholderText": [Function],
"queryAllByRole": [Function],
"queryAllByTestId": [Function],
"queryAllByText": [Function],
"queryAllByTitle": [Function],
"queryByAltText": [Function],
"queryByDisplayValue": [Function],
"queryByLabelText": [Function],
"queryByPlaceholderText": [Function],
"queryByRole": [Function],
"queryByTestId": [Function],
"queryByText": [Function],
"queryByTitle": [Function],
"rerender": [Function],
"unmount": [Function],
}
`;
exports[`AddToNewCase should render the EuiContextMenuItem disabled if user have no create permission 1`] = `
Object {
"asFragment": [Function],
"baseElement": <body>

View file

@ -5,35 +5,93 @@
* 2.0.
*/
import { KibanaContext } from '../../../../hooks';
import { render } from '@testing-library/react';
import React from 'react';
import { generateMockFileIndicator, Indicator } from '../../../../../common/types/indicator';
import { TestProvidersComponent } from '../../../../common/mocks/test_providers';
import { AddToNewCase } from './add_to_new_case';
import { casesPluginMock } from '@kbn/cases-plugin/public/mocks';
const indicator: Indicator = generateMockFileIndicator();
const onClick = () => window.alert('clicked');
const casesServiceMock = casesPluginMock.createStartContract();
describe('AddToNewCase', () => {
it('should render an EuiContextMenuItem', () => {
const indicator: Indicator = generateMockFileIndicator();
const onClick = () => window.alert('clicked');
const mockedServices = {
cases: {
...casesServiceMock,
helpers: {
...casesServiceMock.helpers,
canUseCases: () => ({
create: true,
update: true,
}),
},
},
};
const component = render(
<TestProvidersComponent>
<AddToNewCase indicator={indicator} onClick={onClick} />
<KibanaContext.Provider value={{ services: mockedServices } as any}>
<AddToNewCase indicator={indicator} onClick={onClick} />
</KibanaContext.Provider>
</TestProvidersComponent>
);
expect(component).toMatchSnapshot();
});
it('should render the EuiContextMenuItem disabled', () => {
const indicator: Indicator = generateMockFileIndicator();
it('should render the EuiContextMenuItem disabled if indicator is missing name', () => {
const mockedServices = {
cases: {
...casesServiceMock,
helpers: {
...casesServiceMock.helpers,
canUseCases: () => ({
create: true,
update: true,
}),
},
},
};
const fields = { ...indicator.fields };
delete fields['threat.indicator.name'];
const indicatorMissingName = {
_id: indicator._id,
fields,
};
const onClick = () => window.alert('clicked');
const component = render(
<TestProvidersComponent>
<AddToNewCase indicator={indicatorMissingName} onClick={onClick} />
<KibanaContext.Provider value={{ services: mockedServices } as any}>
<AddToNewCase indicator={indicatorMissingName} onClick={onClick} />
</KibanaContext.Provider>
</TestProvidersComponent>
);
expect(component).toMatchSnapshot();
});
it('should render the EuiContextMenuItem disabled if user have no create permission', () => {
const mockedServices = {
cases: {
...casesServiceMock,
helpers: {
...casesServiceMock.helpers,
canUseCases: () => ({
create: false,
update: false,
}),
},
},
};
const component = render(
<TestProvidersComponent>
<KibanaContext.Provider value={{ services: mockedServices } as any}>
<AddToNewCase indicator={indicator} onClick={onClick} />
</KibanaContext.Provider>
</TestProvidersComponent>
);
expect(component).toMatchSnapshot();

View file

@ -9,7 +9,7 @@ import React, { VFC } from 'react';
import { EuiContextMenuItem } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { CaseAttachmentsWithoutOwner } from '@kbn/cases-plugin/public';
import { EMPTY_VALUE } from '../../../../common/constants';
import { useCaseDisabled } from '../../hooks/use_case_permission';
import {
AttachmentMetadata,
generateAttachmentsMetadata,
@ -52,20 +52,18 @@ export const AddToNewCase: VFC<AddToNewCaseProps> = ({
const id: string = indicator._id as string;
const attachmentMetadata: AttachmentMetadata = generateAttachmentsMetadata(indicator);
const attachments: CaseAttachmentsWithoutOwner = generateAttachmentsWithoutOwner(
id,
attachmentMetadata
);
// disable the item if there isn't an indicator name
// in the case's attachment, the indicator name is the link to open the flyout
const disabled: boolean = attachmentMetadata.indicatorName === EMPTY_VALUE;
const menuItemClicked = () => {
onClick();
createCaseFlyout.open({ attachments });
};
const disabled: boolean = useCaseDisabled(attachmentMetadata.indicatorName);
return (
<EuiContextMenuItem
key="attachmentsNewCase"

View file

@ -24,3 +24,7 @@ export const initComponent = () => {
return <CommentChildren id={indicatorId} metadata={metadata} />;
};
};
// Note: This is for lazy loading
// eslint-disable-next-line import/no-default-export
export default initComponent();

View file

@ -0,0 +1,103 @@
/*
* 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 React, { FC, ReactNode } from 'react';
import { Renderer, renderHook, RenderHookResult } from '@testing-library/react-hooks';
import { casesPluginMock } from '@kbn/cases-plugin/public/mocks';
import { KibanaContext } from '../../../hooks/use_kibana';
import { useCaseDisabled } from './use_case_permission';
import { TestProvidersComponent } from '../../../common/mocks/test_providers';
import { EMPTY_VALUE } from '../../../common/constants';
const casesServiceMock = casesPluginMock.createStartContract();
const getProviderComponent =
(mockedServices: unknown) =>
({ children }: { children: ReactNode }) =>
(
<TestProvidersComponent>
<KibanaContext.Provider value={{ services: mockedServices } as any}>
{children}
</KibanaContext.Provider>
</TestProvidersComponent>
);
describe('useCasePermission', () => {
let hookResult: RenderHookResult<{}, boolean, Renderer<unknown>>;
it('should return false if user has correct permissions and indicator has a name', () => {
const mockedServices = {
cases: {
...casesServiceMock,
helpers: {
...casesServiceMock.helpers,
canUseCases: () => ({
create: true,
update: true,
}),
},
},
};
// @ts-ignore
const ProviderComponent: FC = getProviderComponent(mockedServices);
const indicatorName: string = 'abc';
hookResult = renderHook(() => useCaseDisabled(indicatorName), {
wrapper: ProviderComponent,
});
expect(hookResult.result.current).toEqual(false);
});
it(`should return true if user doesn't have correct permissions`, () => {
const mockedServices = {
cases: {
...casesServiceMock,
helpers: {
...casesServiceMock.helpers,
canUseCases: () => ({
create: false,
update: true,
}),
},
},
};
// @ts-ignore
const ProviderComponent: FC = getProviderComponent(mockedServices);
const indicatorName: string = 'abc';
hookResult = renderHook(() => useCaseDisabled(indicatorName), {
wrapper: ProviderComponent,
});
expect(hookResult.result.current).toEqual(true);
});
it('should return true if indicator name is missing or empty', () => {
const mockedServices = {
cases: {
...casesServiceMock,
helpers: {
...casesServiceMock.helpers,
canUseCases: () => ({
create: true,
update: true,
}),
},
},
};
// @ts-ignore
const ProviderComponent: FC = getProviderComponent(mockedServices);
const indicatorName: string = EMPTY_VALUE;
hookResult = renderHook(() => useCaseDisabled(indicatorName), {
wrapper: ProviderComponent,
});
expect(hookResult.result.current).toEqual(true);
});
});

View file

@ -0,0 +1,30 @@
/*
* 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 { CasesPermissions } from '@kbn/cases-plugin/common';
import { EMPTY_VALUE } from '../../../common/constants';
import { useKibana } from '../../../hooks';
/**
* Decides if we enable or disable the add to existing and add to new case features.
* If the Indicator has no name the features will be disabled.
* If the user doesn't have the correct permissions the features will be disabled.
*
* @param indicatorName the name of the indicator
* @return true if the features are enabled
*/
export const useCaseDisabled = (indicatorName: string): boolean => {
const { cases } = useKibana().services;
const permissions: CasesPermissions = cases.helpers.canUseCases();
// disable the item if there is no indicator name or if the user doesn't have the right permission
// in the case's attachment, the indicator name is the link to open the flyout
const invalidIndicatorName: boolean = indicatorName === EMPTY_VALUE;
const hasPermission: boolean = permissions.create && permissions.update;
return invalidIndicatorName || !hasPermission;
};

View file

@ -48,9 +48,9 @@ describe('generateAttachmentsMetadata', () => {
const result = generateAttachmentsMetadata(indicator);
expect(result).toEqual({
indicatorName: '',
indicatorType: '',
indicatorFeedName: '',
indicatorName: '-',
indicatorType: '-',
indicatorFeedName: '-',
});
});

View file

@ -28,6 +28,10 @@ export interface AttachmentMetadata {
indicatorFeedName: string;
}
const AttachmentChildrenLazy = React.lazy(
() => import('../components/attachment_children/attachment_children')
);
/**
* Create an {@link ExternalReferenceAttachmentType} object used to register an external reference
* to the case plugin with our Threat Intelligence plugin initializes.
@ -49,14 +53,7 @@ export const generateAttachmentType = (): ExternalReferenceAttachmentType => ({
/>
),
timelineAvatar: <EuiAvatar name="indicator" color="subdued" iconType="crosshairs" />,
children: React.lazy(async () => {
const { initComponent } = await import(
'../components/attachment_children/attachment_children'
);
return {
default: initComponent(),
};
}),
children: AttachmentChildrenLazy,
}),
icon: 'crosshairs',
});