diff --git a/x-pack/plugins/cases/common/api/cases/comment.ts b/x-pack/plugins/cases/common/api/cases/comment.ts index 900f90156d431..8cdab3dbd597d 100644 --- a/x-pack/plugins/cases/common/api/cases/comment.ts +++ b/x-pack/plugins/cases/common/api/cases/comment.ts @@ -153,12 +153,15 @@ export const FindQueryParamsRt = rt.partial({ ...SavedObjectFindOptionsRt.props, }); +export const BulkCreateCommentRequestRt = rt.array(CommentRequestRt); + export type FindQueryParams = rt.TypeOf; export type AttributesTypeActions = rt.TypeOf; export type AttributesTypeAlerts = rt.TypeOf; export type AttributesTypeUser = rt.TypeOf; export type CommentAttributes = rt.TypeOf; export type CommentRequest = rt.TypeOf; +export type BulkCreateCommentRequest = rt.TypeOf; export type CommentResponse = rt.TypeOf; export type CommentResponseUserType = rt.TypeOf; export type CommentResponseAlertsType = rt.TypeOf; diff --git a/x-pack/plugins/cases/common/constants.ts b/x-pack/plugins/cases/common/constants.ts index a1ac829b33cce..1fd3b5b7cda5c 100644 --- a/x-pack/plugins/cases/common/constants.ts +++ b/x-pack/plugins/cases/common/constants.ts @@ -70,6 +70,14 @@ export const CASE_DETAILS_ALERTS_URL = `${CASE_DETAILS_URL}/alerts` as const; export const CASE_METRICS_DETAILS_URL = `${CASES_URL}/metrics/{case_id}` as const; +/** + * Internal routes + */ + +export const CASES_INTERNAL_URL = '/internal/cases' as const; +export const INTERNAL_BULK_CREATE_ATTACHMENTS_URL = + `${CASES_INTERNAL_URL}/{case_id}/attachments/_bulk_create` as const; + /** * Action routes */ diff --git a/x-pack/plugins/cases/server/client/attachments/bulk_create.ts b/x-pack/plugins/cases/server/client/attachments/bulk_create.ts new file mode 100644 index 0000000000000..6e855cc0fc542 --- /dev/null +++ b/x-pack/plugins/cases/server/client/attachments/bulk_create.ts @@ -0,0 +1,88 @@ +/* + * 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 Boom from '@hapi/boom'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; + +import { SavedObjectsUtils } from '../../../../../../src/core/server'; + +import { + BulkCreateCommentRequest, + BulkCreateCommentRequestRt, + CaseResponse, + CommentRequest, + throwErrors, +} from '../../../common/api'; + +import { CaseCommentModel } from '../../common/models'; +import { createCaseError } from '../../common/error'; +import { CasesClientArgs } from '..'; + +import { decodeCommentRequest } from '../utils'; +import { Operations, OwnerEntity } from '../../authorization'; + +export interface BulkCreateArgs { + caseId: string; + attachments: BulkCreateCommentRequest; +} + +/** + * Create an attachment to a case. + * + * @ignore + */ +export const bulkCreate = async ( + args: BulkCreateArgs, + clientArgs: CasesClientArgs +): Promise => { + const { attachments, caseId } = args; + + pipe( + BulkCreateCommentRequestRt.decode(attachments), + fold(throwErrors(Boom.badRequest), identity) + ); + + attachments.forEach((attachment) => { + decodeCommentRequest(attachment); + }); + + const { logger, authorization } = clientArgs; + + try { + const [attachmentsWithIds, entities]: [Array<{ id: string } & CommentRequest>, OwnerEntity[]] = + attachments.reduce<[Array<{ id: string } & CommentRequest>, OwnerEntity[]]>( + ([a, e], attachment) => { + const savedObjectID = SavedObjectsUtils.generateId(); + return [ + [...a, { id: savedObjectID, ...attachment }], + [...e, { owner: attachment.owner, id: savedObjectID }], + ]; + }, + [[], []] + ); + + await authorization.ensureAuthorized({ + operation: Operations.createComment, + entities, + }); + + const model = await CaseCommentModel.create(caseId, clientArgs); + const updatedModel = await model.bulkCreate({ + attachments: attachmentsWithIds, + }); + + return await updatedModel.encodeWithComments(); + } catch (error) { + throw createCaseError({ + message: `Failed while bulk creating attachment to case id: ${caseId} error: ${error}`, + error, + logger, + }); + } +}; diff --git a/x-pack/plugins/cases/server/client/attachments/client.ts b/x-pack/plugins/cases/server/client/attachments/client.ts index ab77ae3f01836..63b039890aff9 100644 --- a/x-pack/plugins/cases/server/client/attachments/client.ts +++ b/x-pack/plugins/cases/server/client/attachments/client.ts @@ -12,6 +12,7 @@ import { CasesClientInternal } from '../client_internal'; import { IAllCommentsResponse, ICaseResponse, ICommentsResponse } from '../typedoc_interfaces'; import { CasesClientArgs } from '../types'; import { AddArgs, addComment } from './add'; +import { bulkCreate, BulkCreateArgs } from './bulk_create'; import { DeleteAllArgs, deleteAll, DeleteArgs, deleteComment } from './delete'; import { find, @@ -33,6 +34,7 @@ export interface AttachmentsSubClient { * Adds an attachment to a case. */ add(params: AddArgs): Promise; + bulkCreate(params: BulkCreateArgs): Promise; /** * Deletes all attachments associated with a single case. */ @@ -77,6 +79,7 @@ export const createAttachmentsSubClient = ( ): AttachmentsSubClient => { const attachmentSubClient: AttachmentsSubClient = { add: (params: AddArgs) => addComment(params, clientArgs), + bulkCreate: (params: BulkCreateArgs) => bulkCreate(params, clientArgs), deleteAll: (deleteAllArgs: DeleteAllArgs) => deleteAll(deleteAllArgs, clientArgs), delete: (deleteArgs: DeleteArgs) => deleteComment(deleteArgs, clientArgs), find: (findArgs: FindArgs) => find(findArgs, clientArgs), diff --git a/x-pack/plugins/cases/server/client/mocks.ts b/x-pack/plugins/cases/server/client/mocks.ts index ecedc7cb05071..6ad4663f1e5ea 100644 --- a/x-pack/plugins/cases/server/client/mocks.ts +++ b/x-pack/plugins/cases/server/client/mocks.ts @@ -46,6 +46,7 @@ type AttachmentsSubClientMock = jest.Mocked; const createAttachmentsSubClientMock = (): AttachmentsSubClientMock => { return { add: jest.fn(), + bulkCreate: jest.fn(), deleteAll: jest.fn(), delete: jest.fn(), find: jest.fn(), diff --git a/x-pack/plugins/cases/server/common/models/case_with_comments.ts b/x-pack/plugins/cases/server/common/models/case_with_comments.ts index 14c0b6aec5eac..2e65755aabdc4 100644 --- a/x-pack/plugins/cases/server/common/models/case_with_comments.ts +++ b/x-pack/plugins/cases/server/common/models/case_with_comments.ts @@ -188,17 +188,9 @@ export class CaseCommentModel { id: string; }): Promise { try { - this.validateCreateCommentRequest(commentReq); + this.validateCreateCommentRequest([commentReq]); - let references = this.buildRefsToCase(); - - if (commentReq.type === CommentType.user && commentReq?.comment) { - const commentStringReferences = getOrUpdateLensReferences( - this.params.lensEmbeddableFactory, - commentReq.comment - ); - references = [...references, ...commentStringReferences]; - } + const references = [...this.buildRefsToCase(), ...this.getCommentReferences(commentReq)]; const [comment, commentableCase] = await Promise.all([ this.params.attachmentService.create({ @@ -215,7 +207,7 @@ export class CaseCommentModel { ]); await Promise.all([ - commentableCase.handleAlertComments(comment, commentReq), + commentableCase.handleAlertComments([commentReq]), this.createCommentUserAction(comment, commentReq), ]); @@ -229,12 +221,15 @@ export class CaseCommentModel { } } - private validateCreateCommentRequest(req: CommentRequest) { - if (req.type === CommentType.alert && this.caseInfo.attributes.status === CaseStatuses.closed) { + private validateCreateCommentRequest(req: CommentRequest[]) { + if ( + req.some((attachment) => attachment.type === CommentType.alert) && + this.caseInfo.attributes.status === CaseStatuses.closed + ) { throw Boom.badRequest('Alert cannot be attached to a closed case'); } - if (req.owner !== this.caseInfo.attributes.owner) { + if (req.some((attachment) => attachment.owner !== this.caseInfo.attributes.owner)) { throw Boom.badRequest('The owner field of the comment must match the case'); } } @@ -249,20 +244,38 @@ export class CaseCommentModel { ]; } - private async handleAlertComments(comment: SavedObject, req: CommentRequest) { - if ( - comment.attributes.type === CommentType.alert && - this.caseInfo.attributes.settings.syncAlerts - ) { - await this.updateAlertsStatus(req); + private getCommentReferences(commentReq: CommentRequest) { + let references: SavedObjectReference[] = []; + + if (commentReq.type === CommentType.user && commentReq?.comment) { + const commentStringReferences = getOrUpdateLensReferences( + this.params.lensEmbeddableFactory, + commentReq.comment + ); + references = [...references, ...commentStringReferences]; } + + return references; } - private async updateAlertsStatus(req: CommentRequest) { - const alertsToUpdate = createAlertUpdateRequest({ - comment: req, - status: this.caseInfo.attributes.status, - }); + private async handleAlertComments(attachments: CommentRequest[]) { + const alerts = attachments.filter( + (attachment) => + attachment.type === CommentType.alert && this.caseInfo.attributes.settings.syncAlerts + ); + + await this.updateAlertsStatus(alerts); + } + + private async updateAlertsStatus(alerts: CommentRequest[]) { + const alertsToUpdate = alerts + .map((alert) => + createAlertUpdateRequest({ + comment: alert, + status: this.caseInfo.attributes.status, + }) + ) + .flat(); await this.params.alertsService.updateAlertsStatus(alertsToUpdate); } @@ -285,6 +298,19 @@ export class CaseCommentModel { }); } + private async bulkCreateCommentUserAction(attachments: Array<{ id: string } & CommentRequest>) { + await this.params.userActionService.bulkCreateAttachmentCreation({ + unsecuredSavedObjectsClient: this.params.unsecuredSavedObjectsClient, + caseId: this.caseInfo.id, + attachments: attachments.map(({ id, ...attachment }) => ({ + id, + owner: attachment.owner, + attachment, + })), + user: this.params.user, + }); + } + private formatForEncoding(totalComment: number) { return { id: this.caseInfo.id, @@ -322,4 +348,54 @@ export class CaseCommentModel { }); } } + public async bulkCreate({ + attachments, + }: { + attachments: Array<{ id: string } & CommentRequest>; + }): Promise { + try { + this.validateCreateCommentRequest(attachments); + + const caseReference = this.buildRefsToCase(); + + const [newlyCreatedAttachments, commentableCase] = await Promise.all([ + this.params.attachmentService.bulkCreate({ + unsecuredSavedObjectsClient: this.params.unsecuredSavedObjectsClient, + attachments: attachments.map(({ id, ...attachment }) => { + return { + attributes: transformNewComment({ + createdDate: new Date().toISOString(), + ...attachment, + ...this.params.user, + }), + references: [...caseReference, ...this.getCommentReferences(attachment)], + id, + }; + }), + }), + this.updateCaseUserAndDate(new Date().toISOString()), + ]); + + const savedObjectsWithoutErrors = newlyCreatedAttachments.saved_objects.filter( + (attachment) => attachment.error == null + ); + + const attachmentsWithoutErrors = attachments.filter((attachment) => + savedObjectsWithoutErrors.some((so) => so.id === attachment.id) + ); + + await Promise.all([ + commentableCase.handleAlertComments(attachmentsWithoutErrors), + this.bulkCreateCommentUserAction(attachmentsWithoutErrors), + ]); + + return commentableCase; + } catch (error) { + throw createCaseError({ + message: `Failed bulk creating attachments on a commentable case, case id: ${this.caseInfo.id}: ${error}`, + error, + logger: this.params.logger, + }); + } + } } diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index 5eba2f75cdaaa..d1a585b2aed21 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -39,6 +39,7 @@ import { getExternalRoutes } from './routes/api/get_external_routes'; import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; import { createCasesTelemetry, scheduleCasesTelemetryTask } from './telemetry'; +import { getInternalRoutes } from './routes/api/get_internal_routes'; export interface PluginsSetup { actions: ActionsPluginSetup; @@ -130,7 +131,7 @@ export class CasePlugin { registerRoutes({ router, - routes: getExternalRoutes(), + routes: [...getExternalRoutes(), ...getInternalRoutes()], logger: this.logger, kibanaVersion: this.kibanaVersion, telemetryUsageCounter, diff --git a/x-pack/plugins/cases/server/routes/api/get_internal_routes.ts b/x-pack/plugins/cases/server/routes/api/get_internal_routes.ts new file mode 100644 index 0000000000000..10d379c080abb --- /dev/null +++ b/x-pack/plugins/cases/server/routes/api/get_internal_routes.ts @@ -0,0 +1,11 @@ +/* + * 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 { bulkCreateAttachmentsRoute } from './internal/bulk_create_attachments'; +import { CaseRoute } from './types'; + +export const getInternalRoutes = () => [bulkCreateAttachmentsRoute] as CaseRoute[]; diff --git a/x-pack/plugins/cases/server/routes/api/internal/bulk_create_attachments.ts b/x-pack/plugins/cases/server/routes/api/internal/bulk_create_attachments.ts new file mode 100644 index 0000000000000..1940cd442eb27 --- /dev/null +++ b/x-pack/plugins/cases/server/routes/api/internal/bulk_create_attachments.ts @@ -0,0 +1,40 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { INTERNAL_BULK_CREATE_ATTACHMENTS_URL } from '../../../../common/constants'; +import { BulkCreateCommentRequest } from '../../../../common/api'; +import { createCaseError } from '../../../common/error'; +import { createCasesRoute } from '../create_cases_route'; +import { escapeHatch } from '../utils'; + +export const bulkCreateAttachmentsRoute = createCasesRoute({ + method: 'post', + path: INTERNAL_BULK_CREATE_ATTACHMENTS_URL, + params: { + params: schema.object({ + case_id: schema.string(), + }), + body: schema.arrayOf(escapeHatch), + }, + handler: async ({ context, request, response }) => { + try { + const casesClient = await context.cases.getCasesClient(); + const caseId = request.params.case_id; + const attachments = request.body as BulkCreateCommentRequest; + + return response.ok({ + body: await casesClient.attachments.bulkCreate({ caseId, attachments }), + }); + } catch (error) { + throw createCaseError({ + message: `Failed to bulk create attachments in route case id: ${request.params.case_id}: ${error}`, + error, + }); + } + }, +}); diff --git a/x-pack/plugins/cases/server/services/attachments/index.ts b/x-pack/plugins/cases/server/services/attachments/index.ts index 1b52a6c2bf153..d9656182a5219 100644 --- a/x-pack/plugins/cases/server/services/attachments/index.ts +++ b/x-pack/plugins/cases/server/services/attachments/index.ts @@ -51,6 +51,14 @@ interface CreateAttachmentArgs extends ClientArgs { id: string; } +interface BulkCreateAttachments extends ClientArgs { + attachments: Array<{ + attributes: AttachmentAttributes; + references: SavedObjectReference[]; + id: string; + }>; +} + interface UpdateArgs { attachmentId: string; updatedAttributes: AttachmentPatchAttributes; @@ -245,6 +253,18 @@ export class AttachmentService { } } + public async bulkCreate({ unsecuredSavedObjectsClient, attachments }: BulkCreateAttachments) { + try { + this.log.debug(`Attempting to bulk create attachments`); + return await unsecuredSavedObjectsClient.bulkCreate( + attachments.map((attachment) => ({ type: CASE_COMMENT_SAVED_OBJECT, ...attachment })) + ); + } catch (error) { + this.log.error(`Error on bulk create attachments: ${error}`); + throw error; + } + } + public async update({ unsecuredSavedObjectsClient, attachmentId, diff --git a/x-pack/plugins/cases/server/services/mocks.ts b/x-pack/plugins/cases/server/services/mocks.ts index 8e21db9ccb4e0..e402da2a09753 100644 --- a/x-pack/plugins/cases/server/services/mocks.ts +++ b/x-pack/plugins/cases/server/services/mocks.ts @@ -74,6 +74,7 @@ export const createUserActionServiceMock = (): CaseUserActionServiceMock => { bulkCreateCaseDeletion: jest.fn(), bulkCreateUpdateCase: jest.fn(), bulkCreateAttachmentDeletion: jest.fn(), + bulkCreateAttachmentCreation: jest.fn(), createUserAction: jest.fn(), create: jest.fn(), getAll: jest.fn(), @@ -102,6 +103,7 @@ export const createAttachmentServiceMock = (): AttachmentServiceMock => { get: jest.fn(), delete: jest.fn(), create: jest.fn(), + bulkCreate: jest.fn(), update: jest.fn(), bulkUpdate: jest.fn(), getAllAlertsAttachToCase: jest.fn(), diff --git a/x-pack/plugins/cases/server/services/user_actions/index.ts b/x-pack/plugins/cases/server/services/user_actions/index.ts index c80d2d6da4c98..b7de52d8708fa 100644 --- a/x-pack/plugins/cases/server/services/user_actions/index.ts +++ b/x-pack/plugins/cases/server/services/user_actions/index.ts @@ -91,7 +91,7 @@ interface BulkCreateBulkUpdateCaseUserActions extends ClientArgs { user: User; } -interface BulkCreateAttachmentDeletionUserAction extends Omit { +interface BulkCreateAttachmentUserAction extends Omit { attachments: Array<{ id: string; owner: string; attachment: CommentRequest }>; } @@ -241,18 +241,19 @@ export class CaseUserActionService { await this.bulkCreate({ unsecuredSavedObjectsClient, actions: userActionsWithReferences }); } - public async bulkCreateAttachmentDeletion({ + private async bulkCreateAttachment({ unsecuredSavedObjectsClient, caseId, attachments, user, - }: BulkCreateAttachmentDeletionUserAction): Promise { - this.log.debug(`Attempting to create a create case user action`); + action = Actions.create, + }: BulkCreateAttachmentUserAction): Promise { + this.log.debug(`Attempting to create a bulk create case user action`); const userActionsWithReferences = attachments.reduce( (acc, attachment) => { const userActionBuilder = this.builderFactory.getBuilder(ActionTypes.comment); - const deleteCommentUserAction = userActionBuilder?.build({ - action: Actions.delete, + const commentUserAction = userActionBuilder?.build({ + action, caseId, user, owner: attachment.owner, @@ -260,11 +261,11 @@ export class CaseUserActionService { payload: { attachment: attachment.attachment }, }); - if (deleteCommentUserAction == null) { + if (commentUserAction == null) { return acc; } - return [...acc, deleteCommentUserAction]; + return [...acc, commentUserAction]; }, [] ); @@ -272,6 +273,36 @@ export class CaseUserActionService { await this.bulkCreate({ unsecuredSavedObjectsClient, actions: userActionsWithReferences }); } + public async bulkCreateAttachmentDeletion({ + unsecuredSavedObjectsClient, + caseId, + attachments, + user, + }: BulkCreateAttachmentUserAction): Promise { + await this.bulkCreateAttachment({ + unsecuredSavedObjectsClient, + caseId, + attachments, + user, + action: Actions.delete, + }); + } + + public async bulkCreateAttachmentCreation({ + unsecuredSavedObjectsClient, + caseId, + attachments, + user, + }: BulkCreateAttachmentUserAction): Promise { + await this.bulkCreateAttachment({ + unsecuredSavedObjectsClient, + caseId, + attachments, + user, + action: Actions.create, + }); + } + public async createUserAction({ unsecuredSavedObjectsClient, action, diff --git a/x-pack/test/cases_api_integration/common/lib/utils.ts b/x-pack/test/cases_api_integration/common/lib/utils.ts index 9c56db80e45fc..824b49d28b76c 100644 --- a/x-pack/test/cases_api_integration/common/lib/utils.ts +++ b/x-pack/test/cases_api_integration/common/lib/utils.ts @@ -17,6 +17,7 @@ import type { Client } from '@elastic/elasticsearch'; import type SuperTest from 'supertest'; import { ObjectRemover as ActionsRemover } from '../../../alerting_api_integration/common/lib'; import { + CASES_INTERNAL_URL, CASES_URL, CASE_CONFIGURE_CONNECTORS_URL, CASE_CONFIGURE_URL, @@ -49,8 +50,10 @@ import { CasesByAlertId, CaseResolveResponse, CaseMetricsResponse, + BulkCreateCommentRequest, + CommentType, } from '../../../../plugins/cases/common/api'; -import { getPostCaseRequest } from './mock'; +import { getPostCaseRequest, postCaseReq } from './mock'; import { getCaseUserActionUrl } from '../../../../plugins/cases/common/api/helpers'; import { SignalHit } from '../../../../plugins/security_solution/server/lib/detection_engine/signals/types'; import { ActionResult, FindActionResult } from '../../../../plugins/actions/server/types'; @@ -656,6 +659,31 @@ export const createComment = async ({ return theCase; }; +export const bulkCreateAttachments = async ({ + supertest, + caseId, + params, + auth = { user: superUser, space: null }, + expectedHttpCode = 200, +}: { + supertest: SuperTest.SuperTest; + caseId: string; + params: BulkCreateCommentRequest; + auth?: { user: User; space: string | null }; + expectedHttpCode?: number; +}): Promise => { + const { body: theCase } = await supertest + .post( + `${getSpaceUrlPrefix(auth.space)}${CASES_INTERNAL_URL}/${caseId}/attachments/_bulk_create` + ) + .auth(auth.user.username, auth.user.password) + .set('kbn-xsrf', 'true') + .send(params) + .expect(expectedHttpCode); + + return theCase; +}; + export const updateCase = async ({ supertest, params, @@ -1143,3 +1171,48 @@ export const extractWarningValueFromWarningHeader = (warningHeader: string) => { const warningValue = warningHeader.substring(firstQuote + 1, lastQuote); return warningValue; }; + +export const getAttachments = (numberOfAttachments: number): BulkCreateCommentRequest => { + return [...Array(numberOfAttachments)].map((index) => { + if (index % 0) { + return { + type: CommentType.user, + comment: `Test ${index + 1}`, + owner: 'securitySolutionFixture', + }; + } + + return { + type: CommentType.alert, + alertId: `test-id-${index + 1}`, + index: `test-index-${index + 1}`, + rule: { + id: `rule-test-id-${index + 1}`, + name: `Test ${index + 1}`, + }, + owner: 'securitySolutionFixture', + }; + }); +}; + +export const createCaseAndBulkCreateAttachments = async ({ + supertest, + numberOfAttachments = 3, + auth = { user: superUser, space: null }, + expectedHttpCode = 200, +}: { + supertest: SuperTest.SuperTest; + numberOfAttachments?: number; + auth?: { user: User; space: string | null }; + expectedHttpCode?: number; +}): Promise<{ theCase: CaseResponse; attachments: BulkCreateCommentRequest }> => { + const postedCase = await createCase(supertest, postCaseReq); + const attachments = getAttachments(numberOfAttachments); + const patchedCase = await bulkCreateAttachments({ + supertest, + caseId: postedCase.id, + params: attachments, + }); + + return { theCase: patchedCase, attachments }; +}; diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/comments/post_comment.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/comments/post_comment.ts index 9c208139330ea..9c2dbb9d58d08 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/comments/post_comment.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/comments/post_comment.ts @@ -10,12 +10,12 @@ import expect from '@kbn/expect'; import { ALERT_WORKFLOW_STATUS } from '@kbn/rule-data-utils'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../../plugins/security_solution/common/constants'; import { CommentType, AttributesTypeUser, AttributesTypeAlerts, + CaseStatuses, } from '../../../../../../plugins/cases/common/api'; import { defaultUser, @@ -35,6 +35,7 @@ import { removeServerGeneratedPropertiesFromUserAction, removeServerGeneratedPropertiesFromSavedObject, superUserSpace1Auth, + updateCase, } from '../../../../common/lib/utils'; import { createSignalsIndex, @@ -263,34 +264,29 @@ export default ({ getService }: FtrProviderContext): void => { } }); - it('400s when case is missing', async () => { + it('404s when the case does not exist', async () => { await createComment({ supertest, caseId: 'not-exists', - params: { - // @ts-expect-error - bad: 'comment', - }, - expectedHttpCode: 400, + params: postCommentUserReq, + expectedHttpCode: 404, }); }); it('400s when adding an alert to a closed case', async () => { const postedCase = await createCase(supertest, postCaseReq); - - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ + await updateCase({ + supertest, + params: { cases: [ { id: postedCase.id, version: postedCase.version, - status: 'closed', + status: CaseStatuses.closed, }, ], - }) - .expect(200); + }, + }); await createComment({ supertest, @@ -313,78 +309,28 @@ export default ({ getService }: FtrProviderContext): void => { await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts'); }); - it('should change the status of the alert if sync alert is on', async () => { - const rule = getRuleForSignalTesting(['auditbeat-*']); - const postedCase = await createCase(supertest, postCaseReq); - - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: 'in-progress', - }, - ], - }) - .expect(200); - - const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 1, [id]); - const signals = await getSignalsByIds(supertest, log, [id]); - - const alert = signals.hits.hits[0]; - expect(alert._source?.[ALERT_WORKFLOW_STATUS]).eql('open'); - - await createComment({ - supertest, - caseId: postedCase.id, - params: { - alertId: alert._id, - index: alert._index, - rule: { - id: 'id', - name: 'name', - }, - owner: 'securitySolutionFixture', - type: CommentType.alert, - }, - }); - - await es.indices.refresh({ index: alert._index }); - - const { body: updatedAlert } = await supertest - .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) - .set('kbn-xsrf', 'true') - .send(getQuerySignalIds([alert._id])) - .expect(200); - - expect(updatedAlert.hits.hits[0]._source[ALERT_WORKFLOW_STATUS]).eql('acknowledged'); - }); - - it('should NOT change the status of the alert if sync alert is off', async () => { + const bulkCreateAlertsAndVerifyAlertStatus = async ( + syncAlerts: boolean, + expectedAlertStatus: string + ) => { const rule = getRuleForSignalTesting(['auditbeat-*']); const postedCase = await createCase(supertest, { ...postCaseReq, - settings: { syncAlerts: false }, + settings: { syncAlerts }, }); - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ + await updateCase({ + supertest, + params: { cases: [ { id: postedCase.id, version: postedCase.version, - status: 'in-progress', + status: CaseStatuses['in-progress'], }, ], - }) - .expect(200); + }, + }); const { id } = await createRule(supertest, log, rule); await waitForRuleSuccessOrStatus(supertest, log, id); @@ -417,7 +363,15 @@ export default ({ getService }: FtrProviderContext): void => { .send(getQuerySignalIds([alert._id])) .expect(200); - expect(updatedAlert.hits.hits[0]._source[ALERT_WORKFLOW_STATUS]).eql('open'); + expect(updatedAlert.hits.hits[0]._source[ALERT_WORKFLOW_STATUS]).eql(expectedAlertStatus); + }; + + it('should change the status of the alert if sync alert is on', async () => { + await bulkCreateAlertsAndVerifyAlertStatus(true, 'acknowledged'); + }); + + it('should NOT change the status of the alert if sync alert is off', async () => { + await bulkCreateAlertsAndVerifyAlertStatus(false, 'open'); }); }); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts index a4733d0c87038..f70c8593d3c94 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts @@ -39,6 +39,12 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./metrics/get_case_metrics_actions')); loadTestFile(require.resolve('./metrics/get_case_metrics_connectors')); + /** + * Internal routes + */ + + loadTestFile(require.resolve('./internal/bulk_create_attachments')); + // NOTE: Migrations are not included because they can inadvertently remove the .kibana indices which removes the users and spaces // which causes errors in any tests after them that relies on those }); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/bulk_create_attachments.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/bulk_create_attachments.ts new file mode 100644 index 0000000000000..73aefa716429d --- /dev/null +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/bulk_create_attachments.ts @@ -0,0 +1,584 @@ +/* + * 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 { omit } from 'lodash/fp'; +import expect from '@kbn/expect'; +import { ALERT_WORKFLOW_STATUS } from '@kbn/rule-data-utils'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../../plugins/security_solution/common/constants'; +import { + BulkCreateCommentRequest, + CaseResponse, + CaseStatuses, + CommentRequest, + CommentType, +} from '../../../../../../plugins/cases/common/api'; +import { + defaultUser, + postCaseReq, + postCommentUserReq, + postCommentAlertReq, + getPostCaseRequest, +} from '../../../../common/lib/mock'; +import { + deleteAllCaseItems, + createCase, + getCaseUserActions, + removeServerGeneratedPropertiesFromUserAction, + removeServerGeneratedPropertiesFromSavedObject, + superUserSpace1Auth, + createCaseAndBulkCreateAttachments, + bulkCreateAttachments, + updateCase, +} from '../../../../common/lib/utils'; +import { + createSignalsIndex, + deleteSignalsIndex, + deleteAllAlerts, + getRuleForSignalTesting, + waitForRuleSuccessOrStatus, + waitForSignalsToBePresent, + getSignalsByIds, + createRule, + getQuerySignalIds, +} from '../../../../../detection_engine_api_integration/utils'; +import { + globalRead, + noKibanaPrivileges, + obsOnly, + obsOnlyRead, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../common/lib/authentication/users'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + const log = getService('log'); + + const validateComments = ( + comments: CaseResponse['comments'], + attachments: BulkCreateCommentRequest + ) => { + comments?.forEach((attachment, index) => { + const comment = removeServerGeneratedPropertiesFromSavedObject(attachment); + + expect(comment).to.eql({ + ...attachments[index], + created_by: defaultUser, + pushed_at: null, + pushed_by: null, + updated_by: null, + }); + }); + }; + + describe('bulk_create_attachments', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + describe('creation', () => { + it('should no create an attachment on empty request', async () => { + const { theCase } = await createCaseAndBulkCreateAttachments({ + supertest, + numberOfAttachments: 0, + }); + + expect(theCase.comments?.length).to.be(0); + }); + + it('should create one attachment', async () => { + const { theCase, attachments } = await createCaseAndBulkCreateAttachments({ + supertest, + numberOfAttachments: 1, + }); + + validateComments(theCase.comments, attachments); + }); + + it('should bulk create multiple attachments', async () => { + const { theCase, attachments } = await createCaseAndBulkCreateAttachments({ + supertest, + }); + + expect(theCase.totalComment).to.eql(attachments.length); + expect(theCase.updated_by).to.eql(defaultUser); + + validateComments(theCase.comments, attachments); + }); + + it('creates the correct user action', async () => { + const { theCase, attachments } = await createCaseAndBulkCreateAttachments({ + supertest, + }); + + const userActions = await getCaseUserActions({ supertest, caseID: theCase.id }); + + userActions.slice(1).forEach((userAction, index) => { + const userActionWithoutServerGeneratedAttributes = + removeServerGeneratedPropertiesFromUserAction(userAction); + + expect(userActionWithoutServerGeneratedAttributes).to.eql({ + type: 'comment', + action: 'create', + created_by: defaultUser, + payload: { + comment: { + ...attachments[index], + }, + }, + case_id: theCase.id, + comment_id: theCase.comments?.find((comment) => comment.id === userAction.comment_id) + ?.id, + owner: 'securitySolutionFixture', + }); + }); + }); + }); + + describe('errors', () => { + it('400s when attempting to create a comment with a different owner than the case', async () => { + const postedCase = await createCase( + supertest, + getPostCaseRequest({ owner: 'securitySolutionFixture' }) + ); + + await bulkCreateAttachments({ + supertest, + caseId: postedCase.id, + params: [ + { + type: CommentType.user, + comment: 'test', + owner: 'securitySolutionFixture', + }, + { + type: CommentType.user, + comment: 'test', + owner: 'observabilityFixture', + }, + ], + expectedHttpCode: 400, + }); + }); + + it('400s when type is missing', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await bulkCreateAttachments({ + supertest, + caseId: postedCase.id, + params: [ + { + type: CommentType.user, + comment: 'test', + owner: 'securitySolutionFixture', + }, + { + // @ts-expect-error + bad: 'comment', + }, + ], + expectedHttpCode: 400, + }); + }); + + it('400s when missing attributes for type user', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await bulkCreateAttachments({ + supertest, + caseId: postedCase.id, + params: [ + { + type: CommentType.user, + comment: 'test', + owner: 'securitySolutionFixture', + }, + // @ts-expect-error + { + type: CommentType.user, + }, + ], + expectedHttpCode: 400, + }); + }); + + it('400s when adding excess attributes for type user', async () => { + const postedCase = await createCase(supertest, postCaseReq); + + for (const attribute of ['alertId', 'index']) { + await bulkCreateAttachments({ + supertest, + caseId: postedCase.id, + params: [ + { + type: CommentType.user, + comment: 'test', + owner: 'securitySolutionFixture', + }, + { + type: CommentType.user, + [attribute]: attribute, + comment: 'a comment', + owner: 'securitySolutionFixture', + }, + ], + expectedHttpCode: 400, + }); + } + }); + + it('400s when missing attributes for type alert', async () => { + const postedCase = await createCase(supertest, postCaseReq); + + const allRequestAttributes = { + type: CommentType.alert, + index: 'test-index', + alertId: 'test-id', + rule: { + id: 'id', + name: 'name', + }, + owner: 'securitySolutionFixture', + }; + + for (const attribute of ['alertId', 'index']) { + const requestAttributes = omit(attribute, allRequestAttributes); + await bulkCreateAttachments({ + supertest, + caseId: postedCase.id, + params: [ + { + type: CommentType.user, + comment: 'test', + owner: 'securitySolutionFixture', + }, + // @ts-expect-error + requestAttributes, + ], + expectedHttpCode: 400, + }); + } + }); + + it('400s when adding excess attributes for type alert', async () => { + const postedCase = await createCase(supertest, postCaseReq); + + for (const attribute of ['comment']) { + await bulkCreateAttachments({ + supertest, + caseId: postedCase.id, + params: [ + { + type: CommentType.user, + comment: 'test', + owner: 'securitySolutionFixture', + }, + { + type: CommentType.alert, + [attribute]: attribute, + alertId: 'test-id', + index: 'test-index', + rule: { + id: 'id', + name: 'name', + }, + owner: 'securitySolutionFixture', + }, + ], + expectedHttpCode: 400, + }); + } + }); + + it('404s when the case does not exist', async () => { + await bulkCreateAttachments({ + supertest, + caseId: 'not-exists', + params: [ + { + type: CommentType.user, + comment: 'test', + owner: 'securitySolutionFixture', + }, + ], + expectedHttpCode: 404, + }); + }); + + it('400s when adding an alert to a closed case', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses.closed, + }, + ], + }, + }); + + await bulkCreateAttachments({ + supertest, + caseId: postedCase.id, + params: [ + { + type: CommentType.alert, + alertId: 'test-id', + index: 'test-index', + rule: { + id: 'id', + name: 'name', + }, + owner: 'securitySolutionFixture', + }, + ], + expectedHttpCode: 400, + }); + }); + + it('400s when adding an alert with other attachments to a closed case', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses.closed, + }, + ], + }, + }); + + await createCaseAndBulkCreateAttachments({ supertest, expectedHttpCode: 400 }); + }); + }); + + describe('alerts', () => { + beforeEach(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts'); + await createSignalsIndex(supertest, log); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest, log); + await deleteAllAlerts(supertest, log); + await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts'); + }); + + const bulkCreateAlertsAndVerifyAlertStatus = async ( + syncAlerts: boolean, + expectedAlertStatus: string + ) => { + const rule = getRuleForSignalTesting(['auditbeat-*']); + const postedCase = await createCase(supertest, { + ...postCaseReq, + settings: { syncAlerts }, + }); + + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses['in-progress'], + }, + ], + }, + }); + + const { id } = await createRule(supertest, log, rule); + await waitForRuleSuccessOrStatus(supertest, log, id); + await waitForSignalsToBePresent(supertest, log, 1, [id]); + const signals = await getSignalsByIds(supertest, log, [id]); + const attachments: CommentRequest[] = []; + const indices: string[] = []; + const ids: string[] = []; + + signals.hits.hits.forEach((alert) => { + expect(alert._source?.[ALERT_WORKFLOW_STATUS]).eql('open'); + attachments.push({ + alertId: alert._id, + index: alert._index, + rule: { + id: 'id', + name: 'name', + }, + owner: 'securitySolutionFixture', + type: CommentType.alert, + }); + + indices.push(alert._index); + ids.push(alert._id); + }); + + await bulkCreateAttachments({ + supertest, + caseId: postedCase.id, + params: attachments, + }); + + await es.indices.refresh({ index: indices }); + + const { body: updatedAlerts } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQuerySignalIds(ids)) + .expect(200); + + updatedAlerts.hits.hits.forEach( + (alert: { _source: { 'kibana.alert.workflow_status': string } }) => { + expect(alert._source[ALERT_WORKFLOW_STATUS]).eql(expectedAlertStatus); + } + ); + }; + + it('should change the status of the alerts if sync alert is on', async () => { + await bulkCreateAlertsAndVerifyAlertStatus(true, 'acknowledged'); + }); + + it('should NOT change the status of the alert if sync alert is off', async () => { + await bulkCreateAlertsAndVerifyAlertStatus(false, 'open'); + }); + }); + + describe('alert format', () => { + type AlertComment = CommentType.alert; + + for (const [alertId, index, type] of [ + ['1', ['index1', 'index2'], CommentType.alert], + [['1', '2'], 'index', CommentType.alert], + ]) { + it(`throws an error with an alert comment with contents id: ${alertId} indices: ${index} type: ${type}`, async () => { + const postedCase = await createCase(supertest, postCaseReq); + await bulkCreateAttachments({ + supertest, + caseId: postedCase.id, + params: [{ ...postCommentAlertReq, alertId, index, type: type as AlertComment }], + expectedHttpCode: 400, + }); + }); + } + + it('does not throw an error with correct alert formatting', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const attachments = [ + { + ...postCommentAlertReq, + alertId: '1', + index: ['index1'], + type: CommentType.alert as const, + }, + { + ...postCommentAlertReq, + alertId: ['1', '2'], + index: ['index', 'other-index'], + type: CommentType.alert as const, + }, + ]; + + await bulkCreateAttachments({ + supertest, + caseId: postedCase.id, + params: attachments, + expectedHttpCode: 200, + }); + }); + }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should bulk create attachments when the user has the correct permissions for that owner', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + superUserSpace1Auth + ); + + await bulkCreateAttachments({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: [postCommentUserReq], + auth: { user: secOnly, space: 'space1' }, + }); + }); + + it('should not create a comment when the user does not have permissions for that owner', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { user: obsOnly, space: 'space1' } + ); + + await bulkCreateAttachments({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: [{ ...postCommentUserReq, owner: 'observabilityFixture' }], + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should not create a comment`, async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + superUserSpace1Auth + ); + + await bulkCreateAttachments({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: [postCommentUserReq], + auth: { user, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + } + + it('should not create a comment in a space the user does not have permissions for', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space2' } + ); + + await bulkCreateAttachments({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: [postCommentUserReq], + auth: { user: secOnly, space: 'space2' }, + expectedHttpCode: 403, + }); + }); + }); + }); +}; diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/common/comments/post_comment.ts b/x-pack/test/cases_api_integration/spaces_only/tests/common/comments/post_comment.ts index 72356d0d314ff..22c7aa6925bcb 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/common/comments/post_comment.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/common/comments/post_comment.ts @@ -11,13 +11,11 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { AttributesTypeUser } from '../../../../../../plugins/cases/common/api'; import { nullUser, postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; import { - deleteCasesByESQuery, - deleteCasesUserActions, - deleteComments, createCase, createComment, removeServerGeneratedPropertiesFromSavedObject, getAuthWithSuperUser, + deleteAllCaseItems, } from '../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export @@ -28,9 +26,7 @@ export default ({ getService }: FtrProviderContext): void => { describe('post_comment', () => { afterEach(async () => { - await deleteCasesByESQuery(es); - await deleteComments(es); - await deleteCasesUserActions(es); + await deleteAllCaseItems(es); }); it('should post a comment in space1', async () => { diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/common/index.ts b/x-pack/test/cases_api_integration/spaces_only/tests/common/index.ts index 251a545f10681..0b18a56bdcd11 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/common/index.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/common/index.ts @@ -29,5 +29,10 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./configure/get_configure')); loadTestFile(require.resolve('./configure/patch_configure')); loadTestFile(require.resolve('./configure/post_configure')); + + /** + * Internal routes + */ + loadTestFile(require.resolve('./internal/bulk_create_attachments')); }); }; diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/common/internal/bulk_create_attachments.ts b/x-pack/test/cases_api_integration/spaces_only/tests/common/internal/bulk_create_attachments.ts new file mode 100644 index 0000000000000..c713c3bf74e4b --- /dev/null +++ b/x-pack/test/cases_api_integration/spaces_only/tests/common/internal/bulk_create_attachments.ts @@ -0,0 +1,71 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { AttributesTypeUser } from '../../../../../../plugins/cases/common/api'; +import { nullUser, postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; +import { + createCase, + removeServerGeneratedPropertiesFromSavedObject, + getAuthWithSuperUser, + bulkCreateAttachments, + deleteAllCaseItems, +} from '../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); + + describe('bulk_create_attachments', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should bulk create attachments in space1', async () => { + const postedCase = await createCase(supertestWithoutAuth, postCaseReq, 200, authSpace1); + const patchedCase = await bulkCreateAttachments({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: [postCommentUserReq], + auth: authSpace1, + }); + + const comment = removeServerGeneratedPropertiesFromSavedObject( + patchedCase.comments![0] as AttributesTypeUser + ); + + expect(comment).to.eql({ + type: postCommentUserReq.type, + comment: postCommentUserReq.comment, + created_by: nullUser, + pushed_at: null, + pushed_by: null, + updated_by: null, + owner: 'securitySolutionFixture', + }); + + // updates the case correctly after adding a comment + expect(patchedCase.totalComment).to.eql(patchedCase.comments!.length); + expect(patchedCase.updated_by).to.eql(nullUser); + }); + + it('should not post a comment on a case in a different space', async () => { + const postedCase = await createCase(supertestWithoutAuth, postCaseReq, 200, authSpace1); + await bulkCreateAttachments({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: [postCommentUserReq], + auth: getAuthWithSuperUser('space2'), + expectedHttpCode: 404, + }); + }); + }); +};