Skip to content

Commit

Permalink
[ResponseOps] [Cases] Attach file to case API (#198377)
Browse files Browse the repository at this point in the history
Fixes #22832

## Summary

This PR adds the possibility of adding Files/Attachments to Case in
Kibana via an API call.

### How to test

The new API URL is `https://localhost:5601/api/cases/<CASE_ID>/files`.
You can either use postman or curl to test.

1. Start by creating a case.
2. Call the new API
```
curl --location 'https://localhost:5601/api/cases/<CASE_ID>/files' \
--header 'kbn-xsrf: true' \
--header 'Authorization: Basic ZWxhc3RpYzpjaGFuZ2VtZQ==' \
--form 'filename="Notice"' \
--form 'mimeType="text/plain"' \
--form 'file=@"<FULL_PATH_TO_THE_FILE_YOU_WANT_TO_UPLOAD>"'
```
<img width="1090" alt="Screenshot 2024-10-30 at 15 41 26"
src="https://github.com/user-attachments/assets/b018f92d-2603-4bf1-ac12-f01452f35303">

3. Confirm the user action was created.
<img width="383" alt="Screenshot 2024-10-30 at 15 48 45"
src="https://github.com/user-attachments/assets/04952b8f-e8fb-4f19-a72f-54030f496fe9">

4. Confirm the file exists in the case and:
    - it can be downloaded as expected.
    - it can be previewed as expected(not every MIME type allows this).


### Release Notes

Files can now be attached to cases directly via API.

---------

Co-authored-by: kibanamachine <[email protected]>
Co-authored-by: lcawl <[email protected]>
  • Loading branch information
3 people authored Nov 14, 2024
1 parent 20953fc commit e2702ff
Show file tree
Hide file tree
Showing 29 changed files with 1,254 additions and 42 deletions.
59 changes: 59 additions & 0 deletions oas_docs/output/kibana.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6706,6 +6706,46 @@ paths:
summary: Push a case to an external service
tags:
- cases
/api/cases/{caseId}/files:
post:
description: >
Attach a file to a case. You must have `all` 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 updating. The request must include:

- The `Content-Type: multipart/form-data` HTTP header.

- The location of the file that is being uploaded.
operationId: addCaseFileDefaultSpace
parameters:
- $ref: '#/components/parameters/Cases_kbn_xsrf'
- $ref: '#/components/parameters/Cases_case_id'
requestBody:
content:
multipart/form-data; Elastic-Api-Version=2023-10-31:
schema:
$ref: '#/components/schemas/Cases_add_case_file_request'
required: true
responses:
'200':
content:
application/json; Elastic-Api-Version=2023-10-31:
examples:
addCaseFileResponse:
$ref: '#/components/examples/Cases_add_comment_response'
schema:
$ref: '#/components/schemas/Cases_case_response_properties'
description: Indicates a successful call.
'401':
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
$ref: '#/components/schemas/Cases_4xx_response'
description: Authorization information is missing or invalid.
summary: Attach a file to a case
tags:
- cases
/api/cases/{caseId}/user_actions:
get:
deprecated: true
Expand Down Expand Up @@ -43674,6 +43714,25 @@ components:
- $ref: '#/components/schemas/Cases_add_alert_comment_request_properties'
- $ref: '#/components/schemas/Cases_add_user_comment_request_properties'
title: Add case comment request
Cases_add_case_file_request:
description: >-
Defines the file that will be attached to the case. Optional parameters
will be generated automatically from the file metadata if not defined.
type: object
properties:
file:
description: The file being attached to the case.
format: binary
type: string
filename:
description: >-
The desired name of the file being attached to the case, it can be
different than the name of the file in the filesystem. **This should
not include the file extension.**
type: string
required:
- file
title: Add case file request properties
Cases_add_user_comment_request_properties:
description: Defines properties for case comment requests when type is user.
properties:
Expand Down
3 changes: 3 additions & 0 deletions x-pack/plugins/cases/common/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ export const CASE_FIND_USER_ACTIONS_URL = `${CASE_USER_ACTIONS_URL}/_find` as co
export const CASE_ALERTS_URL = `${CASES_URL}/alerts/{alert_id}` as const;
export const CASE_DETAILS_ALERTS_URL = `${CASE_DETAILS_URL}/alerts` as const;

export const CASE_FILES_URL = `${CASE_DETAILS_URL}/files` as const;

/**
* Internal routes
*/
Expand Down Expand Up @@ -139,6 +141,7 @@ export const MAX_TEMPLATE_DESCRIPTION_LENGTH = 1000 as const;
export const MAX_TEMPLATES_LENGTH = 10 as const;
export const MAX_TEMPLATE_TAG_LENGTH = 50 as const;
export const MAX_TAGS_PER_TEMPLATE = 10 as const;
export const MAX_FILENAME_LENGTH = 160 as const;

/**
* Cases features
Expand Down
27 changes: 23 additions & 4 deletions x-pack/plugins/cases/common/schema/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
limitedArraySchema,
limitedNumberSchema,
limitedStringSchema,
mimeTypeString,
NonEmptyString,
paginationSchema,
limitedNumberAsIntegerSchema,
Expand Down Expand Up @@ -321,14 +322,32 @@ describe('schema', () => {
});
});

describe('mimeTypeString', () => {
it('works correctly when the value is an allowed mime type', () => {
expect(PathReporter.report(mimeTypeString.decode('image/jpx'))).toMatchInlineSnapshot(`
Array [
"No errors!",
]
`);
});

it('fails when the value is not an allowed mime type', () => {
expect(PathReporter.report(mimeTypeString.decode('foo/bar'))).toMatchInlineSnapshot(`
Array [
"The mime type field value foo/bar is not allowed.",
]
`);
});
});

describe('limitedNumberAsIntegerSchema', () => {
it('works correctly the number is safe integer', () => {
expect(PathReporter.report(limitedNumberAsIntegerSchema({ fieldName: 'foo' }).decode(1)))
.toMatchInlineSnapshot(`
Array [
"No errors!",
]
`);
Array [
"No errors!",
]
`);
});

it('fails when given a number that is lower than the minimum', () => {
Expand Down
15 changes: 15 additions & 0 deletions x-pack/plugins/cases/common/schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { either } from 'fp-ts/lib/Either';
import { MAX_DOCS_PER_PAGE } from '../constants';
import type { PartialPaginationType } from './types';
import { PaginationSchemaRt } from './types';
import { ALLOWED_MIME_TYPES } from '../constants/mime_types';

export interface LimitedSchemaType {
fieldName: string;
Expand Down Expand Up @@ -194,3 +195,17 @@ export const regexStringRt = ({ codec, pattern, message }: RegexStringSchemaType
}),
rt.identity
);

export const mimeTypeString = new rt.Type<string, string, unknown>(
'mimeTypeString',
rt.string.is,
(input, context) =>
either.chain(rt.string.validate(input, context), (s) => {
if (!ALLOWED_MIME_TYPES.includes(s)) {
return rt.failure(input, context, `The mime type field value ${s} is not allowed.`);
}

return rt.success(s);
}),
rt.identity
);
52 changes: 51 additions & 1 deletion x-pack/plugins/cases/common/types/api/attachment/v1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
*/

import { PathReporter } from 'io-ts/lib/PathReporter';
import { MAX_BULK_CREATE_ATTACHMENTS, MAX_COMMENT_LENGTH } from '../../../constants';
import {
MAX_BULK_CREATE_ATTACHMENTS,
MAX_COMMENT_LENGTH,
MAX_FILENAME_LENGTH,
} from '../../../constants';
import { AttachmentType } from '../../domain/attachment/v1';
import {
AttachmentPatchRequestRt,
Expand All @@ -17,6 +21,7 @@ import {
BulkGetAttachmentsRequestRt,
BulkGetAttachmentsResponseRt,
FindAttachmentsQueryParamsRt,
PostFileAttachmentRequestRt,
} from './v1';

describe('Attachments', () => {
Expand Down Expand Up @@ -389,4 +394,49 @@ describe('Attachments', () => {
});
});
});

describe('PostFileAttachmentRequestRt', () => {
const defaultRequest = {
file: 'Solve this fast!',
filename: 'filename',
};

it('has the expected attributes in request', () => {
const query = PostFileAttachmentRequestRt.decode(defaultRequest);

expect(query).toStrictEqual({
_tag: 'Right',
right: defaultRequest,
});
});

it('removes foo:bar attributes from request', () => {
const query = PostFileAttachmentRequestRt.decode({ ...defaultRequest, foo: 'bar' });

expect(query).toStrictEqual({
_tag: 'Right',
right: defaultRequest,
});
});

describe('errors', () => {
it('throws an error when the filename is too long', () => {
const longFilename = 'x'.repeat(MAX_FILENAME_LENGTH + 1);

expect(
PathReporter.report(
PostFileAttachmentRequestRt.decode({ ...defaultRequest, filename: longFilename })
)
).toContain('The length of the filename is too long. The maximum length is 160.');
});

it('throws an error when the filename is too small', () => {
expect(
PathReporter.report(
PostFileAttachmentRequestRt.decode({ ...defaultRequest, filename: '' })
)
).toContain('The filename field cannot be an empty string.');
});
});
});
});
17 changes: 17 additions & 0 deletions x-pack/plugins/cases/common/types/api/attachment/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
MAX_COMMENTS_PER_PAGE,
MAX_COMMENT_LENGTH,
MAX_DELETE_FILES,
MAX_FILENAME_LENGTH,
} from '../../../constants';
import {
limitedArraySchema,
Expand Down Expand Up @@ -47,7 +48,23 @@ export const BulkDeleteFileAttachmentsRequestRt = rt.strict({
}),
});

export const PostFileAttachmentRequestRt = rt.intersection([
rt.strict({
file: rt.unknown,
}),
rt.exact(
rt.partial({
filename: limitedStringSchema({ fieldName: 'filename', min: 1, max: MAX_FILENAME_LENGTH }),
})
),
]);

export type BulkDeleteFileAttachmentsRequest = rt.TypeOf<typeof BulkDeleteFileAttachmentsRequestRt>;
export type PostFileAttachmentRequest = rt.TypeOf<typeof PostFileAttachmentRequestRt>;

/**
* Attachments
*/

const BasicAttachmentRequestRt = rt.union([
UserCommentAttachmentPayloadRt,
Expand Down
7 changes: 7 additions & 0 deletions x-pack/plugins/cases/common/types/domain/attachment/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
*/

import * as rt from 'io-ts';
import { limitedStringSchema, mimeTypeString } from '../../../schema';
import { jsonValueRt } from '../../../api';
import { UserRt } from '../user/v1';
import { MAX_FILENAME_LENGTH } from '../../../constants';

/**
* Files
Expand Down Expand Up @@ -35,6 +37,11 @@ export const AttachmentAttributesBasicRt = rt.strict({
updated_by: rt.union([UserRt, rt.null]),
});

export const FileAttachmentMetadataPayloadRt = rt.strict({
mimeType: mimeTypeString,
filename: limitedStringSchema({ fieldName: 'filename', min: 1, max: MAX_FILENAME_LENGTH }),
});

/**
* User comment
*/
Expand Down
74 changes: 74 additions & 0 deletions x-pack/plugins/cases/docs/openapi/bundled.json
Original file line number Diff line number Diff line change
Expand Up @@ -1869,6 +1869,61 @@
}
}
}
},
"/api/cases/{caseId}/files": {
"post": {
"summary": "Attach a file to a case",
"operationId": "addCaseFileDefaultSpace",
"description": "Attach a file to a case. You must have `all` 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 updating. The request must include:\n- The `Content-Type: multipart/form-data` HTTP header.\n- The location of the file that is being uploaded.\n",
"tags": [
"cases"
],
"parameters": [
{
"$ref": "#/components/parameters/kbn_xsrf"
},
{
"$ref": "#/components/parameters/case_id"
}
],
"requestBody": {
"required": true,
"content": {
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/add_case_file_request"
}
}
}
},
"responses": {
"200": {
"description": "Indicates a successful call.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/case_response_properties"
},
"examples": {
"addCaseFileResponse": {
"$ref": "#/components/examples/add_comment_response"
}
}
}
}
},
"401": {
"description": "Authorization information is missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/4xx_response"
}
}
}
}
}
}
}
},
"components": {
Expand Down Expand Up @@ -4798,6 +4853,25 @@
"example": "create_case"
}
}
},
"add_case_file_request": {
"title": "Add case file request properties",
"required": [
"file"
],
"description": "Defines the file that will be attached to the case. Optional parameters will be generated automatically from the file metadata if not defined.",
"type": "object",
"properties": {
"file": {
"description": "The file being attached to the case.",
"type": "string",
"format": "binary"
},
"filename": {
"description": "The desired name of the file being attached to the case, it can be different than the name of the file in the filesystem. **This should not include the file extension.**",
"type": "string"
}
}
}
},
"examples": {
Expand Down
Loading

0 comments on commit e2702ff

Please sign in to comment.