diff --git a/x-pack/plugins/cases/server/client/cases/create.test.ts b/x-pack/plugins/cases/server/client/cases/create.test.ts index ee4134368c559..16b24e638b8d2 100644 --- a/x-pack/plugins/cases/server/client/cases/create.test.ts +++ b/x-pack/plugins/cases/server/client/cases/create.test.ts @@ -17,7 +17,7 @@ import type { CasePostRequest } from '../../../common'; import { SECURITY_SOLUTION_OWNER } from '../../../common'; import { mockCases } from '../../mocks'; import { createCasesClientMock, createCasesClientMockArgs } from '../mocks'; -import { create, throwIfCustomFieldKeysInvalid } from './create'; +import { create, throwIfCustomFieldKeysInvalid, throwIfMissingRequiredCustomField } from './create'; import { CaseSeverity, CaseStatuses, @@ -702,4 +702,131 @@ describe('create', () => { ).rejects.toThrowErrorMatchingInlineSnapshot(`"No custom fields configured."`); }); }); + + describe('throwIfMissingRequiredCustomField', () => { + const casesClient = createCasesClientMock(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('does not throw if all required custom fields are in the request', async () => { + const customFields = [ + { + key: 'first_key', + type: CustomFieldTypes.TEXT as const, + field: { value: ['this is a text field value', 'this is second'] }, + }, + { + key: 'second_key', + type: CustomFieldTypes.TOGGLE as const, + field: { value: null }, + }, + ]; + + casesClient.configure.get = jest.fn().mockResolvedValue([ + { + owner: mockCases[0].attributes.owner, + customFields: [ + { + key: 'first_key', + type: CustomFieldTypes.TEXT, + label: 'foo', + }, + { + key: 'second_key', + type: CustomFieldTypes.TOGGLE, + label: 'foo', + }, + ], + }, + ]); + + await expect( + throwIfMissingRequiredCustomField({ + casePostRequest: { + customFields, + } as unknown as CasePostRequest, + casesClient, + }) + ).resolves.not.toThrow(); + }); + + it('does not throw if there are only optional custom fields in configuration', async () => { + casesClient.configure.get = jest.fn().mockResolvedValue([ + { + owner: mockCases[0].attributes.owner, + customFields: [ + { + key: 'first_key', + type: CustomFieldTypes.TEXT, + label: 'foo', + required: false, + }, + { + key: 'second_key', + type: CustomFieldTypes.TOGGLE, + label: 'foo', + required: false, + }, + ], + }, + ]); + + await expect( + throwIfMissingRequiredCustomField({ + casePostRequest: {} as unknown as CasePostRequest, + casesClient, + }) + ).resolves.not.toThrow(); + }); + + it('does not throw if no configuration found', async () => { + casesClient.configure.get = jest.fn().mockResolvedValue([]); + + await expect( + throwIfMissingRequiredCustomField({ + casePostRequest: {} as unknown as CasePostRequest, + casesClient, + }) + ).resolves.not.toThrow(); + }); + + it('throws if the request has missing required custom fields', async () => { + casesClient.configure.get = jest.fn().mockResolvedValue([ + { + owner: mockCases[0].attributes.owner, + customFields: [ + { + key: 'first_key', + type: CustomFieldTypes.TEXT, + label: 'foo', + required: true, + }, + { + key: 'second_key', + type: CustomFieldTypes.TOGGLE, + label: 'foo', + required: true, + }, + ], + }, + ]); + + await expect( + throwIfMissingRequiredCustomField({ + casePostRequest: { + customFields: [ + { + key: 'second_key', + type: CustomFieldTypes.TOGGLE, + label: 'foo', + }, + ], + } as unknown as CasePostRequest, + casesClient, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Missing required custom fields: first_key"`); + }); + }); }); diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index 8ecfb5a9c5bac..aa45f67e49bb7 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -9,6 +9,7 @@ import Boom from '@hapi/boom'; import { SavedObjectsUtils } from '@kbn/core/server'; +import { differenceWith } from 'lodash'; import type { Case } from '../../../common/types/domain'; import { CaseSeverity, UserActionTypes, CaseRt } from '../../../common/types/domain'; import { decodeWithExcessOrThrow } from '../../../common/api'; @@ -56,6 +57,43 @@ export async function throwIfCustomFieldKeysInvalid({ } } +/** + * Throws if there are required custom fields missing in the request. + */ +export async function throwIfMissingRequiredCustomField({ + casePostRequest, + casesClient, +}: { + casePostRequest: CasePostRequest; + casesClient: CasesClient; +}) { + const requestCustomFields = casePostRequest.customFields; + + if (!Array.isArray(requestCustomFields) || !requestCustomFields.length) { + return; + } + + const configuration = await casesClient.configure.get({ owner: casePostRequest.owner }); + + if (configuration.length === 0) { + return; + } + + const requiredCustomFields = configuration[0].customFields.filter( + (customField) => customField.required + ); + + const invalidCustomFieldKeys = differenceWith( + requiredCustomFields, + requestCustomFields, + (requiredVal, requestedVal) => requiredVal.key === requestedVal.key + ).map((e) => e.key); + + if (invalidCustomFieldKeys.length) { + throw Boom.badRequest(`Missing required custom fields: ${invalidCustomFieldKeys}`); + } +} + /** * Creates a new case. * @@ -78,6 +116,7 @@ export const create = async ( throwIfDuplicatedCustomFieldKeysInRequest({ customFieldsInRequest: query.customFields }); await throwIfCustomFieldKeysInvalid({ casePostRequest: query, casesClient }); + await throwIfMissingRequiredCustomField({ casePostRequest: query, casesClient }); const savedObjectID = SavedObjectsUtils.generateId(); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/post_case.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/post_case.ts index 0a8a504e53dcc..5a07193e48e80 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/post_case.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/post_case.ts @@ -425,6 +425,43 @@ export default ({ getService }: FtrProviderContext): void => { 400 ); }); + + it('400s when trying to create case with a missing required custom field', async () => { + await createConfiguration( + supertest, + getConfigurationRequest({ + overrides: { + customFields: [ + { + key: 'test_custom_field', + label: 'text', + type: CustomFieldTypes.TEXT, + required: true, + }, + { + key: 'toggle_custom_field', + label: 'toggle', + type: CustomFieldTypes.TOGGLE, + required: true, + }, + ], + }, + }) + ); + await createCase( + supertest, + getPostCaseRequest({ + customFields: [ + { + key: 'toggle_custom_field', + type: CustomFieldTypes.TOGGLE, + field: { value: [true] }, + }, + ], + }), + 400 + ); + }); }); });