"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.combineAuthorizedAndOwnerFilter = exports.buildRangeFilter = exports.buildFilter = exports.buildAssigneesFilter = exports.arraysDifference = exports.NodeBuilderOperators = void 0; exports.combineFilters = combineFilters; exports.getCaseToUpdate = exports.getAlertIds = exports.decodeCommentRequest = exports.convertSortField = exports.constructSearch = exports.constructQueryOptions = void 0; exports.stringToKueryNode = stringToKueryNode; var _boom = require("@hapi/boom"); var _lodash = require("lodash"); var _fastDeepEqual = _interopRequireDefault(require("fast-deep-equal")); var _uuid = require("uuid"); var _esQuery = require("@kbn/es-query"); var _namespace = require("@kbn/spaces-plugin/server/lib/utils/namespace"); var _domain = require("../../common/types/domain"); var _api = require("../../common/api"); var _constants = require("../../common/constants"); var _attachments = require("../../common/utils/attachments"); var _utils = require("../authorization/utils"); var _constants2 = require("../common/constants"); 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. */ // TODO: I think we can remove most of this function since we're using a different excess const decodeCommentRequest = (comment, externalRefRegistry) => { if ((0, _utils2.isCommentRequestTypeUser)(comment)) { (0, _api.decodeWithExcessOrThrow)(_domain.UserCommentAttachmentPayloadRt)(comment); } else if ((0, _utils2.isCommentRequestTypeActions)(comment)) { (0, _api.decodeWithExcessOrThrow)(_domain.ActionsAttachmentPayloadRt)(comment); } else if ((0, _utils2.isCommentRequestTypeAlert)(comment)) { (0, _api.decodeWithExcessOrThrow)(_domain.AlertAttachmentPayloadRt)(comment); const { ids, indices } = (0, _utils2.getIDsAndIndicesAsArrays)(comment); /** * The alertId and index field must either be both of type string or they must both be string[] and be the same length. * Having a one-to-one relationship between the id and index of an alert avoids accidentally updating or * retrieving the wrong alert. Elasticsearch only guarantees that the _id (the field we use for alertId) to be * unique within a single index. So if we attempt to update or get a specific alert across multiple indices we could * update or receive the wrong one. * * Consider the situation where we have a alert1 with _id = '100' in index 'my-index-awesome' and also in index * 'my-index-hi'. * If we attempt to update the status of alert1 using an index pattern like `my-index-*` or even providing multiple * indices, there's a chance we'll accidentally update too many alerts. * * This check doesn't enforce that the API request has the correct alert ID to index relationship it just guards * against accidentally making a request like: * { * alertId: [1,2,3], * index: awesome, * } * * Instead this requires the requestor to provide: * { * alertId: [1,2,3], * index: [awesome, awesome, awesome] * } * * Ideally we'd change the format of the comment request to be an array of objects like: * { * alerts: [{id: 1, index: awesome}, {id: 2, index: awesome}] * } * * But we'd need to also implement a migration because the saved object document currently stores the id and index * in separate fields. */ if (ids.length !== indices.length) { throw (0, _boom.badRequest)(`Received an alert comment with ids and indices arrays of different lengths ids: ${JSON.stringify(ids)} indices: ${JSON.stringify(indices)}`); } } else if ((0, _attachments.isCommentRequestTypeExternalReference)(comment)) { decodeExternalReferenceAttachment(comment, externalRefRegistry); } else if ((0, _attachments.isCommentRequestTypePersistableState)(comment)) { (0, _api.decodeWithExcessOrThrow)(_domain.PersistableStateAttachmentPayloadRt)(comment); } else { /** * This assertion ensures that TS will show an error * when we add a new attachment type. This way, we rely on TS * to remind us that we have to do a check for the new attachment. */ (0, _utils2.assertUnreachable)(comment); } }; exports.decodeCommentRequest = decodeCommentRequest; const decodeExternalReferenceAttachment = (attachment, externalRefRegistry) => { if (attachment.externalReferenceStorage.type === _domain.ExternalReferenceStorageType.savedObject) { (0, _api.decodeWithExcessOrThrow)(_domain.ExternalReferenceSOAttachmentPayloadRt)(attachment); } else { (0, _api.decodeWithExcessOrThrow)(_domain.ExternalReferenceNoSOAttachmentPayloadRt)(attachment); } const metadata = attachment.externalReferenceMetadata; if (externalRefRegistry.has(attachment.externalReferenceAttachmentTypeId)) { var _attachmentType$schem; const attachmentType = externalRefRegistry.get(attachment.externalReferenceAttachmentTypeId); (_attachmentType$schem = attachmentType.schemaValidator) === null || _attachmentType$schem === void 0 ? void 0 : _attachmentType$schem.call(attachmentType, metadata); } }; /** * Return the alert IDs from the comment if it is an alert style comment. Otherwise return an empty array. */ const getAlertIds = comment => { if ((0, _utils2.isCommentRequestTypeAlert)(comment)) { return Array.isArray(comment.alertId) ? comment.alertId : [comment.alertId]; } return []; }; exports.getAlertIds = getAlertIds; const addStatusFilter = status => { return _esQuery.nodeBuilder.is(`${_constants.CASE_SAVED_OBJECT}.attributes.status`, `${_constants2.STATUS_EXTERNAL_TO_ESMODEL[status]}`); }; const addSeverityFilter = severity => { return _esQuery.nodeBuilder.is(`${_constants.CASE_SAVED_OBJECT}.attributes.severity`, `${_constants2.SEVERITY_EXTERNAL_TO_ESMODEL[severity]}`); }; const buildCategoryFilter = categories => { if (categories === undefined) { return; } const categoriesAsArray = Array.isArray(categories) ? categories : [categories]; if (categoriesAsArray.length === 0) { return; } const categoryFilters = categoriesAsArray.map(category => _esQuery.nodeBuilder.is(`${_constants.CASE_SAVED_OBJECT}.attributes.category`, `${category}`)); return _esQuery.nodeBuilder.or(categoryFilters); }; const NodeBuilderOperators = { and: 'and', or: 'or' }; exports.NodeBuilderOperators = NodeBuilderOperators; const buildFilter = ({ filters, field, operator, type = _constants.CASE_SAVED_OBJECT }) => { if (filters === undefined) { return; } const filtersAsArray = Array.isArray(filters) ? filters : [filters]; if (filtersAsArray.length === 0) { return; } return _esQuery.nodeBuilder[operator](filtersAsArray.map(filter => _esQuery.nodeBuilder.is(`${type}.attributes.${field}`, filter))); }; /** * Combines the authorized filters with the requested owners. */ exports.buildFilter = buildFilter; const combineAuthorizedAndOwnerFilter = (owner, authorizationFilter, savedObjectType) => { const ownerFilter = buildFilter({ filters: owner, field: _constants.OWNER_FIELD, operator: 'or', type: savedObjectType }); return (0, _utils.combineFilterWithAuthorizationFilter)(ownerFilter, authorizationFilter); }; /** * Combines Kuery nodes and accepts an array with a mixture of undefined and KueryNodes. This will filter out the undefined * filters and return a KueryNode with the filters combined using the specified operator which defaults to and if not defined. */ exports.combineAuthorizedAndOwnerFilter = combineAuthorizedAndOwnerFilter; function combineFilters(nodes, operator = NodeBuilderOperators.and) { const filters = nodes.filter(node => node !== undefined); if (filters.length <= 0) { return; } return _esQuery.nodeBuilder[operator](filters); } /** * Creates a KueryNode from a string expression. Returns undefined if the expression is undefined. */ function stringToKueryNode(expression) { if (!expression) { return; } return (0, _esQuery.fromKueryExpression)(expression); } const buildRangeFilter = ({ from, to, field = 'created_at', savedObjectType = _constants.CASE_SAVED_OBJECT }) => { if (from == null && to == null) { return; } try { const fromKQL = from != null ? `${(0, _esQuery.escapeKuery)(savedObjectType)}.attributes.${(0, _esQuery.escapeKuery)(field)} >= ${(0, _esQuery.escapeKuery)(from)}` : undefined; const toKQL = to != null ? `${(0, _esQuery.escapeKuery)(savedObjectType)}.attributes.${(0, _esQuery.escapeKuery)(field)} <= ${(0, _esQuery.escapeKuery)(to)}` : undefined; const rangeKQLQuery = `${fromKQL != null ? fromKQL : ''} ${fromKQL != null && toKQL != null ? 'and' : ''} ${toKQL != null ? toKQL : ''}`; return stringToKueryNode(rangeKQLQuery); } catch (error) { throw (0, _boom.badRequest)('Invalid "from" and/or "to" query parameters'); } }; exports.buildRangeFilter = buildRangeFilter; const buildAssigneesFilter = ({ assignees }) => { if (assignees === undefined) { return; } const assigneesAsArray = Array.isArray(assignees) ? assignees : [assignees]; if (assigneesAsArray.length === 0) { return; } const assigneesWithoutNone = assigneesAsArray.filter(assignee => assignee !== _constants.NO_ASSIGNEES_FILTERING_KEYWORD); const hasNoneAssignee = assigneesAsArray.some(assignee => assignee === _constants.NO_ASSIGNEES_FILTERING_KEYWORD); const assigneesFilter = assigneesWithoutNone.map(filter => _esQuery.nodeBuilder.is(`${_constants.CASE_SAVED_OBJECT}.attributes.assignees.uid`, filter)); if (!hasNoneAssignee) { return _esQuery.nodeBuilder.or(assigneesFilter); } const filterCasesWithoutAssigneesKueryNode = (0, _esQuery.fromKueryExpression)(`not ${_constants.CASE_SAVED_OBJECT}.attributes.assignees.uid: *`); return _esQuery.nodeBuilder.or([...assigneesFilter, filterCasesWithoutAssigneesKueryNode]); }; exports.buildAssigneesFilter = buildAssigneesFilter; const constructQueryOptions = ({ tags, reporters, status, severity, sortField, owner, authorizationFilter, from, to, assignees, category }) => { const tagsFilter = buildFilter({ filters: tags, field: 'tags', operator: 'or' }); const reportersFilter = createReportersFilter(reporters); const sortByField = convertSortField(sortField); const ownerFilter = buildFilter({ filters: owner, field: _constants.OWNER_FIELD, operator: 'or' }); const statusFilter = status != null ? addStatusFilter(status) : undefined; const severityFilter = severity != null ? addSeverityFilter(severity) : undefined; const rangeFilter = buildRangeFilter({ from, to }); const assigneesFilter = buildAssigneesFilter({ assignees }); const categoryFilter = buildCategoryFilter(category); const filters = combineFilters([statusFilter, severityFilter, tagsFilter, reportersFilter, rangeFilter, ownerFilter, assigneesFilter, categoryFilter]); return { filter: (0, _utils.combineFilterWithAuthorizationFilter)(filters, authorizationFilter), sortField: sortByField }; }; exports.constructQueryOptions = constructQueryOptions; const createReportersFilter = reporters => { const reportersFilter = buildFilter({ filters: reporters, field: 'created_by.username', operator: 'or' }); const reportersProfileUidFilter = buildFilter({ filters: reporters, field: 'created_by.profile_uid', operator: 'or' }); const filters = [reportersFilter, reportersProfileUidFilter].filter(filter => filter != null); if (filters.length <= 0) { return; } return _esQuery.nodeBuilder.or(filters); }; const arraysDifference = (originalValue, updatedValue) => { if (originalValue != null && updatedValue != null && Array.isArray(updatedValue) && Array.isArray(originalValue)) { const addedItems = (0, _lodash.differenceWith)(updatedValue, originalValue, _lodash.isEqual); const deletedItems = (0, _lodash.differenceWith)(originalValue, updatedValue, _lodash.isEqual); if (addedItems.length > 0 || deletedItems.length > 0) { return { addedItems, deletedItems }; } } return null; }; exports.arraysDifference = arraysDifference; const getCaseToUpdate = (currentCase, queryCase) => Object.entries(queryCase).reduce((acc, [key, value]) => { const currentValue = (0, _lodash.get)(currentCase, key); if (Array.isArray(currentValue) && Array.isArray(value)) { if (arraysDifference(value, currentValue)) { acc[key] = value; } } else if ((0, _lodash.isPlainObject)(currentValue) && (0, _lodash.isPlainObject)(value)) { if (!(0, _fastDeepEqual.default)(currentValue, value)) { acc[key] = value; } } else if (currentValue !== undefined && value !== currentValue) { acc[key] = value; } return acc; }, { id: queryCase.id, version: queryCase.version }); /** * TODO: Backend is not connected with the * frontend in x-pack/plugins/cases/common/ui/types.ts. * It is easy to forget to update a sort field. * We should fix it and make it common. * Also the sortField in x-pack/plugins/cases/common/api/cases/case.ts * is set to string. We should narrow it to the * acceptable values */ exports.getCaseToUpdate = getCaseToUpdate; var SortFieldCase; (function (SortFieldCase) { SortFieldCase["closedAt"] = "closed_at"; SortFieldCase["createdAt"] = "created_at"; SortFieldCase["status"] = "status"; SortFieldCase["title"] = "title.keyword"; SortFieldCase["severity"] = "severity"; SortFieldCase["updatedAt"] = "updated_at"; SortFieldCase["category"] = "category"; })(SortFieldCase || (SortFieldCase = {})); const convertSortField = sortField => { switch (sortField) { case 'status': return SortFieldCase.status; case 'createdAt': return SortFieldCase.createdAt; case 'closedAt': return SortFieldCase.closedAt; case 'title': return SortFieldCase.title; case 'severity': return SortFieldCase.severity; case 'updatedAt': return SortFieldCase.updatedAt; case 'category': return SortFieldCase.category; default: return SortFieldCase.createdAt; } }; exports.convertSortField = convertSortField; const constructSearch = (search, spaceId, savedObjectsSerializer) => { if (!search) { return undefined; } if ((0, _uuid.validate)(search)) { const rawId = savedObjectsSerializer.generateRawId((0, _namespace.spaceIdToNamespace)(spaceId), _constants.CASE_SAVED_OBJECT, search); return { search: `"${search}" "${rawId}"`, rootSearchFields: ['_id'] }; } return { search }; }; exports.constructSearch = constructSearch;