mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Cases] Total external references and persistable state attachments per case (#162071)
Connected to https://github.com/elastic/kibana/issues/146945 ## Summary | Description | Limit | Done? | Documented? | ------------- | ---- | :---: | ---- | | Total number of attachments (external references and persistable state) per case | 100 | ✅ | No | ### 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 A case can now only have 100 external references and persistable state(excluding files) attachments combined.
This commit is contained in:
parent
c76b185323
commit
7429c824bf
15 changed files with 700 additions and 96 deletions
|
@ -127,6 +127,7 @@ export const MAX_DELETE_IDS_LENGTH = 100 as const;
|
|||
export const MAX_SUGGESTED_PROFILES = 10 as const;
|
||||
export const MAX_CASES_TO_UPDATE = 100 as const;
|
||||
export const MAX_BULK_CREATE_ATTACHMENTS = 100 as const;
|
||||
export const MAX_PERSISTABLE_STATE_AND_EXTERNAL_REFERENCES = 100 as const;
|
||||
|
||||
/**
|
||||
* Cases features
|
||||
|
|
|
@ -9,7 +9,7 @@ import type { CaseUserActionsDeprecatedResponse } from '../../../common/types/ap
|
|||
import { ConnectorTypes, UserActionActions } from '../../../common/types/domain';
|
||||
import type { Comment, CommentResponseAlertsType } from '../../../common/api';
|
||||
import { CommentType, ExternalReferenceStorageType } from '../../../common/api';
|
||||
import { SECURITY_SOLUTION_OWNER } from '../../../common/constants';
|
||||
import { FILE_ATTACHMENT_TYPE, SECURITY_SOLUTION_OWNER } from '../../../common/constants';
|
||||
|
||||
export const updateUser = {
|
||||
updated_at: '2020-03-13T08:34:53.450Z',
|
||||
|
@ -228,6 +228,16 @@ export const commentPersistableState: Comment = {
|
|||
version: 'WzEsMV0=',
|
||||
};
|
||||
|
||||
export const commentFileExternalReference: Comment = {
|
||||
...commentExternalReference,
|
||||
externalReferenceAttachmentTypeId: FILE_ATTACHMENT_TYPE,
|
||||
externalReferenceMetadata: { files: [{ name: '', extension: '', mimeType: '', created: '' }] },
|
||||
externalReferenceStorage: {
|
||||
type: ExternalReferenceStorageType.savedObject as const,
|
||||
soType: 'file',
|
||||
},
|
||||
};
|
||||
|
||||
export const basicParams = {
|
||||
description: 'a description',
|
||||
title: 'a title',
|
||||
|
|
|
@ -10,8 +10,8 @@ import type { Limiter } from './types';
|
|||
|
||||
interface LimiterParams {
|
||||
limit: number;
|
||||
attachmentType: CommentType;
|
||||
field: string;
|
||||
attachmentType: CommentType | CommentType[];
|
||||
field?: string;
|
||||
attachmentNoun: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import type { AttachmentService } from '../../services';
|
|||
import type { Limiter } from './types';
|
||||
import { AlertLimiter } from './limiters/alerts';
|
||||
import { FileLimiter } from './limiters/files';
|
||||
import { PersistableStateAndExternalReferencesLimiter } from './limiters/persistable_state_and_external_references';
|
||||
|
||||
export class AttachmentLimitChecker {
|
||||
private readonly limiters: Limiter[];
|
||||
|
@ -22,7 +23,11 @@ export class AttachmentLimitChecker {
|
|||
fileService: FileServiceStart,
|
||||
private readonly caseId: string
|
||||
) {
|
||||
this.limiters = [new AlertLimiter(attachmentService), new FileLimiter(fileService)];
|
||||
this.limiters = [
|
||||
new AlertLimiter(attachmentService),
|
||||
new FileLimiter(fileService),
|
||||
new PersistableStateAndExternalReferencesLimiter(attachmentService),
|
||||
];
|
||||
}
|
||||
|
||||
public async validate(requests: CommentRequest[]) {
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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 { createAttachmentServiceMock } from '../../../services/mocks';
|
||||
import { PersistableStateAndExternalReferencesLimiter } from './persistable_state_and_external_references';
|
||||
import {
|
||||
createExternalReferenceRequests,
|
||||
createFileRequests,
|
||||
createPersistableStateRequests,
|
||||
createUserRequests,
|
||||
} from '../test_utils';
|
||||
import { MAX_PERSISTABLE_STATE_AND_EXTERNAL_REFERENCES } from '../../../../common/constants';
|
||||
|
||||
describe('PersistableStateAndExternalReferencesLimiter', () => {
|
||||
const caseId = 'test-id';
|
||||
const attachmentService = createAttachmentServiceMock();
|
||||
attachmentService.countPersistableStateAndExternalReferenceAttachments.mockResolvedValue(1);
|
||||
|
||||
const limiter = new PersistableStateAndExternalReferencesLimiter(attachmentService);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('public fields', () => {
|
||||
it('sets the errorMessage to the 100 limit', () => {
|
||||
expect(limiter.errorMessage).toMatchInlineSnapshot(
|
||||
`"Case has reached the maximum allowed number (100) of attached persistable state and external reference attachments."`
|
||||
);
|
||||
});
|
||||
|
||||
it('sets the limit to 100', () => {
|
||||
expect(limiter.limit).toBe(MAX_PERSISTABLE_STATE_AND_EXTERNAL_REFERENCES);
|
||||
});
|
||||
});
|
||||
|
||||
describe('countOfItemsWithinCase', () => {
|
||||
it('calls the attachment service with the right params', () => {
|
||||
limiter.countOfItemsWithinCase(caseId);
|
||||
|
||||
expect(
|
||||
attachmentService.countPersistableStateAndExternalReferenceAttachments
|
||||
).toHaveBeenCalledWith({ caseId });
|
||||
});
|
||||
});
|
||||
|
||||
describe('countOfItemsInRequest', () => {
|
||||
it('returns 0 when passed an empty array', () => {
|
||||
expect(limiter.countOfItemsInRequest([])).toBe(0);
|
||||
});
|
||||
|
||||
it('returns 0 when the requests are not for persistable state attachments or external references', () => {
|
||||
expect(limiter.countOfItemsInRequest(createUserRequests(2))).toBe(0);
|
||||
});
|
||||
|
||||
it('counts persistable state attachments or external references correctly', () => {
|
||||
expect(
|
||||
limiter.countOfItemsInRequest([
|
||||
createPersistableStateRequests(1)[0],
|
||||
createExternalReferenceRequests(1)[0],
|
||||
createUserRequests(1)[0],
|
||||
createFileRequests({
|
||||
numRequests: 1,
|
||||
numFiles: 1,
|
||||
})[0],
|
||||
])
|
||||
).toBe(2);
|
||||
});
|
||||
|
||||
it('excludes fileAttachmentsRequests from the count', () => {
|
||||
expect(
|
||||
limiter.countOfItemsInRequest(
|
||||
createFileRequests({
|
||||
numRequests: 1,
|
||||
numFiles: 1,
|
||||
})
|
||||
)
|
||||
).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 { AttachmentService } from '../../../services';
|
||||
import { CommentType } from '../../../../common/api';
|
||||
import type { CommentRequest } from '../../../../common/api';
|
||||
import { MAX_PERSISTABLE_STATE_AND_EXTERNAL_REFERENCES } from '../../../../common/constants';
|
||||
import { isFileAttachmentRequest, isPersistableStateOrExternalReference } from '../../utils';
|
||||
import { BaseLimiter } from '../base_limiter';
|
||||
|
||||
export class PersistableStateAndExternalReferencesLimiter extends BaseLimiter {
|
||||
constructor(private readonly attachmentService: AttachmentService) {
|
||||
super({
|
||||
limit: MAX_PERSISTABLE_STATE_AND_EXTERNAL_REFERENCES,
|
||||
attachmentType: [CommentType.persistableState, CommentType.externalReference],
|
||||
attachmentNoun: 'persistable state and external reference attachments',
|
||||
});
|
||||
}
|
||||
|
||||
public async countOfItemsWithinCase(caseId: string): Promise<number> {
|
||||
return this.attachmentService.countPersistableStateAndExternalReferenceAttachments({
|
||||
caseId,
|
||||
});
|
||||
}
|
||||
|
||||
public countOfItemsInRequest(requests: CommentRequest[]): number {
|
||||
const totalReferences = requests
|
||||
.filter(isPersistableStateOrExternalReference)
|
||||
.filter((request) => !isFileAttachmentRequest(request));
|
||||
|
||||
return totalReferences.length;
|
||||
}
|
||||
}
|
|
@ -11,6 +11,8 @@ import type {
|
|||
CommentRequestUserType,
|
||||
CommentRequestAlertType,
|
||||
FileAttachmentMetadata,
|
||||
CommentRequestPersistableStateType,
|
||||
CommentRequestExternalReferenceType,
|
||||
} from '../../../common/api';
|
||||
import type { FileAttachmentRequest } from '../types';
|
||||
|
||||
|
@ -26,6 +28,37 @@ export const createUserRequests = (num: number): CommentRequestUserType[] => {
|
|||
return requests;
|
||||
};
|
||||
|
||||
export const createPersistableStateRequests = (
|
||||
num: number
|
||||
): CommentRequestPersistableStateType[] => {
|
||||
return [...Array(num).keys()].map(() => {
|
||||
return {
|
||||
persistableStateAttachmentTypeId: '.test',
|
||||
persistableStateAttachmentState: {},
|
||||
type: CommentType.persistableState as const,
|
||||
owner: 'test',
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const createExternalReferenceRequests = (
|
||||
num: number
|
||||
): CommentRequestExternalReferenceType[] => {
|
||||
return [...Array(num).keys()].map((value) => {
|
||||
return {
|
||||
type: CommentType.externalReference as const,
|
||||
owner: 'test',
|
||||
externalReferenceAttachmentTypeId: '.test',
|
||||
externalReferenceId: 'so-id',
|
||||
externalReferenceMetadata: {},
|
||||
externalReferenceStorage: {
|
||||
soType: `${value}`,
|
||||
type: ExternalReferenceStorageType.savedObject,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const createFileRequests = ({
|
||||
numRequests,
|
||||
numFiles,
|
||||
|
|
|
@ -10,6 +10,12 @@ import type { SavedObject } from '@kbn/core-saved-objects-api-server';
|
|||
import { createCasesClientMockArgs } from '../../client/mocks';
|
||||
import { alertComment, comment, mockCaseComments, mockCases, multipleAlert } from '../../mocks';
|
||||
import { CaseCommentModel } from './case_with_comments';
|
||||
import { MAX_PERSISTABLE_STATE_AND_EXTERNAL_REFERENCES } from '../../../common/constants';
|
||||
import {
|
||||
commentExternalReference,
|
||||
commentFileExternalReference,
|
||||
commentPersistableState,
|
||||
} from '../../client/cases/mock';
|
||||
|
||||
describe('CaseCommentModel', () => {
|
||||
const theCase = mockCases[0];
|
||||
|
@ -267,6 +273,52 @@ describe('CaseCommentModel', () => {
|
|||
|
||||
expect(clientArgs.services.attachmentService.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('validation', () => {
|
||||
clientArgs.services.attachmentService.countPersistableStateAndExternalReferenceAttachments.mockResolvedValue(
|
||||
MAX_PERSISTABLE_STATE_AND_EXTERNAL_REFERENCES
|
||||
);
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('throws if limit is reached when creating persistable state attachment', async () => {
|
||||
await expect(
|
||||
model.createComment({
|
||||
id: 'comment-1',
|
||||
commentReq: commentPersistableState,
|
||||
createdDate,
|
||||
})
|
||||
).rejects.toThrow(
|
||||
`Case has reached the maximum allowed number (${MAX_PERSISTABLE_STATE_AND_EXTERNAL_REFERENCES}) of attached persistable state and external reference attachments.`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if limit is reached when creating external reference', async () => {
|
||||
await expect(
|
||||
model.createComment({
|
||||
id: 'comment-1',
|
||||
commentReq: commentExternalReference,
|
||||
createdDate,
|
||||
})
|
||||
).rejects.toThrow(
|
||||
`Case has reached the maximum allowed number (${MAX_PERSISTABLE_STATE_AND_EXTERNAL_REFERENCES}) of attached persistable state and external reference attachments.`
|
||||
);
|
||||
});
|
||||
|
||||
it('does not throw if creating a file external reference and the limit is reached', async () => {
|
||||
clientArgs.fileService.find.mockResolvedValue({ total: 0, files: [] });
|
||||
|
||||
await expect(
|
||||
model.createComment({
|
||||
id: 'comment-1',
|
||||
commentReq: commentFileExternalReference,
|
||||
createdDate,
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulkCreate', () => {
|
||||
|
@ -526,5 +578,45 @@ describe('CaseCommentModel', () => {
|
|||
expect(multipleAlertsCall.attributes.alertId).toEqual(['test-id-3', 'test-id-5']);
|
||||
expect(multipleAlertsCall.attributes.index).toEqual(['test-index-3', 'test-index-5']);
|
||||
});
|
||||
|
||||
describe('validation', () => {
|
||||
clientArgs.services.attachmentService.countPersistableStateAndExternalReferenceAttachments.mockResolvedValue(
|
||||
MAX_PERSISTABLE_STATE_AND_EXTERNAL_REFERENCES
|
||||
);
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('throws if limit is reached when creating persistable state attachment', async () => {
|
||||
await expect(
|
||||
model.bulkCreate({
|
||||
attachments: [commentPersistableState],
|
||||
})
|
||||
).rejects.toThrow(
|
||||
`Case has reached the maximum allowed number (${MAX_PERSISTABLE_STATE_AND_EXTERNAL_REFERENCES}) of attached persistable state and external reference attachments.`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if limit is reached when creating external reference', async () => {
|
||||
await expect(
|
||||
model.bulkCreate({
|
||||
attachments: [commentExternalReference],
|
||||
})
|
||||
).rejects.toThrow(
|
||||
`Case has reached the maximum allowed number (${MAX_PERSISTABLE_STATE_AND_EXTERNAL_REFERENCES}) of attached persistable state and external reference attachments.`
|
||||
);
|
||||
});
|
||||
|
||||
it('does not throw if creating a file external reference and the limit is reached', async () => {
|
||||
clientArgs.fileService.find.mockResolvedValue({ total: 0, files: [] });
|
||||
|
||||
await expect(
|
||||
model.bulkCreate({
|
||||
attachments: [commentFileExternalReference],
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -33,6 +33,7 @@ import {
|
|||
getCaseViewPath,
|
||||
isSOError,
|
||||
countUserAttachments,
|
||||
isPersistableStateOrExternalReference,
|
||||
} from './utils';
|
||||
import { newCase } from '../routes/api/__mocks__/request_responses';
|
||||
import { CASE_VIEW_PAGE_TABS } from '../../common/types';
|
||||
|
@ -40,6 +41,12 @@ import { mockCases, mockCaseComments } from '../mocks';
|
|||
import { createAlertAttachment, createUserAttachment } from '../services/attachments/test_utils';
|
||||
import type { CaseConnector } from '../../common/types/domain';
|
||||
import { ConnectorTypes } from '../../common/types/domain';
|
||||
import {
|
||||
createAlertRequests,
|
||||
createExternalReferenceRequests,
|
||||
createPersistableStateRequests,
|
||||
createUserRequests,
|
||||
} from './limiter_checker/test_utils';
|
||||
|
||||
interface CommentReference {
|
||||
ids: string[];
|
||||
|
@ -1353,4 +1360,25 @@ describe('common utils', () => {
|
|||
expect(countUserAttachments(attachments)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPersistableStateOrExternalReference', () => {
|
||||
it('returns true for persistable state request', () => {
|
||||
expect(isPersistableStateOrExternalReference(createPersistableStateRequests(1)[0])).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('returns true for external reference request', () => {
|
||||
expect(isPersistableStateOrExternalReference(createExternalReferenceRequests(1)[0])).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('returns false for other request types', () => {
|
||||
expect(isPersistableStateOrExternalReference(createUserRequests(1)[0])).toBe(false);
|
||||
expect(isPersistableStateOrExternalReference(createAlertRequests(1, 'alert-id')[0])).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -254,6 +254,16 @@ export const isCommentRequestTypeAlert = (
|
|||
return context.type === CommentType.alert;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if a Comment Request is trying to create either a persistableState or an
|
||||
* externalReference attachment.
|
||||
*/
|
||||
export const isPersistableStateOrExternalReference = (context: CommentRequest): boolean => {
|
||||
return (
|
||||
context.type === CommentType.persistableState || context.type === CommentType.externalReference
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* A type narrowing function for file attachments.
|
||||
*/
|
||||
|
|
|
@ -537,4 +537,111 @@ describe('AttachmentService', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('countPersistableStateAndExternalReferenceAttachments', () => {
|
||||
it('does not throw and calls unsecuredSavedObjectsClient.find with the right parameters', async () => {
|
||||
unsecuredSavedObjectsClient.find.mockResolvedValue(
|
||||
createSOFindResponse([{ ...createUserAttachment(), score: 0 }])
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.countPersistableStateAndExternalReferenceAttachments({ caseId: 'test-id' })
|
||||
).resolves.not.toThrow();
|
||||
|
||||
expect(unsecuredSavedObjectsClient.find.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"filter": Object {
|
||||
"arguments": Array [
|
||||
Object {
|
||||
"arguments": Array [
|
||||
Object {
|
||||
"arguments": Array [
|
||||
Object {
|
||||
"isQuoted": false,
|
||||
"type": "literal",
|
||||
"value": "cases-comments.attributes.type",
|
||||
},
|
||||
Object {
|
||||
"isQuoted": false,
|
||||
"type": "literal",
|
||||
"value": "persistableState",
|
||||
},
|
||||
],
|
||||
"function": "is",
|
||||
"type": "function",
|
||||
},
|
||||
Object {
|
||||
"arguments": Array [
|
||||
Object {
|
||||
"isQuoted": false,
|
||||
"type": "literal",
|
||||
"value": "cases-comments.attributes.type",
|
||||
},
|
||||
Object {
|
||||
"isQuoted": false,
|
||||
"type": "literal",
|
||||
"value": "externalReference",
|
||||
},
|
||||
],
|
||||
"function": "is",
|
||||
"type": "function",
|
||||
},
|
||||
],
|
||||
"function": "or",
|
||||
"type": "function",
|
||||
},
|
||||
Object {
|
||||
"arguments": Array [
|
||||
Object {
|
||||
"arguments": Array [
|
||||
Object {
|
||||
"isQuoted": false,
|
||||
"type": "literal",
|
||||
"value": "cases-comments.attributes.externalReferenceAttachmentTypeId",
|
||||
},
|
||||
Object {
|
||||
"isQuoted": false,
|
||||
"type": "literal",
|
||||
"value": ".files",
|
||||
},
|
||||
],
|
||||
"function": "is",
|
||||
"type": "function",
|
||||
},
|
||||
],
|
||||
"function": "not",
|
||||
"type": "function",
|
||||
},
|
||||
],
|
||||
"function": "and",
|
||||
"type": "function",
|
||||
},
|
||||
"hasReference": Object {
|
||||
"id": "test-id",
|
||||
"type": "cases",
|
||||
},
|
||||
"page": 1,
|
||||
"perPage": 1,
|
||||
"sortField": "created_at",
|
||||
"type": "cases-comments",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns the expected total', async () => {
|
||||
const total = 3;
|
||||
|
||||
unsecuredSavedObjectsClient.find.mockResolvedValue(
|
||||
createSOFindResponse(
|
||||
Array(total).fill({ ...createUserAttachment({ foo: 'bar' }), score: 0 })
|
||||
)
|
||||
);
|
||||
|
||||
const res = await service.countPersistableStateAndExternalReferenceAttachments({
|
||||
caseId: 'test-id',
|
||||
});
|
||||
|
||||
expect(res).toBe(total);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,8 +14,13 @@ import type {
|
|||
} from '@kbn/core/server';
|
||||
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { fromKueryExpression } from '@kbn/es-query';
|
||||
import { CommentAttributesRt, CommentType, decodeOrThrow } from '../../../common/api';
|
||||
import { CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT } from '../../../common/constants';
|
||||
import {
|
||||
CASE_COMMENT_SAVED_OBJECT,
|
||||
CASE_SAVED_OBJECT,
|
||||
FILE_ATTACHMENT_TYPE,
|
||||
} from '../../../common/constants';
|
||||
import { buildFilter, combineFilters } from '../../client/utils';
|
||||
import { defaultSortField, isSOError } from '../../common/utils';
|
||||
import type { AggregationResponse } from '../../client/metrics/types';
|
||||
|
@ -124,6 +129,50 @@ export class AttachmentService {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the persistableState and externalReference attachments that are not .files
|
||||
*/
|
||||
public async countPersistableStateAndExternalReferenceAttachments({
|
||||
caseId,
|
||||
}: {
|
||||
caseId: string;
|
||||
}): Promise<number> {
|
||||
try {
|
||||
this.context.log.debug(
|
||||
`Attempting to count persistableState and externalReference attachments for case id ${caseId}`
|
||||
);
|
||||
|
||||
const typeFilter = buildFilter({
|
||||
filters: [CommentType.persistableState, CommentType.externalReference],
|
||||
field: 'type',
|
||||
operator: 'or',
|
||||
type: CASE_COMMENT_SAVED_OBJECT,
|
||||
});
|
||||
|
||||
const excludeFilesFilter = fromKueryExpression(
|
||||
`not ${CASE_COMMENT_SAVED_OBJECT}.attributes.externalReferenceAttachmentTypeId: ${FILE_ATTACHMENT_TYPE}`
|
||||
);
|
||||
|
||||
const combinedFilter = combineFilters([typeFilter, excludeFilesFilter]);
|
||||
|
||||
const response = await this.context.unsecuredSavedObjectsClient.find<{ total: number }>({
|
||||
type: CASE_COMMENT_SAVED_OBJECT,
|
||||
hasReference: { type: CASE_SAVED_OBJECT, id: caseId },
|
||||
page: 1,
|
||||
perPage: 1,
|
||||
sortField: defaultSortField,
|
||||
filter: combinedFilter,
|
||||
});
|
||||
|
||||
return response.total;
|
||||
} catch (error) {
|
||||
this.context.log.error(
|
||||
`Error while attempting to count persistableState and externalReference attachments for case id ${caseId}: ${error}`
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the aggregations against the actions attached to a case.
|
||||
*/
|
||||
|
|
|
@ -177,6 +177,7 @@ export const createAttachmentServiceMock = (): AttachmentServiceMock => {
|
|||
countAlertsAttachedToCase: jest.fn(),
|
||||
executeCaseActionsAggregations: jest.fn(),
|
||||
executeCaseAggregations: jest.fn(),
|
||||
countPersistableStateAndExternalReferenceAttachments: jest.fn(),
|
||||
};
|
||||
|
||||
// the cast here is required because jest.Mocked tries to include private members and would throw an error
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
CaseStatuses,
|
||||
CommentRequestExternalReferenceSOType,
|
||||
CommentRequestAlertType,
|
||||
ExternalReferenceStorageType,
|
||||
} from '@kbn/cases-plugin/common/api';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
import {
|
||||
|
@ -42,6 +43,7 @@ import {
|
|||
getCaseUserActions,
|
||||
removeServerGeneratedPropertiesFromUserAction,
|
||||
getAllComments,
|
||||
bulkCreateAttachments,
|
||||
} from '../../../../common/lib/api';
|
||||
import {
|
||||
createSignalsIndex,
|
||||
|
@ -468,6 +470,76 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
expectedHttpCode: 400,
|
||||
});
|
||||
});
|
||||
|
||||
it('400s when attempting to add a persistable state to a case that already has 100', async () => {
|
||||
const postedCase = await createCase(supertest, postCaseReq);
|
||||
|
||||
const attachments = Array(100).fill({
|
||||
type: CommentType.externalReference as const,
|
||||
owner: 'securitySolutionFixture',
|
||||
externalReferenceAttachmentTypeId: '.test',
|
||||
externalReferenceId: 'so-id',
|
||||
externalReferenceMetadata: {},
|
||||
externalReferenceStorage: {
|
||||
soType: 'external-ref',
|
||||
type: ExternalReferenceStorageType.savedObject as const,
|
||||
},
|
||||
});
|
||||
|
||||
await bulkCreateAttachments({
|
||||
supertest,
|
||||
caseId: postedCase.id,
|
||||
params: attachments,
|
||||
expectedHttpCode: 200,
|
||||
});
|
||||
|
||||
await createComment({
|
||||
supertest,
|
||||
caseId: postedCase.id,
|
||||
params: {
|
||||
persistableStateAttachmentTypeId: '.test',
|
||||
persistableStateAttachmentState: {},
|
||||
type: CommentType.persistableState as const,
|
||||
owner: 'securitySolutionFixture',
|
||||
},
|
||||
expectedHttpCode: 400,
|
||||
});
|
||||
});
|
||||
|
||||
it('400s when attempting to add an external reference to a case that already has 100', async () => {
|
||||
const postedCase = await createCase(supertest, postCaseReq);
|
||||
|
||||
const attachments = Array(100).fill({
|
||||
persistableStateAttachmentTypeId: '.test',
|
||||
persistableStateAttachmentState: {},
|
||||
type: CommentType.persistableState as const,
|
||||
owner: 'securitySolutionFixture',
|
||||
});
|
||||
|
||||
await bulkCreateAttachments({
|
||||
supertest,
|
||||
caseId: postedCase.id,
|
||||
params: attachments,
|
||||
expectedHttpCode: 200,
|
||||
});
|
||||
|
||||
await createComment({
|
||||
supertest,
|
||||
caseId: postedCase.id,
|
||||
params: {
|
||||
type: CommentType.externalReference as const,
|
||||
owner: 'securitySolutionFixture',
|
||||
externalReferenceAttachmentTypeId: '.test',
|
||||
externalReferenceId: 'so-id',
|
||||
externalReferenceMetadata: {},
|
||||
externalReferenceStorage: {
|
||||
soType: 'external-ref',
|
||||
type: ExternalReferenceStorageType.savedObject as const,
|
||||
},
|
||||
},
|
||||
expectedHttpCode: 400,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('alerts', () => {
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
CaseStatuses,
|
||||
CommentRequestExternalReferenceSOType,
|
||||
CommentType,
|
||||
ExternalReferenceStorageType,
|
||||
} from '@kbn/cases-plugin/common/api';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
import {
|
||||
|
@ -42,6 +43,7 @@ import {
|
|||
createAndUploadFile,
|
||||
deleteAllFiles,
|
||||
getAllComments,
|
||||
createComment,
|
||||
} from '../../../../common/lib/api';
|
||||
import {
|
||||
createSignalsIndex,
|
||||
|
@ -619,102 +621,174 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
await createCaseAndBulkCreateAttachments({ supertest, expectedHttpCode: 400 });
|
||||
});
|
||||
|
||||
it('400s when attempting to add more than 1K alerts to a case', async () => {
|
||||
const alerts = [...Array(1001).keys()].map((num) => `test-${num}`);
|
||||
const postedCase = await createCase(supertest, postCaseReq);
|
||||
await bulkCreateAttachments({
|
||||
supertest,
|
||||
caseId: postedCase.id,
|
||||
params: [
|
||||
{
|
||||
...postCommentAlertReq,
|
||||
alertId: alerts,
|
||||
index: alerts,
|
||||
},
|
||||
],
|
||||
expectedHttpCode: 400,
|
||||
});
|
||||
});
|
||||
|
||||
it('400s when attempting to add more than 1K alerts to a case in the same request', async () => {
|
||||
const alerts = [...Array(1001).keys()].map((num) => `test-${num}`);
|
||||
const postedCase = await createCase(supertest, postCaseReq);
|
||||
await bulkCreateAttachments({
|
||||
supertest,
|
||||
caseId: postedCase.id,
|
||||
params: [
|
||||
{
|
||||
...postCommentAlertReq,
|
||||
alertId: alerts.slice(0, 500),
|
||||
index: alerts.slice(0, 500),
|
||||
},
|
||||
{
|
||||
...postCommentAlertReq,
|
||||
alertId: alerts.slice(500, alerts.length),
|
||||
index: alerts.slice(500, alerts.length),
|
||||
},
|
||||
postCommentAlertReq,
|
||||
],
|
||||
expectedHttpCode: 400,
|
||||
});
|
||||
});
|
||||
|
||||
it('400s when attempting to add an alert to a case that already has 1K alerts', async () => {
|
||||
const alerts = [...Array(1000).keys()].map((num) => `test-${num}`);
|
||||
const postedCase = await createCase(supertest, postCaseReq);
|
||||
await bulkCreateAttachments({
|
||||
supertest,
|
||||
caseId: postedCase.id,
|
||||
params: [
|
||||
{
|
||||
...postCommentAlertReq,
|
||||
alertId: alerts,
|
||||
index: alerts,
|
||||
},
|
||||
],
|
||||
describe('validation', () => {
|
||||
it('400s when attempting to add more than 1K alerts to a case', async () => {
|
||||
const alerts = [...Array(1001).keys()].map((num) => `test-${num}`);
|
||||
const postedCase = await createCase(supertest, postCaseReq);
|
||||
await bulkCreateAttachments({
|
||||
supertest,
|
||||
caseId: postedCase.id,
|
||||
params: [
|
||||
{
|
||||
...postCommentAlertReq,
|
||||
alertId: alerts,
|
||||
index: alerts,
|
||||
},
|
||||
],
|
||||
expectedHttpCode: 400,
|
||||
});
|
||||
});
|
||||
|
||||
await bulkCreateAttachments({
|
||||
supertest,
|
||||
caseId: postedCase.id,
|
||||
params: [
|
||||
{
|
||||
...postCommentAlertReq,
|
||||
alertId: 'test-id',
|
||||
index: 'test-index',
|
||||
},
|
||||
],
|
||||
expectedHttpCode: 400,
|
||||
});
|
||||
});
|
||||
|
||||
it('400s when the case already has alerts and the sum of existing and new alerts exceed 1k', async () => {
|
||||
const alerts = [...Array(1200).keys()].map((num) => `test-${num}`);
|
||||
const postedCase = await createCase(supertest, postCaseReq);
|
||||
await bulkCreateAttachments({
|
||||
supertest,
|
||||
caseId: postedCase.id,
|
||||
params: [
|
||||
{
|
||||
...postCommentAlertReq,
|
||||
alertId: alerts.slice(0, 500),
|
||||
index: alerts.slice(0, 500),
|
||||
},
|
||||
],
|
||||
it('400s when attempting to add more than 1K alerts to a case in the same request', async () => {
|
||||
const alerts = [...Array(1001).keys()].map((num) => `test-${num}`);
|
||||
const postedCase = await createCase(supertest, postCaseReq);
|
||||
await bulkCreateAttachments({
|
||||
supertest,
|
||||
caseId: postedCase.id,
|
||||
params: [
|
||||
{
|
||||
...postCommentAlertReq,
|
||||
alertId: alerts.slice(0, 500),
|
||||
index: alerts.slice(0, 500),
|
||||
},
|
||||
{
|
||||
...postCommentAlertReq,
|
||||
alertId: alerts.slice(500, alerts.length),
|
||||
index: alerts.slice(500, alerts.length),
|
||||
},
|
||||
postCommentAlertReq,
|
||||
],
|
||||
expectedHttpCode: 400,
|
||||
});
|
||||
});
|
||||
|
||||
await bulkCreateAttachments({
|
||||
supertest,
|
||||
caseId: postedCase.id,
|
||||
params: [
|
||||
{
|
||||
...postCommentAlertReq,
|
||||
alertId: alerts.slice(500),
|
||||
index: alerts.slice(500),
|
||||
it('400s when attempting to add an alert to a case that already has 1K alerts', async () => {
|
||||
const alerts = [...Array(1000).keys()].map((num) => `test-${num}`);
|
||||
const postedCase = await createCase(supertest, postCaseReq);
|
||||
await bulkCreateAttachments({
|
||||
supertest,
|
||||
caseId: postedCase.id,
|
||||
params: [
|
||||
{
|
||||
...postCommentAlertReq,
|
||||
alertId: alerts,
|
||||
index: alerts,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await bulkCreateAttachments({
|
||||
supertest,
|
||||
caseId: postedCase.id,
|
||||
params: [
|
||||
{
|
||||
...postCommentAlertReq,
|
||||
alertId: 'test-id',
|
||||
index: 'test-index',
|
||||
},
|
||||
],
|
||||
expectedHttpCode: 400,
|
||||
});
|
||||
});
|
||||
|
||||
it('400s when the case already has alerts and the sum of existing and new alerts exceed 1k', async () => {
|
||||
const alerts = [...Array(1200).keys()].map((num) => `test-${num}`);
|
||||
const postedCase = await createCase(supertest, postCaseReq);
|
||||
await bulkCreateAttachments({
|
||||
supertest,
|
||||
caseId: postedCase.id,
|
||||
params: [
|
||||
{
|
||||
...postCommentAlertReq,
|
||||
alertId: alerts.slice(0, 500),
|
||||
index: alerts.slice(0, 500),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await bulkCreateAttachments({
|
||||
supertest,
|
||||
caseId: postedCase.id,
|
||||
params: [
|
||||
{
|
||||
...postCommentAlertReq,
|
||||
alertId: alerts.slice(500),
|
||||
index: alerts.slice(500),
|
||||
},
|
||||
postCommentAlertReq,
|
||||
],
|
||||
expectedHttpCode: 400,
|
||||
});
|
||||
});
|
||||
|
||||
it('400s when attempting to bulk create persistable state attachments reaching the 100 limit', async () => {
|
||||
const postedCase = await createCase(supertest, postCaseReq);
|
||||
|
||||
await createComment({
|
||||
supertest,
|
||||
caseId: postedCase.id,
|
||||
params: {
|
||||
type: CommentType.externalReference as const,
|
||||
owner: 'securitySolutionFixture',
|
||||
externalReferenceAttachmentTypeId: '.test',
|
||||
externalReferenceId: 'so-id',
|
||||
externalReferenceMetadata: {},
|
||||
externalReferenceStorage: {
|
||||
soType: 'external-ref',
|
||||
type: ExternalReferenceStorageType.savedObject as const,
|
||||
},
|
||||
},
|
||||
postCommentAlertReq,
|
||||
],
|
||||
expectedHttpCode: 400,
|
||||
expectedHttpCode: 200,
|
||||
});
|
||||
|
||||
const persistableStateAttachments = Array(100).fill({
|
||||
persistableStateAttachmentTypeId: '.test',
|
||||
persistableStateAttachmentState: {},
|
||||
type: CommentType.persistableState as const,
|
||||
owner: 'securitySolutionFixture',
|
||||
});
|
||||
|
||||
await bulkCreateAttachments({
|
||||
supertest,
|
||||
caseId: postedCase.id,
|
||||
params: persistableStateAttachments,
|
||||
expectedHttpCode: 400,
|
||||
});
|
||||
});
|
||||
|
||||
it('400s when attempting to bulk create >100 external reference attachments reaching the 100 limit', async () => {
|
||||
const postedCase = await createCase(supertest, postCaseReq);
|
||||
|
||||
await createComment({
|
||||
supertest,
|
||||
caseId: postedCase.id,
|
||||
params: {
|
||||
persistableStateAttachmentTypeId: '.test',
|
||||
persistableStateAttachmentState: {},
|
||||
type: CommentType.persistableState as const,
|
||||
owner: 'securitySolutionFixture',
|
||||
},
|
||||
expectedHttpCode: 200,
|
||||
});
|
||||
|
||||
const externalRequestAttachments = Array(100).fill({
|
||||
type: CommentType.externalReference as const,
|
||||
owner: 'securitySolutionFixture',
|
||||
externalReferenceAttachmentTypeId: '.test',
|
||||
externalReferenceId: 'so-id',
|
||||
externalReferenceMetadata: {},
|
||||
externalReferenceStorage: {
|
||||
soType: 'external-ref',
|
||||
type: ExternalReferenceStorageType.savedObject as const,
|
||||
},
|
||||
});
|
||||
|
||||
await bulkCreateAttachments({
|
||||
supertest,
|
||||
caseId: postedCase.id,
|
||||
params: externalRequestAttachments,
|
||||
expectedHttpCode: 400,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue