mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Cases] Adding _find API for user actions (#148861)
This PR adds a new find API for retrieving a subset of the user actions for a case. Issue: https://github.com/elastic/kibana/issues/134344 ``` GET /api/cases/<case_id>/user_actions/_find Query Paramaters { types?: Array of "assignees" | "comment" | "connector" | "description" | "pushed" | "tags" | "title" | "status" | "settings" | "severity" | "create_case" | "delete_case" | "action" | "alert" | "user" | "attachment" sortOrder?: "asc" | "desc" page?: number as a string perPage?: number as a string } ``` <details><summary>Example request and response</summary> Request ``` curl --location --request GET 'http://localhost:5601/api/cases/8df5fe00-96b1-11ed-9341-471c9630b5ec/user_actions/_find?types=create_case&sortOrder=asc' \ --header 'kbn-xsrf: hello' \ --header 'Authorization: Basic ZWxhc3RpYzpjaGFuZ2VtZQ==' \ --data-raw '' ``` Response ``` { "userActions": [ { "created_at": "2023-01-17T21:54:45.527Z", "created_by": { "username": "elastic", "full_name": null, "email": null, "profile_uid": "u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0" }, "owner": "cases", "action": "create", "payload": { "title": "Awesome case", "tags": [], "severity": "low", "description": "super", "assignees": [], "connector": { "name": "none", "type": ".none", "fields": null, "id": "none" }, "settings": { "syncAlerts": false }, "owner": "cases", "status": "open" }, "type": "create_case", "id": "8e121180-96b1-11ed-9341-471c9630b5ec", "case_id": "8df5fe00-96b1-11ed-9341-471c9630b5ec", "comment_id": null } ], "page": 1, "perPage": 20, "total": 1 } ``` </details> ## Notable Changes - Created the new `_find` route - Created a new `UserActionFinder` class and moved the find* methods from the `index.ts` file into there as well as the new find logic - Extracted the transform logic to its own file since its shared between multiple files now - Extracted the user action related integration test functions to the `user_action.ts` utility file Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: lcawl <lcawley@elastic.co>
This commit is contained in:
parent
a31328cedb
commit
a78fece18b
66 changed files with 3948 additions and 1021 deletions
|
@ -23,6 +23,7 @@ Any modifications made to this file will be overwritten.
|
|||
<li><a href="#deleteCase"><code><span class="http-method">delete</span> /s/{spaceId}/api/cases</code></a></li>
|
||||
<li><a href="#deleteCaseComment"><code><span class="http-method">delete</span> /s/{spaceId}/api/cases/{caseId}/comments/{commentId}</code></a></li>
|
||||
<li><a href="#deleteCaseComments"><code><span class="http-method">delete</span> /s/{spaceId}/api/cases/{caseId}/comments</code></a></li>
|
||||
<li><a href="#findCaseActivity"><code><span class="http-method">get</span> /s/{spaceId}/api/cases/{caseId}/user_actions/_find</code></a></li>
|
||||
<li><a href="#getAllCaseComments"><code><span class="http-method">get</span> /s/{spaceId}/api/cases/{caseId}/comments</code></a></li>
|
||||
<li><a href="#getCase"><code><span class="http-method">get</span> /s/{spaceId}/api/cases/{caseId}</code></a></li>
|
||||
<li><a href="#getCaseActivity"><code><span class="http-method">get</span> /s/{spaceId}/api/cases/{caseId}/user_actions</code></a></li>
|
||||
|
@ -435,6 +436,102 @@ Any modifications made to this file will be overwritten.
|
|||
<a href="#4xx_response">4xx_response</a>
|
||||
</div> <!-- method -->
|
||||
<hr/>
|
||||
<div class="method"><a name="findCaseActivity"/>
|
||||
<div class="method-path">
|
||||
<a class="up" href="#__Methods">Up</a>
|
||||
<pre class="get"><code class="huge"><span class="http-method">get</span> /s/{spaceId}/api/cases/{caseId}/user_actions/_find</code></pre></div>
|
||||
<div class="method-summary">Finds user activity for a case. (<span class="nickname">findCaseActivity</span>)</div>
|
||||
<div class="method-notes">You must have <code>read</code> privileges for the <strong>Cases</strong> feature in the <strong>Management</strong>, <strong>Observability</strong>, or <strong>Security</strong> section of the Kibana feature privileges, depending on the owner of the case you're seeking.</div>
|
||||
|
||||
<h3 class="field-label">Path parameters</h3>
|
||||
<div class="field-items">
|
||||
<div class="param">caseId (required)</div>
|
||||
|
||||
<div class="param-desc"><span class="param-type">Path Parameter</span> — The identifier for the case. To retrieve case IDs, use the find cases API. All non-ASCII characters must be URL encoded. default: null </div><div class="param">spaceId (required)</div>
|
||||
|
||||
<div class="param-desc"><span class="param-type">Path Parameter</span> — An identifier for the space. If <code>/s/</code> and the identifier are omitted from the path, the default space is used. default: null </div>
|
||||
</div> <!-- field-items -->
|
||||
|
||||
|
||||
|
||||
|
||||
<h3 class="field-label">Query parameters</h3>
|
||||
<div class="field-items">
|
||||
<div class="param">page (optional)</div>
|
||||
|
||||
<div class="param-desc"><span class="param-type">Query Parameter</span> — The page number to return. default: 1 </div><div class="param">perPage (optional)</div>
|
||||
|
||||
<div class="param-desc"><span class="param-type">Query Parameter</span> — The number of user actions to return per page. default: 20 </div><div class="param">sortOrder (optional)</div>
|
||||
|
||||
<div class="param-desc"><span class="param-type">Query Parameter</span> — Determines the sort order. default: asc </div><div class="param">types (optional)</div>
|
||||
|
||||
<div class="param-desc"><span class="param-type">Query Parameter</span> — Determines the types of user actions to return. default: null </div>
|
||||
</div> <!-- field-items -->
|
||||
|
||||
|
||||
<h3 class="field-label">Return type</h3>
|
||||
<div class="return-type">
|
||||
<a href="#findCaseActivity_200_response">findCaseActivity_200_response</a>
|
||||
|
||||
</div>
|
||||
|
||||
<!--Todo: process Response Object and its headers, schema, examples -->
|
||||
|
||||
<h3 class="field-label">Example data</h3>
|
||||
<div class="example-data-content-type">Content-Type: application/json</div>
|
||||
<pre class="example"><code>{
|
||||
"userActions" : [ {
|
||||
"owner" : "cases",
|
||||
"case_id" : "22df07d0-03b1-11ed-920c-974bfa104448",
|
||||
"action" : "create",
|
||||
"created_at" : "2022-05-13T09:16:17.416Z",
|
||||
"id" : "22fd3e30-03b1-11ed-920c-974bfa104448",
|
||||
"comment_id" : "578608d0-03b1-11ed-920c-974bfa104448",
|
||||
"type" : "create_case",
|
||||
"created_by" : {
|
||||
"full_name" : "full_name",
|
||||
"profile_uid" : "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0",
|
||||
"email" : "email",
|
||||
"username" : "elastic"
|
||||
},
|
||||
"version" : "WzM1ODg4LDFd"
|
||||
}, {
|
||||
"owner" : "cases",
|
||||
"case_id" : "22df07d0-03b1-11ed-920c-974bfa104448",
|
||||
"action" : "create",
|
||||
"created_at" : "2022-05-13T09:16:17.416Z",
|
||||
"id" : "22fd3e30-03b1-11ed-920c-974bfa104448",
|
||||
"comment_id" : "578608d0-03b1-11ed-920c-974bfa104448",
|
||||
"type" : "create_case",
|
||||
"created_by" : {
|
||||
"full_name" : "full_name",
|
||||
"profile_uid" : "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0",
|
||||
"email" : "email",
|
||||
"username" : "elastic"
|
||||
},
|
||||
"version" : "WzM1ODg4LDFd"
|
||||
} ],
|
||||
"total" : 1,
|
||||
"perPage" : 6,
|
||||
"page" : 0
|
||||
}</code></pre>
|
||||
|
||||
<h3 class="field-label">Produces</h3>
|
||||
This API call produces the following media types according to the <span class="header">Accept</span> request header;
|
||||
the media type will be conveyed by the <span class="header">Content-Type</span> response header.
|
||||
<ul>
|
||||
<li><code>application/json</code></li>
|
||||
</ul>
|
||||
|
||||
<h3 class="field-label">Responses</h3>
|
||||
<h4 class="field-label">200</h4>
|
||||
Indicates a successful call.
|
||||
<a href="#findCaseActivity_200_response">findCaseActivity_200_response</a>
|
||||
<h4 class="field-label">401</h4>
|
||||
Authorization information is missing or invalid.
|
||||
<a href="#4xx_response">4xx_response</a>
|
||||
</div> <!-- method -->
|
||||
<hr/>
|
||||
<div class="method"><a name="getAllCaseComments"/>
|
||||
<div class="method-path">
|
||||
<a class="up" href="#__Methods">Up</a>
|
||||
|
@ -1162,7 +1259,7 @@ Any modifications made to this file will be overwritten.
|
|||
|
||||
<div class="param-desc"><span class="param-type">Query Parameter</span> — The page number to return. default: 1 </div><div class="param">perPage (optional)</div>
|
||||
|
||||
<div class="param-desc"><span class="param-type">Query Parameter</span> — The number of rules to return per page. default: 20 </div><div class="param">reporters (optional)</div>
|
||||
<div class="param-desc"><span class="param-type">Query Parameter</span> — The number of cases to return per page. default: 20 </div><div class="param">reporters (optional)</div>
|
||||
|
||||
<div class="param-desc"><span class="param-type">Query Parameter</span> — Filters the returned cases by the user name of the reporter. default: null </div><div class="param">search (optional)</div>
|
||||
|
||||
|
@ -1998,6 +2095,7 @@ Any modifications made to this file will be overwritten.
|
|||
<li><a href="#create_case_request"><code>create_case_request</code> - Create case request</a></li>
|
||||
<li><a href="#create_case_request_connector"><code>create_case_request_connector</code> - </a></li>
|
||||
<li><a href="#external_service"><code>external_service</code> - </a></li>
|
||||
<li><a href="#findCaseActivity_200_response"><code>findCaseActivity_200_response</code> - </a></li>
|
||||
<li><a href="#getCaseComment_200_response"><code>getCaseComment_200_response</code> - </a></li>
|
||||
<li><a href="#getCaseConfiguration_200_response_inner"><code>getCaseConfiguration_200_response_inner</code> - </a></li>
|
||||
<li><a href="#getCaseConfiguration_200_response_inner_connector"><code>getCaseConfiguration_200_response_inner_connector</code> - </a></li>
|
||||
|
@ -2043,6 +2141,7 @@ Any modifications made to this file will be overwritten.
|
|||
<li><a href="#update_case_request"><code>update_case_request</code> - Update case request</a></li>
|
||||
<li><a href="#update_case_request_cases_inner"><code>update_case_request_cases_inner</code> - </a></li>
|
||||
<li><a href="#update_user_comment_request_properties"><code>update_user_comment_request_properties</code> - Update case comment request properties for user comments</a></li>
|
||||
<li><a href="#user_actions_find_response_properties"><code>user_actions_find_response_properties</code> - </a></li>
|
||||
<li><a href="#user_actions_response_properties"><code>user_actions_response_properties</code> - </a></li>
|
||||
<li><a href="#user_actions_response_properties_created_by"><code>user_actions_response_properties_created_by</code> - </a></li>
|
||||
<li><a href="#user_actions_response_properties_payload"><code>user_actions_response_properties_payload</code> - </a></li>
|
||||
|
@ -2450,6 +2549,16 @@ Any modifications made to this file will be overwritten.
|
|||
<div class="param">pushed_by (optional)</div><div class="param-desc"><span class="param-type"><a href="#getCaseConfiguration_200_response_inner_updated_by">getCaseConfiguration_200_response_inner_updated_by</a></span> </div>
|
||||
</div> <!-- field-items -->
|
||||
</div>
|
||||
<div class="model">
|
||||
<h3><a name="findCaseActivity_200_response"><code>findCaseActivity_200_response</code> - </a> <a class="up" href="#__Models">Up</a></h3>
|
||||
<div class='model-description'></div>
|
||||
<div class="field-items">
|
||||
<div class="param">page (optional)</div><div class="param-desc"><span class="param-type"><a href="#integer">Integer</a></span> </div>
|
||||
<div class="param">perPage (optional)</div><div class="param-desc"><span class="param-type"><a href="#integer">Integer</a></span> </div>
|
||||
<div class="param">total (optional)</div><div class="param-desc"><span class="param-type"><a href="#integer">Integer</a></span> </div>
|
||||
<div class="param">userActions (optional)</div><div class="param-desc"><span class="param-type"><a href="#user_actions_find_response_properties">array[user_actions_find_response_properties]</a></span> </div>
|
||||
</div> <!-- field-items -->
|
||||
</div>
|
||||
<div class="model">
|
||||
<h3><a name="getCaseComment_200_response"><code>getCaseComment_200_response</code> - </a> <a class="up" href="#__Models">Up</a></h3>
|
||||
<div class='model-description'></div>
|
||||
|
@ -2887,6 +2996,22 @@ Any modifications made to this file will be overwritten.
|
|||
<div class="param">version </div><div class="param-desc"><span class="param-type"><a href="#string">String</a></span> The current comment version. To retrieve version values, use the get comments API. </div>
|
||||
</div> <!-- field-items -->
|
||||
</div>
|
||||
<div class="model">
|
||||
<h3><a name="user_actions_find_response_properties"><code>user_actions_find_response_properties</code> - </a> <a class="up" href="#__Models">Up</a></h3>
|
||||
<div class='model-description'></div>
|
||||
<div class="field-items">
|
||||
<div class="param">action </div><div class="param-desc"><span class="param-type"><a href="#actions">actions</a></span> </div>
|
||||
<div class="param">case_id </div><div class="param-desc"><span class="param-type"><a href="#string">String</a></span> </div>
|
||||
<div class="param">comment_id </div><div class="param-desc"><span class="param-type"><a href="#string">String</a></span> </div>
|
||||
<div class="param">created_at </div><div class="param-desc"><span class="param-type"><a href="#DateTime">Date</a></span> format: date-time</div>
|
||||
<div class="param">created_by </div><div class="param-desc"><span class="param-type"><a href="#user_actions_response_properties_created_by">user_actions_response_properties_created_by</a></span> </div>
|
||||
<div class="param">id </div><div class="param-desc"><span class="param-type"><a href="#string">String</a></span> </div>
|
||||
<div class="param">owner </div><div class="param-desc"><span class="param-type"><a href="#owners">owners</a></span> </div>
|
||||
<div class="param">payload </div><div class="param-desc"><span class="param-type"><a href="#user_actions_response_properties_payload">user_actions_response_properties_payload</a></span> </div>
|
||||
<div class="param">version </div><div class="param-desc"><span class="param-type"><a href="#string">String</a></span> </div>
|
||||
<div class="param">type </div><div class="param-desc"><span class="param-type"><a href="#action_types">action_types</a></span> </div>
|
||||
</div> <!-- field-items -->
|
||||
</div>
|
||||
<div class="model">
|
||||
<h3><a name="user_actions_response_properties"><code>user_actions_response_properties</code> - </a> <a class="up" href="#__Models">Up</a></h3>
|
||||
<div class='model-description'></div>
|
||||
|
|
|
@ -400,10 +400,19 @@ Refer to the corresponding {es} logs for potential write errors.
|
|||
| `success` | User has accessed the user activity of a case.
|
||||
| `failure` | User is not authorized to access the user activity of a case.
|
||||
|
||||
|
||||
.2+| `case_user_actions_find`
|
||||
| `success` | User has accessed the user activity of a case as part of a search operation.
|
||||
| `failure` | User is not authorized to access the user activity of a case.
|
||||
|
||||
.2+| `case_user_action_get_metrics`
|
||||
| `success` | User has accessed metrics for the user activity of a case.
|
||||
| `failure` | User is not authorized to access metrics for the user activity of a case.
|
||||
|
||||
.2+| `case_connectors_get`
|
||||
| `success` | User has accessed the connectors of a case.
|
||||
| `failure` | User is not authorized to access the connectors of a case.
|
||||
|
||||
3+a|
|
||||
===== Category: web
|
||||
|
||||
|
|
|
@ -8,6 +8,10 @@
|
|||
import * as rt from 'io-ts';
|
||||
import { UserRT } from '../../user';
|
||||
|
||||
/**
|
||||
* These values are used in a number of places including to define the accepted values in the
|
||||
* user_actions/_find api. These values should not be removed only new values can be added.
|
||||
*/
|
||||
export const ActionTypes = {
|
||||
assignees: 'assignees',
|
||||
comment: 'comment',
|
||||
|
@ -43,6 +47,10 @@ export const UserActionCommonAttributesRt = rt.type({
|
|||
action: ActionsRt,
|
||||
});
|
||||
|
||||
/**
|
||||
* This should only be used for the getAll route and it should be removed when the route is removed
|
||||
* @deprecated use CaseUserActionInjectedIdsRt instead
|
||||
*/
|
||||
export const CaseUserActionSavedObjectIdsRt = rt.type({
|
||||
action_id: rt.string,
|
||||
case_id: rt.string,
|
||||
|
@ -51,3 +59,16 @@ export const CaseUserActionSavedObjectIdsRt = rt.type({
|
|||
|
||||
export type UserActionWithAttributes<T> = T & rt.TypeOf<typeof UserActionCommonAttributesRt>;
|
||||
export type UserActionWithResponse<T> = T & rt.TypeOf<typeof CaseUserActionSavedObjectIdsRt>;
|
||||
|
||||
/**
|
||||
* This should be used for all user action types going forward it will be renamed to CaseUserActionSavedObjectIdsRt
|
||||
* Once the UI is switched to using the new user actions _find API
|
||||
*/
|
||||
export const CaseUserActionInjectedIdsRt = rt.type({
|
||||
comment_id: rt.union([rt.string, rt.null]),
|
||||
});
|
||||
|
||||
/**
|
||||
* Temporary type until CaseUserActionInjectedIdsRt replaces CaseUserActionSavedObjectIdsRt
|
||||
*/
|
||||
export type UserActionWithResponseInjection<T> = T & rt.TypeOf<typeof CaseUserActionInjectedIdsRt>;
|
||||
|
|
|
@ -5,23 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as rt from 'io-ts';
|
||||
|
||||
import type { ActionsRt, ActionTypeValues } from './common';
|
||||
import { UserActionCommonAttributesRt, CaseUserActionSavedObjectIdsRt } from './common';
|
||||
import { CreateCaseUserActionRt } from './create_case';
|
||||
import { DescriptionUserActionRt } from './description';
|
||||
import { CommentUserActionRt } from './comment';
|
||||
import { ConnectorUserActionRt } from './connector';
|
||||
import { PushedUserActionRt } from './pushed';
|
||||
import { TagsUserActionRt } from './tags';
|
||||
import { TitleUserActionRt } from './title';
|
||||
import { SettingsUserActionRt } from './settings';
|
||||
import { StatusUserActionRt } from './status';
|
||||
import { DeleteCaseUserActionRt } from './delete_case';
|
||||
import { SeverityUserActionRt } from './severity';
|
||||
import { AssigneesUserActionRt } from './assignees';
|
||||
|
||||
export * from './common';
|
||||
export * from './comment';
|
||||
export * from './connector';
|
||||
|
@ -34,59 +17,5 @@ export * from './status';
|
|||
export * from './tags';
|
||||
export * from './title';
|
||||
export * from './assignees';
|
||||
|
||||
const CommonUserActionsRt = rt.union([
|
||||
DescriptionUserActionRt,
|
||||
CommentUserActionRt,
|
||||
TagsUserActionRt,
|
||||
TitleUserActionRt,
|
||||
SettingsUserActionRt,
|
||||
StatusUserActionRt,
|
||||
SeverityUserActionRt,
|
||||
AssigneesUserActionRt,
|
||||
]);
|
||||
|
||||
export const UserActionsRt = rt.union([
|
||||
CommonUserActionsRt,
|
||||
CreateCaseUserActionRt,
|
||||
ConnectorUserActionRt,
|
||||
PushedUserActionRt,
|
||||
DeleteCaseUserActionRt,
|
||||
]);
|
||||
|
||||
export const UserActionsWithoutConnectorIdRt = rt.union([
|
||||
CommonUserActionsRt,
|
||||
CreateCaseUserActionRt,
|
||||
ConnectorUserActionRt,
|
||||
PushedUserActionRt,
|
||||
DeleteCaseUserActionRt,
|
||||
]);
|
||||
|
||||
const CaseUserActionBasicRt = rt.intersection([UserActionsRt, UserActionCommonAttributesRt]);
|
||||
const CaseUserActionBasicWithoutConnectorIdRt = rt.intersection([
|
||||
UserActionsWithoutConnectorIdRt,
|
||||
UserActionCommonAttributesRt,
|
||||
]);
|
||||
|
||||
const CaseUserActionResponseRt = rt.intersection([
|
||||
CaseUserActionBasicRt,
|
||||
CaseUserActionSavedObjectIdsRt,
|
||||
]);
|
||||
|
||||
export const CaseUserActionAttributesRt = CaseUserActionBasicRt;
|
||||
export const CaseUserActionsResponseRt = rt.array(CaseUserActionResponseRt);
|
||||
|
||||
export type CaseUserActionAttributes = rt.TypeOf<typeof CaseUserActionAttributesRt>;
|
||||
export type CaseUserActionAttributesWithoutConnectorId = rt.TypeOf<
|
||||
typeof CaseUserActionAttributesRt
|
||||
>;
|
||||
export type CaseUserActionsResponse = rt.TypeOf<typeof CaseUserActionsResponseRt>;
|
||||
export type CaseUserActionResponse = rt.TypeOf<typeof CaseUserActionResponseRt>;
|
||||
|
||||
export type UserAction = rt.TypeOf<typeof ActionsRt>;
|
||||
export type UserActionTypes = ActionTypeValues;
|
||||
|
||||
export type CaseUserAction = rt.TypeOf<typeof CaseUserActionBasicRt>;
|
||||
export type CaseUserActionWithoutConnectorId = rt.TypeOf<
|
||||
typeof CaseUserActionBasicWithoutConnectorIdRt
|
||||
>;
|
||||
export * from './operations';
|
||||
export * from './response';
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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 { CaseUserActionsResponseWithoutActionIdRt } from '../response';
|
||||
import { ActionTypes } from '../common';
|
||||
import { NumberFromString } from '../../../saved_object';
|
||||
|
||||
const AdditionalFilterTypes = {
|
||||
action: 'action',
|
||||
alert: 'alert',
|
||||
user: 'user',
|
||||
attachment: 'attachment',
|
||||
} as const;
|
||||
|
||||
export const FindTypes = {
|
||||
...ActionTypes,
|
||||
...AdditionalFilterTypes,
|
||||
} as const;
|
||||
|
||||
const FindTypeFieldRt = rt.keyof(FindTypes);
|
||||
|
||||
export type FindTypeField = rt.TypeOf<typeof FindTypeFieldRt>;
|
||||
|
||||
export const UserActionFindRequestRt = rt.partial({
|
||||
types: rt.array(FindTypeFieldRt),
|
||||
sortOrder: rt.union([rt.literal('desc'), rt.literal('asc')]),
|
||||
page: NumberFromString,
|
||||
perPage: NumberFromString,
|
||||
});
|
||||
|
||||
export type UserActionFindRequest = rt.TypeOf<typeof UserActionFindRequestRt>;
|
||||
|
||||
export const UserActionFindResponseRt = rt.type({
|
||||
userActions: CaseUserActionsResponseWithoutActionIdRt,
|
||||
page: rt.number,
|
||||
perPage: rt.number,
|
||||
total: rt.number,
|
||||
});
|
||||
|
||||
export type UserActionFindResponse = rt.TypeOf<typeof UserActionFindResponseRt>;
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './find';
|
111
x-pack/plugins/cases/common/api/cases/user_actions/response.ts
Normal file
111
x-pack/plugins/cases/common/api/cases/user_actions/response.ts
Normal file
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* 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 type { ActionsRt, ActionTypeValues } from './common';
|
||||
import {
|
||||
CaseUserActionInjectedIdsRt,
|
||||
CaseUserActionSavedObjectIdsRt,
|
||||
UserActionCommonAttributesRt,
|
||||
} from './common';
|
||||
import { CreateCaseUserActionRt } from './create_case';
|
||||
import { DescriptionUserActionRt } from './description';
|
||||
import { CommentUserActionRt } from './comment';
|
||||
import { ConnectorUserActionRt } from './connector';
|
||||
import { PushedUserActionRt } from './pushed';
|
||||
import { TagsUserActionRt } from './tags';
|
||||
import { TitleUserActionRt } from './title';
|
||||
import { SettingsUserActionRt } from './settings';
|
||||
import { StatusUserActionRt } from './status';
|
||||
import { DeleteCaseUserActionRt } from './delete_case';
|
||||
import { SeverityUserActionRt } from './severity';
|
||||
import { AssigneesUserActionRt } from './assignees';
|
||||
|
||||
const CommonUserActionsRt = rt.union([
|
||||
DescriptionUserActionRt,
|
||||
CommentUserActionRt,
|
||||
TagsUserActionRt,
|
||||
TitleUserActionRt,
|
||||
SettingsUserActionRt,
|
||||
StatusUserActionRt,
|
||||
SeverityUserActionRt,
|
||||
AssigneesUserActionRt,
|
||||
]);
|
||||
|
||||
export const UserActionsRt = rt.union([
|
||||
CommonUserActionsRt,
|
||||
CreateCaseUserActionRt,
|
||||
ConnectorUserActionRt,
|
||||
PushedUserActionRt,
|
||||
DeleteCaseUserActionRt,
|
||||
]);
|
||||
|
||||
export const UserActionsWithoutConnectorIdRt = rt.union([
|
||||
CommonUserActionsRt,
|
||||
CreateCaseUserActionRt,
|
||||
ConnectorUserActionRt,
|
||||
PushedUserActionRt,
|
||||
DeleteCaseUserActionRt,
|
||||
]);
|
||||
|
||||
const CaseUserActionBasicRt = rt.intersection([UserActionsRt, UserActionCommonAttributesRt]);
|
||||
const CaseUserActionBasicWithoutConnectorIdRt = rt.intersection([
|
||||
UserActionsWithoutConnectorIdRt,
|
||||
UserActionCommonAttributesRt,
|
||||
]);
|
||||
|
||||
const CaseUserActionResponseRt = rt.intersection([
|
||||
CaseUserActionBasicRt,
|
||||
CaseUserActionSavedObjectIdsRt,
|
||||
]);
|
||||
|
||||
/**
|
||||
* This includes the comment_id but not the action_id or case_id
|
||||
*/
|
||||
const CaseUserActionInjectedAttributesWithoutActionIdRt = rt.intersection([
|
||||
CaseUserActionBasicRt,
|
||||
CaseUserActionInjectedIdsRt,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Rename to CaseUserActionResponseRt when the UI is switching to the new user action _find API
|
||||
*/
|
||||
const CaseUserActionResponseWithoutActionIdRt = rt.intersection([
|
||||
CaseUserActionInjectedAttributesWithoutActionIdRt,
|
||||
rt.type({
|
||||
id: rt.string,
|
||||
version: rt.string,
|
||||
}),
|
||||
]);
|
||||
|
||||
export const CaseUserActionAttributesRt = CaseUserActionBasicRt;
|
||||
export const CaseUserActionsResponseRt = rt.array(CaseUserActionResponseRt);
|
||||
export const CaseUserActionsResponseWithoutActionIdRt = rt.array(
|
||||
CaseUserActionResponseWithoutActionIdRt
|
||||
);
|
||||
|
||||
export type CaseUserActionAttributes = rt.TypeOf<typeof CaseUserActionAttributesRt>;
|
||||
export type CaseUserActionAttributesWithoutConnectorId = rt.TypeOf<
|
||||
typeof CaseUserActionAttributesRt
|
||||
>;
|
||||
export type CaseUserActionsResponse = rt.TypeOf<typeof CaseUserActionsResponseRt>;
|
||||
export type CaseUserActionResponse = rt.TypeOf<typeof CaseUserActionResponseRt>;
|
||||
export type CaseUserActionInjectedAttributesWithoutActionId = rt.TypeOf<
|
||||
typeof CaseUserActionInjectedAttributesWithoutActionIdRt
|
||||
>;
|
||||
export type CaseUserActionsResponseWithoutActionId = rt.TypeOf<
|
||||
typeof CaseUserActionsResponseWithoutActionIdRt
|
||||
>;
|
||||
|
||||
export type UserAction = rt.TypeOf<typeof ActionsRt>;
|
||||
export type UserActionTypes = ActionTypeValues;
|
||||
|
||||
export type CaseUserAction = rt.TypeOf<typeof CaseUserActionBasicRt>;
|
||||
export type CaseUserActionWithoutConnectorId = rt.TypeOf<
|
||||
typeof CaseUserActionBasicWithoutConnectorIdRt
|
||||
>;
|
|
@ -15,6 +15,7 @@ import {
|
|||
CASE_CONFIGURE_DETAILS_URL,
|
||||
CASE_ALERTS_URL,
|
||||
CASE_COMMENT_DELETE_URL,
|
||||
CASE_FIND_USER_ACTIONS_URL,
|
||||
} from '../constants';
|
||||
|
||||
export const getCaseDetailsUrl = (id: string): string => {
|
||||
|
@ -41,6 +42,10 @@ export const getCaseUserActionUrl = (id: string): string => {
|
|||
return CASE_USER_ACTIONS_URL.replace('{case_id}', id);
|
||||
};
|
||||
|
||||
export const getCaseFindUserActionsUrl = (id: string): string => {
|
||||
return CASE_FIND_USER_ACTIONS_URL.replace('{case_id}', id);
|
||||
};
|
||||
|
||||
export const getCasePushUrl = (caseId: string, connectorId: string): string => {
|
||||
return CASE_PUSH_URL.replace('{case_id}', caseId).replace('{connector_id}', connectorId);
|
||||
};
|
||||
|
|
|
@ -73,6 +73,7 @@ export const CASE_REPORTERS_URL = `${CASES_URL}/reporters` as const;
|
|||
export const CASE_STATUS_URL = `${CASES_URL}/status` as const;
|
||||
export const CASE_TAGS_URL = `${CASES_URL}/tags` as const;
|
||||
export const CASE_USER_ACTIONS_URL = `${CASE_DETAILS_URL}/user_actions` as const;
|
||||
export const CASE_FIND_USER_ACTIONS_URL = `${CASE_USER_ACTIONS_URL}/_find` as const;
|
||||
|
||||
export const CASE_ALERTS_URL = `${CASES_URL}/alerts/{alert_id}` as const;
|
||||
export const CASE_DETAILS_ALERTS_URL = `${CASE_DETAILS_URL}/alerts` as const;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# OpenAPI (Experimental)
|
||||
|
||||
The current self-contained spec file is [as JSON](https://raw.githubusercontent.com/elastic/kibana/master/x-pack/plugins/cases/common/openapi/bundled.json) or [as YAML](https://raw.githubusercontent.com/elastic/kibana/master/x-pack/plugins/cases/common/openapi/bundled.yaml) and can be used for online tools like those found at https://openapi.tools/.
|
||||
The current self-contained spec file is [as JSON](https://raw.githubusercontent.com/elastic/kibana/master/x-pack/plugins/cases/common/openapi/bundled.json) or [as YAML](https://raw.githubusercontent.com/elastic/kibana/master/x-pack/plugins/cases/common/openapi/bundled.yaml) and can be used for online tools like those found at <https://openapi.tools/>.
|
||||
This spec is experimental and may be incomplete or change later.
|
||||
|
||||
A guide about the openApi specification can be found at [https://swagger.io/docs/specification/about/](https://swagger.io/docs/specification/about/).
|
||||
|
@ -16,19 +16,19 @@ A guide about the openApi specification can be found at [https://swagger.io/docs
|
|||
It is possible to validate the docs before bundling them with the following
|
||||
command in the `x-pack/plugins/cases/docs/openapi/` folder:
|
||||
|
||||
```
|
||||
```bash
|
||||
npx swagger-cli validate entrypoint.yaml
|
||||
```
|
||||
|
||||
Then you can generate the `bundled` files by running the following commands:
|
||||
|
||||
```
|
||||
```bash
|
||||
npx @redocly/cli bundle entrypoint.yaml --output bundled.yaml --ext yaml
|
||||
npx @redocly/cli bundle entrypoint.yaml --output bundled.json --ext json
|
||||
```
|
||||
|
||||
You can run additional linting with the following command:
|
||||
After generating the json bundle ensure that it is also valid by running the following command:
|
||||
|
||||
```
|
||||
```bash
|
||||
npx @redocly/cli lint bundled.json
|
||||
```
|
||||
|
|
|
@ -282,7 +282,7 @@
|
|||
{
|
||||
"name": "perPage",
|
||||
"in": "query",
|
||||
"description": "The number of rules to return per page.",
|
||||
"description": "The number of cases to return per page.",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"default": 20
|
||||
|
@ -2089,6 +2089,141 @@
|
|||
"url": "https://localhost:5601"
|
||||
}
|
||||
]
|
||||
},
|
||||
"/s/{spaceId}/api/cases/{caseId}/user_actions/_find": {
|
||||
"get": {
|
||||
"summary": "Finds user activity for a case.",
|
||||
"description": "You must have `read` privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the case you're seeking.\n",
|
||||
"operationId": "findCaseActivity",
|
||||
"tags": [
|
||||
"cases"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/case_id"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/parameters/space_id"
|
||||
},
|
||||
{
|
||||
"name": "page",
|
||||
"in": "query",
|
||||
"description": "The page number to return.",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"default": 1
|
||||
},
|
||||
"example": 1
|
||||
},
|
||||
{
|
||||
"name": "perPage",
|
||||
"in": "query",
|
||||
"description": "The number of user actions to return per page.",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"default": 20
|
||||
},
|
||||
"example": 20
|
||||
},
|
||||
{
|
||||
"name": "sortOrder",
|
||||
"in": "query",
|
||||
"description": "Determines the sort order.",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"asc",
|
||||
"desc"
|
||||
],
|
||||
"default": "asc"
|
||||
},
|
||||
"example": "asc"
|
||||
},
|
||||
{
|
||||
"name": "types",
|
||||
"in": "query",
|
||||
"description": "Determines the types of user actions to return.",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"action",
|
||||
"alert",
|
||||
"assignees",
|
||||
"attachment",
|
||||
"comment",
|
||||
"connector",
|
||||
"create_case",
|
||||
"description",
|
||||
"pushed",
|
||||
"settings",
|
||||
"severity",
|
||||
"status",
|
||||
"tags",
|
||||
"title",
|
||||
"user"
|
||||
]
|
||||
}
|
||||
},
|
||||
"example": "create_case"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Indicates a successful call.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"page": {
|
||||
"type": "integer"
|
||||
},
|
||||
"perPage": {
|
||||
"type": "integer"
|
||||
},
|
||||
"total": {
|
||||
"type": "integer"
|
||||
},
|
||||
"userActions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/user_actions_find_response_properties"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"findCaseActivityResponse": {
|
||||
"$ref": "#/components/examples/find_case_activity_response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Authorization information is missing or invalid.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/4xx_response"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "https://localhost:5601"
|
||||
}
|
||||
]
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "https://localhost:5601"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
|
@ -4126,6 +4261,136 @@
|
|||
"$ref": "#/components/schemas/action_types"
|
||||
}
|
||||
}
|
||||
},
|
||||
"user_actions_find_response_properties": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"action",
|
||||
"comment_id",
|
||||
"created_at",
|
||||
"created_by",
|
||||
"id",
|
||||
"owner",
|
||||
"payload",
|
||||
"type",
|
||||
"version"
|
||||
],
|
||||
"properties": {
|
||||
"action": {
|
||||
"$ref": "#/components/schemas/actions"
|
||||
},
|
||||
"comment_id": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"example": "578608d0-03b1-11ed-920c-974bfa104448"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"example": "2022-05-13T09:16:17.416Z"
|
||||
},
|
||||
"created_by": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"example": null,
|
||||
"nullable": true
|
||||
},
|
||||
"full_name": {
|
||||
"type": "string",
|
||||
"example": null,
|
||||
"nullable": true
|
||||
},
|
||||
"username": {
|
||||
"type": "string",
|
||||
"example": "elastic",
|
||||
"nullable": true
|
||||
},
|
||||
"profile_uid": {
|
||||
"type": "string",
|
||||
"example": "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"email",
|
||||
"full_name",
|
||||
"username"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"example": "22fd3e30-03b1-11ed-920c-974bfa104448"
|
||||
},
|
||||
"owner": {
|
||||
"$ref": "#/components/schemas/owners"
|
||||
},
|
||||
"payload": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/payload_alert_comment"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/payload_assignees"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/payload_connector"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/payload_create_case"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/payload_delete"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/payload_description"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/payload_pushed"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/payload_settings"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/payload_severity"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/payload_status"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/payload_tags"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/payload_title"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/payload_user_comment"
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"example": "WzM1ODg4LDFd"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "The type of action.",
|
||||
"enum": [
|
||||
"assignees",
|
||||
"create_case",
|
||||
"comment",
|
||||
"connector",
|
||||
"description",
|
||||
"pushed",
|
||||
"tags",
|
||||
"title",
|
||||
"status",
|
||||
"settings",
|
||||
"severity"
|
||||
],
|
||||
"example": "create_case"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
|
@ -4838,6 +5103,90 @@
|
|||
"type": "delete_case"
|
||||
}
|
||||
]
|
||||
},
|
||||
"find_case_activity_response": {
|
||||
"summary": "Retrieves all activity for a case",
|
||||
"value": {
|
||||
"page": 1,
|
||||
"perPage": 20,
|
||||
"total": 4,
|
||||
"userActions": [
|
||||
{
|
||||
"id": "b4cd0770-07c9-11ed-a5fd-47154cb8767e",
|
||||
"action": "create",
|
||||
"comment_id": "578608d0-03b1-11ed-920c-974bfa104448",
|
||||
"created_at": "2022-07-20T01:17:22.150Z",
|
||||
"created_by": {
|
||||
"username": "elastic",
|
||||
"email": null,
|
||||
"full_name": null
|
||||
},
|
||||
"owner": "cases",
|
||||
"payload": {
|
||||
"assignees": null,
|
||||
"connector": {
|
||||
"name": "none",
|
||||
"type": ".none",
|
||||
"fields": null,
|
||||
"id": "none"
|
||||
},
|
||||
"description": "test",
|
||||
"tags": [
|
||||
"mine"
|
||||
],
|
||||
"title": "test-case",
|
||||
"owner": "cases",
|
||||
"settings": {
|
||||
"syncAlerts": false
|
||||
},
|
||||
"severity": "low",
|
||||
"status": "open",
|
||||
"type": "create_case"
|
||||
},
|
||||
"version": "WzM1ODg4LDFd",
|
||||
"type": "comment"
|
||||
},
|
||||
{
|
||||
"id": "57af14a0-03b1-11ed-920c-974bfa104448",
|
||||
"action": "create",
|
||||
"comment_id": "578608d0-03b1-11ed-920c-974bfa104448",
|
||||
"created_at": "2022-07-14T20:12:53.354Z",
|
||||
"created_by": {
|
||||
"username": "elastic",
|
||||
"email": null,
|
||||
"full_name": null
|
||||
},
|
||||
"owner": "cases",
|
||||
"payload": {
|
||||
"comment": "A new comment",
|
||||
"owner": "cases",
|
||||
"type": "user"
|
||||
},
|
||||
"version": "WzM1ODg4LDFa",
|
||||
"type": "comment"
|
||||
},
|
||||
{
|
||||
"id": "573c6980-6123-11ed-aa41-81a0a61fe447",
|
||||
"action": "add",
|
||||
"comment_id": null,
|
||||
"created_at": "2022-07-20T01:10:28.238Z",
|
||||
"created_by": {
|
||||
"username": "elastic",
|
||||
"email": null,
|
||||
"full_name": null,
|
||||
"profile_uid": "u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0"
|
||||
},
|
||||
"owner": "cases",
|
||||
"payload": {
|
||||
"assignees": {
|
||||
"uid": "u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0"
|
||||
}
|
||||
},
|
||||
"version": "WzM1ODg4LDFb",
|
||||
"type": "assignees"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -171,7 +171,7 @@ paths:
|
|||
example: 1
|
||||
- name: perPage
|
||||
in: query
|
||||
description: The number of rules to return per page.
|
||||
description: The number of cases to return per page.
|
||||
schema:
|
||||
type: integer
|
||||
default: 20
|
||||
|
@ -1293,6 +1293,96 @@ paths:
|
|||
- url: https://localhost:5601
|
||||
servers:
|
||||
- url: https://localhost:5601
|
||||
/s/{spaceId}/api/cases/{caseId}/user_actions/_find:
|
||||
get:
|
||||
summary: Finds user activity for a case.
|
||||
description: |
|
||||
You must have `read` privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the case you're seeking.
|
||||
operationId: findCaseActivity
|
||||
tags:
|
||||
- cases
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/case_id'
|
||||
- $ref: '#/components/parameters/space_id'
|
||||
- name: page
|
||||
in: query
|
||||
description: The page number to return.
|
||||
schema:
|
||||
type: string
|
||||
default: 1
|
||||
example: 1
|
||||
- name: perPage
|
||||
in: query
|
||||
description: The number of user actions to return per page.
|
||||
schema:
|
||||
type: string
|
||||
default: 20
|
||||
example: 20
|
||||
- name: sortOrder
|
||||
in: query
|
||||
description: Determines the sort order.
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- asc
|
||||
- desc
|
||||
default: asc
|
||||
example: asc
|
||||
- name: types
|
||||
in: query
|
||||
description: Determines the types of user actions to return.
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- action
|
||||
- alert
|
||||
- assignees
|
||||
- attachment
|
||||
- comment
|
||||
- connector
|
||||
- create_case
|
||||
- description
|
||||
- pushed
|
||||
- settings
|
||||
- severity
|
||||
- status
|
||||
- tags
|
||||
- title
|
||||
- user
|
||||
example: create_case
|
||||
responses:
|
||||
'200':
|
||||
description: Indicates a successful call.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
page:
|
||||
type: integer
|
||||
perPage:
|
||||
type: integer
|
||||
total:
|
||||
type: integer
|
||||
userActions:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/user_actions_find_response_properties'
|
||||
examples:
|
||||
findCaseActivityResponse:
|
||||
$ref: '#/components/examples/find_case_activity_response'
|
||||
'401':
|
||||
description: Authorization information is missing or invalid.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/4xx_response'
|
||||
servers:
|
||||
- url: https://localhost:5601
|
||||
servers:
|
||||
- url: https://localhost:5601
|
||||
components:
|
||||
securitySchemes:
|
||||
basicAuth:
|
||||
|
@ -2758,6 +2848,90 @@ components:
|
|||
- $ref: '#/components/schemas/payload_user_comment'
|
||||
type:
|
||||
$ref: '#/components/schemas/action_types'
|
||||
user_actions_find_response_properties:
|
||||
type: object
|
||||
required:
|
||||
- action
|
||||
- comment_id
|
||||
- created_at
|
||||
- created_by
|
||||
- id
|
||||
- owner
|
||||
- payload
|
||||
- type
|
||||
- version
|
||||
properties:
|
||||
action:
|
||||
$ref: '#/components/schemas/actions'
|
||||
comment_id:
|
||||
type: string
|
||||
nullable: true
|
||||
example: 578608d0-03b1-11ed-920c-974bfa104448
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: '2022-05-13T09:16:17.416Z'
|
||||
created_by:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
example: null
|
||||
nullable: true
|
||||
full_name:
|
||||
type: string
|
||||
example: null
|
||||
nullable: true
|
||||
username:
|
||||
type: string
|
||||
example: elastic
|
||||
nullable: true
|
||||
profile_uid:
|
||||
type: string
|
||||
example: u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0
|
||||
required:
|
||||
- email
|
||||
- full_name
|
||||
- username
|
||||
id:
|
||||
type: string
|
||||
example: 22fd3e30-03b1-11ed-920c-974bfa104448
|
||||
owner:
|
||||
$ref: '#/components/schemas/owners'
|
||||
payload:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/payload_alert_comment'
|
||||
- $ref: '#/components/schemas/payload_assignees'
|
||||
- $ref: '#/components/schemas/payload_connector'
|
||||
- $ref: '#/components/schemas/payload_create_case'
|
||||
- $ref: '#/components/schemas/payload_delete'
|
||||
- $ref: '#/components/schemas/payload_description'
|
||||
- $ref: '#/components/schemas/payload_pushed'
|
||||
- $ref: '#/components/schemas/payload_settings'
|
||||
- $ref: '#/components/schemas/payload_severity'
|
||||
- $ref: '#/components/schemas/payload_status'
|
||||
- $ref: '#/components/schemas/payload_tags'
|
||||
- $ref: '#/components/schemas/payload_title'
|
||||
- $ref: '#/components/schemas/payload_user_comment'
|
||||
version:
|
||||
type: string
|
||||
example: WzM1ODg4LDFd
|
||||
type:
|
||||
type: string
|
||||
description: The type of action.
|
||||
enum:
|
||||
- assignees
|
||||
- create_case
|
||||
- comment
|
||||
- connector
|
||||
- description
|
||||
- pushed
|
||||
- tags
|
||||
- title
|
||||
- status
|
||||
- settings
|
||||
- severity
|
||||
example: create_case
|
||||
examples:
|
||||
create_case_request:
|
||||
summary: Create a security case that uses a Jira connector.
|
||||
|
@ -3315,6 +3489,71 @@ components:
|
|||
owner: cases
|
||||
payload: null
|
||||
type: delete_case
|
||||
find_case_activity_response:
|
||||
summary: Retrieves all activity for a case
|
||||
value:
|
||||
page: 1
|
||||
perPage: 20
|
||||
total: 4
|
||||
userActions:
|
||||
- id: b4cd0770-07c9-11ed-a5fd-47154cb8767e
|
||||
action: create
|
||||
comment_id: 578608d0-03b1-11ed-920c-974bfa104448
|
||||
created_at: '2022-07-20T01:17:22.150Z'
|
||||
created_by:
|
||||
username: elastic
|
||||
email: null
|
||||
full_name: null
|
||||
owner: cases
|
||||
payload:
|
||||
assignees: null
|
||||
connector:
|
||||
name: none
|
||||
type: .none
|
||||
fields: null
|
||||
id: none
|
||||
description: test
|
||||
tags:
|
||||
- mine
|
||||
title: test-case
|
||||
owner: cases
|
||||
settings:
|
||||
syncAlerts: false
|
||||
severity: low
|
||||
status: open
|
||||
type: create_case
|
||||
version: WzM1ODg4LDFd
|
||||
type: comment
|
||||
- id: 57af14a0-03b1-11ed-920c-974bfa104448
|
||||
action: create
|
||||
comment_id: 578608d0-03b1-11ed-920c-974bfa104448
|
||||
created_at: '2022-07-14T20:12:53.354Z'
|
||||
created_by:
|
||||
username: elastic
|
||||
email: null
|
||||
full_name: null
|
||||
owner: cases
|
||||
payload:
|
||||
comment: A new comment
|
||||
owner: cases
|
||||
type: user
|
||||
version: WzM1ODg4LDFa
|
||||
type: comment
|
||||
- id: 573c6980-6123-11ed-aa41-81a0a61fe447
|
||||
action: add
|
||||
comment_id: null
|
||||
created_at: '2022-07-20T01:10:28.238Z'
|
||||
created_by:
|
||||
username: elastic
|
||||
email: null
|
||||
full_name: null
|
||||
profile_uid: u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0
|
||||
owner: cases
|
||||
payload:
|
||||
assignees:
|
||||
uid: u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0
|
||||
version: WzM1ODg4LDFb
|
||||
type: assignees
|
||||
security:
|
||||
- basicAuth: []
|
||||
- apiKeyAuth: []
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
summary: Retrieves all activity for a case
|
||||
value:
|
||||
page: 1
|
||||
perPage: 20
|
||||
total: 4
|
||||
userActions:
|
||||
- id: b4cd0770-07c9-11ed-a5fd-47154cb8767e
|
||||
action: create
|
||||
comment_id: 578608d0-03b1-11ed-920c-974bfa104448
|
||||
created_at: 2022-07-20T01:17:22.150Z
|
||||
created_by:
|
||||
username: elastic
|
||||
email: null
|
||||
full_name: null
|
||||
owner: cases
|
||||
payload:
|
||||
assignees:
|
||||
connector:
|
||||
name: none
|
||||
type: .none
|
||||
fields: null
|
||||
id: none
|
||||
description: test
|
||||
tags:
|
||||
- mine
|
||||
title: test-case
|
||||
owner: cases
|
||||
settings:
|
||||
syncAlerts: false
|
||||
severity: low
|
||||
status: open
|
||||
type: create_case
|
||||
version: WzM1ODg4LDFd
|
||||
type: comment
|
||||
- id: 57af14a0-03b1-11ed-920c-974bfa104448
|
||||
action: create
|
||||
comment_id: 578608d0-03b1-11ed-920c-974bfa104448
|
||||
created_at: 2022-07-14T20:12:53.354Z
|
||||
created_by:
|
||||
username: elastic
|
||||
email: null
|
||||
full_name: null
|
||||
owner: cases
|
||||
payload:
|
||||
comment: A new comment
|
||||
owner: cases
|
||||
type: user
|
||||
version: WzM1ODg4LDFa
|
||||
type: comment
|
||||
- id: 573c6980-6123-11ed-aa41-81a0a61fe447
|
||||
action: add
|
||||
comment_id: null
|
||||
created_at: 2022-07-20T01:10:28.238Z
|
||||
created_by:
|
||||
username: elastic
|
||||
email: null
|
||||
full_name: null
|
||||
profile_uid: u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0
|
||||
owner: cases
|
||||
payload:
|
||||
assignees:
|
||||
uid: u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0
|
||||
version: WzM1ODg4LDFb
|
||||
type: assignees
|
|
@ -0,0 +1,69 @@
|
|||
type: object
|
||||
required:
|
||||
- action
|
||||
- comment_id
|
||||
- created_at
|
||||
- created_by
|
||||
- id
|
||||
- owner
|
||||
- payload
|
||||
- type
|
||||
- version
|
||||
properties:
|
||||
action:
|
||||
$ref: 'actions.yaml'
|
||||
comment_id:
|
||||
type: string
|
||||
nullable: true
|
||||
example: 578608d0-03b1-11ed-920c-974bfa104448
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: 2022-05-13T09:16:17.416Z
|
||||
created_by:
|
||||
type: object
|
||||
properties:
|
||||
$ref: 'user_properties.yaml'
|
||||
required:
|
||||
- email
|
||||
- full_name
|
||||
- username
|
||||
id:
|
||||
type: string
|
||||
example: 22fd3e30-03b1-11ed-920c-974bfa104448
|
||||
owner:
|
||||
$ref: 'owners.yaml'
|
||||
payload:
|
||||
oneOf:
|
||||
- $ref: 'payload_alert_comment.yaml'
|
||||
- $ref: 'payload_assignees.yaml'
|
||||
- $ref: 'payload_connector.yaml'
|
||||
- $ref: 'payload_create_case.yaml'
|
||||
- $ref: 'payload_delete.yaml'
|
||||
- $ref: 'payload_description.yaml'
|
||||
- $ref: 'payload_pushed.yaml'
|
||||
- $ref: 'payload_settings.yaml'
|
||||
- $ref: 'payload_severity.yaml'
|
||||
- $ref: 'payload_status.yaml'
|
||||
- $ref: 'payload_tags.yaml'
|
||||
- $ref: 'payload_title.yaml'
|
||||
- $ref: 'payload_user_comment.yaml'
|
||||
version:
|
||||
type: string
|
||||
example: WzM1ODg4LDFd
|
||||
type:
|
||||
type: string
|
||||
description: The type of action.
|
||||
enum:
|
||||
- assignees
|
||||
- create_case
|
||||
- comment
|
||||
- connector
|
||||
- description
|
||||
- pushed
|
||||
- tags
|
||||
- title
|
||||
- status
|
||||
- settings
|
||||
- severity
|
||||
example: create_case
|
|
@ -52,4 +52,4 @@ properties:
|
|||
- $ref: 'payload_title.yaml'
|
||||
- $ref: 'payload_user_comment.yaml'
|
||||
type:
|
||||
$ref: 'action_types.yaml'
|
||||
$ref: 'action_types.yaml'
|
||||
|
|
|
@ -45,6 +45,8 @@ paths:
|
|||
$ref: 'paths/s@{spaceid}@api@cases@{caseid}@connector@{connectorid}@_push.yaml'
|
||||
'/s/{spaceId}/api/cases/{caseId}/user_actions':
|
||||
$ref: 'paths/s@{spaceid}@api@cases@{caseid}@user_actions.yaml'
|
||||
'/s/{spaceId}/api/cases/{caseId}/user_actions/_find':
|
||||
$ref: 'paths/s@{spaceid}@api@cases@{caseid}@user_actions@_find.yaml'
|
||||
components:
|
||||
securitySchemes:
|
||||
basicAuth:
|
||||
|
|
|
@ -54,7 +54,7 @@ get:
|
|||
example: 1
|
||||
- name: perPage
|
||||
in: query
|
||||
description: The number of rules to return per page.
|
||||
description: The number of cases to return per page.
|
||||
schema:
|
||||
type: integer
|
||||
default: 20
|
||||
|
@ -83,7 +83,7 @@ get:
|
|||
- type: array
|
||||
items:
|
||||
type: string
|
||||
- $ref: '../components/parameters/severity.yaml'
|
||||
- $ref: '../components/parameters/severity.yaml'
|
||||
- name: sortField
|
||||
in: query
|
||||
description: Determines which field is used to sort the results.
|
||||
|
@ -94,7 +94,7 @@ get:
|
|||
- updatedAt
|
||||
default: createdAt
|
||||
example: updatedAt
|
||||
- name: sortOrder
|
||||
- name: sortOrder
|
||||
in: query
|
||||
description: Determines the sort order.
|
||||
schema:
|
||||
|
@ -104,17 +104,17 @@ get:
|
|||
- desc
|
||||
default: desc
|
||||
example: asc
|
||||
- name: status
|
||||
- name: status
|
||||
in: query
|
||||
description: Filters the returned cases by state.
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- closed
|
||||
- in-progress
|
||||
- open
|
||||
type: string
|
||||
enum:
|
||||
- closed
|
||||
- in-progress
|
||||
- open
|
||||
example: open
|
||||
- name: tags
|
||||
- name: tags
|
||||
in: query
|
||||
description: Filters the returned cases by tags.
|
||||
schema:
|
||||
|
@ -140,7 +140,7 @@ get:
|
|||
'200':
|
||||
description: Indicates a successful call.
|
||||
content:
|
||||
application/json:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -172,4 +172,4 @@ get:
|
|||
servers:
|
||||
- url: https://localhost:5601
|
||||
servers:
|
||||
- url: https://localhost:5601
|
||||
- url: https://localhost:5601
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
get:
|
||||
summary: Finds user activity for a case.
|
||||
description: >
|
||||
You must have `read` privileges for the **Cases** feature in the
|
||||
**Management**, **Observability**, or **Security** section of the Kibana
|
||||
feature privileges, depending on the owner of the case you're seeking.
|
||||
operationId: findCaseActivity
|
||||
tags:
|
||||
- cases
|
||||
parameters:
|
||||
- $ref: '../components/parameters/case_id.yaml'
|
||||
- $ref: '../components/parameters/space_id.yaml'
|
||||
- name: page
|
||||
in: query
|
||||
description: The page number to return.
|
||||
schema:
|
||||
type: string
|
||||
default: 1
|
||||
example: 1
|
||||
- name: perPage
|
||||
in: query
|
||||
description: The number of user actions to return per page.
|
||||
schema:
|
||||
type: string
|
||||
default: 20
|
||||
example: 20
|
||||
- name: sortOrder
|
||||
in: query
|
||||
description: Determines the sort order.
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- asc
|
||||
- desc
|
||||
default: asc
|
||||
example: asc
|
||||
- name: types
|
||||
in: query
|
||||
description: Determines the types of user actions to return.
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- action
|
||||
- alert
|
||||
- assignees
|
||||
- attachment
|
||||
- comment
|
||||
- connector
|
||||
- create_case
|
||||
- description
|
||||
- pushed
|
||||
- settings
|
||||
- severity
|
||||
- status
|
||||
- tags
|
||||
- title
|
||||
- user
|
||||
example: create_case
|
||||
responses:
|
||||
'200':
|
||||
description: Indicates a successful call.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
page:
|
||||
type: integer
|
||||
perPage:
|
||||
type: integer
|
||||
total:
|
||||
type: integer
|
||||
userActions:
|
||||
type: array
|
||||
items:
|
||||
$ref: '../components/schemas/user_actions_find_response_properties.yaml'
|
||||
examples:
|
||||
findCaseActivityResponse:
|
||||
$ref: '../components/examples/find_case_activity_response.yaml'
|
||||
'401':
|
||||
description: Authorization information is missing or invalid.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../components/schemas/4xx_response.yaml'
|
||||
servers:
|
||||
- url: https://localhost:5601
|
||||
servers:
|
||||
- url: https://localhost:5601
|
|
@ -840,6 +840,90 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`audit_logger log function event structure creates the correct audit event for operation: "findUserActions" with an error and entity 1`] = `
|
||||
Object {
|
||||
"error": Object {
|
||||
"code": "Error",
|
||||
"message": "an error",
|
||||
},
|
||||
"event": Object {
|
||||
"action": "case_user_actions_find",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"outcome": "failure",
|
||||
"type": Array [
|
||||
"access",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "1",
|
||||
"type": "cases-user-actions",
|
||||
},
|
||||
},
|
||||
"message": "Failed attempt to access cases-user-actions [id=1] as owner \\"awesome\\"",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`audit_logger log function event structure creates the correct audit event for operation: "findUserActions" with an error but no entity 1`] = `
|
||||
Object {
|
||||
"error": Object {
|
||||
"code": "Error",
|
||||
"message": "an error",
|
||||
},
|
||||
"event": Object {
|
||||
"action": "case_user_actions_find",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"outcome": "failure",
|
||||
"type": Array [
|
||||
"access",
|
||||
],
|
||||
},
|
||||
"message": "Failed attempt to access a user actions as any owners",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`audit_logger log function event structure creates the correct audit event for operation: "findUserActions" without an error but with an entity 1`] = `
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_user_actions_find",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"outcome": "success",
|
||||
"type": Array [
|
||||
"access",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "5",
|
||||
"type": "cases-user-actions",
|
||||
},
|
||||
},
|
||||
"message": "User has accessed cases-user-actions [id=5] as owner \\"super\\"",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`audit_logger log function event structure creates the correct audit event for operation: "findUserActions" without an error or entity 1`] = `
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_user_actions_find",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"outcome": "success",
|
||||
"type": Array [
|
||||
"access",
|
||||
],
|
||||
},
|
||||
"message": "User has accessed a user actions as any owners",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`audit_logger log function event structure creates the correct audit event for operation: "getAlertsAttachedToCase" with an error and entity 1`] = `
|
||||
Object {
|
||||
"error": Object {
|
||||
|
|
|
@ -310,6 +310,14 @@ export const Operations: Record<ReadOperations | WriteOperations, OperationDetai
|
|||
docType: 'cases',
|
||||
savedObjectType: CASE_SAVED_OBJECT,
|
||||
},
|
||||
[ReadOperations.FindUserActions]: {
|
||||
ecsType: EVENT_TYPES.access,
|
||||
name: ACCESS_USER_ACTION_OPERATION,
|
||||
action: 'case_user_actions_find',
|
||||
verbs: accessVerbs,
|
||||
docType: 'user actions',
|
||||
savedObjectType: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
},
|
||||
[ReadOperations.GetUserActions]: {
|
||||
ecsType: EVENT_TYPES.access,
|
||||
name: ACCESS_USER_ACTION_OPERATION,
|
||||
|
|
|
@ -37,6 +37,7 @@ export enum ReadOperations {
|
|||
GetTags = 'getTags',
|
||||
GetReporters = 'getReporters',
|
||||
FindConfigurations = 'findConfigurations',
|
||||
FindUserActions = 'findUserActions',
|
||||
GetUserActions = 'getUserActions',
|
||||
GetConnectors = 'getConnectors',
|
||||
GetAlertsAttachedToCase = 'getAlertsAttachedToCase',
|
||||
|
|
|
@ -34,7 +34,7 @@ export class CasesClient {
|
|||
this._casesClientInternal = createCasesClientInternal(args);
|
||||
this._cases = createCasesSubClient(args, this, this._casesClientInternal);
|
||||
this._attachments = createAttachmentsSubClient(args, this, this._casesClientInternal);
|
||||
this._userActions = createUserActionsSubClient(args);
|
||||
this._userActions = createUserActionsSubClient(args, this);
|
||||
this._configure = createConfigurationSubClient(args, this._casesClientInternal);
|
||||
this._metrics = createMetricsSubClient(args, this);
|
||||
}
|
||||
|
|
|
@ -202,7 +202,7 @@ function createMockClientArgs() {
|
|||
const logger = loggingSystemMock.createLogger();
|
||||
|
||||
const userActionService = createUserActionServiceMock();
|
||||
userActionService.findStatusChanges.mockImplementation(async () => {
|
||||
userActionService.finder.findStatusChanges.mockImplementation(async () => {
|
||||
return [
|
||||
createStatusChangeSavedObject(CaseStatuses['in-progress'], new Date('2021-11-23T20:00:43Z')),
|
||||
];
|
||||
|
|
|
@ -6,9 +6,10 @@
|
|||
*/
|
||||
|
||||
import type { SavedObject } from '@kbn/core/server';
|
||||
import type { CaseUserActionResponse } from '../../../common/api';
|
||||
import type { CaseUserActionInjectedAttributesWithoutActionId } from '../../../common/api';
|
||||
import { CaseStatuses } from '../../../common/api';
|
||||
import { getStatusInfo } from './lifespan';
|
||||
import { createStatusChangeSavedObject } from './test_utils/lifespan';
|
||||
|
||||
describe('lifespan', () => {
|
||||
describe('getStatusInfo', () => {
|
||||
|
@ -119,7 +120,7 @@ describe('lifespan', () => {
|
|||
[
|
||||
{
|
||||
attributes: { payload: { hello: 1, status: CaseStatuses.closed }, type: 'status' },
|
||||
} as unknown as SavedObject<CaseUserActionResponse>,
|
||||
} as unknown as SavedObject<CaseUserActionInjectedAttributesWithoutActionId>,
|
||||
],
|
||||
new Date(0)
|
||||
);
|
||||
|
@ -132,7 +133,7 @@ describe('lifespan', () => {
|
|||
[
|
||||
{
|
||||
attributes: { payload: { status: CaseStatuses.closed }, type: 'awesome' },
|
||||
} as unknown as SavedObject<CaseUserActionResponse>,
|
||||
} as unknown as SavedObject<CaseUserActionInjectedAttributesWithoutActionId>,
|
||||
],
|
||||
new Date(0)
|
||||
);
|
||||
|
@ -148,7 +149,7 @@ describe('lifespan', () => {
|
|||
payload: { status: CaseStatuses.closed, created_at: 'blah' },
|
||||
type: 'status',
|
||||
},
|
||||
} as unknown as SavedObject<CaseUserActionResponse>,
|
||||
} as unknown as SavedObject<CaseUserActionInjectedAttributesWithoutActionId>,
|
||||
],
|
||||
new Date(0)
|
||||
);
|
||||
|
@ -169,31 +170,3 @@ describe('lifespan', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createStatusChangeSavedObject(
|
||||
status: CaseStatuses,
|
||||
createdAt: Date
|
||||
): SavedObject<CaseUserActionResponse> {
|
||||
return {
|
||||
references: [],
|
||||
id: '',
|
||||
type: '',
|
||||
attributes: {
|
||||
created_at: createdAt.toISOString(),
|
||||
created_by: {
|
||||
username: 'j@j.com',
|
||||
email: null,
|
||||
full_name: null,
|
||||
},
|
||||
owner: 'securitySolution',
|
||||
action: 'update',
|
||||
payload: {
|
||||
status,
|
||||
},
|
||||
type: 'status',
|
||||
action_id: '',
|
||||
case_id: '',
|
||||
comment_id: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import type { SavedObject } from '@kbn/core/server';
|
||||
import type {
|
||||
CaseUserActionResponse,
|
||||
CaseUserActionInjectedAttributesWithoutActionId,
|
||||
SingleCaseMetricsResponse,
|
||||
StatusInfo,
|
||||
StatusUserAction,
|
||||
|
@ -47,7 +47,7 @@ export class Lifespan extends SingleCaseBaseHandler {
|
|||
Operations.getUserActionMetrics
|
||||
);
|
||||
|
||||
const statusUserActions = await userActionService.findStatusChanges({
|
||||
const statusUserActions = await userActionService.finder.findStatusChanges({
|
||||
caseId: this.caseId,
|
||||
filter: authorizationFilter,
|
||||
});
|
||||
|
@ -83,7 +83,7 @@ interface StatusCalculations {
|
|||
}
|
||||
|
||||
export function getStatusInfo(
|
||||
statusUserActions: Array<SavedObject<CaseUserActionResponse>>,
|
||||
statusUserActions: Array<SavedObject<CaseUserActionInjectedAttributesWithoutActionId>>,
|
||||
caseOpenTimestamp: Date
|
||||
): StatusInfo {
|
||||
const accStatusInfo = statusUserActions.reduce<StatusCalculations>(
|
||||
|
@ -138,7 +138,7 @@ export function getStatusInfo(
|
|||
}
|
||||
|
||||
function isValidStatusChangeUserAction(
|
||||
attributes: CaseUserActionResponse,
|
||||
attributes: CaseUserActionInjectedAttributesWithoutActionId,
|
||||
newStatusChangeTimestamp: Date
|
||||
): attributes is UserActionWithResponse<StatusUserAction> {
|
||||
return StatusUserActionRt.is(attributes) && isDateValid(newStatusChangeTimestamp);
|
||||
|
|
|
@ -6,12 +6,15 @@
|
|||
*/
|
||||
|
||||
import type { SavedObject } from '@kbn/core/server';
|
||||
import type { CaseStatuses, CaseUserActionResponse } from '../../../../common/api';
|
||||
import type {
|
||||
CaseStatuses,
|
||||
CaseUserActionInjectedAttributesWithoutActionId,
|
||||
} from '../../../../common/api';
|
||||
|
||||
export function createStatusChangeSavedObject(
|
||||
status: CaseStatuses,
|
||||
createdAt: Date
|
||||
): SavedObject<CaseUserActionResponse> {
|
||||
): SavedObject<CaseUserActionInjectedAttributesWithoutActionId> {
|
||||
return {
|
||||
references: [],
|
||||
id: '',
|
||||
|
@ -29,8 +32,6 @@ export function createStatusChangeSavedObject(
|
|||
status,
|
||||
},
|
||||
type: 'status',
|
||||
action_id: '',
|
||||
case_id: '',
|
||||
comment_id: null,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -84,6 +84,7 @@ const createUserActionsSubClientMock = (): UserActionsSubClientMock => {
|
|||
return {
|
||||
getAll: jest.fn(),
|
||||
getConnectors: jest.fn(),
|
||||
find: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -5,34 +5,44 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { GetCaseConnectorsResponse } from '../../../common/api';
|
||||
import type { ICaseUserActionsResponse } from '../typedoc_interfaces';
|
||||
import type {
|
||||
GetCaseConnectorsResponse,
|
||||
CaseUserActionsResponse,
|
||||
UserActionFindResponse,
|
||||
} from '../../../common/api';
|
||||
import type { CasesClientArgs } from '../types';
|
||||
import { get } from './get';
|
||||
import { getConnectors } from './connectors';
|
||||
import type { GetConnectorsRequest, UserActionGet } from './types';
|
||||
import type { GetConnectorsRequest, UserActionFind, UserActionGet } from './types';
|
||||
import { find } from './find';
|
||||
import type { CasesClient } from '../client';
|
||||
|
||||
/**
|
||||
* API for interacting the actions performed by a user when interacting with the cases entities.
|
||||
*/
|
||||
export interface UserActionsSubClient {
|
||||
find(params: UserActionFind): Promise<UserActionFindResponse>;
|
||||
/**
|
||||
* Retrieves all user actions for a particular case.
|
||||
*/
|
||||
getAll(clientArgs: UserActionGet): Promise<ICaseUserActionsResponse>;
|
||||
getAll(params: UserActionGet): Promise<CaseUserActionsResponse>;
|
||||
/**
|
||||
* Retrieves all the connectors used within a given case
|
||||
*/
|
||||
getConnectors(clientArgs: GetConnectorsRequest): Promise<GetCaseConnectorsResponse>;
|
||||
getConnectors(params: GetConnectorsRequest): Promise<GetCaseConnectorsResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an API object for interacting with the user action entities
|
||||
*/
|
||||
export const createUserActionsSubClient = (clientArgs: CasesClientArgs): UserActionsSubClient => {
|
||||
export const createUserActionsSubClient = (
|
||||
clientArgs: CasesClientArgs,
|
||||
casesClient: CasesClient
|
||||
): UserActionsSubClient => {
|
||||
const attachmentSubClient: UserActionsSubClient = {
|
||||
getAll: (params: UserActionGet) => get(params, clientArgs),
|
||||
getConnectors: (params: GetConnectorsRequest) => getConnectors(params, clientArgs),
|
||||
find: (params) => find(params, casesClient, clientArgs),
|
||||
getAll: (params) => get(params, clientArgs),
|
||||
getConnectors: (params) => getConnectors(params, clientArgs),
|
||||
};
|
||||
|
||||
return Object.freeze(attachmentSubClient);
|
||||
|
|
|
@ -7,13 +7,13 @@
|
|||
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
import type { SavedObject } from '@kbn/core/server';
|
||||
import type { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import type { ActionResult, ActionsClient } from '@kbn/actions-plugin/server';
|
||||
import type { SavedObject } from '@kbn/core-saved-objects-common/src/server_types';
|
||||
import type {
|
||||
CaseUserActionResponse,
|
||||
GetCaseConnectorsResponse,
|
||||
CaseConnector,
|
||||
CaseUserActionInjectedAttributesWithoutActionId,
|
||||
} from '../../../common/api';
|
||||
import { GetCaseConnectorsResponseRt } from '../../../common/api';
|
||||
import { isConnectorUserAction, isCreateCaseUserAction } from '../../../common/utils/user_actions';
|
||||
|
@ -68,7 +68,7 @@ const checkConnectorsAuthorization = async ({
|
|||
authorization,
|
||||
}: {
|
||||
connectors: CaseConnectorActivity[];
|
||||
latestUserAction?: SavedObject<CaseUserActionResponse>;
|
||||
latestUserAction?: SavedObject<CaseUserActionInjectedAttributesWithoutActionId>;
|
||||
authorization: PublicMethodsOf<Authorization>;
|
||||
}) => {
|
||||
const entities: OwnerEntity[] = latestUserAction
|
||||
|
@ -109,7 +109,7 @@ const getConnectorsInfo = async ({
|
|||
}: {
|
||||
caseId: string;
|
||||
connectors: CaseConnectorActivity[];
|
||||
latestUserAction?: SavedObject<CaseUserActionResponse>;
|
||||
latestUserAction?: SavedObject<CaseUserActionInjectedAttributesWithoutActionId>;
|
||||
actionsClient: PublicMethodsOf<ActionsClient>;
|
||||
userActionService: CaseUserActionService;
|
||||
}): Promise<GetCaseConnectorsResponse> => {
|
||||
|
@ -180,7 +180,7 @@ const isDateValid = (date: Date): boolean => {
|
|||
};
|
||||
|
||||
const getConnectorInfoFromSavedObject = (
|
||||
savedObject: SavedObject<CaseUserActionResponse> | undefined
|
||||
savedObject: SavedObject<CaseUserActionInjectedAttributesWithoutActionId> | undefined
|
||||
): CaseConnector | undefined => {
|
||||
if (
|
||||
savedObject != null &&
|
||||
|
@ -200,7 +200,7 @@ const createConnectorInfoResult = ({
|
|||
actionConnectors: ActionResult[];
|
||||
connectors: CaseConnectorActivity[];
|
||||
pushInfo: Map<string, EnrichedPushInfo>;
|
||||
latestUserAction?: SavedObject<CaseUserActionResponse>;
|
||||
latestUserAction?: SavedObject<CaseUserActionInjectedAttributesWithoutActionId>;
|
||||
}) => {
|
||||
const results: GetCaseConnectorsResponse = {};
|
||||
|
||||
|
|
79
x-pack/plugins/cases/server/client/user_actions/find.ts
Normal file
79
x-pack/plugins/cases/server/client/user_actions/find.ts
Normal file
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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 Boom from '@hapi/boom';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { fold } from 'fp-ts/lib/Either';
|
||||
import { identity } from 'fp-ts/lib/function';
|
||||
|
||||
import type { UserActionFindResponse } from '../../../common/api';
|
||||
import {
|
||||
UserActionFindRequestRt,
|
||||
throwErrors,
|
||||
excess,
|
||||
UserActionFindResponseRt,
|
||||
} from '../../../common/api';
|
||||
import type { CasesClientArgs } from '../types';
|
||||
import type { UserActionFind } from './types';
|
||||
import { Operations } from '../../authorization';
|
||||
import { formatSavedObjects } from './utils';
|
||||
import { createCaseError } from '../../common/error';
|
||||
import { asArray } from '../../common/utils';
|
||||
import type { CasesClient } from '../client';
|
||||
|
||||
export const find = async (
|
||||
{ caseId, params }: UserActionFind,
|
||||
casesClient: CasesClient,
|
||||
clientArgs: CasesClientArgs
|
||||
): Promise<UserActionFindResponse> => {
|
||||
const {
|
||||
services: { userActionService },
|
||||
logger,
|
||||
authorization,
|
||||
} = clientArgs;
|
||||
|
||||
try {
|
||||
// supertest and query-string encode a single entry in an array as just a string so make sure we have an array
|
||||
const types = asArray(params.types);
|
||||
|
||||
const queryParams = pipe(
|
||||
excess(UserActionFindRequestRt).decode({ ...params, types }),
|
||||
fold(throwErrors(Boom.badRequest), identity)
|
||||
);
|
||||
|
||||
const [authorizationFilterRes] = await Promise.all([
|
||||
authorization.getAuthorizationFilter(Operations.findUserActions),
|
||||
// ensure that we have authorization for reading the case
|
||||
casesClient.cases.resolve({ id: caseId, includeComments: false }),
|
||||
]);
|
||||
|
||||
const { filter: authorizationFilter, ensureSavedObjectsAreAuthorized } = authorizationFilterRes;
|
||||
|
||||
const userActions = await userActionService.finder.find({
|
||||
caseId,
|
||||
...queryParams,
|
||||
filter: authorizationFilter,
|
||||
});
|
||||
|
||||
ensureSavedObjectsAreAuthorized(
|
||||
userActions.saved_objects.map((so) => ({ owner: so.attributes.owner, id: so.id }))
|
||||
);
|
||||
|
||||
return UserActionFindResponseRt.encode({
|
||||
userActions: formatSavedObjects(userActions),
|
||||
page: userActions.page,
|
||||
perPage: userActions.per_page,
|
||||
total: userActions.total,
|
||||
});
|
||||
} catch (error) {
|
||||
throw createCaseError({
|
||||
message: `Failed to find user actions for case id: ${caseId}: ${error}`,
|
||||
error,
|
||||
logger,
|
||||
});
|
||||
}
|
||||
};
|
|
@ -5,13 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { SavedObjectsFindResponse } from '@kbn/core/server';
|
||||
import type { CaseUserActionsResponse, CaseUserActionResponse } from '../../../common/api';
|
||||
import type { CaseUserActionsResponse } from '../../../common/api';
|
||||
import { CaseUserActionsResponseRt } from '../../../common/api';
|
||||
import { createCaseError } from '../../common/error';
|
||||
import type { CasesClientArgs } from '..';
|
||||
import { Operations } from '../../authorization';
|
||||
import type { UserActionGet } from './types';
|
||||
import { extractAttributes } from './utils';
|
||||
|
||||
export const get = async (
|
||||
{ caseId }: UserActionGet,
|
||||
|
@ -45,9 +45,3 @@ export const get = async (
|
|||
});
|
||||
}
|
||||
};
|
||||
|
||||
function extractAttributes(
|
||||
userActions: SavedObjectsFindResponse<CaseUserActionResponse>
|
||||
): CaseUserActionsResponse {
|
||||
return userActions.saved_objects.map((so) => so.attributes);
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { UserActionFindRequest } from '../../../common/api';
|
||||
|
||||
/**
|
||||
* Parameters for retrieving user actions for a particular case
|
||||
*/
|
||||
|
@ -16,3 +18,8 @@ export interface UserActionGet {
|
|||
}
|
||||
|
||||
export type GetConnectorsRequest = UserActionGet;
|
||||
|
||||
export interface UserActionFind {
|
||||
params: UserActionFindRequest;
|
||||
caseId: string;
|
||||
}
|
||||
|
|
25
x-pack/plugins/cases/server/client/user_actions/utils.ts
Normal file
25
x-pack/plugins/cases/server/client/user_actions/utils.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 { SavedObjectsFindResponse } from '@kbn/core-saved-objects-api-server';
|
||||
import type {
|
||||
CaseUserActionResponse,
|
||||
CaseUserActionInjectedAttributesWithoutActionId,
|
||||
CaseUserActionsResponse,
|
||||
CaseUserActionsResponseWithoutActionId,
|
||||
} from '../../../common/api';
|
||||
|
||||
export const extractAttributes = (
|
||||
userActions: SavedObjectsFindResponse<CaseUserActionResponse>
|
||||
): CaseUserActionsResponse => {
|
||||
return userActions.saved_objects.map((so) => so.attributes);
|
||||
};
|
||||
|
||||
export const formatSavedObjects = (
|
||||
response: SavedObjectsFindResponse<CaseUserActionInjectedAttributesWithoutActionId>
|
||||
): CaseUserActionsResponseWithoutActionId =>
|
||||
response.saved_objects.map((so) => ({ id: so.id, version: so.version ?? '', ...so.attributes }));
|
|
@ -180,10 +180,17 @@ export const addSeverityFilter = ({
|
|||
return filters.length > 1 ? nodeBuilder.and(filters) : filters[0];
|
||||
};
|
||||
|
||||
export const NodeBuilderOperators = {
|
||||
and: 'and',
|
||||
or: 'or',
|
||||
} as const;
|
||||
|
||||
type NodeBuilderOperatorsType = keyof typeof NodeBuilderOperators;
|
||||
|
||||
interface FilterField {
|
||||
filters?: string | string[];
|
||||
field: string;
|
||||
operator: 'and' | 'or';
|
||||
operator: NodeBuilderOperatorsType;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
|
@ -273,14 +280,17 @@ export const combineAuthorizedAndOwnerFilter = (
|
|||
|
||||
/**
|
||||
* Combines Kuery nodes and accepts an array with a mixture of undefined and KueryNodes. This will filter out the undefined
|
||||
* filters and return a KueryNode with the filters and'd together.
|
||||
* filters and return a KueryNode with the filters combined using the specified operator which defaults to and if not defined.
|
||||
*/
|
||||
export function combineFilters(nodes: Array<KueryNode | undefined>): KueryNode | undefined {
|
||||
export function combineFilters(
|
||||
nodes: Array<KueryNode | undefined>,
|
||||
operator: NodeBuilderOperatorsType = NodeBuilderOperators.and
|
||||
): KueryNode | undefined {
|
||||
const filters = nodes.filter((node): node is KueryNode => node !== undefined);
|
||||
if (filters.length <= 0) {
|
||||
return;
|
||||
}
|
||||
return nodeBuilder.and(filters);
|
||||
return nodeBuilder[operator](filters);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -31,6 +31,7 @@ import { postCaseConfigureRoute } from './configure/post_configure';
|
|||
import { getAllAlertsAttachedToCaseRoute } from './comments/get_alerts';
|
||||
import { getCaseMetricRoute } from './metrics/get_case_metrics';
|
||||
import { getCasesMetricRoute } from './metrics/get_cases_metrics';
|
||||
import { findUserActionsRoute } from './user_actions/find_user_actions';
|
||||
|
||||
export const getExternalRoutes = () =>
|
||||
[
|
||||
|
@ -41,6 +42,7 @@ export const getExternalRoutes = () =>
|
|||
patchCaseRoute,
|
||||
postCaseRoute,
|
||||
pushCaseRoute,
|
||||
findUserActionsRoute,
|
||||
getUserActionsRoute,
|
||||
getStatusRoute,
|
||||
getCasesByAlertIdRoute,
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
|
||||
import { CASE_FIND_USER_ACTIONS_URL } from '../../../../common/constants';
|
||||
import { createCaseError } from '../../../common/error';
|
||||
import { createCasesRoute } from '../create_cases_route';
|
||||
import type { UserActionFindRequest } from '../../../../common/api';
|
||||
|
||||
export const findUserActionsRoute = createCasesRoute({
|
||||
method: 'get',
|
||||
path: CASE_FIND_USER_ACTIONS_URL,
|
||||
params: {
|
||||
params: schema.object({
|
||||
case_id: schema.string(),
|
||||
}),
|
||||
},
|
||||
handler: async ({ context, request, response }) => {
|
||||
try {
|
||||
const caseContext = await context.cases;
|
||||
const casesClient = await caseContext.getCasesClient();
|
||||
const caseId = request.params.case_id;
|
||||
const options = request.query as UserActionFindRequest;
|
||||
|
||||
return response.ok({
|
||||
body: await casesClient.userActions.find({ caseId, params: options }),
|
||||
});
|
||||
} catch (error) {
|
||||
throw createCaseError({
|
||||
message: `Failed to find user actions in route for case id: ${request.params.case_id}: ${error}`,
|
||||
error,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
|
@ -17,12 +17,21 @@ import type {
|
|||
import type { LicensingService } from './licensing';
|
||||
import type { EmailNotificationService } from './notifications/email_notification_service';
|
||||
import type { UserActionPersister } from './user_actions/operations/create';
|
||||
import type { UserActionFinder } from './user_actions/operations/find';
|
||||
|
||||
interface UserActionServiceOperations {
|
||||
creator: CaseUserActionPersisterServiceMock;
|
||||
finder: CaseUserActionFinderServiceMock;
|
||||
}
|
||||
|
||||
export type CaseServiceMock = jest.Mocked<CasesService>;
|
||||
export type CaseConfigureServiceMock = jest.Mocked<CaseConfigureService>;
|
||||
export type ConnectorMappingsServiceMock = jest.Mocked<ConnectorMappingsService>;
|
||||
export type CaseUserActionServiceMock = jest.Mocked<CaseUserActionService>;
|
||||
export type CaseUserActionServiceMock = jest.Mocked<
|
||||
CaseUserActionService & UserActionServiceOperations
|
||||
>;
|
||||
export type CaseUserActionPersisterServiceMock = jest.Mocked<UserActionPersister>;
|
||||
export type CaseUserActionFinderServiceMock = jest.Mocked<UserActionFinder>;
|
||||
export type AlertServiceMock = jest.Mocked<AlertService>;
|
||||
export type AttachmentServiceMock = jest.Mocked<AttachmentService>;
|
||||
export type LicensingServiceMock = jest.Mocked<LicensingService>;
|
||||
|
@ -87,18 +96,25 @@ const createUserActionPersisterServiceMock = (): CaseUserActionPersisterServiceM
|
|||
return service as unknown as CaseUserActionPersisterServiceMock;
|
||||
};
|
||||
|
||||
type FakeUserActionService = PublicMethodsOf<CaseUserActionService> & {
|
||||
creator: CaseUserActionPersisterServiceMock;
|
||||
const createUserActionFinderServiceMock = (): CaseUserActionFinderServiceMock => {
|
||||
const service: PublicMethodsOf<UserActionFinder> = {
|
||||
find: jest.fn(),
|
||||
findStatusChanges: jest.fn(),
|
||||
};
|
||||
|
||||
return service as unknown as CaseUserActionFinderServiceMock;
|
||||
};
|
||||
|
||||
type FakeUserActionService = PublicMethodsOf<CaseUserActionService> & UserActionServiceOperations;
|
||||
|
||||
export const createUserActionServiceMock = (): CaseUserActionServiceMock => {
|
||||
const service: FakeUserActionService = {
|
||||
creator: createUserActionPersisterServiceMock(),
|
||||
finder: createUserActionFinderServiceMock(),
|
||||
getConnectorFieldsBeforeLatestPush: jest.fn(),
|
||||
getMostRecentUserAction: jest.fn(),
|
||||
getCaseConnectorInformation: jest.fn(),
|
||||
getAll: jest.fn(),
|
||||
findStatusChanges: jest.fn(),
|
||||
getUniqueConnectors: jest.fn(),
|
||||
getUserActionIdsForCases: jest.fn(),
|
||||
};
|
||||
|
|
333
x-pack/plugins/cases/server/services/user_actions/__snapshots__/transform.test.ts.snap
generated
Normal file
333
x-pack/plugins/cases/server/services/user_actions/__snapshots__/transform.test.ts.snap
generated
Normal file
|
@ -0,0 +1,333 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`transform legacyTransformFindResponseToExternalModel external references populates the external references attributes 1`] = `
|
||||
Object {
|
||||
"page": 1,
|
||||
"per_page": 1,
|
||||
"saved_objects": Array [
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"action": "create",
|
||||
"action_id": "100",
|
||||
"case_id": "1",
|
||||
"comment_id": "external-reference-test-id",
|
||||
"created_at": "abc",
|
||||
"created_by": Object {
|
||||
"email": "a",
|
||||
"full_name": "abc",
|
||||
"username": "b",
|
||||
},
|
||||
"owner": "securitySolution",
|
||||
"payload": Object {
|
||||
"comment": Object {
|
||||
"externalReferenceAttachmentTypeId": ".test",
|
||||
"externalReferenceId": "my-id",
|
||||
"externalReferenceMetadata": null,
|
||||
"externalReferenceStorage": Object {
|
||||
"soType": "test-so",
|
||||
"type": "savedObject",
|
||||
},
|
||||
"owner": "securitySolution",
|
||||
"type": "externalReference",
|
||||
},
|
||||
},
|
||||
"type": "comment",
|
||||
},
|
||||
"id": "100",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "my-id",
|
||||
"name": "externalReferenceId",
|
||||
"type": "test-so",
|
||||
},
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "associated-cases",
|
||||
"type": "cases",
|
||||
},
|
||||
Object {
|
||||
"id": "external-reference-test-id",
|
||||
"name": "associated-cases-comments",
|
||||
"type": "cases-comments",
|
||||
},
|
||||
],
|
||||
"score": 0,
|
||||
"type": "cases-user-actions",
|
||||
},
|
||||
],
|
||||
"total": 1,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`transform legacyTransformFindResponseToExternalModel persistable state attachments populates the persistable state 1`] = `
|
||||
Object {
|
||||
"page": 1,
|
||||
"per_page": 1,
|
||||
"saved_objects": Array [
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"action": "create",
|
||||
"action_id": "100",
|
||||
"case_id": "1",
|
||||
"comment_id": "persistable-state-test-id",
|
||||
"created_at": "abc",
|
||||
"created_by": Object {
|
||||
"email": "a",
|
||||
"full_name": "abc",
|
||||
"username": "b",
|
||||
},
|
||||
"owner": "securitySolution",
|
||||
"payload": Object {
|
||||
"comment": Object {
|
||||
"owner": "securitySolutionFixture",
|
||||
"persistableStateAttachmentState": Object {
|
||||
"foo": "foo",
|
||||
"injectedId": "testRef",
|
||||
},
|
||||
"persistableStateAttachmentTypeId": ".test",
|
||||
"type": "persistableState",
|
||||
},
|
||||
},
|
||||
"type": "comment",
|
||||
},
|
||||
"id": "100",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "testRef",
|
||||
"name": "myTestReference",
|
||||
"type": "test-so",
|
||||
},
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "associated-cases",
|
||||
"type": "cases",
|
||||
},
|
||||
Object {
|
||||
"id": "persistable-state-test-id",
|
||||
"name": "associated-cases-comments",
|
||||
"type": "cases-comments",
|
||||
},
|
||||
],
|
||||
"score": 0,
|
||||
"type": "cases-user-actions",
|
||||
},
|
||||
],
|
||||
"total": 1,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`transform legacyTransformFindResponseToExternalModel preserves the saved object fields and attributes when inject the ids 1`] = `
|
||||
Object {
|
||||
"page": 1,
|
||||
"per_page": 1,
|
||||
"saved_objects": Array [
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"action": "create",
|
||||
"action_id": "100",
|
||||
"case_id": "1",
|
||||
"comment_id": null,
|
||||
"created_at": "abc",
|
||||
"created_by": Object {
|
||||
"email": "a",
|
||||
"full_name": "abc",
|
||||
"username": "b",
|
||||
},
|
||||
"owner": "securitySolution",
|
||||
"payload": Object {
|
||||
"connector": Object {
|
||||
"fields": Object {
|
||||
"issueType": "bug",
|
||||
"parent": "2",
|
||||
"priority": "high",
|
||||
},
|
||||
"id": "1",
|
||||
"name": ".jira",
|
||||
"type": ".jira",
|
||||
},
|
||||
},
|
||||
"type": "connector",
|
||||
},
|
||||
"id": "100",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "associated-cases",
|
||||
"type": "cases",
|
||||
},
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "connectorId",
|
||||
"type": "action",
|
||||
},
|
||||
],
|
||||
"score": 0,
|
||||
"type": "cases-user-actions",
|
||||
},
|
||||
],
|
||||
"total": 1,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`transform transformFindResponseToExternalModel external references populates the external references attributes 1`] = `
|
||||
Object {
|
||||
"page": 1,
|
||||
"per_page": 1,
|
||||
"saved_objects": Array [
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"action": "create",
|
||||
"comment_id": "external-reference-test-id",
|
||||
"created_at": "abc",
|
||||
"created_by": Object {
|
||||
"email": "a",
|
||||
"full_name": "abc",
|
||||
"username": "b",
|
||||
},
|
||||
"owner": "securitySolution",
|
||||
"payload": Object {
|
||||
"comment": Object {
|
||||
"externalReferenceAttachmentTypeId": ".test",
|
||||
"externalReferenceId": "my-id",
|
||||
"externalReferenceMetadata": null,
|
||||
"externalReferenceStorage": Object {
|
||||
"soType": "test-so",
|
||||
"type": "savedObject",
|
||||
},
|
||||
"owner": "securitySolution",
|
||||
"type": "externalReference",
|
||||
},
|
||||
},
|
||||
"type": "comment",
|
||||
},
|
||||
"id": "100",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "my-id",
|
||||
"name": "externalReferenceId",
|
||||
"type": "test-so",
|
||||
},
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "associated-cases",
|
||||
"type": "cases",
|
||||
},
|
||||
Object {
|
||||
"id": "external-reference-test-id",
|
||||
"name": "associated-cases-comments",
|
||||
"type": "cases-comments",
|
||||
},
|
||||
],
|
||||
"score": 0,
|
||||
"type": "cases-user-actions",
|
||||
},
|
||||
],
|
||||
"total": 1,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`transform transformFindResponseToExternalModel persistable state attachments populates the persistable state 1`] = `
|
||||
Object {
|
||||
"page": 1,
|
||||
"per_page": 1,
|
||||
"saved_objects": Array [
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"action": "create",
|
||||
"comment_id": "persistable-state-test-id",
|
||||
"created_at": "abc",
|
||||
"created_by": Object {
|
||||
"email": "a",
|
||||
"full_name": "abc",
|
||||
"username": "b",
|
||||
},
|
||||
"owner": "securitySolution",
|
||||
"payload": Object {
|
||||
"comment": Object {
|
||||
"owner": "securitySolutionFixture",
|
||||
"persistableStateAttachmentState": Object {
|
||||
"foo": "foo",
|
||||
"injectedId": "testRef",
|
||||
},
|
||||
"persistableStateAttachmentTypeId": ".test",
|
||||
"type": "persistableState",
|
||||
},
|
||||
},
|
||||
"type": "comment",
|
||||
},
|
||||
"id": "100",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "testRef",
|
||||
"name": "myTestReference",
|
||||
"type": "test-so",
|
||||
},
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "associated-cases",
|
||||
"type": "cases",
|
||||
},
|
||||
Object {
|
||||
"id": "persistable-state-test-id",
|
||||
"name": "associated-cases-comments",
|
||||
"type": "cases-comments",
|
||||
},
|
||||
],
|
||||
"score": 0,
|
||||
"type": "cases-user-actions",
|
||||
},
|
||||
],
|
||||
"total": 1,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`transform transformFindResponseToExternalModel preserves the saved object fields and attributes when inject the ids 1`] = `
|
||||
Object {
|
||||
"page": 1,
|
||||
"per_page": 1,
|
||||
"saved_objects": Array [
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"action": "create",
|
||||
"comment_id": null,
|
||||
"created_at": "abc",
|
||||
"created_by": Object {
|
||||
"email": "a",
|
||||
"full_name": "abc",
|
||||
"username": "b",
|
||||
},
|
||||
"owner": "securitySolution",
|
||||
"payload": Object {
|
||||
"connector": Object {
|
||||
"fields": Object {
|
||||
"issueType": "bug",
|
||||
"parent": "2",
|
||||
"priority": "high",
|
||||
},
|
||||
"id": "1",
|
||||
"name": ".jira",
|
||||
"type": ".jira",
|
||||
},
|
||||
},
|
||||
"type": "connector",
|
||||
},
|
||||
"id": "100",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "associated-cases",
|
||||
"type": "cases",
|
||||
},
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "connectorId",
|
||||
"type": "action",
|
||||
},
|
||||
],
|
||||
"score": 0,
|
||||
"type": "cases-user-actions",
|
||||
},
|
||||
],
|
||||
"total": 1,
|
||||
}
|
||||
`;
|
|
@ -5,47 +5,20 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { get, omit } from 'lodash';
|
||||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
import { savedObjectsClientMock } from '@kbn/core/server/mocks';
|
||||
import type {
|
||||
SavedObject,
|
||||
SavedObjectReference,
|
||||
SavedObjectsBulkCreateObject,
|
||||
SavedObjectsFindResponse,
|
||||
SavedObjectsFindResult,
|
||||
SavedObjectsUpdateResponse,
|
||||
} from '@kbn/core/server';
|
||||
import { ACTION_SAVED_OBJECT_TYPE } from '@kbn/actions-plugin/server';
|
||||
import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
|
||||
import type {
|
||||
CaseAttributes,
|
||||
CaseUserActionAttributes,
|
||||
ConnectorUserAction,
|
||||
UserAction,
|
||||
} from '../../../common/api';
|
||||
import type { CaseAttributes } from '../../../common/api';
|
||||
import { Actions, ActionTypes, CaseSeverity, CaseStatuses } from '../../../common/api';
|
||||
import {
|
||||
CASE_COMMENT_SAVED_OBJECT,
|
||||
CASE_SAVED_OBJECT,
|
||||
CASE_USER_ACTION_SAVED_OBJECT,
|
||||
SECURITY_SOLUTION_OWNER,
|
||||
} from '../../../common/constants';
|
||||
import {
|
||||
CASE_REF_NAME,
|
||||
COMMENT_REF_NAME,
|
||||
CONNECTOR_ID_REFERENCE_NAME,
|
||||
EXTERNAL_REFERENCE_REF_NAME,
|
||||
PUSH_CONNECTOR_ID_REFERENCE_NAME,
|
||||
} from '../../common/constants';
|
||||
import { SECURITY_SOLUTION_OWNER } from '../../../common/constants';
|
||||
|
||||
import {
|
||||
createCaseSavedObjectResponse,
|
||||
createConnectorObject,
|
||||
createExternalService,
|
||||
createJiraConnector,
|
||||
createSOFindResponse,
|
||||
} from '../test_utils';
|
||||
import { createCaseSavedObjectResponse } from '../test_utils';
|
||||
import {
|
||||
casePayload,
|
||||
externalService,
|
||||
|
@ -57,255 +30,10 @@ import {
|
|||
originalCasesWithAssignee,
|
||||
updatedTagsCases,
|
||||
} from './mocks';
|
||||
import { CaseUserActionService, transformFindResponseToExternalModel } from '.';
|
||||
import type { PersistableStateAttachmentTypeRegistry } from '../../attachment_framework/persistable_state_registry';
|
||||
import {
|
||||
externalReferenceAttachmentSO,
|
||||
createPersistableStateAttachmentTypeRegistryMock,
|
||||
persistableStateAttachment,
|
||||
} from '../../attachment_framework/mocks';
|
||||
import { CaseUserActionService } from '.';
|
||||
import { createPersistableStateAttachmentTypeRegistryMock } from '../../attachment_framework/mocks';
|
||||
import { serializerMock } from '@kbn/core-saved-objects-base-server-mocks';
|
||||
|
||||
const createConnectorUserAction = (
|
||||
overrides?: Partial<CaseUserActionAttributes>
|
||||
): SavedObject<CaseUserActionAttributes> => {
|
||||
const { id, ...restConnector } = createConnectorObject().connector;
|
||||
return {
|
||||
...createUserActionSO({
|
||||
action: Actions.create,
|
||||
payload: { connector: restConnector },
|
||||
type: 'connector',
|
||||
connectorId: id,
|
||||
}),
|
||||
...(overrides && { ...overrides }),
|
||||
};
|
||||
};
|
||||
|
||||
const updateConnectorUserAction = ({
|
||||
overrides,
|
||||
}: {
|
||||
overrides?: Partial<CaseUserActionAttributes>;
|
||||
} = {}): SavedObject<CaseUserActionAttributes> => {
|
||||
const { id, ...restConnector } = createJiraConnector();
|
||||
return {
|
||||
...createUserActionSO({
|
||||
action: Actions.update,
|
||||
payload: { connector: restConnector },
|
||||
type: 'connector',
|
||||
connectorId: id,
|
||||
}),
|
||||
...(overrides && { ...overrides }),
|
||||
};
|
||||
};
|
||||
|
||||
const pushConnectorUserAction = ({
|
||||
overrides,
|
||||
}: {
|
||||
overrides?: Partial<CaseUserActionAttributes>;
|
||||
} = {}): SavedObject<CaseUserActionAttributes> => {
|
||||
const { connector_id: connectorId, ...restExternalService } = createExternalService();
|
||||
return {
|
||||
...createUserActionSO({
|
||||
action: Actions.push_to_service,
|
||||
payload: { externalService: restExternalService },
|
||||
pushedConnectorId: connectorId,
|
||||
type: 'pushed',
|
||||
}),
|
||||
...(overrides && { ...overrides }),
|
||||
};
|
||||
};
|
||||
|
||||
const createCaseUserAction = (): SavedObject<CaseUserActionAttributes> => {
|
||||
const { id, ...restConnector } = createJiraConnector();
|
||||
return {
|
||||
...createUserActionSO({
|
||||
action: Actions.create,
|
||||
payload: {
|
||||
connector: restConnector,
|
||||
title: 'a title',
|
||||
description: 'a desc',
|
||||
settings: { syncAlerts: false },
|
||||
status: CaseStatuses.open,
|
||||
severity: CaseSeverity.LOW,
|
||||
tags: [],
|
||||
owner: SECURITY_SOLUTION_OWNER,
|
||||
},
|
||||
connectorId: id,
|
||||
type: 'create_case',
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
const createUserActionFindSO = (
|
||||
userAction: SavedObject<CaseUserActionAttributes>
|
||||
): SavedObjectsFindResult<CaseUserActionAttributes> => ({
|
||||
...userAction,
|
||||
score: 0,
|
||||
});
|
||||
|
||||
const createUserActionSO = ({
|
||||
action,
|
||||
attributesOverrides,
|
||||
commentId,
|
||||
connectorId,
|
||||
pushedConnectorId,
|
||||
payload,
|
||||
type,
|
||||
references = [],
|
||||
}: {
|
||||
action: UserAction;
|
||||
type?: string;
|
||||
payload?: Record<string, unknown>;
|
||||
attributesOverrides?: Partial<CaseUserActionAttributes>;
|
||||
commentId?: string;
|
||||
connectorId?: string;
|
||||
pushedConnectorId?: string;
|
||||
references?: SavedObjectReference[];
|
||||
}): SavedObject<CaseUserActionAttributes> => {
|
||||
const defaultParams = {
|
||||
action,
|
||||
created_at: 'abc',
|
||||
created_by: {
|
||||
email: 'a',
|
||||
username: 'b',
|
||||
full_name: 'abc',
|
||||
},
|
||||
type: type ?? 'title',
|
||||
payload: payload ?? { title: 'a new title' },
|
||||
owner: 'securitySolution',
|
||||
};
|
||||
|
||||
return {
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
id: '100',
|
||||
attributes: {
|
||||
...defaultParams,
|
||||
...(attributesOverrides && { ...attributesOverrides }),
|
||||
},
|
||||
references: [
|
||||
...references,
|
||||
{
|
||||
type: CASE_SAVED_OBJECT,
|
||||
name: CASE_REF_NAME,
|
||||
id: '1',
|
||||
},
|
||||
...(commentId
|
||||
? [
|
||||
{
|
||||
type: CASE_COMMENT_SAVED_OBJECT,
|
||||
name: COMMENT_REF_NAME,
|
||||
id: commentId,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(connectorId
|
||||
? [
|
||||
{
|
||||
type: ACTION_SAVED_OBJECT_TYPE,
|
||||
name: CONNECTOR_ID_REFERENCE_NAME,
|
||||
id: connectorId,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(pushedConnectorId
|
||||
? [
|
||||
{
|
||||
type: ACTION_SAVED_OBJECT_TYPE,
|
||||
name: PUSH_CONNECTOR_ID_REFERENCE_NAME,
|
||||
id: pushedConnectorId,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
} as SavedObject<CaseUserActionAttributes>;
|
||||
};
|
||||
|
||||
const createPersistableStateUserAction = () => {
|
||||
return {
|
||||
...createUserActionSO({
|
||||
action: Actions.create,
|
||||
commentId: 'persistable-state-test-id',
|
||||
payload: {
|
||||
comment: {
|
||||
...persistableStateAttachment,
|
||||
persistableStateAttachmentState: { foo: 'foo' },
|
||||
},
|
||||
},
|
||||
type: 'comment',
|
||||
references: [{ id: 'testRef', name: 'myTestReference', type: 'test-so' }],
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
const createExternalReferenceUserAction = () => {
|
||||
return {
|
||||
...createUserActionSO({
|
||||
action: Actions.create,
|
||||
commentId: 'external-reference-test-id',
|
||||
payload: {
|
||||
comment: omit(externalReferenceAttachmentSO, 'externalReferenceId'),
|
||||
},
|
||||
type: 'comment',
|
||||
references: [{ id: 'my-id', name: EXTERNAL_REFERENCE_REF_NAME, type: 'test-so' }],
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
const testConnectorId = (
|
||||
persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry,
|
||||
userAction: SavedObject<CaseUserActionAttributes>,
|
||||
path: string,
|
||||
expectedConnectorId = '1'
|
||||
) => {
|
||||
it('does set payload.connector.id to none when it cannot find the reference', () => {
|
||||
const userActionWithEmptyRef = { ...userAction, references: [] };
|
||||
const transformed = transformFindResponseToExternalModel(
|
||||
createSOFindResponse([createUserActionFindSO(userActionWithEmptyRef)]),
|
||||
persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
|
||||
expect(get(transformed.saved_objects[0].attributes.payload, path)).toBe('none');
|
||||
});
|
||||
|
||||
it('does not populate the payload.connector.id when the reference exists but the action is not of type connector', () => {
|
||||
const invalidUserAction = {
|
||||
...userAction,
|
||||
attributes: { ...userAction.attributes, type: 'not-connector' },
|
||||
};
|
||||
const transformed = transformFindResponseToExternalModel(
|
||||
createSOFindResponse([
|
||||
createUserActionFindSO(invalidUserAction as SavedObject<CaseUserActionAttributes>),
|
||||
]),
|
||||
persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
|
||||
expect(get(transformed.saved_objects[0].attributes.payload, path)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not populate the payload.connector.id when the reference exists but the payload does not contain a connector', () => {
|
||||
const invalidUserAction = {
|
||||
...userAction,
|
||||
attributes: { ...userAction.attributes, payload: {} },
|
||||
};
|
||||
const transformed = transformFindResponseToExternalModel(
|
||||
createSOFindResponse([
|
||||
createUserActionFindSO(invalidUserAction as SavedObject<CaseUserActionAttributes>),
|
||||
]),
|
||||
persistableStateAttachmentTypeRegistry
|
||||
) as SavedObjectsFindResponse<ConnectorUserAction>;
|
||||
|
||||
expect(get(transformed.saved_objects[0].attributes.payload, path)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('populates the payload.connector.id', () => {
|
||||
const transformed = transformFindResponseToExternalModel(
|
||||
createSOFindResponse([createUserActionFindSO(userAction)]),
|
||||
persistableStateAttachmentTypeRegistry
|
||||
) as SavedObjectsFindResponse<ConnectorUserAction>;
|
||||
|
||||
expect(get(transformed.saved_objects[0].attributes.payload, path)).toEqual(expectedConnectorId);
|
||||
});
|
||||
};
|
||||
import { createUserActionFindSO, createConnectorUserAction } from './test_utils';
|
||||
|
||||
describe('CaseUserActionService', () => {
|
||||
const persistableStateAttachmentTypeRegistry = createPersistableStateAttachmentTypeRegistryMock();
|
||||
|
@ -319,322 +47,6 @@ describe('CaseUserActionService', () => {
|
|||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe('transformFindResponseToExternalModel', () => {
|
||||
it('does not populate the ids when the response is an empty array', () => {
|
||||
expect(
|
||||
transformFindResponseToExternalModel(
|
||||
createSOFindResponse([]),
|
||||
persistableStateAttachmentTypeRegistry
|
||||
)
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"page": 1,
|
||||
"per_page": 0,
|
||||
"saved_objects": Array [],
|
||||
"total": 0,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('preserves the saved object fields and attributes when inject the ids', () => {
|
||||
const transformed = transformFindResponseToExternalModel(
|
||||
createSOFindResponse([createUserActionFindSO(createConnectorUserAction())]),
|
||||
persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
|
||||
expect(transformed).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"page": 1,
|
||||
"per_page": 1,
|
||||
"saved_objects": Array [
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"action": "create",
|
||||
"action_id": "100",
|
||||
"case_id": "1",
|
||||
"comment_id": null,
|
||||
"created_at": "abc",
|
||||
"created_by": Object {
|
||||
"email": "a",
|
||||
"full_name": "abc",
|
||||
"username": "b",
|
||||
},
|
||||
"owner": "securitySolution",
|
||||
"payload": Object {
|
||||
"connector": Object {
|
||||
"fields": Object {
|
||||
"issueType": "bug",
|
||||
"parent": "2",
|
||||
"priority": "high",
|
||||
},
|
||||
"id": "1",
|
||||
"name": ".jira",
|
||||
"type": ".jira",
|
||||
},
|
||||
},
|
||||
"type": "connector",
|
||||
},
|
||||
"id": "100",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "associated-cases",
|
||||
"type": "cases",
|
||||
},
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "connectorId",
|
||||
"type": "action",
|
||||
},
|
||||
],
|
||||
"score": 0,
|
||||
"type": "cases-user-actions",
|
||||
},
|
||||
],
|
||||
"total": 1,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('populates the payload.connector.id for multiple user actions', () => {
|
||||
const transformed = transformFindResponseToExternalModel(
|
||||
createSOFindResponse([
|
||||
createUserActionFindSO(createConnectorUserAction()),
|
||||
createUserActionFindSO(createConnectorUserAction()),
|
||||
]),
|
||||
persistableStateAttachmentTypeRegistry
|
||||
) as SavedObjectsFindResponse<ConnectorUserAction>;
|
||||
|
||||
expect(transformed.saved_objects[0].attributes.payload.connector.id).toEqual('1');
|
||||
expect(transformed.saved_objects[1].attributes.payload.connector.id).toEqual('1');
|
||||
});
|
||||
|
||||
describe('reference ids', () => {
|
||||
it('sets case_id to an empty string when it cannot find the reference', () => {
|
||||
const userAction = {
|
||||
...createConnectorUserAction(),
|
||||
references: [],
|
||||
};
|
||||
const transformed = transformFindResponseToExternalModel(
|
||||
createSOFindResponse([createUserActionFindSO(userAction)]),
|
||||
persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
|
||||
expect(transformed.saved_objects[0].attributes.case_id).toEqual('');
|
||||
});
|
||||
|
||||
it('sets comment_id to null when it cannot find the reference', () => {
|
||||
const userAction = {
|
||||
...createUserActionSO({ action: Actions.create, commentId: '5' }),
|
||||
references: [],
|
||||
};
|
||||
const transformed = transformFindResponseToExternalModel(
|
||||
createSOFindResponse([createUserActionFindSO(userAction)]),
|
||||
persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
|
||||
expect(transformed.saved_objects[0].attributes.comment_id).toBeNull();
|
||||
});
|
||||
|
||||
it('sets case_id correctly when it finds the reference', () => {
|
||||
const userAction = createConnectorUserAction();
|
||||
|
||||
const transformed = transformFindResponseToExternalModel(
|
||||
createSOFindResponse([createUserActionFindSO(userAction)]),
|
||||
persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
|
||||
expect(transformed.saved_objects[0].attributes.case_id).toEqual('1');
|
||||
});
|
||||
|
||||
it('sets comment_id correctly when it finds the reference', () => {
|
||||
const userAction = createUserActionSO({
|
||||
action: Actions.create,
|
||||
commentId: '5',
|
||||
});
|
||||
|
||||
const transformed = transformFindResponseToExternalModel(
|
||||
createSOFindResponse([createUserActionFindSO(userAction)]),
|
||||
persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
|
||||
expect(transformed.saved_objects[0].attributes.comment_id).toEqual('5');
|
||||
});
|
||||
|
||||
it('sets action_id correctly to the saved object id', () => {
|
||||
const userAction = {
|
||||
...createUserActionSO({ action: Actions.create, commentId: '5' }),
|
||||
};
|
||||
|
||||
const transformed = transformFindResponseToExternalModel(
|
||||
createSOFindResponse([createUserActionFindSO(userAction)]),
|
||||
persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
|
||||
expect(transformed.saved_objects[0].attributes.action_id).toEqual('100');
|
||||
});
|
||||
});
|
||||
|
||||
describe('create connector', () => {
|
||||
const userAction = createConnectorUserAction();
|
||||
testConnectorId(persistableStateAttachmentTypeRegistry, userAction, 'connector.id');
|
||||
});
|
||||
|
||||
describe('update connector', () => {
|
||||
const userAction = updateConnectorUserAction();
|
||||
testConnectorId(persistableStateAttachmentTypeRegistry, userAction, 'connector.id');
|
||||
});
|
||||
|
||||
describe('push connector', () => {
|
||||
const userAction = pushConnectorUserAction();
|
||||
testConnectorId(
|
||||
persistableStateAttachmentTypeRegistry,
|
||||
userAction,
|
||||
'externalService.connector_id',
|
||||
'100'
|
||||
);
|
||||
});
|
||||
|
||||
describe('create case', () => {
|
||||
const userAction = createCaseUserAction();
|
||||
testConnectorId(persistableStateAttachmentTypeRegistry, userAction, 'connector.id');
|
||||
});
|
||||
|
||||
describe('persistable state attachments', () => {
|
||||
it('populates the persistable state', () => {
|
||||
const transformed = transformFindResponseToExternalModel(
|
||||
createSOFindResponse([createUserActionFindSO(createPersistableStateUserAction())]),
|
||||
persistableStateAttachmentTypeRegistry
|
||||
) as SavedObjectsFindResponse<ConnectorUserAction>;
|
||||
|
||||
expect(transformed).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"page": 1,
|
||||
"per_page": 1,
|
||||
"saved_objects": Array [
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"action": "create",
|
||||
"action_id": "100",
|
||||
"case_id": "1",
|
||||
"comment_id": "persistable-state-test-id",
|
||||
"created_at": "abc",
|
||||
"created_by": Object {
|
||||
"email": "a",
|
||||
"full_name": "abc",
|
||||
"username": "b",
|
||||
},
|
||||
"owner": "securitySolution",
|
||||
"payload": Object {
|
||||
"comment": Object {
|
||||
"owner": "securitySolutionFixture",
|
||||
"persistableStateAttachmentState": Object {
|
||||
"foo": "foo",
|
||||
"injectedId": "testRef",
|
||||
},
|
||||
"persistableStateAttachmentTypeId": ".test",
|
||||
"type": "persistableState",
|
||||
},
|
||||
},
|
||||
"type": "comment",
|
||||
},
|
||||
"id": "100",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "testRef",
|
||||
"name": "myTestReference",
|
||||
"type": "test-so",
|
||||
},
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "associated-cases",
|
||||
"type": "cases",
|
||||
},
|
||||
Object {
|
||||
"id": "persistable-state-test-id",
|
||||
"name": "associated-cases-comments",
|
||||
"type": "cases-comments",
|
||||
},
|
||||
],
|
||||
"score": 0,
|
||||
"type": "cases-user-actions",
|
||||
},
|
||||
],
|
||||
"total": 1,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('external references', () => {
|
||||
it('populates the external references attributes', () => {
|
||||
const transformed = transformFindResponseToExternalModel(
|
||||
createSOFindResponse([createUserActionFindSO(createExternalReferenceUserAction())]),
|
||||
persistableStateAttachmentTypeRegistry
|
||||
) as SavedObjectsFindResponse<ConnectorUserAction>;
|
||||
|
||||
expect(transformed).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"page": 1,
|
||||
"per_page": 1,
|
||||
"saved_objects": Array [
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"action": "create",
|
||||
"action_id": "100",
|
||||
"case_id": "1",
|
||||
"comment_id": "external-reference-test-id",
|
||||
"created_at": "abc",
|
||||
"created_by": Object {
|
||||
"email": "a",
|
||||
"full_name": "abc",
|
||||
"username": "b",
|
||||
},
|
||||
"owner": "securitySolution",
|
||||
"payload": Object {
|
||||
"comment": Object {
|
||||
"externalReferenceAttachmentTypeId": ".test",
|
||||
"externalReferenceId": "my-id",
|
||||
"externalReferenceMetadata": null,
|
||||
"externalReferenceStorage": Object {
|
||||
"soType": "test-so",
|
||||
"type": "savedObject",
|
||||
},
|
||||
"owner": "securitySolution",
|
||||
"type": "externalReference",
|
||||
},
|
||||
},
|
||||
"type": "comment",
|
||||
},
|
||||
"id": "100",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "my-id",
|
||||
"name": "externalReferenceId",
|
||||
"type": "test-so",
|
||||
},
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "associated-cases",
|
||||
"type": "cases",
|
||||
},
|
||||
Object {
|
||||
"id": "external-reference-test-id",
|
||||
"name": "associated-cases-comments",
|
||||
"type": "cases-comments",
|
||||
},
|
||||
],
|
||||
"score": 0,
|
||||
"type": "cases-user-actions",
|
||||
},
|
||||
],
|
||||
"total": 1,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('methods', () => {
|
||||
let service: CaseUserActionService;
|
||||
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
|
||||
|
|
|
@ -14,39 +14,23 @@ import type {
|
|||
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type { KueryNode } from '@kbn/es-query';
|
||||
import { isCommentRequestTypePersistableState } from '../../../common/utils/attachments';
|
||||
import {
|
||||
isConnectorUserAction,
|
||||
isPushedUserAction,
|
||||
isCreateCaseUserAction,
|
||||
isCommentUserAction,
|
||||
} from '../../../common/utils/user_actions';
|
||||
import type {
|
||||
CaseUserActionAttributes,
|
||||
CaseUserActionAttributesWithoutConnectorId,
|
||||
CaseUserActionInjectedAttributesWithoutActionId,
|
||||
CaseUserActionResponse,
|
||||
} from '../../../common/api';
|
||||
import { Actions, ActionTypes, NONE_CONNECTOR_ID } from '../../../common/api';
|
||||
import { ActionTypes } from '../../../common/api';
|
||||
import {
|
||||
CASE_SAVED_OBJECT,
|
||||
CASE_USER_ACTION_SAVED_OBJECT,
|
||||
MAX_DOCS_PER_PAGE,
|
||||
CASE_COMMENT_SAVED_OBJECT,
|
||||
} from '../../../common/constants';
|
||||
import {
|
||||
CASE_REF_NAME,
|
||||
COMMENT_REF_NAME,
|
||||
CONNECTOR_ID_REFERENCE_NAME,
|
||||
EXTERNAL_REFERENCE_REF_NAME,
|
||||
PUSH_CONNECTOR_ID_REFERENCE_NAME,
|
||||
} from '../../common/constants';
|
||||
import { findConnectorIdReference } from '../transform';
|
||||
import { buildFilter, combineFilters } from '../../client/utils';
|
||||
import type { CaseConnectorActivity, CaseConnectorFields, PushInfo, ServiceContext } from './types';
|
||||
import { defaultSortField, isCommentRequestTypeExternalReferenceSO } from '../../common/utils';
|
||||
import type { PersistableStateAttachmentTypeRegistry } from '../../attachment_framework/persistable_state_registry';
|
||||
import { injectPersistableReferencesToSO } from '../../attachment_framework/so_references';
|
||||
import { defaultSortField } from '../../common/utils';
|
||||
import { UserActionPersister } from './operations/create';
|
||||
import { UserActionFinder } from './operations/find';
|
||||
import { transformToExternalModel, legacyTransformFindResponseToExternalModel } from './transform';
|
||||
|
||||
export interface UserActionItem {
|
||||
attributes: CaseUserActionAttributesWithoutConnectorId;
|
||||
|
@ -97,15 +81,21 @@ interface ConnectorFieldsBeforePushAggsResult {
|
|||
|
||||
export class CaseUserActionService {
|
||||
private readonly _creator: UserActionPersister;
|
||||
private readonly _finder: UserActionFinder;
|
||||
|
||||
constructor(private readonly context: ServiceContext) {
|
||||
this._creator = new UserActionPersister(context);
|
||||
this._finder = new UserActionFinder(context);
|
||||
}
|
||||
|
||||
public get creator() {
|
||||
return this._creator;
|
||||
}
|
||||
|
||||
public get finder() {
|
||||
return this._finder;
|
||||
}
|
||||
|
||||
public async getConnectorFieldsBeforeLatestPush(
|
||||
caseId: string,
|
||||
pushes: PushInfo[]
|
||||
|
@ -271,7 +261,7 @@ export class CaseUserActionService {
|
|||
|
||||
public async getMostRecentUserAction(
|
||||
caseId: string
|
||||
): Promise<SavedObject<CaseUserActionResponse> | undefined> {
|
||||
): Promise<SavedObject<CaseUserActionInjectedAttributesWithoutActionId> | undefined> {
|
||||
try {
|
||||
this.context.log.debug(
|
||||
`Attempting to retrieve the most recent user action for case id: ${caseId}`
|
||||
|
@ -378,7 +368,7 @@ export class CaseUserActionService {
|
|||
rawFieldsDoc = createCase.mostRecent.hits.hits[0];
|
||||
}
|
||||
|
||||
let fieldsDoc: SavedObject<CaseUserActionResponse> | undefined;
|
||||
let fieldsDoc: SavedObject<CaseUserActionInjectedAttributesWithoutActionId> | undefined;
|
||||
if (rawFieldsDoc != null) {
|
||||
const doc =
|
||||
this.context.savedObjectsSerializer.rawToSavedObject<CaseUserActionAttributesWithoutConnectorId>(
|
||||
|
@ -392,7 +382,7 @@ export class CaseUserActionService {
|
|||
}
|
||||
|
||||
const pushInfo = connectorInfo.reverse.connectorActivity.buckets.pushInfo;
|
||||
let pushDoc: SavedObject<CaseUserActionResponse> | undefined;
|
||||
let pushDoc: SavedObject<CaseUserActionInjectedAttributesWithoutActionId> | undefined;
|
||||
|
||||
if (pushInfo.mostRecent.hits.hits.length > 0) {
|
||||
const rawPushDoc = pushInfo.mostRecent.hits.hits[0];
|
||||
|
@ -520,7 +510,7 @@ export class CaseUserActionService {
|
|||
}
|
||||
);
|
||||
|
||||
return transformFindResponseToExternalModel(
|
||||
return legacyTransformFindResponseToExternalModel(
|
||||
userActions,
|
||||
this.context.persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
|
@ -562,60 +552,6 @@ export class CaseUserActionService {
|
|||
}
|
||||
}
|
||||
|
||||
public async findStatusChanges({
|
||||
caseId,
|
||||
filter,
|
||||
}: {
|
||||
caseId: string;
|
||||
filter?: KueryNode;
|
||||
}): Promise<Array<SavedObject<CaseUserActionResponse>>> {
|
||||
try {
|
||||
this.context.log.debug('Attempting to find status changes');
|
||||
|
||||
const updateActionFilter = buildFilter({
|
||||
filters: Actions.update,
|
||||
field: 'action',
|
||||
operator: 'or',
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
});
|
||||
|
||||
const statusChangeFilter = buildFilter({
|
||||
filters: ActionTypes.status,
|
||||
field: 'type',
|
||||
operator: 'or',
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
});
|
||||
|
||||
const combinedFilters = combineFilters([updateActionFilter, statusChangeFilter, filter]);
|
||||
|
||||
const finder =
|
||||
this.context.unsecuredSavedObjectsClient.createPointInTimeFinder<CaseUserActionAttributesWithoutConnectorId>(
|
||||
{
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
hasReference: { type: CASE_SAVED_OBJECT, id: caseId },
|
||||
sortField: defaultSortField,
|
||||
sortOrder: 'asc',
|
||||
filter: combinedFilters,
|
||||
perPage: MAX_DOCS_PER_PAGE,
|
||||
}
|
||||
);
|
||||
|
||||
let userActions: Array<SavedObject<CaseUserActionResponse>> = [];
|
||||
for await (const findResults of finder.find()) {
|
||||
userActions = userActions.concat(
|
||||
findResults.saved_objects.map((so) =>
|
||||
transformToExternalModel(so, this.context.persistableStateAttachmentTypeRegistry)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return userActions;
|
||||
} catch (error) {
|
||||
this.context.log.error(`Error finding status changes: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getUniqueConnectors({
|
||||
caseId,
|
||||
filter,
|
||||
|
@ -691,126 +627,3 @@ export class CaseUserActionService {
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function transformFindResponseToExternalModel(
|
||||
userActions: SavedObjectsFindResponse<CaseUserActionAttributesWithoutConnectorId>,
|
||||
persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry
|
||||
): SavedObjectsFindResponse<CaseUserActionResponse> {
|
||||
return {
|
||||
...userActions,
|
||||
saved_objects: userActions.saved_objects.map((so) => ({
|
||||
...so,
|
||||
...transformToExternalModel(so, persistableStateAttachmentTypeRegistry),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function transformToExternalModel(
|
||||
userAction: SavedObject<CaseUserActionAttributesWithoutConnectorId>,
|
||||
persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry
|
||||
): SavedObject<CaseUserActionResponse> {
|
||||
const { references } = userAction;
|
||||
|
||||
const caseId = findReferenceId(CASE_REF_NAME, CASE_SAVED_OBJECT, references) ?? '';
|
||||
const commentId =
|
||||
findReferenceId(COMMENT_REF_NAME, CASE_COMMENT_SAVED_OBJECT, references) ?? null;
|
||||
const payload = addReferenceIdToPayload(userAction, persistableStateAttachmentTypeRegistry);
|
||||
|
||||
return {
|
||||
...userAction,
|
||||
attributes: {
|
||||
...userAction.attributes,
|
||||
action_id: userAction.id,
|
||||
case_id: caseId,
|
||||
comment_id: commentId,
|
||||
payload,
|
||||
} as CaseUserActionResponse,
|
||||
};
|
||||
}
|
||||
|
||||
const addReferenceIdToPayload = (
|
||||
userAction: SavedObject<CaseUserActionAttributes>,
|
||||
persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry
|
||||
): CaseUserActionAttributes['payload'] => {
|
||||
const connectorId = getConnectorIdFromReferences(userAction);
|
||||
const userActionAttributes = userAction.attributes;
|
||||
|
||||
if (isConnectorUserAction(userActionAttributes) || isCreateCaseUserAction(userActionAttributes)) {
|
||||
return {
|
||||
...userActionAttributes.payload,
|
||||
connector: {
|
||||
...userActionAttributes.payload.connector,
|
||||
id: connectorId ?? NONE_CONNECTOR_ID,
|
||||
},
|
||||
};
|
||||
} else if (isPushedUserAction(userActionAttributes)) {
|
||||
return {
|
||||
...userAction.attributes.payload,
|
||||
externalService: {
|
||||
...userActionAttributes.payload.externalService,
|
||||
connector_id: connectorId ?? NONE_CONNECTOR_ID,
|
||||
},
|
||||
};
|
||||
} else if (isCommentUserAction(userActionAttributes)) {
|
||||
if (isCommentRequestTypeExternalReferenceSO(userActionAttributes.payload.comment)) {
|
||||
const externalReferenceId = findReferenceId(
|
||||
EXTERNAL_REFERENCE_REF_NAME,
|
||||
userActionAttributes.payload.comment.externalReferenceStorage.soType,
|
||||
userAction.references
|
||||
);
|
||||
|
||||
return {
|
||||
...userAction.attributes.payload,
|
||||
comment: {
|
||||
...userActionAttributes.payload.comment,
|
||||
externalReferenceId: externalReferenceId ?? '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (isCommentRequestTypePersistableState(userActionAttributes.payload.comment)) {
|
||||
const injectedAttributes = injectPersistableReferencesToSO(
|
||||
userActionAttributes.payload.comment,
|
||||
userAction.references,
|
||||
{
|
||||
persistableStateAttachmentTypeRegistry,
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
...userAction.attributes.payload,
|
||||
comment: {
|
||||
...userActionAttributes.payload.comment,
|
||||
...injectedAttributes,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return userAction.attributes.payload;
|
||||
};
|
||||
|
||||
function getConnectorIdFromReferences(
|
||||
userAction: SavedObject<CaseUserActionAttributes>
|
||||
): string | null {
|
||||
const { references } = userAction;
|
||||
|
||||
if (
|
||||
isConnectorUserAction(userAction.attributes) ||
|
||||
isCreateCaseUserAction(userAction.attributes)
|
||||
) {
|
||||
return findConnectorIdReference(CONNECTOR_ID_REFERENCE_NAME, references)?.id ?? null;
|
||||
} else if (isPushedUserAction(userAction.attributes)) {
|
||||
return findConnectorIdReference(PUSH_CONNECTOR_ID_REFERENCE_NAME, references)?.id ?? null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function findReferenceId(
|
||||
name: string,
|
||||
type: string,
|
||||
references: SavedObjectReference[]
|
||||
): string | undefined {
|
||||
return references.find((ref) => ref.name === name && ref.type === type)?.id;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,228 @@
|
|||
/*
|
||||
* 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 { KueryNode } from '@kbn/es-query';
|
||||
import { fromKueryExpression } from '@kbn/es-query';
|
||||
import type { SavedObjectsFindResponse } from '@kbn/core-saved-objects-api-server';
|
||||
import type { SavedObject } from '@kbn/core-saved-objects-common';
|
||||
import { DEFAULT_PAGE, DEFAULT_PER_PAGE } from '../../../routes/api';
|
||||
import { defaultSortField } from '../../../common/utils';
|
||||
import type {
|
||||
CaseUserActionAttributesWithoutConnectorId,
|
||||
UserActionFindRequest,
|
||||
ActionTypeValues,
|
||||
FindTypeField,
|
||||
CaseUserActionInjectedAttributesWithoutActionId,
|
||||
} from '../../../../common/api';
|
||||
import { Actions, ActionTypes, CommentType } from '../../../../common/api';
|
||||
import {
|
||||
CASE_SAVED_OBJECT,
|
||||
CASE_USER_ACTION_SAVED_OBJECT,
|
||||
MAX_DOCS_PER_PAGE,
|
||||
} from '../../../../common/constants';
|
||||
|
||||
import type { ServiceContext } from '../types';
|
||||
import { transformFindResponseToExternalModel, transformToExternalModel } from '../transform';
|
||||
import { buildFilter, combineFilters, NodeBuilderOperators } from '../../../client/utils';
|
||||
|
||||
interface FindOptions extends UserActionFindRequest {
|
||||
caseId: string;
|
||||
filter?: KueryNode;
|
||||
}
|
||||
|
||||
export class UserActionFinder {
|
||||
constructor(private readonly context: ServiceContext) {}
|
||||
|
||||
public async find({
|
||||
caseId,
|
||||
sortOrder,
|
||||
types,
|
||||
page,
|
||||
perPage,
|
||||
filter,
|
||||
}: FindOptions): Promise<
|
||||
SavedObjectsFindResponse<CaseUserActionInjectedAttributesWithoutActionId>
|
||||
> {
|
||||
try {
|
||||
this.context.log.debug(`Attempting to find user actions for case id: ${caseId}`);
|
||||
|
||||
const finalFilter = combineFilters([filter, UserActionFinder.buildFilter(types)]);
|
||||
|
||||
const userActions =
|
||||
await this.context.unsecuredSavedObjectsClient.find<CaseUserActionAttributesWithoutConnectorId>(
|
||||
{
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
hasReference: { type: CASE_SAVED_OBJECT, id: caseId },
|
||||
page: page ?? DEFAULT_PAGE,
|
||||
perPage: perPage ?? DEFAULT_PER_PAGE,
|
||||
sortField: 'created_at',
|
||||
sortOrder: sortOrder ?? 'asc',
|
||||
filter: finalFilter,
|
||||
}
|
||||
);
|
||||
|
||||
return transformFindResponseToExternalModel(
|
||||
userActions,
|
||||
this.context.persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
} catch (error) {
|
||||
this.context.log.error(`Error finding user actions for case id: ${caseId}: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private static buildFilter(types: FindOptions['types'] = []) {
|
||||
const filters = types.map((type) => UserActionFinder.buildFilterType(type));
|
||||
return combineFilters(filters, NodeBuilderOperators.or);
|
||||
}
|
||||
|
||||
private static buildFilterType(type: FindTypeField): KueryNode | undefined {
|
||||
switch (type) {
|
||||
case 'action':
|
||||
return UserActionFinder.buildActionFilter();
|
||||
case 'user':
|
||||
return UserActionFinder.buildCommentTypeFilter();
|
||||
case 'alert':
|
||||
return UserActionFinder.buildAlertCommentTypeFilter();
|
||||
case 'attachment':
|
||||
return UserActionFinder.buildAttachmentsFilter();
|
||||
default:
|
||||
return UserActionFinder.buildGenericTypeFilter(type);
|
||||
}
|
||||
}
|
||||
|
||||
private static buildActionFilter(): KueryNode | undefined {
|
||||
const filterForUserActionsExcludingComment = fromKueryExpression(
|
||||
`not ${CASE_USER_ACTION_SAVED_OBJECT}.attributes.payload.comment.type: ${CommentType.user}`
|
||||
);
|
||||
|
||||
return filterForUserActionsExcludingComment;
|
||||
}
|
||||
|
||||
private static buildCommentTypeFilter(): KueryNode | undefined {
|
||||
return combineFilters(
|
||||
[
|
||||
buildFilter({
|
||||
filters: [ActionTypes.comment],
|
||||
field: 'type',
|
||||
operator: 'or',
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
}),
|
||||
buildFilter({
|
||||
filters: [CommentType.user],
|
||||
field: 'payload.comment.type',
|
||||
operator: 'or',
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
}),
|
||||
],
|
||||
NodeBuilderOperators.and
|
||||
);
|
||||
}
|
||||
|
||||
private static buildAlertCommentTypeFilter(): KueryNode | undefined {
|
||||
return combineFilters(
|
||||
[
|
||||
buildFilter({
|
||||
filters: [ActionTypes.comment],
|
||||
field: 'type',
|
||||
operator: 'or',
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
}),
|
||||
buildFilter({
|
||||
filters: [CommentType.alert],
|
||||
field: 'payload.comment.type',
|
||||
operator: 'or',
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
}),
|
||||
],
|
||||
NodeBuilderOperators.and
|
||||
);
|
||||
}
|
||||
|
||||
private static buildAttachmentsFilter(): KueryNode | undefined {
|
||||
return combineFilters(
|
||||
[
|
||||
buildFilter({
|
||||
filters: [ActionTypes.comment],
|
||||
field: 'type',
|
||||
operator: 'or',
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
}),
|
||||
buildFilter({
|
||||
filters: [CommentType.persistableState, CommentType.externalReference],
|
||||
field: 'payload.comment.type',
|
||||
operator: 'or',
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
}),
|
||||
],
|
||||
NodeBuilderOperators.and
|
||||
);
|
||||
}
|
||||
|
||||
private static buildGenericTypeFilter(type: ActionTypeValues): KueryNode | undefined {
|
||||
return buildFilter({
|
||||
filters: [type],
|
||||
field: 'type',
|
||||
operator: 'or',
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
});
|
||||
}
|
||||
|
||||
public async findStatusChanges({
|
||||
caseId,
|
||||
filter,
|
||||
}: {
|
||||
caseId: string;
|
||||
filter?: KueryNode;
|
||||
}): Promise<Array<SavedObject<CaseUserActionInjectedAttributesWithoutActionId>>> {
|
||||
try {
|
||||
this.context.log.debug('Attempting to find status changes');
|
||||
|
||||
const updateActionFilter = buildFilter({
|
||||
filters: Actions.update,
|
||||
field: 'action',
|
||||
operator: 'or',
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
});
|
||||
|
||||
const statusChangeFilter = buildFilter({
|
||||
filters: ActionTypes.status,
|
||||
field: 'type',
|
||||
operator: 'or',
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
});
|
||||
|
||||
const combinedFilters = combineFilters([updateActionFilter, statusChangeFilter, filter]);
|
||||
|
||||
const finder =
|
||||
this.context.unsecuredSavedObjectsClient.createPointInTimeFinder<CaseUserActionAttributesWithoutConnectorId>(
|
||||
{
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
hasReference: { type: CASE_SAVED_OBJECT, id: caseId },
|
||||
sortField: defaultSortField,
|
||||
sortOrder: 'asc',
|
||||
filter: combinedFilters,
|
||||
perPage: MAX_DOCS_PER_PAGE,
|
||||
}
|
||||
);
|
||||
|
||||
let userActions: Array<SavedObject<CaseUserActionInjectedAttributesWithoutActionId>> = [];
|
||||
for await (const findResults of finder.find()) {
|
||||
userActions = userActions.concat(
|
||||
findResults.saved_objects.map((so) =>
|
||||
transformToExternalModel(so, this.context.persistableStateAttachmentTypeRegistry)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return userActions;
|
||||
} catch (error) {
|
||||
this.context.log.error(`Error finding status changes: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
286
x-pack/plugins/cases/server/services/user_actions/test_utils.ts
Normal file
286
x-pack/plugins/cases/server/services/user_actions/test_utils.ts
Normal file
|
@ -0,0 +1,286 @@
|
|||
/*
|
||||
* 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 { ACTION_SAVED_OBJECT_TYPE } from '@kbn/actions-plugin/server';
|
||||
import type {
|
||||
SavedObjectsFindResponse,
|
||||
SavedObjectsFindResult,
|
||||
} from '@kbn/core-saved-objects-api-server';
|
||||
import type { SavedObject, SavedObjectReference } from '@kbn/core-saved-objects-common';
|
||||
import { omit, get } from 'lodash';
|
||||
import {
|
||||
CASE_COMMENT_SAVED_OBJECT,
|
||||
CASE_SAVED_OBJECT,
|
||||
CASE_USER_ACTION_SAVED_OBJECT,
|
||||
SECURITY_SOLUTION_OWNER,
|
||||
} from '../../../common/constants';
|
||||
import type {
|
||||
CaseUserActionAttributes,
|
||||
ConnectorUserAction,
|
||||
UserAction,
|
||||
} from '../../../common/api';
|
||||
import { CaseSeverity, CaseStatuses, Actions } from '../../../common/api';
|
||||
import {
|
||||
CASE_REF_NAME,
|
||||
COMMENT_REF_NAME,
|
||||
CONNECTOR_ID_REFERENCE_NAME,
|
||||
EXTERNAL_REFERENCE_REF_NAME,
|
||||
PUSH_CONNECTOR_ID_REFERENCE_NAME,
|
||||
} from '../../common/constants';
|
||||
import {
|
||||
createConnectorObject,
|
||||
createExternalService,
|
||||
createJiraConnector,
|
||||
createSOFindResponse,
|
||||
} from '../test_utils';
|
||||
import {
|
||||
externalReferenceAttachmentSO,
|
||||
persistableStateAttachment,
|
||||
} from '../../attachment_framework/mocks';
|
||||
import type { PersistableStateAttachmentTypeRegistry } from '../../attachment_framework/persistable_state_registry';
|
||||
import { transformFindResponseToExternalModel } from './transform';
|
||||
|
||||
export const createUserActionFindSO = (
|
||||
userAction: SavedObject<CaseUserActionAttributes>
|
||||
): SavedObjectsFindResult<CaseUserActionAttributes> => ({
|
||||
...userAction,
|
||||
score: 0,
|
||||
});
|
||||
|
||||
export const createConnectorUserAction = (
|
||||
overrides?: Partial<CaseUserActionAttributes>
|
||||
): SavedObject<CaseUserActionAttributes> => {
|
||||
const { id, ...restConnector } = createConnectorObject().connector;
|
||||
return {
|
||||
...createUserActionSO({
|
||||
action: Actions.create,
|
||||
payload: { connector: restConnector },
|
||||
type: 'connector',
|
||||
connectorId: id,
|
||||
}),
|
||||
...(overrides && { ...overrides }),
|
||||
};
|
||||
};
|
||||
|
||||
export const createUserActionSO = ({
|
||||
action,
|
||||
attributesOverrides,
|
||||
commentId,
|
||||
connectorId,
|
||||
pushedConnectorId,
|
||||
payload,
|
||||
type,
|
||||
references = [],
|
||||
}: {
|
||||
action: UserAction;
|
||||
type?: string;
|
||||
payload?: Record<string, unknown>;
|
||||
attributesOverrides?: Partial<CaseUserActionAttributes>;
|
||||
commentId?: string;
|
||||
connectorId?: string;
|
||||
pushedConnectorId?: string;
|
||||
references?: SavedObjectReference[];
|
||||
}): SavedObject<CaseUserActionAttributes> => {
|
||||
const defaultParams = {
|
||||
action,
|
||||
created_at: 'abc',
|
||||
created_by: {
|
||||
email: 'a',
|
||||
username: 'b',
|
||||
full_name: 'abc',
|
||||
},
|
||||
type: type ?? 'title',
|
||||
payload: payload ?? { title: 'a new title' },
|
||||
owner: 'securitySolution',
|
||||
};
|
||||
|
||||
return {
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
id: '100',
|
||||
attributes: {
|
||||
...defaultParams,
|
||||
...(attributesOverrides && { ...attributesOverrides }),
|
||||
},
|
||||
references: [
|
||||
...references,
|
||||
{
|
||||
type: CASE_SAVED_OBJECT,
|
||||
name: CASE_REF_NAME,
|
||||
id: '1',
|
||||
},
|
||||
...(commentId
|
||||
? [
|
||||
{
|
||||
type: CASE_COMMENT_SAVED_OBJECT,
|
||||
name: COMMENT_REF_NAME,
|
||||
id: commentId,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(connectorId
|
||||
? [
|
||||
{
|
||||
type: ACTION_SAVED_OBJECT_TYPE,
|
||||
name: CONNECTOR_ID_REFERENCE_NAME,
|
||||
id: connectorId,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(pushedConnectorId
|
||||
? [
|
||||
{
|
||||
type: ACTION_SAVED_OBJECT_TYPE,
|
||||
name: PUSH_CONNECTOR_ID_REFERENCE_NAME,
|
||||
id: pushedConnectorId,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
} as SavedObject<CaseUserActionAttributes>;
|
||||
};
|
||||
|
||||
export const updateConnectorUserAction = ({
|
||||
overrides,
|
||||
}: {
|
||||
overrides?: Partial<CaseUserActionAttributes>;
|
||||
} = {}): SavedObject<CaseUserActionAttributes> => {
|
||||
const { id, ...restConnector } = createJiraConnector();
|
||||
return {
|
||||
...createUserActionSO({
|
||||
action: Actions.update,
|
||||
payload: { connector: restConnector },
|
||||
type: 'connector',
|
||||
connectorId: id,
|
||||
}),
|
||||
...(overrides && { ...overrides }),
|
||||
};
|
||||
};
|
||||
|
||||
export const pushConnectorUserAction = ({
|
||||
overrides,
|
||||
}: {
|
||||
overrides?: Partial<CaseUserActionAttributes>;
|
||||
} = {}): SavedObject<CaseUserActionAttributes> => {
|
||||
const { connector_id: connectorId, ...restExternalService } = createExternalService();
|
||||
return {
|
||||
...createUserActionSO({
|
||||
action: Actions.push_to_service,
|
||||
payload: { externalService: restExternalService },
|
||||
pushedConnectorId: connectorId,
|
||||
type: 'pushed',
|
||||
}),
|
||||
...(overrides && { ...overrides }),
|
||||
};
|
||||
};
|
||||
|
||||
export const createCaseUserAction = (): SavedObject<CaseUserActionAttributes> => {
|
||||
const { id, ...restConnector } = createJiraConnector();
|
||||
return {
|
||||
...createUserActionSO({
|
||||
action: Actions.create,
|
||||
payload: {
|
||||
connector: restConnector,
|
||||
title: 'a title',
|
||||
description: 'a desc',
|
||||
settings: { syncAlerts: false },
|
||||
status: CaseStatuses.open,
|
||||
severity: CaseSeverity.LOW,
|
||||
tags: [],
|
||||
owner: SECURITY_SOLUTION_OWNER,
|
||||
},
|
||||
connectorId: id,
|
||||
type: 'create_case',
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
export const createPersistableStateUserAction = () => {
|
||||
return {
|
||||
...createUserActionSO({
|
||||
action: Actions.create,
|
||||
commentId: 'persistable-state-test-id',
|
||||
payload: {
|
||||
comment: {
|
||||
...persistableStateAttachment,
|
||||
persistableStateAttachmentState: { foo: 'foo' },
|
||||
},
|
||||
},
|
||||
type: 'comment',
|
||||
references: [{ id: 'testRef', name: 'myTestReference', type: 'test-so' }],
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
export const createExternalReferenceUserAction = () => {
|
||||
return {
|
||||
...createUserActionSO({
|
||||
action: Actions.create,
|
||||
commentId: 'external-reference-test-id',
|
||||
payload: {
|
||||
comment: omit(externalReferenceAttachmentSO, 'externalReferenceId'),
|
||||
},
|
||||
type: 'comment',
|
||||
references: [{ id: 'my-id', name: EXTERNAL_REFERENCE_REF_NAME, type: 'test-so' }],
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
export const testConnectorId = (
|
||||
persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry,
|
||||
userAction: SavedObject<CaseUserActionAttributes>,
|
||||
path: string,
|
||||
expectedConnectorId = '1'
|
||||
) => {
|
||||
it('does set payload.connector.id to none when it cannot find the reference', () => {
|
||||
const userActionWithEmptyRef = { ...userAction, references: [] };
|
||||
const transformed = transformFindResponseToExternalModel(
|
||||
createSOFindResponse([createUserActionFindSO(userActionWithEmptyRef)]),
|
||||
persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
|
||||
expect(get(transformed.saved_objects[0].attributes.payload, path)).toBe('none');
|
||||
});
|
||||
|
||||
it('does not populate the payload.connector.id when the reference exists but the action is not of type connector', () => {
|
||||
const invalidUserAction = {
|
||||
...userAction,
|
||||
attributes: { ...userAction.attributes, type: 'not-connector' },
|
||||
};
|
||||
const transformed = transformFindResponseToExternalModel(
|
||||
createSOFindResponse([
|
||||
createUserActionFindSO(invalidUserAction as SavedObject<CaseUserActionAttributes>),
|
||||
]),
|
||||
persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
|
||||
expect(get(transformed.saved_objects[0].attributes.payload, path)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not populate the payload.connector.id when the reference exists but the payload does not contain a connector', () => {
|
||||
const invalidUserAction = {
|
||||
...userAction,
|
||||
attributes: { ...userAction.attributes, payload: {} },
|
||||
};
|
||||
const transformed = transformFindResponseToExternalModel(
|
||||
createSOFindResponse([
|
||||
createUserActionFindSO(invalidUserAction as SavedObject<CaseUserActionAttributes>),
|
||||
]),
|
||||
persistableStateAttachmentTypeRegistry
|
||||
) as SavedObjectsFindResponse<ConnectorUserAction>;
|
||||
|
||||
expect(get(transformed.saved_objects[0].attributes.payload, path)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('populates the payload.connector.id', () => {
|
||||
const transformed = transformFindResponseToExternalModel(
|
||||
createSOFindResponse([createUserActionFindSO(userAction)]),
|
||||
persistableStateAttachmentTypeRegistry
|
||||
) as SavedObjectsFindResponse<ConnectorUserAction>;
|
||||
|
||||
expect(get(transformed.saved_objects[0].attributes.payload, path)).toEqual(expectedConnectorId);
|
||||
});
|
||||
};
|
|
@ -0,0 +1,229 @@
|
|||
/*
|
||||
* 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 {
|
||||
legacyTransformFindResponseToExternalModel,
|
||||
transformFindResponseToExternalModel,
|
||||
} from './transform';
|
||||
import { createSOFindResponse } from '../test_utils';
|
||||
import {
|
||||
createUserActionFindSO,
|
||||
createConnectorUserAction,
|
||||
createUserActionSO,
|
||||
updateConnectorUserAction,
|
||||
pushConnectorUserAction,
|
||||
createCaseUserAction,
|
||||
createPersistableStateUserAction,
|
||||
createExternalReferenceUserAction,
|
||||
testConnectorId,
|
||||
} from './test_utils';
|
||||
import { createPersistableStateAttachmentTypeRegistryMock } from '../../attachment_framework/mocks';
|
||||
import type { SavedObjectsFindResponse } from '@kbn/core-saved-objects-api-server';
|
||||
import type { ConnectorUserAction } from '../../../common/api';
|
||||
import { Actions } from '../../../common/api';
|
||||
|
||||
describe('transform', () => {
|
||||
const persistableStateAttachmentTypeRegistry = createPersistableStateAttachmentTypeRegistryMock();
|
||||
|
||||
describe('action_id', () => {
|
||||
it('legacyTransformFindResponseToExternalModel sets action_id correctly to the saved object id', () => {
|
||||
const userAction = {
|
||||
...createUserActionSO({ action: Actions.create, commentId: '5' }),
|
||||
};
|
||||
|
||||
const transformed = legacyTransformFindResponseToExternalModel(
|
||||
createSOFindResponse([createUserActionFindSO(userAction)]),
|
||||
persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
|
||||
expect(transformed.saved_objects[0].attributes.action_id).toEqual('100');
|
||||
});
|
||||
|
||||
it('transformFindResponseToExternalModel does not set action_id', () => {
|
||||
const userAction = {
|
||||
...createUserActionSO({ action: Actions.create, commentId: '5' }),
|
||||
};
|
||||
|
||||
const transformed = transformFindResponseToExternalModel(
|
||||
createSOFindResponse([createUserActionFindSO(userAction)]),
|
||||
persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
|
||||
expect(transformed.saved_objects[0].attributes).not.toHaveProperty('action_id');
|
||||
});
|
||||
});
|
||||
|
||||
describe('case_id', () => {
|
||||
describe('legacyTransformFindResponseToExternalModel', () => {
|
||||
it('sets case_id correctly when it finds the reference', () => {
|
||||
const userAction = createConnectorUserAction();
|
||||
|
||||
const transformed = legacyTransformFindResponseToExternalModel(
|
||||
createSOFindResponse([createUserActionFindSO(userAction)]),
|
||||
persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
|
||||
expect(transformed.saved_objects[0].attributes.case_id).toEqual('1');
|
||||
});
|
||||
|
||||
it('sets case_id to an empty string when it cannot find the reference', () => {
|
||||
const userAction = {
|
||||
...createConnectorUserAction(),
|
||||
references: [],
|
||||
};
|
||||
const transformed = legacyTransformFindResponseToExternalModel(
|
||||
createSOFindResponse([createUserActionFindSO(userAction)]),
|
||||
persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
|
||||
expect(transformed.saved_objects[0].attributes.case_id).toEqual('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformFindResponseToExternalModel', () => {
|
||||
it('does not set the case_id when the reference exists', () => {
|
||||
const userAction = createConnectorUserAction();
|
||||
|
||||
const transformed = transformFindResponseToExternalModel(
|
||||
createSOFindResponse([createUserActionFindSO(userAction)]),
|
||||
persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
|
||||
expect(transformed.saved_objects[0].attributes).not.toHaveProperty('case_id');
|
||||
});
|
||||
|
||||
it('does not set the case_id when the reference does not exist', () => {
|
||||
const userAction = {
|
||||
...createConnectorUserAction(),
|
||||
references: [],
|
||||
};
|
||||
|
||||
const transformed = transformFindResponseToExternalModel(
|
||||
createSOFindResponse([createUserActionFindSO(userAction)]),
|
||||
persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
|
||||
expect(transformed.saved_objects[0].attributes).not.toHaveProperty('case_id');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe.each([
|
||||
[transformFindResponseToExternalModel.name, transformFindResponseToExternalModel],
|
||||
[legacyTransformFindResponseToExternalModel.name, legacyTransformFindResponseToExternalModel],
|
||||
])('%s', (functionName, transformer) => {
|
||||
it('does not populate the ids when the response is an empty array', () => {
|
||||
expect(transformer(createSOFindResponse([]), persistableStateAttachmentTypeRegistry))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"page": 1,
|
||||
"per_page": 0,
|
||||
"saved_objects": Array [],
|
||||
"total": 0,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('preserves the saved object fields and attributes when inject the ids', () => {
|
||||
const transformed = transformer(
|
||||
createSOFindResponse([createUserActionFindSO(createConnectorUserAction())]),
|
||||
persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
|
||||
expect(transformed).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('populates the payload.connector.id for multiple user actions', () => {
|
||||
const transformed = transformer(
|
||||
createSOFindResponse([
|
||||
createUserActionFindSO(createConnectorUserAction()),
|
||||
createUserActionFindSO(createConnectorUserAction()),
|
||||
]),
|
||||
persistableStateAttachmentTypeRegistry
|
||||
) as SavedObjectsFindResponse<ConnectorUserAction>;
|
||||
|
||||
expect(transformed.saved_objects[0].attributes.payload.connector.id).toEqual('1');
|
||||
expect(transformed.saved_objects[1].attributes.payload.connector.id).toEqual('1');
|
||||
});
|
||||
|
||||
describe('reference ids', () => {
|
||||
it('sets comment_id to null when it cannot find the reference', () => {
|
||||
const userAction = {
|
||||
...createUserActionSO({ action: Actions.create, commentId: '5' }),
|
||||
references: [],
|
||||
};
|
||||
const transformed = transformer(
|
||||
createSOFindResponse([createUserActionFindSO(userAction)]),
|
||||
persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
|
||||
expect(transformed.saved_objects[0].attributes.comment_id).toBeNull();
|
||||
});
|
||||
|
||||
it('sets comment_id correctly when it finds the reference', () => {
|
||||
const userAction = createUserActionSO({
|
||||
action: Actions.create,
|
||||
commentId: '5',
|
||||
});
|
||||
|
||||
const transformed = transformer(
|
||||
createSOFindResponse([createUserActionFindSO(userAction)]),
|
||||
persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
|
||||
expect(transformed.saved_objects[0].attributes.comment_id).toEqual('5');
|
||||
});
|
||||
});
|
||||
|
||||
describe('create connector', () => {
|
||||
const userAction = createConnectorUserAction();
|
||||
testConnectorId(persistableStateAttachmentTypeRegistry, userAction, 'connector.id');
|
||||
});
|
||||
|
||||
describe('update connector', () => {
|
||||
const userAction = updateConnectorUserAction();
|
||||
testConnectorId(persistableStateAttachmentTypeRegistry, userAction, 'connector.id');
|
||||
});
|
||||
|
||||
describe('push connector', () => {
|
||||
const userAction = pushConnectorUserAction();
|
||||
testConnectorId(
|
||||
persistableStateAttachmentTypeRegistry,
|
||||
userAction,
|
||||
'externalService.connector_id',
|
||||
'100'
|
||||
);
|
||||
});
|
||||
|
||||
describe('create case', () => {
|
||||
const userAction = createCaseUserAction();
|
||||
testConnectorId(persistableStateAttachmentTypeRegistry, userAction, 'connector.id');
|
||||
});
|
||||
|
||||
describe('persistable state attachments', () => {
|
||||
it('populates the persistable state', () => {
|
||||
const transformed = transformer(
|
||||
createSOFindResponse([createUserActionFindSO(createPersistableStateUserAction())]),
|
||||
persistableStateAttachmentTypeRegistry
|
||||
) as SavedObjectsFindResponse<ConnectorUserAction>;
|
||||
|
||||
expect(transformed).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('external references', () => {
|
||||
it('populates the external references attributes', () => {
|
||||
const transformed = transformer(
|
||||
createSOFindResponse([createUserActionFindSO(createExternalReferenceUserAction())]),
|
||||
persistableStateAttachmentTypeRegistry
|
||||
) as SavedObjectsFindResponse<ConnectorUserAction>;
|
||||
|
||||
expect(transformed).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
200
x-pack/plugins/cases/server/services/user_actions/transform.ts
Normal file
200
x-pack/plugins/cases/server/services/user_actions/transform.ts
Normal file
|
@ -0,0 +1,200 @@
|
|||
/*
|
||||
* 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 { SavedObject, SavedObjectReference, SavedObjectsFindResponse } from '@kbn/core/server';
|
||||
|
||||
import { isCommentRequestTypePersistableState } from '../../../common/utils/attachments';
|
||||
import {
|
||||
isConnectorUserAction,
|
||||
isPushedUserAction,
|
||||
isCreateCaseUserAction,
|
||||
isCommentUserAction,
|
||||
} from '../../../common/utils/user_actions';
|
||||
import type {
|
||||
CaseUserActionAttributes,
|
||||
CaseUserActionAttributesWithoutConnectorId,
|
||||
CaseUserActionInjectedAttributesWithoutActionId,
|
||||
CaseUserActionResponse,
|
||||
} from '../../../common/api';
|
||||
import { NONE_CONNECTOR_ID } from '../../../common/api';
|
||||
import { CASE_SAVED_OBJECT, CASE_COMMENT_SAVED_OBJECT } from '../../../common/constants';
|
||||
import {
|
||||
CASE_REF_NAME,
|
||||
COMMENT_REF_NAME,
|
||||
CONNECTOR_ID_REFERENCE_NAME,
|
||||
EXTERNAL_REFERENCE_REF_NAME,
|
||||
PUSH_CONNECTOR_ID_REFERENCE_NAME,
|
||||
} from '../../common/constants';
|
||||
import { findConnectorIdReference } from '../transform';
|
||||
import { isCommentRequestTypeExternalReferenceSO } from '../../common/utils';
|
||||
import type { PersistableStateAttachmentTypeRegistry } from '../../attachment_framework/persistable_state_registry';
|
||||
import { injectPersistableReferencesToSO } from '../../attachment_framework/so_references';
|
||||
|
||||
export function transformFindResponseToExternalModel(
|
||||
userActions: SavedObjectsFindResponse<CaseUserActionAttributesWithoutConnectorId>,
|
||||
persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry
|
||||
): SavedObjectsFindResponse<CaseUserActionInjectedAttributesWithoutActionId> {
|
||||
return {
|
||||
...userActions,
|
||||
saved_objects: userActions.saved_objects.map((so) => ({
|
||||
...so,
|
||||
...transformToExternalModel(so, persistableStateAttachmentTypeRegistry),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function transformToExternalModel(
|
||||
userAction: SavedObject<CaseUserActionAttributesWithoutConnectorId>,
|
||||
persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry
|
||||
): SavedObject<CaseUserActionInjectedAttributesWithoutActionId> {
|
||||
const { references } = userAction;
|
||||
|
||||
const commentId =
|
||||
findReferenceId(COMMENT_REF_NAME, CASE_COMMENT_SAVED_OBJECT, references) ?? null;
|
||||
const payload = addReferenceIdToPayload(userAction, persistableStateAttachmentTypeRegistry);
|
||||
|
||||
return {
|
||||
...userAction,
|
||||
attributes: {
|
||||
...userAction.attributes,
|
||||
comment_id: commentId,
|
||||
payload,
|
||||
} as CaseUserActionInjectedAttributesWithoutActionId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* This function should only be used in the getAll user actions and it is deprecated. It should be removed when the
|
||||
* getAll route is removed.
|
||||
*
|
||||
* @deprecated remove when the getAllRoute is removed
|
||||
*/
|
||||
export function legacyTransformFindResponseToExternalModel(
|
||||
userActions: SavedObjectsFindResponse<CaseUserActionAttributesWithoutConnectorId>,
|
||||
persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry
|
||||
): SavedObjectsFindResponse<CaseUserActionResponse> {
|
||||
return {
|
||||
...userActions,
|
||||
saved_objects: userActions.saved_objects.map((so) => ({
|
||||
...so,
|
||||
...legacyTransformToExternalModel(so, persistableStateAttachmentTypeRegistry),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated remove when the getAll route is removed
|
||||
*/
|
||||
function legacyTransformToExternalModel(
|
||||
userAction: SavedObject<CaseUserActionAttributesWithoutConnectorId>,
|
||||
persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry
|
||||
): SavedObject<CaseUserActionResponse> {
|
||||
const { references } = userAction;
|
||||
|
||||
const caseId = findReferenceId(CASE_REF_NAME, CASE_SAVED_OBJECT, references) ?? '';
|
||||
const commentId =
|
||||
findReferenceId(COMMENT_REF_NAME, CASE_COMMENT_SAVED_OBJECT, references) ?? null;
|
||||
const payload = addReferenceIdToPayload(userAction, persistableStateAttachmentTypeRegistry);
|
||||
|
||||
return {
|
||||
...userAction,
|
||||
attributes: {
|
||||
...userAction.attributes,
|
||||
action_id: userAction.id,
|
||||
case_id: caseId,
|
||||
comment_id: commentId,
|
||||
payload,
|
||||
} as CaseUserActionResponse,
|
||||
};
|
||||
}
|
||||
|
||||
const addReferenceIdToPayload = (
|
||||
userAction: SavedObject<CaseUserActionAttributes>,
|
||||
persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry
|
||||
): CaseUserActionAttributes['payload'] => {
|
||||
const connectorId = getConnectorIdFromReferences(userAction);
|
||||
const userActionAttributes = userAction.attributes;
|
||||
|
||||
if (isConnectorUserAction(userActionAttributes) || isCreateCaseUserAction(userActionAttributes)) {
|
||||
return {
|
||||
...userActionAttributes.payload,
|
||||
connector: {
|
||||
...userActionAttributes.payload.connector,
|
||||
id: connectorId ?? NONE_CONNECTOR_ID,
|
||||
},
|
||||
};
|
||||
} else if (isPushedUserAction(userActionAttributes)) {
|
||||
return {
|
||||
...userAction.attributes.payload,
|
||||
externalService: {
|
||||
...userActionAttributes.payload.externalService,
|
||||
connector_id: connectorId ?? NONE_CONNECTOR_ID,
|
||||
},
|
||||
};
|
||||
} else if (isCommentUserAction(userActionAttributes)) {
|
||||
if (isCommentRequestTypeExternalReferenceSO(userActionAttributes.payload.comment)) {
|
||||
const externalReferenceId = findReferenceId(
|
||||
EXTERNAL_REFERENCE_REF_NAME,
|
||||
userActionAttributes.payload.comment.externalReferenceStorage.soType,
|
||||
userAction.references
|
||||
);
|
||||
|
||||
return {
|
||||
...userAction.attributes.payload,
|
||||
comment: {
|
||||
...userActionAttributes.payload.comment,
|
||||
externalReferenceId: externalReferenceId ?? '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (isCommentRequestTypePersistableState(userActionAttributes.payload.comment)) {
|
||||
const injectedAttributes = injectPersistableReferencesToSO(
|
||||
userActionAttributes.payload.comment,
|
||||
userAction.references,
|
||||
{
|
||||
persistableStateAttachmentTypeRegistry,
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
...userAction.attributes.payload,
|
||||
comment: {
|
||||
...userActionAttributes.payload.comment,
|
||||
...injectedAttributes,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return userAction.attributes.payload;
|
||||
};
|
||||
|
||||
function getConnectorIdFromReferences(
|
||||
userAction: SavedObject<CaseUserActionAttributes>
|
||||
): string | null {
|
||||
const { references } = userAction;
|
||||
|
||||
if (
|
||||
isConnectorUserAction(userAction.attributes) ||
|
||||
isCreateCaseUserAction(userAction.attributes)
|
||||
) {
|
||||
return findConnectorIdReference(CONNECTOR_ID_REFERENCE_NAME, references)?.id ?? null;
|
||||
} else if (isPushedUserAction(userAction.attributes)) {
|
||||
return findConnectorIdReference(PUSH_CONNECTOR_ID_REFERENCE_NAME, references)?.id ?? null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function findReferenceId(
|
||||
name: string,
|
||||
type: string,
|
||||
references: SavedObjectReference[]
|
||||
): string | undefined {
|
||||
return references.find((ref) => ref.name === name && ref.type === type)?.id;
|
||||
}
|
|
@ -19,7 +19,7 @@ import type {
|
|||
CaseSettings,
|
||||
CaseSeverity,
|
||||
CaseStatuses,
|
||||
CaseUserActionResponse,
|
||||
CaseUserActionInjectedAttributesWithoutActionId,
|
||||
CommentUserAction,
|
||||
ConnectorUserAction,
|
||||
PushedUserAction,
|
||||
|
@ -145,11 +145,14 @@ export interface ServiceContext {
|
|||
|
||||
export interface CaseConnectorActivity {
|
||||
connectorId: string;
|
||||
fields: SavedObject<CaseUserActionResponse>;
|
||||
push?: SavedObject<CaseUserActionResponse>;
|
||||
fields: SavedObject<CaseUserActionInjectedAttributesWithoutActionId>;
|
||||
push?: SavedObject<CaseUserActionInjectedAttributesWithoutActionId>;
|
||||
}
|
||||
|
||||
export type CaseConnectorFields = Map<string, SavedObject<CaseUserActionResponse>>;
|
||||
export type CaseConnectorFields = Map<
|
||||
string,
|
||||
SavedObject<CaseUserActionInjectedAttributesWithoutActionId>
|
||||
>;
|
||||
|
||||
export interface PushInfo {
|
||||
date: Date;
|
||||
|
|
|
@ -52,6 +52,7 @@
|
|||
"@kbn/safer-lodash-set",
|
||||
"@kbn/logging-mocks",
|
||||
"@kbn/ecs",
|
||||
"@kbn/core-saved-objects-api-server",
|
||||
"@kbn/core-saved-objects-base-server-mocks",
|
||||
],
|
||||
"exclude": [
|
||||
|
|
70
x-pack/test/cases_api_integration/common/lib/user_actions.ts
Normal file
70
x-pack/test/cases_api_integration/common/lib/user_actions.ts
Normal file
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 {
|
||||
UserActionFindResponse,
|
||||
getCaseFindUserActionsUrl,
|
||||
UserActionFindRequest,
|
||||
CaseUserActionsResponse,
|
||||
getCaseUserActionUrl,
|
||||
CaseUserActionResponse,
|
||||
} from '@kbn/cases-plugin/common/api';
|
||||
import type SuperTest from 'supertest';
|
||||
import { User } from './authentication/types';
|
||||
|
||||
import { superUser } from './authentication/users';
|
||||
import { getSpaceUrlPrefix, removeServerGeneratedPropertiesFromObject } from './utils';
|
||||
|
||||
export const removeServerGeneratedPropertiesFromUserAction = (
|
||||
attributes: CaseUserActionResponse
|
||||
) => {
|
||||
const keysToRemove: Array<keyof CaseUserActionResponse> = ['action_id', 'created_at'];
|
||||
return removeServerGeneratedPropertiesFromObject<
|
||||
CaseUserActionResponse,
|
||||
typeof keysToRemove[number]
|
||||
>(attributes, keysToRemove);
|
||||
};
|
||||
|
||||
export const getCaseUserActions = async ({
|
||||
supertest,
|
||||
caseID,
|
||||
expectedHttpCode = 200,
|
||||
auth = { user: superUser, space: null },
|
||||
}: {
|
||||
supertest: SuperTest.SuperTest<SuperTest.Test>;
|
||||
caseID: string;
|
||||
expectedHttpCode?: number;
|
||||
auth?: { user: User; space: string | null };
|
||||
}): Promise<CaseUserActionsResponse> => {
|
||||
const { body: userActions } = await supertest
|
||||
.get(`${getSpaceUrlPrefix(auth.space)}${getCaseUserActionUrl(caseID)}`)
|
||||
.auth(auth.user.username, auth.user.password)
|
||||
.expect(expectedHttpCode);
|
||||
return userActions;
|
||||
};
|
||||
|
||||
export const findCaseUserActions = async ({
|
||||
supertest,
|
||||
caseID,
|
||||
options = {},
|
||||
expectedHttpCode = 200,
|
||||
auth = { user: superUser, space: null },
|
||||
}: {
|
||||
supertest: SuperTest.SuperTest<SuperTest.Test>;
|
||||
caseID: string;
|
||||
options?: UserActionFindRequest;
|
||||
expectedHttpCode?: number;
|
||||
auth?: { user: User; space: string | null };
|
||||
}): Promise<UserActionFindResponse> => {
|
||||
const { body: userActions } = await supertest
|
||||
.get(`${getSpaceUrlPrefix(auth.space)}${getCaseFindUserActionsUrl(caseID)}`)
|
||||
.query(options)
|
||||
.auth(auth.user.username, auth.user.password)
|
||||
.expect(expectedHttpCode);
|
||||
|
||||
return userActions;
|
||||
};
|
|
@ -33,7 +33,6 @@ import {
|
|||
CasesResponse,
|
||||
CasesFindResponse,
|
||||
CommentRequest,
|
||||
CaseUserActionResponse,
|
||||
CommentResponse,
|
||||
CasesPatchRequest,
|
||||
AllCommentsResponse,
|
||||
|
@ -41,7 +40,6 @@ import {
|
|||
CasesConfigurePatch,
|
||||
CasesStatusResponse,
|
||||
CasesConfigurationsResponse,
|
||||
CaseUserActionsResponse,
|
||||
AlertResponse,
|
||||
ConnectorMappings,
|
||||
CasesByAlertId,
|
||||
|
@ -52,7 +50,6 @@ import {
|
|||
CasesMetricsResponse,
|
||||
CasesBulkGetResponse,
|
||||
} from '@kbn/cases-plugin/common/api';
|
||||
import { getCaseUserActionUrl } from '@kbn/cases-plugin/common/api/helpers';
|
||||
import { SignalHit } from '@kbn/security-solution-plugin/server/lib/detection_engine/signals/types';
|
||||
import { ActionResult } from '@kbn/actions-plugin/server/types';
|
||||
import { ESCasesConfigureAttributes } from '@kbn/cases-plugin/server/services/configure/types';
|
||||
|
@ -231,7 +228,7 @@ interface CommonSavedObjectAttributes {
|
|||
|
||||
const savedObjectCommonAttributes = ['created_at', 'updated_at', 'version', 'id'];
|
||||
|
||||
const removeServerGeneratedPropertiesFromObject = <T extends object, K extends keyof T>(
|
||||
export const removeServerGeneratedPropertiesFromObject = <T extends object, K extends keyof T>(
|
||||
object: T,
|
||||
keys: K[]
|
||||
): Omit<T, K> => {
|
||||
|
@ -249,16 +246,6 @@ export const removeServerGeneratedPropertiesFromSavedObject = <
|
|||
]);
|
||||
};
|
||||
|
||||
export const removeServerGeneratedPropertiesFromUserAction = (
|
||||
attributes: CaseUserActionResponse
|
||||
) => {
|
||||
const keysToRemove: Array<keyof CaseUserActionResponse> = ['action_id', 'created_at'];
|
||||
return removeServerGeneratedPropertiesFromObject<
|
||||
CaseUserActionResponse,
|
||||
typeof keysToRemove[number]
|
||||
>(attributes, keysToRemove);
|
||||
};
|
||||
|
||||
export const removeServerGeneratedPropertiesFromCase = (
|
||||
theCase: CaseResponse
|
||||
): Partial<CaseResponse> => {
|
||||
|
@ -602,24 +589,6 @@ export const updateCase = async ({
|
|||
return cases;
|
||||
};
|
||||
|
||||
export const getCaseUserActions = async ({
|
||||
supertest,
|
||||
caseID,
|
||||
expectedHttpCode = 200,
|
||||
auth = { user: superUser, space: null },
|
||||
}: {
|
||||
supertest: SuperTest.SuperTest<SuperTest.Test>;
|
||||
caseID: string;
|
||||
expectedHttpCode?: number;
|
||||
auth?: { user: User; space: string | null };
|
||||
}): Promise<CaseUserActionsResponse> => {
|
||||
const { body: userActions } = await supertest
|
||||
.get(`${getSpaceUrlPrefix(auth.space)}${getCaseUserActionUrl(caseID)}`)
|
||||
.auth(auth.user.username, auth.user.password)
|
||||
.expect(expectedHttpCode);
|
||||
return userActions;
|
||||
};
|
||||
|
||||
export const deleteComment = async ({
|
||||
supertest,
|
||||
caseId,
|
||||
|
|
|
@ -25,13 +25,13 @@ import {
|
|||
deleteAllCaseItems,
|
||||
createCase,
|
||||
createComment,
|
||||
getCaseUserActions,
|
||||
removeServerGeneratedPropertiesFromSavedObject,
|
||||
bulkCreateAttachments,
|
||||
updateComment,
|
||||
getSOFromKibanaIndex,
|
||||
getReferenceFromEsResponse,
|
||||
} from '../../../../common/lib/utils';
|
||||
import { getCaseUserActions } from '../../../../common/lib/user_actions';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext): void => {
|
||||
|
|
|
@ -24,7 +24,6 @@ import {
|
|||
deleteAllCaseItems,
|
||||
createCase,
|
||||
createComment,
|
||||
getCaseUserActions,
|
||||
removeServerGeneratedPropertiesFromSavedObject,
|
||||
getComment,
|
||||
getSOFromKibanaIndex,
|
||||
|
@ -32,6 +31,7 @@ import {
|
|||
bulkCreateAttachments,
|
||||
updateComment,
|
||||
} from '../../../../common/lib/utils';
|
||||
import { getCaseUserActions } from '../../../../common/lib/user_actions';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext): void => {
|
||||
|
|
|
@ -19,8 +19,8 @@ import {
|
|||
getComment,
|
||||
getCase,
|
||||
superUserSpace1Auth,
|
||||
getCaseUserActions,
|
||||
} from '../../../../common/lib/utils';
|
||||
import { getCaseUserActions } from '../../../../common/lib/user_actions';
|
||||
import {
|
||||
secOnly,
|
||||
secOnlyRead,
|
||||
|
|
|
@ -36,8 +36,8 @@ import {
|
|||
createCase,
|
||||
createComment,
|
||||
findCases,
|
||||
getCaseUserActions,
|
||||
} from '../../../../common/lib/utils';
|
||||
import { getCaseUserActions } from '../../../../common/lib/user_actions';
|
||||
import { getPostCaseRequest, postCommentUserReq } from '../../../../common/lib/mock';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
|
||||
|
|
|
@ -30,14 +30,16 @@ import {
|
|||
createCase,
|
||||
createComment,
|
||||
updateCase,
|
||||
getCaseUserActions,
|
||||
removeServerGeneratedPropertiesFromCase,
|
||||
removeServerGeneratedPropertiesFromUserAction,
|
||||
findCases,
|
||||
superUserSpace1Auth,
|
||||
delay,
|
||||
calculateDuration,
|
||||
} from '../../../../common/lib/utils';
|
||||
import {
|
||||
getCaseUserActions,
|
||||
removeServerGeneratedPropertiesFromUserAction,
|
||||
} from '../../../../common/lib/user_actions';
|
||||
import {
|
||||
createSignalsIndex,
|
||||
deleteSignalsIndex,
|
||||
|
|
|
@ -19,9 +19,11 @@ import {
|
|||
deleteCasesByESQuery,
|
||||
createCase,
|
||||
removeServerGeneratedPropertiesFromCase,
|
||||
removeServerGeneratedPropertiesFromUserAction,
|
||||
getCaseUserActions,
|
||||
} from '../../../../common/lib/utils';
|
||||
import {
|
||||
getCaseUserActions,
|
||||
removeServerGeneratedPropertiesFromUserAction,
|
||||
} from '../../../../common/lib/user_actions';
|
||||
import {
|
||||
secOnly,
|
||||
secOnlyRead,
|
||||
|
|
|
@ -31,12 +31,14 @@ import {
|
|||
deleteComments,
|
||||
createCase,
|
||||
createComment,
|
||||
getCaseUserActions,
|
||||
removeServerGeneratedPropertiesFromUserAction,
|
||||
removeServerGeneratedPropertiesFromSavedObject,
|
||||
superUserSpace1Auth,
|
||||
updateCase,
|
||||
} from '../../../../common/lib/utils';
|
||||
import {
|
||||
getCaseUserActions,
|
||||
removeServerGeneratedPropertiesFromUserAction,
|
||||
} from '../../../../common/lib/user_actions';
|
||||
import {
|
||||
createSignalsIndex,
|
||||
deleteSignalsIndex,
|
||||
|
|
|
@ -30,6 +30,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => {
|
|||
loadTestFile(require.resolve('./cases/status/get_status'));
|
||||
loadTestFile(require.resolve('./cases/tags/get_tags'));
|
||||
loadTestFile(require.resolve('./user_actions/get_all_user_actions'));
|
||||
loadTestFile(require.resolve('./user_actions/find_user_actions'));
|
||||
loadTestFile(require.resolve('./configure/get_configure'));
|
||||
loadTestFile(require.resolve('./configure/patch_configure'));
|
||||
loadTestFile(require.resolve('./configure/post_configure'));
|
||||
|
|
|
@ -28,14 +28,16 @@ import {
|
|||
import {
|
||||
deleteAllCaseItems,
|
||||
createCase,
|
||||
getCaseUserActions,
|
||||
removeServerGeneratedPropertiesFromUserAction,
|
||||
removeServerGeneratedPropertiesFromSavedObject,
|
||||
superUserSpace1Auth,
|
||||
createCaseAndBulkCreateAttachments,
|
||||
bulkCreateAttachments,
|
||||
updateCase,
|
||||
} from '../../../../common/lib/utils';
|
||||
import {
|
||||
getCaseUserActions,
|
||||
removeServerGeneratedPropertiesFromUserAction,
|
||||
} from '../../../../common/lib/user_actions';
|
||||
import {
|
||||
createSignalsIndex,
|
||||
deleteSignalsIndex,
|
||||
|
|
|
@ -0,0 +1,920 @@
|
|||
/*
|
||||
* 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 {
|
||||
ActionTypes,
|
||||
CaseResponse,
|
||||
CaseSeverity,
|
||||
CaseStatuses,
|
||||
CommentUserAction,
|
||||
ConnectorTypes,
|
||||
FindTypes,
|
||||
} from '@kbn/cases-plugin/common/api';
|
||||
import {
|
||||
globalRead,
|
||||
noKibanaPrivileges,
|
||||
obsOnly,
|
||||
obsOnlyRead,
|
||||
secOnly,
|
||||
secOnlyRead,
|
||||
superUser,
|
||||
} from '../../../../common/lib/authentication/users';
|
||||
import { findCaseUserActions, getCaseUserActions } from '../../../../common/lib/user_actions';
|
||||
import {
|
||||
getPostCaseRequest,
|
||||
persistableStateAttachment,
|
||||
postCommentActionsReq,
|
||||
postCommentAlertReq,
|
||||
postCommentUserReq,
|
||||
postExternalReferenceESReq,
|
||||
} from '../../../../common/lib/mock';
|
||||
import {
|
||||
deleteAllCaseItems,
|
||||
createCase,
|
||||
updateCase,
|
||||
createComment,
|
||||
bulkCreateAttachments,
|
||||
} from '../../../../common/lib/utils';
|
||||
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext): void => {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('es');
|
||||
|
||||
describe('find_user_actions', () => {
|
||||
afterEach(async () => {
|
||||
await deleteAllCaseItems(es);
|
||||
});
|
||||
|
||||
it('returns the id and version fields but not action_id', async () => {
|
||||
const theCase = await createCase(supertest, getPostCaseRequest());
|
||||
|
||||
const allUserActions = await getCaseUserActions({ supertest, caseID: theCase.id });
|
||||
|
||||
const response = await findCaseUserActions({
|
||||
caseID: theCase.id,
|
||||
supertest,
|
||||
});
|
||||
|
||||
expect(response.userActions.length).to.be(1);
|
||||
expect(response.userActions[0].id).not.to.be(undefined);
|
||||
expect(response.userActions[0].id).to.eql(allUserActions[0].action_id);
|
||||
expect(response.userActions[0].version).not.to.be(undefined);
|
||||
expect(response.userActions[0]).not.to.have.property('action_id');
|
||||
expect(response.userActions[0]).not.to.have.property('case_id');
|
||||
});
|
||||
|
||||
describe('default parameters', () => {
|
||||
it('performs a search using the default parameters when no query params are sent', async () => {
|
||||
const theCase = await createCase(supertest, getPostCaseRequest());
|
||||
|
||||
await createComment({
|
||||
supertest,
|
||||
caseId: theCase.id,
|
||||
params: postCommentUserReq,
|
||||
});
|
||||
|
||||
const response = await findCaseUserActions({
|
||||
caseID: theCase.id,
|
||||
supertest,
|
||||
});
|
||||
|
||||
expect(response.userActions.length).to.be(2);
|
||||
|
||||
const createCaseUserAction = response.userActions[0];
|
||||
expect(createCaseUserAction.type).to.eql('create_case');
|
||||
expect(createCaseUserAction.action).to.eql('create');
|
||||
|
||||
const commentUserAction = response.userActions[1];
|
||||
expect(commentUserAction.type).to.eql('comment');
|
||||
expect(commentUserAction.action).to.eql('create');
|
||||
|
||||
expect(response.page).to.be(1);
|
||||
expect(response.perPage).to.be(20);
|
||||
expect(response.total).to.be(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sorting', () => {
|
||||
it('sorts the results in descending order by the created_at field', async () => {
|
||||
const theCase = await createCase(supertest, getPostCaseRequest());
|
||||
|
||||
await createComment({
|
||||
supertest,
|
||||
caseId: theCase.id,
|
||||
params: postCommentUserReq,
|
||||
});
|
||||
|
||||
const response = await findCaseUserActions({
|
||||
caseID: theCase.id,
|
||||
supertest,
|
||||
options: {
|
||||
sortOrder: 'desc',
|
||||
types: [ActionTypes.comment, ActionTypes.create_case],
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.userActions.length).to.be(2);
|
||||
|
||||
const commentUserAction = response.userActions[0];
|
||||
expect(commentUserAction.type).to.eql('comment');
|
||||
expect(commentUserAction.action).to.eql('create');
|
||||
});
|
||||
});
|
||||
|
||||
describe('pagination', () => {
|
||||
let theCase: CaseResponse;
|
||||
|
||||
beforeEach(async () => {
|
||||
theCase = await createCase(supertest, getPostCaseRequest());
|
||||
|
||||
await bulkCreateAttachments({
|
||||
supertest,
|
||||
caseId: theCase.id,
|
||||
params: [postCommentUserReq, postCommentUserReq],
|
||||
});
|
||||
});
|
||||
|
||||
it('retrieves only 1 user action when perPage is 1', async () => {
|
||||
const response = await findCaseUserActions({
|
||||
caseID: theCase.id,
|
||||
supertest,
|
||||
options: {
|
||||
sortOrder: 'asc',
|
||||
types: [ActionTypes.comment, ActionTypes.create_case],
|
||||
perPage: 1,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.userActions.length).to.be(1);
|
||||
|
||||
const commentUserAction = response.userActions[0];
|
||||
expect(commentUserAction.type).to.eql('create_case');
|
||||
expect(commentUserAction.action).to.eql('create');
|
||||
});
|
||||
|
||||
it('retrieves 2 user action when perPage is 2 and there are 3 user actions', async () => {
|
||||
const response = await findCaseUserActions({
|
||||
caseID: theCase.id,
|
||||
supertest,
|
||||
options: {
|
||||
sortOrder: 'asc',
|
||||
types: [ActionTypes.comment, ActionTypes.create_case],
|
||||
perPage: 2,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.userActions.length).to.be(2);
|
||||
expect(response.total).to.be(3);
|
||||
|
||||
const createCaseUserAction = response.userActions[0];
|
||||
expect(createCaseUserAction.type).to.eql('create_case');
|
||||
expect(createCaseUserAction.action).to.eql('create');
|
||||
|
||||
const commentUserAction = response.userActions[1];
|
||||
expect(commentUserAction.type).to.eql('comment');
|
||||
expect(commentUserAction.action).to.eql('create');
|
||||
});
|
||||
|
||||
it('retrieves the second page of results', async () => {
|
||||
const response = await findCaseUserActions({
|
||||
caseID: theCase.id,
|
||||
supertest,
|
||||
options: {
|
||||
sortOrder: 'asc',
|
||||
types: [ActionTypes.comment, ActionTypes.create_case],
|
||||
page: 2,
|
||||
perPage: 1,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.userActions.length).to.be(1);
|
||||
expect(response.total).to.be(3);
|
||||
expect(response.userActions[0].type).to.eql('comment');
|
||||
expect(response.userActions[0].action).to.eql('create');
|
||||
});
|
||||
|
||||
it('retrieves the third page of results', async () => {
|
||||
const response = await findCaseUserActions({
|
||||
caseID: theCase.id,
|
||||
supertest,
|
||||
options: {
|
||||
sortOrder: 'asc',
|
||||
types: [ActionTypes.comment, ActionTypes.create_case],
|
||||
page: 3,
|
||||
perPage: 1,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.userActions.length).to.be(1);
|
||||
expect(response.total).to.be(3);
|
||||
expect(response.userActions[0].type).to.eql('comment');
|
||||
expect(response.userActions[0].action).to.eql('create');
|
||||
});
|
||||
|
||||
it('retrieves all the results with a perPage larger than the total', async () => {
|
||||
const response = await findCaseUserActions({
|
||||
caseID: theCase.id,
|
||||
supertest,
|
||||
options: {
|
||||
sortOrder: 'asc',
|
||||
types: [ActionTypes.comment, ActionTypes.create_case],
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.userActions.length).to.be(3);
|
||||
expect(response.total).to.be(3);
|
||||
expect(response.userActions[0].type).to.eql('create_case');
|
||||
expect(response.userActions[0].action).to.eql('create');
|
||||
});
|
||||
});
|
||||
|
||||
describe('filters using the type query parameter', () => {
|
||||
it('returns a 400 when filtering for an invalid type', async () => {
|
||||
await findCaseUserActions({
|
||||
caseID: '123',
|
||||
supertest,
|
||||
options: {
|
||||
sortOrder: 'asc',
|
||||
// @ts-expect-error using an invalid filter type
|
||||
types: ['invalid-type'],
|
||||
},
|
||||
expectedHttpCode: 400,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an empty array when the user action type does not exist', async () => {
|
||||
const theCase = await createCase(supertest, getPostCaseRequest());
|
||||
|
||||
const response = await findCaseUserActions({
|
||||
caseID: theCase.id,
|
||||
supertest,
|
||||
options: {
|
||||
sortOrder: 'asc',
|
||||
types: [ActionTypes.comment],
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.userActions.length).to.be(0);
|
||||
});
|
||||
|
||||
it('retrieves only the comment user actions', async () => {
|
||||
const theCase = await createCase(supertest, getPostCaseRequest());
|
||||
|
||||
await createComment({
|
||||
supertest,
|
||||
caseId: theCase.id,
|
||||
params: postCommentUserReq,
|
||||
});
|
||||
|
||||
const response = await findCaseUserActions({
|
||||
caseID: theCase.id,
|
||||
supertest,
|
||||
options: {
|
||||
sortOrder: 'asc',
|
||||
types: [ActionTypes.comment],
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.userActions.length).to.be(1);
|
||||
|
||||
const commentUserAction = response.userActions[0];
|
||||
expect(commentUserAction.type).to.eql('comment');
|
||||
expect(commentUserAction.action).to.eql('create');
|
||||
expect(commentUserAction.payload).to.eql({
|
||||
comment: postCommentUserReq,
|
||||
});
|
||||
});
|
||||
|
||||
it('retrieves only the connector user actions', async () => {
|
||||
const theCase = await createCase(supertest, getPostCaseRequest());
|
||||
|
||||
await updateCase({
|
||||
params: {
|
||||
cases: [
|
||||
{
|
||||
id: theCase.id,
|
||||
version: theCase.version,
|
||||
connector: {
|
||||
id: 'my-jira',
|
||||
name: 'jira',
|
||||
type: ConnectorTypes.jira,
|
||||
fields: {
|
||||
issueType: 'task',
|
||||
parent: null,
|
||||
priority: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
supertest,
|
||||
});
|
||||
|
||||
const response = await findCaseUserActions({
|
||||
caseID: theCase.id,
|
||||
supertest,
|
||||
options: {
|
||||
sortOrder: 'asc',
|
||||
types: [ActionTypes.connector],
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.userActions.length).to.be(1);
|
||||
|
||||
const updateConnectorUserAction = response.userActions[0];
|
||||
expect(updateConnectorUserAction.type).to.eql('connector');
|
||||
expect(updateConnectorUserAction.action).to.eql('update');
|
||||
expect(updateConnectorUserAction.payload).to.eql({
|
||||
connector: {
|
||||
id: 'my-jira',
|
||||
name: 'jira',
|
||||
type: ConnectorTypes.jira,
|
||||
fields: {
|
||||
issueType: 'task',
|
||||
parent: null,
|
||||
priority: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('retrieves only the description user actions', async () => {
|
||||
const newDesc = 'Such a great description';
|
||||
const theCase = await createCase(supertest, getPostCaseRequest());
|
||||
|
||||
await updateCase({
|
||||
supertest,
|
||||
params: {
|
||||
cases: [
|
||||
{
|
||||
id: theCase.id,
|
||||
version: theCase.version,
|
||||
description: newDesc,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const response = await findCaseUserActions({
|
||||
caseID: theCase.id,
|
||||
supertest,
|
||||
options: {
|
||||
sortOrder: 'asc',
|
||||
types: [ActionTypes.description],
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.userActions.length).to.be(1);
|
||||
|
||||
const descriptionUserAction = response.userActions[0];
|
||||
|
||||
expect(descriptionUserAction.type).to.eql('description');
|
||||
expect(descriptionUserAction.action).to.eql('update');
|
||||
expect(descriptionUserAction.payload).to.eql({ description: newDesc });
|
||||
});
|
||||
|
||||
it('retrieves only the tags user actions', async () => {
|
||||
const theCase = await createCase(supertest, getPostCaseRequest());
|
||||
|
||||
await updateCase({
|
||||
supertest,
|
||||
params: {
|
||||
cases: [
|
||||
{
|
||||
id: theCase.id,
|
||||
version: theCase.version,
|
||||
tags: ['cool', 'neat'],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const response = await findCaseUserActions({
|
||||
caseID: theCase.id,
|
||||
supertest,
|
||||
options: {
|
||||
sortOrder: 'asc',
|
||||
types: [ActionTypes.tags],
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.userActions.length).to.be(2);
|
||||
|
||||
const addTagsUserAction = response.userActions[0];
|
||||
const deleteTagsUserAction = response.userActions[1];
|
||||
|
||||
expect(addTagsUserAction.type).to.eql('tags');
|
||||
expect(addTagsUserAction.action).to.eql('add');
|
||||
expect(addTagsUserAction.payload).to.eql({ tags: ['cool', 'neat'] });
|
||||
expect(deleteTagsUserAction.type).to.eql('tags');
|
||||
expect(deleteTagsUserAction.action).to.eql('delete');
|
||||
expect(deleteTagsUserAction.payload).to.eql({ tags: ['defacement'] });
|
||||
});
|
||||
|
||||
it('retrieves only the title user actions', async () => {
|
||||
const newTitle = 'Such a great title';
|
||||
const theCase = await createCase(supertest, getPostCaseRequest());
|
||||
|
||||
await updateCase({
|
||||
supertest,
|
||||
params: {
|
||||
cases: [
|
||||
{
|
||||
id: theCase.id,
|
||||
version: theCase.version,
|
||||
title: newTitle,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const response = await findCaseUserActions({
|
||||
caseID: theCase.id,
|
||||
supertest,
|
||||
options: {
|
||||
sortOrder: 'asc',
|
||||
types: [ActionTypes.title],
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.userActions.length).to.be(1);
|
||||
|
||||
const descriptionUserAction = response.userActions[0];
|
||||
|
||||
expect(descriptionUserAction.type).to.eql('title');
|
||||
expect(descriptionUserAction.action).to.eql('update');
|
||||
expect(descriptionUserAction.payload).to.eql({ title: newTitle });
|
||||
});
|
||||
|
||||
it('retrieves only the status user actions', async () => {
|
||||
const theCase = await createCase(supertest, getPostCaseRequest());
|
||||
|
||||
await updateCase({
|
||||
supertest,
|
||||
params: {
|
||||
cases: [
|
||||
{
|
||||
id: theCase.id,
|
||||
version: theCase.version,
|
||||
status: CaseStatuses.closed,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const response = await findCaseUserActions({
|
||||
caseID: theCase.id,
|
||||
supertest,
|
||||
options: {
|
||||
sortOrder: 'asc',
|
||||
types: [ActionTypes.status],
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.userActions.length).to.be(1);
|
||||
|
||||
const statusUserAction = response.userActions[0];
|
||||
|
||||
expect(statusUserAction.type).to.eql('status');
|
||||
expect(statusUserAction.action).to.eql('update');
|
||||
expect(statusUserAction.payload).to.eql({ status: 'closed' });
|
||||
});
|
||||
|
||||
it('retrieves only the settings user actions', async () => {
|
||||
const theCase = await createCase(supertest, getPostCaseRequest());
|
||||
|
||||
await updateCase({
|
||||
supertest,
|
||||
params: {
|
||||
cases: [
|
||||
{
|
||||
id: theCase.id,
|
||||
version: theCase.version,
|
||||
settings: { syncAlerts: false },
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const response = await findCaseUserActions({
|
||||
caseID: theCase.id,
|
||||
supertest,
|
||||
options: {
|
||||
sortOrder: 'asc',
|
||||
types: [ActionTypes.settings],
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.userActions.length).to.be(1);
|
||||
|
||||
const settingsUserAction = response.userActions[0];
|
||||
|
||||
expect(settingsUserAction.type).to.eql('settings');
|
||||
expect(settingsUserAction.action).to.eql('update');
|
||||
expect(settingsUserAction.payload).to.eql({ settings: { syncAlerts: false } });
|
||||
});
|
||||
|
||||
it('retrieves only the severity user actions', async () => {
|
||||
const theCase = await createCase(supertest, getPostCaseRequest());
|
||||
|
||||
await updateCase({
|
||||
supertest,
|
||||
params: {
|
||||
cases: [
|
||||
{
|
||||
id: theCase.id,
|
||||
version: theCase.version,
|
||||
severity: CaseSeverity.HIGH,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const response = await findCaseUserActions({
|
||||
caseID: theCase.id,
|
||||
supertest,
|
||||
options: {
|
||||
sortOrder: 'asc',
|
||||
types: [ActionTypes.severity],
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.userActions.length).to.be(1);
|
||||
|
||||
const severityUserAction = response.userActions[0];
|
||||
|
||||
expect(severityUserAction.type).to.eql('severity');
|
||||
expect(severityUserAction.action).to.eql('update');
|
||||
expect(severityUserAction.payload).to.eql({ severity: CaseSeverity.HIGH });
|
||||
});
|
||||
|
||||
it('retrieves only the create_case user actions', async () => {
|
||||
const theCase = await createCase(supertest, getPostCaseRequest());
|
||||
|
||||
const response = await findCaseUserActions({
|
||||
caseID: theCase.id,
|
||||
supertest,
|
||||
options: {
|
||||
sortOrder: 'asc',
|
||||
types: [ActionTypes.create_case],
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.userActions.length).to.be(1);
|
||||
|
||||
const createCaseUserAction = response.userActions[0];
|
||||
|
||||
expect(createCaseUserAction.type).to.eql('create_case');
|
||||
expect(createCaseUserAction.action).to.eql('create');
|
||||
});
|
||||
|
||||
it('retrieves any non user comment user actions using the action filter', async () => {
|
||||
const theCase = await createCase(supertest, getPostCaseRequest());
|
||||
|
||||
const updatedCase = await bulkCreateAttachments({
|
||||
supertest,
|
||||
caseId: theCase.id,
|
||||
params: [
|
||||
postCommentUserReq,
|
||||
postExternalReferenceESReq,
|
||||
persistableStateAttachment,
|
||||
postCommentActionsReq,
|
||||
],
|
||||
});
|
||||
|
||||
await updateCase({
|
||||
supertest,
|
||||
params: {
|
||||
cases: [
|
||||
{
|
||||
id: updatedCase.id,
|
||||
version: updatedCase.version,
|
||||
severity: CaseSeverity.HIGH,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const response = await findCaseUserActions({
|
||||
caseID: theCase.id,
|
||||
supertest,
|
||||
options: {
|
||||
sortOrder: 'asc',
|
||||
types: [FindTypes.action],
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.userActions.length).to.be(5);
|
||||
|
||||
const createCaseUserAction = response.userActions[0];
|
||||
|
||||
expect(createCaseUserAction.type).to.eql('create_case');
|
||||
expect(createCaseUserAction.action).to.eql('create');
|
||||
|
||||
const externalRef = response.userActions[1] as CommentUserAction;
|
||||
|
||||
expect(externalRef.type).to.eql('comment');
|
||||
expect(externalRef.payload.comment.type).to.eql('externalReference');
|
||||
expect(externalRef.action).to.eql('create');
|
||||
|
||||
const persistableState = response.userActions[2] as CommentUserAction;
|
||||
|
||||
expect(persistableState.type).to.eql('comment');
|
||||
expect(persistableState.payload.comment.type).to.eql('persistableState');
|
||||
expect(persistableState.action).to.eql('create');
|
||||
|
||||
const actions = response.userActions[3] as CommentUserAction;
|
||||
|
||||
expect(actions.type).to.eql('comment');
|
||||
expect(actions.payload.comment.type).to.eql('actions');
|
||||
expect(actions.action).to.eql('create');
|
||||
|
||||
expect(response.userActions[4].type).to.eql('severity');
|
||||
expect(response.userActions[4].action).to.eql('update');
|
||||
});
|
||||
|
||||
it('retrieves only alert user actions', async () => {
|
||||
const theCase = await createCase(supertest, getPostCaseRequest());
|
||||
|
||||
await bulkCreateAttachments({
|
||||
supertest,
|
||||
caseId: theCase.id,
|
||||
params: [postCommentUserReq, postCommentAlertReq],
|
||||
});
|
||||
|
||||
const response = await findCaseUserActions({
|
||||
caseID: theCase.id,
|
||||
supertest,
|
||||
options: {
|
||||
sortOrder: 'asc',
|
||||
types: [FindTypes.alert],
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.userActions.length).to.be(1);
|
||||
|
||||
const alertUserAction = response.userActions[0] as CommentUserAction;
|
||||
|
||||
expect(alertUserAction.type).to.eql('comment');
|
||||
expect(alertUserAction.action).to.eql('create');
|
||||
expect(alertUserAction.payload.comment.type).to.eql('alert');
|
||||
});
|
||||
|
||||
it('retrieves only user comment user actions', async () => {
|
||||
const theCase = await createCase(supertest, getPostCaseRequest());
|
||||
|
||||
await bulkCreateAttachments({
|
||||
supertest,
|
||||
caseId: theCase.id,
|
||||
params: [postCommentUserReq, postCommentActionsReq, postCommentAlertReq],
|
||||
});
|
||||
|
||||
const response = await findCaseUserActions({
|
||||
caseID: theCase.id,
|
||||
supertest,
|
||||
options: {
|
||||
sortOrder: 'asc',
|
||||
types: [FindTypes.user],
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.userActions.length).to.be(1);
|
||||
|
||||
const userCommentUserAction = response.userActions[0] as CommentUserAction;
|
||||
|
||||
expect(userCommentUserAction.type).to.eql('comment');
|
||||
expect(userCommentUserAction.action).to.eql('create');
|
||||
expect(userCommentUserAction.payload.comment.type).to.eql('user');
|
||||
});
|
||||
|
||||
it('retrieves attachment user actions', async () => {
|
||||
const theCase = await createCase(supertest, getPostCaseRequest());
|
||||
|
||||
await bulkCreateAttachments({
|
||||
supertest,
|
||||
caseId: theCase.id,
|
||||
params: [
|
||||
// This one should not show up in the filter for attachments
|
||||
postCommentUserReq,
|
||||
postExternalReferenceESReq,
|
||||
persistableStateAttachment,
|
||||
// This one should not show up in the filter for attachments
|
||||
postCommentActionsReq,
|
||||
// This one should not show up in the filter for attachments
|
||||
postCommentAlertReq,
|
||||
],
|
||||
});
|
||||
|
||||
const response = await findCaseUserActions({
|
||||
caseID: theCase.id,
|
||||
supertest,
|
||||
options: {
|
||||
sortOrder: 'asc',
|
||||
types: [FindTypes.attachment],
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.userActions.length).to.be(2);
|
||||
|
||||
const externalRefUserAction = response.userActions[0] as CommentUserAction;
|
||||
|
||||
expect(externalRefUserAction.type).to.eql('comment');
|
||||
expect(externalRefUserAction.action).to.eql('create');
|
||||
expect(externalRefUserAction.payload.comment.type).to.eql('externalReference');
|
||||
|
||||
const peristableStateUserAction = response.userActions[1] as CommentUserAction;
|
||||
|
||||
expect(peristableStateUserAction.type).to.eql('comment');
|
||||
expect(peristableStateUserAction.action).to.eql('create');
|
||||
expect(peristableStateUserAction.payload.comment.type).to.eql('persistableState');
|
||||
});
|
||||
|
||||
describe('filtering on multiple types', () => {
|
||||
it('retrieves the create_case and comment user actions', async () => {
|
||||
const theCase = await createCase(supertest, getPostCaseRequest());
|
||||
|
||||
await createComment({
|
||||
supertest,
|
||||
caseId: theCase.id,
|
||||
params: postCommentUserReq,
|
||||
});
|
||||
|
||||
const response = await findCaseUserActions({
|
||||
caseID: theCase.id,
|
||||
supertest,
|
||||
options: {
|
||||
sortOrder: 'asc',
|
||||
types: [ActionTypes.create_case, ActionTypes.comment],
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.userActions.length).to.be(2);
|
||||
|
||||
const createCaseUserAction = response.userActions[0];
|
||||
expect(createCaseUserAction.type).to.eql('create_case');
|
||||
expect(createCaseUserAction.action).to.eql('create');
|
||||
|
||||
const commentUserAction = response.userActions[1];
|
||||
expect(commentUserAction.type).to.eql('comment');
|
||||
expect(commentUserAction.action).to.eql('create');
|
||||
});
|
||||
|
||||
it('retrieves the create_case user action when there are not valid comment user actions', async () => {
|
||||
const theCase = await createCase(supertest, getPostCaseRequest());
|
||||
|
||||
const response = await findCaseUserActions({
|
||||
caseID: theCase.id,
|
||||
supertest,
|
||||
options: {
|
||||
sortOrder: 'asc',
|
||||
types: [ActionTypes.create_case, ActionTypes.comment],
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.userActions.length).to.be(1);
|
||||
|
||||
const createCaseUserAction = response.userActions[0];
|
||||
expect(createCaseUserAction.type).to.eql('create_case');
|
||||
expect(createCaseUserAction.action).to.eql('create');
|
||||
});
|
||||
});
|
||||
|
||||
describe('rbac', () => {
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
|
||||
let secCase: CaseResponse;
|
||||
let obsCase: CaseResponse;
|
||||
let secCaseSpace2: CaseResponse;
|
||||
|
||||
beforeEach(async () => {
|
||||
[secCase, obsCase, secCaseSpace2] = await Promise.all([
|
||||
createCase(
|
||||
supertestWithoutAuth,
|
||||
getPostCaseRequest({ owner: 'securitySolutionFixture' }),
|
||||
200,
|
||||
{
|
||||
user: secOnly,
|
||||
space: 'space1',
|
||||
}
|
||||
),
|
||||
createCase(
|
||||
supertestWithoutAuth,
|
||||
getPostCaseRequest({ owner: 'observabilityFixture' }),
|
||||
200,
|
||||
{
|
||||
user: obsOnly,
|
||||
space: 'space1',
|
||||
}
|
||||
),
|
||||
createCase(
|
||||
supertestWithoutAuth,
|
||||
getPostCaseRequest({ owner: 'securitySolutionFixture' }),
|
||||
200,
|
||||
{
|
||||
user: superUser,
|
||||
space: 'space2',
|
||||
}
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return with the correct status code when executing with various users', async () => {
|
||||
for (const scenario of [
|
||||
{
|
||||
user: globalRead,
|
||||
id: secCase.id,
|
||||
space: 'space1',
|
||||
},
|
||||
{
|
||||
user: globalRead,
|
||||
id: obsCase.id,
|
||||
space: 'space1',
|
||||
},
|
||||
{
|
||||
user: superUser,
|
||||
id: secCase.id,
|
||||
space: 'space1',
|
||||
},
|
||||
{
|
||||
user: superUser,
|
||||
id: obsCase.id,
|
||||
space: 'space1',
|
||||
},
|
||||
{
|
||||
user: secOnlyRead,
|
||||
id: secCase.id,
|
||||
space: 'space1',
|
||||
},
|
||||
{
|
||||
user: obsOnlyRead,
|
||||
id: obsCase.id,
|
||||
space: 'space1',
|
||||
},
|
||||
]) {
|
||||
const res = await findCaseUserActions({
|
||||
caseID: scenario.id,
|
||||
supertest: supertestWithoutAuth,
|
||||
options: {
|
||||
sortOrder: 'asc',
|
||||
types: [ActionTypes.create_case],
|
||||
},
|
||||
auth: { user: scenario.user, space: scenario.space },
|
||||
});
|
||||
|
||||
expect(res.userActions.length).to.be(1);
|
||||
}
|
||||
});
|
||||
|
||||
it('should fail to find user actions for a case that the user is not authorized for', async () => {
|
||||
for (const scenario of [
|
||||
{
|
||||
user: secOnlyRead,
|
||||
id: obsCase.id,
|
||||
space: 'space1',
|
||||
expectedCode: 403,
|
||||
},
|
||||
{
|
||||
user: obsOnlyRead,
|
||||
id: secCase.id,
|
||||
space: 'space1',
|
||||
expectedCode: 403,
|
||||
},
|
||||
{
|
||||
user: noKibanaPrivileges,
|
||||
id: secCase.id,
|
||||
space: 'space1',
|
||||
expectedCode: 403,
|
||||
},
|
||||
{
|
||||
user: secOnlyRead,
|
||||
id: secCaseSpace2.id,
|
||||
space: 'space2',
|
||||
expectedCode: 403,
|
||||
},
|
||||
]) {
|
||||
await findCaseUserActions({
|
||||
caseID: scenario.id,
|
||||
supertest: supertestWithoutAuth,
|
||||
expectedHttpCode: scenario.expectedCode,
|
||||
options: {
|
||||
sortOrder: 'asc',
|
||||
types: [ActionTypes.create_case],
|
||||
},
|
||||
auth: { user: scenario.user, space: scenario.space },
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -22,7 +22,6 @@ import {
|
|||
deleteAllCaseItems,
|
||||
createCase,
|
||||
updateCase,
|
||||
getCaseUserActions,
|
||||
superUserSpace1Auth,
|
||||
deleteCases,
|
||||
createComment,
|
||||
|
@ -30,6 +29,7 @@ import {
|
|||
deleteComment,
|
||||
extractWarningValueFromWarningHeader,
|
||||
} from '../../../../common/lib/utils';
|
||||
import { getCaseUserActions } from '../../../../common/lib/user_actions';
|
||||
import {
|
||||
globalRead,
|
||||
noKibanaPrivileges,
|
||||
|
@ -51,6 +51,23 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
await deleteAllCaseItems(es);
|
||||
});
|
||||
|
||||
it('populates the action_id', async () => {
|
||||
const theCase = await createCase(supertest, postCaseReq);
|
||||
const userActions = await getCaseUserActions({ supertest, caseID: theCase.id });
|
||||
|
||||
expect(userActions.length).to.be(1);
|
||||
expect(userActions[0].action_id).not.to.be(undefined);
|
||||
expect(userActions[0]).not.to.have.property('id');
|
||||
});
|
||||
|
||||
it('populates the case_id', async () => {
|
||||
const theCase = await createCase(supertest, postCaseReq);
|
||||
const userActions = await getCaseUserActions({ supertest, caseID: theCase.id });
|
||||
|
||||
expect(userActions.length).to.be(1);
|
||||
expect(userActions[0].case_id).not.to.be(undefined);
|
||||
});
|
||||
|
||||
it('creates a create case user action when a case is created', async () => {
|
||||
const theCase = await createCase(supertest, postCaseReq);
|
||||
const userActions = await getCaseUserActions({ supertest, caseID: theCase.id });
|
||||
|
|
|
@ -9,7 +9,8 @@ import expect from '@kbn/expect';
|
|||
import { CASES_URL, SECURITY_SOLUTION_OWNER } from '@kbn/cases-plugin/common/constants';
|
||||
import { ActionTypes, CaseUserActionsResponse, CommentType } from '@kbn/cases-plugin/common/api';
|
||||
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
|
||||
import { deleteAllCaseItems, getCaseUserActions } from '../../../../common/lib/utils';
|
||||
import { deleteAllCaseItems } from '../../../../common/lib/utils';
|
||||
import { getCaseUserActions } from '../../../../common/lib/user_actions';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function createGetTests({ getService }: FtrProviderContext) {
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* 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 http from 'http';
|
||||
import expect from '@kbn/expect';
|
||||
import { ActionTypes } from '@kbn/cases-plugin/common/api';
|
||||
import { getPostCaseRequest } from '../../../../../common/lib/mock';
|
||||
import {
|
||||
deleteAllCaseItems,
|
||||
createCase,
|
||||
updateCase,
|
||||
pushCase,
|
||||
} from '../../../../../common/lib/utils';
|
||||
import {
|
||||
createCaseWithConnector,
|
||||
getServiceNowSimulationServer,
|
||||
} from '../../../../../common/lib/connectors';
|
||||
import { findCaseUserActions } from '../../../../../common/lib/user_actions';
|
||||
|
||||
import { ObjectRemover as ActionsRemover } from '../../../../../../alerting_api_integration/common/lib';
|
||||
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext): void => {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('es');
|
||||
const actionsRemover = new ActionsRemover(supertest);
|
||||
|
||||
describe('find_user_actions', () => {
|
||||
let serviceNowSimulatorURL: string = '';
|
||||
let serviceNowServer: http.Server;
|
||||
|
||||
before(async () => {
|
||||
const { url, server } = await getServiceNowSimulationServer();
|
||||
serviceNowSimulatorURL = url;
|
||||
serviceNowServer = server;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteAllCaseItems(es);
|
||||
await actionsRemover.removeAll();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
serviceNowServer.close();
|
||||
});
|
||||
|
||||
describe('filters using the type query parameter', () => {
|
||||
it('retrieves only the assignees user actions', async () => {
|
||||
const theCase = await createCase(supertest, getPostCaseRequest());
|
||||
|
||||
await updateCase({
|
||||
params: {
|
||||
cases: [
|
||||
{
|
||||
id: theCase.id,
|
||||
version: theCase.version,
|
||||
assignees: [
|
||||
{
|
||||
uid: '123',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
supertest,
|
||||
});
|
||||
|
||||
const response = await findCaseUserActions({
|
||||
caseID: theCase.id,
|
||||
supertest,
|
||||
options: {
|
||||
sortOrder: 'asc',
|
||||
types: [ActionTypes.assignees],
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.userActions.length).to.be(1);
|
||||
|
||||
const addAssigneesUserAction = response.userActions[0];
|
||||
expect(addAssigneesUserAction.type).to.eql('assignees');
|
||||
expect(addAssigneesUserAction.action).to.eql('add');
|
||||
expect(addAssigneesUserAction.payload).to.eql({ assignees: [{ uid: '123' }] });
|
||||
});
|
||||
|
||||
it('retrieves only the pushed user actions', async () => {
|
||||
const { postedCase, connector } = await createCaseWithConnector({
|
||||
supertest,
|
||||
serviceNowSimulatorURL,
|
||||
actionsRemover,
|
||||
});
|
||||
|
||||
const theCase = await pushCase({
|
||||
supertest,
|
||||
caseId: postedCase.id,
|
||||
connectorId: connector.id,
|
||||
});
|
||||
|
||||
const response = await findCaseUserActions({
|
||||
caseID: theCase.id,
|
||||
supertest,
|
||||
options: {
|
||||
sortOrder: 'asc',
|
||||
types: [ActionTypes.pushed],
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.userActions.length).to.be(1);
|
||||
|
||||
const pushedUserAction = response.userActions[0];
|
||||
expect(pushedUserAction.type).to.eql('pushed');
|
||||
expect(pushedUserAction.action).to.eql('push_to_service');
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -17,11 +17,11 @@ import {
|
|||
deleteCasesUserActions,
|
||||
deleteComments,
|
||||
deleteConfiguration,
|
||||
getCaseUserActions,
|
||||
pushCase,
|
||||
updateCase,
|
||||
updateConfiguration,
|
||||
} from '../../../../../common/lib/utils';
|
||||
import { getCaseUserActions } from '../../../../../common/lib/user_actions';
|
||||
import {
|
||||
createCaseWithConnector,
|
||||
getServiceNowSimulationServer,
|
||||
|
|
|
@ -29,6 +29,7 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => {
|
|||
// Trial
|
||||
loadTestFile(require.resolve('./cases/push_case'));
|
||||
loadTestFile(require.resolve('./cases/user_actions/get_all_user_actions'));
|
||||
loadTestFile(require.resolve('./cases/user_actions/find_user_actions'));
|
||||
loadTestFile(require.resolve('./cases/assignees'));
|
||||
loadTestFile(require.resolve('./cases/find_cases'));
|
||||
loadTestFile(require.resolve('./configure'));
|
||||
|
|
|
@ -26,10 +26,10 @@ import {
|
|||
createCase,
|
||||
createComment,
|
||||
deleteAllCaseItems,
|
||||
getCaseUserActions,
|
||||
pushCase,
|
||||
updateCase,
|
||||
} from '../../../../common/lib/utils';
|
||||
import { getCaseUserActions } from '../../../../common/lib/user_actions';
|
||||
import { getPostCaseRequest, postCommentUserReq } from '../../../../common/lib/mock';
|
||||
import {
|
||||
createCaseWithConnector,
|
||||
|
|
|
@ -21,8 +21,8 @@ import {
|
|||
updateComment,
|
||||
getConfigurationRequest,
|
||||
updateConfiguration,
|
||||
getCaseUserActions,
|
||||
} from '../../../../common/lib/utils';
|
||||
import { getCaseUserActions } from '../../../../common/lib/user_actions';
|
||||
import { getPostCaseRequest, postCommentUserReq } from '../../../../common/lib/mock';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
|
|
|
@ -9,12 +9,8 @@ import expect from '@kbn/expect';
|
|||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
|
||||
import { getPostCaseRequest } from '../../../../common/lib/mock';
|
||||
import {
|
||||
deleteAllCaseItems,
|
||||
createCase,
|
||||
getCaseUserActions,
|
||||
getAuthWithSuperUser,
|
||||
} from '../../../../common/lib/utils';
|
||||
import { deleteAllCaseItems, createCase, getAuthWithSuperUser } from '../../../../common/lib/utils';
|
||||
import { getCaseUserActions } from '../../../../common/lib/user_actions';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext): void => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue