diff --git a/x-pack/plugins/cases/common/api/cases/case.ts b/x-pack/plugins/cases/common/api/cases/case.ts index 389caffee1a5c..9b184d437f281 100644 --- a/x-pack/plugins/cases/common/api/cases/case.ts +++ b/x-pack/plugins/cases/common/api/cases/case.ts @@ -38,7 +38,6 @@ const CaseBasicRt = rt.type({ [caseTypeField]: CaseTypeRt, connector: CaseConnectorRt, settings: SettingsRt, - // TODO: should a user be able to update the owner? owner: rt.string, }); diff --git a/x-pack/plugins/cases/common/api/cases/configure.ts b/x-pack/plugins/cases/common/api/cases/configure.ts index 02e2cb6596230..eeeb9ed4ebd04 100644 --- a/x-pack/plugins/cases/common/api/cases/configure.ts +++ b/x-pack/plugins/cases/common/api/cases/configure.ts @@ -10,6 +10,7 @@ import * as rt from 'io-ts'; import { UserRT } from '../user'; import { CaseConnectorRt, ConnectorMappingsRt, ESCaseConnector } from '../connectors'; import { OmitProp } from '../runtime_types'; +import { OWNER_FIELD } from './constants'; // 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')]); @@ -20,7 +21,9 @@ const CasesConfigureBasicRt = rt.type({ owner: rt.string, }); -const CasesConfigureBasicWithoutOwnerRt = rt.type(OmitProp(CasesConfigureBasicRt.props, 'owner')); +const CasesConfigureBasicWithoutOwnerRt = rt.type( + OmitProp(CasesConfigureBasicRt.props, OWNER_FIELD) +); export const CasesConfigureRequestRt = CasesConfigureBasicRt; export const CasesConfigurePatchRt = rt.intersection([ diff --git a/x-pack/plugins/cases/common/api/cases/constants.ts b/x-pack/plugins/cases/common/api/cases/constants.ts new file mode 100644 index 0000000000000..b8dd13c5d490e --- /dev/null +++ b/x-pack/plugins/cases/common/api/cases/constants.ts @@ -0,0 +1,11 @@ +/* + * 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 field used for authorization in various entities within cases. + */ +export const OWNER_FIELD = 'owner'; diff --git a/x-pack/plugins/cases/common/api/cases/index.ts b/x-pack/plugins/cases/common/api/cases/index.ts index 6e7fb818cb2b5..0f78ca9b35377 100644 --- a/x-pack/plugins/cases/common/api/cases/index.ts +++ b/x-pack/plugins/cases/common/api/cases/index.ts @@ -11,3 +11,4 @@ export * from './comment'; export * from './status'; export * from './user_actions'; export * from './sub_case'; +export * from './constants'; diff --git a/x-pack/plugins/cases/common/api/cases/sub_case.ts b/x-pack/plugins/cases/common/api/cases/sub_case.ts index ba6cd6a8affa4..826654cab2d7f 100644 --- a/x-pack/plugins/cases/common/api/cases/sub_case.ts +++ b/x-pack/plugins/cases/common/api/cases/sub_case.ts @@ -26,6 +26,7 @@ export const SubCaseAttributesRt = rt.intersection([ created_by: rt.union([UserRT, rt.null]), updated_at: rt.union([rt.string, rt.null]), updated_by: rt.union([UserRT, rt.null]), + owner: rt.string, }), ]); diff --git a/x-pack/plugins/cases/common/api/cases/user_actions.ts b/x-pack/plugins/cases/common/api/cases/user_actions.ts index 1b53adb002436..03912c550d77a 100644 --- a/x-pack/plugins/cases/common/api/cases/user_actions.ts +++ b/x-pack/plugins/cases/common/api/cases/user_actions.ts @@ -6,6 +6,7 @@ */ import * as rt from 'io-ts'; +import { OWNER_FIELD } from './constants'; import { UserRT } from '../user'; @@ -22,7 +23,7 @@ const UserActionFieldTypeRt = rt.union([ rt.literal('status'), rt.literal('settings'), rt.literal('sub_case'), - rt.literal('owner'), + rt.literal(OWNER_FIELD), ]); const UserActionFieldRt = rt.array(UserActionFieldTypeRt); const UserActionRt = rt.union([ @@ -41,6 +42,7 @@ const CaseUserActionBasicRT = rt.type({ action_by: UserRT, new_value: rt.union([rt.string, rt.null]), old_value: rt.union([rt.string, rt.null]), + owner: rt.string, }); const CaseUserActionResponseRT = rt.intersection([ diff --git a/x-pack/plugins/cases/server/authorization/index.ts b/x-pack/plugins/cases/server/authorization/index.ts index be8ca55ccd262..3a6ec502ff72b 100644 --- a/x-pack/plugins/cases/server/authorization/index.ts +++ b/x-pack/plugins/cases/server/authorization/index.ts @@ -10,6 +10,7 @@ import { CASE_COMMENT_SAVED_OBJECT, CASE_CONFIGURE_SAVED_OBJECT, CASE_SAVED_OBJECT, + CASE_USER_ACTION_SAVED_OBJECT, } from '../../common/constants'; import { Verbs, ReadOperations, WriteOperations, OperationDetails } from './types'; @@ -101,6 +102,14 @@ export const Operations: Record Promise; export enum ReadOperations { GetCase = 'getCase', FindCases = 'findCases', + GetCaseStatuses = 'getCaseStatuses', GetComment = 'getComment', GetAllComments = 'getAllComments', FindComments = 'findComments', GetTags = 'getTags', GetReporters = 'getReporters', FindConfigurations = 'findConfigurations', + GetUserActions = 'getUserActions', } /** @@ -47,6 +49,7 @@ export enum WriteOperations { CreateCase = 'createCase', DeleteCase = 'deleteCase', UpdateCase = 'updateCase', + PushCase = 'pushCase', CreateComment = 'createComment', DeleteAllComments = 'deleteAllComments', DeleteComment = 'deleteComment', diff --git a/x-pack/plugins/cases/server/authorization/utils.ts b/x-pack/plugins/cases/server/authorization/utils.ts index 11d143eb05b2a..eb2dcc1a0f2e4 100644 --- a/x-pack/plugins/cases/server/authorization/utils.ts +++ b/x-pack/plugins/cases/server/authorization/utils.ts @@ -7,12 +7,13 @@ import { remove, uniq } from 'lodash'; import { nodeBuilder, KueryNode } from '../../../../../src/plugins/data/common'; +import { OWNER_FIELD } from '../../common/api'; export const getOwnersFilter = (savedObjectType: string, owners: string[]): KueryNode => { return nodeBuilder.or( owners.reduce((query, owner) => { - ensureFieldIsSafeForQuery('owner', owner); - query.push(nodeBuilder.is(`${savedObjectType}.attributes.owner`, owner)); + ensureFieldIsSafeForQuery(OWNER_FIELD, owner); + query.push(nodeBuilder.is(`${savedObjectType}.attributes.${OWNER_FIELD}`, owner)); return query; }, []) ); @@ -53,5 +54,5 @@ export const includeFieldsRequiredForAuthentication = (fields?: string[]): strin if (fields === undefined) { return; } - return uniq([...fields, 'owner']); + return uniq([...fields, OWNER_FIELD]); }; diff --git a/x-pack/plugins/cases/server/client/attachments/add.ts b/x-pack/plugins/cases/server/client/attachments/add.ts index 4cc9ca7f868ec..9480730a3f137 100644 --- a/x-pack/plugins/cases/server/client/attachments/add.ts +++ b/x-pack/plugins/cases/server/client/attachments/add.ts @@ -105,6 +105,7 @@ async function getSubCase({ subCaseId: newSubCase.id, fields: ['status', 'sub_case'], newValue: JSON.stringify({ status: newSubCase.attributes.status }), + owner: newSubCase.attributes.owner, }), ], }); @@ -222,6 +223,7 @@ const addGeneratedAlerts = async ( commentId: newComment.id, fields: ['comment'], newValue: JSON.stringify(query), + owner: newComment.attributes.owner, }), ], }); @@ -396,6 +398,7 @@ export const addComment = async ( commentId: newComment.id, fields: ['comment'], newValue: JSON.stringify(query), + owner: newComment.attributes.owner, }), ], }); diff --git a/x-pack/plugins/cases/server/client/attachments/delete.ts b/x-pack/plugins/cases/server/client/attachments/delete.ts index f600aef64d1b6..83df367d951ee 100644 --- a/x-pack/plugins/cases/server/client/attachments/delete.ts +++ b/x-pack/plugins/cases/server/client/attachments/delete.ts @@ -95,6 +95,7 @@ export async function deleteAll( subCaseId: subCaseID, commentId: comment.id, fields: ['comment'], + owner: comment.attributes.owner, }) ), }); @@ -167,6 +168,7 @@ export async function deleteComment( subCaseId: subCaseID, commentId: attachmentID, fields: ['comment'], + owner: myComment.attributes.owner, }), ], }); diff --git a/x-pack/plugins/cases/server/client/attachments/update.ts b/x-pack/plugins/cases/server/client/attachments/update.ts index c2c6d6800e51f..26c44509abce8 100644 --- a/x-pack/plugins/cases/server/client/attachments/update.ts +++ b/x-pack/plugins/cases/server/client/attachments/update.ts @@ -181,6 +181,7 @@ export async function update( // myComment.attribute contains also CommentAttributesBasicRt attributes pick(Object.keys(queryRestAttributes), myComment.attributes) ), + owner: myComment.attributes.owner, }), ], }); diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index 3f66db7281c38..4e8a5834d6869 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -20,6 +20,7 @@ import { CasesClientPostRequestRt, CasePostRequest, CaseType, + OWNER_FIELD, } from '../../../common/api'; import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; import { ensureAuthorized, getConnectorFromConfiguration } from '../utils'; @@ -108,8 +109,9 @@ export const create = async ( actionAt: createdDate, actionBy: { username, full_name, email }, caseId: newCase.id, - fields: ['description', 'status', 'tags', 'title', 'connector', 'settings', 'owner'], + fields: ['description', 'status', 'tags', 'title', 'connector', 'settings', OWNER_FIELD], newValue: JSON.stringify(query), + owner: newCase.attributes.owner, }), ], }); diff --git a/x-pack/plugins/cases/server/client/cases/delete.ts b/x-pack/plugins/cases/server/client/cases/delete.ts index 100135e2992eb..256a8be2ccbe0 100644 --- a/x-pack/plugins/cases/server/client/cases/delete.ts +++ b/x-pack/plugins/cases/server/client/cases/delete.ts @@ -14,6 +14,7 @@ import { AttachmentService, CaseService } from '../../services'; import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; import { Operations } from '../../authorization'; import { ensureAuthorized } from '../utils'; +import { OWNER_FIELD } from '../../../common/api'; async function deleteSubCases({ attachmentService, @@ -133,12 +134,12 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P await userActionService.bulkCreate({ soClient, - actions: ids.map((id) => + actions: cases.saved_objects.map((caseInfo) => buildCaseUserActionItem({ action: 'delete', actionAt: deleteDate, actionBy: user, - caseId: id, + caseId: caseInfo.id, fields: [ 'description', 'status', @@ -146,10 +147,11 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P 'title', 'connector', 'settings', - 'owner', + OWNER_FIELD, 'comment', ...(ENABLE_CASE_CONNECTOR ? ['sub_case'] : []), ], + owner: caseInfo.attributes.owner, }) ), }); diff --git a/x-pack/plugins/cases/server/client/cases/find.ts b/x-pack/plugins/cases/server/client/cases/find.ts index 53ae6a2e76b81..0899cd3d0150f 100644 --- a/x-pack/plugins/cases/server/client/cases/find.ts +++ b/x-pack/plugins/cases/server/client/cases/find.ts @@ -80,7 +80,6 @@ export const find = async ( ensureSavedObjectsAreAuthorized([...cases.casesMap.values()]); - // TODO: Make sure we do not leak information when authorization is on const [openCases, inProgressCases, closedCases] = await Promise.all([ ...caseStatuses.map((status) => { const statusQuery = constructQueryOptions({ ...queryArgs, status, authorizationFilter }); @@ -88,6 +87,7 @@ export const find = async ( soClient: savedObjectsClient, caseOptions: statusQuery.case, subCaseOptions: statusQuery.subCase, + ensureSavedObjectsAreAuthorized, }); }), ]); diff --git a/x-pack/plugins/cases/server/client/cases/mock.ts b/x-pack/plugins/cases/server/client/cases/mock.ts index 1d46f5715c4ba..01740c9a41a93 100644 --- a/x-pack/plugins/cases/server/client/cases/mock.ts +++ b/x-pack/plugins/cases/server/client/cases/mock.ts @@ -135,6 +135,7 @@ export const userActions: CaseUserActionsResponse = [ action_id: 'fd830c60-6646-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', comment_id: null, + owner: 'securitySolution', }, { action_field: ['pushed'], @@ -151,6 +152,7 @@ export const userActions: CaseUserActionsResponse = [ action_id: '0a801750-6647-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', comment_id: null, + owner: 'securitySolution', }, { action_field: ['comment'], @@ -166,6 +168,7 @@ export const userActions: CaseUserActionsResponse = [ action_id: '7373eb60-6647-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', comment_id: 'comment-alert-1', + owner: 'securitySolution', }, { action_field: ['comment'], @@ -181,6 +184,7 @@ export const userActions: CaseUserActionsResponse = [ action_id: '7abc6410-6647-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', comment_id: 'comment-alert-2', + owner: 'securitySolution', }, { action_field: ['pushed'], @@ -197,6 +201,7 @@ export const userActions: CaseUserActionsResponse = [ action_id: '9b91d8f0-6647-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', comment_id: null, + owner: 'securitySolution', }, { action_field: ['comment'], @@ -212,5 +217,6 @@ export const userActions: CaseUserActionsResponse = [ action_id: '0818e5e0-6648-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', comment_id: 'comment-user-1', + owner: 'securitySolution', }, ]; diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index b7f416203e078..3991a9730c440 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -6,13 +6,7 @@ */ import Boom from '@hapi/boom'; -import { - SavedObjectsBulkUpdateResponse, - SavedObjectsUpdateResponse, - SavedObjectsFindResponse, - SavedObject, -} from 'kibana/server'; -import { ActionResult } from '../../../../actions/server'; +import { SavedObjectsFindResponse, SavedObject } from 'kibana/server'; import { ActionConnector, @@ -21,8 +15,6 @@ import { CaseStatuses, ExternalServiceResponse, ESCaseAttributes, - CommentAttributes, - CaseUserActionsResponse, ESCasesConfigureAttributes, CaseType, } from '../../../common/api'; @@ -32,6 +24,8 @@ import { createIncident, getCommentContextFromAttributes } from './utils'; import { createCaseError, flattenCaseSavedObject, getAlertInfoFromComments } from '../../common'; import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; import { CasesClient, CasesClientArgs, CasesClientInternal } from '..'; +import { ensureAuthorized } from '../utils'; +import { Operations } from '../../authorization'; /** * Returns true if the case should be closed based on the configuration settings and whether the case @@ -69,18 +63,13 @@ export const push = async ( actionsClient, user, logger, + auditLogger, + authorization, } = clientArgs; - /* Start of push to external service */ - let theCase: CaseResponse; - let connector: ActionResult; - let userActions: CaseUserActionsResponse; - let alerts; - let connectorMappings; - let externalServiceIncident; - try { - [theCase, connector, userActions] = await Promise.all([ + /* Start of push to external service */ + const [theCase, connector, userActions] = await Promise.all([ casesClient.cases.get({ id: caseId, includeComments: true, @@ -89,34 +78,29 @@ export const push = async ( actionsClient.get({ id: connectorId }), casesClient.userActions.getAll({ caseId }), ]); - } catch (e) { - const message = `Error getting case and/or connector and/or user actions: ${e.message}`; - throw createCaseError({ message, error: e, logger }); - } - // We need to change the logic when we support subcases - if (theCase?.status === CaseStatuses.closed) { - throw Boom.conflict( - `This case ${theCase.title} is closed. You can not pushed if the case is closed.` - ); - } + await ensureAuthorized({ + authorization, + auditLogger, + operation: Operations.pushCase, + savedObjectIDs: [caseId], + owners: [theCase.owner], + }); + + // We need to change the logic when we support subcases + if (theCase?.status === CaseStatuses.closed) { + throw Boom.conflict( + `The ${theCase.title} case is closed. Pushing a closed case is not allowed.` + ); + } - const alertsInfo = getAlertInfoFromComments(theCase?.comments); + const alertsInfo = getAlertInfoFromComments(theCase?.comments); - try { - alerts = await casesClientInternal.alerts.get({ + const alerts = await casesClientInternal.alerts.get({ alertsInfo, }); - } catch (e) { - throw createCaseError({ - message: `Error getting alerts for case with id ${theCase.id}: ${e.message}`, - logger, - error: e, - }); - } - try { - connectorMappings = await casesClientInternal.configuration.getMappings({ + const connectorMappings = await casesClientInternal.configuration.getMappings({ connectorId: connector.id, connectorType: connector.actionTypeId, }); @@ -124,13 +108,8 @@ export const push = async ( if (connectorMappings.length === 0) { throw new Error('Connector mapping has not been created'); } - } catch (e) { - const message = `Error getting mapping for connector with id ${connector.id}: ${e.message}`; - throw createCaseError({ message, error: e, logger }); - } - try { - externalServiceIncident = await createIncident({ + const externalServiceIncident = await createIncident({ actionsClient, theCase, userActions, @@ -138,34 +117,25 @@ export const push = async ( mappings: connectorMappings[0].attributes.mappings, alerts, }); - } catch (e) { - const message = `Error creating incident for case with id ${theCase.id}: ${e.message}`; - throw createCaseError({ error: e, message, logger }); - } - const pushRes = await actionsClient.execute({ - actionId: connector?.id ?? '', - params: { - subAction: 'pushToService', - subActionParams: externalServiceIncident, - }, - }); - - if (pushRes.status === 'error') { - throw Boom.failedDependency( - pushRes.serviceMessage ?? pushRes.message ?? 'Error pushing to service' - ); - } + const pushRes = await actionsClient.execute({ + actionId: connector?.id ?? '', + params: { + subAction: 'pushToService', + subActionParams: externalServiceIncident, + }, + }); - /* End of push to external service */ + if (pushRes.status === 'error') { + throw Boom.failedDependency( + pushRes.serviceMessage ?? pushRes.message ?? 'Error pushing to service' + ); + } - /* Start of update case with push information */ - let myCase; - let myCaseConfigure; - let comments; + /* End of push to external service */ - try { - [myCase, myCaseConfigure, comments] = await Promise.all([ + /* Start of update case with push information */ + const [myCase, myCaseConfigure, comments] = await Promise.all([ caseService.getCase({ soClient: savedObjectsClient, id: caseId, @@ -182,33 +152,25 @@ export const push = async ( includeSubCaseComments: ENABLE_CASE_CONNECTOR, }), ]); - } catch (e) { - const message = `Error getting user and/or case and/or case configuration and/or case comments: ${e.message}`; - throw createCaseError({ error: e, message, logger }); - } - - // eslint-disable-next-line @typescript-eslint/naming-convention - const { username, full_name, email } = user; - const pushedDate = new Date().toISOString(); - const externalServiceResponse = pushRes.data as ExternalServiceResponse; - const externalService = { - pushed_at: pushedDate, - pushed_by: { username, full_name, email }, - connector_id: connector.id, - connector_name: connector.name, - external_id: externalServiceResponse.id, - external_title: externalServiceResponse.title, - external_url: externalServiceResponse.url, - }; + // eslint-disable-next-line @typescript-eslint/naming-convention + const { username, full_name, email } = user; + const pushedDate = new Date().toISOString(); + const externalServiceResponse = pushRes.data as ExternalServiceResponse; - let updatedCase: SavedObjectsUpdateResponse; - let updatedComments: SavedObjectsBulkUpdateResponse; + const externalService = { + pushed_at: pushedDate, + pushed_by: { username, full_name, email }, + connector_id: connector.id, + connector_name: connector.name, + external_id: externalServiceResponse.id, + external_title: externalServiceResponse.title, + external_url: externalServiceResponse.url, + }; - const shouldMarkAsClosed = shouldCloseByPush(myCaseConfigure, myCase); + const shouldMarkAsClosed = shouldCloseByPush(myCaseConfigure, myCase); - try { - [updatedCase, updatedComments] = await Promise.all([ + const [updatedCase, updatedComments] = await Promise.all([ caseService.patchCase({ soClient: savedObjectsClient, caseId, @@ -254,6 +216,7 @@ export const push = async ( fields: ['status'], newValue: CaseStatuses.closed, oldValue: myCase.attributes.status, + owner: myCase.attributes.owner, }), ] : []), @@ -264,38 +227,39 @@ export const push = async ( caseId, fields: ['pushed'], newValue: JSON.stringify(externalService), + owner: myCase.attributes.owner, }), ], }), ]); - } catch (e) { - const message = `Error updating case and/or comments and/or creating user action: ${e.message}`; - throw createCaseError({ error: e, message, logger }); - } - /* End of update case with push information */ - return CaseResponseRt.encode( - flattenCaseSavedObject({ - savedObject: { - ...myCase, - ...updatedCase, - attributes: { ...myCase.attributes, ...updatedCase?.attributes }, - references: myCase.references, - }, - comments: comments.saved_objects.map((origComment) => { - const updatedComment = updatedComments.saved_objects.find((c) => c.id === origComment.id); - return { - ...origComment, - ...updatedComment, - attributes: { - ...origComment.attributes, - ...updatedComment?.attributes, - ...getCommentContextFromAttributes(origComment.attributes), - }, - version: updatedComment?.version ?? origComment.version, - references: origComment?.references ?? [], - }; - }), - }) - ); + /* End of update case with push information */ + + return CaseResponseRt.encode( + flattenCaseSavedObject({ + savedObject: { + ...myCase, + ...updatedCase, + attributes: { ...myCase.attributes, ...updatedCase?.attributes }, + references: myCase.references, + }, + comments: comments.saved_objects.map((origComment) => { + const updatedComment = updatedComments.saved_objects.find((c) => c.id === origComment.id); + return { + ...origComment, + ...updatedComment, + attributes: { + ...origComment.attributes, + ...updatedComment?.attributes, + ...getCommentContextFromAttributes(origComment.attributes), + }, + version: updatedComment?.version ?? origComment.version, + references: origComment?.references ?? [], + }; + }), + }) + ); + } catch (error) { + throw createCaseError({ message: `Failed to push case: ${error}`, error, logger }); + } }; diff --git a/x-pack/plugins/cases/server/client/cases/update.ts b/x-pack/plugins/cases/server/client/cases/update.ts index 402e6726a71cd..de3c499db5098 100644 --- a/x-pack/plugins/cases/server/client/cases/update.ts +++ b/x-pack/plugins/cases/server/client/cases/update.ts @@ -36,7 +36,7 @@ import { CommentAttributes, } from '../../../common/api'; import { buildCaseUserActions } from '../../services/user_actions/helpers'; -import { getCaseToUpdate } from '../utils'; +import { ensureAuthorized, getCaseToUpdate } from '../utils'; import { CaseService } from '../../services'; import { @@ -55,6 +55,7 @@ import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; import { UpdateAlertRequest } from '../alerts/client'; import { CasesClientInternal } from '../client_internal'; import { CasesClientArgs } from '..'; +import { Operations } from '../../authorization'; /** * Throws an error if any of the requests attempt to update a collection style cases' status field. @@ -113,6 +114,18 @@ 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); + + if (requestsUpdatingOwner.length > 0) { + const ids = requestsUpdatingOwner.map((req) => req.id); + throw Boom.badRequest(`Updating the owner of a case is not allowed ids: [${ids.join(', ')}]`); + } +} + /** * Throws an error if any of the requests attempt to update an individual style cases' type field to a collection * when alerts are attached to the case. @@ -337,12 +350,53 @@ async function updateAlerts({ await casesClientInternal.alerts.updateStatus({ alerts: alertsToUpdate }); } +function partitionPatchRequest( + casesMap: Map>, + patchReqCases: CasePatchRequest[] +): { + nonExistingCases: CasePatchRequest[]; + conflictedCases: CasePatchRequest[]; + casesToAuthorize: Array>; +} { + const nonExistingCases: CasePatchRequest[] = []; + const conflictedCases: CasePatchRequest[] = []; + const casesToAuthorize: Array> = []; + + for (const reqCase of patchReqCases) { + const foundCase = casesMap.get(reqCase.id); + + if (!foundCase || foundCase.error) { + nonExistingCases.push(reqCase); + } else if (foundCase.version !== reqCase.version) { + conflictedCases.push(reqCase); + // let's try to authorize the conflicted case even though we'll fail after afterwards just in case + casesToAuthorize.push(foundCase); + } else { + casesToAuthorize.push(foundCase); + } + } + + return { + nonExistingCases, + conflictedCases, + casesToAuthorize, + }; +} + export const update = async ( cases: CasesPatchRequest, clientArgs: CasesClientArgs, casesClientInternal: CasesClientInternal ): Promise => { - const { savedObjectsClient, caseService, userActionService, user, logger } = clientArgs; + const { + savedObjectsClient, + caseService, + userActionService, + user, + logger, + authorization, + auditLogger, + } = clientArgs; const query = pipe( excess(CasesPatchRequestRt).decode(cases), fold(throwErrors(Boom.badRequest), identity) @@ -354,15 +408,22 @@ export const update = async ( caseIds: query.cases.map((q) => q.id), }); - let nonExistingCases: CasePatchRequest[] = []; - const conflictedCases = query.cases.filter((q) => { - const myCase = myCases.saved_objects.find((c) => c.id === q.id); + const casesMap = myCases.saved_objects.reduce((acc, so) => { + acc.set(so.id, so); + return acc; + }, new Map>()); - if (myCase && myCase.error) { - nonExistingCases = [...nonExistingCases, q]; - return false; - } - return myCase == null || myCase?.version !== q.version; + const { nonExistingCases, conflictedCases, casesToAuthorize } = partitionPatchRequest( + casesMap, + query.cases + ); + + await ensureAuthorized({ + authorization, + auditLogger, + owners: casesToAuthorize.map((caseInfo) => caseInfo.attributes.owner), + operation: Operations.updateCase, + savedObjectIDs: casesToAuthorize.map((caseInfo) => caseInfo.id), }); if (nonExistingCases.length > 0) { @@ -403,15 +464,11 @@ export const update = async ( throw Boom.notAcceptable('All update fields are identical to current version.'); } - const casesMap = myCases.saved_objects.reduce((acc, so) => { - acc.set(so.id, so); - return acc; - }, new Map>()); - if (!ENABLE_CASE_CONNECTOR) { throwIfUpdateType(updateFilterCases); } + throwIfUpdateOwner(updateFilterCases); throwIfUpdateStatusOfCollection(updateFilterCases, casesMap); throwIfUpdateTypeCollectionToIndividual(updateFilterCases, casesMap); await throwIfInvalidUpdateOfTypeWithAlerts({ diff --git a/x-pack/plugins/cases/server/client/cases/utils.test.ts b/x-pack/plugins/cases/server/client/cases/utils.test.ts index 5f41a95d3c501..391fe5803f81f 100644 --- a/x-pack/plugins/cases/server/client/cases/utils.test.ts +++ b/x-pack/plugins/cases/server/client/cases/utils.test.ts @@ -701,6 +701,7 @@ describe('utils', () => { action_id: '9b91d8f0-6647-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', comment_id: null, + owner: 'securitySolution', }, ]); diff --git a/x-pack/plugins/cases/server/client/stats/client.ts b/x-pack/plugins/cases/server/client/stats/client.ts index 40ced0bfbf4bb..8c18c35e8f4fd 100644 --- a/x-pack/plugins/cases/server/client/stats/client.ts +++ b/x-pack/plugins/cases/server/client/stats/client.ts @@ -7,8 +7,9 @@ import { CasesClientArgs } from '..'; import { CasesStatusResponse, CasesStatusResponseRt, caseStatuses } from '../../../common/api'; +import { Operations } from '../../authorization'; import { createCaseError } from '../../common/error'; -import { constructQueryOptions } from '../utils'; +import { constructQueryOptions, getAuthorizationFilter } from '../utils'; /** * Statistics API contract. @@ -30,19 +31,34 @@ async function getStatusTotalsByType({ savedObjectsClient: soClient, caseService, logger, + authorization, + auditLogger, }: CasesClientArgs): Promise { try { + const { + filter: authorizationFilter, + ensureSavedObjectsAreAuthorized, + logSuccessfulAuthorization, + } = await getAuthorizationFilter({ + authorization, + operation: Operations.getCaseStatuses, + auditLogger, + }); + const [openCases, inProgressCases, closedCases] = await Promise.all([ ...caseStatuses.map((status) => { - const statusQuery = constructQueryOptions({ status }); + const statusQuery = constructQueryOptions({ status, authorizationFilter }); return caseService.findCaseStatusStats({ soClient, caseOptions: statusQuery.case, subCaseOptions: statusQuery.subCase, + ensureSavedObjectsAreAuthorized, }); }), ]); + logSuccessfulAuthorization(); + return CasesStatusResponseRt.encode({ count_open_cases: openCases, count_in_progress_cases: inProgressCases, diff --git a/x-pack/plugins/cases/server/client/sub_cases/client.ts b/x-pack/plugins/cases/server/client/sub_cases/client.ts index ac390710def87..102cbee14a206 100644 --- a/x-pack/plugins/cases/server/client/sub_cases/client.ts +++ b/x-pack/plugins/cases/server/client/sub_cases/client.ts @@ -105,16 +105,17 @@ async function deleteSubCase(ids: string[], clientArgs: CasesClientArgs): Promis await userActionService.bulkCreate({ soClient, - actions: ids.map((id) => + actions: subCases.saved_objects.map((subCase) => buildCaseUserActionItem({ action: 'delete', actionAt: deleteDate, actionBy: user, // if for some reason the sub case didn't have a reference to its parent, we'll still log a user action // but we won't have the case ID - caseId: subCaseIDToParentID.get(id) ?? '', - subCaseId: id, + caseId: subCaseIDToParentID.get(subCase.id) ?? '', + subCaseId: subCase.id, fields: ['sub_case', 'comment', 'status'], + owner: subCase.attributes.owner, }) ), }); diff --git a/x-pack/plugins/cases/server/client/user_actions/get.ts b/x-pack/plugins/cases/server/client/user_actions/get.ts index dac997c3fa90a..0b03fb75614a8 100644 --- a/x-pack/plugins/cases/server/client/user_actions/get.ts +++ b/x-pack/plugins/cases/server/client/user_actions/get.ts @@ -14,6 +14,8 @@ import { CaseUserActionsResponseRt, CaseUserActionsResponse } from '../../../com import { createCaseError } from '../../common/error'; import { checkEnabledCaseConnectorOrThrow } from '../../common'; import { CasesClientArgs } from '..'; +import { ensureAuthorized } from '../utils'; +import { Operations } from '../../authorization'; interface GetParams { caseId: string; @@ -24,7 +26,7 @@ export const get = async ( { caseId, subCaseId }: GetParams, clientArgs: CasesClientArgs ): Promise => { - const { savedObjectsClient, userActionService, logger } = clientArgs; + const { savedObjectsClient, userActionService, logger, authorization, auditLogger } = clientArgs; try { checkEnabledCaseConnectorOrThrow(subCaseId); @@ -35,6 +37,14 @@ export const get = async ( subCaseId, }); + await ensureAuthorized({ + authorization, + auditLogger, + owners: userActions.saved_objects.map((userAction) => userAction.attributes.owner), + savedObjectIDs: userActions.saved_objects.map((userAction) => userAction.id), + operation: Operations.getUserActions, + }); + return CaseUserActionsResponseRt.encode( userActions.saved_objects.reduce((acc, ua) => { if (subCaseId == null && ua.references.some((uar) => uar.type === SUB_CASE_SAVED_OBJECT)) { diff --git a/x-pack/plugins/cases/server/client/utils.ts b/x-pack/plugins/cases/server/client/utils.ts index eb00cce8654ef..931372cc1d6c9 100644 --- a/x-pack/plugins/cases/server/client/utils.ts +++ b/x-pack/plugins/cases/server/client/utils.ts @@ -27,6 +27,7 @@ import { excess, ContextTypeUserRt, AlertCommentRequestRt, + OWNER_FIELD, } from '../../common/api'; import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../common/constants'; import { AuditEvent } from '../../../security/server'; @@ -157,7 +158,7 @@ export const combineAuthorizedAndOwnerFilter = ( ): KueryNode | undefined => { const ownerFilter = buildFilter({ filters: owner, - field: 'owner', + field: OWNER_FIELD, operator: 'or', type: savedObjectType, }); @@ -241,7 +242,7 @@ export const constructQueryOptions = ({ operator: 'or', }); const sortField = sortToSnake(sortByField); - const ownerFilter = buildFilter({ filters: owner ?? [], field: 'owner', operator: 'or' }); + const ownerFilter = buildFilter({ filters: owner ?? [], field: OWNER_FIELD, operator: 'or' }); switch (caseType) { case CaseType.individual: { @@ -570,9 +571,14 @@ interface OwnerEntity { id: string; } +/** + * Function callback for making sure the found saved objects are of the authorized owner + */ +export type EnsureSOAuthCallback = (entities: OwnerEntity[]) => void; + interface AuthFilterHelpers { filter?: KueryNode; - ensureSavedObjectsAreAuthorized: (entities: OwnerEntity[]) => void; + ensureSavedObjectsAreAuthorized: EnsureSOAuthCallback; logSuccessfulAuthorization: () => void; } diff --git a/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts index e5b826cf0ddef..f221942716d08 100644 --- a/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts +++ b/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts @@ -513,6 +513,7 @@ export const mockUserActions: Array> = [ new_value: '{"title":"A case","tags":["case"],"description":"Yeah!","connector":{"id":"connector-od","name":"My Connector","type":".servicenow-sir","fields":{"category":"Denial of Service","destIp":true,"malwareHash":true,"malwareUrl":true,"priority":"2","sourceIp":true,"subcategory":"45"}},"settings":{"syncAlerts":true}}', old_value: null, + owner: 'securitySolution', }, version: 'WzYsMV0=', references: [], @@ -532,6 +533,7 @@ export const mockUserActions: Array> = [ new_value: '{"type":"alert","alertId":"cec3da90fb37a44407145adf1593f3b0d5ad94c4654201f773d63b5d4706128e","index":".siem-signals-default-000008"}', old_value: null, + owner: 'securitySolution', }, version: 'WzYsMV0=', references: [], diff --git a/x-pack/plugins/cases/server/services/cases/index.ts b/x-pack/plugins/cases/server/services/cases/index.ts index 870ba94b1ba13..246872b0af9d4 100644 --- a/x-pack/plugins/cases/server/services/cases/index.ts +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -32,6 +32,7 @@ import { caseTypeField, CasesFindRequest, CaseStatuses, + OWNER_FIELD, } from '../../../common/api'; import { defaultSortField, @@ -48,6 +49,8 @@ import { SUB_CASE_SAVED_OBJECT, } from '../../../common/constants'; import { ClientArgs } from '..'; +import { EnsureSOAuthCallback } from '../../client/utils'; +import { includeFieldsRequiredForAuthentication } from '../../authorization/utils'; interface PushedArgs { pushed_at: string; @@ -183,9 +186,11 @@ interface GetReportersArgs { const transformNewSubCase = ({ createdAt, createdBy, + owner, }: { createdAt: string; createdBy: User; + owner: string; }): SubCaseAttributes => { return { closed_at: null, @@ -195,6 +200,7 @@ const transformNewSubCase = ({ status: CaseStatuses.open, updated_at: null, updated_by: null, + owner, }; }; @@ -298,9 +304,11 @@ export class CaseService { soClient, caseOptions, subCaseOptions, + ensureSavedObjectsAreAuthorized, }: { soClient: SavedObjectsClientContract; caseOptions: SavedObjectFindOptionsKueryNode; + ensureSavedObjectsAreAuthorized: EnsureSOAuthCallback; subCaseOptions?: SavedObjectFindOptionsKueryNode; }): Promise { const casesStats = await this.findCases({ @@ -337,12 +345,17 @@ export class CaseService { soClient, options: { ...caseOptions, - fields: [caseTypeField], + fields: includeFieldsRequiredForAuthentication([caseTypeField]), page: 1, perPage: casesStats.total, }, }); + // make sure that the retrieved cases were correctly filtered by owner + ensureSavedObjectsAreAuthorized( + cases.saved_objects.map((caseInfo) => ({ id: caseInfo.id, owner: caseInfo.attributes.owner })) + ); + const caseIds = cases.saved_objects .filter((caseInfo) => caseInfo.attributes.type === CaseType.collection) .map((caseInfo) => caseInfo.id); @@ -575,7 +588,8 @@ export class CaseService { this.log.debug(`Attempting to POST a new sub case`); return soClient.create( SUB_CASE_SAVED_OBJECT, - transformNewSubCase({ createdAt, createdBy }), + // ENABLE_CASE_CONNECTOR: populate the owner field correctly + transformNewSubCase({ createdAt, createdBy, owner: '' }), { references: [ { @@ -922,7 +936,7 @@ export class CaseService { this.log.debug(`Attempting to GET all reporters`); const firstReporters = await soClient.find({ type: CASE_SAVED_OBJECT, - fields: ['created_by', 'owner'], + fields: ['created_by', OWNER_FIELD], page: 1, perPage: 1, filter: cloneDeep(filter), @@ -930,7 +944,7 @@ export class CaseService { return await soClient.find({ type: CASE_SAVED_OBJECT, - fields: ['created_by', 'owner'], + fields: ['created_by', OWNER_FIELD], page: 1, perPage: firstReporters.total, filter: cloneDeep(filter), @@ -949,7 +963,7 @@ export class CaseService { this.log.debug(`Attempting to GET all cases`); const firstTags = await soClient.find({ type: CASE_SAVED_OBJECT, - fields: ['tags', 'owner'], + fields: ['tags', OWNER_FIELD], page: 1, perPage: 1, filter: cloneDeep(filter), @@ -957,7 +971,7 @@ export class CaseService { return await soClient.find({ type: CASE_SAVED_OBJECT, - fields: ['tags', 'owner'], + fields: ['tags', OWNER_FIELD], page: 1, perPage: firstTags.total, filter: cloneDeep(filter), diff --git a/x-pack/plugins/cases/server/services/user_actions/helpers.ts b/x-pack/plugins/cases/server/services/user_actions/helpers.ts index 2ab3bdb5e1cee..664a9041491a1 100644 --- a/x-pack/plugins/cases/server/services/user_actions/helpers.ts +++ b/x-pack/plugins/cases/server/services/user_actions/helpers.ts @@ -17,6 +17,7 @@ import { User, UserActionFieldType, SubCaseAttributes, + OWNER_FIELD, } from '../../../common/api'; import { isTwoArraysDifference } from '../../client/utils'; import { UserActionItem } from '.'; @@ -34,6 +35,7 @@ export const transformNewUserAction = ({ email, // eslint-disable-next-line @typescript-eslint/naming-convention full_name, + owner, newValue = null, oldValue = null, username, @@ -41,6 +43,7 @@ export const transformNewUserAction = ({ actionField: UserActionField; action: UserAction; actionAt: string; + owner: string; email?: string | null; full_name?: string | null; newValue?: string | null; @@ -53,6 +56,7 @@ export const transformNewUserAction = ({ action_by: { email, full_name, username }, new_value: newValue, old_value: oldValue, + owner, }); interface BuildCaseUserAction { @@ -60,6 +64,7 @@ interface BuildCaseUserAction { actionAt: string; actionBy: User; caseId: string; + owner: string; fields: UserActionField | unknown[]; newValue?: string | unknown; oldValue?: string | unknown; @@ -80,11 +85,13 @@ export const buildCommentUserActionItem = ({ newValue, oldValue, subCaseId, + owner, }: BuildCommentUserActionItem): UserActionItem => ({ attributes: transformNewUserAction({ actionField: fields as UserActionField, action, actionAt, + owner, ...actionBy, newValue: newValue as string, oldValue: oldValue as string, @@ -121,11 +128,13 @@ export const buildCaseUserActionItem = ({ newValue, oldValue, subCaseId, + owner, }: BuildCaseUserAction): UserActionItem => ({ attributes: transformNewUserAction({ actionField: fields as UserActionField, action, actionAt, + owner, ...actionBy, newValue: newValue as string, oldValue: oldValue as string, @@ -157,7 +166,7 @@ const userActionFieldsAllowed: UserActionField = [ 'status', 'settings', 'sub_case', - 'owner', + OWNER_FIELD, ]; interface CaseSubIDs { @@ -180,7 +189,14 @@ interface Getters { getCaseAndSubID: GetCaseAndSubID; } -const buildGenericCaseUserActions = ({ +interface OwnerEntity { + owner: string; +} + +/** + * The entity associated with the user action must contain an owner field + */ +const buildGenericCaseUserActions = ({ actionDate, actionBy, originalCases, @@ -221,6 +237,7 @@ const buildGenericCaseUserActions = ({ fields: [field], newValue: updatedValue, oldValue: origValue, + owner: originalItem.attributes.owner, }), ]; } else if (Array.isArray(origValue) && Array.isArray(updatedValue)) { @@ -236,6 +253,7 @@ const buildGenericCaseUserActions = ({ subCaseId, fields: [field], newValue: compareValues.addedItems.join(', '), + owner: originalItem.attributes.owner, }), ]; } @@ -251,6 +269,7 @@ const buildGenericCaseUserActions = ({ subCaseId, fields: [field], newValue: compareValues.deletedItems.join(', '), + owner: originalItem.attributes.owner, }), ]; } @@ -270,6 +289,7 @@ const buildGenericCaseUserActions = ({ fields: [field], newValue: JSON.stringify(updatedValue), oldValue: JSON.stringify(origValue), + owner: originalItem.attributes.owner, }), ]; } diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts index ef396f75b8575..b7550a0717a28 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts @@ -74,6 +74,7 @@ describe(`cases`, () => { "cases:1.0.0-zeta1:observability/getComment", "cases:1.0.0-zeta1:observability/getTags", "cases:1.0.0-zeta1:observability/getReporters", + "cases:1.0.0-zeta1:observability/getUserActions", "cases:1.0.0-zeta1:observability/findConfigurations", ] `); @@ -112,10 +113,12 @@ describe(`cases`, () => { "cases:1.0.0-zeta1:security/getComment", "cases:1.0.0-zeta1:security/getTags", "cases:1.0.0-zeta1:security/getReporters", + "cases:1.0.0-zeta1:security/getUserActions", "cases:1.0.0-zeta1:security/findConfigurations", "cases:1.0.0-zeta1:security/createCase", "cases:1.0.0-zeta1:security/deleteCase", "cases:1.0.0-zeta1:security/updateCase", + "cases:1.0.0-zeta1:security/pushCase", "cases:1.0.0-zeta1:security/createComment", "cases:1.0.0-zeta1:security/deleteComment", "cases:1.0.0-zeta1:security/updateComment", @@ -159,10 +162,12 @@ describe(`cases`, () => { "cases:1.0.0-zeta1:security/getComment", "cases:1.0.0-zeta1:security/getTags", "cases:1.0.0-zeta1:security/getReporters", + "cases:1.0.0-zeta1:security/getUserActions", "cases:1.0.0-zeta1:security/findConfigurations", "cases:1.0.0-zeta1:security/createCase", "cases:1.0.0-zeta1:security/deleteCase", "cases:1.0.0-zeta1:security/updateCase", + "cases:1.0.0-zeta1:security/pushCase", "cases:1.0.0-zeta1:security/createComment", "cases:1.0.0-zeta1:security/deleteComment", "cases:1.0.0-zeta1:security/updateComment", @@ -172,6 +177,7 @@ describe(`cases`, () => { "cases:1.0.0-zeta1:obs/getComment", "cases:1.0.0-zeta1:obs/getTags", "cases:1.0.0-zeta1:obs/getReporters", + "cases:1.0.0-zeta1:obs/getUserActions", "cases:1.0.0-zeta1:obs/findConfigurations", ] `); @@ -211,10 +217,12 @@ describe(`cases`, () => { "cases:1.0.0-zeta1:security/getComment", "cases:1.0.0-zeta1:security/getTags", "cases:1.0.0-zeta1:security/getReporters", + "cases:1.0.0-zeta1:security/getUserActions", "cases:1.0.0-zeta1:security/findConfigurations", "cases:1.0.0-zeta1:security/createCase", "cases:1.0.0-zeta1:security/deleteCase", "cases:1.0.0-zeta1:security/updateCase", + "cases:1.0.0-zeta1:security/pushCase", "cases:1.0.0-zeta1:security/createComment", "cases:1.0.0-zeta1:security/deleteComment", "cases:1.0.0-zeta1:security/updateComment", @@ -224,10 +232,12 @@ describe(`cases`, () => { "cases:1.0.0-zeta1:other-security/getComment", "cases:1.0.0-zeta1:other-security/getTags", "cases:1.0.0-zeta1:other-security/getReporters", + "cases:1.0.0-zeta1:other-security/getUserActions", "cases:1.0.0-zeta1:other-security/findConfigurations", "cases:1.0.0-zeta1:other-security/createCase", "cases:1.0.0-zeta1:other-security/deleteCase", "cases:1.0.0-zeta1:other-security/updateCase", + "cases:1.0.0-zeta1:other-security/pushCase", "cases:1.0.0-zeta1:other-security/createComment", "cases:1.0.0-zeta1:other-security/deleteComment", "cases:1.0.0-zeta1:other-security/updateComment", @@ -237,11 +247,13 @@ describe(`cases`, () => { "cases:1.0.0-zeta1:obs/getComment", "cases:1.0.0-zeta1:obs/getTags", "cases:1.0.0-zeta1:obs/getReporters", + "cases:1.0.0-zeta1:obs/getUserActions", "cases:1.0.0-zeta1:obs/findConfigurations", "cases:1.0.0-zeta1:other-obs/getCase", "cases:1.0.0-zeta1:other-obs/getComment", "cases:1.0.0-zeta1:other-obs/getTags", "cases:1.0.0-zeta1:other-obs/getReporters", + "cases:1.0.0-zeta1:other-obs/getUserActions", "cases:1.0.0-zeta1:other-obs/findConfigurations", ] `); diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts index 2643d7c6d6aaf..4b04f98704c8f 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts @@ -17,12 +17,14 @@ const readOperations: string[] = [ 'getComment', 'getTags', 'getReporters', + 'getUserActions', 'findConfigurations', ]; const writeOperations: string[] = [ 'createCase', 'deleteCase', 'updateCase', + 'pushCase', 'createComment', 'deleteComment', 'updateComment', diff --git a/x-pack/plugins/security_solution/public/cases/containers/mock.ts b/x-pack/plugins/security_solution/public/cases/containers/mock.ts index 6880a105b1ce6..8e29e3760c8d8 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/mock.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/mock.ts @@ -185,6 +185,7 @@ const basicAction = { newValue: 'what a cool value', caseId: basicCaseId, commentId: null, + owner: 'securitySolution', }; export const cases: Case[] = [ @@ -317,6 +318,7 @@ const basicActionSnake = { new_value: 'what a cool value', case_id: basicCaseId, comment_id: null, + owner: 'securitySolution', }; export const getUserActionSnake = (af: UserActionField, a: UserAction) => ({ ...basicActionSnake, diff --git a/x-pack/test/case_api_integration/common/lib/authentication/index.ts b/x-pack/test/case_api_integration/common/lib/authentication/index.ts index a72141745e577..dfd151344b40c 100644 --- a/x-pack/test/case_api_integration/common/lib/authentication/index.ts +++ b/x-pack/test/case_api_integration/common/lib/authentication/index.ts @@ -7,7 +7,7 @@ import { FtrProviderContext as CommonFtrProviderContext } from '../../../common/ftr_provider_context'; import { Role, User, UserInfo } from './types'; -import { users } from './users'; +import { superUser, users } from './users'; import { roles } from './roles'; import { spaces } from './spaces'; @@ -90,3 +90,5 @@ export const deleteSpacesAndUsers = async (getService: CommonFtrProviderContext[ await deleteSpaces(getService); await deleteUsersAndRoles(getService); }; + +export const superUserSpace1Auth = { user: superUser, space: 'space1' }; diff --git a/x-pack/test/case_api_integration/common/lib/authentication/roles.ts b/x-pack/test/case_api_integration/common/lib/authentication/roles.ts index c08b68bb2721f..5ddecd9206106 100644 --- a/x-pack/test/case_api_integration/common/lib/authentication/roles.ts +++ b/x-pack/test/case_api_integration/common/lib/authentication/roles.ts @@ -37,6 +37,8 @@ export const globalRead: Role = { feature: { securitySolutionFixture: ['read'], observabilityFixture: ['read'], + actions: ['read'], + actionsSimulators: ['read'], }, spaces: ['*'], }, @@ -59,6 +61,8 @@ export const securitySolutionOnlyAll: Role = { { feature: { securitySolutionFixture: ['all'], + actions: ['all'], + actionsSimulators: ['all'], }, spaces: ['space1'], }, @@ -81,6 +85,8 @@ export const securitySolutionOnlyRead: Role = { { feature: { securitySolutionFixture: ['read'], + actions: ['read'], + actionsSimulators: ['read'], }, spaces: ['space1'], }, @@ -103,6 +109,8 @@ export const observabilityOnlyAll: Role = { { feature: { observabilityFixture: ['all'], + actions: ['all'], + actionsSimulators: ['all'], }, spaces: ['space1'], }, @@ -125,6 +133,8 @@ export const observabilityOnlyRead: Role = { { feature: { observabilityFixture: ['read'], + actions: ['read'], + actionsSimulators: ['read'], }, spaces: ['space1'], }, diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts index 43090df495ce9..731ddca08a34e 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -43,9 +43,10 @@ import { CasesConfigurePatch, CasesStatusResponse, CasesConfigurationsResponse, + CaseUserActionsResponse, } from '../../../../plugins/cases/common/api'; import { postCollectionReq, postCommentGenAlertReq } from './mock'; -import { getSubCasesUrl } from '../../../../plugins/cases/common/api/helpers'; +import { getCaseUserActionUrl, getSubCasesUrl } from '../../../../plugins/cases/common/api/helpers'; import { ContextTypeGeneratedAlertType } from '../../../../plugins/cases/server/connectors'; import { SignalHit } from '../../../../plugins/security_solution/server/lib/detection_engine/signals/types'; import { ActionResult, FindActionResult } from '../../../../plugins/actions/server/types'; @@ -634,13 +635,20 @@ export const getAllUserAction = async ( return userActions; }; -export const updateCase = async ( - supertest: st.SuperTest, - params: CasesPatchRequest, - expectedHttpCode: number = 200 -): Promise => { +export const updateCase = async ({ + supertest, + params, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: st.SuperTest; + params: CasesPatchRequest; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): Promise => { const { body: cases } = await supertest - .patch(CASES_URL) + .patch(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}`) + .auth(auth.user.username, auth.user.password) .set('kbn-xsrf', 'true') .send(params) .expect(expectedHttpCode); @@ -648,6 +656,24 @@ export const updateCase = async ( return cases; }; +export const getCaseUserActions = async ({ + supertest, + caseID, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: st.SuperTest; + caseID: string; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): Promise => { + const { body: userActions } = await supertest + .get(`${getSpaceUrlPrefix(auth.space)}${getCaseUserActionUrl(caseID)}`) + .auth(auth.user.username, auth.user.password) + .expect(expectedHttpCode); + return userActions; +}; + export const deleteComment = async ({ supertest, caseId, @@ -796,13 +822,20 @@ export type CreateConnectorResponse = Omit & { connector_type_id: string; }; -export const createConnector = async ( - supertest: st.SuperTest, - req: Record, - expectedHttpCode: number = 200 -): Promise => { +export const createConnector = async ({ + supertest, + req, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: st.SuperTest; + req: Record; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): Promise => { const { body: connector } = await supertest - .post('/api/actions/connector') + .post(`${getSpaceUrlPrefix(auth.space)}/api/actions/connector`) + .auth(auth.user.username, auth.user.password) .set('kbn-xsrf', 'true') .send(req) .expect(expectedHttpCode); @@ -839,12 +872,18 @@ export const updateConfiguration = async ( return configuration; }; -export const getAllCasesStatuses = async ( - supertest: st.SuperTest, - expectedHttpCode: number = 200 -): Promise => { +export const getAllCasesStatuses = async ({ + supertest, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: st.SuperTest; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): Promise => { const { body: statuses } = await supertest - .get(CASE_STATUS_URL) + .get(`${getSpaceUrlPrefix(auth.space)}${CASE_STATUS_URL}`) + .auth(auth.user.username, auth.user.password) .set('kbn-xsrf', 'true') .expect(expectedHttpCode); @@ -939,14 +978,22 @@ export const getReporters = async ({ return res; }; -export const pushCase = async ( - supertest: st.SuperTest, - caseId: string, - connectorId: string, - expectedHttpCode: number = 200 -): Promise => { +export const pushCase = async ({ + supertest, + caseId, + connectorId, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: st.SuperTest; + caseId: string; + connectorId: string; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): Promise => { const { body: res } = await supertest - .post(`${CASES_URL}/${caseId}/connector/${connectorId}/_push`) + .post(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}/${caseId}/connector/${connectorId}/_push`) + .auth(auth.user.username, auth.user.password) .set('kbn-xsrf', 'true') .send({}) .expect(expectedHttpCode); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/basic/cases/push_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/cases/push_case.ts index f964ef3ee8592..5285b57f3be72 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/basic/cases/push_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/cases/push_case.ts @@ -36,7 +36,7 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should get 403 when trying to create a connector', async () => { - await createConnector(supertest, getServiceNowConnector(), 403); + await createConnector({ supertest, req: getServiceNowConnector(), expectedHttpCode: 403 }); }); it('should get 404 when trying to push to a case without a valid connector id', async () => { @@ -65,7 +65,12 @@ export default ({ getService }: FtrProviderContext): void => { }, }); - await pushCase(supertest, postedCase.id, 'not-exist', 404); + await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: 'not-exist', + expectedHttpCode: 404, + }); }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/basic/configure/create_connector.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/configure/create_connector.ts index a403e6d55be86..fe8e311b5e4f6 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/basic/configure/create_connector.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/configure/create_connector.ts @@ -14,7 +14,7 @@ export default function serviceNow({ getService }: FtrProviderContext) { describe('create service now action', () => { it('should return 403 when creating a service now action', async () => { - await createConnector(supertest, getServiceNowConnector(), 403); + await createConnector({ supertest, req: getServiceNowConnector(), expectedHttpCode: 403 }); }); }); } diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/basic/index.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/index.ts index 502c64ccce04a..90fbb10637434 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/basic/index.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/index.ts @@ -21,10 +21,14 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => { after(async () => { await deleteSpacesAndUsers(getService); }); - // Common - loadTestFile(require.resolve('../common')); // Basic loadTestFile(require.resolve('./cases/push_case')); + + // Common + loadTestFile(require.resolve('../common')); + + // NOTE: These need to be at the end because they could delete the .kibana index and inadvertently remove the users and spaces + loadTestFile(require.resolve('../common/migrations')); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/delete_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/delete_cases.ts index 484dca314c9cc..17aac2dd7e285 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/delete_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/delete_cases.ts @@ -39,6 +39,8 @@ import { superUser, } from '../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; + // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertestWithoutAuth = getService('supertestWithoutAuth'); @@ -108,6 +110,7 @@ export default ({ getService }: FtrProviderContext): void => { case_id: `${postedCase.id}`, comment_id: null, sub_case_id: '', + owner: 'securitySolutionFixture', }); }); @@ -237,14 +240,14 @@ export default ({ getService }: FtrProviderContext): void => { supertest: supertestWithoutAuth, caseId: caseSec.id, expectedHttpCode: 200, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); await getCase({ supertest: supertestWithoutAuth, caseId: caseObs.id, expectedHttpCode: 200, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/find_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/find_cases.ts index 6bcd78f98e5eb..b7838dd9299bc 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/find_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/find_cases.ts @@ -99,14 +99,17 @@ export default ({ getService }: FtrProviderContext): void => { it('filters by status', async () => { await createCase(supertest, postCaseReq); const toCloseCase = await createCase(supertest, postCaseReq); - const patchedCase = await updateCase(supertest, { - cases: [ - { - id: toCloseCase.id, - version: toCloseCase.version, - status: CaseStatuses.closed, - }, - ], + const patchedCase = await updateCase({ + supertest, + params: { + cases: [ + { + id: toCloseCase.id, + version: toCloseCase.version, + status: CaseStatuses.closed, + }, + ], + }, }); const cases = await findCases({ supertest, query: { status: CaseStatuses.closed } }); @@ -164,24 +167,30 @@ export default ({ getService }: FtrProviderContext): void => { const inProgressCase = await createCase(supertest, postCaseReq); const postedCase = await createCase(supertest, postCaseReq); - await updateCase(supertest, { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: CaseStatuses.closed, - }, - ], + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses.closed, + }, + ], + }, }); - await updateCase(supertest, { - cases: [ - { - id: inProgressCase.id, - version: inProgressCase.version, - status: CaseStatuses['in-progress'], - }, - ], + await updateCase({ + supertest, + params: { + cases: [ + { + id: inProgressCase.id, + version: inProgressCase.version, + status: CaseStatuses['in-progress'], + }, + ], + }, }); const cases = await findCases({ supertest }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts index b50c18192a05b..674c2c68381b8 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../../plugins/security_solution/common/constants'; import { @@ -18,6 +18,7 @@ import { } from '../../../../../../plugins/cases/common/api'; import { defaultUser, + getPostCaseRequest, postCaseReq, postCaseResp, postCollectionReq, @@ -34,6 +35,7 @@ import { getAllUserAction, removeServerGeneratedPropertiesFromCase, removeServerGeneratedPropertiesFromUserAction, + findCases, } from '../../../../common/lib/utils'; import { createSignalsIndex, @@ -46,6 +48,17 @@ import { createRule, getQuerySignalIds, } from '../../../../../detection_engine_api_integration/utils'; +import { + globalRead, + noKibanaPrivileges, + obsOnly, + obsOnlyRead, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -61,14 +74,17 @@ export default ({ getService }: FtrProviderContext): void => { describe('happy path', () => { it('should patch a case', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCases = await updateCase(supertest, { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - title: 'new title', - }, - ], + const patchedCases = await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', + }, + ], + }, }); const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); @@ -81,14 +97,17 @@ export default ({ getService }: FtrProviderContext): void => { it('should closes the case correctly', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCases = await updateCase(supertest, { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: CaseStatuses.closed, - }, - ], + const patchedCases = await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses.closed, + }, + ], + }, }); const userActions = await getAllUserAction(supertest, postedCase.id); @@ -111,19 +130,23 @@ export default ({ getService }: FtrProviderContext): void => { case_id: `${postedCase.id}`, comment_id: null, sub_case_id: '', + owner: 'securitySolutionFixture', }); }); it('should change the status of case to in-progress correctly', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCases = await updateCase(supertest, { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: CaseStatuses['in-progress'], - }, - ], + const patchedCases = await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses['in-progress'], + }, + ], + }, }); const userActions = await getAllUserAction(supertest, postedCase.id); @@ -145,24 +168,28 @@ export default ({ getService }: FtrProviderContext): void => { case_id: `${postedCase.id}`, comment_id: null, sub_case_id: '', + owner: 'securitySolutionFixture', }); }); it('should patch a case with new connector', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCases = await updateCase(supertest, { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - connector: { - id: 'jira', - name: 'Jira', - type: ConnectorTypes.jira, - fields: { issueType: 'Task', priority: null, parent: null }, + const patchedCases = await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + connector: { + id: 'jira', + name: 'Jira', + type: ConnectorTypes.jira, + fields: { issueType: 'Task', priority: null, parent: null }, + }, }, - }, - ], + ], + }, }); const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); @@ -186,23 +213,43 @@ export default ({ getService }: FtrProviderContext): void => { caseId: postedCase.id, params: postCommentUserReq, }); - await updateCase(supertest, { - cases: [ - { - id: patchedCase.id, - version: patchedCase.version, - type: CaseType.collection, - }, - ], + await updateCase({ + supertest, + params: { + cases: [ + { + id: patchedCase.id, + version: patchedCase.version, + type: CaseType.collection, + }, + ], + }, }); }); }); describe('unhappy path', () => { + it('400s when attempting to change the owner of a case', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + owner: 'observabilityFixture', + }, + ], + }, + expectedHttpCode: 400, + }); + }); + it('404s when case is not there', async () => { - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ { id: 'not-real', @@ -211,14 +258,14 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 404 - ); + expectedHttpCode: 404, + }); }); it('400s when id is missing', async () => { - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ // @ts-expect-error { @@ -227,15 +274,15 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 400 - ); + expectedHttpCode: 400, + }); }); it('406s when fields are identical', async () => { const postedCase = await createCase(supertest, postCaseReq); - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ { id: postedCase.id, @@ -244,14 +291,14 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 406 - ); + expectedHttpCode: 406, + }); }); it('400s when version is missing', async () => { - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ // @ts-expect-error { @@ -260,16 +307,16 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 400 - ); + expectedHttpCode: 400, + }); }); // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests it.skip('should 400 and not allow converting a collection back to an individual case', async () => { const postedCase = await createCase(supertest, postCollectionReq); - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ { id: postedCase.id, @@ -278,15 +325,15 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 400 - ); + expectedHttpCode: 400, + }); }); it('406s when excess data sent', async () => { const postedCase = await createCase(supertest, postCaseReq); - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ { id: postedCase.id, @@ -296,15 +343,15 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 406 - ); + expectedHttpCode: 406, + }); }); it('400s when bad data sent', async () => { const postedCase = await createCase(supertest, postCaseReq); - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ { id: postedCase.id, @@ -314,15 +361,15 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 400 - ); + expectedHttpCode: 400, + }); }); it('400s when unsupported status sent', async () => { const postedCase = await createCase(supertest, postCaseReq); - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ { id: postedCase.id, @@ -332,15 +379,15 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 400 - ); + expectedHttpCode: 400, + }); }); it('400s when bad connector type sent', async () => { const postedCase = await createCase(supertest, postCaseReq); - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ { id: postedCase.id, @@ -350,15 +397,15 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 400 - ); + expectedHttpCode: 400, + }); }); it('400s when bad connector sent', async () => { const postedCase = await createCase(supertest, postCaseReq); - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ { id: postedCase.id, @@ -374,15 +421,15 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 400 - ); + expectedHttpCode: 400, + }); }); it('409s when version does not match', async () => { const postedCase = await createCase(supertest, postCaseReq); - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ { id: postedCase.id, @@ -392,8 +439,8 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 409 - ); + expectedHttpCode: 409, + }); }); it('should 400 when attempting to update an individual case to a collection when it has alerts attached to it', async () => { @@ -403,9 +450,9 @@ export default ({ getService }: FtrProviderContext): void => { caseId: postedCase.id, params: postCommentAlertReq, }); - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ { id: patchedCase.id, @@ -414,16 +461,16 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 400 - ); + expectedHttpCode: 400, + }); }); // ENABLE_CASE_CONNECTOR: once the case connector feature is completed delete these tests it('should 400 when attempting to update the case type when the case connector feature is disabled', async () => { const postedCase = await createCase(supertest, postCaseReq); - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ { id: postedCase.id, @@ -432,16 +479,16 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 400 - ); + expectedHttpCode: 400, + }); }); // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests it.skip("should 400 when attempting to update a collection case's status", async () => { const postedCase = await createCase(supertest, postCollectionReq); - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ { id: postedCase.id, @@ -450,8 +497,8 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 400 - ); + expectedHttpCode: 400, + }); }); }); @@ -562,12 +609,15 @@ export default ({ getService }: FtrProviderContext): void => { // it updates alert status when syncAlerts is turned on // turn on the sync settings - await updateCase(supertest, { - cases: updatedIndWithStatus.map((caseInfo) => ({ - id: caseInfo.id, - version: caseInfo.version, - settings: { syncAlerts: true }, - })), + await updateCase({ + supertest, + params: { + cases: updatedIndWithStatus.map((caseInfo) => ({ + id: caseInfo.id, + version: caseInfo.version, + settings: { syncAlerts: true }, + })), + }, }); await es.indices.refresh({ index: defaultSignalsIndex }); @@ -682,14 +732,17 @@ export default ({ getService }: FtrProviderContext): void => { ).to.be(CaseStatuses.open); // turn on the sync settings - await updateCase(supertest, { - cases: [ - { - id: updatedIndWithStatus[0].id, - version: updatedIndWithStatus[0].version, - settings: { syncAlerts: true }, - }, - ], + await updateCase({ + supertest, + params: { + cases: [ + { + id: updatedIndWithStatus[0].id, + version: updatedIndWithStatus[0].version, + settings: { syncAlerts: true }, + }, + ], + }, }); await es.indices.refresh({ index: defaultSignalsIndex }); @@ -750,14 +803,17 @@ export default ({ getService }: FtrProviderContext): void => { }); await es.indices.refresh({ index: alert._index }); - await updateCase(supertest, { - cases: [ - { - id: caseUpdated.id, - version: caseUpdated.version, - status: CaseStatuses['in-progress'], - }, - ], + await updateCase({ + supertest, + params: { + cases: [ + { + id: caseUpdated.id, + version: caseUpdated.version, + status: CaseStatuses['in-progress'], + }, + ], + }, }); // force a refresh on the index that the signal is stored in so that we can search for it and get the correct @@ -804,14 +860,17 @@ export default ({ getService }: FtrProviderContext): void => { }, }); - await updateCase(supertest, { - cases: [ - { - id: caseUpdated.id, - version: caseUpdated.version, - status: CaseStatuses['in-progress'], - }, - ], + await updateCase({ + supertest, + params: { + cases: [ + { + id: caseUpdated.id, + version: caseUpdated.version, + status: CaseStatuses['in-progress'], + }, + ], + }, }); const { body: updatedAlert } = await supertest @@ -855,25 +914,31 @@ export default ({ getService }: FtrProviderContext): void => { }); // Update the status of the case with sync alerts off - const caseStatusUpdated = await updateCase(supertest, { - cases: [ - { - id: caseUpdated.id, - version: caseUpdated.version, - status: CaseStatuses['in-progress'], - }, - ], + const caseStatusUpdated = await updateCase({ + supertest, + params: { + cases: [ + { + id: caseUpdated.id, + version: caseUpdated.version, + status: CaseStatuses['in-progress'], + }, + ], + }, }); // Turn sync alerts on - await updateCase(supertest, { - cases: [ - { - id: caseStatusUpdated[0].id, - version: caseStatusUpdated[0].version, - settings: { syncAlerts: true }, - }, - ], + await updateCase({ + supertest, + params: { + cases: [ + { + id: caseStatusUpdated[0].id, + version: caseStatusUpdated[0].version, + settings: { syncAlerts: true }, + }, + ], + }, }); // refresh the index because syncAlerts was set to true so the alert's status should have been updated @@ -916,25 +981,31 @@ export default ({ getService }: FtrProviderContext): void => { }); // Turn sync alerts off - const caseSettingsUpdated = await updateCase(supertest, { - cases: [ - { - id: caseUpdated.id, - version: caseUpdated.version, - settings: { syncAlerts: false }, - }, - ], + const caseSettingsUpdated = await updateCase({ + supertest, + params: { + cases: [ + { + id: caseUpdated.id, + version: caseUpdated.version, + settings: { syncAlerts: false }, + }, + ], + }, }); // Update the status of the case with sync alerts off - await updateCase(supertest, { - cases: [ - { - id: caseSettingsUpdated[0].id, - version: caseSettingsUpdated[0].version, - status: CaseStatuses['in-progress'], - }, - ], + await updateCase({ + supertest, + params: { + cases: [ + { + id: caseSettingsUpdated[0].id, + version: caseSettingsUpdated[0].version, + status: CaseStatuses['in-progress'], + }, + ], + }, }); const { body: updatedAlert } = await supertest @@ -947,5 +1018,223 @@ export default ({ getService }: FtrProviderContext): void => { }); }); }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + it('should update a case when the user has the correct permissions', async () => { + const postedCase = await createCase(supertestWithoutAuth, postCaseReq, 200, { + user: secOnly, + space: 'space1', + }); + + const patchedCases = await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', + }, + ], + }, + auth: { user: secOnly, space: 'space1' }, + }); + + expect(patchedCases[0].owner).to.eql('securitySolutionFixture'); + }); + + it('should update multiple cases when the user has the correct permissions', async () => { + const [case1, case2, case3] = await Promise.all([ + createCase(supertestWithoutAuth, postCaseReq, 200, { + user: superUser, + space: 'space1', + }), + createCase(supertestWithoutAuth, postCaseReq, 200, { + user: superUser, + space: 'space1', + }), + createCase(supertestWithoutAuth, postCaseReq, 200, { + user: superUser, + space: 'space1', + }), + ]); + + const patchedCases = await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: case1.id, + version: case1.version, + title: 'new title', + }, + { + id: case2.id, + version: case2.version, + title: 'new title', + }, + { + id: case3.id, + version: case3.version, + title: 'new title', + }, + ], + }, + auth: { user: secOnly, space: 'space1' }, + }); + + expect(patchedCases[0].owner).to.eql('securitySolutionFixture'); + expect(patchedCases[1].owner).to.eql('securitySolutionFixture'); + expect(patchedCases[2].owner).to.eql('securitySolutionFixture'); + }); + + it('should not update a case when the user does not have the correct ownership', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { user: obsOnly, space: 'space1' } + ); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', + }, + ], + }, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + + it('should not update any cases when the user does not have the correct ownership', async () => { + const [case1, case2, case3] = await Promise.all([ + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ), + ]); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: case1.id, + version: case1.version, + title: 'new title', + }, + { + id: case2.id, + version: case2.version, + title: 'new title', + }, + { + id: case3.id, + version: case3.version, + title: 'new title', + }, + ], + }, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 403, + }); + + const resp = await findCases({ supertest, auth: superUserSpace1Auth }); + expect(resp.cases.length).to.eql(3); + // the update should have failed and none of the title should have been changed + expect(resp.cases[0].title).to.eql(postCaseReq.title); + expect(resp.cases[1].title).to.eql(postCaseReq.title); + expect(resp.cases[2].title).to.eql(postCaseReq.title); + }); + + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT update a case`, async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', + }, + ], + }, + auth: { user, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + } + + it('should NOT create a case in a space with no permissions', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space2', + } + ); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', + }, + ], + }, + auth: { user: secOnly, space: 'space2' }, + expectedHttpCode: 403, + }); + }); + }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/post_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/post_case.ts index f2b9027cfb1f1..91fb03604b3c4 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/post_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/post_case.ts @@ -128,6 +128,7 @@ export default ({ getService }: FtrProviderContext): void => { case_id: `${postedCase.id}`, comment_id: null, sub_case_id: '', + owner: 'securitySolutionFixture', }); expect(parsedNewValue).to.eql({ diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts index b71c7105be8f2..f58dfa1522d4a 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts @@ -6,16 +6,26 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { CaseStatuses } from '../../../../../../../plugins/cases/common/api'; -import { postCaseReq } from '../../../../../common/lib/mock'; +import { getPostCaseRequest, postCaseReq } from '../../../../../common/lib/mock'; import { - deleteCasesByESQuery, createCase, updateCase, getAllCasesStatuses, + deleteAllCaseItems, } from '../../../../../common/lib/utils'; +import { + globalRead, + noKibanaPrivileges, + obsOnlyRead, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -24,35 +34,35 @@ export default ({ getService }: FtrProviderContext): void => { describe('get_status', () => { afterEach(async () => { - await deleteCasesByESQuery(es); + await deleteAllCaseItems(es); }); it('should return case statuses', async () => { - await createCase(supertest, postCaseReq); - const inProgressCase = await createCase(supertest, postCaseReq); - const postedCase = await createCase(supertest, postCaseReq); - - await updateCase(supertest, { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: CaseStatuses.closed, - }, - ], - }); + const [, inProgressCase, postedCase] = await Promise.all([ + createCase(supertest, postCaseReq), + createCase(supertest, postCaseReq), + createCase(supertest, postCaseReq), + ]); - await updateCase(supertest, { - cases: [ - { - id: inProgressCase.id, - version: inProgressCase.version, - status: CaseStatuses['in-progress'], - }, - ], + await updateCase({ + supertest, + params: { + cases: [ + { + id: inProgressCase.id, + version: inProgressCase.version, + status: CaseStatuses['in-progress'], + }, + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses.closed, + }, + ], + }, }); - const statuses = await getAllCasesStatuses(supertest); + const statuses = await getAllCasesStatuses({ supertest }); expect(statuses).to.eql({ count_open_cases: 1, @@ -60,5 +70,103 @@ export default ({ getService }: FtrProviderContext): void => { count_in_progress_cases: 1, }); }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + it('should return the correct status stats', async () => { + /** + * Owner: Sec + * open: 0, in-prog: 1, closed: 1 + * Owner: Obs + * open: 1, in-prog: 1 + */ + const [inProgressSec, closedSec, , inProgressObs] = await Promise.all([ + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { + user: superUser, + space: 'space1', + }), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { + user: superUser, + space: 'space1', + }), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + superUserSpace1Auth + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + superUserSpace1Auth + ), + ]); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: inProgressSec.id, + version: inProgressSec.version, + status: CaseStatuses['in-progress'], + }, + { + id: closedSec.id, + version: closedSec.version, + status: CaseStatuses.closed, + }, + { + id: inProgressObs.id, + version: inProgressObs.version, + status: CaseStatuses['in-progress'], + }, + ], + }, + auth: superUserSpace1Auth, + }); + + for (const scenario of [ + { user: globalRead, stats: { open: 1, inProgress: 2, closed: 1 } }, + { user: superUser, stats: { open: 1, inProgress: 2, closed: 1 } }, + { user: secOnlyRead, stats: { open: 0, inProgress: 1, closed: 1 } }, + { user: obsOnlyRead, stats: { open: 1, inProgress: 1, closed: 0 } }, + { user: obsSecRead, stats: { open: 1, inProgress: 2, closed: 1 } }, + ]) { + const statuses = await getAllCasesStatuses({ + supertest: supertestWithoutAuth, + auth: { user: scenario.user, space: 'space1' }, + }); + + expect(statuses).to.eql({ + count_open_cases: scenario.stats.open, + count_closed_cases: scenario.stats.closed, + count_in_progress_cases: scenario.stats.inProgress, + }); + } + }); + + for (const scenario of [ + { user: noKibanaPrivileges, space: 'space1' }, + { user: secOnly, space: 'space2' }, + ]) { + it(`should return a 403 when retrieving the statuses when the user ${ + scenario.user.username + } with role(s) ${scenario.user.roles.join()} and space ${scenario.space}`, async () => { + await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { + user: superUser, + space: scenario.space, + }); + + await getAllCasesStatuses({ + supertest: supertestWithoutAuth, + auth: { user: scenario.user, space: scenario.space }, + expectedHttpCode: 403, + }); + }); + } + }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts index 353974632feb8..73b85ef97d119 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts @@ -33,6 +33,7 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -277,14 +278,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); const commentResp = await createComment({ supertest: supertestWithoutAuth, caseId: postedCase.id, params: postCommentUserReq, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); await deleteComment({ @@ -309,14 +310,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); const commentResp = await createComment({ supertest: supertestWithoutAuth, caseId: postedCase.id, params: postCommentUserReq, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); await deleteComment({ diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts index 470c2481410ff..0f73b1ee7a624 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts @@ -40,6 +40,7 @@ import { globalRead, obsSecRead, } from '../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -312,12 +313,12 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); await createComment({ supertest: supertestWithoutAuth, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, params: { ...postCommentUserReq, owner: 'observabilityFixture' }, caseId: obsCase.id, }); @@ -340,12 +341,12 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); await createComment({ supertest: supertestWithoutAuth, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, params: { ...postCommentUserReq, owner: 'observabilityFixture' }, caseId: obsCase.id, }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts index 2be30ed7bc02c..361e72bdc79bf 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts @@ -31,6 +31,7 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -141,21 +142,21 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); await createComment({ supertest: supertestWithoutAuth, caseId: caseInfo.id, params: postCommentUserReq, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); await createComment({ supertest: supertestWithoutAuth, caseId: caseInfo.id, params: postCommentUserReq, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { @@ -174,14 +175,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); await createComment({ supertest: supertestWithoutAuth, caseId: caseInfo.id, params: postCommentUserReq, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); for (const scenario of [ diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts index 7b55d468312a1..98b6cc5a7a30c 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts @@ -30,6 +30,7 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -94,14 +95,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); const caseWithComment = await createComment({ supertest: supertestWithoutAuth, caseId: caseInfo.id, params: postCommentUserReq, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { @@ -119,14 +120,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); const caseWithComment = await createComment({ supertest: supertestWithoutAuth, caseId: caseInfo.id, params: postCommentUserReq, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); for (const user of [noKibanaPrivileges, obsOnly, obsOnlyRead]) { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts index fcaebddeb8bde..c1f37d5eb2f05 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts @@ -45,6 +45,7 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -511,14 +512,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); const patchedCase = await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); const newComment = 'Well I decided to update my comment. So what? Deal with it.'; @@ -546,14 +547,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); const patchedCase = await createComment({ supertest: supertestWithoutAuth, caseId: postedCase.id, params: postCommentUserReq, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); const newComment = 'Well I decided to update my comment. So what? Deal with it.'; @@ -579,14 +580,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); const patchedCase = await createComment({ supertest: supertestWithoutAuth, caseId: postedCase.id, params: postCommentUserReq, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); const newComment = 'Well I decided to update my comment. So what? Deal with it.'; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts index 0e501648c512b..1fcb49ec10ad4 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts @@ -61,6 +61,7 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -151,6 +152,7 @@ export default ({ getService }: FtrProviderContext): void => { case_id: `${postedCase.id}`, comment_id: `${patchedCase.comments![0].id}`, sub_case_id: '', + owner: 'securitySolutionFixture', }); }); }); @@ -533,7 +535,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); await createComment({ @@ -569,7 +571,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); await createComment({ diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts index c6c68efd7a752..ff2d1b5f37aae 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts @@ -35,9 +35,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./sub_cases/get_sub_case')); loadTestFile(require.resolve('./sub_cases/find_sub_cases')); - // Migrations - loadTestFile(require.resolve('./cases/migrations')); - loadTestFile(require.resolve('./configure/migrations')); - loadTestFile(require.resolve('./user_actions/migrations')); + // NOTE: Migrations are not included because they can inadvertently remove the .kibana indices which removes the users and spaces + // which causes errors in any tests after them that relies on those }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/migrations.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/migrations.ts new file mode 100644 index 0000000000000..17d93e76bbdda --- /dev/null +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/migrations.ts @@ -0,0 +1,18 @@ +/* + * 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 { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('Common migrations', function () { + // Migrations + loadTestFile(require.resolve('./cases/migrations')); + loadTestFile(require.resolve('./configure/migrations')); + loadTestFile(require.resolve('./user_actions/migrations')); + }); +}; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts index 19911890929d2..5cd4082bd3293 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts @@ -9,14 +9,33 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; -import { CommentType } from '../../../../../../plugins/cases/common/api'; -import { userActionPostResp, postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; import { - deleteCasesByESQuery, - deleteCasesUserActions, - deleteComments, - deleteConfiguration, + CaseResponse, + CaseStatuses, + CommentType, +} from '../../../../../../plugins/cases/common/api'; +import { + userActionPostResp, + postCaseReq, + postCommentUserReq, + getPostCaseRequest, +} from '../../../../common/lib/mock'; +import { + deleteAllCaseItems, + createCase, + updateCase, + getCaseUserActions, } from '../../../../common/lib/utils'; +import { + globalRead, + noKibanaPrivileges, + obsSec, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -25,10 +44,7 @@ export default ({ getService }: FtrProviderContext): void => { describe('get_all_user_actions', () => { afterEach(async () => { - await deleteCasesByESQuery(es); - await deleteComments(es); - await deleteConfiguration(es); - await deleteCasesUserActions(es); + await deleteAllCaseItems(es); }); it(`on new case, user action: 'create' should be called with actionFields: ['description', 'status', 'tags', 'title', 'connector', 'settings, owner]`, async () => { @@ -321,5 +337,59 @@ export default ({ getService }: FtrProviderContext): void => { owner: 'securitySolutionFixture', }); }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + let caseInfo: CaseResponse; + beforeEach(async () => { + caseInfo = await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { + user: superUser, + space: 'space1', + }); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: caseInfo.id, + version: caseInfo.version, + status: CaseStatuses.closed, + }, + ], + }, + auth: superUserSpace1Auth, + }); + }); + + it('should get the user actions for a case when the user has the correct permissions', async () => { + for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { + const userActions = await getCaseUserActions({ + supertest: supertestWithoutAuth, + caseID: caseInfo.id, + auth: { user, space: 'space1' }, + }); + + expect(userActions.length).to.eql(2); + } + }); + + for (const scenario of [ + { user: noKibanaPrivileges, space: 'space1' }, + { user: secOnly, space: 'space2' }, + ]) { + it(`should 403 when requesting the user actions of a case with user ${ + scenario.user.username + } with role(s) ${scenario.user.roles.join()} and space ${scenario.space}`, async () => { + await getCaseUserActions({ + supertest: supertestWithoutAuth, + caseID: caseInfo.id, + auth: { user: scenario.user, space: scenario.space }, + expectedHttpCode: 403, + }); + }); + } + }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts index 88f7c15f4a5fe..3c096cb7557c3 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts @@ -8,15 +8,18 @@ /* eslint-disable @typescript-eslint/naming-convention */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import * as st from 'supertest'; +import supertestAsPromised from 'supertest-as-promised'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; -import { postCaseReq, defaultUser, postCommentUserReq } from '../../../../common/lib/mock'; import { - deleteCasesByESQuery, - deleteCasesUserActions, - deleteComments, - deleteConfiguration, + postCaseReq, + defaultUser, + postCommentUserReq, + getPostCaseRequest, +} from '../../../../common/lib/mock'; +import { getConfigurationRequest, getServiceNowConnector, createConnector, @@ -28,6 +31,7 @@ import { updateCase, getAllUserAction, removeServerGeneratedPropertiesFromUserAction, + deleteAllCaseItems, } from '../../../../common/lib/utils'; import { ExternalServiceSimulator, @@ -35,11 +39,23 @@ import { } from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; import { CaseConnector, + CasePostRequest, CaseResponse, CaseStatuses, CaseUserActionResponse, ConnectorTypes, } from '../../../../../../plugins/cases/common/api'; +import { + globalRead, + noKibanaPrivileges, + obsOnlyRead, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../common/lib/authentication/users'; +import { User } from '../../../../common/lib/authentication/types'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -58,56 +74,79 @@ export default ({ getService }: FtrProviderContext): void => { }); afterEach(async () => { - await deleteCasesByESQuery(es); - await deleteComments(es); - await deleteConfiguration(es); - await deleteCasesUserActions(es); + await deleteAllCaseItems(es); await actionsRemover.removeAll(); }); - const createCaseWithConnector = async ( - configureReq = {} - ): Promise<{ + const createCaseWithConnector = async ({ + testAgent = supertest, + configureReq = {}, + auth = { user: superUser, space: null }, + createCaseReq = getPostCaseRequest(), + }: { + testAgent?: st.SuperTest; + configureReq?: Record; + auth?: { user: User; space: string | null }; + createCaseReq?: CasePostRequest; + } = {}): Promise<{ postedCase: CaseResponse; connector: CreateConnectorResponse; }> => { - const connector = await createConnector(supertest, { - ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, + const connector = await createConnector({ + supertest: testAgent, + req: { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }, + auth, }); - actionsRemover.add('default', connector.id, 'action', 'actions'); - await createConfiguration(supertest, { - ...getConfigurationRequest({ - id: connector.id, - name: connector.name, - type: connector.connector_type_id as ConnectorTypes, - }), - ...configureReq, - }); + actionsRemover.add(auth.space ?? 'default', connector.id, 'action', 'actions'); + await createConfiguration( + testAgent, + { + ...getConfigurationRequest({ + id: connector.id, + name: connector.name, + type: connector.connector_type_id as ConnectorTypes, + }), + ...configureReq, + }, + 200, + auth + ); - const postedCase = await createCase(supertest, { - ...postCaseReq, - connector: { - id: connector.id, - name: connector.name, - type: connector.connector_type_id, - fields: { - urgency: '2', - impact: '2', - severity: '2', - category: 'software', - subcategory: 'os', - }, - } as CaseConnector, - }); + const postedCase = await createCase( + testAgent, + { + ...createCaseReq, + connector: { + id: connector.id, + name: connector.name, + type: connector.connector_type_id, + fields: { + urgency: '2', + impact: '2', + severity: '2', + category: 'software', + subcategory: 'os', + }, + } as CaseConnector, + }, + 200, + auth + ); return { postedCase, connector }; }; it('should push a case', async () => { const { postedCase, connector } = await createCaseWithConnector(); - const theCase = await pushCase(supertest, postedCase.id, connector.id); + const theCase = await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: connector.id, + }); const { pushed_at, external_url, ...rest } = theCase.external_service!; @@ -130,23 +169,37 @@ export default ({ getService }: FtrProviderContext): void => { it('pushes a comment appropriately', async () => { const { postedCase, connector } = await createCaseWithConnector(); await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq }); - const theCase = await pushCase(supertest, postedCase.id, connector.id); + const theCase = await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: connector.id, + }); expect(theCase.comments![0].pushed_by).to.eql(defaultUser); }); it('should pushes a case and closes when closure_type: close-by-pushing', async () => { const { postedCase, connector } = await createCaseWithConnector({ - closure_type: 'close-by-pushing', + configureReq: { + closure_type: 'close-by-pushing', + }, + }); + const theCase = await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: connector.id, }); - const theCase = await pushCase(supertest, postedCase.id, connector.id); expect(theCase.status).to.eql('closed'); }); it('should create the correct user action', async () => { const { postedCase, connector } = await createCaseWithConnector(); - const pushedCase = await pushCase(supertest, postedCase.id, connector.id); + const pushedCase = await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: connector.id, + }); const userActions = await getAllUserAction(supertest, pushedCase.id); const pushUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); @@ -161,6 +214,7 @@ export default ({ getService }: FtrProviderContext): void => { case_id: `${postedCase.id}`, comment_id: null, sub_case_id: '', + owner: 'securitySolutionFixture', }); expect(parsedNewValue).to.eql({ @@ -177,15 +231,26 @@ export default ({ getService }: FtrProviderContext): void => { // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests it.skip('should push a collection case but not close it when closure_type: close-by-pushing', async () => { const { postedCase, connector } = await createCaseWithConnector({ - closure_type: 'close-by-pushing', + configureReq: { + closure_type: 'close-by-pushing', + }, }); - const theCase = await pushCase(supertest, postedCase.id, connector.id); + const theCase = await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: connector.id, + }); expect(theCase.status).to.eql(CaseStatuses.open); }); it('unhappy path - 404s when case does not exist', async () => { - await pushCase(supertest, 'fake-id', 'fake-connector', 404); + await pushCase({ + supertest, + caseId: 'fake-id', + connectorId: 'fake-connector', + expectedHttpCode: 404, + }); }); it('unhappy path - 404s when connector does not exist', async () => { @@ -193,22 +258,103 @@ export default ({ getService }: FtrProviderContext): void => { ...postCaseReq, connector: getConfigurationRequest().connector, }); - await pushCase(supertest, postedCase.id, 'fake-connector', 404); + await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: 'fake-connector', + expectedHttpCode: 404, + }); }); it('unhappy path = 409s when case is closed', async () => { const { postedCase, connector } = await createCaseWithConnector(); - await updateCase(supertest, { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: CaseStatuses.closed, - }, - ], + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses.closed, + }, + ], + }, }); - await pushCase(supertest, postedCase.id, connector.id, 409); + await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: connector.id, + expectedHttpCode: 409, + }); + }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + it('should push a case that the user has permissions for', async () => { + const { postedCase, connector } = await createCaseWithConnector({ + testAgent: supertestWithoutAuth, + auth: superUserSpace1Auth, + }); + + await pushCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + connectorId: connector.id, + auth: { user: secOnly, space: 'space1' }, + }); + }); + + it('should not push a case that the user does not have permissions for', async () => { + const { postedCase, connector } = await createCaseWithConnector({ + testAgent: supertestWithoutAuth, + auth: superUserSpace1Auth, + createCaseReq: getPostCaseRequest({ owner: 'observabilityFixture' }), + }); + + await pushCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + connectorId: connector.id, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT push a case`, async () => { + const { postedCase, connector } = await createCaseWithConnector({ + testAgent: supertestWithoutAuth, + auth: superUserSpace1Auth, + }); + + await pushCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + connectorId: connector.id, + auth: { user, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + } + + it('should not push a case in a space that the user does not have permissions for', async () => { + const { postedCase, connector } = await createCaseWithConnector({ + testAgent: supertestWithoutAuth, + auth: { user: superUser, space: 'space2' }, + }); + + await pushCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + connectorId: connector.id, + auth: { user: secOnly, space: 'space2' }, + expectedHttpCode: 403, + }); + }); }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_configure.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_configure.ts index 6d556423893d5..ff8f1cff884af 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_configure.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_configure.ts @@ -45,9 +45,12 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should return a configuration with mapping', async () => { - const connector = await createConnector(supertest, { - ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, + const connector = await createConnector({ + supertest, + req: { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }, }); actionsRemover.add('default', connector.id, 'action', 'actions'); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts index 6faea0e1789bb..bc27dd17a21b6 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts @@ -64,7 +64,7 @@ export default ({ getService }: FtrProviderContext): void => { .send(getResilientConnector()) .expect(200); - const sir = await createConnector(supertest, getServiceNowSIRConnector()); + const sir = await createConnector({ supertest, req: getServiceNowSIRConnector() }); actionsRemover.add('default', sir.id, 'action', 'actions'); actionsRemover.add('default', snConnector.id, 'action', 'actions'); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/patch_configure.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/patch_configure.ts index 9e82ce1f0c233..789b68b19beb6 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/patch_configure.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/patch_configure.ts @@ -47,9 +47,12 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should patch a configuration connector and create mappings', async () => { - const connector = await createConnector(supertest, { - ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, + const connector = await createConnector({ + supertest, + req: { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }, }); actionsRemover.add('default', connector.id, 'action', 'actions'); @@ -100,9 +103,12 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should mappings when updating the connector', async () => { - const connector = await createConnector(supertest, { - ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, + const connector = await createConnector({ + supertest, + req: { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }, }); actionsRemover.add('default', connector.id, 'action', 'actions'); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/post_configure.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/post_configure.ts index 503e0384859ec..96ffcf4bc3f5c 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/post_configure.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/post_configure.ts @@ -46,9 +46,12 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should create a configuration with mapping', async () => { - const connector = await createConnector(supertest, { - ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, + const connector = await createConnector({ + supertest, + req: { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }, }); actionsRemover.add('default', connector.id, 'action', 'actions'); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts index 5ba09dd56bd67..26bc6a072450d 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts @@ -22,11 +22,15 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => { await deleteSpacesAndUsers(getService); }); - // Common - loadTestFile(require.resolve('../common')); - // Trial loadTestFile(require.resolve('./cases/push_case')); + loadTestFile(require.resolve('./cases/user_actions/get_all_user_actions')); loadTestFile(require.resolve('./configure/index')); + + // Common + loadTestFile(require.resolve('../common')); + + // NOTE: These need to be at the end because they could delete the .kibana index and inadvertently remove the users and spaces + loadTestFile(require.resolve('../common/migrations')); }); };