mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Cases] Migrate connector ID to references (#104221)
* Starting configure migration * Initial refactor of configuration connector id * Additional clean up and tests * Adding some tests * Finishing configure tests * Starting case attributes transformation refactor * adding more tests for the cases service * Adding more functionality and tests for cases migration * Finished unit tests for cases transition * Finished tests and moved types * Cleaning up type names * Fixing types and renaming * Adding more tests directly for the transformations * Fixing tests and renaming some functions * Adding transformation helper tests * Adding migration utility tests and some clean up * Begining logic to remove references when it is the none connector * Fixing merge reference bug * Addressing feedback * Changing test name and creating constants file
This commit is contained in:
parent
ec4de0d95e
commit
96f27b9899
48 changed files with 4957 additions and 723 deletions
|
@ -11,7 +11,7 @@ import { NumberFromString } from '../saved_object';
|
|||
import { UserRT } from '../user';
|
||||
import { CommentResponseRt } from './comment';
|
||||
import { CasesStatusResponseRt, CaseStatusRt } from './status';
|
||||
import { CaseConnectorRt, ESCaseConnector } from '../connectors';
|
||||
import { CaseConnectorRt } from '../connectors';
|
||||
import { SubCaseResponseRt } from './sub_case';
|
||||
|
||||
const BucketsAggs = rt.array(
|
||||
|
@ -87,24 +87,17 @@ const CaseBasicRt = rt.type({
|
|||
owner: rt.string,
|
||||
});
|
||||
|
||||
const CaseExternalServiceBasicRt = rt.type({
|
||||
connector_id: rt.string,
|
||||
export const CaseExternalServiceBasicRt = rt.type({
|
||||
connector_id: rt.union([rt.string, rt.null]),
|
||||
connector_name: rt.string,
|
||||
external_id: rt.string,
|
||||
external_title: rt.string,
|
||||
external_url: rt.string,
|
||||
pushed_at: rt.string,
|
||||
pushed_by: UserRT,
|
||||
});
|
||||
|
||||
const CaseFullExternalServiceRt = rt.union([
|
||||
rt.intersection([
|
||||
CaseExternalServiceBasicRt,
|
||||
rt.type({
|
||||
pushed_at: rt.string,
|
||||
pushed_by: UserRT,
|
||||
}),
|
||||
]),
|
||||
rt.null,
|
||||
]);
|
||||
const CaseFullExternalServiceRt = rt.union([CaseExternalServiceBasicRt, rt.null]);
|
||||
|
||||
export const CaseAttributesRt = rt.intersection([
|
||||
CaseBasicRt,
|
||||
|
@ -326,11 +319,6 @@ export type CaseFullExternalService = rt.TypeOf<typeof CaseFullExternalServiceRt
|
|||
export type CaseSettings = rt.TypeOf<typeof SettingsRt>;
|
||||
export type ExternalServiceResponse = rt.TypeOf<typeof ExternalServiceResponseRt>;
|
||||
|
||||
export type ESCaseAttributes = Omit<CaseAttributes, 'connector'> & { connector: ESCaseConnector };
|
||||
export type ESCasePatchRequest = Omit<CasePatchRequest, 'connector'> & {
|
||||
connector?: ESCaseConnector;
|
||||
};
|
||||
|
||||
export type AllTagsFindRequest = rt.TypeOf<typeof AllTagsFindRequestRt>;
|
||||
export type AllReportersFindRequest = AllTagsFindRequest;
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import * as rt from 'io-ts';
|
||||
|
||||
import { UserRT } from '../user';
|
||||
import { CaseConnectorRt, ConnectorMappingsRt, ESCaseConnector } from '../connectors';
|
||||
import { CaseConnectorRt, ConnectorMappingsRt } from '../connectors';
|
||||
|
||||
// TODO: we will need to add this type rt.literal('close-by-third-party')
|
||||
const ClosureTypeRT = rt.union([rt.literal('close-by-user'), rt.literal('close-by-pushing')]);
|
||||
|
@ -83,8 +83,4 @@ export type CasesConfigureAttributes = rt.TypeOf<typeof CaseConfigureAttributesR
|
|||
export type CasesConfigureResponse = rt.TypeOf<typeof CaseConfigureResponseRt>;
|
||||
export type CasesConfigurationsResponse = rt.TypeOf<typeof CaseConfigurationsResponseRt>;
|
||||
|
||||
export type ESCasesConfigureAttributes = Omit<CasesConfigureAttributes, 'connector'> & {
|
||||
connector: ESCaseConnector;
|
||||
};
|
||||
|
||||
export type GetConfigureFindRequest = rt.TypeOf<typeof GetConfigureFindRequestRt>;
|
||||
|
|
|
@ -73,6 +73,8 @@ const ConnectorNoneTypeFieldsRt = rt.type({
|
|||
fields: rt.null,
|
||||
});
|
||||
|
||||
export const noneConnectorId: string = 'none';
|
||||
|
||||
export const ConnectorTypeFieldsRt = rt.union([
|
||||
ConnectorJiraTypeFieldsRt,
|
||||
ConnectorNoneTypeFieldsRt,
|
||||
|
@ -102,16 +104,3 @@ export type ConnectorServiceNowSIRTypeFields = rt.TypeOf<typeof ConnectorService
|
|||
|
||||
// we need to change these types back and forth for storing in ES (arrays overwrite, objects merge)
|
||||
export type ConnectorFields = rt.TypeOf<typeof ConnectorFieldsRt>;
|
||||
|
||||
export type ESConnectorFields = Array<{
|
||||
key: string;
|
||||
value: unknown;
|
||||
}>;
|
||||
|
||||
export type ESCaseConnectorTypes = ConnectorTypes;
|
||||
export interface ESCaseConnector {
|
||||
id: string;
|
||||
name: string;
|
||||
type: ESCaseConnectorTypes;
|
||||
fields: ESConnectorFields | null;
|
||||
}
|
||||
|
|
|
@ -157,10 +157,11 @@ export const getPushedServiceLabelTitle = (action: CaseUserActions, firstPush: b
|
|||
|
||||
export const getPushInfo = (
|
||||
caseServices: CaseServices,
|
||||
parsedValue: { connector_id: string; connector_name: string },
|
||||
// a JSON parse failure will result in null for parsedValue
|
||||
parsedValue: { connector_id: string | null; connector_name: string } | null,
|
||||
index: number
|
||||
) =>
|
||||
parsedValue != null
|
||||
parsedValue != null && parsedValue.connector_id != null
|
||||
? {
|
||||
firstPush: caseServices[parsedValue.connector_id]?.firstPushIndex === index,
|
||||
parsedConnectorId: parsedValue.connector_id,
|
||||
|
|
|
@ -25,15 +25,9 @@ import {
|
|||
MAX_TITLE_LENGTH,
|
||||
} from '../../../common';
|
||||
import { buildCaseUserActionItem } from '../../services/user_actions/helpers';
|
||||
import { getConnectorFromConfiguration } from '../utils';
|
||||
|
||||
import { Operations } from '../../authorization';
|
||||
import {
|
||||
createCaseError,
|
||||
flattenCaseSavedObject,
|
||||
transformCaseConnectorToEsConnector,
|
||||
transformNewCase,
|
||||
} from '../../common';
|
||||
import { createCaseError, flattenCaseSavedObject, transformNewCase } from '../../common';
|
||||
import { CasesClientArgs } from '..';
|
||||
|
||||
/**
|
||||
|
@ -48,7 +42,6 @@ export const create = async (
|
|||
const {
|
||||
unsecuredSavedObjectsClient,
|
||||
caseService,
|
||||
caseConfigureService,
|
||||
userActionService,
|
||||
user,
|
||||
logger,
|
||||
|
@ -90,10 +83,6 @@ 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({
|
||||
unsecuredSavedObjectsClient,
|
||||
});
|
||||
const caseConfigureConnector = getConnectorFromConfiguration(myCaseConfigure);
|
||||
|
||||
const newCase = await caseService.postNewCase({
|
||||
unsecuredSavedObjectsClient,
|
||||
|
@ -103,7 +92,7 @@ export const create = async (
|
|||
username,
|
||||
full_name,
|
||||
email,
|
||||
connector: transformCaseConnectorToEsConnector(query.connector ?? caseConfigureConnector),
|
||||
connector: query.connector,
|
||||
}),
|
||||
id: savedObjectID,
|
||||
});
|
||||
|
|
|
@ -13,7 +13,6 @@ import { SavedObject } from 'kibana/server';
|
|||
import {
|
||||
CaseResponseRt,
|
||||
CaseResponse,
|
||||
ESCaseAttributes,
|
||||
User,
|
||||
UsersRt,
|
||||
AllTagsFindRequest,
|
||||
|
@ -27,6 +26,7 @@ import {
|
|||
ENABLE_CASE_CONNECTOR,
|
||||
CasesByAlertId,
|
||||
CasesByAlertIdRt,
|
||||
CaseAttributes,
|
||||
} from '../../../common';
|
||||
import { countAlertsForID, createCaseError, flattenCaseSavedObject } from '../../common';
|
||||
import { CasesClientArgs } from '..';
|
||||
|
@ -171,7 +171,7 @@ export const get = async (
|
|||
);
|
||||
}
|
||||
|
||||
let theCase: SavedObject<ESCaseAttributes>;
|
||||
let theCase: SavedObject<CaseAttributes>;
|
||||
let subCaseIds: string[] = [];
|
||||
if (ENABLE_CASE_CONNECTOR) {
|
||||
const [caseInfo, subCasesForCaseId] = await Promise.all([
|
||||
|
|
|
@ -14,10 +14,10 @@ import {
|
|||
CaseResponse,
|
||||
CaseStatuses,
|
||||
ExternalServiceResponse,
|
||||
ESCaseAttributes,
|
||||
ESCasesConfigureAttributes,
|
||||
CaseType,
|
||||
ENABLE_CASE_CONNECTOR,
|
||||
CasesConfigureAttributes,
|
||||
CaseAttributes,
|
||||
} from '../../../common';
|
||||
import { buildCaseUserActionItem } from '../../services/user_actions/helpers';
|
||||
|
||||
|
@ -33,8 +33,8 @@ import { casesConnectors } from '../../connectors';
|
|||
* In the future we could allow push to close all the sub cases of a collection but that's not currently supported.
|
||||
*/
|
||||
function shouldCloseByPush(
|
||||
configureSettings: SavedObjectsFindResponse<ESCasesConfigureAttributes>,
|
||||
caseInfo: SavedObject<ESCaseAttributes>
|
||||
configureSettings: SavedObjectsFindResponse<CasesConfigureAttributes>,
|
||||
caseInfo: SavedObject<CaseAttributes>
|
||||
): boolean {
|
||||
return (
|
||||
configureSettings.total > 0 &&
|
||||
|
@ -186,6 +186,7 @@ export const push = async (
|
|||
|
||||
const [updatedCase, updatedComments] = await Promise.all([
|
||||
caseService.patchCase({
|
||||
originalCase: myCase,
|
||||
unsecuredSavedObjectsClient,
|
||||
caseId,
|
||||
updatedAttributes: {
|
||||
|
|
|
@ -34,13 +34,12 @@ import {
|
|||
CommentAttributes,
|
||||
CommentType,
|
||||
ENABLE_CASE_CONNECTOR,
|
||||
ESCaseAttributes,
|
||||
ESCasePatchRequest,
|
||||
excess,
|
||||
MAX_CONCURRENT_SEARCHES,
|
||||
SUB_CASE_SAVED_OBJECT,
|
||||
throwErrors,
|
||||
MAX_TITLE_LENGTH,
|
||||
CaseAttributes,
|
||||
} from '../../../common';
|
||||
import { buildCaseUserActions } from '../../services/user_actions/helpers';
|
||||
import { getCaseToUpdate } from '../utils';
|
||||
|
@ -51,7 +50,6 @@ import {
|
|||
createCaseError,
|
||||
flattenCaseSavedObject,
|
||||
isCommentRequestTypeAlertOrGenAlert,
|
||||
transformCaseConnectorToEsConnector,
|
||||
} from '../../common';
|
||||
import { UpdateAlertRequest } from '../alerts/types';
|
||||
import { CasesClientInternal } from '../client_internal';
|
||||
|
@ -61,17 +59,14 @@ import { Operations, OwnerEntity } from '../../authorization';
|
|||
/**
|
||||
* Throws an error if any of the requests attempt to update a collection style cases' status field.
|
||||
*/
|
||||
function throwIfUpdateStatusOfCollection(
|
||||
requests: ESCasePatchRequest[],
|
||||
casesMap: Map<string, SavedObject<ESCaseAttributes>>
|
||||
) {
|
||||
function throwIfUpdateStatusOfCollection(requests: UpdateRequestWithOriginalCase[]) {
|
||||
const requestsUpdatingStatusOfCollection = requests.filter(
|
||||
(req) =>
|
||||
req.status !== undefined && casesMap.get(req.id)?.attributes.type === CaseType.collection
|
||||
({ updateReq, originalCase }) =>
|
||||
updateReq.status !== undefined && originalCase.attributes.type === CaseType.collection
|
||||
);
|
||||
|
||||
if (requestsUpdatingStatusOfCollection.length > 0) {
|
||||
const ids = requestsUpdatingStatusOfCollection.map((req) => req.id);
|
||||
const ids = requestsUpdatingStatusOfCollection.map(({ updateReq }) => updateReq.id);
|
||||
throw Boom.badRequest(
|
||||
`Updating the status of a collection is not allowed ids: [${ids.join(', ')}]`
|
||||
);
|
||||
|
@ -81,18 +76,14 @@ function throwIfUpdateStatusOfCollection(
|
|||
/**
|
||||
* Throws an error if any of the requests attempt to update a collection style case to an individual one.
|
||||
*/
|
||||
function throwIfUpdateTypeCollectionToIndividual(
|
||||
requests: ESCasePatchRequest[],
|
||||
casesMap: Map<string, SavedObject<ESCaseAttributes>>
|
||||
) {
|
||||
function throwIfUpdateTypeCollectionToIndividual(requests: UpdateRequestWithOriginalCase[]) {
|
||||
const requestsUpdatingTypeCollectionToInd = requests.filter(
|
||||
(req) =>
|
||||
req.type === CaseType.individual &&
|
||||
casesMap.get(req.id)?.attributes.type === CaseType.collection
|
||||
({ updateReq, originalCase }) =>
|
||||
updateReq.type === CaseType.individual && originalCase.attributes.type === CaseType.collection
|
||||
);
|
||||
|
||||
if (requestsUpdatingTypeCollectionToInd.length > 0) {
|
||||
const ids = requestsUpdatingTypeCollectionToInd.map((req) => req.id);
|
||||
const ids = requestsUpdatingTypeCollectionToInd.map(({ updateReq }) => updateReq.id);
|
||||
throw Boom.badRequest(
|
||||
`Converting a collection to an individual case is not allowed ids: [${ids.join(', ')}]`
|
||||
);
|
||||
|
@ -102,11 +93,11 @@ function throwIfUpdateTypeCollectionToIndividual(
|
|||
/**
|
||||
* Throws an error if any of the requests attempt to update the type of a case.
|
||||
*/
|
||||
function throwIfUpdateType(requests: ESCasePatchRequest[]) {
|
||||
const requestsUpdatingType = requests.filter((req) => req.type !== undefined);
|
||||
function throwIfUpdateType(requests: UpdateRequestWithOriginalCase[]) {
|
||||
const requestsUpdatingType = requests.filter(({ updateReq }) => updateReq.type !== undefined);
|
||||
|
||||
if (requestsUpdatingType.length > 0) {
|
||||
const ids = requestsUpdatingType.map((req) => req.id);
|
||||
const ids = requestsUpdatingType.map(({ updateReq }) => updateReq.id);
|
||||
throw Boom.badRequest(
|
||||
`Updating the type of a case when sub cases are disabled is not allowed ids: [${ids.join(
|
||||
', '
|
||||
|
@ -118,11 +109,11 @@ function throwIfUpdateType(requests: ESCasePatchRequest[]) {
|
|||
/**
|
||||
* Throws an error if any of the requests attempt to update the owner of a case.
|
||||
*/
|
||||
function throwIfUpdateOwner(requests: ESCasePatchRequest[]) {
|
||||
const requestsUpdatingOwner = requests.filter((req) => req.owner !== undefined);
|
||||
function throwIfUpdateOwner(requests: UpdateRequestWithOriginalCase[]) {
|
||||
const requestsUpdatingOwner = requests.filter(({ updateReq }) => updateReq.owner !== undefined);
|
||||
|
||||
if (requestsUpdatingOwner.length > 0) {
|
||||
const ids = requestsUpdatingOwner.map((req) => req.id);
|
||||
const ids = requestsUpdatingOwner.map(({ updateReq }) => updateReq.id);
|
||||
throw Boom.badRequest(`Updating the owner of a case is not allowed ids: [${ids.join(', ')}]`);
|
||||
}
|
||||
}
|
||||
|
@ -136,14 +127,14 @@ async function throwIfInvalidUpdateOfTypeWithAlerts({
|
|||
caseService,
|
||||
unsecuredSavedObjectsClient,
|
||||
}: {
|
||||
requests: ESCasePatchRequest[];
|
||||
requests: UpdateRequestWithOriginalCase[];
|
||||
caseService: CasesService;
|
||||
unsecuredSavedObjectsClient: SavedObjectsClientContract;
|
||||
}) {
|
||||
const getAlertsForID = async (caseToUpdate: ESCasePatchRequest) => {
|
||||
const getAlertsForID = async ({ updateReq }: UpdateRequestWithOriginalCase) => {
|
||||
const alerts = await caseService.getAllCaseComments({
|
||||
unsecuredSavedObjectsClient,
|
||||
id: caseToUpdate.id,
|
||||
id: updateReq.id,
|
||||
options: {
|
||||
fields: [],
|
||||
// there should never be generated alerts attached to an individual case but we'll check anyway
|
||||
|
@ -159,11 +150,14 @@ async function throwIfInvalidUpdateOfTypeWithAlerts({
|
|||
},
|
||||
});
|
||||
|
||||
return { id: caseToUpdate.id, alerts };
|
||||
return { id: updateReq.id, alerts };
|
||||
};
|
||||
|
||||
const requestsUpdatingTypeField = requests.filter((req) => req.type === CaseType.collection);
|
||||
const getAlertsMapper = async (caseToUpdate: ESCasePatchRequest) => getAlertsForID(caseToUpdate);
|
||||
const requestsUpdatingTypeField = requests.filter(
|
||||
({ updateReq }) => updateReq.type === CaseType.collection
|
||||
);
|
||||
const getAlertsMapper = async (caseToUpdate: UpdateRequestWithOriginalCase) =>
|
||||
getAlertsForID(caseToUpdate);
|
||||
// Ensuring we don't too many concurrent get running.
|
||||
const casesAlertTotals = await pMap(requestsUpdatingTypeField, getAlertsMapper, {
|
||||
concurrency: MAX_CONCURRENT_SEARCHES,
|
||||
|
@ -185,13 +179,13 @@ async function throwIfInvalidUpdateOfTypeWithAlerts({
|
|||
/**
|
||||
* Throws an error if any of the requests updates a title and the length is over MAX_TITLE_LENGTH.
|
||||
*/
|
||||
function throwIfTitleIsInvalid(requests: ESCasePatchRequest[]) {
|
||||
function throwIfTitleIsInvalid(requests: UpdateRequestWithOriginalCase[]) {
|
||||
const requestsInvalidTitle = requests.filter(
|
||||
(req) => req.title !== undefined && req.title.length > MAX_TITLE_LENGTH
|
||||
({ updateReq }) => updateReq.title !== undefined && updateReq.title.length > MAX_TITLE_LENGTH
|
||||
);
|
||||
|
||||
if (requestsInvalidTitle.length > 0) {
|
||||
const ids = requestsInvalidTitle.map((req) => req.id);
|
||||
const ids = requestsInvalidTitle.map(({ updateReq }) => updateReq.id);
|
||||
throw Boom.badRequest(
|
||||
`The length of the title is too long. The maximum length is ${MAX_TITLE_LENGTH}, ids: [${ids.join(
|
||||
', '
|
||||
|
@ -218,11 +212,11 @@ async function getAlertComments({
|
|||
caseService,
|
||||
unsecuredSavedObjectsClient,
|
||||
}: {
|
||||
casesToSync: ESCasePatchRequest[];
|
||||
casesToSync: UpdateRequestWithOriginalCase[];
|
||||
caseService: CasesService;
|
||||
unsecuredSavedObjectsClient: SavedObjectsClientContract;
|
||||
}): Promise<SavedObjectsFindResponse<CommentAttributes>> {
|
||||
const idsOfCasesToSync = casesToSync.map((casePatchReq) => casePatchReq.id);
|
||||
const idsOfCasesToSync = casesToSync.map(({ updateReq }) => updateReq.id);
|
||||
|
||||
// getAllCaseComments will by default get all the comments, unless page or perPage fields are set
|
||||
return caseService.getAllCaseComments({
|
||||
|
@ -310,14 +304,12 @@ function getSyncStatusForComment({
|
|||
async function updateAlerts({
|
||||
casesWithSyncSettingChangedToOn,
|
||||
casesWithStatusChangedAndSynced,
|
||||
casesMap,
|
||||
caseService,
|
||||
unsecuredSavedObjectsClient,
|
||||
casesClientInternal,
|
||||
}: {
|
||||
casesWithSyncSettingChangedToOn: ESCasePatchRequest[];
|
||||
casesWithStatusChangedAndSynced: ESCasePatchRequest[];
|
||||
casesMap: Map<string, SavedObject<ESCaseAttributes>>;
|
||||
casesWithSyncSettingChangedToOn: UpdateRequestWithOriginalCase[];
|
||||
casesWithStatusChangedAndSynced: UpdateRequestWithOriginalCase[];
|
||||
caseService: CasesService;
|
||||
unsecuredSavedObjectsClient: SavedObjectsClientContract;
|
||||
casesClientInternal: CasesClientInternal;
|
||||
|
@ -331,11 +323,8 @@ async function updateAlerts({
|
|||
// build a map of case id to the status it has
|
||||
// this will have collections in it but the alerts should be associated to sub cases and not collections so it shouldn't
|
||||
// matter.
|
||||
const casesToSyncToStatus = casesToSync.reduce((acc, caseInfo) => {
|
||||
acc.set(
|
||||
caseInfo.id,
|
||||
caseInfo.status ?? casesMap.get(caseInfo.id)?.attributes.status ?? CaseStatuses.open
|
||||
);
|
||||
const casesToSyncToStatus = casesToSync.reduce((acc, { updateReq, originalCase }) => {
|
||||
acc.set(updateReq.id, updateReq.status ?? originalCase.attributes.status ?? CaseStatuses.open);
|
||||
return acc;
|
||||
}, new Map<string, CaseStatuses>());
|
||||
|
||||
|
@ -376,7 +365,7 @@ async function updateAlerts({
|
|||
}
|
||||
|
||||
function partitionPatchRequest(
|
||||
casesMap: Map<string, SavedObject<ESCaseAttributes>>,
|
||||
casesMap: Map<string, SavedObject<CaseAttributes>>,
|
||||
patchReqCases: CasePatchRequest[]
|
||||
): {
|
||||
nonExistingCases: CasePatchRequest[];
|
||||
|
@ -409,6 +398,11 @@ function partitionPatchRequest(
|
|||
};
|
||||
}
|
||||
|
||||
interface UpdateRequestWithOriginalCase {
|
||||
updateReq: CasePatchRequest;
|
||||
originalCase: SavedObject<CaseAttributes>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the specified cases with new values
|
||||
*
|
||||
|
@ -441,7 +435,7 @@ export const update = async (
|
|||
const casesMap = myCases.saved_objects.reduce((acc, so) => {
|
||||
acc.set(so.id, so);
|
||||
return acc;
|
||||
}, new Map<string, SavedObject<ESCaseAttributes>>());
|
||||
}, new Map<string, SavedObject<CaseAttributes>>());
|
||||
|
||||
const { nonExistingCases, conflictedCases, casesToAuthorize } = partitionPatchRequest(
|
||||
casesMap,
|
||||
|
@ -469,38 +463,41 @@ export const update = async (
|
|||
);
|
||||
}
|
||||
|
||||
const updateCases: ESCasePatchRequest[] = query.cases.map((updateCase) => {
|
||||
const currentCase = myCases.saved_objects.find((c) => c.id === updateCase.id);
|
||||
const { connector, ...thisCase } = updateCase;
|
||||
return currentCase != null
|
||||
? getCaseToUpdate(currentCase.attributes, {
|
||||
...thisCase,
|
||||
...(connector != null
|
||||
? { connector: transformCaseConnectorToEsConnector(connector) }
|
||||
: {}),
|
||||
})
|
||||
: { id: thisCase.id, version: thisCase.version };
|
||||
});
|
||||
const updateCases: UpdateRequestWithOriginalCase[] = query.cases.reduce(
|
||||
(acc: UpdateRequestWithOriginalCase[], updateCase) => {
|
||||
const originalCase = casesMap.get(updateCase.id);
|
||||
|
||||
const updateFilterCases = updateCases.filter((updateCase) => {
|
||||
const { id, version, ...updateCaseAttributes } = updateCase;
|
||||
return Object.keys(updateCaseAttributes).length > 0;
|
||||
});
|
||||
if (!originalCase) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (updateFilterCases.length <= 0) {
|
||||
const fieldsToUpdate = getCaseToUpdate(originalCase.attributes, updateCase);
|
||||
|
||||
const { id, version, ...restFields } = fieldsToUpdate;
|
||||
|
||||
if (Object.keys(restFields).length > 0) {
|
||||
acc.push({ originalCase, updateReq: fieldsToUpdate });
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
if (updateCases.length <= 0) {
|
||||
throw Boom.notAcceptable('All update fields are identical to current version.');
|
||||
}
|
||||
|
||||
if (!ENABLE_CASE_CONNECTOR) {
|
||||
throwIfUpdateType(updateFilterCases);
|
||||
throwIfUpdateType(updateCases);
|
||||
}
|
||||
|
||||
throwIfUpdateOwner(updateFilterCases);
|
||||
throwIfTitleIsInvalid(updateFilterCases);
|
||||
throwIfUpdateStatusOfCollection(updateFilterCases, casesMap);
|
||||
throwIfUpdateTypeCollectionToIndividual(updateFilterCases, casesMap);
|
||||
throwIfUpdateOwner(updateCases);
|
||||
throwIfTitleIsInvalid(updateCases);
|
||||
throwIfUpdateStatusOfCollection(updateCases);
|
||||
throwIfUpdateTypeCollectionToIndividual(updateCases);
|
||||
await throwIfInvalidUpdateOfTypeWithAlerts({
|
||||
requests: updateFilterCases,
|
||||
requests: updateCases,
|
||||
caseService,
|
||||
unsecuredSavedObjectsClient,
|
||||
});
|
||||
|
@ -510,9 +507,9 @@ export const update = async (
|
|||
const updatedDt = new Date().toISOString();
|
||||
const updatedCases = await caseService.patchCases({
|
||||
unsecuredSavedObjectsClient,
|
||||
cases: updateFilterCases.map((thisCase) => {
|
||||
cases: updateCases.map(({ updateReq, originalCase }) => {
|
||||
// intentionally removing owner from the case so that we don't accidentally allow it to be updated
|
||||
const { id: caseId, version, owner, ...updateCaseAttributes } = thisCase;
|
||||
const { id: caseId, version, owner, ...updateCaseAttributes } = updateReq;
|
||||
let closedInfo = {};
|
||||
if (updateCaseAttributes.status && updateCaseAttributes.status === CaseStatuses.closed) {
|
||||
closedInfo = {
|
||||
|
@ -531,6 +528,7 @@ export const update = async (
|
|||
}
|
||||
return {
|
||||
caseId,
|
||||
originalCase,
|
||||
updatedAttributes: {
|
||||
...updateCaseAttributes,
|
||||
...closedInfo,
|
||||
|
@ -544,25 +542,23 @@ export const update = async (
|
|||
|
||||
// If a status update occurred and the case is synced then we need to update all alerts' status
|
||||
// attached to the case to the new status.
|
||||
const casesWithStatusChangedAndSynced = updateFilterCases.filter((caseToUpdate) => {
|
||||
const currentCase = myCases.saved_objects.find((c) => c.id === caseToUpdate.id);
|
||||
const casesWithStatusChangedAndSynced = updateCases.filter(({ updateReq, originalCase }) => {
|
||||
return (
|
||||
currentCase != null &&
|
||||
caseToUpdate.status != null &&
|
||||
currentCase.attributes.status !== caseToUpdate.status &&
|
||||
currentCase.attributes.settings.syncAlerts
|
||||
originalCase != null &&
|
||||
updateReq.status != null &&
|
||||
originalCase.attributes.status !== updateReq.status &&
|
||||
originalCase.attributes.settings.syncAlerts
|
||||
);
|
||||
});
|
||||
|
||||
// If syncAlerts setting turned on we need to update all alerts' status
|
||||
// attached to the case to the current status.
|
||||
const casesWithSyncSettingChangedToOn = updateFilterCases.filter((caseToUpdate) => {
|
||||
const currentCase = myCases.saved_objects.find((c) => c.id === caseToUpdate.id);
|
||||
const casesWithSyncSettingChangedToOn = updateCases.filter(({ updateReq, originalCase }) => {
|
||||
return (
|
||||
currentCase != null &&
|
||||
caseToUpdate.settings?.syncAlerts != null &&
|
||||
currentCase.attributes.settings.syncAlerts !== caseToUpdate.settings.syncAlerts &&
|
||||
caseToUpdate.settings.syncAlerts
|
||||
originalCase != null &&
|
||||
updateReq.settings?.syncAlerts != null &&
|
||||
originalCase.attributes.settings.syncAlerts !== updateReq.settings.syncAlerts &&
|
||||
updateReq.settings.syncAlerts
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -573,7 +569,6 @@ export const update = async (
|
|||
caseService,
|
||||
unsecuredSavedObjectsClient,
|
||||
casesClientInternal,
|
||||
casesMap,
|
||||
});
|
||||
|
||||
const returnUpdatedCase = myCases.saved_objects
|
||||
|
|
|
@ -20,13 +20,13 @@ import {
|
|||
CaseConfigurationsResponseRt,
|
||||
CaseConfigureResponseRt,
|
||||
CasesConfigurationsResponse,
|
||||
CasesConfigureAttributes,
|
||||
CasesConfigurePatch,
|
||||
CasesConfigurePatchRt,
|
||||
CasesConfigureRequest,
|
||||
CasesConfigureResponse,
|
||||
ConnectorMappings,
|
||||
ConnectorMappingsAttributes,
|
||||
ESCasesConfigureAttributes,
|
||||
excess,
|
||||
GetConfigureFindRequest,
|
||||
GetConfigureFindRequestRt,
|
||||
|
@ -34,11 +34,7 @@ import {
|
|||
SUPPORTED_CONNECTORS,
|
||||
throwErrors,
|
||||
} from '../../../common';
|
||||
import {
|
||||
createCaseError,
|
||||
transformCaseConnectorToEsConnector,
|
||||
transformESConnectorToCaseConnector,
|
||||
} from '../../common';
|
||||
import { createCaseError } from '../../common';
|
||||
import { CasesClientInternal } from '../client_internal';
|
||||
import { CasesClientArgs } from '../types';
|
||||
import { getMappings } from './get_mappings';
|
||||
|
@ -174,7 +170,7 @@ async function get(
|
|||
|
||||
const configurations = await pMap(
|
||||
myCaseConfigure.saved_objects,
|
||||
async (configuration: SavedObject<ESCasesConfigureAttributes>) => {
|
||||
async (configuration: SavedObject<CasesConfigureAttributes>) => {
|
||||
const { connector, ...caseConfigureWithoutConnector } = configuration?.attributes ?? {
|
||||
connector: null,
|
||||
};
|
||||
|
@ -184,7 +180,7 @@ async function get(
|
|||
if (connector != null) {
|
||||
try {
|
||||
mappings = await casesClientInternal.configuration.getMappings({
|
||||
connector: transformESConnectorToCaseConnector(connector),
|
||||
connector,
|
||||
});
|
||||
} catch (e) {
|
||||
error = e.isBoom
|
||||
|
@ -195,7 +191,7 @@ async function get(
|
|||
|
||||
return {
|
||||
...caseConfigureWithoutConnector,
|
||||
connector: transformESConnectorToCaseConnector(connector),
|
||||
connector,
|
||||
mappings: mappings.length > 0 ? mappings[0].attributes.mappings : [],
|
||||
version: configuration.version ?? '',
|
||||
error,
|
||||
|
@ -292,11 +288,9 @@ async function update(
|
|||
|
||||
try {
|
||||
const resMappings = await casesClientInternal.configuration.getMappings({
|
||||
connector:
|
||||
connector != null
|
||||
? connector
|
||||
: transformESConnectorToCaseConnector(configuration.attributes.connector),
|
||||
connector: connector != null ? connector : configuration.attributes.connector,
|
||||
});
|
||||
|
||||
mappings = resMappings.length > 0 ? resMappings[0].attributes.mappings : [];
|
||||
|
||||
if (connector != null) {
|
||||
|
@ -325,18 +319,17 @@ async function update(
|
|||
configurationId: configuration.id,
|
||||
updatedAttributes: {
|
||||
...queryWithoutVersionAndConnector,
|
||||
...(connector != null ? { connector: transformCaseConnectorToEsConnector(connector) } : {}),
|
||||
...(connector != null && { connector }),
|
||||
updated_at: updateDate,
|
||||
updated_by: user,
|
||||
},
|
||||
originalConfiguration: configuration,
|
||||
});
|
||||
|
||||
return CaseConfigureResponseRt.encode({
|
||||
...configuration.attributes,
|
||||
...patch.attributes,
|
||||
connector: transformESConnectorToCaseConnector(
|
||||
patch.attributes.connector ?? configuration.attributes.connector
|
||||
),
|
||||
connector: patch.attributes.connector ?? configuration.attributes.connector,
|
||||
mappings,
|
||||
version: patch.version ?? '',
|
||||
error,
|
||||
|
@ -397,7 +390,7 @@ async function create(
|
|||
);
|
||||
|
||||
if (myCaseConfigure.saved_objects.length > 0) {
|
||||
const deleteConfigurationMapper = async (c: SavedObject<ESCasesConfigureAttributes>) =>
|
||||
const deleteConfigurationMapper = async (c: SavedObject<CasesConfigureAttributes>) =>
|
||||
caseConfigureService.delete({ unsecuredSavedObjectsClient, configurationId: c.id });
|
||||
|
||||
// Ensuring we don't too many concurrent deletions running.
|
||||
|
@ -431,7 +424,7 @@ async function create(
|
|||
unsecuredSavedObjectsClient,
|
||||
attributes: {
|
||||
...configuration,
|
||||
connector: transformCaseConnectorToEsConnector(configuration.connector),
|
||||
connector: configuration.connector,
|
||||
created_at: creationDate,
|
||||
created_by: user,
|
||||
updated_at: null,
|
||||
|
@ -443,7 +436,7 @@ async function create(
|
|||
return CaseConfigureResponseRt.encode({
|
||||
...post.attributes,
|
||||
// Reserve for future implementations
|
||||
connector: transformESConnectorToCaseConnector(post.attributes.connector),
|
||||
connector: post.attributes.connector,
|
||||
mappings,
|
||||
version: post.version ?? '',
|
||||
error,
|
||||
|
|
|
@ -23,7 +23,6 @@ import {
|
|||
CaseStatuses,
|
||||
CommentAttributes,
|
||||
CommentType,
|
||||
ESCaseAttributes,
|
||||
excess,
|
||||
SUB_CASE_SAVED_OBJECT,
|
||||
SubCaseAttributes,
|
||||
|
@ -35,6 +34,7 @@ import {
|
|||
SubCasesResponseRt,
|
||||
throwErrors,
|
||||
User,
|
||||
CaseAttributes,
|
||||
} from '../../../common';
|
||||
import { getCaseToUpdate } from '../utils';
|
||||
import { buildSubCaseUserActions } from '../../services/user_actions/helpers';
|
||||
|
@ -124,7 +124,7 @@ async function getParentCases({
|
|||
unsecuredSavedObjectsClient: SavedObjectsClientContract;
|
||||
subCaseIDs: string[];
|
||||
subCasesMap: Map<string, SavedObject<SubCaseAttributes>>;
|
||||
}): Promise<Map<string, SavedObject<ESCaseAttributes>>> {
|
||||
}): Promise<Map<string, SavedObject<CaseAttributes>>> {
|
||||
const parentIDInfo = getParentIDs({ subCaseIDs, subCasesMap });
|
||||
|
||||
const parentCases = await caseService.getCases({
|
||||
|
@ -148,7 +148,7 @@ async function getParentCases({
|
|||
acc.set(subCaseId, so);
|
||||
});
|
||||
return acc;
|
||||
}, new Map<string, SavedObject<ESCaseAttributes>>());
|
||||
}, new Map<string, SavedObject<CaseAttributes>>());
|
||||
}
|
||||
|
||||
function getValidUpdateRequests(
|
||||
|
|
|
@ -5,113 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SavedObjectsFindResponse } from 'kibana/server';
|
||||
import {
|
||||
CaseConnector,
|
||||
CaseType,
|
||||
ConnectorTypes,
|
||||
ESCaseConnector,
|
||||
ESCasesConfigureAttributes,
|
||||
} from '../../common/api';
|
||||
import { mockCaseConfigure } from '../routes/api/__fixtures__';
|
||||
import { CaseConnector, CaseType, ConnectorTypes } from '../../common/api';
|
||||
import { newCase } from '../routes/api/__mocks__/request_responses';
|
||||
import {
|
||||
transformCaseConnectorToEsConnector,
|
||||
transformESConnectorToCaseConnector,
|
||||
transformNewCase,
|
||||
} from '../common';
|
||||
import { getConnectorFromConfiguration, sortToSnake } from './utils';
|
||||
import { transformNewCase } from '../common';
|
||||
import { sortToSnake } from './utils';
|
||||
|
||||
describe('utils', () => {
|
||||
const caseConnector: CaseConnector = {
|
||||
id: '123',
|
||||
name: 'Jira',
|
||||
type: ConnectorTypes.jira,
|
||||
fields: { issueType: 'Task', priority: 'High', parent: null },
|
||||
};
|
||||
|
||||
const esCaseConnector: ESCaseConnector = {
|
||||
id: '123',
|
||||
name: 'Jira',
|
||||
type: ConnectorTypes.jira,
|
||||
fields: [
|
||||
{ key: 'issueType', value: 'Task' },
|
||||
{ key: 'priority', value: 'High' },
|
||||
{ key: 'parent', value: null },
|
||||
],
|
||||
};
|
||||
|
||||
const caseConfigure: SavedObjectsFindResponse<ESCasesConfigureAttributes> = {
|
||||
saved_objects: [{ ...mockCaseConfigure[0], score: 0 }],
|
||||
total: 1,
|
||||
per_page: 20,
|
||||
page: 1,
|
||||
};
|
||||
|
||||
describe('transformCaseConnectorToEsConnector', () => {
|
||||
it('transform correctly', () => {
|
||||
expect(transformCaseConnectorToEsConnector(caseConnector)).toEqual(esCaseConnector);
|
||||
});
|
||||
|
||||
it('transform correctly with null attributes', () => {
|
||||
// @ts-ignore this is case the connector does not exist for old cases object or configurations
|
||||
expect(transformCaseConnectorToEsConnector(null)).toEqual({
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformESConnectorToCaseConnector', () => {
|
||||
it('transform correctly', () => {
|
||||
expect(transformESConnectorToCaseConnector(esCaseConnector)).toEqual(caseConnector);
|
||||
});
|
||||
|
||||
it('transform correctly with null attributes', () => {
|
||||
// @ts-ignore this is case the connector does not exist for old cases object or configurations
|
||||
expect(transformESConnectorToCaseConnector(null)).toEqual({
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConnectorFromConfiguration', () => {
|
||||
it('transform correctly', () => {
|
||||
expect(getConnectorFromConfiguration(caseConfigure)).toEqual({
|
||||
id: '789',
|
||||
name: 'My connector 3',
|
||||
type: ConnectorTypes.jira,
|
||||
fields: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('transform correctly with no connector', () => {
|
||||
const caseConfigureNoConnector: SavedObjectsFindResponse<ESCasesConfigureAttributes> = {
|
||||
...caseConfigure,
|
||||
saved_objects: [
|
||||
{
|
||||
...mockCaseConfigure[0],
|
||||
// @ts-ignore this is case the connector does not exist for old cases object or configurations
|
||||
attributes: { ...mockCaseConfigure[0].attributes, connector: null },
|
||||
score: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getConnectorFromConfiguration(caseConfigureNoConnector)).toEqual({
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('sortToSnake', () => {
|
||||
it('it transforms status correctly', () => {
|
||||
expect(sortToSnake('status')).toBe('status');
|
||||
|
@ -139,15 +38,11 @@ describe('utils', () => {
|
|||
});
|
||||
|
||||
describe('transformNewCase', () => {
|
||||
const connector: ESCaseConnector = {
|
||||
const connector: CaseConnector = {
|
||||
id: '123',
|
||||
name: 'My connector',
|
||||
type: ConnectorTypes.jira,
|
||||
fields: [
|
||||
{ key: 'issueType', value: 'Task' },
|
||||
{ key: 'priority', value: 'High' },
|
||||
{ key: 'parent', value: null },
|
||||
],
|
||||
fields: { issueType: 'Task', priority: 'High', parent: null },
|
||||
};
|
||||
it('transform correctly', () => {
|
||||
const myCase = {
|
||||
|
@ -166,20 +61,11 @@ describe('utils', () => {
|
|||
"closed_at": null,
|
||||
"closed_by": null,
|
||||
"connector": Object {
|
||||
"fields": Array [
|
||||
Object {
|
||||
"key": "issueType",
|
||||
"value": "Task",
|
||||
},
|
||||
Object {
|
||||
"key": "priority",
|
||||
"value": "High",
|
||||
},
|
||||
Object {
|
||||
"key": "parent",
|
||||
"value": null,
|
||||
},
|
||||
],
|
||||
"fields": Object {
|
||||
"issueType": "Task",
|
||||
"parent": null,
|
||||
"priority": "High",
|
||||
},
|
||||
"id": "123",
|
||||
"name": "My connector",
|
||||
"type": ".jira",
|
||||
|
@ -223,20 +109,11 @@ describe('utils', () => {
|
|||
"closed_at": null,
|
||||
"closed_by": null,
|
||||
"connector": Object {
|
||||
"fields": Array [
|
||||
Object {
|
||||
"key": "issueType",
|
||||
"value": "Task",
|
||||
},
|
||||
Object {
|
||||
"key": "priority",
|
||||
"value": "High",
|
||||
},
|
||||
Object {
|
||||
"key": "parent",
|
||||
"value": null,
|
||||
},
|
||||
],
|
||||
"fields": Object {
|
||||
"issueType": "Task",
|
||||
"parent": null,
|
||||
"priority": "High",
|
||||
},
|
||||
"id": "123",
|
||||
"name": "My connector",
|
||||
"type": ".jira",
|
||||
|
@ -283,20 +160,11 @@ describe('utils', () => {
|
|||
"closed_at": null,
|
||||
"closed_by": null,
|
||||
"connector": Object {
|
||||
"fields": Array [
|
||||
Object {
|
||||
"key": "issueType",
|
||||
"value": "Task",
|
||||
},
|
||||
Object {
|
||||
"key": "priority",
|
||||
"value": "High",
|
||||
},
|
||||
Object {
|
||||
"key": "parent",
|
||||
"value": null,
|
||||
},
|
||||
],
|
||||
"fields": Object {
|
||||
"issueType": "Task",
|
||||
"parent": null,
|
||||
"priority": "High",
|
||||
},
|
||||
"id": "123",
|
||||
"name": "My connector",
|
||||
"type": ".jira",
|
||||
|
|
|
@ -12,20 +12,16 @@ import { fold } from 'fp-ts/lib/Either';
|
|||
import { identity } from 'fp-ts/lib/function';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
|
||||
import { SavedObjectsFindResponse } from 'kibana/server';
|
||||
import { nodeBuilder, KueryNode } from '../../../../../src/plugins/data/common';
|
||||
import { esKuery } from '../../../../../src/plugins/data/server';
|
||||
import {
|
||||
AlertCommentRequestRt,
|
||||
ActionsCommentRequestRt,
|
||||
CASE_SAVED_OBJECT,
|
||||
CaseConnector,
|
||||
CaseStatuses,
|
||||
CaseType,
|
||||
CommentRequest,
|
||||
ConnectorTypes,
|
||||
ContextTypeUserRt,
|
||||
ESCasesConfigureAttributes,
|
||||
excess,
|
||||
OWNER_FIELD,
|
||||
SUB_CASE_SAVED_OBJECT,
|
||||
|
@ -437,31 +433,6 @@ export const getCaseToUpdate = (
|
|||
{ id: queryCase.id, version: queryCase.version }
|
||||
);
|
||||
|
||||
export const getNoneCaseConnector = () => ({
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: null,
|
||||
});
|
||||
|
||||
export const getConnectorFromConfiguration = (
|
||||
caseConfigure: SavedObjectsFindResponse<ESCasesConfigureAttributes>
|
||||
): CaseConnector => {
|
||||
let caseConnector = getNoneCaseConnector();
|
||||
if (
|
||||
caseConfigure.saved_objects.length > 0 &&
|
||||
caseConfigure.saved_objects[0].attributes.connector
|
||||
) {
|
||||
caseConnector = {
|
||||
id: caseConfigure.saved_objects[0].attributes.connector.id,
|
||||
name: caseConfigure.saved_objects[0].attributes.connector.name,
|
||||
type: caseConfigure.saved_objects[0].attributes.connector.type,
|
||||
fields: null,
|
||||
};
|
||||
}
|
||||
return caseConnector;
|
||||
};
|
||||
|
||||
enum SortFieldCase {
|
||||
closedAt = 'closed_at',
|
||||
createdAt = 'created_at',
|
||||
|
|
17
x-pack/plugins/cases/server/common/constants.ts
Normal file
17
x-pack/plugins/cases/server/common/constants.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The name of the saved object reference indicating the action connector ID. This is stored in the Saved Object reference
|
||||
* field's name property.
|
||||
*/
|
||||
export const CONNECTOR_ID_REFERENCE_NAME = 'connectorId';
|
||||
|
||||
/**
|
||||
* The name of the saved object reference indicating the action connector ID that was used to push a case.
|
||||
*/
|
||||
export const PUSH_CONNECTOR_ID_REFERENCE_NAME = 'pushConnectorId';
|
|
@ -9,3 +9,4 @@ export * from './models';
|
|||
export * from './utils';
|
||||
export * from './types';
|
||||
export * from './error';
|
||||
export * from './constants';
|
||||
|
|
|
@ -25,18 +25,13 @@ import {
|
|||
CommentPatchRequest,
|
||||
CommentRequest,
|
||||
CommentType,
|
||||
ESCaseAttributes,
|
||||
MAX_DOCS_PER_PAGE,
|
||||
SUB_CASE_SAVED_OBJECT,
|
||||
SubCaseAttributes,
|
||||
User,
|
||||
CaseAttributes,
|
||||
} from '../../../common';
|
||||
import {
|
||||
transformESConnectorToCaseConnector,
|
||||
flattenCommentSavedObjects,
|
||||
flattenSubCaseSavedObject,
|
||||
transformNewComment,
|
||||
} from '..';
|
||||
import { flattenCommentSavedObjects, flattenSubCaseSavedObject, transformNewComment } from '..';
|
||||
import { AttachmentService, CasesService } from '../../services';
|
||||
import { createCaseError } from '../error';
|
||||
import { countAlertsForID } from '../index';
|
||||
|
@ -52,7 +47,7 @@ interface NewCommentResp {
|
|||
}
|
||||
|
||||
interface CommentableCaseParams {
|
||||
collection: SavedObject<ESCaseAttributes>;
|
||||
collection: SavedObject<CaseAttributes>;
|
||||
subCase?: SavedObject<SubCaseAttributes>;
|
||||
unsecuredSavedObjectsClient: SavedObjectsClientContract;
|
||||
caseService: CasesService;
|
||||
|
@ -65,7 +60,7 @@ interface CommentableCaseParams {
|
|||
* a Sub Case, Case, and Collection.
|
||||
*/
|
||||
export class CommentableCase {
|
||||
private readonly collection: SavedObject<ESCaseAttributes>;
|
||||
private readonly collection: SavedObject<CaseAttributes>;
|
||||
private readonly subCase?: SavedObject<SubCaseAttributes>;
|
||||
private readonly unsecuredSavedObjectsClient: SavedObjectsClientContract;
|
||||
private readonly caseService: CasesService;
|
||||
|
@ -168,6 +163,7 @@ export class CommentableCase {
|
|||
}
|
||||
|
||||
const updatedCase = await this.caseService.patchCase({
|
||||
originalCase: this.collection,
|
||||
unsecuredSavedObjectsClient: this.unsecuredSavedObjectsClient,
|
||||
caseId: this.collection.id,
|
||||
updatedAttributes: {
|
||||
|
@ -305,7 +301,6 @@ export class CommentableCase {
|
|||
version: this.collection.version ?? '0',
|
||||
totalComment,
|
||||
...this.collection.attributes,
|
||||
connector: transformESConnectorToCaseConnector(this.collection.attributes.connector),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -14,11 +14,7 @@ import {
|
|||
CommentRequest,
|
||||
CommentType,
|
||||
} from '../../common/api';
|
||||
import {
|
||||
mockCaseComments,
|
||||
mockCases,
|
||||
mockCaseNoConnectorId,
|
||||
} from '../routes/api/__fixtures__/mock_saved_objects';
|
||||
import { mockCaseComments, mockCases } from '../routes/api/__fixtures__/mock_saved_objects';
|
||||
import {
|
||||
flattenCaseSavedObject,
|
||||
transformNewComment,
|
||||
|
@ -470,14 +466,13 @@ describe('common utils', () => {
|
|||
`);
|
||||
});
|
||||
|
||||
it('inserts missing connector', () => {
|
||||
it('leaves the connector.id in the attributes', () => {
|
||||
const extraCaseData = {
|
||||
totalComment: 2,
|
||||
};
|
||||
|
||||
const res = flattenCaseSavedObject({
|
||||
// @ts-ignore this is to update old case saved objects to include connector
|
||||
savedObject: mockCaseNoConnectorId,
|
||||
savedObject: mockCases[0],
|
||||
...extraCaseData,
|
||||
});
|
||||
|
||||
|
@ -500,7 +495,8 @@ describe('common utils', () => {
|
|||
},
|
||||
"description": "This is a brand new case of a bad meanie defacing data",
|
||||
"external_service": null,
|
||||
"id": "mock-no-connector_id",
|
||||
"id": "mock-id-1",
|
||||
"owner": "securitySolution",
|
||||
"settings": Object {
|
||||
"syncAlerts": true,
|
||||
},
|
||||
|
@ -513,6 +509,7 @@ describe('common utils', () => {
|
|||
"title": "Super Bad Security Issue",
|
||||
"totalAlerts": 0,
|
||||
"totalComment": 2,
|
||||
"type": "individual",
|
||||
"updated_at": "2019-11-25T21:54:48.952Z",
|
||||
"updated_by": Object {
|
||||
"email": "testemail@elastic.co",
|
||||
|
|
|
@ -12,6 +12,7 @@ import { AlertInfo } from '.';
|
|||
|
||||
import {
|
||||
AssociationType,
|
||||
CaseAttributes,
|
||||
CaseConnector,
|
||||
CaseResponse,
|
||||
CasesClientPostRequest,
|
||||
|
@ -24,11 +25,8 @@ import {
|
|||
CommentResponse,
|
||||
CommentsResponse,
|
||||
CommentType,
|
||||
ConnectorTypeFields,
|
||||
ConnectorTypes,
|
||||
ENABLE_CASE_CONNECTOR,
|
||||
ESCaseAttributes,
|
||||
ESCaseConnector,
|
||||
ESConnectorFields,
|
||||
SubCaseAttributes,
|
||||
SubCaseResponse,
|
||||
SubCasesFindResponse,
|
||||
|
@ -55,13 +53,13 @@ export const transformNewCase = ({
|
|||
newCase,
|
||||
username,
|
||||
}: {
|
||||
connector: ESCaseConnector;
|
||||
connector: CaseConnector;
|
||||
createdDate: string;
|
||||
email?: string | null;
|
||||
full_name?: string | null;
|
||||
newCase: CasesClientPostRequest;
|
||||
username?: string | null;
|
||||
}): ESCaseAttributes => ({
|
||||
}): CaseAttributes => ({
|
||||
...newCase,
|
||||
closed_at: null,
|
||||
closed_by: null,
|
||||
|
@ -135,7 +133,7 @@ export const flattenCaseSavedObject = ({
|
|||
subCases,
|
||||
subCaseIds,
|
||||
}: {
|
||||
savedObject: SavedObject<ESCaseAttributes>;
|
||||
savedObject: SavedObject<CaseAttributes>;
|
||||
comments?: Array<SavedObject<CommentAttributes>>;
|
||||
totalComment?: number;
|
||||
totalAlerts?: number;
|
||||
|
@ -148,7 +146,6 @@ export const flattenCaseSavedObject = ({
|
|||
totalComment,
|
||||
totalAlerts,
|
||||
...savedObject.attributes,
|
||||
connector: transformESConnectorToCaseConnector(savedObject.attributes.connector),
|
||||
subCases,
|
||||
subCaseIds: !isEmpty(subCaseIds) ? subCaseIds : undefined,
|
||||
});
|
||||
|
@ -196,47 +193,6 @@ export const flattenCommentSavedObject = (
|
|||
...savedObject.attributes,
|
||||
});
|
||||
|
||||
export const transformCaseConnectorToEsConnector = (connector: CaseConnector): ESCaseConnector => ({
|
||||
id: connector?.id ?? 'none',
|
||||
name: connector?.name ?? 'none',
|
||||
type: connector?.type ?? '.none',
|
||||
fields:
|
||||
connector?.fields != null
|
||||
? Object.entries(connector.fields).reduce<ESConnectorFields>(
|
||||
(acc, [key, value]) => [
|
||||
...acc,
|
||||
{
|
||||
key,
|
||||
value,
|
||||
},
|
||||
],
|
||||
[]
|
||||
)
|
||||
: [],
|
||||
});
|
||||
|
||||
export const transformESConnectorToCaseConnector = (connector?: ESCaseConnector): CaseConnector => {
|
||||
const connectorTypeField = {
|
||||
type: connector?.type ?? '.none',
|
||||
fields:
|
||||
connector && connector.fields != null && connector.fields.length > 0
|
||||
? connector.fields.reduce(
|
||||
(fields, { key, value }) => ({
|
||||
...fields,
|
||||
[key]: value,
|
||||
}),
|
||||
{}
|
||||
)
|
||||
: null,
|
||||
} as ConnectorTypeFields;
|
||||
|
||||
return {
|
||||
id: connector?.id ?? 'none',
|
||||
name: connector?.name ?? 'none',
|
||||
...connectorTypeField,
|
||||
};
|
||||
};
|
||||
|
||||
export const getIDsAndIndicesAsArrays = (
|
||||
comment: CommentRequestAlertType
|
||||
): { ids: string[]; indices: string[] } => {
|
||||
|
@ -430,3 +386,15 @@ export function checkEnabledCaseConnectorOrThrow(subCaseID: string | undefined)
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a connector that indicates that no connector was set.
|
||||
*
|
||||
* @returns the 'none' connector
|
||||
*/
|
||||
export const getNoneCaseConnector = () => ({
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: null,
|
||||
});
|
||||
|
|
|
@ -8,17 +8,16 @@
|
|||
import { SavedObject } from 'kibana/server';
|
||||
import {
|
||||
AssociationType,
|
||||
CaseAttributes,
|
||||
CaseStatuses,
|
||||
CaseType,
|
||||
CommentAttributes,
|
||||
CommentType,
|
||||
ConnectorTypes,
|
||||
ESCaseAttributes,
|
||||
ESCasesConfigureAttributes,
|
||||
SECURITY_SOLUTION_OWNER,
|
||||
} from '../../../../common';
|
||||
|
||||
export const mockCases: Array<SavedObject<ESCaseAttributes>> = [
|
||||
export const mockCases: Array<SavedObject<CaseAttributes>> = [
|
||||
{
|
||||
type: 'cases',
|
||||
id: 'mock-id-1',
|
||||
|
@ -29,7 +28,7 @@ export const mockCases: Array<SavedObject<ESCaseAttributes>> = [
|
|||
id: 'none',
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: [],
|
||||
fields: null,
|
||||
},
|
||||
created_at: '2019-11-25T21:54:48.952Z',
|
||||
created_by: {
|
||||
|
@ -68,7 +67,7 @@ export const mockCases: Array<SavedObject<ESCaseAttributes>> = [
|
|||
id: 'none',
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: [],
|
||||
fields: null,
|
||||
},
|
||||
created_at: '2019-11-25T22:32:00.900Z',
|
||||
created_by: {
|
||||
|
@ -107,11 +106,7 @@ export const mockCases: Array<SavedObject<ESCaseAttributes>> = [
|
|||
id: '123',
|
||||
name: 'My connector',
|
||||
type: ConnectorTypes.jira,
|
||||
fields: [
|
||||
{ key: 'issueType', value: 'Task' },
|
||||
{ key: 'priority', value: 'High' },
|
||||
{ key: 'parent', value: null },
|
||||
],
|
||||
fields: { issueType: 'Task', priority: 'High', parent: null },
|
||||
},
|
||||
created_at: '2019-11-25T22:32:17.947Z',
|
||||
created_by: {
|
||||
|
@ -154,11 +149,7 @@ export const mockCases: Array<SavedObject<ESCaseAttributes>> = [
|
|||
id: '123',
|
||||
name: 'My connector',
|
||||
type: ConnectorTypes.jira,
|
||||
fields: [
|
||||
{ key: 'issueType', value: 'Task' },
|
||||
{ key: 'priority', value: 'High' },
|
||||
{ key: 'parent', value: null },
|
||||
],
|
||||
fields: { issueType: 'Task', priority: 'High', parent: null },
|
||||
},
|
||||
created_at: '2019-11-25T22:32:17.947Z',
|
||||
created_by: {
|
||||
|
@ -189,38 +180,6 @@ export const mockCases: Array<SavedObject<ESCaseAttributes>> = [
|
|||
},
|
||||
];
|
||||
|
||||
export const mockCaseNoConnectorId: SavedObject<Partial<ESCaseAttributes>> = {
|
||||
type: 'cases',
|
||||
id: 'mock-no-connector_id',
|
||||
attributes: {
|
||||
closed_at: null,
|
||||
closed_by: null,
|
||||
created_at: '2019-11-25T21:54:48.952Z',
|
||||
created_by: {
|
||||
full_name: 'elastic',
|
||||
email: 'testemail@elastic.co',
|
||||
username: 'elastic',
|
||||
},
|
||||
description: 'This is a brand new case of a bad meanie defacing data',
|
||||
external_service: null,
|
||||
title: 'Super Bad Security Issue',
|
||||
status: CaseStatuses.open,
|
||||
tags: ['defacement'],
|
||||
updated_at: '2019-11-25T21:54:48.952Z',
|
||||
updated_by: {
|
||||
full_name: 'elastic',
|
||||
email: 'testemail@elastic.co',
|
||||
username: 'elastic',
|
||||
},
|
||||
settings: {
|
||||
syncAlerts: true,
|
||||
},
|
||||
},
|
||||
references: [],
|
||||
updated_at: '2019-11-25T21:54:48.952Z',
|
||||
version: 'WzAsMV0=',
|
||||
};
|
||||
|
||||
export const mockCasesErrorTriggerData = [
|
||||
{
|
||||
id: 'valid-id',
|
||||
|
@ -446,35 +405,3 @@ export const mockCaseComments: Array<SavedObject<CommentAttributes>> = [
|
|||
version: 'WzYsMV0=',
|
||||
},
|
||||
];
|
||||
|
||||
export const mockCaseConfigure: Array<SavedObject<ESCasesConfigureAttributes>> = [
|
||||
{
|
||||
type: 'cases-configure',
|
||||
id: 'mock-configuration-1',
|
||||
attributes: {
|
||||
connector: {
|
||||
id: '789',
|
||||
name: 'My connector 3',
|
||||
type: ConnectorTypes.jira,
|
||||
fields: null,
|
||||
},
|
||||
closure_type: 'close-by-user',
|
||||
created_at: '2020-04-09T09:43:51.778Z',
|
||||
created_by: {
|
||||
full_name: 'elastic',
|
||||
email: 'testemail@elastic.co',
|
||||
username: 'elastic',
|
||||
},
|
||||
updated_at: '2020-04-09T09:43:51.778Z',
|
||||
updated_by: {
|
||||
full_name: 'elastic',
|
||||
email: 'testemail@elastic.co',
|
||||
username: 'elastic',
|
||||
},
|
||||
owner: SECURITY_SOLUTION_OWNER,
|
||||
},
|
||||
references: [],
|
||||
updated_at: '2020-04-09T09:43:51.778Z',
|
||||
version: 'WzYsMV0=',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -52,9 +52,6 @@ export const caseSavedObjectType: SavedObjectsType = {
|
|||
},
|
||||
connector: {
|
||||
properties: {
|
||||
id: {
|
||||
type: 'keyword',
|
||||
},
|
||||
name: {
|
||||
type: 'text',
|
||||
},
|
||||
|
@ -91,9 +88,6 @@ export const caseSavedObjectType: SavedObjectsType = {
|
|||
},
|
||||
},
|
||||
},
|
||||
connector_id: {
|
||||
type: 'keyword',
|
||||
},
|
||||
connector_name: {
|
||||
type: 'keyword',
|
||||
},
|
||||
|
|
|
@ -33,9 +33,6 @@ export const caseConfigureSavedObjectType: SavedObjectsType = {
|
|||
},
|
||||
connector: {
|
||||
properties: {
|
||||
id: {
|
||||
type: 'keyword',
|
||||
},
|
||||
name: {
|
||||
type: 'text',
|
||||
},
|
||||
|
|
|
@ -0,0 +1,351 @@
|
|||
/*
|
||||
* 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 { SavedObjectSanitizedDoc } from 'kibana/server';
|
||||
import {
|
||||
CaseAttributes,
|
||||
CaseFullExternalService,
|
||||
CASE_SAVED_OBJECT,
|
||||
ConnectorTypes,
|
||||
noneConnectorId,
|
||||
} from '../../../common';
|
||||
import { getNoneCaseConnector } from '../../common';
|
||||
import { createExternalService, ESCaseConnectorWithId } from '../../services/test_utils';
|
||||
import { caseConnectorIdMigration } from './cases';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const create_7_14_0_case = ({
|
||||
connector,
|
||||
externalService,
|
||||
}: { connector?: ESCaseConnectorWithId; externalService?: CaseFullExternalService } = {}) => ({
|
||||
type: CASE_SAVED_OBJECT,
|
||||
id: '1',
|
||||
attributes: {
|
||||
connector,
|
||||
external_service: externalService,
|
||||
},
|
||||
});
|
||||
|
||||
describe('7.15.0 connector ID migration', () => {
|
||||
it('does not create a reference when the connector.id is none', () => {
|
||||
const caseSavedObject = create_7_14_0_case({ connector: getNoneCaseConnector() });
|
||||
|
||||
const migratedConnector = caseConnectorIdMigration(
|
||||
caseSavedObject
|
||||
) as SavedObjectSanitizedDoc<CaseAttributes>;
|
||||
|
||||
expect(migratedConnector.references.length).toBe(0);
|
||||
expect(migratedConnector.attributes.connector).not.toHaveProperty('id');
|
||||
expect(migratedConnector.attributes.connector).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"fields": null,
|
||||
"name": "none",
|
||||
"type": ".none",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('does not create a reference when the connector is undefined', () => {
|
||||
const caseSavedObject = create_7_14_0_case();
|
||||
|
||||
const migratedConnector = caseConnectorIdMigration(
|
||||
caseSavedObject
|
||||
) as SavedObjectSanitizedDoc<CaseAttributes>;
|
||||
|
||||
expect(migratedConnector.references.length).toBe(0);
|
||||
expect(migratedConnector.attributes.connector).not.toHaveProperty('id');
|
||||
expect(migratedConnector.attributes.connector).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"fields": null,
|
||||
"name": "none",
|
||||
"type": ".none",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('sets the connector to the default none connector if the connector.id is undefined', () => {
|
||||
const caseSavedObject = create_7_14_0_case({
|
||||
connector: {
|
||||
fields: null,
|
||||
name: ConnectorTypes.jira,
|
||||
type: ConnectorTypes.jira,
|
||||
} as ESCaseConnectorWithId,
|
||||
});
|
||||
|
||||
const migratedConnector = caseConnectorIdMigration(
|
||||
caseSavedObject
|
||||
) as SavedObjectSanitizedDoc<CaseAttributes>;
|
||||
|
||||
expect(migratedConnector.references.length).toBe(0);
|
||||
expect(migratedConnector.attributes.connector).not.toHaveProperty('id');
|
||||
expect(migratedConnector.attributes.connector).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"fields": null,
|
||||
"name": "none",
|
||||
"type": ".none",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('does not create a reference when the external_service is null', () => {
|
||||
const caseSavedObject = create_7_14_0_case({ externalService: null });
|
||||
|
||||
const migratedConnector = caseConnectorIdMigration(
|
||||
caseSavedObject
|
||||
) as SavedObjectSanitizedDoc<CaseAttributes>;
|
||||
|
||||
expect(migratedConnector.references.length).toBe(0);
|
||||
expect(migratedConnector.attributes.external_service).toBeNull();
|
||||
});
|
||||
|
||||
it('does not create a reference when the external_service is undefined and sets external_service to null', () => {
|
||||
const caseSavedObject = create_7_14_0_case();
|
||||
|
||||
const migratedConnector = caseConnectorIdMigration(
|
||||
caseSavedObject
|
||||
) as SavedObjectSanitizedDoc<CaseAttributes>;
|
||||
|
||||
expect(migratedConnector.references.length).toBe(0);
|
||||
expect(migratedConnector.attributes.external_service).toBeNull();
|
||||
});
|
||||
|
||||
it('does not create a reference when the external_service.connector_id is none', () => {
|
||||
const caseSavedObject = create_7_14_0_case({
|
||||
externalService: createExternalService({ connector_id: noneConnectorId }),
|
||||
});
|
||||
|
||||
const migratedConnector = caseConnectorIdMigration(
|
||||
caseSavedObject
|
||||
) as SavedObjectSanitizedDoc<CaseAttributes>;
|
||||
|
||||
expect(migratedConnector.references.length).toBe(0);
|
||||
expect(migratedConnector.attributes.external_service).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"connector_name": ".jira",
|
||||
"external_id": "100",
|
||||
"external_title": "awesome",
|
||||
"external_url": "http://www.google.com",
|
||||
"pushed_at": "2019-11-25T21:54:48.952Z",
|
||||
"pushed_by": Object {
|
||||
"email": "testemail@elastic.co",
|
||||
"full_name": "elastic",
|
||||
"username": "elastic",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('preserves the existing references when migrating', () => {
|
||||
const caseSavedObject = {
|
||||
...create_7_14_0_case(),
|
||||
references: [{ id: '1', name: 'awesome', type: 'hello' }],
|
||||
};
|
||||
|
||||
const migratedConnector = caseConnectorIdMigration(
|
||||
caseSavedObject
|
||||
) as SavedObjectSanitizedDoc<CaseAttributes>;
|
||||
|
||||
expect(migratedConnector.references.length).toBe(1);
|
||||
expect(migratedConnector.references).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "awesome",
|
||||
"type": "hello",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('creates a connector reference and removes the connector.id field', () => {
|
||||
const caseSavedObject = create_7_14_0_case({
|
||||
connector: {
|
||||
id: '123',
|
||||
fields: null,
|
||||
name: 'connector',
|
||||
type: ConnectorTypes.jira,
|
||||
},
|
||||
});
|
||||
|
||||
const migratedConnector = caseConnectorIdMigration(
|
||||
caseSavedObject
|
||||
) as SavedObjectSanitizedDoc<CaseAttributes>;
|
||||
|
||||
expect(migratedConnector.references.length).toBe(1);
|
||||
expect(migratedConnector.attributes.connector).not.toHaveProperty('id');
|
||||
expect(migratedConnector.attributes.connector).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"fields": null,
|
||||
"name": "connector",
|
||||
"type": ".jira",
|
||||
}
|
||||
`);
|
||||
expect(migratedConnector.references).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"id": "123",
|
||||
"name": "connectorId",
|
||||
"type": "action",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('creates a push connector reference and removes the connector_id field', () => {
|
||||
const caseSavedObject = create_7_14_0_case({
|
||||
externalService: {
|
||||
connector_id: '100',
|
||||
connector_name: '.jira',
|
||||
external_id: '100',
|
||||
external_title: 'awesome',
|
||||
external_url: 'http://www.google.com',
|
||||
pushed_at: '2019-11-25T21:54:48.952Z',
|
||||
pushed_by: {
|
||||
full_name: 'elastic',
|
||||
email: 'testemail@elastic.co',
|
||||
username: 'elastic',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const migratedConnector = caseConnectorIdMigration(
|
||||
caseSavedObject
|
||||
) as SavedObjectSanitizedDoc<CaseAttributes>;
|
||||
|
||||
expect(migratedConnector.references.length).toBe(1);
|
||||
expect(migratedConnector.attributes.external_service).not.toHaveProperty('connector_id');
|
||||
expect(migratedConnector.attributes.external_service).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"connector_name": ".jira",
|
||||
"external_id": "100",
|
||||
"external_title": "awesome",
|
||||
"external_url": "http://www.google.com",
|
||||
"pushed_at": "2019-11-25T21:54:48.952Z",
|
||||
"pushed_by": Object {
|
||||
"email": "testemail@elastic.co",
|
||||
"full_name": "elastic",
|
||||
"username": "elastic",
|
||||
},
|
||||
}
|
||||
`);
|
||||
expect(migratedConnector.references).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"id": "100",
|
||||
"name": "pushConnectorId",
|
||||
"type": "action",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('does not create a reference and preserves the existing external_service fields when connector_id is null', () => {
|
||||
const caseSavedObject = create_7_14_0_case({
|
||||
externalService: {
|
||||
connector_id: null,
|
||||
connector_name: '.jira',
|
||||
external_id: '100',
|
||||
external_title: 'awesome',
|
||||
external_url: 'http://www.google.com',
|
||||
pushed_at: '2019-11-25T21:54:48.952Z',
|
||||
pushed_by: {
|
||||
full_name: 'elastic',
|
||||
email: 'testemail@elastic.co',
|
||||
username: 'elastic',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const migratedConnector = caseConnectorIdMigration(
|
||||
caseSavedObject
|
||||
) as SavedObjectSanitizedDoc<CaseAttributes>;
|
||||
|
||||
expect(migratedConnector.references.length).toBe(0);
|
||||
expect(migratedConnector.attributes.external_service).not.toHaveProperty('connector_id');
|
||||
expect(migratedConnector.attributes.external_service).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"connector_name": ".jira",
|
||||
"external_id": "100",
|
||||
"external_title": "awesome",
|
||||
"external_url": "http://www.google.com",
|
||||
"pushed_at": "2019-11-25T21:54:48.952Z",
|
||||
"pushed_by": Object {
|
||||
"email": "testemail@elastic.co",
|
||||
"full_name": "elastic",
|
||||
"username": "elastic",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('migrates both connector and external_service when provided', () => {
|
||||
const caseSavedObject = create_7_14_0_case({
|
||||
externalService: {
|
||||
connector_id: '100',
|
||||
connector_name: '.jira',
|
||||
external_id: '100',
|
||||
external_title: 'awesome',
|
||||
external_url: 'http://www.google.com',
|
||||
pushed_at: '2019-11-25T21:54:48.952Z',
|
||||
pushed_by: {
|
||||
full_name: 'elastic',
|
||||
email: 'testemail@elastic.co',
|
||||
username: 'elastic',
|
||||
},
|
||||
},
|
||||
connector: {
|
||||
id: '123',
|
||||
fields: null,
|
||||
name: 'connector',
|
||||
type: ConnectorTypes.jira,
|
||||
},
|
||||
});
|
||||
|
||||
const migratedConnector = caseConnectorIdMigration(
|
||||
caseSavedObject
|
||||
) as SavedObjectSanitizedDoc<CaseAttributes>;
|
||||
|
||||
expect(migratedConnector.references.length).toBe(2);
|
||||
expect(migratedConnector.attributes.external_service).not.toHaveProperty('connector_id');
|
||||
expect(migratedConnector.attributes.external_service).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"connector_name": ".jira",
|
||||
"external_id": "100",
|
||||
"external_title": "awesome",
|
||||
"external_url": "http://www.google.com",
|
||||
"pushed_at": "2019-11-25T21:54:48.952Z",
|
||||
"pushed_by": Object {
|
||||
"email": "testemail@elastic.co",
|
||||
"full_name": "elastic",
|
||||
"username": "elastic",
|
||||
},
|
||||
}
|
||||
`);
|
||||
expect(migratedConnector.attributes.connector).not.toHaveProperty('id');
|
||||
expect(migratedConnector.attributes.connector).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"fields": null,
|
||||
"name": "connector",
|
||||
"type": ".jira",
|
||||
}
|
||||
`);
|
||||
expect(migratedConnector.references).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"id": "123",
|
||||
"name": "connectorId",
|
||||
"type": "action",
|
||||
},
|
||||
Object {
|
||||
"id": "100",
|
||||
"name": "pushConnectorId",
|
||||
"type": "action",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
|
||||
import { addOwnerToSO, SanitizedCaseOwner } from '.';
|
||||
import {
|
||||
SavedObjectUnsanitizedDoc,
|
||||
SavedObjectSanitizedDoc,
|
||||
} from '../../../../../../src/core/server';
|
||||
import { ESConnectorFields } from '../../services';
|
||||
import { ConnectorTypes, CaseType } from '../../../common';
|
||||
import { transformConnectorIdToReference, transformPushConnectorIdToReference } from './utils';
|
||||
|
||||
interface UnsanitizedCaseConnector {
|
||||
connector_id: string;
|
||||
}
|
||||
|
||||
interface SanitizedCaseConnector {
|
||||
connector: {
|
||||
id: string;
|
||||
name: string | null;
|
||||
type: string | null;
|
||||
fields: null | ESConnectorFields;
|
||||
};
|
||||
}
|
||||
|
||||
interface SanitizedCaseSettings {
|
||||
settings: {
|
||||
syncAlerts: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface SanitizedCaseType {
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface ConnectorIdFields {
|
||||
connector?: { id?: string };
|
||||
external_service?: { connector_id?: string | null } | null;
|
||||
}
|
||||
|
||||
export const caseConnectorIdMigration = (
|
||||
doc: SavedObjectUnsanitizedDoc<ConnectorIdFields>
|
||||
): SavedObjectSanitizedDoc<unknown> => {
|
||||
// removing the id field since it will be stored in the references instead
|
||||
const { connector, external_service, ...restAttributes } = doc.attributes;
|
||||
|
||||
const { transformedConnector, references: connectorReferences } = transformConnectorIdToReference(
|
||||
connector
|
||||
);
|
||||
|
||||
const {
|
||||
transformedPushConnector,
|
||||
references: pushConnectorReferences,
|
||||
} = transformPushConnectorIdToReference(external_service);
|
||||
|
||||
const { references = [] } = doc;
|
||||
|
||||
return {
|
||||
...doc,
|
||||
attributes: {
|
||||
...restAttributes,
|
||||
...transformedConnector,
|
||||
...transformedPushConnector,
|
||||
},
|
||||
references: [...references, ...connectorReferences, ...pushConnectorReferences],
|
||||
};
|
||||
};
|
||||
|
||||
export const caseMigrations = {
|
||||
'7.10.0': (
|
||||
doc: SavedObjectUnsanitizedDoc<UnsanitizedCaseConnector>
|
||||
): SavedObjectSanitizedDoc<SanitizedCaseConnector> => {
|
||||
const { connector_id, ...attributesWithoutConnectorId } = doc.attributes;
|
||||
|
||||
return {
|
||||
...doc,
|
||||
attributes: {
|
||||
...attributesWithoutConnectorId,
|
||||
connector: {
|
||||
id: connector_id ?? 'none',
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: null,
|
||||
},
|
||||
},
|
||||
references: doc.references || [],
|
||||
};
|
||||
},
|
||||
'7.11.0': (
|
||||
doc: SavedObjectUnsanitizedDoc<Record<string, unknown>>
|
||||
): SavedObjectSanitizedDoc<SanitizedCaseSettings> => {
|
||||
return {
|
||||
...doc,
|
||||
attributes: {
|
||||
...doc.attributes,
|
||||
settings: {
|
||||
syncAlerts: true,
|
||||
},
|
||||
},
|
||||
references: doc.references || [],
|
||||
};
|
||||
},
|
||||
'7.12.0': (
|
||||
doc: SavedObjectUnsanitizedDoc<SanitizedCaseConnector>
|
||||
): SavedObjectSanitizedDoc<SanitizedCaseType & SanitizedCaseConnector> => {
|
||||
const { fields, type } = doc.attributes.connector;
|
||||
return {
|
||||
...doc,
|
||||
attributes: {
|
||||
...doc.attributes,
|
||||
type: CaseType.individual,
|
||||
connector: {
|
||||
...doc.attributes.connector,
|
||||
fields:
|
||||
Array.isArray(fields) && fields.length > 0 && type === ConnectorTypes.serviceNowITSM
|
||||
? [...fields, { key: 'category', value: null }, { key: 'subcategory', value: null }]
|
||||
: fields,
|
||||
},
|
||||
},
|
||||
references: doc.references || [],
|
||||
};
|
||||
},
|
||||
'7.14.0': (
|
||||
doc: SavedObjectUnsanitizedDoc<Record<string, unknown>>
|
||||
): SavedObjectSanitizedDoc<SanitizedCaseOwner> => {
|
||||
return addOwnerToSO(doc);
|
||||
},
|
||||
'7.15.0': caseConnectorIdMigration,
|
||||
};
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* 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 { SavedObjectSanitizedDoc } from 'kibana/server';
|
||||
import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server';
|
||||
import {
|
||||
CASE_CONFIGURE_SAVED_OBJECT,
|
||||
ConnectorTypes,
|
||||
SECURITY_SOLUTION_OWNER,
|
||||
} from '../../../common';
|
||||
import { getNoneCaseConnector, CONNECTOR_ID_REFERENCE_NAME } from '../../common';
|
||||
import { ESCaseConnectorWithId } from '../../services/test_utils';
|
||||
import { ESCasesConfigureAttributes } from '../../services/configure/types';
|
||||
import { configureConnectorIdMigration } from './configuration';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const create_7_14_0_configSchema = (connector?: ESCaseConnectorWithId) => ({
|
||||
type: CASE_CONFIGURE_SAVED_OBJECT,
|
||||
id: '1',
|
||||
attributes: {
|
||||
connector,
|
||||
closure_type: 'close-by-pushing',
|
||||
owner: SECURITY_SOLUTION_OWNER,
|
||||
created_at: '2020-04-09T09:43:51.778Z',
|
||||
created_by: {
|
||||
full_name: 'elastic',
|
||||
email: 'testemail@elastic.co',
|
||||
username: 'elastic',
|
||||
},
|
||||
updated_at: '2020-04-09T09:43:51.778Z',
|
||||
updated_by: {
|
||||
full_name: 'elastic',
|
||||
email: 'testemail@elastic.co',
|
||||
username: 'elastic',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('7.15.0 connector ID migration', () => {
|
||||
it('does not create a reference when the connector ID is none', () => {
|
||||
const configureSavedObject = create_7_14_0_configSchema(getNoneCaseConnector());
|
||||
|
||||
const migratedConnector = configureConnectorIdMigration(
|
||||
configureSavedObject
|
||||
) as SavedObjectSanitizedDoc<ESCasesConfigureAttributes>;
|
||||
|
||||
expect(migratedConnector.references.length).toBe(0);
|
||||
expect(migratedConnector.attributes.connector).not.toHaveProperty('id');
|
||||
});
|
||||
|
||||
it('does not create a reference when the connector is undefined and defaults it to the none connector', () => {
|
||||
const configureSavedObject = create_7_14_0_configSchema();
|
||||
|
||||
const migratedConnector = configureConnectorIdMigration(
|
||||
configureSavedObject
|
||||
) as SavedObjectSanitizedDoc<ESCasesConfigureAttributes>;
|
||||
|
||||
expect(migratedConnector.references.length).toBe(0);
|
||||
expect(migratedConnector.attributes.connector).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"fields": null,
|
||||
"name": "none",
|
||||
"type": ".none",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('creates a reference using the connector id', () => {
|
||||
const configureSavedObject = create_7_14_0_configSchema({
|
||||
id: '123',
|
||||
fields: null,
|
||||
name: 'connector',
|
||||
type: ConnectorTypes.jira,
|
||||
});
|
||||
|
||||
const migratedConnector = configureConnectorIdMigration(
|
||||
configureSavedObject
|
||||
) as SavedObjectSanitizedDoc<ESCasesConfigureAttributes>;
|
||||
|
||||
expect(migratedConnector.references).toEqual([
|
||||
{ id: '123', type: ACTION_SAVED_OBJECT_TYPE, name: CONNECTOR_ID_REFERENCE_NAME },
|
||||
]);
|
||||
expect(migratedConnector.attributes.connector).not.toHaveProperty('id');
|
||||
});
|
||||
|
||||
it('returns the other attributes and default connector when the connector is undefined', () => {
|
||||
const configureSavedObject = create_7_14_0_configSchema();
|
||||
|
||||
const migratedConnector = configureConnectorIdMigration(
|
||||
configureSavedObject
|
||||
) as SavedObjectSanitizedDoc<ESCasesConfigureAttributes>;
|
||||
|
||||
expect(migratedConnector).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"closure_type": "close-by-pushing",
|
||||
"connector": Object {
|
||||
"fields": null,
|
||||
"name": "none",
|
||||
"type": ".none",
|
||||
},
|
||||
"created_at": "2020-04-09T09:43:51.778Z",
|
||||
"created_by": Object {
|
||||
"email": "testemail@elastic.co",
|
||||
"full_name": "elastic",
|
||||
"username": "elastic",
|
||||
},
|
||||
"owner": "securitySolution",
|
||||
"updated_at": "2020-04-09T09:43:51.778Z",
|
||||
"updated_by": Object {
|
||||
"email": "testemail@elastic.co",
|
||||
"full_name": "elastic",
|
||||
"username": "elastic",
|
||||
},
|
||||
},
|
||||
"id": "1",
|
||||
"references": Array [],
|
||||
"type": "cases-configure",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
|
||||
import {
|
||||
SavedObjectUnsanitizedDoc,
|
||||
SavedObjectSanitizedDoc,
|
||||
} from '../../../../../../src/core/server';
|
||||
import { ConnectorTypes } from '../../../common';
|
||||
import { addOwnerToSO, SanitizedCaseOwner } from '.';
|
||||
import { transformConnectorIdToReference } from './utils';
|
||||
|
||||
interface UnsanitizedConfigureConnector {
|
||||
connector_id: string;
|
||||
connector_name: string;
|
||||
}
|
||||
|
||||
interface SanitizedConfigureConnector {
|
||||
connector: {
|
||||
id: string;
|
||||
name: string | null;
|
||||
type: string | null;
|
||||
fields: null;
|
||||
};
|
||||
}
|
||||
|
||||
export const configureConnectorIdMigration = (
|
||||
doc: SavedObjectUnsanitizedDoc<{ connector?: { id: string } }>
|
||||
): SavedObjectSanitizedDoc<unknown> => {
|
||||
// removing the id field since it will be stored in the references instead
|
||||
const { connector, ...restAttributes } = doc.attributes;
|
||||
const { transformedConnector, references: connectorReferences } = transformConnectorIdToReference(
|
||||
connector
|
||||
);
|
||||
const { references = [] } = doc;
|
||||
|
||||
return {
|
||||
...doc,
|
||||
attributes: {
|
||||
...restAttributes,
|
||||
...transformedConnector,
|
||||
},
|
||||
references: [...references, ...connectorReferences],
|
||||
};
|
||||
};
|
||||
|
||||
export const configureMigrations = {
|
||||
'7.10.0': (
|
||||
doc: SavedObjectUnsanitizedDoc<UnsanitizedConfigureConnector>
|
||||
): SavedObjectSanitizedDoc<SanitizedConfigureConnector> => {
|
||||
const { connector_id, connector_name, ...restAttributes } = doc.attributes;
|
||||
|
||||
return {
|
||||
...doc,
|
||||
attributes: {
|
||||
...restAttributes,
|
||||
connector: {
|
||||
id: connector_id ?? 'none',
|
||||
name: connector_name ?? 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: null,
|
||||
},
|
||||
},
|
||||
references: doc.references || [],
|
||||
};
|
||||
},
|
||||
'7.14.0': (
|
||||
doc: SavedObjectUnsanitizedDoc<Record<string, unknown>>
|
||||
): SavedObjectSanitizedDoc<SanitizedCaseOwner> => {
|
||||
return addOwnerToSO(doc);
|
||||
},
|
||||
'7.15.0': configureConnectorIdMigration,
|
||||
};
|
|
@ -7,42 +7,19 @@
|
|||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
|
||||
import { SavedObjectUnsanitizedDoc, SavedObjectSanitizedDoc } from '../../../../../src/core/server';
|
||||
import {
|
||||
SavedObjectUnsanitizedDoc,
|
||||
SavedObjectSanitizedDoc,
|
||||
} from '../../../../../../src/core/server';
|
||||
import {
|
||||
ConnectorTypes,
|
||||
CommentType,
|
||||
CaseType,
|
||||
AssociationType,
|
||||
ESConnectorFields,
|
||||
SECURITY_SOLUTION_OWNER,
|
||||
} from '../../common';
|
||||
} from '../../../common';
|
||||
|
||||
interface UnsanitizedCaseConnector {
|
||||
connector_id: string;
|
||||
}
|
||||
|
||||
interface UnsanitizedConfigureConnector {
|
||||
connector_id: string;
|
||||
connector_name: string;
|
||||
}
|
||||
|
||||
interface SanitizedCaseConnector {
|
||||
connector: {
|
||||
id: string;
|
||||
name: string | null;
|
||||
type: string | null;
|
||||
fields: null | ESConnectorFields;
|
||||
};
|
||||
}
|
||||
|
||||
interface SanitizedConfigureConnector {
|
||||
connector: {
|
||||
id: string;
|
||||
name: string | null;
|
||||
type: string | null;
|
||||
fields: null;
|
||||
};
|
||||
}
|
||||
export { caseMigrations } from './cases';
|
||||
export { configureMigrations } from './configuration';
|
||||
|
||||
interface UserActions {
|
||||
action_field: string[];
|
||||
|
@ -50,21 +27,11 @@ interface UserActions {
|
|||
old_value: string;
|
||||
}
|
||||
|
||||
interface SanitizedCaseSettings {
|
||||
settings: {
|
||||
syncAlerts: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface SanitizedCaseType {
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface SanitizedCaseOwner {
|
||||
export interface SanitizedCaseOwner {
|
||||
owner: string;
|
||||
}
|
||||
|
||||
const addOwnerToSO = <T = Record<string, unknown>>(
|
||||
export const addOwnerToSO = <T = Record<string, unknown>>(
|
||||
doc: SavedObjectUnsanitizedDoc<T>
|
||||
): SavedObjectSanitizedDoc<SanitizedCaseOwner> => ({
|
||||
...doc,
|
||||
|
@ -75,94 +42,6 @@ const addOwnerToSO = <T = Record<string, unknown>>(
|
|||
references: doc.references || [],
|
||||
});
|
||||
|
||||
export const caseMigrations = {
|
||||
'7.10.0': (
|
||||
doc: SavedObjectUnsanitizedDoc<UnsanitizedCaseConnector>
|
||||
): SavedObjectSanitizedDoc<SanitizedCaseConnector> => {
|
||||
const { connector_id, ...attributesWithoutConnectorId } = doc.attributes;
|
||||
|
||||
return {
|
||||
...doc,
|
||||
attributes: {
|
||||
...attributesWithoutConnectorId,
|
||||
connector: {
|
||||
id: connector_id ?? 'none',
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: null,
|
||||
},
|
||||
},
|
||||
references: doc.references || [],
|
||||
};
|
||||
},
|
||||
'7.11.0': (
|
||||
doc: SavedObjectUnsanitizedDoc<Record<string, unknown>>
|
||||
): SavedObjectSanitizedDoc<SanitizedCaseSettings> => {
|
||||
return {
|
||||
...doc,
|
||||
attributes: {
|
||||
...doc.attributes,
|
||||
settings: {
|
||||
syncAlerts: true,
|
||||
},
|
||||
},
|
||||
references: doc.references || [],
|
||||
};
|
||||
},
|
||||
'7.12.0': (
|
||||
doc: SavedObjectUnsanitizedDoc<SanitizedCaseConnector>
|
||||
): SavedObjectSanitizedDoc<SanitizedCaseType & SanitizedCaseConnector> => {
|
||||
const { fields, type } = doc.attributes.connector;
|
||||
return {
|
||||
...doc,
|
||||
attributes: {
|
||||
...doc.attributes,
|
||||
type: CaseType.individual,
|
||||
connector: {
|
||||
...doc.attributes.connector,
|
||||
fields:
|
||||
Array.isArray(fields) && fields.length > 0 && type === ConnectorTypes.serviceNowITSM
|
||||
? [...fields, { key: 'category', value: null }, { key: 'subcategory', value: null }]
|
||||
: fields,
|
||||
},
|
||||
},
|
||||
references: doc.references || [],
|
||||
};
|
||||
},
|
||||
'7.14.0': (
|
||||
doc: SavedObjectUnsanitizedDoc<Record<string, unknown>>
|
||||
): SavedObjectSanitizedDoc<SanitizedCaseOwner> => {
|
||||
return addOwnerToSO(doc);
|
||||
},
|
||||
};
|
||||
|
||||
export const configureMigrations = {
|
||||
'7.10.0': (
|
||||
doc: SavedObjectUnsanitizedDoc<UnsanitizedConfigureConnector>
|
||||
): SavedObjectSanitizedDoc<SanitizedConfigureConnector> => {
|
||||
const { connector_id, connector_name, ...restAttributes } = doc.attributes;
|
||||
|
||||
return {
|
||||
...doc,
|
||||
attributes: {
|
||||
...restAttributes,
|
||||
connector: {
|
||||
id: connector_id ?? 'none',
|
||||
name: connector_name ?? 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: null,
|
||||
},
|
||||
},
|
||||
references: doc.references || [],
|
||||
};
|
||||
},
|
||||
'7.14.0': (
|
||||
doc: SavedObjectUnsanitizedDoc<Record<string, unknown>>
|
||||
): SavedObjectSanitizedDoc<SanitizedCaseOwner> => {
|
||||
return addOwnerToSO(doc);
|
||||
},
|
||||
};
|
||||
|
||||
export const userActionsMigrations = {
|
||||
'7.10.0': (doc: SavedObjectUnsanitizedDoc<UserActions>): SavedObjectSanitizedDoc<UserActions> => {
|
||||
const { action_field, new_value, old_value, ...restAttributes } = doc.attributes;
|
|
@ -0,0 +1,229 @@
|
|||
/*
|
||||
* 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 { noneConnectorId } from '../../../common';
|
||||
import { createExternalService, createJiraConnector } from '../../services/test_utils';
|
||||
import { transformConnectorIdToReference, transformPushConnectorIdToReference } from './utils';
|
||||
|
||||
describe('migration utils', () => {
|
||||
describe('transformConnectorIdToReference', () => {
|
||||
it('returns the default none connector when the connector is undefined', () => {
|
||||
expect(transformConnectorIdToReference().transformedConnector).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"connector": Object {
|
||||
"fields": null,
|
||||
"name": "none",
|
||||
"type": ".none",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns the default none connector when the id is undefined', () => {
|
||||
expect(transformConnectorIdToReference({ id: undefined }).transformedConnector)
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"connector": Object {
|
||||
"fields": null,
|
||||
"name": "none",
|
||||
"type": ".none",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns the default none connector when the id is none', () => {
|
||||
expect(transformConnectorIdToReference({ id: noneConnectorId }).transformedConnector)
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"connector": Object {
|
||||
"fields": null,
|
||||
"name": "none",
|
||||
"type": ".none",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns the default none connector when the id is none and other fields are defined', () => {
|
||||
expect(
|
||||
transformConnectorIdToReference({ ...createJiraConnector(), id: noneConnectorId })
|
||||
.transformedConnector
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"connector": Object {
|
||||
"fields": null,
|
||||
"name": "none",
|
||||
"type": ".none",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns an empty array of references when the connector is undefined', () => {
|
||||
expect(transformConnectorIdToReference().references.length).toBe(0);
|
||||
});
|
||||
|
||||
it('returns an empty array of references when the id is undefined', () => {
|
||||
expect(transformConnectorIdToReference({ id: undefined }).references.length).toBe(0);
|
||||
});
|
||||
|
||||
it('returns an empty array of references when the id is the none connector', () => {
|
||||
expect(transformConnectorIdToReference({ id: noneConnectorId }).references.length).toBe(0);
|
||||
});
|
||||
|
||||
it('returns an empty array of references when the id is the none connector and other fields are defined', () => {
|
||||
expect(
|
||||
transformConnectorIdToReference({ ...createJiraConnector(), id: noneConnectorId })
|
||||
.references.length
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it('returns a jira connector', () => {
|
||||
const transformedFields = transformConnectorIdToReference(createJiraConnector());
|
||||
expect(transformedFields.transformedConnector).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"connector": Object {
|
||||
"fields": Object {
|
||||
"issueType": "bug",
|
||||
"parent": "2",
|
||||
"priority": "high",
|
||||
},
|
||||
"name": ".jira",
|
||||
"type": ".jira",
|
||||
},
|
||||
}
|
||||
`);
|
||||
expect(transformedFields.references).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "connectorId",
|
||||
"type": "action",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformPushConnectorIdToReference', () => {
|
||||
it('sets external_service to null when it is undefined', () => {
|
||||
expect(transformPushConnectorIdToReference().transformedPushConnector).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"external_service": null,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('sets external_service to null when it is null', () => {
|
||||
expect(transformPushConnectorIdToReference(null).transformedPushConnector)
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"external_service": null,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns an object when external_service is defined but connector_id is undefined', () => {
|
||||
expect(
|
||||
transformPushConnectorIdToReference({ connector_id: undefined }).transformedPushConnector
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"external_service": Object {},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns an object when external_service is defined but connector_id is null', () => {
|
||||
expect(transformPushConnectorIdToReference({ connector_id: null }).transformedPushConnector)
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"external_service": Object {},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns an object when external_service is defined but connector_id is none', () => {
|
||||
const otherFields = { otherField: 'hi' };
|
||||
|
||||
expect(
|
||||
transformPushConnectorIdToReference({ ...otherFields, connector_id: noneConnectorId })
|
||||
.transformedPushConnector
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"external_service": Object {
|
||||
"otherField": "hi",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns an empty array of references when the external_service is undefined', () => {
|
||||
expect(transformPushConnectorIdToReference().references.length).toBe(0);
|
||||
});
|
||||
|
||||
it('returns an empty array of references when the external_service is null', () => {
|
||||
expect(transformPushConnectorIdToReference(null).references.length).toBe(0);
|
||||
});
|
||||
|
||||
it('returns an empty array of references when the connector_id is undefined', () => {
|
||||
expect(
|
||||
transformPushConnectorIdToReference({ connector_id: undefined }).references.length
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it('returns an empty array of references when the connector_id is null', () => {
|
||||
expect(
|
||||
transformPushConnectorIdToReference({ connector_id: undefined }).references.length
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it('returns an empty array of references when the connector_id is the none connector', () => {
|
||||
expect(
|
||||
transformPushConnectorIdToReference({ connector_id: noneConnectorId }).references.length
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it('returns an empty array of references when the connector_id is the none connector and other fields are defined', () => {
|
||||
expect(
|
||||
transformPushConnectorIdToReference({
|
||||
...createExternalService(),
|
||||
connector_id: noneConnectorId,
|
||||
}).references.length
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it('returns the external_service connector', () => {
|
||||
const transformedFields = transformPushConnectorIdToReference(createExternalService());
|
||||
expect(transformedFields.transformedPushConnector).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"external_service": Object {
|
||||
"connector_name": ".jira",
|
||||
"external_id": "100",
|
||||
"external_title": "awesome",
|
||||
"external_url": "http://www.google.com",
|
||||
"pushed_at": "2019-11-25T21:54:48.952Z",
|
||||
"pushed_by": Object {
|
||||
"email": "testemail@elastic.co",
|
||||
"full_name": "elastic",
|
||||
"username": "elastic",
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
expect(transformedFields.references).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"id": "100",
|
||||
"name": "pushConnectorId",
|
||||
"type": "action",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
|
||||
import { noneConnectorId } from '../../../common';
|
||||
import { SavedObjectReference } from '../../../../../../src/core/server';
|
||||
import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server';
|
||||
import {
|
||||
getNoneCaseConnector,
|
||||
CONNECTOR_ID_REFERENCE_NAME,
|
||||
PUSH_CONNECTOR_ID_REFERENCE_NAME,
|
||||
} from '../../common';
|
||||
|
||||
export const transformConnectorIdToReference = (connector?: {
|
||||
id?: string;
|
||||
}): { transformedConnector: Record<string, unknown>; references: SavedObjectReference[] } => {
|
||||
const { id: connectorId, ...restConnector } = connector ?? {};
|
||||
|
||||
const references = createConnectorReference(
|
||||
connectorId,
|
||||
ACTION_SAVED_OBJECT_TYPE,
|
||||
CONNECTOR_ID_REFERENCE_NAME
|
||||
);
|
||||
|
||||
const { id: ignoreNoneId, ...restNoneConnector } = getNoneCaseConnector();
|
||||
const connectorFieldsToReturn =
|
||||
connector && references.length > 0 ? restConnector : restNoneConnector;
|
||||
|
||||
return {
|
||||
transformedConnector: {
|
||||
connector: connectorFieldsToReturn,
|
||||
},
|
||||
references,
|
||||
};
|
||||
};
|
||||
|
||||
const createConnectorReference = (
|
||||
id: string | null | undefined,
|
||||
type: string,
|
||||
name: string
|
||||
): SavedObjectReference[] => {
|
||||
return id && id !== noneConnectorId
|
||||
? [
|
||||
{
|
||||
id,
|
||||
type,
|
||||
name,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
};
|
||||
|
||||
export const transformPushConnectorIdToReference = (
|
||||
external_service?: { connector_id?: string | null } | null
|
||||
): { transformedPushConnector: Record<string, unknown>; references: SavedObjectReference[] } => {
|
||||
const { connector_id: pushConnectorId, ...restExternalService } = external_service ?? {};
|
||||
|
||||
const references = createConnectorReference(
|
||||
pushConnectorId,
|
||||
ACTION_SAVED_OBJECT_TYPE,
|
||||
PUSH_CONNECTOR_ID_REFERENCE_NAME
|
||||
);
|
||||
|
||||
return {
|
||||
transformedPushConnector: { external_service: external_service ? restExternalService : null },
|
||||
references,
|
||||
};
|
||||
};
|
1167
x-pack/plugins/cases/server/services/cases/index.test.ts
Normal file
1167
x-pack/plugins/cases/server/services/cases/index.test.ts
Normal file
File diff suppressed because it is too large
Load diff
|
@ -14,6 +14,8 @@ import {
|
|||
SavedObjectsFindResponse,
|
||||
SavedObjectsBulkResponse,
|
||||
SavedObjectsFindResult,
|
||||
SavedObjectsBulkUpdateResponse,
|
||||
SavedObjectsUpdateResponse,
|
||||
} from 'kibana/server';
|
||||
|
||||
import type { estypes } from '@elastic/elasticsearch';
|
||||
|
@ -32,7 +34,6 @@ import {
|
|||
CommentAttributes,
|
||||
CommentType,
|
||||
ENABLE_CASE_CONNECTOR,
|
||||
ESCaseAttributes,
|
||||
GetCaseIdsByAlertIdAggs,
|
||||
MAX_CONCURRENT_SEARCHES,
|
||||
MAX_DOCS_PER_PAGE,
|
||||
|
@ -41,6 +42,7 @@ import {
|
|||
SubCaseAttributes,
|
||||
SubCaseResponse,
|
||||
User,
|
||||
CaseAttributes,
|
||||
} from '../../../common';
|
||||
import {
|
||||
defaultSortField,
|
||||
|
@ -54,6 +56,15 @@ import { ClientArgs } from '..';
|
|||
import { combineFilters } from '../../client/utils';
|
||||
import { includeFieldsRequiredForAuthentication } from '../../authorization/utils';
|
||||
import { EnsureSOAuthCallback } from '../../authorization';
|
||||
import {
|
||||
transformSavedObjectToExternalModel,
|
||||
transformAttributesToESModel,
|
||||
transformUpdateResponseToExternalModel,
|
||||
transformUpdateResponsesToExternalModels,
|
||||
transformBulkResponseToExternalModel,
|
||||
transformFindResponseToExternalModel,
|
||||
} from './transform';
|
||||
import { ESCaseAttributes } from './types';
|
||||
|
||||
interface GetCaseIdsByAlertIdArgs extends ClientArgs {
|
||||
alertId: string;
|
||||
|
@ -111,7 +122,7 @@ interface FindSubCasesStatusStats {
|
|||
}
|
||||
|
||||
interface PostCaseArgs extends ClientArgs {
|
||||
attributes: ESCaseAttributes;
|
||||
attributes: CaseAttributes;
|
||||
id: string;
|
||||
}
|
||||
|
||||
|
@ -123,7 +134,8 @@ interface CreateSubCaseArgs extends ClientArgs {
|
|||
|
||||
interface PatchCase {
|
||||
caseId: string;
|
||||
updatedAttributes: Partial<ESCaseAttributes & PushedArgs>;
|
||||
updatedAttributes: Partial<CaseAttributes & PushedArgs>;
|
||||
originalCase: SavedObject<CaseAttributes>;
|
||||
version?: string;
|
||||
}
|
||||
type PatchCaseArgs = PatchCase & ClientArgs;
|
||||
|
@ -168,7 +180,7 @@ interface FindCommentsByAssociationArgs {
|
|||
}
|
||||
|
||||
interface Collection {
|
||||
case: SavedObjectsFindResult<ESCaseAttributes>;
|
||||
case: SavedObjectsFindResult<CaseAttributes>;
|
||||
subCases?: SubCaseResponse[];
|
||||
}
|
||||
|
||||
|
@ -713,10 +725,14 @@ export class CasesService {
|
|||
public async getCase({
|
||||
unsecuredSavedObjectsClient,
|
||||
id: caseId,
|
||||
}: GetCaseArgs): Promise<SavedObject<ESCaseAttributes>> {
|
||||
}: GetCaseArgs): Promise<SavedObject<CaseAttributes>> {
|
||||
try {
|
||||
this.log.debug(`Attempting to GET case ${caseId}`);
|
||||
return await unsecuredSavedObjectsClient.get<ESCaseAttributes>(CASE_SAVED_OBJECT, caseId);
|
||||
const caseSavedObject = await unsecuredSavedObjectsClient.get<ESCaseAttributes>(
|
||||
CASE_SAVED_OBJECT,
|
||||
caseId
|
||||
);
|
||||
return transformSavedObjectToExternalModel(caseSavedObject);
|
||||
} catch (error) {
|
||||
this.log.error(`Error on GET case ${caseId}: ${error}`);
|
||||
throw error;
|
||||
|
@ -753,12 +769,13 @@ export class CasesService {
|
|||
public async getCases({
|
||||
unsecuredSavedObjectsClient,
|
||||
caseIds,
|
||||
}: GetCasesArgs): Promise<SavedObjectsBulkResponse<ESCaseAttributes>> {
|
||||
}: GetCasesArgs): Promise<SavedObjectsBulkResponse<CaseAttributes>> {
|
||||
try {
|
||||
this.log.debug(`Attempting to GET cases ${caseIds.join(', ')}`);
|
||||
return await unsecuredSavedObjectsClient.bulkGet<ESCaseAttributes>(
|
||||
const cases = await unsecuredSavedObjectsClient.bulkGet<ESCaseAttributes>(
|
||||
caseIds.map((caseId) => ({ type: CASE_SAVED_OBJECT, id: caseId }))
|
||||
);
|
||||
return transformBulkResponseToExternalModel(cases);
|
||||
} catch (error) {
|
||||
this.log.error(`Error on GET cases ${caseIds.join(', ')}: ${error}`);
|
||||
throw error;
|
||||
|
@ -768,14 +785,15 @@ export class CasesService {
|
|||
public async findCases({
|
||||
unsecuredSavedObjectsClient,
|
||||
options,
|
||||
}: FindCasesArgs): Promise<SavedObjectsFindResponse<ESCaseAttributes>> {
|
||||
}: FindCasesArgs): Promise<SavedObjectsFindResponse<CaseAttributes>> {
|
||||
try {
|
||||
this.log.debug(`Attempting to find cases`);
|
||||
return await unsecuredSavedObjectsClient.find<ESCaseAttributes>({
|
||||
const cases = await unsecuredSavedObjectsClient.find<ESCaseAttributes>({
|
||||
sortField: defaultSortField,
|
||||
...options,
|
||||
type: CASE_SAVED_OBJECT,
|
||||
});
|
||||
return transformFindResponseToExternalModel(cases);
|
||||
} catch (error) {
|
||||
this.log.error(`Error on find cases: ${error}`);
|
||||
throw error;
|
||||
|
@ -1041,14 +1059,20 @@ export class CasesService {
|
|||
}
|
||||
}
|
||||
|
||||
public async postNewCase({ unsecuredSavedObjectsClient, attributes, id }: PostCaseArgs) {
|
||||
public async postNewCase({
|
||||
unsecuredSavedObjectsClient,
|
||||
attributes,
|
||||
id,
|
||||
}: PostCaseArgs): Promise<SavedObject<CaseAttributes>> {
|
||||
try {
|
||||
this.log.debug(`Attempting to POST a new case`);
|
||||
return await unsecuredSavedObjectsClient.create<ESCaseAttributes>(
|
||||
const transformedAttributes = transformAttributesToESModel(attributes);
|
||||
const createdCase = await unsecuredSavedObjectsClient.create<ESCaseAttributes>(
|
||||
CASE_SAVED_OBJECT,
|
||||
attributes,
|
||||
{ id }
|
||||
transformedAttributes.attributes,
|
||||
{ id, references: transformedAttributes.referenceHandler.build() }
|
||||
);
|
||||
return transformSavedObjectToExternalModel(createdCase);
|
||||
} catch (error) {
|
||||
this.log.error(`Error on POST a new case: ${error}`);
|
||||
throw error;
|
||||
|
@ -1059,33 +1083,52 @@ export class CasesService {
|
|||
unsecuredSavedObjectsClient,
|
||||
caseId,
|
||||
updatedAttributes,
|
||||
originalCase,
|
||||
version,
|
||||
}: PatchCaseArgs) {
|
||||
}: PatchCaseArgs): Promise<SavedObjectsUpdateResponse<CaseAttributes>> {
|
||||
try {
|
||||
this.log.debug(`Attempting to UPDATE case ${caseId}`);
|
||||
return await unsecuredSavedObjectsClient.update<ESCaseAttributes>(
|
||||
const transformedAttributes = transformAttributesToESModel(updatedAttributes);
|
||||
|
||||
const updatedCase = await unsecuredSavedObjectsClient.update<ESCaseAttributes>(
|
||||
CASE_SAVED_OBJECT,
|
||||
caseId,
|
||||
{ ...updatedAttributes },
|
||||
{ version }
|
||||
transformedAttributes.attributes,
|
||||
{
|
||||
version,
|
||||
references: transformedAttributes.referenceHandler.build(originalCase.references),
|
||||
}
|
||||
);
|
||||
|
||||
return transformUpdateResponseToExternalModel(updatedCase);
|
||||
} catch (error) {
|
||||
this.log.error(`Error on UPDATE case ${caseId}: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async patchCases({ unsecuredSavedObjectsClient, cases }: PatchCasesArgs) {
|
||||
public async patchCases({
|
||||
unsecuredSavedObjectsClient,
|
||||
cases,
|
||||
}: PatchCasesArgs): Promise<SavedObjectsBulkUpdateResponse<CaseAttributes>> {
|
||||
try {
|
||||
this.log.debug(`Attempting to UPDATE case ${cases.map((c) => c.caseId).join(', ')}`);
|
||||
return await unsecuredSavedObjectsClient.bulkUpdate<ESCaseAttributes>(
|
||||
cases.map((c) => ({
|
||||
|
||||
const bulkUpdate = cases.map(({ caseId, updatedAttributes, version, originalCase }) => {
|
||||
const { attributes, referenceHandler } = transformAttributesToESModel(updatedAttributes);
|
||||
return {
|
||||
type: CASE_SAVED_OBJECT,
|
||||
id: c.caseId,
|
||||
attributes: c.updatedAttributes,
|
||||
version: c.version,
|
||||
}))
|
||||
id: caseId,
|
||||
attributes,
|
||||
references: referenceHandler.build(originalCase.references),
|
||||
version,
|
||||
};
|
||||
});
|
||||
|
||||
const updatedCases = await unsecuredSavedObjectsClient.bulkUpdate<ESCaseAttributes>(
|
||||
bulkUpdate
|
||||
);
|
||||
return transformUpdateResponsesToExternalModels(updatedCases);
|
||||
} catch (error) {
|
||||
this.log.error(`Error on UPDATE case ${cases.map((c) => c.caseId).join(', ')}: ${error}`);
|
||||
throw error;
|
||||
|
|
414
x-pack/plugins/cases/server/services/cases/transform.test.ts
Normal file
414
x-pack/plugins/cases/server/services/cases/transform.test.ts
Normal file
|
@ -0,0 +1,414 @@
|
|||
/*
|
||||
* 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 {
|
||||
createCaseSavedObjectResponse,
|
||||
createESJiraConnector,
|
||||
createExternalService,
|
||||
createJiraConnector,
|
||||
} from '../test_utils';
|
||||
import {
|
||||
transformAttributesToESModel,
|
||||
transformSavedObjectToExternalModel,
|
||||
transformUpdateResponseToExternalModel,
|
||||
} from './transform';
|
||||
import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server';
|
||||
import { ConnectorTypes } from '../../../common';
|
||||
import {
|
||||
getNoneCaseConnector,
|
||||
CONNECTOR_ID_REFERENCE_NAME,
|
||||
PUSH_CONNECTOR_ID_REFERENCE_NAME,
|
||||
} from '../../common';
|
||||
|
||||
describe('case transforms', () => {
|
||||
describe('transformUpdateResponseToExternalModel', () => {
|
||||
it('does not return the connector field if it is undefined', () => {
|
||||
expect(
|
||||
transformUpdateResponseToExternalModel({
|
||||
type: 'a',
|
||||
id: '1',
|
||||
attributes: {},
|
||||
references: undefined,
|
||||
}).attributes
|
||||
).not.toHaveProperty('connector');
|
||||
});
|
||||
|
||||
it('does not return the external_service field if it is undefined', () => {
|
||||
expect(
|
||||
transformUpdateResponseToExternalModel({
|
||||
type: 'a',
|
||||
id: '1',
|
||||
attributes: {},
|
||||
references: undefined,
|
||||
}).attributes
|
||||
).not.toHaveProperty('external_service');
|
||||
});
|
||||
|
||||
it('return a null external_service field if it is null', () => {
|
||||
expect(
|
||||
transformUpdateResponseToExternalModel({
|
||||
type: 'a',
|
||||
id: '1',
|
||||
attributes: {
|
||||
external_service: null,
|
||||
},
|
||||
references: undefined,
|
||||
}).attributes.external_service
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('return a null external_service.connector_id field if it is none', () => {
|
||||
expect(
|
||||
transformUpdateResponseToExternalModel({
|
||||
type: 'a',
|
||||
id: '1',
|
||||
attributes: {
|
||||
external_service: createExternalService({ connector_id: 'none' }),
|
||||
},
|
||||
references: undefined,
|
||||
}).attributes.external_service?.connector_id
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('return the external_service fields if it is populated', () => {
|
||||
const { connector_id: ignore, ...restExternalService } = createExternalService()!;
|
||||
expect(
|
||||
transformUpdateResponseToExternalModel({
|
||||
type: 'a',
|
||||
id: '1',
|
||||
attributes: {
|
||||
external_service: restExternalService,
|
||||
},
|
||||
references: undefined,
|
||||
}).attributes.external_service
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"connector_id": null,
|
||||
"connector_name": ".jira",
|
||||
"external_id": "100",
|
||||
"external_title": "awesome",
|
||||
"external_url": "http://www.google.com",
|
||||
"pushed_at": "2019-11-25T21:54:48.952Z",
|
||||
"pushed_by": Object {
|
||||
"email": "testemail@elastic.co",
|
||||
"full_name": "elastic",
|
||||
"username": "elastic",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('populates the connector_id field when it finds a reference', () => {
|
||||
expect(
|
||||
transformUpdateResponseToExternalModel({
|
||||
type: 'a',
|
||||
id: '1',
|
||||
attributes: {
|
||||
external_service: createExternalService(),
|
||||
},
|
||||
references: [
|
||||
{ id: '1', name: PUSH_CONNECTOR_ID_REFERENCE_NAME, type: ACTION_SAVED_OBJECT_TYPE },
|
||||
],
|
||||
}).attributes.external_service?.connector_id
|
||||
).toMatchInlineSnapshot(`"1"`);
|
||||
});
|
||||
|
||||
it('populates the external_service fields when it finds a reference', () => {
|
||||
expect(
|
||||
transformUpdateResponseToExternalModel({
|
||||
type: 'a',
|
||||
id: '1',
|
||||
attributes: {
|
||||
external_service: createExternalService(),
|
||||
},
|
||||
references: [
|
||||
{ id: '1', name: PUSH_CONNECTOR_ID_REFERENCE_NAME, type: ACTION_SAVED_OBJECT_TYPE },
|
||||
],
|
||||
}).attributes.external_service
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"connector_id": "1",
|
||||
"connector_name": ".jira",
|
||||
"external_id": "100",
|
||||
"external_title": "awesome",
|
||||
"external_url": "http://www.google.com",
|
||||
"pushed_at": "2019-11-25T21:54:48.952Z",
|
||||
"pushed_by": Object {
|
||||
"email": "testemail@elastic.co",
|
||||
"full_name": "elastic",
|
||||
"username": "elastic",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('populates the connector fields when it finds a reference', () => {
|
||||
expect(
|
||||
transformUpdateResponseToExternalModel({
|
||||
type: 'a',
|
||||
id: '1',
|
||||
attributes: {
|
||||
connector: {
|
||||
name: ConnectorTypes.jira,
|
||||
type: ConnectorTypes.jira,
|
||||
fields: [{ key: 'issueType', value: 'bug' }],
|
||||
},
|
||||
},
|
||||
references: [
|
||||
{ id: '1', name: CONNECTOR_ID_REFERENCE_NAME, type: ACTION_SAVED_OBJECT_TYPE },
|
||||
],
|
||||
}).attributes.connector
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"fields": Object {
|
||||
"issueType": "bug",
|
||||
},
|
||||
"id": "1",
|
||||
"name": ".jira",
|
||||
"type": ".jira",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns the none connector when it cannot find the reference', () => {
|
||||
expect(
|
||||
transformUpdateResponseToExternalModel({
|
||||
type: 'a',
|
||||
id: '1',
|
||||
attributes: {
|
||||
connector: {
|
||||
name: ConnectorTypes.jira,
|
||||
type: ConnectorTypes.jira,
|
||||
fields: [{ key: 'issueType', value: 'bug' }],
|
||||
},
|
||||
},
|
||||
references: undefined,
|
||||
}).attributes.connector
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"fields": null,
|
||||
"id": "none",
|
||||
"name": "none",
|
||||
"type": ".none",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformAttributesToESModel', () => {
|
||||
it('does not return the external_service field when it is undefined', () => {
|
||||
expect(
|
||||
transformAttributesToESModel({
|
||||
external_service: undefined,
|
||||
}).attributes
|
||||
).not.toHaveProperty('external_service');
|
||||
});
|
||||
|
||||
it('creates an undefined reference when external_service is undefined and the original reference is undefined', () => {
|
||||
expect(
|
||||
transformAttributesToESModel({
|
||||
external_service: undefined,
|
||||
}).referenceHandler.build()
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns a null external_service when it is null', () => {
|
||||
expect(
|
||||
transformAttributesToESModel({
|
||||
external_service: null,
|
||||
}).attributes.external_service
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('creates an undefined reference when external_service is null and the original reference is undefined', () => {
|
||||
expect(
|
||||
transformAttributesToESModel({
|
||||
external_service: null,
|
||||
}).referenceHandler.build()
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns the external_service fields except for the connector_id', () => {
|
||||
const transformedAttributes = transformAttributesToESModel({
|
||||
external_service: createExternalService(),
|
||||
});
|
||||
|
||||
expect(transformedAttributes.attributes).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"external_service": Object {
|
||||
"connector_name": ".jira",
|
||||
"external_id": "100",
|
||||
"external_title": "awesome",
|
||||
"external_url": "http://www.google.com",
|
||||
"pushed_at": "2019-11-25T21:54:48.952Z",
|
||||
"pushed_by": Object {
|
||||
"email": "testemail@elastic.co",
|
||||
"full_name": "elastic",
|
||||
"username": "elastic",
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
expect(transformedAttributes.attributes.external_service).not.toHaveProperty('connector_id');
|
||||
expect(transformedAttributes.referenceHandler.build()).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"id": "100",
|
||||
"name": "pushConnectorId",
|
||||
"type": "action",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('creates an empty references array to delete the connector_id when connector_id is null and the original references is undefined', () => {
|
||||
const transformedAttributes = transformAttributesToESModel({
|
||||
external_service: createExternalService({ connector_id: null }),
|
||||
});
|
||||
|
||||
expect(transformedAttributes.referenceHandler.build()).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not return the connector when it is undefined', () => {
|
||||
expect(transformAttributesToESModel({ connector: undefined }).attributes).not.toHaveProperty(
|
||||
'connector'
|
||||
);
|
||||
});
|
||||
|
||||
it('constructs an undefined reference when the connector is undefined and the original reference is undefined', () => {
|
||||
expect(
|
||||
transformAttributesToESModel({ connector: undefined }).referenceHandler.build()
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns a jira connector', () => {
|
||||
const transformedAttributes = transformAttributesToESModel({
|
||||
connector: createJiraConnector(),
|
||||
});
|
||||
|
||||
expect(transformedAttributes.attributes).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"connector": Object {
|
||||
"fields": Array [
|
||||
Object {
|
||||
"key": "issueType",
|
||||
"value": "bug",
|
||||
},
|
||||
Object {
|
||||
"key": "priority",
|
||||
"value": "high",
|
||||
},
|
||||
Object {
|
||||
"key": "parent",
|
||||
"value": "2",
|
||||
},
|
||||
],
|
||||
"name": ".jira",
|
||||
"type": ".jira",
|
||||
},
|
||||
}
|
||||
`);
|
||||
expect(transformedAttributes.attributes.connector).not.toHaveProperty('id');
|
||||
expect(transformedAttributes.referenceHandler.build()).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "connectorId",
|
||||
"type": "action",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns a none connector without a reference', () => {
|
||||
const transformedAttributes = transformAttributesToESModel({
|
||||
connector: getNoneCaseConnector(),
|
||||
});
|
||||
|
||||
expect(transformedAttributes.attributes).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"connector": Object {
|
||||
"fields": Array [],
|
||||
"name": "none",
|
||||
"type": ".none",
|
||||
},
|
||||
}
|
||||
`);
|
||||
expect(transformedAttributes.attributes.connector).not.toHaveProperty('id');
|
||||
expect(transformedAttributes.referenceHandler.build()).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformSavedObjectToExternalModel', () => {
|
||||
it('returns the default none connector when it cannot find the reference', () => {
|
||||
expect(
|
||||
transformSavedObjectToExternalModel(
|
||||
createCaseSavedObjectResponse({ connector: getNoneCaseConnector() })
|
||||
).attributes.connector
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"fields": null,
|
||||
"id": "none",
|
||||
"name": "none",
|
||||
"type": ".none",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns a jira connector', () => {
|
||||
expect(
|
||||
transformSavedObjectToExternalModel(
|
||||
createCaseSavedObjectResponse({ connector: createESJiraConnector() })
|
||||
).attributes.connector
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"fields": Object {
|
||||
"issueType": "bug",
|
||||
"parent": "2",
|
||||
"priority": "high",
|
||||
},
|
||||
"id": "1",
|
||||
"name": ".jira",
|
||||
"type": ".jira",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('sets external_service to null when it is null', () => {
|
||||
expect(
|
||||
transformSavedObjectToExternalModel(
|
||||
createCaseSavedObjectResponse({ externalService: null })
|
||||
).attributes.external_service
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('sets external_service.connector_id to null when a reference cannot be found', () => {
|
||||
const transformedSO = transformSavedObjectToExternalModel(
|
||||
createCaseSavedObjectResponse({
|
||||
externalService: createExternalService({ connector_id: null }),
|
||||
})
|
||||
);
|
||||
|
||||
expect(transformedSO.attributes.external_service?.connector_id).toBeNull();
|
||||
expect(transformedSO.attributes.external_service).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"connector_id": null,
|
||||
"connector_name": ".jira",
|
||||
"external_id": "100",
|
||||
"external_title": "awesome",
|
||||
"external_url": "http://www.google.com",
|
||||
"pushed_at": "2019-11-25T21:54:48.952Z",
|
||||
"pushed_by": Object {
|
||||
"email": "testemail@elastic.co",
|
||||
"full_name": "elastic",
|
||||
"username": "elastic",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
208
x-pack/plugins/cases/server/services/cases/transform.ts
Normal file
208
x-pack/plugins/cases/server/services/cases/transform.ts
Normal file
|
@ -0,0 +1,208 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
|
||||
import {
|
||||
SavedObject,
|
||||
SavedObjectReference,
|
||||
SavedObjectsBulkResponse,
|
||||
SavedObjectsBulkUpdateResponse,
|
||||
SavedObjectsFindResponse,
|
||||
SavedObjectsUpdateResponse,
|
||||
} from 'kibana/server';
|
||||
import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server';
|
||||
import { ESCaseAttributes, ExternalServicesWithoutConnectorId } from './types';
|
||||
import { CONNECTOR_ID_REFERENCE_NAME, PUSH_CONNECTOR_ID_REFERENCE_NAME } from '../../common';
|
||||
import { CaseAttributes, CaseFullExternalService } from '../../../common';
|
||||
import {
|
||||
findConnectorIdReference,
|
||||
transformFieldsToESModel,
|
||||
transformESConnectorOrUseDefault,
|
||||
transformESConnectorToExternalModel,
|
||||
} from '../transform';
|
||||
import { ConnectorReferenceHandler } from '../connector_reference_handler';
|
||||
|
||||
export function transformUpdateResponsesToExternalModels(
|
||||
response: SavedObjectsBulkUpdateResponse<ESCaseAttributes>
|
||||
): SavedObjectsBulkUpdateResponse<CaseAttributes> {
|
||||
return {
|
||||
...response,
|
||||
saved_objects: response.saved_objects.map((so) => ({
|
||||
...so,
|
||||
...transformUpdateResponseToExternalModel(so),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function transformUpdateResponseToExternalModel(
|
||||
updatedCase: SavedObjectsUpdateResponse<ESCaseAttributes>
|
||||
): SavedObjectsUpdateResponse<CaseAttributes> {
|
||||
const { connector, external_service, ...restUpdateAttributes } = updatedCase.attributes ?? {};
|
||||
|
||||
const transformedConnector = transformESConnectorToExternalModel({
|
||||
// if the saved object had an error the attributes field will not exist
|
||||
connector,
|
||||
references: updatedCase.references,
|
||||
referenceName: CONNECTOR_ID_REFERENCE_NAME,
|
||||
});
|
||||
|
||||
let externalService: CaseFullExternalService | null | undefined;
|
||||
|
||||
// if external_service is not defined then we don't want to include it in the response since it wasn't passed it as an
|
||||
// attribute to update
|
||||
if (external_service !== undefined) {
|
||||
externalService = transformESExternalService(external_service, updatedCase.references);
|
||||
}
|
||||
|
||||
return {
|
||||
...updatedCase,
|
||||
attributes: {
|
||||
...restUpdateAttributes,
|
||||
...(transformedConnector && { connector: transformedConnector }),
|
||||
// if externalService is null that means we intentionally updated it to null within ES so return that as a valid value
|
||||
...(externalService !== undefined && { external_service: externalService }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function transformAttributesToESModel(
|
||||
caseAttributes: CaseAttributes
|
||||
): {
|
||||
attributes: ESCaseAttributes;
|
||||
referenceHandler: ConnectorReferenceHandler;
|
||||
};
|
||||
export function transformAttributesToESModel(
|
||||
caseAttributes: Partial<CaseAttributes>
|
||||
): {
|
||||
attributes: Partial<ESCaseAttributes>;
|
||||
referenceHandler: ConnectorReferenceHandler;
|
||||
};
|
||||
export function transformAttributesToESModel(
|
||||
caseAttributes: Partial<CaseAttributes>
|
||||
): {
|
||||
attributes: Partial<ESCaseAttributes>;
|
||||
referenceHandler: ConnectorReferenceHandler;
|
||||
} {
|
||||
const { connector, external_service, ...restAttributes } = caseAttributes;
|
||||
const { connector_id: pushConnectorId, ...restExternalService } = external_service ?? {};
|
||||
|
||||
const transformedConnector = {
|
||||
...(connector && {
|
||||
connector: {
|
||||
name: connector.name,
|
||||
type: connector.type,
|
||||
fields: transformFieldsToESModel(connector),
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const transformedExternalService = {
|
||||
...(external_service
|
||||
? { external_service: restExternalService }
|
||||
: external_service === null
|
||||
? { external_service: null }
|
||||
: {}),
|
||||
};
|
||||
|
||||
return {
|
||||
attributes: {
|
||||
...restAttributes,
|
||||
...transformedConnector,
|
||||
...transformedExternalService,
|
||||
},
|
||||
referenceHandler: buildReferenceHandler(connector?.id, pushConnectorId),
|
||||
};
|
||||
}
|
||||
|
||||
function buildReferenceHandler(
|
||||
connectorId?: string,
|
||||
pushConnectorId?: string | null
|
||||
): ConnectorReferenceHandler {
|
||||
return new ConnectorReferenceHandler([
|
||||
{ id: connectorId, name: CONNECTOR_ID_REFERENCE_NAME, type: ACTION_SAVED_OBJECT_TYPE },
|
||||
{ id: pushConnectorId, name: PUSH_CONNECTOR_ID_REFERENCE_NAME, type: ACTION_SAVED_OBJECT_TYPE },
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Until Kibana uses typescript 4.3 or higher we'll have to keep these functions separate instead of using an overload
|
||||
* definition like this:
|
||||
*
|
||||
* export function transformArrayResponseToExternalModel(
|
||||
* response: SavedObjectsBulkResponse<ESCaseAttributes> | SavedObjectsFindResponse<ESCaseAttributes>
|
||||
* ): SavedObjectsBulkResponse<CaseAttributes> | SavedObjectsFindResponse<CaseAttributes> {
|
||||
*
|
||||
* See this issue for more details: https://stackoverflow.com/questions/49510832/typescript-how-to-map-over-union-array-type
|
||||
*/
|
||||
|
||||
export function transformBulkResponseToExternalModel(
|
||||
response: SavedObjectsBulkResponse<ESCaseAttributes>
|
||||
): SavedObjectsBulkResponse<CaseAttributes> {
|
||||
return {
|
||||
...response,
|
||||
saved_objects: response.saved_objects.map((so) => ({
|
||||
...so,
|
||||
...transformSavedObjectToExternalModel(so),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function transformFindResponseToExternalModel(
|
||||
response: SavedObjectsFindResponse<ESCaseAttributes>
|
||||
): SavedObjectsFindResponse<CaseAttributes> {
|
||||
return {
|
||||
...response,
|
||||
saved_objects: response.saved_objects.map((so) => ({
|
||||
...so,
|
||||
...transformSavedObjectToExternalModel(so),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function transformSavedObjectToExternalModel(
|
||||
caseSavedObject: SavedObject<ESCaseAttributes>
|
||||
): SavedObject<CaseAttributes> {
|
||||
const connector = transformESConnectorOrUseDefault({
|
||||
// if the saved object had an error the attributes field will not exist
|
||||
connector: caseSavedObject.attributes?.connector,
|
||||
references: caseSavedObject.references,
|
||||
referenceName: CONNECTOR_ID_REFERENCE_NAME,
|
||||
});
|
||||
|
||||
const externalService = transformESExternalService(
|
||||
caseSavedObject.attributes?.external_service,
|
||||
caseSavedObject.references
|
||||
);
|
||||
|
||||
return {
|
||||
...caseSavedObject,
|
||||
attributes: {
|
||||
...caseSavedObject.attributes,
|
||||
connector,
|
||||
external_service: externalService,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function transformESExternalService(
|
||||
// this type needs to match that of CaseFullExternalService except that it does not include the connector_id, see: x-pack/plugins/cases/common/api/cases/case.ts
|
||||
// that's why it can be null here
|
||||
externalService: ExternalServicesWithoutConnectorId | null | undefined,
|
||||
references: SavedObjectReference[] | undefined
|
||||
): CaseFullExternalService | null {
|
||||
const connectorIdRef = findConnectorIdReference(PUSH_CONNECTOR_ID_REFERENCE_NAME, references);
|
||||
|
||||
if (!externalService) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...externalService,
|
||||
connector_id: connectorIdRef?.id ?? null,
|
||||
};
|
||||
}
|
32
x-pack/plugins/cases/server/services/cases/types.ts
Normal file
32
x-pack/plugins/cases/server/services/cases/types.ts
Normal file
|
@ -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 * as rt from 'io-ts';
|
||||
import { CaseAttributes, CaseExternalServiceBasicRt } from '../../../common';
|
||||
import { ESCaseConnector } from '..';
|
||||
|
||||
/**
|
||||
* This type should only be used within the cases service and its helper functions (e.g. the transforms).
|
||||
*
|
||||
* The type represents how the external services portion of the object will be layed out when stored in ES. The external_service will have its
|
||||
* connector_id field removed and placed within the references field.
|
||||
*/
|
||||
export type ExternalServicesWithoutConnectorId = Omit<
|
||||
rt.TypeOf<typeof CaseExternalServiceBasicRt>,
|
||||
'connector_id'
|
||||
>;
|
||||
|
||||
/**
|
||||
* This type should only be used within the cases service and its helper functions (e.g. the transforms).
|
||||
*
|
||||
* The type represents how the Cases object will be layed out in ES. It will not have connector.id or external_service.connector_id.
|
||||
* Instead those fields will be transformed into the references field.
|
||||
*/
|
||||
export type ESCaseAttributes = Omit<CaseAttributes, 'connector' | 'external_service'> & {
|
||||
connector: ESCaseConnector;
|
||||
external_service: ExternalServicesWithoutConnectorId | null;
|
||||
};
|
722
x-pack/plugins/cases/server/services/configure/index.test.ts
Normal file
722
x-pack/plugins/cases/server/services/configure/index.test.ts
Normal file
|
@ -0,0 +1,722 @@
|
|||
/*
|
||||
* 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 {
|
||||
CaseConnector,
|
||||
CasesConfigureAttributes,
|
||||
CasesConfigurePatch,
|
||||
CASE_CONFIGURE_SAVED_OBJECT,
|
||||
ConnectorTypes,
|
||||
SECURITY_SOLUTION_OWNER,
|
||||
} from '../../../common';
|
||||
import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks';
|
||||
import {
|
||||
SavedObject,
|
||||
SavedObjectReference,
|
||||
SavedObjectsCreateOptions,
|
||||
SavedObjectsFindResult,
|
||||
SavedObjectsUpdateOptions,
|
||||
SavedObjectsUpdateResponse,
|
||||
} from 'kibana/server';
|
||||
import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server';
|
||||
import { loggerMock } from '@kbn/logging/target/mocks';
|
||||
import { CaseConfigureService } from '.';
|
||||
import { ESCasesConfigureAttributes } from './types';
|
||||
import { getNoneCaseConnector, CONNECTOR_ID_REFERENCE_NAME } from '../../common';
|
||||
import { createESJiraConnector, createJiraConnector, ESCaseConnectorWithId } from '../test_utils';
|
||||
|
||||
const basicConfigFields = {
|
||||
closure_type: 'close-by-pushing' as const,
|
||||
owner: SECURITY_SOLUTION_OWNER,
|
||||
created_at: '2020-04-09T09:43:51.778Z',
|
||||
created_by: {
|
||||
full_name: 'elastic',
|
||||
email: 'testemail@elastic.co',
|
||||
username: 'elastic',
|
||||
},
|
||||
updated_at: '2020-04-09T09:43:51.778Z',
|
||||
updated_by: {
|
||||
full_name: 'elastic',
|
||||
email: 'testemail@elastic.co',
|
||||
username: 'elastic',
|
||||
},
|
||||
};
|
||||
|
||||
const createConfigUpdateParams = (
|
||||
connector?: CaseConnector
|
||||
): Partial<CasesConfigureAttributes> => ({
|
||||
connector,
|
||||
});
|
||||
|
||||
const createConfigPostParams = (connector: CaseConnector): CasesConfigureAttributes => ({
|
||||
...basicConfigFields,
|
||||
connector,
|
||||
});
|
||||
|
||||
const createUpdateConfigSO = (
|
||||
connector?: ESCaseConnectorWithId
|
||||
): SavedObjectsUpdateResponse<ESCasesConfigureAttributes> => {
|
||||
const references: SavedObjectReference[] =
|
||||
connector && connector.id !== 'none'
|
||||
? [
|
||||
{
|
||||
id: connector.id,
|
||||
name: CONNECTOR_ID_REFERENCE_NAME,
|
||||
type: ACTION_SAVED_OBJECT_TYPE,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
return {
|
||||
type: CASE_CONFIGURE_SAVED_OBJECT,
|
||||
id: '1',
|
||||
attributes: {
|
||||
connector: connector
|
||||
? { name: connector.name, type: connector.type, fields: connector.fields }
|
||||
: undefined,
|
||||
},
|
||||
version: '1',
|
||||
references,
|
||||
};
|
||||
};
|
||||
|
||||
const createConfigSO = (
|
||||
connector?: ESCaseConnectorWithId
|
||||
): SavedObject<ESCasesConfigureAttributes> => {
|
||||
const references: SavedObjectReference[] = connector
|
||||
? [
|
||||
{
|
||||
id: connector.id,
|
||||
name: CONNECTOR_ID_REFERENCE_NAME,
|
||||
type: ACTION_SAVED_OBJECT_TYPE,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const formattedConnector = {
|
||||
type: connector?.type ?? ConnectorTypes.jira,
|
||||
name: connector?.name ?? ConnectorTypes.jira,
|
||||
fields: connector?.fields ?? null,
|
||||
};
|
||||
|
||||
return {
|
||||
type: CASE_CONFIGURE_SAVED_OBJECT,
|
||||
id: '1',
|
||||
attributes: {
|
||||
...basicConfigFields,
|
||||
// if connector is null we'll default this to an incomplete jira value because the service
|
||||
// should switch it to a none connector when the id can't be found in the references array
|
||||
connector: formattedConnector,
|
||||
},
|
||||
references,
|
||||
};
|
||||
};
|
||||
|
||||
const createConfigSOPromise = (
|
||||
connector?: ESCaseConnectorWithId
|
||||
): Promise<SavedObject<ESCasesConfigureAttributes>> => Promise.resolve(createConfigSO(connector));
|
||||
|
||||
const createConfigFindSO = (
|
||||
connector?: ESCaseConnectorWithId
|
||||
): SavedObjectsFindResult<ESCasesConfigureAttributes> => ({
|
||||
...createConfigSO(connector),
|
||||
score: 0,
|
||||
});
|
||||
|
||||
const createSOFindResponse = (
|
||||
savedObjects: Array<SavedObjectsFindResult<ESCasesConfigureAttributes>>
|
||||
) => ({
|
||||
saved_objects: savedObjects,
|
||||
total: savedObjects.length,
|
||||
per_page: savedObjects.length,
|
||||
page: 1,
|
||||
});
|
||||
|
||||
describe('CaseConfigureService', () => {
|
||||
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
|
||||
const mockLogger = loggerMock.create();
|
||||
|
||||
let service: CaseConfigureService;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
service = new CaseConfigureService(mockLogger);
|
||||
});
|
||||
|
||||
describe('transforms the external model to the Elasticsearch model', () => {
|
||||
describe('patch', () => {
|
||||
it('creates the update attributes with the fields that were passed in', async () => {
|
||||
unsecuredSavedObjectsClient.update.mockReturnValue(
|
||||
Promise.resolve({} as SavedObjectsUpdateResponse<CasesConfigurePatch>)
|
||||
);
|
||||
|
||||
await service.patch({
|
||||
configurationId: '1',
|
||||
unsecuredSavedObjectsClient,
|
||||
updatedAttributes: createConfigPostParams(createJiraConnector()),
|
||||
originalConfiguration: {} as SavedObject<CasesConfigureAttributes>,
|
||||
});
|
||||
|
||||
const { connector: ignoreConnector, ...restUpdateAttributes } = unsecuredSavedObjectsClient
|
||||
.update.mock.calls[0][2] as Partial<ESCasesConfigureAttributes>;
|
||||
|
||||
expect(restUpdateAttributes).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"closure_type": "close-by-pushing",
|
||||
"created_at": "2020-04-09T09:43:51.778Z",
|
||||
"created_by": Object {
|
||||
"email": "testemail@elastic.co",
|
||||
"full_name": "elastic",
|
||||
"username": "elastic",
|
||||
},
|
||||
"owner": "securitySolution",
|
||||
"updated_at": "2020-04-09T09:43:51.778Z",
|
||||
"updated_by": Object {
|
||||
"email": "testemail@elastic.co",
|
||||
"full_name": "elastic",
|
||||
"username": "elastic",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('transforms the connector.fields to an array of key/value pairs', async () => {
|
||||
unsecuredSavedObjectsClient.update.mockReturnValue(
|
||||
Promise.resolve({} as SavedObjectsUpdateResponse<CasesConfigurePatch>)
|
||||
);
|
||||
|
||||
await service.patch({
|
||||
configurationId: '1',
|
||||
unsecuredSavedObjectsClient,
|
||||
updatedAttributes: createConfigPostParams(createJiraConnector()),
|
||||
originalConfiguration: {} as SavedObject<CasesConfigureAttributes>,
|
||||
});
|
||||
|
||||
const { connector } = unsecuredSavedObjectsClient.update.mock
|
||||
.calls[0][2] as Partial<ESCasesConfigureAttributes>;
|
||||
|
||||
expect(connector?.fields).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"key": "issueType",
|
||||
"value": "bug",
|
||||
},
|
||||
Object {
|
||||
"key": "priority",
|
||||
"value": "high",
|
||||
},
|
||||
Object {
|
||||
"key": "parent",
|
||||
"value": "2",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('preserves the connector fields but does not include the id', async () => {
|
||||
unsecuredSavedObjectsClient.update.mockReturnValue(
|
||||
Promise.resolve({} as SavedObjectsUpdateResponse<CasesConfigurePatch>)
|
||||
);
|
||||
|
||||
await service.patch({
|
||||
configurationId: '1',
|
||||
unsecuredSavedObjectsClient,
|
||||
updatedAttributes: createConfigPostParams(createJiraConnector()),
|
||||
originalConfiguration: {} as SavedObject<CasesConfigureAttributes>,
|
||||
});
|
||||
|
||||
const { connector } = unsecuredSavedObjectsClient.update.mock
|
||||
.calls[0][2] as Partial<ESCasesConfigureAttributes>;
|
||||
|
||||
expect(connector).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"fields": Array [
|
||||
Object {
|
||||
"key": "issueType",
|
||||
"value": "bug",
|
||||
},
|
||||
Object {
|
||||
"key": "priority",
|
||||
"value": "high",
|
||||
},
|
||||
Object {
|
||||
"key": "parent",
|
||||
"value": "2",
|
||||
},
|
||||
],
|
||||
"name": ".jira",
|
||||
"type": ".jira",
|
||||
}
|
||||
`);
|
||||
expect(connector).not.toHaveProperty('id');
|
||||
});
|
||||
|
||||
it('moves the connector.id to the references', async () => {
|
||||
unsecuredSavedObjectsClient.update.mockReturnValue(
|
||||
Promise.resolve({} as SavedObjectsUpdateResponse<CasesConfigurePatch>)
|
||||
);
|
||||
|
||||
await service.patch({
|
||||
configurationId: '1',
|
||||
unsecuredSavedObjectsClient,
|
||||
updatedAttributes: createConfigPostParams(createJiraConnector()),
|
||||
originalConfiguration: {} as SavedObject<CasesConfigureAttributes>,
|
||||
});
|
||||
|
||||
const updateAttributes = unsecuredSavedObjectsClient.update.mock
|
||||
.calls[0][2] as Partial<ESCasesConfigureAttributes>;
|
||||
|
||||
expect(updateAttributes.connector).not.toHaveProperty('id');
|
||||
|
||||
const updateOptions = unsecuredSavedObjectsClient.update.mock
|
||||
.calls[0][3] as SavedObjectsUpdateOptions;
|
||||
expect(updateOptions.references).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "connectorId",
|
||||
"type": "action",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('moves the connector.id to the references and includes the existing references', async () => {
|
||||
unsecuredSavedObjectsClient.update.mockReturnValue(
|
||||
Promise.resolve({} as SavedObjectsUpdateResponse<CasesConfigurePatch>)
|
||||
);
|
||||
|
||||
await service.patch({
|
||||
configurationId: '1',
|
||||
unsecuredSavedObjectsClient,
|
||||
updatedAttributes: createConfigPostParams(createJiraConnector()),
|
||||
originalConfiguration: {
|
||||
references: [{ id: '123', name: 'awesome', type: 'hello' }],
|
||||
} as SavedObject<CasesConfigureAttributes>,
|
||||
});
|
||||
|
||||
const updateOptions = unsecuredSavedObjectsClient.update.mock
|
||||
.calls[0][3] as SavedObjectsUpdateOptions;
|
||||
expect(updateOptions.references).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"id": "123",
|
||||
"name": "awesome",
|
||||
"type": "hello",
|
||||
},
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "connectorId",
|
||||
"type": "action",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('does not remove the connector.id reference when the update attributes do not include it', async () => {
|
||||
unsecuredSavedObjectsClient.update.mockReturnValue(
|
||||
Promise.resolve({} as SavedObjectsUpdateResponse<CasesConfigurePatch>)
|
||||
);
|
||||
|
||||
await service.patch({
|
||||
configurationId: '1',
|
||||
unsecuredSavedObjectsClient,
|
||||
updatedAttributes: createConfigUpdateParams(),
|
||||
originalConfiguration: {
|
||||
references: [
|
||||
{ id: '123', name: CONNECTOR_ID_REFERENCE_NAME, type: ACTION_SAVED_OBJECT_TYPE },
|
||||
],
|
||||
} as SavedObject<CasesConfigureAttributes>,
|
||||
});
|
||||
|
||||
const updateOptions = unsecuredSavedObjectsClient.update.mock
|
||||
.calls[0][3] as SavedObjectsUpdateOptions;
|
||||
expect(updateOptions.references).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"id": "123",
|
||||
"name": "connectorId",
|
||||
"type": "action",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('creates an empty update object and null reference when there is no connector', async () => {
|
||||
unsecuredSavedObjectsClient.update.mockReturnValue(
|
||||
Promise.resolve({} as SavedObjectsUpdateResponse<CasesConfigurePatch>)
|
||||
);
|
||||
|
||||
await service.patch({
|
||||
configurationId: '1',
|
||||
unsecuredSavedObjectsClient,
|
||||
updatedAttributes: createConfigUpdateParams(),
|
||||
originalConfiguration: {} as SavedObject<CasesConfigureAttributes>,
|
||||
});
|
||||
|
||||
expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(
|
||||
`Object {}`
|
||||
);
|
||||
expect(unsecuredSavedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"references": undefined,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('creates an update object with the none connector', async () => {
|
||||
unsecuredSavedObjectsClient.update.mockReturnValue(
|
||||
Promise.resolve({} as SavedObjectsUpdateResponse<CasesConfigurePatch>)
|
||||
);
|
||||
|
||||
await service.patch({
|
||||
configurationId: '1',
|
||||
unsecuredSavedObjectsClient,
|
||||
updatedAttributes: createConfigUpdateParams(getNoneCaseConnector()),
|
||||
originalConfiguration: {} as SavedObject<CasesConfigureAttributes>,
|
||||
});
|
||||
|
||||
expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"connector": Object {
|
||||
"fields": Array [],
|
||||
"name": "none",
|
||||
"type": ".none",
|
||||
},
|
||||
}
|
||||
`);
|
||||
const updateOptions = unsecuredSavedObjectsClient.update.mock
|
||||
.calls[0][3] as SavedObjectsUpdateOptions;
|
||||
expect(updateOptions.references).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('post', () => {
|
||||
it('includes the creation attributes excluding the connector.id field', async () => {
|
||||
unsecuredSavedObjectsClient.create.mockReturnValue(
|
||||
Promise.resolve({} as SavedObject<ESCasesConfigureAttributes>)
|
||||
);
|
||||
|
||||
await service.post({
|
||||
unsecuredSavedObjectsClient,
|
||||
attributes: createConfigPostParams(createJiraConnector()),
|
||||
id: '1',
|
||||
});
|
||||
|
||||
const creationAttributes = unsecuredSavedObjectsClient.create.mock
|
||||
.calls[0][1] as ESCasesConfigureAttributes;
|
||||
expect(creationAttributes.connector).not.toHaveProperty('id');
|
||||
expect(creationAttributes).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"closure_type": "close-by-pushing",
|
||||
"connector": Object {
|
||||
"fields": Array [
|
||||
Object {
|
||||
"key": "issueType",
|
||||
"value": "bug",
|
||||
},
|
||||
Object {
|
||||
"key": "priority",
|
||||
"value": "high",
|
||||
},
|
||||
Object {
|
||||
"key": "parent",
|
||||
"value": "2",
|
||||
},
|
||||
],
|
||||
"name": ".jira",
|
||||
"type": ".jira",
|
||||
},
|
||||
"created_at": "2020-04-09T09:43:51.778Z",
|
||||
"created_by": Object {
|
||||
"email": "testemail@elastic.co",
|
||||
"full_name": "elastic",
|
||||
"username": "elastic",
|
||||
},
|
||||
"owner": "securitySolution",
|
||||
"updated_at": "2020-04-09T09:43:51.778Z",
|
||||
"updated_by": Object {
|
||||
"email": "testemail@elastic.co",
|
||||
"full_name": "elastic",
|
||||
"username": "elastic",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('moves the connector.id to the references', async () => {
|
||||
unsecuredSavedObjectsClient.create.mockReturnValue(
|
||||
Promise.resolve({} as SavedObject<ESCasesConfigureAttributes>)
|
||||
);
|
||||
|
||||
await service.post({
|
||||
unsecuredSavedObjectsClient,
|
||||
attributes: createConfigPostParams(createJiraConnector()),
|
||||
id: '1',
|
||||
});
|
||||
|
||||
expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"id": "1",
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "connectorId",
|
||||
"type": "action",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('sets connector.fields to an empty array when it is not included', async () => {
|
||||
unsecuredSavedObjectsClient.create.mockReturnValue(
|
||||
Promise.resolve({} as SavedObject<ESCasesConfigureAttributes>)
|
||||
);
|
||||
|
||||
await service.post({
|
||||
unsecuredSavedObjectsClient,
|
||||
attributes: createConfigPostParams(createJiraConnector({ setFieldsToNull: true })),
|
||||
id: '1',
|
||||
});
|
||||
|
||||
const postAttributes = unsecuredSavedObjectsClient.create.mock
|
||||
.calls[0][1] as CasesConfigureAttributes;
|
||||
expect(postAttributes.connector).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"fields": Array [],
|
||||
"name": ".jira",
|
||||
"type": ".jira",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('does not create a reference for a none connector', async () => {
|
||||
unsecuredSavedObjectsClient.create.mockReturnValue(
|
||||
Promise.resolve({} as SavedObject<ESCasesConfigureAttributes>)
|
||||
);
|
||||
|
||||
await service.post({
|
||||
unsecuredSavedObjectsClient,
|
||||
attributes: createConfigPostParams(getNoneCaseConnector()),
|
||||
id: '1',
|
||||
});
|
||||
|
||||
const creationOptions = unsecuredSavedObjectsClient.create.mock
|
||||
.calls[0][2] as SavedObjectsCreateOptions;
|
||||
expect(creationOptions.references).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('transform the Elasticsearch model to the external model', () => {
|
||||
describe('patch', () => {
|
||||
it('returns an object with a none connector and without a reference', async () => {
|
||||
unsecuredSavedObjectsClient.update.mockReturnValue(
|
||||
Promise.resolve(createUpdateConfigSO(getNoneCaseConnector()))
|
||||
);
|
||||
|
||||
const res = await service.patch({
|
||||
configurationId: '1',
|
||||
unsecuredSavedObjectsClient,
|
||||
updatedAttributes: createConfigUpdateParams(),
|
||||
originalConfiguration: {} as SavedObject<CasesConfigureAttributes>,
|
||||
});
|
||||
|
||||
expect(res.attributes).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"connector": Object {
|
||||
"fields": null,
|
||||
"id": "none",
|
||||
"name": "none",
|
||||
"type": ".none",
|
||||
},
|
||||
}
|
||||
`);
|
||||
expect(res.references).toMatchInlineSnapshot(`Array []`);
|
||||
});
|
||||
|
||||
it('returns an undefined connector if it is not returned by the update', async () => {
|
||||
unsecuredSavedObjectsClient.update.mockReturnValue(
|
||||
Promise.resolve({} as SavedObjectsUpdateResponse<ESCasesConfigureAttributes>)
|
||||
);
|
||||
|
||||
const res = await service.patch({
|
||||
configurationId: '1',
|
||||
unsecuredSavedObjectsClient,
|
||||
updatedAttributes: createConfigUpdateParams(),
|
||||
originalConfiguration: {} as SavedObject<CasesConfigureAttributes>,
|
||||
});
|
||||
|
||||
expect(res).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns the default none connector when it cannot find the reference', async () => {
|
||||
const { name, type, fields } = createESJiraConnector();
|
||||
const returnValue: SavedObjectsUpdateResponse<ESCasesConfigureAttributes> = {
|
||||
type: CASE_CONFIGURE_SAVED_OBJECT,
|
||||
id: '1',
|
||||
attributes: {
|
||||
connector: {
|
||||
name,
|
||||
type,
|
||||
fields,
|
||||
},
|
||||
},
|
||||
version: '1',
|
||||
references: undefined,
|
||||
};
|
||||
|
||||
unsecuredSavedObjectsClient.update.mockReturnValue(Promise.resolve(returnValue));
|
||||
|
||||
const res = await service.patch({
|
||||
configurationId: '1',
|
||||
unsecuredSavedObjectsClient,
|
||||
updatedAttributes: createConfigUpdateParams(),
|
||||
originalConfiguration: {} as SavedObject<CasesConfigureAttributes>,
|
||||
});
|
||||
|
||||
expect(res.attributes.connector).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"fields": null,
|
||||
"id": "none",
|
||||
"name": "none",
|
||||
"type": ".none",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns a jira connector', async () => {
|
||||
unsecuredSavedObjectsClient.update.mockReturnValue(
|
||||
Promise.resolve(createUpdateConfigSO(createESJiraConnector()))
|
||||
);
|
||||
|
||||
const res = await service.patch({
|
||||
configurationId: '1',
|
||||
unsecuredSavedObjectsClient,
|
||||
updatedAttributes: createConfigUpdateParams(),
|
||||
originalConfiguration: {} as SavedObject<CasesConfigureAttributes>,
|
||||
});
|
||||
|
||||
expect(res.attributes.connector).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"fields": Object {
|
||||
"issueType": "bug",
|
||||
"parent": "2",
|
||||
"priority": "high",
|
||||
},
|
||||
"id": "1",
|
||||
"name": ".jira",
|
||||
"type": ".jira",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('find', () => {
|
||||
it('includes the id field in the response', async () => {
|
||||
const findMockReturn = createSOFindResponse([
|
||||
createConfigFindSO(createESJiraConnector()),
|
||||
createConfigFindSO(),
|
||||
]);
|
||||
unsecuredSavedObjectsClient.find.mockReturnValue(Promise.resolve(findMockReturn));
|
||||
|
||||
const res = await service.find({ unsecuredSavedObjectsClient });
|
||||
expect(res.saved_objects[0].attributes.connector.id).toMatchInlineSnapshot(`"1"`);
|
||||
});
|
||||
|
||||
it('includes the saved object find response fields in the result', async () => {
|
||||
const findMockReturn = createSOFindResponse([
|
||||
createConfigFindSO(createESJiraConnector()),
|
||||
createConfigFindSO(),
|
||||
]);
|
||||
unsecuredSavedObjectsClient.find.mockReturnValue(Promise.resolve(findMockReturn));
|
||||
|
||||
const res = await service.find({ unsecuredSavedObjectsClient });
|
||||
const { saved_objects: ignored, ...findResponseFields } = res;
|
||||
expect(findResponseFields).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"page": 1,
|
||||
"per_page": 2,
|
||||
"total": 2,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('defaults to the none connector when the id cannot be found in the references', async () => {
|
||||
const findMockReturn = createSOFindResponse([
|
||||
createConfigFindSO(createESJiraConnector()),
|
||||
createConfigFindSO(),
|
||||
]);
|
||||
unsecuredSavedObjectsClient.find.mockReturnValue(Promise.resolve(findMockReturn));
|
||||
|
||||
const res = await service.find({ unsecuredSavedObjectsClient });
|
||||
expect(res.saved_objects[1].attributes.connector).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"fields": null,
|
||||
"id": "none",
|
||||
"name": "none",
|
||||
"type": ".none",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('includes the id field in the response', async () => {
|
||||
unsecuredSavedObjectsClient.get.mockReturnValue(
|
||||
createConfigSOPromise(createESJiraConnector())
|
||||
);
|
||||
const res = await service.get({ unsecuredSavedObjectsClient, configurationId: '1' });
|
||||
|
||||
expect(res.attributes.connector.id).toMatchInlineSnapshot(`"1"`);
|
||||
});
|
||||
|
||||
it('defaults to the none connector when the connector reference cannot be found', async () => {
|
||||
unsecuredSavedObjectsClient.get.mockReturnValue(createConfigSOPromise());
|
||||
const res = await service.get({ unsecuredSavedObjectsClient, configurationId: '1' });
|
||||
|
||||
expect(res.attributes.connector).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"fields": null,
|
||||
"id": "none",
|
||||
"name": "none",
|
||||
"type": ".none",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('defaults to the none connector when attributes is undefined', async () => {
|
||||
unsecuredSavedObjectsClient.get.mockReturnValue(
|
||||
Promise.resolve(({
|
||||
references: [
|
||||
{
|
||||
id: '1',
|
||||
name: CONNECTOR_ID_REFERENCE_NAME,
|
||||
type: ACTION_SAVED_OBJECT_TYPE,
|
||||
},
|
||||
],
|
||||
} as unknown) as SavedObject<ESCasesConfigureAttributes>)
|
||||
);
|
||||
const res = await service.get({ unsecuredSavedObjectsClient, configurationId: '1' });
|
||||
|
||||
expect(res.attributes.connector).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"fields": null,
|
||||
"id": "none",
|
||||
"name": "none",
|
||||
"type": ".none",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -5,10 +5,28 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Logger, SavedObjectsClientContract } from 'kibana/server';
|
||||
import {
|
||||
Logger,
|
||||
SavedObject,
|
||||
SavedObjectsClientContract,
|
||||
SavedObjectsFindResponse,
|
||||
SavedObjectsUpdateResponse,
|
||||
} from 'kibana/server';
|
||||
|
||||
import { SavedObjectFindOptionsKueryNode } from '../../common';
|
||||
import { ESCasesConfigureAttributes, CASE_CONFIGURE_SAVED_OBJECT } from '../../../common';
|
||||
import { SavedObjectFindOptionsKueryNode, CONNECTOR_ID_REFERENCE_NAME } from '../../common';
|
||||
import {
|
||||
CASE_CONFIGURE_SAVED_OBJECT,
|
||||
CasesConfigureAttributes,
|
||||
CasesConfigurePatch,
|
||||
} from '../../../common';
|
||||
import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server';
|
||||
import {
|
||||
transformFieldsToESModel,
|
||||
transformESConnectorToExternalModel,
|
||||
transformESConnectorOrUseDefault,
|
||||
} from '../transform';
|
||||
import { ConnectorReferenceHandler } from '../connector_reference_handler';
|
||||
import { ESCasesConfigureAttributes } from './types';
|
||||
|
||||
interface ClientArgs {
|
||||
unsecuredSavedObjectsClient: SavedObjectsClientContract;
|
||||
|
@ -22,13 +40,14 @@ interface FindCaseConfigureArgs extends ClientArgs {
|
|||
}
|
||||
|
||||
interface PostCaseConfigureArgs extends ClientArgs {
|
||||
attributes: ESCasesConfigureAttributes;
|
||||
attributes: CasesConfigureAttributes;
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface PatchCaseConfigureArgs extends ClientArgs {
|
||||
configurationId: string;
|
||||
updatedAttributes: Partial<ESCasesConfigureAttributes>;
|
||||
updatedAttributes: Partial<CasesConfigureAttributes>;
|
||||
originalConfiguration: SavedObject<CasesConfigureAttributes>;
|
||||
}
|
||||
|
||||
export class CaseConfigureService {
|
||||
|
@ -44,45 +63,61 @@ export class CaseConfigureService {
|
|||
}
|
||||
}
|
||||
|
||||
public async get({ unsecuredSavedObjectsClient, configurationId }: GetCaseConfigureArgs) {
|
||||
public async get({
|
||||
unsecuredSavedObjectsClient,
|
||||
configurationId,
|
||||
}: GetCaseConfigureArgs): Promise<SavedObject<CasesConfigureAttributes>> {
|
||||
try {
|
||||
this.log.debug(`Attempting to GET case configuration ${configurationId}`);
|
||||
return await unsecuredSavedObjectsClient.get<ESCasesConfigureAttributes>(
|
||||
const configuration = await unsecuredSavedObjectsClient.get<ESCasesConfigureAttributes>(
|
||||
CASE_CONFIGURE_SAVED_OBJECT,
|
||||
configurationId
|
||||
);
|
||||
|
||||
return transformToExternalModel(configuration);
|
||||
} catch (error) {
|
||||
this.log.debug(`Error on GET case configuration ${configurationId}: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async find({ unsecuredSavedObjectsClient, options }: FindCaseConfigureArgs) {
|
||||
public async find({
|
||||
unsecuredSavedObjectsClient,
|
||||
options,
|
||||
}: FindCaseConfigureArgs): Promise<SavedObjectsFindResponse<CasesConfigureAttributes>> {
|
||||
try {
|
||||
this.log.debug(`Attempting to find all case configuration`);
|
||||
return await unsecuredSavedObjectsClient.find<ESCasesConfigureAttributes>({
|
||||
|
||||
const findResp = await unsecuredSavedObjectsClient.find<ESCasesConfigureAttributes>({
|
||||
...options,
|
||||
// Get the latest configuration
|
||||
sortField: 'created_at',
|
||||
sortOrder: 'desc',
|
||||
type: CASE_CONFIGURE_SAVED_OBJECT,
|
||||
});
|
||||
|
||||
return transformFindResponseToExternalModel(findResp);
|
||||
} catch (error) {
|
||||
this.log.debug(`Attempting to find all case configuration`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async post({ unsecuredSavedObjectsClient, attributes, id }: PostCaseConfigureArgs) {
|
||||
public async post({
|
||||
unsecuredSavedObjectsClient,
|
||||
attributes,
|
||||
id,
|
||||
}: PostCaseConfigureArgs): Promise<SavedObject<CasesConfigureAttributes>> {
|
||||
try {
|
||||
this.log.debug(`Attempting to POST a new case configuration`);
|
||||
return await unsecuredSavedObjectsClient.create<ESCasesConfigureAttributes>(
|
||||
const esConfigInfo = transformAttributesToESModel(attributes);
|
||||
const createdConfig = await unsecuredSavedObjectsClient.create<ESCasesConfigureAttributes>(
|
||||
CASE_CONFIGURE_SAVED_OBJECT,
|
||||
{
|
||||
...attributes,
|
||||
},
|
||||
{ id }
|
||||
esConfigInfo.attributes,
|
||||
{ id, references: esConfigInfo.referenceHandler.build() }
|
||||
);
|
||||
|
||||
return transformToExternalModel(createdConfig);
|
||||
} catch (error) {
|
||||
this.log.debug(`Error on POST a new case configuration: ${error}`);
|
||||
throw error;
|
||||
|
@ -93,19 +128,124 @@ export class CaseConfigureService {
|
|||
unsecuredSavedObjectsClient,
|
||||
configurationId,
|
||||
updatedAttributes,
|
||||
}: PatchCaseConfigureArgs) {
|
||||
originalConfiguration,
|
||||
}: PatchCaseConfigureArgs): Promise<SavedObjectsUpdateResponse<CasesConfigurePatch>> {
|
||||
try {
|
||||
this.log.debug(`Attempting to UPDATE case configuration ${configurationId}`);
|
||||
return await unsecuredSavedObjectsClient.update<ESCasesConfigureAttributes>(
|
||||
const esUpdateInfo = transformAttributesToESModel(updatedAttributes);
|
||||
|
||||
const updatedConfiguration = await unsecuredSavedObjectsClient.update<ESCasesConfigureAttributes>(
|
||||
CASE_CONFIGURE_SAVED_OBJECT,
|
||||
configurationId,
|
||||
{
|
||||
...updatedAttributes,
|
||||
...esUpdateInfo.attributes,
|
||||
},
|
||||
{
|
||||
references: esUpdateInfo.referenceHandler.build(originalConfiguration.references),
|
||||
}
|
||||
);
|
||||
|
||||
return transformUpdateResponseToExternalModel(updatedConfiguration);
|
||||
} catch (error) {
|
||||
this.log.debug(`Error on UPDATE case configuration ${configurationId}: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function transformUpdateResponseToExternalModel(
|
||||
updatedConfiguration: SavedObjectsUpdateResponse<ESCasesConfigureAttributes>
|
||||
): SavedObjectsUpdateResponse<CasesConfigurePatch> {
|
||||
const { connector, ...restUpdatedAttributes } = updatedConfiguration.attributes ?? {};
|
||||
|
||||
const transformedConnector = transformESConnectorToExternalModel({
|
||||
connector,
|
||||
references: updatedConfiguration.references,
|
||||
referenceName: CONNECTOR_ID_REFERENCE_NAME,
|
||||
});
|
||||
|
||||
return {
|
||||
...updatedConfiguration,
|
||||
attributes: {
|
||||
...restUpdatedAttributes,
|
||||
// this will avoid setting connector to undefined, it won't include to field at all
|
||||
...(transformedConnector && { connector: transformedConnector }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function transformToExternalModel(
|
||||
configuration: SavedObject<ESCasesConfigureAttributes>
|
||||
): SavedObject<CasesConfigureAttributes> {
|
||||
const connector = transformESConnectorOrUseDefault({
|
||||
// if the saved object had an error the attributes field will not exist
|
||||
connector: configuration.attributes?.connector,
|
||||
references: configuration.references,
|
||||
referenceName: CONNECTOR_ID_REFERENCE_NAME,
|
||||
});
|
||||
|
||||
return {
|
||||
...configuration,
|
||||
attributes: {
|
||||
...configuration.attributes,
|
||||
connector,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function transformFindResponseToExternalModel(
|
||||
configurations: SavedObjectsFindResponse<ESCasesConfigureAttributes>
|
||||
): SavedObjectsFindResponse<CasesConfigureAttributes> {
|
||||
return {
|
||||
...configurations,
|
||||
saved_objects: configurations.saved_objects.map((so) => ({
|
||||
...so,
|
||||
...transformToExternalModel(so),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function transformAttributesToESModel(
|
||||
configuration: CasesConfigureAttributes
|
||||
): {
|
||||
attributes: ESCasesConfigureAttributes;
|
||||
referenceHandler: ConnectorReferenceHandler;
|
||||
};
|
||||
function transformAttributesToESModel(
|
||||
configuration: Partial<CasesConfigureAttributes>
|
||||
): {
|
||||
attributes: Partial<ESCasesConfigureAttributes>;
|
||||
referenceHandler: ConnectorReferenceHandler;
|
||||
};
|
||||
function transformAttributesToESModel(
|
||||
configuration: Partial<CasesConfigureAttributes>
|
||||
): {
|
||||
attributes: Partial<ESCasesConfigureAttributes>;
|
||||
referenceHandler: ConnectorReferenceHandler;
|
||||
} {
|
||||
const { connector, ...restWithoutConnector } = configuration;
|
||||
|
||||
const transformedConnector = {
|
||||
...(connector && {
|
||||
connector: {
|
||||
name: connector.name,
|
||||
type: connector.type,
|
||||
fields: transformFieldsToESModel(connector),
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
return {
|
||||
attributes: {
|
||||
...restWithoutConnector,
|
||||
...transformedConnector,
|
||||
},
|
||||
referenceHandler: buildReferenceHandler(connector?.id),
|
||||
};
|
||||
}
|
||||
|
||||
function buildReferenceHandler(id?: string): ConnectorReferenceHandler {
|
||||
return new ConnectorReferenceHandler([
|
||||
{ id, name: CONNECTOR_ID_REFERENCE_NAME, type: ACTION_SAVED_OBJECT_TYPE },
|
||||
]);
|
||||
}
|
||||
|
|
17
x-pack/plugins/cases/server/services/configure/types.ts
Normal file
17
x-pack/plugins/cases/server/services/configure/types.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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 { CasesConfigureAttributes } from '../../../common';
|
||||
import { ESCaseConnector } from '..';
|
||||
|
||||
/**
|
||||
* This type should only be used within the configure service. It represents how the configure saved object will be layed
|
||||
* out in ES.
|
||||
*/
|
||||
export type ESCasesConfigureAttributes = Omit<CasesConfigureAttributes, 'connector'> & {
|
||||
connector: ESCaseConnector;
|
||||
};
|
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
* 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 { noneConnectorId } from '../../common';
|
||||
import { ConnectorReferenceHandler } from './connector_reference_handler';
|
||||
|
||||
describe('ConnectorReferenceHandler', () => {
|
||||
describe('merge', () => {
|
||||
it('overwrites the original reference with the new one', () => {
|
||||
const handler = new ConnectorReferenceHandler([{ id: 'hello2', type: '1', name: 'a' }]);
|
||||
|
||||
expect(handler.build([{ id: 'hello', type: '1', name: 'a' }])).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"id": "hello2",
|
||||
"name": "a",
|
||||
"type": "1",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns the original references if the new references is an empty array', () => {
|
||||
const handler = new ConnectorReferenceHandler([]);
|
||||
|
||||
expect(handler.build([{ id: 'hello', type: '1', name: 'a' }])).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"id": "hello",
|
||||
"name": "a",
|
||||
"type": "1",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns undefined when there are no original references and no new ones', () => {
|
||||
const handler = new ConnectorReferenceHandler([]);
|
||||
|
||||
expect(handler.build()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns an empty array when there is an empty array of original references and no new ones', () => {
|
||||
const handler = new ConnectorReferenceHandler([]);
|
||||
|
||||
expect(handler.build([])).toMatchInlineSnapshot(`Array []`);
|
||||
});
|
||||
|
||||
it('removes a reference when the id field is null', () => {
|
||||
const handler = new ConnectorReferenceHandler([{ id: null, name: 'a', type: '1' }]);
|
||||
|
||||
expect(handler.build([{ id: 'hello', type: '1', name: 'a' }])).toMatchInlineSnapshot(
|
||||
`Array []`
|
||||
);
|
||||
});
|
||||
|
||||
it('removes a reference when the id field is the none connector', () => {
|
||||
const handler = new ConnectorReferenceHandler([
|
||||
{ id: noneConnectorId, name: 'a', type: '1' },
|
||||
]);
|
||||
|
||||
expect(handler.build([{ id: 'hello', type: '1', name: 'a' }])).toMatchInlineSnapshot(
|
||||
`Array []`
|
||||
);
|
||||
});
|
||||
|
||||
it('does not remove a reference when the id field is undefined', () => {
|
||||
const handler = new ConnectorReferenceHandler([{ id: undefined, name: 'a', type: '1' }]);
|
||||
|
||||
expect(handler.build([{ id: 'hello', type: '1', name: 'a' }])).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"id": "hello",
|
||||
"name": "a",
|
||||
"type": "1",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('adds a new reference to existing ones', () => {
|
||||
const handler = new ConnectorReferenceHandler([{ id: 'awesome', type: '2', name: 'b' }]);
|
||||
|
||||
expect(handler.build([{ id: 'hello', type: '1', name: 'a' }])).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"id": "hello",
|
||||
"name": "a",
|
||||
"type": "1",
|
||||
},
|
||||
Object {
|
||||
"id": "awesome",
|
||||
"name": "b",
|
||||
"type": "2",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('adds new references to an undefined original reference array', () => {
|
||||
const handler = new ConnectorReferenceHandler([
|
||||
{ id: 'awesome', type: '2', name: 'a' },
|
||||
{ id: 'awesome', type: '2', name: 'b' },
|
||||
]);
|
||||
|
||||
expect(handler.build()).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"id": "awesome",
|
||||
"name": "a",
|
||||
"type": "2",
|
||||
},
|
||||
Object {
|
||||
"id": "awesome",
|
||||
"name": "b",
|
||||
"type": "2",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('adds new references to an empty original reference array', () => {
|
||||
const handler = new ConnectorReferenceHandler([
|
||||
{ id: 'awesome', type: '2', name: 'a' },
|
||||
{ id: 'awesome', type: '2', name: 'b' },
|
||||
]);
|
||||
|
||||
expect(handler.build()).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"id": "awesome",
|
||||
"name": "a",
|
||||
"type": "2",
|
||||
},
|
||||
Object {
|
||||
"id": "awesome",
|
||||
"name": "b",
|
||||
"type": "2",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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 { SavedObjectReference } from 'kibana/server';
|
||||
import { noneConnectorId } from '../../common';
|
||||
|
||||
interface Reference {
|
||||
soReference?: SavedObjectReference;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export class ConnectorReferenceHandler {
|
||||
private newReferences: Reference[] = [];
|
||||
|
||||
constructor(references: Array<{ id?: string | null; name: string; type: string }>) {
|
||||
for (const { id, name, type } of references) {
|
||||
// When id is null, or the none connector we'll try to remove the reference if it exists
|
||||
// When id is undefined it means that we're doing a patch request and this particular field shouldn't be updated
|
||||
// so we'll ignore it. If it was already in the reference array then it'll stay there when we merge them together below
|
||||
if (id === null || id === noneConnectorId) {
|
||||
this.newReferences.push({ name });
|
||||
} else if (id) {
|
||||
this.newReferences.push({ soReference: { id, name, type }, name });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the references passed to the constructor into the original references passed into this function
|
||||
*
|
||||
* @param originalReferences existing saved object references
|
||||
* @returns a merged reference list or undefined when there are no new or existing references
|
||||
*/
|
||||
public build(originalReferences?: SavedObjectReference[]): SavedObjectReference[] | undefined {
|
||||
if (this.newReferences.length <= 0) {
|
||||
return originalReferences;
|
||||
}
|
||||
|
||||
const refMap = new Map<string, SavedObjectReference>(
|
||||
originalReferences?.map((ref) => [ref.name, ref])
|
||||
);
|
||||
|
||||
for (const newRef of this.newReferences) {
|
||||
if (newRef.soReference) {
|
||||
refMap.set(newRef.name, newRef.soReference);
|
||||
} else {
|
||||
refMap.delete(newRef.name);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(refMap.values());
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { SavedObjectsClientContract } from 'kibana/server';
|
||||
import { ConnectorTypes } from '../../common';
|
||||
|
||||
export { CasesService } from './cases';
|
||||
export { CaseConfigureService } from './configure';
|
||||
|
@ -17,3 +18,14 @@ export { AttachmentService } from './attachments';
|
|||
export interface ClientArgs {
|
||||
unsecuredSavedObjectsClient: SavedObjectsClientContract;
|
||||
}
|
||||
|
||||
export type ESConnectorFields = Array<{
|
||||
key: string;
|
||||
value: unknown;
|
||||
}>;
|
||||
|
||||
export interface ESCaseConnector {
|
||||
name: string;
|
||||
type: ConnectorTypes;
|
||||
fields: ESConnectorFields | null;
|
||||
}
|
||||
|
|
200
x-pack/plugins/cases/server/services/test_utils.ts
Normal file
200
x-pack/plugins/cases/server/services/test_utils.ts
Normal file
|
@ -0,0 +1,200 @@
|
|||
/*
|
||||
* 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 { SavedObject, SavedObjectReference } from 'kibana/server';
|
||||
import { ESConnectorFields } from '.';
|
||||
import { CONNECTOR_ID_REFERENCE_NAME, PUSH_CONNECTOR_ID_REFERENCE_NAME } from '../common';
|
||||
import {
|
||||
CaseConnector,
|
||||
CaseFullExternalService,
|
||||
CaseStatuses,
|
||||
CaseType,
|
||||
CASE_SAVED_OBJECT,
|
||||
ConnectorTypes,
|
||||
noneConnectorId,
|
||||
SECURITY_SOLUTION_OWNER,
|
||||
} from '../../common';
|
||||
import { ESCaseAttributes, ExternalServicesWithoutConnectorId } from './cases/types';
|
||||
import { ACTION_SAVED_OBJECT_TYPE } from '../../../actions/server';
|
||||
|
||||
/**
|
||||
* This is only a utility interface to help with constructing test cases. After the migration, the ES format will no longer
|
||||
* have the id field. Instead it will be moved to the references array.
|
||||
*/
|
||||
export interface ESCaseConnectorWithId {
|
||||
id: string;
|
||||
name: string;
|
||||
type: ConnectorTypes;
|
||||
fields: ESConnectorFields | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* This file contains utility functions to aid unit test development
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create an Elasticsearch jira connector.
|
||||
*
|
||||
* @param overrides fields used to override the default jira connector
|
||||
* @returns a jira Elasticsearch connector (it has key value pairs for the fields) by default
|
||||
*/
|
||||
export const createESJiraConnector = (
|
||||
overrides?: Partial<ESCaseConnectorWithId>
|
||||
): ESCaseConnectorWithId => {
|
||||
return {
|
||||
id: '1',
|
||||
name: ConnectorTypes.jira,
|
||||
fields: [
|
||||
{ key: 'issueType', value: 'bug' },
|
||||
{ key: 'priority', value: 'high' },
|
||||
{ key: 'parent', value: '2' },
|
||||
],
|
||||
type: ConnectorTypes.jira,
|
||||
...(overrides && { ...overrides }),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a jira CaseConnector (has the actual fields defined in the object instead of key value paris)
|
||||
* @param setFieldsToNull a flag that controls setting the fields property to null
|
||||
* @returns a jira connector
|
||||
*/
|
||||
export const createJiraConnector = ({
|
||||
setFieldsToNull,
|
||||
}: { setFieldsToNull?: boolean } = {}): CaseConnector => {
|
||||
return {
|
||||
id: '1',
|
||||
name: ConnectorTypes.jira,
|
||||
type: ConnectorTypes.jira,
|
||||
fields: setFieldsToNull
|
||||
? null
|
||||
: {
|
||||
issueType: 'bug',
|
||||
priority: 'high',
|
||||
parent: '2',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const createExternalService = (
|
||||
overrides?: Partial<CaseFullExternalService>
|
||||
): CaseFullExternalService => ({
|
||||
connector_id: '100',
|
||||
connector_name: '.jira',
|
||||
external_id: '100',
|
||||
external_title: 'awesome',
|
||||
external_url: 'http://www.google.com',
|
||||
pushed_at: '2019-11-25T21:54:48.952Z',
|
||||
pushed_by: {
|
||||
full_name: 'elastic',
|
||||
email: 'testemail@elastic.co',
|
||||
username: 'elastic',
|
||||
},
|
||||
...(overrides && { ...overrides }),
|
||||
});
|
||||
|
||||
export const basicCaseFields = {
|
||||
closed_at: null,
|
||||
closed_by: null,
|
||||
created_at: '2019-11-25T21:54:48.952Z',
|
||||
created_by: {
|
||||
full_name: 'elastic',
|
||||
email: 'testemail@elastic.co',
|
||||
username: 'elastic',
|
||||
},
|
||||
description: 'This is a brand new case of a bad meanie defacing data',
|
||||
title: 'Super Bad Security Issue',
|
||||
status: CaseStatuses.open,
|
||||
tags: ['defacement'],
|
||||
type: CaseType.individual,
|
||||
updated_at: '2019-11-25T21:54:48.952Z',
|
||||
updated_by: {
|
||||
full_name: 'elastic',
|
||||
email: 'testemail@elastic.co',
|
||||
username: 'elastic',
|
||||
},
|
||||
settings: {
|
||||
syncAlerts: true,
|
||||
},
|
||||
owner: SECURITY_SOLUTION_OWNER,
|
||||
};
|
||||
|
||||
export const createCaseSavedObjectResponse = ({
|
||||
connector,
|
||||
externalService,
|
||||
}: {
|
||||
connector?: ESCaseConnectorWithId;
|
||||
externalService?: CaseFullExternalService;
|
||||
} = {}): SavedObject<ESCaseAttributes> => {
|
||||
const references: SavedObjectReference[] = createSavedObjectReferences({
|
||||
connector,
|
||||
externalService,
|
||||
});
|
||||
|
||||
const formattedConnector = {
|
||||
type: connector?.type ?? ConnectorTypes.jira,
|
||||
name: connector?.name ?? ConnectorTypes.jira,
|
||||
fields: connector?.fields ?? null,
|
||||
};
|
||||
|
||||
let restExternalService: ExternalServicesWithoutConnectorId | null = null;
|
||||
if (externalService !== null) {
|
||||
const { connector_id: ignored, ...rest } = externalService ?? {
|
||||
connector_name: '.jira',
|
||||
external_id: '100',
|
||||
external_title: 'awesome',
|
||||
external_url: 'http://www.google.com',
|
||||
pushed_at: '2019-11-25T21:54:48.952Z',
|
||||
pushed_by: {
|
||||
full_name: 'elastic',
|
||||
email: 'testemail@elastic.co',
|
||||
username: 'elastic',
|
||||
},
|
||||
};
|
||||
restExternalService = rest;
|
||||
}
|
||||
|
||||
return {
|
||||
type: CASE_SAVED_OBJECT,
|
||||
id: '1',
|
||||
attributes: {
|
||||
...basicCaseFields,
|
||||
// if connector is null we'll default this to an incomplete jira value because the service
|
||||
// should switch it to a none connector when the id can't be found in the references array
|
||||
connector: formattedConnector,
|
||||
external_service: restExternalService,
|
||||
},
|
||||
references,
|
||||
};
|
||||
};
|
||||
|
||||
export const createSavedObjectReferences = ({
|
||||
connector,
|
||||
externalService,
|
||||
}: {
|
||||
connector?: ESCaseConnectorWithId;
|
||||
externalService?: CaseFullExternalService;
|
||||
} = {}): SavedObjectReference[] => [
|
||||
...(connector && connector.id !== noneConnectorId
|
||||
? [
|
||||
{
|
||||
id: connector.id,
|
||||
name: CONNECTOR_ID_REFERENCE_NAME,
|
||||
type: ACTION_SAVED_OBJECT_TYPE,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(externalService && externalService.connector_id
|
||||
? [
|
||||
{
|
||||
id: externalService.connector_id,
|
||||
name: PUSH_CONNECTOR_ID_REFERENCE_NAME,
|
||||
type: ACTION_SAVED_OBJECT_TYPE,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
211
x-pack/plugins/cases/server/services/transform.test.ts
Normal file
211
x-pack/plugins/cases/server/services/transform.test.ts
Normal file
|
@ -0,0 +1,211 @@
|
|||
/*
|
||||
* 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 { ACTION_SAVED_OBJECT_TYPE } from '../../../actions/server';
|
||||
import { ConnectorTypes } from '../../common';
|
||||
import { createESJiraConnector, createJiraConnector } from './test_utils';
|
||||
import {
|
||||
findConnectorIdReference,
|
||||
transformESConnectorOrUseDefault,
|
||||
transformESConnectorToExternalModel,
|
||||
transformFieldsToESModel,
|
||||
} from './transform';
|
||||
|
||||
describe('service transform helpers', () => {
|
||||
describe('findConnectorIdReference', () => {
|
||||
it('finds the reference when it exists', () => {
|
||||
expect(
|
||||
findConnectorIdReference('a', [{ id: 'hello', type: ACTION_SAVED_OBJECT_TYPE, name: 'a' }])
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it('does not find the reference when the name is different', () => {
|
||||
expect(
|
||||
findConnectorIdReference('a', [{ id: 'hello', type: ACTION_SAVED_OBJECT_TYPE, name: 'b' }])
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not find the reference when references is empty', () => {
|
||||
expect(findConnectorIdReference('a', [])).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not find the reference when references is undefined', () => {
|
||||
expect(findConnectorIdReference('a', undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not find the reference when the type is different', () => {
|
||||
expect(
|
||||
findConnectorIdReference('a', [{ id: 'hello', type: 'yo', name: 'a' }])
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformESConnectorToExternalModel', () => {
|
||||
it('returns undefined when the connector is undefined', () => {
|
||||
expect(transformESConnectorToExternalModel({ referenceName: 'a' })).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns the default connector when it cannot find the reference', () => {
|
||||
expect(
|
||||
transformESConnectorToExternalModel({
|
||||
connector: createESJiraConnector(),
|
||||
referenceName: 'a',
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"fields": null,
|
||||
"id": "none",
|
||||
"name": "none",
|
||||
"type": ".none",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('converts the connector.fields to an object', () => {
|
||||
expect(
|
||||
transformESConnectorToExternalModel({
|
||||
connector: createESJiraConnector(),
|
||||
references: [{ id: 'hello', type: ACTION_SAVED_OBJECT_TYPE, name: 'a' }],
|
||||
referenceName: 'a',
|
||||
})?.fields
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"issueType": "bug",
|
||||
"parent": "2",
|
||||
"priority": "high",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns the full jira connector', () => {
|
||||
expect(
|
||||
transformESConnectorToExternalModel({
|
||||
connector: createESJiraConnector(),
|
||||
references: [{ id: 'hello', type: ACTION_SAVED_OBJECT_TYPE, name: 'a' }],
|
||||
referenceName: 'a',
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"fields": Object {
|
||||
"issueType": "bug",
|
||||
"parent": "2",
|
||||
"priority": "high",
|
||||
},
|
||||
"id": "hello",
|
||||
"name": ".jira",
|
||||
"type": ".jira",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('sets fields to null if it is an empty array', () => {
|
||||
expect(
|
||||
transformESConnectorToExternalModel({
|
||||
connector: createESJiraConnector({ fields: [] }),
|
||||
references: [{ id: 'hello', type: ACTION_SAVED_OBJECT_TYPE, name: 'a' }],
|
||||
referenceName: 'a',
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"fields": null,
|
||||
"id": "hello",
|
||||
"name": ".jira",
|
||||
"type": ".jira",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('sets fields to null if it is null', () => {
|
||||
expect(
|
||||
transformESConnectorToExternalModel({
|
||||
connector: createESJiraConnector({ fields: null }),
|
||||
references: [{ id: 'hello', type: ACTION_SAVED_OBJECT_TYPE, name: 'a' }],
|
||||
referenceName: 'a',
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"fields": null,
|
||||
"id": "hello",
|
||||
"name": ".jira",
|
||||
"type": ".jira",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('sets fields to null if it is undefined', () => {
|
||||
expect(
|
||||
transformESConnectorToExternalModel({
|
||||
connector: createESJiraConnector({ fields: undefined }),
|
||||
references: [{ id: 'hello', type: ACTION_SAVED_OBJECT_TYPE, name: 'a' }],
|
||||
referenceName: 'a',
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"fields": null,
|
||||
"id": "hello",
|
||||
"name": ".jira",
|
||||
"type": ".jira",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformESConnectorOrUseDefault', () => {
|
||||
it('returns the default connector when the connector is undefined', () => {
|
||||
expect(transformESConnectorOrUseDefault({ referenceName: 'a' })).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"fields": null,
|
||||
"id": "none",
|
||||
"name": "none",
|
||||
"type": ".none",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformFieldsToESModel', () => {
|
||||
it('returns an empty array when fields is null', () => {
|
||||
expect(transformFieldsToESModel(createJiraConnector({ setFieldsToNull: true })).length).toBe(
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
it('returns an empty array when fields is an empty object', () => {
|
||||
expect(
|
||||
transformFieldsToESModel({
|
||||
id: '1',
|
||||
name: ConnectorTypes.jira,
|
||||
type: ConnectorTypes.jira,
|
||||
fields: {} as {
|
||||
issueType: string;
|
||||
priority: string;
|
||||
parent: string;
|
||||
},
|
||||
}).length
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it('returns an array with the key/value pairs', () => {
|
||||
expect(transformFieldsToESModel(createJiraConnector())).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"key": "issueType",
|
||||
"value": "bug",
|
||||
},
|
||||
Object {
|
||||
"key": "priority",
|
||||
"value": "high",
|
||||
},
|
||||
Object {
|
||||
"key": "parent",
|
||||
"value": "2",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
100
x-pack/plugins/cases/server/services/transform.ts
Normal file
100
x-pack/plugins/cases/server/services/transform.ts
Normal file
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* 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 { SavedObjectReference } from 'kibana/server';
|
||||
import { CaseConnector, ConnectorTypeFields } from '../../common';
|
||||
import { ACTION_SAVED_OBJECT_TYPE } from '../../../actions/server';
|
||||
import { getNoneCaseConnector } from '../common';
|
||||
import { ESCaseConnector, ESConnectorFields } from '.';
|
||||
|
||||
export function findConnectorIdReference(
|
||||
name: string,
|
||||
references?: SavedObjectReference[]
|
||||
): SavedObjectReference | undefined {
|
||||
return references?.find((ref) => ref.type === ACTION_SAVED_OBJECT_TYPE && ref.name === name);
|
||||
}
|
||||
|
||||
export function transformESConnectorToExternalModel({
|
||||
connector,
|
||||
references,
|
||||
referenceName,
|
||||
}: {
|
||||
connector?: ESCaseConnector;
|
||||
references?: SavedObjectReference[];
|
||||
referenceName: string;
|
||||
}): CaseConnector | undefined {
|
||||
const connectorIdRef = findConnectorIdReference(referenceName, references);
|
||||
return transformConnectorFieldsToExternalModel(connector, connectorIdRef?.id);
|
||||
}
|
||||
|
||||
function transformConnectorFieldsToExternalModel(
|
||||
connector?: ESCaseConnector,
|
||||
connectorId?: string
|
||||
): CaseConnector | undefined {
|
||||
if (!connector) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if the connector is valid, but we can't find it's ID in the reference, then it must be malformed
|
||||
// or it was a none connector which doesn't have a reference (a none connector doesn't point to any actual connector
|
||||
// saved object)
|
||||
if (!connectorId) {
|
||||
return getNoneCaseConnector();
|
||||
}
|
||||
|
||||
const connectorTypeField = {
|
||||
type: connector.type,
|
||||
fields:
|
||||
connector.fields != null && connector.fields.length > 0
|
||||
? connector.fields.reduce(
|
||||
(fields, { key, value }) => ({
|
||||
...fields,
|
||||
[key]: value,
|
||||
}),
|
||||
{}
|
||||
)
|
||||
: null,
|
||||
} as ConnectorTypeFields;
|
||||
|
||||
return {
|
||||
id: connectorId,
|
||||
name: connector.name,
|
||||
...connectorTypeField,
|
||||
};
|
||||
}
|
||||
|
||||
export function transformESConnectorOrUseDefault({
|
||||
connector,
|
||||
references,
|
||||
referenceName,
|
||||
}: {
|
||||
connector?: ESCaseConnector;
|
||||
references?: SavedObjectReference[];
|
||||
referenceName: string;
|
||||
}): CaseConnector {
|
||||
return (
|
||||
transformESConnectorToExternalModel({ connector, references, referenceName }) ??
|
||||
getNoneCaseConnector()
|
||||
);
|
||||
}
|
||||
|
||||
export function transformFieldsToESModel(connector: CaseConnector): ESConnectorFields {
|
||||
if (!connector.fields) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.entries(connector.fields).reduce<ESConnectorFields>(
|
||||
(acc, [key, value]) => [
|
||||
...acc,
|
||||
{
|
||||
key,
|
||||
value,
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
}
|
|
@ -13,18 +13,16 @@ import {
|
|||
CASE_COMMENT_SAVED_OBJECT,
|
||||
CASE_SAVED_OBJECT,
|
||||
CaseUserActionAttributes,
|
||||
ESCaseAttributes,
|
||||
OWNER_FIELD,
|
||||
SUB_CASE_SAVED_OBJECT,
|
||||
SubCaseAttributes,
|
||||
User,
|
||||
UserAction,
|
||||
UserActionField,
|
||||
UserActionFieldType,
|
||||
CaseAttributes,
|
||||
} from '../../../common';
|
||||
import { isTwoArraysDifference } from '../../client/utils';
|
||||
import { UserActionItem } from '.';
|
||||
import { transformESConnectorToCaseConnector } from '../../common';
|
||||
|
||||
export const transformNewUserAction = ({
|
||||
actionField,
|
||||
|
@ -173,17 +171,12 @@ interface CaseSubIDs {
|
|||
}
|
||||
|
||||
type GetCaseAndSubID = <T>(so: SavedObjectsUpdateResponse<T>) => CaseSubIDs;
|
||||
type GetField = <T>(
|
||||
attributes: Pick<SavedObjectsUpdateResponse<T>, 'attributes'>,
|
||||
field: UserActionFieldType
|
||||
) => unknown;
|
||||
|
||||
/**
|
||||
* Abstraction functions to retrieve a given field and the caseId and subCaseId depending on
|
||||
* whether we're interacting with a case or a sub case.
|
||||
*/
|
||||
interface Getters {
|
||||
getField: GetField;
|
||||
getCaseAndSubID: GetCaseAndSubID;
|
||||
}
|
||||
|
||||
|
@ -209,7 +202,7 @@ const buildGenericCaseUserActions = <T extends OwnerEntity>({
|
|||
allowedFields: UserActionField;
|
||||
getters: Getters;
|
||||
}): UserActionItem[] => {
|
||||
const { getCaseAndSubID, getField } = getters;
|
||||
const { getCaseAndSubID } = getters;
|
||||
return updatedCases.reduce<UserActionItem[]>((acc, updatedItem) => {
|
||||
const { caseId, subCaseId } = getCaseAndSubID(updatedItem);
|
||||
// regardless of whether we're looking at a sub case or case, the id field will always be used to match between
|
||||
|
@ -220,8 +213,8 @@ const buildGenericCaseUserActions = <T extends OwnerEntity>({
|
|||
const updatedFields = Object.keys(updatedItem.attributes) as UserActionField;
|
||||
updatedFields.forEach((field) => {
|
||||
if (allowedFields.includes(field)) {
|
||||
const origValue = getField(originalItem, field);
|
||||
const updatedValue = getField(updatedItem, field);
|
||||
const origValue = get(originalItem, ['attributes', field]);
|
||||
const updatedValue = get(updatedItem, ['attributes', field]);
|
||||
|
||||
if (isString(origValue) && isString(updatedValue) && origValue !== updatedValue) {
|
||||
userActions = [
|
||||
|
@ -308,18 +301,12 @@ export const buildSubCaseUserActions = (args: {
|
|||
originalSubCases: Array<SavedObject<SubCaseAttributes>>;
|
||||
updatedSubCases: Array<SavedObjectsUpdateResponse<SubCaseAttributes>>;
|
||||
}): UserActionItem[] => {
|
||||
const getField = (
|
||||
so: Pick<SavedObjectsUpdateResponse<SubCaseAttributes>, 'attributes'>,
|
||||
field: UserActionFieldType
|
||||
) => get(so, ['attributes', field]);
|
||||
|
||||
const getCaseAndSubID = (so: SavedObjectsUpdateResponse<SubCaseAttributes>): CaseSubIDs => {
|
||||
const caseId = so.references?.find((ref) => ref.type === CASE_SAVED_OBJECT)?.id ?? '';
|
||||
return { caseId, subCaseId: so.id };
|
||||
};
|
||||
|
||||
const getters: Getters = {
|
||||
getField,
|
||||
getCaseAndSubID,
|
||||
};
|
||||
|
||||
|
@ -339,24 +326,14 @@ export const buildSubCaseUserActions = (args: {
|
|||
export const buildCaseUserActions = (args: {
|
||||
actionDate: string;
|
||||
actionBy: User;
|
||||
originalCases: Array<SavedObject<ESCaseAttributes>>;
|
||||
updatedCases: Array<SavedObjectsUpdateResponse<ESCaseAttributes>>;
|
||||
originalCases: Array<SavedObject<CaseAttributes>>;
|
||||
updatedCases: Array<SavedObjectsUpdateResponse<CaseAttributes>>;
|
||||
}): UserActionItem[] => {
|
||||
const getField = (
|
||||
so: Pick<SavedObjectsUpdateResponse<ESCaseAttributes>, 'attributes'>,
|
||||
field: UserActionFieldType
|
||||
) => {
|
||||
return field === 'connector' && so.attributes.connector
|
||||
? transformESConnectorToCaseConnector(so.attributes.connector)
|
||||
: get(so, ['attributes', field]);
|
||||
};
|
||||
|
||||
const caseGetIds: GetCaseAndSubID = <T>(so: SavedObjectsUpdateResponse<T>): CaseSubIDs => {
|
||||
return { caseId: so.id };
|
||||
};
|
||||
|
||||
const getters: Getters = {
|
||||
getField,
|
||||
getCaseAndSubID: caseGetIds,
|
||||
};
|
||||
|
||||
|
|
|
@ -55,6 +55,7 @@ export class CaseUserActionService {
|
|||
public async bulkCreate({ unsecuredSavedObjectsClient, actions }: PostCaseUserActionArgs) {
|
||||
try {
|
||||
this.log.debug(`Attempting to POST a new case user action`);
|
||||
|
||||
return await unsecuredSavedObjectsClient.bulkCreate<CaseUserActionAttributes>(
|
||||
actions.map((action) => ({ type: CASE_USER_ACTION_SAVED_OBJECT, ...action }))
|
||||
);
|
||||
|
|
|
@ -56,6 +56,8 @@ import { SignalHit } from '../../../../plugins/security_solution/server/lib/dete
|
|||
import { ActionResult, FindActionResult } from '../../../../plugins/actions/server/types';
|
||||
import { User } from './authentication/types';
|
||||
import { superUser } from './authentication/users';
|
||||
import { ESCasesConfigureAttributes } from '../../../../plugins/cases/server/services/configure/types';
|
||||
import { ESCaseAttributes } from '../../../../plugins/cases/server/services/cases/types';
|
||||
|
||||
function toArray<T>(input: T | T[]): T[] {
|
||||
if (Array.isArray(input)) {
|
||||
|
@ -605,6 +607,49 @@ export const getConnectorMappingsFromES = async ({ es }: { es: KibanaClient }) =
|
|||
return mappings;
|
||||
};
|
||||
|
||||
interface ConfigureSavedObject {
|
||||
'cases-configure': ESCasesConfigureAttributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns configure saved objects from Elasticsearch directly.
|
||||
*/
|
||||
export const getConfigureSavedObjectsFromES = async ({ es }: { es: KibanaClient }) => {
|
||||
const configure: ApiResponse<estypes.SearchResponse<ConfigureSavedObject>> = await es.search({
|
||||
index: '.kibana',
|
||||
body: {
|
||||
query: {
|
||||
term: {
|
||||
type: {
|
||||
value: 'cases-configure',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return configure;
|
||||
};
|
||||
|
||||
export const getCaseSavedObjectsFromES = async ({ es }: { es: KibanaClient }) => {
|
||||
const configure: ApiResponse<
|
||||
estypes.SearchResponse<{ cases: ESCaseAttributes }>
|
||||
> = await es.search({
|
||||
index: '.kibana',
|
||||
body: {
|
||||
query: {
|
||||
term: {
|
||||
type: {
|
||||
value: 'cases',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return configure;
|
||||
};
|
||||
|
||||
export const createCaseWithConnector = async ({
|
||||
supertest,
|
||||
configureReq = {},
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
CASES_URL,
|
||||
SECURITY_SOLUTION_OWNER,
|
||||
} from '../../../../../../plugins/cases/common/constants';
|
||||
import { getCase } from '../../../../common/lib/utils';
|
||||
import { getCase, getCaseSavedObjectsFromES } from '../../../../common/lib/utils';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function createGetTests({ getService }: FtrProviderContext) {
|
||||
|
@ -121,13 +121,90 @@ export default function createGetTests({ getService }: FtrProviderContext) {
|
|||
await esArchiver.unload('x-pack/test/functional/es_archives/cases/migrations/7.13.2');
|
||||
});
|
||||
|
||||
it('adds the owner field', async () => {
|
||||
const theCase = await getCase({
|
||||
supertest,
|
||||
caseId: 'e49ad6e0-cf9d-11eb-a603-13e7747d215c',
|
||||
describe('owner field', () => {
|
||||
it('adds the owner field', async () => {
|
||||
const theCase = await getCase({
|
||||
supertest,
|
||||
caseId: 'e49ad6e0-cf9d-11eb-a603-13e7747d215c',
|
||||
});
|
||||
|
||||
expect(theCase.owner).to.be(SECURITY_SOLUTION_OWNER);
|
||||
});
|
||||
});
|
||||
|
||||
describe('migrating connector id to a reference', () => {
|
||||
const es = getService('es');
|
||||
|
||||
it('preserves the connector id after migration in the API response', async () => {
|
||||
const theCase = await getCase({
|
||||
supertest,
|
||||
caseId: 'e49ad6e0-cf9d-11eb-a603-13e7747d215c',
|
||||
});
|
||||
|
||||
expect(theCase.connector.id).to.be('d68508f0-cf9d-11eb-a603-13e7747d215c');
|
||||
});
|
||||
|
||||
expect(theCase.owner).to.be(SECURITY_SOLUTION_OWNER);
|
||||
it('preserves the connector fields after migration in the API response', async () => {
|
||||
const theCase = await getCase({
|
||||
supertest,
|
||||
caseId: 'e49ad6e0-cf9d-11eb-a603-13e7747d215c',
|
||||
});
|
||||
|
||||
expect(theCase.connector).to.eql({
|
||||
fields: {
|
||||
issueType: '10002',
|
||||
parent: null,
|
||||
priority: null,
|
||||
},
|
||||
id: 'd68508f0-cf9d-11eb-a603-13e7747d215c',
|
||||
name: 'Test Jira',
|
||||
type: '.jira',
|
||||
});
|
||||
});
|
||||
|
||||
it('removes the connector id field in the saved object', async () => {
|
||||
const casesFromES = await getCaseSavedObjectsFromES({ es });
|
||||
expect(casesFromES.body.hits.hits[0]._source?.cases.connector).to.not.have.property('id');
|
||||
});
|
||||
|
||||
it('preserves the external_service.connector_id after migration in the API response', async () => {
|
||||
const theCase = await getCase({
|
||||
supertest,
|
||||
caseId: 'e49ad6e0-cf9d-11eb-a603-13e7747d215c',
|
||||
});
|
||||
|
||||
expect(theCase.external_service?.connector_id).to.be(
|
||||
'd68508f0-cf9d-11eb-a603-13e7747d215c'
|
||||
);
|
||||
});
|
||||
|
||||
it('preserves the external_service fields after migration in the API response', async () => {
|
||||
const theCase = await getCase({
|
||||
supertest,
|
||||
caseId: 'e49ad6e0-cf9d-11eb-a603-13e7747d215c',
|
||||
});
|
||||
|
||||
expect(theCase.external_service).to.eql({
|
||||
connector_id: 'd68508f0-cf9d-11eb-a603-13e7747d215c',
|
||||
connector_name: 'Test Jira',
|
||||
external_id: '10106',
|
||||
external_title: 'TPN-99',
|
||||
external_url: 'https://cases-testing.atlassian.net/browse/TPN-99',
|
||||
pushed_at: '2021-06-17T18:57:45.524Z',
|
||||
pushed_by: {
|
||||
email: null,
|
||||
full_name: 'j@j.com',
|
||||
username: '711621466',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('removes the connector_id field in the saved object', async () => {
|
||||
const casesFromES = await getCaseSavedObjectsFromES({ es });
|
||||
expect(
|
||||
casesFromES.body.hits.hits[0]._source?.cases.external_service
|
||||
).to.not.have.property('id');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,7 +11,11 @@ import {
|
|||
CASE_CONFIGURE_URL,
|
||||
SECURITY_SOLUTION_OWNER,
|
||||
} from '../../../../../../plugins/cases/common/constants';
|
||||
import { getConfiguration, getConnectorMappingsFromES } from '../../../../common/lib/utils';
|
||||
import {
|
||||
getConfiguration,
|
||||
getConfigureSavedObjectsFromES,
|
||||
getConnectorMappingsFromES,
|
||||
} from '../../../../common/lib/utils';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
|
@ -57,23 +61,57 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
await esArchiver.unload('x-pack/test/functional/es_archives/cases/migrations/7.13.2');
|
||||
});
|
||||
|
||||
it('adds the owner field', async () => {
|
||||
const configuration = await getConfiguration({
|
||||
supertest,
|
||||
query: { owner: SECURITY_SOLUTION_OWNER },
|
||||
describe('owner field', () => {
|
||||
it('adds the owner field', async () => {
|
||||
const configuration = await getConfiguration({
|
||||
supertest,
|
||||
query: { owner: SECURITY_SOLUTION_OWNER },
|
||||
});
|
||||
|
||||
expect(configuration[0].owner).to.be(SECURITY_SOLUTION_OWNER);
|
||||
});
|
||||
|
||||
expect(configuration[0].owner).to.be(SECURITY_SOLUTION_OWNER);
|
||||
it('adds the owner field to the connector mapping', async () => {
|
||||
// We don't get the owner field back from the mappings when we retrieve the configuration so the only way to
|
||||
// check that the migration worked is by checking the saved object stored in Elasticsearch directly
|
||||
const mappings = await getConnectorMappingsFromES({ es });
|
||||
expect(mappings.body.hits.hits.length).to.be(1);
|
||||
expect(mappings.body.hits.hits[0]._source?.['cases-connector-mappings'].owner).to.eql(
|
||||
SECURITY_SOLUTION_OWNER
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('adds the owner field to the connector mapping', async () => {
|
||||
// We don't get the owner field back from the mappings when we retrieve the configuration so the only way to
|
||||
// check that the migration worked is by checking the saved object stored in Elasticsearch directly
|
||||
const mappings = await getConnectorMappingsFromES({ es });
|
||||
expect(mappings.body.hits.hits.length).to.be(1);
|
||||
expect(mappings.body.hits.hits[0]._source?.['cases-connector-mappings'].owner).to.eql(
|
||||
SECURITY_SOLUTION_OWNER
|
||||
);
|
||||
describe('migrating connector id to a reference', () => {
|
||||
it('preserves the connector id after migration in the API response', async () => {
|
||||
const configuration = await getConfiguration({
|
||||
supertest,
|
||||
query: { owner: SECURITY_SOLUTION_OWNER },
|
||||
});
|
||||
|
||||
expect(configuration[0].connector.id).to.be('d68508f0-cf9d-11eb-a603-13e7747d215c');
|
||||
});
|
||||
|
||||
it('preserves the connector fields after migration in the API response', async () => {
|
||||
const configuration = await getConfiguration({
|
||||
supertest,
|
||||
query: { owner: SECURITY_SOLUTION_OWNER },
|
||||
});
|
||||
|
||||
expect(configuration[0].connector).to.eql({
|
||||
fields: null,
|
||||
id: 'd68508f0-cf9d-11eb-a603-13e7747d215c',
|
||||
name: 'Test Jira',
|
||||
type: '.jira',
|
||||
});
|
||||
});
|
||||
|
||||
it('removes the connector id field in the saved object', async () => {
|
||||
const configurationFromES = await getConfigureSavedObjectsFromES({ es });
|
||||
expect(
|
||||
configurationFromES.body.hits.hits[0]._source?.['cases-configure'].connector
|
||||
).to.not.have.property('id');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -31,6 +31,7 @@ import {
|
|||
createConnector,
|
||||
getServiceNowConnector,
|
||||
getConnectorMappingsFromES,
|
||||
getCase,
|
||||
} from '../../../../common/lib/utils';
|
||||
import {
|
||||
ExternalServiceSimulator,
|
||||
|
@ -102,6 +103,72 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
).to.equal(true);
|
||||
});
|
||||
|
||||
it('preserves the connector.id after pushing a case', async () => {
|
||||
const { postedCase, connector } = await createCaseWithConnector({
|
||||
supertest,
|
||||
servicenowSimulatorURL,
|
||||
actionsRemover,
|
||||
});
|
||||
const theCase = await pushCase({
|
||||
supertest,
|
||||
caseId: postedCase.id,
|
||||
connectorId: connector.id,
|
||||
});
|
||||
|
||||
expect(theCase.connector.id).to.eql(connector.id);
|
||||
});
|
||||
|
||||
it('preserves the external_service.connector_id after updating the connector', async () => {
|
||||
const { postedCase, connector: pushConnector } = await createCaseWithConnector({
|
||||
supertest,
|
||||
servicenowSimulatorURL,
|
||||
actionsRemover,
|
||||
});
|
||||
|
||||
const theCaseAfterPush = await pushCase({
|
||||
supertest,
|
||||
caseId: postedCase.id,
|
||||
connectorId: pushConnector.id,
|
||||
});
|
||||
|
||||
const newConnector = await createConnector({
|
||||
supertest,
|
||||
req: {
|
||||
...getServiceNowConnector(),
|
||||
config: { apiUrl: servicenowSimulatorURL },
|
||||
},
|
||||
});
|
||||
|
||||
actionsRemover.add('default', newConnector.id, 'action', 'actions');
|
||||
await updateCase({
|
||||
supertest,
|
||||
params: {
|
||||
cases: [
|
||||
{
|
||||
id: postedCase.id,
|
||||
version: theCaseAfterPush.version,
|
||||
connector: {
|
||||
id: newConnector.id,
|
||||
name: newConnector.name,
|
||||
type: newConnector.connector_type_id,
|
||||
fields: {
|
||||
urgency: '2',
|
||||
impact: '2',
|
||||
severity: '2',
|
||||
category: 'software',
|
||||
subcategory: 'os',
|
||||
},
|
||||
} as CaseConnector,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const theCaseAfterUpdate = await getCase({ supertest, caseId: postedCase.id });
|
||||
expect(theCaseAfterUpdate.connector.id).to.eql(newConnector.id);
|
||||
expect(theCaseAfterUpdate.external_service?.connector_id).to.eql(pushConnector.id);
|
||||
});
|
||||
|
||||
it('should create the mappings when pushing a case', async () => {
|
||||
// create a connector but not a configuration so that the mapping will not be present
|
||||
const connector = await createConnector({
|
||||
|
|
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue