"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AttachmentGetter = void 0; var _common = require("@kbn/files-plugin/common"); var _api = require("../../../../common/api"); var _attachments = require("../../../common/types/attachments"); var _constants = require("../../../../common/constants"); var _utils = require("../../../client/utils"); var _domain = require("../../../../common/types/domain"); var _so_references = require("../../so_references"); var _partitioning = require("../../../common/partitioning"); var _references = require("../../../common/references"); var _utils2 = require("../../../common/utils"); /* * 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. */ class AttachmentGetter { constructor(context) { this.context = context; } async bulkGet(attachmentIds) { try { this.context.log.debug(`Attempting to retrieve attachments with ids: ${attachmentIds.join()}`); const response = await this.context.unsecuredSavedObjectsClient.bulkGet(attachmentIds.map(id => ({ id, type: _constants.CASE_COMMENT_SAVED_OBJECT }))); return this.transformAndDecodeBulkGetResponse(response); } catch (error) { this.context.log.error(`Error retrieving attachments with ids ${attachmentIds.join()}: ${error}`); throw error; } } transformAndDecodeBulkGetResponse(response) { const validatedAttachments = []; for (const so of response.saved_objects) { if ((0, _utils2.isSOError)(so)) { // Forcing the type here even though it is an error. The caller is responsible for // determining what to do with the errors // TODO: we should fix the return type of this bulkGet so that it can return errors validatedAttachments.push(so); } else { const transformedAttachment = (0, _so_references.injectAttachmentAttributesAndHandleErrors)(so, this.context.persistableStateAttachmentTypeRegistry); const validatedAttributes = (0, _api.decodeOrThrow)(_attachments.AttachmentTransformedAttributesRt)(transformedAttachment.attributes); validatedAttachments.push(Object.assign(transformedAttachment, { attributes: validatedAttributes })); } } return Object.assign(response, { saved_objects: validatedAttachments }); } async getAttachmentIdsForCases({ caseIds }) { try { this.context.log.debug(`Attempting to retrieve attachments associated with cases: [${caseIds}]`); // We are intentionally not adding the type here because we only want to interact with the id and this function // should not use the attributes const finder = this.context.unsecuredSavedObjectsClient.createPointInTimeFinder({ type: _constants.CASE_COMMENT_SAVED_OBJECT, hasReference: caseIds.map(id => ({ id, type: _constants.CASE_SAVED_OBJECT })), sortField: 'created_at', sortOrder: 'asc', /** * We only care about the ids so to reduce the data returned we should limit the fields in the response. Core * doesn't support retrieving no fields (id would always be returned anyway) so to limit it we'll only request * the owner even though we don't need it. */ fields: ['owner'], perPage: _constants.MAX_DOCS_PER_PAGE }); const ids = []; for await (const attachmentSavedObject of finder.find()) { ids.push(...attachmentSavedObject.saved_objects.map(attachment => attachment.id)); } return ids; } catch (error) { this.context.log.error(`Error retrieving attachments associated with cases: [${caseIds}]: ${error}`); throw error; } } /** * Retrieves all the alerts attached to a case. */ async getAllAlertsAttachToCase({ caseId, filter }) { try { this.context.log.debug(`Attempting to GET all alerts for case id ${caseId}`); const alertsFilter = (0, _utils.buildFilter)({ filters: [_domain.AttachmentType.alert], field: 'type', operator: 'or', type: _constants.CASE_COMMENT_SAVED_OBJECT }); const combinedFilter = (0, _utils.combineFilters)([alertsFilter, filter]); const finder = this.context.unsecuredSavedObjectsClient.createPointInTimeFinder({ type: _constants.CASE_COMMENT_SAVED_OBJECT, hasReference: { type: _constants.CASE_SAVED_OBJECT, id: caseId }, sortField: 'created_at', sortOrder: 'asc', filter: combinedFilter, perPage: _constants.MAX_DOCS_PER_PAGE }); let result = []; for await (const userActionSavedObject of finder.find()) { result = result.concat(AttachmentGetter.decodeAlerts(userActionSavedObject)); } return result; } catch (error) { this.context.log.error(`Error on GET all alerts for case id ${caseId}: ${error}`); throw error; } } static decodeAlerts(response) { return response.saved_objects.map(so => { const validatedAttributes = (0, _api.decodeOrThrow)(_domain.AlertAttachmentAttributesRt)(so.attributes); return Object.assign(so, { attributes: validatedAttributes }); }); } /** * Retrieves all the alerts attached to a case. */ async getAllAlertIds({ caseId }) { try { var _res$aggregations$ale, _res$aggregations; this.context.log.debug(`Attempting to GET all alerts ids for case id ${caseId}`); const alertsFilter = (0, _utils.buildFilter)({ filters: [_domain.AttachmentType.alert], field: 'type', operator: 'or', type: _constants.CASE_COMMENT_SAVED_OBJECT }); const res = await this.context.unsecuredSavedObjectsClient.find({ type: _constants.CASE_COMMENT_SAVED_OBJECT, hasReference: { type: _constants.CASE_SAVED_OBJECT, id: caseId }, sortField: 'created_at', sortOrder: 'asc', filter: alertsFilter, perPage: 0, aggs: { alertIds: { terms: { field: `${_constants.CASE_COMMENT_SAVED_OBJECT}.attributes.alertId`, size: _constants.MAX_ALERTS_PER_CASE } } } }); const alertIds = (_res$aggregations$ale = (_res$aggregations = res.aggregations) === null || _res$aggregations === void 0 ? void 0 : _res$aggregations.alertIds.buckets.map(bucket => bucket.key)) !== null && _res$aggregations$ale !== void 0 ? _res$aggregations$ale : []; return new Set(alertIds); } catch (error) { this.context.log.error(`Error on GET all alerts ids for case id ${caseId}: ${error}`); throw error; } } async get({ attachmentId }) { try { this.context.log.debug(`Attempting to GET attachment ${attachmentId}`); const res = await this.context.unsecuredSavedObjectsClient.get(_constants.CASE_COMMENT_SAVED_OBJECT, attachmentId); const transformedAttachment = (0, _so_references.injectAttachmentSOAttributesFromRefs)(res, this.context.persistableStateAttachmentTypeRegistry); const validatedAttributes = (0, _api.decodeOrThrow)(_attachments.AttachmentTransformedAttributesRt)(transformedAttachment.attributes); return Object.assign(transformedAttachment, { attributes: validatedAttributes }); } catch (error) { this.context.log.error(`Error on GET attachment ${attachmentId}: ${error}`); throw error; } } async getCaseCommentStats({ caseIds }) { var _res$aggregations$ref, _res$aggregations2; if (caseIds.length <= 0) { return new Map(); } const res = await this.context.unsecuredSavedObjectsClient.find({ hasReference: caseIds.map(id => ({ type: _constants.CASE_SAVED_OBJECT, id })), hasReferenceOperator: 'OR', type: _constants.CASE_COMMENT_SAVED_OBJECT, perPage: 0, aggs: AttachmentGetter.buildCommentStatsAggs(caseIds) }); return (_res$aggregations$ref = (_res$aggregations2 = res.aggregations) === null || _res$aggregations2 === void 0 ? void 0 : _res$aggregations2.references.caseIds.buckets.reduce((acc, idBucket) => { acc.set(idBucket.key, { userComments: idBucket.reverse.comments.doc_count, alerts: idBucket.reverse.alerts.value }); return acc; }, new Map())) !== null && _res$aggregations$ref !== void 0 ? _res$aggregations$ref : new Map(); } static buildCommentStatsAggs(ids) { return { references: { nested: { path: `${_constants.CASE_COMMENT_SAVED_OBJECT}.references` }, aggregations: { caseIds: { terms: { field: `${_constants.CASE_COMMENT_SAVED_OBJECT}.references.id`, size: ids.length }, aggregations: { reverse: { reverse_nested: {}, aggregations: { alerts: { cardinality: { field: `${_constants.CASE_COMMENT_SAVED_OBJECT}.attributes.alertId` } }, comments: { filter: { term: { [`${_constants.CASE_COMMENT_SAVED_OBJECT}.attributes.type`]: _domain.AttachmentType.user } } } } } } } } } }; } async getFileAttachments({ caseId, fileIds }) { try { this.context.log.debug('Attempting to find file attachments'); /** * This is making a big assumption that a single file service saved object can only be associated within a single * case. If a single file can be attached to multiple cases it will complicate deleting a file. * * The file's metadata would have to contain all case ids and deleting a file would need to removing a case id from * array instead of deleting the entire saved object in the situation where the file is attached to multiple cases. */ const references = fileIds.map(id => ({ id, type: _common.FILE_SO_TYPE })); /** * In the event that we add the ability to attach a file to a case that has already been uploaded we'll run into a * scenario where a single file id could be associated with multiple case attachments. So we need * to retrieve them all. */ const finder = this.context.unsecuredSavedObjectsClient.createPointInTimeFinder({ type: _constants.CASE_COMMENT_SAVED_OBJECT, hasReference: references, sortField: 'created_at', sortOrder: 'asc', perPage: _constants.MAX_DOCS_PER_PAGE }); const foundAttachments = []; for await (const attachmentSavedObjects of finder.find()) { foundAttachments.push(...this.transformAndDecodeFileAttachments(attachmentSavedObjects)); } const [validFileAttachments, invalidFileAttachments] = (0, _partitioning.partitionByCaseAssociation)(caseId, foundAttachments); this.logInvalidFileAssociations(invalidFileAttachments, fileIds, caseId); return validFileAttachments; } catch (error) { this.context.log.error(`Error retrieving file attachments file ids: ${fileIds}: ${error}`); throw error; } } transformAndDecodeFileAttachments(response) { return response.saved_objects.map(so => { const transformedFileAttachment = (0, _so_references.injectAttachmentSOAttributesFromRefs)(so, this.context.persistableStateAttachmentTypeRegistry); const validatedAttributes = (0, _api.decodeOrThrow)(_attachments.AttachmentTransformedAttributesRt)(transformedFileAttachment.attributes); return Object.assign(transformedFileAttachment, { attributes: validatedAttributes }); }); } logInvalidFileAssociations(attachments, fileIds, targetCaseId) { const caseIds = []; for (const attachment of attachments) { const caseRefId = (0, _references.getCaseReferenceId)(attachment.references); if (caseRefId != null) { caseIds.push(caseRefId); } } if (caseIds.length > 0) { this.context.log.warn(`Found files associated to cases outside of request: ${caseIds} file ids: ${fileIds} target case id: ${targetCaseId}`); } } } exports.AttachmentGetter = AttachmentGetter;