[Cases][Sharable SOs] Resolve deep link ids (#111984)
* backend implementation * front UI changes to manage all resolve outcomes * add spaces dependency to o11y * tests fixed * hack removed * added resolve api integration test * unit tests for the front changes * add base resolve cypress test * non-null assertions removed * add resolve operation for logging * eslint fix new rule * create test snapshot * migration resolve functional test
This commit is contained in:
parent
854e0d4d85
commit
9c30f87ff4
29 changed files with 845 additions and 186 deletions
|
@ -254,6 +254,16 @@ export const CaseResponseRt = rt.intersection([
|
|||
}),
|
||||
]);
|
||||
|
||||
export const CaseResolveResponseRt = rt.intersection([
|
||||
rt.type({
|
||||
case: CaseResponseRt,
|
||||
outcome: rt.union([rt.literal('exactMatch'), rt.literal('aliasMatch'), rt.literal('conflict')]),
|
||||
}),
|
||||
rt.partial({
|
||||
alias_target_id: rt.string,
|
||||
}),
|
||||
]);
|
||||
|
||||
export const CasesFindResponseRt = rt.intersection([
|
||||
rt.type({
|
||||
cases: rt.array(CaseResponseRt),
|
||||
|
@ -319,6 +329,7 @@ export type CaseAttributes = rt.TypeOf<typeof CaseAttributesRt>;
|
|||
export type CasesClientPostRequest = rt.TypeOf<typeof CasesClientPostRequestRt>;
|
||||
export type CasePostRequest = rt.TypeOf<typeof CasePostRequestRt>;
|
||||
export type CaseResponse = rt.TypeOf<typeof CaseResponseRt>;
|
||||
export type CaseResolveResponse = rt.TypeOf<typeof CaseResolveResponseRt>;
|
||||
export type CasesResponse = rt.TypeOf<typeof CasesResponseRt>;
|
||||
export type CasesFindRequest = rt.TypeOf<typeof CasesFindRequestRt>;
|
||||
export type CasesByAlertIDRequest = rt.TypeOf<typeof CasesByAlertIDRequestRt>;
|
||||
|
|
|
@ -114,6 +114,12 @@ export interface Case extends BasicCase {
|
|||
type: CaseType;
|
||||
}
|
||||
|
||||
export interface ResolvedCase {
|
||||
case: Case;
|
||||
outcome: 'exactMatch' | 'aliasMatch' | 'conflict';
|
||||
aliasTargetId?: string;
|
||||
}
|
||||
|
||||
export interface QueryParams {
|
||||
page: number;
|
||||
perPage: number;
|
||||
|
|
|
@ -13,6 +13,7 @@ import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_r
|
|||
import { StartServices } from '../../../types';
|
||||
import { EuiTheme } from '../../../../../../../src/plugins/kibana_react/common';
|
||||
import { securityMock } from '../../../../../security/public/mocks';
|
||||
import { spacesPluginMock } from '../../../../../spaces/public/mocks';
|
||||
import { triggersActionsUiMock } from '../../../../../triggers_actions_ui/public/mocks';
|
||||
|
||||
export const createStartServicesMock = (): StartServices =>
|
||||
|
@ -25,6 +26,7 @@ export const createStartServicesMock = (): StartServices =>
|
|||
},
|
||||
security: securityMock.createStart(),
|
||||
triggersActionsUi: triggersActionsUiMock.createStart(),
|
||||
spaces: spacesPluginMock.createStartContract(),
|
||||
} as unknown as StartServices);
|
||||
|
||||
export const createWithKibanaMock = () => {
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
getAlertUserAction,
|
||||
} from '../../containers/mock';
|
||||
import { TestProviders } from '../../common/mock';
|
||||
import { SpacesApi } from '../../../../spaces/public';
|
||||
import { useUpdateCase } from '../../containers/use_update_case';
|
||||
import { useGetCase } from '../../containers/use_get_case';
|
||||
import { useGetCaseUserActions } from '../../containers/use_get_case_user_actions';
|
||||
|
@ -47,6 +48,13 @@ const useConnectorsMock = useConnectors as jest.Mock;
|
|||
const usePostPushToServiceMock = usePostPushToService as jest.Mock;
|
||||
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
|
||||
|
||||
const spacesUiApiMock = {
|
||||
redirectLegacyUrl: jest.fn().mockResolvedValue(undefined),
|
||||
components: {
|
||||
getLegacyUrlConflict: jest.fn().mockReturnValue(<div data-test-subj="conflict-component" />),
|
||||
},
|
||||
};
|
||||
|
||||
const alertsHit = [
|
||||
{
|
||||
_id: 'alert-id-1',
|
||||
|
@ -138,6 +146,7 @@ describe('CaseView ', () => {
|
|||
isLoading: false,
|
||||
isError: false,
|
||||
data,
|
||||
resolveOutcome: 'exactMatch',
|
||||
updateCase,
|
||||
fetchCase,
|
||||
};
|
||||
|
@ -174,6 +183,7 @@ describe('CaseView ', () => {
|
|||
actionTypeTitle: '.servicenow',
|
||||
iconClass: 'logoSecurity',
|
||||
});
|
||||
useKibanaMock().services.spaces = { ui: spacesUiApiMock } as unknown as SpacesApi;
|
||||
});
|
||||
|
||||
it('should render CaseComponent', async () => {
|
||||
|
@ -395,36 +405,7 @@ describe('CaseView ', () => {
|
|||
}));
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<CaseView
|
||||
{...{
|
||||
allCasesNavigation: {
|
||||
href: 'all-cases-href',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
caseDetailsNavigation: {
|
||||
href: 'case-details-href',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
caseId: '1234',
|
||||
configureCasesNavigation: {
|
||||
href: 'configure-cases-href',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
getCaseDetailHrefWithCommentId: jest.fn(),
|
||||
onComponentInitialized: jest.fn(),
|
||||
actionsNavigation: {
|
||||
href: jest.fn(),
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
ruleDetailsNavigation: {
|
||||
href: jest.fn(),
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
showAlertDetails: jest.fn(),
|
||||
useFetchAlertData: jest.fn().mockReturnValue([false, alertsHit[0]]),
|
||||
userCanCrud: true,
|
||||
}}
|
||||
/>
|
||||
<CaseView {...caseProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() => {
|
||||
|
@ -439,36 +420,7 @@ describe('CaseView ', () => {
|
|||
}));
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<CaseView
|
||||
{...{
|
||||
allCasesNavigation: {
|
||||
href: 'all-cases-href',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
caseDetailsNavigation: {
|
||||
href: 'case-details-href',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
caseId: '1234',
|
||||
configureCasesNavigation: {
|
||||
href: 'configure-cases-href',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
getCaseDetailHrefWithCommentId: jest.fn(),
|
||||
onComponentInitialized: jest.fn(),
|
||||
actionsNavigation: {
|
||||
href: jest.fn(),
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
ruleDetailsNavigation: {
|
||||
href: jest.fn(),
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
showAlertDetails: jest.fn(),
|
||||
useFetchAlertData: jest.fn().mockReturnValue([false, alertsHit[0]]),
|
||||
userCanCrud: true,
|
||||
}}
|
||||
/>
|
||||
<CaseView {...caseProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() => {
|
||||
|
@ -477,43 +429,66 @@ describe('CaseView ', () => {
|
|||
});
|
||||
|
||||
it('should return case view when data is there', async () => {
|
||||
(useGetCase as jest.Mock).mockImplementation(() => defaultGetCase);
|
||||
(useGetCase as jest.Mock).mockImplementation(() => ({
|
||||
...defaultGetCase,
|
||||
resolveOutcome: 'exactMatch',
|
||||
}));
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<CaseView
|
||||
{...{
|
||||
allCasesNavigation: {
|
||||
href: 'all-cases-href',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
caseDetailsNavigation: {
|
||||
href: 'case-details-href',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
caseId: '1234',
|
||||
configureCasesNavigation: {
|
||||
href: 'configure-cases-href',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
getCaseDetailHrefWithCommentId: jest.fn(),
|
||||
onComponentInitialized: jest.fn(),
|
||||
actionsNavigation: {
|
||||
href: jest.fn(),
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
ruleDetailsNavigation: {
|
||||
href: jest.fn(),
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
showAlertDetails: jest.fn(),
|
||||
useFetchAlertData: jest.fn().mockReturnValue([false, alertsHit[0]]),
|
||||
userCanCrud: true,
|
||||
}}
|
||||
/>
|
||||
<CaseView {...caseProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find('[data-test-subj="case-view-title"]').exists()).toBeTruthy();
|
||||
expect(spacesUiApiMock.components.getLegacyUrlConflict).not.toHaveBeenCalled();
|
||||
expect(spacesUiApiMock.redirectLegacyUrl).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should redirect case view when resolves to alias match', async () => {
|
||||
const resolveAliasId = `${defaultGetCase.data.id}_2`;
|
||||
(useGetCase as jest.Mock).mockImplementation(() => ({
|
||||
...defaultGetCase,
|
||||
resolveOutcome: 'aliasMatch',
|
||||
resolveAliasId,
|
||||
}));
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<CaseView {...caseProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find('[data-test-subj="case-view-title"]').exists()).toBeTruthy();
|
||||
expect(spacesUiApiMock.components.getLegacyUrlConflict).not.toHaveBeenCalled();
|
||||
expect(spacesUiApiMock.redirectLegacyUrl).toHaveBeenCalledWith(
|
||||
`cases/${resolveAliasId}`,
|
||||
'case'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should redirect case view when resolves to conflict', async () => {
|
||||
const resolveAliasId = `${defaultGetCase.data.id}_2`;
|
||||
(useGetCase as jest.Mock).mockImplementation(() => ({
|
||||
...defaultGetCase,
|
||||
resolveOutcome: 'conflict',
|
||||
resolveAliasId,
|
||||
}));
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<CaseView {...caseProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find('[data-test-subj="case-view-title"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="conflict-component"]').exists()).toBeTruthy();
|
||||
expect(spacesUiApiMock.redirectLegacyUrl).not.toHaveBeenCalled();
|
||||
expect(spacesUiApiMock.components.getLegacyUrlConflict).toHaveBeenCalledWith({
|
||||
objectNoun: 'case',
|
||||
currentObjectId: defaultGetCase.data.id,
|
||||
otherObjectId: resolveAliasId,
|
||||
otherObjectPath: `cases/${resolveAliasId}`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -521,41 +496,12 @@ describe('CaseView ', () => {
|
|||
(useGetCase as jest.Mock).mockImplementation(() => defaultGetCase);
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<CaseView
|
||||
{...{
|
||||
allCasesNavigation: {
|
||||
href: 'all-cases-href',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
caseDetailsNavigation: {
|
||||
href: 'case-details-href',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
caseId: '1234',
|
||||
configureCasesNavigation: {
|
||||
href: 'configure-cases-href',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
getCaseDetailHrefWithCommentId: jest.fn(),
|
||||
onComponentInitialized: jest.fn(),
|
||||
actionsNavigation: {
|
||||
href: jest.fn(),
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
ruleDetailsNavigation: {
|
||||
href: jest.fn(),
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
showAlertDetails: jest.fn(),
|
||||
useFetchAlertData: jest.fn().mockReturnValue([false, alertsHit[0]]),
|
||||
userCanCrud: true,
|
||||
}}
|
||||
/>
|
||||
<CaseView {...caseProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find('[data-test-subj="case-refresh"]').first().simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(fetchCaseUserActions).toBeCalledWith('1234', 'resilient-2', undefined);
|
||||
expect(fetchCaseUserActions).toBeCalledWith(caseProps.caseData.id, 'resilient-2', undefined);
|
||||
expect(fetchCase).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -40,6 +40,7 @@ import { CasesNavigation } from '../links';
|
|||
import { OwnerProvider } from '../owner_context';
|
||||
import { getConnectorById } from '../utils';
|
||||
import { DoesNotExist } from './does_not_exist';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
|
||||
export interface CaseViewComponentProps {
|
||||
allCasesNavigation: CasesNavigation;
|
||||
|
@ -499,6 +500,14 @@ export const CaseComponent = React.memo<CaseComponentProps>(
|
|||
}
|
||||
);
|
||||
|
||||
export const CaseViewLoading = () => (
|
||||
<MyEuiFlexGroup gutterSize="none" justifyContent="center" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner data-test-subj="case-view-loading" size="xl" />
|
||||
</EuiFlexItem>
|
||||
</MyEuiFlexGroup>
|
||||
);
|
||||
|
||||
export const CaseView = React.memo(
|
||||
({
|
||||
allCasesNavigation,
|
||||
|
@ -518,27 +527,59 @@ export const CaseView = React.memo(
|
|||
refreshRef,
|
||||
hideSyncAlerts,
|
||||
}: CaseViewProps) => {
|
||||
const { data, isLoading, isError, fetchCase, updateCase } = useGetCase(caseId, subCaseId);
|
||||
if (isError) {
|
||||
return <DoesNotExist allCasesNavigation={allCasesNavigation} caseId={caseId} />;
|
||||
}
|
||||
if (isLoading) {
|
||||
return (
|
||||
<MyEuiFlexGroup gutterSize="none" justifyContent="center" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner data-test-subj="case-view-loading" size="xl" />
|
||||
</EuiFlexItem>
|
||||
</MyEuiFlexGroup>
|
||||
);
|
||||
}
|
||||
if (onCaseDataSuccess && data) {
|
||||
onCaseDataSuccess(data);
|
||||
}
|
||||
const { data, resolveOutcome, resolveAliasId, isLoading, isError, fetchCase, updateCase } =
|
||||
useGetCase(caseId, subCaseId);
|
||||
const { spaces: spacesApi, http } = useKibana().services;
|
||||
|
||||
return (
|
||||
useEffect(() => {
|
||||
if (onCaseDataSuccess && data) {
|
||||
onCaseDataSuccess(data);
|
||||
}
|
||||
}, [data, onCaseDataSuccess]);
|
||||
|
||||
useEffect(() => {
|
||||
if (spacesApi && resolveOutcome === 'aliasMatch' && resolveAliasId != null) {
|
||||
// CAUTION: the path /cases/:detailName is working in both Observability (/app/observability/cases/:detailName) and
|
||||
// Security Solutions (/app/security/cases/:detailName) plugins. This will need to be changed if this component is loaded
|
||||
// under any another path, passing a path builder function by props from every parent plugin.
|
||||
const newPath = http.basePath.prepend(
|
||||
`cases/${resolveAliasId}${window.location.search}${window.location.hash}`
|
||||
);
|
||||
spacesApi.ui.redirectLegacyUrl(newPath, i18n.CASE);
|
||||
}
|
||||
}, [resolveOutcome, resolveAliasId, spacesApi, http]);
|
||||
|
||||
const getLegacyUrlConflictCallout = useCallback(() => {
|
||||
// This function returns a callout component *if* we have encountered a "legacy URL conflict" scenario
|
||||
if (data && spacesApi && resolveOutcome === 'conflict' && resolveAliasId != null) {
|
||||
// We have resolved to one object, but another object has a legacy URL alias associated with this ID/page. We should display a
|
||||
// callout with a warning for the user, and provide a way for them to navigate to the other object.
|
||||
const otherObjectId = resolveAliasId; // This is always defined if outcome === 'conflict'
|
||||
// CAUTION: the path /cases/:detailName is working in both Observability (/app/observability/cases/:detailName) and
|
||||
// Security Solutions (/app/security/cases/:detailName) plugins. This will need to be changed if this component is loaded
|
||||
// under any another path, passing a path builder function by props from every parent plugin.
|
||||
const otherObjectPath = http.basePath.prepend(
|
||||
`cases/${otherObjectId}${window.location.search}${window.location.hash}`
|
||||
);
|
||||
return spacesApi.ui.components.getLegacyUrlConflict({
|
||||
objectNoun: i18n.CASE,
|
||||
currentObjectId: data.id,
|
||||
otherObjectId,
|
||||
otherObjectPath,
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}, [data, resolveAliasId, resolveOutcome, spacesApi, http.basePath]);
|
||||
|
||||
return isError ? (
|
||||
<DoesNotExist allCasesNavigation={allCasesNavigation} caseId={caseId} />
|
||||
) : isLoading ? (
|
||||
<CaseViewLoading />
|
||||
) : (
|
||||
data && (
|
||||
<CasesTimelineIntegrationProvider timelineIntegration={timelineIntegration}>
|
||||
<OwnerProvider owner={[data.owner]}>
|
||||
{getLegacyUrlConflictCallout()}
|
||||
<CaseComponent
|
||||
allCasesNavigation={allCasesNavigation}
|
||||
caseData={data}
|
||||
|
@ -566,6 +607,7 @@ export const CaseView = React.memo(
|
|||
);
|
||||
|
||||
CaseComponent.displayName = 'CaseComponent';
|
||||
CaseViewLoading.displayName = 'CaseViewLoading';
|
||||
CaseView.displayName = 'CaseView';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
basicCase,
|
||||
basicCaseCommentPatch,
|
||||
basicCasePost,
|
||||
basicResolvedCase,
|
||||
casesStatus,
|
||||
caseUserActions,
|
||||
pushedCase,
|
||||
|
@ -33,6 +34,7 @@ import {
|
|||
CommentRequest,
|
||||
User,
|
||||
CaseStatuses,
|
||||
ResolvedCase,
|
||||
} from '../../../common';
|
||||
|
||||
export const getCase = async (
|
||||
|
@ -41,6 +43,12 @@ export const getCase = async (
|
|||
signal: AbortSignal
|
||||
): Promise<Case> => Promise.resolve(basicCase);
|
||||
|
||||
export const resolveCase = async (
|
||||
caseId: string,
|
||||
includeComments: boolean = true,
|
||||
signal: AbortSignal
|
||||
): Promise<ResolvedCase> => Promise.resolve(basicResolvedCase);
|
||||
|
||||
export const getCasesStatus = async (signal: AbortSignal): Promise<CasesStatus> =>
|
||||
Promise.resolve(casesStatus);
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ import {
|
|||
postCase,
|
||||
postComment,
|
||||
pushCase,
|
||||
resolveCase,
|
||||
} from './api';
|
||||
|
||||
import {
|
||||
|
@ -68,7 +69,7 @@ describe('Case Configuration API', () => {
|
|||
});
|
||||
const data = ['1', '2'];
|
||||
|
||||
test('check url, method, signal', async () => {
|
||||
test('should be called with correct check url, method, signal', async () => {
|
||||
await deleteCases(data, abortCtrl.signal);
|
||||
expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}`, {
|
||||
method: 'DELETE',
|
||||
|
@ -77,7 +78,7 @@ describe('Case Configuration API', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('happy path', async () => {
|
||||
test('should return correct response', async () => {
|
||||
const resp = await deleteCases(data, abortCtrl.signal);
|
||||
expect(resp).toEqual('');
|
||||
});
|
||||
|
@ -89,7 +90,7 @@ describe('Case Configuration API', () => {
|
|||
fetchMock.mockResolvedValue(actionLicenses);
|
||||
});
|
||||
|
||||
test('check url, method, signal', async () => {
|
||||
test('should be called with correct check url, method, signal', async () => {
|
||||
await getActionLicense(abortCtrl.signal);
|
||||
expect(fetchMock).toHaveBeenCalledWith(`/api/actions/connector_types`, {
|
||||
method: 'GET',
|
||||
|
@ -97,7 +98,7 @@ describe('Case Configuration API', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('happy path', async () => {
|
||||
test('should return correct response', async () => {
|
||||
const resp = await getActionLicense(abortCtrl.signal);
|
||||
expect(resp).toEqual(actionLicenses);
|
||||
});
|
||||
|
@ -110,7 +111,7 @@ describe('Case Configuration API', () => {
|
|||
});
|
||||
const data = basicCase.id;
|
||||
|
||||
test('check url, method, signal', async () => {
|
||||
test('should be called with correct check url, method, signal', async () => {
|
||||
await getCase(data, true, abortCtrl.signal);
|
||||
expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${basicCase.id}`, {
|
||||
method: 'GET',
|
||||
|
@ -119,18 +120,46 @@ describe('Case Configuration API', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('happy path', async () => {
|
||||
test('should return correct response', async () => {
|
||||
const resp = await getCase(data, true, abortCtrl.signal);
|
||||
expect(resp).toEqual(basicCase);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveCase', () => {
|
||||
const targetAliasId = '12345';
|
||||
const basicResolveCase = {
|
||||
outcome: 'aliasMatch',
|
||||
case: basicCaseSnake,
|
||||
};
|
||||
const caseId = basicCase.id;
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.mockClear();
|
||||
fetchMock.mockResolvedValue({ ...basicResolveCase, target_alias_id: targetAliasId });
|
||||
});
|
||||
|
||||
test('should be called with correct check url, method, signal', async () => {
|
||||
await resolveCase(caseId, true, abortCtrl.signal);
|
||||
expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${caseId}/resolve`, {
|
||||
method: 'GET',
|
||||
query: { includeComments: true },
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
});
|
||||
|
||||
test('should return correct response', async () => {
|
||||
const resp = await resolveCase(caseId, true, abortCtrl.signal);
|
||||
expect(resp).toEqual({ ...basicResolveCase, case: basicCase, targetAliasId });
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCases', () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.mockClear();
|
||||
fetchMock.mockResolvedValue(allCasesSnake);
|
||||
});
|
||||
test('check url, method, signal', async () => {
|
||||
test('should be called with correct check url, method, signal', async () => {
|
||||
await getCases({
|
||||
filterOptions: { ...DEFAULT_FILTER_OPTIONS, owner: [SECURITY_SOLUTION_OWNER] },
|
||||
queryParams: DEFAULT_QUERY_PARAMS,
|
||||
|
@ -148,7 +177,7 @@ describe('Case Configuration API', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('correctly applies filters', async () => {
|
||||
test('should applies correct filters', async () => {
|
||||
await getCases({
|
||||
filterOptions: {
|
||||
...DEFAULT_FILTER_OPTIONS,
|
||||
|
@ -175,7 +204,7 @@ describe('Case Configuration API', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('tags with weird chars get handled gracefully', async () => {
|
||||
test('should handle tags with weird chars', async () => {
|
||||
const weirdTags: string[] = ['(', '"double"'];
|
||||
|
||||
await getCases({
|
||||
|
@ -204,7 +233,7 @@ describe('Case Configuration API', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('happy path', async () => {
|
||||
test('should return correct response', async () => {
|
||||
const resp = await getCases({
|
||||
filterOptions: { ...DEFAULT_FILTER_OPTIONS, owner: [SECURITY_SOLUTION_OWNER] },
|
||||
queryParams: DEFAULT_QUERY_PARAMS,
|
||||
|
@ -219,7 +248,7 @@ describe('Case Configuration API', () => {
|
|||
fetchMock.mockClear();
|
||||
fetchMock.mockResolvedValue(casesStatusSnake);
|
||||
});
|
||||
test('check url, method, signal', async () => {
|
||||
test('should be called with correct check url, method, signal', async () => {
|
||||
await getCasesStatus(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]);
|
||||
expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/status`, {
|
||||
method: 'GET',
|
||||
|
@ -228,7 +257,7 @@ describe('Case Configuration API', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('happy path', async () => {
|
||||
test('should return correct response', async () => {
|
||||
const resp = await getCasesStatus(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]);
|
||||
expect(resp).toEqual(casesStatus);
|
||||
});
|
||||
|
@ -240,7 +269,7 @@ describe('Case Configuration API', () => {
|
|||
fetchMock.mockResolvedValue(caseUserActionsSnake);
|
||||
});
|
||||
|
||||
test('check url, method, signal', async () => {
|
||||
test('should be called with correct check url, method, signal', async () => {
|
||||
await getCaseUserActions(basicCase.id, abortCtrl.signal);
|
||||
expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${basicCase.id}/user_actions`, {
|
||||
method: 'GET',
|
||||
|
@ -248,7 +277,7 @@ describe('Case Configuration API', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('happy path', async () => {
|
||||
test('should return correct response', async () => {
|
||||
const resp = await getCaseUserActions(basicCase.id, abortCtrl.signal);
|
||||
expect(resp).toEqual(caseUserActions);
|
||||
});
|
||||
|
@ -260,7 +289,7 @@ describe('Case Configuration API', () => {
|
|||
fetchMock.mockResolvedValue(respReporters);
|
||||
});
|
||||
|
||||
test('check url, method, signal', async () => {
|
||||
test('should be called with correct check url, method, signal', async () => {
|
||||
await getReporters(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]);
|
||||
expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/reporters`, {
|
||||
method: 'GET',
|
||||
|
@ -271,7 +300,7 @@ describe('Case Configuration API', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('happy path', async () => {
|
||||
test('should return correct response', async () => {
|
||||
const resp = await getReporters(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]);
|
||||
expect(resp).toEqual(respReporters);
|
||||
});
|
||||
|
@ -283,7 +312,7 @@ describe('Case Configuration API', () => {
|
|||
fetchMock.mockResolvedValue(tags);
|
||||
});
|
||||
|
||||
test('check url, method, signal', async () => {
|
||||
test('should be called with correct check url, method, signal', async () => {
|
||||
await getTags(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]);
|
||||
expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/tags`, {
|
||||
method: 'GET',
|
||||
|
@ -294,7 +323,7 @@ describe('Case Configuration API', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('happy path', async () => {
|
||||
test('should return correct response', async () => {
|
||||
const resp = await getTags(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]);
|
||||
expect(resp).toEqual(tags);
|
||||
});
|
||||
|
@ -306,7 +335,7 @@ describe('Case Configuration API', () => {
|
|||
fetchMock.mockResolvedValue([basicCaseSnake]);
|
||||
});
|
||||
const data = { description: 'updated description' };
|
||||
test('check url, method, signal', async () => {
|
||||
test('should be called with correct check url, method, signal', async () => {
|
||||
await patchCase(basicCase.id, data, basicCase.version, abortCtrl.signal);
|
||||
expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}`, {
|
||||
method: 'PATCH',
|
||||
|
@ -317,7 +346,7 @@ describe('Case Configuration API', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('happy path', async () => {
|
||||
test('should return correct response', async () => {
|
||||
const resp = await patchCase(
|
||||
basicCase.id,
|
||||
{ description: 'updated description' },
|
||||
|
@ -341,7 +370,7 @@ describe('Case Configuration API', () => {
|
|||
},
|
||||
];
|
||||
|
||||
test('check url, method, signal', async () => {
|
||||
test('should be called with correct check url, method, signal', async () => {
|
||||
await patchCasesStatus(data, abortCtrl.signal);
|
||||
expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}`, {
|
||||
method: 'PATCH',
|
||||
|
@ -350,7 +379,7 @@ describe('Case Configuration API', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('happy path', async () => {
|
||||
test('should return correct response', async () => {
|
||||
const resp = await patchCasesStatus(data, abortCtrl.signal);
|
||||
expect(resp).toEqual({ ...cases });
|
||||
});
|
||||
|
@ -362,7 +391,7 @@ describe('Case Configuration API', () => {
|
|||
fetchMock.mockResolvedValue(basicCaseSnake);
|
||||
});
|
||||
|
||||
test('check url, method, signal', async () => {
|
||||
test('should be called with correct check url, method, signal', async () => {
|
||||
await patchComment({
|
||||
caseId: basicCase.id,
|
||||
commentId: basicCase.comments[0].id,
|
||||
|
@ -384,7 +413,7 @@ describe('Case Configuration API', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('happy path', async () => {
|
||||
test('should return correct response', async () => {
|
||||
const resp = await patchComment({
|
||||
caseId: basicCase.id,
|
||||
commentId: basicCase.comments[0].id,
|
||||
|
@ -418,7 +447,7 @@ describe('Case Configuration API', () => {
|
|||
owner: SECURITY_SOLUTION_OWNER,
|
||||
};
|
||||
|
||||
test('check url, method, signal', async () => {
|
||||
test('should be called with correct check url, method, signal', async () => {
|
||||
await postCase(data, abortCtrl.signal);
|
||||
expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}`, {
|
||||
method: 'POST',
|
||||
|
@ -427,7 +456,7 @@ describe('Case Configuration API', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('happy path', async () => {
|
||||
test('should return correct response', async () => {
|
||||
const resp = await postCase(data, abortCtrl.signal);
|
||||
expect(resp).toEqual(basicCase);
|
||||
});
|
||||
|
@ -444,7 +473,7 @@ describe('Case Configuration API', () => {
|
|||
type: CommentType.user as const,
|
||||
};
|
||||
|
||||
test('check url, method, signal', async () => {
|
||||
test('should be called with correct check url, method, signal', async () => {
|
||||
await postComment(data, basicCase.id, abortCtrl.signal);
|
||||
expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${basicCase.id}/comments`, {
|
||||
method: 'POST',
|
||||
|
@ -453,7 +482,7 @@ describe('Case Configuration API', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('happy path', async () => {
|
||||
test('should return correct response', async () => {
|
||||
const resp = await postComment(data, basicCase.id, abortCtrl.signal);
|
||||
expect(resp).toEqual(basicCase);
|
||||
});
|
||||
|
@ -467,7 +496,7 @@ describe('Case Configuration API', () => {
|
|||
fetchMock.mockResolvedValue(pushedCaseSnake);
|
||||
});
|
||||
|
||||
test('check url, method, signal', async () => {
|
||||
test('should be called with correct check url, method, signal', async () => {
|
||||
await pushCase(basicCase.id, connectorId, abortCtrl.signal);
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
`${CASES_URL}/${basicCase.id}/connector/${connectorId}/_push`,
|
||||
|
@ -479,7 +508,7 @@ describe('Case Configuration API', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('happy path', async () => {
|
||||
test('should return correct response', async () => {
|
||||
const resp = await pushCase(basicCase.id, connectorId, abortCtrl.signal);
|
||||
expect(resp).toEqual(pushedCase);
|
||||
});
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
CasePatchRequest,
|
||||
CasePostRequest,
|
||||
CaseResponse,
|
||||
CaseResolveResponse,
|
||||
CASES_URL,
|
||||
CasesFindResponse,
|
||||
CasesResponse,
|
||||
|
@ -35,6 +36,7 @@ import {
|
|||
SubCaseResponse,
|
||||
SubCasesResponse,
|
||||
User,
|
||||
ResolvedCase,
|
||||
} from '../../common';
|
||||
|
||||
import { getAllConnectorTypesUrl } from '../../common/utils/connectors_api';
|
||||
|
@ -61,6 +63,7 @@ import {
|
|||
decodeCasesFindResponse,
|
||||
decodeCasesStatusResponse,
|
||||
decodeCaseUserActionsResponse,
|
||||
decodeCaseResolveResponse,
|
||||
} from './utils';
|
||||
|
||||
export const getCase = async (
|
||||
|
@ -78,6 +81,24 @@ export const getCase = async (
|
|||
return convertToCamelCase<CaseResponse, Case>(decodeCaseResponse(response));
|
||||
};
|
||||
|
||||
export const resolveCase = async (
|
||||
caseId: string,
|
||||
includeComments: boolean = true,
|
||||
signal: AbortSignal
|
||||
): Promise<ResolvedCase> => {
|
||||
const response = await KibanaServices.get().http.fetch<CaseResolveResponse>(
|
||||
getCaseDetailsUrl(caseId) + '/resolve',
|
||||
{
|
||||
method: 'GET',
|
||||
query: {
|
||||
includeComments,
|
||||
},
|
||||
signal,
|
||||
}
|
||||
);
|
||||
return convertToCamelCase<CaseResolveResponse, ResolvedCase>(decodeCaseResolveResponse(response));
|
||||
};
|
||||
|
||||
export const getSubCase = async (
|
||||
caseId: string,
|
||||
subCaseId: string,
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
CommentResponse,
|
||||
CommentType,
|
||||
ConnectorTypes,
|
||||
ResolvedCase,
|
||||
isCreateConnector,
|
||||
isPush,
|
||||
isUpdateConnector,
|
||||
|
@ -163,6 +164,12 @@ export const basicCase: Case = {
|
|||
subCaseIds: [],
|
||||
};
|
||||
|
||||
export const basicResolvedCase: ResolvedCase = {
|
||||
case: basicCase,
|
||||
outcome: 'aliasMatch',
|
||||
aliasTargetId: `${basicCase.id}_2`,
|
||||
};
|
||||
|
||||
export const collectionCase: Case = {
|
||||
type: CaseType.collection,
|
||||
owner: SECURITY_SOLUTION_OWNER,
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { useGetCase, UseGetCase } from './use_get_case';
|
||||
import { basicCase } from './mock';
|
||||
import { basicCase, basicResolvedCase } from './mock';
|
||||
import * as api from './api';
|
||||
|
||||
jest.mock('./api');
|
||||
|
@ -28,6 +28,7 @@ describe('useGetCase', () => {
|
|||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual({
|
||||
data: null,
|
||||
resolveOutcome: null,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
fetchCase: result.current.fetchCase,
|
||||
|
@ -36,13 +37,13 @@ describe('useGetCase', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('calls getCase with correct arguments', async () => {
|
||||
const spyOnGetCase = jest.spyOn(api, 'getCase');
|
||||
it('calls resolveCase with correct arguments', async () => {
|
||||
const spyOnResolveCase = jest.spyOn(api, 'resolveCase');
|
||||
await act(async () => {
|
||||
const { waitForNextUpdate } = renderHook<string, UseGetCase>(() => useGetCase(basicCase.id));
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
expect(spyOnGetCase).toBeCalledWith(basicCase.id, true, abortCtrl.signal);
|
||||
expect(spyOnResolveCase).toBeCalledWith(basicCase.id, true, abortCtrl.signal);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -55,6 +56,8 @@ describe('useGetCase', () => {
|
|||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual({
|
||||
data: basicCase,
|
||||
resolveOutcome: basicResolvedCase.outcome,
|
||||
resolveAliasId: basicResolvedCase.aliasTargetId,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
fetchCase: result.current.fetchCase,
|
||||
|
@ -64,7 +67,7 @@ describe('useGetCase', () => {
|
|||
});
|
||||
|
||||
it('refetch case', async () => {
|
||||
const spyOnGetCase = jest.spyOn(api, 'getCase');
|
||||
const spyOnResolveCase = jest.spyOn(api, 'resolveCase');
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, UseGetCase>(() =>
|
||||
useGetCase(basicCase.id)
|
||||
|
@ -72,7 +75,7 @@ describe('useGetCase', () => {
|
|||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
result.current.fetchCase();
|
||||
expect(spyOnGetCase).toHaveBeenCalledTimes(2);
|
||||
expect(spyOnResolveCase).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -103,8 +106,8 @@ describe('useGetCase', () => {
|
|||
});
|
||||
|
||||
it('unhappy path', async () => {
|
||||
const spyOnGetCase = jest.spyOn(api, 'getCase');
|
||||
spyOnGetCase.mockImplementation(() => {
|
||||
const spyOnResolveCase = jest.spyOn(api, 'resolveCase');
|
||||
spyOnResolveCase.mockImplementation(() => {
|
||||
throw new Error('Something went wrong');
|
||||
});
|
||||
|
||||
|
@ -117,6 +120,7 @@ describe('useGetCase', () => {
|
|||
|
||||
expect(result.current).toEqual({
|
||||
data: null,
|
||||
resolveOutcome: null,
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
fetchCase: result.current.fetchCase,
|
||||
|
|
|
@ -7,20 +7,22 @@
|
|||
|
||||
import { useEffect, useReducer, useCallback, useRef } from 'react';
|
||||
|
||||
import { Case } from './types';
|
||||
import { Case, ResolvedCase } from './types';
|
||||
import * as i18n from './translations';
|
||||
import { useToasts } from '../common/lib/kibana';
|
||||
import { getCase, getSubCase } from './api';
|
||||
import { resolveCase, getSubCase } from './api';
|
||||
|
||||
interface CaseState {
|
||||
data: Case | null;
|
||||
resolveOutcome: ResolvedCase['outcome'] | null;
|
||||
resolveAliasId?: string;
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
}
|
||||
|
||||
type Action =
|
||||
| { type: 'FETCH_INIT'; payload: { silent: boolean } }
|
||||
| { type: 'FETCH_SUCCESS'; payload: Case }
|
||||
| { type: 'FETCH_SUCCESS'; payload: ResolvedCase }
|
||||
| { type: 'FETCH_FAILURE' }
|
||||
| { type: 'UPDATE_CASE'; payload: Case };
|
||||
|
||||
|
@ -40,7 +42,9 @@ const dataFetchReducer = (state: CaseState, action: Action): CaseState => {
|
|||
...state,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
data: action.payload,
|
||||
data: action.payload.case,
|
||||
resolveOutcome: action.payload.outcome,
|
||||
resolveAliasId: action.payload.aliasTargetId,
|
||||
};
|
||||
case 'FETCH_FAILURE':
|
||||
return {
|
||||
|
@ -72,6 +76,7 @@ export const useGetCase = (caseId: string, subCaseId?: string): UseGetCase => {
|
|||
isLoading: false,
|
||||
isError: false,
|
||||
data: null,
|
||||
resolveOutcome: null,
|
||||
});
|
||||
const toasts = useToasts();
|
||||
const isCancelledRef = useRef(false);
|
||||
|
@ -89,9 +94,12 @@ export const useGetCase = (caseId: string, subCaseId?: string): UseGetCase => {
|
|||
abortCtrlRef.current = new AbortController();
|
||||
dispatch({ type: 'FETCH_INIT', payload: { silent } });
|
||||
|
||||
const response = await (subCaseId
|
||||
? getSubCase(caseId, subCaseId, true, abortCtrlRef.current.signal)
|
||||
: getCase(caseId, true, abortCtrlRef.current.signal));
|
||||
const response: ResolvedCase = subCaseId
|
||||
? {
|
||||
case: await getSubCase(caseId, subCaseId, true, abortCtrlRef.current.signal),
|
||||
outcome: 'exactMatch', // sub-cases are not resolved, forced to exactMatch always
|
||||
}
|
||||
: await resolveCase(caseId, true, abortCtrlRef.current.signal);
|
||||
|
||||
if (!isCancelledRef.current) {
|
||||
dispatch({ type: 'FETCH_SUCCESS', payload: response });
|
||||
|
|
|
@ -30,6 +30,8 @@ import {
|
|||
CaseUserActionsResponseRt,
|
||||
CommentType,
|
||||
CasePatchRequest,
|
||||
CaseResolveResponse,
|
||||
CaseResolveResponseRt,
|
||||
} from '../../common';
|
||||
import { AllCases, Case, UpdateByKey } from './types';
|
||||
import * as i18n from './translations';
|
||||
|
@ -80,6 +82,12 @@ export const createToasterPlainError = (message: string) => new ToasterError([me
|
|||
export const decodeCaseResponse = (respCase?: CaseResponse) =>
|
||||
pipe(CaseResponseRt.decode(respCase), fold(throwErrors(createToasterPlainError), identity));
|
||||
|
||||
export const decodeCaseResolveResponse = (respCase?: CaseResolveResponse) =>
|
||||
pipe(
|
||||
CaseResolveResponseRt.decode(respCase),
|
||||
fold(throwErrors(createToasterPlainError), identity)
|
||||
);
|
||||
|
||||
export const decodeCasesResponse = (respCase?: CasesResponse) =>
|
||||
pipe(CasesResponseRt.decode(respCase), fold(throwErrors(createToasterPlainError), identity));
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ import type {
|
|||
} from '../../triggers_actions_ui/public';
|
||||
import type { DataPublicPluginStart } from '../../../../src/plugins/data/public';
|
||||
import type { EmbeddableStart } from '../../../../src/plugins/embeddable/public';
|
||||
import type { SpacesPluginStart } from '../../spaces/public';
|
||||
import type { Storage } from '../../../../src/plugins/kibana_utils/public';
|
||||
|
||||
import { AllCasesProps } from './components/all_cases';
|
||||
|
@ -36,6 +37,7 @@ export interface StartPlugins {
|
|||
lens: LensPublicStart;
|
||||
storage: Storage;
|
||||
triggersActionsUi: TriggersActionsStart;
|
||||
spaces?: SpacesPluginStart;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1596,6 +1596,90 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`audit_logger log function event structure creates the correct audit event for operation: "resolveCase" with an error and entity 1`] = `
|
||||
Object {
|
||||
"error": Object {
|
||||
"code": "Error",
|
||||
"message": "an error",
|
||||
},
|
||||
"event": Object {
|
||||
"action": "case_resolve",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"outcome": "failure",
|
||||
"type": Array [
|
||||
"access",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "1",
|
||||
"type": "cases",
|
||||
},
|
||||
},
|
||||
"message": "Failed attempt to access cases [id=1] as owner \\"awesome\\"",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`audit_logger log function event structure creates the correct audit event for operation: "resolveCase" with an error but no entity 1`] = `
|
||||
Object {
|
||||
"error": Object {
|
||||
"code": "Error",
|
||||
"message": "an error",
|
||||
},
|
||||
"event": Object {
|
||||
"action": "case_resolve",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"outcome": "failure",
|
||||
"type": Array [
|
||||
"access",
|
||||
],
|
||||
},
|
||||
"message": "Failed attempt to access a case as any owners",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`audit_logger log function event structure creates the correct audit event for operation: "resolveCase" without an error but with an entity 1`] = `
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_resolve",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"outcome": "success",
|
||||
"type": Array [
|
||||
"access",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "5",
|
||||
"type": "cases",
|
||||
},
|
||||
},
|
||||
"message": "User has accessed cases [id=5] as owner \\"super\\"",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`audit_logger log function event structure creates the correct audit event for operation: "resolveCase" without an error or entity 1`] = `
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_resolve",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"outcome": "success",
|
||||
"type": Array [
|
||||
"access",
|
||||
],
|
||||
},
|
||||
"message": "User has accessed a case as any owners",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`audit_logger log function event structure creates the correct audit event for operation: "updateCase" with an error and entity 1`] = `
|
||||
Object {
|
||||
"error": Object {
|
||||
|
|
|
@ -152,6 +152,14 @@ export const Operations: Record<ReadOperations | WriteOperations, OperationDetai
|
|||
docType: 'case',
|
||||
savedObjectType: CASE_SAVED_OBJECT,
|
||||
},
|
||||
[ReadOperations.ResolveCase]: {
|
||||
ecsType: EVENT_TYPES.access,
|
||||
name: ACCESS_CASE_OPERATION,
|
||||
action: 'case_resolve',
|
||||
verbs: accessVerbs,
|
||||
docType: 'case',
|
||||
savedObjectType: CASE_SAVED_OBJECT,
|
||||
},
|
||||
[ReadOperations.FindCases]: {
|
||||
ecsType: EVENT_TYPES.access,
|
||||
name: ACCESS_CASE_OPERATION,
|
||||
|
|
|
@ -28,6 +28,7 @@ export type GetSpaceFn = (request: KibanaRequest) => Promise<Space | undefined>;
|
|||
*/
|
||||
export enum ReadOperations {
|
||||
GetCase = 'getCase',
|
||||
ResolveCase = 'resolveCase',
|
||||
FindCases = 'findCases',
|
||||
GetCaseIDsByAlertID = 'getCaseIDsByAlertID',
|
||||
GetCaseStatuses = 'getCaseStatuses',
|
||||
|
|
|
@ -18,6 +18,7 @@ import { CasesClient } from '../client';
|
|||
import { CasesClientInternal } from '../client_internal';
|
||||
import {
|
||||
ICasePostRequest,
|
||||
ICaseResolveResponse,
|
||||
ICaseResponse,
|
||||
ICasesFindRequest,
|
||||
ICasesFindResponse,
|
||||
|
@ -31,6 +32,7 @@ import { find } from './find';
|
|||
import {
|
||||
CasesByAlertIDParams,
|
||||
get,
|
||||
resolve,
|
||||
getCasesByAlertID,
|
||||
GetParams,
|
||||
getReporters,
|
||||
|
@ -57,6 +59,11 @@ export interface CasesSubClient {
|
|||
* Retrieves a single case with the specified ID.
|
||||
*/
|
||||
get(params: GetParams): Promise<ICaseResponse>;
|
||||
/**
|
||||
* @experimental
|
||||
* Retrieves a single case resolving the specified ID.
|
||||
*/
|
||||
resolve(params: GetParams): Promise<ICaseResolveResponse>;
|
||||
/**
|
||||
* Pushes a specific case to an external system.
|
||||
*/
|
||||
|
@ -99,6 +106,7 @@ export const createCasesSubClient = (
|
|||
create: (data: CasePostRequest) => create(data, clientArgs),
|
||||
find: (params: CasesFindRequest) => find(params, clientArgs),
|
||||
get: (params: GetParams) => get(params, clientArgs),
|
||||
resolve: (params: GetParams) => resolve(params, clientArgs),
|
||||
push: (params: PushParams) => push(params, clientArgs, casesClient, casesClientInternal),
|
||||
update: (cases: CasesPatchRequest) => update(cases, clientArgs, casesClientInternal),
|
||||
delete: (ids: string[]) => deleteCases(ids, clientArgs),
|
||||
|
|
|
@ -9,10 +9,12 @@ import { pipe } from 'fp-ts/lib/pipeable';
|
|||
import { fold } from 'fp-ts/lib/Either';
|
||||
import { identity } from 'fp-ts/lib/function';
|
||||
|
||||
import { SavedObject } from 'kibana/server';
|
||||
import { SavedObject, SavedObjectsResolveResponse } from 'kibana/server';
|
||||
import {
|
||||
CaseResponseRt,
|
||||
CaseResponse,
|
||||
CaseResolveResponseRt,
|
||||
CaseResolveResponse,
|
||||
User,
|
||||
UsersRt,
|
||||
AllTagsFindRequest,
|
||||
|
@ -230,6 +232,86 @@ export const get = async (
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves a case resolving its ID and optionally loading its comments and sub case comments.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export const resolve = async (
|
||||
{ id, includeComments, includeSubCaseComments }: GetParams,
|
||||
clientArgs: CasesClientArgs
|
||||
): Promise<CaseResolveResponse> => {
|
||||
const { unsecuredSavedObjectsClient, caseService, logger, authorization } = clientArgs;
|
||||
|
||||
try {
|
||||
if (!ENABLE_CASE_CONNECTOR && includeSubCaseComments) {
|
||||
throw Boom.badRequest(
|
||||
'The `includeSubCaseComments` is not supported when the case connector feature is disabled'
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
saved_object: savedObject,
|
||||
...resolveData
|
||||
}: SavedObjectsResolveResponse<CaseAttributes> = await caseService.getResolveCase({
|
||||
unsecuredSavedObjectsClient,
|
||||
id,
|
||||
});
|
||||
|
||||
await authorization.ensureAuthorized({
|
||||
operation: Operations.resolveCase,
|
||||
entities: [
|
||||
{
|
||||
id: savedObject.id,
|
||||
owner: savedObject.attributes.owner,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let subCaseIds: string[] = [];
|
||||
if (ENABLE_CASE_CONNECTOR) {
|
||||
const subCasesForCaseId = await caseService.findSubCasesByCaseId({
|
||||
unsecuredSavedObjectsClient,
|
||||
ids: [id],
|
||||
});
|
||||
subCaseIds = subCasesForCaseId.saved_objects.map((so) => so.id);
|
||||
}
|
||||
|
||||
if (!includeComments) {
|
||||
return CaseResolveResponseRt.encode({
|
||||
...resolveData,
|
||||
case: flattenCaseSavedObject({
|
||||
savedObject,
|
||||
subCaseIds,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const theComments = await caseService.getAllCaseComments({
|
||||
unsecuredSavedObjectsClient,
|
||||
id,
|
||||
options: {
|
||||
sortField: 'created_at',
|
||||
sortOrder: 'asc',
|
||||
},
|
||||
includeSubCaseComments: ENABLE_CASE_CONNECTOR && includeSubCaseComments,
|
||||
});
|
||||
|
||||
return CaseResolveResponseRt.encode({
|
||||
...resolveData,
|
||||
case: flattenCaseSavedObject({
|
||||
savedObject,
|
||||
subCaseIds,
|
||||
comments: theComments.saved_objects,
|
||||
totalComment: theComments.total,
|
||||
totalAlerts: countAlertsForID({ comments: theComments, id }),
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
throw createCaseError({ message: `Failed to resolve case id: ${id}: ${error}`, error, logger });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the tags from all the cases.
|
||||
*/
|
||||
|
|
|
@ -22,6 +22,7 @@ const createCasesSubClientMock = (): CasesSubClientMock => {
|
|||
return {
|
||||
create: jest.fn(),
|
||||
find: jest.fn(),
|
||||
resolve: jest.fn(),
|
||||
get: jest.fn(),
|
||||
push: jest.fn(),
|
||||
update: jest.fn(),
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
import {
|
||||
AllCommentsResponse,
|
||||
CasePostRequest,
|
||||
CaseResolveResponse,
|
||||
CaseResponse,
|
||||
CasesConfigurePatch,
|
||||
CasesConfigureRequest,
|
||||
|
@ -40,6 +41,7 @@ export interface ICasePostRequest extends CasePostRequest {}
|
|||
export interface ICasesFindRequest extends CasesFindRequest {}
|
||||
export interface ICasesPatchRequest extends CasesPatchRequest {}
|
||||
export interface ICaseResponse extends CaseResponse {}
|
||||
export interface ICaseResolveResponse extends CaseResolveResponse {}
|
||||
export interface ICasesResponse extends CasesResponse {}
|
||||
export interface ICasesFindResponse extends CasesFindResponse {}
|
||||
|
||||
|
|
|
@ -45,4 +45,38 @@ export function initGetCaseApi({ router, logger }: RouteDeps) {
|
|||
}
|
||||
}
|
||||
);
|
||||
|
||||
router.get(
|
||||
{
|
||||
path: `${CASE_DETAILS_URL}/resolve`,
|
||||
validate: {
|
||||
params: schema.object({
|
||||
case_id: schema.string(),
|
||||
}),
|
||||
query: schema.object({
|
||||
includeComments: schema.boolean({ defaultValue: true }),
|
||||
includeSubCaseComments: schema.maybe(schema.boolean({ defaultValue: false })),
|
||||
}),
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
try {
|
||||
const casesClient = await context.cases.getCasesClient();
|
||||
const id = request.params.case_id;
|
||||
|
||||
return response.ok({
|
||||
body: await casesClient.cases.resolve({
|
||||
id,
|
||||
includeComments: request.query.includeComments,
|
||||
includeSubCaseComments: request.query.includeSubCaseComments,
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to retrieve case in resolve route case id: ${request.params.case_id} \ninclude comments: ${request.query.includeComments} \ninclude sub comments: ${request.query.includeSubCaseComments}: ${error}`
|
||||
);
|
||||
return response.customError(wrapError(error));
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
SavedObjectsFindResult,
|
||||
SavedObjectsBulkUpdateResponse,
|
||||
SavedObjectsUpdateResponse,
|
||||
SavedObjectsResolveResponse,
|
||||
} from 'kibana/server';
|
||||
|
||||
import type { estypes } from '@elastic/elasticsearch';
|
||||
|
@ -738,6 +739,27 @@ export class CasesService {
|
|||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getResolveCase({
|
||||
unsecuredSavedObjectsClient,
|
||||
id: caseId,
|
||||
}: GetCaseArgs): Promise<SavedObjectsResolveResponse<CaseAttributes>> {
|
||||
try {
|
||||
this.log.debug(`Attempting to resolve case ${caseId}`);
|
||||
const resolveCaseResult = await unsecuredSavedObjectsClient.resolve<ESCaseAttributes>(
|
||||
CASE_SAVED_OBJECT,
|
||||
caseId
|
||||
);
|
||||
return {
|
||||
...resolveCaseResult,
|
||||
saved_object: transformSavedObjectToExternalModel(resolveCaseResult.saved_object),
|
||||
};
|
||||
} catch (error) {
|
||||
this.log.error(`Error on resolve case ${caseId}: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getSubCase({
|
||||
unsecuredSavedObjectsClient,
|
||||
id,
|
||||
|
|
|
@ -36,6 +36,7 @@ export const createCaseServiceMock = (): CaseServiceMock => {
|
|||
getCases: jest.fn(),
|
||||
getCaseIdsByAlertId: jest.fn(),
|
||||
getMostRecentSubCase: jest.fn(),
|
||||
getResolveCase: jest.fn(),
|
||||
getSubCase: jest.fn(),
|
||||
getSubCases: jest.fn(),
|
||||
getTags: jest.fn(),
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
"home",
|
||||
"lens",
|
||||
"licensing",
|
||||
"spaces",
|
||||
"usageCollection"
|
||||
],
|
||||
"requiredPlugins": [
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
{ "path": "../cases/tsconfig.json" },
|
||||
{ "path": "../lens/tsconfig.json" },
|
||||
{ "path": "../rule_registry/tsconfig.json" },
|
||||
{ "path": "../spaces/tsconfig.json" },
|
||||
{ "path": "../timelines/tsconfig.json"},
|
||||
{ "path": "../translations/tsconfig.json" }
|
||||
]
|
||||
|
|
|
@ -47,6 +47,7 @@ import {
|
|||
AlertResponse,
|
||||
ConnectorMappings,
|
||||
CasesByAlertId,
|
||||
CaseResolveResponse,
|
||||
} from '../../../../plugins/cases/common/api';
|
||||
import { getPostCaseRequest, postCollectionReq, postCommentGenAlertReq } from './mock';
|
||||
import { getCaseUserActionUrl, getSubCasesUrl } from '../../../../plugins/cases/common/api/helpers';
|
||||
|
@ -1066,6 +1067,32 @@ export const getCase = async ({
|
|||
return theCase;
|
||||
};
|
||||
|
||||
export const resolveCase = async ({
|
||||
supertest,
|
||||
caseId,
|
||||
includeComments = false,
|
||||
expectedHttpCode = 200,
|
||||
auth = { user: superUser, space: null },
|
||||
}: {
|
||||
supertest: SuperTest.SuperTest<SuperTest.Test>;
|
||||
caseId: string;
|
||||
includeComments?: boolean;
|
||||
expectedHttpCode?: number;
|
||||
auth?: { user: User; space: string | null };
|
||||
}): Promise<CaseResolveResponse> => {
|
||||
const { body: theResolvedCase } = await supertest
|
||||
.get(
|
||||
`${getSpaceUrlPrefix(
|
||||
auth?.space
|
||||
)}${CASES_URL}/${caseId}/resolve?includeComments=${includeComments}`
|
||||
)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.auth(auth.user.username, auth.user.password)
|
||||
.expect(expectedHttpCode);
|
||||
|
||||
return theResolvedCase;
|
||||
};
|
||||
|
||||
export const findCases = async ({
|
||||
supertest,
|
||||
query = {},
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
CASES_URL,
|
||||
SECURITY_SOLUTION_OWNER,
|
||||
} from '../../../../../../plugins/cases/common/constants';
|
||||
import { getCase, getCaseSavedObjectsFromES } from '../../../../common/lib/utils';
|
||||
import { getCase, getCaseSavedObjectsFromES, resolveCase } from '../../../../common/lib/utils';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function createGetTests({ getService }: FtrProviderContext) {
|
||||
|
@ -207,5 +207,76 @@ export default function createGetTests({ getService }: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('7.16.0', () => {
|
||||
before(async () => {
|
||||
await esArchiver.load('x-pack/test/functional/es_archives/cases/migrations/7.13.2');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await esArchiver.unload('x-pack/test/functional/es_archives/cases/migrations/7.13.2');
|
||||
});
|
||||
|
||||
describe('resolve', () => {
|
||||
it('should return exactMatch outcome', async () => {
|
||||
const { outcome } = await resolveCase({
|
||||
supertest,
|
||||
caseId: 'e49ad6e0-cf9d-11eb-a603-13e7747d215c',
|
||||
});
|
||||
|
||||
expect(outcome).to.be('exactMatch');
|
||||
});
|
||||
|
||||
it('should preserve the same case info', async () => {
|
||||
const { case: theCase } = await resolveCase({
|
||||
supertest,
|
||||
caseId: 'e49ad6e0-cf9d-11eb-a603-13e7747d215c',
|
||||
});
|
||||
|
||||
expect(theCase.title).to.be('A case');
|
||||
expect(theCase.description).to.be('asdf');
|
||||
expect(theCase.owner).to.be(SECURITY_SOLUTION_OWNER);
|
||||
});
|
||||
|
||||
it('should preserve the same connector', async () => {
|
||||
const { case: theCase } = await resolveCase({
|
||||
supertest,
|
||||
caseId: 'e49ad6e0-cf9d-11eb-a603-13e7747d215c',
|
||||
});
|
||||
|
||||
expect(theCase.connector).to.eql({
|
||||
fields: {
|
||||
issueType: '10002',
|
||||
parent: null,
|
||||
priority: null,
|
||||
},
|
||||
id: 'd68508f0-cf9d-11eb-a603-13e7747d215c',
|
||||
name: 'Test Jira',
|
||||
type: '.jira',
|
||||
});
|
||||
});
|
||||
|
||||
it('should preserve the same external service', async () => {
|
||||
const { case: theCase } = await resolveCase({
|
||||
supertest,
|
||||
caseId: 'e49ad6e0-cf9d-11eb-a603-13e7747d215c',
|
||||
});
|
||||
|
||||
expect(theCase.external_service).to.eql({
|
||||
connector_id: 'd68508f0-cf9d-11eb-a603-13e7747d215c',
|
||||
connector_name: 'Test Jira',
|
||||
external_id: '10106',
|
||||
external_title: 'TPN-99',
|
||||
external_url: 'https://cases-testing.atlassian.net/browse/TPN-99',
|
||||
pushed_at: '2021-06-17T18:57:45.524Z',
|
||||
pushed_by: {
|
||||
email: null,
|
||||
full_name: 'j@j.com',
|
||||
username: '711621466',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,221 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
|
||||
import { AttributesTypeUser } from '../../../../../../plugins/cases/common/api';
|
||||
import { CASES_URL } from '../../../../../../plugins/cases/common/constants';
|
||||
import {
|
||||
defaultUser,
|
||||
postCaseReq,
|
||||
postCaseResp,
|
||||
postCommentUserReq,
|
||||
getPostCaseRequest,
|
||||
} from '../../../../common/lib/mock';
|
||||
import {
|
||||
deleteCasesByESQuery,
|
||||
createCase,
|
||||
resolveCase,
|
||||
createComment,
|
||||
removeServerGeneratedPropertiesFromCase,
|
||||
removeServerGeneratedPropertiesFromSavedObject,
|
||||
} from '../../../../common/lib/utils';
|
||||
import {
|
||||
secOnly,
|
||||
obsOnly,
|
||||
globalRead,
|
||||
superUser,
|
||||
secOnlyRead,
|
||||
obsOnlyRead,
|
||||
obsSecRead,
|
||||
noKibanaPrivileges,
|
||||
obsSec,
|
||||
} from '../../../../common/lib/authentication/users';
|
||||
import { getUserInfo } from '../../../../common/lib/authentication';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext): void => {
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const es = getService('es');
|
||||
|
||||
describe('resolve_case', () => {
|
||||
afterEach(async () => {
|
||||
await deleteCasesByESQuery(es);
|
||||
});
|
||||
|
||||
it('should resolve a case with no comments', async () => {
|
||||
const postedCase = await createCase(supertest, getPostCaseRequest());
|
||||
const resolvedCase = await resolveCase({
|
||||
supertest,
|
||||
caseId: postedCase.id,
|
||||
includeComments: true,
|
||||
});
|
||||
|
||||
const data = removeServerGeneratedPropertiesFromCase(resolvedCase.case);
|
||||
expect(data).to.eql(postCaseResp());
|
||||
expect(data.comments?.length).to.eql(0);
|
||||
});
|
||||
|
||||
it('should resolve a case with comments', async () => {
|
||||
const postedCase = await createCase(supertest, postCaseReq);
|
||||
await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq });
|
||||
const resolvedCase = await resolveCase({
|
||||
supertest,
|
||||
caseId: postedCase.id,
|
||||
includeComments: true,
|
||||
});
|
||||
|
||||
const comment = removeServerGeneratedPropertiesFromSavedObject(
|
||||
resolvedCase.case.comments![0] as AttributesTypeUser
|
||||
);
|
||||
|
||||
expect(resolvedCase.case.comments?.length).to.eql(1);
|
||||
expect(comment).to.eql({
|
||||
type: postCommentUserReq.type,
|
||||
comment: postCommentUserReq.comment,
|
||||
associationType: 'case',
|
||||
created_by: defaultUser,
|
||||
pushed_at: null,
|
||||
pushed_by: null,
|
||||
updated_by: null,
|
||||
owner: 'securitySolutionFixture',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a 400 when passing the includeSubCaseComments', async () => {
|
||||
const { body } = await supertest
|
||||
.get(`${CASES_URL}/case-id/resolve?includeSubCaseComments=true`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(400);
|
||||
|
||||
expect(body.message).to.contain('disabled');
|
||||
});
|
||||
|
||||
it('unhappy path - 404s when case is not there', async () => {
|
||||
await supertest
|
||||
.get(`${CASES_URL}/fake-id/resolve`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
describe('rbac', () => {
|
||||
it('should resolve a case', async () => {
|
||||
const newCase = await createCase(
|
||||
supertestWithoutAuth,
|
||||
getPostCaseRequest({ owner: 'securitySolutionFixture' }),
|
||||
200,
|
||||
{
|
||||
user: superUser,
|
||||
space: 'space1',
|
||||
}
|
||||
);
|
||||
|
||||
for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) {
|
||||
const resolvedCase = await resolveCase({
|
||||
supertest: supertestWithoutAuth,
|
||||
caseId: newCase.id,
|
||||
auth: { user, space: 'space1' },
|
||||
});
|
||||
|
||||
expect(resolvedCase.case.owner).to.eql('securitySolutionFixture');
|
||||
expect(resolvedCase.outcome).to.eql('exactMatch');
|
||||
expect(resolvedCase.alias_target_id).to.eql(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
it('should resolve a case with comments', async () => {
|
||||
const postedCase = await createCase(
|
||||
supertestWithoutAuth,
|
||||
getPostCaseRequest({ owner: 'securitySolutionFixture' }),
|
||||
200,
|
||||
{
|
||||
user: secOnly,
|
||||
space: 'space1',
|
||||
}
|
||||
);
|
||||
|
||||
await createComment({
|
||||
supertest: supertestWithoutAuth,
|
||||
caseId: postedCase.id,
|
||||
params: postCommentUserReq,
|
||||
expectedHttpCode: 200,
|
||||
auth: {
|
||||
user: secOnly,
|
||||
space: 'space1',
|
||||
},
|
||||
});
|
||||
|
||||
const resolvedCase = await resolveCase({
|
||||
supertest: supertestWithoutAuth,
|
||||
caseId: postedCase.id,
|
||||
includeComments: true,
|
||||
auth: { user: secOnly, space: 'space1' },
|
||||
});
|
||||
|
||||
const comment = removeServerGeneratedPropertiesFromSavedObject(
|
||||
resolvedCase.case.comments![0] as AttributesTypeUser
|
||||
);
|
||||
|
||||
expect(resolvedCase.case.comments?.length).to.eql(1);
|
||||
expect(comment).to.eql({
|
||||
type: postCommentUserReq.type,
|
||||
comment: postCommentUserReq.comment,
|
||||
associationType: 'case',
|
||||
created_by: getUserInfo(secOnly),
|
||||
pushed_at: null,
|
||||
pushed_by: null,
|
||||
updated_by: null,
|
||||
owner: 'securitySolutionFixture',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not resolve a case when the user does not have access to owner', async () => {
|
||||
const newCase = await createCase(
|
||||
supertestWithoutAuth,
|
||||
getPostCaseRequest({ owner: 'securitySolutionFixture' }),
|
||||
200,
|
||||
{
|
||||
user: superUser,
|
||||
space: 'space1',
|
||||
}
|
||||
);
|
||||
|
||||
for (const user of [noKibanaPrivileges, obsOnly, obsOnlyRead]) {
|
||||
await resolveCase({
|
||||
supertest: supertestWithoutAuth,
|
||||
caseId: newCase.id,
|
||||
expectedHttpCode: 403,
|
||||
auth: { user, space: 'space1' },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('should NOT resolve a case in a space with no permissions', async () => {
|
||||
const newCase = await createCase(
|
||||
supertestWithoutAuth,
|
||||
getPostCaseRequest({ owner: 'securitySolutionFixture' }),
|
||||
200,
|
||||
{
|
||||
user: superUser,
|
||||
space: 'space2',
|
||||
}
|
||||
);
|
||||
|
||||
await resolveCase({
|
||||
supertest: supertestWithoutAuth,
|
||||
caseId: newCase.id,
|
||||
expectedHttpCode: 403,
|
||||
auth: { user: secOnly, space: 'space2' },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -25,6 +25,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => {
|
|||
loadTestFile(require.resolve('./cases/get_case'));
|
||||
loadTestFile(require.resolve('./cases/patch_cases'));
|
||||
loadTestFile(require.resolve('./cases/post_case'));
|
||||
loadTestFile(require.resolve('./cases/resolve_case'));
|
||||
loadTestFile(require.resolve('./cases/reporters/get_reporters'));
|
||||
loadTestFile(require.resolve('./cases/status/get_status'));
|
||||
loadTestFile(require.resolve('./cases/tags/get_tags'));
|
||||
|
|
Loading…
Reference in a new issue