[Cases] Restrict the Find Comment API query params (#156863)

Fixes #155983

## Summary

This PR changes the accepted params for the Find Comments API to be only
`perPage`, `page` and `sort_order`.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: lcawl <lcawley@elastic.co>
This commit is contained in:
Antonio 2023-05-17 01:25:54 +09:00 committed by GitHub
parent 15f3a0f418
commit 927743ae52
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 544 additions and 342 deletions

View file

@ -24,6 +24,7 @@ Any modifications made to this file will be overwritten.
<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="#findCaseComments"><code><span class="http-method">get</span> /s/{spaceId}/api/cases/{caseId}/comments/_find</code></a></li>
<li><a href="#findCaseConnectors"><code><span class="http-method">get</span> /s/{spaceId}/api/cases/configure/connectors/_find</code></a></li>
<li><a href="#findCases"><code><span class="http-method">get</span> /s/{spaceId}/api/cases/_find</code></a></li>
<li><a href="#getAllCaseComments"><code><span class="http-method">get</span> /s/{spaceId}/api/cases/{caseId}/comments</code></a></li>
@ -461,9 +462,9 @@ Any modifications made to this file will be overwritten.
<div class="param-desc"><span class="param-type">Query Parameter</span> &mdash; 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> &mdash; 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> &mdash; The number of items to return. default: 20 </div><div class="param">sortOrder (optional)</div>
<div class="param-desc"><span class="param-type">Query Parameter</span> &mdash; Determines the sort order. default: asc </div><div class="param">types (optional)</div>
<div class="param-desc"><span class="param-type">Query Parameter</span> &mdash; Determines the sort order. default: desc </div><div class="param">types (optional)</div>
<div class="param-desc"><span class="param-type">Query Parameter</span> &mdash; Determines the types of user actions to return. default: null </div>
</div> <!-- field-items -->
@ -530,6 +531,119 @@ Any modifications made to this file will be overwritten.
<a href="#4xx_response">4xx_response</a>
</div> <!-- method -->
<hr/>
<div class="method"><a name="findCaseComments"/>
<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}/comments/_find</code></pre></div>
<div class="method-summary">Retrieves all the user comments from a case. (<span class="nickname">findCaseComments</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 cases with the comments 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> &mdash; 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> &mdash; 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> &mdash; 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> &mdash; The number of items to return. default: 20 </div><div class="param">sortOrder (optional)</div>
<div class="param-desc"><span class="param-type">Query Parameter</span> &mdash; Determines the sort order. default: desc </div>
</div> <!-- field-items -->
<h3 class="field-label">Return type</h3>
<div class="return-type">
<a href="#case_response_properties">case_response_properties</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>{
"owner" : "cases",
"totalComment" : 0,
"settings" : {
"syncAlerts" : true
},
"totalAlerts" : 0,
"closed_at" : "2000-01-23T04:56:07.000+00:00",
"comments" : [ null, null ],
"assignees" : [ {
"uid" : "u_0wpfV1MqYDaXzLtRVY-gLMrddKDEmfz51Fszhj7hWC8_0"
}, {
"uid" : "u_0wpfV1MqYDaXzLtRVY-gLMrddKDEmfz51Fszhj7hWC8_0"
} ],
"created_at" : "2022-05-13T09:16:17.416Z",
"description" : "A case description.",
"title" : "Case title 1",
"created_by" : {
"full_name" : "full_name",
"profile_uid" : "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0",
"email" : "email",
"username" : "elastic"
},
"version" : "WzUzMiwxXQ==",
"closed_by" : {
"full_name" : "full_name",
"profile_uid" : "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0",
"email" : "email",
"username" : "elastic"
},
"tags" : [ "tag-1" ],
"duration" : 120,
"updated_at" : "2000-01-23T04:56:07.000+00:00",
"updated_by" : {
"full_name" : "full_name",
"profile_uid" : "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0",
"email" : "email",
"username" : "elastic"
},
"id" : "66b9aa00-94fa-11ea-9f74-e7e108796192",
"external_service" : {
"external_title" : "external_title",
"pushed_by" : {
"full_name" : "full_name",
"profile_uid" : "u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0",
"email" : "email",
"username" : "elastic"
},
"external_url" : "external_url",
"pushed_at" : "2000-01-23T04:56:07.000+00:00",
"connector_id" : "connector_id",
"external_id" : "external_id",
"connector_name" : "connector_name"
}
}</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="#case_response_properties">case_response_properties</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="findCaseConnectors"/>
<div class="method-path">
<a class="up" href="#__Methods">Up</a>
@ -620,7 +734,7 @@ Any modifications made to this file will be overwritten.
<div class="param-desc"><span class="param-type">Query Parameter</span> &mdash; 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> &mdash; 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> &mdash; The number of items to return. default: 20 </div><div class="param">reporters (optional)</div>
<div class="param-desc"><span class="param-type">Query Parameter</span> &mdash; Filters the returned cases by the user name of the reporter. default: null </div><div class="param">search (optional)</div>
@ -2880,7 +2994,7 @@ Any modifications made to this file will be overwritten.
</div>
<div class="model">
<h3><a name="search_fields"><code>search_fields</code> - </a> <a class="up" href="#__Models">Up</a></h3>
<div class='model-description'></div>
<div class='model-description'>The fields to perform the <code>simple_query_string</code> parsed query against.</div>
<div class="field-items">
</div> <!-- field-items -->
</div>

View file

@ -7,7 +7,7 @@
import * as rt from 'io-ts';
import { jsonValueRt } from '../../runtime_types';
import { SavedObjectFindOptionsRt } from '../../saved_object';
import { NumberFromString } from '../../saved_object';
import { UserRt } from '../../user';
@ -273,10 +273,28 @@ export const CommentsFindResponseRt = rt.type({
export const CommentsRt = rt.array(CommentRt);
export const FindQueryParamsRt = rt.partial({
...SavedObjectFindOptionsRt.props,
export const FindCommentsQueryParamsRt = rt.partial({
/**
* The page of objects to return
*/
page: rt.union([rt.number, NumberFromString]),
/**
* The number of objects to return for a page
*/
perPage: rt.union([rt.number, NumberFromString]),
/**
* Order to sort the response
*/
sortOrder: rt.union([rt.literal('desc'), rt.literal('asc')]),
});
export const FindCommentsArgsRt = rt.intersection([
rt.type({
caseID: rt.string,
}),
rt.type({ queryParams: rt.union([FindCommentsQueryParamsRt, rt.undefined]) }),
]);
export const BulkCreateCommentRequestRt = rt.array(CommentRequestRt);
export const BulkGetAttachmentsRequestRt = rt.type({
@ -295,7 +313,7 @@ export const BulkGetAttachmentsResponseRt = rt.type({
),
});
export type FindQueryParams = rt.TypeOf<typeof FindQueryParamsRt>;
export type FindCommentsQueryParams = rt.TypeOf<typeof FindCommentsQueryParamsRt>;
export type AttributesTypeActions = rt.TypeOf<typeof AttributesTypeActionsRt>;
export type AttributesTypeAlerts = rt.TypeOf<typeof AttributesTypeAlertsRt>;
export type AttributesTypeUser = rt.TypeOf<typeof AttributesTypeUserRt>;

View file

@ -19,52 +19,3 @@ export const NumberFromString = new rt.Type<number, string, unknown>(
}),
String
);
const ReferenceRt = rt.type({ id: rt.string, type: rt.string });
export const SavedObjectFindOptionsRt = rt.partial({
/**
* The default operator to use for the simple_query_string
*/
defaultSearchOperator: rt.union([rt.literal('AND'), rt.literal('OR')]),
/**
* The operator for controlling the logic of the `hasReference` field
*/
hasReferenceOperator: rt.union([rt.literal('AND'), rt.literal('OR')]),
/**
* Filter by objects that have an association to another object
*/
hasReference: rt.union([rt.array(ReferenceRt), ReferenceRt]),
/**
* The fields to return in the attributes key of the response
*/
fields: rt.array(rt.string),
/**
* The filter is a KQL string with the caveat that if you filter with an attribute from your saved object type, it should look like that: savedObjectType.attributes.title: "myTitle". However, If you use a root attribute of a saved object such as updated_at, you will have to define your filter like that: savedObjectType.updated_at > 2018-12-22
*/
filter: rt.string,
/**
* The page of objects to return
*/
page: NumberFromString,
/**
* The number of objects to return for a page
*/
perPage: NumberFromString,
/**
* An Elasticsearch simple_query_string query that filters the objects in the response
*/
search: rt.string,
/**
* The fields to perform the simple_query_string parsed query against
*/
searchFields: rt.array(rt.string),
/**
* Sorts the response. Includes "root" and "type" fields. "root" fields exist for all saved objects, such as "updated_at". "type" fields are specific to an object type, such as fields returned in the attributes key of the response. When a single type is defined in the type parameter, the "root" and "type" fields are allowed, and validity checks are made in that order. When multiple types are defined in the type parameter, only "root" fields are allowed
*/
sortField: rt.string,
/**
* Order to sort the response
*/
sortOrder: rt.union([rt.literal('desc'), rt.literal('asc')]),
});

View file

@ -259,24 +259,10 @@
"$ref": "#/components/parameters/owner"
},
{
"name": "page",
"in": "query",
"description": "The page number to return.",
"schema": {
"type": "integer",
"default": 1
},
"example": 1
"$ref": "#/components/parameters/page_index"
},
{
"name": "perPage",
"in": "query",
"description": "The number of cases to return per page.",
"schema": {
"type": "integer",
"default": 20
},
"example": 20
"$ref": "#/components/parameters/page_size"
},
{
"name": "reporters",
@ -341,18 +327,7 @@
"example": "updatedAt"
},
{
"name": "sortOrder",
"in": "query",
"description": "Determines the sort order.",
"schema": {
"type": "string",
"enum": [
"asc",
"desc"
],
"default": "desc"
},
"example": "asc"
"$ref": "#/components/parameters/sort_order"
},
{
"name": "status",
@ -1842,6 +1817,65 @@
}
]
},
"/s/{spaceId}/api/cases/{caseId}/comments/_find": {
"get": {
"summary": "Retrieves all the user comments from a case.",
"operationId": "findCaseComments",
"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 cases with the comments you're seeking.\n",
"tags": [
"cases"
],
"parameters": [
{
"$ref": "#/components/parameters/case_id"
},
{
"$ref": "#/components/parameters/page_index"
},
{
"$ref": "#/components/parameters/page_size"
},
{
"$ref": "#/components/parameters/sort_order"
},
{
"$ref": "#/components/parameters/space_id"
}
],
"responses": {
"200": {
"description": "Indicates a successful call.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/case_response_properties"
}
}
}
},
"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"
}
]
},
"/s/{spaceId}/api/cases/{caseId}/comments/{commentId}": {
"delete": {
"summary": "Deletes a comment or alert from a case.",
@ -2095,38 +2129,14 @@
"$ref": "#/components/parameters/space_id"
},
{
"name": "page",
"in": "query",
"description": "The page number to return.",
"schema": {
"type": "string",
"default": "1"
},
"$ref": "#/components/parameters/page_index",
"example": "1"
},
{
"name": "perPage",
"in": "query",
"description": "The number of user actions to return per page.",
"schema": {
"type": "string",
"default": "20"
},
"example": "20"
"$ref": "#/components/parameters/page_size"
},
{
"name": "sortOrder",
"in": "query",
"description": "Determines the sort order.",
"schema": {
"type": "string",
"enum": [
"asc",
"desc"
],
"default": "asc"
},
"example": "asc"
"$ref": "#/components/parameters/sort_order"
},
{
"name": "types",
@ -2266,6 +2276,26 @@
},
"example": "cases"
},
"page_index": {
"in": "query",
"name": "page",
"description": "The page number to return.",
"required": false,
"schema": {
"type": "integer",
"default": 1
}
},
"page_size": {
"in": "query",
"name": "perPage",
"description": "The number of items to return.",
"required": false,
"schema": {
"type": "integer",
"default": 20
}
},
"severity": {
"in": "query",
"name": "severity",
@ -2280,6 +2310,20 @@
]
}
},
"sort_order": {
"in": "query",
"name": "sortOrder",
"description": "Determines the sort order.",
"required": false,
"schema": {
"type": "string",
"enum": [
"asc",
"desc"
],
"default": "desc"
}
},
"alert_id": {
"in": "path",
"name": "alertId",
@ -3455,6 +3499,7 @@
},
"search_fields": {
"type": "string",
"description": "The fields to perform the `simple_query_string` parsed query against.",
"enum": [
"closed_by.username",
"closed_by.full_name",

View file

@ -155,20 +155,8 @@ paths:
type: string
example: now-1d
- $ref: '#/components/parameters/owner'
- name: page
in: query
description: The page number to return.
schema:
type: integer
default: 1
example: 1
- name: perPage
in: query
description: The number of cases to return per page.
schema:
type: integer
default: 20
example: 20
- $ref: '#/components/parameters/page_index'
- $ref: '#/components/parameters/page_size'
- name: reporters
in: query
description: Filters the returned cases by the user name of the reporter.
@ -204,16 +192,7 @@ paths:
- updatedAt
default: createdAt
example: updatedAt
- name: sortOrder
in: query
description: Determines the sort order.
schema:
type: string
enum:
- asc
- desc
default: desc
example: asc
- $ref: '#/components/parameters/sort_order'
- name: status
in: query
description: Filters the returned cases by state.
@ -1156,6 +1135,37 @@ paths:
- url: https://localhost:5601
servers:
- url: https://localhost:5601
/s/{spaceId}/api/cases/{caseId}/comments/_find:
get:
summary: Retrieves all the user comments from a case.
operationId: findCaseComments
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 cases with the comments you're seeking.
tags:
- cases
parameters:
- $ref: '#/components/parameters/case_id'
- $ref: '#/components/parameters/page_index'
- $ref: '#/components/parameters/page_size'
- $ref: '#/components/parameters/sort_order'
- $ref: '#/components/parameters/space_id'
responses:
'200':
description: Indicates a successful call.
content:
application/json:
schema:
$ref: '#/components/schemas/case_response_properties'
'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
/s/{spaceId}/api/cases/{caseId}/comments/{commentId}:
delete:
summary: Deletes a comment or alert from a case.
@ -1297,30 +1307,10 @@ paths:
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'
- $ref: '#/components/parameters/page_index'
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
- $ref: '#/components/parameters/page_size'
- $ref: '#/components/parameters/sort_order'
- name: types
in: query
description: Determines the types of user actions to return.
@ -1413,6 +1403,22 @@ components:
items:
$ref: '#/components/schemas/owners'
example: cases
page_index:
in: query
name: page
description: The page number to return.
required: false
schema:
type: integer
default: 1
page_size:
in: query
name: perPage
description: The number of items to return.
required: false
schema:
type: integer
default: 20
severity:
in: query
name: severity
@ -1424,6 +1430,17 @@ components:
- high
- low
- medium
sort_order:
in: query
name: sortOrder
description: Determines the sort order.
required: false
schema:
type: string
enum:
- asc
- desc
default: desc
alert_id:
in: path
name: alertId
@ -2284,6 +2301,7 @@ components:
type: string
search_fields:
type: string
description: The fields to perform the `simple_query_string` parsed query against.
enum:
- closed_by.username
- closed_by.full_name

View file

@ -0,0 +1,7 @@
in: query
name: page
description: The page number to return.
required: false
schema:
type: integer
default: 1

View file

@ -0,0 +1,7 @@
in: query
name: perPage
description: The number of items to return.
required: false
schema:
type: integer
default: 20

View file

@ -1,4 +1,5 @@
type: string
description: The fields to perform the `simple_query_string` parsed query against.
enum:
- closed_by.username
- closed_by.full_name

View file

@ -0,0 +1,8 @@
in: query
name: sortOrder
description: Determines the sort order.
required: false
schema:
type: string
enum: [asc, desc]
default: desc

View file

@ -39,6 +39,8 @@ paths:
$ref: 'paths/s@{spaceid}@api@cases@{caseid}@alerts.yaml'
'/s/{spaceId}/api/cases/{caseId}/comments':
$ref: 'paths/s@{spaceid}@api@cases@{caseid}@comments.yaml'
'/s/{spaceId}/api/cases/{caseId}/comments/_find':
$ref: 'paths/s@{spaceid}@api@cases@{caseid}@comments@_find.yaml'
'/s/{spaceId}/api/cases/{caseId}/comments/{commentId}':
$ref: 'paths/s@{spaceid}@api@cases@{caseid}@comments@{commentid}.yaml'
'/s/{spaceId}/api/cases/{caseId}/connector/{connectorId}/_push':

View file

@ -38,20 +38,8 @@ get:
type: string
example: now-1d
- $ref: '../components/parameters/owner.yaml'
- name: page
in: query
description: The page number to return.
schema:
type: integer
default: 1
example: 1
- name: perPage
in: query
description: The number of cases to return per page.
schema:
type: integer
default: 20
example: 20
- $ref: '../components/parameters/page_index.yaml'
- $ref: '../components/parameters/page_size.yaml'
- name: reporters
in: query
description: Filters the returned cases by the user name of the reporter.
@ -87,16 +75,7 @@ get:
- updatedAt
default: createdAt
example: updatedAt
- name: sortOrder
in: query
description: Determines the sort order.
schema:
type: string
enum:
- asc
- desc
default: desc
example: asc
- $ref: '../components/parameters/sort_order.yaml'
- name: status
in: query
description: Filters the returned cases by state.

View file

@ -25,7 +25,7 @@ post:
'200':
description: Indicates a successful call.
content:
application/json:
application/json:
schema:
$ref: '../components/schemas/case_response_properties.yaml'
examples:
@ -38,7 +38,7 @@ post:
schema:
$ref: '../components/schemas/4xx_response.yaml'
servers:
- url: https://localhost:5601
- url: https://localhost:5601
delete:
summary: Deletes all comments and alerts from a case.
@ -52,7 +52,7 @@ delete:
parameters:
- $ref: '../components/headers/kbn_xsrf.yaml'
- $ref: '../components/parameters/case_id.yaml'
- $ref: '../components/parameters/space_id.yaml'
- $ref: '../components/parameters/space_id.yaml'
responses:
'204':
description: Indicates a successful call.
@ -94,7 +94,7 @@ patch:
content:
application/json:
schema:
$ref: '../components/schemas/case_response_properties.yaml'
$ref: '../components/schemas/case_response_properties.yaml'
examples:
updateCaseCommentResponse:
$ref: '../components/examples/update_comment_response.yaml'
@ -105,7 +105,7 @@ patch:
schema:
$ref: '../components/schemas/4xx_response.yaml'
servers:
- url: https://localhost:5601
- url: https://localhost:5601
get:
summary: Retrieves all the comments from a case.
@ -137,5 +137,6 @@ get:
$ref: '../components/schemas/4xx_response.yaml'
servers:
- url: https://localhost:5601
servers:
- url: https://localhost:5601
- url: https://localhost:5601

View file

@ -0,0 +1,33 @@
get:
summary: Retrieves all the user comments from a case.
operationId: findCaseComments
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 cases with the comments you're seeking.
tags:
- cases
parameters:
- $ref: '../components/parameters/case_id.yaml'
- $ref: '../components/parameters/page_index.yaml'
- $ref: '../components/parameters/page_size.yaml'
- $ref: '../components/parameters/sort_order.yaml'
- $ref: '../components/parameters/space_id.yaml'
responses:
'200':
description: Indicates a successful call.
content:
application/json:
schema:
$ref: '../components/schemas/case_response_properties.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

View file

@ -10,30 +10,10 @@ get:
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"
- $ref: '../components/parameters/page_index.yaml'
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
- $ref: '../components/parameters/page_size.yaml'
- $ref: '../components/parameters/sort_order.yaml'
- name: types
in: query
description: Determines the types of user actions to return.

View file

@ -23,7 +23,7 @@ import type {
AddArgs,
DeleteAllArgs,
DeleteArgs,
FindArgs,
FindCommentsArgs,
GetAllAlertsAttachToCase,
GetAllArgs,
GetArgs,
@ -60,7 +60,7 @@ export interface AttachmentsSubClient {
/**
* Retrieves all comments matching the search criteria.
*/
find(findArgs: FindArgs): Promise<CommentsFindResponse>;
find(findArgs: FindCommentsArgs): Promise<CommentsFindResponse>;
/**
* Retrieves all alerts attach to a case given a single case ID
*/

View file

@ -0,0 +1,34 @@
/*
* 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 { createCasesClientMockArgs } from '../mocks';
import { find as findComment } from './get';
describe('get', () => {
describe('findComment', () => {
const clientArgs = createCasesClientMockArgs();
beforeEach(() => {
jest.clearAllMocks();
});
it('Invalid fields result in error', async () => {
expect(() =>
// @ts-expect-error
findComment({ caseID: 'mock-id', foo: 'bar' }, clientArgs)
).rejects.toThrow('excess properties: ["foo"]');
});
it('Invalid total items results in error', async () => {
await expect(() =>
findComment({ caseID: 'mock-id', queryParams: { page: 2, perPage: 9001 } }, clientArgs)
).rejects.toThrow(
'The number of documents is too high. Paginating through more than 10,000 documents is not possible.'
);
});
});
});

View file

@ -4,8 +4,16 @@
* 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 { SavedObject } from '@kbn/core/server';
import type { CasesClient } from '../client';
import type { CasesClientArgs } from '../types';
import type {
AlertResponse,
Comments,
@ -13,7 +21,22 @@ import type {
Comment,
CommentsFindResponse,
} from '../../../common/api';
import { CommentsRt, CommentRt, CommentsFindResponseRt } from '../../../common/api';
import type { FindCommentsArgs, GetAllAlertsAttachToCase, GetAllArgs, GetArgs } from './types';
import {
CASE_COMMENT_SAVED_OBJECT,
CASE_SAVED_OBJECT,
MAX_DOCS_PER_PAGE,
} from '../../../common/constants';
import {
FindCommentsArgsRt,
CommentType,
CommentsRt,
CommentRt,
CommentsFindResponseRt,
excess,
throwErrors,
} from '../../../common/api';
import {
defaultSortField,
transformComments,
@ -23,12 +46,8 @@ import {
} from '../../common/utils';
import { createCaseError } from '../../common/error';
import { DEFAULT_PAGE, DEFAULT_PER_PAGE } from '../../routes/api';
import type { CasesClientArgs } from '../types';
import { combineFilters, stringToKueryNode } from '../utils';
import { buildFilter, combineFilters } from '../utils';
import { Operations } from '../../authorization';
import { includeFieldsRequiredForAuthentication } from '../../authorization/utils';
import type { CasesClient } from '../client';
import type { FindArgs, GetAllAlertsAttachToCase, GetAllArgs, GetArgs } from './types';
const normalizeAlertResponse = (alerts: Array<SavedObject<AttributesTypeAlerts>>): AlertResponse =>
alerts.reduce((acc: AlertResponse, alert) => {
@ -98,59 +117,54 @@ export const getAllAlertsAttachToCase = async (
* Retrieves the attachments for a case entity. This support pagination.
*/
export async function find(
{ caseID, queryParams }: FindArgs,
data: FindCommentsArgs,
clientArgs: CasesClientArgs
): Promise<CommentsFindResponse> {
const {
unsecuredSavedObjectsClient,
services: { caseService },
services: { attachmentService },
logger,
authorization,
} = clientArgs;
const { caseID, queryParams } = pipe(
excess(FindCommentsArgsRt).decode(data),
fold(throwErrors(Boom.badRequest), identity)
);
if (
queryParams?.page &&
queryParams?.perPage &&
queryParams?.page * queryParams?.perPage > MAX_DOCS_PER_PAGE
) {
throw Boom.badRequest(
'The number of documents is too high. Paginating through more than 10,000 documents is not possible.'
);
}
try {
const { filter: authorizationFilter, ensureSavedObjectsAreAuthorized } =
await authorization.getAuthorizationFilter(Operations.findComments);
const id = caseID;
const { filter, ...queryWithoutFilter } = queryParams ?? {};
const filter = combineFilters([
buildFilter({
filters: [CommentType.user],
field: 'type',
operator: 'or',
type: CASE_COMMENT_SAVED_OBJECT,
}),
authorizationFilter,
]);
// if the fields property was defined, make sure we include the 'owner' field in the response
const fields = includeFieldsRequiredForAuthentication(queryWithoutFilter.fields);
// combine any passed in filter property and the filter for the appropriate owner
const combinedFilter = combineFilters([stringToKueryNode(filter), authorizationFilter]);
const args = queryParams
? {
caseService,
unsecuredSavedObjectsClient,
id,
options: {
// We need this because the default behavior of getAllCaseComments is to return all the comments
// unless the page and/or perPage is specified. Since we're spreading the query after the request can
// still override this behavior.
page: DEFAULT_PAGE,
perPage: DEFAULT_PER_PAGE,
sortField: 'created_at',
filter: combinedFilter,
...queryWithoutFilter,
fields,
},
}
: {
caseService,
unsecuredSavedObjectsClient,
id,
options: {
page: DEFAULT_PAGE,
perPage: DEFAULT_PER_PAGE,
sortField: 'created_at',
filter: combinedFilter,
},
};
const theComments = await caseService.getAllCaseComments(args);
const theComments = await attachmentService.find({
options: {
page: queryParams?.page ?? DEFAULT_PAGE,
perPage: queryParams?.perPage ?? DEFAULT_PER_PAGE,
...(queryParams?.sortOrder && { sortOrder: queryParams?.sortOrder }),
sortField: 'created_at',
hasReference: { type: CASE_SAVED_OBJECT, id: caseID },
filter,
},
});
ensureSavedObjectsAreAuthorized(
theComments.saved_objects.map((comment) => ({

View file

@ -9,7 +9,7 @@ import type {
BulkCreateCommentRequest,
CommentPatchRequest,
CommentRequest,
FindQueryParams,
FindCommentsQueryParams,
} from '../../../common/api';
/**
@ -72,7 +72,7 @@ export interface DeleteArgs {
/**
* Parameters for finding attachments of a case
*/
export interface FindArgs {
export interface FindCommentsArgs {
/**
* The case ID for finding associated attachments
*/
@ -80,7 +80,7 @@ export interface FindArgs {
/**
* Optional parameters for filtering the returned attachments
*/
queryParams?: FindQueryParams;
queryParams?: FindCommentsQueryParams;
}
/**

View file

@ -12,7 +12,7 @@ import { pipe } from 'fp-ts/lib/pipeable';
import { fold } from 'fp-ts/lib/Either';
import { identity } from 'fp-ts/lib/function';
import { FindQueryParamsRt, throwErrors, excess } from '../../../../common/api';
import { FindCommentsQueryParamsRt, throwErrors, excess } from '../../../../common/api';
import { CASE_FIND_ATTACHMENTS_URL } from '../../../../common/constants';
import { createCasesRoute } from '../create_cases_route';
import { createCaseError } from '../../../common/error';
@ -28,7 +28,7 @@ export const findCommentsRoute = createCasesRoute({
handler: async ({ context, request, response }) => {
try {
const query = pipe(
excess(FindQueryParamsRt).decode(request.query),
excess(FindCommentsQueryParamsRt).decode(request.query),
fold(throwErrors(Boom.badRequest), identity)
);

View file

@ -36,6 +36,7 @@ import {
listFiles,
findAttachments,
bulkCreateAttachments,
getAllComments,
} from '../../../../common/lib/api';
import {
secOnly,
@ -516,18 +517,15 @@ export default ({ getService }: FtrProviderContext): void => {
},
auth: { user: superUser, space: 'space1' },
}),
findAttachments({
getAllComments({
supertest: supertestWithoutAuth,
caseId: postedCase.id,
query: {
perPage: MAX_DOCS_PER_PAGE,
},
auth: { user: secAllUser, space: 'space1' },
}),
]);
expect(filesAfterDelete.total).to.be(1);
expect(attachmentsAfterDelete.comments.length).to.be(1);
expect(attachmentsAfterDelete.length).to.be(1);
});
});

View file

@ -7,14 +7,20 @@
import expect from '@kbn/expect';
import { CASES_URL } from '@kbn/cases-plugin/common/constants';
import { CommentsFindResponse } from '@kbn/cases-plugin/common/api';
import {
CASES_URL,
INTERNAL_BULK_CREATE_ATTACHMENTS_URL,
} from '@kbn/cases-plugin/common/constants';
import { CommentType } from '@kbn/cases-plugin/common/api';
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
import {
getPostCaseRequest,
persistableStateAttachment,
postCaseReq,
postCommentActionsReq,
postCommentAlertReq,
postCommentUserReq,
postExternalReferenceESReq,
} from '../../../../common/lib/mock';
import {
createComment,
@ -25,7 +31,6 @@ import {
ensureSavedObjectIsAuthorized,
getSpaceUrlPrefix,
createCase,
superUserSpace1Auth,
findAttachments,
} from '../../../../common/lib/api';
@ -81,36 +86,37 @@ export default ({ getService }: FtrProviderContext): void => {
expect(caseComments.comments).to.eql(patchedCase.comments);
});
it('should filter case comments', async () => {
it('should find only case comments of the correct type', async () => {
const { body: postedCase } = await supertest
.post(CASES_URL)
.set('kbn-xsrf', 'true')
.send(postCaseReq)
.expect(200);
// post 2 comments
// post 5 comments of all possible types
await supertest
.post(`${CASES_URL}/${postedCase.id}/comments`)
.post(INTERNAL_BULK_CREATE_ATTACHMENTS_URL.replace('{case_id}', postedCase.id))
.set('kbn-xsrf', 'true')
.send(postCommentUserReq)
.expect(200);
const { body: patchedCase } = await supertest
.post(`${CASES_URL}/${postedCase.id}/comments`)
.set('kbn-xsrf', 'true')
.send({ ...postCommentUserReq, comment: 'unique' })
.send([
postCommentUserReq,
postCommentAlertReq,
postCommentActionsReq,
postExternalReferenceESReq,
persistableStateAttachment,
])
.expect(200);
const { body: caseComments } = await supertest
.get(`${CASES_URL}/${postedCase.id}/comments/_find?search=unique`)
.get(`${CASES_URL}/${postedCase.id}/comments/_find`)
.set('kbn-xsrf', 'true')
.send()
.expect(200);
expect(caseComments.comments).to.eql([patchedCase.comments[1]]);
expect(caseComments.comments.length).to.eql(1);
expect(caseComments.comments[0].type).to.eql(CommentType.user);
});
it('unhappy path - 400s when query is bad', async () => {
it('unhappy path - 400s when query is wrong type', async () => {
const { body: postedCase } = await supertest
.post(CASES_URL)
.set('kbn-xsrf', 'true')
@ -130,6 +136,46 @@ export default ({ getService }: FtrProviderContext): void => {
.expect(400);
});
it('unhappy path - 400s when field is unkown', async () => {
const { body: postedCase } = await supertest
.post(CASES_URL)
.set('kbn-xsrf', 'true')
.send(postCaseReq)
.expect(200);
await supertest
.post(`${CASES_URL}/${postedCase.id}/comments`)
.set('kbn-xsrf', 'true')
.send(postCommentUserReq)
.expect(200);
await supertest
.get(`${CASES_URL}/${postedCase.id}/comments/_find?foobar=true`)
.set('kbn-xsrf', 'true')
.send()
.expect(400);
});
it('unhappy path - 400s when total items invalid', async () => {
const { body: postedCase } = await supertest
.post(CASES_URL)
.set('kbn-xsrf', 'true')
.send(postCaseReq)
.expect(200);
await supertest
.post(`${CASES_URL}/${postedCase.id}/comments`)
.set('kbn-xsrf', 'true')
.send(postCommentUserReq)
.expect(200);
await supertest
.get(`${CASES_URL}/${postedCase.id}/comments/_find?page=2&perPage=9001`)
.set('kbn-xsrf', 'true')
.send()
.expect(400);
});
describe('rbac', () => {
const supertestWithoutAuth = getService('supertestWithoutAuth');
@ -167,7 +213,7 @@ export default ({ getService }: FtrProviderContext): void => {
createComment({
supertest: supertestWithoutAuth,
caseId: obsCase.id,
params: { ...postCommentAlertReq, owner: 'observabilityFixture' },
params: { ...postCommentUserReq, owner: 'observabilityFixture' },
auth: { user: obsOnly, space: space1 },
}),
]);
@ -266,60 +312,6 @@ export default ({ getService }: FtrProviderContext): void => {
});
}
it('should not return any comments when trying to exploit RBAC through the search query parameter', async () => {
const obsCase = await createCase(
supertestWithoutAuth,
getPostCaseRequest({ owner: 'observabilityFixture' }),
200,
superUserSpace1Auth
);
await createComment({
supertest: supertestWithoutAuth,
auth: superUserSpace1Auth,
params: { ...postCommentUserReq, owner: 'observabilityFixture' },
caseId: obsCase.id,
});
const { body: res }: { body: CommentsFindResponse } = await supertestWithoutAuth
.get(
`${getSpaceUrlPrefix('space1')}${CASES_URL}/${
obsCase.id
}/comments/_find?search=securitySolutionFixture+observabilityFixture`
)
.auth(secOnly.username, secOnly.password)
.expect(200);
// shouldn't find any comments since they were created under the observability ownership
ensureSavedObjectIsAuthorized(res.comments, 0, ['securitySolutionFixture']);
});
it('should not allow retrieving unauthorized comments using the filter field', async () => {
const obsCase = await createCase(
supertestWithoutAuth,
getPostCaseRequest({ owner: 'observabilityFixture' }),
200,
superUserSpace1Auth
);
await createComment({
supertest: supertestWithoutAuth,
auth: superUserSpace1Auth,
params: { ...postCommentUserReq, owner: 'observabilityFixture' },
caseId: obsCase.id,
});
const { body: res } = await supertestWithoutAuth
.get(
`${getSpaceUrlPrefix('space1')}${CASES_URL}/${
obsCase.id
}/comments/_find?filter=cases-comments.attributes.owner:"observabilityFixture"`
)
.auth(secOnly.username, secOnly.password)
.expect(200);
expect(res.comments.length).to.be(0);
});
// This test ensures that the user is not allowed to define the namespaces query param
// so she cannot search across spaces
it('should NOT allow to pass a namespaces query parameter', async () => {