mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 11:05:39 -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 { CommentResponseRt } from './comment';
|
||||||
import { CasesStatusResponseRt, CaseStatusRt } from './status';
|
import { CasesStatusResponseRt, CaseStatusRt } from './status';
|
||||||
import { CaseConnectorRt } from '../connectors';
|
import { CaseConnectorRt } from '../connectors';
|
||||||
|
import { CaseAssigneesRt } from './assignee';
|
||||||
|
|
||||||
const BucketsAggs = rt.array(
|
const BucketsAggs = rt.array(
|
||||||
rt.type({
|
rt.type({
|
||||||
|
@ -86,6 +87,10 @@ const CaseBasicRt = rt.type({
|
||||||
* The severity of the case
|
* The severity of the case
|
||||||
*/
|
*/
|
||||||
severity: CaseSeverityRt,
|
severity: CaseSeverityRt,
|
||||||
|
/**
|
||||||
|
* The users assigned to this case
|
||||||
|
*/
|
||||||
|
assignees: CaseAssigneesRt,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -153,6 +158,10 @@ export const CasePostRequestRt = rt.intersection([
|
||||||
owner: rt.string,
|
owner: rt.string,
|
||||||
}),
|
}),
|
||||||
rt.partial({
|
rt.partial({
|
||||||
|
/**
|
||||||
|
* The users assigned to the case
|
||||||
|
*/
|
||||||
|
assignees: CaseAssigneesRt,
|
||||||
/**
|
/**
|
||||||
* The severity of the case. The severity is
|
* The severity of the case. The severity is
|
||||||
* default it to "low" if not provided.
|
* default it to "low" if not provided.
|
||||||
|
@ -174,6 +183,10 @@ export const CasesFindRequestRt = rt.partial({
|
||||||
* The severity of the case
|
* The severity of the case
|
||||||
*/
|
*/
|
||||||
severity: CaseSeverityRt,
|
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
|
* The reporters to filter by
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -13,3 +13,4 @@ export * from './user_actions';
|
||||||
export * from './constants';
|
export * from './constants';
|
||||||
export * from './alerts';
|
export * from './alerts';
|
||||||
export * from './user_profiles';
|
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';
|
import { UserRT } from '../../user';
|
||||||
|
|
||||||
export const ActionTypes = {
|
export const ActionTypes = {
|
||||||
|
assignees: 'assignees',
|
||||||
comment: 'comment',
|
comment: 'comment',
|
||||||
connector: 'connector',
|
connector: 'connector',
|
||||||
description: 'description',
|
description: 'description',
|
||||||
|
@ -22,6 +23,9 @@ export const ActionTypes = {
|
||||||
delete_case: 'delete_case',
|
delete_case: 'delete_case',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export type ActionTypeKeys = keyof typeof ActionTypes;
|
||||||
|
export type ActionTypeValues = typeof ActionTypes[ActionTypeKeys];
|
||||||
|
|
||||||
export const Actions = {
|
export const Actions = {
|
||||||
add: 'add',
|
add: 'add',
|
||||||
create: 'create',
|
create: 'create',
|
||||||
|
@ -30,6 +34,9 @@ export const Actions = {
|
||||||
push_to_service: 'push_to_service',
|
push_to_service: 'push_to_service',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export type ActionOperationKeys = keyof typeof Actions;
|
||||||
|
export type ActionOperationValues = typeof Actions[ActionOperationKeys];
|
||||||
|
|
||||||
/* To the next developer, if you add/removed fields here
|
/* 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
|
* 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 * as rt from 'io-ts';
|
||||||
|
import { AssigneesUserActionPayloadRt } from './assignees';
|
||||||
import { ActionTypes, UserActionWithAttributes } from './common';
|
import { ActionTypes, UserActionWithAttributes } from './common';
|
||||||
import {
|
import {
|
||||||
ConnectorUserActionPayloadRt,
|
ConnectorUserActionPayloadRt,
|
||||||
|
@ -21,6 +22,7 @@ export const CommonFieldsRt = rt.type({
|
||||||
});
|
});
|
||||||
|
|
||||||
const CommonPayloadAttributesRt = rt.type({
|
const CommonPayloadAttributesRt = rt.type({
|
||||||
|
assignees: AssigneesUserActionPayloadRt.props.assignees,
|
||||||
description: DescriptionUserActionPayloadRt.props.description,
|
description: DescriptionUserActionPayloadRt.props.description,
|
||||||
status: rt.string,
|
status: rt.string,
|
||||||
severity: rt.string,
|
severity: rt.string,
|
||||||
|
|
|
@ -24,6 +24,7 @@ import { SettingsUserActionRt } from './settings';
|
||||||
import { StatusUserActionRt } from './status';
|
import { StatusUserActionRt } from './status';
|
||||||
import { DeleteCaseUserActionRt } from './delete_case';
|
import { DeleteCaseUserActionRt } from './delete_case';
|
||||||
import { SeverityUserActionRt } from './severity';
|
import { SeverityUserActionRt } from './severity';
|
||||||
|
import { AssigneesUserActionRt } from './assignees';
|
||||||
|
|
||||||
export * from './common';
|
export * from './common';
|
||||||
export * from './comment';
|
export * from './comment';
|
||||||
|
@ -36,6 +37,7 @@ export * from './settings';
|
||||||
export * from './status';
|
export * from './status';
|
||||||
export * from './tags';
|
export * from './tags';
|
||||||
export * from './title';
|
export * from './title';
|
||||||
|
export * from './assignees';
|
||||||
|
|
||||||
const CommonUserActionsRt = rt.union([
|
const CommonUserActionsRt = rt.union([
|
||||||
DescriptionUserActionRt,
|
DescriptionUserActionRt,
|
||||||
|
@ -45,6 +47,7 @@ const CommonUserActionsRt = rt.union([
|
||||||
SettingsUserActionRt,
|
SettingsUserActionRt,
|
||||||
StatusUserActionRt,
|
StatusUserActionRt,
|
||||||
SeverityUserActionRt,
|
SeverityUserActionRt,
|
||||||
|
AssigneesUserActionRt,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const UserActionsRt = rt.union([
|
export const UserActionsRt = rt.union([
|
||||||
|
|
|
@ -16,3 +16,9 @@ export const SuggestUserProfilesRequestRt = rt.intersection([
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type SuggestUserProfilesRequest = rt.TypeOf<typeof SuggestUserProfilesRequestRt>;
|
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');
|
wrapper.find('[data-test-subj="cases-table-row-select-1"]').first().simulate('click');
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(onRowClick).toHaveBeenCalledWith({
|
expect(onRowClick).toHaveBeenCalledWith({
|
||||||
|
assignees: [],
|
||||||
closedAt: null,
|
closedAt: null,
|
||||||
closedBy: null,
|
closedBy: null,
|
||||||
comments: [],
|
comments: [],
|
||||||
|
|
|
@ -33,6 +33,7 @@ const initialCaseValue: FormProps = {
|
||||||
fields: null,
|
fields: null,
|
||||||
syncAlerts: true,
|
syncAlerts: true,
|
||||||
selectedOwner: null,
|
selectedOwner: null,
|
||||||
|
assignees: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Props {
|
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.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { createAssigneesUserActionBuilder } from './assignees';
|
||||||
import { createCommentUserActionBuilder } from './comment/comment';
|
import { createCommentUserActionBuilder } from './comment/comment';
|
||||||
import { createConnectorUserActionBuilder } from './connector';
|
import { createConnectorUserActionBuilder } from './connector';
|
||||||
import { createDescriptionUserActionBuilder } from './description';
|
import { createDescriptionUserActionBuilder } from './description';
|
||||||
|
@ -26,4 +27,5 @@ export const builderMap: UserActionBuilderMap = {
|
||||||
comment: createCommentUserActionBuilder,
|
comment: createCommentUserActionBuilder,
|
||||||
description: createDescriptionUserActionBuilder,
|
description: createDescriptionUserActionBuilder,
|
||||||
settings: createSettingsUserActionBuilder,
|
settings: createSettingsUserActionBuilder,
|
||||||
|
assignees: createAssigneesUserActionBuilder,
|
||||||
};
|
};
|
||||||
|
|
|
@ -228,6 +228,7 @@ export const basicCase: Case = {
|
||||||
settings: {
|
settings: {
|
||||||
syncAlerts: true,
|
syncAlerts: true,
|
||||||
},
|
},
|
||||||
|
assignees: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const caseWithAlerts = {
|
export const caseWithAlerts = {
|
||||||
|
@ -329,6 +330,7 @@ export const mockCase: Case = {
|
||||||
settings: {
|
settings: {
|
||||||
syncAlerts: true,
|
syncAlerts: true,
|
||||||
},
|
},
|
||||||
|
assignees: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const basicCasePost: Case = {
|
export const basicCasePost: Case = {
|
||||||
|
@ -631,6 +633,7 @@ export const getUserAction = (
|
||||||
tags: ['a tag'],
|
tags: ['a tag'],
|
||||||
settings: { syncAlerts: true },
|
settings: { syncAlerts: true },
|
||||||
owner: SECURITY_SOLUTION_OWNER,
|
owner: SECURITY_SOLUTION_OWNER,
|
||||||
|
assignees: [],
|
||||||
},
|
},
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
|
|
|
@ -86,7 +86,11 @@ export const create = async (
|
||||||
unsecuredSavedObjectsClient,
|
unsecuredSavedObjectsClient,
|
||||||
caseId: newCase.id,
|
caseId: newCase.id,
|
||||||
user,
|
user,
|
||||||
payload: { ...query, severity: query.severity ?? CaseSeverity.LOW },
|
payload: {
|
||||||
|
...query,
|
||||||
|
severity: query.severity ?? CaseSeverity.LOW,
|
||||||
|
assignees: query.assignees ?? [],
|
||||||
|
},
|
||||||
owner: newCase.attributes.owner,
|
owner: newCase.attributes.owner,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -62,6 +62,7 @@ export const find = async (
|
||||||
owner: queryParams.owner,
|
owner: queryParams.owner,
|
||||||
from: queryParams.from,
|
from: queryParams.from,
|
||||||
to: queryParams.to,
|
to: queryParams.to,
|
||||||
|
assignees: queryParams.assignees,
|
||||||
};
|
};
|
||||||
|
|
||||||
const statusStatsOptions = constructQueryOptions({
|
const statusStatsOptions = constructQueryOptions({
|
||||||
|
|
|
@ -242,6 +242,7 @@ export const userActions: CaseUserActionsResponse = [
|
||||||
status: 'open',
|
status: 'open',
|
||||||
severity: 'low',
|
severity: 'low',
|
||||||
owner: SECURITY_SOLUTION_OWNER,
|
owner: SECURITY_SOLUTION_OWNER,
|
||||||
|
assignees: [],
|
||||||
},
|
},
|
||||||
action_id: 'fd830c60-6646-11eb-a291-51bf6b175a53',
|
action_id: 'fd830c60-6646-11eb-a291-51bf6b175a53',
|
||||||
case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53',
|
case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53',
|
||||||
|
|
|
@ -57,4 +57,5 @@ export interface ConstructQueryParams {
|
||||||
authorizationFilter?: KueryNode;
|
authorizationFilter?: KueryNode;
|
||||||
from?: string;
|
from?: string;
|
||||||
to?: string;
|
to?: string;
|
||||||
|
assignees?: string | string[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,13 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { arraysDifference, buildRangeFilter, constructQueryOptions, sortToSnake } from './utils';
|
import {
|
||||||
|
arraysDifference,
|
||||||
|
buildNestedFilter,
|
||||||
|
buildRangeFilter,
|
||||||
|
constructQueryOptions,
|
||||||
|
sortToSnake,
|
||||||
|
} from './utils';
|
||||||
import { toElasticsearchQuery } from '@kbn/es-query';
|
import { toElasticsearchQuery } from '@kbn/es-query';
|
||||||
import { CaseStatuses } from '../../common';
|
import { CaseStatuses } from '../../common';
|
||||||
import { CaseSeverity } from '../../common/api';
|
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', () => {
|
describe('arraysDifference', () => {
|
||||||
it('returns null if originalValue is null', () => {
|
it('returns null if originalValue is null', () => {
|
||||||
expect(arraysDifference(null, [])).toBeNull();
|
expect(arraysDifference(null, [])).toBeNull();
|
||||||
|
|
|
@ -177,6 +177,10 @@ interface FilterField {
|
||||||
type?: string;
|
type?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface NestedFilterField extends FilterField {
|
||||||
|
nestedField: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const buildFilter = ({
|
export const buildFilter = ({
|
||||||
filters,
|
filters,
|
||||||
field,
|
field,
|
||||||
|
@ -194,7 +198,48 @@ export const buildFilter = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return nodeBuilder[operator](
|
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,
|
authorizationFilter,
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
|
assignees,
|
||||||
}: ConstructQueryParams): SavedObjectFindOptionsKueryNode => {
|
}: ConstructQueryParams): SavedObjectFindOptionsKueryNode => {
|
||||||
const tagsFilter = buildFilter({ filters: tags, field: 'tags', operator: 'or' });
|
const tagsFilter = buildFilter({ filters: tags, field: 'tags', operator: 'or' });
|
||||||
const reportersFilter = buildFilter({
|
const reportersFilter = buildFilter({
|
||||||
|
@ -297,6 +343,11 @@ export const constructQueryOptions = ({
|
||||||
const statusFilter = status != null ? addStatusFilter({ status }) : undefined;
|
const statusFilter = status != null ? addStatusFilter({ status }) : undefined;
|
||||||
const severityFilter = severity != null ? addSeverityFilter({ severity }) : undefined;
|
const severityFilter = severity != null ? addSeverityFilter({ severity }) : undefined;
|
||||||
const rangeFilter = buildRangeFilter({ from, to });
|
const rangeFilter = buildRangeFilter({ from, to });
|
||||||
|
const assigneesFilter = buildFilter({
|
||||||
|
filters: assignees,
|
||||||
|
field: 'assignees.uid',
|
||||||
|
operator: 'or',
|
||||||
|
});
|
||||||
|
|
||||||
const filters = combineFilters([
|
const filters = combineFilters([
|
||||||
statusFilter,
|
statusFilter,
|
||||||
|
@ -305,6 +356,7 @@ export const constructQueryOptions = ({
|
||||||
reportersFilter,
|
reportersFilter,
|
||||||
rangeFilter,
|
rangeFilter,
|
||||||
ownerFilter,
|
ownerFilter,
|
||||||
|
assigneesFilter,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -103,6 +103,7 @@ describe('common utils', () => {
|
||||||
|
|
||||||
expect(res).toMatchInlineSnapshot(`
|
expect(res).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
|
"assignees": Array [],
|
||||||
"closed_at": null,
|
"closed_at": null,
|
||||||
"closed_by": null,
|
"closed_by": null,
|
||||||
"connector": Object {
|
"connector": Object {
|
||||||
|
@ -155,6 +156,7 @@ describe('common utils', () => {
|
||||||
|
|
||||||
expect(res).toMatchInlineSnapshot(`
|
expect(res).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
|
"assignees": Array [],
|
||||||
"closed_at": null,
|
"closed_at": null,
|
||||||
"closed_by": null,
|
"closed_by": null,
|
||||||
"connector": Object {
|
"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', () => {
|
describe('transformCases', () => {
|
||||||
|
@ -214,6 +273,7 @@ describe('common utils', () => {
|
||||||
Object {
|
Object {
|
||||||
"cases": Array [
|
"cases": Array [
|
||||||
Object {
|
Object {
|
||||||
|
"assignees": Array [],
|
||||||
"closed_at": null,
|
"closed_at": null,
|
||||||
"closed_by": null,
|
"closed_by": null,
|
||||||
"comments": Array [],
|
"comments": Array [],
|
||||||
|
@ -254,6 +314,7 @@ describe('common utils', () => {
|
||||||
"version": "WzAsMV0=",
|
"version": "WzAsMV0=",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
|
"assignees": Array [],
|
||||||
"closed_at": null,
|
"closed_at": null,
|
||||||
"closed_by": null,
|
"closed_by": null,
|
||||||
"comments": Array [],
|
"comments": Array [],
|
||||||
|
@ -294,6 +355,7 @@ describe('common utils', () => {
|
||||||
"version": "WzQsMV0=",
|
"version": "WzQsMV0=",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
|
"assignees": Array [],
|
||||||
"closed_at": null,
|
"closed_at": null,
|
||||||
"closed_by": null,
|
"closed_by": null,
|
||||||
"comments": Array [],
|
"comments": Array [],
|
||||||
|
@ -338,6 +400,7 @@ describe('common utils', () => {
|
||||||
"version": "WzUsMV0=",
|
"version": "WzUsMV0=",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
|
"assignees": Array [],
|
||||||
"closed_at": "2019-11-25T22:32:17.947Z",
|
"closed_at": "2019-11-25T22:32:17.947Z",
|
||||||
"closed_by": Object {
|
"closed_by": Object {
|
||||||
"email": "testemail@elastic.co",
|
"email": "testemail@elastic.co",
|
||||||
|
@ -407,6 +470,7 @@ describe('common utils', () => {
|
||||||
|
|
||||||
expect(res).toMatchInlineSnapshot(`
|
expect(res).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
|
"assignees": Array [],
|
||||||
"closed_at": null,
|
"closed_at": null,
|
||||||
"closed_by": null,
|
"closed_by": null,
|
||||||
"comments": Array [],
|
"comments": Array [],
|
||||||
|
@ -463,6 +527,7 @@ describe('common utils', () => {
|
||||||
|
|
||||||
expect(res).toMatchInlineSnapshot(`
|
expect(res).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
|
"assignees": Array [],
|
||||||
"closed_at": null,
|
"closed_at": null,
|
||||||
"closed_by": null,
|
"closed_by": null,
|
||||||
"comments": Array [],
|
"comments": Array [],
|
||||||
|
@ -520,6 +585,7 @@ describe('common utils', () => {
|
||||||
|
|
||||||
expect(res).toMatchInlineSnapshot(`
|
expect(res).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
|
"assignees": Array [],
|
||||||
"closed_at": null,
|
"closed_at": null,
|
||||||
"closed_by": null,
|
"closed_by": null,
|
||||||
"comments": Array [
|
"comments": Array [
|
||||||
|
@ -600,6 +666,7 @@ describe('common utils', () => {
|
||||||
|
|
||||||
expect(res).toMatchInlineSnapshot(`
|
expect(res).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
|
"assignees": Array [],
|
||||||
"closed_at": null,
|
"closed_at": null,
|
||||||
"closed_by": null,
|
"closed_by": null,
|
||||||
"comments": Array [],
|
"comments": Array [],
|
||||||
|
|
|
@ -69,6 +69,7 @@ export const transformNewCase = ({
|
||||||
status: CaseStatuses.open,
|
status: CaseStatuses.open,
|
||||||
updated_at: null,
|
updated_at: null,
|
||||||
updated_by: null,
|
updated_by: null,
|
||||||
|
assignees: newCase.assignees ?? [],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const transformCases = ({
|
export const transformCases = ({
|
||||||
|
|
|
@ -52,6 +52,7 @@ export const mockCases: Array<SavedObject<CaseAttributes>> = [
|
||||||
syncAlerts: true,
|
syncAlerts: true,
|
||||||
},
|
},
|
||||||
owner: SECURITY_SOLUTION_OWNER,
|
owner: SECURITY_SOLUTION_OWNER,
|
||||||
|
assignees: [],
|
||||||
},
|
},
|
||||||
references: [],
|
references: [],
|
||||||
updated_at: '2019-11-25T21:54:48.952Z',
|
updated_at: '2019-11-25T21:54:48.952Z',
|
||||||
|
@ -92,6 +93,7 @@ export const mockCases: Array<SavedObject<CaseAttributes>> = [
|
||||||
syncAlerts: true,
|
syncAlerts: true,
|
||||||
},
|
},
|
||||||
owner: SECURITY_SOLUTION_OWNER,
|
owner: SECURITY_SOLUTION_OWNER,
|
||||||
|
assignees: [],
|
||||||
},
|
},
|
||||||
references: [],
|
references: [],
|
||||||
updated_at: '2019-11-25T22:32:00.900Z',
|
updated_at: '2019-11-25T22:32:00.900Z',
|
||||||
|
@ -132,6 +134,7 @@ export const mockCases: Array<SavedObject<CaseAttributes>> = [
|
||||||
syncAlerts: true,
|
syncAlerts: true,
|
||||||
},
|
},
|
||||||
owner: SECURITY_SOLUTION_OWNER,
|
owner: SECURITY_SOLUTION_OWNER,
|
||||||
|
assignees: [],
|
||||||
},
|
},
|
||||||
references: [],
|
references: [],
|
||||||
updated_at: '2019-11-25T22:32:17.947Z',
|
updated_at: '2019-11-25T22:32:17.947Z',
|
||||||
|
@ -176,6 +179,7 @@ export const mockCases: Array<SavedObject<CaseAttributes>> = [
|
||||||
syncAlerts: true,
|
syncAlerts: true,
|
||||||
},
|
},
|
||||||
owner: SECURITY_SOLUTION_OWNER,
|
owner: SECURITY_SOLUTION_OWNER,
|
||||||
|
assignees: [],
|
||||||
},
|
},
|
||||||
references: [],
|
references: [],
|
||||||
updated_at: '2019-11-25T22:32:17.947Z',
|
updated_at: '2019-11-25T22:32:17.947Z',
|
||||||
|
|
|
@ -27,6 +27,13 @@ export const createCaseSavedObjectType = (
|
||||||
convertToMultiNamespaceTypeVersion: '8.0.0',
|
convertToMultiNamespaceTypeVersion: '8.0.0',
|
||||||
mappings: {
|
mappings: {
|
||||||
properties: {
|
properties: {
|
||||||
|
assignees: {
|
||||||
|
properties: {
|
||||||
|
uid: {
|
||||||
|
type: 'keyword',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
closed_at: {
|
closed_at: {
|
||||||
type: 'date',
|
type: 'date',
|
||||||
},
|
},
|
||||||
|
|
|
@ -16,7 +16,13 @@ import {
|
||||||
import { CASE_SAVED_OBJECT } from '../../../common/constants';
|
import { CASE_SAVED_OBJECT } from '../../../common/constants';
|
||||||
import { getNoneCaseConnector } from '../../common/utils';
|
import { getNoneCaseConnector } from '../../common/utils';
|
||||||
import { createExternalService, ESCaseConnectorWithId } from '../../services/test_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
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
const create_7_14_0_case = ({
|
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 ?? [] };
|
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 = {
|
export const caseMigrations = {
|
||||||
'7.10.0': (
|
'7.10.0': (
|
||||||
doc: SavedObjectUnsanitizedDoc<UnsanitizedCaseConnector>
|
doc: SavedObjectUnsanitizedDoc<UnsanitizedCaseConnector>
|
||||||
|
@ -184,4 +191,5 @@ export const caseMigrations = {
|
||||||
'7.15.0': caseConnectorIdMigration,
|
'7.15.0': caseConnectorIdMigration,
|
||||||
'8.1.0': removeCaseType,
|
'8.1.0': removeCaseType,
|
||||||
'8.3.0': pipeMigrations(addDuration, addSeverity),
|
'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 { addSeverityToCreateUserAction } from './severity';
|
||||||
import { UserActions } from './types';
|
import { UserActions } from './types';
|
||||||
import { getAllPersistableAttachmentMigrations } from '../get_all_persistable_attachment_migrations';
|
import { getAllPersistableAttachmentMigrations } from '../get_all_persistable_attachment_migrations';
|
||||||
|
import { addAssigneesToCreateUserAction } from './assignees';
|
||||||
|
|
||||||
export interface UserActionsMigrationsDeps {
|
export interface UserActionsMigrationsDeps {
|
||||||
persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry;
|
persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry;
|
||||||
|
@ -98,6 +99,7 @@ export const createUserActionsMigrations = (
|
||||||
'8.0.0': removeRuleInformation,
|
'8.0.0': removeRuleInformation,
|
||||||
'8.1.0': payloadMigration,
|
'8.1.0': payloadMigration,
|
||||||
'8.3.0': addSeverityToCreateUserAction,
|
'8.3.0': addSeverityToCreateUserAction,
|
||||||
|
'8.5.0': addAssigneesToCreateUserAction,
|
||||||
};
|
};
|
||||||
|
|
||||||
return mergeSavedObjectMigrationMaps(persistableStateAttachmentMigrations, userActionsMigrations);
|
return mergeSavedObjectMigrationMaps(persistableStateAttachmentMigrations, userActionsMigrations);
|
||||||
|
|
|
@ -157,6 +157,7 @@ describe('CasesService', () => {
|
||||||
} = unsecuredSavedObjectsClient.update.mock.calls[0][2] as Partial<ESCaseAttributes>;
|
} = unsecuredSavedObjectsClient.update.mock.calls[0][2] as Partial<ESCaseAttributes>;
|
||||||
expect(restUpdateAttributes).toMatchInlineSnapshot(`
|
expect(restUpdateAttributes).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
|
"assignees": Array [],
|
||||||
"closed_at": null,
|
"closed_at": null,
|
||||||
"closed_by": null,
|
"closed_by": null,
|
||||||
"created_at": "2019-11-25T21:54:48.952Z",
|
"created_at": "2019-11-25T21:54:48.952Z",
|
||||||
|
@ -481,6 +482,7 @@ describe('CasesService', () => {
|
||||||
expect(creationAttributes.external_service).not.toHaveProperty('connector_id');
|
expect(creationAttributes.external_service).not.toHaveProperty('connector_id');
|
||||||
expect(creationAttributes).toMatchInlineSnapshot(`
|
expect(creationAttributes).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
|
"assignees": Array [],
|
||||||
"closed_at": null,
|
"closed_at": null,
|
||||||
"closed_by": null,
|
"closed_by": null,
|
||||||
"connector": Object {
|
"connector": Object {
|
||||||
|
|
|
@ -126,14 +126,17 @@ export const basicCaseFields: CaseAttributes = {
|
||||||
syncAlerts: true,
|
syncAlerts: true,
|
||||||
},
|
},
|
||||||
owner: SECURITY_SOLUTION_OWNER,
|
owner: SECURITY_SOLUTION_OWNER,
|
||||||
|
assignees: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createCaseSavedObjectResponse = ({
|
export const createCaseSavedObjectResponse = ({
|
||||||
connector,
|
connector,
|
||||||
externalService,
|
externalService,
|
||||||
|
overrides,
|
||||||
}: {
|
}: {
|
||||||
connector?: ESCaseConnectorWithId;
|
connector?: ESCaseConnectorWithId;
|
||||||
externalService?: CaseFullExternalService;
|
externalService?: CaseFullExternalService;
|
||||||
|
overrides?: Partial<CaseAttributes>;
|
||||||
} = {}): SavedObject<ESCaseAttributes> => {
|
} = {}): SavedObject<ESCaseAttributes> => {
|
||||||
const references: SavedObjectReference[] = createSavedObjectReferences({
|
const references: SavedObjectReference[] = createSavedObjectReferences({
|
||||||
connector,
|
connector,
|
||||||
|
@ -168,6 +171,7 @@ export const createCaseSavedObjectResponse = ({
|
||||||
id: '1',
|
id: '1',
|
||||||
attributes: {
|
attributes: {
|
||||||
...basicCaseFields,
|
...basicCaseFields,
|
||||||
|
...overrides,
|
||||||
// if connector is null we'll default this to an incomplete jira value because the service
|
// 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
|
// should switch it to a none connector when the id can't be found in the references array
|
||||||
connector: formattedConnector,
|
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', () => {
|
it('builds a settings user action correctly', () => {
|
||||||
const builder = builderFactory.getBuilder(ActionTypes.settings)!;
|
const builder = builderFactory.getBuilder(ActionTypes.settings)!;
|
||||||
const userAction = builder.build({
|
const userAction = builder.build({
|
||||||
|
@ -601,6 +642,11 @@ describe('UserActionBuilder', () => {
|
||||||
},
|
},
|
||||||
"owner": "securitySolution",
|
"owner": "securitySolution",
|
||||||
"payload": Object {
|
"payload": Object {
|
||||||
|
"assignees": Array [
|
||||||
|
Object {
|
||||||
|
"uid": "1",
|
||||||
|
},
|
||||||
|
],
|
||||||
"connector": Object {
|
"connector": Object {
|
||||||
"fields": Object {
|
"fields": Object {
|
||||||
"category": "Denial of Service",
|
"category": "Denial of Service",
|
||||||
|
|
|
@ -20,8 +20,10 @@ import { UserActionBuilder } from './abstract_builder';
|
||||||
import { SeverityUserActionBuilder } from './builders/severity';
|
import { SeverityUserActionBuilder } from './builders/severity';
|
||||||
import { PersistableStateAttachmentTypeRegistry } from '../../attachment_framework/persistable_state_registry';
|
import { PersistableStateAttachmentTypeRegistry } from '../../attachment_framework/persistable_state_registry';
|
||||||
import { BuilderDeps } from './types';
|
import { BuilderDeps } from './types';
|
||||||
|
import { AssigneesUserActionBuilder } from './builders/assignees';
|
||||||
|
|
||||||
const builderMap = {
|
const builderMap = {
|
||||||
|
assignees: AssigneesUserActionBuilder,
|
||||||
title: TitleUserActionBuilder,
|
title: TitleUserActionBuilder,
|
||||||
create_case: CreateCaseUserActionBuilder,
|
create_case: CreateCaseUserActionBuilder,
|
||||||
connector: ConnectorUserActionBuilder,
|
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,
|
SavedObjectReference,
|
||||||
SavedObjectsFindResponse,
|
SavedObjectsFindResponse,
|
||||||
SavedObjectsFindResult,
|
SavedObjectsFindResult,
|
||||||
|
SavedObjectsUpdateResponse,
|
||||||
} from '@kbn/core/server';
|
} from '@kbn/core/server';
|
||||||
import { ACTION_SAVED_OBJECT_TYPE } from '@kbn/actions-plugin/server';
|
import { ACTION_SAVED_OBJECT_TYPE } from '@kbn/actions-plugin/server';
|
||||||
import {
|
import {
|
||||||
Actions,
|
Actions,
|
||||||
ActionTypes,
|
ActionTypes,
|
||||||
|
CaseAttributes,
|
||||||
CaseSeverity,
|
CaseSeverity,
|
||||||
CaseStatuses,
|
CaseStatuses,
|
||||||
CaseUserActionAttributes,
|
CaseUserActionAttributes,
|
||||||
|
@ -39,6 +41,7 @@ import {
|
||||||
} from '../../common/constants';
|
} from '../../common/constants';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
createCaseSavedObjectResponse,
|
||||||
createConnectorObject,
|
createConnectorObject,
|
||||||
createExternalService,
|
createExternalService,
|
||||||
createJiraConnector,
|
createJiraConnector,
|
||||||
|
@ -51,6 +54,9 @@ import {
|
||||||
updatedCases,
|
updatedCases,
|
||||||
comment,
|
comment,
|
||||||
attachments,
|
attachments,
|
||||||
|
updatedAssigneesCases,
|
||||||
|
originalCasesWithAssignee,
|
||||||
|
updatedTagsCases,
|
||||||
} from './mocks';
|
} from './mocks';
|
||||||
import { CaseUserActionService, transformFindResponseToExternalModel } from '.';
|
import { CaseUserActionService, transformFindResponseToExternalModel } from '.';
|
||||||
import { PersistableStateAttachmentTypeRegistry } from '../../attachment_framework/persistable_state_registry';
|
import { PersistableStateAttachmentTypeRegistry } from '../../attachment_framework/persistable_state_registry';
|
||||||
|
@ -667,6 +673,7 @@ describe('CaseUserActionService', () => {
|
||||||
type: 'create_case',
|
type: 'create_case',
|
||||||
owner: 'securitySolution',
|
owner: 'securitySolution',
|
||||||
payload: {
|
payload: {
|
||||||
|
assignees: [{ uid: '1' }],
|
||||||
connector: {
|
connector: {
|
||||||
fields: {
|
fields: {
|
||||||
category: 'Denial of Service',
|
category: 'Denial of Service',
|
||||||
|
@ -1068,6 +1075,267 @@ describe('CaseUserActionService', () => {
|
||||||
{ refresh: undefined }
|
{ 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', () => {
|
describe('bulkCreateAttachmentDeletion', () => {
|
||||||
|
|
|
@ -27,12 +27,16 @@ import {
|
||||||
isCommentUserAction,
|
isCommentUserAction,
|
||||||
} from '../../../common/utils/user_actions';
|
} from '../../../common/utils/user_actions';
|
||||||
import {
|
import {
|
||||||
|
ActionOperationValues,
|
||||||
Actions,
|
Actions,
|
||||||
ActionTypes,
|
ActionTypes,
|
||||||
|
ActionTypeValues,
|
||||||
CaseAttributes,
|
CaseAttributes,
|
||||||
CaseUserActionAttributes,
|
CaseUserActionAttributes,
|
||||||
CaseUserActionAttributesWithoutConnectorId,
|
CaseUserActionAttributesWithoutConnectorId,
|
||||||
CaseUserActionResponse,
|
CaseUserActionResponse,
|
||||||
|
CaseUserProfile,
|
||||||
|
CaseAssignees,
|
||||||
CommentRequest,
|
CommentRequest,
|
||||||
NONE_CONNECTOR_ID,
|
NONE_CONNECTOR_ID,
|
||||||
User,
|
User,
|
||||||
|
@ -53,13 +57,19 @@ import {
|
||||||
} from '../../common/constants';
|
} from '../../common/constants';
|
||||||
import { findConnectorIdReference } from '../transform';
|
import { findConnectorIdReference } from '../transform';
|
||||||
import { buildFilter, combineFilters, arraysDifference } from '../../client/utils';
|
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 { BuilderFactory } from './builder_factory';
|
||||||
import { defaultSortField, isCommentRequestTypeExternalReferenceSO } from '../../common/utils';
|
import { defaultSortField, isCommentRequestTypeExternalReferenceSO } from '../../common/utils';
|
||||||
import { PersistableStateAttachmentTypeRegistry } from '../../attachment_framework/persistable_state_registry';
|
import { PersistableStateAttachmentTypeRegistry } from '../../attachment_framework/persistable_state_registry';
|
||||||
import { injectPersistableReferencesToSO } from '../../attachment_framework/so_references';
|
import { injectPersistableReferencesToSO } from '../../attachment_framework/so_references';
|
||||||
import { IndexRefresh } from '../types';
|
import { IndexRefresh } from '../types';
|
||||||
import { isStringArray } from './type_guards';
|
import { isAssigneesArray, isStringArray } from './type_guards';
|
||||||
|
|
||||||
interface GetCaseUserActionArgs extends ClientArgs {
|
interface GetCaseUserActionArgs extends ClientArgs {
|
||||||
caseId: string;
|
caseId: string;
|
||||||
|
@ -92,6 +102,11 @@ interface GetUserActionItemByDifference extends CommonUserActionArgs {
|
||||||
newValue: unknown;
|
newValue: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TypedUserActionDiffedItems<T> extends GetUserActionItemByDifference {
|
||||||
|
originalValue: T[];
|
||||||
|
newValue: T[];
|
||||||
|
}
|
||||||
|
|
||||||
interface BulkCreateBulkUpdateCaseUserActions extends ClientArgs, IndexRefresh {
|
interface BulkCreateBulkUpdateCaseUserActions extends ClientArgs, IndexRefresh {
|
||||||
originalCases: Array<SavedObject<CaseAttributes>>;
|
originalCases: Array<SavedObject<CaseAttributes>>;
|
||||||
updatedCases: Array<SavedObjectsUpdateResponse<CaseAttributes>>;
|
updatedCases: Array<SavedObjectsUpdateResponse<CaseAttributes>>;
|
||||||
|
@ -106,6 +121,10 @@ type CreateUserActionClient<T extends keyof BuilderParameters> = CreateUserActio
|
||||||
CommonUserActionArgs &
|
CommonUserActionArgs &
|
||||||
IndexRefresh;
|
IndexRefresh;
|
||||||
|
|
||||||
|
type CreatePayloadFunction<Item, ActionType extends ActionTypeValues> = (
|
||||||
|
items: Item[]
|
||||||
|
) => UserActionParameters<ActionType>['payload'];
|
||||||
|
|
||||||
export class CaseUserActionService {
|
export class CaseUserActionService {
|
||||||
private static readonly userActionFieldsAllowed: Set<string> = new Set(Object.keys(ActionTypes));
|
private static readonly userActionFieldsAllowed: Set<string> = new Set(Object.keys(ActionTypes));
|
||||||
|
|
||||||
|
@ -120,55 +139,26 @@ export class CaseUserActionService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private getUserActionItemByDifference({
|
private getUserActionItemByDifference(
|
||||||
field,
|
params: GetUserActionItemByDifference
|
||||||
originalValue,
|
): BuilderReturnValue[] {
|
||||||
newValue,
|
const { field, originalValue, newValue, caseId, owner, user } = params;
|
||||||
caseId,
|
|
||||||
owner,
|
|
||||||
user,
|
|
||||||
}: GetUserActionItemByDifference): BuilderReturnValue[] {
|
|
||||||
if (!CaseUserActionService.userActionFieldsAllowed.has(field)) {
|
if (!CaseUserActionService.userActionFieldsAllowed.has(field)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
} else if (
|
||||||
|
field === ActionTypes.assignees &&
|
||||||
if (field === ActionTypes.tags && isStringArray(originalValue) && isStringArray(newValue)) {
|
isAssigneesArray(originalValue) &&
|
||||||
const tagsUserActionBuilder = this.builderFactory.getBuilder(ActionTypes.tags);
|
isAssigneesArray(newValue)
|
||||||
const compareValues = arraysDifference(originalValue, newValue);
|
) {
|
||||||
const userActions = [];
|
return this.buildAssigneesUserActions({ ...params, originalValue, newValue });
|
||||||
|
} else if (
|
||||||
if (compareValues && compareValues.addedItems.length > 0) {
|
field === ActionTypes.tags &&
|
||||||
const tagAddUserAction = tagsUserActionBuilder?.build({
|
isStringArray(originalValue) &&
|
||||||
action: Actions.add,
|
isStringArray(newValue)
|
||||||
caseId,
|
) {
|
||||||
user,
|
return this.buildTagsUserActions({ ...params, originalValue, newValue });
|
||||||
owner,
|
} else if (isUserActionType(field) && newValue != null) {
|
||||||
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) {
|
|
||||||
const userActionBuilder = this.builderFactory.getBuilder(ActionTypes[field]);
|
const userActionBuilder = this.builderFactory.getBuilder(ActionTypes[field]);
|
||||||
const fieldUserAction = userActionBuilder?.build({
|
const fieldUserAction = userActionBuilder?.build({
|
||||||
caseId,
|
caseId,
|
||||||
|
@ -183,6 +173,85 @@ export class CaseUserActionService {
|
||||||
return [];
|
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({
|
public async bulkCreateCaseDeletion({
|
||||||
unsecuredSavedObjectsClient,
|
unsecuredSavedObjectsClient,
|
||||||
cases,
|
cases,
|
||||||
|
|
|
@ -7,11 +7,17 @@
|
||||||
|
|
||||||
import { CASE_SAVED_OBJECT } from '../../../common/constants';
|
import { CASE_SAVED_OBJECT } from '../../../common/constants';
|
||||||
import { SECURITY_SOLUTION_OWNER } from '../../../common';
|
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 { createCaseSavedObjectResponse } from '../test_utils';
|
||||||
import { transformSavedObjectToExternalModel } from '../cases/transform';
|
import { transformSavedObjectToExternalModel } from '../cases/transform';
|
||||||
|
|
||||||
export const casePayload = {
|
export const casePayload: CasePostRequest = {
|
||||||
title: 'Case SIR',
|
title: 'Case SIR',
|
||||||
tags: ['sir'],
|
tags: ['sir'],
|
||||||
description: 'testing sir',
|
description: 'testing sir',
|
||||||
|
@ -32,6 +38,7 @@ export const casePayload = {
|
||||||
settings: { syncAlerts: true },
|
settings: { syncAlerts: true },
|
||||||
severity: CaseSeverity.LOW,
|
severity: CaseSeverity.LOW,
|
||||||
owner: SECURITY_SOLUTION_OWNER,
|
owner: SECURITY_SOLUTION_OWNER,
|
||||||
|
assignees: [{ uid: '1' }],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const externalService = {
|
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 = {
|
export const comment = {
|
||||||
comment: 'a comment',
|
comment: 'a comment',
|
||||||
type: CommentType.user as const,
|
type: CommentType.user as const,
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { isObjectArray, isStringArray } from './type_guards';
|
import { isAssigneesArray, isStringArray } from './type_guards';
|
||||||
|
|
||||||
describe('type_guards', () => {
|
describe('type_guards', () => {
|
||||||
describe('isStringArray', () => {
|
describe('isStringArray', () => {
|
||||||
|
@ -30,25 +30,33 @@ describe('type_guards', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('isObjectArray', () => {
|
describe('isAssigneesArray', () => {
|
||||||
it('returns true when the value is an empty array', () => {
|
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', () => {
|
it('returns false when the value is not an array of assignees', () => {
|
||||||
expect(isObjectArray([{ a: '1' }])).toBeTruthy();
|
expect(isAssigneesArray([{ a: '1' }])).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns true when the value is an array of multiple strings', () => {
|
it('returns false when the value is an array of assignees and non assignee objects', () => {
|
||||||
expect(isObjectArray([{ a: 'a' }, { b: 'b' }])).toBeTruthy();
|
expect(isAssigneesArray([{ uid: '1' }, { hi: '2' }])).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns false when the value is an array of strings and numbers', () => {
|
it('returns true when the value is an array of a single assignee', () => {
|
||||||
expect(isObjectArray([{ a: 'a' }, 1])).toBeFalsy();
|
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', () => {
|
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.
|
* 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[] => {
|
export const isStringArray = (value: unknown): value is string[] => {
|
||||||
return Array.isArray(value) && value.every((val) => isString(val));
|
return Array.isArray(value) && value.every((val) => isString(val));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isObjectArray = (value: unknown): value is Array<Record<string, unknown>> => {
|
export const isAssigneesArray = (value: unknown): value is CaseAssignees => {
|
||||||
return Array.isArray(value) && value.every((val) => isPlainObject(val));
|
return CaseAssigneesRt.is(value);
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { SavedObjectReference } from '@kbn/core/server';
|
import { SavedObjectReference } from '@kbn/core/server';
|
||||||
|
import { CaseAssignees } from '../../../common/api/cases/assignee';
|
||||||
import {
|
import {
|
||||||
CasePostRequest,
|
CasePostRequest,
|
||||||
CaseSettings,
|
CaseSettings,
|
||||||
|
@ -36,6 +37,9 @@ export interface BuilderParameters {
|
||||||
tags: {
|
tags: {
|
||||||
parameters: { payload: { tags: string[] } };
|
parameters: { payload: { tags: string[] } };
|
||||||
};
|
};
|
||||||
|
assignees: {
|
||||||
|
parameters: { payload: { assignees: CaseAssignees } };
|
||||||
|
};
|
||||||
pushed: {
|
pushed: {
|
||||||
parameters: {
|
parameters: {
|
||||||
payload: {
|
payload: {
|
||||||
|
|
|
@ -23,7 +23,14 @@ export interface FixtureStartDeps {
|
||||||
export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, FixtureStartDeps> {
|
export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, FixtureStartDeps> {
|
||||||
public setup(core: CoreSetup<FixtureStartDeps>, deps: FixtureSetupDeps) {
|
public setup(core: CoreSetup<FixtureStartDeps>, deps: FixtureSetupDeps) {
|
||||||
const { features } = deps;
|
const { features } = deps;
|
||||||
|
this.registerFeatures(features);
|
||||||
|
}
|
||||||
|
|
||||||
|
public start() {}
|
||||||
|
|
||||||
|
public stop() {}
|
||||||
|
|
||||||
|
private registerFeatures(features: FeaturesPluginSetup) {
|
||||||
features.registerKibanaFeature({
|
features.registerKibanaFeature({
|
||||||
id: 'securitySolutionFixture',
|
id: 'securitySolutionFixture',
|
||||||
name: '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 { FtrProviderContext as CommonFtrProviderContext } from '../../ftr_provider_context';
|
||||||
import { Role, User, UserInfo } from './types';
|
import { Role, User, UserInfo } from './types';
|
||||||
import { users } from './users';
|
import { obsOnly, secOnly, secOnlyNoDelete, secOnlyRead, users } from './users';
|
||||||
import { roles } from './roles';
|
import { roles } from './roles';
|
||||||
import { spaces } from './spaces';
|
import { spaces } from './spaces';
|
||||||
|
import { loginUsers } from '../utils';
|
||||||
|
|
||||||
export const getUserInfo = (user: User): UserInfo => ({
|
export const getUserInfo = (user: User): UserInfo => ({
|
||||||
username: user.username,
|
username: user.username,
|
||||||
|
@ -103,3 +104,12 @@ export const deleteSpacesAndUsers = async (getService: CommonFtrProviderContext[
|
||||||
await deleteSpaces(getService);
|
await deleteSpaces(getService);
|
||||||
await deleteUsersAndRoles(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,
|
syncAlerts: true,
|
||||||
},
|
},
|
||||||
owner: 'securitySolutionFixture',
|
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 { 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
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default ({ loadTestFile, getService }: FtrProviderContext): void => {
|
export default ({ loadTestFile, getService }: FtrProviderContext): void => {
|
||||||
describe('cases security and spaces enabled: basic', function () {
|
describe('cases security and spaces enabled: basic', function () {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
await createSpacesAndUsers(getService);
|
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 () => {
|
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,
|
...restCreateCase,
|
||||||
status: CaseStatuses.open,
|
status: CaseStatuses.open,
|
||||||
severity: CaseSeverity.LOW,
|
severity: CaseSeverity.LOW,
|
||||||
|
assignees: [],
|
||||||
});
|
});
|
||||||
expect(restParsedConnector).to.eql(restConnector);
|
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,
|
owner: postedCase.owner,
|
||||||
status: CaseStatuses.open,
|
status: CaseStatuses.open,
|
||||||
severity: CaseSeverity.LOW,
|
severity: CaseSeverity.LOW,
|
||||||
|
assignees: [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -25,6 +25,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => {
|
||||||
loadTestFile(require.resolve('./cases/get_case'));
|
loadTestFile(require.resolve('./cases/get_case'));
|
||||||
loadTestFile(require.resolve('./cases/patch_cases'));
|
loadTestFile(require.resolve('./cases/patch_cases'));
|
||||||
loadTestFile(require.resolve('./cases/post_case'));
|
loadTestFile(require.resolve('./cases/post_case'));
|
||||||
|
loadTestFile(require.resolve('./cases/assignees'));
|
||||||
loadTestFile(require.resolve('./cases/resolve_case'));
|
loadTestFile(require.resolve('./cases/resolve_case'));
|
||||||
loadTestFile(require.resolve('./cases/reporters/get_reporters'));
|
loadTestFile(require.resolve('./cases/reporters/get_reporters'));
|
||||||
loadTestFile(require.resolve('./cases/status/get_status'));
|
loadTestFile(require.resolve('./cases/status/get_status'));
|
||||||
|
|
|
@ -9,10 +9,7 @@ import expect from '@kbn/expect';
|
||||||
import { loginUsers, suggestUserProfiles } from '../../../../common/lib/utils';
|
import { loginUsers, suggestUserProfiles } from '../../../../common/lib/utils';
|
||||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||||
import {
|
import {
|
||||||
secOnly,
|
|
||||||
superUser,
|
superUser,
|
||||||
secOnlyNoDelete,
|
|
||||||
secOnlyRead,
|
|
||||||
obsOnly,
|
obsOnly,
|
||||||
noCasesPrivilegesSpace1,
|
noCasesPrivilegesSpace1,
|
||||||
} from '../../../../common/lib/authentication/users';
|
} from '../../../../common/lib/authentication/users';
|
||||||
|
@ -24,13 +21,6 @@ export default function ({ getService }: FtrProviderContext) {
|
||||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||||
|
|
||||||
describe('suggest_user_profiles', () => {
|
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 () => {
|
it('finds the profile for the user without deletion privileges', async () => {
|
||||||
const profiles = await suggestUserProfiles({
|
const profiles = await suggestUserProfiles({
|
||||||
supertest: supertestWithoutAuth,
|
supertest: supertestWithoutAuth,
|
||||||
|
|
|
@ -197,6 +197,37 @@ export default ({ getService }: FtrProviderContext): void => {
|
||||||
expect(deleteTagsUserAction.payload).to.eql({ tags: ['defacement'] });
|
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 () => {
|
it('creates an update title user action', async () => {
|
||||||
const newTitle = 'Such a great title';
|
const newTitle = 'Such a great title';
|
||||||
const theCase = await createCase(supertest, postCaseReq);
|
const theCase = await createCase(supertest, postCaseReq);
|
||||||
|
|
|
@ -78,6 +78,7 @@ export default function createGetTests({ getService }: FtrProviderContext) {
|
||||||
id: 'none',
|
id: 'none',
|
||||||
},
|
},
|
||||||
severity: 'low',
|
severity: 'low',
|
||||||
|
assignees: [],
|
||||||
owner: 'securitySolution',
|
owner: 'securitySolution',
|
||||||
settings: { syncAlerts: true },
|
settings: { syncAlerts: true },
|
||||||
},
|
},
|
||||||
|
@ -191,6 +192,7 @@ export default function createGetTests({ getService }: FtrProviderContext) {
|
||||||
syncAlerts: true,
|
syncAlerts: true,
|
||||||
},
|
},
|
||||||
severity: 'low',
|
severity: 'low',
|
||||||
|
assignees: [],
|
||||||
owner: 'securitySolution',
|
owner: 'securitySolution',
|
||||||
},
|
},
|
||||||
type: 'create_case',
|
type: 'create_case',
|
||||||
|
@ -298,6 +300,7 @@ export default function createGetTests({ getService }: FtrProviderContext) {
|
||||||
syncAlerts: true,
|
syncAlerts: true,
|
||||||
},
|
},
|
||||||
severity: 'low',
|
severity: 'low',
|
||||||
|
assignees: [],
|
||||||
owner: 'securitySolution',
|
owner: 'securitySolution',
|
||||||
},
|
},
|
||||||
type: 'create_case',
|
type: 'create_case',
|
||||||
|
@ -327,6 +330,7 @@ export default function createGetTests({ getService }: FtrProviderContext) {
|
||||||
syncAlerts: true,
|
syncAlerts: true,
|
||||||
},
|
},
|
||||||
severity: 'low',
|
severity: 'low',
|
||||||
|
assignees: [],
|
||||||
},
|
},
|
||||||
type: 'create_case',
|
type: 'create_case',
|
||||||
action_id: 'b3094de0-005e-11ec-91f1-6daf2ab59fb5',
|
action_id: 'b3094de0-005e-11ec-91f1-6daf2ab59fb5',
|
||||||
|
@ -372,6 +376,7 @@ export default function createGetTests({ getService }: FtrProviderContext) {
|
||||||
},
|
},
|
||||||
owner: 'securitySolution',
|
owner: 'securitySolution',
|
||||||
severity: 'low',
|
severity: 'low',
|
||||||
|
assignees: [],
|
||||||
},
|
},
|
||||||
type: 'create_case',
|
type: 'create_case',
|
||||||
action_id: 'e7882d70-005e-11ec-91f1-6daf2ab59fb5',
|
action_id: 'e7882d70-005e-11ec-91f1-6daf2ab59fb5',
|
||||||
|
@ -744,6 +749,7 @@ export default function createGetTests({ getService }: FtrProviderContext) {
|
||||||
title: 'User actions',
|
title: 'User actions',
|
||||||
owner: 'securitySolution',
|
owner: 'securitySolution',
|
||||||
severity: 'low',
|
severity: 'low',
|
||||||
|
assignees: [],
|
||||||
},
|
},
|
||||||
type: 'create_case',
|
type: 'create_case',
|
||||||
},
|
},
|
||||||
|
@ -1109,6 +1115,7 @@ export default function createGetTests({ getService }: FtrProviderContext) {
|
||||||
title: 'User actions',
|
title: 'User actions',
|
||||||
owner: 'securitySolution',
|
owner: 'securitySolution',
|
||||||
severity: 'low',
|
severity: 'low',
|
||||||
|
assignees: [],
|
||||||
},
|
},
|
||||||
type: 'create_case',
|
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 { 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
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default ({ loadTestFile, getService }: FtrProviderContext): void => {
|
export default ({ loadTestFile, getService }: FtrProviderContext): void => {
|
||||||
describe('cases security and spaces enabled: trial', function () {
|
describe('cases security and spaces enabled: trial', function () {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
await createSpacesAndUsers(getService);
|
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 () => {
|
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.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { CaseConnector, CasePostRequest } from '@kbn/cases-plugin/common/api';
|
||||||
import uuid from 'uuid';
|
import uuid from 'uuid';
|
||||||
|
|
||||||
export function generateRandomCaseWithoutConnector() {
|
export function generateRandomCaseWithoutConnector(): CasePostRequest {
|
||||||
return {
|
return {
|
||||||
title: 'random-' + uuid.v4(),
|
title: 'random-' + uuid.v4(),
|
||||||
tags: ['test', uuid.v4()],
|
tags: ['test', uuid.v4()],
|
||||||
|
@ -17,7 +18,7 @@ export function generateRandomCaseWithoutConnector() {
|
||||||
name: 'none',
|
name: 'none',
|
||||||
type: '.none',
|
type: '.none',
|
||||||
fields: null,
|
fields: null,
|
||||||
},
|
} as CaseConnector,
|
||||||
settings: {
|
settings: {
|
||||||
syncAlerts: false,
|
syncAlerts: false,
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue