mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[ResponseOps][Cases] Add assignee field and filtering (#137532)
* Refactoring services, auth * Adding suggest api and tests * Working integration tests * Switching suggest api tags * Adding assignees field * Adding tests for size and owner * Adding assignee integration tests * Starting user actions changes * Using lodash for array comparison logic and tests * Fixing type error * Adding assignees user action * [ResponseOps][Cases] Refactoring client args and authentication (#137345) * Refactoring services, auth * Fixing type errors * Adding assignees migration and tests * Fixing types and added more tests * Fixing cypress test * Fixing test * Adding migration for assignees field and tests * Adding comments and a few more tests * Updating comments and spelling * Addressing feedback * Removing optional owners * Forgot rest of files * Adding default empty array for user actions * Fixing test error Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
0f314a60bd
commit
aa7d8e8ab7
56 changed files with 1688 additions and 85 deletions
13
x-pack/plugins/cases/common/api/cases/assignee.ts
Normal file
13
x-pack/plugins/cases/common/api/cases/assignee.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as rt from 'io-ts';
|
||||
import { CaseUserProfileRt } from './user_profiles';
|
||||
|
||||
export const CaseAssigneesRt = rt.array(CaseUserProfileRt);
|
||||
|
||||
export type CaseAssignees = rt.TypeOf<typeof CaseAssigneesRt>;
|
|
@ -12,6 +12,7 @@ import { UserRT } from '../user';
|
|||
import { CommentResponseRt } from './comment';
|
||||
import { CasesStatusResponseRt, CaseStatusRt } from './status';
|
||||
import { CaseConnectorRt } from '../connectors';
|
||||
import { CaseAssigneesRt } from './assignee';
|
||||
|
||||
const BucketsAggs = rt.array(
|
||||
rt.type({
|
||||
|
@ -86,6 +87,10 @@ const CaseBasicRt = rt.type({
|
|||
* The severity of the case
|
||||
*/
|
||||
severity: CaseSeverityRt,
|
||||
/**
|
||||
* The users assigned to this case
|
||||
*/
|
||||
assignees: CaseAssigneesRt,
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -153,6 +158,10 @@ export const CasePostRequestRt = rt.intersection([
|
|||
owner: rt.string,
|
||||
}),
|
||||
rt.partial({
|
||||
/**
|
||||
* The users assigned to the case
|
||||
*/
|
||||
assignees: CaseAssigneesRt,
|
||||
/**
|
||||
* The severity of the case. The severity is
|
||||
* default it to "low" if not provided.
|
||||
|
@ -174,6 +183,10 @@ export const CasesFindRequestRt = rt.partial({
|
|||
* The severity of the case
|
||||
*/
|
||||
severity: CaseSeverityRt,
|
||||
/**
|
||||
* The uids of the user profiles to filter by
|
||||
*/
|
||||
assignees: rt.union([rt.array(rt.string), rt.string]),
|
||||
/**
|
||||
* The reporters to filter by
|
||||
*/
|
||||
|
|
|
@ -13,3 +13,4 @@ export * from './user_actions';
|
|||
export * from './constants';
|
||||
export * from './alerts';
|
||||
export * from './user_profiles';
|
||||
export * from './assignee';
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as rt from 'io-ts';
|
||||
|
||||
export const SuggestUserProfilesRequestRt = rt.intersection([
|
||||
rt.type({
|
||||
name: rt.string,
|
||||
owners: rt.array(rt.string),
|
||||
}),
|
||||
rt.partial({ size: rt.number }),
|
||||
]);
|
||||
|
||||
export type SuggestUserProfilesRequest = rt.TypeOf<typeof SuggestUserProfilesRequestRt>;
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 * as rt from 'io-ts';
|
||||
import { CaseAssigneesRt } from '../assignee';
|
||||
import { ActionTypes, UserActionWithAttributes } from './common';
|
||||
|
||||
export const AssigneesUserActionPayloadRt = rt.type({ assignees: CaseAssigneesRt });
|
||||
|
||||
export const AssigneesUserActionRt = rt.type({
|
||||
type: rt.literal(ActionTypes.assignees),
|
||||
payload: AssigneesUserActionPayloadRt,
|
||||
});
|
||||
|
||||
export type AssigneesUserAction = UserActionWithAttributes<rt.TypeOf<typeof AssigneesUserActionRt>>;
|
|
@ -9,6 +9,7 @@ import * as rt from 'io-ts';
|
|||
import { UserRT } from '../../user';
|
||||
|
||||
export const ActionTypes = {
|
||||
assignees: 'assignees',
|
||||
comment: 'comment',
|
||||
connector: 'connector',
|
||||
description: 'description',
|
||||
|
@ -22,6 +23,9 @@ export const ActionTypes = {
|
|||
delete_case: 'delete_case',
|
||||
} as const;
|
||||
|
||||
export type ActionTypeKeys = keyof typeof ActionTypes;
|
||||
export type ActionTypeValues = typeof ActionTypes[ActionTypeKeys];
|
||||
|
||||
export const Actions = {
|
||||
add: 'add',
|
||||
create: 'create',
|
||||
|
@ -30,6 +34,9 @@ export const Actions = {
|
|||
push_to_service: 'push_to_service',
|
||||
} as const;
|
||||
|
||||
export type ActionOperationKeys = keyof typeof Actions;
|
||||
export type ActionOperationValues = typeof Actions[ActionOperationKeys];
|
||||
|
||||
/* To the next developer, if you add/removed fields here
|
||||
* make sure to check this file (x-pack/plugins/cases/server/services/user_actions/helpers.ts) too
|
||||
*/
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import * as rt from 'io-ts';
|
||||
import { AssigneesUserActionPayloadRt } from './assignees';
|
||||
import { ActionTypes, UserActionWithAttributes } from './common';
|
||||
import {
|
||||
ConnectorUserActionPayloadRt,
|
||||
|
@ -21,6 +22,7 @@ export const CommonFieldsRt = rt.type({
|
|||
});
|
||||
|
||||
const CommonPayloadAttributesRt = rt.type({
|
||||
assignees: AssigneesUserActionPayloadRt.props.assignees,
|
||||
description: DescriptionUserActionPayloadRt.props.description,
|
||||
status: rt.string,
|
||||
severity: rt.string,
|
||||
|
|
|
@ -24,6 +24,7 @@ import { SettingsUserActionRt } from './settings';
|
|||
import { StatusUserActionRt } from './status';
|
||||
import { DeleteCaseUserActionRt } from './delete_case';
|
||||
import { SeverityUserActionRt } from './severity';
|
||||
import { AssigneesUserActionRt } from './assignees';
|
||||
|
||||
export * from './common';
|
||||
export * from './comment';
|
||||
|
@ -36,6 +37,7 @@ export * from './settings';
|
|||
export * from './status';
|
||||
export * from './tags';
|
||||
export * from './title';
|
||||
export * from './assignees';
|
||||
|
||||
const CommonUserActionsRt = rt.union([
|
||||
DescriptionUserActionRt,
|
||||
|
@ -45,6 +47,7 @@ const CommonUserActionsRt = rt.union([
|
|||
SettingsUserActionRt,
|
||||
StatusUserActionRt,
|
||||
SeverityUserActionRt,
|
||||
AssigneesUserActionRt,
|
||||
]);
|
||||
|
||||
export const UserActionsRt = rt.union([
|
||||
|
|
|
@ -16,3 +16,9 @@ export const SuggestUserProfilesRequestRt = rt.intersection([
|
|||
]);
|
||||
|
||||
export type SuggestUserProfilesRequest = rt.TypeOf<typeof SuggestUserProfilesRequestRt>;
|
||||
|
||||
export const CaseUserProfileRt = rt.type({
|
||||
uid: rt.string,
|
||||
});
|
||||
|
||||
export type CaseUserProfile = rt.TypeOf<typeof CaseUserProfileRt>;
|
||||
|
|
|
@ -581,6 +581,7 @@ describe('AllCasesListGeneric', () => {
|
|||
wrapper.find('[data-test-subj="cases-table-row-select-1"]').first().simulate('click');
|
||||
await waitFor(() => {
|
||||
expect(onRowClick).toHaveBeenCalledWith({
|
||||
assignees: [],
|
||||
closedAt: null,
|
||||
closedBy: null,
|
||||
comments: [],
|
||||
|
|
|
@ -33,6 +33,7 @@ const initialCaseValue: FormProps = {
|
|||
fields: null,
|
||||
syncAlerts: true,
|
||||
selectedOwner: null,
|
||||
assignees: [],
|
||||
};
|
||||
|
||||
interface Props {
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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 type { UserActionBuilder } from './types';
|
||||
|
||||
export const createAssigneesUserActionBuilder: UserActionBuilder = ({
|
||||
userAction,
|
||||
handleOutlineComment,
|
||||
}) => ({
|
||||
build: () => {
|
||||
return [];
|
||||
},
|
||||
});
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { createAssigneesUserActionBuilder } from './assignees';
|
||||
import { createCommentUserActionBuilder } from './comment/comment';
|
||||
import { createConnectorUserActionBuilder } from './connector';
|
||||
import { createDescriptionUserActionBuilder } from './description';
|
||||
|
@ -26,4 +27,5 @@ export const builderMap: UserActionBuilderMap = {
|
|||
comment: createCommentUserActionBuilder,
|
||||
description: createDescriptionUserActionBuilder,
|
||||
settings: createSettingsUserActionBuilder,
|
||||
assignees: createAssigneesUserActionBuilder,
|
||||
};
|
||||
|
|
|
@ -228,6 +228,7 @@ export const basicCase: Case = {
|
|||
settings: {
|
||||
syncAlerts: true,
|
||||
},
|
||||
assignees: [],
|
||||
};
|
||||
|
||||
export const caseWithAlerts = {
|
||||
|
@ -329,6 +330,7 @@ export const mockCase: Case = {
|
|||
settings: {
|
||||
syncAlerts: true,
|
||||
},
|
||||
assignees: [],
|
||||
};
|
||||
|
||||
export const basicCasePost: Case = {
|
||||
|
@ -631,6 +633,7 @@ export const getUserAction = (
|
|||
tags: ['a tag'],
|
||||
settings: { syncAlerts: true },
|
||||
owner: SECURITY_SOLUTION_OWNER,
|
||||
assignees: [],
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
|
|
|
@ -86,7 +86,11 @@ export const create = async (
|
|||
unsecuredSavedObjectsClient,
|
||||
caseId: newCase.id,
|
||||
user,
|
||||
payload: { ...query, severity: query.severity ?? CaseSeverity.LOW },
|
||||
payload: {
|
||||
...query,
|
||||
severity: query.severity ?? CaseSeverity.LOW,
|
||||
assignees: query.assignees ?? [],
|
||||
},
|
||||
owner: newCase.attributes.owner,
|
||||
});
|
||||
|
||||
|
|
|
@ -62,6 +62,7 @@ export const find = async (
|
|||
owner: queryParams.owner,
|
||||
from: queryParams.from,
|
||||
to: queryParams.to,
|
||||
assignees: queryParams.assignees,
|
||||
};
|
||||
|
||||
const statusStatsOptions = constructQueryOptions({
|
||||
|
|
|
@ -242,6 +242,7 @@ export const userActions: CaseUserActionsResponse = [
|
|||
status: 'open',
|
||||
severity: 'low',
|
||||
owner: SECURITY_SOLUTION_OWNER,
|
||||
assignees: [],
|
||||
},
|
||||
action_id: 'fd830c60-6646-11eb-a291-51bf6b175a53',
|
||||
case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53',
|
||||
|
|
|
@ -57,4 +57,5 @@ export interface ConstructQueryParams {
|
|||
authorizationFilter?: KueryNode;
|
||||
from?: string;
|
||||
to?: string;
|
||||
assignees?: string | string[];
|
||||
}
|
||||
|
|
|
@ -5,7 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { arraysDifference, buildRangeFilter, constructQueryOptions, sortToSnake } from './utils';
|
||||
import {
|
||||
arraysDifference,
|
||||
buildNestedFilter,
|
||||
buildRangeFilter,
|
||||
constructQueryOptions,
|
||||
sortToSnake,
|
||||
} from './utils';
|
||||
import { toElasticsearchQuery } from '@kbn/es-query';
|
||||
import { CaseStatuses } from '../../common';
|
||||
import { CaseSeverity } from '../../common/api';
|
||||
|
@ -490,6 +496,163 @@ describe('utils', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('buildNestedFilter', () => {
|
||||
it('returns undefined if filters is undefined', () => {
|
||||
expect(buildNestedFilter({ field: '', nestedField: '', operator: 'or' })).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined when the filters array is empty', () => {
|
||||
expect(
|
||||
buildNestedFilter({ filters: [], field: '', nestedField: '', operator: 'or' })
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns a KueryNode for a single filter', () => {
|
||||
expect(
|
||||
toElasticsearchQuery(
|
||||
buildNestedFilter({
|
||||
filters: ['hello'],
|
||||
field: 'uid',
|
||||
nestedField: 'nestedField',
|
||||
operator: 'or',
|
||||
})!
|
||||
)
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"nested": Object {
|
||||
"path": "cases.attributes.nestedField",
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"minimum_should_match": 1,
|
||||
"should": Array [
|
||||
Object {
|
||||
"match": Object {
|
||||
"cases.attributes.nestedField.uid": "hello",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"score_mode": "none",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it("returns a KueryNode for multiple filters or'd together", () => {
|
||||
expect(
|
||||
toElasticsearchQuery(
|
||||
buildNestedFilter({
|
||||
filters: ['uid1', 'uid2'],
|
||||
field: 'uid',
|
||||
nestedField: 'nestedField',
|
||||
operator: 'or',
|
||||
})!
|
||||
)
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"bool": Object {
|
||||
"minimum_should_match": 1,
|
||||
"should": Array [
|
||||
Object {
|
||||
"nested": Object {
|
||||
"path": "cases.attributes.nestedField",
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"minimum_should_match": 1,
|
||||
"should": Array [
|
||||
Object {
|
||||
"match": Object {
|
||||
"cases.attributes.nestedField.uid": "uid1",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"score_mode": "none",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"nested": Object {
|
||||
"path": "cases.attributes.nestedField",
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"minimum_should_match": 1,
|
||||
"should": Array [
|
||||
Object {
|
||||
"match": Object {
|
||||
"cases.attributes.nestedField.uid": "uid2",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"score_mode": "none",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it("returns a KueryNode for multiple filters and'ed together", () => {
|
||||
expect(
|
||||
toElasticsearchQuery(
|
||||
buildNestedFilter({
|
||||
filters: ['uid1', 'uid2'],
|
||||
field: 'uid',
|
||||
nestedField: 'nestedField',
|
||||
operator: 'and',
|
||||
})!
|
||||
)
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
Object {
|
||||
"nested": Object {
|
||||
"path": "cases.attributes.nestedField",
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"minimum_should_match": 1,
|
||||
"should": Array [
|
||||
Object {
|
||||
"match": Object {
|
||||
"cases.attributes.nestedField.uid": "uid1",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"score_mode": "none",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"nested": Object {
|
||||
"path": "cases.attributes.nestedField",
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"minimum_should_match": 1,
|
||||
"should": Array [
|
||||
Object {
|
||||
"match": Object {
|
||||
"cases.attributes.nestedField.uid": "uid2",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"score_mode": "none",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('arraysDifference', () => {
|
||||
it('returns null if originalValue is null', () => {
|
||||
expect(arraysDifference(null, [])).toBeNull();
|
||||
|
|
|
@ -177,6 +177,10 @@ interface FilterField {
|
|||
type?: string;
|
||||
}
|
||||
|
||||
interface NestedFilterField extends FilterField {
|
||||
nestedField: string;
|
||||
}
|
||||
|
||||
export const buildFilter = ({
|
||||
filters,
|
||||
field,
|
||||
|
@ -194,7 +198,48 @@ export const buildFilter = ({
|
|||
}
|
||||
|
||||
return nodeBuilder[operator](
|
||||
filtersAsArray.map((filter) => nodeBuilder.is(`${type}.attributes.${field}`, filter))
|
||||
filtersAsArray.map((filter) =>
|
||||
nodeBuilder.is(`${escapeKuery(type)}.attributes.${escapeKuery(field)}`, escapeKuery(filter))
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a KueryNode filter for the Saved Object find API's filter field. This handles constructing a filter for
|
||||
* a nested field.
|
||||
*
|
||||
* @param filters is a string or array of strings that defines the values to search for
|
||||
* @param field is the location to search for
|
||||
* @param nestedField is the field in the saved object that has a type of 'nested'
|
||||
* @param operator whether to 'or'/'and' the created filters together
|
||||
* @type the type of saved object being searched
|
||||
* @returns a constructed KueryNode representing the filter or undefined if one could not be built
|
||||
*/
|
||||
export const buildNestedFilter = ({
|
||||
filters,
|
||||
field,
|
||||
nestedField,
|
||||
operator,
|
||||
type = CASE_SAVED_OBJECT,
|
||||
}: NestedFilterField): KueryNode | undefined => {
|
||||
if (filters === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filtersAsArray = Array.isArray(filters) ? filters : [filters];
|
||||
|
||||
if (filtersAsArray.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
return nodeBuilder[operator](
|
||||
filtersAsArray.map((filter) =>
|
||||
fromKueryExpression(
|
||||
`${escapeKuery(type)}.attributes.${escapeKuery(nestedField)}:{ ${escapeKuery(
|
||||
field
|
||||
)}: ${escapeKuery(filter)} }`
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -284,6 +329,7 @@ export const constructQueryOptions = ({
|
|||
authorizationFilter,
|
||||
from,
|
||||
to,
|
||||
assignees,
|
||||
}: ConstructQueryParams): SavedObjectFindOptionsKueryNode => {
|
||||
const tagsFilter = buildFilter({ filters: tags, field: 'tags', operator: 'or' });
|
||||
const reportersFilter = buildFilter({
|
||||
|
@ -297,6 +343,11 @@ export const constructQueryOptions = ({
|
|||
const statusFilter = status != null ? addStatusFilter({ status }) : undefined;
|
||||
const severityFilter = severity != null ? addSeverityFilter({ severity }) : undefined;
|
||||
const rangeFilter = buildRangeFilter({ from, to });
|
||||
const assigneesFilter = buildFilter({
|
||||
filters: assignees,
|
||||
field: 'assignees.uid',
|
||||
operator: 'or',
|
||||
});
|
||||
|
||||
const filters = combineFilters([
|
||||
statusFilter,
|
||||
|
@ -305,6 +356,7 @@ export const constructQueryOptions = ({
|
|||
reportersFilter,
|
||||
rangeFilter,
|
||||
ownerFilter,
|
||||
assigneesFilter,
|
||||
]);
|
||||
|
||||
return {
|
||||
|
|
|
@ -103,6 +103,7 @@ describe('common utils', () => {
|
|||
|
||||
expect(res).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"assignees": Array [],
|
||||
"closed_at": null,
|
||||
"closed_by": null,
|
||||
"connector": Object {
|
||||
|
@ -155,6 +156,7 @@ describe('common utils', () => {
|
|||
|
||||
expect(res).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"assignees": Array [],
|
||||
"closed_at": null,
|
||||
"closed_by": null,
|
||||
"connector": Object {
|
||||
|
@ -192,6 +194,63 @@ describe('common utils', () => {
|
|||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('transform correctly with assignees provided', () => {
|
||||
const myCase = {
|
||||
newCase: { ...newCase, connector, assignees: [{ uid: '1' }] },
|
||||
user: {
|
||||
email: 'elastic@elastic.co',
|
||||
full_name: 'Elastic',
|
||||
username: 'elastic',
|
||||
},
|
||||
};
|
||||
|
||||
const res = transformNewCase(myCase);
|
||||
|
||||
expect(res).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"assignees": Array [
|
||||
Object {
|
||||
"uid": "1",
|
||||
},
|
||||
],
|
||||
"closed_at": null,
|
||||
"closed_by": null,
|
||||
"connector": Object {
|
||||
"fields": Object {
|
||||
"issueType": "Task",
|
||||
"parent": null,
|
||||
"priority": "High",
|
||||
},
|
||||
"id": "123",
|
||||
"name": "My connector",
|
||||
"type": ".jira",
|
||||
},
|
||||
"created_at": "2020-04-09T09:43:51.778Z",
|
||||
"created_by": Object {
|
||||
"email": "elastic@elastic.co",
|
||||
"full_name": "Elastic",
|
||||
"username": "elastic",
|
||||
},
|
||||
"description": "A description",
|
||||
"duration": null,
|
||||
"external_service": null,
|
||||
"owner": "securitySolution",
|
||||
"settings": Object {
|
||||
"syncAlerts": true,
|
||||
},
|
||||
"severity": "low",
|
||||
"status": "open",
|
||||
"tags": Array [
|
||||
"new",
|
||||
"case",
|
||||
],
|
||||
"title": "My new case",
|
||||
"updated_at": null,
|
||||
"updated_by": null,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformCases', () => {
|
||||
|
@ -214,6 +273,7 @@ describe('common utils', () => {
|
|||
Object {
|
||||
"cases": Array [
|
||||
Object {
|
||||
"assignees": Array [],
|
||||
"closed_at": null,
|
||||
"closed_by": null,
|
||||
"comments": Array [],
|
||||
|
@ -254,6 +314,7 @@ describe('common utils', () => {
|
|||
"version": "WzAsMV0=",
|
||||
},
|
||||
Object {
|
||||
"assignees": Array [],
|
||||
"closed_at": null,
|
||||
"closed_by": null,
|
||||
"comments": Array [],
|
||||
|
@ -294,6 +355,7 @@ describe('common utils', () => {
|
|||
"version": "WzQsMV0=",
|
||||
},
|
||||
Object {
|
||||
"assignees": Array [],
|
||||
"closed_at": null,
|
||||
"closed_by": null,
|
||||
"comments": Array [],
|
||||
|
@ -338,6 +400,7 @@ describe('common utils', () => {
|
|||
"version": "WzUsMV0=",
|
||||
},
|
||||
Object {
|
||||
"assignees": Array [],
|
||||
"closed_at": "2019-11-25T22:32:17.947Z",
|
||||
"closed_by": Object {
|
||||
"email": "testemail@elastic.co",
|
||||
|
@ -407,6 +470,7 @@ describe('common utils', () => {
|
|||
|
||||
expect(res).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"assignees": Array [],
|
||||
"closed_at": null,
|
||||
"closed_by": null,
|
||||
"comments": Array [],
|
||||
|
@ -463,6 +527,7 @@ describe('common utils', () => {
|
|||
|
||||
expect(res).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"assignees": Array [],
|
||||
"closed_at": null,
|
||||
"closed_by": null,
|
||||
"comments": Array [],
|
||||
|
@ -520,6 +585,7 @@ describe('common utils', () => {
|
|||
|
||||
expect(res).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"assignees": Array [],
|
||||
"closed_at": null,
|
||||
"closed_by": null,
|
||||
"comments": Array [
|
||||
|
@ -600,6 +666,7 @@ describe('common utils', () => {
|
|||
|
||||
expect(res).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"assignees": Array [],
|
||||
"closed_at": null,
|
||||
"closed_by": null,
|
||||
"comments": Array [],
|
||||
|
|
|
@ -69,6 +69,7 @@ export const transformNewCase = ({
|
|||
status: CaseStatuses.open,
|
||||
updated_at: null,
|
||||
updated_by: null,
|
||||
assignees: newCase.assignees ?? [],
|
||||
});
|
||||
|
||||
export const transformCases = ({
|
||||
|
|
|
@ -52,6 +52,7 @@ export const mockCases: Array<SavedObject<CaseAttributes>> = [
|
|||
syncAlerts: true,
|
||||
},
|
||||
owner: SECURITY_SOLUTION_OWNER,
|
||||
assignees: [],
|
||||
},
|
||||
references: [],
|
||||
updated_at: '2019-11-25T21:54:48.952Z',
|
||||
|
@ -92,6 +93,7 @@ export const mockCases: Array<SavedObject<CaseAttributes>> = [
|
|||
syncAlerts: true,
|
||||
},
|
||||
owner: SECURITY_SOLUTION_OWNER,
|
||||
assignees: [],
|
||||
},
|
||||
references: [],
|
||||
updated_at: '2019-11-25T22:32:00.900Z',
|
||||
|
@ -132,6 +134,7 @@ export const mockCases: Array<SavedObject<CaseAttributes>> = [
|
|||
syncAlerts: true,
|
||||
},
|
||||
owner: SECURITY_SOLUTION_OWNER,
|
||||
assignees: [],
|
||||
},
|
||||
references: [],
|
||||
updated_at: '2019-11-25T22:32:17.947Z',
|
||||
|
@ -176,6 +179,7 @@ export const mockCases: Array<SavedObject<CaseAttributes>> = [
|
|||
syncAlerts: true,
|
||||
},
|
||||
owner: SECURITY_SOLUTION_OWNER,
|
||||
assignees: [],
|
||||
},
|
||||
references: [],
|
||||
updated_at: '2019-11-25T22:32:17.947Z',
|
||||
|
|
|
@ -27,6 +27,13 @@ export const createCaseSavedObjectType = (
|
|||
convertToMultiNamespaceTypeVersion: '8.0.0',
|
||||
mappings: {
|
||||
properties: {
|
||||
assignees: {
|
||||
properties: {
|
||||
uid: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
closed_at: {
|
||||
type: 'date',
|
||||
},
|
||||
|
|
|
@ -16,7 +16,13 @@ import {
|
|||
import { CASE_SAVED_OBJECT } from '../../../common/constants';
|
||||
import { getNoneCaseConnector } from '../../common/utils';
|
||||
import { createExternalService, ESCaseConnectorWithId } from '../../services/test_utils';
|
||||
import { addDuration, addSeverity, caseConnectorIdMigration, removeCaseType } from './cases';
|
||||
import {
|
||||
addAssignees,
|
||||
addDuration,
|
||||
addSeverity,
|
||||
caseConnectorIdMigration,
|
||||
removeCaseType,
|
||||
} from './cases';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const create_7_14_0_case = ({
|
||||
|
@ -538,4 +544,41 @@ describe('case migrations', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('addAssignees', () => {
|
||||
it('adds the assignees field correctly when none is present', () => {
|
||||
const doc = {
|
||||
id: '123',
|
||||
attributes: {},
|
||||
type: 'abc',
|
||||
references: [],
|
||||
} as unknown as SavedObjectSanitizedDoc<CaseAttributes>;
|
||||
expect(addAssignees(doc)).toEqual({
|
||||
...doc,
|
||||
attributes: {
|
||||
...doc.attributes,
|
||||
assignees: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the existing assignees value if the field already exists', () => {
|
||||
const assignees = [{ uid: '1' }];
|
||||
const doc = {
|
||||
id: '123',
|
||||
attributes: {
|
||||
assignees,
|
||||
},
|
||||
type: 'abc',
|
||||
references: [],
|
||||
} as unknown as SavedObjectSanitizedDoc<CaseAttributes>;
|
||||
expect(addAssignees(doc)).toEqual({
|
||||
...doc,
|
||||
attributes: {
|
||||
...doc.attributes,
|
||||
assignees,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -122,6 +122,13 @@ export const addSeverity = (
|
|||
return { ...doc, attributes: { ...doc.attributes, severity }, references: doc.references ?? [] };
|
||||
};
|
||||
|
||||
export const addAssignees = (
|
||||
doc: SavedObjectUnsanitizedDoc<CaseAttributes>
|
||||
): SavedObjectSanitizedDoc<CaseAttributes> => {
|
||||
const assignees = doc.attributes.assignees ?? [];
|
||||
return { ...doc, attributes: { ...doc.attributes, assignees }, references: doc.references ?? [] };
|
||||
};
|
||||
|
||||
export const caseMigrations = {
|
||||
'7.10.0': (
|
||||
doc: SavedObjectUnsanitizedDoc<UnsanitizedCaseConnector>
|
||||
|
@ -184,4 +191,5 @@ export const caseMigrations = {
|
|||
'7.15.0': caseConnectorIdMigration,
|
||||
'8.1.0': removeCaseType,
|
||||
'8.3.0': pipeMigrations(addDuration, addSeverity),
|
||||
'8.5.0': addAssignees,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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 { ActionTypes } from '../../../../common/api';
|
||||
import { CASE_USER_ACTION_SAVED_OBJECT } from '../../../../common/constants';
|
||||
import { addAssigneesToCreateUserAction } from './assignees';
|
||||
|
||||
describe('assignees migration', () => {
|
||||
const userAction = {
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
id: '1',
|
||||
attributes: {
|
||||
action_at: '2022-01-09T22:00:00.000Z',
|
||||
action_by: {
|
||||
email: 'elastic@elastic.co',
|
||||
full_name: 'Elastic User',
|
||||
username: 'elastic',
|
||||
},
|
||||
payload: {},
|
||||
type: ActionTypes.create_case,
|
||||
},
|
||||
};
|
||||
|
||||
it('adds the assignees field to the create_case user action', () => {
|
||||
// @ts-expect-error payload does not include the required fields
|
||||
const migratedUserAction = addAssigneesToCreateUserAction(userAction);
|
||||
expect(migratedUserAction).toEqual({
|
||||
attributes: {
|
||||
action_at: '2022-01-09T22:00:00.000Z',
|
||||
action_by: {
|
||||
email: 'elastic@elastic.co',
|
||||
full_name: 'Elastic User',
|
||||
username: 'elastic',
|
||||
},
|
||||
payload: {
|
||||
assignees: [],
|
||||
},
|
||||
type: 'create_case',
|
||||
},
|
||||
id: '1',
|
||||
references: [],
|
||||
type: 'cases-user-actions',
|
||||
});
|
||||
});
|
||||
|
||||
it('does NOT add the assignees field non-create_case user actions', () => {
|
||||
Object.keys(ActionTypes)
|
||||
.filter((type) => type !== ActionTypes.create_case)
|
||||
.forEach((type) => {
|
||||
const migratedUserAction = addAssigneesToCreateUserAction({
|
||||
...userAction,
|
||||
// @ts-expect-error override the type, it is only expecting create_case
|
||||
attributes: { ...userAction.attributes, type },
|
||||
});
|
||||
|
||||
expect(migratedUserAction).toEqual({
|
||||
attributes: {
|
||||
action_at: '2022-01-09T22:00:00.000Z',
|
||||
action_by: {
|
||||
email: 'elastic@elastic.co',
|
||||
full_name: 'Elastic User',
|
||||
username: 'elastic',
|
||||
},
|
||||
payload: {},
|
||||
type,
|
||||
},
|
||||
id: '1',
|
||||
references: [],
|
||||
type: 'cases-user-actions',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 { SavedObjectUnsanitizedDoc, SavedObjectSanitizedDoc } from '@kbn/core/server';
|
||||
import { ActionTypes, CreateCaseUserAction } from '../../../../common/api';
|
||||
|
||||
export const addAssigneesToCreateUserAction = (
|
||||
doc: SavedObjectUnsanitizedDoc<CreateCaseUserAction>
|
||||
): SavedObjectSanitizedDoc<CreateCaseUserAction> => {
|
||||
if (doc.attributes.type !== ActionTypes.create_case) {
|
||||
return { ...doc, references: doc.references ?? [] };
|
||||
}
|
||||
|
||||
const payload = {
|
||||
...doc.attributes.payload,
|
||||
assignees: doc?.attributes?.payload?.assignees ?? [],
|
||||
};
|
||||
return { ...doc, attributes: { ...doc.attributes, payload }, references: doc.references ?? [] };
|
||||
};
|
|
@ -32,6 +32,7 @@ import { payloadMigration } from './payload';
|
|||
import { addSeverityToCreateUserAction } from './severity';
|
||||
import { UserActions } from './types';
|
||||
import { getAllPersistableAttachmentMigrations } from '../get_all_persistable_attachment_migrations';
|
||||
import { addAssigneesToCreateUserAction } from './assignees';
|
||||
|
||||
export interface UserActionsMigrationsDeps {
|
||||
persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry;
|
||||
|
@ -98,6 +99,7 @@ export const createUserActionsMigrations = (
|
|||
'8.0.0': removeRuleInformation,
|
||||
'8.1.0': payloadMigration,
|
||||
'8.3.0': addSeverityToCreateUserAction,
|
||||
'8.5.0': addAssigneesToCreateUserAction,
|
||||
};
|
||||
|
||||
return mergeSavedObjectMigrationMaps(persistableStateAttachmentMigrations, userActionsMigrations);
|
||||
|
|
|
@ -157,6 +157,7 @@ describe('CasesService', () => {
|
|||
} = unsecuredSavedObjectsClient.update.mock.calls[0][2] as Partial<ESCaseAttributes>;
|
||||
expect(restUpdateAttributes).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"assignees": Array [],
|
||||
"closed_at": null,
|
||||
"closed_by": null,
|
||||
"created_at": "2019-11-25T21:54:48.952Z",
|
||||
|
@ -481,6 +482,7 @@ describe('CasesService', () => {
|
|||
expect(creationAttributes.external_service).not.toHaveProperty('connector_id');
|
||||
expect(creationAttributes).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"assignees": Array [],
|
||||
"closed_at": null,
|
||||
"closed_by": null,
|
||||
"connector": Object {
|
||||
|
|
|
@ -126,14 +126,17 @@ export const basicCaseFields: CaseAttributes = {
|
|||
syncAlerts: true,
|
||||
},
|
||||
owner: SECURITY_SOLUTION_OWNER,
|
||||
assignees: [],
|
||||
};
|
||||
|
||||
export const createCaseSavedObjectResponse = ({
|
||||
connector,
|
||||
externalService,
|
||||
overrides,
|
||||
}: {
|
||||
connector?: ESCaseConnectorWithId;
|
||||
externalService?: CaseFullExternalService;
|
||||
overrides?: Partial<CaseAttributes>;
|
||||
} = {}): SavedObject<ESCaseAttributes> => {
|
||||
const references: SavedObjectReference[] = createSavedObjectReferences({
|
||||
connector,
|
||||
|
@ -168,6 +171,7 @@ export const createCaseSavedObjectResponse = ({
|
|||
id: '1',
|
||||
attributes: {
|
||||
...basicCaseFields,
|
||||
...overrides,
|
||||
// if connector is null we'll default this to an incomplete jira value because the service
|
||||
// should switch it to a none connector when the id can't be found in the references array
|
||||
connector: formattedConnector,
|
||||
|
|
|
@ -546,6 +546,47 @@ describe('UserActionBuilder', () => {
|
|||
`);
|
||||
});
|
||||
|
||||
it('builds an assign user action correctly', () => {
|
||||
const builder = builderFactory.getBuilder(ActionTypes.assignees)!;
|
||||
const userAction = builder.build({
|
||||
payload: { assignees: [{ uid: '1' }, { uid: '2' }] },
|
||||
...commonArgs,
|
||||
});
|
||||
|
||||
expect(userAction).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"action": "add",
|
||||
"created_at": "2022-01-09T22:00:00.000Z",
|
||||
"created_by": Object {
|
||||
"email": "elastic@elastic.co",
|
||||
"full_name": "Elastic User",
|
||||
"username": "elastic",
|
||||
},
|
||||
"owner": "securitySolution",
|
||||
"payload": Object {
|
||||
"assignees": Array [
|
||||
Object {
|
||||
"uid": "1",
|
||||
},
|
||||
Object {
|
||||
"uid": "2",
|
||||
},
|
||||
],
|
||||
},
|
||||
"type": "assignees",
|
||||
},
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "123",
|
||||
"name": "associated-cases",
|
||||
"type": "cases",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('builds a settings user action correctly', () => {
|
||||
const builder = builderFactory.getBuilder(ActionTypes.settings)!;
|
||||
const userAction = builder.build({
|
||||
|
@ -601,6 +642,11 @@ describe('UserActionBuilder', () => {
|
|||
},
|
||||
"owner": "securitySolution",
|
||||
"payload": Object {
|
||||
"assignees": Array [
|
||||
Object {
|
||||
"uid": "1",
|
||||
},
|
||||
],
|
||||
"connector": Object {
|
||||
"fields": Object {
|
||||
"category": "Denial of Service",
|
||||
|
|
|
@ -20,8 +20,10 @@ import { UserActionBuilder } from './abstract_builder';
|
|||
import { SeverityUserActionBuilder } from './builders/severity';
|
||||
import { PersistableStateAttachmentTypeRegistry } from '../../attachment_framework/persistable_state_registry';
|
||||
import { BuilderDeps } from './types';
|
||||
import { AssigneesUserActionBuilder } from './builders/assignees';
|
||||
|
||||
const builderMap = {
|
||||
assignees: AssigneesUserActionBuilder,
|
||||
title: TitleUserActionBuilder,
|
||||
create_case: CreateCaseUserActionBuilder,
|
||||
connector: ConnectorUserActionBuilder,
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { ActionTypes, Actions } from '../../../../common/api';
|
||||
import { UserActionBuilder } from '../abstract_builder';
|
||||
import { UserActionParameters, BuilderReturnValue } from '../types';
|
||||
|
||||
export class AssigneesUserActionBuilder extends UserActionBuilder {
|
||||
build(args: UserActionParameters<'assignees'>): BuilderReturnValue {
|
||||
return this.buildCommonUserAction({
|
||||
...args,
|
||||
action: args.action ?? Actions.add,
|
||||
valueKey: 'assignees',
|
||||
value: args.payload.assignees,
|
||||
type: ActionTypes.assignees,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -13,11 +13,13 @@ import {
|
|||
SavedObjectReference,
|
||||
SavedObjectsFindResponse,
|
||||
SavedObjectsFindResult,
|
||||
SavedObjectsUpdateResponse,
|
||||
} from '@kbn/core/server';
|
||||
import { ACTION_SAVED_OBJECT_TYPE } from '@kbn/actions-plugin/server';
|
||||
import {
|
||||
Actions,
|
||||
ActionTypes,
|
||||
CaseAttributes,
|
||||
CaseSeverity,
|
||||
CaseStatuses,
|
||||
CaseUserActionAttributes,
|
||||
|
@ -39,6 +41,7 @@ import {
|
|||
} from '../../common/constants';
|
||||
|
||||
import {
|
||||
createCaseSavedObjectResponse,
|
||||
createConnectorObject,
|
||||
createExternalService,
|
||||
createJiraConnector,
|
||||
|
@ -51,6 +54,9 @@ import {
|
|||
updatedCases,
|
||||
comment,
|
||||
attachments,
|
||||
updatedAssigneesCases,
|
||||
originalCasesWithAssignee,
|
||||
updatedTagsCases,
|
||||
} from './mocks';
|
||||
import { CaseUserActionService, transformFindResponseToExternalModel } from '.';
|
||||
import { PersistableStateAttachmentTypeRegistry } from '../../attachment_framework/persistable_state_registry';
|
||||
|
@ -667,6 +673,7 @@ describe('CaseUserActionService', () => {
|
|||
type: 'create_case',
|
||||
owner: 'securitySolution',
|
||||
payload: {
|
||||
assignees: [{ uid: '1' }],
|
||||
connector: {
|
||||
fields: {
|
||||
category: 'Denial of Service',
|
||||
|
@ -1068,6 +1075,267 @@ describe('CaseUserActionService', () => {
|
|||
{ refresh: undefined }
|
||||
);
|
||||
});
|
||||
|
||||
it('creates the correct user actions when an assignee is added', async () => {
|
||||
await service.bulkCreateUpdateCase({
|
||||
...commonArgs,
|
||||
originalCases,
|
||||
updatedCases: updatedAssigneesCases,
|
||||
user: commonArgs.user,
|
||||
});
|
||||
|
||||
expect(unsecuredSavedObjectsClient.bulkCreate.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"action": "add",
|
||||
"created_at": "2022-01-09T22:00:00.000Z",
|
||||
"created_by": Object {
|
||||
"email": "elastic@elastic.co",
|
||||
"full_name": "Elastic User",
|
||||
"username": "elastic",
|
||||
},
|
||||
"owner": "securitySolution",
|
||||
"payload": Object {
|
||||
"assignees": Array [
|
||||
Object {
|
||||
"uid": "1",
|
||||
},
|
||||
],
|
||||
},
|
||||
"type": "assignees",
|
||||
},
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "associated-cases",
|
||||
"type": "cases",
|
||||
},
|
||||
],
|
||||
"type": "cases-user-actions",
|
||||
},
|
||||
],
|
||||
Object {
|
||||
"refresh": undefined,
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('creates the correct user actions when an assignee is removed', async () => {
|
||||
const casesWithAssigneeRemoved: Array<SavedObjectsUpdateResponse<CaseAttributes>> = [
|
||||
{
|
||||
...createCaseSavedObjectResponse(),
|
||||
id: '1',
|
||||
attributes: {
|
||||
assignees: [],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
await service.bulkCreateUpdateCase({
|
||||
...commonArgs,
|
||||
originalCases: originalCasesWithAssignee,
|
||||
updatedCases: casesWithAssigneeRemoved,
|
||||
user: commonArgs.user,
|
||||
});
|
||||
|
||||
expect(unsecuredSavedObjectsClient.bulkCreate.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"action": "delete",
|
||||
"created_at": "2022-01-09T22:00:00.000Z",
|
||||
"created_by": Object {
|
||||
"email": "elastic@elastic.co",
|
||||
"full_name": "Elastic User",
|
||||
"username": "elastic",
|
||||
},
|
||||
"owner": "securitySolution",
|
||||
"payload": Object {
|
||||
"assignees": Array [
|
||||
Object {
|
||||
"uid": "1",
|
||||
},
|
||||
],
|
||||
},
|
||||
"type": "assignees",
|
||||
},
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "associated-cases",
|
||||
"type": "cases",
|
||||
},
|
||||
],
|
||||
"type": "cases-user-actions",
|
||||
},
|
||||
],
|
||||
Object {
|
||||
"refresh": undefined,
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('creates the correct user actions when assignees are added and removed', async () => {
|
||||
const caseAssignees: Array<SavedObjectsUpdateResponse<CaseAttributes>> = [
|
||||
{
|
||||
...createCaseSavedObjectResponse(),
|
||||
id: '1',
|
||||
attributes: {
|
||||
assignees: [{ uid: '2' }],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
await service.bulkCreateUpdateCase({
|
||||
...commonArgs,
|
||||
originalCases: originalCasesWithAssignee,
|
||||
updatedCases: caseAssignees,
|
||||
user: commonArgs.user,
|
||||
});
|
||||
|
||||
expect(unsecuredSavedObjectsClient.bulkCreate.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"action": "add",
|
||||
"created_at": "2022-01-09T22:00:00.000Z",
|
||||
"created_by": Object {
|
||||
"email": "elastic@elastic.co",
|
||||
"full_name": "Elastic User",
|
||||
"username": "elastic",
|
||||
},
|
||||
"owner": "securitySolution",
|
||||
"payload": Object {
|
||||
"assignees": Array [
|
||||
Object {
|
||||
"uid": "2",
|
||||
},
|
||||
],
|
||||
},
|
||||
"type": "assignees",
|
||||
},
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "associated-cases",
|
||||
"type": "cases",
|
||||
},
|
||||
],
|
||||
"type": "cases-user-actions",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"action": "delete",
|
||||
"created_at": "2022-01-09T22:00:00.000Z",
|
||||
"created_by": Object {
|
||||
"email": "elastic@elastic.co",
|
||||
"full_name": "Elastic User",
|
||||
"username": "elastic",
|
||||
},
|
||||
"owner": "securitySolution",
|
||||
"payload": Object {
|
||||
"assignees": Array [
|
||||
Object {
|
||||
"uid": "1",
|
||||
},
|
||||
],
|
||||
},
|
||||
"type": "assignees",
|
||||
},
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "associated-cases",
|
||||
"type": "cases",
|
||||
},
|
||||
],
|
||||
"type": "cases-user-actions",
|
||||
},
|
||||
],
|
||||
Object {
|
||||
"refresh": undefined,
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('creates the correct user actions when tags are added and removed', async () => {
|
||||
await service.bulkCreateUpdateCase({
|
||||
...commonArgs,
|
||||
originalCases,
|
||||
updatedCases: updatedTagsCases,
|
||||
user: commonArgs.user,
|
||||
});
|
||||
|
||||
expect(unsecuredSavedObjectsClient.bulkCreate.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"action": "add",
|
||||
"created_at": "2022-01-09T22:00:00.000Z",
|
||||
"created_by": Object {
|
||||
"email": "elastic@elastic.co",
|
||||
"full_name": "Elastic User",
|
||||
"username": "elastic",
|
||||
},
|
||||
"owner": "securitySolution",
|
||||
"payload": Object {
|
||||
"tags": Array [
|
||||
"a",
|
||||
"b",
|
||||
],
|
||||
},
|
||||
"type": "tags",
|
||||
},
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "associated-cases",
|
||||
"type": "cases",
|
||||
},
|
||||
],
|
||||
"type": "cases-user-actions",
|
||||
},
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"action": "delete",
|
||||
"created_at": "2022-01-09T22:00:00.000Z",
|
||||
"created_by": Object {
|
||||
"email": "elastic@elastic.co",
|
||||
"full_name": "Elastic User",
|
||||
"username": "elastic",
|
||||
},
|
||||
"owner": "securitySolution",
|
||||
"payload": Object {
|
||||
"tags": Array [
|
||||
"defacement",
|
||||
],
|
||||
},
|
||||
"type": "tags",
|
||||
},
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "associated-cases",
|
||||
"type": "cases",
|
||||
},
|
||||
],
|
||||
"type": "cases-user-actions",
|
||||
},
|
||||
],
|
||||
Object {
|
||||
"refresh": undefined,
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulkCreateAttachmentDeletion', () => {
|
||||
|
|
|
@ -27,12 +27,16 @@ import {
|
|||
isCommentUserAction,
|
||||
} from '../../../common/utils/user_actions';
|
||||
import {
|
||||
ActionOperationValues,
|
||||
Actions,
|
||||
ActionTypes,
|
||||
ActionTypeValues,
|
||||
CaseAttributes,
|
||||
CaseUserActionAttributes,
|
||||
CaseUserActionAttributesWithoutConnectorId,
|
||||
CaseUserActionResponse,
|
||||
CaseUserProfile,
|
||||
CaseAssignees,
|
||||
CommentRequest,
|
||||
NONE_CONNECTOR_ID,
|
||||
User,
|
||||
|
@ -53,13 +57,19 @@ import {
|
|||
} from '../../common/constants';
|
||||
import { findConnectorIdReference } from '../transform';
|
||||
import { buildFilter, combineFilters, arraysDifference } from '../../client/utils';
|
||||
import { BuilderParameters, BuilderReturnValue, CommonArguments, CreateUserAction } from './types';
|
||||
import {
|
||||
BuilderParameters,
|
||||
BuilderReturnValue,
|
||||
CommonArguments,
|
||||
CreateUserAction,
|
||||
UserActionParameters,
|
||||
} from './types';
|
||||
import { BuilderFactory } from './builder_factory';
|
||||
import { defaultSortField, isCommentRequestTypeExternalReferenceSO } from '../../common/utils';
|
||||
import { PersistableStateAttachmentTypeRegistry } from '../../attachment_framework/persistable_state_registry';
|
||||
import { injectPersistableReferencesToSO } from '../../attachment_framework/so_references';
|
||||
import { IndexRefresh } from '../types';
|
||||
import { isStringArray } from './type_guards';
|
||||
import { isAssigneesArray, isStringArray } from './type_guards';
|
||||
|
||||
interface GetCaseUserActionArgs extends ClientArgs {
|
||||
caseId: string;
|
||||
|
@ -92,6 +102,11 @@ interface GetUserActionItemByDifference extends CommonUserActionArgs {
|
|||
newValue: unknown;
|
||||
}
|
||||
|
||||
interface TypedUserActionDiffedItems<T> extends GetUserActionItemByDifference {
|
||||
originalValue: T[];
|
||||
newValue: T[];
|
||||
}
|
||||
|
||||
interface BulkCreateBulkUpdateCaseUserActions extends ClientArgs, IndexRefresh {
|
||||
originalCases: Array<SavedObject<CaseAttributes>>;
|
||||
updatedCases: Array<SavedObjectsUpdateResponse<CaseAttributes>>;
|
||||
|
@ -106,6 +121,10 @@ type CreateUserActionClient<T extends keyof BuilderParameters> = CreateUserActio
|
|||
CommonUserActionArgs &
|
||||
IndexRefresh;
|
||||
|
||||
type CreatePayloadFunction<Item, ActionType extends ActionTypeValues> = (
|
||||
items: Item[]
|
||||
) => UserActionParameters<ActionType>['payload'];
|
||||
|
||||
export class CaseUserActionService {
|
||||
private static readonly userActionFieldsAllowed: Set<string> = new Set(Object.keys(ActionTypes));
|
||||
|
||||
|
@ -120,55 +139,26 @@ export class CaseUserActionService {
|
|||
});
|
||||
}
|
||||
|
||||
private getUserActionItemByDifference({
|
||||
field,
|
||||
originalValue,
|
||||
newValue,
|
||||
caseId,
|
||||
owner,
|
||||
user,
|
||||
}: GetUserActionItemByDifference): BuilderReturnValue[] {
|
||||
private getUserActionItemByDifference(
|
||||
params: GetUserActionItemByDifference
|
||||
): BuilderReturnValue[] {
|
||||
const { field, originalValue, newValue, caseId, owner, user } = params;
|
||||
|
||||
if (!CaseUserActionService.userActionFieldsAllowed.has(field)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (field === ActionTypes.tags && isStringArray(originalValue) && isStringArray(newValue)) {
|
||||
const tagsUserActionBuilder = this.builderFactory.getBuilder(ActionTypes.tags);
|
||||
const compareValues = arraysDifference(originalValue, newValue);
|
||||
const userActions = [];
|
||||
|
||||
if (compareValues && compareValues.addedItems.length > 0) {
|
||||
const tagAddUserAction = tagsUserActionBuilder?.build({
|
||||
action: Actions.add,
|
||||
caseId,
|
||||
user,
|
||||
owner,
|
||||
payload: { tags: compareValues.addedItems },
|
||||
});
|
||||
|
||||
if (tagAddUserAction) {
|
||||
userActions.push(tagAddUserAction);
|
||||
}
|
||||
}
|
||||
|
||||
if (compareValues && compareValues.deletedItems.length > 0) {
|
||||
const tagsDeleteUserAction = tagsUserActionBuilder?.build({
|
||||
action: Actions.delete,
|
||||
caseId,
|
||||
user,
|
||||
owner,
|
||||
payload: { tags: compareValues.deletedItems },
|
||||
});
|
||||
|
||||
if (tagsDeleteUserAction) {
|
||||
userActions.push(tagsDeleteUserAction);
|
||||
}
|
||||
}
|
||||
|
||||
return userActions;
|
||||
}
|
||||
|
||||
if (isUserActionType(field) && newValue != null) {
|
||||
} else if (
|
||||
field === ActionTypes.assignees &&
|
||||
isAssigneesArray(originalValue) &&
|
||||
isAssigneesArray(newValue)
|
||||
) {
|
||||
return this.buildAssigneesUserActions({ ...params, originalValue, newValue });
|
||||
} else if (
|
||||
field === ActionTypes.tags &&
|
||||
isStringArray(originalValue) &&
|
||||
isStringArray(newValue)
|
||||
) {
|
||||
return this.buildTagsUserActions({ ...params, originalValue, newValue });
|
||||
} else if (isUserActionType(field) && newValue != null) {
|
||||
const userActionBuilder = this.builderFactory.getBuilder(ActionTypes[field]);
|
||||
const fieldUserAction = userActionBuilder?.build({
|
||||
caseId,
|
||||
|
@ -183,6 +173,85 @@ export class CaseUserActionService {
|
|||
return [];
|
||||
}
|
||||
|
||||
private buildAssigneesUserActions(params: TypedUserActionDiffedItems<CaseUserProfile>) {
|
||||
const createPayload: CreatePayloadFunction<CaseUserProfile, typeof ActionTypes.assignees> = (
|
||||
items: CaseAssignees
|
||||
) => ({ assignees: items });
|
||||
|
||||
return this.buildAddDeleteUserActions(params, createPayload, ActionTypes.assignees);
|
||||
}
|
||||
|
||||
private buildTagsUserActions(params: TypedUserActionDiffedItems<string>) {
|
||||
const createPayload: CreatePayloadFunction<string, typeof ActionTypes.tags> = (
|
||||
items: string[]
|
||||
) => ({
|
||||
tags: items,
|
||||
});
|
||||
|
||||
return this.buildAddDeleteUserActions(params, createPayload, ActionTypes.tags);
|
||||
}
|
||||
|
||||
private buildAddDeleteUserActions<Item, ActionType extends ActionTypeValues>(
|
||||
params: TypedUserActionDiffedItems<Item>,
|
||||
createPayload: CreatePayloadFunction<Item, ActionType>,
|
||||
actionType: ActionType
|
||||
) {
|
||||
const { originalValue, newValue } = params;
|
||||
const compareValues = arraysDifference(originalValue, newValue);
|
||||
|
||||
const addUserAction = this.buildUserAction({
|
||||
commonArgs: params,
|
||||
actionType,
|
||||
action: Actions.add,
|
||||
createPayload,
|
||||
modifiedItems: compareValues?.addedItems,
|
||||
});
|
||||
const deleteUserAction = this.buildUserAction({
|
||||
commonArgs: params,
|
||||
actionType,
|
||||
action: Actions.delete,
|
||||
createPayload,
|
||||
modifiedItems: compareValues?.deletedItems,
|
||||
});
|
||||
|
||||
return [
|
||||
...(addUserAction ? [addUserAction] : []),
|
||||
...(deleteUserAction ? [deleteUserAction] : []),
|
||||
];
|
||||
}
|
||||
|
||||
private buildUserAction<Item, ActionType extends ActionTypeValues>({
|
||||
commonArgs,
|
||||
actionType,
|
||||
action,
|
||||
createPayload,
|
||||
modifiedItems,
|
||||
}: {
|
||||
commonArgs: CommonUserActionArgs;
|
||||
actionType: ActionType;
|
||||
action: ActionOperationValues;
|
||||
createPayload: CreatePayloadFunction<Item, ActionType>;
|
||||
modifiedItems?: Item[] | null;
|
||||
}) {
|
||||
const userActionBuilder = this.builderFactory.getBuilder(actionType);
|
||||
|
||||
if (!userActionBuilder || !modifiedItems || modifiedItems.length <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { caseId, owner, user } = commonArgs;
|
||||
|
||||
const userAction = userActionBuilder.build({
|
||||
action,
|
||||
caseId,
|
||||
user,
|
||||
owner,
|
||||
payload: createPayload(modifiedItems),
|
||||
});
|
||||
|
||||
return userAction;
|
||||
}
|
||||
|
||||
public async bulkCreateCaseDeletion({
|
||||
unsecuredSavedObjectsClient,
|
||||
cases,
|
||||
|
|
|
@ -7,11 +7,17 @@
|
|||
|
||||
import { CASE_SAVED_OBJECT } from '../../../common/constants';
|
||||
import { SECURITY_SOLUTION_OWNER } from '../../../common';
|
||||
import { CaseSeverity, CaseStatuses, CommentType, ConnectorTypes } from '../../../common/api';
|
||||
import {
|
||||
CasePostRequest,
|
||||
CaseSeverity,
|
||||
CaseStatuses,
|
||||
CommentType,
|
||||
ConnectorTypes,
|
||||
} from '../../../common/api';
|
||||
import { createCaseSavedObjectResponse } from '../test_utils';
|
||||
import { transformSavedObjectToExternalModel } from '../cases/transform';
|
||||
|
||||
export const casePayload = {
|
||||
export const casePayload: CasePostRequest = {
|
||||
title: 'Case SIR',
|
||||
tags: ['sir'],
|
||||
description: 'testing sir',
|
||||
|
@ -32,6 +38,7 @@ export const casePayload = {
|
|||
settings: { syncAlerts: true },
|
||||
severity: CaseSeverity.LOW,
|
||||
owner: SECURITY_SOLUTION_OWNER,
|
||||
assignees: [{ uid: '1' }],
|
||||
};
|
||||
|
||||
export const externalService = {
|
||||
|
@ -76,6 +83,30 @@ export const updatedCases = [
|
|||
},
|
||||
];
|
||||
|
||||
export const originalCasesWithAssignee = [
|
||||
{ ...createCaseSavedObjectResponse({ overrides: { assignees: [{ uid: '1' }] } }), id: '1' },
|
||||
].map((so) => transformSavedObjectToExternalModel(so));
|
||||
|
||||
export const updatedAssigneesCases = [
|
||||
{
|
||||
...createCaseSavedObjectResponse(),
|
||||
id: '1',
|
||||
attributes: {
|
||||
assignees: [{ uid: '1' }],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const updatedTagsCases = [
|
||||
{
|
||||
...createCaseSavedObjectResponse(),
|
||||
id: '1',
|
||||
attributes: {
|
||||
tags: ['a', 'b'],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const comment = {
|
||||
comment: 'a comment',
|
||||
type: CommentType.user as const,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isObjectArray, isStringArray } from './type_guards';
|
||||
import { isAssigneesArray, isStringArray } from './type_guards';
|
||||
|
||||
describe('type_guards', () => {
|
||||
describe('isStringArray', () => {
|
||||
|
@ -30,25 +30,33 @@ describe('type_guards', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('isObjectArray', () => {
|
||||
describe('isAssigneesArray', () => {
|
||||
it('returns true when the value is an empty array', () => {
|
||||
expect(isObjectArray([])).toBeTruthy();
|
||||
expect(isAssigneesArray([])).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns true when the value is an array of a single string', () => {
|
||||
expect(isObjectArray([{ a: '1' }])).toBeTruthy();
|
||||
it('returns false when the value is not an array of assignees', () => {
|
||||
expect(isAssigneesArray([{ a: '1' }])).toBeFalsy();
|
||||
});
|
||||
|
||||
it('returns true when the value is an array of multiple strings', () => {
|
||||
expect(isObjectArray([{ a: 'a' }, { b: 'b' }])).toBeTruthy();
|
||||
it('returns false when the value is an array of assignees and non assignee objects', () => {
|
||||
expect(isAssigneesArray([{ uid: '1' }, { hi: '2' }])).toBeFalsy();
|
||||
});
|
||||
|
||||
it('returns false when the value is an array of strings and numbers', () => {
|
||||
expect(isObjectArray([{ a: 'a' }, 1])).toBeFalsy();
|
||||
it('returns true when the value is an array of a single assignee', () => {
|
||||
expect(isAssigneesArray([{ uid: '1' }])).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns true when the value is an array of multiple assignees', () => {
|
||||
expect(isAssigneesArray([{ uid: 'a' }, { uid: 'b' }])).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns false when the value is an array of assignees and numbers', () => {
|
||||
expect(isAssigneesArray([{ uid: 'a' }, 1])).toBeFalsy();
|
||||
});
|
||||
|
||||
it('returns false when the value is an array of strings and objects', () => {
|
||||
expect(isObjectArray(['a', {}])).toBeFalsy();
|
||||
expect(isAssigneesArray(['a', {}])).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,12 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isPlainObject, isString } from 'lodash';
|
||||
import { isString } from 'lodash';
|
||||
import { CaseAssignees, CaseAssigneesRt } from '../../../common/api/cases/assignee';
|
||||
|
||||
export const isStringArray = (value: unknown): value is string[] => {
|
||||
return Array.isArray(value) && value.every((val) => isString(val));
|
||||
};
|
||||
|
||||
export const isObjectArray = (value: unknown): value is Array<Record<string, unknown>> => {
|
||||
return Array.isArray(value) && value.every((val) => isPlainObject(val));
|
||||
export const isAssigneesArray = (value: unknown): value is CaseAssignees => {
|
||||
return CaseAssigneesRt.is(value);
|
||||
};
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { SavedObjectReference } from '@kbn/core/server';
|
||||
import { CaseAssignees } from '../../../common/api/cases/assignee';
|
||||
import {
|
||||
CasePostRequest,
|
||||
CaseSettings,
|
||||
|
@ -36,6 +37,9 @@ export interface BuilderParameters {
|
|||
tags: {
|
||||
parameters: { payload: { tags: string[] } };
|
||||
};
|
||||
assignees: {
|
||||
parameters: { payload: { assignees: CaseAssignees } };
|
||||
};
|
||||
pushed: {
|
||||
parameters: {
|
||||
payload: {
|
||||
|
|
|
@ -23,7 +23,14 @@ export interface FixtureStartDeps {
|
|||
export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, FixtureStartDeps> {
|
||||
public setup(core: CoreSetup<FixtureStartDeps>, deps: FixtureSetupDeps) {
|
||||
const { features } = deps;
|
||||
this.registerFeatures(features);
|
||||
}
|
||||
|
||||
public start() {}
|
||||
|
||||
public stop() {}
|
||||
|
||||
private registerFeatures(features: FeaturesPluginSetup) {
|
||||
features.registerKibanaFeature({
|
||||
id: 'securitySolutionFixture',
|
||||
name: 'SecuritySolutionFixture',
|
||||
|
@ -118,6 +125,4 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
|
|||
},
|
||||
});
|
||||
}
|
||||
public start() {}
|
||||
public stop() {}
|
||||
}
|
||||
|
|
|
@ -7,9 +7,10 @@
|
|||
|
||||
import { FtrProviderContext as CommonFtrProviderContext } from '../../ftr_provider_context';
|
||||
import { Role, User, UserInfo } from './types';
|
||||
import { users } from './users';
|
||||
import { obsOnly, secOnly, secOnlyNoDelete, secOnlyRead, users } from './users';
|
||||
import { roles } from './roles';
|
||||
import { spaces } from './spaces';
|
||||
import { loginUsers } from '../utils';
|
||||
|
||||
export const getUserInfo = (user: User): UserInfo => ({
|
||||
username: user.username,
|
||||
|
@ -103,3 +104,12 @@ export const deleteSpacesAndUsers = async (getService: CommonFtrProviderContext[
|
|||
await deleteSpaces(getService);
|
||||
await deleteUsersAndRoles(getService);
|
||||
};
|
||||
|
||||
export const activateUserProfiles = async (getService: CommonFtrProviderContext['getService']) => {
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
|
||||
await loginUsers({
|
||||
supertest: supertestWithoutAuth,
|
||||
users: [secOnly, secOnlyNoDelete, secOnlyRead, obsOnly],
|
||||
});
|
||||
};
|
||||
|
|
|
@ -45,6 +45,7 @@ export const postCaseReq: CasePostRequest = {
|
|||
syncAlerts: true,
|
||||
},
|
||||
owner: 'securitySolutionFixture',
|
||||
assignees: [],
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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 type SuperTest from 'supertest';
|
||||
|
||||
import { UserProfileBulkGetParams, UserProfileServiceStart } from '@kbn/security-plugin/server';
|
||||
import { superUser } from './authentication/users';
|
||||
import { User } from './authentication/types';
|
||||
import { getSpaceUrlPrefix } from './utils';
|
||||
|
||||
type BulkGetUserProfilesParams = Omit<UserProfileBulkGetParams, 'uids'> & { uids: string[] };
|
||||
|
||||
export const bulkGetUserProfiles = async ({
|
||||
supertest,
|
||||
req,
|
||||
expectedHttpCode = 200,
|
||||
auth = { user: superUser, space: null },
|
||||
}: {
|
||||
supertest: SuperTest.SuperTest<SuperTest.Test>;
|
||||
req: BulkGetUserProfilesParams;
|
||||
expectedHttpCode?: number;
|
||||
auth?: { user: User; space: string | null };
|
||||
}): ReturnType<UserProfileServiceStart['bulkGet']> => {
|
||||
const { uids, ...restParams } = req;
|
||||
const uniqueIDs = [...new Set(uids)];
|
||||
|
||||
const { body: profiles } = await supertest
|
||||
.post(`${getSpaceUrlPrefix(auth.space)}/internal/security/user_profile/_bulk_get`)
|
||||
.auth(auth.user.username, auth.user.password)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({ uids: uniqueIDs, ...restParams })
|
||||
.expect(expectedHttpCode);
|
||||
|
||||
return profiles;
|
||||
};
|
|
@ -6,13 +6,20 @@
|
|||
*/
|
||||
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
import { createSpacesAndUsers, deleteSpacesAndUsers } from '../../../common/lib/authentication';
|
||||
import {
|
||||
createSpacesAndUsers,
|
||||
deleteSpacesAndUsers,
|
||||
activateUserProfiles,
|
||||
} from '../../../common/lib/authentication';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ loadTestFile, getService }: FtrProviderContext): void => {
|
||||
describe('cases security and spaces enabled: basic', function () {
|
||||
before(async () => {
|
||||
await createSpacesAndUsers(getService);
|
||||
// once a user profile is created the only way to remove it is to delete the user and roles, so best to activate
|
||||
// before all the tests
|
||||
await activateUserProfiles(getService);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
|
|
|
@ -0,0 +1,252 @@
|
|||
/*
|
||||
* 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 { findCasesResp, getPostCaseRequest, postCaseReq } from '../../../../common/lib/mock';
|
||||
import {
|
||||
createCase,
|
||||
suggestUserProfiles,
|
||||
getCase,
|
||||
findCases,
|
||||
updateCase,
|
||||
deleteAllCaseItems,
|
||||
} from '../../../../common/lib/utils';
|
||||
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
import { bulkGetUserProfiles } from '../../../../common/lib/user_profiles';
|
||||
import { superUser } from '../../../../common/lib/authentication/users';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext): void => {
|
||||
const es = getService('es');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const supertest = getService('supertest');
|
||||
|
||||
describe('assignees', () => {
|
||||
afterEach(async () => {
|
||||
await deleteAllCaseItems(es);
|
||||
});
|
||||
|
||||
it('allows the assignees field to be an empty array', async () => {
|
||||
const postedCase = await createCase(supertest, getPostCaseRequest());
|
||||
|
||||
expect(postedCase.assignees).to.eql([]);
|
||||
});
|
||||
|
||||
it('allows creating a case without the assignees field in the request', async () => {
|
||||
const postReq = getPostCaseRequest();
|
||||
const { assignees, ...restRequest } = postReq;
|
||||
|
||||
const postedCase = await createCase(supertest, restRequest);
|
||||
|
||||
expect(postedCase.assignees).to.eql([]);
|
||||
});
|
||||
|
||||
it('assigns a user to a case and retrieves the users profile', async () => {
|
||||
const profile = await suggestUserProfiles({
|
||||
supertest: supertestWithoutAuth,
|
||||
req: {
|
||||
name: 'delete',
|
||||
owners: ['securitySolutionFixture'],
|
||||
size: 1,
|
||||
},
|
||||
auth: { user: superUser, space: 'space1' },
|
||||
});
|
||||
|
||||
const postedCase = await createCase(
|
||||
supertest,
|
||||
getPostCaseRequest({
|
||||
assignees: [{ uid: profile[0].uid }],
|
||||
})
|
||||
);
|
||||
|
||||
const retrievedProfiles = await bulkGetUserProfiles({
|
||||
supertest,
|
||||
req: {
|
||||
uids: postedCase.assignees.map((assignee) => assignee.uid),
|
||||
dataPath: 'avatar',
|
||||
},
|
||||
});
|
||||
|
||||
expect(retrievedProfiles[0]).to.eql(profile[0]);
|
||||
});
|
||||
|
||||
it('assigns multiple users to a case and retrieves their profiles', async () => {
|
||||
const profiles = await suggestUserProfiles({
|
||||
supertest: supertestWithoutAuth,
|
||||
req: {
|
||||
name: 'only',
|
||||
owners: ['securitySolutionFixture'],
|
||||
size: 2,
|
||||
},
|
||||
auth: { user: superUser, space: 'space1' },
|
||||
});
|
||||
|
||||
const postedCase = await createCase(
|
||||
supertest,
|
||||
getPostCaseRequest({
|
||||
assignees: profiles.map((profile) => ({ uid: profile.uid })),
|
||||
})
|
||||
);
|
||||
|
||||
const retrievedProfiles = await bulkGetUserProfiles({
|
||||
supertest,
|
||||
req: {
|
||||
uids: postedCase.assignees.map((assignee) => assignee.uid),
|
||||
dataPath: 'avatar',
|
||||
},
|
||||
});
|
||||
|
||||
expect(retrievedProfiles).to.eql(profiles);
|
||||
});
|
||||
|
||||
it('assigns a user to a case and retrieves the users profile from a get case call', async () => {
|
||||
const profile = await suggestUserProfiles({
|
||||
supertest: supertestWithoutAuth,
|
||||
req: {
|
||||
name: 'delete',
|
||||
owners: ['securitySolutionFixture'],
|
||||
size: 1,
|
||||
},
|
||||
auth: { user: superUser, space: 'space1' },
|
||||
});
|
||||
|
||||
const postedCase = await createCase(
|
||||
supertest,
|
||||
getPostCaseRequest({
|
||||
assignees: [{ uid: profile[0].uid }],
|
||||
})
|
||||
);
|
||||
|
||||
const retrievedCase = await getCase({ caseId: postedCase.id, supertest });
|
||||
|
||||
const retrievedProfiles = await bulkGetUserProfiles({
|
||||
supertest,
|
||||
req: {
|
||||
uids: retrievedCase.assignees.map((assignee) => assignee.uid),
|
||||
dataPath: 'avatar',
|
||||
},
|
||||
});
|
||||
|
||||
expect(retrievedProfiles[0]).to.eql(profile[0]);
|
||||
});
|
||||
|
||||
it('filters cases using the assigned user', async () => {
|
||||
const profile = await suggestUserProfiles({
|
||||
supertest: supertestWithoutAuth,
|
||||
req: {
|
||||
name: 'delete',
|
||||
owners: ['securitySolutionFixture'],
|
||||
size: 1,
|
||||
},
|
||||
auth: { user: superUser, space: 'space1' },
|
||||
});
|
||||
|
||||
await createCase(supertest, postCaseReq);
|
||||
const caseWithDeleteAssignee1 = await createCase(
|
||||
supertest,
|
||||
getPostCaseRequest({
|
||||
assignees: [{ uid: profile[0].uid }],
|
||||
})
|
||||
);
|
||||
const caseWithDeleteAssignee2 = await createCase(
|
||||
supertest,
|
||||
getPostCaseRequest({
|
||||
assignees: [{ uid: profile[0].uid }],
|
||||
})
|
||||
);
|
||||
|
||||
const cases = await findCases({
|
||||
supertest,
|
||||
query: { assignees: [profile[0].uid] },
|
||||
});
|
||||
|
||||
expect(cases).to.eql({
|
||||
...findCasesResp,
|
||||
total: 2,
|
||||
cases: [caseWithDeleteAssignee1, caseWithDeleteAssignee2],
|
||||
count_open_cases: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it("filters cases using the assigned users by constructing an or'd filter", async () => {
|
||||
const profileUidsToFilter = await suggestUserProfiles({
|
||||
supertest: supertestWithoutAuth,
|
||||
req: {
|
||||
name: 'only',
|
||||
owners: ['securitySolutionFixture'],
|
||||
size: 2,
|
||||
},
|
||||
auth: { user: superUser, space: 'space1' },
|
||||
});
|
||||
|
||||
await createCase(supertest, postCaseReq);
|
||||
const caseWithDeleteAssignee1 = await createCase(
|
||||
supertest,
|
||||
getPostCaseRequest({
|
||||
assignees: [{ uid: profileUidsToFilter[0].uid }],
|
||||
})
|
||||
);
|
||||
const caseWithDeleteAssignee2 = await createCase(
|
||||
supertest,
|
||||
getPostCaseRequest({
|
||||
assignees: [{ uid: profileUidsToFilter[1].uid }],
|
||||
})
|
||||
);
|
||||
|
||||
const cases = await findCases({
|
||||
supertest,
|
||||
query: { assignees: [profileUidsToFilter[0].uid, profileUidsToFilter[1].uid] },
|
||||
});
|
||||
|
||||
expect(cases).to.eql({
|
||||
...findCasesResp,
|
||||
total: 2,
|
||||
cases: [caseWithDeleteAssignee1, caseWithDeleteAssignee2],
|
||||
count_open_cases: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('updates the assignees on a case', async () => {
|
||||
const profiles = await suggestUserProfiles({
|
||||
supertest: supertestWithoutAuth,
|
||||
req: {
|
||||
name: 'delete',
|
||||
owners: ['securitySolutionFixture'],
|
||||
size: 1,
|
||||
},
|
||||
auth: { user: superUser, space: 'space1' },
|
||||
});
|
||||
|
||||
const postedCase = await createCase(supertest, getPostCaseRequest());
|
||||
|
||||
const patchedCases = await updateCase({
|
||||
supertest,
|
||||
params: {
|
||||
cases: [
|
||||
{
|
||||
id: postedCase.id,
|
||||
version: postedCase.version,
|
||||
assignees: [{ uid: profiles[0].uid }],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const retrievedProfiles = await bulkGetUserProfiles({
|
||||
supertest,
|
||||
req: {
|
||||
uids: patchedCases[0].assignees.map((assignee) => assignee.uid),
|
||||
dataPath: 'avatar',
|
||||
},
|
||||
});
|
||||
|
||||
expect(retrievedProfiles).to.eql(profiles);
|
||||
});
|
||||
});
|
||||
};
|
|
@ -245,6 +245,7 @@ const expectCaseCreateUserAction = (
|
|||
...restCreateCase,
|
||||
status: CaseStatuses.open,
|
||||
severity: CaseSeverity.LOW,
|
||||
assignees: [],
|
||||
});
|
||||
expect(restParsedConnector).to.eql(restConnector);
|
||||
};
|
||||
|
|
|
@ -427,5 +427,54 @@ export default function createGetTests({ getService }: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('8.5.0', () => {
|
||||
before(async () => {
|
||||
await kibanaServer.importExport.load(
|
||||
'x-pack/test/functional/fixtures/kbn_archiver/cases/8.2.0/cases_duration.json'
|
||||
);
|
||||
await kibanaServer.importExport.load(
|
||||
'x-pack/test/functional/fixtures/kbn_archiver/cases/8.5.0/cases_assignees.json'
|
||||
);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await kibanaServer.importExport.unload(
|
||||
'x-pack/test/functional/fixtures/kbn_archiver/cases/8.2.0/cases_duration.json'
|
||||
);
|
||||
await kibanaServer.importExport.unload(
|
||||
'x-pack/test/functional/fixtures/kbn_archiver/cases/8.5.0/cases_assignees.json'
|
||||
);
|
||||
await deleteAllCaseItems(es);
|
||||
});
|
||||
|
||||
describe('assignees', () => {
|
||||
it('adds the assignees field for existing documents', async () => {
|
||||
const caseInfo = await getCase({
|
||||
supertest,
|
||||
// This case exists in the 8.2.0 cases_duration.json file and does not contain an assignees field
|
||||
caseId: '4537b380-a512-11ec-b92f-859b9e89e434',
|
||||
});
|
||||
|
||||
expect(caseInfo).to.have.property('assignees');
|
||||
expect(caseInfo.assignees).to.eql([]);
|
||||
});
|
||||
|
||||
it('does not overwrite the assignees field if it already exists', async () => {
|
||||
const caseInfo = await getCase({
|
||||
supertest,
|
||||
// This case exists in the 8.5.0 cases_assignees.json file and does contain an assignees field
|
||||
caseId: '063d5820-1284-11ed-81af-63a2bdfb2bf9',
|
||||
});
|
||||
|
||||
expect(caseInfo).to.have.property('assignees');
|
||||
expect(caseInfo.assignees).to.eql([
|
||||
{
|
||||
uid: 'abc',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -150,6 +150,7 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
owner: postedCase.owner,
|
||||
status: CaseStatuses.open,
|
||||
severity: CaseSeverity.LOW,
|
||||
assignees: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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/assignees'));
|
||||
loadTestFile(require.resolve('./cases/resolve_case'));
|
||||
loadTestFile(require.resolve('./cases/reporters/get_reporters'));
|
||||
loadTestFile(require.resolve('./cases/status/get_status'));
|
||||
|
|
|
@ -9,10 +9,7 @@ import expect from '@kbn/expect';
|
|||
import { loginUsers, suggestUserProfiles } from '../../../../common/lib/utils';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
import {
|
||||
secOnly,
|
||||
superUser,
|
||||
secOnlyNoDelete,
|
||||
secOnlyRead,
|
||||
obsOnly,
|
||||
noCasesPrivilegesSpace1,
|
||||
} from '../../../../common/lib/authentication/users';
|
||||
|
@ -24,13 +21,6 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
|
||||
describe('suggest_user_profiles', () => {
|
||||
before(async () => {
|
||||
await loginUsers({
|
||||
supertest: supertestWithoutAuth,
|
||||
users: [secOnly, secOnlyNoDelete, secOnlyRead, obsOnly],
|
||||
});
|
||||
});
|
||||
|
||||
it('finds the profile for the user without deletion privileges', async () => {
|
||||
const profiles = await suggestUserProfiles({
|
||||
supertest: supertestWithoutAuth,
|
||||
|
|
|
@ -197,6 +197,37 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
expect(deleteTagsUserAction.payload).to.eql({ tags: ['defacement'] });
|
||||
});
|
||||
|
||||
it('creates an add and delete assignees user action', async () => {
|
||||
const theCase = await createCase(
|
||||
supertest,
|
||||
getPostCaseRequest({ assignees: [{ uid: '1' }] })
|
||||
);
|
||||
await updateCase({
|
||||
supertest,
|
||||
params: {
|
||||
cases: [
|
||||
{
|
||||
id: theCase.id,
|
||||
version: theCase.version,
|
||||
assignees: [{ uid: '2' }, { uid: '3' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const userActions = await getCaseUserActions({ supertest, caseID: theCase.id });
|
||||
const addAssigneesUserAction = userActions[1];
|
||||
const deleteAssigneesUserAction = userActions[2];
|
||||
|
||||
expect(userActions.length).to.eql(3);
|
||||
expect(addAssigneesUserAction.type).to.eql('assignees');
|
||||
expect(addAssigneesUserAction.action).to.eql('add');
|
||||
expect(addAssigneesUserAction.payload).to.eql({ assignees: [{ uid: '2' }, { uid: '3' }] });
|
||||
expect(deleteAssigneesUserAction.type).to.eql('assignees');
|
||||
expect(deleteAssigneesUserAction.action).to.eql('delete');
|
||||
expect(deleteAssigneesUserAction.payload).to.eql({ assignees: [{ uid: '1' }] });
|
||||
});
|
||||
|
||||
it('creates an update title user action', async () => {
|
||||
const newTitle = 'Such a great title';
|
||||
const theCase = await createCase(supertest, postCaseReq);
|
||||
|
|
|
@ -78,6 +78,7 @@ export default function createGetTests({ getService }: FtrProviderContext) {
|
|||
id: 'none',
|
||||
},
|
||||
severity: 'low',
|
||||
assignees: [],
|
||||
owner: 'securitySolution',
|
||||
settings: { syncAlerts: true },
|
||||
},
|
||||
|
@ -191,6 +192,7 @@ export default function createGetTests({ getService }: FtrProviderContext) {
|
|||
syncAlerts: true,
|
||||
},
|
||||
severity: 'low',
|
||||
assignees: [],
|
||||
owner: 'securitySolution',
|
||||
},
|
||||
type: 'create_case',
|
||||
|
@ -298,6 +300,7 @@ export default function createGetTests({ getService }: FtrProviderContext) {
|
|||
syncAlerts: true,
|
||||
},
|
||||
severity: 'low',
|
||||
assignees: [],
|
||||
owner: 'securitySolution',
|
||||
},
|
||||
type: 'create_case',
|
||||
|
@ -327,6 +330,7 @@ export default function createGetTests({ getService }: FtrProviderContext) {
|
|||
syncAlerts: true,
|
||||
},
|
||||
severity: 'low',
|
||||
assignees: [],
|
||||
},
|
||||
type: 'create_case',
|
||||
action_id: 'b3094de0-005e-11ec-91f1-6daf2ab59fb5',
|
||||
|
@ -372,6 +376,7 @@ export default function createGetTests({ getService }: FtrProviderContext) {
|
|||
},
|
||||
owner: 'securitySolution',
|
||||
severity: 'low',
|
||||
assignees: [],
|
||||
},
|
||||
type: 'create_case',
|
||||
action_id: 'e7882d70-005e-11ec-91f1-6daf2ab59fb5',
|
||||
|
@ -744,6 +749,7 @@ export default function createGetTests({ getService }: FtrProviderContext) {
|
|||
title: 'User actions',
|
||||
owner: 'securitySolution',
|
||||
severity: 'low',
|
||||
assignees: [],
|
||||
},
|
||||
type: 'create_case',
|
||||
},
|
||||
|
@ -1109,6 +1115,7 @@ export default function createGetTests({ getService }: FtrProviderContext) {
|
|||
title: 'User actions',
|
||||
owner: 'securitySolution',
|
||||
severity: 'low',
|
||||
assignees: [],
|
||||
},
|
||||
type: 'create_case',
|
||||
});
|
||||
|
@ -1130,6 +1137,85 @@ export default function createGetTests({ getService }: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('8.5.0', () => {
|
||||
const CASE_ID = '5257a000-5e7d-11ec-9ee9-cd64f0b77b3c';
|
||||
const CREATE_UA_ID = '5275af50-5e7d-11ec-9ee9-cd64f0b77b3c';
|
||||
|
||||
before(async () => {
|
||||
await kibanaServer.importExport.load(
|
||||
'x-pack/test/functional/fixtures/kbn_archiver/cases/8.0.0/cases.json'
|
||||
);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await kibanaServer.importExport.unload(
|
||||
'x-pack/test/functional/fixtures/kbn_archiver/cases/8.0.0/cases.json'
|
||||
);
|
||||
await deleteAllCaseItems(es);
|
||||
});
|
||||
|
||||
describe('assignees', () => {
|
||||
it('adds the assignees field to the create case user action', async () => {
|
||||
const userActions = await getCaseUserActions({
|
||||
supertest,
|
||||
caseID: CASE_ID,
|
||||
});
|
||||
|
||||
const createUserAction = userActions.find(
|
||||
(userAction) => userAction.action_id === CREATE_UA_ID
|
||||
);
|
||||
|
||||
expect(createUserAction).to.eql({
|
||||
action: 'create',
|
||||
action_id: '5275af50-5e7d-11ec-9ee9-cd64f0b77b3c',
|
||||
case_id: '5257a000-5e7d-11ec-9ee9-cd64f0b77b3c',
|
||||
comment_id: null,
|
||||
created_at: '2021-12-16T14:34:48.709Z',
|
||||
created_by: {
|
||||
email: '',
|
||||
full_name: '',
|
||||
username: 'elastic',
|
||||
},
|
||||
owner: 'securitySolution',
|
||||
payload: {
|
||||
connector: {
|
||||
fields: null,
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
type: '.none',
|
||||
},
|
||||
description: 'migrating user actions',
|
||||
settings: {
|
||||
syncAlerts: true,
|
||||
},
|
||||
status: 'open',
|
||||
tags: ['user', 'actions'],
|
||||
title: 'User actions',
|
||||
owner: 'securitySolution',
|
||||
severity: 'low',
|
||||
assignees: [],
|
||||
},
|
||||
type: 'create_case',
|
||||
});
|
||||
});
|
||||
|
||||
it('does NOT add the assignees field to the other user actions', async () => {
|
||||
const userActions = await getCaseUserActions({
|
||||
supertest,
|
||||
caseID: CASE_ID,
|
||||
});
|
||||
|
||||
const userActionsWithoutCreateAction = userActions.filter(
|
||||
(userAction) => userAction.type !== ActionTypes.create_case
|
||||
);
|
||||
|
||||
for (const userAction of userActionsWithoutCreateAction) {
|
||||
expect(userAction.payload).not.to.have.property('assignees');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -6,13 +6,20 @@
|
|||
*/
|
||||
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
import { createSpacesAndUsers, deleteSpacesAndUsers } from '../../../common/lib/authentication';
|
||||
import {
|
||||
createSpacesAndUsers,
|
||||
deleteSpacesAndUsers,
|
||||
activateUserProfiles,
|
||||
} from '../../../common/lib/authentication';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ loadTestFile, getService }: FtrProviderContext): void => {
|
||||
describe('cases security and spaces enabled: trial', function () {
|
||||
before(async () => {
|
||||
await createSpacesAndUsers(getService);
|
||||
// once a user profile is created the only way to remove it is to delete the user and roles, so best to activate
|
||||
// before all the tests
|
||||
await activateUserProfiles(getService);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
{
|
||||
"attributes": {
|
||||
"assignees": [
|
||||
{
|
||||
"uid": "abc"
|
||||
}
|
||||
],
|
||||
"closed_at": null,
|
||||
"closed_by": null,
|
||||
"connector": {
|
||||
"fields": [],
|
||||
"name": "none",
|
||||
"type": ".none"
|
||||
},
|
||||
"created_at": "2022-08-02T16:56:16.806Z",
|
||||
"created_by": {
|
||||
"email": null,
|
||||
"full_name": null,
|
||||
"username": "elastic"
|
||||
},
|
||||
"description": "a case description",
|
||||
"duration": null,
|
||||
"external_service": null,
|
||||
"owner": "cases",
|
||||
"settings": {
|
||||
"syncAlerts": false
|
||||
},
|
||||
"severity": "low",
|
||||
"status": "open",
|
||||
"tags": [
|
||||
"super",
|
||||
"awesome"
|
||||
],
|
||||
"title": "Test case",
|
||||
"updated_at": null,
|
||||
"updated_by": null
|
||||
},
|
||||
"coreMigrationVersion": "8.5.0",
|
||||
"id": "063d5820-1284-11ed-81af-63a2bdfb2bf9",
|
||||
"migrationVersion": {
|
||||
"cases": "8.5.0"
|
||||
},
|
||||
"references": [],
|
||||
"type": "cases",
|
||||
"updated_at": "2022-08-02T16:56:16.808Z",
|
||||
"version": "WzE3MywxXQ=="
|
||||
}
|
||||
|
||||
{
|
||||
"attributes": {
|
||||
"action": "create",
|
||||
"created_at": "2022-08-02T16:56:17.195Z",
|
||||
"created_by": {
|
||||
"email": null,
|
||||
"full_name": null,
|
||||
"username": "elastic"
|
||||
},
|
||||
"owner": "cases",
|
||||
"payload": {
|
||||
"assignees": [
|
||||
{
|
||||
"uid": "abc"
|
||||
}
|
||||
],
|
||||
"connector": {
|
||||
"fields": null,
|
||||
"name": "none",
|
||||
"type": ".none"
|
||||
},
|
||||
"description": "a case description",
|
||||
"owner": "cases",
|
||||
"settings": {
|
||||
"syncAlerts": false
|
||||
},
|
||||
"severity": "low",
|
||||
"status": "open",
|
||||
"tags": [
|
||||
"super",
|
||||
"awesome"
|
||||
],
|
||||
"title": "Test case"
|
||||
},
|
||||
"type": "create_case"
|
||||
},
|
||||
"coreMigrationVersion": "8.5.0",
|
||||
"id": "06794fb0-1284-11ed-81af-63a2bdfb2bf9",
|
||||
"migrationVersion": {
|
||||
"cases-user-actions": "8.5.0"
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"id": "063d5820-1284-11ed-81af-63a2bdfb2bf9",
|
||||
"name": "associated-cases",
|
||||
"type": "cases"
|
||||
}
|
||||
],
|
||||
"score": null,
|
||||
"sort": [
|
||||
1659459377195,
|
||||
334
|
||||
],
|
||||
"type": "cases-user-actions",
|
||||
"updated_at": "2022-08-02T16:56:17.195Z",
|
||||
"version": "WzE3NCwxXQ=="
|
||||
}
|
|
@ -5,9 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CaseConnector, CasePostRequest } from '@kbn/cases-plugin/common/api';
|
||||
import uuid from 'uuid';
|
||||
|
||||
export function generateRandomCaseWithoutConnector() {
|
||||
export function generateRandomCaseWithoutConnector(): CasePostRequest {
|
||||
return {
|
||||
title: 'random-' + uuid.v4(),
|
||||
tags: ['test', uuid.v4()],
|
||||
|
@ -17,7 +18,7 @@ export function generateRandomCaseWithoutConnector() {
|
|||
name: 'none',
|
||||
type: '.none',
|
||||
fields: null,
|
||||
},
|
||||
} as CaseConnector,
|
||||
settings: {
|
||||
syncAlerts: false,
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue