-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Cases] Registering file attachment type (#152399)
This PR registers the file attachment type `.files` within the cases attachment framework. Notable changes: - Attachment framework accepts an optional validation function for use when a request is received for that attachment type, this allows validating that the file metadata is correct - Refactored the logic for enforcing that only 1000 alerts can be attached to a case and 100 files Issue: #151933 <details><summary>cUrl request to create file attachments</summary> ``` curl --location --request POST 'http://elastic:changeme@localhost:5601/internal/cases/<case id>/attachments/_bulk_create' \ --header 'kbn-xsrf: hello' \ --header 'Content-Type: application/json' \ --data-raw '[ { "type": "externalReference", "externalReferenceStorage": { "type": "savedObject", "soType": "file" }, "externalReferenceId": "my-id", "externalReferenceAttachmentTypeId": ".files", "externalReferenceMetadata": { "file": { "name": "test_file", "extension": "png", "mimeType": "image/png", "createdAt": "2023-02-27T20:26:54.345Z" } }, "owner": "cases" }, { "type": "externalReference", "externalReferenceStorage": { "type": "savedObject", "soType": "file" }, "externalReferenceId": "my-id", "externalReferenceAttachmentTypeId": ".files", "externalReferenceMetadata": { "file": { "name": "test_file", "extension": "png", "mimeType": "image/png", "createdAt": "2023-02-27T20:26:54.345Z" } }, "owner": "cases" }, { "type": "externalReference", "externalReferenceStorage": { "type": "savedObject", "soType": "file" }, "externalReferenceId": "my-id", "externalReferenceAttachmentTypeId": ".files", "externalReferenceMetadata": { "file": { "name": "test_file", "extension": "jpeg", "mimeType": "image/jpeg", "createdAt": "2023-02-27T20:26:54.345Z" } }, "owner": "cases" } ] ' ``` </details>
- Loading branch information
1 parent
ccb1db8
commit 94fbed0
Showing
29 changed files
with
1,192 additions
and
75 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0; you may not use this file except in compliance with the Elastic License | ||
* 2.0. | ||
*/ | ||
|
||
import * as rt from 'io-ts'; | ||
|
||
export const FileAttachmentMetadataRt = rt.type({ | ||
files: rt.array( | ||
rt.type({ | ||
name: rt.string, | ||
extension: rt.string, | ||
mimeType: rt.string, | ||
createdAt: rt.string, | ||
}) | ||
), | ||
}); | ||
|
||
export type FileAttachmentMetadata = rt.TypeOf<typeof FileAttachmentMetadataRt>; | ||
|
||
export const FILE_ATTACHMENT_TYPE = '.files'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
65 changes: 65 additions & 0 deletions
65
x-pack/plugins/cases/server/common/limiter_checker/base_limiter.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
/* | ||
* 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 * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; | ||
import type { KueryNode } from '@kbn/es-query'; | ||
import { CASE_COMMENT_SAVED_OBJECT } from '../../../common/constants'; | ||
import type { CommentRequest, CommentType } from '../../../common/api'; | ||
import type { AttachmentService } from '../../services'; | ||
import type { Limiter } from './types'; | ||
|
||
interface LimiterParams { | ||
limit: number; | ||
attachmentType: CommentType; | ||
field: string; | ||
attachmentNoun: string; | ||
filter?: KueryNode; | ||
} | ||
|
||
export abstract class BaseLimiter implements Limiter { | ||
public readonly limit: number; | ||
public readonly errorMessage: string; | ||
|
||
private readonly limitAggregation: Record<string, estypes.AggregationsAggregationContainer>; | ||
private readonly params: LimiterParams; | ||
|
||
constructor(params: LimiterParams) { | ||
this.params = params; | ||
this.limit = params.limit; | ||
this.errorMessage = makeErrorMessage(this.limit, params.attachmentNoun); | ||
|
||
this.limitAggregation = { | ||
limiter: { | ||
value_count: { | ||
field: `${CASE_COMMENT_SAVED_OBJECT}.attributes.${params.field}`, | ||
}, | ||
}, | ||
}; | ||
} | ||
|
||
public async countOfItemsWithinCase( | ||
attachmentService: AttachmentService, | ||
caseId: string | ||
): Promise<number> { | ||
const itemsAttachedToCase = await attachmentService.executeCaseAggregations<{ | ||
limiter: { value: number }; | ||
}>({ | ||
caseId, | ||
aggregations: this.limitAggregation, | ||
attachmentType: this.params.attachmentType, | ||
filter: this.params.filter, | ||
}); | ||
|
||
return itemsAttachedToCase?.limiter?.value ?? 0; | ||
} | ||
|
||
public abstract countOfItemsInRequest(requests: CommentRequest[]): number; | ||
} | ||
|
||
const makeErrorMessage = (limit: number, noun: string) => { | ||
return `Case has reached the maximum allowed number (${limit}) of attached ${noun}.`; | ||
}; |
142 changes: 142 additions & 0 deletions
142
x-pack/plugins/cases/server/common/limiter_checker/index.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
/* | ||
* 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 { AttachmentLimitChecker } from '.'; | ||
import { createAttachmentServiceMock } from '../../services/mocks'; | ||
import { createAlertRequests, createFileRequests, createUserRequests } from './test_utils'; | ||
|
||
describe('AttachmentLimitChecker', () => { | ||
const mockAttachmentService = createAttachmentServiceMock(); | ||
const checker = new AttachmentLimitChecker(mockAttachmentService, 'id'); | ||
|
||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
|
||
mockAttachmentService.executeCaseAggregations.mockImplementation(async () => { | ||
return { | ||
limiter: { | ||
value: 5, | ||
}, | ||
}; | ||
}); | ||
}); | ||
|
||
describe('validate', () => { | ||
it('does not throw when called with an empty array', async () => { | ||
expect.assertions(1); | ||
|
||
await expect(checker.validate([])).resolves.not.toThrow(); | ||
}); | ||
|
||
it('does not throw when none of the requests are alerts or files', async () => { | ||
expect.assertions(1); | ||
|
||
await expect(checker.validate(createUserRequests(2))).resolves.not.toThrow(); | ||
}); | ||
|
||
describe('files', () => { | ||
it('throws when the files in a single request are greater than 100', async () => { | ||
expect.assertions(1); | ||
|
||
await expect( | ||
checker.validate(createFileRequests({ numRequests: 1, numFiles: 101 })) | ||
).rejects.toMatchInlineSnapshot( | ||
`[Error: Case has reached the maximum allowed number (100) of attached files.]` | ||
); | ||
}); | ||
|
||
it('throws when there are 101 requests with a single file', async () => { | ||
expect.assertions(1); | ||
|
||
await expect( | ||
checker.validate(createFileRequests({ numRequests: 101, numFiles: 1 })) | ||
).rejects.toMatchInlineSnapshot( | ||
`[Error: Case has reached the maximum allowed number (100) of attached files.]` | ||
); | ||
}); | ||
|
||
it('does not throw when the sum of the file requests and files within the case are only 100', async () => { | ||
expect.assertions(1); | ||
|
||
await expect( | ||
checker.validate(createFileRequests({ numFiles: 1, numRequests: 95 })) | ||
).resolves.not.toThrow(); | ||
}); | ||
|
||
it('throws when the sum of the file requests and files within the case are 101', async () => { | ||
expect.assertions(1); | ||
|
||
await expect( | ||
checker.validate(createFileRequests({ numFiles: 1, numRequests: 96 })) | ||
).rejects.toMatchInlineSnapshot( | ||
`[Error: Case has reached the maximum allowed number (100) of attached files.]` | ||
); | ||
}); | ||
|
||
it('throws when the sum of the file requests and files within the case are 101 with a single file request', async () => { | ||
expect.assertions(1); | ||
|
||
await expect( | ||
checker.validate(createFileRequests({ numFiles: 96, numRequests: 1 })) | ||
).rejects.toMatchInlineSnapshot( | ||
`[Error: Case has reached the maximum allowed number (100) of attached files.]` | ||
); | ||
}); | ||
}); | ||
|
||
describe('alerts', () => { | ||
it('throws when the alerts in a single request are greater than 1000', async () => { | ||
expect.assertions(1); | ||
|
||
const alertIds = [...Array(1001).keys()].map((key) => `${key}`); | ||
await expect( | ||
checker.validate(createAlertRequests(1, alertIds)) | ||
).rejects.toMatchInlineSnapshot( | ||
`[Error: Case has reached the maximum allowed number (1000) of attached alerts.]` | ||
); | ||
}); | ||
|
||
it('throws when there are 1001 requests with a single alert id', async () => { | ||
expect.assertions(1); | ||
|
||
await expect( | ||
checker.validate(createAlertRequests(1001, 'alertId')) | ||
).rejects.toMatchInlineSnapshot( | ||
`[Error: Case has reached the maximum allowed number (1000) of attached alerts.]` | ||
); | ||
}); | ||
|
||
it('does not throw when the sum of the alert requests and alerts within the case are only 1000', async () => { | ||
expect.assertions(1); | ||
|
||
await expect(checker.validate(createAlertRequests(995, 'alertId'))).resolves.not.toThrow(); | ||
}); | ||
|
||
it('throws when the sum of the alert requests and alerts within the case are 1001', async () => { | ||
expect.assertions(1); | ||
|
||
await expect( | ||
checker.validate(createAlertRequests(996, 'alertId')) | ||
).rejects.toMatchInlineSnapshot( | ||
`[Error: Case has reached the maximum allowed number (1000) of attached alerts.]` | ||
); | ||
}); | ||
|
||
it('throws when the sum of the alert requests and alerts within the case are 1001 with a single request', async () => { | ||
expect.assertions(1); | ||
|
||
const alertIds = [...Array(996).keys()].map((key) => `${key}`); | ||
|
||
await expect( | ||
checker.validate(createAlertRequests(1, alertIds)) | ||
).rejects.toMatchInlineSnapshot( | ||
`[Error: Case has reached the maximum allowed number (1000) of attached alerts.]` | ||
); | ||
}); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.