From f2a5c4748487e098af409867d213fa98f33c18c2 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 8 Jun 2021 22:05:37 +0300 Subject: [PATCH] [Cases] Performance and RBAC improvements (#101465) (#101652) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../cases/common/api/cases/configure.ts | 20 +-- .../plugins/cases/common/api/runtime_types.ts | 4 - x-pack/plugins/cases/common/constants.ts | 3 + .../cases/server/client/attachments/delete.ts | 28 +++-- .../cases/server/client/cases/delete.ts | 102 ++++++++------- .../plugins/cases/server/client/cases/find.ts | 1 + .../cases/server/client/cases/update.ts | 10 +- .../cases/server/client/configure/client.ts | 30 +++-- .../cases/server/client/stats/client.ts | 1 + .../cases/server/client/sub_cases/client.ts | 25 ++-- .../server/common/models/commentable_case.ts | 20 ++- .../server/services/attachments/index.ts | 4 +- .../cases/server/services/cases/index.ts | 116 +++++------------- .../cases/server/services/configure/index.ts | 3 +- .../server/services/user_actions/index.ts | 10 +- 15 files changed, 181 insertions(+), 196 deletions(-) diff --git a/x-pack/plugins/cases/common/api/cases/configure.ts b/x-pack/plugins/cases/common/api/cases/configure.ts index 2814dd44f513..6c92702c523b 100644 --- a/x-pack/plugins/cases/common/api/cases/configure.ts +++ b/x-pack/plugins/cases/common/api/cases/configure.ts @@ -9,13 +9,11 @@ import * as rt from 'io-ts'; import { UserRT } from '../user'; import { CaseConnectorRt, ConnectorMappingsRt, ESCaseConnector } from '../connectors'; -import { OmitProp } from '../runtime_types'; -import { OWNER_FIELD } from './constants'; // TODO: we will need to add this type rt.literal('close-by-third-party') const ClosureTypeRT = rt.union([rt.literal('close-by-user'), rt.literal('close-by-pushing')]); -const CasesConfigureBasicRt = rt.type({ +const CasesConfigureBasicWithoutOwnerRt = rt.type({ /** * The external connector */ @@ -24,15 +22,17 @@ const CasesConfigureBasicRt = rt.type({ * Whether to close the case after it has been synced with the external system */ closure_type: ClosureTypeRT, - /** - * The plugin owner that manages this configuration - */ - owner: rt.string, }); -const CasesConfigureBasicWithoutOwnerRt = rt.type( - OmitProp(CasesConfigureBasicRt.props, OWNER_FIELD) -); +const CasesConfigureBasicRt = rt.intersection([ + CasesConfigureBasicWithoutOwnerRt, + rt.type({ + /** + * The plugin owner that manages this configuration + */ + owner: rt.string, + }), +]); export const CasesConfigureRequestRt = CasesConfigureBasicRt; export const CasesConfigurePatchRt = rt.intersection([ diff --git a/x-pack/plugins/cases/common/api/runtime_types.ts b/x-pack/plugins/cases/common/api/runtime_types.ts index 361786985c6d..8817764e261d 100644 --- a/x-pack/plugins/cases/common/api/runtime_types.ts +++ b/x-pack/plugins/cases/common/api/runtime_types.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { omit } from 'lodash'; import { either, fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; @@ -14,9 +13,6 @@ import { isObject } from 'lodash/fp'; type ErrorFactory = (message: string) => Error; -export const OmitProp = (o: O, k: K): Omit => - omit(o, k); - /** * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/format_errors/index.ts * Bug fix for the TODO is in the format_errors package diff --git a/x-pack/plugins/cases/common/constants.ts b/x-pack/plugins/cases/common/constants.ts index 72c21aa12dcf..f0d3e8ccbcde 100644 --- a/x-pack/plugins/cases/common/constants.ts +++ b/x-pack/plugins/cases/common/constants.ts @@ -92,3 +92,6 @@ export const ENABLE_CASE_CONNECTOR = false; if (ENABLE_CASE_CONNECTOR) { SAVED_OBJECT_TYPES.push(SUB_CASE_SAVED_OBJECT); } + +export const MAX_DOCS_PER_PAGE = 10000; +export const MAX_CONCURRENT_SEARCHES = 10; diff --git a/x-pack/plugins/cases/server/client/attachments/delete.ts b/x-pack/plugins/cases/server/client/attachments/delete.ts index d935a0c8f09d..89e12d7f7ea3 100644 --- a/x-pack/plugins/cases/server/client/attachments/delete.ts +++ b/x-pack/plugins/cases/server/client/attachments/delete.ts @@ -6,9 +6,15 @@ */ import Boom from '@hapi/boom'; -import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../common/constants'; +import pMap from 'p-map'; -import { AssociationType } from '../../../common/api'; +import { SavedObject } from 'kibana/public'; +import { + CASE_SAVED_OBJECT, + MAX_CONCURRENT_SEARCHES, + SUB_CASE_SAVED_OBJECT, +} from '../../../common/constants'; +import { AssociationType, CommentAttributes } from '../../../common/api'; import { CasesClientArgs } from '../types'; import { buildCommentUserActionItem } from '../../services/user_actions/helpers'; import { createCaseError } from '../../common/error'; @@ -88,14 +94,16 @@ export async function deleteAll( })), }); - await Promise.all( - comments.saved_objects.map((comment) => - attachmentService.delete({ - unsecuredSavedObjectsClient, - attachmentId: comment.id, - }) - ) - ); + const mapper = async (comment: SavedObject) => + attachmentService.delete({ + unsecuredSavedObjectsClient, + attachmentId: comment.id, + }); + + // Ensuring we don't too many concurrent deletions running. + await pMap(comments.saved_objects, mapper, { + concurrency: MAX_CONCURRENT_SEARCHES, + }); const deleteDate = new Date().toISOString(); diff --git a/x-pack/plugins/cases/server/client/cases/delete.ts b/x-pack/plugins/cases/server/client/cases/delete.ts index b66abc6cc7be..8e99e39ec473 100644 --- a/x-pack/plugins/cases/server/client/cases/delete.ts +++ b/x-pack/plugins/cases/server/client/cases/delete.ts @@ -5,15 +5,16 @@ * 2.0. */ +import pMap from 'p-map'; import { Boom } from '@hapi/boom'; -import { SavedObjectsClientContract } from 'kibana/server'; -import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; +import { SavedObject, SavedObjectsClientContract, SavedObjectsFindResponse } from 'kibana/server'; +import { ENABLE_CASE_CONNECTOR, MAX_CONCURRENT_SEARCHES } from '../../../common/constants'; import { CasesClientArgs } from '..'; import { createCaseError } from '../../common/error'; import { AttachmentService, CasesService } from '../../services'; import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; import { Operations, OwnerEntity } from '../../authorization'; -import { OWNER_FIELD } from '../../../common/api'; +import { OWNER_FIELD, SubCaseAttributes, CommentAttributes } from '../../../common/api'; async function deleteSubCases({ attachmentService, @@ -37,19 +38,24 @@ async function deleteSubCases({ id: subCaseIDs, }); - // This shouldn't actually delete anything because all the comments should be deleted when comments are deleted - // per case ID - await Promise.all( - commentsForSubCases.saved_objects.map((commentSO) => - attachmentService.delete({ unsecuredSavedObjectsClient, attachmentId: commentSO.id }) - ) - ); + const commentMapper = (commentSO: SavedObject) => + attachmentService.delete({ unsecuredSavedObjectsClient, attachmentId: commentSO.id }); - await Promise.all( - subCasesForCaseIds.saved_objects.map((subCaseSO) => - caseService.deleteSubCase(unsecuredSavedObjectsClient, subCaseSO.id) - ) - ); + const subCasesMapper = (subCaseSO: SavedObject) => + caseService.deleteSubCase(unsecuredSavedObjectsClient, subCaseSO.id); + + /** + * This shouldn't actually delete anything because + * all the comments should be deleted when comments are deleted + * per case ID. We also ensure that we don't too many concurrent deletions running. + */ + await pMap(commentsForSubCases.saved_objects, commentMapper, { + concurrency: MAX_CONCURRENT_SEARCHES, + }); + + await pMap(subCasesForCaseIds.saved_objects, subCasesMapper, { + concurrency: MAX_CONCURRENT_SEARCHES, + }); } /** @@ -88,38 +94,46 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P entities: Array.from(entities.values()), }); - await Promise.all( - ids.map((id) => - caseService.deleteCase({ - unsecuredSavedObjectsClient, - id, - }) - ) - ); + const deleteCasesMapper = async (id: string) => + caseService.deleteCase({ + unsecuredSavedObjectsClient, + id, + }); - const comments = await Promise.all( - ids.map((id) => - caseService.getAllCaseComments({ - unsecuredSavedObjectsClient, - id, - }) - ) - ); + // Ensuring we don't too many concurrent deletions running. + await pMap(ids, deleteCasesMapper, { + concurrency: MAX_CONCURRENT_SEARCHES, + }); - if (comments.some((c) => c.saved_objects.length > 0)) { - await Promise.all( - comments.map((c) => - Promise.all( - c.saved_objects.map(({ id }) => - attachmentService.delete({ - unsecuredSavedObjectsClient, - attachmentId: id, - }) - ) - ) - ) + const getCommentsMapper = async (id: string) => + caseService.getAllCaseComments({ + unsecuredSavedObjectsClient, + id, + }); + + // Ensuring we don't too many concurrent get running. + const comments = await pMap(ids, getCommentsMapper, { + concurrency: MAX_CONCURRENT_SEARCHES, + }); + + /** + * This is a nested pMap.Mapper. + * Each element of the comments array contains all comments of a particular case. + * For that reason we need first to create a map that iterate over all cases + * and return a pMap that deletes the comments for that case + */ + const deleteCommentsMapper = async (commentRes: SavedObjectsFindResponse) => + pMap(commentRes.saved_objects, (comment) => + attachmentService.delete({ + unsecuredSavedObjectsClient, + attachmentId: comment.id, + }) ); - } + + // Ensuring we don't too many concurrent deletions running. + await pMap(comments, deleteCommentsMapper, { + concurrency: MAX_CONCURRENT_SEARCHES, + }); if (ENABLE_CASE_CONNECTOR) { await deleteSubCases({ diff --git a/x-pack/plugins/cases/server/client/cases/find.ts b/x-pack/plugins/cases/server/client/cases/find.ts index 3b4efe78f642..73eca5e7abb9 100644 --- a/x-pack/plugins/cases/server/client/cases/find.ts +++ b/x-pack/plugins/cases/server/client/cases/find.ts @@ -77,6 +77,7 @@ export const find = async ( ensureSavedObjectsAreAuthorized([...cases.casesMap.values()]); + // casesStatuses are bounded by us. No need to limit concurrent calls. const [openCases, inProgressCases, closedCases] = await Promise.all([ ...caseStatuses.map((status) => { const statusQuery = constructQueryOptions({ ...queryArgs, status, authorizationFilter }); diff --git a/x-pack/plugins/cases/server/client/cases/update.ts b/x-pack/plugins/cases/server/client/cases/update.ts index db20ba831844..608c726f1853 100644 --- a/x-pack/plugins/cases/server/client/cases/update.ts +++ b/x-pack/plugins/cases/server/client/cases/update.ts @@ -5,6 +5,7 @@ * 2.0. */ +import pMap from 'p-map'; import Boom from '@hapi/boom'; import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; @@ -42,6 +43,7 @@ import { CasesService } from '../../services'; import { CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT, + MAX_CONCURRENT_SEARCHES, SUB_CASE_SAVED_OBJECT, } from '../../../common/constants'; import { @@ -162,9 +164,11 @@ async function throwIfInvalidUpdateOfTypeWithAlerts({ }; const requestsUpdatingTypeField = requests.filter((req) => req.type === CaseType.collection); - const casesAlertTotals = await Promise.all( - requestsUpdatingTypeField.map((caseToUpdate) => getAlertsForID(caseToUpdate)) - ); + const getAlertsMapper = async (caseToUpdate: ESCasePatchRequest) => getAlertsForID(caseToUpdate); + // Ensuring we don't too many concurrent get running. + const casesAlertTotals = await pMap(requestsUpdatingTypeField, getAlertsMapper, { + concurrency: MAX_CONCURRENT_SEARCHES, + }); // grab the cases that have at least one alert comment attached to them const typeUpdateWithAlerts = casesAlertTotals.filter((caseInfo) => caseInfo.alerts.total > 0); diff --git a/x-pack/plugins/cases/server/client/configure/client.ts b/x-pack/plugins/cases/server/client/configure/client.ts index 14348e03f99c..d95667d5eee0 100644 --- a/x-pack/plugins/cases/server/client/configure/client.ts +++ b/x-pack/plugins/cases/server/client/configure/client.ts @@ -4,13 +4,19 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + +import pMap from 'p-map'; 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 { SavedObjectsFindResponse, SavedObjectsUtils } from '../../../../../../src/core/server'; -import { SUPPORTED_CONNECTORS } from '../../../common/constants'; +import { + SavedObject, + SavedObjectsFindResponse, + SavedObjectsUtils, +} from '../../../../../../src/core/server'; +import { MAX_CONCURRENT_SEARCHES, SUPPORTED_CONNECTORS } from '../../../common/constants'; import { CaseConfigureResponseRt, CasesConfigurePatch, @@ -26,6 +32,7 @@ import { CaseConfigurationsResponseRt, CasesConfigurePatchRt, ConnectorMappings, + ESCasesConfigureAttributes, } from '../../../common/api'; import { createCaseError } from '../../common/error'; import { @@ -175,8 +182,9 @@ async function get( })) ); - const configurations = await Promise.all( - myCaseConfigure.saved_objects.map(async (configuration) => { + const configurations = await pMap( + myCaseConfigure.saved_objects, + async (configuration: SavedObject) => { const { connector, ...caseConfigureWithoutConnector } = configuration?.attributes ?? { connector: null, }; @@ -204,7 +212,7 @@ async function get( error, id: configuration.id, }; - }) + } ); return CaseConfigurationsResponseRt.encode(configurations); @@ -400,11 +408,13 @@ async function create( ); if (myCaseConfigure.saved_objects.length > 0) { - await Promise.all( - myCaseConfigure.saved_objects.map((cc) => - caseConfigureService.delete({ unsecuredSavedObjectsClient, configurationId: cc.id }) - ) - ); + const deleteConfigurationMapper = async (c: SavedObject) => + caseConfigureService.delete({ unsecuredSavedObjectsClient, configurationId: c.id }); + + // Ensuring we don't too many concurrent deletions running. + await pMap(myCaseConfigure.saved_objects, deleteConfigurationMapper, { + concurrency: MAX_CONCURRENT_SEARCHES, + }); } const savedObjectID = SavedObjectsUtils.generateId(); diff --git a/x-pack/plugins/cases/server/client/stats/client.ts b/x-pack/plugins/cases/server/client/stats/client.ts index 0e222d54ab21..4fc8be4ccbfb 100644 --- a/x-pack/plugins/cases/server/client/stats/client.ts +++ b/x-pack/plugins/cases/server/client/stats/client.ts @@ -63,6 +63,7 @@ async function getStatusTotalsByType( ensureSavedObjectsAreAuthorized, } = await authorization.getAuthorizationFilter(Operations.getCaseStatuses); + // casesStatuses are bounded by us. No need to limit concurrent calls. const [openCases, inProgressCases, closedCases] = await Promise.all([ ...caseStatuses.map((status) => { const statusQuery = constructQueryOptions({ 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 b35d58ce0601..9a7dad77909e 100644 --- a/x-pack/plugins/cases/server/client/sub_cases/client.ts +++ b/x-pack/plugins/cases/server/client/sub_cases/client.ts @@ -5,10 +5,13 @@ * 2.0. */ +import pMap from 'p-map'; import Boom from '@hapi/boom'; +import { SavedObject } from 'kibana/server'; import { caseStatuses, + CommentAttributes, SubCaseResponse, SubCaseResponseRt, SubCasesFindRequest, @@ -19,7 +22,7 @@ import { import { CasesClientArgs, CasesClientInternal } from '..'; import { countAlertsForID, flattenSubCaseSavedObject, transformSubCases } from '../../common'; import { createCaseError } from '../../common/error'; -import { CASE_SAVED_OBJECT } from '../../../common/constants'; +import { CASE_SAVED_OBJECT, MAX_CONCURRENT_SEARCHES } from '../../../common/constants'; import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; import { constructQueryOptions } from '../utils'; import { defaultPage, defaultPerPage } from '../../routes/api'; @@ -121,13 +124,20 @@ async function deleteSubCase(ids: string[], clientArgs: CasesClientArgs): Promis return acc; }, new Map()); - await Promise.all( - comments.saved_objects.map((comment) => - attachmentService.delete({ unsecuredSavedObjectsClient, attachmentId: comment.id }) - ) - ); + const deleteCommentMapper = async (comment: SavedObject) => + attachmentService.delete({ unsecuredSavedObjectsClient, attachmentId: comment.id }); - await Promise.all(ids.map((id) => caseService.deleteSubCase(unsecuredSavedObjectsClient, id))); + const deleteSubCasesMapper = async (id: string) => + caseService.deleteSubCase(unsecuredSavedObjectsClient, id); + + // Ensuring we don't too many concurrent deletions running. + await pMap(comments.saved_objects, deleteCommentMapper, { + concurrency: MAX_CONCURRENT_SEARCHES, + }); + + await pMap(ids, deleteSubCasesMapper, { + concurrency: MAX_CONCURRENT_SEARCHES, + }); const deleteDate = new Date().toISOString(); @@ -181,6 +191,7 @@ async function find( }, }); + // casesStatuses are bounded by us. No need to limit concurrent calls. const [open, inProgress, closed] = await Promise.all([ ...caseStatuses.map((status) => { const { subCase: statusQueryOptions } = constructQueryOptions({ diff --git a/x-pack/plugins/cases/server/common/models/commentable_case.ts b/x-pack/plugins/cases/server/common/models/commentable_case.ts index 2d1e1e18b509..241278c77ab4 100644 --- a/x-pack/plugins/cases/server/common/models/commentable_case.ts +++ b/x-pack/plugins/cases/server/common/models/commentable_case.ts @@ -34,7 +34,11 @@ import { flattenSubCaseSavedObject, transformNewComment, } from '..'; -import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../common/constants'; +import { + CASE_SAVED_OBJECT, + MAX_DOCS_PER_PAGE, + SUB_CASE_SAVED_OBJECT, +} from '../../../common/constants'; import { AttachmentService, CasesService } from '../../services'; import { createCaseError } from '../error'; import { countAlertsForID } from '../index'; @@ -309,23 +313,13 @@ export class CommentableCase { public async encode(): Promise { try { - const collectionCommentStats = await this.caseService.getAllCaseComments({ - unsecuredSavedObjectsClient: this.unsecuredSavedObjectsClient, - id: this.collection.id, - options: { - fields: [], - page: 1, - perPage: 1, - }, - }); - const collectionComments = await this.caseService.getAllCaseComments({ unsecuredSavedObjectsClient: this.unsecuredSavedObjectsClient, id: this.collection.id, options: { fields: [], page: 1, - perPage: collectionCommentStats.total, + perPage: MAX_DOCS_PER_PAGE, }, }); @@ -335,7 +329,7 @@ export class CommentableCase { const caseResponse = { comments: flattenCommentSavedObjects(collectionComments.saved_objects), totalAlerts: collectionTotalAlerts, - ...this.formatCollectionForEncoding(collectionCommentStats.total), + ...this.formatCollectionForEncoding(collectionComments.total), }; if (this.subCase) { diff --git a/x-pack/plugins/cases/server/services/attachments/index.ts b/x-pack/plugins/cases/server/services/attachments/index.ts index c9b9d11a8968..077f00a571fa 100644 --- a/x-pack/plugins/cases/server/services/attachments/index.ts +++ b/x-pack/plugins/cases/server/services/attachments/index.ts @@ -57,10 +57,10 @@ export class AttachmentService { public async delete({ unsecuredSavedObjectsClient, attachmentId }: GetAttachmentArgs) { try { - this.log.debug(`Attempting to GET attachment ${attachmentId}`); + this.log.debug(`Attempting to DELETE attachment ${attachmentId}`); return await unsecuredSavedObjectsClient.delete(CASE_COMMENT_SAVED_OBJECT, attachmentId); } catch (error) { - this.log.error(`Error on GET attachment ${attachmentId}: ${error}`); + this.log.error(`Error on DELETE attachment ${attachmentId}: ${error}`); throw error; } } diff --git a/x-pack/plugins/cases/server/services/cases/index.ts b/x-pack/plugins/cases/server/services/cases/index.ts index 5618f6c83ff0..fd0a5e878bfb 100644 --- a/x-pack/plugins/cases/server/services/cases/index.ts +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { cloneDeep } from 'lodash'; +import pMap from 'p-map'; import { KibanaRequest, Logger, @@ -43,7 +43,11 @@ import { groupTotalAlertsByID, SavedObjectFindOptionsKueryNode, } from '../../common'; -import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; +import { + ENABLE_CASE_CONNECTOR, + MAX_CONCURRENT_SEARCHES, + MAX_DOCS_PER_PAGE, +} from '../../../common/constants'; import { defaultPage, defaultPerPage } from '../../routes/api'; import { CASE_SAVED_OBJECT, @@ -250,7 +254,7 @@ export class CasesService { filter, ]); - let response = await unsecuredSavedObjectsClient.find< + const response = await unsecuredSavedObjectsClient.find< CommentAttributes, GetCaseIdsByAlertIdAggs >({ @@ -259,23 +263,9 @@ export class CasesService { page: 1, perPage: 1, sortField: defaultSortField, - aggs: this.buildCaseIdsAggs(), + aggs: this.buildCaseIdsAggs(MAX_DOCS_PER_PAGE), filter: combinedFilter, }); - if (response.total > 100) { - response = await unsecuredSavedObjectsClient.find< - CommentAttributes, - GetCaseIdsByAlertIdAggs - >({ - type: CASE_COMMENT_SAVED_OBJECT, - fields: includeFieldsRequiredForAuthentication(), - page: 1, - perPage: 1, - sortField: defaultSortField, - aggs: this.buildCaseIdsAggs(response.total), - filter: combinedFilter, - }); - } return response; } catch (error) { this.log.error(`Error on GET all cases for alert id ${alertId}: ${error}`); @@ -393,16 +383,6 @@ export class CasesService { ensureSavedObjectsAreAuthorized: EnsureSOAuthCallback; subCaseOptions?: SavedObjectFindOptionsKueryNode; }): Promise { - const casesStats = await this.findCases({ - unsecuredSavedObjectsClient, - options: { - ...caseOptions, - fields: [], - page: 1, - perPage: 1, - }, - }); - /** * This could be made more performant. What we're doing here is retrieving all cases * that match the API request's filters instead of just counts. This is because we need to grab @@ -429,7 +409,7 @@ export class CasesService { ...caseOptions, fields: includeFieldsRequiredForAuthentication([caseTypeField]), page: 1, - perPage: casesStats.total, + perPage: MAX_DOCS_PER_PAGE, }, }); @@ -447,7 +427,7 @@ export class CasesService { if (ENABLE_CASE_CONNECTOR && subCaseOptions) { subCasesTotal = await this.findSubCaseStatusStats({ unsecuredSavedObjectsClient, - options: cloneDeep(subCaseOptions), + options: subCaseOptions, ids: caseIds, }); } @@ -505,16 +485,18 @@ export class CasesService { const refType = associationType === AssociationType.case ? CASE_SAVED_OBJECT : SUB_CASE_SAVED_OBJECT; - const allComments = await Promise.all( - ids.map((id) => - this.getCommentsByAssociation({ - unsecuredSavedObjectsClient, - associationType, - id, - options: { page: 1, perPage: 1 }, - }) - ) - ); + const getCommentsMapper = async (id: string) => + this.getCommentsByAssociation({ + unsecuredSavedObjectsClient, + associationType, + id, + options: { page: 1, perPage: 1 }, + }); + + // Ensuring we don't too many concurrent get running. + const allComments = await pMap(ids, getCommentsMapper, { + concurrency: MAX_CONCURRENT_SEARCHES, + }); const alerts = await this.getCommentsByAssociation({ unsecuredSavedObjectsClient, @@ -795,7 +777,7 @@ export class CasesService { this.log.debug(`Attempting to find cases`); return await unsecuredSavedObjectsClient.find({ sortField: defaultSortField, - ...cloneDeep(options), + ...options, type: CASE_SAVED_OBJECT, }); } catch (error) { @@ -815,24 +797,16 @@ export class CasesService { if (options?.page !== undefined || options?.perPage !== undefined) { return unsecuredSavedObjectsClient.find({ sortField: defaultSortField, - ...cloneDeep(options), + ...options, type: SUB_CASE_SAVED_OBJECT, }); } - const stats = await unsecuredSavedObjectsClient.find({ - fields: [], - page: 1, - perPage: 1, - sortField: defaultSortField, - ...cloneDeep(options), - type: SUB_CASE_SAVED_OBJECT, - }); return unsecuredSavedObjectsClient.find({ page: 1, - perPage: stats.total, + perPage: MAX_DOCS_PER_PAGE, sortField: defaultSortField, - ...cloneDeep(options), + ...options, type: SUB_CASE_SAVED_OBJECT, }); } catch (error) { @@ -902,26 +876,16 @@ export class CasesService { return unsecuredSavedObjectsClient.find({ type: CASE_COMMENT_SAVED_OBJECT, sortField: defaultSortField, - ...cloneDeep(options), + ...options, }); } - // get the total number of comments that are in ES then we'll grab them all in one go - const stats = await unsecuredSavedObjectsClient.find({ - type: CASE_COMMENT_SAVED_OBJECT, - fields: [], - page: 1, - perPage: 1, - sortField: defaultSortField, - // spread the options after so the caller can override the default behavior if they want - ...cloneDeep(options), - }); return unsecuredSavedObjectsClient.find({ type: CASE_COMMENT_SAVED_OBJECT, page: 1, - perPage: stats.total, + perPage: MAX_DOCS_PER_PAGE, sortField: defaultSortField, - ...cloneDeep(options), + ...options, }); } catch (error) { this.log.error(`Error on GET all comments internal for ${JSON.stringify(id)}: ${error}`); @@ -1022,20 +986,13 @@ export class CasesService { }: GetReportersArgs): Promise> { try { this.log.debug(`Attempting to GET all reporters`); - const firstReporters = await unsecuredSavedObjectsClient.find({ - type: CASE_SAVED_OBJECT, - fields: ['created_by', OWNER_FIELD], - page: 1, - perPage: 1, - filter: cloneDeep(filter), - }); return await unsecuredSavedObjectsClient.find({ type: CASE_SAVED_OBJECT, fields: ['created_by', OWNER_FIELD], page: 1, - perPage: firstReporters.total, - filter: cloneDeep(filter), + perPage: MAX_DOCS_PER_PAGE, + filter, }); } catch (error) { this.log.error(`Error on GET all reporters: ${error}`); @@ -1049,20 +1006,13 @@ export class CasesService { }: GetTagsArgs): Promise> { try { this.log.debug(`Attempting to GET all cases`); - const firstTags = await unsecuredSavedObjectsClient.find({ - type: CASE_SAVED_OBJECT, - fields: ['tags', OWNER_FIELD], - page: 1, - perPage: 1, - filter: cloneDeep(filter), - }); return await unsecuredSavedObjectsClient.find({ type: CASE_SAVED_OBJECT, fields: ['tags', OWNER_FIELD], page: 1, - perPage: firstTags.total, - filter: cloneDeep(filter), + perPage: MAX_DOCS_PER_PAGE, + filter, }); } catch (error) { this.log.error(`Error on GET tags: ${error}`); diff --git a/x-pack/plugins/cases/server/services/configure/index.ts b/x-pack/plugins/cases/server/services/configure/index.ts index 8ea1c903622b..745e64d9016f 100644 --- a/x-pack/plugins/cases/server/services/configure/index.ts +++ b/x-pack/plugins/cases/server/services/configure/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { cloneDeep } from 'lodash'; import { Logger, SavedObjectsClientContract } from 'kibana/server'; import { SavedObjectFindOptionsKueryNode } from '../../common'; @@ -63,7 +62,7 @@ export class CaseConfigureService { try { this.log.debug(`Attempting to find all case configuration`); return await unsecuredSavedObjectsClient.find({ - ...cloneDeep(options), + ...options, // Get the latest configuration sortField: 'created_at', sortOrder: 'desc', 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 e691b9305fb3..2c20bf808ad4 100644 --- a/x-pack/plugins/cases/server/services/user_actions/index.ts +++ b/x-pack/plugins/cases/server/services/user_actions/index.ts @@ -12,6 +12,7 @@ import { CASE_USER_ACTION_SAVED_OBJECT, CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT, + MAX_DOCS_PER_PAGE, } from '../../../common/constants'; import { ClientArgs } from '..'; @@ -36,19 +37,12 @@ export class CaseUserActionService { try { const id = subCaseId ?? caseId; const type = subCaseId ? SUB_CASE_SAVED_OBJECT : CASE_SAVED_OBJECT; - const caseUserActionInfo = await unsecuredSavedObjectsClient.find({ - type: CASE_USER_ACTION_SAVED_OBJECT, - fields: [], - hasReference: { type, id }, - page: 1, - perPage: 1, - }); return await unsecuredSavedObjectsClient.find({ type: CASE_USER_ACTION_SAVED_OBJECT, hasReference: { type, id }, page: 1, - perPage: caseUserActionInfo.total, + perPage: MAX_DOCS_PER_PAGE, sortField: 'action_at', sortOrder: 'asc', });