[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:
Jonathan Buttner 2021-08-04 10:39:21 -04:00 committed by GitHub
parent ec4de0d95e
commit 96f27b9899
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 4957 additions and 723 deletions

View file

@ -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;

View file

@ -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>;

View file

@ -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;
}

View file

@ -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,

View file

@ -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,
});

View file

@ -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([

View file

@ -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: {

View file

@ -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

View file

@ -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,

View file

@ -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(

View file

@ -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",

View file

@ -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',

View 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';

View file

@ -9,3 +9,4 @@ export * from './models';
export * from './utils';
export * from './types';
export * from './error';
export * from './constants';

View file

@ -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),
};
}

View file

@ -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",

View file

@ -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,
});

View file

@ -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=',
},
];

View file

@ -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',
},

View file

@ -33,9 +33,6 @@ export const caseConfigureSavedObjectType: SavedObjectsType = {
},
connector: {
properties: {
id: {
type: 'keyword',
},
name: {
type: 'text',
},

View file

@ -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",
},
]
`);
});
});

View file

@ -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,
};

View file

@ -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",
}
`);
});
});

View file

@ -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,
};

View file

@ -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;

View file

@ -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",
},
]
`);
});
});
});

View file

@ -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,
};
};

File diff suppressed because it is too large Load diff

View file

@ -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;

View 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",
},
}
`);
});
});
});

View 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,
};
}

View 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;
};

View 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",
}
`);
});
});
});
});

View file

@ -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 },
]);
}

View 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;
};

View file

@ -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",
},
]
`);
});
});
});

View file

@ -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());
}
}

View file

@ -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;
}

View 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,
},
]
: []),
];

View 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",
},
]
`);
});
});
});

View 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,
},
],
[]
);
}

View file

@ -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,
};

View file

@ -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 }))
);

View file

@ -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 = {},

View file

@ -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');
});
});
});
});

View file

@ -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');
});
});
});
});

View file

@ -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({