diff --git a/x-pack/plugins/cases/common/api/cases/case.ts b/x-pack/plugins/cases/common/api/cases/case.ts index a8b0717104304..389caffee1a5c 100644 --- a/x-pack/plugins/cases/common/api/cases/case.ts +++ b/x-pack/plugins/cases/common/api/cases/case.ts @@ -176,6 +176,12 @@ export const ExternalServiceResponseRt = rt.intersection([ }), ]); +export const AllTagsFindRequestRt = rt.partial({ + owner: rt.union([rt.array(rt.string), rt.string]), +}); + +export const AllReportersFindRequestRt = AllTagsFindRequestRt; + export type CaseAttributes = rt.TypeOf; /** * This field differs from the CasePostRequest in that the post request's type field can be optional. This type requires @@ -198,3 +204,6 @@ export type ESCaseAttributes = Omit & { connector: export type ESCasePatchRequest = Omit & { connector?: ESCaseConnector; }; + +export type AllTagsFindRequest = rt.TypeOf; +export type AllReportersFindRequest = AllTagsFindRequest; diff --git a/x-pack/plugins/cases/common/api/cases/configure.ts b/x-pack/plugins/cases/common/api/cases/configure.ts index b5a89efde1767..02e2cb6596230 100644 --- a/x-pack/plugins/cases/common/api/cases/configure.ts +++ b/x-pack/plugins/cases/common/api/cases/configure.ts @@ -9,6 +9,7 @@ import * as rt from 'io-ts'; import { UserRT } from '../user'; import { CaseConnectorRt, ConnectorMappingsRt, ESCaseConnector } from '../connectors'; +import { OmitProp } from '../runtime_types'; // 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')]); @@ -16,11 +17,14 @@ const ClosureTypeRT = rt.union([rt.literal('close-by-user'), rt.literal('close-b const CasesConfigureBasicRt = rt.type({ connector: CaseConnectorRt, closure_type: ClosureTypeRT, + owner: rt.string, }); +const CasesConfigureBasicWithoutOwnerRt = rt.type(OmitProp(CasesConfigureBasicRt.props, 'owner')); + export const CasesConfigureRequestRt = CasesConfigureBasicRt; export const CasesConfigurePatchRt = rt.intersection([ - rt.partial(CasesConfigureBasicRt.props), + rt.partial(CasesConfigureBasicWithoutOwnerRt.props), rt.type({ version: rt.string }), ]); @@ -38,18 +42,33 @@ export const CaseConfigureResponseRt = rt.intersection([ CaseConfigureAttributesRt, ConnectorMappingsRt, rt.type({ + id: rt.string, version: rt.string, error: rt.union([rt.string, rt.null]), + owner: rt.string, }), ]); +export const GetConfigureFindRequestRt = rt.partial({ + owner: rt.union([rt.array(rt.string), rt.string]), +}); + +export const CaseConfigureRequestParamsRt = rt.type({ + configuration_id: rt.string, +}); + +export const CaseConfigurationsResponseRt = rt.array(CaseConfigureResponseRt); + export type ClosureType = rt.TypeOf; export type CasesConfigure = rt.TypeOf; export type CasesConfigureRequest = rt.TypeOf; export type CasesConfigurePatch = rt.TypeOf; export type CasesConfigureAttributes = rt.TypeOf; export type CasesConfigureResponse = rt.TypeOf; +export type CasesConfigurationsResponse = rt.TypeOf; export type ESCasesConfigureAttributes = Omit & { connector: ESCaseConnector; }; + +export type GetConfigureFindRequest = rt.TypeOf; diff --git a/x-pack/plugins/cases/common/api/connectors/mappings.ts b/x-pack/plugins/cases/common/api/connectors/mappings.ts index 3d2013af47688..e0fdd2d7e62dc 100644 --- a/x-pack/plugins/cases/common/api/connectors/mappings.ts +++ b/x-pack/plugins/cases/common/api/connectors/mappings.ts @@ -31,6 +31,7 @@ export const ConnectorMappingsAttributesRT = rt.type({ export const ConnectorMappingsRt = rt.type({ mappings: rt.array(ConnectorMappingsAttributesRT), + owner: rt.string, }); export type ConnectorMappingsAttributes = rt.TypeOf; diff --git a/x-pack/plugins/cases/common/api/runtime_types.ts b/x-pack/plugins/cases/common/api/runtime_types.ts index 9785c0f410744..c3202cca6718f 100644 --- a/x-pack/plugins/cases/common/api/runtime_types.ts +++ b/x-pack/plugins/cases/common/api/runtime_types.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { omit } from 'lodash'; import { either, fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; @@ -13,6 +14,9 @@ import { isObject } from 'lodash/fp'; type ErrorFactory = (message: string) => Error; +export const OmitProp = (o: O, k: K): Omit => + omit(o, k); + export const formatErrors = (errors: rt.Errors): string[] => { const err = errors.map((error) => { if (error.message != null) { diff --git a/x-pack/plugins/cases/common/constants.ts b/x-pack/plugins/cases/common/constants.ts index ed759a6c64168..9eb100edeee46 100644 --- a/x-pack/plugins/cases/common/constants.ts +++ b/x-pack/plugins/cases/common/constants.ts @@ -32,6 +32,7 @@ export const SAVED_OBJECT_TYPES = [ export const CASES_URL = '/api/cases'; export const CASE_DETAILS_URL = `${CASES_URL}/{case_id}`; export const CASE_CONFIGURE_URL = `${CASES_URL}/configure`; +export const CASE_CONFIGURE_DETAILS_URL = `${CASES_URL}/configure/{configuration_id}`; export const CASE_CONFIGURE_CONNECTORS_URL = `${CASE_CONFIGURE_URL}/connectors`; export const SUB_CASES_PATCH_DEL_URL = `${CASES_URL}/sub_cases`; diff --git a/x-pack/plugins/cases/server/authorization/index.ts b/x-pack/plugins/cases/server/authorization/index.ts index 3203398ff51a5..9f30e8cf7a8da 100644 --- a/x-pack/plugins/cases/server/authorization/index.ts +++ b/x-pack/plugins/cases/server/authorization/index.ts @@ -6,7 +6,7 @@ */ import { EventType } from '../../../security/server'; -import { CASE_SAVED_OBJECT } from '../../common/constants'; +import { CASE_CONFIGURE_SAVED_OBJECT, CASE_SAVED_OBJECT } from '../../common/constants'; import { Verbs, ReadOperations, WriteOperations, OperationDetails } from './types'; export * from './authorization'; @@ -66,6 +66,22 @@ export const Operations: Record Promise; // TODO: we need to have an operation per entity route so I think we need to create a bunch like // getCase, getComment, getSubCase etc for each, need to think of a clever way of creating them for all the routes easily? + +// if you add a value here you'll likely also need to make changes here: +// x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts export enum ReadOperations { GetCase = 'getCase', FindCases = 'findCases', + GetTags = 'getTags', + GetReporters = 'getReporters', + FindConfigurations = 'findConfigurations', } // TODO: comments @@ -33,6 +39,8 @@ export enum WriteOperations { CreateCase = 'createCase', DeleteCase = 'deleteCase', UpdateCase = 'updateCase', + CreateConfiguration = 'createConfiguration', + UpdateConfiguration = 'updateConfiguration', } /** diff --git a/x-pack/plugins/cases/server/client/alerts/client.ts b/x-pack/plugins/cases/server/client/alerts/client.ts index dfa06c0277bda..19dc95982613f 100644 --- a/x-pack/plugins/cases/server/client/alerts/client.ts +++ b/x-pack/plugins/cases/server/client/alerts/client.ts @@ -34,24 +34,10 @@ export interface AlertSubClient { updateStatus(args: AlertUpdateStatus): Promise; } -export const createAlertsSubClient = (args: CasesClientArgs): AlertSubClient => { - const { alertsService, scopedClusterClient, logger } = args; - +export const createAlertsSubClient = (clientArgs: CasesClientArgs): AlertSubClient => { const alertsSubClient: AlertSubClient = { - get: (params: AlertGet) => - get({ - ...params, - alertsService, - scopedClusterClient, - logger, - }), - updateStatus: (params: AlertUpdateStatus) => - updateStatus({ - ...params, - alertsService, - scopedClusterClient, - logger, - }), + get: (params: AlertGet) => get(params, clientArgs), + updateStatus: (params: AlertUpdateStatus) => updateStatus(params, clientArgs), }; return Object.freeze(alertsSubClient); diff --git a/x-pack/plugins/cases/server/client/alerts/get.ts b/x-pack/plugins/cases/server/client/alerts/get.ts index 88298450e499a..186f914aa2cd7 100644 --- a/x-pack/plugins/cases/server/client/alerts/get.ts +++ b/x-pack/plugins/cases/server/client/alerts/get.ts @@ -5,24 +5,19 @@ * 2.0. */ -import { ElasticsearchClient, Logger } from 'kibana/server'; import { AlertInfo } from '../../common'; -import { AlertServiceContract } from '../../services'; import { CasesClientGetAlertsResponse } from './types'; +import { CasesClientArgs } from '..'; interface GetParams { - alertsService: AlertServiceContract; alertsInfo: AlertInfo[]; - scopedClusterClient: ElasticsearchClient; - logger: Logger; } -export const get = async ({ - alertsService, - alertsInfo, - scopedClusterClient, - logger, -}: GetParams): Promise => { +export const get = async ( + { alertsInfo }: GetParams, + clientArgs: CasesClientArgs +): Promise => { + const { alertsService, scopedClusterClient, logger } = clientArgs; if (alertsInfo.length === 0) { return []; } diff --git a/x-pack/plugins/cases/server/client/alerts/update_status.ts b/x-pack/plugins/cases/server/client/alerts/update_status.ts index e02a98c396e0a..3c7f60ecae15d 100644 --- a/x-pack/plugins/cases/server/client/alerts/update_status.ts +++ b/x-pack/plugins/cases/server/client/alerts/update_status.ts @@ -5,22 +5,17 @@ * 2.0. */ -import { ElasticsearchClient, Logger } from 'src/core/server'; -import { AlertServiceContract } from '../../services'; import { UpdateAlertRequest } from './client'; +import { CasesClientArgs } from '..'; interface UpdateAlertsStatusArgs { - alertsService: AlertServiceContract; alerts: UpdateAlertRequest[]; - scopedClusterClient: ElasticsearchClient; - logger: Logger; } -export const updateStatus = async ({ - alertsService, - alerts, - scopedClusterClient, - logger, -}: UpdateAlertsStatusArgs): Promise => { +export const updateStatus = async ( + { alerts }: UpdateAlertsStatusArgs, + clientArgs: CasesClientArgs +): Promise => { + const { alertsService, scopedClusterClient, logger } = clientArgs; await alertsService.updateAlertsStatus({ alerts, scopedClusterClient, logger }); }; diff --git a/x-pack/plugins/cases/server/client/attachments/add.ts b/x-pack/plugins/cases/server/client/attachments/add.ts index e77115ba4e228..cb0d7ef5a1e14 100644 --- a/x-pack/plugins/cases/server/client/attachments/add.ts +++ b/x-pack/plugins/cases/server/client/attachments/add.ts @@ -286,15 +286,13 @@ async function getCombinedCase({ interface AddCommentArgs { caseId: string; comment: CommentRequest; - casesClientInternal: CasesClientInternal; } -export const addComment = async ({ - caseId, - comment, - casesClientInternal, - ...rest -}: AddCommentArgs & CasesClientArgs): Promise => { +export const addComment = async ( + { caseId, comment }: AddCommentArgs, + clientArgs: CasesClientArgs, + casesClientInternal: CasesClientInternal +): Promise => { const query = pipe( CommentRequestRt.decode(comment), fold(throwErrors(Boom.badRequest), identity) @@ -307,7 +305,7 @@ export const addComment = async ({ attachmentService, user, logger, - } = rest; + } = clientArgs; if (isCommentRequestTypeGenAlert(comment)) { if (!ENABLE_CASE_CONNECTOR) { diff --git a/x-pack/plugins/cases/server/client/attachments/client.ts b/x-pack/plugins/cases/server/client/attachments/client.ts index 27fb5e1cf61f0..7ffbb8684f959 100644 --- a/x-pack/plugins/cases/server/client/attachments/client.ts +++ b/x-pack/plugins/cases/server/client/attachments/client.ts @@ -26,7 +26,7 @@ interface AttachmentsAdd { } export interface AttachmentsSubClient { - add(args: AttachmentsAdd): Promise; + add(params: AttachmentsAdd): Promise; deleteAll(deleteAllArgs: DeleteAllArgs): Promise; delete(deleteArgs: DeleteArgs): Promise; find(findArgs: FindArgs): Promise; @@ -36,23 +36,17 @@ export interface AttachmentsSubClient { } export const createAttachmentsSubClient = ( - args: CasesClientArgs, + clientArgs: CasesClientArgs, casesClientInternal: CasesClientInternal ): AttachmentsSubClient => { const attachmentSubClient: AttachmentsSubClient = { - add: ({ caseId, comment }: AttachmentsAdd) => - addComment({ - ...args, - casesClientInternal, - caseId, - comment, - }), - deleteAll: (deleteAllArgs: DeleteAllArgs) => deleteAll(deleteAllArgs, args), - delete: (deleteArgs: DeleteArgs) => deleteComment(deleteArgs, args), - find: (findArgs: FindArgs) => find(findArgs, args), - getAll: (getAllArgs: GetAllArgs) => getAll(getAllArgs, args), - get: (getArgs: GetArgs) => get(getArgs, args), - update: (updateArgs: UpdateArgs) => update(updateArgs, args), + add: (params: AttachmentsAdd) => addComment(params, clientArgs, casesClientInternal), + deleteAll: (deleteAllArgs: DeleteAllArgs) => deleteAll(deleteAllArgs, clientArgs), + delete: (deleteArgs: DeleteArgs) => deleteComment(deleteArgs, clientArgs), + find: (findArgs: FindArgs) => find(findArgs, clientArgs), + getAll: (getAllArgs: GetAllArgs) => getAll(getAllArgs, clientArgs), + get: (getArgs: GetArgs) => get(getArgs, clientArgs), + update: (updateArgs: UpdateArgs) => update(updateArgs, clientArgs), }; return Object.freeze(attachmentSubClient); diff --git a/x-pack/plugins/cases/server/client/cases/client.ts b/x-pack/plugins/cases/server/client/cases/client.ts index 423863528184a..fd2f148d304ab 100644 --- a/x-pack/plugins/cases/server/client/cases/client.ts +++ b/x-pack/plugins/cases/server/client/cases/client.ts @@ -14,6 +14,8 @@ import { CasesFindRequest, CasesFindResponse, User, + AllTagsFindRequest, + AllReportersFindRequest, } from '../../../common/api'; import { CasesClient } from '../client'; import { CasesClientInternal } from '../client_internal'; @@ -41,91 +43,33 @@ interface CasePush { * The public API for interacting with cases. */ export interface CasesSubClient { - create(theCase: CasePostRequest): Promise; - find(args: CasesFindRequest): Promise; - get(args: CaseGet): Promise; + create(data: CasePostRequest): Promise; + find(params: CasesFindRequest): Promise; + get(params: CaseGet): Promise; push(args: CasePush): Promise; - update(args: CasesPatchRequest): Promise; + update(cases: CasesPatchRequest): Promise; delete(ids: string[]): Promise; - getTags(): Promise; - getReporters(): Promise; + getTags(params: AllTagsFindRequest): Promise; + getReporters(params: AllReportersFindRequest): Promise; } /** * Creates the interface for CRUD on cases objects. */ export const createCasesSubClient = ( - args: CasesClientArgs, + clientArgs: CasesClientArgs, casesClient: CasesClient, casesClientInternal: CasesClientInternal ): CasesSubClient => { - const { - attachmentService, - caseConfigureService, - caseService, - user, - savedObjectsClient, - userActionService, - logger, - authorization, - auditLogger, - } = args; - const casesSubClient: CasesSubClient = { - create: (theCase: CasePostRequest) => - create({ - savedObjectsClient, - caseService, - caseConfigureService, - userActionService, - user, - theCase, - logger, - auth: authorization, - auditLogger, - }), - find: (options: CasesFindRequest) => - find({ - savedObjectsClient, - caseService, - logger, - auth: authorization, - options, - auditLogger, - }), - get: (params: CaseGet) => - get({ - ...params, - caseService, - savedObjectsClient, - logger, - }), - push: (params: CasePush) => - push({ - ...params, - attachmentService, - savedObjectsClient, - caseService, - userActionService, - user, - casesClient, - casesClientInternal, - caseConfigureService, - logger, - }), - update: (cases: CasesPatchRequest) => - update({ - savedObjectsClient, - caseService, - userActionService, - user, - cases, - casesClientInternal, - logger, - }), - delete: (ids: string[]) => deleteCases(ids, args), - getTags: () => getTags(args), - getReporters: () => getReporters(args), + create: (data: CasePostRequest) => create(data, clientArgs), + find: (params: CasesFindRequest) => find(params, clientArgs), + get: (params: CaseGet) => get(params, clientArgs), + push: (params: CasePush) => push(params, clientArgs, casesClient, casesClientInternal), + update: (cases: CasesPatchRequest) => update(cases, clientArgs, casesClientInternal), + delete: (ids: string[]) => deleteCases(ids, clientArgs), + getTags: (params: AllTagsFindRequest) => getTags(params, clientArgs), + getReporters: (params: AllReportersFindRequest) => getReporters(params, clientArgs), }; return Object.freeze(casesSubClient); diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index 2109424575ed3..15fbd34628182 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -9,13 +9,8 @@ import Boom from '@hapi/boom'; import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import type { PublicMethodsOf } from '@kbn/utility-types'; -import { - SavedObjectsClientContract, - Logger, - SavedObjectsUtils, -} from '../../../../../../src/core/server'; +import { SavedObjectsUtils } from '../../../../../../src/core/server'; import { throwErrors, @@ -25,51 +20,41 @@ import { CasesClientPostRequestRt, CasePostRequest, CaseType, - User, } from '../../../common/api'; import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; import { createAuditMsg, ensureAuthorized, getConnectorFromConfiguration } from '../utils'; -import { CaseConfigureService, CaseService, CaseUserActionService } from '../../services'; import { createCaseError } from '../../common/error'; -import { Authorization } from '../../authorization/authorization'; import { Operations } from '../../authorization'; -import { AuditLogger, EventOutcome } from '../../../../security/server'; +import { EventOutcome } from '../../../../security/server'; import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; import { flattenCaseSavedObject, transformCaseConnectorToEsConnector, transformNewCase, } from '../../common'; - -interface CreateCaseArgs { - caseConfigureService: CaseConfigureService; - caseService: CaseService; - user: User; - savedObjectsClient: SavedObjectsClientContract; - userActionService: CaseUserActionService; - theCase: CasePostRequest; - logger: Logger; - auth: PublicMethodsOf; - auditLogger?: AuditLogger; -} +import { CasesClientArgs } from '..'; /** * Creates a new case. */ -export const create = async ({ - savedObjectsClient, - caseService, - caseConfigureService, - userActionService, - user, - theCase, - logger, - auth, - auditLogger, -}: CreateCaseArgs): Promise => { +export const create = async ( + data: CasePostRequest, + clientArgs: CasesClientArgs +): Promise => { + const { + savedObjectsClient, + caseService, + caseConfigureService, + userActionService, + user, + logger, + authorization: auth, + auditLogger, + } = clientArgs; + // default to an individual case if the type is not defined. - const { type = CaseType.individual, ...nonTypeCaseFields } = theCase; + const { type = CaseType.individual, ...nonTypeCaseFields } = data; if (!ENABLE_CASE_CONNECTOR && type === CaseType.collection) { throw Boom.badRequest( diff --git a/x-pack/plugins/cases/server/client/cases/delete.ts b/x-pack/plugins/cases/server/client/cases/delete.ts index 1bc94b5a0b4c8..4657df2e71b30 100644 --- a/x-pack/plugins/cases/server/client/cases/delete.ts +++ b/x-pack/plugins/cases/server/client/cases/delete.ts @@ -5,12 +5,16 @@ * 2.0. */ +import { Boom } from '@hapi/boom'; import { SavedObjectsClientContract } from 'kibana/server'; import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; import { CasesClientArgs } from '..'; import { createCaseError } from '../../common/error'; import { AttachmentService, CaseService } from '../../services'; import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; +import { Operations } from '../../authorization'; +import { createAuditMsg, ensureAuthorized } from '../utils'; +import { EventOutcome } from '../../../../security/server'; async function deleteSubCases({ attachmentService, @@ -54,8 +58,47 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P user, userActionService, logger, + authorization: auth, + auditLogger, } = clientArgs; try { + const cases = await caseService.getCases({ soClient, caseIds: ids }); + const soIds = new Set(); + const owners = new Set(); + + for (const theCase of cases.saved_objects) { + // bulkGet can return an error. + if (theCase.error != null) { + throw createCaseError({ + message: `Failed to delete cases ids: ${JSON.stringify(ids)}: ${theCase.error.error}`, + error: new Boom(theCase.error.message, { statusCode: theCase.error.statusCode }), + logger, + }); + } + + soIds.add(theCase.id); + owners.add(theCase.attributes.owner); + } + + await ensureAuthorized({ + operation: Operations.deleteCase, + owners: [...owners.values()], + authorization: auth, + auditLogger, + savedObjectIDs: [...soIds.values()], + }); + + // log that we're attempting to delete a case + for (const savedObjectID of soIds) { + auditLogger?.log( + createAuditMsg({ + operation: Operations.deleteCase, + outcome: EventOutcome.UNKNOWN, + savedObjectID, + }) + ); + } + await Promise.all( ids.map((id) => caseService.deleteCase({ @@ -64,6 +107,7 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P }) ) ); + const comments = await Promise.all( ids.map((id) => caseService.getAllCaseComments({ @@ -103,16 +147,19 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P soClient, actions: ids.map((id) => buildCaseUserActionItem({ - action: 'create', + action: 'delete', actionAt: deleteDate, actionBy: user, caseId: id, fields: [ - 'comment', 'description', 'status', 'tags', 'title', + 'connector', + 'settings', + 'owner', + 'comment', ...(ENABLE_CASE_CONNECTOR ? ['sub_case'] : []), ], }) diff --git a/x-pack/plugins/cases/server/client/cases/find.ts b/x-pack/plugins/cases/server/client/cases/find.ts index 8334beb102cb9..988812da0d852 100644 --- a/x-pack/plugins/cases/server/client/cases/find.ts +++ b/x-pack/plugins/cases/server/client/cases/find.ts @@ -6,12 +6,10 @@ */ import Boom from '@hapi/boom'; -import { SavedObjectsClientContract, Logger } from 'kibana/server'; import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import type { PublicMethodsOf } from '@kbn/utility-types'; import { CasesFindResponse, CasesFindRequest, @@ -22,38 +20,25 @@ import { excess, } from '../../../common/api'; -import { CaseService } from '../../services'; import { createCaseError } from '../../common/error'; import { constructQueryOptions, getAuthorizationFilter } from '../utils'; -import { Authorization } from '../../authorization/authorization'; import { includeFieldsRequiredForAuthentication } from '../../authorization/utils'; import { Operations } from '../../authorization'; -import { AuditLogger } from '../../../../security/server'; import { transformCases } from '../../common'; - -interface FindParams { - savedObjectsClient: SavedObjectsClientContract; - caseService: CaseService; - logger: Logger; - auth: PublicMethodsOf; - options: CasesFindRequest; - auditLogger?: AuditLogger; -} +import { CasesClientArgs } from '..'; /** * Retrieves a case and optionally its comments and sub case comments. */ -export const find = async ({ - savedObjectsClient, - caseService, - logger, - auth, - options, - auditLogger, -}: FindParams): Promise => { +export const find = async ( + params: CasesFindRequest, + clientArgs: CasesClientArgs +): Promise => { + const { savedObjectsClient, caseService, authorization: auth, auditLogger, logger } = clientArgs; + try { const queryParams = pipe( - excess(CasesFindRequestRt).decode(options), + excess(CasesFindRequestRt).decode(params), fold(throwErrors(Boom.badRequest), identity) ); @@ -124,7 +109,7 @@ export const find = async ({ ); } catch (error) { throw createCaseError({ - message: `Failed to find cases: ${JSON.stringify(options)}: ${error}`, + message: `Failed to find cases: ${JSON.stringify(params)}: ${error}`, error, logger, }); diff --git a/x-pack/plugins/cases/server/client/cases/get.ts b/x-pack/plugins/cases/server/client/cases/get.ts index 58fff0d5e435d..73ca65d52e566 100644 --- a/x-pack/plugins/cases/server/client/cases/get.ts +++ b/x-pack/plugins/cases/server/client/cases/get.ts @@ -5,35 +5,50 @@ * 2.0. */ import Boom from '@hapi/boom'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; -import { SavedObjectsClientContract, Logger, SavedObject } from 'kibana/server'; -import { CaseResponseRt, CaseResponse, ESCaseAttributes, User, UsersRt } from '../../../common/api'; -import { CaseService } from '../../services'; +import { SavedObject } from 'kibana/server'; +import { + CaseResponseRt, + CaseResponse, + ESCaseAttributes, + User, + UsersRt, + AllTagsFindRequest, + AllTagsFindRequestRt, + excess, + throwErrors, + AllReportersFindRequestRt, + AllReportersFindRequest, +} from '../../../common/api'; import { countAlertsForID, flattenCaseSavedObject } from '../../common'; import { createCaseError } from '../../common/error'; import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; import { CasesClientArgs } from '..'; +import { Operations } from '../../authorization'; +import { + combineAuthorizedAndOwnerFilter, + ensureAuthorized, + getAuthorizationFilter, +} from '../utils'; interface GetParams { - savedObjectsClient: SavedObjectsClientContract; - caseService: CaseService; id: string; includeComments?: boolean; includeSubCaseComments?: boolean; - logger: Logger; } /** * Retrieves a case and optionally its comments and sub case comments. */ -export const get = async ({ - savedObjectsClient, - caseService, - id, - logger, - includeComments = false, - includeSubCaseComments = false, -}: GetParams): Promise => { +export const get = async ( + { id, includeComments, includeSubCaseComments }: GetParams, + clientArgs: CasesClientArgs +): Promise => { + const { savedObjectsClient, caseService, logger, authorization: auth, auditLogger } = clientArgs; + try { if (!ENABLE_CASE_CONNECTOR && includeSubCaseComments) { throw Boom.badRequest( @@ -62,6 +77,14 @@ export const get = async ({ }); } + await ensureAuthorized({ + operation: Operations.getCase, + owners: [theCase.attributes.owner], + authorization: auth, + auditLogger, + savedObjectIDs: [theCase.id], + }); + if (!includeComments) { return CaseResponseRt.encode( flattenCaseSavedObject({ @@ -70,6 +93,7 @@ export const get = async ({ }) ); } + const theComments = await caseService.getAllCaseComments({ soClient: savedObjectsClient, id, @@ -97,15 +121,61 @@ export const get = async ({ /** * Retrieves the tags from all the cases. */ -export async function getTags({ - savedObjectsClient: soClient, - caseService, - logger, -}: CasesClientArgs): Promise { + +export async function getTags( + params: AllTagsFindRequest, + clientArgs: CasesClientArgs +): Promise { + const { + savedObjectsClient: soClient, + caseService, + logger, + authorization: auth, + auditLogger, + } = clientArgs; + try { - return await caseService.getTags({ + const queryParams = pipe( + excess(AllTagsFindRequestRt).decode(params), + fold(throwErrors(Boom.badRequest), identity) + ); + + const { + filter: authorizationFilter, + ensureSavedObjectsAreAuthorized, + logSuccessfulAuthorization, + } = await getAuthorizationFilter({ + authorization: auth, + operation: Operations.findCases, + auditLogger, + }); + + const filter = combineAuthorizedAndOwnerFilter(queryParams.owner, authorizationFilter); + + const cases = await caseService.getTags({ soClient, + filter, + }); + + const tags = new Set(); + const mappedCases: Array<{ + owner: string; + id: string; + }> = []; + + // Gather all necessary information in one pass + cases.saved_objects.forEach((theCase) => { + theCase.attributes.tags.forEach((tag) => tags.add(tag)); + mappedCases.push({ + id: theCase.id, + owner: theCase.attributes.owner, + }); }); + + ensureSavedObjectsAreAuthorized(mappedCases); + logSuccessfulAuthorization(); + + return [...tags.values()]; } catch (error) { throw createCaseError({ message: `Failed to get tags: ${error}`, error, logger }); } @@ -114,16 +184,64 @@ export async function getTags({ /** * Retrieves the reporters from all the cases. */ -export async function getReporters({ - savedObjectsClient: soClient, - caseService, - logger, -}: CasesClientArgs): Promise { +export async function getReporters( + params: AllReportersFindRequest, + clientArgs: CasesClientArgs +): Promise { + const { + savedObjectsClient: soClient, + caseService, + logger, + authorization: auth, + auditLogger, + } = clientArgs; + try { - const reporters = await caseService.getReporters({ + const queryParams = pipe( + excess(AllReportersFindRequestRt).decode(params), + fold(throwErrors(Boom.badRequest), identity) + ); + + const { + filter: authorizationFilter, + ensureSavedObjectsAreAuthorized, + logSuccessfulAuthorization, + } = await getAuthorizationFilter({ + authorization: auth, + operation: Operations.getReporters, + auditLogger, + }); + + const filter = combineAuthorizedAndOwnerFilter(queryParams.owner, authorizationFilter); + + const cases = await caseService.getReporters({ soClient, + filter, }); - return UsersRt.encode(reporters); + + const reporters = new Map(); + const mappedCases: Array<{ + owner: string; + id: string; + }> = []; + + // Gather all necessary information in one pass + cases.saved_objects.forEach((theCase) => { + const user = theCase.attributes.created_by; + if (user.username != null) { + reporters.set(user.username, user); + } + + mappedCases.push({ + id: theCase.id, + owner: theCase.attributes.owner, + }); + }); + + ensureSavedObjectsAreAuthorized(mappedCases); + logSuccessfulAuthorization(); + + return UsersRt.encode([...reporters.values()]); } catch (error) { throw createCaseError({ message: `Failed to get reporters: ${error}`, error, logger }); } diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index ae690c8b6a086..b7f416203e078 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -8,13 +8,11 @@ import Boom from '@hapi/boom'; import { SavedObjectsBulkUpdateResponse, - SavedObjectsClientContract, SavedObjectsUpdateResponse, - Logger, SavedObjectsFindResponse, SavedObject, } from 'kibana/server'; -import { ActionResult, ActionsClient } from '../../../../actions/server'; +import { ActionResult } from '../../../../actions/server'; import { ActionConnector, @@ -25,22 +23,15 @@ import { ESCaseAttributes, CommentAttributes, CaseUserActionsResponse, - User, ESCasesConfigureAttributes, CaseType, } from '../../../common/api'; import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; import { createIncident, getCommentContextFromAttributes } from './utils'; -import { - CaseConfigureService, - CaseService, - CaseUserActionService, - AttachmentService, -} from '../../services'; import { createCaseError, flattenCaseSavedObject, getAlertInfoFromComments } from '../../common'; import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; -import { CasesClient, CasesClientInternal } from '..'; +import { CasesClient, CasesClientArgs, CasesClientInternal } from '..'; /** * Returns true if the case should be closed based on the configuration settings and whether the case @@ -59,34 +50,27 @@ function shouldCloseByPush( } interface PushParams { - savedObjectsClient: SavedObjectsClientContract; - caseService: CaseService; - caseConfigureService: CaseConfigureService; - userActionService: CaseUserActionService; - attachmentService: AttachmentService; - user: User; caseId: string; connectorId: string; - casesClient: CasesClient; - casesClientInternal: CasesClientInternal; - actionsClient: ActionsClient; - logger: Logger; } -export const push = async ({ - savedObjectsClient, - attachmentService, - caseService, - caseConfigureService, - userActionService, - casesClient, - casesClientInternal, - actionsClient, - connectorId, - caseId, - user, - logger, -}: PushParams): Promise => { +export const push = async ( + { connectorId, caseId }: PushParams, + clientArgs: CasesClientArgs, + casesClient: CasesClient, + casesClientInternal: CasesClientInternal +): Promise => { + const { + savedObjectsClient, + attachmentService, + caseService, + caseConfigureService, + userActionService, + actionsClient, + user, + logger, + } = clientArgs; + /* Start of push to external service */ let theCase: CaseResponse; let connector: ActionResult; @@ -136,6 +120,10 @@ export const push = async ({ connectorId: connector.id, connectorType: connector.actionTypeId, }); + + 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 }); @@ -147,7 +135,7 @@ export const push = async ({ theCase, userActions, connector: connector as ActionConnector, - mappings: connectorMappings, + mappings: connectorMappings[0].attributes.mappings, alerts, }); } catch (e) { diff --git a/x-pack/plugins/cases/server/client/cases/update.ts b/x-pack/plugins/cases/server/client/cases/update.ts index dcd66ebbcae26..402e6726a71cd 100644 --- a/x-pack/plugins/cases/server/client/cases/update.ts +++ b/x-pack/plugins/cases/server/client/cases/update.ts @@ -15,7 +15,6 @@ import { SavedObjectsClientContract, SavedObjectsFindResponse, SavedObjectsFindResult, - Logger, } from 'kibana/server'; import { nodeBuilder } from '../../../../../../src/plugins/data/common'; @@ -35,12 +34,11 @@ import { CasesPatchRequest, AssociationType, CommentAttributes, - User, } from '../../../common/api'; import { buildCaseUserActions } from '../../services/user_actions/helpers'; import { getCaseToUpdate } from '../utils'; -import { CaseService, CaseUserActionService } from '../../services'; +import { CaseService } from '../../services'; import { CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT, @@ -56,6 +54,7 @@ import { createCaseError } from '../../common/error'; import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; import { UpdateAlertRequest } from '../alerts/client'; import { CasesClientInternal } from '../client_internal'; +import { CasesClientArgs } from '..'; /** * Throws an error if any of the requests attempt to update a collection style cases' status field. @@ -338,25 +337,12 @@ async function updateAlerts({ await casesClientInternal.alerts.updateStatus({ alerts: alertsToUpdate }); } -interface UpdateArgs { - savedObjectsClient: SavedObjectsClientContract; - caseService: CaseService; - userActionService: CaseUserActionService; - user: User; - casesClientInternal: CasesClientInternal; - cases: CasesPatchRequest; - logger: Logger; -} - -export const update = async ({ - savedObjectsClient, - caseService, - userActionService, - user, - casesClientInternal, - cases, - logger, -}: UpdateArgs): Promise => { +export const update = async ( + cases: CasesPatchRequest, + clientArgs: CasesClientArgs, + casesClientInternal: CasesClientInternal +): Promise => { + const { savedObjectsClient, caseService, userActionService, user, logger } = clientArgs; const query = pipe( excess(CasesPatchRequestRt).decode(cases), fold(throwErrors(Boom.badRequest), identity) diff --git a/x-pack/plugins/cases/server/client/configure/client.ts b/x-pack/plugins/cases/server/client/configure/client.ts index 2b9048a4518e9..1037a2ff9d893 100644 --- a/x-pack/plugins/cases/server/client/configure/client.ts +++ b/x-pack/plugins/cases/server/client/configure/client.ts @@ -5,7 +5,11 @@ * 2.0. */ import Boom from '@hapi/boom'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; +import { SavedObjectsFindResponse, SavedObjectsUtils } from '../../../../../../src/core/server'; import { SUPPORTED_CONNECTORS } from '../../../common/constants'; import { CaseConfigureResponseRt, @@ -13,13 +17,22 @@ import { CasesConfigureRequest, CasesConfigureResponse, ConnectorMappingsAttributes, + excess, + GetConfigureFindRequest, + GetConfigureFindRequestRt, GetFieldsResponse, + throwErrors, + CasesConfigurationsResponse, + CaseConfigurationsResponseRt, + CasesConfigurePatchRt, + ConnectorMappings, } from '../../../common/api'; import { createCaseError } from '../../common/error'; import { transformCaseConnectorToEsConnector, transformESConnectorToCaseConnector, } from '../../common'; +import { EventOutcome } from '../../../../security/server'; import { CasesClientInternal } from '../client_internal'; import { CasesClientArgs } from '../types'; import { getFields } from './get_fields'; @@ -28,32 +41,44 @@ import { getMappings } from './get_mappings'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { FindActionResult } from '../../../../actions/server/types'; import { ActionType } from '../../../../actions/common'; - -interface ConfigurationGetFields { - connectorId: string; - connectorType: string; -} - -interface ConfigurationGetMappings { - connectorId: string; - connectorType: string; -} +import { Operations } from '../../authorization'; +import { + combineAuthorizedAndOwnerFilter, + createAuditMsg, + ensureAuthorized, + getAuthorizationFilter, +} from '../utils'; +import { + ConfigurationGetFields, + MappingsArgs, + CreateMappingsArgs, + UpdateMappingsArgs, +} from './types'; +import { createMappings } from './create_mappings'; +import { updateMappings } from './update_mappings'; /** * Defines the internal helper functions. */ export interface InternalConfigureSubClient { - getFields(args: ConfigurationGetFields): Promise; - getMappings(args: ConfigurationGetMappings): Promise; + getFields(params: ConfigurationGetFields): Promise; + getMappings( + params: MappingsArgs + ): Promise['saved_objects']>; + createMappings(params: CreateMappingsArgs): Promise; + updateMappings(params: UpdateMappingsArgs): Promise; } /** * This is the public API for interacting with the connector configuration for cases. */ export interface ConfigureSubClient { - get(): Promise; + get(params: GetConfigureFindRequest): Promise; getConnectors(): Promise; - update(configurations: CasesConfigurePatch): Promise; + update( + configurationId: string, + configurations: CasesConfigurePatch + ): Promise; create(configuration: CasesConfigureRequest): Promise; } @@ -62,21 +87,16 @@ export interface ConfigureSubClient { * configurations. */ export const createInternalConfigurationSubClient = ( - args: CasesClientArgs, + clientArgs: CasesClientArgs, casesClientInternal: CasesClientInternal ): InternalConfigureSubClient => { - const { savedObjectsClient, connectorMappingsService, logger, actionsClient } = args; - const configureSubClient: InternalConfigureSubClient = { - getFields: (fields: ConfigurationGetFields) => getFields({ ...fields, actionsClient }), - getMappings: (params: ConfigurationGetMappings) => - getMappings({ - ...params, - savedObjectsClient, - connectorMappingsService, - casesClientInternal, - logger, - }), + getFields: (params: ConfigurationGetFields) => getFields(params, clientArgs), + getMappings: (params: MappingsArgs) => getMappings(params, clientArgs), + createMappings: (params: CreateMappingsArgs) => + createMappings(params, clientArgs, casesClientInternal), + updateMappings: (params: UpdateMappingsArgs) => + updateMappings(params, clientArgs, casesClientInternal), }; return Object.freeze(configureSubClient); @@ -87,50 +107,97 @@ export const createConfigurationSubClient = ( casesInternalClient: CasesClientInternal ): ConfigureSubClient => { return Object.freeze({ - get: () => get(clientArgs, casesInternalClient), + get: (params: GetConfigureFindRequest) => get(params, clientArgs, casesInternalClient), getConnectors: () => getConnectors(clientArgs), - update: (configuration: CasesConfigurePatch) => - update(configuration, clientArgs, casesInternalClient), + update: (configurationId: string, configuration: CasesConfigurePatch) => + update(configurationId, configuration, clientArgs, casesInternalClient), create: (configuration: CasesConfigureRequest) => create(configuration, clientArgs, casesInternalClient), }); }; async function get( + params: GetConfigureFindRequest, clientArgs: CasesClientArgs, casesClientInternal: CasesClientInternal -): Promise { - const { savedObjectsClient: soClient, caseConfigureService, logger } = clientArgs; +): Promise { + const { + savedObjectsClient: soClient, + caseConfigureService, + logger, + authorization, + auditLogger, + } = clientArgs; try { + const queryParams = pipe( + excess(GetConfigureFindRequestRt).decode(params), + fold(throwErrors(Boom.badRequest), identity) + ); + + const { + filter: authorizationFilter, + ensureSavedObjectsAreAuthorized, + logSuccessfulAuthorization, + } = await getAuthorizationFilter({ + authorization, + operation: Operations.findConfigurations, + auditLogger, + }); + + const filter = combineAuthorizedAndOwnerFilter( + queryParams.owner, + authorizationFilter, + Operations.findConfigurations.savedObjectType + ); + let error: string | null = null; + const myCaseConfigure = await caseConfigureService.find({ + soClient, + options: { filter }, + }); + + ensureSavedObjectsAreAuthorized( + myCaseConfigure.saved_objects.map((configuration) => ({ + id: configuration.id, + owner: configuration.attributes.owner, + })) + ); - const myCaseConfigure = await caseConfigureService.find({ soClient }); + logSuccessfulAuthorization(); - const { connector, ...caseConfigureWithoutConnector } = myCaseConfigure.saved_objects[0] - ?.attributes ?? { connector: null }; - let mappings: ConnectorMappingsAttributes[] = []; - if (connector != null) { - try { - mappings = await casesClientInternal.configuration.getMappings({ - connectorId: connector.id, - connectorType: connector.type, - }); - } catch (e) { - error = e.isBoom - ? e.output.payload.message - : `Error connecting to ${connector.name} instance`; - } - } + const configurations = await Promise.all( + myCaseConfigure.saved_objects.map(async (configuration) => { + const { connector, ...caseConfigureWithoutConnector } = configuration?.attributes ?? { + connector: null, + }; + + let mappings: SavedObjectsFindResponse['saved_objects'] = []; - return myCaseConfigure.saved_objects.length > 0 - ? CaseConfigureResponseRt.encode({ + if (connector != null) { + try { + mappings = await casesClientInternal.configuration.getMappings({ + connectorId: connector.id, + connectorType: connector.type, + }); + } catch (e) { + error = e.isBoom + ? e.output.payload.message + : `Failed to retrieve mapping for ${connector.name}`; + } + } + + return { ...caseConfigureWithoutConnector, connector: transformESConnectorToCaseConnector(connector), - mappings, - version: myCaseConfigure.saved_objects[0].version ?? '', + mappings: mappings.length > 0 ? mappings[0].attributes.mappings : [], + version: configuration.version ?? '', error, - }) - : {}; + id: configuration.id, + }; + }) + ); + + return CaseConfigurationsResponseRt.encode(configurations); } catch (error) { throw createCaseError({ message: `Failed to get case configure: ${error}`, error, logger }); } @@ -162,63 +229,124 @@ async function getConnectors({ } async function update( - configurations: CasesConfigurePatch, + configurationId: string, + req: CasesConfigurePatch, clientArgs: CasesClientArgs, casesClientInternal: CasesClientInternal ): Promise { - const { caseConfigureService, logger, savedObjectsClient: soClient, user } = clientArgs; + const { + caseConfigureService, + logger, + savedObjectsClient: soClient, + user, + authorization, + auditLogger, + } = clientArgs; try { - let error = null; + const request = pipe( + CasesConfigurePatchRt.decode(req), + fold(throwErrors(Boom.badRequest), identity) + ); - const myCaseConfigure = await caseConfigureService.find({ soClient }); - const { version, connector, ...queryWithoutVersion } = configurations; - if (myCaseConfigure.saved_objects.length === 0) { - throw Boom.conflict( - 'You can not patch this configuration since you did not created first with a post.' - ); - } + const { version, ...queryWithoutVersion } = request; - if (version !== myCaseConfigure.saved_objects[0].version) { + /** + * Excess function does not supports union or intersection types. + * For that reason we need to check manually for excess properties + * in the partial attributes. + * + * The owner attribute should not be allowed. + */ + pipe( + excess(CasesConfigurePatchRt.types[0]).decode(queryWithoutVersion), + fold(throwErrors(Boom.badRequest), identity) + ); + + const configuration = await caseConfigureService.get({ + soClient, + configurationId, + }); + + await ensureAuthorized({ + operation: Operations.updateConfiguration, + owners: [configuration.attributes.owner], + authorization, + auditLogger, + savedObjectIDs: [configuration.id], + }); + + // log that we're attempting to update a configuration + auditLogger?.log( + createAuditMsg({ + operation: Operations.updateConfiguration, + outcome: EventOutcome.UNKNOWN, + savedObjectID: configuration.id, + }) + ); + + if (version !== configuration.version) { throw Boom.conflict( 'This configuration has been updated. Please refresh before saving additional updates.' ); } + let error = null; const updateDate = new Date().toISOString(); - let mappings: ConnectorMappingsAttributes[] = []; - if (connector != null) { - try { - mappings = await casesClientInternal.configuration.getMappings({ - connectorId: connector.id, - connectorType: connector.type, - }); - } catch (e) { - error = e.isBoom - ? e.output.payload.message - : `Error connecting to ${connector.name} instance`; + const { connector, ...queryWithoutVersionAndConnector } = queryWithoutVersion; + + try { + const resMappings = await casesClientInternal.configuration.getMappings({ + connectorId: connector != null ? connector.id : configuration.attributes.connector.id, + connectorType: connector != null ? connector.type : configuration.attributes.connector.type, + }); + mappings = resMappings.length > 0 ? resMappings[0].attributes.mappings : []; + + if (connector != null) { + if (resMappings.length !== 0) { + mappings = await casesClientInternal.configuration.updateMappings({ + connectorId: connector.id, + connectorType: connector.type, + mappingId: resMappings[0].id, + }); + } else { + mappings = await casesClientInternal.configuration.createMappings({ + connectorId: connector.id, + connectorType: connector.type, + owner: configuration.attributes.owner, + }); + } } + } catch (e) { + error = e.isBoom + ? e.output.payload.message + : `Error connecting to ${ + connector != null ? connector.name : configuration.attributes.connector.name + } instance`; } + const patch = await caseConfigureService.patch({ soClient, - caseConfigureId: myCaseConfigure.saved_objects[0].id, + configurationId: configuration.id, updatedAttributes: { - ...queryWithoutVersion, + ...queryWithoutVersionAndConnector, ...(connector != null ? { connector: transformCaseConnectorToEsConnector(connector) } : {}), updated_at: updateDate, updated_by: user, }, }); + return CaseConfigureResponseRt.encode({ - ...myCaseConfigure.saved_objects[0].attributes, + ...configuration.attributes, ...patch.attributes, connector: transformESConnectorToCaseConnector( - patch.attributes.connector ?? myCaseConfigure.saved_objects[0].attributes.connector + patch.attributes.connector ?? configuration.attributes.connector ), mappings, version: patch.version ?? '', error, + id: patch.id, }); } catch (error) { throw createCaseError({ @@ -234,31 +362,94 @@ async function create( clientArgs: CasesClientArgs, casesClientInternal: CasesClientInternal ): Promise { - const { savedObjectsClient: soClient, caseConfigureService, logger, user } = clientArgs; + const { + savedObjectsClient: soClient, + caseConfigureService, + logger, + user, + authorization, + auditLogger, + } = clientArgs; try { let error = null; - const myCaseConfigure = await caseConfigureService.find({ soClient }); + const { + filter: authorizationFilter, + ensureSavedObjectsAreAuthorized, + logSuccessfulAuthorization, + } = await getAuthorizationFilter({ + authorization, + /** + * The operation is createConfiguration because the procedure is part of + * the create route. The user should have all + * permissions to delete the results. + */ + operation: Operations.createConfiguration, + auditLogger, + }); + + const filter = combineAuthorizedAndOwnerFilter( + configuration.owner, + authorizationFilter, + Operations.createConfiguration.savedObjectType + ); + + const myCaseConfigure = await caseConfigureService.find({ + soClient, + options: { filter }, + }); + + ensureSavedObjectsAreAuthorized( + myCaseConfigure.saved_objects.map((conf) => ({ + id: conf.id, + owner: conf.attributes.owner, + })) + ); + + logSuccessfulAuthorization(); + if (myCaseConfigure.saved_objects.length > 0) { await Promise.all( myCaseConfigure.saved_objects.map((cc) => - caseConfigureService.delete({ soClient, caseConfigureId: cc.id }) + caseConfigureService.delete({ soClient, configurationId: cc.id }) ) ); } + const savedObjectID = SavedObjectsUtils.generateId(); + + await ensureAuthorized({ + operation: Operations.createConfiguration, + owners: [configuration.owner], + authorization, + auditLogger, + savedObjectIDs: [savedObjectID], + }); + + // log that we're attempting to create a configuration + auditLogger?.log( + createAuditMsg({ + operation: Operations.createConfiguration, + outcome: EventOutcome.UNKNOWN, + savedObjectID, + }) + ); + const creationDate = new Date().toISOString(); let mappings: ConnectorMappingsAttributes[] = []; + try { - mappings = await casesClientInternal.configuration.getMappings({ + mappings = await casesClientInternal.configuration.createMappings({ connectorId: configuration.connector.id, connectorType: configuration.connector.type, + owner: configuration.owner, }); } catch (e) { error = e.isBoom ? e.output.payload.message : `Error connecting to ${configuration.connector.name} instance`; } + const post = await caseConfigureService.post({ soClient, attributes: { @@ -269,6 +460,7 @@ async function create( updated_at: null, updated_by: null, }, + id: savedObjectID, }); return CaseConfigureResponseRt.encode({ @@ -278,6 +470,7 @@ async function create( mappings, version: post.version ?? '', error, + id: post.id, }); } catch (error) { throw createCaseError({ diff --git a/x-pack/plugins/cases/server/client/configure/create_mappings.ts b/x-pack/plugins/cases/server/client/configure/create_mappings.ts new file mode 100644 index 0000000000000..73fd59e15da53 --- /dev/null +++ b/x-pack/plugins/cases/server/client/configure/create_mappings.ts @@ -0,0 +1,55 @@ +/* + * 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 { ConnectorMappingsAttributes, ConnectorTypes } from '../../../common/api'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server/saved_objects'; +import { createCaseError } from '../../common/error'; +import { CasesClientArgs, CasesClientInternal } from '..'; +import { CreateMappingsArgs } from './types'; + +export const createMappings = async ( + { connectorType, connectorId, owner }: CreateMappingsArgs, + clientArgs: CasesClientArgs, + casesClientInternal: CasesClientInternal +): Promise => { + const { savedObjectsClient, connectorMappingsService, logger } = clientArgs; + + try { + if (connectorType === ConnectorTypes.none) { + return []; + } + + const res = await casesClientInternal.configuration.getFields({ + connectorId, + connectorType, + }); + + const theMapping = await connectorMappingsService.post({ + soClient: savedObjectsClient, + attributes: { + mappings: res.defaultMappings, + owner, + }, + references: [ + { + type: ACTION_SAVED_OBJECT_TYPE, + name: `associated-${ACTION_SAVED_OBJECT_TYPE}`, + id: connectorId, + }, + ], + }); + + return theMapping.attributes.mappings; + } catch (error) { + throw createCaseError({ + message: `Failed to create mapping connector id: ${connectorId} type: ${connectorType}: ${error}`, + error, + logger, + }); + } +}; diff --git a/x-pack/plugins/cases/server/client/configure/get_fields.ts b/x-pack/plugins/cases/server/client/configure/get_fields.ts index 8a6b20256328f..78627cfaca6ed 100644 --- a/x-pack/plugins/cases/server/client/configure/get_fields.ts +++ b/x-pack/plugins/cases/server/client/configure/get_fields.ts @@ -6,23 +6,21 @@ */ import Boom from '@hapi/boom'; -import { PublicMethodsOf } from '@kbn/utility-types'; -import { ActionsClient } from '../../../../actions/server'; import { GetFieldsResponse } from '../../../common/api'; import { createDefaultMapping, formatFields } from './utils'; +import { CasesClientArgs } from '..'; interface ConfigurationGetFields { connectorId: string; connectorType: string; - actionsClient: PublicMethodsOf; } -export const getFields = async ({ - actionsClient, - connectorType, - connectorId, -}: ConfigurationGetFields): Promise => { +export const getFields = async ( + { connectorType, connectorId }: ConfigurationGetFields, + clientArgs: CasesClientArgs +): Promise => { + const { actionsClient } = clientArgs; const results = await actionsClient.execute({ actionId: connectorId, params: { diff --git a/x-pack/plugins/cases/server/client/configure/get_mappings.ts b/x-pack/plugins/cases/server/client/configure/get_mappings.ts index 4f8b8c6cbf32a..31435e7c7cdb2 100644 --- a/x-pack/plugins/cases/server/client/configure/get_mappings.ts +++ b/x-pack/plugins/cases/server/client/configure/get_mappings.ts @@ -5,35 +5,25 @@ * 2.0. */ -import { SavedObjectsClientContract, Logger } from 'src/core/server'; -import { ConnectorMappingsAttributes, ConnectorTypes } from '../../../common/api'; +import { SavedObjectsFindResponse } from 'kibana/server'; +import { ConnectorMappings, ConnectorTypes } from '../../../common/api'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server/saved_objects'; -import { ConnectorMappingsService } from '../../services'; -import { CasesClientInternal } from '..'; import { createCaseError } from '../../common/error'; +import { CasesClientArgs } from '..'; +import { MappingsArgs } from './types'; -interface GetMappingsArgs { - savedObjectsClient: SavedObjectsClientContract; - connectorMappingsService: ConnectorMappingsService; - casesClientInternal: CasesClientInternal; - connectorType: string; - connectorId: string; - logger: Logger; -} +export const getMappings = async ( + { connectorType, connectorId }: MappingsArgs, + clientArgs: CasesClientArgs +): Promise['saved_objects']> => { + const { savedObjectsClient, connectorMappingsService, logger } = clientArgs; -export const getMappings = async ({ - savedObjectsClient, - connectorMappingsService, - casesClientInternal, - connectorType, - connectorId, - logger, -}: GetMappingsArgs): Promise => { try { if (connectorType === ConnectorTypes.none) { return []; } + const myConnectorMappings = await connectorMappingsService.find({ soClient: savedObjectsClient, options: { @@ -43,30 +33,8 @@ export const getMappings = async ({ }, }, }); - let theMapping; - // Create connector mappings if there are none - if (myConnectorMappings.total === 0) { - const res = await casesClientInternal.configuration.getFields({ - connectorId, - connectorType, - }); - theMapping = await connectorMappingsService.post({ - soClient: savedObjectsClient, - attributes: { - mappings: res.defaultMappings, - }, - references: [ - { - type: ACTION_SAVED_OBJECT_TYPE, - name: `associated-${ACTION_SAVED_OBJECT_TYPE}`, - id: connectorId, - }, - ], - }); - } else { - theMapping = myConnectorMappings.saved_objects[0]; - } - return theMapping ? theMapping.attributes.mappings : []; + + return myConnectorMappings.saved_objects; } catch (error) { throw createCaseError({ message: `Failed to retrieve mapping connector id: ${connectorId} type: ${connectorType}: ${error}`, diff --git a/x-pack/plugins/cases/server/client/configure/types.ts b/x-pack/plugins/cases/server/client/configure/types.ts new file mode 100644 index 0000000000000..a34251690db48 --- /dev/null +++ b/x-pack/plugins/cases/server/client/configure/types.ts @@ -0,0 +1,24 @@ +/* + * 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. + */ + +export interface MappingsArgs { + connectorType: string; + connectorId: string; +} + +export interface CreateMappingsArgs extends MappingsArgs { + owner: string; +} + +export interface UpdateMappingsArgs extends MappingsArgs { + mappingId: string; +} + +export interface ConfigurationGetFields { + connectorId: string; + connectorType: string; +} diff --git a/x-pack/plugins/cases/server/client/configure/update_mappings.ts b/x-pack/plugins/cases/server/client/configure/update_mappings.ts new file mode 100644 index 0000000000000..d7acbbd5f74f7 --- /dev/null +++ b/x-pack/plugins/cases/server/client/configure/update_mappings.ts @@ -0,0 +1,55 @@ +/* + * 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 { ConnectorMappingsAttributes, ConnectorTypes } from '../../../common/api'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server/saved_objects'; +import { createCaseError } from '../../common/error'; +import { CasesClientArgs, CasesClientInternal } from '..'; +import { UpdateMappingsArgs } from './types'; + +export const updateMappings = async ( + { connectorType, connectorId, mappingId }: UpdateMappingsArgs, + clientArgs: CasesClientArgs, + casesClientInternal: CasesClientInternal +): Promise => { + const { savedObjectsClient, connectorMappingsService, logger } = clientArgs; + + try { + if (connectorType === ConnectorTypes.none) { + return []; + } + + const res = await casesClientInternal.configuration.getFields({ + connectorId, + connectorType, + }); + + const theMapping = await connectorMappingsService.update({ + soClient: savedObjectsClient, + mappingId, + attributes: { + mappings: res.defaultMappings, + }, + references: [ + { + type: ACTION_SAVED_OBJECT_TYPE, + name: `associated-${ACTION_SAVED_OBJECT_TYPE}`, + id: connectorId, + }, + ], + }); + + return theMapping.attributes.mappings ?? []; + } catch (error) { + throw createCaseError({ + message: `Failed to create mapping connector id: ${connectorId} type: ${connectorType}: ${error}`, + error, + logger, + }); + } +}; diff --git a/x-pack/plugins/cases/server/client/user_actions/client.ts b/x-pack/plugins/cases/server/client/user_actions/client.ts index 8098714f8f955..909c533785302 100644 --- a/x-pack/plugins/cases/server/client/user_actions/client.ts +++ b/x-pack/plugins/cases/server/client/user_actions/client.ts @@ -15,20 +15,12 @@ export interface UserActionGet { } export interface UserActionsSubClient { - getAll(args: UserActionGet): Promise; + getAll(clientArgs: UserActionGet): Promise; } -export const createUserActionsSubClient = (args: CasesClientArgs): UserActionsSubClient => { - const { savedObjectsClient, userActionService, logger } = args; - +export const createUserActionsSubClient = (clientArgs: CasesClientArgs): UserActionsSubClient => { const attachmentSubClient: UserActionsSubClient = { - getAll: (params: UserActionGet) => - get({ - ...params, - savedObjectsClient, - userActionService, - logger, - }), + getAll: (params: UserActionGet) => get(params, clientArgs), }; return Object.freeze(attachmentSubClient); 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 4a8d1101d19cf..dac997c3fa90a 100644 --- a/x-pack/plugins/cases/server/client/user_actions/get.ts +++ b/x-pack/plugins/cases/server/client/user_actions/get.ts @@ -5,32 +5,27 @@ * 2.0. */ -import { SavedObjectsClientContract, Logger } from 'kibana/server'; import { SUB_CASE_SAVED_OBJECT, CASE_SAVED_OBJECT, CASE_COMMENT_SAVED_OBJECT, } from '../../../common/constants'; import { CaseUserActionsResponseRt, CaseUserActionsResponse } from '../../../common/api'; -import { CaseUserActionService } from '../../services'; import { createCaseError } from '../../common/error'; import { checkEnabledCaseConnectorOrThrow } from '../../common'; +import { CasesClientArgs } from '..'; interface GetParams { - savedObjectsClient: SavedObjectsClientContract; - userActionService: CaseUserActionService; caseId: string; subCaseId?: string; - logger: Logger; } -export const get = async ({ - savedObjectsClient, - userActionService, - caseId, - subCaseId, - logger, -}: GetParams): Promise => { +export const get = async ( + { caseId, subCaseId }: GetParams, + clientArgs: CasesClientArgs +): Promise => { + const { savedObjectsClient, userActionService, logger } = clientArgs; + try { checkEnabledCaseConnectorOrThrow(subCaseId); diff --git a/x-pack/plugins/cases/server/client/utils.ts b/x-pack/plugins/cases/server/client/utils.ts index 0dcbf61fa0894..b61de9f2beb6a 100644 --- a/x-pack/plugins/cases/server/client/utils.ts +++ b/x-pack/plugins/cases/server/client/utils.ts @@ -140,6 +140,24 @@ export const buildFilter = ({ ); }; +export const combineAuthorizedAndOwnerFilter = ( + owner?: string[] | string, + authorizationFilter?: KueryNode, + savedObjectType?: string +): KueryNode | undefined => { + const filters = Array.isArray(owner) ? owner : owner != null ? [owner] : []; + const ownerFilter = buildFilter({ + filters, + field: 'owner', + operator: 'or', + type: savedObjectType, + }); + + return authorizationFilter != null && ownerFilter != null + ? combineFilterWithAuthorizationFilter(ownerFilter, authorizationFilter) + : authorizationFilter ?? ownerFilter ?? undefined; +}; + /** * Constructs the filters used for finding cases and sub cases. * There are a few scenarios that this function tries to handle when constructing the filters used for finding cases 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 bb4e529192df3..933a59cf06016 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 @@ -471,6 +471,7 @@ export const mockCaseConfigure: Array> = email: 'testemail@elastic.co', username: 'elastic', }, + owner: 'securitySolution', }, references: [], updated_at: '2020-04-09T09:43:51.778Z', @@ -484,6 +485,7 @@ export const mockCaseMappings: Array> = [ id: 'mock-mappings-1', attributes: { mappings: mappings[ConnectorTypes.jira], + owner: 'securitySolution', }, references: [], }, diff --git a/x-pack/plugins/cases/server/routes/api/cases/reporters/get_reporters.ts b/x-pack/plugins/cases/server/routes/api/cases/reporters/get_reporters.ts index 2836c7572e810..a7a0e4f8bb141 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/reporters/get_reporters.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/reporters/get_reporters.ts @@ -6,20 +6,28 @@ */ import { RouteDeps } from '../../types'; -import { wrapError } from '../../utils'; +import { wrapError, escapeHatch } from '../../utils'; import { CASE_REPORTERS_URL } from '../../../../../common/constants'; +import { AllReportersFindRequest } from '../../../../../common/api'; export function initGetReportersApi({ router, logger }: RouteDeps) { router.get( { path: CASE_REPORTERS_URL, - validate: {}, + validate: { + query: escapeHatch, + }, }, async (context, request, response) => { try { + if (!context.cases) { + return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); + } + const client = await context.cases.getCasesClient(); + const options = request.query as AllReportersFindRequest; - return response.ok({ body: await client.cases.getReporters() }); + return response.ok({ body: await client.cases.getReporters({ ...options }) }); } catch (error) { logger.error(`Failed to get reporters in route: ${error}`); return response.customError(wrapError(error)); diff --git a/x-pack/plugins/cases/server/routes/api/cases/tags/get_tags.ts b/x-pack/plugins/cases/server/routes/api/cases/tags/get_tags.ts index e13974b514c08..a62c3247b01df 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/tags/get_tags.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/tags/get_tags.ts @@ -6,20 +6,28 @@ */ import { RouteDeps } from '../../types'; -import { wrapError } from '../../utils'; +import { wrapError, escapeHatch } from '../../utils'; import { CASE_TAGS_URL } from '../../../../../common/constants'; +import { AllTagsFindRequest } from '../../../../../common/api'; export function initGetTagsApi({ router, logger }: RouteDeps) { router.get( { path: CASE_TAGS_URL, - validate: {}, + validate: { + query: escapeHatch, + }, }, async (context, request, response) => { try { + if (!context.cases) { + return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); + } + const client = await context.cases.getCasesClient(); + const options = request.query as AllTagsFindRequest; - return response.ok({ body: await client.cases.getTags() }); + return response.ok({ body: await client.cases.getTags({ ...options }) }); } catch (error) { logger.error(`Failed to retrieve tags in route: ${error}`); return response.customError(wrapError(error)); diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/delete_all_comments.ts b/x-pack/plugins/cases/server/routes/api/comments/delete_all_comments.ts similarity index 89% rename from x-pack/plugins/cases/server/routes/api/cases/comments/delete_all_comments.ts rename to x-pack/plugins/cases/server/routes/api/comments/delete_all_comments.ts index 08c4491f7b151..a41d4683af2d0 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/delete_all_comments.ts +++ b/x-pack/plugins/cases/server/routes/api/comments/delete_all_comments.ts @@ -6,9 +6,9 @@ */ import { schema } from '@kbn/config-schema'; -import { RouteDeps } from '../../types'; -import { wrapError } from '../../utils'; -import { CASE_COMMENTS_URL } from '../../../../../common/constants'; +import { RouteDeps } from '../types'; +import { wrapError } from '../utils'; +import { CASE_COMMENTS_URL } from '../../../../common/constants'; export function initDeleteAllCommentsApi({ router, logger }: RouteDeps) { router.delete( diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.ts b/x-pack/plugins/cases/server/routes/api/comments/delete_comment.ts similarity index 89% rename from x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.ts rename to x-pack/plugins/cases/server/routes/api/comments/delete_comment.ts index 284013ff36c09..f145fc62efc8a 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/comments/delete_comment.ts @@ -7,9 +7,9 @@ import { schema } from '@kbn/config-schema'; -import { RouteDeps } from '../../types'; -import { wrapError } from '../../utils'; -import { CASE_COMMENT_DETAILS_URL } from '../../../../../common/constants'; +import { RouteDeps } from '../types'; +import { wrapError } from '../utils'; +import { CASE_COMMENT_DETAILS_URL } from '../../../../common/constants'; export function initDeleteCommentApi({ router, logger }: RouteDeps) { router.delete( diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/find_comments.ts b/x-pack/plugins/cases/server/routes/api/comments/find_comments.ts similarity index 89% rename from x-pack/plugins/cases/server/routes/api/cases/comments/find_comments.ts rename to x-pack/plugins/cases/server/routes/api/comments/find_comments.ts index b7b8a3b44146f..c992e7d0c114c 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/find_comments.ts +++ b/x-pack/plugins/cases/server/routes/api/comments/find_comments.ts @@ -14,10 +14,10 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { SavedObjectFindOptionsRt, throwErrors } from '../../../../../common/api'; -import { RouteDeps } from '../../types'; -import { escapeHatch, wrapError } from '../../utils'; -import { CASE_COMMENTS_URL } from '../../../../../common/constants'; +import { SavedObjectFindOptionsRt, throwErrors } from '../../../../common/api'; +import { RouteDeps } from '../types'; +import { escapeHatch, wrapError } from '../utils'; +import { CASE_COMMENTS_URL } from '../../../../common/constants'; const FindQueryParamsRt = rt.partial({ ...SavedObjectFindOptionsRt.props, diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts b/x-pack/plugins/cases/server/routes/api/comments/get_all_comment.ts similarity index 90% rename from x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts rename to x-pack/plugins/cases/server/routes/api/comments/get_all_comment.ts index 7777a0b36a1f1..b916e22c6b0ed 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/comments/get_all_comment.ts @@ -7,9 +7,9 @@ import { schema } from '@kbn/config-schema'; -import { RouteDeps } from '../../types'; -import { wrapError } from '../../utils'; -import { CASE_COMMENTS_URL } from '../../../../../common/constants'; +import { RouteDeps } from '../types'; +import { wrapError } from '../utils'; +import { CASE_COMMENTS_URL } from '../../../../common/constants'; export function initGetAllCommentsApi({ router, logger }: RouteDeps) { router.get( diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/get_comment.ts b/x-pack/plugins/cases/server/routes/api/comments/get_comment.ts similarity index 87% rename from x-pack/plugins/cases/server/routes/api/cases/comments/get_comment.ts rename to x-pack/plugins/cases/server/routes/api/comments/get_comment.ts index cf6f7d62dcf6e..09805c00cb10a 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/get_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/comments/get_comment.ts @@ -7,9 +7,9 @@ import { schema } from '@kbn/config-schema'; -import { RouteDeps } from '../../types'; -import { wrapError } from '../../utils'; -import { CASE_COMMENT_DETAILS_URL } from '../../../../../common/constants'; +import { RouteDeps } from '../types'; +import { wrapError } from '../utils'; +import { CASE_COMMENT_DETAILS_URL } from '../../../../common/constants'; export function initGetCommentApi({ router, logger }: RouteDeps) { router.get( diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/patch_comment.ts b/x-pack/plugins/cases/server/routes/api/comments/patch_comment.ts similarity index 90% rename from x-pack/plugins/cases/server/routes/api/cases/comments/patch_comment.ts rename to x-pack/plugins/cases/server/routes/api/comments/patch_comment.ts index 28852eca3af41..aecdeb46756c0 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/patch_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/comments/patch_comment.ts @@ -11,10 +11,10 @@ import { identity } from 'fp-ts/lib/function'; import { schema } from '@kbn/config-schema'; import Boom from '@hapi/boom'; -import { RouteDeps } from '../../types'; -import { escapeHatch, wrapError } from '../../utils'; -import { CASE_COMMENTS_URL } from '../../../../../common/constants'; -import { CommentPatchRequestRt, throwErrors } from '../../../../../common/api'; +import { RouteDeps } from '../types'; +import { escapeHatch, wrapError } from '../utils'; +import { CASE_COMMENTS_URL } from '../../../../common/constants'; +import { CommentPatchRequestRt, throwErrors } from '../../../../common/api'; export function initPatchCommentApi({ router, logger }: RouteDeps) { router.patch( diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/post_comment.ts b/x-pack/plugins/cases/server/routes/api/comments/post_comment.ts similarity index 90% rename from x-pack/plugins/cases/server/routes/api/cases/comments/post_comment.ts rename to x-pack/plugins/cases/server/routes/api/comments/post_comment.ts index 7dbfb2a62c46f..1919aef7b72b4 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/post_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/comments/post_comment.ts @@ -7,10 +7,10 @@ import Boom from '@hapi/boom'; import { schema } from '@kbn/config-schema'; -import { escapeHatch, wrapError } from '../../utils'; -import { RouteDeps } from '../../types'; -import { CASE_COMMENTS_URL, ENABLE_CASE_CONNECTOR } from '../../../../../common/constants'; -import { CommentRequest } from '../../../../../common/api'; +import { escapeHatch, wrapError } from '../utils'; +import { RouteDeps } from '../types'; +import { CASE_COMMENTS_URL, ENABLE_CASE_CONNECTOR } from '../../../../common/constants'; +import { CommentRequest } from '../../../../common/api'; export function initPostCommentApi({ router, logger }: RouteDeps) { router.post( diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.ts b/x-pack/plugins/cases/server/routes/api/configure/get_configure.ts similarity index 63% rename from x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.ts rename to x-pack/plugins/cases/server/routes/api/configure/get_configure.ts index 933a53eb8a870..8222ac8fe5690 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.ts +++ b/x-pack/plugins/cases/server/routes/api/configure/get_configure.ts @@ -5,22 +5,26 @@ * 2.0. */ -import { RouteDeps } from '../../types'; -import { wrapError } from '../../utils'; -import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; +import { RouteDeps } from '../types'; +import { escapeHatch, wrapError } from '../utils'; +import { CASE_CONFIGURE_URL } from '../../../../common/constants'; +import { GetConfigureFindRequest } from '../../../../common/api'; export function initGetCaseConfigure({ router, logger }: RouteDeps) { router.get( { path: CASE_CONFIGURE_URL, - validate: false, + validate: { + query: escapeHatch, + }, }, async (context, request, response) => { try { const client = await context.cases.getCasesClient(); + const options = request.query as GetConfigureFindRequest; return response.ok({ - body: await client.configure.get(), + body: await client.configure.get({ ...options }), }); } catch (error) { logger.error(`Failed to get case configure in route: ${error}`); diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/get_connectors.ts b/x-pack/plugins/cases/server/routes/api/configure/get_connectors.ts similarity index 84% rename from x-pack/plugins/cases/server/routes/api/cases/configure/get_connectors.ts rename to x-pack/plugins/cases/server/routes/api/configure/get_connectors.ts index be05d1c3b8230..46c110bbb8ba5 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/get_connectors.ts +++ b/x-pack/plugins/cases/server/routes/api/configure/get_connectors.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { RouteDeps } from '../../types'; -import { wrapError } from '../../utils'; +import { RouteDeps } from '../types'; +import { wrapError } from '../utils'; -import { CASE_CONFIGURE_CONNECTORS_URL } from '../../../../../common/constants'; +import { CASE_CONFIGURE_CONNECTORS_URL } from '../../../../common/constants'; /* * Be aware that this api will only return 20 connectors diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.ts b/x-pack/plugins/cases/server/routes/api/configure/patch_configure.ts similarity index 61% rename from x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.ts rename to x-pack/plugins/cases/server/routes/api/configure/patch_configure.ts index d32c7151f6df5..49288c72eadee 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.ts +++ b/x-pack/plugins/cases/server/routes/api/configure/patch_configure.ts @@ -10,30 +10,37 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { CasesConfigurePatchRt, throwErrors } from '../../../../../common/api'; -import { RouteDeps } from '../../types'; -import { wrapError, escapeHatch } from '../../utils'; -import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; +import { + CaseConfigureRequestParamsRt, + throwErrors, + CasesConfigurePatch, + excess, +} from '../../../../common/api'; +import { RouteDeps } from '../types'; +import { wrapError, escapeHatch } from '../utils'; +import { CASE_CONFIGURE_DETAILS_URL } from '../../../../common/constants'; export function initPatchCaseConfigure({ router, logger }: RouteDeps) { router.patch( { - path: CASE_CONFIGURE_URL, + path: CASE_CONFIGURE_DETAILS_URL, validate: { + params: escapeHatch, body: escapeHatch, }, }, async (context, request, response) => { try { - const query = pipe( - CasesConfigurePatchRt.decode(request.body), + const params = pipe( + excess(CaseConfigureRequestParamsRt).decode(request.params), fold(throwErrors(Boom.badRequest), identity) ); const client = await context.cases.getCasesClient(); + const configuration = request.body as CasesConfigurePatch; return response.ok({ - body: await client.configure.update(query), + body: await client.configure.update(params.configuration_id, configuration), }); } catch (error) { logger.error(`Failed to get patch configure in route: ${error}`); diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.ts b/x-pack/plugins/cases/server/routes/api/configure/post_configure.ts similarity index 86% rename from x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.ts rename to x-pack/plugins/cases/server/routes/api/configure/post_configure.ts index ca25a29d6a1de..fe8ffedbc85f6 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.ts +++ b/x-pack/plugins/cases/server/routes/api/configure/post_configure.ts @@ -10,10 +10,10 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { CasesConfigureRequestRt, throwErrors } from '../../../../../common/api'; -import { RouteDeps } from '../../types'; -import { wrapError, escapeHatch } from '../../utils'; -import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; +import { CasesConfigureRequestRt, throwErrors } from '../../../../common/api'; +import { RouteDeps } from '../types'; +import { wrapError, escapeHatch } from '../utils'; +import { CASE_CONFIGURE_URL } from '../../../../common/constants'; export function initPostCaseConfigure({ router, logger }: RouteDeps) { router.post( diff --git a/x-pack/plugins/cases/server/routes/api/index.ts b/x-pack/plugins/cases/server/routes/api/index.ts index c5b7aa85dc33e..f05bd3b229256 100644 --- a/x-pack/plugins/cases/server/routes/api/index.ts +++ b/x-pack/plugins/cases/server/routes/api/index.ts @@ -12,31 +12,31 @@ import { initPatchCasesApi } from './cases/patch_cases'; import { initPostCaseApi } from './cases/post_case'; import { initPushCaseApi } from './cases/push_case'; import { initGetReportersApi } from './cases/reporters/get_reporters'; -import { initGetCasesStatusApi } from './cases/status/get_status'; +import { initGetCasesStatusApi } from './stats/get_status'; import { initGetTagsApi } from './cases/tags/get_tags'; import { initGetAllCaseUserActionsApi, initGetAllSubCaseUserActionsApi, -} from './cases/user_actions/get_all_user_actions'; +} from './user_actions/get_all_user_actions'; -import { initDeleteCommentApi } from './cases/comments/delete_comment'; -import { initDeleteAllCommentsApi } from './cases/comments/delete_all_comments'; -import { initFindCaseCommentsApi } from './cases/comments/find_comments'; -import { initGetAllCommentsApi } from './cases/comments/get_all_comment'; -import { initGetCommentApi } from './cases/comments/get_comment'; -import { initPatchCommentApi } from './cases/comments/patch_comment'; -import { initPostCommentApi } from './cases/comments/post_comment'; +import { initDeleteCommentApi } from './comments/delete_comment'; +import { initDeleteAllCommentsApi } from './comments/delete_all_comments'; +import { initFindCaseCommentsApi } from './comments/find_comments'; +import { initGetAllCommentsApi } from './comments/get_all_comment'; +import { initGetCommentApi } from './comments/get_comment'; +import { initPatchCommentApi } from './comments/patch_comment'; +import { initPostCommentApi } from './comments/post_comment'; -import { initCaseConfigureGetActionConnector } from './cases/configure/get_connectors'; -import { initGetCaseConfigure } from './cases/configure/get_configure'; -import { initPatchCaseConfigure } from './cases/configure/patch_configure'; -import { initPostCaseConfigure } from './cases/configure/post_configure'; +import { initCaseConfigureGetActionConnector } from './configure/get_connectors'; +import { initGetCaseConfigure } from './configure/get_configure'; +import { initPatchCaseConfigure } from './configure/patch_configure'; +import { initPostCaseConfigure } from './configure/post_configure'; import { RouteDeps } from './types'; -import { initGetSubCaseApi } from './cases/sub_case/get_sub_case'; -import { initPatchSubCasesApi } from './cases/sub_case/patch_sub_cases'; -import { initFindSubCasesApi } from './cases/sub_case/find_sub_cases'; -import { initDeleteSubCasesApi } from './cases/sub_case/delete_sub_cases'; +import { initGetSubCaseApi } from './sub_case/get_sub_case'; +import { initPatchSubCasesApi } from './sub_case/patch_sub_cases'; +import { initFindSubCasesApi } from './sub_case/find_sub_cases'; +import { initDeleteSubCasesApi } from './sub_case/delete_sub_cases'; import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; /** diff --git a/x-pack/plugins/cases/server/routes/api/cases/status/get_status.ts b/x-pack/plugins/cases/server/routes/api/stats/get_status.ts similarity index 84% rename from x-pack/plugins/cases/server/routes/api/cases/status/get_status.ts rename to x-pack/plugins/cases/server/routes/api/stats/get_status.ts index 6ba5963580782..3d9dc73860ef9 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/status/get_status.ts +++ b/x-pack/plugins/cases/server/routes/api/stats/get_status.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { RouteDeps } from '../../types'; -import { wrapError } from '../../utils'; +import { RouteDeps } from '../types'; +import { wrapError } from '../utils'; -import { CASE_STATUS_URL } from '../../../../../common/constants'; +import { CASE_STATUS_URL } from '../../../../common/constants'; export function initGetCasesStatusApi({ router, logger }: RouteDeps) { router.get( diff --git a/x-pack/plugins/cases/server/routes/api/cases/sub_case/delete_sub_cases.ts b/x-pack/plugins/cases/server/routes/api/sub_case/delete_sub_cases.ts similarity index 86% rename from x-pack/plugins/cases/server/routes/api/cases/sub_case/delete_sub_cases.ts rename to x-pack/plugins/cases/server/routes/api/sub_case/delete_sub_cases.ts index 4f4870496f77f..45899735ddb04 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/sub_case/delete_sub_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/sub_case/delete_sub_cases.ts @@ -6,9 +6,9 @@ */ import { schema } from '@kbn/config-schema'; -import { RouteDeps } from '../../types'; -import { wrapError } from '../../utils'; -import { SUB_CASES_PATCH_DEL_URL } from '../../../../../common/constants'; +import { RouteDeps } from '../types'; +import { wrapError } from '../utils'; +import { SUB_CASES_PATCH_DEL_URL } from '../../../../common/constants'; export function initDeleteSubCasesApi({ caseService, router, logger }: RouteDeps) { router.delete( diff --git a/x-pack/plugins/cases/server/routes/api/cases/sub_case/find_sub_cases.ts b/x-pack/plugins/cases/server/routes/api/sub_case/find_sub_cases.ts similarity index 89% rename from x-pack/plugins/cases/server/routes/api/cases/sub_case/find_sub_cases.ts rename to x-pack/plugins/cases/server/routes/api/sub_case/find_sub_cases.ts index 80cfbbd6b584f..8243e4a952993 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/sub_case/find_sub_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/sub_case/find_sub_cases.ts @@ -12,10 +12,10 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { SubCasesFindRequestRt, throwErrors } from '../../../../../common/api'; -import { RouteDeps } from '../../types'; -import { escapeHatch, wrapError } from '../../utils'; -import { SUB_CASES_URL } from '../../../../../common/constants'; +import { SubCasesFindRequestRt, throwErrors } from '../../../../common/api'; +import { RouteDeps } from '../types'; +import { escapeHatch, wrapError } from '../utils'; +import { SUB_CASES_URL } from '../../../../common/constants'; export function initFindSubCasesApi({ caseService, router, logger }: RouteDeps) { router.get( diff --git a/x-pack/plugins/cases/server/routes/api/cases/sub_case/get_sub_case.ts b/x-pack/plugins/cases/server/routes/api/sub_case/get_sub_case.ts similarity index 89% rename from x-pack/plugins/cases/server/routes/api/cases/sub_case/get_sub_case.ts rename to x-pack/plugins/cases/server/routes/api/sub_case/get_sub_case.ts index 44ec5d68e9653..db3e29f5ed96e 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/sub_case/get_sub_case.ts +++ b/x-pack/plugins/cases/server/routes/api/sub_case/get_sub_case.ts @@ -7,9 +7,9 @@ import { schema } from '@kbn/config-schema'; -import { RouteDeps } from '../../types'; -import { wrapError } from '../../utils'; -import { SUB_CASE_DETAILS_URL } from '../../../../../common/constants'; +import { RouteDeps } from '../types'; +import { wrapError } from '../utils'; +import { SUB_CASE_DETAILS_URL } from '../../../../common/constants'; export function initGetSubCaseApi({ router, logger }: RouteDeps) { router.get( diff --git a/x-pack/plugins/cases/server/routes/api/cases/sub_case/patch_sub_cases.ts b/x-pack/plugins/cases/server/routes/api/sub_case/patch_sub_cases.ts similarity index 79% rename from x-pack/plugins/cases/server/routes/api/cases/sub_case/patch_sub_cases.ts rename to x-pack/plugins/cases/server/routes/api/sub_case/patch_sub_cases.ts index c1cd4b317da9b..ce03c3bf970ab 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/sub_case/patch_sub_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/sub_case/patch_sub_cases.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { SubCasesPatchRequest } from '../../../../../common/api'; -import { SUB_CASES_PATCH_DEL_URL } from '../../../../../common/constants'; -import { RouteDeps } from '../../types'; -import { escapeHatch, wrapError } from '../../utils'; +import { SubCasesPatchRequest } from '../../../../common/api'; +import { SUB_CASES_PATCH_DEL_URL } from '../../../../common/constants'; +import { RouteDeps } from '../types'; +import { escapeHatch, wrapError } from '../utils'; export function initPatchSubCasesApi({ router, caseService, logger }: RouteDeps) { router.patch( diff --git a/x-pack/plugins/cases/server/routes/api/cases/user_actions/get_all_user_actions.ts b/x-pack/plugins/cases/server/routes/api/user_actions/get_all_user_actions.ts similarity index 95% rename from x-pack/plugins/cases/server/routes/api/cases/user_actions/get_all_user_actions.ts rename to x-pack/plugins/cases/server/routes/api/user_actions/get_all_user_actions.ts index 07f1353f19854..5944ff6176d78 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/user_actions/get_all_user_actions.ts +++ b/x-pack/plugins/cases/server/routes/api/user_actions/get_all_user_actions.ts @@ -7,9 +7,9 @@ import { schema } from '@kbn/config-schema'; -import { RouteDeps } from '../../types'; -import { wrapError } from '../../utils'; -import { CASE_USER_ACTIONS_URL, SUB_CASE_USER_ACTIONS_URL } from '../../../../../common/constants'; +import { RouteDeps } from '../types'; +import { wrapError } from '../utils'; +import { CASE_USER_ACTIONS_URL, SUB_CASE_USER_ACTIONS_URL } from '../../../../common/constants'; export function initGetAllCaseUserActionsApi({ router, logger }: RouteDeps) { router.get( diff --git a/x-pack/plugins/cases/server/services/cases/index.ts b/x-pack/plugins/cases/server/services/cases/index.ts index c7d94b3c66329..2362d893739a0 100644 --- a/x-pack/plugins/cases/server/services/cases/index.ts +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -47,8 +47,6 @@ import { CASE_COMMENT_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT, } from '../../../common/constants'; -import { readReporters } from './read_reporters'; -import { readTags } from './read_tags'; import { ClientArgs } from '..'; interface PushedArgs { @@ -172,6 +170,16 @@ interface CasesMapWithPageInfo { type FindCaseOptions = CasesFindRequest & SavedObjectFindOptionsKueryNode; +interface GetTagsArgs { + soClient: SavedObjectsClientContract; + filter?: KueryNode; +} + +interface GetReportersArgs { + soClient: SavedObjectsClientContract; + filter?: KueryNode; +} + const transformNewSubCase = ({ createdAt, createdBy, @@ -906,19 +914,54 @@ export class CaseService { } } - public async getReporters({ soClient }: ClientArgs) { + public async getReporters({ + soClient, + filter, + }: GetReportersArgs): Promise> { try { this.log.debug(`Attempting to GET all reporters`); - return await readReporters({ soClient }); + const firstReporters = await soClient.find({ + type: CASE_SAVED_OBJECT, + fields: ['created_by', 'owner'], + page: 1, + perPage: 1, + filter: cloneDeep(filter), + }); + + return await soClient.find({ + type: CASE_SAVED_OBJECT, + fields: ['created_by', 'owner'], + page: 1, + perPage: firstReporters.total, + filter: cloneDeep(filter), + }); } catch (error) { this.log.error(`Error on GET all reporters: ${error}`); throw error; } } - public async getTags({ soClient }: ClientArgs) { + + public async getTags({ + soClient, + filter, + }: GetTagsArgs): Promise> { try { this.log.debug(`Attempting to GET all cases`); - return await readTags({ soClient }); + const firstTags = await soClient.find({ + type: CASE_SAVED_OBJECT, + fields: ['tags', 'owner'], + page: 1, + perPage: 1, + filter: cloneDeep(filter), + }); + + return await soClient.find({ + type: CASE_SAVED_OBJECT, + fields: ['tags', 'owner'], + page: 1, + perPage: firstTags.total, + filter: cloneDeep(filter), + }); } catch (error) { this.log.error(`Error on GET cases: ${error}`); throw error; diff --git a/x-pack/plugins/cases/server/services/cases/read_reporters.ts b/x-pack/plugins/cases/server/services/cases/read_reporters.ts deleted file mode 100644 index f7e88c2649ae6..0000000000000 --- a/x-pack/plugins/cases/server/services/cases/read_reporters.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SavedObject, SavedObjectsClientContract } from 'kibana/server'; - -import { CaseAttributes, User } from '../../../common/api'; -import { CASE_SAVED_OBJECT } from '../../../common/constants'; - -export const convertToReporters = (caseObjects: Array>): User[] => - caseObjects.reduce((accum, caseObj) => { - if ( - caseObj && - caseObj.attributes && - caseObj.attributes.created_by && - caseObj.attributes.created_by.username && - !accum.some((item) => item.username === caseObj.attributes.created_by.username) - ) { - return [...accum, caseObj.attributes.created_by]; - } else { - return accum; - } - }, []); - -export const readReporters = async ({ - soClient, -}: { - soClient: SavedObjectsClientContract; - perPage?: number; -}): Promise => { - const firstReporters = await soClient.find({ - type: CASE_SAVED_OBJECT, - fields: ['created_by'], - page: 1, - perPage: 1, - }); - const reporters = await soClient.find({ - type: CASE_SAVED_OBJECT, - fields: ['created_by'], - page: 1, - perPage: firstReporters.total, - }); - return convertToReporters(reporters.saved_objects); -}; diff --git a/x-pack/plugins/cases/server/services/cases/read_tags.ts b/x-pack/plugins/cases/server/services/cases/read_tags.ts deleted file mode 100644 index a977c473327f8..0000000000000 --- a/x-pack/plugins/cases/server/services/cases/read_tags.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SavedObject, SavedObjectsClientContract } from 'kibana/server'; - -import { CaseAttributes } from '../../../common/api'; -import { CASE_SAVED_OBJECT } from '../../../common/constants'; - -export const convertToTags = (tagObjects: Array>): string[] => - tagObjects.reduce((accum, tagObj) => { - if (tagObj && tagObj.attributes && tagObj.attributes.tags) { - return [...accum, ...tagObj.attributes.tags]; - } else { - return accum; - } - }, []); - -export const convertTagsToSet = (tagObjects: Array>): Set => { - return new Set(convertToTags(tagObjects)); -}; - -// Note: This is doing an in-memory aggregation of the tags by calling each of the case -// records in batches of this const setting and uses the fields to try to get the least -// amount of data per record back. If saved objects at some point supports aggregations -// then this should be replaced with a an aggregation call. -// Ref: https://www.elastic.co/guide/en/kibana/master/saved-objects-api.html -export const readTags = async ({ - soClient, -}: { - soClient: SavedObjectsClientContract; - perPage?: number; -}): Promise => { - const tags = await readRawTags({ soClient }); - return tags; -}; - -export const readRawTags = async ({ - soClient, -}: { - soClient: SavedObjectsClientContract; -}): Promise => { - const firstTags = await soClient.find({ - type: CASE_SAVED_OBJECT, - fields: ['tags'], - page: 1, - perPage: 1, - }); - const tags = await soClient.find({ - type: CASE_SAVED_OBJECT, - fields: ['tags'], - page: 1, - perPage: firstTags.total, - }); - - return Array.from(convertTagsToSet(tags.saved_objects)); -}; diff --git a/x-pack/plugins/cases/server/services/configure/index.ts b/x-pack/plugins/cases/server/services/configure/index.ts index 45a9cd714145f..28e9af01f9d73 100644 --- a/x-pack/plugins/cases/server/services/configure/index.ts +++ b/x-pack/plugins/cases/server/services/configure/index.ts @@ -5,9 +5,11 @@ * 2.0. */ +import { cloneDeep } from 'lodash'; import { Logger, SavedObjectsClientContract } from 'kibana/server'; -import { ESCasesConfigureAttributes, SavedObjectFindOptions } from '../../../common/api'; +import { SavedObjectFindOptionsKueryNode } from '../../common'; +import { ESCasesConfigureAttributes } from '../../../common/api'; import { CASE_CONFIGURE_SAVED_OBJECT } from '../../../common/constants'; interface ClientArgs { @@ -15,43 +17,44 @@ interface ClientArgs { } interface GetCaseConfigureArgs extends ClientArgs { - caseConfigureId: string; + configurationId: string; } interface FindCaseConfigureArgs extends ClientArgs { - options?: SavedObjectFindOptions; + options?: SavedObjectFindOptionsKueryNode; } interface PostCaseConfigureArgs extends ClientArgs { attributes: ESCasesConfigureAttributes; + id: string; } interface PatchCaseConfigureArgs extends ClientArgs { - caseConfigureId: string; + configurationId: string; updatedAttributes: Partial; } export class CaseConfigureService { constructor(private readonly log: Logger) {} - public async delete({ soClient, caseConfigureId }: GetCaseConfigureArgs) { + public async delete({ soClient, configurationId }: GetCaseConfigureArgs) { try { - this.log.debug(`Attempting to DELETE case configure ${caseConfigureId}`); - return await soClient.delete(CASE_CONFIGURE_SAVED_OBJECT, caseConfigureId); + this.log.debug(`Attempting to DELETE case configure ${configurationId}`); + return await soClient.delete(CASE_CONFIGURE_SAVED_OBJECT, configurationId); } catch (error) { - this.log.debug(`Error on DELETE case configure ${caseConfigureId}: ${error}`); + this.log.debug(`Error on DELETE case configure ${configurationId}: ${error}`); throw error; } } - public async get({ soClient, caseConfigureId }: GetCaseConfigureArgs) { + public async get({ soClient, configurationId }: GetCaseConfigureArgs) { try { - this.log.debug(`Attempting to GET case configuration ${caseConfigureId}`); + this.log.debug(`Attempting to GET case configuration ${configurationId}`); return await soClient.get( CASE_CONFIGURE_SAVED_OBJECT, - caseConfigureId + configurationId ); } catch (error) { - this.log.debug(`Error on GET case configuration ${caseConfigureId}: ${error}`); + this.log.debug(`Error on GET case configuration ${configurationId}: ${error}`); throw error; } } @@ -60,7 +63,10 @@ export class CaseConfigureService { try { this.log.debug(`Attempting to find all case configuration`); return await soClient.find({ - ...options, + ...cloneDeep(options), + // Get the latest configuration + sortField: 'created_at', + sortOrder: 'desc', type: CASE_CONFIGURE_SAVED_OBJECT, }); } catch (error) { @@ -69,30 +75,34 @@ export class CaseConfigureService { } } - public async post({ soClient, attributes }: PostCaseConfigureArgs) { + public async post({ soClient, attributes, id }: PostCaseConfigureArgs) { try { this.log.debug(`Attempting to POST a new case configuration`); - return await soClient.create(CASE_CONFIGURE_SAVED_OBJECT, { - ...attributes, - }); + return await soClient.create( + CASE_CONFIGURE_SAVED_OBJECT, + { + ...attributes, + }, + { id } + ); } catch (error) { this.log.debug(`Error on POST a new case configuration: ${error}`); throw error; } } - public async patch({ soClient, caseConfigureId, updatedAttributes }: PatchCaseConfigureArgs) { + public async patch({ soClient, configurationId, updatedAttributes }: PatchCaseConfigureArgs) { try { - this.log.debug(`Attempting to UPDATE case configuration ${caseConfigureId}`); + this.log.debug(`Attempting to UPDATE case configuration ${configurationId}`); return await soClient.update( CASE_CONFIGURE_SAVED_OBJECT, - caseConfigureId, + configurationId, { ...updatedAttributes, } ); } catch (error) { - this.log.debug(`Error on UPDATE case configuration ${caseConfigureId}: ${error}`); + this.log.debug(`Error on UPDATE case configuration ${configurationId}: ${error}`); throw error; } } diff --git a/x-pack/plugins/cases/server/services/connector_mappings/index.ts b/x-pack/plugins/cases/server/services/connector_mappings/index.ts index 0d51e12a55ac7..4489233645821 100644 --- a/x-pack/plugins/cases/server/services/connector_mappings/index.ts +++ b/x-pack/plugins/cases/server/services/connector_mappings/index.ts @@ -7,14 +7,15 @@ import { Logger, SavedObjectReference, SavedObjectsClientContract } from 'kibana/server'; -import { ConnectorMappings, SavedObjectFindOptions } from '../../../common/api'; +import { ConnectorMappings } from '../../../common/api'; import { CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT } from '../../../common/constants'; +import { SavedObjectFindOptionsKueryNode } from '../../common'; interface ClientArgs { soClient: SavedObjectsClientContract; } interface FindConnectorMappingsArgs extends ClientArgs { - options?: SavedObjectFindOptions; + options?: SavedObjectFindOptionsKueryNode; } interface PostConnectorMappingsArgs extends ClientArgs { @@ -22,6 +23,12 @@ interface PostConnectorMappingsArgs extends ClientArgs { references: SavedObjectReference[]; } +interface UpdateConnectorMappingsArgs extends ClientArgs { + mappingId: string; + attributes: Partial; + references: SavedObjectReference[]; +} + export class ConnectorMappingsService { constructor(private readonly log: Logger) {} @@ -53,4 +60,26 @@ export class ConnectorMappingsService { throw error; } } + + public async update({ + soClient, + mappingId, + attributes, + references, + }: UpdateConnectorMappingsArgs) { + try { + this.log.debug(`Attempting to UPDATE connector mappings ${mappingId}`); + return await soClient.update( + CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, + mappingId, + attributes, + { + references, + } + ); + } catch (error) { + this.log.error(`Error on UPDATE connector mappings ${mappingId}: ${error}`); + throw error; + } + } } diff --git a/x-pack/plugins/cases/server/services/mocks.ts b/x-pack/plugins/cases/server/services/mocks.ts index 5e5b4ff31309e..2b58cd023a8ad 100644 --- a/x-pack/plugins/cases/server/services/mocks.ts +++ b/x-pack/plugins/cases/server/services/mocks.ts @@ -71,7 +71,11 @@ export const createConfigureServiceMock = (): CaseConfigureServiceMock => { }; export const connectorMappingsServiceMock = (): ConnectorMappingsServiceMock => { - const service: PublicMethodsOf = { find: jest.fn(), post: jest.fn() }; + const service: PublicMethodsOf = { + find: jest.fn(), + post: jest.fn(), + update: jest.fn(), + }; // the cast here is required because jest.Mocked tries to include private members and would throw an error return (service as unknown) as ConnectorMappingsServiceMock; 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 1b1932f864090..4ca2bd01d9a2d 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 @@ -72,6 +72,9 @@ describe(`cases`, () => { Array [ "cases:1.0.0-zeta1:observability/getCase", "cases:1.0.0-zeta1:observability/findCases", + "cases:1.0.0-zeta1:observability/getTags", + "cases:1.0.0-zeta1:observability/getReporters", + "cases:1.0.0-zeta1:observability/findConfigurations", ] `); }); @@ -107,9 +110,14 @@ describe(`cases`, () => { Array [ "cases:1.0.0-zeta1:security/getCase", "cases:1.0.0-zeta1:security/findCases", + "cases:1.0.0-zeta1:security/getTags", + "cases:1.0.0-zeta1:security/getReporters", + "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/createConfiguration", + "cases:1.0.0-zeta1:security/updateConfiguration", ] `); }); @@ -146,11 +154,19 @@ describe(`cases`, () => { Array [ "cases:1.0.0-zeta1:security/getCase", "cases:1.0.0-zeta1:security/findCases", + "cases:1.0.0-zeta1:security/getTags", + "cases:1.0.0-zeta1:security/getReporters", + "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/createConfiguration", + "cases:1.0.0-zeta1:security/updateConfiguration", "cases:1.0.0-zeta1:obs/getCase", "cases:1.0.0-zeta1:obs/findCases", + "cases:1.0.0-zeta1:obs/getTags", + "cases:1.0.0-zeta1:obs/getReporters", + "cases:1.0.0-zeta1:obs/findConfigurations", ] `); }); @@ -187,18 +203,34 @@ describe(`cases`, () => { Array [ "cases:1.0.0-zeta1:security/getCase", "cases:1.0.0-zeta1:security/findCases", + "cases:1.0.0-zeta1:security/getTags", + "cases:1.0.0-zeta1:security/getReporters", + "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/createConfiguration", + "cases:1.0.0-zeta1:security/updateConfiguration", "cases:1.0.0-zeta1:other-security/getCase", "cases:1.0.0-zeta1:other-security/findCases", + "cases:1.0.0-zeta1:other-security/getTags", + "cases:1.0.0-zeta1:other-security/getReporters", + "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/createConfiguration", + "cases:1.0.0-zeta1:other-security/updateConfiguration", "cases:1.0.0-zeta1:obs/getCase", "cases:1.0.0-zeta1:obs/findCases", + "cases:1.0.0-zeta1:obs/getTags", + "cases:1.0.0-zeta1:obs/getReporters", + "cases:1.0.0-zeta1:obs/findConfigurations", "cases:1.0.0-zeta1:other-obs/getCase", "cases:1.0.0-zeta1:other-obs/findCases", + "cases:1.0.0-zeta1:other-obs/getTags", + "cases:1.0.0-zeta1:other-obs/getReporters", + "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 8608653c41b34..1ff72e9ad3fe1 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 @@ -12,8 +12,20 @@ import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; // if you add a value here you'll likely also need to make changes here: // x-pack/plugins/cases/server/authorization/index.ts -const readOperations: string[] = ['getCase', 'findCases']; -const writeOperations: string[] = ['createCase', 'deleteCase', 'updateCase']; +const readOperations: string[] = [ + 'getCase', + 'findCases', + 'getTags', + 'getReporters', + 'findConfigurations', +]; +const writeOperations: string[] = [ + 'createCase', + 'deleteCase', + 'updateCase', + 'createConfiguration', + 'updateConfiguration', +]; const allOperations: string[] = [...readOperations, ...writeOperations]; export class FeaturePrivilegeCasesBuilder extends BaseFeaturePrivilegeBuilder { diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/api.test.ts b/x-pack/plugins/security_solution/public/cases/containers/configure/api.test.ts index 0c7ae422be861..999cb8d29d745 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/configure/api.test.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/api.test.ts @@ -86,7 +86,7 @@ describe('Case Configuration API', () => { await postCaseConfigure(caseConfigurationMock, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith('/api/cases/configure', { body: - '{"connector":{"id":"123","name":"My connector","type":".jira","fields":null},"closure_type":"close-by-user"}', + '{"connector":{"id":"123","name":"My connector","type":".jira","fields":null},"owner":"securitySolution","closure_type":"close-by-user"}', method: 'POST', signal: abortCtrl.signal, }); diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/mock.ts b/x-pack/plugins/security_solution/public/cases/containers/configure/mock.ts index 4e71c9a990ece..a76ca16d799aa 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/configure/mock.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/mock.ts @@ -116,6 +116,7 @@ export const actionTypesMock: ActionTypeConnector[] = [ ]; export const caseConfigurationResposeMock: CasesConfigureResponse = { + id: '123', created_at: '2020-04-06T13:03:18.657Z', created_by: { username: 'elastic', full_name: 'Elastic', email: 'elastic@elastic.co' }, connector: { @@ -129,6 +130,7 @@ export const caseConfigurationResposeMock: CasesConfigureResponse = { mappings: [], updated_at: '2020-04-06T14:03:18.657Z', updated_by: { username: 'elastic', full_name: 'Elastic', email: 'elastic@elastic.co' }, + owner: 'securitySolution', version: 'WzHJ12', }; @@ -139,10 +141,12 @@ export const caseConfigurationMock: CasesConfigureRequest = { type: ConnectorTypes.jira, fields: null, }, + owner: 'securitySolution', closure_type: 'close-by-user', }; export const caseConfigurationCamelCaseResponseMock: CaseConfigure = { + id: '123', createdAt: '2020-04-06T13:03:18.657Z', createdBy: { username: 'elastic', fullName: 'Elastic', email: 'elastic@elastic.co' }, connector: { @@ -157,4 +161,5 @@ export const caseConfigurationCamelCaseResponseMock: CaseConfigure = { updatedAt: '2020-04-06T14:03:18.657Z', updatedBy: { username: 'elastic', fullName: 'Elastic', email: 'elastic@elastic.co' }, version: 'WzHJ12', + owner: 'securitySolution', }; diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/types.ts b/x-pack/plugins/security_solution/public/cases/containers/configure/types.ts index aa86d1bfdb0b1..b628705569bd0 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/configure/types.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/types.ts @@ -34,6 +34,7 @@ export interface CaseConnectorMapping { } export interface CaseConfigure { + id: string; closureType: ClosureType; connector: CasesConfigure['connector']; createdAt: string; @@ -43,4 +44,5 @@ export interface CaseConfigure { updatedAt: string; updatedBy: ElasticUser; version: string; + owner: string; } diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.tsx b/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.tsx index 2ec2a73363bfe..ca817747e9191 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.tsx @@ -278,6 +278,8 @@ export const useCaseConfigure = (): ReturnUseCaseConfigure => { const connectorObj = { connector, closure_type: closureType, + // TODO: use constant after https://github.com/elastic/kibana/pull/97646 is being merged + owner: 'securitySolution', }; const res = 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 f7a54244b3bf5..bcc23896f85f8 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 @@ -6,11 +6,17 @@ */ import { FtrProviderContext as CommonFtrProviderContext } from '../../../common/ftr_provider_context'; -import { Role, User } from './types'; +import { Role, User, UserInfo } from './types'; import { users } from './users'; import { roles } from './roles'; import { spaces } from './spaces'; +export const getUserInfo = (user: User): UserInfo => ({ + username: user.username, + full_name: user.username.replace('_', ' '), + email: `${user.username}@elastic.co`, +}); + export const createSpaces = async (getService: CommonFtrProviderContext['getService']) => { const spacesService = getService('spaces'); for (const space of spaces) { @@ -25,12 +31,14 @@ const createUsersAndRoles = async (getService: CommonFtrProviderContext['getServ return await security.role.create(name, privileges); }; - const createUser = async ({ username, password, roles: userRoles }: User) => { - return await security.user.create(username, { - password, - roles: userRoles, - full_name: username.replace('_', ' '), - email: `${username}@elastic.co`, + const createUser = async (user: User) => { + const userInfo = getUserInfo(user); + + return await security.user.create(user.username, { + password: user.password, + roles: user.roles, + full_name: userInfo.full_name, + email: userInfo.email, }); }; diff --git a/x-pack/test/case_api_integration/common/lib/authentication/types.ts b/x-pack/test/case_api_integration/common/lib/authentication/types.ts index 2b61ae992fa64..3bf3629441f93 100644 --- a/x-pack/test/case_api_integration/common/lib/authentication/types.ts +++ b/x-pack/test/case_api_integration/common/lib/authentication/types.ts @@ -19,6 +19,12 @@ export interface User { roles: string[]; } +export interface UserInfo { + username: string; + full_name: string; + email: string; +} + interface FeaturesPrivileges { [featureId: string]: string[]; } 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 2ff5e9d71985b..0a0151d37d3f8 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -16,7 +16,9 @@ import { CASES_URL, CASE_CONFIGURE_CONNECTORS_URL, CASE_CONFIGURE_URL, + CASE_REPORTERS_URL, CASE_STATUS_URL, + CASE_TAGS_URL, SUB_CASES_PATCH_DEL_URL, } from '../../../../plugins/cases/common/constants'; import { @@ -40,13 +42,15 @@ import { CommentPatchRequest, CasesConfigurePatch, CasesStatusResponse, + CasesConfigurationsResponse, } from '../../../../plugins/cases/common/api'; -import { getPostCaseRequest, postCollectionReq, postCommentGenAlertReq } from './mock'; +import { postCollectionReq, postCommentGenAlertReq } from './mock'; import { 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'; import { User } from './authentication/types'; +import { superUser } from './authentication/users'; function toArray(input: T | T[]): T[] { if (Array.isArray(input)) { @@ -281,6 +285,7 @@ export const getConfigurationRequest = ({ fields, } as CaseConnector, closure_type: 'close-by-user', + owner: 'securitySolutionFixture', }; }; @@ -527,72 +532,32 @@ export const deleteMappings = async (es: KibanaClient): Promise => { }); }; -export const getSpaceUrlPrefix = (spaceId: string) => { +export const getSpaceUrlPrefix = (spaceId?: string | null) => { return spaceId && spaceId !== 'default' ? `/s/${spaceId}` : ``; }; -export const createCaseAsUser = async ({ - supertestWithoutAuth, - user, - space, - owner, - expectedHttpCode = 200, -}: { - supertestWithoutAuth: st.SuperTest; - user: User; - space: string; - owner?: string; - expectedHttpCode?: number; -}): Promise => { - const { body: theCase } = await supertestWithoutAuth - .post(`${getSpaceUrlPrefix(space)}${CASES_URL}`) - .auth(user.username, user.password) - .set('kbn-xsrf', 'true') - .send(getPostCaseRequest({ owner })) - .expect(expectedHttpCode); - - return theCase; -}; - -export const findCasesAsUser = async ({ - supertestWithoutAuth, - user, - space, - expectedHttpCode = 200, - appendToUrl = '', -}: { - supertestWithoutAuth: st.SuperTest; - user: User; - space: string; - expectedHttpCode?: number; - appendToUrl?: string; -}): Promise => { - const { body: res } = await supertestWithoutAuth - .get(`${getSpaceUrlPrefix(space)}${CASES_URL}/_find?sortOrder=asc&${appendToUrl}`) - .auth(user.username, user.password) - .set('kbn-xsrf', 'true') - .send() - .expect(expectedHttpCode); - - return res; -}; +interface OwnerEntity { + owner: string; +} export const ensureSavedObjectIsAuthorized = ( - cases: CaseResponse[], + entities: OwnerEntity[], numberOfExpectedCases: number, owners: string[] ) => { - expect(cases.length).to.eql(numberOfExpectedCases); - cases.forEach((theCase) => expect(owners.includes(theCase.owner)).to.be(true)); + expect(entities.length).to.eql(numberOfExpectedCases); + entities.forEach((entity) => expect(owners.includes(entity.owner)).to.be(true)); }; export const createCase = async ( supertest: st.SuperTest, params: CasePostRequest, - expectedHttpCode: number = 200 + expectedHttpCode: number = 200, + auth: { user: User; space: string | null } = { user: superUser, space: null } ): Promise => { const { body: theCase } = await supertest - .post(CASES_URL) + .post(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}`) + .auth(auth.user.username, auth.user.password) .set('kbn-xsrf', 'true') .send(params) .expect(expectedHttpCode); @@ -607,13 +572,16 @@ export const deleteCases = async ({ supertest, caseIDs, expectedHttpCode = 204, + auth = { user: superUser, space: null }, }: { supertest: st.SuperTest; caseIDs: string[]; expectedHttpCode?: number; + auth?: { user: User; space: string | null }; }) => { const { body } = await supertest - .delete(`${CASES_URL}`) + .delete(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}`) + .auth(auth.user.username, auth.user.password) // we need to json stringify here because just passing in the array of case IDs will cause a 400 with Kibana // not being able to parse the array correctly. The format ids=["1", "2"] seems to work, which stringify outputs. .query({ ids: JSON.stringify(caseIDs) }) @@ -628,10 +596,12 @@ export const createComment = async ( supertest: st.SuperTest, caseId: string, params: CommentRequest, - expectedHttpCode: number = 200 + expectedHttpCode: number = 200, + auth: { user: User; space: string | null } = { user: superUser, space: null } ): Promise => { const { body: theCase } = await supertest - .post(`${CASES_URL}/${caseId}/comments`) + .post(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}/${caseId}/comments`) + .auth(auth.user.username, auth.user.password) .set('kbn-xsrf', 'true') .send(params) .expect(expectedHttpCode); @@ -647,7 +617,6 @@ export const getAllUserAction = async ( const { body: userActions } = await supertest .get(`${CASES_URL}/${caseId}/user_actions`) .set('kbn-xsrf', 'true') - .send() .expect(expectedHttpCode); return userActions; @@ -690,7 +659,6 @@ export const getAllComments = async ( const { body: comments } = await supertest .get(`${CASES_URL}/${caseId}/comments`) .set('kbn-xsrf', 'true') - .send() .expect(expectedHttpCode); return comments; @@ -705,7 +673,6 @@ export const getComment = async ( const { body: comment } = await supertest .get(`${CASES_URL}/${caseId}/comments/${commentId}`) .set('kbn-xsrf', 'true') - .send() .expect(expectedHttpCode); return comment; @@ -726,14 +693,22 @@ export const updateComment = async ( return res; }; -export const getConfiguration = async ( - supertest: st.SuperTest, - expectedHttpCode: number = 200 -): Promise => { +export const getConfiguration = async ({ + supertest, + query = { owner: 'securitySolutionFixture' }, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: st.SuperTest; + query?: Record; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): Promise => { const { body: configuration } = await supertest - .get(CASE_CONFIGURE_URL) + .get(`${getSpaceUrlPrefix(auth.space)}${CASE_CONFIGURE_URL}`) + .auth(auth.user.username, auth.user.password) .set('kbn-xsrf', 'true') - .send() + .query(query) .expect(expectedHttpCode); return configuration; @@ -742,10 +717,12 @@ export const getConfiguration = async ( export const createConfiguration = async ( supertest: st.SuperTest, req: CasesConfigureRequest = getConfigurationRequest(), - expectedHttpCode: number = 200 + expectedHttpCode: number = 200, + auth: { user: User; space: string | null } = { user: superUser, space: null } ): Promise => { const { body: configuration } = await supertest - .post(CASE_CONFIGURE_URL) + .post(`${getSpaceUrlPrefix(auth.space)}${CASE_CONFIGURE_URL}`) + .auth(auth.user.username, auth.user.password) .set('kbn-xsrf', 'true') .send(req) .expect(expectedHttpCode); @@ -778,7 +755,6 @@ export const getCaseConnectors = async ( const { body: connectors } = await supertest .get(`${CASE_CONFIGURE_CONNECTORS_URL}/_find`) .set('kbn-xsrf', 'true') - .send() .expect(expectedHttpCode); return connectors; @@ -786,11 +762,14 @@ export const getCaseConnectors = async ( export const updateConfiguration = async ( supertest: st.SuperTest, + id: string, req: CasesConfigurePatch, - expectedHttpCode: number = 200 + expectedHttpCode: number = 200, + auth: { user: User; space: string | null } = { user: superUser, space: null } ): Promise => { const { body: configuration } = await supertest - .patch(CASE_CONFIGURE_URL) + .patch(`${getSpaceUrlPrefix(auth.space)}${CASE_CONFIGURE_URL}/${id}`) + .auth(auth.user.username, auth.user.password) .set('kbn-xsrf', 'true') .send(req) .expect(expectedHttpCode); @@ -805,34 +784,49 @@ export const getAllCasesStatuses = async ( const { body: statuses } = await supertest .get(CASE_STATUS_URL) .set('kbn-xsrf', 'true') - .send() .expect(expectedHttpCode); return statuses; }; -export const getCase = async ( - supertest: st.SuperTest, - caseId: string, - includeComments: boolean = false, - expectedHttpCode: number = 200 -): Promise => { +export const getCase = async ({ + supertest, + caseId, + includeComments = false, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: st.SuperTest; + caseId: string; + includeComments?: boolean; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): Promise => { const { body: theCase } = await supertest - .get(`${CASES_URL}/${caseId}?includeComments=${includeComments}`) + .get( + `${getSpaceUrlPrefix(auth?.space)}${CASES_URL}/${caseId}?includeComments=${includeComments}` + ) .set('kbn-xsrf', 'true') - .send() + .auth(auth.user.username, auth.user.password) .expect(expectedHttpCode); return theCase; }; -export const findCases = async ( - supertest: st.SuperTest, - query: Record = {}, - expectedHttpCode: number = 200 -): Promise => { +export const findCases = async ({ + supertest, + query = {}, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: st.SuperTest; + query?: Record; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): Promise => { const { body: res } = await supertest - .get(`${CASES_URL}/_find`) + .get(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}/_find`) + .auth(auth.user.username, auth.user.password) .query({ sortOrder: 'asc', ...query }) .set('kbn-xsrf', 'true') .send() @@ -841,6 +835,48 @@ export const findCases = async ( return res; }; +export const getTags = async ({ + supertest, + query = {}, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: st.SuperTest; + query?: Record; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): Promise => { + const { body: res } = await supertest + .get(`${getSpaceUrlPrefix(auth.space)}${CASE_TAGS_URL}`) + .auth(auth.user.username, auth.user.password) + .set('kbn-xsrf', 'true') + .query({ ...query }) + .expect(expectedHttpCode); + + return res; +}; + +export const getReporters = async ({ + supertest, + query = {}, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: st.SuperTest; + query?: Record; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): Promise => { + const { body: res } = await supertest + .get(`${getSpaceUrlPrefix(auth.space)}${CASE_REPORTERS_URL}`) + .auth(auth.user.username, auth.user.password) + .set('kbn-xsrf', 'true') + .query({ ...query }) + .expect(expectedHttpCode); + + return res; +}; + export const pushCase = async ( supertest: st.SuperTest, caseId: string, 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 2c50ac8a453f9..9ebc16f5e07aa 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 @@ -6,10 +6,10 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; -import { getPostCaseRequest, postCommentUserReq } from '../../../../common/lib/mock'; +import { defaultUser, getPostCaseRequest, postCommentUserReq } from '../../../../common/lib/mock'; import { createCaseAction, createSubCase, @@ -22,12 +22,26 @@ import { deleteCases, createComment, getComment, + getAllUserAction, + removeServerGeneratedPropertiesFromUserAction, + getCase, } from '../../../../common/lib/utils'; import { getSubCaseDetailsUrl } from '../../../../../../plugins/cases/common/api/helpers'; import { CaseResponse } from '../../../../../../plugins/cases/common/api'; +import { + secOnly, + secOnlyRead, + globalRead, + obsOnlyRead, + obsSecRead, + noKibanaPrivileges, + obsOnly, + superUser, +} from '../../../../common/lib/authentication/users'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); const supertest = getService('supertest'); const es = getService('es'); @@ -57,6 +71,33 @@ export default ({ getService }: FtrProviderContext): void => { await getComment(supertest, postedCase.id, patchedCase.comments![0].id, 404); }); + it('should create a user action when creating a case', async () => { + const postedCase = await createCase(supertest, getPostCaseRequest()); + await deleteCases({ supertest, caseIDs: [postedCase.id] }); + const userActions = await getAllUserAction(supertest, postedCase.id); + const creationUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); + + expect(creationUserAction).to.eql({ + action_field: [ + 'description', + 'status', + 'tags', + 'title', + 'connector', + 'settings', + 'owner', + 'comment', + ], + action: 'delete', + action_by: defaultUser, + old_value: null, + new_value: null, + case_id: `${postedCase.id}`, + comment_id: null, + sub_case_id: '', + }); + }); + it('unhappy path - 404s when case is not there', async () => { await deleteCases({ supertest, caseIDs: ['fake-id'], expectedHttpCode: 404 }); }); @@ -110,5 +151,136 @@ export default ({ getService }: FtrProviderContext): void => { await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(404); }); }); + + describe('rbac', () => { + it('User: security solution only - should delete a case', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: secOnly, + space: 'space1', + } + ); + + await deleteCases({ + supertest, + caseIDs: [postedCase.id], + expectedHttpCode: 204, + auth: { user: secOnly, space: 'space1' }, + }); + }); + + it('User: security solution only - should NOT delete a case of different owner', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: secOnly, + space: 'space1', + } + ); + + await deleteCases({ + supertest: supertestWithoutAuth, + caseIDs: [postedCase.id], + expectedHttpCode: 403, + auth: { user: obsOnly, space: 'space1' }, + }); + }); + + it('should get an error if the user has not permissions to all requested cases', async () => { + const caseSec = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: secOnly, + space: 'space1', + } + ); + + const caseObs = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { + user: obsOnly, + space: 'space1', + } + ); + + await deleteCases({ + supertest: supertestWithoutAuth, + caseIDs: [caseSec.id, caseObs.id], + expectedHttpCode: 403, + auth: { user: obsOnly, space: 'space1' }, + }); + + // Cases should have not been deleted. + await getCase({ + supertest: supertestWithoutAuth, + caseId: caseSec.id, + expectedHttpCode: 200, + auth: { user: superUser, space: 'space1' }, + }); + + await getCase({ + supertest: supertestWithoutAuth, + caseId: caseObs.id, + expectedHttpCode: 200, + auth: { user: superUser, space: 'space1' }, + }); + }); + + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT delete a case`, async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ); + + await deleteCases({ + supertest: supertestWithoutAuth, + caseIDs: [postedCase.id], + expectedHttpCode: 403, + auth: { user, space: 'space1' }, + }); + }); + } + + it('should NOT delete a case in a space with no permissions', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space2', + } + ); + + /** + * We expect a 404 because the bulkGet inside the delete + * route should return a 404 when requesting a case from + * a different space. + * */ + await deleteCases({ + supertest: supertestWithoutAuth, + caseIDs: [postedCase.id], + expectedHttpCode: 404, + auth: { user: secOnly, space: 'space1' }, + }); + }); + }); }); }; 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 ca3b0201c1454..c537d2477cb59 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 @@ -13,7 +13,12 @@ import { CASES_URL, SUB_CASES_PATCH_DEL_URL, } from '../../../../../../plugins/cases/common/constants'; -import { postCaseReq, postCommentUserReq, findCasesResp } from '../../../../common/lib/mock'; +import { + postCaseReq, + postCommentUserReq, + findCasesResp, + getPostCaseRequest, +} from '../../../../common/lib/mock'; import { deleteAllCaseItems, createSubCase, @@ -21,9 +26,7 @@ import { CreateSubCaseResp, createCaseAction, deleteCaseAction, - createCaseAsUser, ensureSavedObjectIsAuthorized, - findCasesAsUser, findCases, createCase, updateCase, @@ -61,7 +64,7 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should return empty response', async () => { - const cases = await findCases(supertest); + const cases = await findCases({ supertest }); expect(cases).to.eql(findCasesResp); }); @@ -70,7 +73,7 @@ export default ({ getService }: FtrProviderContext): void => { const b = await createCase(supertest, postCaseReq); const c = await createCase(supertest, postCaseReq); - const cases = await findCases(supertest); + const cases = await findCases({ supertest }); expect(cases).to.eql({ ...findCasesResp, @@ -83,7 +86,7 @@ export default ({ getService }: FtrProviderContext): void => { it('filters by tags', async () => { await createCase(supertest, postCaseReq); const postedCase = await createCase(supertest, { ...postCaseReq, tags: ['unique'] }); - const cases = await findCases(supertest, { tags: ['unique'] }); + const cases = await findCases({ supertest, query: { tags: ['unique'] } }); expect(cases).to.eql({ ...findCasesResp, @@ -106,7 +109,7 @@ export default ({ getService }: FtrProviderContext): void => { ], }); - const cases = await findCases(supertest, { status: CaseStatuses.closed }); + const cases = await findCases({ supertest, query: { status: CaseStatuses.closed } }); expect(cases).to.eql({ ...findCasesResp, @@ -120,7 +123,7 @@ export default ({ getService }: FtrProviderContext): void => { it('filters by reporters', async () => { const postedCase = await createCase(supertest, postCaseReq); - const cases = await findCases(supertest, { reporters: 'elastic' }); + const cases = await findCases({ supertest, query: { reporters: 'elastic' } }); expect(cases).to.eql({ ...findCasesResp, @@ -137,7 +140,7 @@ export default ({ getService }: FtrProviderContext): void => { await createComment(supertest, postedCase.id, postCommentUserReq); const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); - const cases = await findCases(supertest); + const cases = await findCases({ supertest }); expect(cases).to.eql({ ...findCasesResp, total: 1, @@ -177,14 +180,14 @@ export default ({ getService }: FtrProviderContext): void => { ], }); - const cases = await findCases(supertest); + const cases = await findCases({ supertest }); expect(cases.count_open_cases).to.eql(1); expect(cases.count_closed_cases).to.eql(1); expect(cases.count_in_progress_cases).to.eql(1); }); it('unhappy path - 400s when bad query supplied', async () => { - await findCases(supertest, { perPage: true }, 400); + await findCases({ supertest, query: { perPage: true }, expectedHttpCode: 400 }); }); // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests @@ -233,7 +236,7 @@ export default ({ getService }: FtrProviderContext): void => { }); }); it('correctly counts stats without using a filter', async () => { - const cases = await findCases(supertest); + const cases = await findCases({ supertest }); expect(cases.total).to.eql(3); expect(cases.count_closed_cases).to.eql(1); @@ -242,7 +245,7 @@ export default ({ getService }: FtrProviderContext): void => { }); it('correctly counts stats with a filter for open cases', async () => { - const cases = await findCases(supertest, { status: CaseStatuses.open }); + const cases = await findCases({ supertest, query: { status: CaseStatuses.open } }); expect(cases.cases.length).to.eql(1); @@ -258,7 +261,7 @@ export default ({ getService }: FtrProviderContext): void => { }); it('correctly counts stats with a filter for individual cases', async () => { - const cases = await findCases(supertest, { type: CaseType.individual }); + const cases = await findCases({ supertest, query: { type: CaseType.individual } }); expect(cases.total).to.eql(2); expect(cases.count_closed_cases).to.eql(1); @@ -270,7 +273,7 @@ export default ({ getService }: FtrProviderContext): void => { // this will force the first sub case attached to the collection to be closed // so we'll have one closed sub case and one open sub case await createSubCase({ supertest, caseID: collection.newSubCaseInfo.id, actionID }); - const cases = await findCases(supertest, { type: CaseType.collection }); + const cases = await findCases({ supertest, query: { type: CaseType.collection } }); expect(cases.total).to.eql(1); expect(cases.cases[0].subCases?.length).to.eql(2); @@ -283,9 +286,12 @@ export default ({ getService }: FtrProviderContext): void => { // this will force the first sub case attached to the collection to be closed // so we'll have one closed sub case and one open sub case await createSubCase({ supertest, caseID: collection.newSubCaseInfo.id, actionID }); - const cases = await findCases(supertest, { - type: CaseType.collection, - status: CaseStatuses.open, + const cases = await findCases({ + supertest, + query: { + type: CaseType.collection, + status: CaseStatuses.open, + }, }); expect(cases.total).to.eql(1); @@ -305,7 +311,7 @@ export default ({ getService }: FtrProviderContext): void => { .send() .expect(204); - const cases = await findCases(supertest, { type: CaseType.collection }); + const cases = await findCases({ supertest, query: { type: CaseType.collection } }); // it should include the collection without sub cases because we did not pass in a filter on status expect(cases.total).to.eql(3); @@ -324,7 +330,7 @@ export default ({ getService }: FtrProviderContext): void => { .send() .expect(204); - const cases = await findCases(supertest, { tags: ['defacement'] }); + const cases = await findCases({ supertest, query: { tags: ['defacement'] } }); // it should include the collection without sub cases because we did not pass in a filter on status expect(cases.total).to.eql(3); @@ -334,7 +340,7 @@ export default ({ getService }: FtrProviderContext): void => { }); it('does not return collections without sub cases matching the requested status', async () => { - const cases = await findCases(supertest, { status: CaseStatuses.closed }); + const cases = await findCases({ supertest, query: { status: CaseStatuses.closed } }); expect(cases.cases.length).to.eql(1); // it should not include the collection that has a sub case as in-progress @@ -357,7 +363,7 @@ export default ({ getService }: FtrProviderContext): void => { .send() .expect(204); - const cases = await findCases(supertest, { status: CaseStatuses.closed }); + const cases = await findCases({ supertest, query: { status: CaseStatuses.closed } }); expect(cases.cases.length).to.eql(1); @@ -418,9 +424,12 @@ export default ({ getService }: FtrProviderContext): void => { }; it('returns the correct total when perPage is less than the total', async () => { - const cases = await findCases(supertest, { - page: 1, - perPage: 5, + const cases = await findCases({ + supertest, + query: { + page: 1, + perPage: 5, + }, }); expect(cases.cases.length).to.eql(5); @@ -433,9 +442,12 @@ export default ({ getService }: FtrProviderContext): void => { }); it('returns the correct total when perPage is greater than the total', async () => { - const cases = await findCases(supertest, { - page: 1, - perPage: 11, + const cases = await findCases({ + supertest, + query: { + page: 1, + perPage: 11, + }, }); expect(cases.total).to.eql(10); @@ -448,9 +460,12 @@ export default ({ getService }: FtrProviderContext): void => { }); it('returns the correct total when perPage is equal to the total', async () => { - const cases = await findCases(supertest, { - page: 1, - perPage: 10, + const cases = await findCases({ + supertest, + query: { + page: 1, + perPage: 10, + }, }); expect(cases.total).to.eql(10); @@ -464,9 +479,12 @@ export default ({ getService }: FtrProviderContext): void => { it('returns the second page of results', async () => { const perPage = 5; - const cases = await findCases(supertest, { - page: 2, - perPage, + const cases = await findCases({ + supertest, + query: { + page: 2, + perPage, + }, }); expect(cases.total).to.eql(10); @@ -492,9 +510,12 @@ export default ({ getService }: FtrProviderContext): void => { // it's less than or equal here because the page starts at 1, so page 5 is a valid page number // and should have case titles 9, and 10 for (let currentPage = 1; currentPage <= total / perPage; currentPage++) { - const cases = await findCases(supertest, { - page: currentPage, - perPage, + const cases = await findCases({ + supertest, + query: { + page: currentPage, + perPage, + }, }); expect(cases.total).to.eql(total); @@ -518,10 +539,13 @@ export default ({ getService }: FtrProviderContext): void => { }); it('retrieves the last three cases', async () => { - const cases = await findCases(supertest, { - // this should skip the first 7 cases and only return the last 3 - page: 2, - perPage: 7, + const cases = await findCases({ + supertest, + query: { + // this should skip the first 7 cases and only return the last 3 + page: 2, + perPage: 7, + }, }); expect(cases.total).to.eql(10); @@ -542,19 +566,25 @@ export default ({ getService }: FtrProviderContext): void => { it('should return the correct cases', async () => { await Promise.all([ // Create case owned by the security solution user - await createCaseAsUser({ + await createCase( supertestWithoutAuth, - user: secOnly, - space: 'space1', - owner: 'securitySolutionFixture', - }), + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: secOnly, + space: 'space1', + } + ), // Create case owned by the observability user - await createCaseAsUser({ + await createCase( supertestWithoutAuth, - user: obsOnly, - space: 'space1', - owner: 'observabilityFixture', - }), + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { + user: obsOnly, + space: 'space1', + } + ), ]); for (const scenario of [ @@ -576,10 +606,12 @@ export default ({ getService }: FtrProviderContext): void => { owners: ['securitySolutionFixture', 'observabilityFixture'], }, ]) { - const res = await findCasesAsUser({ - supertestWithoutAuth, - user: scenario.user, - space: 'space1', + const res = await findCases({ + supertest: supertestWithoutAuth, + auth: { + user: scenario.user, + space: 'space1', + }, }); ensureSavedObjectIsAuthorized(res.cases, scenario.numberOfExpectedCases, scenario.owners); @@ -594,18 +626,23 @@ export default ({ getService }: FtrProviderContext): void => { scenario.space } - should NOT read a case`, async () => { // super user creates a case at the appropriate space - await createCaseAsUser({ + await createCase( supertestWithoutAuth, - user: superUser, - space: scenario.space, - owner: 'securitySolutionFixture', - }); + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: scenario.space, + } + ); // user should not be able to read cases at the appropriate space - await findCasesAsUser({ - supertestWithoutAuth, - user: scenario.user, - space: scenario.space, + await findCases({ + supertest: supertestWithoutAuth, + auth: { + user: scenario.user, + space: scenario.space, + }, expectedHttpCode: 403, }); }); @@ -614,26 +651,37 @@ export default ({ getService }: FtrProviderContext): void => { it('should return the correct cases when trying to exploit RBAC through the search query parameter', async () => { await Promise.all([ // super user creates a case with owner securitySolutionFixture - await createCaseAsUser({ + await createCase( supertestWithoutAuth, - user: superUser, - space: 'space1', - owner: 'securitySolutionFixture', - }), + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ), // super user creates a case with owner observabilityFixture - await createCaseAsUser({ + await createCase( supertestWithoutAuth, - user: superUser, - space: 'space1', - owner: 'observabilityFixture', - }), + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ), ]); - const res = await findCasesAsUser({ - supertestWithoutAuth, - user: secOnly, - space: 'space1', - appendToUrl: 'search=securitySolutionFixture+observabilityFixture&searchFields=owner', + const res = await findCases({ + supertest: supertestWithoutAuth, + query: { + search: 'securitySolutionFixture observabilityFixture', + searchFields: 'owner', + }, + auth: { + user: secOnly, + space: 'space1', + }, }); ensureSavedObjectIsAuthorized(res.cases, 1, ['securitySolutionFixture']); @@ -677,25 +725,36 @@ export default ({ getService }: FtrProviderContext): void => { it('should respect the owner filter when having permissions', async () => { await Promise.all([ - await createCaseAsUser({ + await createCase( supertestWithoutAuth, - user: obsSec, - space: 'space1', - owner: 'securitySolutionFixture', - }), - await createCaseAsUser({ + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: obsSec, + space: 'space1', + } + ), + await createCase( supertestWithoutAuth, - user: obsSec, - space: 'space1', - owner: 'observabilityFixture', - }), + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { + user: obsSec, + space: 'space1', + } + ), ]); - const res = await findCasesAsUser({ - supertestWithoutAuth, - user: obsSec, - space: 'space1', - appendToUrl: 'owner=securitySolutionFixture', + const res = await findCases({ + supertest: supertestWithoutAuth, + query: { + owner: 'securitySolutionFixture', + searchFields: 'owner', + }, + auth: { + user: obsSec, + space: 'space1', + }, }); ensureSavedObjectIsAuthorized(res.cases, 1, ['securitySolutionFixture']); @@ -703,26 +762,36 @@ export default ({ getService }: FtrProviderContext): void => { it('should return the correct cases when trying to exploit RBAC through the owner query parameter', async () => { await Promise.all([ - await createCaseAsUser({ + await createCase( supertestWithoutAuth, - user: obsSec, - space: 'space1', - owner: 'securitySolutionFixture', - }), - await createCaseAsUser({ + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: obsSec, + space: 'space1', + } + ), + await createCase( supertestWithoutAuth, - user: obsSec, - space: 'space1', - owner: 'observabilityFixture', - }), + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { + user: obsSec, + space: 'space1', + } + ), ]); // User with permissions only to security solution request cases from observability - const res = await findCasesAsUser({ - supertestWithoutAuth, - user: secOnly, - space: 'space1', - appendToUrl: 'owner=securitySolutionFixture&owner=observabilityFixture', + const res = await findCases({ + supertest: supertestWithoutAuth, + query: { + owner: ['securitySolutionFixture', 'observabilityFixture'], + }, + auth: { + user: secOnly, + space: 'space1', + }, }); // Only security solution cases are being returned diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/get_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/get_case.ts index 8239cbadbaa2f..187c84be7c196 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/get_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/get_case.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 { AttributesTypeUser } from '../../../../../../plugins/cases/common/api'; import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; @@ -15,6 +15,7 @@ import { postCaseReq, postCaseResp, postCommentUserReq, + getPostCaseRequest, } from '../../../../common/lib/mock'; import { deleteCasesByESQuery, @@ -24,10 +25,23 @@ import { removeServerGeneratedPropertiesFromCase, removeServerGeneratedPropertiesFromSavedObject, } from '../../../../common/lib/utils'; +import { + secOnly, + obsOnly, + globalRead, + superUser, + secOnlyRead, + obsOnlyRead, + obsSecRead, + noKibanaPrivileges, + obsSec, +} from '../../../../common/lib/authentication/users'; +import { getUserInfo } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); describe('get_case', () => { @@ -36,8 +50,8 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should return a case with no comments', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const theCase = await getCase(supertest, postedCase.id, true); + const postedCase = await createCase(supertest, getPostCaseRequest()); + const theCase = await getCase({ supertest, caseId: postedCase.id, includeComments: true }); const data = removeServerGeneratedPropertiesFromCase(theCase); expect(data).to.eql(postCaseResp()); @@ -47,7 +61,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should return a case with comments', async () => { const postedCase = await createCase(supertest, postCaseReq); await createComment(supertest, postedCase.id, postCommentUserReq); - const theCase = await getCase(supertest, postedCase.id, true); + const theCase = await getCase({ supertest, caseId: postedCase.id, includeComments: true }); const comment = removeServerGeneratedPropertiesFromSavedObject( theCase.comments![0] as AttributesTypeUser @@ -78,5 +92,108 @@ export default ({ getService }: FtrProviderContext): void => { it('unhappy path - 404s when case is not there', async () => { await supertest.get(`${CASES_URL}/fake-id`).set('kbn-xsrf', 'true').send().expect(404); }); + + describe('rbac', () => { + it('should get a case', async () => { + const newCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ); + + for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { + const theCase = await getCase({ + supertest: supertestWithoutAuth, + caseId: newCase.id, + auth: { user, space: 'space1' }, + }); + + expect(theCase.owner).to.eql('securitySolutionFixture'); + } + }); + + it('should get a case with comments', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: secOnly, + space: 'space1', + } + ); + + await createComment(supertestWithoutAuth, postedCase.id, postCommentUserReq, 200, { + user: secOnly, + space: 'space1', + }); + + const theCase = await getCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + includeComments: true, + auth: { user: secOnly, space: 'space1' }, + }); + + const comment = removeServerGeneratedPropertiesFromSavedObject( + theCase.comments![0] as AttributesTypeUser + ); + + expect(theCase.comments?.length).to.eql(1); + expect(comment).to.eql({ + type: postCommentUserReq.type, + comment: postCommentUserReq.comment, + associationType: 'case', + created_by: getUserInfo(secOnly), + pushed_at: null, + pushed_by: null, + updated_by: null, + }); + }); + + it('should not get a case when the user does not have access to owner', async () => { + const newCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ); + + for (const user of [noKibanaPrivileges, obsOnly, obsOnlyRead]) { + await getCase({ + supertest: supertestWithoutAuth, + caseId: newCase.id, + expectedHttpCode: 403, + auth: { user, space: 'space1' }, + }); + } + }); + + it('should NOT get a case in a space with no permissions', async () => { + const newCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space2', + } + ); + + await getCase({ + supertest: supertestWithoutAuth, + caseId: newCase.id, + expectedHttpCode: 403, + auth: { user: secOnly, space: 'space2' }, + }); + }); + }); }); }; 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 1971cb5398b52..f2b9027cfb1f1 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 @@ -18,7 +18,6 @@ import { } from '../../../../../../plugins/cases/common/api'; import { getPostCaseRequest, postCaseResp, defaultUser } from '../../../../common/lib/mock'; import { - createCaseAsUser, deleteCasesByESQuery, createCase, removeServerGeneratedPropertiesFromCase, @@ -236,47 +235,56 @@ export default ({ getService }: FtrProviderContext): void => { describe('rbac', () => { it('User: security solution only - should create a case', async () => { - const theCase = await createCaseAsUser({ + const theCase = await createCase( supertestWithoutAuth, - user: secOnly, - space: 'space1', - owner: 'securitySolutionFixture', - }); + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: secOnly, + space: 'space1', + } + ); expect(theCase.owner).to.eql('securitySolutionFixture'); }); it('User: security solution only - should NOT create a case of different owner', async () => { - await createCaseAsUser({ + await createCase( supertestWithoutAuth, - user: secOnly, - space: 'space1', - owner: 'observabilityFixture', - expectedHttpCode: 403, - }); + getPostCaseRequest({ owner: 'observabilityFixture' }), + 403, + { + user: secOnly, + space: 'space1', + } + ); }); for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { it(`User ${ user.username } with role(s) ${user.roles.join()} - should NOT create a case`, async () => { - await createCaseAsUser({ + await createCase( supertestWithoutAuth, - user, - space: 'space1', - owner: 'securitySolutionFixture', - expectedHttpCode: 403, - }); + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 403, + { + user, + space: 'space1', + } + ); }); } it('should NOT create a case in a space with no permissions', async () => { - await createCaseAsUser({ + await createCase( supertestWithoutAuth, - user: secOnly, - space: 'space2', - owner: 'securitySolutionFixture', - expectedHttpCode: 403, - }); + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 403, + { + user: secOnly, + space: 'space2', + } + ); }); }); }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/reporters/get_reporters.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/reporters/get_reporters.ts index c811c0982840e..e34d9ccad39ac 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/reporters/get_reporters.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/reporters/get_reporters.ts @@ -6,15 +6,27 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { CASES_URL, CASE_REPORTERS_URL } from '../../../../../../../plugins/cases/common/constants'; -import { defaultUser, postCaseReq } from '../../../../../common/lib/mock'; -import { deleteCasesByESQuery } from '../../../../../common/lib/utils'; +import { defaultUser, getPostCaseRequest } from '../../../../../common/lib/mock'; +import { createCase, deleteCasesByESQuery, getReporters } from '../../../../../common/lib/utils'; +import { + secOnly, + obsOnly, + globalRead, + superUser, + secOnlyRead, + obsOnlyRead, + obsSecRead, + noKibanaPrivileges, + obsSec, +} from '../../../../../common/lib/authentication/users'; +import { getUserInfo } from '../../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); describe('get_reporters', () => { @@ -23,15 +35,167 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should return reporters', async () => { - await supertest.post(CASES_URL).set('kbn-xsrf', 'true').send(postCaseReq).expect(200); + await createCase(supertest, getPostCaseRequest()); + const reporters = await getReporters({ supertest: supertestWithoutAuth }); - const { body } = await supertest - .get(CASE_REPORTERS_URL) - .set('kbn-xsrf', 'true') - .send() - .expect(200); + expect(reporters).to.eql([defaultUser]); + }); + + it('should return unique reporters', async () => { + await createCase(supertest, getPostCaseRequest()); + await createCase(supertest, getPostCaseRequest()); + const reporters = await getReporters({ supertest: supertestWithoutAuth }); + + expect(reporters).to.eql([defaultUser]); + }); + + describe('rbac', () => { + it('User: security solution only - should read the correct reporters', async () => { + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: secOnly, + space: 'space1', + } + ); + + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { + user: obsOnly, + space: 'space1', + } + ); + + for (const scenario of [ + { + user: globalRead, + expectedReporters: [getUserInfo(secOnly), getUserInfo(obsOnly)], + }, + { + user: superUser, + expectedReporters: [getUserInfo(secOnly), getUserInfo(obsOnly)], + }, + { user: secOnlyRead, expectedReporters: [getUserInfo(secOnly)] }, + { user: obsOnlyRead, expectedReporters: [getUserInfo(obsOnly)] }, + { + user: obsSecRead, + expectedReporters: [getUserInfo(secOnly), getUserInfo(obsOnly)], + }, + ]) { + const reporters = await getReporters({ + supertest: supertestWithoutAuth, + expectedHttpCode: 200, + auth: { + user: scenario.user, + space: 'space1', + }, + }); + + expect(reporters).to.eql(scenario.expectedReporters); + } + }); + + for (const scenario of [ + { user: noKibanaPrivileges, space: 'space1' }, + { user: secOnly, space: 'space2' }, + ]) { + it(`User ${scenario.user.username} with role(s) ${scenario.user.roles.join()} and space ${ + scenario.space + } - should NOT get all reporters`, async () => { + // super user creates a case at the appropriate space + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: scenario.space, + } + ); + + // user should not be able to get all reporters at the appropriate space + await getReporters({ + supertest: supertestWithoutAuth, + expectedHttpCode: 403, + auth: { user: scenario.user, space: scenario.space }, + }); + }); + } + + it('should respect the owner filter when having permissions', async () => { + await Promise.all([ + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: secOnly, + space: 'space1', + } + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { + user: obsOnly, + space: 'space1', + } + ), + ]); + + const reporters = await getReporters({ + supertest: supertestWithoutAuth, + auth: { + user: obsSec, + space: 'space1', + }, + query: { owner: 'securitySolutionFixture' }, + }); + + expect(reporters).to.eql([getUserInfo(secOnly)]); + }); + + it('should return the correct cases when trying to exploit RBAC through the owner query parameter', async () => { + await Promise.all([ + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: secOnly, + space: 'space1', + } + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { + user: obsOnly, + space: 'space1', + } + ), + ]); + + // User with permissions only to security solution request reporters from observability + const reporters = await getReporters({ + supertest: supertestWithoutAuth, + auth: { + user: secOnly, + space: 'space1', + }, + query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, + }); - expect(body).to.eql([defaultUser]); + // Only security solution reporters are being returned + expect(reporters).to.eql([getUserInfo(secOnly)]); + }); }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/tags/get_tags.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/tags/get_tags.ts index a47cf12158a34..0c7237683666f 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/tags/get_tags.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/tags/get_tags.ts @@ -6,15 +6,26 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { CASES_URL, CASE_TAGS_URL } from '../../../../../../../plugins/cases/common/constants'; -import { postCaseReq } from '../../../../../common/lib/mock'; -import { deleteCasesByESQuery } from '../../../../../common/lib/utils'; +import { deleteCasesByESQuery, createCase, getTags } from '../../../../../common/lib/utils'; +import { getPostCaseRequest } from '../../../../../common/lib/mock'; +import { + secOnly, + obsOnly, + globalRead, + superUser, + secOnlyRead, + obsOnlyRead, + obsSecRead, + noKibanaPrivileges, + obsSec, +} from '../../../../../common/lib/authentication/users'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); describe('get_tags', () => { @@ -23,20 +34,168 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should return case tags', async () => { - await supertest.post(CASES_URL).set('kbn-xsrf', 'true').send(postCaseReq); - await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ ...postCaseReq, tags: ['unique'] }) - .expect(200); - - const { body } = await supertest - .get(CASE_TAGS_URL) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(body).to.eql(['defacement', 'unique']); + await createCase(supertest, getPostCaseRequest()); + await createCase(supertest, getPostCaseRequest({ tags: ['unique'] })); + + const tags = await getTags({ supertest }); + expect(tags).to.eql(['defacement', 'unique']); + }); + + it('should return unique tags', async () => { + await createCase(supertest, getPostCaseRequest()); + await createCase(supertest, getPostCaseRequest()); + + const tags = await getTags({ supertest }); + expect(tags).to.eql(['defacement']); + }); + + describe('rbac', () => { + it('should read the correct tags', async () => { + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture', tags: ['sec'] }), + 200, + { + user: secOnly, + space: 'space1', + } + ); + + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture', tags: ['obs'] }), + 200, + { + user: obsOnly, + space: 'space1', + } + ); + + for (const scenario of [ + { + user: globalRead, + expectedTags: ['sec', 'obs'], + }, + { + user: superUser, + expectedTags: ['sec', 'obs'], + }, + { user: secOnlyRead, expectedTags: ['sec'] }, + { user: obsOnlyRead, expectedTags: ['obs'] }, + { + user: obsSecRead, + expectedTags: ['sec', 'obs'], + }, + ]) { + const tags = await getTags({ + supertest: supertestWithoutAuth, + expectedHttpCode: 200, + auth: { + user: scenario.user, + space: 'space1', + }, + }); + + expect(tags).to.eql(scenario.expectedTags); + } + }); + + for (const scenario of [ + { user: noKibanaPrivileges, space: 'space1' }, + { user: secOnly, space: 'space2' }, + ]) { + it(`User ${scenario.user.username} with role(s) ${scenario.user.roles.join()} and space ${ + scenario.space + } - should NOT get all tags`, async () => { + // super user creates a case at the appropriate space + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture', tags: ['sec'] }), + 200, + { + user: superUser, + space: scenario.space, + } + ); + + // user should not be able to get all tags at the appropriate space + await getTags({ + supertest: supertestWithoutAuth, + expectedHttpCode: 403, + auth: { user: scenario.user, space: scenario.space }, + }); + }); + } + + it('should respect the owner filter when having permissions', async () => { + await Promise.all([ + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture', tags: ['sec'] }), + 200, + { + user: obsSec, + space: 'space1', + } + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture', tags: ['obs'] }), + 200, + { + user: obsSec, + space: 'space1', + } + ), + ]); + + const tags = await getTags({ + supertest: supertestWithoutAuth, + auth: { + user: obsSec, + space: 'space1', + }, + query: { owner: 'securitySolutionFixture' }, + }); + + expect(tags).to.eql(['sec']); + }); + + it('should return the correct cases when trying to exploit RBAC through the owner query parameter', async () => { + await Promise.all([ + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture', tags: ['sec'] }), + 200, + { + user: obsSec, + space: 'space1', + } + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture', tags: ['obs'] }), + 200, + { + user: obsSec, + space: 'space1', + } + ), + ]); + + // User with permissions only to security solution request tags from observability + const tags = await getTags({ + supertest: supertestWithoutAuth, + auth: { + user: secOnly, + space: 'space1', + }, + query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, + }); + + // Only security solution tags are being returned + expect(tags).to.eql(['sec']); + }); }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/get_configure.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/get_configure.ts index 1f36ecc812c5f..b26e8a3f3b381 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/get_configure.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/get_configure.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 { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; import { @@ -23,11 +23,24 @@ import { getConfigurationRequest, createConnector, getServiceNowConnector, + ensureSavedObjectIsAuthorized, } from '../../../../common/lib/utils'; +import { + obsOnly, + secOnly, + obsOnlyRead, + secOnlyRead, + noKibanaPrivileges, + superUser, + globalRead, + obsSecRead, + obsSec, +} from '../../../../common/lib/authentication/users'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); const kibanaServer = getService('kibanaServer'); @@ -47,15 +60,34 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should return an empty find body correctly if no configuration is loaded', async () => { - const configuration = await getConfiguration(supertest); - expect(configuration).to.eql({}); + const configuration = await getConfiguration({ supertest }); + expect(configuration).to.eql([]); }); it('should return a configuration', async () => { await createConfiguration(supertest); - const configuration = await getConfiguration(supertest); + const configuration = await getConfiguration({ supertest }); + + const data = removeServerGeneratedPropertiesFromSavedObject(configuration[0]); + expect(data).to.eql(getConfigurationOutput()); + }); + + it('should get a single configuration', async () => { + await createConfiguration(supertest, getConfigurationRequest({ id: 'connector-2' })); + await createConfiguration(supertest); + const res = await getConfiguration({ supertest }); + + expect(res.length).to.eql(1); + const data = removeServerGeneratedPropertiesFromSavedObject(res[0]); + expect(data).to.eql(getConfigurationOutput()); + }); + + it('should return by descending order', async () => { + await createConfiguration(supertest, getConfigurationRequest({ id: 'connector-2' })); + await createConfiguration(supertest); + const res = await getConfiguration({ supertest }); - const data = removeServerGeneratedPropertiesFromSavedObject(configuration); + const data = removeServerGeneratedPropertiesFromSavedObject(res[0]); expect(data).to.eql(getConfigurationOutput()); }); @@ -76,8 +108,8 @@ export default ({ getService }: FtrProviderContext): void => { }) ); - const configuration = await getConfiguration(supertest); - const data = removeServerGeneratedPropertiesFromSavedObject(configuration); + const configuration = await getConfiguration({ supertest }); + const data = removeServerGeneratedPropertiesFromSavedObject(configuration[0]); expect(data).to.eql( getConfigurationOutput(false, { mappings: [ @@ -106,5 +138,145 @@ export default ({ getService }: FtrProviderContext): void => { }) ); }); + + describe('rbac', () => { + it('should return the correct configuration', async () => { + await createConfiguration(supertestWithoutAuth, getConfigurationRequest(), 200, { + user: secOnly, + space: 'space1', + }); + + await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'observabilityFixture' }, + 200, + { + user: obsOnly, + space: 'space1', + } + ); + + for (const scenario of [ + { + user: globalRead, + numberOfExpectedCases: 2, + owners: ['securitySolutionFixture', 'observabilityFixture'], + }, + { + user: superUser, + numberOfExpectedCases: 2, + owners: ['securitySolutionFixture', 'observabilityFixture'], + }, + { user: secOnlyRead, numberOfExpectedCases: 1, owners: ['securitySolutionFixture'] }, + { user: obsOnlyRead, numberOfExpectedCases: 1, owners: ['observabilityFixture'] }, + { + user: obsSecRead, + numberOfExpectedCases: 2, + owners: ['securitySolutionFixture', 'observabilityFixture'], + }, + ]) { + const configuration = await getConfiguration({ + supertest: supertestWithoutAuth, + query: { owner: scenario.owners }, + expectedHttpCode: 200, + auth: { + user: scenario.user, + space: 'space1', + }, + }); + + ensureSavedObjectIsAuthorized( + configuration, + scenario.numberOfExpectedCases, + scenario.owners + ); + } + }); + + for (const scenario of [ + { user: noKibanaPrivileges, space: 'space1' }, + { user: secOnly, space: 'space2' }, + ]) { + it(`User ${scenario.user.username} with role(s) ${scenario.user.roles.join()} and space ${ + scenario.space + } - should NOT read a case configuration`, async () => { + // super user creates a configuration at the appropriate space + await createConfiguration(supertestWithoutAuth, getConfigurationRequest(), 200, { + user: superUser, + space: scenario.space, + }); + + // user should not be able to read configurations at the appropriate space + await getConfiguration({ + supertest: supertestWithoutAuth, + expectedHttpCode: 403, + auth: { + user: scenario.user, + space: scenario.space, + }, + }); + }); + } + + it('should respect the owner filter when having permissions', async () => { + await Promise.all([ + createConfiguration(supertestWithoutAuth, getConfigurationRequest(), 200, { + user: obsSec, + space: 'space1', + }), + createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'observabilityFixture' }, + 200, + { + user: obsSec, + space: 'space1', + } + ), + ]); + + const res = await getConfiguration({ + supertest: supertestWithoutAuth, + query: { owner: 'securitySolutionFixture' }, + auth: { + user: obsSec, + space: 'space1', + }, + }); + + ensureSavedObjectIsAuthorized(res, 1, ['securitySolutionFixture']); + }); + + it('should return the correct cases when trying to exploit RBAC through the owner query parameter', async () => { + await Promise.all([ + createConfiguration(supertestWithoutAuth, getConfigurationRequest(), 200, { + user: obsSec, + space: 'space1', + }), + createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'observabilityFixture' }, + 200, + { + user: obsSec, + space: 'space1', + } + ), + ]); + + // User with permissions only to security solution request cases from observability + const res = await getConfiguration({ + supertest: supertestWithoutAuth, + query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, + auth: { + user: secOnly, + space: 'space1', + }, + }); + + // Only security solution cases are being returned + ensureSavedObjectIsAuthorized(res, 1, ['securitySolutionFixture']); + }); + }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/patch_configure.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/patch_configure.ts index 8901447e37b3a..c76e5f408e475 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/patch_configure.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/patch_configure.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 { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; import { ExternalServiceSimulator, @@ -24,10 +24,20 @@ import { createConnector, } from '../../../../common/lib/utils'; import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; +import { + secOnly, + obsOnlyRead, + secOnlyRead, + noKibanaPrivileges, + globalRead, + obsSecRead, + superUser, +} from '../../../../common/lib/authentication/users'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); const kibanaServer = getService('kibanaServer'); @@ -48,7 +58,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should patch a configuration', async () => { const configuration = await createConfiguration(supertest); - const newConfiguration = await updateConfiguration(supertest, { + const newConfiguration = await updateConfiguration(supertest, configuration.id, { closure_type: 'close-by-pushing', version: configuration.version, }); @@ -57,7 +67,7 @@ export default ({ getService }: FtrProviderContext): void => { expect(data).to.eql({ ...getConfigurationOutput(true), closure_type: 'close-by-pushing' }); }); - it('should patch a configuration: connector', async () => { + it('should patch a configuration connector and create mappings', async () => { const connector = await createConnector(supertest, { ...getServiceNowConnector(), config: { apiUrl: servicenowSimulatorURL }, @@ -65,8 +75,10 @@ export default ({ getService }: FtrProviderContext): void => { actionsRemover.add('default', connector.id, 'action', 'actions'); + // Configuration is created with no connector so the mappings are empty const configuration = await createConfiguration(supertest); - const newConfiguration = await updateConfiguration(supertest, { + + const newConfiguration = await updateConfiguration(supertest, configuration.id, { ...getConfigurationRequest({ id: connector.id, name: connector.name, @@ -105,10 +117,68 @@ export default ({ getService }: FtrProviderContext): void => { }); }); + it('should mappings when updating the connector', async () => { + const connector = await createConnector(supertest, { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }); + + actionsRemover.add('default', connector.id, 'action', 'actions'); + + // Configuration is created with connector so the mappings are created + const configuration = await createConfiguration( + supertest, + getConfigurationRequest({ + id: connector.id, + name: connector.name, + type: connector.connector_type_id as ConnectorTypes, + }) + ); + + const newConfiguration = await updateConfiguration(supertest, configuration.id, { + ...getConfigurationRequest({ + id: connector.id, + name: 'New name', + type: connector.connector_type_id as ConnectorTypes, + fields: null, + }), + version: configuration.version, + }); + + const data = removeServerGeneratedPropertiesFromSavedObject(newConfiguration); + expect(data).to.eql({ + ...getConfigurationOutput(true), + connector: { + id: connector.id, + name: 'New name', + type: connector.connector_type_id as ConnectorTypes, + fields: null, + }, + mappings: [ + { + action_type: 'overwrite', + source: 'title', + target: 'short_description', + }, + { + action_type: 'overwrite', + source: 'description', + target: 'description', + }, + { + action_type: 'append', + source: 'comments', + target: 'work_notes', + }, + ], + }); + }); + it('should not patch a configuration with unsupported connector type', async () => { - await createConfiguration(supertest); + const configuration = await createConfiguration(supertest); await updateConfiguration( supertest, + configuration.id, // @ts-expect-error getConfigurationRequest({ type: '.unsupported' }), 400 @@ -116,9 +186,10 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should not patch a configuration with unsupported connector fields', async () => { - await createConfiguration(supertest); + const configuration = await createConfiguration(supertest); await updateConfiguration( supertest, + configuration.id, // @ts-expect-error getConfigurationRequest({ type: '.jira', fields: { unsupported: 'value' } }), 400 @@ -128,22 +199,23 @@ export default ({ getService }: FtrProviderContext): void => { it('should handle patch request when there is no configuration', async () => { const error = await updateConfiguration( supertest, + 'not-exist', { closure_type: 'close-by-pushing', version: 'no-version' }, - 409 + 404 ); expect(error).to.eql({ - error: 'Conflict', - message: - 'You can not patch this configuration since you did not created first with a post.', - statusCode: 409, + error: 'Not Found', + message: 'Saved object [cases-configure/not-exist] not found', + statusCode: 404, }); }); it('should handle patch request when versions are different', async () => { - await createConfiguration(supertest); + const configuration = await createConfiguration(supertest); const error = await updateConfiguration( supertest, + configuration.id, { closure_type: 'close-by-pushing', version: 'no-version' }, 409 ); @@ -155,5 +227,139 @@ export default ({ getService }: FtrProviderContext): void => { statusCode: 409, }); }); + + it('should not allow to change the owner of the configuration', async () => { + const configuration = await createConfiguration(supertest); + await updateConfiguration( + supertest, + configuration.id, + // @ts-expect-error + { owner: 'observabilityFixture', version: configuration.version }, + 400 + ); + }); + + it('should not allow excess attributes', async () => { + const configuration = await createConfiguration(supertest); + await updateConfiguration( + supertest, + configuration.id, + // @ts-expect-error + { notExist: 'not-exist', version: configuration.version }, + 400 + ); + }); + + describe('rbac', () => { + it('User: security solution only - should update a configuration', async () => { + const configuration = await createConfiguration( + supertestWithoutAuth, + getConfigurationRequest(), + 200, + { + user: secOnly, + space: 'space1', + } + ); + + const newConfiguration = await updateConfiguration( + supertestWithoutAuth, + configuration.id, + { + closure_type: 'close-by-pushing', + version: configuration.version, + }, + 200, + { + user: secOnly, + space: 'space1', + } + ); + + expect(newConfiguration.owner).to.eql('securitySolutionFixture'); + }); + + it('User: security solution only - should NOT update a configuration of different owner', async () => { + const configuration = await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'observabilityFixture' }, + 200, + { + user: superUser, + space: 'space1', + } + ); + + await updateConfiguration( + supertestWithoutAuth, + configuration.id, + { + closure_type: 'close-by-pushing', + version: configuration.version, + }, + 403, + { + user: secOnly, + space: 'space1', + } + ); + }); + + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT update a configuration`, async () => { + const configuration = await createConfiguration( + supertestWithoutAuth, + getConfigurationRequest(), + 200, + { + user: superUser, + space: 'space1', + } + ); + + await updateConfiguration( + supertestWithoutAuth, + configuration.id, + { + closure_type: 'close-by-pushing', + version: configuration.version, + }, + 403, + { + user, + space: 'space1', + } + ); + }); + } + + it('should NOT update a configuration in a space with no permissions', async () => { + const configuration = await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, + 200, + { + user: superUser, + space: 'space2', + } + ); + + await updateConfiguration( + supertestWithoutAuth, + configuration.id, + { + closure_type: 'close-by-pushing', + version: configuration.version, + }, + 404, + { + user: secOnly, + space: 'space1', + } + ); + }); + }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/post_configure.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/post_configure.ts index c74e048edcfa0..a47c10efe5037 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/post_configure.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/post_configure.ts @@ -7,7 +7,12 @@ import expect from '@kbn/expect'; import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; +import { + ExternalServiceSimulator, + getExternalServiceSimulatorPath, +} from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; import { getConfigurationRequest, @@ -15,17 +20,42 @@ import { getConfigurationOutput, deleteConfiguration, createConfiguration, + createConnector, + getServiceNowConnector, getConfiguration, + ensureSavedObjectIsAuthorized, } from '../../../../common/lib/utils'; +import { + secOnly, + obsOnlyRead, + secOnlyRead, + noKibanaPrivileges, + globalRead, + obsSecRead, + superUser, +} from '../../../../common/lib/authentication/users'; + // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); + const kibanaServer = getService('kibanaServer'); describe('post_configure', () => { + const actionsRemover = new ActionsRemover(supertest); + let servicenowSimulatorURL: string = ''; + + before(() => { + servicenowSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) + ); + }); + afterEach(async () => { await deleteConfiguration(es); + await actionsRemover.removeAll(); }); it('should create a configuration', async () => { @@ -38,10 +68,70 @@ export default ({ getService }: FtrProviderContext): void => { it('should keep only the latest configuration', async () => { await createConfiguration(supertest, getConfigurationRequest({ id: 'connector-2' })); await createConfiguration(supertest); - const configuration = await getConfiguration(supertest); + const configuration = await getConfiguration({ supertest }); - const data = removeServerGeneratedPropertiesFromSavedObject(configuration); - expect(data).to.eql(getConfigurationOutput()); + expect(configuration.length).to.be(1); + }); + + it('should create a configuration with mapping', async () => { + const connector = await createConnector(supertest, { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }); + + actionsRemover.add('default', connector.id, 'action', 'actions'); + + const postRes = await createConfiguration( + supertest, + getConfigurationRequest({ + id: connector.id, + name: connector.name, + type: connector.connector_type_id as ConnectorTypes, + }) + ); + + const data = removeServerGeneratedPropertiesFromSavedObject(postRes); + expect(data).to.eql( + getConfigurationOutput(false, { + mappings: [ + { + action_type: 'overwrite', + source: 'title', + target: 'short_description', + }, + { + action_type: 'overwrite', + source: 'description', + target: 'description', + }, + { + action_type: 'append', + source: 'comments', + target: 'work_notes', + }, + ], + connector: { + id: connector.id, + name: connector.name, + type: connector.connector_type_id, + fields: null, + }, + }) + ); + }); + + it('should return an error when failing to get mapping', async () => { + const postRes = await createConfiguration( + supertest, + getConfigurationRequest({ + id: 'not-exists', + name: 'not-exists', + type: ConnectorTypes.jira, + }) + ); + + expect(postRes.error).to.not.be(null); + expect(postRes.mappings).to.eql([]); }); it('should not create a configuration when missing connector.id', async () => { @@ -124,7 +214,18 @@ export default ({ getService }: FtrProviderContext): void => { ); }); - it('should not create a configuration when when fields are not null', async () => { + it('should not create a configuration when missing connector', async () => { + await createConfiguration( + supertest, + // @ts-expect-error + { + closure_type: 'close-by-user', + }, + 400 + ); + }); + + it('should not create a configuration when fields are not null', async () => { await createConfiguration( supertest, { @@ -154,5 +255,105 @@ export default ({ getService }: FtrProviderContext): void => { 400 ); }); + + describe('rbac', () => { + it('User: security solution only - should create a configuration', async () => { + const configuration = await createConfiguration( + supertestWithoutAuth, + getConfigurationRequest(), + 200, + { + user: secOnly, + space: 'space1', + } + ); + + expect(configuration.owner).to.eql('securitySolutionFixture'); + }); + + it('User: security solution only - should NOT create a configuration of different owner', async () => { + await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'observabilityFixture' }, + 403, + { + user: secOnly, + space: 'space1', + } + ); + }); + + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT create a configuration`, async () => { + await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, + 403, + { + user, + space: 'space1', + } + ); + }); + } + + it('should NOT create a configuration in a space with no permissions', async () => { + await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, + 403, + { + user: secOnly, + space: 'space2', + } + ); + }); + + it('it deletes the correct configurations', async () => { + await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, + 200, + { + user: superUser, + space: 'space1', + } + ); + + /** + * This API call should not delete the previously created configuration + * as it belongs to a different owner + */ + await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'observabilityFixture' }, + 200, + { + user: superUser, + space: 'space1', + } + ); + + const configuration = await getConfiguration({ + supertest: supertestWithoutAuth, + query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, + auth: { + user: superUser, + space: 'space1', + }, + }); + + /** + * This ensures that both configuration are returned as expected + * and neither of has been deleted + */ + ensureSavedObjectIsAuthorized(configuration, 2, [ + 'securitySolutionFixture', + 'observabilityFixture', + ]); + }); + }); }); };