From 5c879fbd0114bb8143fe83d4fce1a48f1e2e70b0 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Tue, 6 Apr 2021 17:16:16 -0400 Subject: [PATCH 1/4] Adding sub cases client --- x-pack/plugins/cases/server/client/client.ts | 7 ++ .../cases/server/client/sub_cases/client.ts | 72 +++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 x-pack/plugins/cases/server/client/sub_cases/client.ts diff --git a/x-pack/plugins/cases/server/client/client.ts b/x-pack/plugins/cases/server/client/client.ts index 5f6cb8851c34c..5900fd3f8a706 100644 --- a/x-pack/plugins/cases/server/client/client.ts +++ b/x-pack/plugins/cases/server/client/client.ts @@ -10,18 +10,21 @@ import { CasesSubClient, createCasesSubClient } from './cases/client'; import { AttachmentsSubClient, createAttachmentsSubClient } from './attachments/client'; import { UserActionsSubClient, createUserActionsSubClient } from './user_actions/client'; import { CasesClientInternal, createCasesClientInternal } from './client_internal'; +import { SubCasesClient } from './sub_cases/client'; export class CasesClient { private readonly _casesClientInternal: CasesClientInternal; private readonly _cases: CasesSubClient; private readonly _attachments: AttachmentsSubClient; private readonly _userActions: UserActionsSubClient; + private readonly _subCases: SubCasesClient; constructor(args: CasesClientArgs) { this._casesClientInternal = createCasesClientInternal(args); this._cases = createCasesSubClient(args, this, this._casesClientInternal); this._attachments = createAttachmentsSubClient(args, this._casesClientInternal); this._userActions = createUserActionsSubClient(args); + this._subCases = new SubCasesClient(args); } public get cases() { @@ -36,6 +39,10 @@ export class CasesClient { return this._userActions; } + public get subCases() { + return this._subCases; + } + // TODO: Remove it when all routes will be moved to the cases client. public get casesClientInternal() { return this._casesClientInternal; diff --git a/x-pack/plugins/cases/server/client/sub_cases/client.ts b/x-pack/plugins/cases/server/client/sub_cases/client.ts new file mode 100644 index 0000000000000..f3f57cd3302d0 --- /dev/null +++ b/x-pack/plugins/cases/server/client/sub_cases/client.ts @@ -0,0 +1,72 @@ +/* + * 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 { SavedObjectsClientContract } from 'kibana/server'; +import { SubCaseResponseRt } from '../../../common/api'; +import { CasesClientArgs } from '..'; +import { flattenSubCaseSavedObject } from '../../routes/api/utils'; +import { countAlertsForID } from '../../common'; +import { createCaseError } from '../../common/error'; + +/** + * Client for handling the different exposed API routes for interacting with sub cases. + */ +export class SubCasesClient { + constructor(private readonly args: CasesClientArgs) {} + + public async get({ + includeComments, + id, + soClient, + }: { + includeComments: boolean; + id: string; + soClient: SavedObjectsClientContract; + }) { + try { + const subCase = await this.args.caseService.getSubCase({ + soClient, + id, + }); + + if (!includeComments) { + return SubCaseResponseRt.encode( + flattenSubCaseSavedObject({ + savedObject: subCase, + }) + ); + } + + const theComments = await this.args.caseService.getAllSubCaseComments({ + soClient, + id, + options: { + sortField: 'created_at', + sortOrder: 'asc', + }, + }); + + return SubCaseResponseRt.encode( + flattenSubCaseSavedObject({ + savedObject: subCase, + comments: theComments.saved_objects, + totalComment: theComments.total, + totalAlerts: countAlertsForID({ + comments: theComments, + id, + }), + }) + ); + } catch (error) { + throw createCaseError({ + message: `Failed to get sub case id: ${id}: ${error}`, + error, + logger: this.args.logger, + }); + } + } +} From 25384fc7ab2d59e51802bac46d0420843b552015 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Wed, 7 Apr 2021 13:03:47 -0400 Subject: [PATCH 2/4] Move sub case routes to case client --- .../cases/common/api/cases/sub_case.ts | 1 + x-pack/plugins/cases/server/client/client.ts | 4 +- .../cases/server/client/sub_cases/client.ts | 263 +++++++++-- .../cases/server/client/sub_cases/update.ts | 407 ++++++++++++++++ .../api/cases/sub_case/delete_sub_cases.ts | 68 +-- .../api/cases/sub_case/find_sub_cases.ts | 60 +-- .../routes/api/cases/sub_case/get_sub_case.ts | 47 +- .../api/cases/sub_case/patch_sub_cases.ts | 437 +----------------- 8 files changed, 656 insertions(+), 631 deletions(-) create mode 100644 x-pack/plugins/cases/server/client/sub_cases/update.ts diff --git a/x-pack/plugins/cases/common/api/cases/sub_case.ts b/x-pack/plugins/cases/common/api/cases/sub_case.ts index 4bbdfd5b7d368..ba6cd6a8affa4 100644 --- a/x-pack/plugins/cases/common/api/cases/sub_case.ts +++ b/x-pack/plugins/cases/common/api/cases/sub_case.ts @@ -79,3 +79,4 @@ export type SubCasesResponse = rt.TypeOf; export type SubCasesFindResponse = rt.TypeOf; export type SubCasePatchRequest = rt.TypeOf; export type SubCasesPatchRequest = rt.TypeOf; +export type SubCasesFindRequest = rt.TypeOf; diff --git a/x-pack/plugins/cases/server/client/client.ts b/x-pack/plugins/cases/server/client/client.ts index 5900fd3f8a706..ca30200b95ea8 100644 --- a/x-pack/plugins/cases/server/client/client.ts +++ b/x-pack/plugins/cases/server/client/client.ts @@ -10,7 +10,7 @@ import { CasesSubClient, createCasesSubClient } from './cases/client'; import { AttachmentsSubClient, createAttachmentsSubClient } from './attachments/client'; import { UserActionsSubClient, createUserActionsSubClient } from './user_actions/client'; import { CasesClientInternal, createCasesClientInternal } from './client_internal'; -import { SubCasesClient } from './sub_cases/client'; +import { createSubCasesClient, SubCasesClient } from './sub_cases/client'; export class CasesClient { private readonly _casesClientInternal: CasesClientInternal; @@ -24,7 +24,7 @@ export class CasesClient { this._cases = createCasesSubClient(args, this, this._casesClientInternal); this._attachments = createAttachmentsSubClient(args, this._casesClientInternal); this._userActions = createUserActionsSubClient(args); - this._subCases = new SubCasesClient(args); + this._subCases = createSubCasesClient(args, this); } public get cases() { diff --git a/x-pack/plugins/cases/server/client/sub_cases/client.ts b/x-pack/plugins/cases/server/client/sub_cases/client.ts index f3f57cd3302d0..1ed328d0a8be6 100644 --- a/x-pack/plugins/cases/server/client/sub_cases/client.ts +++ b/x-pack/plugins/cases/server/client/sub_cases/client.ts @@ -5,68 +5,233 @@ * 2.0. */ +import Boom from '@hapi/boom'; + import { SavedObjectsClientContract } from 'kibana/server'; -import { SubCaseResponseRt } from '../../../common/api'; +import { + caseStatuses, + SubCaseResponse, + SubCaseResponseRt, + SubCasesFindRequest, + SubCasesFindResponse, + SubCasesFindResponseRt, + SubCasesResponse, + User, +} from '../../../common/api'; import { CasesClientArgs } from '..'; -import { flattenSubCaseSavedObject } from '../../routes/api/utils'; +import { flattenSubCaseSavedObject, transformSubCases } from '../../routes/api/utils'; import { countAlertsForID } from '../../common'; import { createCaseError } from '../../common/error'; +import { CASE_SAVED_OBJECT } from '../../../common/constants'; +import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; +import { constructQueryOptions } from '../../routes/api/cases/helpers'; +import { defaultPage, defaultPerPage } from '../../routes/api'; +import { CasesClient } from '../client'; +import { update, UpdateArgs } from './update'; + +interface DeleteArgs { + soClient: SavedObjectsClientContract; + ids: string[]; + user: User; +} + +interface FindArgs { + soClient: SavedObjectsClientContract; + caseID: string; + queryParams: SubCasesFindRequest; +} + +interface GetArgs { + includeComments: boolean; + id: string; + soClient: SavedObjectsClientContract; +} /** - * Client for handling the different exposed API routes for interacting with sub cases. + * The API routes for interacting with sub cases. */ -export class SubCasesClient { - constructor(private readonly args: CasesClientArgs) {} - - public async get({ - includeComments, - id, - soClient, - }: { - includeComments: boolean; - id: string; - soClient: SavedObjectsClientContract; - }) { - try { - const subCase = await this.args.caseService.getSubCase({ - soClient, - id, - }); - - if (!includeComments) { - return SubCaseResponseRt.encode( - flattenSubCaseSavedObject({ - savedObject: subCase, - }) - ); - } - - const theComments = await this.args.caseService.getAllSubCaseComments({ - soClient, - id, - options: { - sortField: 'created_at', - sortOrder: 'asc', - }, - }); +export interface SubCasesClient { + delete(deleteArgs: DeleteArgs): Promise; + find(findArgs: FindArgs): Promise; + get(getArgs: GetArgs): Promise; + update(updateArgs: UpdateArgs): Promise; +} + +/** + * Creates a client for handling the different exposed API routes for interacting with sub cases. + */ +export function createSubCasesClient( + clientArgs: CasesClientArgs, + casesClient: CasesClient +): SubCasesClient { + return Object.freeze({ + delete: (deleteArgs: DeleteArgs) => deleteSubCase(deleteArgs, clientArgs), + find: (findArgs: FindArgs) => find(findArgs, clientArgs), + get: (getArgs: GetArgs) => get(getArgs, clientArgs), + update: (updateArgs: UpdateArgs) => update(updateArgs, clientArgs, casesClient), + }); +} + +async function deleteSubCase( + { soClient, ids, user }: DeleteArgs, + clientArgs: CasesClientArgs +): Promise { + try { + const [comments, subCases] = await Promise.all([ + clientArgs.caseService.getAllSubCaseComments({ soClient, id: ids }), + clientArgs.caseService.getSubCases({ soClient, ids }), + ]); + + const subCaseErrors = subCases.saved_objects.filter((subCase) => subCase.error !== undefined); + + if (subCaseErrors.length > 0) { + throw Boom.notFound( + `These sub cases ${subCaseErrors + .map((c) => c.id) + .join(', ')} do not exist. Please check you have the correct ids.` + ); + } + const subCaseIDToParentID = subCases.saved_objects.reduce((acc, subCase) => { + const parentID = subCase.references.find((ref) => ref.type === CASE_SAVED_OBJECT); + acc.set(subCase.id, parentID?.id); + return acc; + }, new Map()); + + await Promise.all( + comments.saved_objects.map((comment) => + clientArgs.attachmentService.delete({ soClient, attachmentId: comment.id }) + ) + ); + + await Promise.all(ids.map((id) => clientArgs.caseService.deleteSubCase(soClient, id))); + + const deleteDate = new Date().toISOString(); + + await clientArgs.userActionService.bulkCreate({ + soClient, + actions: ids.map((id) => + buildCaseUserActionItem({ + action: 'delete', + actionAt: deleteDate, + actionBy: user, + // if for some reason the sub case didn't have a reference to its parent, we'll still log a user action + // but we won't have the case ID + caseId: subCaseIDToParentID.get(id) ?? '', + subCaseId: id, + fields: ['sub_case', 'comment', 'status'], + }) + ), + }); + } catch (error) { + throw createCaseError({ + message: `Failed to delete sub cases ids: ${JSON.stringify(ids)}: ${error}`, + error, + logger: clientArgs.logger, + }); + } +} + +async function find( + { soClient, caseID, queryParams }: FindArgs, + clientArgs: CasesClientArgs +): Promise { + try { + const ids = [caseID]; + const { subCase: subCaseQueryOptions } = constructQueryOptions({ + status: queryParams.status, + sortByField: queryParams.sortField, + }); + + const subCases = await clientArgs.caseService.findSubCasesGroupByCase({ + soClient, + ids, + options: { + sortField: 'created_at', + page: defaultPage, + perPage: defaultPerPage, + ...queryParams, + ...subCaseQueryOptions, + }, + }); + + const [open, inProgress, closed] = await Promise.all([ + ...caseStatuses.map((status) => { + const { subCase: statusQueryOptions } = constructQueryOptions({ + status, + sortByField: queryParams.sortField, + }); + return clientArgs.caseService.findSubCaseStatusStats({ + soClient, + options: statusQueryOptions ?? {}, + ids, + }); + }), + ]); + + return SubCasesFindResponseRt.encode( + transformSubCases({ + page: subCases.page, + perPage: subCases.perPage, + total: subCases.total, + subCasesMap: subCases.subCasesMap, + open, + inProgress, + closed, + }) + ); + } catch (error) { + throw createCaseError({ + message: `Failed to find sub cases for case id: ${caseID}: ${error}`, + error, + logger: clientArgs.logger, + }); + } +} + +async function get( + { includeComments, id, soClient }: GetArgs, + clientArgs: CasesClientArgs +): Promise { + try { + const subCase = await clientArgs.caseService.getSubCase({ + soClient, + id, + }); + + if (!includeComments) { return SubCaseResponseRt.encode( flattenSubCaseSavedObject({ savedObject: subCase, - comments: theComments.saved_objects, - totalComment: theComments.total, - totalAlerts: countAlertsForID({ - comments: theComments, - id, - }), }) ); - } catch (error) { - throw createCaseError({ - message: `Failed to get sub case id: ${id}: ${error}`, - error, - logger: this.args.logger, - }); } + + const theComments = await clientArgs.caseService.getAllSubCaseComments({ + soClient, + id, + options: { + sortField: 'created_at', + sortOrder: 'asc', + }, + }); + + return SubCaseResponseRt.encode( + flattenSubCaseSavedObject({ + savedObject: subCase, + comments: theComments.saved_objects, + totalComment: theComments.total, + totalAlerts: countAlertsForID({ + comments: theComments, + id, + }), + }) + ); + } catch (error) { + throw createCaseError({ + message: `Failed to get sub case id: ${id}: ${error}`, + error, + logger: clientArgs.logger, + }); } } diff --git a/x-pack/plugins/cases/server/client/sub_cases/update.ts b/x-pack/plugins/cases/server/client/sub_cases/update.ts new file mode 100644 index 0000000000000..a328e6dbb0d15 --- /dev/null +++ b/x-pack/plugins/cases/server/client/sub_cases/update.ts @@ -0,0 +1,407 @@ +/* + * 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 { + SavedObjectsClientContract, + SavedObject, + SavedObjectsFindResponse, + Logger, +} from 'kibana/server'; + +import { nodeBuilder } from '../../../../../../src/plugins/data/common'; +import { CasesClient } from '../../client'; +import { CaseService } from '../../services'; +import { + CaseStatuses, + SubCasesPatchRequest, + SubCasesPatchRequestRt, + CommentType, + excess, + throwErrors, + SubCasesResponse, + SubCasePatchRequest, + SubCaseAttributes, + ESCaseAttributes, + SubCaseResponse, + SubCasesResponseRt, + User, + CommentAttributes, +} from '../../../common/api'; +import { CASE_COMMENT_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../common/constants'; +import { + flattenSubCaseSavedObject, + isCommentRequestTypeAlertOrGenAlert, +} from '../../routes/api/utils'; +import { getCaseToUpdate } from '../../routes/api/cases/helpers'; +import { buildSubCaseUserActions } from '../../services/user_actions/helpers'; +import { createAlertUpdateRequest } from '../../common'; +import { createCaseError } from '../../common/error'; +import { UpdateAlertRequest } from '../../client/alerts/client'; +import { CasesClientArgs } from '../types'; + +/** + * The parameters that come from the API route itself. + */ +export interface UpdateArgs { + soClient: SavedObjectsClientContract; + user: User; + subCases: SubCasesPatchRequest; +} + +function checkNonExistingOrConflict( + toUpdate: SubCasePatchRequest[], + fromStorage: Map> +) { + const nonExistingSubCases: SubCasePatchRequest[] = []; + const conflictedSubCases: SubCasePatchRequest[] = []; + for (const subCaseToUpdate of toUpdate) { + const bulkEntry = fromStorage.get(subCaseToUpdate.id); + + if (bulkEntry && bulkEntry.error) { + nonExistingSubCases.push(subCaseToUpdate); + } + + if (!bulkEntry || bulkEntry.version !== subCaseToUpdate.version) { + conflictedSubCases.push(subCaseToUpdate); + } + } + + if (nonExistingSubCases.length > 0) { + throw Boom.notFound( + `These sub cases ${nonExistingSubCases + .map((c) => c.id) + .join(', ')} do not exist. Please check you have the correct ids.` + ); + } + + if (conflictedSubCases.length > 0) { + throw Boom.conflict( + `These sub cases ${conflictedSubCases + .map((c) => c.id) + .join(', ')} has been updated. Please refresh before saving additional updates.` + ); + } +} + +interface GetParentIDsResult { + ids: string[]; + parentIDToSubID: Map; +} + +function getParentIDs({ + subCasesMap, + subCaseIDs, +}: { + subCasesMap: Map>; + subCaseIDs: string[]; +}): GetParentIDsResult { + return subCaseIDs.reduce( + (acc, id) => { + const subCase = subCasesMap.get(id); + if (subCase && subCase.references.length > 0) { + const parentID = subCase.references[0].id; + acc.ids.push(parentID); + let subIDs = acc.parentIDToSubID.get(parentID); + if (subIDs === undefined) { + subIDs = []; + } + subIDs.push(id); + acc.parentIDToSubID.set(parentID, subIDs); + } + return acc; + }, + { ids: [], parentIDToSubID: new Map() } + ); +} + +async function getParentCases({ + caseService, + soClient, + subCaseIDs, + subCasesMap, +}: { + caseService: CaseService; + soClient: SavedObjectsClientContract; + subCaseIDs: string[]; + subCasesMap: Map>; +}): Promise>> { + const parentIDInfo = getParentIDs({ subCaseIDs, subCasesMap }); + + const parentCases = await caseService.getCases({ + soClient, + caseIds: parentIDInfo.ids, + }); + + const parentCaseErrors = parentCases.saved_objects.filter((so) => so.error !== undefined); + + if (parentCaseErrors.length > 0) { + throw Boom.badRequest( + `Unable to find parent cases: ${parentCaseErrors + .map((c) => c.id) + .join(', ')} for sub cases: ${subCaseIDs.join(', ')}` + ); + } + + return parentCases.saved_objects.reduce((acc, so) => { + const subCaseIDsWithParent = parentIDInfo.parentIDToSubID.get(so.id); + subCaseIDsWithParent?.forEach((subCaseId) => { + acc.set(subCaseId, so); + }); + return acc; + }, new Map>()); +} + +function getValidUpdateRequests( + toUpdate: SubCasePatchRequest[], + subCasesMap: Map> +): SubCasePatchRequest[] { + const validatedSubCaseAttributes: SubCasePatchRequest[] = toUpdate.map((updateCase) => { + const currentCase = subCasesMap.get(updateCase.id); + return currentCase != null + ? getCaseToUpdate(currentCase.attributes, { + ...updateCase, + }) + : { id: updateCase.id, version: updateCase.version }; + }); + + return validatedSubCaseAttributes.filter((updateCase: SubCasePatchRequest) => { + const { id, version, ...updateCaseAttributes } = updateCase; + return Object.keys(updateCaseAttributes).length > 0; + }); +} + +/** + * Get the id from a reference in a comment for a sub case + */ +function getID(comment: SavedObject): string | undefined { + return comment.references.find((ref) => ref.type === SUB_CASE_SAVED_OBJECT)?.id; +} + +/** + * Get all the alert comments for a set of sub cases + */ +async function getAlertComments({ + subCasesToSync, + caseService, + soClient, +}: { + subCasesToSync: SubCasePatchRequest[]; + caseService: CaseService; + soClient: SavedObjectsClientContract; +}): Promise> { + const ids = subCasesToSync.map((subCase) => subCase.id); + return caseService.getAllSubCaseComments({ + soClient, + id: ids, + options: { + filter: nodeBuilder.or([ + nodeBuilder.is(`${CASE_COMMENT_SAVED_OBJECT}.attributes.type`, CommentType.alert), + nodeBuilder.is(`${CASE_COMMENT_SAVED_OBJECT}.attributes.type`, CommentType.generatedAlert), + ]), + }, + }); +} + +/** + * Updates the status of alerts for the specified sub cases. + */ +async function updateAlerts({ + caseService, + soClient, + casesClient, + logger, + subCasesToSync, +}: { + caseService: CaseService; + soClient: SavedObjectsClientContract; + casesClient: CasesClient; + logger: Logger; + subCasesToSync: SubCasePatchRequest[]; +}) { + try { + const subCasesToSyncMap = subCasesToSync.reduce((acc, subCase) => { + acc.set(subCase.id, subCase); + return acc; + }, new Map()); + // get all the alerts for all sub cases that need to be synced + const totalAlerts = await getAlertComments({ caseService, soClient, subCasesToSync }); + // create a map of the status (open, closed, etc) to alert info that needs to be updated + const alertsToUpdate = totalAlerts.saved_objects.reduce( + (acc: UpdateAlertRequest[], alertComment) => { + if (isCommentRequestTypeAlertOrGenAlert(alertComment.attributes)) { + const id = getID(alertComment); + const status = + id !== undefined + ? subCasesToSyncMap.get(id)?.status ?? CaseStatuses.open + : CaseStatuses.open; + + acc.push(...createAlertUpdateRequest({ comment: alertComment.attributes, status })); + } + return acc; + }, + [] + ); + + await casesClient.casesClientInternal.alerts.updateStatus({ alerts: alertsToUpdate }); + } catch (error) { + throw createCaseError({ + message: `Failed to update alert status while updating sub cases: ${JSON.stringify( + subCasesToSync + )}: ${error}`, + logger, + error, + }); + } +} + +/** + * Handles updating the fields in a sub case. + */ +export async function update( + { soClient, user, subCases }: UpdateArgs, + clientArgs: CasesClientArgs, + casesClient: CasesClient +): Promise { + const query = pipe( + excess(SubCasesPatchRequestRt).decode(subCases), + fold(throwErrors(Boom.badRequest), identity) + ); + + try { + const bulkSubCases = await clientArgs.caseService.getSubCases({ + soClient, + ids: query.subCases.map((q) => q.id), + }); + + const subCasesMap = bulkSubCases.saved_objects.reduce((acc, so) => { + acc.set(so.id, so); + return acc; + }, new Map>()); + + checkNonExistingOrConflict(query.subCases, subCasesMap); + + const nonEmptySubCaseRequests = getValidUpdateRequests(query.subCases, subCasesMap); + + if (nonEmptySubCaseRequests.length <= 0) { + throw Boom.notAcceptable('All update fields are identical to current version.'); + } + + const subIDToParentCase = await getParentCases({ + soClient, + caseService: clientArgs.caseService, + subCaseIDs: nonEmptySubCaseRequests.map((subCase) => subCase.id), + subCasesMap, + }); + + const updatedAt = new Date().toISOString(); + const updatedCases = await clientArgs.caseService.patchSubCases({ + soClient, + subCases: nonEmptySubCaseRequests.map((thisCase) => { + const { id: subCaseId, version, ...updateSubCaseAttributes } = thisCase; + let closedInfo: { closed_at: string | null; closed_by: User | null } = { + closed_at: null, + closed_by: null, + }; + + if ( + updateSubCaseAttributes.status && + updateSubCaseAttributes.status === CaseStatuses.closed + ) { + closedInfo = { + closed_at: updatedAt, + closed_by: user, + }; + } else if ( + updateSubCaseAttributes.status && + (updateSubCaseAttributes.status === CaseStatuses.open || + updateSubCaseAttributes.status === CaseStatuses['in-progress']) + ) { + closedInfo = { + closed_at: null, + closed_by: null, + }; + } + return { + subCaseId, + updatedAttributes: { + ...updateSubCaseAttributes, + ...closedInfo, + updated_at: updatedAt, + updated_by: user, + }, + version, + }; + }), + }); + + const subCasesToSyncAlertsFor = nonEmptySubCaseRequests.filter((subCaseToUpdate) => { + const storedSubCase = subCasesMap.get(subCaseToUpdate.id); + const parentCase = subIDToParentCase.get(subCaseToUpdate.id); + return ( + storedSubCase !== undefined && + subCaseToUpdate.status !== undefined && + storedSubCase.attributes.status !== subCaseToUpdate.status && + parentCase?.attributes.settings.syncAlerts + ); + }); + + await updateAlerts({ + caseService: clientArgs.caseService, + soClient, + casesClient, + subCasesToSync: subCasesToSyncAlertsFor, + logger: clientArgs.logger, + }); + + const returnUpdatedSubCases = updatedCases.saved_objects.reduce( + (acc, updatedSO) => { + const originalSubCase = subCasesMap.get(updatedSO.id); + if (originalSubCase) { + acc.push( + flattenSubCaseSavedObject({ + savedObject: { + ...originalSubCase, + ...updatedSO, + attributes: { ...originalSubCase.attributes, ...updatedSO.attributes }, + references: originalSubCase.references, + version: updatedSO.version ?? originalSubCase.version, + }, + }) + ); + } + return acc; + }, + [] + ); + + await clientArgs.userActionService.bulkCreate({ + soClient, + actions: buildSubCaseUserActions({ + originalSubCases: bulkSubCases.saved_objects, + updatedSubCases: updatedCases.saved_objects, + actionDate: updatedAt, + actionBy: user, + }), + }); + + return SubCasesResponseRt.encode(returnUpdatedSubCases); + } catch (error) { + const idVersions = query.subCases.map((subCase) => ({ + id: subCase.id, + version: subCase.version, + })); + throw createCaseError({ + message: `Failed to update sub cases: ${JSON.stringify(idVersions)}: ${error}`, + error, + logger: clientArgs.logger, + }); + } +} diff --git a/x-pack/plugins/cases/server/routes/api/cases/sub_case/delete_sub_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/sub_case/delete_sub_cases.ts index 15eb5a421358b..3f05dc1aa989b 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/sub_case/delete_sub_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/sub_case/delete_sub_cases.ts @@ -5,24 +5,12 @@ * 2.0. */ -import Boom from '@hapi/boom'; import { schema } from '@kbn/config-schema'; -import { buildCaseUserActionItem } from '../../../../services/user_actions/helpers'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; -import { - SUB_CASES_PATCH_DEL_URL, - SAVED_OBJECT_TYPES, - CASE_SAVED_OBJECT, -} from '../../../../../common/constants'; +import { SUB_CASES_PATCH_DEL_URL, SAVED_OBJECT_TYPES } from '../../../../../common/constants'; -export function initDeleteSubCasesApi({ - attachmentService, - caseService, - router, - userActionService, - logger, -}: RouteDeps) { +export function initDeleteSubCasesApi({ caseService, router, logger }: RouteDeps) { router.delete( { path: SUB_CASES_PATCH_DEL_URL, @@ -38,56 +26,10 @@ export function initDeleteSubCasesApi({ includedHiddenTypes: SAVED_OBJECT_TYPES, }); - const [comments, subCases] = await Promise.all([ - caseService.getAllSubCaseComments({ soClient, id: request.query.ids }), - caseService.getSubCases({ soClient, ids: request.query.ids }), - ]); + const user = caseService.getUser({ request }); - const subCaseErrors = subCases.saved_objects.filter( - (subCase) => subCase.error !== undefined - ); - - if (subCaseErrors.length > 0) { - throw Boom.notFound( - `These sub cases ${subCaseErrors - .map((c) => c.id) - .join(', ')} do not exist. Please check you have the correct ids.` - ); - } - - const subCaseIDToParentID = subCases.saved_objects.reduce((acc, subCase) => { - const parentID = subCase.references.find((ref) => ref.type === CASE_SAVED_OBJECT); - acc.set(subCase.id, parentID?.id); - return acc; - }, new Map()); - - await Promise.all( - comments.saved_objects.map((comment) => - attachmentService.delete({ soClient, attachmentId: comment.id }) - ) - ); - - await Promise.all(request.query.ids.map((id) => caseService.deleteSubCase(soClient, id))); - - // eslint-disable-next-line @typescript-eslint/naming-convention - const { username, full_name, email } = await caseService.getUser({ request }); - const deleteDate = new Date().toISOString(); - - await userActionService.bulkCreate({ - soClient, - actions: request.query.ids.map((id) => - buildCaseUserActionItem({ - action: 'delete', - actionAt: deleteDate, - actionBy: { username, full_name, email }, - // if for some reason the sub case didn't have a reference to its parent, we'll still log a user action - // but we won't have the case ID - caseId: subCaseIDToParentID.get(id) ?? '', - subCaseId: id, - fields: ['sub_case', 'comment', 'status'], - }) - ), - }); + const client = await context.cases.getCasesClient(); + await client.subCases.delete({ soClient, ids: request.query.ids, user }); return response.noContent(); } catch (error) { diff --git a/x-pack/plugins/cases/server/routes/api/cases/sub_case/find_sub_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/sub_case/find_sub_cases.ts index f9d077cbe3b12..5f1ba235d8953 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/sub_case/find_sub_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/sub_case/find_sub_cases.ts @@ -12,17 +12,10 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { - caseStatuses, - SubCasesFindRequestRt, - SubCasesFindResponseRt, - throwErrors, -} from '../../../../../common/api'; +import { SubCasesFindRequestRt, throwErrors } from '../../../../../common/api'; import { RouteDeps } from '../../types'; -import { escapeHatch, transformSubCases, wrapError } from '../../utils'; +import { escapeHatch, wrapError } from '../../utils'; import { SUB_CASES_URL, SAVED_OBJECT_TYPES } from '../../../../../common/constants'; -import { constructQueryOptions } from '../helpers'; -import { defaultPage, defaultPerPage } from '../..'; export function initFindSubCasesApi({ caseService, router, logger }: RouteDeps) { router.get( @@ -45,50 +38,13 @@ export function initFindSubCasesApi({ caseService, router, logger }: RouteDeps) fold(throwErrors(Boom.badRequest), identity) ); - const ids = [request.params.case_id]; - const { subCase: subCaseQueryOptions } = constructQueryOptions({ - status: queryParams.status, - sortByField: queryParams.sortField, - }); - - const subCases = await caseService.findSubCasesGroupByCase({ - soClient, - ids, - options: { - sortField: 'created_at', - page: defaultPage, - perPage: defaultPerPage, - ...queryParams, - ...subCaseQueryOptions, - }, - }); - - const [open, inProgress, closed] = await Promise.all([ - ...caseStatuses.map((status) => { - const { subCase: statusQueryOptions } = constructQueryOptions({ - status, - sortByField: queryParams.sortField, - }); - return caseService.findSubCaseStatusStats({ - soClient, - options: statusQueryOptions ?? {}, - ids, - }); - }), - ]); - + const client = await context.cases.getCasesClient(); return response.ok({ - body: SubCasesFindResponseRt.encode( - transformSubCases({ - page: subCases.page, - perPage: subCases.perPage, - total: subCases.total, - subCasesMap: subCases.subCasesMap, - open, - inProgress, - closed, - }) - ), + body: await client.subCases.find({ + soClient, + caseID: request.params.case_id, + queryParams, + }), }); } catch (error) { logger.error( diff --git a/x-pack/plugins/cases/server/routes/api/cases/sub_case/get_sub_case.ts b/x-pack/plugins/cases/server/routes/api/cases/sub_case/get_sub_case.ts index afeaef639326d..e5e11fbfd8ea2 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/sub_case/get_sub_case.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/sub_case/get_sub_case.ts @@ -7,13 +7,11 @@ import { schema } from '@kbn/config-schema'; -import { SubCaseResponseRt } from '../../../../../common/api'; import { RouteDeps } from '../../types'; -import { flattenSubCaseSavedObject, wrapError } from '../../utils'; +import { wrapError } from '../../utils'; import { SUB_CASE_DETAILS_URL, SAVED_OBJECT_TYPES } from '../../../../../common/constants'; -import { countAlertsForID } from '../../../../common'; -export function initGetSubCaseApi({ caseService, router, logger }: RouteDeps) { +export function initGetSubCaseApi({ router, logger }: RouteDeps) { router.get( { path: SUB_CASE_DETAILS_URL, @@ -32,44 +30,15 @@ export function initGetSubCaseApi({ caseService, router, logger }: RouteDeps) { const soClient = context.core.savedObjects.getClient({ includedHiddenTypes: SAVED_OBJECT_TYPES, }); - const includeComments = request.query.includeComments; - const subCase = await caseService.getSubCase({ - soClient, - id: request.params.sub_case_id, - }); - - if (!includeComments) { - return response.ok({ - body: SubCaseResponseRt.encode( - flattenSubCaseSavedObject({ - savedObject: subCase, - }) - ), - }); - } - - const theComments = await caseService.getAllSubCaseComments({ - soClient, - id: request.params.sub_case_id, - options: { - sortField: 'created_at', - sortOrder: 'asc', - }, - }); + const client = await context.cases.getCasesClient(); return response.ok({ - body: SubCaseResponseRt.encode( - flattenSubCaseSavedObject({ - savedObject: subCase, - comments: theComments.saved_objects, - totalComment: theComments.total, - totalAlerts: countAlertsForID({ - comments: theComments, - id: request.params.sub_case_id, - }), - }) - ), + body: await client.subCases.get({ + id: request.params.sub_case_id, + soClient, + includeComments: request.query.includeComments, + }), }); } catch (error) { logger.error( diff --git a/x-pack/plugins/cases/server/routes/api/cases/sub_case/patch_sub_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/sub_case/patch_sub_cases.ts index 4a407fc261a9b..f021e4bc6bd13 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/sub_case/patch_sub_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/sub_case/patch_sub_cases.ts @@ -5,424 +5,12 @@ * 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 { - SavedObjectsClientContract, - KibanaRequest, - SavedObject, - SavedObjectsFindResponse, - Logger, -} from 'kibana/server'; - -import { nodeBuilder } from '../../../../../../../../src/plugins/data/common'; -import { CasesClient } from '../../../../client'; -import { CaseService, CaseUserActionService } from '../../../../services'; -import { - CaseStatuses, - SubCasesPatchRequest, - SubCasesPatchRequestRt, - CommentType, - excess, - throwErrors, - SubCasesResponse, - SubCasePatchRequest, - SubCaseAttributes, - ESCaseAttributes, - SubCaseResponse, - SubCasesResponseRt, - User, - CommentAttributes, -} from '../../../../../common/api'; -import { - SUB_CASES_PATCH_DEL_URL, - CASE_COMMENT_SAVED_OBJECT, - SUB_CASE_SAVED_OBJECT, -} from '../../../../../common/constants'; +import { SubCasesPatchRequest } from '../../../../../common/api'; +import { SUB_CASES_PATCH_DEL_URL, SAVED_OBJECT_TYPES } from '../../../../../common/constants'; import { RouteDeps } from '../../types'; -import { - escapeHatch, - flattenSubCaseSavedObject, - isCommentRequestTypeAlertOrGenAlert, - wrapError, -} from '../../utils'; -import { getCaseToUpdate } from '../helpers'; -import { buildSubCaseUserActions } from '../../../../services/user_actions/helpers'; -import { createAlertUpdateRequest } from '../../../../common'; -import { createCaseError } from '../../../../common/error'; -import { UpdateAlertRequest } from '../../../../client/alerts/client'; - -interface UpdateArgs { - soClient: SavedObjectsClientContract; - caseService: CaseService; - userActionService: CaseUserActionService; - request: KibanaRequest; - casesClient: CasesClient; - subCases: SubCasesPatchRequest; - logger: Logger; -} - -function checkNonExistingOrConflict( - toUpdate: SubCasePatchRequest[], - fromStorage: Map> -) { - const nonExistingSubCases: SubCasePatchRequest[] = []; - const conflictedSubCases: SubCasePatchRequest[] = []; - for (const subCaseToUpdate of toUpdate) { - const bulkEntry = fromStorage.get(subCaseToUpdate.id); - - if (bulkEntry && bulkEntry.error) { - nonExistingSubCases.push(subCaseToUpdate); - } - - if (!bulkEntry || bulkEntry.version !== subCaseToUpdate.version) { - conflictedSubCases.push(subCaseToUpdate); - } - } - - if (nonExistingSubCases.length > 0) { - throw Boom.notFound( - `These sub cases ${nonExistingSubCases - .map((c) => c.id) - .join(', ')} do not exist. Please check you have the correct ids.` - ); - } - - if (conflictedSubCases.length > 0) { - throw Boom.conflict( - `These sub cases ${conflictedSubCases - .map((c) => c.id) - .join(', ')} has been updated. Please refresh before saving additional updates.` - ); - } -} - -interface GetParentIDsResult { - ids: string[]; - parentIDToSubID: Map; -} - -function getParentIDs({ - subCasesMap, - subCaseIDs, -}: { - subCasesMap: Map>; - subCaseIDs: string[]; -}): GetParentIDsResult { - return subCaseIDs.reduce( - (acc, id) => { - const subCase = subCasesMap.get(id); - if (subCase && subCase.references.length > 0) { - const parentID = subCase.references[0].id; - acc.ids.push(parentID); - let subIDs = acc.parentIDToSubID.get(parentID); - if (subIDs === undefined) { - subIDs = []; - } - subIDs.push(id); - acc.parentIDToSubID.set(parentID, subIDs); - } - return acc; - }, - { ids: [], parentIDToSubID: new Map() } - ); -} - -async function getParentCases({ - caseService, - soClient, - subCaseIDs, - subCasesMap, -}: { - caseService: CaseService; - soClient: SavedObjectsClientContract; - subCaseIDs: string[]; - subCasesMap: Map>; -}): Promise>> { - const parentIDInfo = getParentIDs({ subCaseIDs, subCasesMap }); - - const parentCases = await caseService.getCases({ - soClient, - caseIds: parentIDInfo.ids, - }); - - const parentCaseErrors = parentCases.saved_objects.filter((so) => so.error !== undefined); - - if (parentCaseErrors.length > 0) { - throw Boom.badRequest( - `Unable to find parent cases: ${parentCaseErrors - .map((c) => c.id) - .join(', ')} for sub cases: ${subCaseIDs.join(', ')}` - ); - } - - return parentCases.saved_objects.reduce((acc, so) => { - const subCaseIDsWithParent = parentIDInfo.parentIDToSubID.get(so.id); - subCaseIDsWithParent?.forEach((subCaseId) => { - acc.set(subCaseId, so); - }); - return acc; - }, new Map>()); -} - -function getValidUpdateRequests( - toUpdate: SubCasePatchRequest[], - subCasesMap: Map> -): SubCasePatchRequest[] { - const validatedSubCaseAttributes: SubCasePatchRequest[] = toUpdate.map((updateCase) => { - const currentCase = subCasesMap.get(updateCase.id); - return currentCase != null - ? getCaseToUpdate(currentCase.attributes, { - ...updateCase, - }) - : { id: updateCase.id, version: updateCase.version }; - }); - - return validatedSubCaseAttributes.filter((updateCase: SubCasePatchRequest) => { - const { id, version, ...updateCaseAttributes } = updateCase; - return Object.keys(updateCaseAttributes).length > 0; - }); -} - -/** - * Get the id from a reference in a comment for a sub case - */ -function getID(comment: SavedObject): string | undefined { - return comment.references.find((ref) => ref.type === SUB_CASE_SAVED_OBJECT)?.id; -} - -/** - * Get all the alert comments for a set of sub cases - */ -async function getAlertComments({ - subCasesToSync, - caseService, - soClient, -}: { - subCasesToSync: SubCasePatchRequest[]; - caseService: CaseService; - soClient: SavedObjectsClientContract; -}): Promise> { - const ids = subCasesToSync.map((subCase) => subCase.id); - return caseService.getAllSubCaseComments({ - soClient, - id: ids, - options: { - filter: nodeBuilder.or([ - nodeBuilder.is(`${CASE_COMMENT_SAVED_OBJECT}.attributes.type`, CommentType.alert), - nodeBuilder.is(`${CASE_COMMENT_SAVED_OBJECT}.attributes.type`, CommentType.generatedAlert), - ]), - }, - }); -} - -/** - * Updates the status of alerts for the specified sub cases. - */ -async function updateAlerts({ - caseService, - soClient, - casesClient, - logger, - subCasesToSync, -}: { - caseService: CaseService; - soClient: SavedObjectsClientContract; - casesClient: CasesClient; - logger: Logger; - subCasesToSync: SubCasePatchRequest[]; -}) { - try { - const subCasesToSyncMap = subCasesToSync.reduce((acc, subCase) => { - acc.set(subCase.id, subCase); - return acc; - }, new Map()); - // get all the alerts for all sub cases that need to be synced - const totalAlerts = await getAlertComments({ caseService, soClient, subCasesToSync }); - // create a map of the status (open, closed, etc) to alert info that needs to be updated - const alertsToUpdate = totalAlerts.saved_objects.reduce( - (acc: UpdateAlertRequest[], alertComment) => { - if (isCommentRequestTypeAlertOrGenAlert(alertComment.attributes)) { - const id = getID(alertComment); - const status = - id !== undefined - ? subCasesToSyncMap.get(id)?.status ?? CaseStatuses.open - : CaseStatuses.open; - - acc.push(...createAlertUpdateRequest({ comment: alertComment.attributes, status })); - } - return acc; - }, - [] - ); - - await casesClient.casesClientInternal.alerts.updateStatus({ alerts: alertsToUpdate }); - } catch (error) { - throw createCaseError({ - message: `Failed to update alert status while updating sub cases: ${JSON.stringify( - subCasesToSync - )}: ${error}`, - logger, - error, - }); - } -} - -async function update({ - soClient, - caseService, - userActionService, - request, - casesClient, - subCases, - logger, -}: UpdateArgs): Promise { - const query = pipe( - excess(SubCasesPatchRequestRt).decode(subCases), - fold(throwErrors(Boom.badRequest), identity) - ); - - try { - const bulkSubCases = await caseService.getSubCases({ - soClient, - ids: query.subCases.map((q) => q.id), - }); +import { escapeHatch, wrapError } from '../../utils'; - const subCasesMap = bulkSubCases.saved_objects.reduce((acc, so) => { - acc.set(so.id, so); - return acc; - }, new Map>()); - - checkNonExistingOrConflict(query.subCases, subCasesMap); - - const nonEmptySubCaseRequests = getValidUpdateRequests(query.subCases, subCasesMap); - - if (nonEmptySubCaseRequests.length <= 0) { - throw Boom.notAcceptable('All update fields are identical to current version.'); - } - - const subIDToParentCase = await getParentCases({ - soClient, - caseService, - subCaseIDs: nonEmptySubCaseRequests.map((subCase) => subCase.id), - subCasesMap, - }); - - // eslint-disable-next-line @typescript-eslint/naming-convention - const { username, full_name, email } = await caseService.getUser({ request }); - const updatedAt = new Date().toISOString(); - const updatedCases = await caseService.patchSubCases({ - soClient, - subCases: nonEmptySubCaseRequests.map((thisCase) => { - const { id: subCaseId, version, ...updateSubCaseAttributes } = thisCase; - let closedInfo: { closed_at: string | null; closed_by: User | null } = { - closed_at: null, - closed_by: null, - }; - - if ( - updateSubCaseAttributes.status && - updateSubCaseAttributes.status === CaseStatuses.closed - ) { - closedInfo = { - closed_at: updatedAt, - closed_by: { email, full_name, username }, - }; - } else if ( - updateSubCaseAttributes.status && - (updateSubCaseAttributes.status === CaseStatuses.open || - updateSubCaseAttributes.status === CaseStatuses['in-progress']) - ) { - closedInfo = { - closed_at: null, - closed_by: null, - }; - } - return { - subCaseId, - updatedAttributes: { - ...updateSubCaseAttributes, - ...closedInfo, - updated_at: updatedAt, - updated_by: { email, full_name, username }, - }, - version, - }; - }), - }); - - const subCasesToSyncAlertsFor = nonEmptySubCaseRequests.filter((subCaseToUpdate) => { - const storedSubCase = subCasesMap.get(subCaseToUpdate.id); - const parentCase = subIDToParentCase.get(subCaseToUpdate.id); - return ( - storedSubCase !== undefined && - subCaseToUpdate.status !== undefined && - storedSubCase.attributes.status !== subCaseToUpdate.status && - parentCase?.attributes.settings.syncAlerts - ); - }); - - await updateAlerts({ - caseService, - soClient, - casesClient, - subCasesToSync: subCasesToSyncAlertsFor, - logger, - }); - - const returnUpdatedSubCases = updatedCases.saved_objects.reduce( - (acc, updatedSO) => { - const originalSubCase = subCasesMap.get(updatedSO.id); - if (originalSubCase) { - acc.push( - flattenSubCaseSavedObject({ - savedObject: { - ...originalSubCase, - ...updatedSO, - attributes: { ...originalSubCase.attributes, ...updatedSO.attributes }, - references: originalSubCase.references, - version: updatedSO.version ?? originalSubCase.version, - }, - }) - ); - } - return acc; - }, - [] - ); - - await userActionService.bulkCreate({ - soClient, - actions: buildSubCaseUserActions({ - originalSubCases: bulkSubCases.saved_objects, - updatedSubCases: updatedCases.saved_objects, - actionDate: updatedAt, - actionBy: { email, full_name, username }, - }), - }); - - return SubCasesResponseRt.encode(returnUpdatedSubCases); - } catch (error) { - const idVersions = query.subCases.map((subCase) => ({ - id: subCase.id, - version: subCase.version, - })); - throw createCaseError({ - message: `Failed to update sub cases: ${JSON.stringify(idVersions)}: ${error}`, - error, - logger, - }); - } -} - -export function initPatchSubCasesApi({ - router, - caseService, - userActionService, - logger, -}: RouteDeps) { +export function initPatchSubCasesApi({ router, caseService, logger }: RouteDeps) { router.patch( { path: SUB_CASES_PATCH_DEL_URL, @@ -432,19 +20,16 @@ export function initPatchSubCasesApi({ }, async (context, request, response) => { try { + const soClient = context.core.savedObjects.getClient({ + includedHiddenTypes: SAVED_OBJECT_TYPES, + }); + + const user = caseService.getUser({ request }); + const casesClient = await context.cases.getCasesClient(); const subCases = request.body as SubCasesPatchRequest; - return response.ok({ - body: await update({ - request, - subCases, - casesClient, - soClient: context.core.savedObjects.client, - caseService, - userActionService, - logger, - }), + body: await casesClient.subCases.update({ soClient, subCases, user }), }); } catch (error) { logger.error(`Failed to patch sub cases in route: ${error}`); From 382315ae517336384449401bf178298ca1b311d2 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Wed, 7 Apr 2021 17:13:02 -0400 Subject: [PATCH 3/4] Throw when attempting to access the sub cases client --- x-pack/plugins/cases/server/client/client.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/x-pack/plugins/cases/server/client/client.ts b/x-pack/plugins/cases/server/client/client.ts index ca30200b95ea8..f02a1a542246f 100644 --- a/x-pack/plugins/cases/server/client/client.ts +++ b/x-pack/plugins/cases/server/client/client.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import Boom from '@hapi/boom'; import { CasesClientArgs } from './types'; import { CasesSubClient, createCasesSubClient } from './cases/client'; @@ -11,6 +12,7 @@ import { AttachmentsSubClient, createAttachmentsSubClient } from './attachments/ import { UserActionsSubClient, createUserActionsSubClient } from './user_actions/client'; import { CasesClientInternal, createCasesClientInternal } from './client_internal'; import { createSubCasesClient, SubCasesClient } from './sub_cases/client'; +import { ENABLE_CASE_CONNECTOR } from '../../common/constants'; export class CasesClient { private readonly _casesClientInternal: CasesClientInternal; @@ -40,6 +42,9 @@ export class CasesClient { } public get subCases() { + if (!ENABLE_CASE_CONNECTOR) { + throw Boom.badRequest('The case connector feature is disabled'); + } return this._subCases; } From a1677f0b5012b5f31a8e34f68a46c5b505620be9 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Thu, 8 Apr 2021 09:24:33 -0400 Subject: [PATCH 4/4] Fixing throw and removing user ans soclients --- x-pack/plugins/cases/server/client/client.ts | 2 +- .../cases/server/client/sub_cases/client.ts | 60 +++++++++---------- .../cases/server/client/sub_cases/update.ts | 23 +++---- .../api/cases/sub_case/delete_sub_cases.ts | 10 +--- .../api/cases/sub_case/find_sub_cases.ts | 6 +- .../routes/api/cases/sub_case/get_sub_case.ts | 7 +-- .../api/cases/sub_case/patch_sub_cases.ts | 10 +--- 7 files changed, 45 insertions(+), 73 deletions(-) diff --git a/x-pack/plugins/cases/server/client/client.ts b/x-pack/plugins/cases/server/client/client.ts index f02a1a542246f..702329f7bcca2 100644 --- a/x-pack/plugins/cases/server/client/client.ts +++ b/x-pack/plugins/cases/server/client/client.ts @@ -43,7 +43,7 @@ export class CasesClient { public get subCases() { if (!ENABLE_CASE_CONNECTOR) { - throw Boom.badRequest('The case connector feature is disabled'); + throw new Error('The case connector feature is disabled'); } return this._subCases; } diff --git a/x-pack/plugins/cases/server/client/sub_cases/client.ts b/x-pack/plugins/cases/server/client/sub_cases/client.ts index 1ed328d0a8be6..aef780ecb3ac9 100644 --- a/x-pack/plugins/cases/server/client/sub_cases/client.ts +++ b/x-pack/plugins/cases/server/client/sub_cases/client.ts @@ -7,7 +7,6 @@ import Boom from '@hapi/boom'; -import { SavedObjectsClientContract } from 'kibana/server'; import { caseStatuses, SubCaseResponse, @@ -15,8 +14,8 @@ import { SubCasesFindRequest, SubCasesFindResponse, SubCasesFindResponseRt, + SubCasesPatchRequest, SubCasesResponse, - User, } from '../../../common/api'; import { CasesClientArgs } from '..'; import { flattenSubCaseSavedObject, transformSubCases } from '../../routes/api/utils'; @@ -27,16 +26,9 @@ import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; import { constructQueryOptions } from '../../routes/api/cases/helpers'; import { defaultPage, defaultPerPage } from '../../routes/api'; import { CasesClient } from '../client'; -import { update, UpdateArgs } from './update'; - -interface DeleteArgs { - soClient: SavedObjectsClientContract; - ids: string[]; - user: User; -} +import { update } from './update'; interface FindArgs { - soClient: SavedObjectsClientContract; caseID: string; queryParams: SubCasesFindRequest; } @@ -44,17 +36,16 @@ interface FindArgs { interface GetArgs { includeComments: boolean; id: string; - soClient: SavedObjectsClientContract; } /** * The API routes for interacting with sub cases. */ export interface SubCasesClient { - delete(deleteArgs: DeleteArgs): Promise; + delete(ids: string[]): Promise; find(findArgs: FindArgs): Promise; get(getArgs: GetArgs): Promise; - update(updateArgs: UpdateArgs): Promise; + update(subCases: SubCasesPatchRequest): Promise; } /** @@ -65,21 +56,26 @@ export function createSubCasesClient( casesClient: CasesClient ): SubCasesClient { return Object.freeze({ - delete: (deleteArgs: DeleteArgs) => deleteSubCase(deleteArgs, clientArgs), + delete: (ids: string[]) => deleteSubCase(ids, clientArgs), find: (findArgs: FindArgs) => find(findArgs, clientArgs), get: (getArgs: GetArgs) => get(getArgs, clientArgs), - update: (updateArgs: UpdateArgs) => update(updateArgs, clientArgs, casesClient), + update: (subCases: SubCasesPatchRequest) => update(subCases, clientArgs, casesClient), }); } -async function deleteSubCase( - { soClient, ids, user }: DeleteArgs, - clientArgs: CasesClientArgs -): Promise { +async function deleteSubCase(ids: string[], clientArgs: CasesClientArgs): Promise { try { + const { + savedObjectsClient: soClient, + user, + userActionService, + caseService, + attachmentService, + } = clientArgs; + const [comments, subCases] = await Promise.all([ - clientArgs.caseService.getAllSubCaseComments({ soClient, id: ids }), - clientArgs.caseService.getSubCases({ soClient, ids }), + caseService.getAllSubCaseComments({ soClient, id: ids }), + caseService.getSubCases({ soClient, ids }), ]); const subCaseErrors = subCases.saved_objects.filter((subCase) => subCase.error !== undefined); @@ -100,15 +96,15 @@ async function deleteSubCase( await Promise.all( comments.saved_objects.map((comment) => - clientArgs.attachmentService.delete({ soClient, attachmentId: comment.id }) + attachmentService.delete({ soClient, attachmentId: comment.id }) ) ); - await Promise.all(ids.map((id) => clientArgs.caseService.deleteSubCase(soClient, id))); + await Promise.all(ids.map((id) => caseService.deleteSubCase(soClient, id))); const deleteDate = new Date().toISOString(); - await clientArgs.userActionService.bulkCreate({ + await userActionService.bulkCreate({ soClient, actions: ids.map((id) => buildCaseUserActionItem({ @@ -133,17 +129,19 @@ async function deleteSubCase( } async function find( - { soClient, caseID, queryParams }: FindArgs, + { caseID, queryParams }: FindArgs, clientArgs: CasesClientArgs ): Promise { try { + const { savedObjectsClient: soClient, caseService } = clientArgs; + const ids = [caseID]; const { subCase: subCaseQueryOptions } = constructQueryOptions({ status: queryParams.status, sortByField: queryParams.sortField, }); - const subCases = await clientArgs.caseService.findSubCasesGroupByCase({ + const subCases = await caseService.findSubCasesGroupByCase({ soClient, ids, options: { @@ -161,7 +159,7 @@ async function find( status, sortByField: queryParams.sortField, }); - return clientArgs.caseService.findSubCaseStatusStats({ + return caseService.findSubCaseStatusStats({ soClient, options: statusQueryOptions ?? {}, ids, @@ -190,11 +188,13 @@ async function find( } async function get( - { includeComments, id, soClient }: GetArgs, + { includeComments, id }: GetArgs, clientArgs: CasesClientArgs ): Promise { try { - const subCase = await clientArgs.caseService.getSubCase({ + const { savedObjectsClient: soClient, caseService } = clientArgs; + + const subCase = await caseService.getSubCase({ soClient, id, }); @@ -207,7 +207,7 @@ async function get( ); } - const theComments = await clientArgs.caseService.getAllSubCaseComments({ + const theComments = await caseService.getAllSubCaseComments({ soClient, id, options: { diff --git a/x-pack/plugins/cases/server/client/sub_cases/update.ts b/x-pack/plugins/cases/server/client/sub_cases/update.ts index a328e6dbb0d15..27e6e1261c0af 100644 --- a/x-pack/plugins/cases/server/client/sub_cases/update.ts +++ b/x-pack/plugins/cases/server/client/sub_cases/update.ts @@ -47,15 +47,6 @@ import { createCaseError } from '../../common/error'; import { UpdateAlertRequest } from '../../client/alerts/client'; import { CasesClientArgs } from '../types'; -/** - * The parameters that come from the API route itself. - */ -export interface UpdateArgs { - soClient: SavedObjectsClientContract; - user: User; - subCases: SubCasesPatchRequest; -} - function checkNonExistingOrConflict( toUpdate: SubCasePatchRequest[], fromStorage: Map> @@ -266,7 +257,7 @@ async function updateAlerts({ * Handles updating the fields in a sub case. */ export async function update( - { soClient, user, subCases }: UpdateArgs, + subCases: SubCasesPatchRequest, clientArgs: CasesClientArgs, casesClient: CasesClient ): Promise { @@ -276,7 +267,9 @@ export async function update( ); try { - const bulkSubCases = await clientArgs.caseService.getSubCases({ + const { savedObjectsClient: soClient, user, caseService, userActionService } = clientArgs; + + const bulkSubCases = await caseService.getSubCases({ soClient, ids: query.subCases.map((q) => q.id), }); @@ -296,13 +289,13 @@ export async function update( const subIDToParentCase = await getParentCases({ soClient, - caseService: clientArgs.caseService, + caseService, subCaseIDs: nonEmptySubCaseRequests.map((subCase) => subCase.id), subCasesMap, }); const updatedAt = new Date().toISOString(); - const updatedCases = await clientArgs.caseService.patchSubCases({ + const updatedCases = await caseService.patchSubCases({ soClient, subCases: nonEmptySubCaseRequests.map((thisCase) => { const { id: subCaseId, version, ...updateSubCaseAttributes } = thisCase; @@ -354,7 +347,7 @@ export async function update( }); await updateAlerts({ - caseService: clientArgs.caseService, + caseService, soClient, casesClient, subCasesToSync: subCasesToSyncAlertsFor, @@ -382,7 +375,7 @@ export async function update( [] ); - await clientArgs.userActionService.bulkCreate({ + await userActionService.bulkCreate({ soClient, actions: buildSubCaseUserActions({ originalSubCases: bulkSubCases.saved_objects, diff --git a/x-pack/plugins/cases/server/routes/api/cases/sub_case/delete_sub_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/sub_case/delete_sub_cases.ts index 3f05dc1aa989b..4f4870496f77f 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/sub_case/delete_sub_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/sub_case/delete_sub_cases.ts @@ -8,7 +8,7 @@ import { schema } from '@kbn/config-schema'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; -import { SUB_CASES_PATCH_DEL_URL, SAVED_OBJECT_TYPES } from '../../../../../common/constants'; +import { SUB_CASES_PATCH_DEL_URL } from '../../../../../common/constants'; export function initDeleteSubCasesApi({ caseService, router, logger }: RouteDeps) { router.delete( @@ -22,14 +22,8 @@ export function initDeleteSubCasesApi({ caseService, router, logger }: RouteDeps }, async (context, request, response) => { try { - const soClient = context.core.savedObjects.getClient({ - includedHiddenTypes: SAVED_OBJECT_TYPES, - }); - - const user = caseService.getUser({ request }); - const client = await context.cases.getCasesClient(); - await client.subCases.delete({ soClient, ids: request.query.ids, user }); + await client.subCases.delete(request.query.ids); return response.noContent(); } catch (error) { diff --git a/x-pack/plugins/cases/server/routes/api/cases/sub_case/find_sub_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/sub_case/find_sub_cases.ts index 5f1ba235d8953..80cfbbd6b584f 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/sub_case/find_sub_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/sub_case/find_sub_cases.ts @@ -15,7 +15,7 @@ import { identity } from 'fp-ts/lib/function'; import { SubCasesFindRequestRt, throwErrors } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { escapeHatch, wrapError } from '../../utils'; -import { SUB_CASES_URL, SAVED_OBJECT_TYPES } from '../../../../../common/constants'; +import { SUB_CASES_URL } from '../../../../../common/constants'; export function initFindSubCasesApi({ caseService, router, logger }: RouteDeps) { router.get( @@ -30,9 +30,6 @@ export function initFindSubCasesApi({ caseService, router, logger }: RouteDeps) }, async (context, request, response) => { try { - const soClient = context.core.savedObjects.getClient({ - includedHiddenTypes: SAVED_OBJECT_TYPES, - }); const queryParams = pipe( SubCasesFindRequestRt.decode(request.query), fold(throwErrors(Boom.badRequest), identity) @@ -41,7 +38,6 @@ export function initFindSubCasesApi({ caseService, router, logger }: RouteDeps) const client = await context.cases.getCasesClient(); return response.ok({ body: await client.subCases.find({ - soClient, caseID: request.params.case_id, queryParams, }), diff --git a/x-pack/plugins/cases/server/routes/api/cases/sub_case/get_sub_case.ts b/x-pack/plugins/cases/server/routes/api/cases/sub_case/get_sub_case.ts index e5e11fbfd8ea2..44ec5d68e9653 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/sub_case/get_sub_case.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/sub_case/get_sub_case.ts @@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; -import { SUB_CASE_DETAILS_URL, SAVED_OBJECT_TYPES } from '../../../../../common/constants'; +import { SUB_CASE_DETAILS_URL } from '../../../../../common/constants'; export function initGetSubCaseApi({ router, logger }: RouteDeps) { router.get( @@ -27,16 +27,11 @@ export function initGetSubCaseApi({ router, logger }: RouteDeps) { }, async (context, request, response) => { try { - const soClient = context.core.savedObjects.getClient({ - includedHiddenTypes: SAVED_OBJECT_TYPES, - }); - const client = await context.cases.getCasesClient(); return response.ok({ body: await client.subCases.get({ id: request.params.sub_case_id, - soClient, includeComments: request.query.includeComments, }), }); diff --git a/x-pack/plugins/cases/server/routes/api/cases/sub_case/patch_sub_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/sub_case/patch_sub_cases.ts index f021e4bc6bd13..c1cd4b317da9b 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/sub_case/patch_sub_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/sub_case/patch_sub_cases.ts @@ -6,7 +6,7 @@ */ import { SubCasesPatchRequest } from '../../../../../common/api'; -import { SUB_CASES_PATCH_DEL_URL, SAVED_OBJECT_TYPES } from '../../../../../common/constants'; +import { SUB_CASES_PATCH_DEL_URL } from '../../../../../common/constants'; import { RouteDeps } from '../../types'; import { escapeHatch, wrapError } from '../../utils'; @@ -20,16 +20,10 @@ export function initPatchSubCasesApi({ router, caseService, logger }: RouteDeps) }, async (context, request, response) => { try { - const soClient = context.core.savedObjects.getClient({ - includedHiddenTypes: SAVED_OBJECT_TYPES, - }); - - const user = caseService.getUser({ request }); - const casesClient = await context.cases.getCasesClient(); const subCases = request.body as SubCasesPatchRequest; return response.ok({ - body: await casesClient.subCases.update({ soClient, subCases, user }), + body: await casesClient.subCases.update(subCases), }); } catch (error) { logger.error(`Failed to patch sub cases in route: ${error}`);