Skip to content

Commit

Permalink
[Cases] Total external references and persistable state attachments p…
Browse files Browse the repository at this point in the history
…er case (elastic#162071)

Connected to elastic#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.
  • Loading branch information
adcoelho authored and Devon Thomson committed Aug 1, 2023
1 parent 62eb5a2 commit 1975440
Show file tree
Hide file tree
Showing 15 changed files with 693 additions and 89 deletions.
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 @@ -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
Expand Down
12 changes: 11 additions & 1 deletion x-pack/plugins/cases/server/client/cases/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import type { Limiter } from './types';

interface LimiterParams {
limit: number;
attachmentType: CommentType;
field: string;
attachmentType: CommentType | CommentType[];
field?: string;
attachmentNoun: string;
}

Expand Down
7 changes: 6 additions & 1 deletion x-pack/plugins/cases/server/common/limiter_checker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand All @@ -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[]) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
Original file line number Diff line number Diff line change
@@ -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;
}
}
33 changes: 33 additions & 0 deletions x-pack/plugins/cases/server/common/limiter_checker/test_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import type {
CommentRequestUserType,
CommentRequestAlertType,
FileAttachmentMetadata,
CommentRequestPersistableStateType,
CommentRequestExternalReferenceType,
} from '../../../common/api';
import type { FileAttachmentRequest } from '../types';

Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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();
});
});
});
});
28 changes: 28 additions & 0 deletions x-pack/plugins/cases/server/common/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,20 @@ import {
getCaseViewPath,
isSOError,
countUserAttachments,
isPersistableStateOrExternalReference,
} from './utils';
import { newCase } from '../routes/api/__mocks__/request_responses';
import { CASE_VIEW_PAGE_TABS } from '../../common/types';
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[];
Expand Down Expand Up @@ -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
);
});
});
});
Loading

0 comments on commit 1975440

Please sign in to comment.