Skip to content

Commit

Permalink
[Cases] Delete Cases API Guardrails (#160846)
Browse files Browse the repository at this point in the history
Connected to #146945

## Summary

| Description  | Limit | Done? | Documented?
| ------------- | ---- | :---: | ---- |
| Total number of cases to be deleted | 100 | ✅ | Yes |

- Used schema validation.
- Updated documentation.
- Added jest and e2e tests.

### Checklist

Delete any items that are not applicable to this PR.

- [x]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

### Release notes

The Delete cases API now limits the number of cases to be deleted to
100.
  • Loading branch information
adcoelho authored Jun 30, 2023
1 parent 646539c commit b12238b
Show file tree
Hide file tree
Showing 9 changed files with 73 additions and 15 deletions.
11 changes: 10 additions & 1 deletion x-pack/plugins/cases/common/api/cases/case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ import { CommentRt } from './comment';
import { CasesStatusResponseRt, CaseStatusRt } from './status';
import { CaseConnectorRt } from '../connectors/connector';
import { CaseAssigneesRt } from './assignee';
import { limitedArraySchema, NonEmptyString } from '../../schema';
import {
MAX_DELETE_IDS_LENGTH,
MAX_ASSIGNEES_FILTER_LENGTH,
MAX_REPORTERS_FILTER_LENGTH,
MAX_TAGS_FILTER_LENGTH,
} from '../../constants';
import { limitedArraySchema } from '../../schema';

export const AttachmentTotalsRt = rt.strict({
alerts: rt.number,
Expand Down Expand Up @@ -295,6 +296,13 @@ export const CasesFindRequestRt = rt.exact(
})
);

export const CasesDeleteRequestRt = limitedArraySchema(
NonEmptyString,
1,
MAX_DELETE_IDS_LENGTH,
'ids'
);

export const CasesByAlertIDRequestRt = rt.exact(
rt.partial({
/**
Expand Down Expand Up @@ -432,6 +440,7 @@ export type CasePostRequest = rt.TypeOf<typeof CasePostRequestRt>;
export type Case = rt.TypeOf<typeof CaseRt>;
export type CaseResolveResponse = rt.TypeOf<typeof CaseResolveResponseRt>;
export type Cases = rt.TypeOf<typeof CasesRt>;
export type CasesDeleteRequest = rt.TypeOf<typeof CasesDeleteRequestRt>;
export type CasesFindRequest = rt.TypeOf<typeof CasesFindRequestRt>;
export type CasesByAlertIDRequest = rt.TypeOf<typeof CasesByAlertIDRequestRt>;
export type CasesFindResponse = rt.TypeOf<typeof CasesFindResponseRt>;
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/cases/common/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export const MAX_REPORTERS_FILTER_LENGTH = 100 as const;

export const MAX_TITLE_LENGTH = 160 as const;
export const MAX_CATEGORY_LENGTH = 50 as const;
export const MAX_DELETE_IDS_LENGTH = 100 as const;

/**
* Cases features
Expand Down
7 changes: 6 additions & 1 deletion x-pack/plugins/cases/docs/openapi/bundled.json
Original file line number Diff line number Diff line change
Expand Up @@ -3809,7 +3809,12 @@
"in": "query",
"required": true,
"schema": {
"type": "string"
"type": "array",
"items": {
"type": "string",
"minItems": 1,
"maxItems": 100
}
},
"example": "d4e7abb0-b462-11ec-9a8d-698504725a43"
},
Expand Down
6 changes: 5 additions & 1 deletion x-pack/plugins/cases/docs/openapi/bundled.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2310,7 +2310,11 @@ components:
in: query
required: true
schema:
type: string
type: array
items:
type: string
minItems: 1
maxItems: 100
example: d4e7abb0-b462-11ec-9a8d-698504725a43
assignees:
in: query
Expand Down
11 changes: 6 additions & 5 deletions x-pack/plugins/cases/docs/openapi/components/parameters/ids.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ description: >
in: query
required: true
schema:
type: string
example: d4e7abb0-b462-11ec-9a8d-698504725a43



type: array
items:
type: string
minItems: 1
maxItems: 100
example: d4e7abb0-b462-11ec-9a8d-698504725a43
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ post:
'200':
description: Indicates a successful call.
content:
application/json:
application/json:
schema:
$ref: '../components/schemas/case_response_properties.yaml'
examples:
Expand All @@ -36,7 +36,7 @@ post:
schema:
$ref: '../components/schemas/4xx_response.yaml'
servers:
- url: https://localhost:5601
- url: https://localhost:5601

delete:
summary: Deletes one or more cases.
Expand Down Expand Up @@ -103,7 +103,7 @@ patch:
schema:
$ref: '../components/schemas/4xx_response.yaml'
servers:
- url: https://localhost:5601
- url: https://localhost:5601

servers:
- url: https://localhost:5601
- url: https://localhost:5601
18 changes: 17 additions & 1 deletion x-pack/plugins/cases/server/client/cases/delete.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { MAX_FILES_PER_CASE } from '../../../common/constants';
import { MAX_DELETE_IDS_LENGTH, MAX_FILES_PER_CASE } from '../../../common/constants';
import type { FindFileArgs } from '@kbn/files-plugin/server';
import { createFileServiceMock } from '@kbn/files-plugin/server/mocks';
import type { FileJSON } from '@kbn/shared-ux-file-types';
Expand Down Expand Up @@ -95,6 +95,22 @@ describe('delete', () => {
});
});
});

describe('errors', () => {
it(`throws 400 when trying to delete more than ${MAX_DELETE_IDS_LENGTH} cases at a time`, async () => {
const caseIds = new Array(MAX_DELETE_IDS_LENGTH + 1).fill('id');

await expect(deleteCases(caseIds, clientArgs)).rejects.toThrowError(
'Error: The length of the field ids is too long. Array must be of length <= 100.'
);
});

it('throws 400 when no id is passed to delete', async () => {
await expect(deleteCases([], clientArgs)).rejects.toThrowError(
'Error: The length of the field ids is too short. Array must be of length >= 1.'
);
});
});
});
});

Expand Down
10 changes: 8 additions & 2 deletions x-pack/plugins/cases/server/client/cases/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import pMap from 'p-map';
import { chunk } from 'lodash';
import type { SavedObjectsBulkDeleteObject } from '@kbn/core/server';
import type { FileServiceStart } from '@kbn/files-plugin/server';
import type { CasesDeleteRequest } from '../../../common/api';
import { CasesDeleteRequestRt, decodeWithExcessOrThrow } from '../../../common/api';
import {
CASE_COMMENT_SAVED_OBJECT,
CASE_SAVED_OBJECT,
Expand All @@ -26,7 +28,10 @@ import { createFileEntities, deleteFiles } from '../files';
/**
* Deletes the specified cases and their attachments.
*/
export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): Promise<void> {
export async function deleteCases(
ids: CasesDeleteRequest,
clientArgs: CasesClientArgs
): Promise<void> {
const {
services: { caseService, attachmentService, userActionService, alertsService },
logger,
Expand All @@ -35,7 +40,8 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P
} = clientArgs;

try {
const cases = await caseService.getCases({ caseIds: ids });
const caseIds = decodeWithExcessOrThrow(CasesDeleteRequestRt)(ids);
const cases = await caseService.getCases({ caseIds });
const entities = new Map<string, OwnerEntity>();

for (const theCase of cases.saved_objects) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,22 @@ export default ({ getService }: FtrProviderContext): void => {
await deleteCases({ supertest, caseIDs: ['fake-id'], expectedHttpCode: 404 });
});

it('unhappy path - 400s when trying to delete more than 100 cases at a time', async () => {
await deleteCases({
supertest: supertestWithoutAuth,
caseIDs: new Array(101).fill('id'),
expectedHttpCode: 400,
});
});

it('unhappy path - 400s when trying to delete 0 cases at a time', async () => {
await deleteCases({
supertest: supertestWithoutAuth,
caseIDs: [],
expectedHttpCode: 400,
});
});

describe('files', () => {
afterEach(async () => {
await deleteAllFiles({
Expand Down

0 comments on commit b12238b

Please sign in to comment.