diff --git a/x-pack/plugins/cases/server/authorization/utils.ts b/x-pack/plugins/cases/server/authorization/utils.ts index b44c94d21fb5ba..a7e210d07d214f 100644 --- a/x-pack/plugins/cases/server/authorization/utils.ts +++ b/x-pack/plugins/cases/server/authorization/utils.ts @@ -6,8 +6,7 @@ */ import { remove, uniq } from 'lodash'; -import { nodeBuilder } from '../../../../../src/plugins/data/common'; -import { KueryNode } from '../../../../../src/plugins/data/server'; +import { nodeBuilder, KueryNode } from '../../../../../src/plugins/data/common'; export const getOwnersFilter = (savedObjectType: string, owners: string[]): KueryNode => { return nodeBuilder.or( diff --git a/x-pack/plugins/cases/server/client/alerts/client.ts b/x-pack/plugins/cases/server/client/alerts/client.ts new file mode 100644 index 00000000000000..dfa06c0277bda2 --- /dev/null +++ b/x-pack/plugins/cases/server/client/alerts/client.ts @@ -0,0 +1,58 @@ +/* + * 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 { CaseStatuses } from '../../../common/api'; +import { AlertInfo } from '../../common'; +import { CasesClientGetAlertsResponse } from './types'; +import { get } from './get'; +import { updateStatus } from './update_status'; +import { CasesClientArgs } from '../types'; + +/** + * Defines the fields necessary to update an alert's status. + */ +export interface UpdateAlertRequest { + id: string; + index: string; + status: CaseStatuses; +} + +export interface AlertUpdateStatus { + alerts: UpdateAlertRequest[]; +} + +export interface AlertGet { + alertsInfo: AlertInfo[]; +} + +export interface AlertSubClient { + get(args: AlertGet): Promise; + updateStatus(args: AlertUpdateStatus): Promise; +} + +export const createAlertsSubClient = (args: CasesClientArgs): AlertSubClient => { + const { alertsService, scopedClusterClient, logger } = args; + + const alertsSubClient: AlertSubClient = { + get: (params: AlertGet) => + get({ + ...params, + alertsService, + scopedClusterClient, + logger, + }), + updateStatus: (params: AlertUpdateStatus) => + updateStatus({ + ...params, + alertsService, + scopedClusterClient, + logger, + }), + }; + + return Object.freeze(alertsSubClient); +}; diff --git a/x-pack/plugins/cases/server/client/alerts/update_status.test.ts b/x-pack/plugins/cases/server/client/alerts/update_status.test.ts index 5dfe6060da1db8..44d6fc244270ab 100644 --- a/x-pack/plugins/cases/server/client/alerts/update_status.test.ts +++ b/x-pack/plugins/cases/server/client/alerts/update_status.test.ts @@ -14,7 +14,7 @@ describe('updateAlertsStatus', () => { const savedObjectsClient = createMockSavedObjectsRepository(); const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - await casesClient.client.updateAlertsStatus({ + await casesClient.client.updateStatus({ alerts: [{ id: 'alert-id-1', index: '.siem-signals', status: CaseStatuses.closed }], }); diff --git a/x-pack/plugins/cases/server/client/alerts/update_status.ts b/x-pack/plugins/cases/server/client/alerts/update_status.ts index cd6f97273d6d7e..e02a98c396e0a9 100644 --- a/x-pack/plugins/cases/server/client/alerts/update_status.ts +++ b/x-pack/plugins/cases/server/client/alerts/update_status.ts @@ -7,7 +7,7 @@ import { ElasticsearchClient, Logger } from 'src/core/server'; import { AlertServiceContract } from '../../services'; -import { UpdateAlertRequest } from '../types'; +import { UpdateAlertRequest } from './client'; interface UpdateAlertsStatusArgs { alertsService: AlertServiceContract; @@ -16,7 +16,7 @@ interface UpdateAlertsStatusArgs { logger: Logger; } -export const updateAlertsStatus = async ({ +export const updateStatus = async ({ alertsService, alerts, scopedClusterClient, diff --git a/x-pack/plugins/cases/server/client/comments/add.test.ts b/x-pack/plugins/cases/server/client/attachments/add.test.ts similarity index 100% rename from x-pack/plugins/cases/server/client/comments/add.test.ts rename to x-pack/plugins/cases/server/client/attachments/add.test.ts diff --git a/x-pack/plugins/cases/server/client/comments/add.ts b/x-pack/plugins/cases/server/client/attachments/add.ts similarity index 82% rename from x-pack/plugins/cases/server/client/comments/add.ts rename to x-pack/plugins/cases/server/client/attachments/add.ts index f077571019f600..659ff14418d05f 100644 --- a/x-pack/plugins/cases/server/client/comments/add.ts +++ b/x-pack/plugins/cases/server/client/attachments/add.ts @@ -32,9 +32,9 @@ import { buildCommentUserActionItem, } from '../../services/user_actions/helpers'; -import { CaseServiceSetup, CaseUserActionServiceSetup } from '../../services'; +import { AttachmentService, CaseService, CaseUserActionService } from '../../services'; import { CommentableCase, createAlertUpdateRequest } from '../../common'; -import { CasesClientHandler } from '..'; +import { CasesClientArgs, CasesClientInternal } from '..'; import { createCaseError } from '../../common/error'; import { MAX_GENERATED_ALERTS_PER_SUB_CASE, @@ -50,17 +50,17 @@ async function getSubCase({ userActionService, user, }: { - caseService: CaseServiceSetup; + caseService: CaseService; savedObjectsClient: SavedObjectsClientContract; caseId: string; createdAt: string; - userActionService: CaseUserActionServiceSetup; + userActionService: CaseUserActionService; user: User; }): Promise> { const mostRecentSubCase = await caseService.getMostRecentSubCase(savedObjectsClient, caseId); if (mostRecentSubCase && mostRecentSubCase.attributes.status !== CaseStatuses.closed) { const subCaseAlertsAttachement = await caseService.getAllSubCaseComments({ - client: savedObjectsClient, + soClient: savedObjectsClient, id: mostRecentSubCase.id, options: { fields: [], @@ -79,13 +79,13 @@ async function getSubCase({ } const newSubCase = await caseService.createSubCase({ - client: savedObjectsClient, + soClient: savedObjectsClient, createdAt, caseId, createdBy: user, }); - await userActionService.postUserActions({ - client: savedObjectsClient, + await userActionService.bulkCreate({ + soClient: savedObjectsClient, actions: [ buildCaseUserActionItem({ action: 'create', @@ -102,20 +102,22 @@ async function getSubCase({ } interface AddCommentFromRuleArgs { - casesClient: CasesClientHandler; + casesClientInternal: CasesClientInternal; caseId: string; comment: CommentRequestAlertType; savedObjectsClient: SavedObjectsClientContract; - caseService: CaseServiceSetup; - userActionService: CaseUserActionServiceSetup; + attachmentService: AttachmentService; + caseService: CaseService; + userActionService: CaseUserActionService; logger: Logger; } const addGeneratedAlerts = async ({ savedObjectsClient, + attachmentService, caseService, userActionService, - casesClient, + casesClientInternal, caseId, comment, logger, @@ -136,7 +138,7 @@ const addGeneratedAlerts = async ({ const createdDate = new Date().toISOString(); const caseInfo = await caseService.getCase({ - client: savedObjectsClient, + soClient: savedObjectsClient, id: caseId, }); @@ -167,7 +169,8 @@ const addGeneratedAlerts = async ({ collection: caseInfo, subCase, soClient: savedObjectsClient, - service: caseService, + caseService, + attachmentService, }); const { @@ -184,13 +187,13 @@ const addGeneratedAlerts = async ({ comment: query, status: subCase.attributes.status, }); - await casesClient.updateAlertsStatus({ + await casesClientInternal.alerts.updateStatus({ alerts: alertsToUpdate, }); } - await userActionService.postUserActions({ - client: savedObjectsClient, + await userActionService.bulkCreate({ + soClient: savedObjectsClient, actions: [ buildCommentUserActionItem({ action: 'create', @@ -216,25 +219,27 @@ const addGeneratedAlerts = async ({ }; async function getCombinedCase({ - service, - client, + caseService, + attachmentService, + soClient, id, logger, }: { - service: CaseServiceSetup; - client: SavedObjectsClientContract; + caseService: CaseService; + attachmentService: AttachmentService; + soClient: SavedObjectsClientContract; id: string; logger: Logger; }): Promise { const [casePromise, subCasePromise] = await Promise.allSettled([ - service.getCase({ - client, + caseService.getCase({ + soClient, id, }), ...(ENABLE_CASE_CONNECTOR ? [ - service.getSubCase({ - client, + caseService.getSubCase({ + soClient, id, }), ] @@ -243,16 +248,17 @@ async function getCombinedCase({ if (subCasePromise.status === 'fulfilled') { if (subCasePromise.value.references.length > 0) { - const caseValue = await service.getCase({ - client, + const caseValue = await caseService.getCase({ + soClient, id: subCasePromise.value.references[0].id, }); return new CommentableCase({ logger, collection: caseValue, subCase: subCasePromise.value, - service, - soClient: client, + caseService, + attachmentService, + soClient, }); } else { throw Boom.badRequest('Sub case found without reference to collection'); @@ -265,38 +271,39 @@ async function getCombinedCase({ return new CommentableCase({ logger, collection: casePromise.value, - service, - soClient: client, + caseService, + attachmentService, + soClient, }); } } interface AddCommentArgs { - casesClient: CasesClientHandler; caseId: string; comment: CommentRequest; - savedObjectsClient: SavedObjectsClientContract; - caseService: CaseServiceSetup; - userActionService: CaseUserActionServiceSetup; - user: User; - logger: Logger; + casesClientInternal: CasesClientInternal; } export const addComment = async ({ - savedObjectsClient, - caseService, - userActionService, - casesClient, caseId, comment, - user, - logger, -}: AddCommentArgs): Promise => { + casesClientInternal, + ...rest +}: AddCommentArgs & CasesClientArgs): Promise => { const query = pipe( CommentRequestRt.decode(comment), fold(throwErrors(Boom.badRequest), identity) ); + const { + savedObjectsClient, + caseService, + userActionService, + attachmentService, + user, + logger, + } = rest; + if (isCommentRequestTypeGenAlert(comment)) { if (!ENABLE_CASE_CONNECTOR) { throw Boom.badRequest( @@ -307,10 +314,11 @@ export const addComment = async ({ return addGeneratedAlerts({ caseId, comment, - casesClient, + casesClientInternal, savedObjectsClient, userActionService, caseService, + attachmentService, logger, }); } @@ -320,8 +328,9 @@ export const addComment = async ({ const createdDate = new Date().toISOString(); const combinedCase = await getCombinedCase({ - service: caseService, - client: savedObjectsClient, + caseService, + attachmentService, + soClient: savedObjectsClient, id: caseId, logger, }); @@ -346,13 +355,13 @@ export const addComment = async ({ status: updatedCase.status, }); - await casesClient.updateAlertsStatus({ + await casesClientInternal.alerts.updateStatus({ alerts: alertsToUpdate, }); } - await userActionService.postUserActions({ - client: savedObjectsClient, + await userActionService.bulkCreate({ + soClient: savedObjectsClient, actions: [ buildCommentUserActionItem({ action: 'create', diff --git a/x-pack/plugins/cases/server/client/attachments/client.ts b/x-pack/plugins/cases/server/client/attachments/client.ts new file mode 100644 index 00000000000000..f3ee3098a3153f --- /dev/null +++ b/x-pack/plugins/cases/server/client/attachments/client.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CaseResponse, CommentRequest as AttachmentsRequest } from '../../../common/api'; +import { CasesClientInternal } from '../client_internal'; +import { CasesClientArgs } from '../types'; +import { addComment } from './add'; + +export interface AttachmentsAdd { + caseId: string; + comment: AttachmentsRequest; +} + +export interface AttachmentsSubClient { + add(args: AttachmentsAdd): Promise; +} + +export const createAttachmentsSubClient = ( + args: CasesClientArgs, + casesClientInternal: CasesClientInternal +): AttachmentsSubClient => { + const attachmentSubClient: AttachmentsSubClient = { + add: ({ caseId, comment }: AttachmentsAdd) => + addComment({ + ...args, + casesClientInternal, + caseId, + comment, + }), + }; + + return Object.freeze(attachmentSubClient); +}; diff --git a/x-pack/plugins/cases/server/client/cases/client.ts b/x-pack/plugins/cases/server/client/cases/client.ts new file mode 100644 index 00000000000000..9c9bf1fa7641d2 --- /dev/null +++ b/x-pack/plugins/cases/server/client/cases/client.ts @@ -0,0 +1,115 @@ +/* + * 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 { ActionsClient } from '../../../../actions/server'; +import { + CasePostRequest, + CaseResponse, + CasesPatchRequest, + CasesResponse, + CasesFindRequest, + CasesFindResponse, +} from '../../../common/api'; +import { CasesClient } from '../client'; +import { CasesClientInternal } from '../client_internal'; +import { CasesClientArgs } from '../types'; +import { create } from './create'; +import { find } from './find'; +import { get } from './get'; +import { push } from './push'; +import { update } from './update'; + +export interface CaseGet { + id: string; + includeComments?: boolean; + includeSubCaseComments?: boolean; +} + +export interface CasePush { + actionsClient: ActionsClient; + caseId: string; + connectorId: string; +} + +export interface CasesSubClient { + create(theCase: CasePostRequest): Promise; + find(args: CasesFindRequest): Promise; + get(args: CaseGet): Promise; + push(args: CasePush): Promise; + update(args: CasesPatchRequest): Promise; +} + +export const createCasesSubClient = ( + args: CasesClientArgs, + casesClient: CasesClient, + casesClientInternal: CasesClientInternal +): CasesSubClient => { + const { + attachmentService, + caseConfigureService, + caseService, + user, + savedObjectsClient, + userActionService, + logger, + authorization, + } = args; + + const casesSubClient: CasesSubClient = { + create: (theCase: CasePostRequest) => + create({ + savedObjectsClient, + caseService, + caseConfigureService, + userActionService, + user, + theCase, + logger, + auth: authorization, + }), + find: (options: CasesFindRequest) => + find({ + savedObjectsClient, + caseService, + logger, + auth: authorization, + options, + }), + get: (params: CaseGet) => + get({ + ...params, + caseService, + savedObjectsClient, + logger, + }), + push: (params: CasePush) => + push({ + ...params, + attachmentService, + savedObjectsClient, + caseService, + userActionService, + user, + casesClient, + casesClientInternal, + caseConfigureService, + logger, + }), + update: (cases: CasesPatchRequest) => + update({ + savedObjectsClient, + caseService, + userActionService, + user, + cases, + casesClientInternal, + logger, + }), + }; + + return Object.freeze(casesSubClient); +}; diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index 34fdb7aff14a21..935ca6d3199d2f 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -30,22 +30,18 @@ import { transformCaseConnectorToEsConnector, } from '../../routes/api/cases/helpers'; -import { - CaseConfigureServiceSetup, - CaseServiceSetup, - CaseUserActionServiceSetup, -} from '../../services'; +import { CaseConfigureService, CaseService, CaseUserActionService } from '../../services'; import { createCaseError } from '../../common/error'; import { Authorization } from '../../authorization/authorization'; import { WriteOperations } from '../../authorization/types'; import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; interface CreateCaseArgs { - caseConfigureService: CaseConfigureServiceSetup; - caseService: CaseServiceSetup; + caseConfigureService: CaseConfigureService; + caseService: CaseService; user: User; savedObjectsClient: SavedObjectsClientContract; - userActionService: CaseUserActionServiceSetup; + userActionService: CaseUserActionService; theCase: CasePostRequest; logger: Logger; auth: PublicMethodsOf; @@ -93,11 +89,11 @@ export const create = async ({ // eslint-disable-next-line @typescript-eslint/naming-convention const { username, full_name, email } = user; const createdDate = new Date().toISOString(); - const myCaseConfigure = await caseConfigureService.find({ client: savedObjectsClient }); + const myCaseConfigure = await caseConfigureService.find({ soClient: savedObjectsClient }); const caseConfigureConnector = getConnectorFromConfiguration(myCaseConfigure); const newCase = await caseService.postNewCase({ - client: savedObjectsClient, + soClient: savedObjectsClient, attributes: transformNewCase({ createdDate, newCase: query, @@ -108,8 +104,8 @@ export const create = async ({ }), }); - await userActionService.postUserActions({ - client: savedObjectsClient, + await userActionService.bulkCreate({ + soClient: savedObjectsClient, actions: [ buildCaseUserActionItem({ action: 'create', diff --git a/x-pack/plugins/cases/server/client/cases/find.ts b/x-pack/plugins/cases/server/client/cases/find.ts index 24e8cb6ec5f886..33545a39258893 100644 --- a/x-pack/plugins/cases/server/client/cases/find.ts +++ b/x-pack/plugins/cases/server/client/cases/find.ts @@ -23,7 +23,7 @@ import { } from '../../../common/api'; import { CASE_SAVED_OBJECT } from '../../../common/constants'; -import { CaseServiceSetup } from '../../services'; +import { CaseService } from '../../services'; import { createCaseError } from '../../common/error'; import { constructQueryOptions } from '../../routes/api/cases/helpers'; import { transformCases } from '../../routes/api/utils'; @@ -32,7 +32,7 @@ import { includeFieldsRequiredForAuthentication } from '../../authorization/util interface FindParams { savedObjectsClient: SavedObjectsClientContract; - caseService: CaseServiceSetup; + caseService: CaseService; logger: Logger; auth: PublicMethodsOf; options: CasesFindRequest; @@ -71,7 +71,7 @@ export const find = async ({ const caseQueries = constructQueryOptions({ ...queryArgs, authorizationFilter }); const cases = await caseService.findCasesGroupedByID({ - client: savedObjectsClient, + soClient: savedObjectsClient, caseOptions: { ...queryParams, ...caseQueries.case, @@ -97,7 +97,7 @@ export const find = async ({ ...caseStatuses.map((status) => { const statusQuery = constructQueryOptions({ ...queryArgs, status, authorizationFilter }); return caseService.findCaseStatusStats({ - client: savedObjectsClient, + soClient: savedObjectsClient, caseOptions: statusQuery.case, subCaseOptions: statusQuery.subCase, }); diff --git a/x-pack/plugins/cases/server/client/cases/get.ts b/x-pack/plugins/cases/server/client/cases/get.ts index 50725879278e4b..ccef35007118f8 100644 --- a/x-pack/plugins/cases/server/client/cases/get.ts +++ b/x-pack/plugins/cases/server/client/cases/get.ts @@ -8,14 +8,14 @@ import { SavedObjectsClientContract, Logger, SavedObject } from 'kibana/server'; import { flattenCaseSavedObject } from '../../routes/api/utils'; import { CaseResponseRt, CaseResponse, ESCaseAttributes } from '../../../common/api'; -import { CaseServiceSetup } from '../../services'; +import { CaseService } from '../../services'; import { countAlertsForID } from '../../common'; import { createCaseError } from '../../common/error'; import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; interface GetParams { savedObjectsClient: SavedObjectsClientContract; - caseService: CaseServiceSetup; + caseService: CaseService; id: string; includeComments?: boolean; includeSubCaseComments?: boolean; @@ -40,17 +40,17 @@ export const get = async ({ if (ENABLE_CASE_CONNECTOR) { const [caseInfo, subCasesForCaseId] = await Promise.all([ caseService.getCase({ - client: savedObjectsClient, + soClient: savedObjectsClient, id, }), - caseService.findSubCasesByCaseId({ client: savedObjectsClient, ids: [id] }), + caseService.findSubCasesByCaseId({ soClient: savedObjectsClient, ids: [id] }), ]); theCase = caseInfo; subCaseIds = subCasesForCaseId.saved_objects.map((so) => so.id); } else { theCase = await caseService.getCase({ - client: savedObjectsClient, + soClient: savedObjectsClient, id, }); } @@ -64,7 +64,7 @@ export const get = async ({ ); } const theComments = await caseService.getAllCaseComments({ - client: savedObjectsClient, + soClient: savedObjectsClient, id, options: { sortField: 'created_at', diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index 216ef109534fbd..c2c4d11da991dc 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -34,13 +34,14 @@ import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; import { createIncident, getCommentContextFromAttributes } from './utils'; import { - CaseConfigureServiceSetup, - CaseServiceSetup, - CaseUserActionServiceSetup, + CaseConfigureService, + CaseService, + CaseUserActionService, + AttachmentService, } from '../../services'; -import { CasesClientHandler } from '../client'; import { createCaseError } from '../../common/error'; import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; +import { CasesClient, CasesClientInternal } from '..'; /** * Returns true if the case should be closed based on the configuration settings and whether the case @@ -60,23 +61,27 @@ function shouldCloseByPush( interface PushParams { savedObjectsClient: SavedObjectsClientContract; - caseService: CaseServiceSetup; - caseConfigureService: CaseConfigureServiceSetup; - userActionService: CaseUserActionServiceSetup; + caseService: CaseService; + caseConfigureService: CaseConfigureService; + userActionService: CaseUserActionService; + attachmentService: AttachmentService; user: User; caseId: string; connectorId: string; - casesClient: CasesClientHandler; + casesClient: CasesClient; + casesClientInternal: CasesClientInternal; actionsClient: ActionsClient; logger: Logger; } export const push = async ({ savedObjectsClient, + attachmentService, caseService, caseConfigureService, userActionService, casesClient, + casesClientInternal, actionsClient, connectorId, caseId, @@ -93,13 +98,13 @@ export const push = async ({ try { [theCase, connector, userActions] = await Promise.all([ - casesClient.get({ + casesClient.cases.get({ id: caseId, includeComments: true, includeSubCaseComments: ENABLE_CASE_CONNECTOR, }), actionsClient.get({ id: connectorId }), - casesClient.getUserActions({ caseId }), + casesClient.userActions.getAll({ caseId }), ]); } catch (e) { const message = `Error getting case and/or connector and/or user actions: ${e.message}`; @@ -116,7 +121,7 @@ export const push = async ({ const alertsInfo = getAlertInfoFromComments(theCase?.comments); try { - alerts = await casesClient.getAlerts({ + alerts = await casesClientInternal.alerts.get({ alertsInfo, }); } catch (e) { @@ -128,7 +133,7 @@ export const push = async ({ } try { - connectorMappings = await casesClient.getMappings({ + connectorMappings = await casesClientInternal.configuration.getMappings({ actionsClient, connectorId: connector.id, connectorType: connector.actionTypeId, @@ -176,12 +181,12 @@ export const push = async ({ try { [myCase, myCaseConfigure, comments] = await Promise.all([ caseService.getCase({ - client: savedObjectsClient, + soClient: savedObjectsClient, id: caseId, }), - caseConfigureService.find({ client: savedObjectsClient }), + caseConfigureService.find({ soClient: savedObjectsClient }), caseService.getAllCaseComments({ - client: savedObjectsClient, + soClient: savedObjectsClient, id: caseId, options: { fields: [], @@ -219,7 +224,7 @@ export const push = async ({ try { [updatedCase, updatedComments] = await Promise.all([ caseService.patchCase({ - client: savedObjectsClient, + soClient: savedObjectsClient, caseId, updatedAttributes: { ...(shouldMarkAsClosed @@ -236,12 +241,12 @@ export const push = async ({ version: myCase.version, }), - caseService.patchComments({ - client: savedObjectsClient, + attachmentService.bulkUpdate({ + soClient: savedObjectsClient, comments: comments.saved_objects .filter((comment) => comment.attributes.pushed_at == null) .map((comment) => ({ - commentId: comment.id, + attachmentId: comment.id, updatedAttributes: { pushed_at: pushedDate, pushed_by: { username, full_name, email }, @@ -250,8 +255,8 @@ export const push = async ({ })), }), - userActionService.postUserActions({ - client: savedObjectsClient, + userActionService.bulkCreate({ + soClient: savedObjectsClient, actions: [ ...(shouldMarkAsClosed ? [ diff --git a/x-pack/plugins/cases/server/client/cases/update.ts b/x-pack/plugins/cases/server/client/cases/update.ts index fa9df2060ac5b7..52674e4c1b461c 100644 --- a/x-pack/plugins/cases/server/client/cases/update.ts +++ b/x-pack/plugins/cases/server/client/cases/update.ts @@ -47,17 +47,17 @@ import { transformCaseConnectorToEsConnector, } from '../../routes/api/cases/helpers'; -import { CaseServiceSetup, CaseUserActionServiceSetup } from '../../services'; +import { CaseService, CaseUserActionService } from '../../services'; import { CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT, } from '../../../common/constants'; -import { CasesClientHandler } from '..'; import { createAlertUpdateRequest } from '../../common'; -import { UpdateAlertRequest } from '../types'; import { createCaseError } from '../../common/error'; import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; +import { UpdateAlertRequest } from '../alerts/client'; +import { CasesClientInternal } from '../client_internal'; /** * Throws an error if any of the requests attempt to update a collection style cases' status field. @@ -123,15 +123,15 @@ function throwIfUpdateType(requests: ESCasePatchRequest[]) { async function throwIfInvalidUpdateOfTypeWithAlerts({ requests, caseService, - client, + soClient, }: { requests: ESCasePatchRequest[]; - caseService: CaseServiceSetup; - client: SavedObjectsClientContract; + caseService: CaseService; + soClient: SavedObjectsClientContract; }) { const getAlertsForID = async (caseToUpdate: ESCasePatchRequest) => { const alerts = await caseService.getAllCaseComments({ - client, + soClient, id: caseToUpdate.id, options: { fields: [], @@ -185,17 +185,17 @@ function getID( async function getAlertComments({ casesToSync, caseService, - client, + soClient, }: { casesToSync: ESCasePatchRequest[]; - caseService: CaseServiceSetup; - client: SavedObjectsClientContract; + caseService: CaseService; + soClient: SavedObjectsClientContract; }): Promise> { const idsOfCasesToSync = casesToSync.map((casePatchReq) => casePatchReq.id); // getAllCaseComments will by default get all the comments, unless page or perPage fields are set return caseService.getAllCaseComments({ - client, + soClient, id: idsOfCasesToSync, includeSubCaseComments: true, options: { @@ -214,11 +214,11 @@ async function getAlertComments({ async function getSubCasesToStatus({ totalAlerts, caseService, - client, + soClient, }: { totalAlerts: SavedObjectsFindResponse; - caseService: CaseServiceSetup; - client: SavedObjectsClientContract; + caseService: CaseService; + soClient: SavedObjectsClientContract; }): Promise> { const subCasesToRetrieve = totalAlerts.saved_objects.reduce((acc, alertComment) => { if ( @@ -235,7 +235,7 @@ async function getSubCasesToStatus({ const subCases = await caseService.getSubCases({ ids: Array.from(subCasesToRetrieve.values()), - client, + soClient, }); return subCases.saved_objects.reduce((acc, subCase) => { @@ -281,15 +281,15 @@ async function updateAlerts({ casesWithStatusChangedAndSynced, casesMap, caseService, - client, - casesClient, + soClient, + casesClientInternal, }: { casesWithSyncSettingChangedToOn: ESCasePatchRequest[]; casesWithStatusChangedAndSynced: ESCasePatchRequest[]; casesMap: Map>; - caseService: CaseServiceSetup; - client: SavedObjectsClientContract; - casesClient: CasesClientHandler; + caseService: CaseService; + soClient: SavedObjectsClientContract; + casesClientInternal: CasesClientInternal; }) { /** * It's possible that a case ID can appear multiple times in each array. I'm intentionally placing the status changes @@ -313,11 +313,11 @@ async function updateAlerts({ const totalAlerts = await getAlertComments({ casesToSync, caseService, - client, + soClient, }); // get a map of sub case id to the sub case status - const subCasesToStatus = await getSubCasesToStatus({ totalAlerts, client, caseService }); + const subCasesToStatus = await getSubCasesToStatus({ totalAlerts, soClient, caseService }); // create an array of requests that indicate the id, index, and status to update an alert const alertsToUpdate = totalAlerts.saved_objects.reduce( @@ -337,15 +337,15 @@ async function updateAlerts({ [] ); - await casesClient.updateAlertsStatus({ alerts: alertsToUpdate }); + await casesClientInternal.alerts.updateStatus({ alerts: alertsToUpdate }); } interface UpdateArgs { savedObjectsClient: SavedObjectsClientContract; - caseService: CaseServiceSetup; - userActionService: CaseUserActionServiceSetup; + caseService: CaseService; + userActionService: CaseUserActionService; user: User; - casesClient: CasesClientHandler; + casesClientInternal: CasesClientInternal; cases: CasesPatchRequest; logger: Logger; } @@ -355,7 +355,7 @@ export const update = async ({ caseService, userActionService, user, - casesClient, + casesClientInternal, cases, logger, }: UpdateArgs): Promise => { @@ -366,7 +366,7 @@ export const update = async ({ try { const myCases = await caseService.getCases({ - client: savedObjectsClient, + soClient: savedObjectsClient, caseIds: query.cases.map((q) => q.id), }); @@ -433,14 +433,14 @@ export const update = async ({ await throwIfInvalidUpdateOfTypeWithAlerts({ requests: updateFilterCases, caseService, - client: savedObjectsClient, + soClient: savedObjectsClient, }); // eslint-disable-next-line @typescript-eslint/naming-convention const { username, full_name, email } = user; const updatedDt = new Date().toISOString(); const updatedCases = await caseService.patchCases({ - client: savedObjectsClient, + soClient: savedObjectsClient, cases: updateFilterCases.map((thisCase) => { const { id: caseId, version, ...updateCaseAttributes } = thisCase; let closedInfo = {}; @@ -501,8 +501,8 @@ export const update = async ({ casesWithStatusChangedAndSynced, casesWithSyncSettingChangedToOn, caseService, - client: savedObjectsClient, - casesClient, + soClient: savedObjectsClient, + casesClientInternal, casesMap, }); @@ -523,8 +523,8 @@ export const update = async ({ }); }); - await userActionService.postUserActions({ - client: savedObjectsClient, + await userActionService.bulkCreate({ + soClient: savedObjectsClient, actions: buildCaseUserActions({ originalCases: myCases.saved_objects, updatedCases: updatedCases.saved_objects, diff --git a/x-pack/plugins/cases/server/client/client.ts b/x-pack/plugins/cases/server/client/client.ts index e9bfd1ef754b05..5f6cb8851c34cf 100644 --- a/x-pack/plugins/cases/server/client/client.ts +++ b/x-pack/plugins/cases/server/client/client.ts @@ -5,272 +5,43 @@ * 2.0. */ -import type { PublicMethodsOf } from '@kbn/utility-types'; -import { ElasticsearchClient, SavedObjectsClientContract, Logger } from 'src/core/server'; -import { - CasesClientConstructorArguments, - CasesClient, - ConfigureFields, - MappingsClient, - CasesClientUpdateAlertsStatus, - CasesClientAddComment, - CasesClientGet, - CasesClientGetUserActions, - CasesClientGetAlerts, - CasesClientPush, -} from './types'; -import { create } from './cases/create'; -import { update } from './cases/update'; -import { addComment } from './comments/add'; -import { getFields } from './configure/get_fields'; -import { getMappings } from './configure/get_mappings'; -import { updateAlertsStatus } from './alerts/update_status'; -import { - CaseConfigureServiceSetup, - CaseServiceSetup, - ConnectorMappingsServiceSetup, - CaseUserActionServiceSetup, - AlertServiceContract, -} from '../services'; -import { CasesPatchRequest, CasePostRequest, User, CasesFindRequest } from '../../common/api'; -import { get } from './cases/get'; -import { get as getUserActions } from './user_actions/get'; -import { get as getAlerts } from './alerts/get'; -import { push } from './cases/push'; -import { createCaseError } from '../common/error'; -import { Authorization } from '../authorization/authorization'; -import { find } from './cases/find'; +import { CasesClientArgs } from './types'; +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'; -/** - * This class is a pass through for common case functionality (like creating, get a case). - */ -export class CasesClientHandler implements CasesClient { - private readonly _scopedClusterClient: ElasticsearchClient; - private readonly _caseConfigureService: CaseConfigureServiceSetup; - private readonly _caseService: CaseServiceSetup; - private readonly _connectorMappingsService: ConnectorMappingsServiceSetup; - private readonly user: User; - private readonly _savedObjectsClient: SavedObjectsClientContract; - private readonly _userActionService: CaseUserActionServiceSetup; - private readonly _alertsService: AlertServiceContract; - private readonly logger: Logger; - private readonly authorization: PublicMethodsOf; - - constructor(clientArgs: CasesClientConstructorArguments) { - this._scopedClusterClient = clientArgs.scopedClusterClient; - this._caseConfigureService = clientArgs.caseConfigureService; - this._caseService = clientArgs.caseService; - this._connectorMappingsService = clientArgs.connectorMappingsService; - this.user = clientArgs.user; - this._savedObjectsClient = clientArgs.savedObjectsClient; - this._userActionService = clientArgs.userActionService; - this._alertsService = clientArgs.alertsService; - this.logger = clientArgs.logger; - this.authorization = clientArgs.authorization; - } - - public async create(caseInfo: CasePostRequest) { - try { - // TODO: authorize the user - return create({ - savedObjectsClient: this._savedObjectsClient, - caseService: this._caseService, - caseConfigureService: this._caseConfigureService, - userActionService: this._userActionService, - user: this.user, - theCase: caseInfo, - logger: this.logger, - auth: this.authorization, - }); - } catch (error) { - throw createCaseError({ - message: `Failed to create a new case using client: ${error}`, - error, - logger: this.logger, - }); - } - } - - public async find(options: CasesFindRequest) { - try { - // TODO: authorize the user - return find({ - savedObjectsClient: this._savedObjectsClient, - caseService: this._caseService, - logger: this.logger, - auth: this.authorization, - options, - }); - } catch (error) { - throw createCaseError({ - message: `Failed to find cases using client: ${error}`, - error, - logger: this.logger, - }); - } - } - - public async update(cases: CasesPatchRequest) { - try { - return update({ - savedObjectsClient: this._savedObjectsClient, - caseService: this._caseService, - userActionService: this._userActionService, - user: this.user, - cases, - casesClient: this, - logger: this.logger, - }); - } catch (error) { - const caseIDVersions = cases.cases.map((caseInfo) => ({ - id: caseInfo.id, - version: caseInfo.version, - })); - throw createCaseError({ - message: `Failed to update cases using client: ${JSON.stringify(caseIDVersions)}: ${error}`, - error, - logger: this.logger, - }); - } - } - - public async addComment({ caseId, comment }: CasesClientAddComment) { - try { - return addComment({ - savedObjectsClient: this._savedObjectsClient, - caseService: this._caseService, - userActionService: this._userActionService, - casesClient: this, - caseId, - comment, - user: this.user, - logger: this.logger, - }); - } catch (error) { - throw createCaseError({ - message: `Failed to add comment using client case id: ${caseId}: ${error}`, - error, - logger: this.logger, - }); - } - } - - public async getFields(fields: ConfigureFields) { - try { - return getFields(fields); - } catch (error) { - throw createCaseError({ - message: `Failed to retrieve fields using client: ${error}`, - error, - logger: this.logger, - }); - } - } - - public async getMappings(args: MappingsClient) { - try { - return getMappings({ - ...args, - savedObjectsClient: this._savedObjectsClient, - connectorMappingsService: this._connectorMappingsService, - casesClient: this, - logger: this.logger, - }); - } catch (error) { - throw createCaseError({ - message: `Failed to get mappings using client: ${error}`, - error, - logger: this.logger, - }); - } - } +export class CasesClient { + private readonly _casesClientInternal: CasesClientInternal; + private readonly _cases: CasesSubClient; + private readonly _attachments: AttachmentsSubClient; + private readonly _userActions: UserActionsSubClient; - public async updateAlertsStatus(args: CasesClientUpdateAlertsStatus) { - try { - return updateAlertsStatus({ - ...args, - alertsService: this._alertsService, - scopedClusterClient: this._scopedClusterClient, - logger: this.logger, - }); - } catch (error) { - throw createCaseError({ - message: `Failed to update alerts status using client alerts: ${JSON.stringify( - args.alerts - )}: ${error}`, - error, - logger: this.logger, - }); - } + constructor(args: CasesClientArgs) { + this._casesClientInternal = createCasesClientInternal(args); + this._cases = createCasesSubClient(args, this, this._casesClientInternal); + this._attachments = createAttachmentsSubClient(args, this._casesClientInternal); + this._userActions = createUserActionsSubClient(args); } - public async get(args: CasesClientGet) { - try { - return get({ - ...args, - caseService: this._caseService, - savedObjectsClient: this._savedObjectsClient, - logger: this.logger, - }); - } catch (error) { - this.logger.error(`Failed to get case using client id: ${args.id}: ${error}`); - throw error; - } + public get cases() { + return this._cases; } - public async getUserActions(args: CasesClientGetUserActions) { - try { - return getUserActions({ - ...args, - savedObjectsClient: this._savedObjectsClient, - userActionService: this._userActionService, - }); - } catch (error) { - throw createCaseError({ - message: `Failed to get user actions using client id: ${args.caseId}: ${error}`, - error, - logger: this.logger, - }); - } + public get attachments() { + return this._attachments; } - public async getAlerts(args: CasesClientGetAlerts) { - try { - return getAlerts({ - ...args, - alertsService: this._alertsService, - scopedClusterClient: this._scopedClusterClient, - logger: this.logger, - }); - } catch (error) { - throw createCaseError({ - message: `Failed to get alerts using client requested alerts: ${JSON.stringify( - args.alertsInfo - )}: ${error}`, - error, - logger: this.logger, - }); - } + public get userActions() { + return this._userActions; } - public async push(args: CasesClientPush) { - try { - return push({ - ...args, - savedObjectsClient: this._savedObjectsClient, - caseService: this._caseService, - userActionService: this._userActionService, - user: this.user, - casesClient: this, - caseConfigureService: this._caseConfigureService, - logger: this.logger, - }); - } catch (error) { - throw createCaseError({ - message: `Failed to push case using client id: ${args.caseId}: ${error}`, - error, - logger: this.logger, - }); - } + // TODO: Remove it when all routes will be moved to the cases client. + public get casesClientInternal() { + return this._casesClientInternal; } } + +export const createCasesClient = (args: CasesClientArgs): CasesClient => { + return new CasesClient(args); +}; diff --git a/x-pack/plugins/cases/server/client/client_internal.ts b/x-pack/plugins/cases/server/client/client_internal.ts new file mode 100644 index 00000000000000..79f107e17af35d --- /dev/null +++ b/x-pack/plugins/cases/server/client/client_internal.ts @@ -0,0 +1,32 @@ +/* + * 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 { CasesClientArgs } from './types'; +import { AlertSubClient, createAlertsSubClient } from './alerts/client'; +import { ConfigureSubClient, createConfigurationSubClient } from './configure/client'; + +export class CasesClientInternal { + private readonly _alerts: AlertSubClient; + private readonly _configuration: ConfigureSubClient; + + constructor(args: CasesClientArgs) { + this._alerts = createAlertsSubClient(args); + this._configuration = createConfigurationSubClient(args, this); + } + + public get alerts() { + return this._alerts; + } + + public get configuration() { + return this._configuration; + } +} + +export const createCasesClientInternal = (args: CasesClientArgs): CasesClientInternal => { + return new CasesClientInternal(args); +}; diff --git a/x-pack/plugins/cases/server/client/configure/client.ts b/x-pack/plugins/cases/server/client/configure/client.ts new file mode 100644 index 00000000000000..8ea91415fd1635 --- /dev/null +++ b/x-pack/plugins/cases/server/client/configure/client.ts @@ -0,0 +1,51 @@ +/* + * 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 { ActionsClient } from '../../../../actions/server'; +import { ConnectorMappingsAttributes, GetFieldsResponse } from '../../../common/api'; +import { CasesClientInternal } from '../client_internal'; +import { CasesClientArgs } from '../types'; +import { getFields } from './get_fields'; +import { getMappings } from './get_mappings'; + +export interface ConfigurationGetFields { + actionsClient: ActionsClient; + connectorId: string; + connectorType: string; +} + +export interface ConfigurationGetMappings { + actionsClient: ActionsClient; + connectorId: string; + connectorType: string; +} + +export interface ConfigureSubClient { + getFields(args: ConfigurationGetFields): Promise; + getMappings(args: ConfigurationGetMappings): Promise; +} + +export const createConfigurationSubClient = ( + args: CasesClientArgs, + casesClientInternal: CasesClientInternal +): ConfigureSubClient => { + const { savedObjectsClient, connectorMappingsService, logger } = args; + + const configureSubClient: ConfigureSubClient = { + getFields: (fields: ConfigurationGetFields) => getFields(fields), + getMappings: (params: ConfigurationGetMappings) => + getMappings({ + ...params, + savedObjectsClient, + connectorMappingsService, + casesClientInternal, + logger, + }), + }; + + return Object.freeze(configureSubClient); +}; diff --git a/x-pack/plugins/cases/server/client/configure/get_fields.ts b/x-pack/plugins/cases/server/client/configure/get_fields.ts index deabae33810b2e..799f50845dda6b 100644 --- a/x-pack/plugins/cases/server/client/configure/get_fields.ts +++ b/x-pack/plugins/cases/server/client/configure/get_fields.ts @@ -8,14 +8,14 @@ import Boom from '@hapi/boom'; import { GetFieldsResponse } from '../../../common/api'; -import { ConfigureFields } from '../types'; +import { ConfigurationGetFields } from './client'; import { createDefaultMapping, formatFields } from './utils'; export const getFields = async ({ actionsClient, connectorType, connectorId, -}: ConfigureFields): Promise => { +}: ConfigurationGetFields): Promise => { const results = await actionsClient.execute({ actionId: connectorId, params: { diff --git a/x-pack/plugins/cases/server/client/configure/get_mappings.ts b/x-pack/plugins/cases/server/client/configure/get_mappings.ts index 558c961f89e5bd..c157252909f669 100644 --- a/x-pack/plugins/cases/server/client/configure/get_mappings.ts +++ b/x-pack/plugins/cases/server/client/configure/get_mappings.ts @@ -10,15 +10,15 @@ import { ActionsClient } from '../../../../actions/server'; import { ConnectorMappingsAttributes, ConnectorTypes } from '../../../common/api'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server/saved_objects'; -import { ConnectorMappingsServiceSetup } from '../../services'; -import { CasesClientHandler } from '..'; +import { ConnectorMappingsService } from '../../services'; +import { CasesClientInternal } from '..'; import { createCaseError } from '../../common/error'; interface GetMappingsArgs { savedObjectsClient: SavedObjectsClientContract; - connectorMappingsService: ConnectorMappingsServiceSetup; + connectorMappingsService: ConnectorMappingsService; actionsClient: ActionsClient; - casesClient: CasesClientHandler; + casesClientInternal: CasesClientInternal; connectorType: string; connectorId: string; logger: Logger; @@ -28,7 +28,7 @@ export const getMappings = async ({ savedObjectsClient, connectorMappingsService, actionsClient, - casesClient, + casesClientInternal, connectorType, connectorId, logger, @@ -38,7 +38,7 @@ export const getMappings = async ({ return []; } const myConnectorMappings = await connectorMappingsService.find({ - client: savedObjectsClient, + soClient: savedObjectsClient, options: { hasReference: { type: ACTION_SAVED_OBJECT_TYPE, @@ -49,13 +49,13 @@ export const getMappings = async ({ let theMapping; // Create connector mappings if there are none if (myConnectorMappings.total === 0) { - const res = await casesClient.getFields({ + const res = await casesClientInternal.configuration.getFields({ actionsClient, connectorId, connectorType, }); theMapping = await connectorMappingsService.post({ - client: savedObjectsClient, + soClient: savedObjectsClient, attributes: { mappings: res.defaultMappings, }, diff --git a/x-pack/plugins/cases/server/client/factory.ts b/x-pack/plugins/cases/server/client/factory.ts index 89ee0cdf78c75a..d622861ac65b40 100644 --- a/x-pack/plugins/cases/server/client/factory.ts +++ b/x-pack/plugins/cases/server/client/factory.ts @@ -17,20 +17,22 @@ import { Authorization } from '../authorization/authorization'; import { GetSpaceFn } from '../authorization/types'; import { AlertServiceContract, - CaseConfigureServiceSetup, - CaseServiceSetup, - CaseUserActionServiceSetup, - ConnectorMappingsServiceSetup, + CaseConfigureService, + CaseService, + CaseUserActionService, + ConnectorMappingsService, + AttachmentService, } from '../services'; -import { CasesClientHandler } from './client'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; +import { CasesClient, createCasesClient } from '.'; interface CasesClientFactoryArgs { - caseConfigureService: CaseConfigureServiceSetup; - caseService: CaseServiceSetup; - connectorMappingsService: ConnectorMappingsServiceSetup; - userActionService: CaseUserActionServiceSetup; + caseConfigureService: CaseConfigureService; + caseService: CaseService; + connectorMappingsService: ConnectorMappingsService; + userActionService: CaseUserActionService; alertsService: AlertServiceContract; + attachmentService: AttachmentService; securityPluginSetup?: SecurityPluginSetup; securityPluginStart?: SecurityPluginStart; getSpace: GetSpaceFn; @@ -39,7 +41,7 @@ interface CasesClientFactoryArgs { } /** - * This class handles the logic for creating a CasesClientHandler. We need this because some of the member variables + * This class handles the logic for creating a CasesClient. We need this because some of the member variables * can't be initialized until a plugin's start() method but we need to register the case context in the setup() method. */ export class CasesClientFactory { @@ -71,7 +73,7 @@ export class CasesClientFactory { request?: KibanaRequest; savedObjectsService?: SavedObjectsServiceStart; scopedClusterClient: ElasticsearchClient; - }): Promise { + }): Promise { if (!this.isInitialized || !this.options) { throw new Error('CasesClientFactory must be initialized before calling create'); } @@ -93,7 +95,7 @@ export class CasesClientFactory { const user = this.options.caseService.getUser({ request }); - return new CasesClientHandler({ + return createCasesClient({ alertsService: this.options.alertsService, scopedClusterClient, savedObjectsClient: savedObjectsService.getScopedClient(request, { @@ -104,6 +106,7 @@ export class CasesClientFactory { caseConfigureService: this.options.caseConfigureService, connectorMappingsService: this.options.connectorMappingsService, userActionService: this.options.userActionService, + attachmentService: this.options.attachmentService, logger: this.logger, authorization: auth, }); diff --git a/x-pack/plugins/cases/server/client/index.ts b/x-pack/plugins/cases/server/client/index.ts index 39c7f6f98c2595..7904e65ca62766 100644 --- a/x-pack/plugins/cases/server/client/index.ts +++ b/x-pack/plugins/cases/server/client/index.ts @@ -5,18 +5,8 @@ * 2.0. */ -import { CasesClientConstructorArguments, CasesClient } from './types'; -import { CasesClientHandler } from './client'; - -export { CasesClientHandler } from './client'; -export { CasesClient } from './types'; - -/** - * Create a CasesClientHandler to external services (other plugins). - */ -export const createExternalCasesClient = ( - clientArgs: CasesClientConstructorArguments -): CasesClient => { - const client = new CasesClientHandler(clientArgs); - return client; -}; +export { CasesClient } from './client'; +export { CasesClientInternal } from './client_internal'; +export { CasesClientArgs } from './types'; +export { createCasesClient } from './client'; +export { createCasesClientInternal } from './client_internal'; diff --git a/x-pack/plugins/cases/server/client/mocks.ts b/x-pack/plugins/cases/server/client/mocks.ts index 84aa566086663f..174904c1f66be6 100644 --- a/x-pack/plugins/cases/server/client/mocks.ts +++ b/x-pack/plugins/cases/server/client/mocks.ts @@ -16,7 +16,7 @@ import { AlertServiceContract, CaseConfigureService, CaseService, - CaseUserActionServiceSetup, + CaseUserActionService, ConnectorMappingsService, } from '../services'; import { CasesClient } from './types'; @@ -51,7 +51,7 @@ export const createCasesClientWithMockSavedObjectsClient = async ({ }): Promise<{ client: CasesClient; services: { - userActionService: jest.Mocked; + userActionService: jest.Mocked; alertsService: jest.Mocked; }; esClient: DeeplyMockedKeys; diff --git a/x-pack/plugins/cases/server/client/types.ts b/x-pack/plugins/cases/server/client/types.ts index b0276fc10aadae..0592dd321819de 100644 --- a/x-pack/plugins/cases/server/client/types.ts +++ b/x-pack/plugins/cases/server/client/types.ts @@ -7,115 +7,27 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import { ElasticsearchClient, SavedObjectsClientContract, Logger } from 'kibana/server'; -import { ActionsClient } from '../../../actions/server'; -import { - CasePostRequest, - CaseResponse, - CasesPatchRequest, - CasesResponse, - CaseStatuses, - CommentRequest, - ConnectorMappingsAttributes, - GetFieldsResponse, - CaseUserActionsResponse, - User, - CasesFindRequest, - CasesFindResponse, -} from '../../common/api'; +import { User } from '../../common/api'; import { Authorization } from '../authorization/authorization'; -import { AlertInfo } from '../common'; import { - CaseConfigureServiceSetup, - CaseServiceSetup, - CaseUserActionServiceSetup, AlertServiceContract, + CaseConfigureService, + CaseService, + CaseUserActionService, + ConnectorMappingsService, + AttachmentService, } from '../services'; -import { ConnectorMappingsServiceSetup } from '../services/connector_mappings'; -import { CasesClientGetAlertsResponse } from './alerts/types'; - -export interface CasesClientGet { - id: string; - includeComments?: boolean; - includeSubCaseComments?: boolean; -} - -export interface CasesClientPush { - actionsClient: ActionsClient; - caseId: string; - connectorId: string; -} - -export interface CasesClientAddComment { - caseId: string; - comment: CommentRequest; -} - -export interface CasesClientUpdateAlertsStatus { - alerts: UpdateAlertRequest[]; -} - -export interface CasesClientGetAlerts { - alertsInfo: AlertInfo[]; -} - -export interface CasesClientGetUserActions { - caseId: string; - subCaseId?: string; -} - -export interface MappingsClient { - actionsClient: ActionsClient; - connectorId: string; - connectorType: string; -} - -export interface CasesClientConstructorArguments { - scopedClusterClient: ElasticsearchClient; - caseConfigureService: CaseConfigureServiceSetup; - caseService: CaseServiceSetup; - connectorMappingsService: ConnectorMappingsServiceSetup; - user: User; - savedObjectsClient: SavedObjectsClientContract; - userActionService: CaseUserActionServiceSetup; - alertsService: AlertServiceContract; - logger: Logger; - authorization: PublicMethodsOf; -} - -export interface ConfigureFields { - actionsClient: ActionsClient; - connectorId: string; - connectorType: string; -} - -/** - * Defines the fields necessary to update an alert's status. - */ -export interface UpdateAlertRequest { - id: string; - index: string; - status: CaseStatuses; -} - -/** - * This represents the interface that other plugins can access. - */ -export interface CasesClient { - addComment(args: CasesClientAddComment): Promise; - create(theCase: CasePostRequest): Promise; - get(args: CasesClientGet): Promise; - getAlerts(args: CasesClientGetAlerts): Promise; - getFields(args: ConfigureFields): Promise; - getMappings(args: MappingsClient): Promise; - getUserActions(args: CasesClientGetUserActions): Promise; - find(args: CasesFindRequest): Promise; - push(args: CasesClientPush): Promise; - update(args: CasesPatchRequest): Promise; - updateAlertsStatus(args: CasesClientUpdateAlertsStatus): Promise; -} -export interface MappingsClient { - actionsClient: ActionsClient; - connectorId: string; - connectorType: string; +export interface CasesClientArgs { + readonly scopedClusterClient: ElasticsearchClient; + readonly caseConfigureService: CaseConfigureService; + readonly caseService: CaseService; + readonly connectorMappingsService: ConnectorMappingsService; + readonly user: User; + readonly savedObjectsClient: SavedObjectsClientContract; + readonly userActionService: CaseUserActionService; + readonly alertsService: AlertServiceContract; + readonly attachmentService: AttachmentService; + readonly logger: Logger; + readonly authorization: PublicMethodsOf; } diff --git a/x-pack/plugins/cases/server/client/user_actions/client.ts b/x-pack/plugins/cases/server/client/user_actions/client.ts new file mode 100644 index 00000000000000..50d9270440e43b --- /dev/null +++ b/x-pack/plugins/cases/server/client/user_actions/client.ts @@ -0,0 +1,34 @@ +/* + * 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 { CaseUserActionsResponse } from '../../../common/api'; +import { CasesClientArgs } from '../types'; +import { get } from './get'; + +export interface UserActionGet { + caseId: string; + subCaseId?: string; +} + +export interface UserActionsSubClient { + getAll(args: UserActionGet): Promise; +} + +export const createUserActionsSubClient = (args: CasesClientArgs): UserActionsSubClient => { + const { savedObjectsClient, userActionService } = args; + + const attachmentSubClient: UserActionsSubClient = { + getAll: (params: UserActionGet) => + get({ + ...params, + savedObjectsClient, + userActionService, + }), + }; + + return Object.freeze(attachmentSubClient); +}; diff --git a/x-pack/plugins/cases/server/client/user_actions/get.ts b/x-pack/plugins/cases/server/client/user_actions/get.ts index 92c67b0d1591d5..cebd3da1b6f7e0 100644 --- a/x-pack/plugins/cases/server/client/user_actions/get.ts +++ b/x-pack/plugins/cases/server/client/user_actions/get.ts @@ -12,11 +12,11 @@ import { CASE_COMMENT_SAVED_OBJECT, } from '../../../common/constants'; import { CaseUserActionsResponseRt, CaseUserActionsResponse } from '../../../common/api'; -import { CaseUserActionServiceSetup } from '../../services'; +import { CaseUserActionService } from '../../services'; interface GetParams { savedObjectsClient: SavedObjectsClientContract; - userActionService: CaseUserActionServiceSetup; + userActionService: CaseUserActionService; caseId: string; subCaseId?: string; } @@ -27,8 +27,8 @@ export const get = async ({ caseId, subCaseId, }: GetParams): Promise => { - const userActions = await userActionService.getUserActions({ - client: savedObjectsClient, + const userActions = await userActionService.getAll({ + soClient: savedObjectsClient, caseId, subCaseId, }); 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 527d851631583d..fb34c5fecea394 100644 --- a/x-pack/plugins/cases/server/common/models/commentable_case.ts +++ b/x-pack/plugins/cases/server/common/models/commentable_case.ts @@ -35,7 +35,7 @@ import { transformNewComment, } from '../../routes/api/utils'; import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../common/constants'; -import { CaseServiceSetup } from '../../services'; +import { AttachmentService, CaseService } from '../../services'; import { createCaseError } from '../error'; import { countAlertsForID } from '../index'; @@ -53,7 +53,8 @@ interface CommentableCaseParams { collection: SavedObject; subCase?: SavedObject; soClient: SavedObjectsClientContract; - service: CaseServiceSetup; + caseService: CaseService; + attachmentService: AttachmentService; logger: Logger; } @@ -65,14 +66,23 @@ export class CommentableCase { private readonly collection: SavedObject; private readonly subCase?: SavedObject; private readonly soClient: SavedObjectsClientContract; - private readonly service: CaseServiceSetup; + private readonly caseService: CaseService; + private readonly attachmentService: AttachmentService; private readonly logger: Logger; - constructor({ collection, subCase, soClient, service, logger }: CommentableCaseParams) { + constructor({ + collection, + subCase, + soClient, + caseService, + attachmentService, + logger, + }: CommentableCaseParams) { this.collection = collection; this.subCase = subCase; this.soClient = soClient; - this.service = service; + this.caseService = caseService; + this.attachmentService = attachmentService; this.logger = logger; } @@ -129,8 +139,8 @@ export class CommentableCase { let updatedSubCaseAttributes: SavedObject | undefined; if (this.subCase) { - const updatedSubCase = await this.service.patchSubCase({ - client: this.soClient, + const updatedSubCase = await this.caseService.patchSubCase({ + soClient: this.soClient, subCaseId: this.subCase.id, updatedAttributes: { updated_at: date, @@ -151,8 +161,8 @@ export class CommentableCase { }; } - const updatedCase = await this.service.patchCase({ - client: this.soClient, + const updatedCase = await this.caseService.patchCase({ + soClient: this.soClient, caseId: this.collection.id, updatedAttributes: { updated_at: date, @@ -173,7 +183,8 @@ export class CommentableCase { }, subCase: updatedSubCaseAttributes, soClient: this.soClient, - service: this.service, + caseService: this.caseService, + attachmentService: this.attachmentService, logger: this.logger, }); } catch (error) { @@ -201,9 +212,9 @@ export class CommentableCase { const { id, version, ...queryRestAttributes } = updateRequest; const [comment, commentableCase] = await Promise.all([ - this.service.patchComment({ - client: this.soClient, - commentId: id, + this.attachmentService.update({ + soClient: this.soClient, + attachmentId: id, updatedAttributes: { ...queryRestAttributes, updated_at: updatedAt, @@ -250,8 +261,8 @@ export class CommentableCase { } const [comment, commentableCase] = await Promise.all([ - this.service.postNewComment({ - client: this.soClient, + this.attachmentService.create({ + soClient: this.soClient, attributes: transformNewComment({ associationType: this.subCase ? AssociationType.subCase : AssociationType.case, createdDate, @@ -287,8 +298,8 @@ export class CommentableCase { public async encode(): Promise { try { - const collectionCommentStats = await this.service.getAllCaseComments({ - client: this.soClient, + const collectionCommentStats = await this.caseService.getAllCaseComments({ + soClient: this.soClient, id: this.collection.id, options: { fields: [], @@ -297,8 +308,8 @@ export class CommentableCase { }, }); - const collectionComments = await this.service.getAllCaseComments({ - client: this.soClient, + const collectionComments = await this.caseService.getAllCaseComments({ + soClient: this.soClient, id: this.collection.id, options: { fields: [], @@ -317,8 +328,8 @@ export class CommentableCase { }; if (this.subCase) { - const subCaseComments = await this.service.getAllSubCaseComments({ - client: this.soClient, + const subCaseComments = await this.caseService.getAllSubCaseComments({ + soClient: this.soClient, id: this.subCase.id, }); const totalAlerts = diff --git a/x-pack/plugins/cases/server/common/utils.ts b/x-pack/plugins/cases/server/common/utils.ts index 88cce82389c4df..36f5dc9cbb00a8 100644 --- a/x-pack/plugins/cases/server/common/utils.ts +++ b/x-pack/plugins/cases/server/common/utils.ts @@ -13,7 +13,7 @@ import { CommentType, User, } from '../../common/api'; -import { UpdateAlertRequest } from '../client/types'; +import { UpdateAlertRequest } from '../client/alerts/client'; import { getAlertInfoFromComments } from '../routes/api/utils'; /** diff --git a/x-pack/plugins/cases/server/connectors/case/index.ts b/x-pack/plugins/cases/server/connectors/case/index.ts index 5c069135b92f64..6f8132d77a05fb 100644 --- a/x-pack/plugins/cases/server/connectors/case/index.ts +++ b/x-pack/plugins/cases/server/connectors/case/index.ts @@ -76,7 +76,7 @@ async function executor( if (subAction === 'create') { try { - data = await casesClient.create({ + data = await casesClient.cases.create({ ...(subActionParams as CasePostRequest), }); } catch (error) { @@ -98,7 +98,7 @@ async function executor( ); try { - data = await casesClient.update({ cases: [updateParamsWithoutNullValues] }); + data = await casesClient.cases.update({ cases: [updateParamsWithoutNullValues] }); } catch (error) { throw createCaseError({ message: `Failed to update case using connector id: ${updateParamsWithoutNullValues?.id} version: ${updateParamsWithoutNullValues?.version}: ${error}`, @@ -112,7 +112,7 @@ async function executor( const { caseId, comment } = subActionParams as ExecutorSubActionAddCommentParams; try { const formattedComment = transformConnectorComment(comment, logger); - data = await casesClient.addComment({ caseId, comment: formattedComment }); + data = await casesClient.attachments.add({ caseId, comment: formattedComment }); } catch (error) { throw createCaseError({ message: `Failed to create comment using connector case id: ${caseId}: ${error}`, diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index d641e581c22049..2ccc362280b9f7 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -24,13 +24,9 @@ import { } from './saved_object_types'; import { CaseConfigureService, - CaseConfigureServiceSetup, CaseService, - CaseServiceSetup, CaseUserActionService, - CaseUserActionServiceSetup, ConnectorMappingsService, - ConnectorMappingsServiceSetup, AlertService, } from './services'; import { CasesClient } from './client'; @@ -39,6 +35,7 @@ import type { CasesRequestHandlerContext } from './types'; import { CasesClientFactory } from './client/factory'; import { SpacesPluginStart } from '../../spaces/server'; import { PluginStartContract as FeaturesPluginStart } from '../../features/server'; +import { AttachmentService } from './services/attachments'; function createConfig(context: PluginInitializerContext) { return context.config.get(); @@ -57,17 +54,18 @@ export interface PluginsStart { export class CasePlugin { private readonly log: Logger; - private caseConfigureService?: CaseConfigureServiceSetup; - private caseService?: CaseServiceSetup; - private connectorMappingsService?: ConnectorMappingsServiceSetup; - private userActionService?: CaseUserActionServiceSetup; + private caseConfigureService?: CaseConfigureService; + private caseService?: CaseService; + private connectorMappingsService?: ConnectorMappingsService; + private userActionService?: CaseUserActionService; private alertsService?: AlertService; + private attachmentService?: AttachmentService; private clientFactory: CasesClientFactory; private securityPluginSetup?: SecurityPluginSetup; private config?: ConfigType; constructor(private readonly initializerContext: PluginInitializerContext) { - this.log = this.initializerContext.logger.get(); + this.log = this.initializerContext.logger.get('plugins', 'cases'); this.clientFactory = new CasesClientFactory(this.log); } @@ -98,10 +96,11 @@ export class CasePlugin { this.log, plugins.security != null ? plugins.security.authc : undefined ); - this.caseConfigureService = await new CaseConfigureService(this.log).setup(); - this.connectorMappingsService = await new ConnectorMappingsService(this.log).setup(); - this.userActionService = await new CaseUserActionService(this.log).setup(); + this.caseConfigureService = new CaseConfigureService(this.log); + this.connectorMappingsService = new ConnectorMappingsService(this.log); + this.userActionService = new CaseUserActionService(this.log); this.alertsService = new AlertService(); + this.attachmentService = new AttachmentService(this.log); core.http.registerRouteHandlerContext( APP_ID, @@ -117,6 +116,7 @@ export class CasePlugin { caseConfigureService: this.caseConfigureService, connectorMappingsService: this.connectorMappingsService, userActionService: this.userActionService, + attachmentService: this.attachmentService, router, }); @@ -139,6 +139,7 @@ export class CasePlugin { caseService: this.caseService!, connectorMappingsService: this.connectorMappingsService!, userActionService: this.userActionService!, + attachmentService: this.attachmentService!, securityPluginSetup: this.securityPluginSetup, securityPluginStart: plugins.security, getSpace: async (request: KibanaRequest) => { diff --git a/x-pack/plugins/cases/server/routes/api/__fixtures__/route_contexts.ts b/x-pack/plugins/cases/server/routes/api/__fixtures__/route_contexts.ts index a1f1a7fe47eed7..3306712c1e550f 100644 --- a/x-pack/plugins/cases/server/routes/api/__fixtures__/route_contexts.ts +++ b/x-pack/plugins/cases/server/routes/api/__fixtures__/route_contexts.ts @@ -51,7 +51,7 @@ export const createRouteContext = async (client: any, badAuth = false) => { savedObjectsService.getScopedClient.mockReturnValue(client); const contextMock = xpackMocks.createRequestHandlerContext(); - // The tests check the calls on the saved object client, so we need to make sure it is the same one returned by + // The tests check the calls on the saved object soClient, so we need to make sure it is the same one returned by // getScopedClient and .client contextMock.core.savedObjects.getClient = jest.fn(() => client); contextMock.core.savedObjects.client = client; diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/delete_all_comments.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/delete_all_comments.ts index 62a372f6d69e15..4439b215599a9c 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/delete_all_comments.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/delete_all_comments.ts @@ -18,6 +18,7 @@ import { import { AssociationType } from '../../../../../common/api'; export function initDeleteAllCommentsApi({ + attachmentService, caseService, router, userActionService, @@ -45,7 +46,7 @@ export function initDeleteAllCommentsApi({ ); } - const client = context.core.savedObjects.getClient({ + const soClient = context.core.savedObjects.getClient({ includedHiddenTypes: SAVED_OBJECT_TYPES, }); // eslint-disable-next-line @typescript-eslint/naming-convention @@ -55,22 +56,22 @@ export function initDeleteAllCommentsApi({ const subCaseId = request.query?.subCaseId; const id = subCaseId ?? request.params.case_id; const comments = await caseService.getCommentsByAssociation({ - client, + soClient, id, associationType: subCaseId ? AssociationType.subCase : AssociationType.case, }); await Promise.all( comments.saved_objects.map((comment) => - caseService.deleteComment({ - client, - commentId: comment.id, + attachmentService.delete({ + soClient, + attachmentId: comment.id, }) ) ); - await userActionService.postUserActions({ - client, + await userActionService.bulkCreate({ + soClient, actions: comments.saved_objects.map((comment) => buildCommentUserActionItem({ action: 'delete', diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.ts index 189dbc684cb821..4818ec607cc26d 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.ts @@ -20,6 +20,7 @@ import { } from '../../../../../common/constants'; export function initDeleteCommentApi({ + attachmentService, caseService, router, userActionService, @@ -48,16 +49,16 @@ export function initDeleteCommentApi({ ); } - const client = context.core.savedObjects.getClient({ + const soClient = context.core.savedObjects.getClient({ includedHiddenTypes: SAVED_OBJECT_TYPES, }); // eslint-disable-next-line @typescript-eslint/naming-convention const { username, full_name, email } = caseService.getUser({ request }); const deleteDate = new Date().toISOString(); - const myComment = await caseService.getComment({ - client, - commentId: request.params.comment_id, + const myComment = await attachmentService.get({ + soClient, + attachmentId: request.params.comment_id, }); if (myComment == null) { @@ -74,13 +75,13 @@ export function initDeleteCommentApi({ ); } - await caseService.deleteComment({ - client, - commentId: request.params.comment_id, + await attachmentService.delete({ + soClient, + attachmentId: request.params.comment_id, }); - await userActionService.postUserActions({ - client, + await userActionService.bulkCreate({ + soClient, actions: [ buildCommentUserActionItem({ action: 'delete', diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/find_comments.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/find_comments.ts index 9e23a28c0725b2..988d0324ec02a5 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/find_comments.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/find_comments.ts @@ -48,7 +48,7 @@ export function initFindCaseCommentsApi({ caseService, router, logger }: RouteDe }, async (context, request, response) => { try { - const client = context.core.savedObjects.getClient({ + const soClient = context.core.savedObjects.getClient({ includedHiddenTypes: SAVED_OBJECT_TYPES, }); const query = pipe( @@ -68,7 +68,7 @@ export function initFindCaseCommentsApi({ caseService, router, logger }: RouteDe const args = query ? { caseService, - client, + soClient, id, options: { // We need this because the default behavior of getAllCaseComments is to return all the comments @@ -84,7 +84,7 @@ export function initFindCaseCommentsApi({ caseService, router, logger }: RouteDe } : { caseService, - client, + soClient, id, options: { page: defaultPage, diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts index 8f48dbbf0348ca..af87cbccb3bf34 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts @@ -37,7 +37,7 @@ export function initGetAllCommentsApi({ caseService, router, logger }: RouteDeps }, async (context, request, response) => { try { - const client = context.core.savedObjects.getClient({ + const soClient = context.core.savedObjects.getClient({ includedHiddenTypes: SAVED_OBJECT_TYPES, }); let comments: SavedObjectsFindResponse; @@ -54,7 +54,7 @@ export function initGetAllCommentsApi({ caseService, router, logger }: RouteDeps if (request.query?.subCaseId) { comments = await caseService.getAllSubCaseComments({ - client, + soClient, id: request.query.subCaseId, options: { sortField: defaultSortField, @@ -62,7 +62,7 @@ export function initGetAllCommentsApi({ caseService, router, logger }: RouteDeps }); } else { comments = await caseService.getAllCaseComments({ - client, + soClient, id: request.params.case_id, includeSubCaseComments: request.query?.includeSubCaseComments, options: { diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/get_comment.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/get_comment.ts index f188a67417f6d1..a03ed4a66e805c 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/get_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/get_comment.ts @@ -12,7 +12,7 @@ import { RouteDeps } from '../../types'; import { flattenCommentSavedObject, wrapError } from '../../utils'; import { CASE_COMMENT_DETAILS_URL, SAVED_OBJECT_TYPES } from '../../../../../common/constants'; -export function initGetCommentApi({ caseService, router, logger }: RouteDeps) { +export function initGetCommentApi({ attachmentService, router, logger }: RouteDeps) { router.get( { path: CASE_COMMENT_DETAILS_URL, @@ -25,13 +25,13 @@ export function initGetCommentApi({ caseService, router, logger }: RouteDeps) { }, async (context, request, response) => { try { - const client = context.core.savedObjects.getClient({ + const soClient = context.core.savedObjects.getClient({ includedHiddenTypes: SAVED_OBJECT_TYPES, }); - const comment = await caseService.getComment({ - client, - commentId: request.params.comment_id, + const comment = await attachmentService.get({ + soClient, + attachmentId: request.params.comment_id, }); return response.ok({ body: CommentResponseRt.encode(flattenCommentSavedObject(comment)), diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/patch_comment.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/patch_comment.ts index 06c28513c2d6c1..b9755cae411332 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/patch_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/patch_comment.ts @@ -25,51 +25,66 @@ import { ENABLE_CASE_CONNECTOR, } from '../../../../../common/constants'; import { CommentPatchRequestRt, throwErrors, User } from '../../../../../common/api'; -import { CaseServiceSetup } from '../../../../services'; +import { CaseService, AttachmentService } from '../../../../services'; interface CombinedCaseParams { - service: CaseServiceSetup; - client: SavedObjectsClientContract; + attachmentService: AttachmentService; + caseService: CaseService; + soClient: SavedObjectsClientContract; caseID: string; logger: Logger; subCaseId?: string; } async function getCommentableCase({ - service, - client, + attachmentService, + caseService, + soClient, caseID, subCaseId, logger, }: CombinedCaseParams) { if (subCaseId) { const [caseInfo, subCase] = await Promise.all([ - service.getCase({ - client, + caseService.getCase({ + soClient, id: caseID, }), - service.getSubCase({ - client, + caseService.getSubCase({ + soClient, id: subCaseId, }), ]); return new CommentableCase({ + attachmentService, + caseService, collection: caseInfo, - service, subCase, - soClient: client, + soClient, logger, }); } else { - const caseInfo = await service.getCase({ - client, + const caseInfo = await caseService.getCase({ + soClient, id: caseID, }); - return new CommentableCase({ collection: caseInfo, service, soClient: client, logger }); + return new CommentableCase({ + attachmentService, + caseService, + collection: caseInfo, + soClient, + logger, + }); } } -export function initPatchCommentApi({ caseService, router, userActionService, logger }: RouteDeps) { +export function initPatchCommentApi({ + attachmentService, + caseService, + router, + userActionService, + logger, +}: RouteDeps) { router.patch( { path: CASE_COMMENTS_URL, @@ -93,7 +108,7 @@ export function initPatchCommentApi({ caseService, router, userActionService, lo ); } - const client = context.core.savedObjects.getClient({ + const soClient = context.core.savedObjects.getClient({ includedHiddenTypes: SAVED_OBJECT_TYPES, }); const query = pipe( @@ -105,16 +120,17 @@ export function initPatchCommentApi({ caseService, router, userActionService, lo decodeCommentRequest(queryRestAttributes); const commentableCase = await getCommentableCase({ - service: caseService, - client, + attachmentService, + caseService, + soClient, caseID: request.params.case_id, subCaseId: request.query?.subCaseId, logger, }); - const myComment = await caseService.getComment({ - client, - commentId: queryCommentId, + const myComment = await attachmentService.get({ + soClient, + attachmentId: queryCommentId, }); if (myComment == null) { @@ -158,8 +174,8 @@ export function initPatchCommentApi({ caseService, router, userActionService, lo user: userInfo, }); - await userActionService.postUserActions({ - client, + await userActionService.bulkCreate({ + soClient, actions: [ buildCommentUserActionItem({ action: 'update', diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/post_comment.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/post_comment.ts index e3b42943ebc2aa..7dbfb2a62c46fa 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/post_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/post_comment.ts @@ -45,7 +45,7 @@ export function initPostCommentApi({ router, logger }: RouteDeps) { const comment = request.body as CommentRequest; return response.ok({ - body: await casesClient.addComment({ caseId, comment }), + body: await casesClient.attachments.add({ caseId, comment }), }); } catch (error) { logger.error( diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.ts b/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.ts index 663595b60b8bac..fa97796228bd1b 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.ts @@ -21,11 +21,11 @@ export function initGetCaseConfigure({ caseConfigureService, router, logger }: R async (context, request, response) => { try { let error = null; - const client = context.core.savedObjects.getClient({ + const soClient = context.core.savedObjects.getClient({ includedHiddenTypes: SAVED_OBJECT_TYPES, }); - const myCaseConfigure = await caseConfigureService.find({ client }); + const myCaseConfigure = await caseConfigureService.find({ soClient }); const { connector, ...caseConfigureWithoutConnector } = myCaseConfigure.saved_objects[0] ?.attributes ?? { connector: null }; @@ -40,7 +40,7 @@ export function initGetCaseConfigure({ caseConfigureService, router, logger }: R throw Boom.notFound('Action client not found'); } try { - mappings = await casesClient.getMappings({ + mappings = await casesClient.casesClientInternal.configuration.getMappings({ actionsClient, connectorId: connector.id, connectorType: connector.type, diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.ts b/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.ts index ed3c2e98d25798..61f3e4719520a4 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.ts @@ -40,7 +40,7 @@ export function initPatchCaseConfigure({ async (context, request, response) => { try { let error = null; - const client = context.core.savedObjects.getClient({ + const soClient = context.core.savedObjects.getClient({ includedHiddenTypes: SAVED_OBJECT_TYPES, }); const query = pipe( @@ -48,7 +48,7 @@ export function initPatchCaseConfigure({ fold(throwErrors(Boom.badRequest), identity) ); - const myCaseConfigure = await caseConfigureService.find({ client }); + const myCaseConfigure = await caseConfigureService.find({ soClient }); const { version, connector, ...queryWithoutVersion } = query; if (myCaseConfigure.saved_objects.length === 0) { throw Boom.conflict( @@ -78,7 +78,7 @@ export function initPatchCaseConfigure({ throw Boom.notFound('Action client have not been found'); } try { - mappings = await casesClient.getMappings({ + mappings = await casesClient.casesClientInternal.configuration.getMappings({ actionsClient, connectorId: connector.id, connectorType: connector.type, @@ -90,7 +90,7 @@ export function initPatchCaseConfigure({ } } const patch = await caseConfigureService.patch({ - client, + soClient, caseConfigureId: myCaseConfigure.saved_objects[0].id, updatedAttributes: { ...queryWithoutVersion, diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.ts b/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.ts index d8e6b2a8ecf75d..62fa7cad324fce 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.ts @@ -43,24 +43,28 @@ export function initPostCaseConfigure({ if (!context.cases) { throw Boom.badRequest('RouteHandlerContext is not registered for cases'); } + const casesClient = await context.cases.getCasesClient(); const actionsClient = context.actions?.getActionsClient(); + if (actionsClient == null) { throw Boom.notFound('Action client not found'); } - const client = context.core.savedObjects.getClient({ + + const soClient = context.core.savedObjects.getClient({ includedHiddenTypes: SAVED_OBJECT_TYPES, }); + const query = pipe( CasesConfigureRequestRt.decode(request.body), fold(throwErrors(Boom.badRequest), identity) ); - const myCaseConfigure = await caseConfigureService.find({ client }); + const myCaseConfigure = await caseConfigureService.find({ soClient }); if (myCaseConfigure.saved_objects.length > 0) { await Promise.all( myCaseConfigure.saved_objects.map((cc) => - caseConfigureService.delete({ client, caseConfigureId: cc.id }) + caseConfigureService.delete({ soClient, caseConfigureId: cc.id }) ) ); } @@ -70,7 +74,7 @@ export function initPostCaseConfigure({ const creationDate = new Date().toISOString(); let mappings: ConnectorMappingsAttributes[] = []; try { - mappings = await casesClient.getMappings({ + mappings = await casesClient.casesClientInternal.configuration.getMappings({ actionsClient, connectorId: query.connector.id, connectorType: query.connector.type, @@ -81,7 +85,7 @@ export function initPostCaseConfigure({ : `Error connecting to ${query.connector.name} instance`; } const post = await caseConfigureService.post({ - client, + soClient, attributes: { ...query, connector: transformCaseConnectorToEsConnector(query.connector), diff --git a/x-pack/plugins/cases/server/routes/api/cases/delete_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/delete_cases.ts index 645e6333300265..a9be4a314adeb5 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/delete_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/delete_cases.ts @@ -12,22 +12,24 @@ import { buildCaseUserActionItem } from '../../../services/user_actions/helpers' import { RouteDeps } from '../types'; import { wrapError } from '../utils'; import { CASES_URL, SAVED_OBJECT_TYPES, ENABLE_CASE_CONNECTOR } from '../../../../common/constants'; -import { CaseServiceSetup } from '../../../services'; +import { CaseService, AttachmentService } from '../../../services'; async function deleteSubCases({ + attachmentService, caseService, - client, + soClient, caseIds, }: { - caseService: CaseServiceSetup; - client: SavedObjectsClientContract; + attachmentService: AttachmentService; + caseService: CaseService; + soClient: SavedObjectsClientContract; caseIds: string[]; }) { - const subCasesForCaseIds = await caseService.findSubCasesByCaseId({ client, ids: caseIds }); + const subCasesForCaseIds = await caseService.findSubCasesByCaseId({ soClient, ids: caseIds }); const subCaseIDs = subCasesForCaseIds.saved_objects.map((subCase) => subCase.id); const commentsForSubCases = await caseService.getAllSubCaseComments({ - client, + soClient, id: subCaseIDs, }); @@ -35,18 +37,24 @@ async function deleteSubCases({ // per case ID await Promise.all( commentsForSubCases.saved_objects.map((commentSO) => - caseService.deleteComment({ client, commentId: commentSO.id }) + attachmentService.delete({ soClient, attachmentId: commentSO.id }) ) ); await Promise.all( subCasesForCaseIds.saved_objects.map((subCaseSO) => - caseService.deleteSubCase(client, subCaseSO.id) + caseService.deleteSubCase(soClient, subCaseSO.id) ) ); } -export function initDeleteCasesApi({ caseService, router, userActionService, logger }: RouteDeps) { +export function initDeleteCasesApi({ + attachmentService, + caseService, + router, + userActionService, + logger, +}: RouteDeps) { router.delete( { path: CASES_URL, @@ -58,13 +66,13 @@ export function initDeleteCasesApi({ caseService, router, userActionService, log }, async (context, request, response) => { try { - const client = context.core.savedObjects.getClient({ + const soClient = context.core.savedObjects.getClient({ includedHiddenTypes: SAVED_OBJECT_TYPES, }); await Promise.all( request.query.ids.map((id) => caseService.deleteCase({ - client, + soClient, id, }) ) @@ -72,7 +80,7 @@ export function initDeleteCasesApi({ caseService, router, userActionService, log const comments = await Promise.all( request.query.ids.map((id) => caseService.getAllCaseComments({ - client, + soClient, id, }) ) @@ -83,9 +91,9 @@ export function initDeleteCasesApi({ caseService, router, userActionService, log comments.map((c) => Promise.all( c.saved_objects.map(({ id }) => - caseService.deleteComment({ - client, - commentId: id, + attachmentService.delete({ + soClient, + attachmentId: id, }) ) ) @@ -94,15 +102,20 @@ export function initDeleteCasesApi({ caseService, router, userActionService, log } if (ENABLE_CASE_CONNECTOR) { - await deleteSubCases({ caseService, client, caseIds: request.query.ids }); + await deleteSubCases({ + attachmentService, + caseService, + soClient, + caseIds: request.query.ids, + }); } // eslint-disable-next-line @typescript-eslint/naming-convention const { username, full_name, email } = await caseService.getUser({ request }); const deleteDate = new Date().toISOString(); - await userActionService.postUserActions({ - client, + await userActionService.bulkCreate({ + soClient, actions: request.query.ids.map((id) => buildCaseUserActionItem({ action: 'create', diff --git a/x-pack/plugins/cases/server/routes/api/cases/find_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/find_cases.ts index 7bee574894d39a..c6ec5245ebd8ae 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/find_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/find_cases.ts @@ -27,7 +27,7 @@ export function initFindCasesApi({ caseService, router, logger }: RouteDeps) { const options = request.query as CasesFindRequest; return response.ok({ - body: await casesClient.find({ ...options }), + body: await casesClient.cases.find({ ...options }), }); } catch (error) { logger.error(`Failed to find cases in route: ${error}`); diff --git a/x-pack/plugins/cases/server/routes/api/cases/get_case.ts b/x-pack/plugins/cases/server/routes/api/cases/get_case.ts index 1f39762d5512bd..e48806567e5745 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/get_case.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/get_case.ts @@ -37,7 +37,7 @@ export function initGetCaseApi({ router, logger }: RouteDeps) { const id = request.params.case_id; return response.ok({ - body: await casesClient.get({ + body: await casesClient.cases.get({ id, includeComments: request.query.includeComments, includeSubCaseComments: request.query.includeSubCaseComments, diff --git a/x-pack/plugins/cases/server/routes/api/cases/helpers.ts b/x-pack/plugins/cases/server/routes/api/cases/helpers.ts index 697b4d5df7ad1d..f6570bb5c88cd8 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/helpers.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/helpers.ts @@ -9,8 +9,7 @@ import { get, isPlainObject } from 'lodash'; import deepEqual from 'fast-deep-equal'; import { SavedObjectsFindResponse } from 'kibana/server'; -import { nodeBuilder } from '../../../../../../../src/plugins/data/common'; -import { KueryNode } from '../../../../../../../src/plugins/data/server'; +import { nodeBuilder, KueryNode } from '../../../../../../../src/plugins/data/common'; import { CaseConnector, ESCaseConnector, diff --git a/x-pack/plugins/cases/server/routes/api/cases/patch_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/patch_cases.ts index 5c417a3d98b938..244ab1a8f16aee 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/patch_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/patch_cases.ts @@ -28,7 +28,7 @@ export function initPatchCasesApi({ router, logger }: RouteDeps) { const cases = request.body as CasesPatchRequest; return response.ok({ - body: await casesClient.update(cases), + body: await casesClient.cases.update(cases), }); } catch (error) { logger.error(`Failed to patch cases in route: ${error}`); diff --git a/x-pack/plugins/cases/server/routes/api/cases/post_case.ts b/x-pack/plugins/cases/server/routes/api/cases/post_case.ts index d5f38c76fae3f4..391310cb810100 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/post_case.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/post_case.ts @@ -28,7 +28,7 @@ export function initPostCaseApi({ router, logger }: RouteDeps) { const theCase = request.body as CasePostRequest; return response.ok({ - body: await casesClient.create({ ...theCase }), + body: await casesClient.cases.create({ ...theCase }), }); } catch (error) { logger.error(`Failed to post case in route: ${error}`); diff --git a/x-pack/plugins/cases/server/routes/api/cases/push_case.ts b/x-pack/plugins/cases/server/routes/api/cases/push_case.ts index 02423943c05572..9818c97d883c45 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/push_case.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/push_case.ts @@ -44,7 +44,7 @@ export function initPushCaseApi({ router, logger }: RouteDeps) { ); return response.ok({ - body: await casesClient.push({ + body: await casesClient.cases.push({ actionsClient, caseId: params.case_id, connectorId: params.connector_id, diff --git a/x-pack/plugins/cases/server/routes/api/cases/reporters/get_reporters.ts b/x-pack/plugins/cases/server/routes/api/cases/reporters/get_reporters.ts index bbb21da1b71f40..1ce60442ee9c9e 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/reporters/get_reporters.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/reporters/get_reporters.ts @@ -18,11 +18,11 @@ export function initGetReportersApi({ caseService, router, logger }: RouteDeps) }, async (context, request, response) => { try { - const client = context.core.savedObjects.getClient({ + const soClient = context.core.savedObjects.getClient({ includedHiddenTypes: SAVED_OBJECT_TYPES, }); const reporters = await caseService.getReporters({ - client, + soClient, }); return response.ok({ body: UsersRt.encode(reporters) }); } catch (error) { diff --git a/x-pack/plugins/cases/server/routes/api/cases/status/get_status.ts b/x-pack/plugins/cases/server/routes/api/cases/status/get_status.ts index 27f5e0e0177376..ddfa5e39c01b00 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/status/get_status.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/status/get_status.ts @@ -20,7 +20,7 @@ export function initGetCasesStatusApi({ caseService, router, logger }: RouteDeps }, async (context, request, response) => { try { - const client = context.core.savedObjects.getClient({ + const soClient = context.core.savedObjects.getClient({ includedHiddenTypes: SAVED_OBJECT_TYPES, }); @@ -28,7 +28,7 @@ export function initGetCasesStatusApi({ caseService, router, logger }: RouteDeps ...caseStatuses.map((status) => { const statusQuery = constructQueryOptions({ status }); return caseService.findCaseStatusStats({ - client, + soClient, caseOptions: statusQuery.case, subCaseOptions: statusQuery.subCase, }); 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 77e94f9eb7e8f0..15eb5a421358b6 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 @@ -17,6 +17,7 @@ import { } from '../../../../../common/constants'; export function initDeleteSubCasesApi({ + attachmentService, caseService, router, userActionService, @@ -33,13 +34,13 @@ export function initDeleteSubCasesApi({ }, async (context, request, response) => { try { - const client = context.core.savedObjects.getClient({ + const soClient = context.core.savedObjects.getClient({ includedHiddenTypes: SAVED_OBJECT_TYPES, }); const [comments, subCases] = await Promise.all([ - caseService.getAllSubCaseComments({ client, id: request.query.ids }), - caseService.getSubCases({ client, ids: request.query.ids }), + caseService.getAllSubCaseComments({ soClient, id: request.query.ids }), + caseService.getSubCases({ soClient, ids: request.query.ids }), ]); const subCaseErrors = subCases.saved_objects.filter( @@ -62,18 +63,18 @@ export function initDeleteSubCasesApi({ await Promise.all( comments.saved_objects.map((comment) => - caseService.deleteComment({ client, commentId: comment.id }) + attachmentService.delete({ soClient, attachmentId: comment.id }) ) ); - await Promise.all(request.query.ids.map((id) => caseService.deleteSubCase(client, 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.postUserActions({ - client, + await userActionService.bulkCreate({ + soClient, actions: request.query.ids.map((id) => buildCaseUserActionItem({ action: 'delete', 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 fd1e84e8a012c1..f9d077cbe3b122 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 @@ -37,7 +37,7 @@ export function initFindSubCasesApi({ caseService, router, logger }: RouteDeps) }, async (context, request, response) => { try { - const client = context.core.savedObjects.getClient({ + const soClient = context.core.savedObjects.getClient({ includedHiddenTypes: SAVED_OBJECT_TYPES, }); const queryParams = pipe( @@ -52,7 +52,7 @@ export function initFindSubCasesApi({ caseService, router, logger }: RouteDeps) }); const subCases = await caseService.findSubCasesGroupByCase({ - client, + soClient, ids, options: { sortField: 'created_at', @@ -70,7 +70,7 @@ export function initFindSubCasesApi({ caseService, router, logger }: RouteDeps) sortByField: queryParams.sortField, }); return caseService.findSubCaseStatusStats({ - client, + soClient, options: statusQueryOptions ?? {}, ids, }); 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 093165a7281842..afeaef639326d0 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 @@ -29,13 +29,13 @@ export function initGetSubCaseApi({ caseService, router, logger }: RouteDeps) { }, async (context, request, response) => { try { - const client = context.core.savedObjects.getClient({ + const soClient = context.core.savedObjects.getClient({ includedHiddenTypes: SAVED_OBJECT_TYPES, }); const includeComments = request.query.includeComments; const subCase = await caseService.getSubCase({ - client, + soClient, id: request.params.sub_case_id, }); @@ -50,7 +50,7 @@ export function initGetSubCaseApi({ caseService, router, logger }: RouteDeps) { } const theComments = await caseService.getAllSubCaseComments({ - client, + soClient, id: request.params.sub_case_id, options: { sortField: 'created_at', 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 5b623815f027fa..4a407fc261a9b6 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 @@ -19,7 +19,7 @@ import { import { nodeBuilder } from '../../../../../../../../src/plugins/data/common'; import { CasesClient } from '../../../../client'; -import { CaseServiceSetup, CaseUserActionServiceSetup } from '../../../../services'; +import { CaseService, CaseUserActionService } from '../../../../services'; import { CaseStatuses, SubCasesPatchRequest, @@ -51,13 +51,13 @@ import { import { getCaseToUpdate } from '../helpers'; import { buildSubCaseUserActions } from '../../../../services/user_actions/helpers'; import { createAlertUpdateRequest } from '../../../../common'; -import { UpdateAlertRequest } from '../../../../client/types'; import { createCaseError } from '../../../../common/error'; +import { UpdateAlertRequest } from '../../../../client/alerts/client'; interface UpdateArgs { - client: SavedObjectsClientContract; - caseService: CaseServiceSetup; - userActionService: CaseUserActionServiceSetup; + soClient: SavedObjectsClientContract; + caseService: CaseService; + userActionService: CaseUserActionService; request: KibanaRequest; casesClient: CasesClient; subCases: SubCasesPatchRequest; @@ -132,19 +132,19 @@ function getParentIDs({ async function getParentCases({ caseService, - client, + soClient, subCaseIDs, subCasesMap, }: { - caseService: CaseServiceSetup; - client: SavedObjectsClientContract; + caseService: CaseService; + soClient: SavedObjectsClientContract; subCaseIDs: string[]; subCasesMap: Map>; }): Promise>> { const parentIDInfo = getParentIDs({ subCaseIDs, subCasesMap }); const parentCases = await caseService.getCases({ - client, + soClient, caseIds: parentIDInfo.ids, }); @@ -199,15 +199,15 @@ function getID(comment: SavedObject): string | undefined { async function getAlertComments({ subCasesToSync, caseService, - client, + soClient, }: { subCasesToSync: SubCasePatchRequest[]; - caseService: CaseServiceSetup; - client: SavedObjectsClientContract; + caseService: CaseService; + soClient: SavedObjectsClientContract; }): Promise> { const ids = subCasesToSync.map((subCase) => subCase.id); return caseService.getAllSubCaseComments({ - client, + soClient, id: ids, options: { filter: nodeBuilder.or([ @@ -222,17 +222,17 @@ async function getAlertComments({ * Updates the status of alerts for the specified sub cases. */ async function updateAlerts({ - subCasesToSync, caseService, - client, + soClient, casesClient, logger, + subCasesToSync, }: { - subCasesToSync: SubCasePatchRequest[]; - caseService: CaseServiceSetup; - client: SavedObjectsClientContract; + caseService: CaseService; + soClient: SavedObjectsClientContract; casesClient: CasesClient; logger: Logger; + subCasesToSync: SubCasePatchRequest[]; }) { try { const subCasesToSyncMap = subCasesToSync.reduce((acc, subCase) => { @@ -240,7 +240,7 @@ async function updateAlerts({ return acc; }, new Map()); // get all the alerts for all sub cases that need to be synced - const totalAlerts = await getAlertComments({ caseService, client, subCasesToSync }); + 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) => { @@ -258,7 +258,7 @@ async function updateAlerts({ [] ); - await casesClient.updateAlertsStatus({ alerts: alertsToUpdate }); + await casesClient.casesClientInternal.alerts.updateStatus({ alerts: alertsToUpdate }); } catch (error) { throw createCaseError({ message: `Failed to update alert status while updating sub cases: ${JSON.stringify( @@ -271,7 +271,7 @@ async function updateAlerts({ } async function update({ - client, + soClient, caseService, userActionService, request, @@ -286,7 +286,7 @@ async function update({ try { const bulkSubCases = await caseService.getSubCases({ - client, + soClient, ids: query.subCases.map((q) => q.id), }); @@ -304,7 +304,7 @@ async function update({ } const subIDToParentCase = await getParentCases({ - client, + soClient, caseService, subCaseIDs: nonEmptySubCaseRequests.map((subCase) => subCase.id), subCasesMap, @@ -314,7 +314,7 @@ async function update({ const { username, full_name, email } = await caseService.getUser({ request }); const updatedAt = new Date().toISOString(); const updatedCases = await caseService.patchSubCases({ - client, + soClient, subCases: nonEmptySubCaseRequests.map((thisCase) => { const { id: subCaseId, version, ...updateSubCaseAttributes } = thisCase; let closedInfo: { closed_at: string | null; closed_by: User | null } = { @@ -366,7 +366,7 @@ async function update({ await updateAlerts({ caseService, - client, + soClient, casesClient, subCasesToSync: subCasesToSyncAlertsFor, logger, @@ -393,8 +393,8 @@ async function update({ [] ); - await userActionService.postUserActions({ - client, + await userActionService.bulkCreate({ + soClient, actions: buildSubCaseUserActions({ originalSubCases: bulkSubCases.saved_objects, updatedSubCases: updatedCases.saved_objects, @@ -440,7 +440,7 @@ export function initPatchSubCasesApi({ request, subCases, casesClient, - client: context.core.savedObjects.client, + soClient: context.core.savedObjects.client, caseService, userActionService, logger, diff --git a/x-pack/plugins/cases/server/routes/api/cases/tags/get_tags.ts b/x-pack/plugins/cases/server/routes/api/cases/tags/get_tags.ts index 18231edd16353b..10c15d2518f349 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/tags/get_tags.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/tags/get_tags.ts @@ -17,11 +17,11 @@ export function initGetTagsApi({ caseService, router }: RouteDeps) { }, async (context, request, response) => { try { - const client = context.core.savedObjects.getClient({ + const soClient = context.core.savedObjects.getClient({ includedHiddenTypes: SAVED_OBJECT_TYPES, }); const tags = await caseService.getTags({ - client, + soClient, }); return response.ok({ body: tags }); } catch (error) { diff --git a/x-pack/plugins/cases/server/routes/api/cases/user_actions/get_all_user_actions.ts b/x-pack/plugins/cases/server/routes/api/cases/user_actions/get_all_user_actions.ts index ce0b4636130d73..07f1353f19854b 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/user_actions/get_all_user_actions.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/user_actions/get_all_user_actions.ts @@ -31,7 +31,7 @@ export function initGetAllCaseUserActionsApi({ router, logger }: RouteDeps) { const caseId = request.params.case_id; return response.ok({ - body: await casesClient.getUserActions({ caseId }), + body: await casesClient.userActions.getAll({ caseId }), }); } catch (error) { logger.error( @@ -65,7 +65,7 @@ export function initGetAllSubCaseUserActionsApi({ router, logger }: RouteDeps) { const subCaseId = request.params.sub_case_id; return response.ok({ - body: await casesClient.getUserActions({ caseId, subCaseId }), + body: await casesClient.userActions.getAll({ caseId, subCaseId }), }); } catch (error) { logger.error( diff --git a/x-pack/plugins/cases/server/routes/api/types.ts b/x-pack/plugins/cases/server/routes/api/types.ts index 6ce40e01c77520..76fad3fcc33bc6 100644 --- a/x-pack/plugins/cases/server/routes/api/types.ts +++ b/x-pack/plugins/cases/server/routes/api/types.ts @@ -8,20 +8,22 @@ import type { Logger } from 'kibana/server'; import type { - CaseConfigureServiceSetup, - CaseServiceSetup, - CaseUserActionServiceSetup, - ConnectorMappingsServiceSetup, + CaseConfigureService, + CaseService, + CaseUserActionService, + ConnectorMappingsService, + AttachmentService, } from '../../services'; import type { CasesRouter } from '../../types'; export interface RouteDeps { - caseConfigureService: CaseConfigureServiceSetup; - caseService: CaseServiceSetup; - connectorMappingsService: ConnectorMappingsServiceSetup; + caseConfigureService: CaseConfigureService; + caseService: CaseService; + connectorMappingsService: ConnectorMappingsService; router: CasesRouter; - userActionService: CaseUserActionServiceSetup; + userActionService: CaseUserActionService; + attachmentService: AttachmentService; logger: Logger; } diff --git a/x-pack/plugins/cases/server/services/alerts/index.ts b/x-pack/plugins/cases/server/services/alerts/index.ts index db8e841f45ee4a..e7b331138d73ca 100644 --- a/x-pack/plugins/cases/server/services/alerts/index.ts +++ b/x-pack/plugins/cases/server/services/alerts/index.ts @@ -11,9 +11,9 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import { ElasticsearchClient, Logger } from 'kibana/server'; import { MAX_ALERTS_PER_SUB_CASE } from '../../../common/constants'; -import { UpdateAlertRequest } from '../../client/types'; import { AlertInfo } from '../../common'; import { createCaseError } from '../../common/error'; +import { UpdateAlertRequest } from '../../client/alerts/client'; export type AlertServiceContract = PublicMethodsOf; diff --git a/x-pack/plugins/cases/server/services/attachments/index.ts b/x-pack/plugins/cases/server/services/attachments/index.ts new file mode 100644 index 00000000000000..fdfa722d18defb --- /dev/null +++ b/x-pack/plugins/cases/server/services/attachments/index.ts @@ -0,0 +1,116 @@ +/* + * 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 { Logger, SavedObject, SavedObjectReference } from 'kibana/server'; + +import { + CommentAttributes as AttachmentAttributes, + CommentPatchAttributes as AttachmentPatchAttributes, +} from '../../../common/api'; +import { CASE_COMMENT_SAVED_OBJECT } from '../../../common/constants'; +import { ClientArgs } from '..'; + +interface GetAttachmentArgs extends ClientArgs { + attachmentId: string; +} + +interface CreateAttachmentArgs extends ClientArgs { + attributes: AttachmentAttributes; + references: SavedObjectReference[]; +} + +interface UpdateArgs { + attachmentId: string; + updatedAttributes: AttachmentPatchAttributes; + version?: string; +} + +type UpdateAttachmentArgs = UpdateArgs & ClientArgs; + +interface BulkUpdateAttachmentArgs extends ClientArgs { + comments: UpdateArgs[]; +} + +export class AttachmentService { + constructor(private readonly log: Logger) {} + + public async get({ + soClient, + attachmentId, + }: GetAttachmentArgs): Promise> { + try { + this.log.debug(`Attempting to GET attachment ${attachmentId}`); + return await soClient.get(CASE_COMMENT_SAVED_OBJECT, attachmentId); + } catch (error) { + this.log.error(`Error on GET attachment ${attachmentId}: ${error}`); + throw error; + } + } + + public async delete({ soClient, attachmentId }: GetAttachmentArgs) { + try { + this.log.debug(`Attempting to GET attachment ${attachmentId}`); + return await soClient.delete(CASE_COMMENT_SAVED_OBJECT, attachmentId); + } catch (error) { + this.log.error(`Error on GET attachment ${attachmentId}: ${error}`); + throw error; + } + } + + public async create({ soClient, attributes, references }: CreateAttachmentArgs) { + try { + this.log.debug(`Attempting to POST a new comment`); + return await soClient.create(CASE_COMMENT_SAVED_OBJECT, attributes, { + references, + }); + } catch (error) { + this.log.error(`Error on POST a new comment: ${error}`); + throw error; + } + } + + public async update({ + soClient, + attachmentId, + updatedAttributes, + version, + }: UpdateAttachmentArgs) { + try { + this.log.debug(`Attempting to UPDATE comment ${attachmentId}`); + return await soClient.update( + CASE_COMMENT_SAVED_OBJECT, + attachmentId, + updatedAttributes, + { version } + ); + } catch (error) { + this.log.error(`Error on UPDATE comment ${attachmentId}: ${error}`); + throw error; + } + } + + public async bulkUpdate({ soClient, comments }: BulkUpdateAttachmentArgs) { + try { + this.log.debug( + `Attempting to UPDATE comments ${comments.map((c) => c.attachmentId).join(', ')}` + ); + return await soClient.bulkUpdate( + comments.map((c) => ({ + type: CASE_COMMENT_SAVED_OBJECT, + id: c.attachmentId, + attributes: c.updatedAttributes, + version: c.version, + })) + ); + } catch (error) { + this.log.error( + `Error on UPDATE comments ${comments.map((c) => c.attachmentId).join(', ')}: ${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 new file mode 100644 index 00000000000000..bbb82214d70a54 --- /dev/null +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -0,0 +1,1015 @@ +/* + * 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 { cloneDeep } from 'lodash'; +import { + KibanaRequest, + Logger, + SavedObject, + SavedObjectsClientContract, + SavedObjectsFindResponse, + SavedObjectsBulkResponse, + SavedObjectsFindResult, +} from 'kibana/server'; + +import { nodeBuilder, KueryNode } from '../../../../../../src/plugins/data/common'; + +import { SecurityPluginSetup } from '../../../../security/server'; +import { + ESCaseAttributes, + CommentAttributes, + User, + SubCaseAttributes, + AssociationType, + SubCaseResponse, + CommentType, + CaseType, + CaseResponse, + caseTypeField, + CasesFindRequest, +} from '../../../common/api'; +import { + defaultSortField, + groupTotalAlertsByID, + SavedObjectFindOptionsKueryNode, +} from '../../common'; +import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; +import { defaultPage, defaultPerPage } from '../../routes/api'; +import { + flattenCaseSavedObject, + flattenSubCaseSavedObject, + transformNewSubCase, +} from '../../routes/api/utils'; +import { + CASE_SAVED_OBJECT, + CASE_COMMENT_SAVED_OBJECT, + SUB_CASE_SAVED_OBJECT, +} from '../../../common/constants'; +import { readReporters } from './read_reporters'; +import { readTags } from './read_tags'; +import { ClientArgs } from '..'; + +interface PushedArgs { + pushed_at: string; + pushed_by: User; +} + +interface GetCaseArgs extends ClientArgs { + id: string; +} + +interface GetCasesArgs extends ClientArgs { + caseIds: string[]; +} + +interface GetSubCasesArgs extends ClientArgs { + ids: string[]; +} + +interface FindCommentsArgs { + soClient: SavedObjectsClientContract; + id: string | string[]; + options?: SavedObjectFindOptionsKueryNode; +} + +interface FindCaseCommentsArgs { + soClient: SavedObjectsClientContract; + id: string | string[]; + options?: SavedObjectFindOptionsKueryNode; + includeSubCaseComments?: boolean; +} + +interface FindSubCaseCommentsArgs { + soClient: SavedObjectsClientContract; + id: string | string[]; + options?: SavedObjectFindOptionsKueryNode; +} + +interface FindCasesArgs extends ClientArgs { + options?: SavedObjectFindOptionsKueryNode; +} + +interface FindSubCasesByIDArgs extends FindCasesArgs { + ids: string[]; +} + +interface FindSubCasesStatusStats { + soClient: SavedObjectsClientContract; + options: SavedObjectFindOptionsKueryNode; + ids: string[]; +} + +interface PostCaseArgs extends ClientArgs { + attributes: ESCaseAttributes; +} + +interface CreateSubCaseArgs extends ClientArgs { + createdAt: string; + caseId: string; + createdBy: User; +} + +interface PatchCase { + caseId: string; + updatedAttributes: Partial; + version?: string; +} +type PatchCaseArgs = PatchCase & ClientArgs; + +interface PatchCasesArgs extends ClientArgs { + cases: PatchCase[]; +} + +interface PatchSubCase { + soClient: SavedObjectsClientContract; + subCaseId: string; + updatedAttributes: Partial; + version?: string; +} + +interface PatchSubCases { + soClient: SavedObjectsClientContract; + subCases: Array>; +} + +interface GetUserArgs { + request: KibanaRequest; +} + +interface SubCasesMapWithPageInfo { + subCasesMap: Map; + page: number; + perPage: number; + total: number; +} + +interface CaseCommentStats { + commentTotals: Map; + alertTotals: Map; +} + +interface FindCommentsByAssociationArgs { + soClient: SavedObjectsClientContract; + id: string | string[]; + associationType: AssociationType; + options?: SavedObjectFindOptionsKueryNode; +} + +interface Collection { + case: SavedObjectsFindResult; + subCases?: SubCaseResponse[]; +} + +interface CasesMapWithPageInfo { + casesMap: Map; + page: number; + perPage: number; + total: number; +} + +type FindCaseOptions = CasesFindRequest & SavedObjectFindOptionsKueryNode; + +export class CaseService { + constructor( + private readonly log: Logger, + private readonly authentication?: SecurityPluginSetup['authc'] + ) {} + + /** + * Returns a map of all cases combined with their sub cases if they are collections. + */ + public async findCasesGroupedByID({ + soClient, + caseOptions, + subCaseOptions, + }: { + soClient: SavedObjectsClientContract; + caseOptions: FindCaseOptions; + subCaseOptions?: SavedObjectFindOptionsKueryNode; + }): Promise { + const cases = await this.findCases({ + soClient, + options: caseOptions, + }); + + const subCasesResp = ENABLE_CASE_CONNECTOR + ? await this.findSubCasesGroupByCase({ + soClient, + options: subCaseOptions, + ids: cases.saved_objects + .filter((caseInfo) => caseInfo.attributes.type === CaseType.collection) + .map((caseInfo) => caseInfo.id), + }) + : { subCasesMap: new Map(), page: 0, perPage: 0 }; + + const casesMap = cases.saved_objects.reduce((accMap, caseInfo) => { + const subCasesForCase = subCasesResp.subCasesMap.get(caseInfo.id); + + /** + * If this case is an individual add it to the return map + * If it is a collection and it has sub cases add it to the return map + * If it is a collection and it does not have sub cases, check and see if we're filtering on a status, + * if we're filtering on a status then exclude the empty collection from the results + * if we're not filtering on a status then include the empty collection (that way we can display all the collections + * when the UI isn't doing any filtering) + */ + if ( + caseInfo.attributes.type === CaseType.individual || + subCasesForCase !== undefined || + !caseOptions.status + ) { + accMap.set(caseInfo.id, { case: caseInfo, subCases: subCasesForCase }); + } + return accMap; + }, new Map()); + + /** + * One potential optimization here is to get all comment stats for individual cases, parent cases, and sub cases + * in a single request. This can be done because comments that are for sub cases have a reference to both the sub case + * and the parent. The associationType field allows us to determine which type of case the comment is attached to. + * + * So we could use the ids for all the valid cases (individual cases and parents with sub cases) to grab everything. + * Once we have it we can build the maps. + * + * Currently we get all comment stats for all sub cases in one go and we get all comment stats for cases (individual and parent) + * in another request (the one below this comment). + */ + const totalCommentsForCases = await this.getCaseCommentStats({ + soClient, + ids: Array.from(casesMap.keys()), + associationType: AssociationType.case, + }); + + const casesWithComments = new Map(); + for (const [id, caseInfo] of casesMap.entries()) { + casesWithComments.set( + id, + flattenCaseSavedObject({ + savedObject: caseInfo.case, + totalComment: totalCommentsForCases.commentTotals.get(id) ?? 0, + totalAlerts: totalCommentsForCases.alertTotals.get(id) ?? 0, + subCases: caseInfo.subCases, + }) + ); + } + + return { + casesMap: casesWithComments, + page: cases.page, + perPage: cases.per_page, + total: cases.total, + }; + } + + /** + * Retrieves the number of cases that exist with a given status (open, closed, etc). + * This also counts sub cases. Parent cases are excluded from the statistics. + */ + public async findCaseStatusStats({ + soClient, + caseOptions, + subCaseOptions, + }: { + soClient: SavedObjectsClientContract; + caseOptions: SavedObjectFindOptionsKueryNode; + subCaseOptions?: SavedObjectFindOptionsKueryNode; + }): Promise { + const casesStats = await this.findCases({ + soClient, + 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 + * the ids for the parent cases that match those filters. Then we use those IDS to count how many + * sub cases those parents have to calculate the total amount of cases that are open, closed, or in-progress. + * + * Another solution would be to store ALL filterable fields on both a case and sub case. That we could do a single + * query for each type to calculate the totals using the filters. This has drawbacks though: + * + * We'd have to sync up the parent case's editable attributes with the sub case any time they were change to avoid + * them getting out of sync and causing issues when we do these types of stats aggregations. This would result in a lot + * of update requests if the user is editing their case details often. Which could potentially cause conflict failures. + * + * Another option is to prevent the ability from update the parent case's details all together once it's created. A user + * could instead modify the sub case details directly. This could be weird though because individual sub cases for the same + * parent would have different titles, tags, etc. + * + * Another potential issue with this approach is when you push a case and all its sub case information. If the sub cases + * don't have the same title and tags, we'd need to account for that as well. + */ + const cases = await this.findCases({ + soClient, + options: { + ...caseOptions, + fields: [caseTypeField], + page: 1, + perPage: casesStats.total, + }, + }); + + const caseIds = cases.saved_objects + .filter((caseInfo) => caseInfo.attributes.type === CaseType.collection) + .map((caseInfo) => caseInfo.id); + + let subCasesTotal = 0; + + if (ENABLE_CASE_CONNECTOR && subCaseOptions) { + subCasesTotal = await this.findSubCaseStatusStats({ + soClient, + options: cloneDeep(subCaseOptions), + ids: caseIds, + }); + } + + const total = + cases.saved_objects.filter((caseInfo) => caseInfo.attributes.type !== CaseType.collection) + .length + subCasesTotal; + + return total; + } + + /** + * Retrieves the comments attached to a case or sub case. + */ + public async getCommentsByAssociation({ + soClient, + id, + associationType, + options, + }: FindCommentsByAssociationArgs): Promise> { + if (associationType === AssociationType.subCase) { + return this.getAllSubCaseComments({ + soClient, + id, + options, + }); + } else { + return this.getAllCaseComments({ + soClient, + id, + options, + }); + } + } + + /** + * Returns the number of total comments and alerts for a case (or sub case) + */ + public async getCaseCommentStats({ + soClient, + ids, + associationType, + }: { + soClient: SavedObjectsClientContract; + ids: string[]; + associationType: AssociationType; + }): Promise { + if (ids.length <= 0) { + return { + commentTotals: new Map(), + alertTotals: new Map(), + }; + } + + const refType = + associationType === AssociationType.case ? CASE_SAVED_OBJECT : SUB_CASE_SAVED_OBJECT; + + const allComments = await Promise.all( + ids.map((id) => + this.getCommentsByAssociation({ + soClient, + associationType, + id, + options: { page: 1, perPage: 1 }, + }) + ) + ); + + const alerts = await this.getCommentsByAssociation({ + soClient, + associationType, + 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 + ), + ]), + }, + }); + + const getID = (comments: SavedObjectsFindResponse) => { + return comments.saved_objects.length > 0 + ? comments.saved_objects[0].references.find((ref) => ref.type === refType)?.id + : undefined; + }; + + const groupedComments = allComments.reduce((acc, comments) => { + const id = getID(comments); + if (id) { + acc.set(id, comments.total); + } + return acc; + }, new Map()); + + const groupedAlerts = groupTotalAlertsByID({ comments: alerts }); + return { commentTotals: groupedComments, alertTotals: groupedAlerts }; + } + + /** + * Returns all the sub cases for a set of case IDs. Comment statistics are also returned. + */ + public async findSubCasesGroupByCase({ + soClient, + options, + ids, + }: { + soClient: SavedObjectsClientContract; + options?: SavedObjectFindOptionsKueryNode; + ids: string[]; + }): Promise { + const getCaseID = (subCase: SavedObjectsFindResult): string | undefined => { + return subCase.references.length > 0 ? subCase.references[0].id : undefined; + }; + + const emptyResponse = { + subCasesMap: new Map(), + page: 0, + perPage: 0, + total: 0, + }; + + if (!options) { + return emptyResponse; + } + + if (ids.length <= 0) { + return emptyResponse; + } + + const subCases = await this.findSubCases({ + soClient, + options: { + ...options, + hasReference: ids.map((id) => { + return { + id, + type: CASE_SAVED_OBJECT, + }; + }), + }, + }); + + const subCaseComments = await this.getCaseCommentStats({ + soClient, + ids: subCases.saved_objects.map((subCase) => subCase.id), + associationType: AssociationType.subCase, + }); + + const subCasesMap = subCases.saved_objects.reduce((accMap, subCase) => { + const parentCaseID = getCaseID(subCase); + if (parentCaseID) { + const subCaseFromMap = accMap.get(parentCaseID); + + if (subCaseFromMap === undefined) { + const subCasesForID = [ + flattenSubCaseSavedObject({ + savedObject: subCase, + totalComment: subCaseComments.commentTotals.get(subCase.id) ?? 0, + totalAlerts: subCaseComments.alertTotals.get(subCase.id) ?? 0, + }), + ]; + accMap.set(parentCaseID, subCasesForID); + } else { + subCaseFromMap.push( + flattenSubCaseSavedObject({ + savedObject: subCase, + totalComment: subCaseComments.commentTotals.get(subCase.id) ?? 0, + totalAlerts: subCaseComments.alertTotals.get(subCase.id) ?? 0, + }) + ); + } + } + return accMap; + }, new Map()); + + return { subCasesMap, page: subCases.page, perPage: subCases.per_page, total: subCases.total }; + } + + /** + * Calculates the number of sub cases for a given set of options for a set of case IDs. + */ + public async findSubCaseStatusStats({ + soClient, + options, + ids, + }: FindSubCasesStatusStats): Promise { + if (ids.length <= 0) { + return 0; + } + + const subCases = await this.findSubCases({ + soClient, + options: { + ...options, + page: 1, + perPage: 1, + fields: [], + hasReference: ids.map((id) => { + return { + id, + type: CASE_SAVED_OBJECT, + }; + }), + }, + }); + + return subCases.total; + } + + public async createSubCase({ + soClient, + createdAt, + caseId, + createdBy, + }: CreateSubCaseArgs): Promise> { + try { + this.log.debug(`Attempting to POST a new sub case`); + return soClient.create( + SUB_CASE_SAVED_OBJECT, + transformNewSubCase({ createdAt, createdBy }), + { + references: [ + { + type: CASE_SAVED_OBJECT, + name: `associated-${CASE_SAVED_OBJECT}`, + id: caseId, + }, + ], + } + ); + } catch (error) { + this.log.error(`Error on POST a new sub case for id ${caseId}: ${error}`); + throw error; + } + } + + public async getMostRecentSubCase(soClient: SavedObjectsClientContract, caseId: string) { + try { + this.log.debug(`Attempting to find most recent sub case for caseID: ${caseId}`); + const subCases = await soClient.find({ + perPage: 1, + sortField: 'created_at', + sortOrder: 'desc', + type: SUB_CASE_SAVED_OBJECT, + hasReference: { type: CASE_SAVED_OBJECT, id: caseId }, + }); + if (subCases.saved_objects.length <= 0) { + return; + } + + return subCases.saved_objects[0]; + } catch (error) { + this.log.error(`Error finding the most recent sub case for case: ${caseId}: ${error}`); + throw error; + } + } + + public async deleteSubCase(soClient: SavedObjectsClientContract, id: string) { + try { + this.log.debug(`Attempting to DELETE sub case ${id}`); + return await soClient.delete(SUB_CASE_SAVED_OBJECT, id); + } catch (error) { + this.log.error(`Error on DELETE sub case ${id}: ${error}`); + throw error; + } + } + + public async deleteCase({ soClient, id: caseId }: GetCaseArgs) { + try { + this.log.debug(`Attempting to DELETE case ${caseId}`); + return await soClient.delete(CASE_SAVED_OBJECT, caseId); + } catch (error) { + this.log.error(`Error on DELETE case ${caseId}: ${error}`); + throw error; + } + } + + public async getCase({ + soClient, + id: caseId, + }: GetCaseArgs): Promise> { + try { + this.log.debug(`Attempting to GET case ${caseId}`); + return await soClient.get(CASE_SAVED_OBJECT, caseId); + } catch (error) { + this.log.error(`Error on GET case ${caseId}: ${error}`); + throw error; + } + } + public async getSubCase({ soClient, id }: GetCaseArgs): Promise> { + try { + this.log.debug(`Attempting to GET sub case ${id}`); + return await soClient.get(SUB_CASE_SAVED_OBJECT, id); + } catch (error) { + this.log.error(`Error on GET sub case ${id}: ${error}`); + throw error; + } + } + + public async getSubCases({ + soClient, + ids, + }: GetSubCasesArgs): Promise> { + try { + this.log.debug(`Attempting to GET sub cases ${ids.join(', ')}`); + return await soClient.bulkGet( + ids.map((id) => ({ type: SUB_CASE_SAVED_OBJECT, id })) + ); + } catch (error) { + this.log.error(`Error on GET cases ${ids.join(', ')}: ${error}`); + throw error; + } + } + + public async getCases({ + soClient, + caseIds, + }: GetCasesArgs): Promise> { + try { + this.log.debug(`Attempting to GET cases ${caseIds.join(', ')}`); + return await soClient.bulkGet( + caseIds.map((caseId) => ({ type: CASE_SAVED_OBJECT, id: caseId })) + ); + } catch (error) { + this.log.error(`Error on GET cases ${caseIds.join(', ')}: ${error}`); + throw error; + } + } + + public async findCases({ + soClient, + options, + }: FindCasesArgs): Promise> { + try { + this.log.debug(`Attempting to find cases`); + return await soClient.find({ + sortField: defaultSortField, + ...cloneDeep(options), + type: CASE_SAVED_OBJECT, + }); + } catch (error) { + this.log.error(`Error on find cases: ${error}`); + throw error; + } + } + + public async findSubCases({ + soClient, + options, + }: FindCasesArgs): Promise> { + try { + this.log.debug(`Attempting to find sub cases`); + // if the page or perPage options are set then respect those instead of trying to + // grab all sub cases + if (options?.page !== undefined || options?.perPage !== undefined) { + return soClient.find({ + sortField: defaultSortField, + ...cloneDeep(options), + type: SUB_CASE_SAVED_OBJECT, + }); + } + + const stats = await soClient.find({ + fields: [], + page: 1, + perPage: 1, + sortField: defaultSortField, + ...cloneDeep(options), + type: SUB_CASE_SAVED_OBJECT, + }); + return soClient.find({ + page: 1, + perPage: stats.total, + sortField: defaultSortField, + ...cloneDeep(options), + type: SUB_CASE_SAVED_OBJECT, + }); + } catch (error) { + this.log.error(`Error on find sub cases: ${error}`); + throw error; + } + } + + /** + * Find sub cases using a collection's ID. This would try to retrieve the maximum amount of sub cases + * by default. + * + * @param id the saved object ID of the parent collection to find sub cases for. + */ + public async findSubCasesByCaseId({ + soClient, + ids, + options, + }: FindSubCasesByIDArgs): Promise> { + if (ids.length <= 0) { + return { + total: 0, + saved_objects: [], + page: options?.page ?? defaultPage, + per_page: options?.perPage ?? defaultPerPage, + }; + } + + try { + this.log.debug(`Attempting to GET sub cases for case collection id ${ids.join(', ')}`); + return this.findSubCases({ + soClient, + options: { + ...options, + hasReference: ids.map((id) => ({ + type: CASE_SAVED_OBJECT, + id, + })), + }, + }); + } catch (error) { + this.log.error( + `Error on GET all sub cases for case collection id ${ids.join(', ')}: ${error}` + ); + throw error; + } + } + + private asArray(id: string | string[] | undefined): string[] { + if (id === undefined) { + return []; + } else if (Array.isArray(id)) { + return id; + } else { + return [id]; + } + } + + private async getAllComments({ + soClient, + id, + options, + }: FindCommentsArgs): Promise> { + try { + this.log.debug(`Attempting to GET all comments for id ${JSON.stringify(id)}`); + if (options?.page !== undefined || options?.perPage !== undefined) { + return soClient.find({ + type: CASE_COMMENT_SAVED_OBJECT, + sortField: defaultSortField, + ...cloneDeep(options), + }); + } + // get the total number of comments that are in ES then we'll grab them all in one go + const stats = await soClient.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 soClient.find({ + type: CASE_COMMENT_SAVED_OBJECT, + page: 1, + perPage: stats.total, + sortField: defaultSortField, + ...cloneDeep(options), + }); + } catch (error) { + this.log.error(`Error on GET all comments for ${JSON.stringify(id)}: ${error}`); + throw error; + } + } + + /** + * Default behavior is to retrieve all comments that adhere to a given filter (if one is included). + * to override this pass in the either the page or perPage options. + * + * @param includeSubCaseComments is a flag to indicate that sub case comments should be included as well, by default + * sub case comments are excluded. If the `filter` field is included in the options, it will override this behavior + */ + public async getAllCaseComments({ + soClient, + id, + options, + includeSubCaseComments = false, + }: FindCaseCommentsArgs): Promise> { + try { + const refs = this.asArray(id).map((caseID) => ({ type: CASE_SAVED_OBJECT, id: caseID })); + if (refs.length <= 0) { + return { + saved_objects: [], + total: 0, + per_page: options?.perPage ?? defaultPerPage, + page: options?.page ?? defaultPage, + }; + } + + let filter: KueryNode | undefined; + if (!includeSubCaseComments) { + // if other filters were passed in then combine them to filter out sub case comments + const associationTypeFilter = nodeBuilder.is( + `${CASE_COMMENT_SAVED_OBJECT}.attributes.associationType`, + AssociationType.case + ); + + filter = + options?.filter != null + ? nodeBuilder.and([options.filter, associationTypeFilter]) + : associationTypeFilter; + } + + this.log.debug(`Attempting to GET all comments for case caseID ${JSON.stringify(id)}`); + return this.getAllComments({ + soClient, + id, + options: { + hasReferenceOperator: 'OR', + hasReference: refs, + filter, + ...options, + }, + }); + } catch (error) { + this.log.error(`Error on GET all comments for case ${JSON.stringify(id)}: ${error}`); + throw error; + } + } + + public async getAllSubCaseComments({ + soClient, + id, + options, + }: FindSubCaseCommentsArgs): Promise> { + try { + const refs = this.asArray(id).map((caseID) => ({ type: SUB_CASE_SAVED_OBJECT, id: caseID })); + if (refs.length <= 0) { + return { + saved_objects: [], + total: 0, + per_page: options?.perPage ?? defaultPerPage, + page: options?.page ?? defaultPage, + }; + } + + this.log.debug(`Attempting to GET all comments for sub case caseID ${JSON.stringify(id)}`); + return this.getAllComments({ + soClient, + id, + options: { + hasReferenceOperator: 'OR', + hasReference: refs, + ...options, + }, + }); + } catch (error) { + this.log.error(`Error on GET all comments for sub case ${JSON.stringify(id)}: ${error}`); + throw error; + } + } + + public async getReporters({ soClient }: ClientArgs) { + try { + this.log.debug(`Attempting to GET all reporters`); + return await readReporters({ soClient }); + } catch (error) { + this.log.error(`Error on GET all reporters: ${error}`); + throw error; + } + } + public async getTags({ soClient }: ClientArgs) { + try { + this.log.debug(`Attempting to GET all cases`); + return await readTags({ soClient }); + } catch (error) { + this.log.error(`Error on GET cases: ${error}`); + throw error; + } + } + + public getUser({ request }: GetUserArgs) { + try { + this.log.debug(`Attempting to authenticate a user`); + if (this.authentication != null) { + const user = this.authentication.getCurrentUser(request); + if (!user) { + return { + username: null, + full_name: null, + email: null, + }; + } + return user; + } + return { + username: null, + full_name: null, + email: null, + }; + } catch (error) { + this.log.error(`Error on GET cases: ${error}`); + throw error; + } + } + + public async postNewCase({ soClient, attributes }: PostCaseArgs) { + try { + this.log.debug(`Attempting to POST a new case`); + return await soClient.create(CASE_SAVED_OBJECT, { + ...attributes, + }); + } catch (error) { + this.log.error(`Error on POST a new case: ${error}`); + throw error; + } + } + + public async patchCase({ soClient, caseId, updatedAttributes, version }: PatchCaseArgs) { + try { + this.log.debug(`Attempting to UPDATE case ${caseId}`); + return await soClient.update( + CASE_SAVED_OBJECT, + caseId, + { ...updatedAttributes }, + { version } + ); + } catch (error) { + this.log.error(`Error on UPDATE case ${caseId}: ${error}`); + throw error; + } + } + + public async patchCases({ soClient, cases }: PatchCasesArgs) { + try { + this.log.debug(`Attempting to UPDATE case ${cases.map((c) => c.caseId).join(', ')}`); + return await soClient.bulkUpdate( + cases.map((c) => ({ + type: CASE_SAVED_OBJECT, + id: c.caseId, + attributes: c.updatedAttributes, + version: c.version, + })) + ); + } catch (error) { + this.log.error(`Error on UPDATE case ${cases.map((c) => c.caseId).join(', ')}: ${error}`); + throw error; + } + } + + public async patchSubCase({ soClient, subCaseId, updatedAttributes, version }: PatchSubCase) { + try { + this.log.debug(`Attempting to UPDATE sub case ${subCaseId}`); + return await soClient.update( + SUB_CASE_SAVED_OBJECT, + subCaseId, + { ...updatedAttributes }, + { version } + ); + } catch (error) { + this.log.error(`Error on UPDATE sub case ${subCaseId}: ${error}`); + throw error; + } + } + + public async patchSubCases({ soClient, subCases }: PatchSubCases) { + try { + this.log.debug( + `Attempting to UPDATE sub case ${subCases.map((c) => c.subCaseId).join(', ')}` + ); + return await soClient.bulkUpdate( + subCases.map((c) => ({ + type: SUB_CASE_SAVED_OBJECT, + id: c.subCaseId, + attributes: c.updatedAttributes, + version: c.version, + })) + ); + } catch (error) { + this.log.error( + `Error on UPDATE sub case ${subCases.map((c) => c.subCaseId).join(', ')}: ${error}` + ); + throw error; + } + } +} diff --git a/x-pack/plugins/cases/server/services/reporters/read_reporters.ts b/x-pack/plugins/cases/server/services/cases/read_reporters.ts similarity index 89% rename from x-pack/plugins/cases/server/services/reporters/read_reporters.ts rename to x-pack/plugins/cases/server/services/cases/read_reporters.ts index e6dea6b6ee1e8f..f7e88c2649ae68 100644 --- a/x-pack/plugins/cases/server/services/reporters/read_reporters.ts +++ b/x-pack/plugins/cases/server/services/cases/read_reporters.ts @@ -26,18 +26,18 @@ export const convertToReporters = (caseObjects: Array => { - const firstReporters = await client.find({ + const firstReporters = await soClient.find({ type: CASE_SAVED_OBJECT, fields: ['created_by'], page: 1, perPage: 1, }); - const reporters = await client.find({ + const reporters = await soClient.find({ type: CASE_SAVED_OBJECT, fields: ['created_by'], page: 1, diff --git a/x-pack/plugins/cases/server/services/tags/read_tags.ts b/x-pack/plugins/cases/server/services/cases/read_tags.ts similarity index 87% rename from x-pack/plugins/cases/server/services/tags/read_tags.ts rename to x-pack/plugins/cases/server/services/cases/read_tags.ts index 7ac4ff41e0aa8d..a977c473327f86 100644 --- a/x-pack/plugins/cases/server/services/tags/read_tags.ts +++ b/x-pack/plugins/cases/server/services/cases/read_tags.ts @@ -29,27 +29,27 @@ export const convertTagsToSet = (tagObjects: Array>) // then this should be replaced with a an aggregation call. // Ref: https://www.elastic.co/guide/en/kibana/master/saved-objects-api.html export const readTags = async ({ - client, + soClient, }: { - client: SavedObjectsClientContract; + soClient: SavedObjectsClientContract; perPage?: number; }): Promise => { - const tags = await readRawTags({ client }); + const tags = await readRawTags({ soClient }); return tags; }; export const readRawTags = async ({ - client, + soClient, }: { - client: SavedObjectsClientContract; + soClient: SavedObjectsClientContract; }): Promise => { - const firstTags = await client.find({ + const firstTags = await soClient.find({ type: CASE_SAVED_OBJECT, fields: ['tags'], page: 1, perPage: 1, }); - const tags = await client.find({ + const tags = await soClient.find({ type: CASE_SAVED_OBJECT, fields: ['tags'], page: 1, diff --git a/x-pack/plugins/cases/server/services/configure/index.ts b/x-pack/plugins/cases/server/services/configure/index.ts index 74ad23dd93ba01..45a9cd714145ff 100644 --- a/x-pack/plugins/cases/server/services/configure/index.ts +++ b/x-pack/plugins/cases/server/services/configure/index.ts @@ -5,19 +5,13 @@ * 2.0. */ -import { - Logger, - SavedObject, - SavedObjectsClientContract, - SavedObjectsFindResponse, - SavedObjectsUpdateResponse, -} from 'kibana/server'; +import { Logger, SavedObjectsClientContract } from 'kibana/server'; import { ESCasesConfigureAttributes, SavedObjectFindOptions } from '../../../common/api'; import { CASE_CONFIGURE_SAVED_OBJECT } from '../../../common/constants'; interface ClientArgs { - client: SavedObjectsClientContract; + soClient: SavedObjectsClientContract; } interface GetCaseConfigureArgs extends ClientArgs { @@ -36,65 +30,70 @@ interface PatchCaseConfigureArgs extends ClientArgs { updatedAttributes: Partial; } -export interface CaseConfigureServiceSetup { - delete(args: GetCaseConfigureArgs): Promise<{}>; - get(args: GetCaseConfigureArgs): Promise>; - find(args: FindCaseConfigureArgs): Promise>; - patch( - args: PatchCaseConfigureArgs - ): Promise>; - post(args: PostCaseConfigureArgs): Promise>; -} - export class CaseConfigureService { constructor(private readonly log: Logger) {} - public setup = async (): Promise => ({ - delete: async ({ client, caseConfigureId }: GetCaseConfigureArgs) => { - try { - this.log.debug(`Attempting to DELETE case configure ${caseConfigureId}`); - return await client.delete(CASE_CONFIGURE_SAVED_OBJECT, caseConfigureId); - } catch (error) { - this.log.debug(`Error on DELETE case configure ${caseConfigureId}: ${error}`); - throw error; - } - }, - get: async ({ client, caseConfigureId }: GetCaseConfigureArgs) => { - try { - this.log.debug(`Attempting to GET case configuration ${caseConfigureId}`); - return await client.get(CASE_CONFIGURE_SAVED_OBJECT, caseConfigureId); - } catch (error) { - this.log.debug(`Error on GET case configuration ${caseConfigureId}: ${error}`); - throw error; - } - }, - find: async ({ client, options }: FindCaseConfigureArgs) => { - try { - this.log.debug(`Attempting to find all case configuration`); - return await client.find({ ...options, type: CASE_CONFIGURE_SAVED_OBJECT }); - } catch (error) { - this.log.debug(`Attempting to find all case configuration`); - throw error; - } - }, - post: async ({ client, attributes }: PostCaseConfigureArgs) => { - try { - this.log.debug(`Attempting to POST a new case configuration`); - return await client.create(CASE_CONFIGURE_SAVED_OBJECT, { ...attributes }); - } catch (error) { - this.log.debug(`Error on POST a new case configuration: ${error}`); - throw error; - } - }, - patch: async ({ client, caseConfigureId, updatedAttributes }: PatchCaseConfigureArgs) => { - try { - this.log.debug(`Attempting to UPDATE case configuration ${caseConfigureId}`); - return await client.update(CASE_CONFIGURE_SAVED_OBJECT, caseConfigureId, { + + public async delete({ soClient, caseConfigureId }: GetCaseConfigureArgs) { + try { + this.log.debug(`Attempting to DELETE case configure ${caseConfigureId}`); + return await soClient.delete(CASE_CONFIGURE_SAVED_OBJECT, caseConfigureId); + } catch (error) { + this.log.debug(`Error on DELETE case configure ${caseConfigureId}: ${error}`); + throw error; + } + } + + public async get({ soClient, caseConfigureId }: GetCaseConfigureArgs) { + try { + this.log.debug(`Attempting to GET case configuration ${caseConfigureId}`); + return await soClient.get( + CASE_CONFIGURE_SAVED_OBJECT, + caseConfigureId + ); + } catch (error) { + this.log.debug(`Error on GET case configuration ${caseConfigureId}: ${error}`); + throw error; + } + } + + public async find({ soClient, options }: FindCaseConfigureArgs) { + try { + this.log.debug(`Attempting to find all case configuration`); + return await soClient.find({ + ...options, + type: CASE_CONFIGURE_SAVED_OBJECT, + }); + } catch (error) { + this.log.debug(`Attempting to find all case configuration`); + throw error; + } + } + + public async post({ soClient, attributes }: PostCaseConfigureArgs) { + try { + this.log.debug(`Attempting to POST a new case configuration`); + return await soClient.create(CASE_CONFIGURE_SAVED_OBJECT, { + ...attributes, + }); + } catch (error) { + this.log.debug(`Error on POST a new case configuration: ${error}`); + throw error; + } + } + + public async patch({ soClient, caseConfigureId, updatedAttributes }: PatchCaseConfigureArgs) { + try { + this.log.debug(`Attempting to UPDATE case configuration ${caseConfigureId}`); + return await soClient.update( + CASE_CONFIGURE_SAVED_OBJECT, + caseConfigureId, + { ...updatedAttributes, - }); - } catch (error) { - this.log.debug(`Error on UPDATE case configuration ${caseConfigureId}: ${error}`); - throw error; - } - }, - }); + } + ); + } catch (error) { + this.log.debug(`Error on UPDATE case configuration ${caseConfigureId}: ${error}`); + throw error; + } + } } diff --git a/x-pack/plugins/cases/server/services/connector_mappings/index.ts b/x-pack/plugins/cases/server/services/connector_mappings/index.ts index 5cb338e17bf75b..0d51e12a55ac76 100644 --- a/x-pack/plugins/cases/server/services/connector_mappings/index.ts +++ b/x-pack/plugins/cases/server/services/connector_mappings/index.ts @@ -5,19 +5,13 @@ * 2.0. */ -import { - Logger, - SavedObject, - SavedObjectReference, - SavedObjectsClientContract, - SavedObjectsFindResponse, -} from 'kibana/server'; +import { Logger, SavedObjectReference, SavedObjectsClientContract } from 'kibana/server'; import { ConnectorMappings, SavedObjectFindOptions } from '../../../common/api'; import { CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT } from '../../../common/constants'; interface ClientArgs { - client: SavedObjectsClientContract; + soClient: SavedObjectsClientContract; } interface FindConnectorMappingsArgs extends ClientArgs { options?: SavedObjectFindOptions; @@ -28,33 +22,35 @@ interface PostConnectorMappingsArgs extends ClientArgs { references: SavedObjectReference[]; } -export interface ConnectorMappingsServiceSetup { - find(args: FindConnectorMappingsArgs): Promise>; - post(args: PostConnectorMappingsArgs): Promise>; -} - export class ConnectorMappingsService { constructor(private readonly log: Logger) {} - public setup = async (): Promise => ({ - find: async ({ client, options }: FindConnectorMappingsArgs) => { - try { - this.log.debug(`Attempting to find all connector mappings`); - return await client.find({ ...options, type: CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT }); - } catch (error) { - this.log.error(`Attempting to find all connector mappings: ${error}`); - throw error; - } - }, - post: async ({ client, attributes, references }: PostConnectorMappingsArgs) => { - try { - this.log.debug(`Attempting to POST a new connector mappings`); - return await client.create(CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, attributes, { + + public async find({ soClient, options }: FindConnectorMappingsArgs) { + try { + this.log.debug(`Attempting to find all connector mappings`); + return await soClient.find({ + ...options, + type: CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, + }); + } catch (error) { + this.log.error(`Attempting to find all connector mappings: ${error}`); + throw error; + } + } + + public async post({ soClient, attributes, references }: PostConnectorMappingsArgs) { + try { + this.log.debug(`Attempting to POST a new connector mappings`); + return await soClient.create( + CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, + attributes, + { references, - }); - } catch (error) { - this.log.error(`Error on POST a new connector mappings: ${error}`); - throw error; - } - }, - }); + } + ); + } catch (error) { + this.log.error(`Error on POST a new connector mappings: ${error}`); + throw error; + } + } } diff --git a/x-pack/plugins/cases/server/services/index.ts b/x-pack/plugins/cases/server/services/index.ts index cb275b3f5d44d3..cffe7df91743fc 100644 --- a/x-pack/plugins/cases/server/services/index.ts +++ b/x-pack/plugins/cases/server/services/index.ts @@ -5,1151 +5,15 @@ * 2.0. */ -import { cloneDeep } from 'lodash'; -import { - KibanaRequest, - Logger, - SavedObject, - SavedObjectsClientContract, - SavedObjectsFindResponse, - SavedObjectsUpdateResponse, - SavedObjectReference, - SavedObjectsBulkUpdateResponse, - SavedObjectsBulkResponse, - SavedObjectsFindResult, -} from 'kibana/server'; +import { SavedObjectsClientContract } from 'kibana/server'; -import { nodeBuilder } from '../../../../../src/plugins/data/common'; -import { KueryNode } from '../../../../../src/plugins/data/server'; - -import { AuthenticatedUser, SecurityPluginSetup } from '../../../security/server'; -import { - ESCaseAttributes, - CommentAttributes, - User, - CommentPatchAttributes, - SubCaseAttributes, - AssociationType, - SubCaseResponse, - CommentType, - CaseType, - CaseResponse, - caseTypeField, - CasesFindRequest, -} from '../../common/api'; -import { defaultSortField, groupTotalAlertsByID, SavedObjectFindOptionsKueryNode } from '../common'; -import { ENABLE_CASE_CONNECTOR } from '../../common/constants'; -import { defaultPage, defaultPerPage } from '../routes/api'; -import { - flattenCaseSavedObject, - flattenSubCaseSavedObject, - transformNewSubCase, -} from '../routes/api/utils'; -import { - CASE_SAVED_OBJECT, - CASE_COMMENT_SAVED_OBJECT, - SUB_CASE_SAVED_OBJECT, -} from '../../common/constants'; -import { readReporters } from './reporters/read_reporters'; -import { readTags } from './tags/read_tags'; - -export { CaseConfigureService, CaseConfigureServiceSetup } from './configure'; -export { CaseUserActionService, CaseUserActionServiceSetup } from './user_actions'; -export { ConnectorMappingsService, ConnectorMappingsServiceSetup } from './connector_mappings'; +export { CaseService } from './cases'; +export { CaseConfigureService } from './configure'; +export { CaseUserActionService } from './user_actions'; +export { ConnectorMappingsService } from './connector_mappings'; export { AlertService, AlertServiceContract } from './alerts'; +export { AttachmentService } from './attachments'; export interface ClientArgs { - client: SavedObjectsClientContract; -} - -interface PushedArgs { - pushed_at: string; - pushed_by: User; -} - -interface GetCaseArgs extends ClientArgs { - id: string; -} - -interface GetCasesArgs extends ClientArgs { - caseIds: string[]; -} - -interface GetSubCasesArgs extends ClientArgs { - ids: string[]; -} - -interface FindCommentsArgs { - client: SavedObjectsClientContract; - id: string | string[]; - options?: SavedObjectFindOptionsKueryNode; -} - -interface FindCaseCommentsArgs { - client: SavedObjectsClientContract; - id: string | string[]; - options?: SavedObjectFindOptionsKueryNode; - includeSubCaseComments?: boolean; -} - -interface FindSubCaseCommentsArgs { - client: SavedObjectsClientContract; - id: string | string[]; - options?: SavedObjectFindOptionsKueryNode; -} - -interface FindCasesArgs extends ClientArgs { - options?: SavedObjectFindOptionsKueryNode; -} - -interface FindSubCasesByIDArgs extends FindCasesArgs { - ids: string[]; -} - -interface FindSubCasesStatusStats { - client: SavedObjectsClientContract; - options: SavedObjectFindOptionsKueryNode; - ids: string[]; -} - -interface GetCommentArgs extends ClientArgs { - commentId: string; -} - -interface PostCaseArgs extends ClientArgs { - attributes: ESCaseAttributes; -} - -interface CreateSubCaseArgs extends ClientArgs { - createdAt: string; - caseId: string; - createdBy: User; -} - -interface PostCommentArgs extends ClientArgs { - attributes: CommentAttributes; - references: SavedObjectReference[]; -} - -interface PatchCase { - caseId: string; - updatedAttributes: Partial; - version?: string; -} -type PatchCaseArgs = PatchCase & ClientArgs; - -interface PatchCasesArgs extends ClientArgs { - cases: PatchCase[]; -} - -interface PatchComment { - commentId: string; - updatedAttributes: CommentPatchAttributes; - version?: string; -} - -type UpdateCommentArgs = PatchComment & ClientArgs; - -interface PatchComments extends ClientArgs { - comments: PatchComment[]; -} - -interface PatchSubCase { - client: SavedObjectsClientContract; - subCaseId: string; - updatedAttributes: Partial; - version?: string; -} - -interface PatchSubCases { - client: SavedObjectsClientContract; - subCases: Array>; -} - -interface GetUserArgs { - request: KibanaRequest; -} - -interface SubCasesMapWithPageInfo { - subCasesMap: Map; - page: number; - perPage: number; - total: number; -} - -interface CaseCommentStats { - commentTotals: Map; - alertTotals: Map; -} - -interface FindCommentsByAssociationArgs { - client: SavedObjectsClientContract; - id: string | string[]; - associationType: AssociationType; - options?: SavedObjectFindOptionsKueryNode; -} - -interface Collection { - case: SavedObjectsFindResult; - subCases?: SubCaseResponse[]; -} - -interface CasesMapWithPageInfo { - casesMap: Map; - page: number; - perPage: number; - total: number; -} - -type FindCaseOptions = CasesFindRequest & SavedObjectFindOptionsKueryNode; - -export interface CaseServiceSetup { - deleteCase(args: GetCaseArgs): Promise<{}>; - deleteComment(args: GetCommentArgs): Promise<{}>; - deleteSubCase(client: SavedObjectsClientContract, id: string): Promise<{}>; - findCases(args: FindCasesArgs): Promise>; - findSubCases(args: FindCasesArgs): Promise>; - findSubCasesByCaseId( - args: FindSubCasesByIDArgs - ): Promise>; - getAllCaseComments( - args: FindCaseCommentsArgs - ): Promise>; - getAllSubCaseComments( - args: FindSubCaseCommentsArgs - ): Promise>; - getCase(args: GetCaseArgs): Promise>; - getSubCase(args: GetCaseArgs): Promise>; - getSubCases(args: GetSubCasesArgs): Promise>; - getCases(args: GetCasesArgs): Promise>; - getComment(args: GetCommentArgs): Promise>; - getTags(args: ClientArgs): Promise; - getReporters(args: ClientArgs): Promise; - getUser(args: GetUserArgs): AuthenticatedUser | User; - postNewCase(args: PostCaseArgs): Promise>; - postNewComment(args: PostCommentArgs): Promise>; - patchCase(args: PatchCaseArgs): Promise>; - patchCases(args: PatchCasesArgs): Promise>; - patchComment(args: UpdateCommentArgs): Promise>; - patchComments(args: PatchComments): Promise>; - getMostRecentSubCase( - client: SavedObjectsClientContract, - caseId: string - ): Promise | undefined>; - createSubCase(args: CreateSubCaseArgs): Promise>; - patchSubCase(args: PatchSubCase): Promise>; - patchSubCases(args: PatchSubCases): Promise>; - findSubCaseStatusStats(args: FindSubCasesStatusStats): Promise; - getCommentsByAssociation( - args: FindCommentsByAssociationArgs - ): Promise>; - getCaseCommentStats(args: { - client: SavedObjectsClientContract; - ids: string[]; - associationType: AssociationType; - }): Promise; - findSubCasesGroupByCase(args: { - client: SavedObjectsClientContract; - options?: SavedObjectFindOptionsKueryNode; - ids: string[]; - }): Promise; - findCaseStatusStats(args: { - client: SavedObjectsClientContract; - caseOptions: SavedObjectFindOptionsKueryNode; - subCaseOptions?: SavedObjectFindOptionsKueryNode; - }): Promise; - findCasesGroupedByID(args: { - client: SavedObjectsClientContract; - caseOptions: SavedObjectFindOptionsKueryNode; - subCaseOptions?: SavedObjectFindOptionsKueryNode; - }): Promise; -} - -export class CaseService implements CaseServiceSetup { - constructor( - private readonly log: Logger, - private readonly authentication?: SecurityPluginSetup['authc'] - ) {} - - /** - * Returns a map of all cases combined with their sub cases if they are collections. - */ - public async findCasesGroupedByID({ - client, - caseOptions, - subCaseOptions, - }: { - client: SavedObjectsClientContract; - caseOptions: FindCaseOptions; - subCaseOptions?: SavedObjectFindOptionsKueryNode; - }): Promise { - const cases = await this.findCases({ - client, - options: caseOptions, - }); - - const subCasesResp = ENABLE_CASE_CONNECTOR - ? await this.findSubCasesGroupByCase({ - client, - options: subCaseOptions, - ids: cases.saved_objects - .filter((caseInfo) => caseInfo.attributes.type === CaseType.collection) - .map((caseInfo) => caseInfo.id), - }) - : { subCasesMap: new Map(), page: 0, perPage: 0 }; - - const casesMap = cases.saved_objects.reduce((accMap, caseInfo) => { - const subCasesForCase = subCasesResp.subCasesMap.get(caseInfo.id); - - /** - * If this case is an individual add it to the return map - * If it is a collection and it has sub cases add it to the return map - * If it is a collection and it does not have sub cases, check and see if we're filtering on a status, - * if we're filtering on a status then exclude the empty collection from the results - * if we're not filtering on a status then include the empty collection (that way we can display all the collections - * when the UI isn't doing any filtering) - */ - if ( - caseInfo.attributes.type === CaseType.individual || - subCasesForCase !== undefined || - !caseOptions.status - ) { - accMap.set(caseInfo.id, { case: caseInfo, subCases: subCasesForCase }); - } - return accMap; - }, new Map()); - - /** - * One potential optimization here is to get all comment stats for individual cases, parent cases, and sub cases - * in a single request. This can be done because comments that are for sub cases have a reference to both the sub case - * and the parent. The associationType field allows us to determine which type of case the comment is attached to. - * - * So we could use the ids for all the valid cases (individual cases and parents with sub cases) to grab everything. - * Once we have it we can build the maps. - * - * Currently we get all comment stats for all sub cases in one go and we get all comment stats for cases (individual and parent) - * in another request (the one below this comment). - */ - const totalCommentsForCases = await this.getCaseCommentStats({ - client, - ids: Array.from(casesMap.keys()), - associationType: AssociationType.case, - }); - - const casesWithComments = new Map(); - for (const [id, caseInfo] of casesMap.entries()) { - casesWithComments.set( - id, - flattenCaseSavedObject({ - savedObject: caseInfo.case, - totalComment: totalCommentsForCases.commentTotals.get(id) ?? 0, - totalAlerts: totalCommentsForCases.alertTotals.get(id) ?? 0, - subCases: caseInfo.subCases, - }) - ); - } - - return { - casesMap: casesWithComments, - page: cases.page, - perPage: cases.per_page, - total: cases.total, - }; - } - - /** - * Retrieves the number of cases that exist with a given status (open, closed, etc). - * This also counts sub cases. Parent cases are excluded from the statistics. - */ - public async findCaseStatusStats({ - client, - caseOptions, - subCaseOptions, - }: { - client: SavedObjectsClientContract; - caseOptions: SavedObjectFindOptionsKueryNode; - subCaseOptions?: SavedObjectFindOptionsKueryNode; - }): Promise { - const casesStats = await this.findCases({ - client, - 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 - * the ids for the parent cases that match those filters. Then we use those IDS to count how many - * sub cases those parents have to calculate the total amount of cases that are open, closed, or in-progress. - * - * Another solution would be to store ALL filterable fields on both a case and sub case. That we could do a single - * query for each type to calculate the totals using the filters. This has drawbacks though: - * - * We'd have to sync up the parent case's editable attributes with the sub case any time they were change to avoid - * them getting out of sync and causing issues when we do these types of stats aggregations. This would result in a lot - * of update requests if the user is editing their case details often. Which could potentially cause conflict failures. - * - * Another option is to prevent the ability from update the parent case's details all together once it's created. A user - * could instead modify the sub case details directly. This could be weird though because individual sub cases for the same - * parent would have different titles, tags, etc. - * - * Another potential issue with this approach is when you push a case and all its sub case information. If the sub cases - * don't have the same title and tags, we'd need to account for that as well. - */ - const cases = await this.findCases({ - client, - options: { - ...caseOptions, - fields: [caseTypeField], - page: 1, - perPage: casesStats.total, - }, - }); - - const caseIds = cases.saved_objects - .filter((caseInfo) => caseInfo.attributes.type === CaseType.collection) - .map((caseInfo) => caseInfo.id); - - let subCasesTotal = 0; - - if (ENABLE_CASE_CONNECTOR && subCaseOptions) { - subCasesTotal = await this.findSubCaseStatusStats({ - client, - options: cloneDeep(subCaseOptions), - ids: caseIds, - }); - } - - const total = - cases.saved_objects.filter((caseInfo) => caseInfo.attributes.type !== CaseType.collection) - .length + subCasesTotal; - - return total; - } - - /** - * Retrieves the comments attached to a case or sub case. - */ - public async getCommentsByAssociation({ - client, - id, - associationType, - options, - }: FindCommentsByAssociationArgs): Promise> { - if (associationType === AssociationType.subCase) { - return this.getAllSubCaseComments({ - client, - id, - options, - }); - } else { - return this.getAllCaseComments({ - client, - id, - options, - }); - } - } - - /** - * Returns the number of total comments and alerts for a case (or sub case) - */ - public async getCaseCommentStats({ - client, - ids, - associationType, - }: { - client: SavedObjectsClientContract; - ids: string[]; - associationType: AssociationType; - }): Promise { - if (ids.length <= 0) { - return { - commentTotals: new Map(), - alertTotals: new Map(), - }; - } - - const refType = - associationType === AssociationType.case ? CASE_SAVED_OBJECT : SUB_CASE_SAVED_OBJECT; - - const allComments = await Promise.all( - ids.map((id) => - this.getCommentsByAssociation({ - client, - associationType, - id, - options: { page: 1, perPage: 1 }, - }) - ) - ); - - const alerts = await this.getCommentsByAssociation({ - client, - associationType, - 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 - ), - ]), - }, - }); - - const getID = (comments: SavedObjectsFindResponse) => { - return comments.saved_objects.length > 0 - ? comments.saved_objects[0].references.find((ref) => ref.type === refType)?.id - : undefined; - }; - - const groupedComments = allComments.reduce((acc, comments) => { - const id = getID(comments); - if (id) { - acc.set(id, comments.total); - } - return acc; - }, new Map()); - - const groupedAlerts = groupTotalAlertsByID({ comments: alerts }); - return { commentTotals: groupedComments, alertTotals: groupedAlerts }; - } - - /** - * Returns all the sub cases for a set of case IDs. Comment statistics are also returned. - */ - public async findSubCasesGroupByCase({ - client, - options, - ids, - }: { - client: SavedObjectsClientContract; - options?: SavedObjectFindOptionsKueryNode; - ids: string[]; - }): Promise { - const getCaseID = (subCase: SavedObjectsFindResult): string | undefined => { - return subCase.references.length > 0 ? subCase.references[0].id : undefined; - }; - - const emptyResponse = { - subCasesMap: new Map(), - page: 0, - perPage: 0, - total: 0, - }; - - if (!options) { - return emptyResponse; - } - - if (ids.length <= 0) { - return emptyResponse; - } - - const subCases = await this.findSubCases({ - client, - options: { - ...options, - hasReference: ids.map((id) => { - return { - id, - type: CASE_SAVED_OBJECT, - }; - }), - }, - }); - - const subCaseComments = await this.getCaseCommentStats({ - client, - ids: subCases.saved_objects.map((subCase) => subCase.id), - associationType: AssociationType.subCase, - }); - - const subCasesMap = subCases.saved_objects.reduce((accMap, subCase) => { - const parentCaseID = getCaseID(subCase); - if (parentCaseID) { - const subCaseFromMap = accMap.get(parentCaseID); - - if (subCaseFromMap === undefined) { - const subCasesForID = [ - flattenSubCaseSavedObject({ - savedObject: subCase, - totalComment: subCaseComments.commentTotals.get(subCase.id) ?? 0, - totalAlerts: subCaseComments.alertTotals.get(subCase.id) ?? 0, - }), - ]; - accMap.set(parentCaseID, subCasesForID); - } else { - subCaseFromMap.push( - flattenSubCaseSavedObject({ - savedObject: subCase, - totalComment: subCaseComments.commentTotals.get(subCase.id) ?? 0, - totalAlerts: subCaseComments.alertTotals.get(subCase.id) ?? 0, - }) - ); - } - } - return accMap; - }, new Map()); - - return { subCasesMap, page: subCases.page, perPage: subCases.per_page, total: subCases.total }; - } - - /** - * Calculates the number of sub cases for a given set of options for a set of case IDs. - */ - public async findSubCaseStatusStats({ - client, - options, - ids, - }: FindSubCasesStatusStats): Promise { - if (ids.length <= 0) { - return 0; - } - - const subCases = await this.findSubCases({ - client, - options: { - ...options, - page: 1, - perPage: 1, - fields: [], - hasReference: ids.map((id) => { - return { - id, - type: CASE_SAVED_OBJECT, - }; - }), - }, - }); - - return subCases.total; - } - - public async createSubCase({ - client, - createdAt, - caseId, - createdBy, - }: CreateSubCaseArgs): Promise> { - try { - this.log.debug(`Attempting to POST a new sub case`); - return client.create(SUB_CASE_SAVED_OBJECT, transformNewSubCase({ createdAt, createdBy }), { - references: [ - { - type: CASE_SAVED_OBJECT, - name: `associated-${CASE_SAVED_OBJECT}`, - id: caseId, - }, - ], - }); - } catch (error) { - this.log.error(`Error on POST a new sub case for id ${caseId}: ${error}`); - throw error; - } - } - - public async getMostRecentSubCase(client: SavedObjectsClientContract, caseId: string) { - try { - this.log.debug(`Attempting to find most recent sub case for caseID: ${caseId}`); - const subCases: SavedObjectsFindResponse = await client.find({ - perPage: 1, - sortField: 'created_at', - sortOrder: 'desc', - type: SUB_CASE_SAVED_OBJECT, - hasReference: { type: CASE_SAVED_OBJECT, id: caseId }, - }); - if (subCases.saved_objects.length <= 0) { - return; - } - - return subCases.saved_objects[0]; - } catch (error) { - this.log.error(`Error finding the most recent sub case for case: ${caseId}: ${error}`); - throw error; - } - } - - public async deleteSubCase(client: SavedObjectsClientContract, id: string) { - try { - this.log.debug(`Attempting to DELETE sub case ${id}`); - return await client.delete(SUB_CASE_SAVED_OBJECT, id); - } catch (error) { - this.log.error(`Error on DELETE sub case ${id}: ${error}`); - throw error; - } - } - - public async deleteCase({ client, id: caseId }: GetCaseArgs) { - try { - this.log.debug(`Attempting to DELETE case ${caseId}`); - return await client.delete(CASE_SAVED_OBJECT, caseId); - } catch (error) { - this.log.error(`Error on DELETE case ${caseId}: ${error}`); - throw error; - } - } - public async deleteComment({ client, commentId }: GetCommentArgs) { - try { - this.log.debug(`Attempting to GET comment ${commentId}`); - return await client.delete(CASE_COMMENT_SAVED_OBJECT, commentId); - } catch (error) { - this.log.error(`Error on GET comment ${commentId}: ${error}`); - throw error; - } - } - public async getCase({ - client, - id: caseId, - }: GetCaseArgs): Promise> { - try { - this.log.debug(`Attempting to GET case ${caseId}`); - return await client.get(CASE_SAVED_OBJECT, caseId); - } catch (error) { - this.log.error(`Error on GET case ${caseId}: ${error}`); - throw error; - } - } - public async getSubCase({ client, id }: GetCaseArgs): Promise> { - try { - this.log.debug(`Attempting to GET sub case ${id}`); - return await client.get(SUB_CASE_SAVED_OBJECT, id); - } catch (error) { - this.log.error(`Error on GET sub case ${id}: ${error}`); - throw error; - } - } - - public async getSubCases({ - client, - ids, - }: GetSubCasesArgs): Promise> { - try { - this.log.debug(`Attempting to GET sub cases ${ids.join(', ')}`); - return await client.bulkGet(ids.map((id) => ({ type: SUB_CASE_SAVED_OBJECT, id }))); - } catch (error) { - this.log.error(`Error on GET cases ${ids.join(', ')}: ${error}`); - throw error; - } - } - - public async getCases({ - client, - caseIds, - }: GetCasesArgs): Promise> { - try { - this.log.debug(`Attempting to GET cases ${caseIds.join(', ')}`); - return await client.bulkGet( - caseIds.map((caseId) => ({ type: CASE_SAVED_OBJECT, id: caseId })) - ); - } catch (error) { - this.log.error(`Error on GET cases ${caseIds.join(', ')}: ${error}`); - throw error; - } - } - public async getComment({ - client, - commentId, - }: GetCommentArgs): Promise> { - try { - this.log.debug(`Attempting to GET comment ${commentId}`); - return await client.get(CASE_COMMENT_SAVED_OBJECT, commentId); - } catch (error) { - this.log.error(`Error on GET comment ${commentId}: ${error}`); - throw error; - } - } - - public async findCases({ - client, - options, - }: FindCasesArgs): Promise> { - try { - this.log.debug(`Attempting to find cases`); - return await client.find({ - sortField: defaultSortField, - ...cloneDeep(options), - type: CASE_SAVED_OBJECT, - }); - } catch (error) { - this.log.error(`Error on find cases: ${error}`); - throw error; - } - } - - public async findSubCases({ - client, - options, - }: FindCasesArgs): Promise> { - try { - this.log.debug(`Attempting to find sub cases`); - // if the page or perPage options are set then respect those instead of trying to - // grab all sub cases - if (options?.page !== undefined || options?.perPage !== undefined) { - return client.find({ - sortField: defaultSortField, - ...cloneDeep(options), - type: SUB_CASE_SAVED_OBJECT, - }); - } - - const stats = await client.find({ - fields: [], - page: 1, - perPage: 1, - sortField: defaultSortField, - ...cloneDeep(options), - type: SUB_CASE_SAVED_OBJECT, - }); - return client.find({ - page: 1, - perPage: stats.total, - sortField: defaultSortField, - ...cloneDeep(options), - type: SUB_CASE_SAVED_OBJECT, - }); - } catch (error) { - this.log.error(`Error on find sub cases: ${error}`); - throw error; - } - } - - /** - * Find sub cases using a collection's ID. This would try to retrieve the maximum amount of sub cases - * by default. - * - * @param id the saved object ID of the parent collection to find sub cases for. - */ - public async findSubCasesByCaseId({ - client, - ids, - options, - }: FindSubCasesByIDArgs): Promise> { - if (ids.length <= 0) { - return { - total: 0, - saved_objects: [], - page: options?.page ?? defaultPage, - per_page: options?.perPage ?? defaultPerPage, - }; - } - - try { - this.log.debug(`Attempting to GET sub cases for case collection id ${ids.join(', ')}`); - return this.findSubCases({ - client, - options: { - ...options, - hasReference: ids.map((id) => ({ - type: CASE_SAVED_OBJECT, - id, - })), - }, - }); - } catch (error) { - this.log.error( - `Error on GET all sub cases for case collection id ${ids.join(', ')}: ${error}` - ); - throw error; - } - } - - private asArray(id: string | string[] | undefined): string[] { - if (id === undefined) { - return []; - } else if (Array.isArray(id)) { - return id; - } else { - return [id]; - } - } - - private async getAllComments({ - client, - id, - options, - }: FindCommentsArgs): Promise> { - try { - this.log.debug(`Attempting to GET all comments for id ${JSON.stringify(id)}`); - if (options?.page !== undefined || options?.perPage !== undefined) { - return client.find({ - type: CASE_COMMENT_SAVED_OBJECT, - sortField: defaultSortField, - ...cloneDeep(options), - }); - } - // get the total number of comments that are in ES then we'll grab them all in one go - const stats = await client.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 client.find({ - type: CASE_COMMENT_SAVED_OBJECT, - page: 1, - perPage: stats.total, - sortField: defaultSortField, - ...cloneDeep(options), - }); - } catch (error) { - this.log.error(`Error on GET all comments for ${JSON.stringify(id)}: ${error}`); - throw error; - } - } - - /** - * Default behavior is to retrieve all comments that adhere to a given filter (if one is included). - * to override this pass in the either the page or perPage options. - * - * @param includeSubCaseComments is a flag to indicate that sub case comments should be included as well, by default - * sub case comments are excluded. If the `filter` field is included in the options, it will override this behavior - */ - public async getAllCaseComments({ - client, - id, - options, - includeSubCaseComments = false, - }: FindCaseCommentsArgs): Promise> { - try { - const refs = this.asArray(id).map((caseID) => ({ type: CASE_SAVED_OBJECT, id: caseID })); - if (refs.length <= 0) { - return { - saved_objects: [], - total: 0, - per_page: options?.perPage ?? defaultPerPage, - page: options?.page ?? defaultPage, - }; - } - - let filter: KueryNode | undefined; - if (!includeSubCaseComments) { - // if other filters were passed in then combine them to filter out sub case comments - const associationFilter = nodeBuilder.is( - `${CASE_COMMENT_SAVED_OBJECT}.attributes.associationType`, - AssociationType.case - ); - - filter = - options?.filter != null - ? nodeBuilder.and([options?.filter, associationFilter]) - : associationFilter; - } - - this.log.debug(`Attempting to GET all comments for case caseID ${JSON.stringify(id)}`); - return this.getAllComments({ - client, - id, - options: { - hasReferenceOperator: 'OR', - hasReference: refs, - filter, - ...options, - }, - }); - } catch (error) { - this.log.error(`Error on GET all comments for case ${JSON.stringify(id)}: ${error}`); - throw error; - } - } - - public async getAllSubCaseComments({ - client, - id, - options, - }: FindSubCaseCommentsArgs): Promise> { - try { - const refs = this.asArray(id).map((caseID) => ({ type: SUB_CASE_SAVED_OBJECT, id: caseID })); - if (refs.length <= 0) { - return { - saved_objects: [], - total: 0, - per_page: options?.perPage ?? defaultPerPage, - page: options?.page ?? defaultPage, - }; - } - - this.log.debug(`Attempting to GET all comments for sub case caseID ${JSON.stringify(id)}`); - return this.getAllComments({ - client, - id, - options: { - hasReferenceOperator: 'OR', - hasReference: refs, - ...options, - }, - }); - } catch (error) { - this.log.error(`Error on GET all comments for sub case ${JSON.stringify(id)}: ${error}`); - throw error; - } - } - - public async getReporters({ client }: ClientArgs) { - try { - this.log.debug(`Attempting to GET all reporters`); - return await readReporters({ client }); - } catch (error) { - this.log.error(`Error on GET all reporters: ${error}`); - throw error; - } - } - public async getTags({ client }: ClientArgs) { - try { - this.log.debug(`Attempting to GET all cases`); - return await readTags({ client }); - } catch (error) { - this.log.error(`Error on GET cases: ${error}`); - throw error; - } - } - - public getUser({ request }: GetUserArgs) { - try { - this.log.debug(`Attempting to authenticate a user`); - if (this.authentication != null) { - const user = this.authentication.getCurrentUser(request); - if (!user) { - return { - username: null, - full_name: null, - email: null, - }; - } - return user; - } - return { - username: null, - full_name: null, - email: null, - }; - } catch (error) { - this.log.error(`Error on GET cases: ${error}`); - throw error; - } - } - public async postNewCase({ client, attributes }: PostCaseArgs) { - try { - this.log.debug(`Attempting to POST a new case`); - return await client.create(CASE_SAVED_OBJECT, { ...attributes }); - } catch (error) { - this.log.error(`Error on POST a new case: ${error}`); - throw error; - } - } - public async postNewComment({ client, attributes, references }: PostCommentArgs) { - try { - this.log.debug(`Attempting to POST a new comment`); - return await client.create(CASE_COMMENT_SAVED_OBJECT, attributes, { references }); - } catch (error) { - this.log.error(`Error on POST a new comment: ${error}`); - throw error; - } - } - public async patchCase({ client, caseId, updatedAttributes, version }: PatchCaseArgs) { - try { - this.log.debug(`Attempting to UPDATE case ${caseId}`); - return await client.update(CASE_SAVED_OBJECT, caseId, { ...updatedAttributes }, { version }); - } catch (error) { - this.log.error(`Error on UPDATE case ${caseId}: ${error}`); - throw error; - } - } - public async patchCases({ client, cases }: PatchCasesArgs) { - try { - this.log.debug(`Attempting to UPDATE case ${cases.map((c) => c.caseId).join(', ')}`); - return await client.bulkUpdate( - cases.map((c) => ({ - type: CASE_SAVED_OBJECT, - id: c.caseId, - attributes: c.updatedAttributes, - version: c.version, - })) - ); - } catch (error) { - this.log.error(`Error on UPDATE case ${cases.map((c) => c.caseId).join(', ')}: ${error}`); - throw error; - } - } - public async patchComment({ client, commentId, updatedAttributes, version }: UpdateCommentArgs) { - try { - this.log.debug(`Attempting to UPDATE comment ${commentId}`); - return await client.update( - CASE_COMMENT_SAVED_OBJECT, - commentId, - { - ...updatedAttributes, - }, - { version } - ); - } catch (error) { - this.log.error(`Error on UPDATE comment ${commentId}: ${error}`); - throw error; - } - } - public async patchComments({ client, comments }: PatchComments) { - try { - this.log.debug( - `Attempting to UPDATE comments ${comments.map((c) => c.commentId).join(', ')}` - ); - return await client.bulkUpdate( - comments.map((c) => ({ - type: CASE_COMMENT_SAVED_OBJECT, - id: c.commentId, - attributes: c.updatedAttributes, - version: c.version, - })) - ); - } catch (error) { - this.log.error( - `Error on UPDATE comments ${comments.map((c) => c.commentId).join(', ')}: ${error}` - ); - throw error; - } - } - public async patchSubCase({ client, subCaseId, updatedAttributes, version }: PatchSubCase) { - try { - this.log.debug(`Attempting to UPDATE sub case ${subCaseId}`); - return await client.update( - SUB_CASE_SAVED_OBJECT, - subCaseId, - { ...updatedAttributes }, - { version } - ); - } catch (error) { - this.log.error(`Error on UPDATE sub case ${subCaseId}: ${error}`); - throw error; - } - } - - public async patchSubCases({ client, subCases }: PatchSubCases) { - try { - this.log.debug( - `Attempting to UPDATE sub case ${subCases.map((c) => c.subCaseId).join(', ')}` - ); - return await client.bulkUpdate( - subCases.map((c) => ({ - type: SUB_CASE_SAVED_OBJECT, - id: c.subCaseId, - attributes: c.updatedAttributes, - version: c.version, - })) - ); - } catch (error) { - this.log.error( - `Error on UPDATE sub case ${subCases.map((c) => c.subCaseId).join(', ')}: ${error}` - ); - throw error; - } - } + soClient: SavedObjectsClientContract; } diff --git a/x-pack/plugins/cases/server/services/mocks.ts b/x-pack/plugins/cases/server/services/mocks.ts index 51eb0bbb1a7e43..77129e45348b1f 100644 --- a/x-pack/plugins/cases/server/services/mocks.ts +++ b/x-pack/plugins/cases/server/services/mocks.ts @@ -7,16 +7,16 @@ import { AlertServiceContract, - CaseConfigureServiceSetup, - CaseServiceSetup, - CaseUserActionServiceSetup, - ConnectorMappingsServiceSetup, + CaseConfigureService, + CaseService, + CaseUserActionService, + ConnectorMappingsService, } from '.'; -export type CaseServiceMock = jest.Mocked; -export type CaseConfigureServiceMock = jest.Mocked; -export type ConnectorMappingsServiceMock = jest.Mocked; -export type CaseUserActionServiceMock = jest.Mocked; +export type CaseServiceMock = jest.Mocked; +export type CaseConfigureServiceMock = jest.Mocked; +export type ConnectorMappingsServiceMock = jest.Mocked; +export type CaseUserActionServiceMock = jest.Mocked; export type AlertServiceMock = jest.Mocked; export const createCaseServiceMock = (): CaseServiceMock => ({ 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 192ab9341e4ee8..0b65657092469b 100644 --- a/x-pack/plugins/cases/server/services/user_actions/index.ts +++ b/x-pack/plugins/cases/server/services/user_actions/index.ts @@ -5,12 +5,7 @@ * 2.0. */ -import { - SavedObjectsFindResponse, - Logger, - SavedObjectsBulkResponse, - SavedObjectReference, -} from 'kibana/server'; +import { Logger, SavedObjectReference } from 'kibana/server'; import { CaseUserActionAttributes } from '../../../common/api'; import { @@ -34,52 +29,44 @@ interface PostCaseUserActionArgs extends ClientArgs { actions: UserActionItem[]; } -export interface CaseUserActionServiceSetup { - getUserActions( - args: GetCaseUserActionArgs - ): Promise>; - postUserActions( - args: PostCaseUserActionArgs - ): Promise>; -} - export class CaseUserActionService { constructor(private readonly log: Logger) {} - public setup = async (): Promise => ({ - getUserActions: async ({ client, caseId, subCaseId }: GetCaseUserActionArgs) => { - try { - const id = subCaseId ?? caseId; - const type = subCaseId ? SUB_CASE_SAVED_OBJECT : CASE_SAVED_OBJECT; - const caseUserActionInfo = await client.find({ - type: CASE_USER_ACTION_SAVED_OBJECT, - fields: [], - hasReference: { type, id }, - page: 1, - perPage: 1, - }); - return await client.find({ - type: CASE_USER_ACTION_SAVED_OBJECT, - hasReference: { type, id }, - page: 1, - perPage: caseUserActionInfo.total, - sortField: 'action_at', - sortOrder: 'asc', - }); - } catch (error) { - this.log.error(`Error on GET case user action case id: ${caseId}: ${error}`); - throw error; - } - }, - postUserActions: async ({ client, actions }: PostCaseUserActionArgs) => { - try { - this.log.debug(`Attempting to POST a new case user action`); - return await client.bulkCreate( - actions.map((action) => ({ type: CASE_USER_ACTION_SAVED_OBJECT, ...action })) - ); - } catch (error) { - this.log.error(`Error on POST a new case user action: ${error}`); - throw error; - } - }, - }); + + public async getAll({ soClient, caseId, subCaseId }: GetCaseUserActionArgs) { + try { + const id = subCaseId ?? caseId; + const type = subCaseId ? SUB_CASE_SAVED_OBJECT : CASE_SAVED_OBJECT; + const caseUserActionInfo = await soClient.find({ + type: CASE_USER_ACTION_SAVED_OBJECT, + fields: [], + hasReference: { type, id }, + page: 1, + perPage: 1, + }); + + return await soClient.find({ + type: CASE_USER_ACTION_SAVED_OBJECT, + hasReference: { type, id }, + page: 1, + perPage: caseUserActionInfo.total, + sortField: 'action_at', + sortOrder: 'asc', + }); + } catch (error) { + this.log.error(`Error on GET case user action case id: ${caseId}: ${error}`); + throw error; + } + } + + public async bulkCreate({ soClient, actions }: PostCaseUserActionArgs) { + try { + this.log.debug(`Attempting to POST a new case user action`); + return await soClient.bulkCreate( + actions.map((action) => ({ type: CASE_USER_ACTION_SAVED_OBJECT, ...action })) + ); + } catch (error) { + this.log.error(`Error on POST a new case user action: ${error}`); + throw error; + } + } }