From 7f1a7dd71e410a8a4bb7df0f304684cdea41cfa4 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Tue, 9 Mar 2021 16:21:57 -0500 Subject: [PATCH 001/113] Adding feature flag for auth --- x-pack/plugins/case/server/config.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugins/case/server/config.ts b/x-pack/plugins/case/server/config.ts index 7679a5a389051c..c4dca0f9ff9559 100644 --- a/x-pack/plugins/case/server/config.ts +++ b/x-pack/plugins/case/server/config.ts @@ -9,6 +9,8 @@ import { schema, TypeOf } from '@kbn/config-schema'; export const ConfigSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), + // TODO: remove once authorization is complete + enableAuthorization: schema.boolean({ defaultValue: false }), }); export type ConfigType = TypeOf; From 164582d9431271d7fc55d8c9949add57d231aed0 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Tue, 9 Mar 2021 16:24:05 -0500 Subject: [PATCH 002/113] Hiding SOs and adding consumer field --- x-pack/plugins/case/common/constants.ts | 3 + .../case/server/saved_object_types/cases.ts | 5 +- .../server/saved_object_types/comments.ts | 5 +- .../server/saved_object_types/configure.ts | 5 +- .../saved_object_types/connector_mappings.ts | 7 +- .../server/saved_object_types/migrations.ts | 89 +++++++++++++++++++ .../server/saved_object_types/sub_case.ts | 7 +- .../server/saved_object_types/user_actions.ts | 5 +- 8 files changed, 120 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/case/common/constants.ts b/x-pack/plugins/case/common/constants.ts index cc69c7ecc29094..2a031cd87eb3f0 100644 --- a/x-pack/plugins/case/common/constants.ts +++ b/x-pack/plugins/case/common/constants.ts @@ -55,3 +55,6 @@ export const SUPPORTED_CONNECTORS = [ export const MAX_ALERTS_PER_SUB_CASE = 5000; export const MAX_GENERATED_ALERTS_PER_SUB_CASE = MAX_ALERTS_PER_SUB_CASE / DEFAULT_MAX_SIGNALS; + +// TODO: figure out what the value will actually be when a security solution case is created +export const SECURITY_SOLUTION_PLUGIN = 'security-solution'; diff --git a/x-pack/plugins/case/server/saved_object_types/cases.ts b/x-pack/plugins/case/server/saved_object_types/cases.ts index 5f413ea27c4a75..03fc1f3268caf3 100644 --- a/x-pack/plugins/case/server/saved_object_types/cases.ts +++ b/x-pack/plugins/case/server/saved_object_types/cases.ts @@ -12,7 +12,7 @@ export const CASE_SAVED_OBJECT = 'cases'; export const caseSavedObjectType: SavedObjectsType = { name: CASE_SAVED_OBJECT, - hidden: false, + hidden: true, namespaceType: 'single', mappings: { properties: { @@ -32,6 +32,9 @@ export const caseSavedObjectType: SavedObjectsType = { }, }, }, + consumer: { + type: 'keyword', + }, created_at: { type: 'date', }, diff --git a/x-pack/plugins/case/server/saved_object_types/comments.ts b/x-pack/plugins/case/server/saved_object_types/comments.ts index a4fdc24b6e4eec..089746cd6014fa 100644 --- a/x-pack/plugins/case/server/saved_object_types/comments.ts +++ b/x-pack/plugins/case/server/saved_object_types/comments.ts @@ -12,7 +12,7 @@ export const CASE_COMMENT_SAVED_OBJECT = 'cases-comments'; export const caseCommentSavedObjectType: SavedObjectsType = { name: CASE_COMMENT_SAVED_OBJECT, - hidden: false, + hidden: true, namespaceType: 'single', mappings: { properties: { @@ -22,6 +22,9 @@ export const caseCommentSavedObjectType: SavedObjectsType = { comment: { type: 'text', }, + consumer: { + type: 'keyword', + }, type: { type: 'keyword', }, diff --git a/x-pack/plugins/case/server/saved_object_types/configure.ts b/x-pack/plugins/case/server/saved_object_types/configure.ts index 8944e0678f59ca..171d76baa87ff0 100644 --- a/x-pack/plugins/case/server/saved_object_types/configure.ts +++ b/x-pack/plugins/case/server/saved_object_types/configure.ts @@ -12,10 +12,13 @@ export const CASE_CONFIGURE_SAVED_OBJECT = 'cases-configure'; export const caseConfigureSavedObjectType: SavedObjectsType = { name: CASE_CONFIGURE_SAVED_OBJECT, - hidden: false, + hidden: true, namespaceType: 'single', mappings: { properties: { + consumer: { + type: 'keyword', + }, created_at: { type: 'date', }, diff --git a/x-pack/plugins/case/server/saved_object_types/connector_mappings.ts b/x-pack/plugins/case/server/saved_object_types/connector_mappings.ts index df469108fac0b3..1033e1279d5745 100644 --- a/x-pack/plugins/case/server/saved_object_types/connector_mappings.ts +++ b/x-pack/plugins/case/server/saved_object_types/connector_mappings.ts @@ -6,17 +6,21 @@ */ import { SavedObjectsType } from 'src/core/server'; +import { connectorMappingsMigrations } from './migrations'; export const CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT = 'cases-connector-mappings'; export const caseConnectorMappingsSavedObjectType: SavedObjectsType = { name: CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, - hidden: false, + hidden: true, namespaceType: 'single', mappings: { properties: { mappings: { properties: { + consumer: { + type: 'keyword', + }, source: { type: 'keyword', }, @@ -30,4 +34,5 @@ export const caseConnectorMappingsSavedObjectType: SavedObjectsType = { }, }, }, + migrations: connectorMappingsMigrations, }; diff --git a/x-pack/plugins/case/server/saved_object_types/migrations.ts b/x-pack/plugins/case/server/saved_object_types/migrations.ts index bf9694d7e6bb0d..37ab1c58fca7b3 100644 --- a/x-pack/plugins/case/server/saved_object_types/migrations.ts +++ b/x-pack/plugins/case/server/saved_object_types/migrations.ts @@ -15,6 +15,7 @@ import { AssociationType, ESConnectorFields, } from '../../common/api'; +import { SECURITY_SOLUTION_PLUGIN } from '../../common/constants'; interface UnsanitizedCaseConnector { connector_id: string; @@ -59,6 +60,10 @@ interface SanitizedCaseType { type: string; } +interface SanitizedCaseConsumer { + consumer: string; +} + export const caseMigrations = { '7.10.0': ( doc: SavedObjectUnsanitizedDoc @@ -113,6 +118,19 @@ export const caseMigrations = { references: doc.references || [], }; }, + '7.13.0': ( + doc: SavedObjectUnsanitizedDoc> + ): SavedObjectSanitizedDoc => { + return { + ...doc, + attributes: { + ...doc.attributes, + // TODO: figure out what to default this to + consumer: SECURITY_SOLUTION_PLUGIN, + }, + references: doc.references || [], + }; + }, }; export const configureMigrations = { @@ -135,6 +153,19 @@ export const configureMigrations = { references: doc.references || [], }; }, + '7.13.0': ( + doc: SavedObjectUnsanitizedDoc> + ): SavedObjectSanitizedDoc => { + return { + ...doc, + attributes: { + ...doc.attributes, + // TODO: figure out what to default this to + consumer: SECURITY_SOLUTION_PLUGIN, + }, + references: doc.references || [], + }; + }, }; export const userActionsMigrations = { @@ -176,6 +207,19 @@ export const userActionsMigrations = { references: doc.references || [], }; }, + '7.13.0': ( + doc: SavedObjectUnsanitizedDoc> + ): SavedObjectSanitizedDoc => { + return { + ...doc, + attributes: { + ...doc.attributes, + // TODO: figure out what to default this to + consumer: SECURITY_SOLUTION_PLUGIN, + }, + references: doc.references || [], + }; + }, }; interface UnsanitizedComment { @@ -226,4 +270,49 @@ export const commentsMigrations = { references: doc.references || [], }; }, + '7.13.0': ( + doc: SavedObjectUnsanitizedDoc> + ): SavedObjectSanitizedDoc => { + return { + ...doc, + attributes: { + ...doc.attributes, + // TODO: figure out what to default this to + consumer: SECURITY_SOLUTION_PLUGIN, + }, + references: doc.references || [], + }; + }, +}; + +export const connectorMappingsMigrations = { + '7.13.0': ( + doc: SavedObjectUnsanitizedDoc> + ): SavedObjectSanitizedDoc => { + return { + ...doc, + attributes: { + ...doc.attributes, + // TODO: figure out what to default this to + consumer: SECURITY_SOLUTION_PLUGIN, + }, + references: doc.references || [], + }; + }, +}; + +export const subCasesMigrations = { + '7.13.0': ( + doc: SavedObjectUnsanitizedDoc> + ): SavedObjectSanitizedDoc => { + return { + ...doc, + attributes: { + ...doc.attributes, + // TODO: figure out what to default this to + consumer: SECURITY_SOLUTION_PLUGIN, + }, + references: doc.references || [], + }; + }, }; diff --git a/x-pack/plugins/case/server/saved_object_types/sub_case.ts b/x-pack/plugins/case/server/saved_object_types/sub_case.ts index da89b19346e4e1..2f4642d54f7f10 100644 --- a/x-pack/plugins/case/server/saved_object_types/sub_case.ts +++ b/x-pack/plugins/case/server/saved_object_types/sub_case.ts @@ -6,12 +6,13 @@ */ import { SavedObjectsType } from 'src/core/server'; +import { subCasesMigrations } from './migrations'; export const SUB_CASE_SAVED_OBJECT = 'cases-sub-case'; export const subCaseSavedObjectType: SavedObjectsType = { name: SUB_CASE_SAVED_OBJECT, - hidden: false, + hidden: true, namespaceType: 'single', mappings: { properties: { @@ -31,6 +32,9 @@ export const subCaseSavedObjectType: SavedObjectsType = { }, }, }, + consumer: { + type: 'keyword', + }, created_at: { type: 'date', }, @@ -68,4 +72,5 @@ export const subCaseSavedObjectType: SavedObjectsType = { }, }, }, + migrations: subCasesMigrations, }; diff --git a/x-pack/plugins/case/server/saved_object_types/user_actions.ts b/x-pack/plugins/case/server/saved_object_types/user_actions.ts index 745dc10e5aac9a..90ef745123d910 100644 --- a/x-pack/plugins/case/server/saved_object_types/user_actions.ts +++ b/x-pack/plugins/case/server/saved_object_types/user_actions.ts @@ -12,7 +12,7 @@ export const CASE_USER_ACTION_SAVED_OBJECT = 'cases-user-actions'; export const caseUserActionSavedObjectType: SavedObjectsType = { name: CASE_USER_ACTION_SAVED_OBJECT, - hidden: false, + hidden: true, namespaceType: 'single', mappings: { properties: { @@ -38,6 +38,9 @@ export const caseUserActionSavedObjectType: SavedObjectsType = { }, }, }, + consumer: { + type: 'keyword', + }, new_value: { type: 'text', }, From 7eaf41e317216181a26b18026e534e208fa2d6fa Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Tue, 9 Mar 2021 17:29:46 -0500 Subject: [PATCH 003/113] First pass at adding security changes --- .../server/authorization/actions/actions.ts | 3 ++ .../server/authorization/actions/cases.ts | 28 +++++++++++++++ .../feature_privilege_builder/cases.ts | 36 +++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 x-pack/plugins/security/server/authorization/actions/cases.ts create mode 100644 x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts diff --git a/x-pack/plugins/security/server/authorization/actions/actions.ts b/x-pack/plugins/security/server/authorization/actions/actions.ts index 23d07f73f04bec..d0466645213fa3 100644 --- a/x-pack/plugins/security/server/authorization/actions/actions.ts +++ b/x-pack/plugins/security/server/authorization/actions/actions.ts @@ -8,6 +8,7 @@ import { AlertingActions } from './alerting'; import { ApiActions } from './api'; import { AppActions } from './app'; +import { CasesActions } from './cases'; import { SavedObjectActions } from './saved_object'; import { SpaceActions } from './space'; import { UIActions } from './ui'; @@ -21,6 +22,8 @@ export class Actions { public readonly app = new AppActions(this.versionNumber); + public readonly cases = new CasesActions(this.versionNumber); + public readonly login = 'login:'; public readonly savedObject = new SavedObjectActions(this.versionNumber); diff --git a/x-pack/plugins/security/server/authorization/actions/cases.ts b/x-pack/plugins/security/server/authorization/actions/cases.ts new file mode 100644 index 00000000000000..ef6aeb288297ac --- /dev/null +++ b/x-pack/plugins/security/server/authorization/actions/cases.ts @@ -0,0 +1,28 @@ +/* + * 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 { isString } from 'lodash'; + +export class CasesActions { + private readonly prefix: string; + + constructor(versionNumber: string) { + this.prefix = `cases:${versionNumber}:`; + } + + public get(consumer: string, operation: string): string { + if (!operation || !isString(operation)) { + throw new Error('operation is required and must be a string'); + } + + if (!consumer || !isString(consumer)) { + throw new Error('consumer is required and must be a string'); + } + + return `${this.prefix}${consumer}/${operation}`; + } +} 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 new file mode 100644 index 00000000000000..2695280f0ecf74 --- /dev/null +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts @@ -0,0 +1,36 @@ +/* + * 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 { uniq } from 'lodash'; + +import type { FeatureKibanaPrivileges, KibanaFeature } from '../../../../../features/server'; +import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; + +const readOperations: string[] = ['get', 'find']; +const writeOperations: string[] = ['create', 'delete', 'update']; +const allOperations: string[] = [...readOperations, ...writeOperations]; + +export class FeaturePrivilegeCasesBuilder extends BaseFeaturePrivilegeBuilder { + public getActions( + privilegeDefinition: FeatureKibanaPrivileges, + feature: KibanaFeature + ): string[] { + const getCasesPrivilege = (operations: string[], consumer: string) => + operations.map((operation) => this.actions.cases.get(consumer, operation)); + + // TODO: make sure we don't need to add a cases array or flag? to the FeatureKibanaPrivileges + // I think we'd only need to do that if we wanted a plugin to be able to get permissions for cases from other plugins? + // I think we only want the plugin to get access to the cases that are created through itself and not allow it to have + // access to other plugins + + // It may make sense to add a cases field as a flag so plugins have to opt in to getting access to cases + return uniq([ + ...getCasesPrivilege(allOperations, feature.id), + ...getCasesPrivilege(readOperations, feature.id), + ]); + } +} From f8e62c6c37dad7f24e721d1efb1e508400a797f3 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 10 Mar 2021 09:51:31 +0200 Subject: [PATCH 004/113] Consumer as the app's plugin ID --- x-pack/plugins/case/common/constants.ts | 8 +++++--- .../server/saved_object_types/migrations.ts | 20 +++++++------------ 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/case/common/constants.ts b/x-pack/plugins/case/common/constants.ts index 2a031cd87eb3f0..33d731c92d422a 100644 --- a/x-pack/plugins/case/common/constants.ts +++ b/x-pack/plugins/case/common/constants.ts @@ -5,7 +5,10 @@ * 2.0. */ -import { DEFAULT_MAX_SIGNALS } from '../../security_solution/common/constants'; +import { + DEFAULT_MAX_SIGNALS, + APP_ID as SECURITY_SOLUTION_PLUGIN_APP_ID, +} from '../../security_solution/common/constants'; export const APP_ID = 'case'; @@ -56,5 +59,4 @@ export const SUPPORTED_CONNECTORS = [ export const MAX_ALERTS_PER_SUB_CASE = 5000; export const MAX_GENERATED_ALERTS_PER_SUB_CASE = MAX_ALERTS_PER_SUB_CASE / DEFAULT_MAX_SIGNALS; -// TODO: figure out what the value will actually be when a security solution case is created -export const SECURITY_SOLUTION_PLUGIN = 'security-solution'; +export const SECURITY_SOLUTION_CONSUMER = SECURITY_SOLUTION_PLUGIN_APP_ID; diff --git a/x-pack/plugins/case/server/saved_object_types/migrations.ts b/x-pack/plugins/case/server/saved_object_types/migrations.ts index 37ab1c58fca7b3..2bc1adb2c638d1 100644 --- a/x-pack/plugins/case/server/saved_object_types/migrations.ts +++ b/x-pack/plugins/case/server/saved_object_types/migrations.ts @@ -15,7 +15,7 @@ import { AssociationType, ESConnectorFields, } from '../../common/api'; -import { SECURITY_SOLUTION_PLUGIN } from '../../common/constants'; +import { SECURITY_SOLUTION_CONSUMER } from '../../common/constants'; interface UnsanitizedCaseConnector { connector_id: string; @@ -125,8 +125,7 @@ export const caseMigrations = { ...doc, attributes: { ...doc.attributes, - // TODO: figure out what to default this to - consumer: SECURITY_SOLUTION_PLUGIN, + consumer: SECURITY_SOLUTION_CONSUMER, }, references: doc.references || [], }; @@ -160,8 +159,7 @@ export const configureMigrations = { ...doc, attributes: { ...doc.attributes, - // TODO: figure out what to default this to - consumer: SECURITY_SOLUTION_PLUGIN, + consumer: SECURITY_SOLUTION_CONSUMER, }, references: doc.references || [], }; @@ -214,8 +212,7 @@ export const userActionsMigrations = { ...doc, attributes: { ...doc.attributes, - // TODO: figure out what to default this to - consumer: SECURITY_SOLUTION_PLUGIN, + consumer: SECURITY_SOLUTION_CONSUMER, }, references: doc.references || [], }; @@ -277,8 +274,7 @@ export const commentsMigrations = { ...doc, attributes: { ...doc.attributes, - // TODO: figure out what to default this to - consumer: SECURITY_SOLUTION_PLUGIN, + consumer: SECURITY_SOLUTION_CONSUMER, }, references: doc.references || [], }; @@ -293,8 +289,7 @@ export const connectorMappingsMigrations = { ...doc, attributes: { ...doc.attributes, - // TODO: figure out what to default this to - consumer: SECURITY_SOLUTION_PLUGIN, + consumer: SECURITY_SOLUTION_CONSUMER, }, references: doc.references || [], }; @@ -309,8 +304,7 @@ export const subCasesMigrations = { ...doc, attributes: { ...doc.attributes, - // TODO: figure out what to default this to - consumer: SECURITY_SOLUTION_PLUGIN, + consumer: SECURITY_SOLUTION_CONSUMER, }, references: doc.references || [], }; From 09589c3915c9d69dc047a3590fd549009da01ab4 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 10 Mar 2021 09:59:42 +0200 Subject: [PATCH 005/113] Create addConsumerToSO migration helper --- .../server/saved_object_types/migrations.ts | 56 ++++++------------- 1 file changed, 16 insertions(+), 40 deletions(-) diff --git a/x-pack/plugins/case/server/saved_object_types/migrations.ts b/x-pack/plugins/case/server/saved_object_types/migrations.ts index 2bc1adb2c638d1..cca6e017b384ed 100644 --- a/x-pack/plugins/case/server/saved_object_types/migrations.ts +++ b/x-pack/plugins/case/server/saved_object_types/migrations.ts @@ -64,6 +64,17 @@ interface SanitizedCaseConsumer { consumer: string; } +const addConsumerToSO = >( + doc: SavedObjectUnsanitizedDoc +): SavedObjectSanitizedDoc => ({ + ...doc, + attributes: { + ...doc.attributes, + consumer: SECURITY_SOLUTION_CONSUMER, + }, + references: doc.references || [], +}); + export const caseMigrations = { '7.10.0': ( doc: SavedObjectUnsanitizedDoc @@ -121,14 +132,7 @@ export const caseMigrations = { '7.13.0': ( doc: SavedObjectUnsanitizedDoc> ): SavedObjectSanitizedDoc => { - return { - ...doc, - attributes: { - ...doc.attributes, - consumer: SECURITY_SOLUTION_CONSUMER, - }, - references: doc.references || [], - }; + return addConsumerToSO(doc); }, }; @@ -208,14 +212,7 @@ export const userActionsMigrations = { '7.13.0': ( doc: SavedObjectUnsanitizedDoc> ): SavedObjectSanitizedDoc => { - return { - ...doc, - attributes: { - ...doc.attributes, - consumer: SECURITY_SOLUTION_CONSUMER, - }, - references: doc.references || [], - }; + return addConsumerToSO(doc); }, }; @@ -270,14 +267,7 @@ export const commentsMigrations = { '7.13.0': ( doc: SavedObjectUnsanitizedDoc> ): SavedObjectSanitizedDoc => { - return { - ...doc, - attributes: { - ...doc.attributes, - consumer: SECURITY_SOLUTION_CONSUMER, - }, - references: doc.references || [], - }; + return addConsumerToSO(doc); }, }; @@ -285,14 +275,7 @@ export const connectorMappingsMigrations = { '7.13.0': ( doc: SavedObjectUnsanitizedDoc> ): SavedObjectSanitizedDoc => { - return { - ...doc, - attributes: { - ...doc.attributes, - consumer: SECURITY_SOLUTION_CONSUMER, - }, - references: doc.references || [], - }; + return addConsumerToSO(doc); }, }; @@ -300,13 +283,6 @@ export const subCasesMigrations = { '7.13.0': ( doc: SavedObjectUnsanitizedDoc> ): SavedObjectSanitizedDoc => { - return { - ...doc, - attributes: { - ...doc.attributes, - consumer: SECURITY_SOLUTION_CONSUMER, - }, - references: doc.references || [], - }; + return addConsumerToSO(doc); }, }; From eb75eb0050d2a8cf54fbfd80e95fc7df684c6134 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 10 Mar 2021 10:01:24 +0200 Subject: [PATCH 006/113] Fix mapping's SO consumer --- .../case/server/saved_object_types/connector_mappings.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/case/server/saved_object_types/connector_mappings.ts b/x-pack/plugins/case/server/saved_object_types/connector_mappings.ts index 1033e1279d5745..55888e45b51d03 100644 --- a/x-pack/plugins/case/server/saved_object_types/connector_mappings.ts +++ b/x-pack/plugins/case/server/saved_object_types/connector_mappings.ts @@ -18,9 +18,6 @@ export const caseConnectorMappingsSavedObjectType: SavedObjectsType = { properties: { mappings: { properties: { - consumer: { - type: 'keyword', - }, source: { type: 'keyword', }, @@ -32,6 +29,9 @@ export const caseConnectorMappingsSavedObjectType: SavedObjectsType = { }, }, }, + consumer: { + type: 'keyword', + }, }, }, migrations: connectorMappingsMigrations, From a930f031310412ae9cc2e3f5079d6a6f3c699065 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 10 Mar 2021 10:11:19 +0200 Subject: [PATCH 007/113] Add test for CasesActions --- .../actions/__snapshots__/cases.test.ts.snap | 25 ++++++++++++++ .../authorization/actions/cases.test.ts | 33 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 x-pack/plugins/security/server/authorization/actions/__snapshots__/cases.test.ts.snap create mode 100644 x-pack/plugins/security/server/authorization/actions/cases.test.ts diff --git a/x-pack/plugins/security/server/authorization/actions/__snapshots__/cases.test.ts.snap b/x-pack/plugins/security/server/authorization/actions/__snapshots__/cases.test.ts.snap new file mode 100644 index 00000000000000..3f5c0c9b3d44b1 --- /dev/null +++ b/x-pack/plugins/security/server/authorization/actions/__snapshots__/cases.test.ts.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`#get consumer of "" throws error 1`] = `"consumer is required and must be a string"`; + +exports[`#get consumer of {} throws error 1`] = `"consumer is required and must be a string"`; + +exports[`#get consumer of 1 throws error 1`] = `"consumer is required and must be a string"`; + +exports[`#get consumer of null throws error 1`] = `"consumer is required and must be a string"`; + +exports[`#get consumer of true throws error 1`] = `"consumer is required and must be a string"`; + +exports[`#get consumer of undefined throws error 1`] = `"consumer is required and must be a string"`; + +exports[`#get operation of "" throws error 1`] = `"operation is required and must be a string"`; + +exports[`#get operation of {} throws error 1`] = `"operation is required and must be a string"`; + +exports[`#get operation of 1 throws error 1`] = `"operation is required and must be a string"`; + +exports[`#get operation of null throws error 1`] = `"operation is required and must be a string"`; + +exports[`#get operation of true throws error 1`] = `"operation is required and must be a string"`; + +exports[`#get operation of undefined throws error 1`] = `"operation is required and must be a string"`; diff --git a/x-pack/plugins/security/server/authorization/actions/cases.test.ts b/x-pack/plugins/security/server/authorization/actions/cases.test.ts new file mode 100644 index 00000000000000..8f0ef64af4747d --- /dev/null +++ b/x-pack/plugins/security/server/authorization/actions/cases.test.ts @@ -0,0 +1,33 @@ +/* + * 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 { CasesActions } from './cases'; + +const version = '1.0.0-zeta1'; + +describe('#get', () => { + [null, undefined, '', 1, true, {}].forEach((operation: any) => { + test(`operation of ${JSON.stringify(operation)} throws error`, () => { + const alertingActions = new CasesActions(version); + expect(() => alertingActions.get('consumer', operation)).toThrowErrorMatchingSnapshot(); + }); + }); + + [null, undefined, '', 1, true, {}].forEach((consumer: any) => { + test(`consumer of ${JSON.stringify(consumer)} throws error`, () => { + const alertingActions = new CasesActions(version); + expect(() => alertingActions.get(consumer, 'operation')).toThrowErrorMatchingSnapshot(); + }); + }); + + test('returns `cases:${consumer}/${operation}`', () => { + const alertingActions = new CasesActions(version); + expect(alertingActions.get('consumer', 'bar-operation')).toBe( + 'cases:1.0.0-zeta1:consumer/bar-operation' + ); + }); +}); From d6f3b0916c59eec1f51e2ed6aee0a19cc73d1555 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 10 Mar 2021 14:33:13 +0200 Subject: [PATCH 008/113] Declare hidden types on SO client --- x-pack/plugins/case/common/constants.ts | 16 ++++++++++++++++ .../plugins/case/server/client/cases/update.ts | 2 +- .../plugins/case/server/client/comments/add.ts | 6 ++++-- .../case/server/client/user_actions/get.ts | 2 +- .../server/common/models/commentable_case.ts | 2 +- x-pack/plugins/case/server/connectors/index.ts | 4 ++-- x-pack/plugins/case/server/connectors/types.ts | 17 ++--------------- x-pack/plugins/case/server/plugin.ts | 12 ++++++++---- .../__fixtures__/create_mock_so_repository.ts | 2 +- .../api/__fixtures__/mock_saved_objects.ts | 2 +- .../api/cases/comments/delete_all_comments.ts | 6 ++++-- .../routes/api/cases/comments/delete_comment.ts | 12 +++++++++--- .../routes/api/cases/comments/find_comments.ts | 6 ++++-- .../api/cases/comments/get_all_comment.ts | 6 ++++-- .../routes/api/cases/comments/get_comment.ts | 6 ++++-- .../routes/api/cases/comments/patch_comment.ts | 12 +++++++++--- .../routes/api/cases/configure/get_configure.ts | 6 ++++-- .../api/cases/configure/patch_configure.ts | 6 ++++-- .../api/cases/configure/post_configure.ts | 6 ++++-- .../server/routes/api/cases/delete_cases.ts | 6 ++++-- .../case/server/routes/api/cases/find_cases.ts | 6 ++++-- .../case/server/routes/api/cases/helpers.ts | 2 +- .../routes/api/cases/reporters/get_reporters.ts | 6 ++++-- .../routes/api/cases/status/get_status.ts | 6 ++++-- .../api/cases/sub_case/delete_sub_cases.ts | 11 ++++++++--- .../routes/api/cases/sub_case/find_sub_cases.ts | 6 ++++-- .../routes/api/cases/sub_case/get_sub_case.ts | 6 ++++-- .../api/cases/sub_case/patch_sub_cases.ts | 7 +++++-- .../server/routes/api/cases/tags/get_tags.ts | 6 ++++-- .../case/server/saved_object_types/cases.ts | 3 +-- .../case/server/saved_object_types/comments.ts | 3 +-- .../case/server/saved_object_types/configure.ts | 3 +-- .../saved_object_types/connector_mappings.ts | 3 +-- .../case/server/saved_object_types/index.ts | 15 ++++++--------- .../case/server/saved_object_types/sub_case.ts | 3 +-- .../server/saved_object_types/user_actions.ts | 3 +-- .../case/server/services/configure/index.ts | 2 +- .../server/services/connector_mappings/index.ts | 2 +- x-pack/plugins/case/server/services/index.ts | 2 +- .../server/services/reporters/read_reporters.ts | 2 +- .../case/server/services/tags/read_tags.ts | 2 +- .../server/services/user_actions/helpers.ts | 2 +- .../case/server/services/user_actions/index.ts | 2 +- x-pack/plugins/case/server/types.ts | 16 ++++++++++++++++ 44 files changed, 160 insertions(+), 96 deletions(-) diff --git a/x-pack/plugins/case/common/constants.ts b/x-pack/plugins/case/common/constants.ts index 33d731c92d422a..d16b2e302095e9 100644 --- a/x-pack/plugins/case/common/constants.ts +++ b/x-pack/plugins/case/common/constants.ts @@ -12,6 +12,22 @@ import { export const APP_ID = 'case'; +export const CASE_SAVED_OBJECT = 'cases'; +export const CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT = 'cases-connector-mappings'; +export const SUB_CASE_SAVED_OBJECT = 'cases-sub-case'; +export const CASE_USER_ACTION_SAVED_OBJECT = 'cases-user-actions'; +export const CASE_COMMENT_SAVED_OBJECT = 'cases-comments'; +export const CASE_CONFIGURE_SAVED_OBJECT = 'cases-configure'; + +export const SAVED_OBJECT_TYPES = [ + CASE_SAVED_OBJECT, + CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, + SUB_CASE_SAVED_OBJECT, + CASE_USER_ACTION_SAVED_OBJECT, + CASE_COMMENT_SAVED_OBJECT, + CASE_CONFIGURE_SAVED_OBJECT, +]; + /** * Case routes */ diff --git a/x-pack/plugins/case/server/client/cases/update.ts b/x-pack/plugins/case/server/client/cases/update.ts index 8c788d6f3bcd92..ffb767f83ae72c 100644 --- a/x-pack/plugins/case/server/client/cases/update.ts +++ b/x-pack/plugins/case/server/client/cases/update.ts @@ -50,7 +50,7 @@ import { CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT, -} from '../../saved_object_types'; +} from '../../../common/constants'; import { CaseClientHandler } from '..'; import { createAlertUpdateRequest } from '../../common'; import { UpdateAlertRequest } from '../types'; diff --git a/x-pack/plugins/case/server/client/comments/add.ts b/x-pack/plugins/case/server/client/comments/add.ts index 22a59e4d0539bd..8e289921c5d185 100644 --- a/x-pack/plugins/case/server/client/comments/add.ts +++ b/x-pack/plugins/case/server/client/comments/add.ts @@ -35,8 +35,10 @@ import { CaseServiceSetup, CaseUserActionServiceSetup } from '../../services'; import { CommentableCase, createAlertUpdateRequest } from '../../common'; import { CaseClientHandler } from '..'; import { createCaseError } from '../../common/error'; -import { CASE_COMMENT_SAVED_OBJECT } from '../../saved_object_types'; -import { MAX_GENERATED_ALERTS_PER_SUB_CASE } from '../../../common/constants'; +import { + MAX_GENERATED_ALERTS_PER_SUB_CASE, + CASE_COMMENT_SAVED_OBJECT, +} from '../../../common/constants'; async function getSubCase({ caseService, diff --git a/x-pack/plugins/case/server/client/user_actions/get.ts b/x-pack/plugins/case/server/client/user_actions/get.ts index f6371b8e8b1e7b..d5df4fc35f246b 100644 --- a/x-pack/plugins/case/server/client/user_actions/get.ts +++ b/x-pack/plugins/case/server/client/user_actions/get.ts @@ -10,7 +10,7 @@ import { CASE_SAVED_OBJECT, CASE_COMMENT_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT, -} from '../../saved_object_types'; +} from '../../../common/constants'; import { CaseUserActionsResponseRt, CaseUserActionsResponse } from '../../../common/api'; import { CaseUserActionServiceSetup } from '../../services'; diff --git a/x-pack/plugins/case/server/common/models/commentable_case.ts b/x-pack/plugins/case/server/common/models/commentable_case.ts index 1ff5b7beadcaf1..527d851631583d 100644 --- a/x-pack/plugins/case/server/common/models/commentable_case.ts +++ b/x-pack/plugins/case/server/common/models/commentable_case.ts @@ -34,7 +34,7 @@ import { flattenSubCaseSavedObject, transformNewComment, } from '../../routes/api/utils'; -import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../saved_object_types'; +import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../common/constants'; import { CaseServiceSetup } from '../../services'; import { createCaseError } from '../error'; import { countAlertsForID } from '../index'; diff --git a/x-pack/plugins/case/server/connectors/index.ts b/x-pack/plugins/case/server/connectors/index.ts index a6b6e193361bed..e55850fb6c02c2 100644 --- a/x-pack/plugins/case/server/connectors/index.ts +++ b/x-pack/plugins/case/server/connectors/index.ts @@ -29,7 +29,7 @@ export { transformConnectorComment } from './case'; export const separator = '__SEPARATOR__'; export const registerConnectors = ({ - actionsRegisterType, + registerActionType, logger, caseService, caseConfigureService, @@ -37,7 +37,7 @@ export const registerConnectors = ({ userActionService, alertsService, }: RegisterConnectorsArgs) => { - actionsRegisterType( + registerActionType( getCaseConnector({ logger, caseService, diff --git a/x-pack/plugins/case/server/connectors/types.ts b/x-pack/plugins/case/server/connectors/types.ts index ffda6f96ae3ba0..c35ce38a2730f2 100644 --- a/x-pack/plugins/case/server/connectors/types.ts +++ b/x-pack/plugins/case/server/connectors/types.ts @@ -6,13 +6,6 @@ */ import { Logger } from 'kibana/server'; -import { - ActionTypeConfig, - ActionTypeSecrets, - ActionTypeParams, - ActionType, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../actions/server/types'; import { CaseResponse, ConnectorTypes } from '../../common/api'; import { CaseClientGetAlertsResponse } from '../client/alerts/types'; import { @@ -22,6 +15,7 @@ import { ConnectorMappingsServiceSetup, AlertServiceContract, } from '../services'; +import { RegisterActionType } from '../types'; export { ContextTypeGeneratedAlertType, @@ -39,14 +33,7 @@ export interface GetActionTypeParams { } export interface RegisterConnectorsArgs extends GetActionTypeParams { - actionsRegisterType< - Config extends ActionTypeConfig = ActionTypeConfig, - Secrets extends ActionTypeSecrets = ActionTypeSecrets, - Params extends ActionTypeParams = ActionTypeParams, - ExecutorResultData = void - >( - actionType: ActionType - ): void; + registerActionType: RegisterActionType; } export type FormatterConnectorTypes = Exclude; diff --git a/x-pack/plugins/case/server/plugin.ts b/x-pack/plugins/case/server/plugin.ts index 43daa519584297..deb6296bfc8606 100644 --- a/x-pack/plugins/case/server/plugin.ts +++ b/x-pack/plugins/case/server/plugin.ts @@ -10,7 +10,7 @@ import { CoreSetup, CoreStart } from 'src/core/server'; import { SecurityPluginSetup } from '../../security/server'; import { PluginSetupContract as ActionsPluginSetup } from '../../actions/server'; -import { APP_ID } from '../common/constants'; +import { APP_ID, SAVED_OBJECT_TYPES } from '../common/constants'; import { ConfigType } from './config'; import { initCaseApi } from './routes/api'; @@ -112,7 +112,7 @@ export class CasePlugin { }); registerConnectors({ - actionsRegisterType: plugins.actions.registerType, + registerActionType: plugins.actions.registerType, logger: this.log, caseService: this.caseService, caseConfigureService: this.caseConfigureService, @@ -132,7 +132,9 @@ export class CasePlugin { const user = await this.caseService!.getUser({ request }); return createExternalCaseClient({ scopedClusterClient: context.core.elasticsearch.client.asCurrentUser, - savedObjectsClient: core.savedObjects.getScopedClient(request), + savedObjectsClient: core.savedObjects.getScopedClient(request, { + includedHiddenTypes: SAVED_OBJECT_TYPES, + }), user, caseService: this.caseService!, caseConfigureService: this.caseConfigureService!, @@ -176,7 +178,9 @@ export class CasePlugin { getCaseClient: () => { return new CaseClientHandler({ scopedClusterClient: context.core.elasticsearch.client.asCurrentUser, - savedObjectsClient: savedObjects.getScopedClient(request), + savedObjectsClient: savedObjects.getScopedClient(request, { + includedHiddenTypes: SAVED_OBJECT_TYPES, + }), caseService, caseConfigureService, connectorMappingsService, diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts index a33226bcde8998..a6acd917e4eea4 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts @@ -20,7 +20,7 @@ import { CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT, CASE_USER_ACTION_SAVED_OBJECT, -} from '../../../saved_object_types'; +} from '../../../../common/constants'; export const createMockSavedObjectsRepository = ({ caseSavedObject = [], diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts index f2318c45e6ed39..e37b3a2ac257b9 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts @@ -21,7 +21,7 @@ import { import { CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, CASE_USER_ACTION_SAVED_OBJECT, -} from '../../../saved_object_types'; +} from '../../../../common/constants'; import { mappings } from '../../../client/configure/mock'; export const mockCases: Array> = [ diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts b/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts index fd250b74fff1e3..25548d8e024df1 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts @@ -10,7 +10,7 @@ import { schema } from '@kbn/config-schema'; import { buildCommentUserActionItem } from '../../../../services/user_actions/helpers'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; -import { CASE_COMMENTS_URL } from '../../../../../common/constants'; +import { CASE_COMMENTS_URL, SAVED_OBJECT_TYPES } from '../../../../../common/constants'; import { AssociationType } from '../../../../../common/api'; export function initDeleteAllCommentsApi({ @@ -35,7 +35,9 @@ export function initDeleteAllCommentsApi({ }, async (context, request, response) => { try { - const client = context.core.savedObjects.client; + const client = context.core.savedObjects.getClient({ + includedHiddenTypes: SAVED_OBJECT_TYPES, + }); // eslint-disable-next-line @typescript-eslint/naming-convention const { username, full_name, email } = await caseService.getUser({ request }); const deleteDate = new Date().toISOString(); diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts index f1c5fdc2b7cc82..6b124c76f8d43d 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts @@ -8,11 +8,15 @@ import Boom from '@hapi/boom'; import { schema } from '@kbn/config-schema'; -import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../../saved_object_types'; import { buildCommentUserActionItem } from '../../../../services/user_actions/helpers'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; -import { CASE_COMMENT_DETAILS_URL } from '../../../../../common/constants'; +import { + CASE_COMMENT_DETAILS_URL, + SAVED_OBJECT_TYPES, + CASE_SAVED_OBJECT, + SUB_CASE_SAVED_OBJECT, +} from '../../../../../common/constants'; export function initDeleteCommentApi({ caseService, @@ -37,7 +41,9 @@ export function initDeleteCommentApi({ }, async (context, request, response) => { try { - const client = context.core.savedObjects.client; + const client = context.core.savedObjects.getClient({ + includedHiddenTypes: SAVED_OBJECT_TYPES, + }); // eslint-disable-next-line @typescript-eslint/naming-convention const { username, full_name, email } = await caseService.getUser({ request }); const deleteDate = new Date().toISOString(); diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/find_comments.ts b/x-pack/plugins/case/server/routes/api/cases/comments/find_comments.ts index 57ddd84e8742c5..a33f76a0fcbb18 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/find_comments.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/find_comments.ts @@ -22,7 +22,7 @@ import { } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { escapeHatch, transformComments, wrapError } from '../../utils'; -import { CASE_COMMENTS_URL } from '../../../../../common/constants'; +import { CASE_COMMENTS_URL, SAVED_OBJECT_TYPES } from '../../../../../common/constants'; import { defaultPage, defaultPerPage } from '../..'; const FindQueryParamsRt = rt.partial({ @@ -43,7 +43,9 @@ export function initFindCaseCommentsApi({ caseService, router, logger }: RouteDe }, async (context, request, response) => { try { - const client = context.core.savedObjects.client; + const client = context.core.savedObjects.getClient({ + includedHiddenTypes: SAVED_OBJECT_TYPES, + }); const query = pipe( FindQueryParamsRt.decode(request.query), fold(throwErrors(Boom.badRequest), identity) diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/get_all_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/get_all_comment.ts index 770efe0109744c..a0424a81793747 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/get_all_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/get_all_comment.ts @@ -11,7 +11,7 @@ import { SavedObjectsFindResponse } from 'kibana/server'; import { AllCommentsResponseRt, CommentAttributes } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { flattenCommentSavedObjects, wrapError } from '../../utils'; -import { CASE_COMMENTS_URL } from '../../../../../common/constants'; +import { CASE_COMMENTS_URL, SAVED_OBJECT_TYPES } from '../../../../../common/constants'; import { defaultSortField } from '../../../../common'; export function initGetAllCommentsApi({ caseService, router, logger }: RouteDeps) { @@ -32,7 +32,9 @@ export function initGetAllCommentsApi({ caseService, router, logger }: RouteDeps }, async (context, request, response) => { try { - const client = context.core.savedObjects.client; + const client = context.core.savedObjects.getClient({ + includedHiddenTypes: SAVED_OBJECT_TYPES, + }); let comments: SavedObjectsFindResponse; if (request.query?.subCaseId) { diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.ts index 9dedfccd3a250e..f188a67417f6d1 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.ts @@ -10,7 +10,7 @@ import { schema } from '@kbn/config-schema'; import { CommentResponseRt } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { flattenCommentSavedObject, wrapError } from '../../utils'; -import { CASE_COMMENT_DETAILS_URL } from '../../../../../common/constants'; +import { CASE_COMMENT_DETAILS_URL, SAVED_OBJECT_TYPES } from '../../../../../common/constants'; export function initGetCommentApi({ caseService, router, logger }: RouteDeps) { router.get( @@ -25,7 +25,9 @@ export function initGetCommentApi({ caseService, router, logger }: RouteDeps) { }, async (context, request, response) => { try { - const client = context.core.savedObjects.client; + const client = context.core.savedObjects.getClient({ + includedHiddenTypes: SAVED_OBJECT_TYPES, + }); const comment = await caseService.getComment({ client, diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts index f5db2dc004a1d7..a93550299ff496 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts @@ -15,11 +15,15 @@ import Boom from '@hapi/boom'; import { SavedObjectsClientContract, Logger } from 'kibana/server'; import { CommentableCase } from '../../../../common'; import { CommentPatchRequestRt, throwErrors, User } from '../../../../../common/api'; -import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../../saved_object_types'; import { buildCommentUserActionItem } from '../../../../services/user_actions/helpers'; import { RouteDeps } from '../../types'; import { escapeHatch, wrapError, decodeCommentRequest } from '../../utils'; -import { CASE_COMMENTS_URL } from '../../../../../common/constants'; +import { + CASE_COMMENTS_URL, + SAVED_OBJECT_TYPES, + CASE_SAVED_OBJECT, + SUB_CASE_SAVED_OBJECT, +} from '../../../../../common/constants'; import { CaseServiceSetup } from '../../../../services'; interface CombinedCaseParams { @@ -82,7 +86,9 @@ export function initPatchCommentApi({ caseService, router, userActionService, lo }, async (context, request, response) => { try { - const client = context.core.savedObjects.client; + const client = context.core.savedObjects.getClient({ + includedHiddenTypes: SAVED_OBJECT_TYPES, + }); const query = pipe( CommentPatchRequestRt.decode(request.body), fold(throwErrors(Boom.badRequest), identity) diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts index 2ca34d25482dd4..9430074a5277e4 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts @@ -9,7 +9,7 @@ import Boom from '@hapi/boom'; import { CaseConfigureResponseRt, ConnectorMappingsAttributes } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; -import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; +import { CASE_CONFIGURE_URL, SAVED_OBJECT_TYPES } from '../../../../../common/constants'; import { transformESConnectorToCaseConnector } from '../helpers'; export function initGetCaseConfigure({ caseConfigureService, router, logger }: RouteDeps) { @@ -21,7 +21,9 @@ export function initGetCaseConfigure({ caseConfigureService, router, logger }: R async (context, request, response) => { try { let error = null; - const client = context.core.savedObjects.client; + const client = context.core.savedObjects.getClient({ + includedHiddenTypes: SAVED_OBJECT_TYPES, + }); const myCaseConfigure = await caseConfigureService.find({ client }); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts index cd764bb0e8a3e5..db9ad493d6e93c 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts @@ -18,7 +18,7 @@ import { } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { wrapError, escapeHatch } from '../../utils'; -import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; +import { CASE_CONFIGURE_URL, SAVED_OBJECT_TYPES } from '../../../../../common/constants'; import { transformCaseConnectorToEsConnector, transformESConnectorToCaseConnector, @@ -40,7 +40,9 @@ export function initPatchCaseConfigure({ async (context, request, response) => { try { let error = null; - const client = context.core.savedObjects.client; + const client = context.core.savedObjects.getClient({ + includedHiddenTypes: SAVED_OBJECT_TYPES, + }); const query = pipe( CasesConfigurePatchRt.decode(request.body), fold(throwErrors(Boom.badRequest), identity) diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts index f619a727e2e7aa..a000f5a6b87ad2 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts @@ -18,7 +18,7 @@ import { } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { wrapError, escapeHatch } from '../../utils'; -import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; +import { CASE_CONFIGURE_URL, SAVED_OBJECT_TYPES } from '../../../../../common/constants'; import { transformCaseConnectorToEsConnector, transformESConnectorToCaseConnector, @@ -48,7 +48,9 @@ export function initPostCaseConfigure({ if (actionsClient == null) { throw Boom.notFound('Action client not found'); } - const client = context.core.savedObjects.client; + const client = context.core.savedObjects.getClient({ + includedHiddenTypes: SAVED_OBJECT_TYPES, + }); const query = pipe( CasesConfigureRequestRt.decode(request.body), fold(throwErrors(Boom.badRequest), identity) diff --git a/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts b/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts index 5f2a6c67220c3f..2eb4992dfc9bb2 100644 --- a/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts @@ -11,7 +11,7 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { buildCaseUserActionItem } from '../../../services/user_actions/helpers'; import { RouteDeps } from '../types'; import { wrapError } from '../utils'; -import { CASES_URL } from '../../../../common/constants'; +import { CASES_URL, SAVED_OBJECT_TYPES } from '../../../../common/constants'; import { CaseServiceSetup } from '../../../services'; async function deleteSubCases({ @@ -58,7 +58,9 @@ export function initDeleteCasesApi({ caseService, router, userActionService, log }, async (context, request, response) => { try { - const client = context.core.savedObjects.client; + const client = context.core.savedObjects.getClient({ + includedHiddenTypes: SAVED_OBJECT_TYPES, + }); await Promise.all( request.query.ids.map((id) => caseService.deleteCase({ diff --git a/x-pack/plugins/case/server/routes/api/cases/find_cases.ts b/x-pack/plugins/case/server/routes/api/cases/find_cases.ts index bc6907f52b9eba..98f3d260db724d 100644 --- a/x-pack/plugins/case/server/routes/api/cases/find_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/find_cases.ts @@ -19,7 +19,7 @@ import { } from '../../../../common/api'; import { transformCases, wrapError, escapeHatch } from '../utils'; import { RouteDeps } from '../types'; -import { CASES_URL } from '../../../../common/constants'; +import { CASES_URL, SAVED_OBJECT_TYPES } from '../../../../common/constants'; import { constructQueryOptions } from './helpers'; export function initFindCasesApi({ caseService, router, logger }: RouteDeps) { @@ -32,7 +32,9 @@ export function initFindCasesApi({ caseService, router, logger }: RouteDeps) { }, async (context, request, response) => { try { - const client = context.core.savedObjects.client; + const client = context.core.savedObjects.getClient({ + includedHiddenTypes: SAVED_OBJECT_TYPES, + }); const queryParams = pipe( CasesFindRequestRt.decode(request.query), fold(throwErrors(Boom.badRequest), identity) diff --git a/x-pack/plugins/case/server/routes/api/cases/helpers.ts b/x-pack/plugins/case/server/routes/api/cases/helpers.ts index 8659ab02d6d532..8be100919fbdfe 100644 --- a/x-pack/plugins/case/server/routes/api/cases/helpers.ts +++ b/x-pack/plugins/case/server/routes/api/cases/helpers.ts @@ -19,7 +19,7 @@ import { SavedObjectFindOptions, } from '../../../../common/api'; import { ESConnectorFields, ConnectorTypeFields } from '../../../../common/api/connectors'; -import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../saved_object_types'; +import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../../common/constants'; import { sortToSnake } from '../utils'; import { combineFilters } from '../../../common'; diff --git a/x-pack/plugins/case/server/routes/api/cases/reporters/get_reporters.ts b/x-pack/plugins/case/server/routes/api/cases/reporters/get_reporters.ts index e5433f49722395..bbb21da1b71f40 100644 --- a/x-pack/plugins/case/server/routes/api/cases/reporters/get_reporters.ts +++ b/x-pack/plugins/case/server/routes/api/cases/reporters/get_reporters.ts @@ -8,7 +8,7 @@ import { UsersRt } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; -import { CASE_REPORTERS_URL } from '../../../../../common/constants'; +import { CASE_REPORTERS_URL, SAVED_OBJECT_TYPES } from '../../../../../common/constants'; export function initGetReportersApi({ caseService, router, logger }: RouteDeps) { router.get( @@ -18,7 +18,9 @@ export function initGetReportersApi({ caseService, router, logger }: RouteDeps) }, async (context, request, response) => { try { - const client = context.core.savedObjects.client; + const client = context.core.savedObjects.getClient({ + includedHiddenTypes: SAVED_OBJECT_TYPES, + }); const reporters = await caseService.getReporters({ client, }); diff --git a/x-pack/plugins/case/server/routes/api/cases/status/get_status.ts b/x-pack/plugins/case/server/routes/api/cases/status/get_status.ts index d0addfff091243..291a5541cf42ac 100644 --- a/x-pack/plugins/case/server/routes/api/cases/status/get_status.ts +++ b/x-pack/plugins/case/server/routes/api/cases/status/get_status.ts @@ -9,7 +9,7 @@ import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; import { CasesStatusResponseRt, caseStatuses } from '../../../../../common/api'; -import { CASE_STATUS_URL } from '../../../../../common/constants'; +import { CASE_STATUS_URL, SAVED_OBJECT_TYPES } from '../../../../../common/constants'; import { constructQueryOptions } from '../helpers'; export function initGetCasesStatusApi({ caseService, router, logger }: RouteDeps) { @@ -20,7 +20,9 @@ export function initGetCasesStatusApi({ caseService, router, logger }: RouteDeps }, async (context, request, response) => { try { - const client = context.core.savedObjects.client; + const client = context.core.savedObjects.getClient({ + includedHiddenTypes: SAVED_OBJECT_TYPES, + }); const [openCases, inProgressCases, closedCases] = await Promise.all([ ...caseStatuses.map((status) => { diff --git a/x-pack/plugins/case/server/routes/api/cases/sub_case/delete_sub_cases.ts b/x-pack/plugins/case/server/routes/api/cases/sub_case/delete_sub_cases.ts index fd33afbd7df8ee..77e94f9eb7e8f0 100644 --- a/x-pack/plugins/case/server/routes/api/cases/sub_case/delete_sub_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/sub_case/delete_sub_cases.ts @@ -10,8 +10,11 @@ import { schema } from '@kbn/config-schema'; import { buildCaseUserActionItem } from '../../../../services/user_actions/helpers'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; -import { SUB_CASES_PATCH_DEL_URL } from '../../../../../common/constants'; -import { CASE_SAVED_OBJECT } from '../../../../saved_object_types'; +import { + SUB_CASES_PATCH_DEL_URL, + SAVED_OBJECT_TYPES, + CASE_SAVED_OBJECT, +} from '../../../../../common/constants'; export function initDeleteSubCasesApi({ caseService, @@ -30,7 +33,9 @@ export function initDeleteSubCasesApi({ }, async (context, request, response) => { try { - const client = context.core.savedObjects.client; + const client = context.core.savedObjects.getClient({ + includedHiddenTypes: SAVED_OBJECT_TYPES, + }); const [comments, subCases] = await Promise.all([ caseService.getAllSubCaseComments({ client, id: request.query.ids }), diff --git a/x-pack/plugins/case/server/routes/api/cases/sub_case/find_sub_cases.ts b/x-pack/plugins/case/server/routes/api/cases/sub_case/find_sub_cases.ts index c24dde1944f832..093d6853af87e8 100644 --- a/x-pack/plugins/case/server/routes/api/cases/sub_case/find_sub_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/sub_case/find_sub_cases.ts @@ -20,7 +20,7 @@ import { } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { escapeHatch, transformSubCases, wrapError } from '../../utils'; -import { SUB_CASES_URL } from '../../../../../common/constants'; +import { SUB_CASES_URL, SAVED_OBJECT_TYPES } from '../../../../../common/constants'; import { constructQueryOptions } from '../helpers'; import { defaultPage, defaultPerPage } from '../..'; @@ -37,7 +37,9 @@ export function initFindSubCasesApi({ caseService, router, logger }: RouteDeps) }, async (context, request, response) => { try { - const client = context.core.savedObjects.client; + const client = context.core.savedObjects.getClient({ + includedHiddenTypes: SAVED_OBJECT_TYPES, + }); const queryParams = pipe( SubCasesFindRequestRt.decode(request.query), fold(throwErrors(Boom.badRequest), identity) diff --git a/x-pack/plugins/case/server/routes/api/cases/sub_case/get_sub_case.ts b/x-pack/plugins/case/server/routes/api/cases/sub_case/get_sub_case.ts index 32dcc924e1a083..093165a7281842 100644 --- a/x-pack/plugins/case/server/routes/api/cases/sub_case/get_sub_case.ts +++ b/x-pack/plugins/case/server/routes/api/cases/sub_case/get_sub_case.ts @@ -10,7 +10,7 @@ import { schema } from '@kbn/config-schema'; import { SubCaseResponseRt } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { flattenSubCaseSavedObject, wrapError } from '../../utils'; -import { SUB_CASE_DETAILS_URL } from '../../../../../common/constants'; +import { SUB_CASE_DETAILS_URL, SAVED_OBJECT_TYPES } from '../../../../../common/constants'; import { countAlertsForID } from '../../../../common'; export function initGetSubCaseApi({ caseService, router, logger }: RouteDeps) { @@ -29,7 +29,9 @@ export function initGetSubCaseApi({ caseService, router, logger }: RouteDeps) { }, async (context, request, response) => { try { - const client = context.core.savedObjects.client; + const client = context.core.savedObjects.getClient({ + includedHiddenTypes: SAVED_OBJECT_TYPES, + }); const includeComments = request.query.includeComments; const subCase = await caseService.getSubCase({ diff --git a/x-pack/plugins/case/server/routes/api/cases/sub_case/patch_sub_cases.ts b/x-pack/plugins/case/server/routes/api/cases/sub_case/patch_sub_cases.ts index da7ec956cad1d4..51f4fccecf9e96 100644 --- a/x-pack/plugins/case/server/routes/api/cases/sub_case/patch_sub_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/sub_case/patch_sub_cases.ts @@ -18,7 +18,6 @@ import { } from 'kibana/server'; import { CaseClient } from '../../../../client'; -import { CASE_COMMENT_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../../saved_object_types'; import { CaseServiceSetup, CaseUserActionServiceSetup } from '../../../../services'; import { CaseStatuses, @@ -36,7 +35,11 @@ import { User, CommentAttributes, } from '../../../../../common/api'; -import { SUB_CASES_PATCH_DEL_URL } from '../../../../../common/constants'; +import { + SUB_CASES_PATCH_DEL_URL, + CASE_COMMENT_SAVED_OBJECT, + SUB_CASE_SAVED_OBJECT, +} from '../../../../../common/constants'; import { RouteDeps } from '../../types'; import { escapeHatch, diff --git a/x-pack/plugins/case/server/routes/api/cases/tags/get_tags.ts b/x-pack/plugins/case/server/routes/api/cases/tags/get_tags.ts index f066aa70ec4722..18231edd16353b 100644 --- a/x-pack/plugins/case/server/routes/api/cases/tags/get_tags.ts +++ b/x-pack/plugins/case/server/routes/api/cases/tags/get_tags.ts @@ -7,7 +7,7 @@ import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; -import { CASE_TAGS_URL } from '../../../../../common/constants'; +import { CASE_TAGS_URL, SAVED_OBJECT_TYPES } from '../../../../../common/constants'; export function initGetTagsApi({ caseService, router }: RouteDeps) { router.get( @@ -17,7 +17,9 @@ export function initGetTagsApi({ caseService, router }: RouteDeps) { }, async (context, request, response) => { try { - const client = context.core.savedObjects.client; + const client = context.core.savedObjects.getClient({ + includedHiddenTypes: SAVED_OBJECT_TYPES, + }); const tags = await caseService.getTags({ client, }); diff --git a/x-pack/plugins/case/server/saved_object_types/cases.ts b/x-pack/plugins/case/server/saved_object_types/cases.ts index 03fc1f3268caf3..fb0e54b07ccff7 100644 --- a/x-pack/plugins/case/server/saved_object_types/cases.ts +++ b/x-pack/plugins/case/server/saved_object_types/cases.ts @@ -6,10 +6,9 @@ */ import { SavedObjectsType } from 'src/core/server'; +import { CASE_SAVED_OBJECT } from '../../common/constants'; import { caseMigrations } from './migrations'; -export const CASE_SAVED_OBJECT = 'cases'; - export const caseSavedObjectType: SavedObjectsType = { name: CASE_SAVED_OBJECT, hidden: true, diff --git a/x-pack/plugins/case/server/saved_object_types/comments.ts b/x-pack/plugins/case/server/saved_object_types/comments.ts index 089746cd6014fa..3ab072f92ce5e3 100644 --- a/x-pack/plugins/case/server/saved_object_types/comments.ts +++ b/x-pack/plugins/case/server/saved_object_types/comments.ts @@ -6,10 +6,9 @@ */ import { SavedObjectsType } from 'src/core/server'; +import { CASE_COMMENT_SAVED_OBJECT } from '../../common/constants'; import { commentsMigrations } from './migrations'; -export const CASE_COMMENT_SAVED_OBJECT = 'cases-comments'; - export const caseCommentSavedObjectType: SavedObjectsType = { name: CASE_COMMENT_SAVED_OBJECT, hidden: true, diff --git a/x-pack/plugins/case/server/saved_object_types/configure.ts b/x-pack/plugins/case/server/saved_object_types/configure.ts index 171d76baa87ff0..609cf526a6e402 100644 --- a/x-pack/plugins/case/server/saved_object_types/configure.ts +++ b/x-pack/plugins/case/server/saved_object_types/configure.ts @@ -6,10 +6,9 @@ */ import { SavedObjectsType } from 'src/core/server'; +import { CASE_CONFIGURE_SAVED_OBJECT } from '../../common/constants'; import { configureMigrations } from './migrations'; -export const CASE_CONFIGURE_SAVED_OBJECT = 'cases-configure'; - export const caseConfigureSavedObjectType: SavedObjectsType = { name: CASE_CONFIGURE_SAVED_OBJECT, hidden: true, diff --git a/x-pack/plugins/case/server/saved_object_types/connector_mappings.ts b/x-pack/plugins/case/server/saved_object_types/connector_mappings.ts index 55888e45b51d03..9f0163f42b9b67 100644 --- a/x-pack/plugins/case/server/saved_object_types/connector_mappings.ts +++ b/x-pack/plugins/case/server/saved_object_types/connector_mappings.ts @@ -6,10 +6,9 @@ */ import { SavedObjectsType } from 'src/core/server'; +import { CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT } from '../../common/constants'; import { connectorMappingsMigrations } from './migrations'; -export const CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT = 'cases-connector-mappings'; - export const caseConnectorMappingsSavedObjectType: SavedObjectsType = { name: CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, hidden: true, diff --git a/x-pack/plugins/case/server/saved_object_types/index.ts b/x-pack/plugins/case/server/saved_object_types/index.ts index 91f104335df8b8..1c6bcf6ca710a7 100644 --- a/x-pack/plugins/case/server/saved_object_types/index.ts +++ b/x-pack/plugins/case/server/saved_object_types/index.ts @@ -5,12 +5,9 @@ * 2.0. */ -export { caseSavedObjectType, CASE_SAVED_OBJECT } from './cases'; -export { subCaseSavedObjectType, SUB_CASE_SAVED_OBJECT } from './sub_case'; -export { caseConfigureSavedObjectType, CASE_CONFIGURE_SAVED_OBJECT } from './configure'; -export { caseCommentSavedObjectType, CASE_COMMENT_SAVED_OBJECT } from './comments'; -export { caseUserActionSavedObjectType, CASE_USER_ACTION_SAVED_OBJECT } from './user_actions'; -export { - caseConnectorMappingsSavedObjectType, - CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, -} from './connector_mappings'; +export { caseSavedObjectType } from './cases'; +export { subCaseSavedObjectType } from './sub_case'; +export { caseConfigureSavedObjectType } from './configure'; +export { caseCommentSavedObjectType } from './comments'; +export { caseUserActionSavedObjectType } from './user_actions'; +export { caseConnectorMappingsSavedObjectType } from './connector_mappings'; diff --git a/x-pack/plugins/case/server/saved_object_types/sub_case.ts b/x-pack/plugins/case/server/saved_object_types/sub_case.ts index 2f4642d54f7f10..605e49bfda063a 100644 --- a/x-pack/plugins/case/server/saved_object_types/sub_case.ts +++ b/x-pack/plugins/case/server/saved_object_types/sub_case.ts @@ -6,10 +6,9 @@ */ import { SavedObjectsType } from 'src/core/server'; +import { SUB_CASE_SAVED_OBJECT } from '../../common/constants'; import { subCasesMigrations } from './migrations'; -export const SUB_CASE_SAVED_OBJECT = 'cases-sub-case'; - export const subCaseSavedObjectType: SavedObjectsType = { name: SUB_CASE_SAVED_OBJECT, hidden: true, diff --git a/x-pack/plugins/case/server/saved_object_types/user_actions.ts b/x-pack/plugins/case/server/saved_object_types/user_actions.ts index 90ef745123d910..48d91880d02cf9 100644 --- a/x-pack/plugins/case/server/saved_object_types/user_actions.ts +++ b/x-pack/plugins/case/server/saved_object_types/user_actions.ts @@ -6,10 +6,9 @@ */ import { SavedObjectsType } from 'src/core/server'; +import { CASE_USER_ACTION_SAVED_OBJECT } from '../../common/constants'; import { userActionsMigrations } from './migrations'; -export const CASE_USER_ACTION_SAVED_OBJECT = 'cases-user-actions'; - export const caseUserActionSavedObjectType: SavedObjectsType = { name: CASE_USER_ACTION_SAVED_OBJECT, hidden: true, diff --git a/x-pack/plugins/case/server/services/configure/index.ts b/x-pack/plugins/case/server/services/configure/index.ts index 46dca4d9a0d0ee..74ad23dd93ba01 100644 --- a/x-pack/plugins/case/server/services/configure/index.ts +++ b/x-pack/plugins/case/server/services/configure/index.ts @@ -14,7 +14,7 @@ import { } from 'kibana/server'; import { ESCasesConfigureAttributes, SavedObjectFindOptions } from '../../../common/api'; -import { CASE_CONFIGURE_SAVED_OBJECT } from '../../saved_object_types'; +import { CASE_CONFIGURE_SAVED_OBJECT } from '../../../common/constants'; interface ClientArgs { client: SavedObjectsClientContract; diff --git a/x-pack/plugins/case/server/services/connector_mappings/index.ts b/x-pack/plugins/case/server/services/connector_mappings/index.ts index d4fda10276d2b5..5cb338e17bf75b 100644 --- a/x-pack/plugins/case/server/services/connector_mappings/index.ts +++ b/x-pack/plugins/case/server/services/connector_mappings/index.ts @@ -14,7 +14,7 @@ import { } from 'kibana/server'; import { ConnectorMappings, SavedObjectFindOptions } from '../../../common/api'; -import { CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT } from '../../saved_object_types'; +import { CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT } from '../../../common/constants'; interface ClientArgs { client: SavedObjectsClientContract; diff --git a/x-pack/plugins/case/server/services/index.ts b/x-pack/plugins/case/server/services/index.ts index 11ceb48d11e9fc..ff84e405bd9cf8 100644 --- a/x-pack/plugins/case/server/services/index.ts +++ b/x-pack/plugins/case/server/services/index.ts @@ -45,7 +45,7 @@ import { CASE_SAVED_OBJECT, CASE_COMMENT_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT, -} from '../saved_object_types'; +} from '../../common/constants'; import { readReporters } from './reporters/read_reporters'; import { readTags } from './tags/read_tags'; diff --git a/x-pack/plugins/case/server/services/reporters/read_reporters.ts b/x-pack/plugins/case/server/services/reporters/read_reporters.ts index d2708780b2ccf2..e6dea6b6ee1e8f 100644 --- a/x-pack/plugins/case/server/services/reporters/read_reporters.ts +++ b/x-pack/plugins/case/server/services/reporters/read_reporters.ts @@ -8,7 +8,7 @@ import { SavedObject, SavedObjectsClientContract } from 'kibana/server'; import { CaseAttributes, User } from '../../../common/api'; -import { CASE_SAVED_OBJECT } from '../../saved_object_types'; +import { CASE_SAVED_OBJECT } from '../../../common/constants'; export const convertToReporters = (caseObjects: Array>): User[] => caseObjects.reduce((accum, caseObj) => { diff --git a/x-pack/plugins/case/server/services/tags/read_tags.ts b/x-pack/plugins/case/server/services/tags/read_tags.ts index 4c4a948453730a..7ac4ff41e0aa8d 100644 --- a/x-pack/plugins/case/server/services/tags/read_tags.ts +++ b/x-pack/plugins/case/server/services/tags/read_tags.ts @@ -8,7 +8,7 @@ import { SavedObject, SavedObjectsClientContract } from 'kibana/server'; import { CaseAttributes } from '../../../common/api'; -import { CASE_SAVED_OBJECT } from '../../saved_object_types'; +import { CASE_SAVED_OBJECT } from '../../../common/constants'; export const convertToTags = (tagObjects: Array>): string[] => tagObjects.reduce((accum, tagObj) => { diff --git a/x-pack/plugins/case/server/services/user_actions/helpers.ts b/x-pack/plugins/case/server/services/user_actions/helpers.ts index c600a96234b3d7..ebfdcd9792f317 100644 --- a/x-pack/plugins/case/server/services/user_actions/helpers.ts +++ b/x-pack/plugins/case/server/services/user_actions/helpers.ts @@ -27,7 +27,7 @@ import { CASE_SAVED_OBJECT, CASE_COMMENT_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT, -} from '../../saved_object_types'; +} from '../../../common/constants'; export const transformNewUserAction = ({ actionField, diff --git a/x-pack/plugins/case/server/services/user_actions/index.ts b/x-pack/plugins/case/server/services/user_actions/index.ts index 785c81021b584f..192ab9341e4ee8 100644 --- a/x-pack/plugins/case/server/services/user_actions/index.ts +++ b/x-pack/plugins/case/server/services/user_actions/index.ts @@ -17,7 +17,7 @@ import { CASE_USER_ACTION_SAVED_OBJECT, CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT, -} from '../../saved_object_types'; +} from '../../../common/constants'; import { ClientArgs } from '..'; interface GetCaseUserActionArgs extends ClientArgs { diff --git a/x-pack/plugins/case/server/types.ts b/x-pack/plugins/case/server/types.ts index d01aedeaaba4c7..b790fac99668ed 100644 --- a/x-pack/plugins/case/server/types.ts +++ b/x-pack/plugins/case/server/types.ts @@ -8,6 +8,13 @@ import type { IRouter, RequestHandlerContext } from 'src/core/server'; import type { AppRequestContext } from '../../security_solution/server'; import type { ActionsApiRequestHandlerContext } from '../../actions/server'; +import { + ActionTypeConfig, + ActionTypeSecrets, + ActionTypeParams, + ActionType, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../actions/server/types'; import { CaseClient } from './client'; export interface CaseRequestContext { @@ -29,3 +36,12 @@ export interface CasesRequestHandlerContext extends RequestHandlerContext { * @internal */ export type CasesRouter = IRouter; + +export type RegisterActionType = < + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets, + Params extends ActionTypeParams = ActionTypeParams, + ExecutorResultData = void +>( + actionType: ActionType +) => void; From b82e686ca1d35c857faa38d658328a98b9918e21 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 10 Mar 2021 15:10:40 +0200 Subject: [PATCH 009/113] Restructure integration tests --- .../config.ts => security_and_spaces/config_basic.ts} | 6 +----- .../security_and_spaces/config_trial.ts | 11 +++++++++++ .../tests/cases/comments/delete_comment.ts | 4 +++- .../tests/cases/comments/find_comments.ts | 0 .../tests/cases/comments/get_all_comments.ts | 0 .../tests/cases/comments/get_comment.ts | 0 .../tests/cases/comments/migrations.ts | 0 .../tests/cases/comments/patch_comment.ts | 0 .../tests/cases/comments/post_comment.ts | 0 .../tests/cases/delete_cases.ts | 0 .../tests/cases/find_cases.ts | 0 .../tests/cases/get_case.ts | 0 .../tests/cases/migrations.ts | 0 .../tests/cases/patch_cases.ts | 0 .../tests/cases/post_case.ts | 0 .../tests/cases/push_case.ts | 0 .../tests/cases/reporters/get_reporters.ts | 0 .../tests/cases/status/get_status.ts | 0 .../tests/cases/sub_cases/delete_sub_cases.ts | 0 .../tests/cases/sub_cases/find_sub_cases.ts | 0 .../tests/cases/sub_cases/get_sub_case.ts | 0 .../tests/cases/sub_cases/patch_sub_cases.ts | 0 .../tests/cases/tags/get_tags.ts | 0 .../tests/cases/user_actions/get_all_user_actions.ts | 0 .../tests/cases/user_actions/migrations.ts | 0 .../tests/configure/get_configure.ts | 0 .../tests/configure/get_connectors.ts | 0 .../tests/configure/migrations.ts | 0 .../tests/configure/patch_configure.ts | 0 .../tests/configure/post_configure.ts | 0 .../tests/connectors/case.ts | 0 .../{basic => security_and_spaces}/tests/index.ts | 0 32 files changed, 15 insertions(+), 6 deletions(-) rename x-pack/test/case_api_integration/{basic/config.ts => security_and_spaces/config_basic.ts} (77%) create mode 100644 x-pack/test/case_api_integration/security_and_spaces/config_trial.ts rename x-pack/test/case_api_integration/{basic => security_and_spaces}/tests/cases/comments/delete_comment.ts (99%) rename x-pack/test/case_api_integration/{basic => security_and_spaces}/tests/cases/comments/find_comments.ts (100%) rename x-pack/test/case_api_integration/{basic => security_and_spaces}/tests/cases/comments/get_all_comments.ts (100%) rename x-pack/test/case_api_integration/{basic => security_and_spaces}/tests/cases/comments/get_comment.ts (100%) rename x-pack/test/case_api_integration/{basic => security_and_spaces}/tests/cases/comments/migrations.ts (100%) rename x-pack/test/case_api_integration/{basic => security_and_spaces}/tests/cases/comments/patch_comment.ts (100%) rename x-pack/test/case_api_integration/{basic => security_and_spaces}/tests/cases/comments/post_comment.ts (100%) rename x-pack/test/case_api_integration/{basic => security_and_spaces}/tests/cases/delete_cases.ts (100%) rename x-pack/test/case_api_integration/{basic => security_and_spaces}/tests/cases/find_cases.ts (100%) rename x-pack/test/case_api_integration/{basic => security_and_spaces}/tests/cases/get_case.ts (100%) rename x-pack/test/case_api_integration/{basic => security_and_spaces}/tests/cases/migrations.ts (100%) rename x-pack/test/case_api_integration/{basic => security_and_spaces}/tests/cases/patch_cases.ts (100%) rename x-pack/test/case_api_integration/{basic => security_and_spaces}/tests/cases/post_case.ts (100%) rename x-pack/test/case_api_integration/{basic => security_and_spaces}/tests/cases/push_case.ts (100%) rename x-pack/test/case_api_integration/{basic => security_and_spaces}/tests/cases/reporters/get_reporters.ts (100%) rename x-pack/test/case_api_integration/{basic => security_and_spaces}/tests/cases/status/get_status.ts (100%) rename x-pack/test/case_api_integration/{basic => security_and_spaces}/tests/cases/sub_cases/delete_sub_cases.ts (100%) rename x-pack/test/case_api_integration/{basic => security_and_spaces}/tests/cases/sub_cases/find_sub_cases.ts (100%) rename x-pack/test/case_api_integration/{basic => security_and_spaces}/tests/cases/sub_cases/get_sub_case.ts (100%) rename x-pack/test/case_api_integration/{basic => security_and_spaces}/tests/cases/sub_cases/patch_sub_cases.ts (100%) rename x-pack/test/case_api_integration/{basic => security_and_spaces}/tests/cases/tags/get_tags.ts (100%) rename x-pack/test/case_api_integration/{basic => security_and_spaces}/tests/cases/user_actions/get_all_user_actions.ts (100%) rename x-pack/test/case_api_integration/{basic => security_and_spaces}/tests/cases/user_actions/migrations.ts (100%) rename x-pack/test/case_api_integration/{basic => security_and_spaces}/tests/configure/get_configure.ts (100%) rename x-pack/test/case_api_integration/{basic => security_and_spaces}/tests/configure/get_connectors.ts (100%) rename x-pack/test/case_api_integration/{basic => security_and_spaces}/tests/configure/migrations.ts (100%) rename x-pack/test/case_api_integration/{basic => security_and_spaces}/tests/configure/patch_configure.ts (100%) rename x-pack/test/case_api_integration/{basic => security_and_spaces}/tests/configure/post_configure.ts (100%) rename x-pack/test/case_api_integration/{basic => security_and_spaces}/tests/connectors/case.ts (100%) rename x-pack/test/case_api_integration/{basic => security_and_spaces}/tests/index.ts (100%) diff --git a/x-pack/test/case_api_integration/basic/config.ts b/x-pack/test/case_api_integration/security_and_spaces/config_basic.ts similarity index 77% rename from x-pack/test/case_api_integration/basic/config.ts rename to x-pack/test/case_api_integration/security_and_spaces/config_basic.ts index ca4622c16ac92e..57698eb2fe836f 100644 --- a/x-pack/test/case_api_integration/basic/config.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/config_basic.ts @@ -8,8 +8,4 @@ import { createTestConfig } from '../common/config'; // eslint-disable-next-line import/no-default-export -export default createTestConfig('basic', { - disabledPlugins: [], - license: 'trial', - ssl: true, -}); +export default createTestConfig('security_and_spaces', { license: 'basic', ssl: true }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/config_trial.ts b/x-pack/test/case_api_integration/security_and_spaces/config_trial.ts new file mode 100644 index 00000000000000..3256ed599e9822 --- /dev/null +++ b/x-pack/test/case_api_integration/security_and_spaces/config_trial.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('security_and_spaces', { license: 'trial', ssl: true }); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/delete_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/delete_comment.ts similarity index 99% rename from x-pack/test/case_api_integration/basic/tests/cases/comments/delete_comment.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/delete_comment.ts index c58ca0242a5b5a..0586e415b85fc1 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/delete_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/delete_comment.ts @@ -99,7 +99,7 @@ export default ({ getService }: FtrProviderContext): void => { it('deletes a comment from a sub case', async () => { const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - await supertest + const res = await supertest .delete( `${CASES_URL}/${caseInfo.id}/comments/${caseInfo.comments![0].id}?subCaseId=${ caseInfo.subCases![0].id @@ -108,9 +108,11 @@ export default ({ getService }: FtrProviderContext): void => { .set('kbn-xsrf', 'true') .send() .expect(204); + const { body } = await supertest.get( `${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}` ); + expect(body.length).to.eql(0); }); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/find_comments.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/find_comments.ts similarity index 100% rename from x-pack/test/case_api_integration/basic/tests/cases/comments/find_comments.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/find_comments.ts diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/get_all_comments.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/get_all_comments.ts similarity index 100% rename from x-pack/test/case_api_integration/basic/tests/cases/comments/get_all_comments.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/get_all_comments.ts diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/get_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/get_comment.ts similarity index 100% rename from x-pack/test/case_api_integration/basic/tests/cases/comments/get_comment.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/get_comment.ts diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/migrations.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/migrations.ts similarity index 100% rename from x-pack/test/case_api_integration/basic/tests/cases/comments/migrations.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/migrations.ts diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/patch_comment.ts similarity index 100% rename from x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/patch_comment.ts diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/post_comment.ts similarity index 100% rename from x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/post_comment.ts diff --git a/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/cases/delete_cases.ts similarity index 100% rename from x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/cases/delete_cases.ts diff --git a/x-pack/test/case_api_integration/basic/tests/cases/find_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/cases/find_cases.ts similarity index 100% rename from x-pack/test/case_api_integration/basic/tests/cases/find_cases.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/cases/find_cases.ts diff --git a/x-pack/test/case_api_integration/basic/tests/cases/get_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/cases/get_case.ts similarity index 100% rename from x-pack/test/case_api_integration/basic/tests/cases/get_case.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/cases/get_case.ts diff --git a/x-pack/test/case_api_integration/basic/tests/cases/migrations.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/cases/migrations.ts similarity index 100% rename from x-pack/test/case_api_integration/basic/tests/cases/migrations.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/cases/migrations.ts diff --git a/x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/cases/patch_cases.ts similarity index 100% rename from x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/cases/patch_cases.ts diff --git a/x-pack/test/case_api_integration/basic/tests/cases/post_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/cases/post_case.ts similarity index 100% rename from x-pack/test/case_api_integration/basic/tests/cases/post_case.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/cases/post_case.ts diff --git a/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/cases/push_case.ts similarity index 100% rename from x-pack/test/case_api_integration/basic/tests/cases/push_case.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/cases/push_case.ts diff --git a/x-pack/test/case_api_integration/basic/tests/cases/reporters/get_reporters.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/cases/reporters/get_reporters.ts similarity index 100% rename from x-pack/test/case_api_integration/basic/tests/cases/reporters/get_reporters.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/cases/reporters/get_reporters.ts diff --git a/x-pack/test/case_api_integration/basic/tests/cases/status/get_status.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/cases/status/get_status.ts similarity index 100% rename from x-pack/test/case_api_integration/basic/tests/cases/status/get_status.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/cases/status/get_status.ts diff --git a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/delete_sub_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/cases/sub_cases/delete_sub_cases.ts similarity index 100% rename from x-pack/test/case_api_integration/basic/tests/cases/sub_cases/delete_sub_cases.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/cases/sub_cases/delete_sub_cases.ts diff --git a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/find_sub_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/cases/sub_cases/find_sub_cases.ts similarity index 100% rename from x-pack/test/case_api_integration/basic/tests/cases/sub_cases/find_sub_cases.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/cases/sub_cases/find_sub_cases.ts diff --git a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/get_sub_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/cases/sub_cases/get_sub_case.ts similarity index 100% rename from x-pack/test/case_api_integration/basic/tests/cases/sub_cases/get_sub_case.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/cases/sub_cases/get_sub_case.ts diff --git a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/patch_sub_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/cases/sub_cases/patch_sub_cases.ts similarity index 100% rename from x-pack/test/case_api_integration/basic/tests/cases/sub_cases/patch_sub_cases.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/cases/sub_cases/patch_sub_cases.ts diff --git a/x-pack/test/case_api_integration/basic/tests/cases/tags/get_tags.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/cases/tags/get_tags.ts similarity index 100% rename from x-pack/test/case_api_integration/basic/tests/cases/tags/get_tags.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/cases/tags/get_tags.ts diff --git a/x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/cases/user_actions/get_all_user_actions.ts similarity index 100% rename from x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/cases/user_actions/get_all_user_actions.ts diff --git a/x-pack/test/case_api_integration/basic/tests/cases/user_actions/migrations.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/cases/user_actions/migrations.ts similarity index 100% rename from x-pack/test/case_api_integration/basic/tests/cases/user_actions/migrations.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/cases/user_actions/migrations.ts diff --git a/x-pack/test/case_api_integration/basic/tests/configure/get_configure.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/configure/get_configure.ts similarity index 100% rename from x-pack/test/case_api_integration/basic/tests/configure/get_configure.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/configure/get_configure.ts diff --git a/x-pack/test/case_api_integration/basic/tests/configure/get_connectors.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/configure/get_connectors.ts similarity index 100% rename from x-pack/test/case_api_integration/basic/tests/configure/get_connectors.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/configure/get_connectors.ts diff --git a/x-pack/test/case_api_integration/basic/tests/configure/migrations.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/configure/migrations.ts similarity index 100% rename from x-pack/test/case_api_integration/basic/tests/configure/migrations.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/configure/migrations.ts diff --git a/x-pack/test/case_api_integration/basic/tests/configure/patch_configure.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/configure/patch_configure.ts similarity index 100% rename from x-pack/test/case_api_integration/basic/tests/configure/patch_configure.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/configure/patch_configure.ts diff --git a/x-pack/test/case_api_integration/basic/tests/configure/post_configure.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/configure/post_configure.ts similarity index 100% rename from x-pack/test/case_api_integration/basic/tests/configure/post_configure.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/configure/post_configure.ts diff --git a/x-pack/test/case_api_integration/basic/tests/connectors/case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/connectors/case.ts similarity index 100% rename from x-pack/test/case_api_integration/basic/tests/connectors/case.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/connectors/case.ts diff --git a/x-pack/test/case_api_integration/basic/tests/index.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/index.ts similarity index 100% rename from x-pack/test/case_api_integration/basic/tests/index.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/index.ts From 4d05175bae70101bc88a00a5fbf232abf159fafd Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 10 Mar 2021 15:22:59 +0200 Subject: [PATCH 010/113] Init spaces_only integration tests --- .../security_and_spaces/tests/index.ts | 2 +- .../case_api_integration/spaces_only/config.ts | 15 +++++++++++++++ .../spaces_only/tests/index.ts | 16 ++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 x-pack/test/case_api_integration/spaces_only/config.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/index.ts diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/index.ts index 837e6503084a70..321c19558d522e 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default ({ loadTestFile }: FtrProviderContext): void => { - describe('case api basic', function () { + describe('cases security and spaces enabled', function () { // Fastest ciGroup for the moment. this.tags('ciGroup5'); diff --git a/x-pack/test/case_api_integration/spaces_only/config.ts b/x-pack/test/case_api_integration/spaces_only/config.ts new file mode 100644 index 00000000000000..310830a220fb84 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/config.ts @@ -0,0 +1,15 @@ +/* + * 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 { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('spaces_only', { + disabledPlugins: ['security'], + license: 'basic', + ssl: true, +}); diff --git a/x-pack/test/case_api_integration/spaces_only/tests/index.ts b/x-pack/test/case_api_integration/spaces_only/tests/index.ts new file mode 100644 index 00000000000000..38ca7f40706160 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('cases spaces only enabled', function () { + // Fastest ciGroup for the moment. + this.tags('ciGroup5'); + }); +}; From 75d72aec2c8dfc6b58289c8114015d9a5ed77321 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Wed, 10 Mar 2021 11:12:11 -0500 Subject: [PATCH 011/113] Implementing the cases security string --- x-pack/plugins/case/common/constants.ts | 11 ++--- .../case/server/saved_object_types/cases.ts | 2 +- .../server/saved_object_types/comments.ts | 2 +- .../server/saved_object_types/configure.ts | 2 +- .../saved_object_types/connector_mappings.ts | 2 +- .../server/saved_object_types/migrations.ts | 41 ++++++++----------- .../server/saved_object_types/sub_case.ts | 2 +- .../server/saved_object_types/user_actions.ts | 2 +- .../common/feature_kibana_privileges.ts | 28 +++++++++++++ .../plugins/features/common/kibana_feature.ts | 5 +++ .../server/authorization/actions/cases.ts | 8 ++-- .../feature_privilege_builder/cases.ts | 17 ++++---- .../security_solution/server/plugin.ts | 7 ++++ 13 files changed, 80 insertions(+), 49 deletions(-) diff --git a/x-pack/plugins/case/common/constants.ts b/x-pack/plugins/case/common/constants.ts index d16b2e302095e9..b7320cc783ce6b 100644 --- a/x-pack/plugins/case/common/constants.ts +++ b/x-pack/plugins/case/common/constants.ts @@ -5,10 +5,7 @@ * 2.0. */ -import { - DEFAULT_MAX_SIGNALS, - APP_ID as SECURITY_SOLUTION_PLUGIN_APP_ID, -} from '../../security_solution/common/constants'; +import { DEFAULT_MAX_SIGNALS } from '../../security_solution/common/constants'; export const APP_ID = 'case'; @@ -75,4 +72,8 @@ export const SUPPORTED_CONNECTORS = [ export const MAX_ALERTS_PER_SUB_CASE = 5000; export const MAX_GENERATED_ALERTS_PER_SUB_CASE = MAX_ALERTS_PER_SUB_CASE / DEFAULT_MAX_SIGNALS; -export const SECURITY_SOLUTION_CONSUMER = SECURITY_SOLUTION_PLUGIN_APP_ID; +/** + * This must be the same value that the security solution plugin uses to define the case kind when it registers the + * feature for the 7.13 migration only. + */ +export const SECURITY_SOLUTION_CONSUMER = 'securitySolution'; diff --git a/x-pack/plugins/case/server/saved_object_types/cases.ts b/x-pack/plugins/case/server/saved_object_types/cases.ts index fb0e54b07ccff7..4464adf8562ab4 100644 --- a/x-pack/plugins/case/server/saved_object_types/cases.ts +++ b/x-pack/plugins/case/server/saved_object_types/cases.ts @@ -31,7 +31,7 @@ export const caseSavedObjectType: SavedObjectsType = { }, }, }, - consumer: { + class: { type: 'keyword', }, created_at: { diff --git a/x-pack/plugins/case/server/saved_object_types/comments.ts b/x-pack/plugins/case/server/saved_object_types/comments.ts index 3ab072f92ce5e3..66c44f1588d02b 100644 --- a/x-pack/plugins/case/server/saved_object_types/comments.ts +++ b/x-pack/plugins/case/server/saved_object_types/comments.ts @@ -21,7 +21,7 @@ export const caseCommentSavedObjectType: SavedObjectsType = { comment: { type: 'text', }, - consumer: { + class: { type: 'keyword', }, type: { diff --git a/x-pack/plugins/case/server/saved_object_types/configure.ts b/x-pack/plugins/case/server/saved_object_types/configure.ts index 609cf526a6e402..2b7588cca7b6e6 100644 --- a/x-pack/plugins/case/server/saved_object_types/configure.ts +++ b/x-pack/plugins/case/server/saved_object_types/configure.ts @@ -15,7 +15,7 @@ export const caseConfigureSavedObjectType: SavedObjectsType = { namespaceType: 'single', mappings: { properties: { - consumer: { + class: { type: 'keyword', }, created_at: { diff --git a/x-pack/plugins/case/server/saved_object_types/connector_mappings.ts b/x-pack/plugins/case/server/saved_object_types/connector_mappings.ts index 9f0163f42b9b67..5bcf2dc319c717 100644 --- a/x-pack/plugins/case/server/saved_object_types/connector_mappings.ts +++ b/x-pack/plugins/case/server/saved_object_types/connector_mappings.ts @@ -28,7 +28,7 @@ export const caseConnectorMappingsSavedObjectType: SavedObjectsType = { }, }, }, - consumer: { + class: { type: 'keyword', }, }, diff --git a/x-pack/plugins/case/server/saved_object_types/migrations.ts b/x-pack/plugins/case/server/saved_object_types/migrations.ts index cca6e017b384ed..905ea2c2be3ba1 100644 --- a/x-pack/plugins/case/server/saved_object_types/migrations.ts +++ b/x-pack/plugins/case/server/saved_object_types/migrations.ts @@ -60,17 +60,17 @@ interface SanitizedCaseType { type: string; } -interface SanitizedCaseConsumer { - consumer: string; +interface SanitizedCaseClass { + class: string; } -const addConsumerToSO = >( +const addClassToSO = >( doc: SavedObjectUnsanitizedDoc -): SavedObjectSanitizedDoc => ({ +): SavedObjectSanitizedDoc => ({ ...doc, attributes: { ...doc.attributes, - consumer: SECURITY_SOLUTION_CONSUMER, + class: SECURITY_SOLUTION_CONSUMER, }, references: doc.references || [], }); @@ -131,8 +131,8 @@ export const caseMigrations = { }, '7.13.0': ( doc: SavedObjectUnsanitizedDoc> - ): SavedObjectSanitizedDoc => { - return addConsumerToSO(doc); + ): SavedObjectSanitizedDoc => { + return addClassToSO(doc); }, }; @@ -158,15 +158,8 @@ export const configureMigrations = { }, '7.13.0': ( doc: SavedObjectUnsanitizedDoc> - ): SavedObjectSanitizedDoc => { - return { - ...doc, - attributes: { - ...doc.attributes, - consumer: SECURITY_SOLUTION_CONSUMER, - }, - references: doc.references || [], - }; + ): SavedObjectSanitizedDoc => { + return addClassToSO(doc); }, }; @@ -211,8 +204,8 @@ export const userActionsMigrations = { }, '7.13.0': ( doc: SavedObjectUnsanitizedDoc> - ): SavedObjectSanitizedDoc => { - return addConsumerToSO(doc); + ): SavedObjectSanitizedDoc => { + return addClassToSO(doc); }, }; @@ -266,23 +259,23 @@ export const commentsMigrations = { }, '7.13.0': ( doc: SavedObjectUnsanitizedDoc> - ): SavedObjectSanitizedDoc => { - return addConsumerToSO(doc); + ): SavedObjectSanitizedDoc => { + return addClassToSO(doc); }, }; export const connectorMappingsMigrations = { '7.13.0': ( doc: SavedObjectUnsanitizedDoc> - ): SavedObjectSanitizedDoc => { - return addConsumerToSO(doc); + ): SavedObjectSanitizedDoc => { + return addClassToSO(doc); }, }; export const subCasesMigrations = { '7.13.0': ( doc: SavedObjectUnsanitizedDoc> - ): SavedObjectSanitizedDoc => { - return addConsumerToSO(doc); + ): SavedObjectSanitizedDoc => { + return addClassToSO(doc); }, }; diff --git a/x-pack/plugins/case/server/saved_object_types/sub_case.ts b/x-pack/plugins/case/server/saved_object_types/sub_case.ts index 605e49bfda063a..d2e49e3574e972 100644 --- a/x-pack/plugins/case/server/saved_object_types/sub_case.ts +++ b/x-pack/plugins/case/server/saved_object_types/sub_case.ts @@ -31,7 +31,7 @@ export const subCaseSavedObjectType: SavedObjectsType = { }, }, }, - consumer: { + class: { type: 'keyword', }, created_at: { diff --git a/x-pack/plugins/case/server/saved_object_types/user_actions.ts b/x-pack/plugins/case/server/saved_object_types/user_actions.ts index 48d91880d02cf9..a94b23f63c1a87 100644 --- a/x-pack/plugins/case/server/saved_object_types/user_actions.ts +++ b/x-pack/plugins/case/server/saved_object_types/user_actions.ts @@ -37,7 +37,7 @@ export const caseUserActionSavedObjectType: SavedObjectsType = { }, }, }, - consumer: { + class: { type: 'keyword', }, new_value: { diff --git a/x-pack/plugins/features/common/feature_kibana_privileges.ts b/x-pack/plugins/features/common/feature_kibana_privileges.ts index 7febba197647d0..d95c12df5deb9c 100644 --- a/x-pack/plugins/features/common/feature_kibana_privileges.ts +++ b/x-pack/plugins/features/common/feature_kibana_privileges.ts @@ -104,6 +104,34 @@ export interface FeatureKibanaPrivileges { */ read?: readonly string[]; }; + + /** + * If your feature requires access to specific types of cases, then specify your access needs here. The values here should + * be a unique identifier for the type of case you want access to. + */ + cases?: { + /** + * List of case types which users should have full read/write access to when granted this privilege. + * @example + * ```ts + * { + * all: ['securitySolution'] + * } + * ``` + */ + all?: readonly string[]; + /** + * List of case types which users should have read-only access to when granted this privilege. + * @example + * ```ts + * { + * read: ['securitySolution'] + * } + * ``` + */ + read?: readonly string[]; + }; + /** * If your feature requires access to specific saved objects, then specify your access needs here. */ diff --git a/x-pack/plugins/features/common/kibana_feature.ts b/x-pack/plugins/features/common/kibana_feature.ts index 7c9f930c106b04..096e310b450840 100644 --- a/x-pack/plugins/features/common/kibana_feature.ts +++ b/x-pack/plugins/features/common/kibana_feature.ts @@ -98,6 +98,11 @@ export interface KibanaFeatureConfig { */ alerting?: readonly string[]; + /** + * If your feature grants access to specific case types, you can specify them here to control visibility based on the current space. + */ + cases?: readonly string[]; + /** * Feature privilege definition. * diff --git a/x-pack/plugins/security/server/authorization/actions/cases.ts b/x-pack/plugins/security/server/authorization/actions/cases.ts index ef6aeb288297ac..c428f8c0f0ecb6 100644 --- a/x-pack/plugins/security/server/authorization/actions/cases.ts +++ b/x-pack/plugins/security/server/authorization/actions/cases.ts @@ -14,15 +14,15 @@ export class CasesActions { this.prefix = `cases:${versionNumber}:`; } - public get(consumer: string, operation: string): string { + public get(className: string, operation: string): string { if (!operation || !isString(operation)) { throw new Error('operation is required and must be a string'); } - if (!consumer || !isString(consumer)) { - throw new Error('consumer is required and must be a string'); + if (!className || !isString(className)) { + throw new Error('class is required and must be a string'); } - return `${this.prefix}${consumer}/${operation}`; + return `${this.prefix}${className}/${operation}`; } } 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 2695280f0ecf74..4b5c42361543db 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 @@ -19,18 +19,15 @@ export class FeaturePrivilegeCasesBuilder extends BaseFeaturePrivilegeBuilder { privilegeDefinition: FeatureKibanaPrivileges, feature: KibanaFeature ): string[] { - const getCasesPrivilege = (operations: string[], consumer: string) => - operations.map((operation) => this.actions.cases.get(consumer, operation)); + const getCasesPrivilege = (operations: string[], classes: readonly string[]) => { + return classes.flatMap((className) => + operations.map((operation) => this.actions.cases.get(className, operation)) + ); + }; - // TODO: make sure we don't need to add a cases array or flag? to the FeatureKibanaPrivileges - // I think we'd only need to do that if we wanted a plugin to be able to get permissions for cases from other plugins? - // I think we only want the plugin to get access to the cases that are created through itself and not allow it to have - // access to other plugins - - // It may make sense to add a cases field as a flag so plugins have to opt in to getting access to cases return uniq([ - ...getCasesPrivilege(allOperations, feature.id), - ...getCasesPrivilege(readOperations, feature.id), + ...getCasesPrivilege(allOperations, privilegeDefinition.cases?.all ?? []), + ...getCasesPrivilege(readOperations, privilegeDefinition.cases?.read ?? []), ]); } } diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 905078f676eefc..b993017dbc7cc6 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -216,8 +216,12 @@ export class Plugin implements IPlugin Date: Wed, 10 Mar 2021 13:24:48 -0500 Subject: [PATCH 012/113] Adding security plugin tests for cases --- .../actions/__snapshots__/cases.test.ts.snap | 24 +- .../authorization/actions/cases.test.ts | 38 ++-- .../feature_privilege_builder/cases.test.ts | 207 ++++++++++++++++++ .../tests/cases/comments/delete_comment.ts | 2 +- 4 files changed, 245 insertions(+), 26 deletions(-) create mode 100644 x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts diff --git a/x-pack/plugins/security/server/authorization/actions/__snapshots__/cases.test.ts.snap b/x-pack/plugins/security/server/authorization/actions/__snapshots__/cases.test.ts.snap index 3f5c0c9b3d44b1..2208105694fe9f 100644 --- a/x-pack/plugins/security/server/authorization/actions/__snapshots__/cases.test.ts.snap +++ b/x-pack/plugins/security/server/authorization/actions/__snapshots__/cases.test.ts.snap @@ -1,25 +1,25 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`#get consumer of "" throws error 1`] = `"consumer is required and must be a string"`; +exports[`#get class of "" 1`] = `"class is required and must be a string"`; -exports[`#get consumer of {} throws error 1`] = `"consumer is required and must be a string"`; +exports[`#get class of "{}" 1`] = `"class is required and must be a string"`; -exports[`#get consumer of 1 throws error 1`] = `"consumer is required and must be a string"`; +exports[`#get class of "1" 1`] = `"class is required and must be a string"`; -exports[`#get consumer of null throws error 1`] = `"consumer is required and must be a string"`; +exports[`#get class of "null" 1`] = `"class is required and must be a string"`; -exports[`#get consumer of true throws error 1`] = `"consumer is required and must be a string"`; +exports[`#get class of "true" 1`] = `"class is required and must be a string"`; -exports[`#get consumer of undefined throws error 1`] = `"consumer is required and must be a string"`; +exports[`#get class of "undefined" 1`] = `"class is required and must be a string"`; -exports[`#get operation of "" throws error 1`] = `"operation is required and must be a string"`; +exports[`#get operation of "" 1`] = `"operation is required and must be a string"`; -exports[`#get operation of {} throws error 1`] = `"operation is required and must be a string"`; +exports[`#get operation of "{}" 1`] = `"operation is required and must be a string"`; -exports[`#get operation of 1 throws error 1`] = `"operation is required and must be a string"`; +exports[`#get operation of "1" 1`] = `"operation is required and must be a string"`; -exports[`#get operation of null throws error 1`] = `"operation is required and must be a string"`; +exports[`#get operation of "null" 1`] = `"operation is required and must be a string"`; -exports[`#get operation of true throws error 1`] = `"operation is required and must be a string"`; +exports[`#get operation of "true" 1`] = `"operation is required and must be a string"`; -exports[`#get operation of undefined throws error 1`] = `"operation is required and must be a string"`; +exports[`#get operation of "undefined" 1`] = `"operation is required and must be a string"`; diff --git a/x-pack/plugins/security/server/authorization/actions/cases.test.ts b/x-pack/plugins/security/server/authorization/actions/cases.test.ts index 8f0ef64af4747d..e1c91543570356 100644 --- a/x-pack/plugins/security/server/authorization/actions/cases.test.ts +++ b/x-pack/plugins/security/server/authorization/actions/cases.test.ts @@ -10,24 +10,36 @@ import { CasesActions } from './cases'; const version = '1.0.0-zeta1'; describe('#get', () => { - [null, undefined, '', 1, true, {}].forEach((operation: any) => { - test(`operation of ${JSON.stringify(operation)} throws error`, () => { - const alertingActions = new CasesActions(version); - expect(() => alertingActions.get('consumer', operation)).toThrowErrorMatchingSnapshot(); - }); + it.each` + operation + ${null} + ${undefined} + ${''} + ${1} + ${true} + ${{}} + `(`operation of ${JSON.stringify('$operation')}`, ({ operation }) => { + const actions = new CasesActions(version); + expect(() => actions.get('class', operation)).toThrowErrorMatchingSnapshot(); }); - [null, undefined, '', 1, true, {}].forEach((consumer: any) => { - test(`consumer of ${JSON.stringify(consumer)} throws error`, () => { - const alertingActions = new CasesActions(version); - expect(() => alertingActions.get(consumer, 'operation')).toThrowErrorMatchingSnapshot(); - }); + it.each` + className + ${null} + ${undefined} + ${''} + ${1} + ${true} + ${{}} + `(`class of ${JSON.stringify('$className')}`, ({ className }) => { + const actions = new CasesActions(version); + expect(() => actions.get(className, 'operation')).toThrowErrorMatchingSnapshot(); }); - test('returns `cases:${consumer}/${operation}`', () => { + it('returns `cases:${class}/${operation}`', () => { const alertingActions = new CasesActions(version); - expect(alertingActions.get('consumer', 'bar-operation')).toBe( - 'cases:1.0.0-zeta1:consumer/bar-operation' + expect(alertingActions.get('security', 'bar-operation')).toBe( + 'cases:1.0.0-zeta1:security/bar-operation' ); }); }); 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 new file mode 100644 index 00000000000000..55920aabe993d8 --- /dev/null +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts @@ -0,0 +1,207 @@ +/* + * 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 type { FeatureKibanaPrivileges } from '../../../../../features/server'; +import { KibanaFeature } from '../../../../../features/server'; +import { Actions } from '../../actions'; +import { FeaturePrivilegeCasesBuilder } from './cases'; + +const version = '1.0.0-zeta1'; + +describe(`cases`, () => { + describe(`feature_privilege_builder`, () => { + it('grants no privileges by default', () => { + const actions = new Actions(version); + const casesFeaturePrivileges = new FeaturePrivilegeCasesBuilder(actions); + + const privilege: FeatureKibanaPrivileges = { + savedObject: { + all: [], + read: [], + }, + ui: [], + }; + + const feature = new KibanaFeature({ + id: 'my-feature', + name: 'my-feature', + app: [], + category: { id: 'foo', label: 'foo' }, + privileges: { + all: privilege, + read: privilege, + }, + }); + + expect(casesFeaturePrivileges.getActions(privilege, feature)).toEqual([]); + }); + + describe(`within feature`, () => { + it('grants `read` privileges under feature', () => { + const actions = new Actions(version); + const casesFeaturePrivilege = new FeaturePrivilegeCasesBuilder(actions); + + const privilege: FeatureKibanaPrivileges = { + cases: { + read: ['observability'], + }, + + savedObject: { + all: [], + read: [], + }, + ui: [], + }; + + const feature = new KibanaFeature({ + id: 'my-feature', + name: 'my-feature', + app: [], + category: { id: 'foo', label: 'foo' }, + privileges: { + all: privilege, + read: privilege, + }, + }); + + expect(casesFeaturePrivilege.getActions(privilege, feature)).toMatchInlineSnapshot(` + Array [ + "cases:1.0.0-zeta1:observability/get", + "cases:1.0.0-zeta1:observability/find", + ] + `); + }); + + it('grants `all` privileges under feature', () => { + const actions = new Actions(version); + const casesFeaturePrivilege = new FeaturePrivilegeCasesBuilder(actions); + + const privilege: FeatureKibanaPrivileges = { + cases: { + all: ['security'], + }, + + savedObject: { + all: [], + read: [], + }, + ui: [], + }; + + const feature = new KibanaFeature({ + id: 'my-feature', + name: 'my-feature', + app: [], + category: { id: 'foo', label: 'foo' }, + privileges: { + all: privilege, + read: privilege, + }, + }); + + expect(casesFeaturePrivilege.getActions(privilege, feature)).toMatchInlineSnapshot(` + Array [ + "cases:1.0.0-zeta1:security/get", + "cases:1.0.0-zeta1:security/find", + "cases:1.0.0-zeta1:security/create", + "cases:1.0.0-zeta1:security/delete", + "cases:1.0.0-zeta1:security/update", + ] + `); + }); + + it('grants both `all` and `read` privileges under feature', () => { + const actions = new Actions(version); + const casesFeaturePrivilege = new FeaturePrivilegeCasesBuilder(actions); + + const privilege: FeatureKibanaPrivileges = { + cases: { + all: ['security'], + read: ['obs'], + }, + + savedObject: { + all: [], + read: [], + }, + ui: [], + }; + + const feature = new KibanaFeature({ + id: 'my-feature', + name: 'my-feature', + app: [], + category: { id: 'foo', label: 'foo' }, + privileges: { + all: privilege, + read: privilege, + }, + }); + + expect(casesFeaturePrivilege.getActions(privilege, feature)).toMatchInlineSnapshot(` + Array [ + "cases:1.0.0-zeta1:security/get", + "cases:1.0.0-zeta1:security/find", + "cases:1.0.0-zeta1:security/create", + "cases:1.0.0-zeta1:security/delete", + "cases:1.0.0-zeta1:security/update", + "cases:1.0.0-zeta1:obs/get", + "cases:1.0.0-zeta1:obs/find", + ] + `); + }); + + it('grants both `all` and `read` privileges under feature with multiple values in cases array', () => { + const actions = new Actions(version); + const casesFeaturePrivilege = new FeaturePrivilegeCasesBuilder(actions); + + const privilege: FeatureKibanaPrivileges = { + cases: { + all: ['security', 'other-security'], + read: ['obs', 'other-obs'], + }, + + savedObject: { + all: [], + read: [], + }, + ui: [], + }; + + const feature = new KibanaFeature({ + id: 'my-feature', + name: 'my-feature', + app: [], + category: { id: 'foo', label: 'foo' }, + privileges: { + all: privilege, + read: privilege, + }, + }); + + expect(casesFeaturePrivilege.getActions(privilege, feature)).toMatchInlineSnapshot(` + Array [ + "cases:1.0.0-zeta1:security/get", + "cases:1.0.0-zeta1:security/find", + "cases:1.0.0-zeta1:security/create", + "cases:1.0.0-zeta1:security/delete", + "cases:1.0.0-zeta1:security/update", + "cases:1.0.0-zeta1:other-security/get", + "cases:1.0.0-zeta1:other-security/find", + "cases:1.0.0-zeta1:other-security/create", + "cases:1.0.0-zeta1:other-security/delete", + "cases:1.0.0-zeta1:other-security/update", + "cases:1.0.0-zeta1:obs/get", + "cases:1.0.0-zeta1:obs/find", + "cases:1.0.0-zeta1:other-obs/get", + "cases:1.0.0-zeta1:other-obs/find", + ] + `); + }); + }); + }); +}); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/delete_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/delete_comment.ts index 80dcdcf32a3240..90d46fe65a2e01 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/delete_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/delete_comment.ts @@ -99,7 +99,7 @@ export default ({ getService }: FtrProviderContext): void => { it('deletes a comment from a sub case', async () => { const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - const res = await supertest + await supertest .delete( `${CASES_URL}/${caseInfo.id}/comments/${caseInfo.comments![0].id}?subCaseId=${ caseInfo.subCases![0].id From 4560d426a22695fd4a020687ac12f6133c50bde6 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Wed, 10 Mar 2021 16:59:29 -0500 Subject: [PATCH 013/113] Rough concept for authorization class --- .../server/authorization/authorization.ts | 89 +++++++++++++++++++ .../plugins/features/common/kibana_feature.ts | 4 + 2 files changed, 93 insertions(+) create mode 100644 x-pack/plugins/cases/server/authorization/authorization.ts diff --git a/x-pack/plugins/cases/server/authorization/authorization.ts b/x-pack/plugins/cases/server/authorization/authorization.ts new file mode 100644 index 00000000000000..c5f82ad79cb386 --- /dev/null +++ b/x-pack/plugins/cases/server/authorization/authorization.ts @@ -0,0 +1,89 @@ +/* + * 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 { KibanaRequest } from 'kibana/server'; +import { Space } from '../../../spaces/server'; +import { SecurityPluginStart } from '../../../security/server'; +import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; + +// TODO: probably should move these to the types.ts file +// TODO: Larry would prefer if we 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? +export enum ReadOperations { + Get = 'get', + Find = 'find', +} + +export enum WriteOperations { + Create = 'create', + Delete = 'delete', + Update = 'update', +} + +type GetSpaceFn = (request: KibanaRequest) => Promise; + +export class Authorization { + private readonly request: KibanaRequest; + private readonly securityAuth: SecurityPluginStart['authz'] | undefined; + private readonly featureCaseClasses: Set; + // TODO: create this + // private readonly auditLogger: AuthorizationAuditLogger; + + private constructor({ + request, + securityAuth, + caseClasses, + }: { + request: KibanaRequest; + securityAuth?: SecurityPluginStart['authz']; + caseClasses: Set; + }) { + this.request = request; + this.securityAuth = securityAuth; + this.featureCaseClasses = caseClasses; + } + + static async create({ + request, + securityAuth, + getSpace, + features, + }: { + request: KibanaRequest; + securityAuth?: SecurityPluginStart['authz']; + getSpace: GetSpaceFn; + features: FeaturesPluginStart; + }): Promise { + let caseClasses: Set; + try { + const disabledFeatures = new Set((await getSpace(request))?.disabledFeatures ?? []); + + caseClasses = new Set( + features + .getKibanaFeatures() + // get all the features' cases classes that aren't disabled + .filter(({ id }) => !disabledFeatures.has(id)) + .flatMap((feature) => feature.cases ?? []) + ); + } catch (error) { + caseClasses = new Set(); + } + + return new Authorization({ request, securityAuth, caseClasses }); + } + + private shouldCheckAuthorization(): boolean { + return this.securityAuth?.mode?.useRbacForRequest(this.request) ?? false; + } + + public async ensureAuthorized(classes: string[], operation: ReadOperations | WriteOperations) { + // TODO: throw if the request is not authorized + if (this.shouldCheckAuthorization()) { + // TODO: implement ensure logic + } + } +} diff --git a/x-pack/plugins/features/common/kibana_feature.ts b/x-pack/plugins/features/common/kibana_feature.ts index 096e310b450840..089389c7bc7fa7 100644 --- a/x-pack/plugins/features/common/kibana_feature.ts +++ b/x-pack/plugins/features/common/kibana_feature.ts @@ -188,6 +188,10 @@ export class KibanaFeature { return this.config.alerting; } + public get cases() { + return this.config.cases; + } + public get excludeFromBasePrivileges() { return this.config.excludeFromBasePrivileges ?? false; } From ef9b3b23bdb43896a3b925f9e4e0cab25c029f4e Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Wed, 10 Mar 2021 17:03:38 -0500 Subject: [PATCH 014/113] Adding comments --- .../plugins/cases/server/authorization/authorization.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/x-pack/plugins/cases/server/authorization/authorization.ts b/x-pack/plugins/cases/server/authorization/authorization.ts index c5f82ad79cb386..9079daf644277d 100644 --- a/x-pack/plugins/cases/server/authorization/authorization.ts +++ b/x-pack/plugins/cases/server/authorization/authorization.ts @@ -26,6 +26,10 @@ export enum WriteOperations { type GetSpaceFn = (request: KibanaRequest) => Promise; +/** + * This class handles ensuring that the user making a request has the correct permissions + * for the API request. + */ export class Authorization { private readonly request: KibanaRequest; private readonly securityAuth: SecurityPluginStart['authz'] | undefined; @@ -47,6 +51,9 @@ export class Authorization { this.featureCaseClasses = caseClasses; } + /** + * Creates an Authorization object. + */ static async create({ request, securityAuth, @@ -58,6 +65,7 @@ export class Authorization { getSpace: GetSpaceFn; features: FeaturesPluginStart; }): Promise { + // Since we need to do async operations, this static method handles that before creating the Auth class let caseClasses: Set; try { const disabledFeatures = new Set((await getSpace(request))?.disabledFeatures ?? []); From b22a0321a0566afd06612ce031540f41ac9b8edd Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 11 Mar 2021 11:47:55 +0200 Subject: [PATCH 015/113] Fix merge --- x-pack/plugins/cases/server/client/cases/update.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/cases/server/client/cases/update.ts b/x-pack/plugins/cases/server/client/cases/update.ts index ff3c0a62407a18..9ca0a0804a0740 100644 --- a/x-pack/plugins/cases/server/client/cases/update.ts +++ b/x-pack/plugins/cases/server/client/cases/update.ts @@ -50,7 +50,7 @@ import { CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT, -} from '../../saved_object_types'; +} from '../../../common/constants'; import { CasesClientHandler } from '..'; import { createAlertUpdateRequest } from '../../common'; import { UpdateAlertRequest } from '../types'; From ddc22809e38e61ce8d3696e9fd8f18a6a80321b3 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 11 Mar 2021 11:48:27 +0200 Subject: [PATCH 016/113] Get requiredPrivileges for classes --- x-pack/plugins/cases/server/authorization/authorization.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/cases/server/authorization/authorization.ts b/x-pack/plugins/cases/server/authorization/authorization.ts index 9079daf644277d..a16e1df71ed61f 100644 --- a/x-pack/plugins/cases/server/authorization/authorization.ts +++ b/x-pack/plugins/cases/server/authorization/authorization.ts @@ -89,9 +89,13 @@ export class Authorization { } public async ensureAuthorized(classes: string[], operation: ReadOperations | WriteOperations) { + const { securityAuth } = this; // TODO: throw if the request is not authorized - if (this.shouldCheckAuthorization()) { + if (securityAuth && this.shouldCheckAuthorization()) { // TODO: implement ensure logic + const requiredPrivileges: string[] = classes.map((className) => + securityAuth.actions.cases.get(className, operation) + ); } } } From 9d008d806a3af8c3e7ac8e4880722ed6ee5dba2f Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 11 Mar 2021 13:35:59 +0200 Subject: [PATCH 017/113] Check privillages --- x-pack/plugins/cases/server/authorization/authorization.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/x-pack/plugins/cases/server/authorization/authorization.ts b/x-pack/plugins/cases/server/authorization/authorization.ts index a16e1df71ed61f..6fb9d92316f0e1 100644 --- a/x-pack/plugins/cases/server/authorization/authorization.ts +++ b/x-pack/plugins/cases/server/authorization/authorization.ts @@ -96,6 +96,12 @@ export class Authorization { const requiredPrivileges: string[] = classes.map((className) => securityAuth.actions.cases.get(className, operation) ); + + const checkPrivileges = securityAuth.checkPrivilegesDynamicallyWithRequest(this.request); + const { hasAllRequested, username, privileges } = checkPrivileges({ + kibana: requiredPrivileges, + }); + } } } From 7bb23dd5e9bcbbaf0efcd5390ed797aa6adcc2b6 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 11 Mar 2021 13:36:14 +0200 Subject: [PATCH 018/113] Ensure that all classes are available --- .../cases/server/authorization/authorization.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/x-pack/plugins/cases/server/authorization/authorization.ts b/x-pack/plugins/cases/server/authorization/authorization.ts index 6fb9d92316f0e1..1123fe9dacb7b3 100644 --- a/x-pack/plugins/cases/server/authorization/authorization.ts +++ b/x-pack/plugins/cases/server/authorization/authorization.ts @@ -90,6 +90,9 @@ export class Authorization { public async ensureAuthorized(classes: string[], operation: ReadOperations | WriteOperations) { const { securityAuth } = this; + const areAllClassAvailable = classes.every((className) => + this.featureCaseClasses.has(className) + ); // TODO: throw if the request is not authorized if (securityAuth && this.shouldCheckAuthorization()) { // TODO: implement ensure logic @@ -102,6 +105,16 @@ export class Authorization { kibana: requiredPrivileges, }); + if (!areAllClassAvailable) { + // TODO: throw if any of the class are not available + /** + * Under most circumstances this would have been caught by `checkPrivileges` as + * a user can't have Privileges to an unknown class, but super users + * don't actually get "privilege checked" so the made up class *will* return + * as Privileged. + * This check will ensure we don't accidentally let these through + */ + } } } } From 65d4c6b51b90431887c423ecd4ce798b4bd20ab6 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 11 Mar 2021 13:52:10 +0200 Subject: [PATCH 019/113] Success if hasAllRequested is true --- x-pack/plugins/cases/server/authorization/authorization.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/cases/server/authorization/authorization.ts b/x-pack/plugins/cases/server/authorization/authorization.ts index 1123fe9dacb7b3..997e151ddb34b7 100644 --- a/x-pack/plugins/cases/server/authorization/authorization.ts +++ b/x-pack/plugins/cases/server/authorization/authorization.ts @@ -101,7 +101,7 @@ export class Authorization { ); const checkPrivileges = securityAuth.checkPrivilegesDynamicallyWithRequest(this.request); - const { hasAllRequested, username, privileges } = checkPrivileges({ + const { hasAllRequested, username, privileges } = await checkPrivileges({ kibana: requiredPrivileges, }); @@ -115,6 +115,10 @@ export class Authorization { * This check will ensure we don't accidentally let these through */ } + + if (hasAllRequested) { + // TODO: log success + } } } } From 59e4045f24f0097c677f1ae1cb048c1b7bb2f64f Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 11 Mar 2021 14:19:26 +0200 Subject: [PATCH 020/113] Failure if hasAllRequested is false --- .../cases/server/authorization/authorization.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/cases/server/authorization/authorization.ts b/x-pack/plugins/cases/server/authorization/authorization.ts index 997e151ddb34b7..776b98b1139fde 100644 --- a/x-pack/plugins/cases/server/authorization/authorization.ts +++ b/x-pack/plugins/cases/server/authorization/authorization.ts @@ -117,8 +117,23 @@ export class Authorization { } if (hasAllRequested) { - // TODO: log success + // TODO: user authorized. log success + } else { + const authorizedPrivileges = privileges.kibana.reduce((acc, privilege) => { + if (privilege.authorized) { + return [...acc, privilege.privilege]; + } + return acc; + }, []); + + const unauthorizedPrivilages = requiredPrivileges.filter( + (privilege) => !authorizedPrivileges.includes(privilege) + ); + + // TODO: User unauthorized. throw an error. authorizedPrivileges & unauthorizedPrivilages are needed for logging. } + } else if (!areAllClassAvailable) { + // TODO: throw an error } } } From 40927336fb5ede3c4f71d55ac744223036fac30a Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Thu, 11 Mar 2021 17:06:58 -0500 Subject: [PATCH 021/113] Adding schema updates for feature plugin --- .../server/authorization/authorization.ts | 27 ++++++++++++------- .../plugins/features/server/feature_schema.ts | 10 +++++++ 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/cases/server/authorization/authorization.ts b/x-pack/plugins/cases/server/authorization/authorization.ts index 776b98b1139fde..3281672ffb921a 100644 --- a/x-pack/plugins/cases/server/authorization/authorization.ts +++ b/x-pack/plugins/cases/server/authorization/authorization.ts @@ -6,6 +6,7 @@ */ import { KibanaRequest } from 'kibana/server'; +import Boom from '@hapi/boom'; import { Space } from '../../../spaces/server'; import { SecurityPluginStart } from '../../../security/server'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; @@ -34,6 +35,7 @@ export class Authorization { private readonly request: KibanaRequest; private readonly securityAuth: SecurityPluginStart['authz'] | undefined; private readonly featureCaseClasses: Set; + private readonly isAuthEnabled: boolean; // TODO: create this // private readonly auditLogger: AuthorizationAuditLogger; @@ -41,14 +43,17 @@ export class Authorization { request, securityAuth, caseClasses, + isAuthEnabled, }: { request: KibanaRequest; securityAuth?: SecurityPluginStart['authz']; caseClasses: Set; + isAuthEnabled: boolean; }) { this.request = request; this.securityAuth = securityAuth; this.featureCaseClasses = caseClasses; + this.isAuthEnabled = isAuthEnabled; } /** @@ -59,11 +64,13 @@ export class Authorization { securityAuth, getSpace, features, + isAuthEnabled, }: { request: KibanaRequest; securityAuth?: SecurityPluginStart['authz']; getSpace: GetSpaceFn; features: FeaturesPluginStart; + isAuthEnabled: boolean; }): Promise { // Since we need to do async operations, this static method handles that before creating the Auth class let caseClasses: Set; @@ -81,31 +88,27 @@ export class Authorization { caseClasses = new Set(); } - return new Authorization({ request, securityAuth, caseClasses }); + return new Authorization({ request, securityAuth, caseClasses, isAuthEnabled }); } private shouldCheckAuthorization(): boolean { return this.securityAuth?.mode?.useRbacForRequest(this.request) ?? false; } - public async ensureAuthorized(classes: string[], operation: ReadOperations | WriteOperations) { + public async ensureAuthorized(className: string, operation: ReadOperations | WriteOperations) { const { securityAuth } = this; - const areAllClassAvailable = classes.every((className) => - this.featureCaseClasses.has(className) - ); + const isAvailableClass = this.featureCaseClasses.has(className); // TODO: throw if the request is not authorized if (securityAuth && this.shouldCheckAuthorization()) { // TODO: implement ensure logic - const requiredPrivileges: string[] = classes.map((className) => - securityAuth.actions.cases.get(className, operation) - ); + const requiredPrivileges: string[] = [securityAuth.actions.cases.get(className, operation)]; const checkPrivileges = securityAuth.checkPrivilegesDynamicallyWithRequest(this.request); const { hasAllRequested, username, privileges } = await checkPrivileges({ kibana: requiredPrivileges, }); - if (!areAllClassAvailable) { + if (!isAvailableClass) { // TODO: throw if any of the class are not available /** * Under most circumstances this would have been caught by `checkPrivileges` as @@ -114,6 +117,7 @@ export class Authorization { * as Privileged. * This check will ensure we don't accidentally let these through */ + throw Boom.forbidden('User does not have permissions for this class'); } if (hasAllRequested) { @@ -130,10 +134,13 @@ export class Authorization { (privilege) => !authorizedPrivileges.includes(privilege) ); + // TODO: audit log // TODO: User unauthorized. throw an error. authorizedPrivileges & unauthorizedPrivilages are needed for logging. + throw Boom.forbidden('Not authorized for this class'); } - } else if (!areAllClassAvailable) { + } else { // TODO: throw an error + throw Boom.forbidden('Security is disabled'); } } } diff --git a/x-pack/plugins/features/server/feature_schema.ts b/x-pack/plugins/features/server/feature_schema.ts index 204c5bdfe24698..e3525f82607e7a 100644 --- a/x-pack/plugins/features/server/feature_schema.ts +++ b/x-pack/plugins/features/server/feature_schema.ts @@ -33,6 +33,7 @@ const managementSchema = Joi.object().pattern( ); const catalogueSchema = Joi.array().items(Joi.string().regex(uiCapabilitiesRegex)); const alertingSchema = Joi.array().items(Joi.string()); +const casesSchema = Joi.array().items(Joi.string()); const appCategorySchema = Joi.object({ id: Joi.string().required(), @@ -52,6 +53,10 @@ const kibanaPrivilegeSchema = Joi.object({ all: alertingSchema, read: alertingSchema, }), + cases: Joi.object({ + all: casesSchema, + read: casesSchema, + }), savedObject: Joi.object({ all: Joi.array().items(Joi.string()).required(), read: Joi.array().items(Joi.string()).required(), @@ -70,6 +75,10 @@ const kibanaIndependentSubFeaturePrivilegeSchema = Joi.object({ all: alertingSchema, read: alertingSchema, }), + cases: Joi.object({ + all: casesSchema, + read: casesSchema, + }), api: Joi.array().items(Joi.string()), app: Joi.array().items(Joi.string()), savedObject: Joi.object({ @@ -113,6 +122,7 @@ const kibanaFeatureSchema = Joi.object({ management: managementSchema, catalogue: catalogueSchema, alerting: alertingSchema, + cases: casesSchema, privileges: Joi.object({ all: kibanaPrivilegeSchema, read: kibanaPrivilegeSchema, From 311e3f489c89a57f06ddd020a1fae279406972fc Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 16 Mar 2021 12:05:09 +0200 Subject: [PATCH 022/113] Seperate basic from trial --- .../case_api_integration/common/config.ts | 5 +- .../security_and_spaces/config_basic.ts | 6 +- .../security_and_spaces/config_trial.ts | 6 +- .../tests/basic/cases/push_case.ts | 86 +++++++++++++ .../security_and_spaces/tests/basic/index.ts | 22 ++++ .../cases/comments/delete_comment.ts | 8 +- .../cases/comments/find_comments.ts | 10 +- .../cases/comments/get_all_comments.ts | 10 +- .../cases/comments/get_comment.ts | 10 +- .../{ => common}/cases/comments/migrations.ts | 4 +- .../cases/comments/patch_comment.ts | 10 +- .../cases/comments/post_comment.ts | 14 +-- .../tests/{ => common}/cases/delete_cases.ts | 12 +- .../tests/{ => common}/cases/find_cases.ts | 17 ++- .../tests/{ => common}/cases/get_case.ts | 8 +- .../tests/{ => common}/cases/migrations.ts | 4 +- .../tests/{ => common}/cases/patch_cases.ts | 14 +-- .../tests/{ => common}/cases/post_case.ts | 8 +- .../cases/reporters/get_reporters.ts | 8 +- .../{ => common}/cases/status/get_status.ts | 8 +- .../cases/sub_cases/delete_sub_cases.ts | 12 +- .../cases/sub_cases/find_sub_cases.ts | 12 +- .../cases/sub_cases/get_sub_case.ts | 10 +- .../cases/sub_cases/patch_sub_cases.ts | 14 +-- .../tests/{ => common}/cases/tags/get_tags.ts | 8 +- .../user_actions/get_all_user_actions.ts | 93 +------------- .../cases/user_actions/migrations.ts | 4 +- .../{ => common}/configure/get_configure.ts | 6 +- .../tests/common/configure/get_connectors.ts | 34 ++++++ .../{ => common}/configure/migrations.ts | 4 +- .../{ => common}/configure/patch_configure.ts | 6 +- .../{ => common}/configure/post_configure.ts | 6 +- .../tests/{ => common}/connectors/case.ts | 10 +- .../tests/{ => common}/index.ts | 8 +- .../tests/{ => trial}/cases/push_case.ts | 14 +-- .../user_actions/get_all_user_actions.ts | 115 ++++++++++++++++++ .../{ => trial}/configure/get_connectors.ts | 18 +-- .../security_and_spaces/tests/trial/index.ts | 22 ++++ 38 files changed, 432 insertions(+), 234 deletions(-) create mode 100644 x-pack/test/case_api_integration/security_and_spaces/tests/basic/cases/push_case.ts create mode 100644 x-pack/test/case_api_integration/security_and_spaces/tests/basic/index.ts rename x-pack/test/case_api_integration/security_and_spaces/tests/{ => common}/cases/comments/delete_comment.ts (94%) rename x-pack/test/case_api_integration/security_and_spaces/tests/{ => common}/cases/comments/find_comments.ts (93%) rename x-pack/test/case_api_integration/security_and_spaces/tests/{ => common}/cases/comments/get_all_comments.ts (89%) rename x-pack/test/case_api_integration/security_and_spaces/tests/{ => common}/cases/comments/get_comment.ts (87%) rename x-pack/test/case_api_integration/security_and_spaces/tests/{ => common}/cases/comments/migrations.ts (86%) rename x-pack/test/case_api_integration/security_and_spaces/tests/{ => common}/cases/comments/patch_comment.ts (97%) rename x-pack/test/case_api_integration/security_and_spaces/tests/{ => common}/cases/comments/post_comment.ts (96%) rename x-pack/test/case_api_integration/security_and_spaces/tests/{ => common}/cases/delete_cases.ts (91%) rename x-pack/test/case_api_integration/security_and_spaces/tests/{ => common}/cases/find_cases.ts (97%) rename x-pack/test/case_api_integration/security_and_spaces/tests/{ => common}/cases/get_case.ts (83%) rename x-pack/test/case_api_integration/security_and_spaces/tests/{ => common}/cases/migrations.ts (95%) rename x-pack/test/case_api_integration/security_and_spaces/tests/{ => common}/cases/patch_cases.ts (98%) rename x-pack/test/case_api_integration/security_and_spaces/tests/{ => common}/cases/post_case.ts (89%) rename x-pack/test/case_api_integration/security_and_spaces/tests/{ => common}/cases/reporters/get_reporters.ts (79%) rename x-pack/test/case_api_integration/security_and_spaces/tests/{ => common}/cases/status/get_status.ts (86%) rename x-pack/test/case_api_integration/security_and_spaces/tests/{ => common}/cases/sub_cases/delete_sub_cases.ts (87%) rename x-pack/test/case_api_integration/security_and_spaces/tests/{ => common}/cases/sub_cases/find_sub_cases.ts (95%) rename x-pack/test/case_api_integration/security_and_spaces/tests/{ => common}/cases/sub_cases/get_sub_case.ts (92%) rename x-pack/test/case_api_integration/security_and_spaces/tests/{ => common}/cases/sub_cases/patch_sub_cases.ts (96%) rename x-pack/test/case_api_integration/security_and_spaces/tests/{ => common}/cases/tags/get_tags.ts (77%) rename x-pack/test/case_api_integration/security_and_spaces/tests/{ => common}/cases/user_actions/get_all_user_actions.ts (75%) rename x-pack/test/case_api_integration/security_and_spaces/tests/{ => common}/cases/user_actions/migrations.ts (90%) rename x-pack/test/case_api_integration/security_and_spaces/tests/{ => common}/configure/get_configure.ts (87%) create mode 100644 x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/get_connectors.ts rename x-pack/test/case_api_integration/security_and_spaces/tests/{ => common}/configure/migrations.ts (87%) rename x-pack/test/case_api_integration/security_and_spaces/tests/{ => common}/configure/patch_configure.ts (94%) rename x-pack/test/case_api_integration/security_and_spaces/tests/{ => common}/configure/post_configure.ts (92%) rename x-pack/test/case_api_integration/security_and_spaces/tests/{ => common}/connectors/case.ts (99%) rename x-pack/test/case_api_integration/security_and_spaces/tests/{ => common}/index.ts (89%) rename x-pack/test/case_api_integration/security_and_spaces/tests/{ => trial}/cases/push_case.ts (95%) create mode 100644 x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/user_actions/get_all_user_actions.ts rename x-pack/test/case_api_integration/security_and_spaces/tests/{ => trial}/configure/get_connectors.ts (84%) create mode 100644 x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts diff --git a/x-pack/test/case_api_integration/common/config.ts b/x-pack/test/case_api_integration/common/config.ts index 3c814072764532..ab12154652dc11 100644 --- a/x-pack/test/case_api_integration/common/config.ts +++ b/x-pack/test/case_api_integration/common/config.ts @@ -17,6 +17,7 @@ interface CreateTestConfigOptions { license: string; disabledPlugins?: string[]; ssl?: boolean; + testFiles?: string[]; } // test.not-enabled is specifically not enabled @@ -39,7 +40,7 @@ const enabledActionTypes = [ ]; export function createTestConfig(name: string, options: CreateTestConfigOptions) { - const { license = 'trial', disabledPlugins = [], ssl = false } = options; + const { license = 'trial', disabledPlugins = [], ssl = false, testFiles = [] } = options; return async ({ readConfigFile }: FtrConfigProviderContext) => { const xPackApiIntegrationTestsConfig = await readConfigFile( @@ -83,7 +84,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) ); return { - testFiles: [require.resolve(`../${name}/tests/`)], + testFiles: testFiles ? testFiles : [require.resolve('../tests/common')], servers, services, junit: { diff --git a/x-pack/test/case_api_integration/security_and_spaces/config_basic.ts b/x-pack/test/case_api_integration/security_and_spaces/config_basic.ts index 57698eb2fe836f..98b7b1abe98e78 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/config_basic.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/config_basic.ts @@ -8,4 +8,8 @@ import { createTestConfig } from '../common/config'; // eslint-disable-next-line import/no-default-export -export default createTestConfig('security_and_spaces', { license: 'basic', ssl: true }); +export default createTestConfig('security_and_spaces', { + license: 'basic', + ssl: true, + testFiles: [require.resolve('./tests/basic')], +}); diff --git a/x-pack/test/case_api_integration/security_and_spaces/config_trial.ts b/x-pack/test/case_api_integration/security_and_spaces/config_trial.ts index 3256ed599e9822..b5328fd83c2cbc 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/config_trial.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/config_trial.ts @@ -8,4 +8,8 @@ import { createTestConfig } from '../common/config'; // eslint-disable-next-line import/no-default-export -export default createTestConfig('security_and_spaces', { license: 'trial', ssl: true }); +export default createTestConfig('security_and_spaces', { + license: 'trial', + ssl: true, + testFiles: [require.resolve('./tests/trial')], +}); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/basic/cases/push_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/cases/push_case.ts new file mode 100644 index 00000000000000..067171cef30a44 --- /dev/null +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/cases/push_case.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +import { CASE_CONFIGURE_URL, CASES_URL } from '../../../../../../plugins/cases/common/constants'; + +import { postCaseReq } from '../../../../common/lib/mock'; +import { + deleteCases, + deleteCasesUserActions, + deleteComments, + deleteConfiguration, + getConfiguration, + getServiceNowConnector, +} from '../../../../common/lib/utils'; +import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('push_case', () => { + afterEach(async () => { + await deleteCases(es); + await deleteComments(es); + await deleteConfiguration(es); + await deleteCasesUserActions(es); + }); + + it('should get 403 when trying to create a connector', async () => { + await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'true') + .send({ + ...getServiceNowConnector(), + }) + .expect(403); + }); + + it('should get 404 when trying to push to a case without a valid connector id', async () => { + await supertest + .post(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send( + getConfiguration({ + id: 'not-exist', + name: 'Not exist', + type: ConnectorTypes.serviceNowITSM, + }) + ) + .expect(200); + + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + ...postCaseReq, + connector: getConfiguration({ + id: 'not-exist', + name: 'Not exist', + type: ConnectorTypes.serviceNowITSM, + fields: { + urgency: '2', + impact: '2', + severity: '2', + category: 'software', + subcategory: 'os', + }, + }).connector, + }) + .expect(200); + + await supertest + .post(`${CASES_URL}/${postedCase.id}/connector/not-exist/_push`) + .set('kbn-xsrf', 'true') + .send({}) + .expect(404); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/basic/index.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/index.ts new file mode 100644 index 00000000000000..95174be3ab1b73 --- /dev/null +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('cases security and spaces enabled: basic', function () { + // Fastest ciGroup for the moment. + this.tags('ciGroup5'); + + // Common + loadTestFile(require.resolve('../common')); + + // Basic + loadTestFile(require.resolve('./cases/push_case')); + }); +}; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/delete_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/delete_comment.ts similarity index 94% rename from x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/delete_comment.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/delete_comment.ts index 90d46fe65a2e01..e6d582809b3211 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/delete_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/delete_comment.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 { postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; +import { CASES_URL } from '../../../../../../../plugins/cases/common/constants'; +import { postCaseReq, postCommentUserReq } from '../../../../../common/lib/mock'; import { createCaseAction, createSubCase, @@ -18,7 +18,7 @@ import { deleteCases, deleteCasesUserActions, deleteComments, -} from '../../../../common/lib/utils'; +} from '../../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/find_comments.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/find_comments.ts similarity index 93% rename from x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/find_comments.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/find_comments.ts index 7bbc8e344ee23e..b8c18140da474c 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/find_comments.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/find_comments.ts @@ -6,11 +6,11 @@ */ 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 { CommentsResponse, CommentType } from '../../../../../../plugins/cases/common/api'; -import { postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; +import { CASES_URL } from '../../../../../../../plugins/cases/common/constants'; +import { CommentsResponse, CommentType } from '../../../../../../../plugins/cases/common/api'; +import { postCaseReq, postCommentUserReq } from '../../../../../common/lib/mock'; import { createCaseAction, createSubCase, @@ -19,7 +19,7 @@ import { deleteCases, deleteCasesUserActions, deleteComments, -} from '../../../../common/lib/utils'; +} from '../../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/get_all_comments.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/get_all_comments.ts similarity index 89% rename from x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/get_all_comments.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/get_all_comments.ts index 723c9eba33bebe..d66e094969d133 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/get_all_comments.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/get_all_comments.ts @@ -6,17 +6,17 @@ */ 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 { postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; +import { CASES_URL } from '../../../../../../../plugins/cases/common/constants'; +import { postCaseReq, postCommentUserReq } from '../../../../../common/lib/mock'; import { createCaseAction, createSubCase, deleteAllCaseItems, deleteCaseAction, -} from '../../../../common/lib/utils'; -import { CommentType } from '../../../../../../plugins/cases/common/api'; +} from '../../../../../common/lib/utils'; +import { CommentType } from '../../../../../../../plugins/cases/common/api'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/get_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/get_comment.ts similarity index 87% rename from x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/get_comment.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/get_comment.ts index 1a1bb727bd429b..54f617b36d1b92 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/get_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/get_comment.ts @@ -6,17 +6,17 @@ */ 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 { postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; +import { CASES_URL } from '../../../../../../../plugins/cases/common/constants'; +import { postCaseReq, postCommentUserReq } from '../../../../../common/lib/mock'; import { createCaseAction, createSubCase, deleteAllCaseItems, deleteCaseAction, -} from '../../../../common/lib/utils'; -import { CommentResponse, CommentType } from '../../../../../../plugins/cases/common/api'; +} from '../../../../../common/lib/utils'; +import { CommentResponse, CommentType } from '../../../../../../../plugins/cases/common/api'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/migrations.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/migrations.ts similarity index 86% rename from x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/migrations.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/migrations.ts index 264ac2a0898e0c..8ceb81017ecdb0 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/migrations.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/migrations.ts @@ -6,8 +6,8 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; +import { CASES_URL } from '../../../../../../../plugins/cases/common/constants'; // eslint-disable-next-line import/no-default-export export default function createGetTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/patch_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/patch_comment.ts similarity index 97% rename from x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/patch_comment.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/patch_comment.ts index bddc620535dda8..d99ea6e4b7da1a 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/patch_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/patch_comment.ts @@ -7,16 +7,16 @@ import { omit } from 'lodash/fp'; 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 { CaseResponse, CommentType } from '../../../../../../plugins/cases/common/api'; +import { CASES_URL } from '../../../../../../../plugins/cases/common/constants'; +import { CaseResponse, CommentType } from '../../../../../../../plugins/cases/common/api'; import { defaultUser, postCaseReq, postCommentUserReq, postCommentAlertReq, -} from '../../../../common/lib/mock'; +} from '../../../../../common/lib/mock'; import { createCaseAction, createSubCase, @@ -25,7 +25,7 @@ import { deleteCases, deleteCasesUserActions, deleteComments, -} from '../../../../common/lib/utils'; +} from '../../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/post_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/post_comment.ts similarity index 96% rename from x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/post_comment.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/post_comment.ts index 5e48e39164e6be..032249b27aae7c 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/comments/post_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/post_comment.ts @@ -7,11 +7,11 @@ import { omit } from 'lodash/fp'; 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 { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../../plugins/security_solution/common/constants'; -import { CommentsResponse, CommentType } from '../../../../../../plugins/cases/common/api'; +import { CASES_URL } from '../../../../../../../plugins/cases/common/constants'; +import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../../../plugins/security_solution/common/constants'; +import { CommentsResponse, CommentType } from '../../../../../../../plugins/cases/common/api'; import { defaultUser, postCaseReq, @@ -19,7 +19,7 @@ import { postCommentAlertReq, postCollectionReq, postCommentGenAlertReq, -} from '../../../../common/lib/mock'; +} from '../../../../../common/lib/mock'; import { createCaseAction, createSubCase, @@ -28,7 +28,7 @@ import { deleteCases, deleteCasesUserActions, deleteComments, -} from '../../../../common/lib/utils'; +} from '../../../../../common/lib/utils'; import { createSignalsIndex, deleteSignalsIndex, @@ -39,7 +39,7 @@ import { getSignalsByIds, createRule, getQuerySignalIds, -} from '../../../../../detection_engine_api_integration/utils'; +} from '../../../../../../detection_engine_api_integration/utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/delete_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/delete_cases.ts similarity index 91% rename from x-pack/test/case_api_integration/security_and_spaces/tests/cases/delete_cases.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/delete_cases.ts index b5187931a9f01a..444838bd5841e7 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/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 { postCaseReq, postCommentUserReq } from '../../../common/lib/mock'; +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; import { createCaseAction, createSubCase, @@ -18,9 +18,9 @@ import { deleteCases, deleteCasesUserActions, deleteComments, -} from '../../../common/lib/utils'; -import { getSubCaseDetailsUrl } from '../../../../../plugins/cases/common/api/helpers'; -import { CaseResponse } from '../../../../../plugins/cases/common/api'; +} from '../../../../common/lib/utils'; +import { getSubCaseDetailsUrl } from '../../../../../../plugins/cases/common/api/helpers'; +import { CaseResponse } from '../../../../../../plugins/cases/common/api'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/find_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/find_cases.ts similarity index 97% rename from x-pack/test/case_api_integration/security_and_spaces/tests/cases/find_cases.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/find_cases.ts index b808ff4ccdf35a..06e00c2d2219f3 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/find_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/find_cases.ts @@ -6,10 +6,13 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { CASES_URL, SUB_CASES_PATCH_DEL_URL } from '../../../../../plugins/cases/common/constants'; -import { postCaseReq, postCommentUserReq, findCasesResp } from '../../../common/lib/mock'; +import { + CASES_URL, + SUB_CASES_PATCH_DEL_URL, +} from '../../../../../../plugins/cases/common/constants'; +import { postCaseReq, postCommentUserReq, findCasesResp } from '../../../../common/lib/mock'; import { deleteAllCaseItems, createSubCase, @@ -17,8 +20,12 @@ import { CreateSubCaseResp, createCaseAction, deleteCaseAction, -} from '../../../common/lib/utils'; -import { CasesFindResponse, CaseStatuses, CaseType } from '../../../../../plugins/cases/common/api'; +} from '../../../../common/lib/utils'; +import { + CasesFindResponse, + CaseStatuses, + CaseType, +} from '../../../../../../plugins/cases/common/api'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/get_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/get_case.ts similarity index 83% rename from x-pack/test/case_api_integration/security_and_spaces/tests/cases/get_case.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/get_case.ts index fb4ab2c86469a7..03e97a43978d84 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/get_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/get_case.ts @@ -6,15 +6,15 @@ */ 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 { CASES_URL } from '../../../../../../plugins/cases/common/constants'; import { postCaseReq, postCaseResp, removeServerGeneratedPropertiesFromCase, -} from '../../../common/lib/mock'; -import { deleteCases } from '../../../common/lib/utils'; +} from '../../../../common/lib/mock'; +import { deleteCases } from '../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/migrations.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/migrations.ts similarity index 95% rename from x-pack/test/case_api_integration/security_and_spaces/tests/cases/migrations.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/migrations.ts index abbb749a2aaca3..42fcace768b157 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/migrations.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/migrations.ts @@ -6,8 +6,8 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { CASES_URL } from '../../../../../plugins/cases/common/constants'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; // eslint-disable-next-line import/no-default-export export default function createGetTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/patch_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts similarity index 98% rename from x-pack/test/case_api_integration/security_and_spaces/tests/cases/patch_cases.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts index 950fde37e30784..bec038e881cc52 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/patch_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts @@ -6,16 +6,16 @@ */ 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 { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../plugins/security_solution/common/constants'; +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../../plugins/security_solution/common/constants'; import { CasesResponse, CaseStatuses, CaseType, CommentType, -} from '../../../../../plugins/cases/common/api'; +} from '../../../../../../plugins/cases/common/api'; import { defaultUser, postCaseReq, @@ -24,8 +24,8 @@ import { postCommentAlertReq, postCommentUserReq, removeServerGeneratedPropertiesFromCase, -} from '../../../common/lib/mock'; -import { deleteAllCaseItems, getSignalsWithES, setStatus } from '../../../common/lib/utils'; +} from '../../../../common/lib/mock'; +import { deleteAllCaseItems, getSignalsWithES, setStatus } from '../../../../common/lib/utils'; import { createSignalsIndex, deleteSignalsIndex, @@ -36,7 +36,7 @@ import { getSignalsByIds, createRule, getQuerySignalIds, -} from '../../../../detection_engine_api_integration/utils'; +} from '../../../../../detection_engine_api_integration/utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/post_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/post_case.ts similarity index 89% rename from x-pack/test/case_api_integration/security_and_spaces/tests/cases/post_case.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/post_case.ts index 5de5644ccf68a4..afcc36d041c111 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/post_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/post_case.ts @@ -6,15 +6,15 @@ */ 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 { CASES_URL } from '../../../../../../plugins/cases/common/constants'; import { postCaseReq, postCaseResp, removeServerGeneratedPropertiesFromCase, -} from '../../../common/lib/mock'; -import { deleteCases } from '../../../common/lib/utils'; +} from '../../../../common/lib/mock'; +import { deleteCases } from '../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/reporters/get_reporters.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/reporters/get_reporters.ts similarity index 79% rename from x-pack/test/case_api_integration/security_and_spaces/tests/cases/reporters/get_reporters.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/reporters/get_reporters.ts index c51bfda5bd8b09..c6e84766e46382 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/reporters/get_reporters.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/reporters/get_reporters.ts @@ -6,11 +6,11 @@ */ 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 { deleteCases } from '../../../../common/lib/utils'; +import { CASES_URL, CASE_REPORTERS_URL } from '../../../../../../../plugins/cases/common/constants'; +import { defaultUser, postCaseReq } from '../../../../../common/lib/mock'; +import { deleteCases } from '../../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/status/get_status.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts similarity index 86% rename from x-pack/test/case_api_integration/security_and_spaces/tests/cases/status/get_status.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts index 1657293953246f..71602f993a1d4f 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/status/get_status.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts @@ -6,11 +6,11 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; -import { CASES_URL, CASE_STATUS_URL } from '../../../../../../plugins/cases/common/constants'; -import { postCaseReq } from '../../../../common/lib/mock'; -import { deleteCases } from '../../../../common/lib/utils'; +import { CASES_URL, CASE_STATUS_URL } from '../../../../../../../plugins/cases/common/constants'; +import { postCaseReq } from '../../../../../common/lib/mock'; +import { deleteCases } from '../../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/sub_cases/delete_sub_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/sub_cases/delete_sub_cases.ts similarity index 87% rename from x-pack/test/case_api_integration/security_and_spaces/tests/cases/sub_cases/delete_sub_cases.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/sub_cases/delete_sub_cases.ts index d179120cd3d853..92f7e46204d058 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/sub_cases/delete_sub_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/sub_cases/delete_sub_cases.ts @@ -5,21 +5,21 @@ * 2.0. */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; import { CASES_URL, SUB_CASES_PATCH_DEL_URL, -} from '../../../../../../plugins/cases/common/constants'; -import { postCommentUserReq } from '../../../../common/lib/mock'; +} from '../../../../../../../plugins/cases/common/constants'; +import { postCommentUserReq } from '../../../../../common/lib/mock'; import { createCaseAction, createSubCase, deleteAllCaseItems, deleteCaseAction, -} from '../../../../common/lib/utils'; -import { getSubCaseDetailsUrl } from '../../../../../../plugins/cases/common/api/helpers'; -import { CaseResponse } from '../../../../../../plugins/cases/common/api'; +} from '../../../../../common/lib/utils'; +import { getSubCaseDetailsUrl } from '../../../../../../../plugins/cases/common/api/helpers'; +import { CaseResponse } from '../../../../../../../plugins/cases/common/api'; // eslint-disable-next-line import/no-default-export export default function ({ getService }: FtrProviderContext) { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/sub_cases/find_sub_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/sub_cases/find_sub_cases.ts similarity index 95% rename from x-pack/test/case_api_integration/security_and_spaces/tests/cases/sub_cases/find_sub_cases.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/sub_cases/find_sub_cases.ts index 2c1bd9c7bd8835..edc49a2fd74af8 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/sub_cases/find_sub_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/sub_cases/find_sub_cases.ts @@ -6,23 +6,23 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; -import { findSubCasesResp, postCollectionReq } from '../../../../common/lib/mock'; +import { findSubCasesResp, postCollectionReq } from '../../../../../common/lib/mock'; import { createCaseAction, createSubCase, deleteAllCaseItems, deleteCaseAction, setStatus, -} from '../../../../common/lib/utils'; -import { getSubCasesUrl } from '../../../../../../plugins/cases/common/api/helpers'; +} from '../../../../../common/lib/utils'; +import { getSubCasesUrl } from '../../../../../../../plugins/cases/common/api/helpers'; import { CaseResponse, CaseStatuses, SubCasesFindResponse, -} from '../../../../../../plugins/cases/common/api'; -import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +} from '../../../../../../../plugins/cases/common/api'; +import { CASES_URL } from '../../../../../../../plugins/cases/common/constants'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/sub_cases/get_sub_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/sub_cases/get_sub_case.ts similarity index 92% rename from x-pack/test/case_api_integration/security_and_spaces/tests/cases/sub_cases/get_sub_case.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/sub_cases/get_sub_case.ts index 440731cd07fe79..d03db38bf3758a 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/sub_cases/get_sub_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/sub_cases/get_sub_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 { commentsResp, @@ -14,23 +14,23 @@ import { removeServerGeneratedPropertiesFromComments, removeServerGeneratedPropertiesFromSubCase, subCaseResp, -} from '../../../../common/lib/mock'; +} from '../../../../../common/lib/mock'; import { createCaseAction, createSubCase, defaultCreateSubComment, deleteAllCaseItems, deleteCaseAction, -} from '../../../../common/lib/utils'; +} from '../../../../../common/lib/utils'; import { getCaseCommentsUrl, getSubCaseDetailsUrl, -} from '../../../../../../plugins/cases/common/api/helpers'; +} from '../../../../../../../plugins/cases/common/api/helpers'; import { AssociationType, CaseResponse, SubCaseResponse, -} from '../../../../../../plugins/cases/common/api'; +} from '../../../../../../../plugins/cases/common/api'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/sub_cases/patch_sub_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/sub_cases/patch_sub_cases.ts similarity index 96% rename from x-pack/test/case_api_integration/security_and_spaces/tests/cases/sub_cases/patch_sub_cases.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/sub_cases/patch_sub_cases.ts index d647bb09f804a0..815bb05728d4fd 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/sub_cases/patch_sub_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/sub_cases/patch_sub_cases.ts @@ -5,12 +5,12 @@ * 2.0. */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; import { CASES_URL, SUB_CASES_PATCH_DEL_URL, -} from '../../../../../../plugins/cases/common/constants'; +} from '../../../../../../../plugins/cases/common/constants'; import { createCaseAction, createSubCase, @@ -18,15 +18,15 @@ import { deleteCaseAction, getSignalsWithES, setStatus, -} from '../../../../common/lib/utils'; -import { getSubCaseDetailsUrl } from '../../../../../../plugins/cases/common/api/helpers'; +} from '../../../../../common/lib/utils'; +import { getSubCaseDetailsUrl } from '../../../../../../../plugins/cases/common/api/helpers'; import { CaseStatuses, CommentType, SubCaseResponse, -} from '../../../../../../plugins/cases/common/api'; -import { createAlertsString } from '../../../../../../plugins/cases/server/connectors'; -import { postCaseReq, postCollectionReq } from '../../../../common/lib/mock'; +} from '../../../../../../../plugins/cases/common/api'; +import { createAlertsString } from '../../../../../../../plugins/cases/server/connectors'; +import { postCaseReq, postCollectionReq } from '../../../../../common/lib/mock'; const defaultSignalsIndex = '.siem-signals-default-000001'; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/tags/get_tags.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/tags/get_tags.ts similarity index 77% rename from x-pack/test/case_api_integration/security_and_spaces/tests/cases/tags/get_tags.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/tags/get_tags.ts index f5cbb7c7f0eb01..3ca8e9b6aa3ce8 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/tags/get_tags.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/tags/get_tags.ts @@ -6,11 +6,11 @@ */ 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 { deleteCases } from '../../../../common/lib/utils'; +import { CASES_URL, CASE_TAGS_URL } from '../../../../../../../plugins/cases/common/constants'; +import { postCaseReq } from '../../../../../common/lib/mock'; +import { deleteCases } from '../../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/user_actions/get_all_user_actions.ts similarity index 75% rename from x-pack/test/case_api_integration/security_and_spaces/tests/cases/user_actions/get_all_user_actions.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/user_actions/get_all_user_actions.ts index a3bc2a4399db27..8f047602acc38d 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/user_actions/get_all_user_actions.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/user_actions/get_all_user_actions.ts @@ -6,51 +6,33 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; -import { CASE_CONFIGURE_URL, CASES_URL } from '../../../../../../plugins/cases/common/constants'; -import { CommentType } from '../../../../../../plugins/cases/common/api'; +import { CASES_URL } from '../../../../../../../plugins/cases/common/constants'; +import { CommentType } from '../../../../../../../plugins/cases/common/api'; import { userActionPostResp, - defaultUser, postCaseReq, postCommentUserReq, -} from '../../../../common/lib/mock'; +} from '../../../../../common/lib/mock'; import { deleteCases, deleteCasesUserActions, deleteComments, deleteConfiguration, - getConfiguration, - getServiceNowConnector, -} from '../../../../common/lib/utils'; - -import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; -import { - ExternalServiceSimulator, - getExternalServiceSimulatorPath, -} from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; +} from '../../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); - const actionsRemover = new ActionsRemover(supertest); - const kibanaServer = getService('kibanaServer'); describe('get_all_user_actions', () => { - let servicenowSimulatorURL: string = ''; - before(() => { - servicenowSimulatorURL = kibanaServer.resolveUrl( - getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) - ); - }); afterEach(async () => { await deleteCases(es); await deleteComments(es); await deleteConfiguration(es); await deleteCasesUserActions(es); - await actionsRemover.removeAll(); }); it(`on new case, user action: 'create' should be called with actionFields: ['description', 'status', 'tags', 'title', 'connector', 'settings]`, async () => { @@ -336,70 +318,5 @@ export default ({ getService }: FtrProviderContext): void => { type: CommentType.user, }); }); - - it(`on new push to service, user action: 'push-to-service' should be called with actionFields: ['pushed']`, async () => { - const { body: connector } = await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'true') - .send({ - ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, - }) - .expect(200); - - actionsRemover.add('default', connector.id, 'action', 'actions'); - - const { body: configure } = await supertest - .post(CASE_CONFIGURE_URL) - .set('kbn-xsrf', 'true') - .send( - getConfiguration({ - id: connector.id, - name: connector.name, - type: connector.actionTypeId, - }) - ) - .expect(200); - - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - ...postCaseReq, - connector: getConfiguration({ - id: connector.id, - name: connector.name, - type: connector.actionTypeId, - fields: { - urgency: '2', - impact: '2', - severity: '2', - category: 'software', - subcategory: 'os', - }, - }).connector, - }) - .expect(200); - - await supertest - .post(`${CASES_URL}/${postedCase.id}/connector/${connector.id}/_push`) - .set('kbn-xsrf', 'true') - .send({}) - .expect(200); - - const { body } = await supertest - .get(`${CASES_URL}/${postedCase.id}/user_actions`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(body.length).to.eql(2); - expect(body[1].action_field).to.eql(['pushed']); - expect(body[1].action).to.eql('push-to-service'); - expect(body[1].old_value).to.eql(null); - const newValue = JSON.parse(body[1].new_value); - expect(newValue.connector_id).to.eql(configure.connector.id); - expect(newValue.pushed_by).to.eql(defaultUser); - }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/user_actions/migrations.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/user_actions/migrations.ts similarity index 90% rename from x-pack/test/case_api_integration/security_and_spaces/tests/cases/user_actions/migrations.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/user_actions/migrations.ts index d0f852b3f57e7b..8bba29a56cd9d7 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/user_actions/migrations.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/user_actions/migrations.ts @@ -6,8 +6,8 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; +import { CASES_URL } from '../../../../../../../plugins/cases/common/constants'; // eslint-disable-next-line import/no-default-export export default function createGetTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/configure/get_configure.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/get_configure.ts similarity index 87% rename from x-pack/test/case_api_integration/security_and_spaces/tests/configure/get_configure.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/get_configure.ts index c892edff2d458b..391cb3a4e5a2ab 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/configure/get_configure.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/get_configure.ts @@ -6,15 +6,15 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { CASE_CONFIGURE_URL } from '../../../../../plugins/cases/common/constants'; +import { CASE_CONFIGURE_URL } from '../../../../../../plugins/cases/common/constants'; import { getConfiguration, removeServerGeneratedPropertiesFromConfigure, getConfigurationOutput, deleteConfiguration, -} from '../../../common/lib/utils'; +} from '../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/get_connectors.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/get_connectors.ts new file mode 100644 index 00000000000000..1b6cf2ad56c593 --- /dev/null +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/get_connectors.ts @@ -0,0 +1,34 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +import { CASE_CONFIGURE_CONNECTORS_URL } from '../../../../../../plugins/cases/common/constants'; +import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const actionsRemover = new ActionsRemover(supertest); + + describe('get_connectors', () => { + afterEach(async () => { + await actionsRemover.removeAll(); + }); + + it('should return an empty find body correctly if no connectors are loaded', async () => { + const { body } = await supertest + .get(`${CASE_CONFIGURE_CONNECTORS_URL}/_find`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body).to.eql([]); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/configure/migrations.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/migrations.ts similarity index 87% rename from x-pack/test/case_api_integration/security_and_spaces/tests/configure/migrations.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/migrations.ts index 4ee2021399faee..fd9baf39b49f96 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/configure/migrations.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/migrations.ts @@ -6,8 +6,8 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { CASE_CONFIGURE_URL } from '../../../../../plugins/cases/common/constants'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { CASE_CONFIGURE_URL } from '../../../../../../plugins/cases/common/constants'; // eslint-disable-next-line import/no-default-export export default function createGetTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/configure/patch_configure.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/patch_configure.ts similarity index 94% rename from x-pack/test/case_api_integration/security_and_spaces/tests/configure/patch_configure.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/patch_configure.ts index ea4982a8f04ada..1e2ef74479ffd9 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/configure/patch_configure.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/patch_configure.ts @@ -6,15 +6,15 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { CASE_CONFIGURE_URL } from '../../../../../plugins/cases/common/constants'; +import { CASE_CONFIGURE_URL } from '../../../../../../plugins/cases/common/constants'; import { getConfiguration, removeServerGeneratedPropertiesFromConfigure, getConfigurationOutput, deleteConfiguration, -} from '../../../common/lib/utils'; +} from '../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/configure/post_configure.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/post_configure.ts similarity index 92% rename from x-pack/test/case_api_integration/security_and_spaces/tests/configure/post_configure.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/post_configure.ts index 7ab98a07cf0462..9d0fad202a5179 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/configure/post_configure.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/post_configure.ts @@ -6,15 +6,15 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { CASE_CONFIGURE_URL } from '../../../../../plugins/cases/common/constants'; +import { CASE_CONFIGURE_URL } from '../../../../../../plugins/cases/common/constants'; import { getConfiguration, removeServerGeneratedPropertiesFromConfigure, getConfigurationOutput, deleteConfiguration, -} from '../../../common/lib/utils'; +} from '../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/connectors/case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/connectors/case.ts similarity index 99% rename from x-pack/test/case_api_integration/security_and_spaces/tests/connectors/case.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/connectors/case.ts index ee4d671f7880f5..9ba8958d6532f5 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/connectors/case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/connectors/case.ts @@ -7,16 +7,16 @@ import { omit } from 'lodash/fp'; 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 { CommentType } from '../../../../../plugins/cases/common/api'; +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { CommentType } from '../../../../../../plugins/cases/common/api'; import { postCaseReq, postCaseResp, removeServerGeneratedPropertiesFromCase, removeServerGeneratedPropertiesFromComments, -} from '../../../common/lib/mock'; +} from '../../../../common/lib/mock'; import { createRule, createSignalsIndex, @@ -26,7 +26,7 @@ import { getSignalsByIds, waitForRuleSuccessOrStatus, waitForSignalsToBePresent, -} from '../../../../detection_engine_api_integration/utils'; +} from '../../../../../detection_engine_api_integration/utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts similarity index 89% rename from x-pack/test/case_api_integration/security_and_spaces/tests/index.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts index 321c19558d522e..ba5a865b35778d 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts @@ -5,14 +5,11 @@ * 2.0. */ -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default ({ loadTestFile }: FtrProviderContext): void => { - describe('cases security and spaces enabled', function () { - // Fastest ciGroup for the moment. - this.tags('ciGroup5'); - + describe('Common', function () { loadTestFile(require.resolve('./cases/comments/delete_comment')); loadTestFile(require.resolve('./cases/comments/find_comments')); loadTestFile(require.resolve('./cases/comments/get_comment')); @@ -24,7 +21,6 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./cases/get_case')); loadTestFile(require.resolve('./cases/patch_cases')); loadTestFile(require.resolve('./cases/post_case')); - loadTestFile(require.resolve('./cases/push_case')); loadTestFile(require.resolve('./cases/reporters/get_reporters')); loadTestFile(require.resolve('./cases/status/get_status')); loadTestFile(require.resolve('./cases/tags/get_tags')); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/push_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts similarity index 95% rename from x-pack/test/case_api_integration/security_and_spaces/tests/cases/push_case.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts index 2db15eb603f7c7..f7d908320e7ad8 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/cases/push_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts @@ -6,16 +6,16 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { ObjectRemover as ActionsRemover } from '../../../../alerting_api_integration/common/lib'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; -import { CASE_CONFIGURE_URL, CASES_URL } from '../../../../../plugins/cases/common/constants'; +import { CASE_CONFIGURE_URL, CASES_URL } from '../../../../../../plugins/cases/common/constants'; import { postCaseReq, defaultUser, postCommentUserReq, postCollectionReq, -} from '../../../common/lib/mock'; +} from '../../../../common/lib/mock'; import { deleteCases, deleteCasesUserActions, @@ -23,12 +23,12 @@ import { deleteConfiguration, getConfiguration, getServiceNowConnector, -} from '../../../common/lib/utils'; +} from '../../../../common/lib/utils'; import { ExternalServiceSimulator, getExternalServiceSimulatorPath, -} from '../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; -import { CaseStatuses } from '../../../../../plugins/cases/common/api'; +} from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; +import { CaseStatuses } from '../../../../../../plugins/cases/common/api'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/user_actions/get_all_user_actions.ts new file mode 100644 index 00000000000000..0b66200a3fab09 --- /dev/null +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/user_actions/get_all_user_actions.ts @@ -0,0 +1,115 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; + +import { CASE_CONFIGURE_URL, CASES_URL } from '../../../../../../../plugins/cases/common/constants'; +import { defaultUser, postCaseReq } from '../../../../../common/lib/mock'; +import { + deleteCases, + deleteCasesUserActions, + deleteComments, + deleteConfiguration, + getConfiguration, + getServiceNowConnector, +} from '../../../../../common/lib/utils'; + +import { ObjectRemover as ActionsRemover } from '../../../../../../alerting_api_integration/common/lib'; +import { + ExternalServiceSimulator, + getExternalServiceSimulatorPath, +} from '../../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const actionsRemover = new ActionsRemover(supertest); + const kibanaServer = getService('kibanaServer'); + + describe('get_all_user_actions', () => { + let servicenowSimulatorURL: string = ''; + before(() => { + servicenowSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) + ); + }); + afterEach(async () => { + await deleteCases(es); + await deleteComments(es); + await deleteConfiguration(es); + await deleteCasesUserActions(es); + await actionsRemover.removeAll(); + }); + + it(`on new push to service, user action: 'push-to-service' should be called with actionFields: ['pushed']`, async () => { + const { body: connector } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'true') + .send({ + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }) + .expect(200); + + actionsRemover.add('default', connector.id, 'action', 'actions'); + + const { body: configure } = await supertest + .post(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send( + getConfiguration({ + id: connector.id, + name: connector.name, + type: connector.actionTypeId, + }) + ) + .expect(200); + + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + ...postCaseReq, + connector: getConfiguration({ + id: connector.id, + name: connector.name, + type: connector.actionTypeId, + fields: { + urgency: '2', + impact: '2', + severity: '2', + category: 'software', + subcategory: 'os', + }, + }).connector, + }) + .expect(200); + + await supertest + .post(`${CASES_URL}/${postedCase.id}/connector/${connector.id}/_push`) + .set('kbn-xsrf', 'true') + .send({}) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${postedCase.id}/user_actions`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.length).to.eql(2); + expect(body[1].action_field).to.eql(['pushed']); + expect(body[1].action).to.eql('push-to-service'); + expect(body[1].old_value).to.eql(null); + const newValue = JSON.parse(body[1].new_value); + expect(newValue.connector_id).to.eql(configure.connector.id); + expect(newValue.pushed_by).to.eql(defaultUser); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/configure/get_connectors.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts similarity index 84% rename from x-pack/test/case_api_integration/security_and_spaces/tests/configure/get_connectors.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts index 1789fa719ec9f2..0b6c755c79b505 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/configure/get_connectors.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts @@ -6,15 +6,15 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { CASE_CONFIGURE_CONNECTORS_URL } from '../../../../../plugins/cases/common/constants'; -import { ObjectRemover as ActionsRemover } from '../../../../alerting_api_integration/common/lib'; +import { CASE_CONFIGURE_CONNECTORS_URL } from '../../../../../../plugins/cases/common/constants'; +import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; import { getServiceNowConnector, getJiraConnector, getResilientConnector, -} from '../../../common/lib/utils'; +} from '../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -26,16 +26,6 @@ export default ({ getService }: FtrProviderContext): void => { await actionsRemover.removeAll(); }); - it('should return an empty find body correctly if no connectors are loaded', async () => { - const { body } = await supertest - .get(`${CASE_CONFIGURE_CONNECTORS_URL}/_find`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(body).to.eql([]); - }); - it('should return the correct connectors', async () => { const { body: snConnector } = await supertest .post('/api/actions/action') diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts new file mode 100644 index 00000000000000..5e6be87b624019 --- /dev/null +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('cases security and spaces enabled: trial', function () { + // Fastest ciGroup for the moment. + this.tags('ciGroup5'); + + // Common + loadTestFile(require.resolve('../common')); + + // Trial + loadTestFile(require.resolve('./cases/push_case')); + }); +}; From f2a50d309d3c535661d632c2721c358308fd1baf Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 16 Mar 2021 13:12:54 +0200 Subject: [PATCH 023/113] Enable SIR on integration tests --- x-pack/test/case_api_integration/common/config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/test/case_api_integration/common/config.ts b/x-pack/test/case_api_integration/common/config.ts index ab12154652dc11..fe663cfa8dc073 100644 --- a/x-pack/test/case_api_integration/common/config.ts +++ b/x-pack/test/case_api_integration/common/config.ts @@ -29,6 +29,7 @@ const enabledActionTypes = [ '.resilient', '.server-log', '.servicenow', + '.servicenow-sir', '.slack', '.webhook', '.case', From 00d89ca97933819e972cd7cf8a02424697b534b5 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Tue, 16 Mar 2021 17:41:41 -0400 Subject: [PATCH 024/113] Starting the plumbing for authorization in plugin --- .../server/authorization/authorization.ts | 4 +- .../cases/server/authorization/types.ts | 11 ++ x-pack/plugins/cases/server/client/client.ts | 8 +- x-pack/plugins/cases/server/client/factory.ts | 108 ++++++++++++++++++ x-pack/plugins/cases/server/client/index.ts | 6 +- x-pack/plugins/cases/server/client/types.ts | 4 +- .../server/connectors/case/index.test.ts | 27 ++++- .../cases/server/connectors/case/index.ts | 41 ++----- .../plugins/cases/server/connectors/index.ts | 12 +- .../plugins/cases/server/connectors/types.ts | 14 +-- x-pack/plugins/cases/server/plugin.ts | 100 ++++++++-------- .../routes/api/__fixtures__/route_contexts.ts | 21 +++- .../api/cases/comments/delete_comment.ts | 2 +- .../api/cases/configure/get_configure.test.ts | 9 +- .../api/cases/configure/get_configure.ts | 2 +- .../cases/configure/patch_configure.test.ts | 9 +- .../api/cases/configure/patch_configure.ts | 2 +- .../cases/configure/post_configure.test.ts | 9 +- .../api/cases/configure/post_configure.ts | 2 +- .../cases/server/routes/api/cases/get_case.ts | 2 +- .../server/routes/api/cases/patch_cases.ts | 2 +- .../server/routes/api/cases/post_case.ts | 2 +- .../server/routes/api/cases/push_case.test.ts | 2 +- .../server/routes/api/cases/push_case.ts | 2 +- .../api/cases/sub_case/patch_sub_cases.ts | 2 +- .../user_actions/get_all_user_actions.ts | 4 +- x-pack/plugins/cases/server/services/index.ts | 4 +- x-pack/plugins/cases/server/types.ts | 2 +- 28 files changed, 260 insertions(+), 153 deletions(-) create mode 100644 x-pack/plugins/cases/server/authorization/types.ts create mode 100644 x-pack/plugins/cases/server/client/factory.ts diff --git a/x-pack/plugins/cases/server/authorization/authorization.ts b/x-pack/plugins/cases/server/authorization/authorization.ts index 3281672ffb921a..b9f2a927b90990 100644 --- a/x-pack/plugins/cases/server/authorization/authorization.ts +++ b/x-pack/plugins/cases/server/authorization/authorization.ts @@ -7,9 +7,9 @@ import { KibanaRequest } from 'kibana/server'; import Boom from '@hapi/boom'; -import { Space } from '../../../spaces/server'; import { SecurityPluginStart } from '../../../security/server'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; +import { GetSpaceFn } from './types'; // TODO: probably should move these to the types.ts file // TODO: Larry would prefer if we have an operation per entity route so I think we need to create a bunch like @@ -25,8 +25,6 @@ export enum WriteOperations { Update = 'update', } -type GetSpaceFn = (request: KibanaRequest) => Promise; - /** * This class handles ensuring that the user making a request has the correct permissions * for the API request. diff --git a/x-pack/plugins/cases/server/authorization/types.ts b/x-pack/plugins/cases/server/authorization/types.ts new file mode 100644 index 00000000000000..bcdd0f55650e02 --- /dev/null +++ b/x-pack/plugins/cases/server/authorization/types.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { KibanaRequest } from 'kibana/server'; +import { Space } from '../../../spaces/server'; + +export type GetSpaceFn = (request: KibanaRequest) => Promise; diff --git a/x-pack/plugins/cases/server/client/client.ts b/x-pack/plugins/cases/server/client/client.ts index 8f9058654d6fd6..6cd84be9afb5a0 100644 --- a/x-pack/plugins/cases/server/client/client.ts +++ b/x-pack/plugins/cases/server/client/client.ts @@ -7,7 +7,7 @@ import { ElasticsearchClient, SavedObjectsClientContract, Logger } from 'src/core/server'; import { - CasesClientFactoryArguments, + CasesClientConstructorArguments, CasesClient, ConfigureFields, MappingsClient, @@ -37,6 +37,7 @@ import { get as getUserActions } from './user_actions/get'; import { get as getAlerts } from './alerts/get'; import { push } from './cases/push'; import { createCaseError } from '../common/error'; +import { Authorization } from '../authorization/authorization'; /** * This class is a pass through for common case functionality (like creating, get a case). @@ -51,8 +52,9 @@ export class CasesClientHandler implements CasesClient { private readonly _userActionService: CaseUserActionServiceSetup; private readonly _alertsService: AlertServiceContract; private readonly logger: Logger; + private readonly authorization: Authorization; - constructor(clientArgs: CasesClientFactoryArguments) { + constructor(clientArgs: CasesClientConstructorArguments) { this._scopedClusterClient = clientArgs.scopedClusterClient; this._caseConfigureService = clientArgs.caseConfigureService; this._caseService = clientArgs.caseService; @@ -62,10 +64,12 @@ export class CasesClientHandler implements CasesClient { this._userActionService = clientArgs.userActionService; this._alertsService = clientArgs.alertsService; this.logger = clientArgs.logger; + this.authorization = clientArgs.authorization; } public async create(caseInfo: CasePostRequest) { try { + // TODO: authorize the user return create({ savedObjectsClient: this._savedObjectsClient, caseService: this._caseService, diff --git a/x-pack/plugins/cases/server/client/factory.ts b/x-pack/plugins/cases/server/client/factory.ts new file mode 100644 index 00000000000000..2cd99981968797 --- /dev/null +++ b/x-pack/plugins/cases/server/client/factory.ts @@ -0,0 +1,108 @@ +/* + * 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 { + KibanaRequest, + SavedObjectsServiceStart, + Logger, + ElasticsearchClient, +} from 'kibana/server'; +import { SecurityPluginSetup, SecurityPluginStart } from '../../../security/server'; +import { SAVED_OBJECT_TYPES } from '../../common/constants'; +import { Authorization } from '../authorization/authorization'; +import { GetSpaceFn } from '../authorization/types'; +import { + AlertServiceContract, + CaseConfigureServiceSetup, + CaseServiceSetup, + CaseUserActionServiceSetup, + ConnectorMappingsServiceSetup, +} from '../services'; +import { CasesClientHandler } from './client'; +import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; + +interface CasesClientFactoryArgs { + caseConfigureService: CaseConfigureServiceSetup; + caseService: CaseServiceSetup; + connectorMappingsService: ConnectorMappingsServiceSetup; + userActionService: CaseUserActionServiceSetup; + alertsService: AlertServiceContract; + securityPluginSetup?: SecurityPluginSetup; + securityPluginStart?: SecurityPluginStart; + getSpace: GetSpaceFn; + featuresPluginStart: FeaturesPluginStart; + isAuthEnabled: boolean; +} + +/** + * This class handles the logic for creating a CasesClientHandler. We need this because some of the member variables + * can't be initialized until a plugin's start() method but we need to register the case context in the setup() method. + */ +export class CasesClientFactory { + private isInitialized = false; + private readonly logger: Logger; + private options?: CasesClientFactoryArgs; + + constructor(logger: Logger) { + this.logger = logger; + } + + public initialize(options: CasesClientFactoryArgs) { + if (this.isInitialized) { + throw new Error('CasesClientFactory already initialized'); + } + this.isInitialized = true; + this.options = options; + } + + public async create({ + request, + scopedClusterClient, + savedObjectsService, + }: { + // TODO: make these required when the case connector can get a request and savedObjectsService + request?: KibanaRequest; + savedObjectsService?: SavedObjectsServiceStart; + scopedClusterClient: ElasticsearchClient; + }): Promise { + if (!this.options) { + throw new Error('CasesClientFactory must be initialized before calling create'); + } + + // TODO: remove this + if (!request || !savedObjectsService) { + throw new Error( + 'CasesClientFactory must be initialized with a request and saved object service' + ); + } + + const auth = await Authorization.create({ + request, + securityAuth: this.options.securityPluginStart?.authz, + getSpace: this.options.getSpace, + features: this.options.featuresPluginStart, + isAuthEnabled: this.options.isAuthEnabled, + }); + + const user = this.options.caseService.getUser({ request }); + + return new CasesClientHandler({ + alertsService: this.options.alertsService, + scopedClusterClient, + savedObjectsClient: savedObjectsService.getScopedClient(request, { + includedHiddenTypes: SAVED_OBJECT_TYPES, + }), + user, + caseService: this.options.caseService, + caseConfigureService: this.options.caseConfigureService, + connectorMappingsService: this.options.connectorMappingsService, + userActionService: this.options.userActionService, + logger: this.logger, + authorization: auth, + }); + } +} diff --git a/x-pack/plugins/cases/server/client/index.ts b/x-pack/plugins/cases/server/client/index.ts index fd7cae0edd2ead..39c7f6f98c2595 100644 --- a/x-pack/plugins/cases/server/client/index.ts +++ b/x-pack/plugins/cases/server/client/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CasesClientFactoryArguments, CasesClient } from './types'; +import { CasesClientConstructorArguments, CasesClient } from './types'; import { CasesClientHandler } from './client'; export { CasesClientHandler } from './client'; @@ -14,7 +14,9 @@ export { CasesClient } from './types'; /** * Create a CasesClientHandler to external services (other plugins). */ -export const createExternalCasesClient = (clientArgs: CasesClientFactoryArguments): CasesClient => { +export const createExternalCasesClient = ( + clientArgs: CasesClientConstructorArguments +): CasesClient => { const client = new CasesClientHandler(clientArgs); return client; }; diff --git a/x-pack/plugins/cases/server/client/types.ts b/x-pack/plugins/cases/server/client/types.ts index c62b3913da7639..51c0825b760be6 100644 --- a/x-pack/plugins/cases/server/client/types.ts +++ b/x-pack/plugins/cases/server/client/types.ts @@ -19,6 +19,7 @@ import { CaseUserActionsResponse, User, } from '../../common/api'; +import { Authorization } from '../authorization/authorization'; import { AlertInfo } from '../common'; import { CaseConfigureServiceSetup, @@ -65,7 +66,7 @@ export interface MappingsClient { connectorType: string; } -export interface CasesClientFactoryArguments { +export interface CasesClientConstructorArguments { scopedClusterClient: ElasticsearchClient; caseConfigureService: CaseConfigureServiceSetup; caseService: CaseServiceSetup; @@ -75,6 +76,7 @@ export interface CasesClientFactoryArguments { userActionService: CaseUserActionServiceSetup; alertsService: AlertServiceContract; logger: Logger; + authorization: Authorization; } export interface ConfigureFields { diff --git a/x-pack/plugins/cases/server/connectors/case/index.test.ts b/x-pack/plugins/cases/server/connectors/case/index.test.ts index 122f6bd77c6936..5824eef7308f66 100644 --- a/x-pack/plugins/cases/server/connectors/case/index.test.ts +++ b/x-pack/plugins/cases/server/connectors/case/index.test.ts @@ -6,7 +6,7 @@ */ import { omit } from 'lodash/fp'; -import { Logger } from '../../../../../../src/core/server'; +import { KibanaRequest, Logger } from '../../../../../../src/core/server'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { actionsMock } from '../../../../actions/server/mocks'; import { validateParams } from '../../../../actions/server/lib'; @@ -29,6 +29,9 @@ import { import { CaseActionType, CaseActionTypeExecutorOptions, CaseExecutorParams } from './types'; import { getActionType } from '.'; import { createExternalCasesClientMock } from '../../client/mocks'; +import { CasesClientFactory } from '../../client/factory'; +import { featuresPluginMock } from '../../../../features/server/mocks'; +import { securityMock } from '../../../../security/server/mocks'; const mockCasesClient = createExternalCasesClientMock(); @@ -48,13 +51,24 @@ describe('case connector', () => { const connectorMappingsService = connectorMappingsServiceMock(); const userActionService = createUserActionServiceMock(); const alertsService = createAlertServiceMock(); - caseActionType = getActionType({ - logger, - caseService, + const factory = new CasesClientFactory(logger); + + factory.initialize({ + alertsService, caseConfigureService, + caseService, connectorMappingsService, userActionService, - alertsService, + featuresPluginStart: featuresPluginMock.createStart(), + getSpace: async (req: KibanaRequest) => undefined, + isAuthEnabled: true, + securityPluginSetup: securityMock.createSetup(), + securityPluginStart: securityMock.createStart(), + }); + + caseActionType = getActionType({ + logger, + factory, }); }); @@ -822,7 +836,8 @@ describe('case connector', () => { }); }); - describe('execute', () => { + // TODO: enable these when the actions framework provides a request and a saved objects service + describe.skip('execute', () => { it('allows only supported sub-actions', async () => { expect.assertions(2); const actionId = 'some-id'; diff --git a/x-pack/plugins/cases/server/connectors/case/index.ts b/x-pack/plugins/cases/server/connectors/case/index.ts index da993faf0ef5ca..f21cb1ee0e79bf 100644 --- a/x-pack/plugins/cases/server/connectors/case/index.ts +++ b/x-pack/plugins/cases/server/connectors/case/index.ts @@ -14,7 +14,6 @@ import { CommentRequest, CommentType, } from '../../../common/api'; -import { createExternalCasesClient } from '../../client'; import { CaseExecutorParamsSchema, CaseConfigurationSchema, CommentSchemaType } from './schema'; import { CaseExecutorResponse, @@ -25,20 +24,12 @@ import { import * as i18n from './translations'; import { GetActionTypeParams, isCommentGeneratedAlert, separator } from '..'; -import { nullUser } from '../../common'; import { createCaseError } from '../../common/error'; const supportedSubActions: string[] = ['create', 'update', 'addComment']; // action type definition -export function getActionType({ - logger, - caseService, - caseConfigureService, - connectorMappingsService, - userActionService, - alertsService, -}: GetActionTypeParams): CaseActionType { +export function getActionType({ logger, factory }: GetActionTypeParams): CaseActionType { return { id: '.case', minimumLicenseRequired: 'basic', @@ -48,44 +39,26 @@ export function getActionType({ params: CaseExecutorParamsSchema, }, executor: curry(executor)({ - alertsService, - caseConfigureService, - caseService, - connectorMappingsService, + factory, logger, - userActionService, }), }; } // action executor async function executor( - { - alertsService, - caseConfigureService, - caseService, - connectorMappingsService, - logger, - userActionService, - }: GetActionTypeParams, + { logger, factory }: GetActionTypeParams, execOptions: CaseActionTypeExecutorOptions ): Promise> { const { actionId, params, services } = execOptions; const { subAction, subActionParams } = params; let data: CaseExecutorResponse | null = null; - const { savedObjectsClient, scopedClusterClient } = services; - const casesClient = createExternalCasesClient({ - savedObjectsClient, + const { scopedClusterClient } = services; + const casesClient = await factory.create({ + request: undefined, + savedObjectsService: undefined, scopedClusterClient, - // we might want the user information to be passed as part of the action request - user: nullUser, - caseService, - caseConfigureService, - connectorMappingsService, - userActionService, - alertsService, - logger, }); if (!supportedSubActions.includes(subAction)) { diff --git a/x-pack/plugins/cases/server/connectors/index.ts b/x-pack/plugins/cases/server/connectors/index.ts index e55850fb6c02c2..b5b73f26635182 100644 --- a/x-pack/plugins/cases/server/connectors/index.ts +++ b/x-pack/plugins/cases/server/connectors/index.ts @@ -31,20 +31,12 @@ export const separator = '__SEPARATOR__'; export const registerConnectors = ({ registerActionType, logger, - caseService, - caseConfigureService, - connectorMappingsService, - userActionService, - alertsService, + factory, }: RegisterConnectorsArgs) => { registerActionType( getCaseConnector({ logger, - caseService, - caseConfigureService, - connectorMappingsService, - userActionService, - alertsService, + factory, }) ); }; diff --git a/x-pack/plugins/cases/server/connectors/types.ts b/x-pack/plugins/cases/server/connectors/types.ts index f81a5d5b04a473..98cbe9683546b6 100644 --- a/x-pack/plugins/cases/server/connectors/types.ts +++ b/x-pack/plugins/cases/server/connectors/types.ts @@ -8,13 +8,7 @@ import { Logger } from 'kibana/server'; import { CaseResponse, ConnectorTypes } from '../../common/api'; import { CasesClientGetAlertsResponse } from '../client/alerts/types'; -import { - CaseServiceSetup, - CaseConfigureServiceSetup, - CaseUserActionServiceSetup, - ConnectorMappingsServiceSetup, - AlertServiceContract, -} from '../services'; +import { CasesClientFactory } from '../client/factory'; import { RegisterActionType } from '../types'; export { @@ -25,11 +19,7 @@ export { export interface GetActionTypeParams { logger: Logger; - caseService: CaseServiceSetup; - caseConfigureService: CaseConfigureServiceSetup; - connectorMappingsService: ConnectorMappingsServiceSetup; - userActionService: CaseUserActionServiceSetup; - alertsService: AlertServiceContract; + factory: CasesClientFactory; } export interface RegisterConnectorsArgs extends GetActionTypeParams { diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index 5ed4ef1c9d218d..a8f1e0fddd62fa 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -8,9 +8,9 @@ import { IContextProvider, KibanaRequest, Logger, PluginInitializerContext } from 'kibana/server'; import { CoreSetup, CoreStart } from 'src/core/server'; -import { SecurityPluginSetup } from '../../security/server'; +import { SecurityPluginSetup, SecurityPluginStart } from '../../security/server'; import { PluginSetupContract as ActionsPluginSetup } from '../../actions/server'; -import { APP_ID, SAVED_OBJECT_TYPES } from '../common/constants'; +import { APP_ID } from '../common/constants'; import { ConfigType } from './config'; import { initCaseApi } from './routes/api'; @@ -32,11 +32,13 @@ import { ConnectorMappingsService, ConnectorMappingsServiceSetup, AlertService, - AlertServiceContract, } from './services'; -import { CasesClientHandler, createExternalCasesClient } from './client'; +import { CasesClient } from './client'; import { registerConnectors } from './connectors'; import type { CasesRequestHandlerContext } from './types'; +import { CasesClientFactory } from './client/factory'; +import { SpacesPluginStart } from '../../spaces/server'; +import { PluginStartContract as FeaturesPluginStart } from '../../features/server'; function createConfig(context: PluginInitializerContext) { return context.config.get(); @@ -47,6 +49,12 @@ export interface PluginsSetup { actions: ActionsPluginSetup; } +export interface PluginsStart { + security: SecurityPluginStart; + features: FeaturesPluginStart; + spaces?: SpacesPluginStart; +} + export class CasePlugin { private readonly log: Logger; private caseConfigureService?: CaseConfigureServiceSetup; @@ -54,9 +62,13 @@ export class CasePlugin { private connectorMappingsService?: ConnectorMappingsServiceSetup; private userActionService?: CaseUserActionServiceSetup; private alertsService?: AlertService; + private clientFactory: CasesClientFactory; + private securityPluginSetup?: SecurityPluginSetup; + private config?: ConfigType; constructor(private readonly initializerContext: PluginInitializerContext) { this.log = this.initializerContext.logger.get(); + this.clientFactory = new CasesClientFactory(this.log); } public async setup(core: CoreSetup, plugins: PluginsSetup) { @@ -66,6 +78,10 @@ export class CasePlugin { return; } + // save instance variables for the client factor initialization call + this.config = config; + this.securityPluginSetup = plugins.security; + core.savedObjects.registerType(caseCommentSavedObjectType); core.savedObjects.registerType(caseConfigureSavedObjectType); core.savedObjects.registerType(caseConnectorMappingsSavedObjectType); @@ -92,12 +108,6 @@ export class CasePlugin { APP_ID, this.createRouteHandlerContext({ core, - caseService: this.caseService, - caseConfigureService: this.caseConfigureService, - connectorMappingsService: this.connectorMappingsService, - userActionService: this.userActionService, - alertsService: this.alertsService, - logger: this.log, }) ); @@ -114,34 +124,37 @@ export class CasePlugin { registerConnectors({ registerActionType: plugins.actions.registerType, logger: this.log, - caseService: this.caseService, - caseConfigureService: this.caseConfigureService, - connectorMappingsService: this.connectorMappingsService, - userActionService: this.userActionService, - alertsService: this.alertsService, + factory: this.clientFactory, }); } - public start(core: CoreStart) { + public start(core: CoreStart, plugins: PluginsStart) { this.log.debug(`Starting Case Workflow`); + this.clientFactory.initialize({ + alertsService: this.alertsService!, + caseConfigureService: this.caseConfigureService!, + caseService: this.caseService!, + connectorMappingsService: this.connectorMappingsService!, + userActionService: this.userActionService!, + securityPluginSetup: this.securityPluginSetup, + securityPluginStart: plugins.security, + getSpace: async (request: KibanaRequest) => { + return plugins.spaces?.spacesService.getActiveSpace(request); + }, + featuresPluginStart: plugins.features, + // we'll be removing this eventually but let's just default it to false if it wasn't specified explicitly in the config file + isAuthEnabled: this.config?.enabled ?? false, + }); + const getCasesClientWithRequestAndContext = async ( context: CasesRequestHandlerContext, request: KibanaRequest - ) => { - const user = await this.caseService!.getUser({ request }); - return createExternalCasesClient({ + ): Promise => { + return this.clientFactory.create({ + request, scopedClusterClient: context.core.elasticsearch.client.asCurrentUser, - savedObjectsClient: core.savedObjects.getScopedClient(request, { - includedHiddenTypes: SAVED_OBJECT_TYPES, - }), - user, - caseService: this.caseService!, - caseConfigureService: this.caseConfigureService!, - connectorMappingsService: this.connectorMappingsService!, - userActionService: this.userActionService!, - alertsService: this.alertsService!, - logger: this.log, + savedObjectsService: core.savedObjects, }); }; @@ -156,38 +169,17 @@ export class CasePlugin { private createRouteHandlerContext = ({ core, - caseService, - caseConfigureService, - connectorMappingsService, - userActionService, - alertsService, - logger, }: { core: CoreSetup; - caseService: CaseServiceSetup; - caseConfigureService: CaseConfigureServiceSetup; - connectorMappingsService: ConnectorMappingsServiceSetup; - userActionService: CaseUserActionServiceSetup; - alertsService: AlertServiceContract; - logger: Logger; }): IContextProvider => { return async (context, request, response) => { const [{ savedObjects }] = await core.getStartServices(); - const user = await caseService.getUser({ request }); return { - getCasesClient: () => { - return new CasesClientHandler({ + getCasesClient: async () => { + return this.clientFactory.create({ + request, scopedClusterClient: context.core.elasticsearch.client.asCurrentUser, - savedObjectsClient: savedObjects.getScopedClient(request, { - includedHiddenTypes: SAVED_OBJECT_TYPES, - }), - caseService, - caseConfigureService, - connectorMappingsService, - userActionService, - alertsService, - user, - logger, + savedObjectsService: savedObjects, }); }, }; diff --git a/x-pack/plugins/cases/server/routes/api/__fixtures__/route_contexts.ts b/x-pack/plugins/cases/server/routes/api/__fixtures__/route_contexts.ts index 42e8561c2ac54a..433eca0f41350e 100644 --- a/x-pack/plugins/cases/server/routes/api/__fixtures__/route_contexts.ts +++ b/x-pack/plugins/cases/server/routes/api/__fixtures__/route_contexts.ts @@ -6,6 +6,7 @@ */ import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks'; +import { KibanaRequest } from 'kibana/server'; import { createExternalCasesClient } from '../../../client'; import { AlertService, @@ -17,6 +18,9 @@ import { import { authenticationMock } from '../__fixtures__'; import type { CasesRequestHandlerContext } from '../../../types'; import { createActionsClient } from './mock_actions_client'; +import { featuresPluginMock } from '../../../../../features/server/mocks'; +import { securityMock } from '../../../../../security/server/mocks'; +import { CasesClientFactory } from '../../../client/factory'; export const createRouteContext = async (client: any, badAuth = false) => { const actionsMock = createActionsClient(); @@ -31,9 +35,23 @@ export const createRouteContext = async (client: any, badAuth = false) => { const connectorMappingsServicePlugin = new ConnectorMappingsService(log); const caseUserActionsServicePlugin = new CaseUserActionService(log); + const connectorMappingsService = await connectorMappingsServicePlugin.setup(); const caseConfigureService = await caseConfigureServicePlugin.setup(); const userActionService = await caseUserActionsServicePlugin.setup(); const alertsService = new AlertService(); + const factory = new CasesClientFactory(log); + factory.initialize({ + alertsService, + caseConfigureService, + caseService, + connectorMappingsService, + userActionService, + featuresPluginStart: featuresPluginMock.createStart(), + getSpace: async (req: KibanaRequest) => undefined, + isAuthEnabled: true, + securityPluginSetup: securityMock.createSetup(), + securityPluginStart: securityMock.createStart(), + }); const context = ({ core: { @@ -43,11 +61,10 @@ export const createRouteContext = async (client: any, badAuth = false) => { }, actions: { getActionsClient: () => actionsMock }, cases: { - getCasesClient: () => casesClient, + getCasesClient: async () => casesClient, }, } as unknown) as CasesRequestHandlerContext; - const connectorMappingsService = await connectorMappingsServicePlugin.setup(); const casesClient = createExternalCasesClient({ savedObjectsClient: client, user: authc.getCurrentUser(), diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.ts index 6b124c76f8d43d..f953ac6c596f6c 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.ts @@ -45,7 +45,7 @@ export function initDeleteCommentApi({ includedHiddenTypes: SAVED_OBJECT_TYPES, }); // eslint-disable-next-line @typescript-eslint/naming-convention - const { username, full_name, email } = await caseService.getUser({ request }); + const { username, full_name, email } = caseService.getUser({ request }); const deleteDate = new Date().toISOString(); const myComment = await caseService.getComment({ diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.test.ts b/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.test.ts index f328844acfd00f..0735671384845b 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.test.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.test.ts @@ -143,13 +143,14 @@ describe('GET configuration', () => { ...context, cases: { ...context.cases, - getCasesClient: () => - ({ - ...context?.cases?.getCasesClient(), + getCasesClient: async () => { + return { + ...(await context?.cases?.getCasesClient()), getMappings: () => { throw new Error(); }, - } as CasesClient), + } as CasesClient; + }, }, }; diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.ts b/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.ts index f9807c2356e042..663595b60b8bac 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.ts @@ -34,7 +34,7 @@ export function initGetCaseConfigure({ caseConfigureService, router, logger }: R if (!context.cases) { throw Boom.badRequest('RouteHandlerContext is not registered for cases'); } - const casesClient = context.cases.getCasesClient(); + const casesClient = await context.cases.getCasesClient(); const actionsClient = context.actions?.getActionsClient(); if (actionsClient == null) { throw Boom.notFound('Action client not found'); diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.test.ts b/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.test.ts index 48d88e0f622f59..a131061f2ba86d 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.test.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.test.ts @@ -163,13 +163,14 @@ describe('PATCH configuration', () => { ...context, cases: { ...context.cases, - getCasesClient: () => - ({ - ...context?.cases?.getCasesClient(), + getCasesClient: async () => { + return { + ...(await context?.cases?.getCasesClient()), getMappings: () => { throw new Error(); }, - } as CasesClient), + } as CasesClient; + }, }, }; diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.ts b/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.ts index aca312dca780b7..ed3c2e98d25798 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.ts @@ -72,7 +72,7 @@ export function initPatchCaseConfigure({ if (!context.cases) { throw Boom.badRequest('RouteHandlerContext is not registered for cases'); } - const casesClient = context.cases.getCasesClient(); + const casesClient = await context.cases.getCasesClient(); const actionsClient = context.actions?.getActionsClient(); if (actionsClient == null) { throw Boom.notFound('Action client have not been found'); diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.test.ts b/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.test.ts index 882a10742d7338..db0488d87dc5cb 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.test.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.test.ts @@ -83,13 +83,14 @@ describe('POST configuration', () => { ...context, cases: { ...context.cases, - getCasesClient: () => - ({ - ...context?.cases?.getCasesClient(), + getCasesClient: async () => { + return { + ...(await context?.cases?.getCasesClient()), getMappings: () => { throw new Error(); }, - } as CasesClient), + } as CasesClient; + }, }, }; diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.ts b/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.ts index c4c1b258bed302..d8e6b2a8ecf75d 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.ts @@ -43,7 +43,7 @@ export function initPostCaseConfigure({ if (!context.cases) { throw Boom.badRequest('RouteHandlerContext is not registered for cases'); } - const casesClient = context.cases.getCasesClient(); + const casesClient = await context.cases.getCasesClient(); const actionsClient = context.actions?.getActionsClient(); if (actionsClient == null) { throw Boom.notFound('Action client not found'); diff --git a/x-pack/plugins/cases/server/routes/api/cases/get_case.ts b/x-pack/plugins/cases/server/routes/api/cases/get_case.ts index f464f7e47fe7a4..051870e892ea32 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/get_case.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/get_case.ts @@ -27,7 +27,7 @@ export function initGetCaseApi({ router, logger }: RouteDeps) { }, async (context, request, response) => { try { - const casesClient = context.cases.getCasesClient(); + const casesClient = await context.cases.getCasesClient(); const id = request.params.case_id; return response.ok({ diff --git a/x-pack/plugins/cases/server/routes/api/cases/patch_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/patch_cases.ts index 8e779087bcafe3..5c417a3d98b938 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/patch_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/patch_cases.ts @@ -24,7 +24,7 @@ export function initPatchCasesApi({ router, logger }: RouteDeps) { return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); } - const casesClient = context.cases.getCasesClient(); + const casesClient = await context.cases.getCasesClient(); const cases = request.body as CasesPatchRequest; return response.ok({ diff --git a/x-pack/plugins/cases/server/routes/api/cases/post_case.ts b/x-pack/plugins/cases/server/routes/api/cases/post_case.ts index e2d71c58373537..d5f38c76fae3f4 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/post_case.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/post_case.ts @@ -24,7 +24,7 @@ export function initPostCaseApi({ router, logger }: RouteDeps) { if (!context.cases) { return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); } - const casesClient = context.cases.getCasesClient(); + const casesClient = await context.cases.getCasesClient(); const theCase = request.body as CasePostRequest; return response.ok({ diff --git a/x-pack/plugins/cases/server/routes/api/cases/push_case.test.ts b/x-pack/plugins/cases/server/routes/api/cases/push_case.test.ts index fb0ba5e3b5d9a0..adac2c9f7ee382 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/push_case.test.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/push_case.test.ts @@ -126,7 +126,7 @@ describe('Push case', () => { }) ); - const casesClient = context.cases.getCasesClient(); + const casesClient = await context.cases.getCasesClient(); casesClient.getAlerts = jest.fn().mockResolvedValue([]); const response = await routeHandler(context, request, kibanaResponseFactory); diff --git a/x-pack/plugins/cases/server/routes/api/cases/push_case.ts b/x-pack/plugins/cases/server/routes/api/cases/push_case.ts index 7395758210cf45..02423943c05572 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/push_case.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/push_case.ts @@ -31,7 +31,7 @@ export function initPushCaseApi({ router, logger }: RouteDeps) { return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); } - const casesClient = context.cases.getCasesClient(); + const casesClient = await context.cases.getCasesClient(); const actionsClient = context.actions?.getActionsClient(); if (actionsClient == null) { 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/cases/sub_case/patch_sub_cases.ts index 1c9441e2faf283..3808cd3dc45ddc 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/sub_case/patch_sub_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/sub_case/patch_sub_cases.ts @@ -428,7 +428,7 @@ export function initPatchSubCasesApi({ }, async (context, request, response) => { try { - const casesClient = context.cases.getCasesClient(); + const casesClient = await context.cases.getCasesClient(); const subCases = request.body as SubCasesPatchRequest; return response.ok({ 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/cases/user_actions/get_all_user_actions.ts index b5c564648c185d..ce0b4636130d73 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/cases/user_actions/get_all_user_actions.ts @@ -27,7 +27,7 @@ export function initGetAllCaseUserActionsApi({ router, logger }: RouteDeps) { return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); } - const casesClient = context.cases.getCasesClient(); + const casesClient = await context.cases.getCasesClient(); const caseId = request.params.case_id; return response.ok({ @@ -60,7 +60,7 @@ export function initGetAllSubCaseUserActionsApi({ router, logger }: RouteDeps) { return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); } - const casesClient = context.cases.getCasesClient(); + const casesClient = await context.cases.getCasesClient(); const caseId = request.params.case_id; const subCaseId = request.params.sub_case_id; diff --git a/x-pack/plugins/cases/server/services/index.ts b/x-pack/plugins/cases/server/services/index.ts index ff84e405bd9cf8..456cf8cf83125a 100644 --- a/x-pack/plugins/cases/server/services/index.ts +++ b/x-pack/plugins/cases/server/services/index.ts @@ -219,7 +219,7 @@ export interface CaseServiceSetup { getComment(args: GetCommentArgs): Promise>; getTags(args: ClientArgs): Promise; getReporters(args: ClientArgs): Promise; - getUser(args: GetUserArgs): Promise; + getUser(args: GetUserArgs): AuthenticatedUser | User; postNewCase(args: PostCaseArgs): Promise>; postNewComment(args: PostCommentArgs): Promise>; patchCase(args: PatchCaseArgs): Promise>; @@ -996,7 +996,7 @@ export class CaseService implements CaseServiceSetup { } } - public async getUser({ request }: GetUserArgs) { + public getUser({ request }: GetUserArgs) { try { this.log.debug(`Attempting to authenticate a user`); if (this.authentication != null) { diff --git a/x-pack/plugins/cases/server/types.ts b/x-pack/plugins/cases/server/types.ts index f7969841889df4..db035b83960ef2 100644 --- a/x-pack/plugins/cases/server/types.ts +++ b/x-pack/plugins/cases/server/types.ts @@ -18,7 +18,7 @@ import { import { CasesClient } from './client'; export interface CaseRequestContext { - getCasesClient: () => CasesClient; + getCasesClient: () => Promise; } /** From 644a7ac710c4e03e9bd5298b519b5fed221b5d99 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Wed, 17 Mar 2021 12:04:09 -0400 Subject: [PATCH 025/113] Unit tests working --- x-pack/plugins/cases/server/client/factory.ts | 5 +- .../routes/api/__fixtures__/route_contexts.ts | 52 +++++++++++-------- .../routes/api/cases/comments/post_comment.ts | 2 +- 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/x-pack/plugins/cases/server/client/factory.ts b/x-pack/plugins/cases/server/client/factory.ts index 2cd99981968797..89ee0cdf78c75a 100644 --- a/x-pack/plugins/cases/server/client/factory.ts +++ b/x-pack/plugins/cases/server/client/factory.ts @@ -51,6 +51,9 @@ export class CasesClientFactory { this.logger = logger; } + /** + * This should be called by the plugin's start() method. + */ public initialize(options: CasesClientFactoryArgs) { if (this.isInitialized) { throw new Error('CasesClientFactory already initialized'); @@ -69,7 +72,7 @@ export class CasesClientFactory { savedObjectsService?: SavedObjectsServiceStart; scopedClusterClient: ElasticsearchClient; }): Promise { - if (!this.options) { + if (!this.isInitialized || !this.options) { throw new Error('CasesClientFactory must be initialized before calling create'); } diff --git a/x-pack/plugins/cases/server/routes/api/__fixtures__/route_contexts.ts b/x-pack/plugins/cases/server/routes/api/__fixtures__/route_contexts.ts index 433eca0f41350e..6fc2de3da62a92 100644 --- a/x-pack/plugins/cases/server/routes/api/__fixtures__/route_contexts.ts +++ b/x-pack/plugins/cases/server/routes/api/__fixtures__/route_contexts.ts @@ -5,9 +5,13 @@ * 2.0. */ -import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks'; +import { + elasticsearchServiceMock, + loggingSystemMock, + savedObjectsServiceMock, +} from 'src/core/server/mocks'; + import { KibanaRequest } from 'kibana/server'; -import { createExternalCasesClient } from '../../../client'; import { AlertService, CaseService, @@ -21,6 +25,7 @@ import { createActionsClient } from './mock_actions_client'; import { featuresPluginMock } from '../../../../../features/server/mocks'; import { securityMock } from '../../../../../security/server/mocks'; import { CasesClientFactory } from '../../../client/factory'; +import { xpackMocks } from '../../../../../../mocks'; export const createRouteContext = async (client: any, badAuth = false) => { const actionsMock = createActionsClient(); @@ -39,6 +44,18 @@ export const createRouteContext = async (client: any, badAuth = false) => { const caseConfigureService = await caseConfigureServicePlugin.setup(); const userActionService = await caseUserActionsServicePlugin.setup(); const alertsService = new AlertService(); + + // since the cases saved objects are hidden we need to use getScopedClient(), we'll just have it return the mock client + // that is passed in to createRouteContext + const savedObjectsService = savedObjectsServiceMock.createStartContract(); + savedObjectsService.getScopedClient.mockReturnValue(client); + + const contextMock = xpackMocks.createRequestHandlerContext(); + // The tests check the calls on the saved object client, so we need to make sure it is the same one returned by + // getScopedClient and .client + contextMock.core.savedObjects.getClient = jest.fn(() => client); + contextMock.core.savedObjects.client = client; + const factory = new CasesClientFactory(log); factory.initialize({ alertsService, @@ -48,34 +65,27 @@ export const createRouteContext = async (client: any, badAuth = false) => { userActionService, featuresPluginStart: featuresPluginMock.createStart(), getSpace: async (req: KibanaRequest) => undefined, - isAuthEnabled: true, + isAuthEnabled: false, securityPluginSetup: securityMock.createSetup(), securityPluginStart: securityMock.createStart(), }); + // create a single reference to the caseClient so we can mock its methods + const caseClient = factory.create({ + savedObjectsService, + // Since authorization is disabled for these unit tests we don't need any information from the request object + // so just pass in an empty one + request: {} as KibanaRequest, + scopedClusterClient: esClient, + }); + const context = ({ - core: { - savedObjects: { - client, - }, - }, + ...contextMock, actions: { getActionsClient: () => actionsMock }, cases: { - getCasesClient: async () => casesClient, + getCasesClient: async () => caseClient, }, } as unknown) as CasesRequestHandlerContext; - const casesClient = createExternalCasesClient({ - savedObjectsClient: client, - user: authc.getCurrentUser(), - caseService, - caseConfigureService, - connectorMappingsService, - userActionService, - alertsService, - scopedClusterClient: esClient, - logger: log, - }); - return { context, services: { userActionService } }; }; diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/post_comment.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/post_comment.ts index 110a16a610014c..059b70d23f3332 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/post_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/post_comment.ts @@ -32,7 +32,7 @@ export function initPostCommentApi({ router, logger }: RouteDeps) { return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); } - const casesClient = context.cases.getCasesClient(); + const casesClient = await context.cases.getCasesClient(); const caseId = request.query?.subCaseId ?? request.params.case_id; const comment = request.body as CommentRequest; From 12d6e2e18f7bb6c5fcc74d95415566bc890e0f34 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 17 Mar 2021 19:23:59 +0200 Subject: [PATCH 026/113] Move find route logic to case client --- .../plugins/cases/server/client/cases/find.ts | 91 +++++++++++++++++++ x-pack/plugins/cases/server/client/client.ts | 21 ++++- x-pack/plugins/cases/server/client/types.ts | 3 + .../server/routes/api/cases/find_cases.ts | 65 ++----------- 4 files changed, 123 insertions(+), 57 deletions(-) create mode 100644 x-pack/plugins/cases/server/client/cases/find.ts diff --git a/x-pack/plugins/cases/server/client/cases/find.ts b/x-pack/plugins/cases/server/client/cases/find.ts new file mode 100644 index 00000000000000..c96c7a43626b24 --- /dev/null +++ b/x-pack/plugins/cases/server/client/cases/find.ts @@ -0,0 +1,91 @@ +/* + * 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 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 { + CasesFindResponse, + CasesFindRequest, + CasesFindRequestRt, + throwErrors, + caseStatuses, + CasesFindResponseRt, +} from '../../../common'; +import { CaseServiceSetup } from '../../services'; +import { createCaseError } from '../../common/error'; +import { constructQueryOptions } from '../../routes/api/cases/helpers'; +import { transformCases } from '../../routes/api/utils'; + +interface FindParams { + savedObjectsClient: SavedObjectsClientContract; + caseService: CaseServiceSetup; + logger: Logger; + options: CasesFindRequest; +} + +/** + * Retrieves a case and optionally its comments and sub case comments. + */ +export const find = async ({ + savedObjectsClient, + caseService, + logger, + options, +}: FindParams): Promise => { + try { + const queryParams = pipe( + CasesFindRequestRt.decode(options), + fold(throwErrors(Boom.badRequest), identity) + ); + + const queryArgs = { + tags: queryParams.tags, + reporters: queryParams.reporters, + sortByField: queryParams.sortField, + status: queryParams.status, + caseType: queryParams.type, + }; + + const caseQueries = constructQueryOptions(queryArgs); + const cases = await caseService.findCasesGroupedByID({ + client: savedObjectsClient, + caseOptions: { ...queryParams, ...caseQueries.case }, + subCaseOptions: caseQueries.subCase, + }); + + const [openCases, inProgressCases, closedCases] = await Promise.all([ + ...caseStatuses.map((status) => { + const statusQuery = constructQueryOptions({ ...queryArgs, status }); + return caseService.findCaseStatusStats({ + client: savedObjectsClient, + caseOptions: statusQuery.case, + subCaseOptions: statusQuery.subCase, + }); + }), + ]); + + return CasesFindResponseRt.encode( + transformCases({ + ...cases, + countOpenCases: openCases, + countInProgressCases: inProgressCases, + countClosedCases: closedCases, + total: cases.casesMap.size, + }) + ); + } catch (error) { + throw createCaseError({ + message: `Failed to find cases: ${JSON.stringify(options)}: ${error}`, + error, + logger, + }); + } +}; diff --git a/x-pack/plugins/cases/server/client/client.ts b/x-pack/plugins/cases/server/client/client.ts index 6495c47c6e6a4b..ed8bf4f7541de9 100644 --- a/x-pack/plugins/cases/server/client/client.ts +++ b/x-pack/plugins/cases/server/client/client.ts @@ -31,13 +31,14 @@ import { CaseUserActionServiceSetup, AlertServiceContract, } from '../services'; -import { CasesPatchRequest, CasePostRequest, User } from '../../common'; +import { CasesPatchRequest, CasePostRequest, User, CasesFindRequest } from '../../common'; import { get } from './cases/get'; import { get as getUserActions } from './user_actions/get'; import { get as getAlerts } from './alerts/get'; import { push } from './cases/push'; import { createCaseError } from '../common/error'; import { Authorization } from '../authorization/authorization'; +import { find } from './cases/find'; /** * This class is a pass through for common case functionality (like creating, get a case). @@ -88,6 +89,24 @@ export class CasesClientHandler implements CasesClient { } } + public async find(options: CasesFindRequest) { + try { + // TODO: authorize the user + return find({ + savedObjectsClient: this._savedObjectsClient, + caseService: this._caseService, + logger: this.logger, + options, + }); + } catch (error) { + throw createCaseError({ + message: `Failed to find cases using client: ${error}`, + error, + logger: this.logger, + }); + } + } + public async update(cases: CasesPatchRequest) { try { return update({ diff --git a/x-pack/plugins/cases/server/client/types.ts b/x-pack/plugins/cases/server/client/types.ts index 70fedf67b5a6ae..dd7e4bebcbb7e8 100644 --- a/x-pack/plugins/cases/server/client/types.ts +++ b/x-pack/plugins/cases/server/client/types.ts @@ -18,6 +18,8 @@ import { GetFieldsResponse, CaseUserActionsResponse, User, + CasesFindResponse, + CasesFindRequest, } from '../../common'; import { Authorization } from '../authorization/authorization'; import { AlertInfo } from '../common'; @@ -105,6 +107,7 @@ export interface CasesClient { getFields(args: ConfigureFields): Promise; getMappings(args: MappingsClient): Promise; getUserActions(args: CasesClientGetUserActions): Promise; + find(args: CasesFindRequest): Promise; push(args: CasesClientPush): Promise; update(args: CasesPatchRequest): Promise; updateAlertsStatus(args: CasesClientUpdateAlertsStatus): Promise; diff --git a/x-pack/plugins/cases/server/routes/api/cases/find_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/find_cases.ts index 9d16ef6e91917c..be2f8b96dea914 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/find_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/find_cases.ts @@ -5,22 +5,10 @@ * 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 { - CasesFindResponseRt, - CasesFindRequestRt, - throwErrors, - caseStatuses, -} from '../../../../common'; -import { transformCases, wrapError, escapeHatch } from '../utils'; +import { CasesFindRequest } from '../../../../common'; +import { wrapError, escapeHatch } from '../utils'; import { RouteDeps } from '../types'; -import { CASES_URL, SAVED_OBJECT_TYPES } from '../../../../common'; -import { constructQueryOptions } from './helpers'; +import { CASES_URL } from '../../../../common'; export function initFindCasesApi({ caseService, router, logger }: RouteDeps) { router.get( @@ -32,49 +20,14 @@ export function initFindCasesApi({ caseService, router, logger }: RouteDeps) { }, async (context, request, response) => { try { - const client = context.core.savedObjects.getClient({ - includedHiddenTypes: SAVED_OBJECT_TYPES, - }); - const queryParams = pipe( - CasesFindRequestRt.decode(request.query), - fold(throwErrors(Boom.badRequest), identity) - ); - const queryArgs = { - tags: queryParams.tags, - reporters: queryParams.reporters, - sortByField: queryParams.sortField, - status: queryParams.status, - caseType: queryParams.type, - }; - - const caseQueries = constructQueryOptions(queryArgs); - const cases = await caseService.findCasesGroupedByID({ - client, - caseOptions: { ...queryParams, ...caseQueries.case }, - subCaseOptions: caseQueries.subCase, - }); - - const [openCases, inProgressCases, closedCases] = await Promise.all([ - ...caseStatuses.map((status) => { - const statusQuery = constructQueryOptions({ ...queryArgs, status }); - return caseService.findCaseStatusStats({ - client, - caseOptions: statusQuery.case, - subCaseOptions: statusQuery.subCase, - }); - }), - ]); + if (!context.cases) { + return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); + } + const casesClient = await context.cases.getCasesClient(); + const options = request.body as CasesFindRequest; return response.ok({ - body: CasesFindResponseRt.encode( - transformCases({ - ...cases, - countOpenCases: openCases, - countInProgressCases: inProgressCases, - countClosedCases: closedCases, - total: cases.casesMap.size, - }) - ), + body: await casesClient.find({ ...options }), }); } catch (error) { logger.error(`Failed to find cases in route: ${error}`); From 84d9167095290179759619457e602a12e1e247de Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 17 Mar 2021 20:48:02 +0200 Subject: [PATCH 027/113] Create integration test helper functions --- .../common/lib/authentication.ts | 78 +++++++++++++++++++ .../case_api_integration/common/lib/types.ts | 48 ++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 x-pack/test/case_api_integration/common/lib/authentication.ts create mode 100644 x-pack/test/case_api_integration/common/lib/types.ts diff --git a/x-pack/test/case_api_integration/common/lib/authentication.ts b/x-pack/test/case_api_integration/common/lib/authentication.ts new file mode 100644 index 00000000000000..911012540a3799 --- /dev/null +++ b/x-pack/test/case_api_integration/common/lib/authentication.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext as CommonFtrProviderContext } from '../../../common/ftr_provider_context'; +import { Role, User, Space } from './types'; + +const space1: Space = { + id: 'space1', + name: 'Space 1', + disabledFeatures: [], +}; + +const space2: Space = { + id: 'space2', + name: 'Space 2', + disabledFeatures: [], +}; + +const spaces: Space[] = [space1, space2]; + +const superUser: User = { + username: 'superuser', + password: 'superuser', + roles: ['superuser'], +}; + +const noKibanaPrivileges: Role = { + name: 'no_kibana_privileges', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + }, +}; + +const users = [superUser]; +const roles = [noKibanaPrivileges]; + +export const createSpaces = async (getService: CommonFtrProviderContext['getService']) => { + const spacesService = getService('spaces'); + for (const space of spaces) { + await spacesService.create(space); + } +}; + +export const createUsersAndRoles = async (getService: CommonFtrProviderContext['getService']) => { + const security = getService('security'); + + const createRole = async ({ name, privileges }: Role) => { + 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`, + }); + }; + + for (const role of Object.values(roles)) { + await createRole(role); + } + + for (const user of Object.values(users)) { + await createUser(user); + } +}; diff --git a/x-pack/test/case_api_integration/common/lib/types.ts b/x-pack/test/case_api_integration/common/lib/types.ts new file mode 100644 index 00000000000000..2b61ae992fa64b --- /dev/null +++ b/x-pack/test/case_api_integration/common/lib/types.ts @@ -0,0 +1,48 @@ +/* + * 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 Space { + id: string; + namespace?: string; + name: string; + disabledFeatures: string[]; +} + +export interface User { + username: string; + password: string; + description?: string; + roles: string[]; +} + +interface FeaturesPrivileges { + [featureId: string]: string[]; +} + +interface ElasticsearchIndices { + names: string[]; + privileges: string[]; +} + +export interface ElasticSearchPrivilege { + cluster?: string[]; + indices?: ElasticsearchIndices[]; +} + +export interface KibanaPrivilege { + spaces: string[]; + base?: string[]; + feature?: FeaturesPrivileges; +} + +export interface Role { + name: string; + privileges: { + elasticsearch?: ElasticSearchPrivilege; + kibana?: KibanaPrivilege[]; + }; +} From 4bed45851b527a239d76303b4f39b75780eb0c46 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Wed, 17 Mar 2021 17:40:34 -0400 Subject: [PATCH 028/113] Adding auth to create call --- x-pack/plugins/cases/common/api/cases/case.ts | 5 ++- x-pack/plugins/cases/kibana.json | 2 +- .../server/authorization/authorization.ts | 29 ++++++------- .../cases/server/authorization/types.ts | 14 +++++++ .../cases/server/client/cases/create.test.ts | 10 ++++- .../cases/server/client/cases/create.ts | 11 +++++ x-pack/plugins/cases/server/client/client.ts | 1 + x-pack/plugins/cases/server/client/mocks.ts | 42 +++++++++++++++---- x-pack/plugins/cases/server/plugin.ts | 4 +- .../routes/api/__fixtures__/route_contexts.ts | 2 +- .../server/routes/api/cases/post_case.test.ts | 6 +++ .../cases/components/create/form_context.tsx | 2 + .../public/cases/components/create/schema.tsx | 3 +- .../security_solution/server/plugin.ts | 1 + 14 files changed, 99 insertions(+), 33 deletions(-) diff --git a/x-pack/plugins/cases/common/api/cases/case.ts b/x-pack/plugins/cases/common/api/cases/case.ts index a2bba7dba4b395..8ebf3a5173a6e1 100644 --- a/x-pack/plugins/cases/common/api/cases/case.ts +++ b/x-pack/plugins/cases/common/api/cases/case.ts @@ -38,6 +38,8 @@ const CaseBasicRt = rt.type({ [caseTypeField]: CaseTypeRt, connector: CaseConnectorRt, settings: SettingsRt, + // TODO: should a user be able to update the class? + class: rt.string, }); const CaseExternalServiceBasicRt = rt.type({ @@ -78,6 +80,7 @@ const CasePostRequestNoTypeRt = rt.type({ title: rt.string, connector: CaseConnectorRt, settings: SettingsRt, + class: rt.string, }); /** @@ -95,7 +98,7 @@ export const CasesClientPostRequestRt = rt.type({ * has all the necessary fields. CasesClientPostRequestRt is used for validation. */ export const CasePostRequestRt = rt.intersection([ - rt.partial({ type: CaseTypeRt }), + rt.partial({ [caseTypeField]: CaseTypeRt }), CasePostRequestNoTypeRt, ]); diff --git a/x-pack/plugins/cases/kibana.json b/x-pack/plugins/cases/kibana.json index 27b36d7e86e1f7..fca5ba72a9eb33 100644 --- a/x-pack/plugins/cases/kibana.json +++ b/x-pack/plugins/cases/kibana.json @@ -3,7 +3,7 @@ "id": "cases", "kibanaVersion": "kibana", "extraPublicDirs": ["common"], - "requiredPlugins": ["actions", "esUiShared", "kibanaReact", "triggersActionsUi"], + "requiredPlugins": ["actions", "esUiShared", "kibanaReact", "triggersActionsUi", "features"], "optionalPlugins": [ "spaces", "security" diff --git a/x-pack/plugins/cases/server/authorization/authorization.ts b/x-pack/plugins/cases/server/authorization/authorization.ts index b9f2a927b90990..d9b008232b6c2f 100644 --- a/x-pack/plugins/cases/server/authorization/authorization.ts +++ b/x-pack/plugins/cases/server/authorization/authorization.ts @@ -9,21 +9,7 @@ import { KibanaRequest } from 'kibana/server'; import Boom from '@hapi/boom'; import { SecurityPluginStart } from '../../../security/server'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; -import { GetSpaceFn } from './types'; - -// TODO: probably should move these to the types.ts file -// TODO: Larry would prefer if we 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? -export enum ReadOperations { - Get = 'get', - Find = 'find', -} - -export enum WriteOperations { - Create = 'create', - Delete = 'delete', - Update = 'update', -} +import { GetSpaceFn, ReadOperations, WriteOperations } from './types'; /** * This class handles ensuring that the user making a request has the correct permissions @@ -94,8 +80,14 @@ export class Authorization { } public async ensureAuthorized(className: string, operation: ReadOperations | WriteOperations) { + // TODO: remove + if (!this.isAuthEnabled) { + return; + } + const { securityAuth } = this; const isAvailableClass = this.featureCaseClasses.has(className); + // TODO: throw if the request is not authorized if (securityAuth && this.shouldCheckAuthorization()) { // TODO: implement ensure logic @@ -115,6 +107,7 @@ export class Authorization { * as Privileged. * This check will ensure we don't accidentally let these through */ + // TODO: audit log using `username` throw Boom.forbidden('User does not have permissions for this class'); } @@ -136,9 +129,11 @@ export class Authorization { // TODO: User unauthorized. throw an error. authorizedPrivileges & unauthorizedPrivilages are needed for logging. throw Boom.forbidden('Not authorized for this class'); } - } else { + } else if (!isAvailableClass) { // TODO: throw an error - throw Boom.forbidden('Security is disabled'); + throw Boom.forbidden('Security is disabled but no class was found'); } + + // else security is disabled so let the operation proceed } } diff --git a/x-pack/plugins/cases/server/authorization/types.ts b/x-pack/plugins/cases/server/authorization/types.ts index bcdd0f55650e02..07249d858c1872 100644 --- a/x-pack/plugins/cases/server/authorization/types.ts +++ b/x-pack/plugins/cases/server/authorization/types.ts @@ -9,3 +9,17 @@ import { KibanaRequest } from 'kibana/server'; import { Space } from '../../../spaces/server'; export type GetSpaceFn = (request: KibanaRequest) => 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? +export enum ReadOperations { + Get = 'get', + Find = 'find', +} + +// TODO: comments +export enum WriteOperations { + Create = 'create', + Delete = 'delete', + Update = 'update', +} diff --git a/x-pack/plugins/cases/server/client/cases/create.test.ts b/x-pack/plugins/cases/server/client/cases/create.test.ts index 9cbe2a448d3b43..a6a187884b22f2 100644 --- a/x-pack/plugins/cases/server/client/cases/create.test.ts +++ b/x-pack/plugins/cases/server/client/cases/create.test.ts @@ -40,6 +40,7 @@ describe('create', () => { settings: { syncAlerts: true, }, + class: 'awesome', }; const savedObjectsClient = createMockSavedObjectsRepository({ @@ -51,6 +52,7 @@ describe('create', () => { expect(res).toMatchInlineSnapshot(` Object { + "class": "awesome", "closed_at": null, "closed_by": null, "comments": Array [], @@ -114,7 +116,7 @@ describe('create', () => { "connector", "settings", ], - "new_value": "{\\"type\\":\\"individual\\",\\"description\\":\\"This is a brand new case of a bad meanie defacing data\\",\\"title\\":\\"Super Bad Security Issue\\",\\"tags\\":[\\"defacement\\"],\\"connector\\":{\\"id\\":\\"123\\",\\"name\\":\\"Jira\\",\\"type\\":\\".jira\\",\\"fields\\":{\\"issueType\\":\\"Task\\",\\"priority\\":\\"High\\",\\"parent\\":null}},\\"settings\\":{\\"syncAlerts\\":true}}", + "new_value": "{\\"type\\":\\"individual\\",\\"description\\":\\"This is a brand new case of a bad meanie defacing data\\",\\"title\\":\\"Super Bad Security Issue\\",\\"tags\\":[\\"defacement\\"],\\"connector\\":{\\"id\\":\\"123\\",\\"name\\":\\"Jira\\",\\"type\\":\\".jira\\",\\"fields\\":{\\"issueType\\":\\"Task\\",\\"priority\\":\\"High\\",\\"parent\\":null}},\\"settings\\":{\\"syncAlerts\\":true},\\"class\\":\\"awesome\\"}", "old_value": null, }, "references": Array [ @@ -144,6 +146,7 @@ describe('create', () => { settings: { syncAlerts: true, }, + class: 'awesome', }; const savedObjectsClient = createMockSavedObjectsRepository({ @@ -154,6 +157,7 @@ describe('create', () => { expect(res).toMatchInlineSnapshot(` Object { + "class": "awesome", "closed_at": null, "closed_by": null, "comments": Array [], @@ -207,6 +211,7 @@ describe('create', () => { settings: { syncAlerts: true, }, + class: 'awesome', }; const savedObjectsClient = createMockSavedObjectsRepository({ @@ -220,6 +225,7 @@ describe('create', () => { expect(res).toMatchInlineSnapshot(` Object { + "class": "awesome", "closed_at": null, "closed_by": null, "comments": Array [], @@ -418,6 +424,7 @@ describe('create', () => { settings: { syncAlerts: true, }, + class: 'awesome', }; const savedObjectsClient = createMockSavedObjectsRepository({ @@ -446,6 +453,7 @@ describe('create', () => { settings: { syncAlerts: true, }, + class: 'awesome', }; const savedObjectsClient = createMockSavedObjectsRepository({ caseSavedObject: mockCases, diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index 1dbb2dc496a997..064d6510d97eff 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -35,6 +35,8 @@ import { CaseUserActionServiceSetup, } from '../../services'; import { createCaseError } from '../../common/error'; +import { Authorization } from '../../authorization/authorization'; +import { WriteOperations } from '../../authorization/types'; interface CreateCaseArgs { caseConfigureService: CaseConfigureServiceSetup; @@ -44,6 +46,7 @@ interface CreateCaseArgs { userActionService: CaseUserActionServiceSetup; theCase: CasePostRequest; logger: Logger; + auth: Authorization; } /** @@ -57,6 +60,7 @@ export const create = async ({ user, theCase, logger, + auth, }: CreateCaseArgs): Promise => { // default to an individual case if the type is not defined. const { type = CaseType.individual, ...nonTypeCaseFields } = theCase; @@ -67,6 +71,13 @@ export const create = async ({ ); try { + try { + await auth.ensureAuthorized(query.class, WriteOperations.Create); + } catch (error) { + // TODO: log error using audit logger + throw error; + } + // eslint-disable-next-line @typescript-eslint/naming-convention const { username, full_name, email } = user; const createdDate = new Date().toISOString(); diff --git a/x-pack/plugins/cases/server/client/client.ts b/x-pack/plugins/cases/server/client/client.ts index ed8bf4f7541de9..dc918387f17769 100644 --- a/x-pack/plugins/cases/server/client/client.ts +++ b/x-pack/plugins/cases/server/client/client.ts @@ -79,6 +79,7 @@ export class CasesClientHandler implements CasesClient { user: this.user, theCase: caseInfo, logger: this.logger, + auth: this.authorization, }); } catch (error) { throw createCaseError({ diff --git a/x-pack/plugins/cases/server/client/mocks.ts b/x-pack/plugins/cases/server/client/mocks.ts index 51119070a798d9..84aa566086663f 100644 --- a/x-pack/plugins/cases/server/client/mocks.ts +++ b/x-pack/plugins/cases/server/client/mocks.ts @@ -5,9 +5,13 @@ * 2.0. */ -import { ElasticsearchClient } from 'kibana/server'; +import { ElasticsearchClient, KibanaRequest } from 'kibana/server'; import { DeeplyMockedKeys } from 'packages/kbn-utility-types/target/jest'; -import { loggingSystemMock, elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; +import { + loggingSystemMock, + elasticsearchServiceMock, + savedObjectsServiceMock, +} from '../../../../../src/core/server/mocks'; import { AlertServiceContract, CaseConfigureService, @@ -17,7 +21,9 @@ import { } from '../services'; import { CasesClient } from './types'; import { authenticationMock } from '../routes/api/__fixtures__'; -import { createExternalCasesClient } from '.'; +import { featuresPluginMock } from '../../../features/server/mocks'; +import { securityMock } from '../../../security/server/mocks'; +import { CasesClientFactory } from './factory'; export type CasesClientPluginContractMock = jest.Mocked; export const createExternalCasesClientMock = (): CasesClientPluginContractMock => ({ @@ -31,6 +37,7 @@ export const createExternalCasesClientMock = (): CasesClientPluginContractMock = getUserActions: jest.fn(), update: jest.fn(), updateAlertsStatus: jest.fn(), + find: jest.fn(), }); export const createCasesClientWithMockSavedObjectsClient = async ({ @@ -71,17 +78,34 @@ export const createCasesClientWithMockSavedObjectsClient = async ({ getAlerts: jest.fn(), }; - const casesClient = createExternalCasesClient({ - savedObjectsClient, - user: auth.getCurrentUser(), - caseService, + // since the cases saved objects are hidden we need to use getScopedClient(), we'll just have it return the mock client + // that is passed in to createRouteContext + const savedObjectsService = savedObjectsServiceMock.createStartContract(); + savedObjectsService.getScopedClient.mockReturnValue(savedObjectsClient); + + const factory = new CasesClientFactory(log); + factory.initialize({ + alertsService, caseConfigureService, + caseService, connectorMappingsService, userActionService, - alertsService, + featuresPluginStart: featuresPluginMock.createStart(), + getSpace: async (req: KibanaRequest) => undefined, + isAuthEnabled: false, + securityPluginSetup: securityMock.createSetup(), + securityPluginStart: securityMock.createStart(), + }); + + // create a single reference to the caseClient so we can mock its methods + const casesClient = await factory.create({ + savedObjectsService, + // Since authorization is disabled for these unit tests we don't need any information from the request object + // so just pass in an empty one + request: {} as KibanaRequest, scopedClusterClient: esClient, - logger: log, }); + return { client: casesClient, services: { userActionService, alertsService }, diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index a8f1e0fddd62fa..6a7d9dd8cf3c2c 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -45,12 +45,12 @@ function createConfig(context: PluginInitializerContext) { } export interface PluginsSetup { - security: SecurityPluginSetup; + security?: SecurityPluginSetup; actions: ActionsPluginSetup; } export interface PluginsStart { - security: SecurityPluginStart; + security?: SecurityPluginStart; features: FeaturesPluginStart; spaces?: SpacesPluginStart; } diff --git a/x-pack/plugins/cases/server/routes/api/__fixtures__/route_contexts.ts b/x-pack/plugins/cases/server/routes/api/__fixtures__/route_contexts.ts index 6fc2de3da62a92..a1f1a7fe47eed7 100644 --- a/x-pack/plugins/cases/server/routes/api/__fixtures__/route_contexts.ts +++ b/x-pack/plugins/cases/server/routes/api/__fixtures__/route_contexts.ts @@ -71,7 +71,7 @@ export const createRouteContext = async (client: any, badAuth = false) => { }); // create a single reference to the caseClient so we can mock its methods - const caseClient = factory.create({ + const caseClient = await factory.create({ savedObjectsService, // Since authorization is disabled for these unit tests we don't need any information from the request object // so just pass in an empty one diff --git a/x-pack/plugins/cases/server/routes/api/cases/post_case.test.ts b/x-pack/plugins/cases/server/routes/api/cases/post_case.test.ts index 669d3a5e58874a..b78725a8aba870 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/post_case.test.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/post_case.test.ts @@ -46,6 +46,7 @@ describe('POST cases', () => { settings: { syncAlerts: true, }, + class: 'awesome', }, }); @@ -85,6 +86,7 @@ describe('POST cases', () => { settings: { syncAlerts: true, }, + class: 'awesome', }, }); @@ -118,6 +120,7 @@ describe('POST cases', () => { settings: { syncAlerts: true, }, + class: 'awesome', }, }); @@ -143,6 +146,7 @@ describe('POST cases', () => { settings: { syncAlerts: true, }, + class: 'awesome', }, }); @@ -176,6 +180,7 @@ describe('POST cases', () => { settings: { syncAlerts: true, }, + class: 'awesome', }, }); @@ -191,6 +196,7 @@ describe('POST cases', () => { expect(response.status).toEqual(200); expect(response.payload).toMatchInlineSnapshot(` Object { + "class": "awesome", "closed_at": null, "closed_by": null, "comments": Array [], diff --git a/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx b/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx index 597726e7bb3f34..99e9e191ea9768 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx @@ -83,6 +83,8 @@ export const FormContext: React.FC = ({ type: caseType, connector: connectorToUpdate, settings: { syncAlerts }, + // TODO: need to replace this with the value that the plugin registers in the feature registration + class: 'securitySolution', }); if (afterCaseCreated && updatedCase) { diff --git a/x-pack/plugins/security_solution/public/cases/components/create/schema.tsx b/x-pack/plugins/security_solution/public/cases/components/create/schema.tsx index 38321cdbeab50a..f34000f24ec055 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/schema.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/schema.tsx @@ -19,7 +19,8 @@ export const schemaTags = { labelAppend: OptionalFieldLabel, }; -export type FormProps = Omit & { +// TODO: remove class from here? +export type FormProps = Omit & { connectorId: string; fields: ConnectorTypeFields['fields']; syncAlerts: boolean; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 2cb3ae4bea1ddc..5f2129e4ca60b3 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -217,6 +217,7 @@ export class Plugin implements IPlugin Date: Thu, 18 Mar 2021 15:24:55 +0200 Subject: [PATCH 029/113] Create getClassFilter helper --- .../authorization/authorization_query.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 x-pack/plugins/cases/server/authorization/authorization_query.ts diff --git a/x-pack/plugins/cases/server/authorization/authorization_query.ts b/x-pack/plugins/cases/server/authorization/authorization_query.ts new file mode 100644 index 00000000000000..02fcbac1a4fda7 --- /dev/null +++ b/x-pack/plugins/cases/server/authorization/authorization_query.ts @@ -0,0 +1,36 @@ +/* + * 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 { remove } from 'lodash'; + +export function getClassFilter(savedObjectType: string, classNames: string[]): string { + const firstQueryItem = + classNames.length > 0 ? `${savedObjectType}.attributes.class: ${classNames[0]}` : ''; + + const reducesQuery = classNames.slice(1).reduce((query, className) => { + ensureFieldIsSafeForQuery('class', className); + return `${query} OR ${savedObjectType}.attributes.class: ${className}`; + }, firstQueryItem); + + return `(${reducesQuery})`; +} + +export function ensureFieldIsSafeForQuery(field: string, value: string): boolean { + const invalid = value.match(/([>=<\*:()]+|\s+)/g); + if (invalid) { + const whitespace = remove(invalid, (chars) => chars.trim().length === 0); + const errors = []; + if (whitespace.length) { + errors.push(`whitespace`); + } + if (invalid.length) { + errors.push(`invalid character${invalid.length > 1 ? `s` : ``}: ${invalid?.join(`, `)}`); + } + throw new Error(`expected ${field} not to include ${errors.join(' and ')}`); + } + return true; +} From fe1d8c8d929513c7afa09aab38390660d2cae470 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 18 Mar 2021 15:25:43 +0200 Subject: [PATCH 030/113] Add class attribute to find request --- x-pack/plugins/cases/common/api/cases/case.ts | 1 + x-pack/plugins/cases/common/api/cases/sub_case.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/x-pack/plugins/cases/common/api/cases/case.ts b/x-pack/plugins/cases/common/api/cases/case.ts index 8ebf3a5173a6e1..f529baca53b922 100644 --- a/x-pack/plugins/cases/common/api/cases/case.ts +++ b/x-pack/plugins/cases/common/api/cases/case.ts @@ -115,6 +115,7 @@ export const CasesFindRequestRt = rt.partial({ searchFields: rt.array(rt.string), sortField: rt.string, sortOrder: rt.union([rt.literal('desc'), rt.literal('asc')]), + class: rt.string, }); export const CaseResponseRt = rt.intersection([ diff --git a/x-pack/plugins/cases/common/api/cases/sub_case.ts b/x-pack/plugins/cases/common/api/cases/sub_case.ts index c46f87c547d50b..7c8cdf023d8d72 100644 --- a/x-pack/plugins/cases/common/api/cases/sub_case.ts +++ b/x-pack/plugins/cases/common/api/cases/sub_case.ts @@ -39,6 +39,7 @@ export const SubCasesFindRequestRt = rt.partial({ searchFields: rt.array(rt.string), sortField: rt.string, sortOrder: rt.union([rt.literal('desc'), rt.literal('asc')]), + class: rt.string, }); export const SubCaseResponseRt = rt.intersection([ From 22e77528831884f25c8b8e80d9652c9e7ecdd2e0 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 18 Mar 2021 15:27:14 +0200 Subject: [PATCH 031/113] Create getFindAuthorizationFilter --- .../server/authorization/authorization.ts | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/x-pack/plugins/cases/server/authorization/authorization.ts b/x-pack/plugins/cases/server/authorization/authorization.ts index d9b008232b6c2f..d9316a3ce4ed1b 100644 --- a/x-pack/plugins/cases/server/authorization/authorization.ts +++ b/x-pack/plugins/cases/server/authorization/authorization.ts @@ -10,6 +10,7 @@ import Boom from '@hapi/boom'; import { SecurityPluginStart } from '../../../security/server'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; import { GetSpaceFn, ReadOperations, WriteOperations } from './types'; +import { getClassFilter } from './authorization_query'; /** * This class handles ensuring that the user making a request has the correct permissions @@ -136,4 +137,73 @@ export class Authorization { // else security is disabled so let the operation proceed } + + public async getFindAuthorizationFilter(savedObjectType: string) { + const { securityAuth } = this; + if (securityAuth && this.shouldCheckAuthorization()) { + const { authorizedClassNames } = await this.getAuthorizedClassNames([ReadOperations.Find]); + + if (!authorizedClassNames.length) { + // TODO: Better error message, log error + throw Boom.forbidden('Not authorized for this class'); + } + + return { + filter: getClassFilter(savedObjectType, authorizedClassNames), + ensureSavedObjectIsAuthorized: (className: string) => { + if (!authorizedClassNames.includes(className)) { + // TODO: log error + throw Boom.forbidden('Not authorized for this class'); + } + }, + }; + } + } + + private async getAuthorizedClassNames( + operations: Array + ): Promise<{ + username?: string; + hasAllRequested: boolean; + authorizedClassNames: string[]; + }> { + const { securityAuth, featureCaseClasses } = this; + if (securityAuth && this.shouldCheckAuthorization()) { + const checkPrivileges = securityAuth.checkPrivilegesDynamicallyWithRequest(this.request); + const requiredPrivileges = new Map(); + + for (const className of featureCaseClasses) { + for (const operation of operations) { + requiredPrivileges.set(securityAuth.actions.cases.get(className, operation), [className]); + } + } + + const { hasAllRequested, username, privileges } = await checkPrivileges({ + kibana: [...requiredPrivileges.keys()], + }); + + return { + hasAllRequested, + username, + authorizedClassNames: hasAllRequested + ? Array.from(featureCaseClasses) + : privileges.kibana.reduce( + (authorizedClassNames, { authorized, privilege }) => { + if (authorized && requiredPrivileges.has(privilege)) { + const [className] = requiredPrivileges.get(privilege)!; + authorizedClassNames.push(className); + } + + return authorizedClassNames; + }, + [] + ), + }; + } else { + return { + hasAllRequested: true, + authorizedClassNames: Array.from(featureCaseClasses), + }; + } + } } From 96f81a406a9151fc6f254dac28c414f2e0042d96 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 18 Mar 2021 17:07:09 +0200 Subject: [PATCH 032/113] Ensure savedObject is authorized in find method --- .../server/authorization/authorization.ts | 9 ++++++++- .../plugins/cases/server/client/cases/find.ts | 18 ++++++++++++++++-- x-pack/plugins/cases/server/client/client.ts | 1 + .../cases/server/routes/api/cases/helpers.ts | 2 ++ 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/cases/server/authorization/authorization.ts b/x-pack/plugins/cases/server/authorization/authorization.ts index d9316a3ce4ed1b..efec07b14a17fc 100644 --- a/x-pack/plugins/cases/server/authorization/authorization.ts +++ b/x-pack/plugins/cases/server/authorization/authorization.ts @@ -138,7 +138,12 @@ export class Authorization { // else security is disabled so let the operation proceed } - public async getFindAuthorizationFilter(savedObjectType: string) { + public async getFindAuthorizationFilter( + savedObjectType: string + ): Promise<{ + filter?: string; + ensureSavedObjectIsAuthorized: (className: string) => void; + }> { const { securityAuth } = this; if (securityAuth && this.shouldCheckAuthorization()) { const { authorizedClassNames } = await this.getAuthorizedClassNames([ReadOperations.Find]); @@ -158,6 +163,8 @@ export class Authorization { }, }; } + + return { ensureSavedObjectIsAuthorized: (className: string) => {} }; } private async getAuthorizedClassNames( diff --git a/x-pack/plugins/cases/server/client/cases/find.ts b/x-pack/plugins/cases/server/client/cases/find.ts index c96c7a43626b24..0fb9d0c1e85bc6 100644 --- a/x-pack/plugins/cases/server/client/cases/find.ts +++ b/x-pack/plugins/cases/server/client/cases/find.ts @@ -18,16 +18,19 @@ import { throwErrors, caseStatuses, CasesFindResponseRt, + CASE_SAVED_OBJECT, } from '../../../common'; import { CaseServiceSetup } from '../../services'; import { createCaseError } from '../../common/error'; import { constructQueryOptions } from '../../routes/api/cases/helpers'; import { transformCases } from '../../routes/api/utils'; +import { Authorization } from '../../authorization/authorization'; interface FindParams { savedObjectsClient: SavedObjectsClientContract; caseService: CaseServiceSetup; logger: Logger; + auth: Authorization; options: CasesFindRequest; } @@ -38,6 +41,7 @@ export const find = async ({ savedObjectsClient, caseService, logger, + auth, options, }: FindParams): Promise => { try { @@ -46,6 +50,12 @@ export const find = async ({ fold(throwErrors(Boom.badRequest), identity) ); + // TODO: Maybe surround it with try/catch + const { + filter: authorizationFilter, + ensureSavedObjectIsAuthorized, + } = await auth.getFindAuthorizationFilter(CASE_SAVED_OBJECT); + const queryArgs = { tags: queryParams.tags, reporters: queryParams.reporters, @@ -54,16 +64,20 @@ export const find = async ({ caseType: queryParams.type, }; - const caseQueries = constructQueryOptions(queryArgs); + const caseQueries = constructQueryOptions({ ...queryArgs, authorizationFilter }); const cases = await caseService.findCasesGroupedByID({ client: savedObjectsClient, caseOptions: { ...queryParams, ...caseQueries.case }, subCaseOptions: caseQueries.subCase, }); + for (const theCase of cases.casesMap.values()) { + ensureSavedObjectIsAuthorized(theCase.class); + } + const [openCases, inProgressCases, closedCases] = await Promise.all([ ...caseStatuses.map((status) => { - const statusQuery = constructQueryOptions({ ...queryArgs, status }); + const statusQuery = constructQueryOptions({ ...queryArgs, status, authorizationFilter }); return caseService.findCaseStatusStats({ client: savedObjectsClient, caseOptions: statusQuery.case, diff --git a/x-pack/plugins/cases/server/client/client.ts b/x-pack/plugins/cases/server/client/client.ts index dc918387f17769..03d5ebaba7d98f 100644 --- a/x-pack/plugins/cases/server/client/client.ts +++ b/x-pack/plugins/cases/server/client/client.ts @@ -97,6 +97,7 @@ export class CasesClientHandler implements CasesClient { savedObjectsClient: this._savedObjectsClient, caseService: this._caseService, logger: this.logger, + auth: this.authorization, options, }); } catch (error) { diff --git a/x-pack/plugins/cases/server/routes/api/cases/helpers.ts b/x-pack/plugins/cases/server/routes/api/cases/helpers.ts index c3aa0f415fb0af..300d1466e269ee 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/helpers.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/helpers.ts @@ -101,12 +101,14 @@ export const constructQueryOptions = ({ status, sortByField, caseType, + authorizationFilter, }: { tags?: string | string[]; reporters?: string | string[]; status?: CaseStatuses; sortByField?: string; caseType?: CaseType; + authorizationFilter?: string; }): { case: SavedObjectFindOptions; subCase?: SavedObjectFindOptions } => { const tagsFilter = buildFilter({ filters: tags, field: 'tags', operator: 'OR' }); const reportersFilter = buildFilter({ From 06d7c64508964d21083b74d4a5060ada994a5a8e Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 22 Mar 2021 12:05:53 +0200 Subject: [PATCH 033/113] Include fields for authorization --- .../cases/server/authorization/authorization.ts | 2 +- .../{authorization_query.ts => utils.ts} | 13 ++++++++----- x-pack/plugins/cases/server/client/cases/find.ts | 14 +++++++++++--- 3 files changed, 20 insertions(+), 9 deletions(-) rename x-pack/plugins/cases/server/authorization/{authorization_query.ts => utils.ts} (76%) diff --git a/x-pack/plugins/cases/server/authorization/authorization.ts b/x-pack/plugins/cases/server/authorization/authorization.ts index efec07b14a17fc..c2041aeee14c05 100644 --- a/x-pack/plugins/cases/server/authorization/authorization.ts +++ b/x-pack/plugins/cases/server/authorization/authorization.ts @@ -10,7 +10,7 @@ import Boom from '@hapi/boom'; import { SecurityPluginStart } from '../../../security/server'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; import { GetSpaceFn, ReadOperations, WriteOperations } from './types'; -import { getClassFilter } from './authorization_query'; +import { getClassFilter } from './utils'; /** * This class handles ensuring that the user making a request has the correct permissions diff --git a/x-pack/plugins/cases/server/authorization/authorization_query.ts b/x-pack/plugins/cases/server/authorization/utils.ts similarity index 76% rename from x-pack/plugins/cases/server/authorization/authorization_query.ts rename to x-pack/plugins/cases/server/authorization/utils.ts index 02fcbac1a4fda7..3c5d570c63154d 100644 --- a/x-pack/plugins/cases/server/authorization/authorization_query.ts +++ b/x-pack/plugins/cases/server/authorization/utils.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { remove } from 'lodash'; +import { remove, uniq } from 'lodash'; -export function getClassFilter(savedObjectType: string, classNames: string[]): string { +export const getClassFilter = (savedObjectType: string, classNames: string[]): string => { const firstQueryItem = classNames.length > 0 ? `${savedObjectType}.attributes.class: ${classNames[0]}` : ''; @@ -17,9 +17,9 @@ export function getClassFilter(savedObjectType: string, classNames: string[]): s }, firstQueryItem); return `(${reducesQuery})`; -} +}; -export function ensureFieldIsSafeForQuery(field: string, value: string): boolean { +export const ensureFieldIsSafeForQuery = (field: string, value: string): boolean => { const invalid = value.match(/([>=<\*:()]+|\s+)/g); if (invalid) { const whitespace = remove(invalid, (chars) => chars.trim().length === 0); @@ -33,4 +33,7 @@ export function ensureFieldIsSafeForQuery(field: string, value: string): boolean throw new Error(`expected ${field} not to include ${errors.join(' and ')}`); } return true; -} +}; + +export const includeFieldsRequiredForAuthentication = (fields: string[]): string[] => + uniq([...fields, 'class']); diff --git a/x-pack/plugins/cases/server/client/cases/find.ts b/x-pack/plugins/cases/server/client/cases/find.ts index 0fb9d0c1e85bc6..7466a0ac23c429 100644 --- a/x-pack/plugins/cases/server/client/cases/find.ts +++ b/x-pack/plugins/cases/server/client/cases/find.ts @@ -18,13 +18,15 @@ import { throwErrors, caseStatuses, CasesFindResponseRt, - CASE_SAVED_OBJECT, -} from '../../../common'; +} from '../../../common/api'; + +import { CASE_SAVED_OBJECT } from '../../../common/constants'; import { CaseServiceSetup } from '../../services'; import { createCaseError } from '../../common/error'; import { constructQueryOptions } from '../../routes/api/cases/helpers'; import { transformCases } from '../../routes/api/utils'; import { Authorization } from '../../authorization/authorization'; +import { includeFieldsRequiredForAuthentication } from '../../authorization/utils'; interface FindParams { savedObjectsClient: SavedObjectsClientContract; @@ -67,7 +69,13 @@ export const find = async ({ const caseQueries = constructQueryOptions({ ...queryArgs, authorizationFilter }); const cases = await caseService.findCasesGroupedByID({ client: savedObjectsClient, - caseOptions: { ...queryParams, ...caseQueries.case }, + caseOptions: { + ...queryParams, + ...caseQueries.case, + fields: queryParams.fields + ? includeFieldsRequiredForAuthentication(queryParams.fields) + : queryParams.fields, + }, subCaseOptions: caseQueries.subCase, }); From 2ca4134d06e50d561fbf7d8d9676d3c7b79d1f56 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 22 Mar 2021 12:50:38 +0200 Subject: [PATCH 034/113] Combine authorization filter with cases & subcases filter --- .../cases/server/authorization/utils.ts | 8 ++++++ .../plugins/cases/server/client/cases/find.ts | 1 + .../cases/server/routes/api/cases/helpers.ts | 28 +++++++++++++++---- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/cases/server/authorization/utils.ts b/x-pack/plugins/cases/server/authorization/utils.ts index 3c5d570c63154d..3f66693ed4306d 100644 --- a/x-pack/plugins/cases/server/authorization/utils.ts +++ b/x-pack/plugins/cases/server/authorization/utils.ts @@ -19,6 +19,14 @@ export const getClassFilter = (savedObjectType: string, classNames: string[]): s return `(${reducesQuery})`; }; +export const combineFilterWithAuthorizationFilter = ( + filter: string, + authorizationFilter: string +) => { + const suffix = `AND ${authorizationFilter}`; + return filter.startsWith('(') ? `${filter} ${suffix}` : `(${filter}) ${suffix}`; +}; + export const ensureFieldIsSafeForQuery = (field: string, value: string): boolean => { const invalid = value.match(/([>=<\*:()]+|\s+)/g); if (invalid) { diff --git a/x-pack/plugins/cases/server/client/cases/find.ts b/x-pack/plugins/cases/server/client/cases/find.ts index 7466a0ac23c429..39c5435c8ff5db 100644 --- a/x-pack/plugins/cases/server/client/cases/find.ts +++ b/x-pack/plugins/cases/server/client/cases/find.ts @@ -83,6 +83,7 @@ export const find = async ({ ensureSavedObjectIsAuthorized(theCase.class); } + // TODO: Make sure we do not leak information when authorization is on const [openCases, inProgressCases, closedCases] = await Promise.all([ ...caseStatuses.map((status) => { const statusQuery = constructQueryOptions({ ...queryArgs, status, authorizationFilter }); diff --git a/x-pack/plugins/cases/server/routes/api/cases/helpers.ts b/x-pack/plugins/cases/server/routes/api/cases/helpers.ts index 76f572da815ee7..aeac3f156e2502 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/helpers.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/helpers.ts @@ -21,6 +21,7 @@ import { import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../../common/constants'; import { sortToSnake } from '../utils'; import { combineFilters } from '../../../common'; +import { combineFilterWithAuthorizationFilter } from '../../../authorization/utils'; export const addStatusFilter = ({ status, @@ -128,7 +129,10 @@ export const constructQueryOptions = ({ }); return { case: { - filter: caseFilters, + filter: + authorizationFilter != null + ? combineFilterWithAuthorizationFilter(caseFilters, authorizationFilter) + : caseFilters, sortField, }, }; @@ -138,14 +142,21 @@ export const constructQueryOptions = ({ // The sub case filter will use the query.status if it exists const typeFilter = `${CASE_SAVED_OBJECT}.attributes.type: ${CaseType.collection}`; const caseFilters = combineFilters([tagsFilter, reportersFilter, typeFilter], 'AND'); + const subCaseFilters = addStatusFilter({ status, type: SUB_CASE_SAVED_OBJECT }); return { case: { - filter: caseFilters, + filter: + authorizationFilter != null + ? combineFilterWithAuthorizationFilter(caseFilters, authorizationFilter) + : caseFilters, sortField, }, subCase: { - filter: addStatusFilter({ status, type: SUB_CASE_SAVED_OBJECT }), + filter: + authorizationFilter != null + ? combineFilterWithAuthorizationFilter(caseFilters, authorizationFilter) + : subCaseFilters, sortField, }, }; @@ -167,14 +178,21 @@ export const constructQueryOptions = ({ const statusFilter = combineFilters([addStatusFilter({ status }), typeIndividual], 'AND'); const statusAndType = combineFilters([statusFilter, typeParent], 'OR'); const caseFilters = combineFilters([statusAndType, tagsFilter, reportersFilter], 'AND'); + const subCaseFilters = addStatusFilter({ status, type: SUB_CASE_SAVED_OBJECT }); return { case: { - filter: caseFilters, + filter: + authorizationFilter != null + ? combineFilterWithAuthorizationFilter(caseFilters, authorizationFilter) + : caseFilters, sortField, }, subCase: { - filter: addStatusFilter({ status, type: SUB_CASE_SAVED_OBJECT }), + filter: + authorizationFilter != null + ? combineFilterWithAuthorizationFilter(caseFilters, authorizationFilter) + : subCaseFilters, sortField, }, }; From 17110b14faf64ffdf6a8e643805e8cabf0a49eb4 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 22 Mar 2021 12:51:01 +0200 Subject: [PATCH 035/113] Fix isAuthorized flag --- x-pack/plugins/cases/server/plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index 6a7d9dd8cf3c2c..c60ed2ede37c68 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -144,7 +144,7 @@ export class CasePlugin { }, featuresPluginStart: plugins.features, // we'll be removing this eventually but let's just default it to false if it wasn't specified explicitly in the config file - isAuthEnabled: this.config?.enabled ?? false, + isAuthEnabled: this.config?.enableAuthorization ?? false, }); const getCasesClientWithRequestAndContext = async ( From bc062649a8d3e369f2f8cb19df06a0ec24ceadf1 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 22 Mar 2021 12:59:59 +0200 Subject: [PATCH 036/113] Fix merge issue --- x-pack/plugins/cases/kibana.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/cases/kibana.json b/x-pack/plugins/cases/kibana.json index 1aaf84decbe369..070462a4d9ec60 100644 --- a/x-pack/plugins/cases/kibana.json +++ b/x-pack/plugins/cases/kibana.json @@ -2,7 +2,7 @@ "configPath": ["xpack", "cases"], "id": "cases", "kibanaVersion": "kibana", - "requiredPlugins": ["actions", "securitySolution"], + "requiredPlugins": ["actions", "securitySolution", "features"], "optionalPlugins": [ "spaces", "security" From a04e0d7d9353b078eb6199788905622d41135e19 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 22 Mar 2021 14:02:21 +0200 Subject: [PATCH 037/113] Create/delete spaces & users before and after tests --- .../common/lib/authentication.ts | 34 +++++++++++++++++-- .../security_and_spaces/tests/basic/index.ts | 10 +++++- .../security_and_spaces/tests/trial/index.ts | 11 +++++- .../spaces_only/tests/index.ts | 11 +++++- 4 files changed, 60 insertions(+), 6 deletions(-) diff --git a/x-pack/test/case_api_integration/common/lib/authentication.ts b/x-pack/test/case_api_integration/common/lib/authentication.ts index 911012540a3799..9ff676d2d15756 100644 --- a/x-pack/test/case_api_integration/common/lib/authentication.ts +++ b/x-pack/test/case_api_integration/common/lib/authentication.ts @@ -52,7 +52,7 @@ export const createSpaces = async (getService: CommonFtrProviderContext['getServ } }; -export const createUsersAndRoles = async (getService: CommonFtrProviderContext['getService']) => { +const createUsersAndRoles = async (getService: CommonFtrProviderContext['getService']) => { const security = getService('security'); const createRole = async ({ name, privileges }: Role) => { @@ -68,11 +68,39 @@ export const createUsersAndRoles = async (getService: CommonFtrProviderContext[' }); }; - for (const role of Object.values(roles)) { + for (const role of roles) { await createRole(role); } - for (const user of Object.values(users)) { + for (const user of users) { await createUser(user); } }; + +export const deleteSpaces = async (getService: CommonFtrProviderContext['getService']) => { + const spacesService = getService('spaces'); + for (const space of spaces) { + await spacesService.delete(space.id); + } +}; +const deleteUsersAndRoles = async (getService: CommonFtrProviderContext['getService']) => { + const security = getService('security'); + + for (const user of users) { + await security.user.delete(user.username); + } + + for (const role of roles) { + await security.role.delete(role.name); + } +}; + +export const createSpacesAndUsers = async (getService: CommonFtrProviderContext['getService']) => { + await createSpaces(getService); + await createUsersAndRoles(getService); +}; + +export const deleteSpacesAndUsers = async (getService: CommonFtrProviderContext['getService']) => { + await deleteSpaces(getService); + await deleteUsersAndRoles(getService); +}; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/basic/index.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/index.ts index 95174be3ab1b73..502c64ccce04a4 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/basic/index.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/index.ts @@ -6,13 +6,21 @@ */ import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { createSpacesAndUsers, deleteSpacesAndUsers } from '../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export -export default ({ loadTestFile }: FtrProviderContext): void => { +export default ({ loadTestFile, getService }: FtrProviderContext): void => { describe('cases security and spaces enabled: basic', function () { // Fastest ciGroup for the moment. this.tags('ciGroup5'); + before(async () => { + await createSpacesAndUsers(getService); + }); + + after(async () => { + await deleteSpacesAndUsers(getService); + }); // Common loadTestFile(require.resolve('../common')); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts index 5e6be87b624019..6f2c3a6bb27013 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts @@ -6,13 +6,22 @@ */ import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { createSpacesAndUsers, deleteSpacesAndUsers } from '../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export -export default ({ loadTestFile }: FtrProviderContext): void => { +export default ({ loadTestFile, getService }: FtrProviderContext): void => { describe('cases security and spaces enabled: trial', function () { // Fastest ciGroup for the moment. this.tags('ciGroup5'); + before(async () => { + await createSpacesAndUsers(getService); + }); + + after(async () => { + await deleteSpacesAndUsers(getService); + }); + // Common loadTestFile(require.resolve('../common')); diff --git a/x-pack/test/case_api_integration/spaces_only/tests/index.ts b/x-pack/test/case_api_integration/spaces_only/tests/index.ts index 38ca7f40706160..d35743ea0c7d97 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/index.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/index.ts @@ -6,11 +6,20 @@ */ import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { createSpaces, deleteSpaces } from '../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export -export default ({ loadTestFile }: FtrProviderContext): void => { +export default ({ loadTestFile, getService }: FtrProviderContext): void => { describe('cases spaces only enabled', function () { // Fastest ciGroup for the moment. this.tags('ciGroup5'); + + before(async () => { + await createSpaces(getService); + }); + + after(async () => { + await deleteSpaces(getService); + }); }); }; From 7fe4e40197c84d65d522ce76e923f8cb62c064e4 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 22 Mar 2021 14:28:36 +0200 Subject: [PATCH 038/113] Add more user and roles --- .../index.ts} | 42 +------- .../common/lib/authentication/roles.ts | 98 +++++++++++++++++++ .../common/lib/authentication/spaces.ts | 22 +++++ .../common/lib/{ => authentication}/types.ts | 0 .../common/lib/authentication/users.ts | 35 +++++++ 5 files changed, 159 insertions(+), 38 deletions(-) rename x-pack/test/case_api_integration/common/lib/{authentication.ts => authentication/index.ts} (76%) create mode 100644 x-pack/test/case_api_integration/common/lib/authentication/roles.ts create mode 100644 x-pack/test/case_api_integration/common/lib/authentication/spaces.ts rename x-pack/test/case_api_integration/common/lib/{ => authentication}/types.ts (100%) create mode 100644 x-pack/test/case_api_integration/common/lib/authentication/users.ts diff --git a/x-pack/test/case_api_integration/common/lib/authentication.ts b/x-pack/test/case_api_integration/common/lib/authentication/index.ts similarity index 76% rename from x-pack/test/case_api_integration/common/lib/authentication.ts rename to x-pack/test/case_api_integration/common/lib/authentication/index.ts index 9ff676d2d15756..f7a54244b3bf58 100644 --- a/x-pack/test/case_api_integration/common/lib/authentication.ts +++ b/x-pack/test/case_api_integration/common/lib/authentication/index.ts @@ -6,44 +6,10 @@ */ import { FtrProviderContext as CommonFtrProviderContext } from '../../../common/ftr_provider_context'; -import { Role, User, Space } from './types'; - -const space1: Space = { - id: 'space1', - name: 'Space 1', - disabledFeatures: [], -}; - -const space2: Space = { - id: 'space2', - name: 'Space 2', - disabledFeatures: [], -}; - -const spaces: Space[] = [space1, space2]; - -const superUser: User = { - username: 'superuser', - password: 'superuser', - roles: ['superuser'], -}; - -const noKibanaPrivileges: Role = { - name: 'no_kibana_privileges', - privileges: { - elasticsearch: { - indices: [ - { - names: ['*'], - privileges: ['all'], - }, - ], - }, - }, -}; - -const users = [superUser]; -const roles = [noKibanaPrivileges]; +import { Role, User } from './types'; +import { users } from './users'; +import { roles } from './roles'; +import { spaces } from './spaces'; export const createSpaces = async (getService: CommonFtrProviderContext['getService']) => { const spacesService = getService('spaces'); diff --git a/x-pack/test/case_api_integration/common/lib/authentication/roles.ts b/x-pack/test/case_api_integration/common/lib/authentication/roles.ts new file mode 100644 index 00000000000000..e711a59229e773 --- /dev/null +++ b/x-pack/test/case_api_integration/common/lib/authentication/roles.ts @@ -0,0 +1,98 @@ +/* + * 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 { Role } from './types'; + +export const noKibanaPrivileges: Role = { + name: 'no_kibana_privileges', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + }, +}; + +export const globalRead: Role = { + name: 'global_read', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + cases: ['read'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const securitySolutionOnlyAll: Role = { + name: 'sec_only_all', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + siem: ['all'], + }, + spaces: ['space1'], + }, + ], + }, +}; + +export const observabilityOnlyAll: Role = { + name: 'sec_only_all', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + logs: ['all'], + infrastructure: ['all'], + apm: ['all'], + uptime: ['all'], + }, + spaces: ['space1'], + }, + ], + }, +}; + +export const roles = [ + noKibanaPrivileges, + globalRead, + securitySolutionOnlyAll, + observabilityOnlyAll, +]; diff --git a/x-pack/test/case_api_integration/common/lib/authentication/spaces.ts b/x-pack/test/case_api_integration/common/lib/authentication/spaces.ts new file mode 100644 index 00000000000000..1f8efd242b9c77 --- /dev/null +++ b/x-pack/test/case_api_integration/common/lib/authentication/spaces.ts @@ -0,0 +1,22 @@ +/* + * 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 { Space } from './types'; + +const space1: Space = { + id: 'space1', + name: 'Space 1', + disabledFeatures: [], +}; + +const space2: Space = { + id: 'space2', + name: 'Space 2', + disabledFeatures: [], +}; + +export const spaces: Space[] = [space1, space2]; diff --git a/x-pack/test/case_api_integration/common/lib/types.ts b/x-pack/test/case_api_integration/common/lib/authentication/types.ts similarity index 100% rename from x-pack/test/case_api_integration/common/lib/types.ts rename to x-pack/test/case_api_integration/common/lib/authentication/types.ts diff --git a/x-pack/test/case_api_integration/common/lib/authentication/users.ts b/x-pack/test/case_api_integration/common/lib/authentication/users.ts new file mode 100644 index 00000000000000..43e21b79ee4b61 --- /dev/null +++ b/x-pack/test/case_api_integration/common/lib/authentication/users.ts @@ -0,0 +1,35 @@ +/* + * 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 { securitySolutionOnlyAll, observabilityOnlyAll } from './roles'; +import { User } from './types'; + +const superUser: User = { + username: 'superuser', + password: 'superuser', + roles: ['superuser'], +}; + +const secOnly: User = { + username: 'sec_only', + password: 'sec_only', + roles: [securitySolutionOnlyAll.name], +}; + +const obsOnly: User = { + username: 'obs_only', + password: 'obs_only', + roles: [observabilityOnlyAll.name], +}; + +const obsSec: User = { + username: 'obs_sec', + password: 'obs_sec', + roles: [securitySolutionOnlyAll.name, observabilityOnlyAll.name], +}; + +export const users = [superUser, secOnly, obsOnly, obsSec]; From 2847861b096a67e572d74b2ca82436ae615464f7 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 24 Mar 2021 15:15:10 +0200 Subject: [PATCH 039/113] [Cases] Convert filters from strings to KueryNode (#95288) --- .../server/authorization/authorization.ts | 3 +- .../cases/server/authorization/utils.ts | 27 ++++--- x-pack/plugins/cases/server/common/types.ts | 7 ++ x-pack/plugins/cases/server/common/utils.ts | 22 ------ .../cases/server/routes/api/cases/helpers.ts | 74 +++++++++++-------- x-pack/plugins/cases/server/services/index.ts | 31 ++++---- 6 files changed, 82 insertions(+), 82 deletions(-) diff --git a/x-pack/plugins/cases/server/authorization/authorization.ts b/x-pack/plugins/cases/server/authorization/authorization.ts index c2041aeee14c05..daa46957e14a44 100644 --- a/x-pack/plugins/cases/server/authorization/authorization.ts +++ b/x-pack/plugins/cases/server/authorization/authorization.ts @@ -7,6 +7,7 @@ import { KibanaRequest } from 'kibana/server'; import Boom from '@hapi/boom'; +import { KueryNode } from '../../../../../src/plugins/data/server'; import { SecurityPluginStart } from '../../../security/server'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; import { GetSpaceFn, ReadOperations, WriteOperations } from './types'; @@ -141,7 +142,7 @@ export class Authorization { public async getFindAuthorizationFilter( savedObjectType: string ): Promise<{ - filter?: string; + filter?: KueryNode; ensureSavedObjectIsAuthorized: (className: string) => void; }> { const { securityAuth } = this; diff --git a/x-pack/plugins/cases/server/authorization/utils.ts b/x-pack/plugins/cases/server/authorization/utils.ts index 3f66693ed4306d..435ef18c4feec5 100644 --- a/x-pack/plugins/cases/server/authorization/utils.ts +++ b/x-pack/plugins/cases/server/authorization/utils.ts @@ -6,25 +6,24 @@ */ import { remove, uniq } from 'lodash'; +import { nodeBuilder } from '../../../../../src/plugins/data/common'; +import { KueryNode } from '../../../../../src/plugins/data/server'; -export const getClassFilter = (savedObjectType: string, classNames: string[]): string => { - const firstQueryItem = - classNames.length > 0 ? `${savedObjectType}.attributes.class: ${classNames[0]}` : ''; - - const reducesQuery = classNames.slice(1).reduce((query, className) => { - ensureFieldIsSafeForQuery('class', className); - return `${query} OR ${savedObjectType}.attributes.class: ${className}`; - }, firstQueryItem); - - return `(${reducesQuery})`; +export const getClassFilter = (savedObjectType: string, classNames: string[]): KueryNode => { + return nodeBuilder.or( + classNames.reduce((query, className) => { + ensureFieldIsSafeForQuery('class', className); + query.push(nodeBuilder.is(`${savedObjectType}.attributes.class`, className)); + return query; + }, []) + ); }; export const combineFilterWithAuthorizationFilter = ( - filter: string, - authorizationFilter: string + filter: KueryNode, + authorizationFilter: KueryNode ) => { - const suffix = `AND ${authorizationFilter}`; - return filter.startsWith('(') ? `${filter} ${suffix}` : `(${filter}) ${suffix}`; + return nodeBuilder.and([filter, authorizationFilter]); }; export const ensureFieldIsSafeForQuery = (field: string, value: string): boolean => { diff --git a/x-pack/plugins/cases/server/common/types.ts b/x-pack/plugins/cases/server/common/types.ts index b58d8ec0e849e7..b99612f1b1cfec 100644 --- a/x-pack/plugins/cases/server/common/types.ts +++ b/x-pack/plugins/cases/server/common/types.ts @@ -5,6 +5,9 @@ * 2.0. */ +import { KueryNode } from '../../../../../src/plugins/data/server'; +import { SavedObjectFindOptions } from '../../common/api'; + /** * This structure holds the alert ID and index from an alert comment */ @@ -12,3 +15,7 @@ export interface AlertInfo { id: string; index: string; } + +export type SavedObjectFindOptionsKueryNode = Omit & { + filter?: KueryNode; +}; diff --git a/x-pack/plugins/cases/server/common/utils.ts b/x-pack/plugins/cases/server/common/utils.ts index dce26f3d5998a0..88cce82389c4df 100644 --- a/x-pack/plugins/cases/server/common/utils.ts +++ b/x-pack/plugins/cases/server/common/utils.ts @@ -39,28 +39,6 @@ export function createAlertUpdateRequest({ return getAlertInfoFromComments([comment]).map((alert) => ({ ...alert, status })); } -/** - * Combines multiple filter expressions using the specified operator and parenthesis if multiple expressions exist. - * This will ignore empty string filters. If a single valid filter is found it will not wrap in parenthesis. - * - * @param filters an array of filters to combine using the specified operator - * @param operator AND or OR - */ -export const combineFilters = (filters: string[] | undefined, operator: 'OR' | 'AND'): string => { - const noEmptyStrings = filters?.filter((value) => value !== ''); - const joinedExp = noEmptyStrings?.join(` ${operator} `); - // if undefined or an empty string - if (!joinedExp) { - return ''; - } else if ((noEmptyStrings?.length ?? 0) > 1) { - // if there were multiple filters, wrap them in () - return `(${joinedExp})`; - } else { - // return a single value not wrapped in () - return joinedExp; - } -}; - /** * Counts the total alert IDs within a single comment. */ diff --git a/x-pack/plugins/cases/server/routes/api/cases/helpers.ts b/x-pack/plugins/cases/server/routes/api/cases/helpers.ts index aeac3f156e2502..e53b549debd08b 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/helpers.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/helpers.ts @@ -9,6 +9,8 @@ import { get, isPlainObject } from 'lodash'; import deepEqual from 'fast-deep-equal'; import { SavedObjectsFindResponse } from 'kibana/server'; +import { nodeBuilder } from '../../../../../../../src/plugins/data/common'; +import { KueryNode } from '../../../../../../../src/plugins/data/server'; import { CaseConnector, ESCaseConnector, @@ -16,12 +18,13 @@ import { ConnectorTypes, CaseStatuses, CaseType, - SavedObjectFindOptions, + ESConnectorFields, + ConnectorTypeFields, } from '../../../../common/api'; import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../../common/constants'; import { sortToSnake } from '../utils'; -import { combineFilters } from '../../../common'; import { combineFilterWithAuthorizationFilter } from '../../../authorization/utils'; +import { SavedObjectFindOptionsKueryNode } from '../../../common'; export const addStatusFilter = ({ status, @@ -29,18 +32,19 @@ export const addStatusFilter = ({ type = CASE_SAVED_OBJECT, }: { status?: CaseStatuses; - appendFilter?: string; + appendFilter?: KueryNode; type?: string; -}) => { - const filters: string[] = []; +}): KueryNode => { + const filters: KueryNode[] = []; if (status) { - filters.push(`${type}.attributes.status: ${status}`); + filters.push(nodeBuilder.is(`${type}.attributes.status`, status)); } if (appendFilter) { filters.push(appendFilter); } - return combineFilters(filters, 'AND'); + + return nodeBuilder.and(filters); }; export const buildFilter = ({ @@ -51,19 +55,12 @@ export const buildFilter = ({ }: { filters: string | string[] | undefined; field: string; - operator: 'OR' | 'AND'; + operator: 'or' | 'and'; type?: string; -}): string => { - // if it is an empty string, empty array of strings, or undefined just return - if (!filters || filters.length <= 0) { - return ''; - } - - const arrayFilters = !Array.isArray(filters) ? [filters] : filters; - - return combineFilters( - arrayFilters.map((filter) => `${type}.attributes.${field}: ${filter}`), - operator +}): KueryNode => { + const filtersAsArray = Array.isArray(filters) ? filters : filters != null ? [filters] : []; + return nodeBuilder[operator]( + filtersAsArray.map((filter) => nodeBuilder.is(`${type}.attributes.${field}`, filter)) ); }; @@ -106,13 +103,13 @@ export const constructQueryOptions = ({ status?: CaseStatuses; sortByField?: string; caseType?: CaseType; - authorizationFilter?: string; -}): { case: SavedObjectFindOptions; subCase?: SavedObjectFindOptions } => { - const tagsFilter = buildFilter({ filters: tags, field: 'tags', operator: 'OR' }); + authorizationFilter?: KueryNode; +}): { case: SavedObjectFindOptionsKueryNode; subCase?: SavedObjectFindOptionsKueryNode } => { + const tagsFilter = buildFilter({ filters: tags, field: 'tags', operator: 'or' }); const reportersFilter = buildFilter({ filters: reporters, field: 'created_by.username', - operator: 'OR', + operator: 'or', }); const sortField = sortToSnake(sortByField); @@ -122,11 +119,15 @@ export const constructQueryOptions = ({ // The subCase filter will be undefined because we don't need to find sub cases if type === individual // We do not want to support multiple type's being used, so force it to be a single filter value - const typeFilter = `${CASE_SAVED_OBJECT}.attributes.type: ${CaseType.individual}`; + const typeFilter = nodeBuilder.is( + `${CASE_SAVED_OBJECT}.attributes.type`, + CaseType.individual + ); const caseFilters = addStatusFilter({ status, - appendFilter: combineFilters([tagsFilter, reportersFilter, typeFilter], 'AND'), + appendFilter: nodeBuilder.and([tagsFilter, reportersFilter, typeFilter]), }); + return { case: { filter: @@ -140,8 +141,11 @@ export const constructQueryOptions = ({ case CaseType.collection: { // The cases filter will result in this structure "(type == parent) and (tags == blah) and (reporter == yo)" // The sub case filter will use the query.status if it exists - const typeFilter = `${CASE_SAVED_OBJECT}.attributes.type: ${CaseType.collection}`; - const caseFilters = combineFilters([tagsFilter, reportersFilter, typeFilter], 'AND'); + const typeFilter = nodeBuilder.is( + `${CASE_SAVED_OBJECT}.attributes.type`, + CaseType.collection + ); + const caseFilters = nodeBuilder.and([tagsFilter, reportersFilter, typeFilter]); const subCaseFilters = addStatusFilter({ status, type: SUB_CASE_SAVED_OBJECT }); return { @@ -172,12 +176,18 @@ export const constructQueryOptions = ({ * The cases filter will result in this structure "((status == open and type === individual) or type == parent) and (tags == blah) and (reporter == yo)" * The sub case filter will use the query.status if it exists */ - const typeIndividual = `${CASE_SAVED_OBJECT}.attributes.type: ${CaseType.individual}`; - const typeParent = `${CASE_SAVED_OBJECT}.attributes.type: ${CaseType.collection}`; + const typeIndividual = nodeBuilder.is( + `${CASE_SAVED_OBJECT}.attributes.type`, + CaseType.individual + ); + const typeParent = nodeBuilder.is( + `${CASE_SAVED_OBJECT}.attributes.type`, + CaseType.collection + ); - const statusFilter = combineFilters([addStatusFilter({ status }), typeIndividual], 'AND'); - const statusAndType = combineFilters([statusFilter, typeParent], 'OR'); - const caseFilters = combineFilters([statusAndType, tagsFilter, reportersFilter], 'AND'); + const statusFilter = nodeBuilder.and([addStatusFilter({ status }), typeIndividual]); + const statusAndType = nodeBuilder.or([statusFilter, typeParent]); + const caseFilters = nodeBuilder.and([statusAndType, tagsFilter, reportersFilter]); const subCaseFilters = addStatusFilter({ status, type: SUB_CASE_SAVED_OBJECT }); return { diff --git a/x-pack/plugins/cases/server/services/index.ts b/x-pack/plugins/cases/server/services/index.ts index 456cf8cf83125a..c3c4c0a49f3ebc 100644 --- a/x-pack/plugins/cases/server/services/index.ts +++ b/x-pack/plugins/cases/server/services/index.ts @@ -34,7 +34,12 @@ import { caseTypeField, CasesFindRequest, } from '../../common/api'; -import { combineFilters, defaultSortField, groupTotalAlertsByID } from '../common'; +import { + combineFilters, + defaultSortField, + groupTotalAlertsByID, + SavedObjectFindOptionsKueryNode, +} from '../common'; import { defaultPage, defaultPerPage } from '../routes/api'; import { flattenCaseSavedObject, @@ -95,7 +100,7 @@ interface FindSubCaseCommentsArgs { } interface FindCasesArgs extends ClientArgs { - options?: SavedObjectFindOptions; + options?: SavedObjectFindOptionsKueryNode; } interface FindSubCasesByIDArgs extends FindCasesArgs { @@ -104,7 +109,7 @@ interface FindSubCasesByIDArgs extends FindCasesArgs { interface FindSubCasesStatusStats { client: SavedObjectsClientContract; - options: SavedObjectFindOptions; + options: SavedObjectFindOptionsKueryNode; ids: string[]; } @@ -195,7 +200,7 @@ interface CasesMapWithPageInfo { perPage: number; } -type FindCaseOptions = CasesFindRequest & SavedObjectFindOptions; +type FindCaseOptions = CasesFindRequest & SavedObjectFindOptionsKueryNode; export interface CaseServiceSetup { deleteCase(args: GetCaseArgs): Promise<{}>; @@ -244,18 +249,18 @@ export interface CaseServiceSetup { }): Promise; findSubCasesGroupByCase(args: { client: SavedObjectsClientContract; - options?: SavedObjectFindOptions; + options?: SavedObjectFindOptionsKueryNode; ids: string[]; }): Promise; findCaseStatusStats(args: { client: SavedObjectsClientContract; - caseOptions: SavedObjectFindOptions; - subCaseOptions?: SavedObjectFindOptions; + caseOptions: SavedObjectFindOptionsKueryNode; + subCaseOptions?: SavedObjectFindOptionsKueryNode; }): Promise; findCasesGroupedByID(args: { client: SavedObjectsClientContract; - caseOptions: SavedObjectFindOptions; - subCaseOptions?: SavedObjectFindOptions; + caseOptions: SavedObjectFindOptionsKueryNode; + subCaseOptions?: SavedObjectFindOptionsKueryNode; }): Promise; } @@ -275,7 +280,7 @@ export class CaseService implements CaseServiceSetup { }: { client: SavedObjectsClientContract; caseOptions: FindCaseOptions; - subCaseOptions?: SavedObjectFindOptions; + subCaseOptions?: SavedObjectFindOptionsKueryNode; }): Promise { const cases = await this.findCases({ client, @@ -358,8 +363,8 @@ export class CaseService implements CaseServiceSetup { subCaseOptions, }: { client: SavedObjectsClientContract; - caseOptions: SavedObjectFindOptions; - subCaseOptions?: SavedObjectFindOptions; + caseOptions: SavedObjectFindOptionsKueryNode; + subCaseOptions?: SavedObjectFindOptionsKueryNode; }): Promise { const casesStats = await this.findCases({ client, @@ -515,7 +520,7 @@ export class CaseService implements CaseServiceSetup { ids, }: { client: SavedObjectsClientContract; - options?: SavedObjectFindOptions; + options?: SavedObjectFindOptionsKueryNode; ids: string[]; }): Promise { const getCaseID = (subCase: SavedObjectsFindResult): string | undefined => { From a2e1da8a55327304e659d0abe4f100c8a555819f Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 26 Mar 2021 17:30:13 +0200 Subject: [PATCH 040/113] [Cases] RBAC: Rename class to scope (#95535) --- x-pack/plugins/cases/common/api/cases/case.ts | 6 +- .../cases/common/api/cases/sub_case.ts | 2 +- x-pack/plugins/cases/common/constants.ts | 2 +- .../server/authorization/authorization.ts | 95 +++++++++---------- .../cases/server/authorization/utils.ts | 10 +- .../cases/server/client/cases/create.test.ts | 18 ++-- .../cases/server/client/cases/create.ts | 2 +- .../plugins/cases/server/client/cases/find.ts | 2 +- .../server/routes/api/cases/post_case.test.ts | 12 +-- .../cases/server/saved_object_types/cases.ts | 6 +- .../server/saved_object_types/comments.ts | 2 +- .../server/saved_object_types/configure.ts | 6 +- .../saved_object_types/connector_mappings.ts | 2 +- .../server/saved_object_types/migrations.ts | 20 ++-- .../server/saved_object_types/sub_case.ts | 6 +- .../server/saved_object_types/user_actions.ts | 6 +- .../authorization/actions/cases.test.ts | 10 +- .../server/authorization/actions/cases.ts | 8 +- .../feature_privilege_builder/cases.ts | 6 +- .../cases/components/create/form_context.tsx | 2 +- .../public/cases/components/create/schema.tsx | 4 +- 21 files changed, 112 insertions(+), 115 deletions(-) diff --git a/x-pack/plugins/cases/common/api/cases/case.ts b/x-pack/plugins/cases/common/api/cases/case.ts index f529baca53b922..3477791a555614 100644 --- a/x-pack/plugins/cases/common/api/cases/case.ts +++ b/x-pack/plugins/cases/common/api/cases/case.ts @@ -38,8 +38,8 @@ const CaseBasicRt = rt.type({ [caseTypeField]: CaseTypeRt, connector: CaseConnectorRt, settings: SettingsRt, - // TODO: should a user be able to update the class? - class: rt.string, + // TODO: should a user be able to update the scope? + scope: rt.string, }); const CaseExternalServiceBasicRt = rt.type({ @@ -80,7 +80,7 @@ const CasePostRequestNoTypeRt = rt.type({ title: rt.string, connector: CaseConnectorRt, settings: SettingsRt, - class: rt.string, + scope: rt.string, }); /** diff --git a/x-pack/plugins/cases/common/api/cases/sub_case.ts b/x-pack/plugins/cases/common/api/cases/sub_case.ts index 7c8cdf023d8d72..0940f2951d4012 100644 --- a/x-pack/plugins/cases/common/api/cases/sub_case.ts +++ b/x-pack/plugins/cases/common/api/cases/sub_case.ts @@ -39,7 +39,7 @@ export const SubCasesFindRequestRt = rt.partial({ searchFields: rt.array(rt.string), sortField: rt.string, sortOrder: rt.union([rt.literal('desc'), rt.literal('asc')]), - class: rt.string, + scope: rt.string, }); export const SubCaseResponseRt = rt.intersection([ diff --git a/x-pack/plugins/cases/common/constants.ts b/x-pack/plugins/cases/common/constants.ts index 4cac3a7b73a220..6198dc0ab1f4fc 100644 --- a/x-pack/plugins/cases/common/constants.ts +++ b/x-pack/plugins/cases/common/constants.ts @@ -76,4 +76,4 @@ export const MAX_GENERATED_ALERTS_PER_SUB_CASE = MAX_ALERTS_PER_SUB_CASE / DEFAU * This must be the same value that the security solution plugin uses to define the case kind when it registers the * feature for the 7.13 migration only. */ -export const SECURITY_SOLUTION_CONSUMER = 'securitySolution'; +export const SECURITY_SOLUTION_SCOPE = 'securitySolution'; diff --git a/x-pack/plugins/cases/server/authorization/authorization.ts b/x-pack/plugins/cases/server/authorization/authorization.ts index daa46957e14a44..832ee6acccbe5b 100644 --- a/x-pack/plugins/cases/server/authorization/authorization.ts +++ b/x-pack/plugins/cases/server/authorization/authorization.ts @@ -11,7 +11,7 @@ import { KueryNode } from '../../../../../src/plugins/data/server'; import { SecurityPluginStart } from '../../../security/server'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; import { GetSpaceFn, ReadOperations, WriteOperations } from './types'; -import { getClassFilter } from './utils'; +import { getScopesFilter } from './utils'; /** * This class handles ensuring that the user making a request has the correct permissions @@ -20,7 +20,7 @@ import { getClassFilter } from './utils'; export class Authorization { private readonly request: KibanaRequest; private readonly securityAuth: SecurityPluginStart['authz'] | undefined; - private readonly featureCaseClasses: Set; + private readonly featureCaseScopes: Set; private readonly isAuthEnabled: boolean; // TODO: create this // private readonly auditLogger: AuthorizationAuditLogger; @@ -28,17 +28,17 @@ export class Authorization { private constructor({ request, securityAuth, - caseClasses, + caseScopes, isAuthEnabled, }: { request: KibanaRequest; securityAuth?: SecurityPluginStart['authz']; - caseClasses: Set; + caseScopes: Set; isAuthEnabled: boolean; }) { this.request = request; this.securityAuth = securityAuth; - this.featureCaseClasses = caseClasses; + this.featureCaseScopes = caseScopes; this.isAuthEnabled = isAuthEnabled; } @@ -59,58 +59,58 @@ export class Authorization { isAuthEnabled: boolean; }): Promise { // Since we need to do async operations, this static method handles that before creating the Auth class - let caseClasses: Set; + let caseScopes: Set; try { const disabledFeatures = new Set((await getSpace(request))?.disabledFeatures ?? []); - caseClasses = new Set( + caseScopes = new Set( features .getKibanaFeatures() - // get all the features' cases classes that aren't disabled + // get all the features' cases scopes that aren't disabled .filter(({ id }) => !disabledFeatures.has(id)) .flatMap((feature) => feature.cases ?? []) ); } catch (error) { - caseClasses = new Set(); + caseScopes = new Set(); } - return new Authorization({ request, securityAuth, caseClasses, isAuthEnabled }); + return new Authorization({ request, securityAuth, caseScopes, isAuthEnabled }); } private shouldCheckAuthorization(): boolean { return this.securityAuth?.mode?.useRbacForRequest(this.request) ?? false; } - public async ensureAuthorized(className: string, operation: ReadOperations | WriteOperations) { + public async ensureAuthorized(scope: string, operation: ReadOperations | WriteOperations) { // TODO: remove if (!this.isAuthEnabled) { return; } const { securityAuth } = this; - const isAvailableClass = this.featureCaseClasses.has(className); + const isScopeAvailable = this.featureCaseScopes.has(scope); // TODO: throw if the request is not authorized if (securityAuth && this.shouldCheckAuthorization()) { // TODO: implement ensure logic - const requiredPrivileges: string[] = [securityAuth.actions.cases.get(className, operation)]; + const requiredPrivileges: string[] = [securityAuth.actions.cases.get(scope, operation)]; const checkPrivileges = securityAuth.checkPrivilegesDynamicallyWithRequest(this.request); const { hasAllRequested, username, privileges } = await checkPrivileges({ kibana: requiredPrivileges, }); - if (!isAvailableClass) { - // TODO: throw if any of the class are not available + if (!isScopeAvailable) { + // TODO: throw if any of the scope are not available /** * Under most circumstances this would have been caught by `checkPrivileges` as - * a user can't have Privileges to an unknown class, but super users - * don't actually get "privilege checked" so the made up class *will* return + * a user can't have Privileges to an unknown scope, but super users + * don't actually get "privilege checked" so the made up scope *will* return * as Privileged. * This check will ensure we don't accidentally let these through */ // TODO: audit log using `username` - throw Boom.forbidden('User does not have permissions for this class'); + throw Boom.forbidden('User does not have permissions for this scope'); } if (hasAllRequested) { @@ -129,11 +129,11 @@ export class Authorization { // TODO: audit log // TODO: User unauthorized. throw an error. authorizedPrivileges & unauthorizedPrivilages are needed for logging. - throw Boom.forbidden('Not authorized for this class'); + throw Boom.forbidden('Not authorized for this scope'); } - } else if (!isAvailableClass) { + } else if (!isScopeAvailable) { // TODO: throw an error - throw Boom.forbidden('Security is disabled but no class was found'); + throw Boom.forbidden('Security is disabled but no scope was found'); } // else security is disabled so let the operation proceed @@ -143,46 +143,46 @@ export class Authorization { savedObjectType: string ): Promise<{ filter?: KueryNode; - ensureSavedObjectIsAuthorized: (className: string) => void; + ensureSavedObjectIsAuthorized: (scope: string) => void; }> { const { securityAuth } = this; if (securityAuth && this.shouldCheckAuthorization()) { - const { authorizedClassNames } = await this.getAuthorizedClassNames([ReadOperations.Find]); + const { authorizedScopes } = await this.getAuthorizedScopes([ReadOperations.Find]); - if (!authorizedClassNames.length) { + if (!authorizedScopes.length) { // TODO: Better error message, log error - throw Boom.forbidden('Not authorized for this class'); + throw Boom.forbidden('Not authorized for this scope'); } return { - filter: getClassFilter(savedObjectType, authorizedClassNames), - ensureSavedObjectIsAuthorized: (className: string) => { - if (!authorizedClassNames.includes(className)) { + filter: getScopesFilter(savedObjectType, authorizedScopes), + ensureSavedObjectIsAuthorized: (scope: string) => { + if (!authorizedScopes.includes(scope)) { // TODO: log error - throw Boom.forbidden('Not authorized for this class'); + throw Boom.forbidden('Not authorized for this scope'); } }, }; } - return { ensureSavedObjectIsAuthorized: (className: string) => {} }; + return { ensureSavedObjectIsAuthorized: (scope: string) => {} }; } - private async getAuthorizedClassNames( + private async getAuthorizedScopes( operations: Array ): Promise<{ username?: string; hasAllRequested: boolean; - authorizedClassNames: string[]; + authorizedScopes: string[]; }> { - const { securityAuth, featureCaseClasses } = this; + const { securityAuth, featureCaseScopes } = this; if (securityAuth && this.shouldCheckAuthorization()) { const checkPrivileges = securityAuth.checkPrivilegesDynamicallyWithRequest(this.request); const requiredPrivileges = new Map(); - for (const className of featureCaseClasses) { + for (const scope of featureCaseScopes) { for (const operation of operations) { - requiredPrivileges.set(securityAuth.actions.cases.get(className, operation), [className]); + requiredPrivileges.set(securityAuth.actions.cases.get(scope, operation), [scope]); } } @@ -193,24 +193,21 @@ export class Authorization { return { hasAllRequested, username, - authorizedClassNames: hasAllRequested - ? Array.from(featureCaseClasses) - : privileges.kibana.reduce( - (authorizedClassNames, { authorized, privilege }) => { - if (authorized && requiredPrivileges.has(privilege)) { - const [className] = requiredPrivileges.get(privilege)!; - authorizedClassNames.push(className); - } - - return authorizedClassNames; - }, - [] - ), + authorizedScopes: hasAllRequested + ? Array.from(featureCaseScopes) + : privileges.kibana.reduce((authorizedScopes, { authorized, privilege }) => { + if (authorized && requiredPrivileges.has(privilege)) { + const [scope] = requiredPrivileges.get(privilege)!; + authorizedScopes.push(scope); + } + + return authorizedScopes; + }, []), }; } else { return { hasAllRequested: true, - authorizedClassNames: Array.from(featureCaseClasses), + authorizedScopes: Array.from(featureCaseScopes), }; } } diff --git a/x-pack/plugins/cases/server/authorization/utils.ts b/x-pack/plugins/cases/server/authorization/utils.ts index 435ef18c4feec5..e06556326e98b2 100644 --- a/x-pack/plugins/cases/server/authorization/utils.ts +++ b/x-pack/plugins/cases/server/authorization/utils.ts @@ -9,11 +9,11 @@ import { remove, uniq } from 'lodash'; import { nodeBuilder } from '../../../../../src/plugins/data/common'; import { KueryNode } from '../../../../../src/plugins/data/server'; -export const getClassFilter = (savedObjectType: string, classNames: string[]): KueryNode => { +export const getScopesFilter = (savedObjectType: string, scopes: string[]): KueryNode => { return nodeBuilder.or( - classNames.reduce((query, className) => { - ensureFieldIsSafeForQuery('class', className); - query.push(nodeBuilder.is(`${savedObjectType}.attributes.class`, className)); + scopes.reduce((query, scope) => { + ensureFieldIsSafeForQuery('scope', scope); + query.push(nodeBuilder.is(`${savedObjectType}.attributes.scope`, scope)); return query; }, []) ); @@ -43,4 +43,4 @@ export const ensureFieldIsSafeForQuery = (field: string, value: string): boolean }; export const includeFieldsRequiredForAuthentication = (fields: string[]): string[] => - uniq([...fields, 'class']); + uniq([...fields, 'scope']); diff --git a/x-pack/plugins/cases/server/client/cases/create.test.ts b/x-pack/plugins/cases/server/client/cases/create.test.ts index cb37b3f810734e..9ad755725bdb79 100644 --- a/x-pack/plugins/cases/server/client/cases/create.test.ts +++ b/x-pack/plugins/cases/server/client/cases/create.test.ts @@ -45,7 +45,7 @@ describe('create', () => { settings: { syncAlerts: true, }, - class: 'awesome', + scope: 'awesome', }; const savedObjectsClient = createMockSavedObjectsRepository({ @@ -57,7 +57,7 @@ describe('create', () => { expect(res).toMatchInlineSnapshot(` Object { - "class": "awesome", + "scope": "awesome", "closed_at": null, "closed_by": null, "comments": Array [], @@ -121,7 +121,7 @@ describe('create', () => { "connector", "settings", ], - "new_value": "{\\"type\\":\\"individual\\",\\"description\\":\\"This is a brand new case of a bad meanie defacing data\\",\\"title\\":\\"Super Bad Security Issue\\",\\"tags\\":[\\"defacement\\"],\\"connector\\":{\\"id\\":\\"123\\",\\"name\\":\\"Jira\\",\\"type\\":\\".jira\\",\\"fields\\":{\\"issueType\\":\\"Task\\",\\"priority\\":\\"High\\",\\"parent\\":null}},\\"settings\\":{\\"syncAlerts\\":true},\\"class\\":\\"awesome\\"}", + "new_value": "{\\"type\\":\\"individual\\",\\"description\\":\\"This is a brand new case of a bad meanie defacing data\\",\\"title\\":\\"Super Bad Security Issue\\",\\"tags\\":[\\"defacement\\"],\\"connector\\":{\\"id\\":\\"123\\",\\"name\\":\\"Jira\\",\\"type\\":\\".jira\\",\\"fields\\":{\\"issueType\\":\\"Task\\",\\"priority\\":\\"High\\",\\"parent\\":null}},\\"settings\\":{\\"syncAlerts\\":true},\\"scope\\":\\"awesome\\"}", "old_value": null, }, "references": Array [ @@ -151,7 +151,7 @@ describe('create', () => { settings: { syncAlerts: true, }, - class: 'awesome', + scope: 'awesome', }; const savedObjectsClient = createMockSavedObjectsRepository({ @@ -162,7 +162,7 @@ describe('create', () => { expect(res).toMatchInlineSnapshot(` Object { - "class": "awesome", + "scope": "awesome", "closed_at": null, "closed_by": null, "comments": Array [], @@ -216,7 +216,7 @@ describe('create', () => { settings: { syncAlerts: true, }, - class: 'awesome', + scope: 'awesome', }; const savedObjectsClient = createMockSavedObjectsRepository({ @@ -230,7 +230,7 @@ describe('create', () => { expect(res).toMatchInlineSnapshot(` Object { - "class": "awesome", + "scope": "awesome", "closed_at": null, "closed_by": null, "comments": Array [], @@ -429,7 +429,7 @@ describe('create', () => { settings: { syncAlerts: true, }, - class: 'awesome', + scope: 'awesome', }; const savedObjectsClient = createMockSavedObjectsRepository({ @@ -458,7 +458,7 @@ describe('create', () => { settings: { syncAlerts: true, }, - class: 'awesome', + scope: 'awesome', }; const savedObjectsClient = createMockSavedObjectsRepository({ caseSavedObject: mockCases, diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index 736ee0b13baa74..2ceabad6545779 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -72,7 +72,7 @@ export const create = async ({ try { try { - await auth.ensureAuthorized(query.class, WriteOperations.Create); + await auth.ensureAuthorized(query.scope, WriteOperations.Create); } catch (error) { // TODO: log error using audit logger throw error; diff --git a/x-pack/plugins/cases/server/client/cases/find.ts b/x-pack/plugins/cases/server/client/cases/find.ts index 39c5435c8ff5db..58d7d08b2dcfd3 100644 --- a/x-pack/plugins/cases/server/client/cases/find.ts +++ b/x-pack/plugins/cases/server/client/cases/find.ts @@ -80,7 +80,7 @@ export const find = async ({ }); for (const theCase of cases.casesMap.values()) { - ensureSavedObjectIsAuthorized(theCase.class); + ensureSavedObjectIsAuthorized(theCase.scope); } // TODO: Make sure we do not leak information when authorization is on diff --git a/x-pack/plugins/cases/server/routes/api/cases/post_case.test.ts b/x-pack/plugins/cases/server/routes/api/cases/post_case.test.ts index c211ee1320694e..7c11a15b6a836f 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/post_case.test.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/post_case.test.ts @@ -46,7 +46,7 @@ describe('POST cases', () => { settings: { syncAlerts: true, }, - class: 'awesome', + scope: 'awesome', }, }); @@ -86,7 +86,7 @@ describe('POST cases', () => { settings: { syncAlerts: true, }, - class: 'awesome', + scope: 'awesome', }, }); @@ -120,7 +120,7 @@ describe('POST cases', () => { settings: { syncAlerts: true, }, - class: 'awesome', + scope: 'awesome', }, }); @@ -146,7 +146,7 @@ describe('POST cases', () => { settings: { syncAlerts: true, }, - class: 'awesome', + scope: 'awesome', }, }); @@ -180,7 +180,7 @@ describe('POST cases', () => { settings: { syncAlerts: true, }, - class: 'awesome', + scope: 'awesome', }, }); @@ -196,7 +196,7 @@ describe('POST cases', () => { expect(response.status).toEqual(200); expect(response.payload).toMatchInlineSnapshot(` Object { - "class": "awesome", + "scope": "awesome", "closed_at": null, "closed_by": null, "comments": Array [], diff --git a/x-pack/plugins/cases/server/saved_object_types/cases.ts b/x-pack/plugins/cases/server/saved_object_types/cases.ts index 4464adf8562ab4..02708b80587687 100644 --- a/x-pack/plugins/cases/server/saved_object_types/cases.ts +++ b/x-pack/plugins/cases/server/saved_object_types/cases.ts @@ -31,9 +31,6 @@ export const caseSavedObjectType: SavedObjectsType = { }, }, }, - class: { - type: 'keyword', - }, created_at: { type: 'date', }, @@ -114,6 +111,9 @@ export const caseSavedObjectType: SavedObjectsType = { title: { type: 'keyword', }, + scope: { + type: 'keyword', + }, status: { type: 'keyword', }, diff --git a/x-pack/plugins/cases/server/saved_object_types/comments.ts b/x-pack/plugins/cases/server/saved_object_types/comments.ts index 66c44f1588d02b..bba7e6fc524d99 100644 --- a/x-pack/plugins/cases/server/saved_object_types/comments.ts +++ b/x-pack/plugins/cases/server/saved_object_types/comments.ts @@ -21,7 +21,7 @@ export const caseCommentSavedObjectType: SavedObjectsType = { comment: { type: 'text', }, - class: { + scope: { type: 'keyword', }, type: { diff --git a/x-pack/plugins/cases/server/saved_object_types/configure.ts b/x-pack/plugins/cases/server/saved_object_types/configure.ts index 2b7588cca7b6e6..1d525b2a4a7349 100644 --- a/x-pack/plugins/cases/server/saved_object_types/configure.ts +++ b/x-pack/plugins/cases/server/saved_object_types/configure.ts @@ -15,9 +15,6 @@ export const caseConfigureSavedObjectType: SavedObjectsType = { namespaceType: 'single', mappings: { properties: { - class: { - type: 'keyword', - }, created_at: { type: 'date', }, @@ -60,6 +57,9 @@ export const caseConfigureSavedObjectType: SavedObjectsType = { closure_type: { type: 'keyword', }, + scope: { + type: 'keyword', + }, updated_at: { type: 'date', }, diff --git a/x-pack/plugins/cases/server/saved_object_types/connector_mappings.ts b/x-pack/plugins/cases/server/saved_object_types/connector_mappings.ts index 5bcf2dc319c717..5ac333c7e9fb7d 100644 --- a/x-pack/plugins/cases/server/saved_object_types/connector_mappings.ts +++ b/x-pack/plugins/cases/server/saved_object_types/connector_mappings.ts @@ -28,7 +28,7 @@ export const caseConnectorMappingsSavedObjectType: SavedObjectsType = { }, }, }, - class: { + scope: { type: 'keyword', }, }, diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations.ts b/x-pack/plugins/cases/server/saved_object_types/migrations.ts index 905ea2c2be3ba1..b7ba955e295ac8 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations.ts @@ -15,7 +15,7 @@ import { AssociationType, ESConnectorFields, } from '../../common/api'; -import { SECURITY_SOLUTION_CONSUMER } from '../../common/constants'; +import { SECURITY_SOLUTION_SCOPE } from '../../common/constants'; interface UnsanitizedCaseConnector { connector_id: string; @@ -61,16 +61,16 @@ interface SanitizedCaseType { } interface SanitizedCaseClass { - class: string; + scope: string; } -const addClassToSO = >( +const addScopeToSO = >( doc: SavedObjectUnsanitizedDoc ): SavedObjectSanitizedDoc => ({ ...doc, attributes: { ...doc.attributes, - class: SECURITY_SOLUTION_CONSUMER, + scope: SECURITY_SOLUTION_SCOPE, }, references: doc.references || [], }); @@ -132,7 +132,7 @@ export const caseMigrations = { '7.13.0': ( doc: SavedObjectUnsanitizedDoc> ): SavedObjectSanitizedDoc => { - return addClassToSO(doc); + return addScopeToSO(doc); }, }; @@ -159,7 +159,7 @@ export const configureMigrations = { '7.13.0': ( doc: SavedObjectUnsanitizedDoc> ): SavedObjectSanitizedDoc => { - return addClassToSO(doc); + return addScopeToSO(doc); }, }; @@ -205,7 +205,7 @@ export const userActionsMigrations = { '7.13.0': ( doc: SavedObjectUnsanitizedDoc> ): SavedObjectSanitizedDoc => { - return addClassToSO(doc); + return addScopeToSO(doc); }, }; @@ -260,7 +260,7 @@ export const commentsMigrations = { '7.13.0': ( doc: SavedObjectUnsanitizedDoc> ): SavedObjectSanitizedDoc => { - return addClassToSO(doc); + return addScopeToSO(doc); }, }; @@ -268,7 +268,7 @@ export const connectorMappingsMigrations = { '7.13.0': ( doc: SavedObjectUnsanitizedDoc> ): SavedObjectSanitizedDoc => { - return addClassToSO(doc); + return addScopeToSO(doc); }, }; @@ -276,6 +276,6 @@ export const subCasesMigrations = { '7.13.0': ( doc: SavedObjectUnsanitizedDoc> ): SavedObjectSanitizedDoc => { - return addClassToSO(doc); + return addScopeToSO(doc); }, }; diff --git a/x-pack/plugins/cases/server/saved_object_types/sub_case.ts b/x-pack/plugins/cases/server/saved_object_types/sub_case.ts index d2e49e3574e972..f7d3264ddd8974 100644 --- a/x-pack/plugins/cases/server/saved_object_types/sub_case.ts +++ b/x-pack/plugins/cases/server/saved_object_types/sub_case.ts @@ -31,9 +31,6 @@ export const subCaseSavedObjectType: SavedObjectsType = { }, }, }, - class: { - type: 'keyword', - }, created_at: { type: 'date', }, @@ -50,6 +47,9 @@ export const subCaseSavedObjectType: SavedObjectsType = { }, }, }, + scope: { + type: 'keyword', + }, status: { type: 'keyword', }, diff --git a/x-pack/plugins/cases/server/saved_object_types/user_actions.ts b/x-pack/plugins/cases/server/saved_object_types/user_actions.ts index a94b23f63c1a87..44c3029bbff1cf 100644 --- a/x-pack/plugins/cases/server/saved_object_types/user_actions.ts +++ b/x-pack/plugins/cases/server/saved_object_types/user_actions.ts @@ -37,15 +37,15 @@ export const caseUserActionSavedObjectType: SavedObjectsType = { }, }, }, - class: { - type: 'keyword', - }, new_value: { type: 'text', }, old_value: { type: 'text', }, + scope: { + type: 'keyword', + }, }, }, migrations: userActionsMigrations, diff --git a/x-pack/plugins/security/server/authorization/actions/cases.test.ts b/x-pack/plugins/security/server/authorization/actions/cases.test.ts index e1c91543570356..877f59112fd348 100644 --- a/x-pack/plugins/security/server/authorization/actions/cases.test.ts +++ b/x-pack/plugins/security/server/authorization/actions/cases.test.ts @@ -20,23 +20,23 @@ describe('#get', () => { ${{}} `(`operation of ${JSON.stringify('$operation')}`, ({ operation }) => { const actions = new CasesActions(version); - expect(() => actions.get('class', operation)).toThrowErrorMatchingSnapshot(); + expect(() => actions.get('scope', operation)).toThrowErrorMatchingSnapshot(); }); it.each` - className + scope ${null} ${undefined} ${''} ${1} ${true} ${{}} - `(`class of ${JSON.stringify('$className')}`, ({ className }) => { + `(`scope of ${JSON.stringify('$scope')}`, ({ scope }) => { const actions = new CasesActions(version); - expect(() => actions.get(className, 'operation')).toThrowErrorMatchingSnapshot(); + expect(() => actions.get(scope, 'operation')).toThrowErrorMatchingSnapshot(); }); - it('returns `cases:${class}/${operation}`', () => { + it('returns `cases:${scope}/${operation}`', () => { const alertingActions = new CasesActions(version); expect(alertingActions.get('security', 'bar-operation')).toBe( 'cases:1.0.0-zeta1:security/bar-operation' diff --git a/x-pack/plugins/security/server/authorization/actions/cases.ts b/x-pack/plugins/security/server/authorization/actions/cases.ts index c428f8c0f0ecb6..622c732513e031 100644 --- a/x-pack/plugins/security/server/authorization/actions/cases.ts +++ b/x-pack/plugins/security/server/authorization/actions/cases.ts @@ -14,15 +14,15 @@ export class CasesActions { this.prefix = `cases:${versionNumber}:`; } - public get(className: string, operation: string): string { + public get(scope: string, operation: string): string { if (!operation || !isString(operation)) { throw new Error('operation is required and must be a string'); } - if (!className || !isString(className)) { - throw new Error('class is required and must be a string'); + if (!scope || !isString(scope)) { + throw new Error('scope is required and must be a string'); } - return `${this.prefix}${className}/${operation}`; + return `${this.prefix}${scope}/${operation}`; } } 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 4b5c42361543db..3cdbc8278ac719 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 @@ -19,9 +19,9 @@ export class FeaturePrivilegeCasesBuilder extends BaseFeaturePrivilegeBuilder { privilegeDefinition: FeatureKibanaPrivileges, feature: KibanaFeature ): string[] { - const getCasesPrivilege = (operations: string[], classes: readonly string[]) => { - return classes.flatMap((className) => - operations.map((operation) => this.actions.cases.get(className, operation)) + const getCasesPrivilege = (operations: string[], scopes: readonly string[]) => { + return scopes.flatMap((scope) => + operations.map((operation) => this.actions.cases.get(scope, operation)) ); }; diff --git a/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx b/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx index 722c08552702d8..e098321829d8aa 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx @@ -84,7 +84,7 @@ export const FormContext: React.FC = ({ connector: connectorToUpdate, settings: { syncAlerts }, // TODO: need to replace this with the value that the plugin registers in the feature registration - class: 'securitySolution', + scope: 'securitySolution', }); if (afterCaseCreated && updatedCase) { diff --git a/x-pack/plugins/security_solution/public/cases/components/create/schema.tsx b/x-pack/plugins/security_solution/public/cases/components/create/schema.tsx index bc0fc0e770a834..da475e7046bf25 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/schema.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/schema.tsx @@ -19,8 +19,8 @@ export const schemaTags = { labelAppend: OptionalFieldLabel, }; -// TODO: remove class from here? -export type FormProps = Omit & { +// TODO: remove scope from here? +export type FormProps = Omit & { connectorId: string; fields: ConnectorTypeFields['fields']; syncAlerts: boolean; From 7cf9172e5cd4d056e16f6c70039ac267d76756aa Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 1 Apr 2021 18:04:26 +0300 Subject: [PATCH 041/113] [Cases][RBAC] Rename scope to owner (#96035) --- x-pack/plugins/cases/common/api/cases/case.ts | 6 +- .../cases/common/api/cases/sub_case.ts | 2 +- x-pack/plugins/cases/common/constants.ts | 2 +- .../server/authorization/authorization.ts | 84 +++++++++---------- .../cases/server/authorization/utils.ts | 10 +-- .../cases/server/client/cases/create.test.ts | 18 ++-- .../cases/server/client/cases/create.ts | 2 +- .../plugins/cases/server/client/cases/find.ts | 2 +- .../server/routes/api/cases/post_case.test.ts | 12 +-- .../cases/server/saved_object_types/cases.ts | 4 +- .../server/saved_object_types/comments.ts | 2 +- .../server/saved_object_types/configure.ts | 2 +- .../saved_object_types/connector_mappings.ts | 2 +- .../server/saved_object_types/migrations.ts | 36 ++++---- .../server/saved_object_types/sub_case.ts | 2 +- .../server/saved_object_types/user_actions.ts | 2 +- .../authorization/actions/cases.test.ts | 10 +-- .../server/authorization/actions/cases.ts | 8 +- .../feature_privilege_builder/cases.ts | 6 +- .../cases/components/create/form_context.tsx | 2 +- .../public/cases/components/create/schema.tsx | 4 +- 21 files changed, 109 insertions(+), 109 deletions(-) diff --git a/x-pack/plugins/cases/common/api/cases/case.ts b/x-pack/plugins/cases/common/api/cases/case.ts index 3477791a555614..4050b217556d3c 100644 --- a/x-pack/plugins/cases/common/api/cases/case.ts +++ b/x-pack/plugins/cases/common/api/cases/case.ts @@ -38,8 +38,8 @@ const CaseBasicRt = rt.type({ [caseTypeField]: CaseTypeRt, connector: CaseConnectorRt, settings: SettingsRt, - // TODO: should a user be able to update the scope? - scope: rt.string, + // TODO: should a user be able to update the owner? + owner: rt.string, }); const CaseExternalServiceBasicRt = rt.type({ @@ -80,7 +80,7 @@ const CasePostRequestNoTypeRt = rt.type({ title: rt.string, connector: CaseConnectorRt, settings: SettingsRt, - scope: rt.string, + owner: rt.string, }); /** diff --git a/x-pack/plugins/cases/common/api/cases/sub_case.ts b/x-pack/plugins/cases/common/api/cases/sub_case.ts index 0940f2951d4012..4bbdfd5b7d3688 100644 --- a/x-pack/plugins/cases/common/api/cases/sub_case.ts +++ b/x-pack/plugins/cases/common/api/cases/sub_case.ts @@ -39,7 +39,7 @@ export const SubCasesFindRequestRt = rt.partial({ searchFields: rt.array(rt.string), sortField: rt.string, sortOrder: rt.union([rt.literal('desc'), rt.literal('asc')]), - scope: rt.string, + owner: rt.string, }); export const SubCaseResponseRt = rt.intersection([ diff --git a/x-pack/plugins/cases/common/constants.ts b/x-pack/plugins/cases/common/constants.ts index 46364be9f0b60e..c6715f28f13f4c 100644 --- a/x-pack/plugins/cases/common/constants.ts +++ b/x-pack/plugins/cases/common/constants.ts @@ -76,7 +76,7 @@ export const MAX_GENERATED_ALERTS_PER_SUB_CASE = MAX_ALERTS_PER_SUB_CASE / DEFAU * This must be the same value that the security solution plugin uses to define the case kind when it registers the * feature for the 7.13 migration only. */ -export const SECURITY_SOLUTION_SCOPE = 'securitySolution'; +export const SECURITY_SOLUTION_OWNER = 'securitySolution'; /** * This flag governs enabling the case as a connector feature. It is disabled by default as the feature is not complete. diff --git a/x-pack/plugins/cases/server/authorization/authorization.ts b/x-pack/plugins/cases/server/authorization/authorization.ts index 832ee6acccbe5b..ab6f9c0f6fef23 100644 --- a/x-pack/plugins/cases/server/authorization/authorization.ts +++ b/x-pack/plugins/cases/server/authorization/authorization.ts @@ -11,7 +11,7 @@ import { KueryNode } from '../../../../../src/plugins/data/server'; import { SecurityPluginStart } from '../../../security/server'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; import { GetSpaceFn, ReadOperations, WriteOperations } from './types'; -import { getScopesFilter } from './utils'; +import { getOwnersFilter } from './utils'; /** * This class handles ensuring that the user making a request has the correct permissions @@ -20,7 +20,7 @@ import { getScopesFilter } from './utils'; export class Authorization { private readonly request: KibanaRequest; private readonly securityAuth: SecurityPluginStart['authz'] | undefined; - private readonly featureCaseScopes: Set; + private readonly featureCaseOwners: Set; private readonly isAuthEnabled: boolean; // TODO: create this // private readonly auditLogger: AuthorizationAuditLogger; @@ -28,17 +28,17 @@ export class Authorization { private constructor({ request, securityAuth, - caseScopes, + caseOwners, isAuthEnabled, }: { request: KibanaRequest; securityAuth?: SecurityPluginStart['authz']; - caseScopes: Set; + caseOwners: Set; isAuthEnabled: boolean; }) { this.request = request; this.securityAuth = securityAuth; - this.featureCaseScopes = caseScopes; + this.featureCaseOwners = caseOwners; this.isAuthEnabled = isAuthEnabled; } @@ -59,58 +59,58 @@ export class Authorization { isAuthEnabled: boolean; }): Promise { // Since we need to do async operations, this static method handles that before creating the Auth class - let caseScopes: Set; + let caseOwners: Set; try { const disabledFeatures = new Set((await getSpace(request))?.disabledFeatures ?? []); - caseScopes = new Set( + caseOwners = new Set( features .getKibanaFeatures() - // get all the features' cases scopes that aren't disabled + // get all the features' cases owners that aren't disabled .filter(({ id }) => !disabledFeatures.has(id)) .flatMap((feature) => feature.cases ?? []) ); } catch (error) { - caseScopes = new Set(); + caseOwners = new Set(); } - return new Authorization({ request, securityAuth, caseScopes, isAuthEnabled }); + return new Authorization({ request, securityAuth, caseOwners, isAuthEnabled }); } private shouldCheckAuthorization(): boolean { return this.securityAuth?.mode?.useRbacForRequest(this.request) ?? false; } - public async ensureAuthorized(scope: string, operation: ReadOperations | WriteOperations) { + public async ensureAuthorized(owner: string, operation: ReadOperations | WriteOperations) { // TODO: remove if (!this.isAuthEnabled) { return; } const { securityAuth } = this; - const isScopeAvailable = this.featureCaseScopes.has(scope); + const isOwnerAvailable = this.featureCaseOwners.has(owner); // TODO: throw if the request is not authorized if (securityAuth && this.shouldCheckAuthorization()) { // TODO: implement ensure logic - const requiredPrivileges: string[] = [securityAuth.actions.cases.get(scope, operation)]; + const requiredPrivileges: string[] = [securityAuth.actions.cases.get(owner, operation)]; const checkPrivileges = securityAuth.checkPrivilegesDynamicallyWithRequest(this.request); const { hasAllRequested, username, privileges } = await checkPrivileges({ kibana: requiredPrivileges, }); - if (!isScopeAvailable) { - // TODO: throw if any of the scope are not available + if (!isOwnerAvailable) { + // TODO: throw if any of the owner are not available /** * Under most circumstances this would have been caught by `checkPrivileges` as - * a user can't have Privileges to an unknown scope, but super users - * don't actually get "privilege checked" so the made up scope *will* return + * a user can't have Privileges to an unknown owner, but super users + * don't actually get "privilege checked" so the made up owner *will* return * as Privileged. * This check will ensure we don't accidentally let these through */ // TODO: audit log using `username` - throw Boom.forbidden('User does not have permissions for this scope'); + throw Boom.forbidden('User does not have permissions for this owner'); } if (hasAllRequested) { @@ -129,11 +129,11 @@ export class Authorization { // TODO: audit log // TODO: User unauthorized. throw an error. authorizedPrivileges & unauthorizedPrivilages are needed for logging. - throw Boom.forbidden('Not authorized for this scope'); + throw Boom.forbidden('Not authorized for this owner'); } - } else if (!isScopeAvailable) { + } else if (!isOwnerAvailable) { // TODO: throw an error - throw Boom.forbidden('Security is disabled but no scope was found'); + throw Boom.forbidden('Security is disabled but no owner was found'); } // else security is disabled so let the operation proceed @@ -143,46 +143,46 @@ export class Authorization { savedObjectType: string ): Promise<{ filter?: KueryNode; - ensureSavedObjectIsAuthorized: (scope: string) => void; + ensureSavedObjectIsAuthorized: (owner: string) => void; }> { const { securityAuth } = this; if (securityAuth && this.shouldCheckAuthorization()) { - const { authorizedScopes } = await this.getAuthorizedScopes([ReadOperations.Find]); + const { authorizedOwners } = await this.getAuthorizedOwners([ReadOperations.Find]); - if (!authorizedScopes.length) { + if (!authorizedOwners.length) { // TODO: Better error message, log error - throw Boom.forbidden('Not authorized for this scope'); + throw Boom.forbidden('Not authorized for this owner'); } return { - filter: getScopesFilter(savedObjectType, authorizedScopes), - ensureSavedObjectIsAuthorized: (scope: string) => { - if (!authorizedScopes.includes(scope)) { + filter: getOwnersFilter(savedObjectType, authorizedOwners), + ensureSavedObjectIsAuthorized: (owner: string) => { + if (!authorizedOwners.includes(owner)) { // TODO: log error - throw Boom.forbidden('Not authorized for this scope'); + throw Boom.forbidden('Not authorized for this owner'); } }, }; } - return { ensureSavedObjectIsAuthorized: (scope: string) => {} }; + return { ensureSavedObjectIsAuthorized: (owner: string) => {} }; } - private async getAuthorizedScopes( + private async getAuthorizedOwners( operations: Array ): Promise<{ username?: string; hasAllRequested: boolean; - authorizedScopes: string[]; + authorizedOwners: string[]; }> { - const { securityAuth, featureCaseScopes } = this; + const { securityAuth, featureCaseOwners } = this; if (securityAuth && this.shouldCheckAuthorization()) { const checkPrivileges = securityAuth.checkPrivilegesDynamicallyWithRequest(this.request); const requiredPrivileges = new Map(); - for (const scope of featureCaseScopes) { + for (const owner of featureCaseOwners) { for (const operation of operations) { - requiredPrivileges.set(securityAuth.actions.cases.get(scope, operation), [scope]); + requiredPrivileges.set(securityAuth.actions.cases.get(owner, operation), [owner]); } } @@ -193,21 +193,21 @@ export class Authorization { return { hasAllRequested, username, - authorizedScopes: hasAllRequested - ? Array.from(featureCaseScopes) - : privileges.kibana.reduce((authorizedScopes, { authorized, privilege }) => { + authorizedOwners: hasAllRequested + ? Array.from(featureCaseOwners) + : privileges.kibana.reduce((authorizedOwners, { authorized, privilege }) => { if (authorized && requiredPrivileges.has(privilege)) { - const [scope] = requiredPrivileges.get(privilege)!; - authorizedScopes.push(scope); + const [owner] = requiredPrivileges.get(privilege)!; + authorizedOwners.push(owner); } - return authorizedScopes; + return authorizedOwners; }, []), }; } else { return { hasAllRequested: true, - authorizedScopes: Array.from(featureCaseScopes), + authorizedOwners: Array.from(featureCaseOwners), }; } } diff --git a/x-pack/plugins/cases/server/authorization/utils.ts b/x-pack/plugins/cases/server/authorization/utils.ts index e06556326e98b2..b44c94d21fb5ba 100644 --- a/x-pack/plugins/cases/server/authorization/utils.ts +++ b/x-pack/plugins/cases/server/authorization/utils.ts @@ -9,11 +9,11 @@ import { remove, uniq } from 'lodash'; import { nodeBuilder } from '../../../../../src/plugins/data/common'; import { KueryNode } from '../../../../../src/plugins/data/server'; -export const getScopesFilter = (savedObjectType: string, scopes: string[]): KueryNode => { +export const getOwnersFilter = (savedObjectType: string, owners: string[]): KueryNode => { return nodeBuilder.or( - scopes.reduce((query, scope) => { - ensureFieldIsSafeForQuery('scope', scope); - query.push(nodeBuilder.is(`${savedObjectType}.attributes.scope`, scope)); + owners.reduce((query, owner) => { + ensureFieldIsSafeForQuery('owner', owner); + query.push(nodeBuilder.is(`${savedObjectType}.attributes.owner`, owner)); return query; }, []) ); @@ -43,4 +43,4 @@ export const ensureFieldIsSafeForQuery = (field: string, value: string): boolean }; export const includeFieldsRequiredForAuthentication = (fields: string[]): string[] => - uniq([...fields, 'scope']); + uniq([...fields, 'owner']); diff --git a/x-pack/plugins/cases/server/client/cases/create.test.ts b/x-pack/plugins/cases/server/client/cases/create.test.ts index 9ad755725bdb79..bd9f4da2b0131c 100644 --- a/x-pack/plugins/cases/server/client/cases/create.test.ts +++ b/x-pack/plugins/cases/server/client/cases/create.test.ts @@ -45,7 +45,7 @@ describe('create', () => { settings: { syncAlerts: true, }, - scope: 'awesome', + owner: 'awesome', }; const savedObjectsClient = createMockSavedObjectsRepository({ @@ -57,7 +57,7 @@ describe('create', () => { expect(res).toMatchInlineSnapshot(` Object { - "scope": "awesome", + "owner": "awesome", "closed_at": null, "closed_by": null, "comments": Array [], @@ -121,7 +121,7 @@ describe('create', () => { "connector", "settings", ], - "new_value": "{\\"type\\":\\"individual\\",\\"description\\":\\"This is a brand new case of a bad meanie defacing data\\",\\"title\\":\\"Super Bad Security Issue\\",\\"tags\\":[\\"defacement\\"],\\"connector\\":{\\"id\\":\\"123\\",\\"name\\":\\"Jira\\",\\"type\\":\\".jira\\",\\"fields\\":{\\"issueType\\":\\"Task\\",\\"priority\\":\\"High\\",\\"parent\\":null}},\\"settings\\":{\\"syncAlerts\\":true},\\"scope\\":\\"awesome\\"}", + "new_value": "{\\"type\\":\\"individual\\",\\"description\\":\\"This is a brand new case of a bad meanie defacing data\\",\\"title\\":\\"Super Bad Security Issue\\",\\"tags\\":[\\"defacement\\"],\\"connector\\":{\\"id\\":\\"123\\",\\"name\\":\\"Jira\\",\\"type\\":\\".jira\\",\\"fields\\":{\\"issueType\\":\\"Task\\",\\"priority\\":\\"High\\",\\"parent\\":null}},\\"settings\\":{\\"syncAlerts\\":true},\\"owner\\":\\"awesome\\"}", "old_value": null, }, "references": Array [ @@ -151,7 +151,7 @@ describe('create', () => { settings: { syncAlerts: true, }, - scope: 'awesome', + owner: 'awesome', }; const savedObjectsClient = createMockSavedObjectsRepository({ @@ -162,7 +162,7 @@ describe('create', () => { expect(res).toMatchInlineSnapshot(` Object { - "scope": "awesome", + "owner": "awesome", "closed_at": null, "closed_by": null, "comments": Array [], @@ -216,7 +216,7 @@ describe('create', () => { settings: { syncAlerts: true, }, - scope: 'awesome', + owner: 'awesome', }; const savedObjectsClient = createMockSavedObjectsRepository({ @@ -230,7 +230,7 @@ describe('create', () => { expect(res).toMatchInlineSnapshot(` Object { - "scope": "awesome", + "owner": "awesome", "closed_at": null, "closed_by": null, "comments": Array [], @@ -429,7 +429,7 @@ describe('create', () => { settings: { syncAlerts: true, }, - scope: 'awesome', + owner: 'awesome', }; const savedObjectsClient = createMockSavedObjectsRepository({ @@ -458,7 +458,7 @@ describe('create', () => { settings: { syncAlerts: true, }, - scope: 'awesome', + owner: 'awesome', }; const savedObjectsClient = createMockSavedObjectsRepository({ caseSavedObject: mockCases, diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index 32384227a6f6f9..a03bef06ddb1ad 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -83,7 +83,7 @@ export const create = async ({ try { try { - await auth.ensureAuthorized(query.scope, WriteOperations.Create); + await auth.ensureAuthorized(query.owner, WriteOperations.Create); } catch (error) { // TODO: log error using audit logger throw error; diff --git a/x-pack/plugins/cases/server/client/cases/find.ts b/x-pack/plugins/cases/server/client/cases/find.ts index 97461a40d90f65..8907a7f2dacf1f 100644 --- a/x-pack/plugins/cases/server/client/cases/find.ts +++ b/x-pack/plugins/cases/server/client/cases/find.ts @@ -80,7 +80,7 @@ export const find = async ({ }); for (const theCase of cases.casesMap.values()) { - ensureSavedObjectIsAuthorized(theCase.scope); + ensureSavedObjectIsAuthorized(theCase.owner); } // TODO: Make sure we do not leak information when authorization is on diff --git a/x-pack/plugins/cases/server/routes/api/cases/post_case.test.ts b/x-pack/plugins/cases/server/routes/api/cases/post_case.test.ts index 7c11a15b6a836f..d75dcada0a9638 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/post_case.test.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/post_case.test.ts @@ -46,7 +46,7 @@ describe('POST cases', () => { settings: { syncAlerts: true, }, - scope: 'awesome', + owner: 'awesome', }, }); @@ -86,7 +86,7 @@ describe('POST cases', () => { settings: { syncAlerts: true, }, - scope: 'awesome', + owner: 'awesome', }, }); @@ -120,7 +120,7 @@ describe('POST cases', () => { settings: { syncAlerts: true, }, - scope: 'awesome', + owner: 'awesome', }, }); @@ -146,7 +146,7 @@ describe('POST cases', () => { settings: { syncAlerts: true, }, - scope: 'awesome', + owner: 'awesome', }, }); @@ -180,7 +180,7 @@ describe('POST cases', () => { settings: { syncAlerts: true, }, - scope: 'awesome', + owner: 'awesome', }, }); @@ -196,7 +196,7 @@ describe('POST cases', () => { expect(response.status).toEqual(200); expect(response.payload).toMatchInlineSnapshot(` Object { - "scope": "awesome", + "owner": "awesome", "closed_at": null, "closed_by": null, "comments": Array [], diff --git a/x-pack/plugins/cases/server/saved_object_types/cases.ts b/x-pack/plugins/cases/server/saved_object_types/cases.ts index 02708b80587687..2a260a9bcf2ae4 100644 --- a/x-pack/plugins/cases/server/saved_object_types/cases.ts +++ b/x-pack/plugins/cases/server/saved_object_types/cases.ts @@ -108,10 +108,10 @@ export const caseSavedObjectType: SavedObjectsType = { }, }, }, - title: { + owner: { type: 'keyword', }, - scope: { + title: { type: 'keyword', }, status: { diff --git a/x-pack/plugins/cases/server/saved_object_types/comments.ts b/x-pack/plugins/cases/server/saved_object_types/comments.ts index bba7e6fc524d99..2ba6e2562a5495 100644 --- a/x-pack/plugins/cases/server/saved_object_types/comments.ts +++ b/x-pack/plugins/cases/server/saved_object_types/comments.ts @@ -21,7 +21,7 @@ export const caseCommentSavedObjectType: SavedObjectsType = { comment: { type: 'text', }, - scope: { + owner: { type: 'keyword', }, type: { diff --git a/x-pack/plugins/cases/server/saved_object_types/configure.ts b/x-pack/plugins/cases/server/saved_object_types/configure.ts index 1d525b2a4a7349..98a60ac3959874 100644 --- a/x-pack/plugins/cases/server/saved_object_types/configure.ts +++ b/x-pack/plugins/cases/server/saved_object_types/configure.ts @@ -57,7 +57,7 @@ export const caseConfigureSavedObjectType: SavedObjectsType = { closure_type: { type: 'keyword', }, - scope: { + owner: { type: 'keyword', }, updated_at: { diff --git a/x-pack/plugins/cases/server/saved_object_types/connector_mappings.ts b/x-pack/plugins/cases/server/saved_object_types/connector_mappings.ts index 5ac333c7e9fb7d..16aba01616c3dd 100644 --- a/x-pack/plugins/cases/server/saved_object_types/connector_mappings.ts +++ b/x-pack/plugins/cases/server/saved_object_types/connector_mappings.ts @@ -28,7 +28,7 @@ export const caseConnectorMappingsSavedObjectType: SavedObjectsType = { }, }, }, - scope: { + owner: { type: 'keyword', }, }, diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations.ts b/x-pack/plugins/cases/server/saved_object_types/migrations.ts index b7ba955e295ac8..20a9ed79e1c0e1 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations.ts @@ -15,7 +15,7 @@ import { AssociationType, ESConnectorFields, } from '../../common/api'; -import { SECURITY_SOLUTION_SCOPE } from '../../common/constants'; +import { SECURITY_SOLUTION_OWNER } from '../../common/constants'; interface UnsanitizedCaseConnector { connector_id: string; @@ -60,17 +60,17 @@ interface SanitizedCaseType { type: string; } -interface SanitizedCaseClass { - scope: string; +interface SanitizedCaseOwner { + owner: string; } -const addScopeToSO = >( +const addOwnerToSO = >( doc: SavedObjectUnsanitizedDoc -): SavedObjectSanitizedDoc => ({ +): SavedObjectSanitizedDoc => ({ ...doc, attributes: { ...doc.attributes, - scope: SECURITY_SOLUTION_SCOPE, + owner: SECURITY_SOLUTION_OWNER, }, references: doc.references || [], }); @@ -131,8 +131,8 @@ export const caseMigrations = { }, '7.13.0': ( doc: SavedObjectUnsanitizedDoc> - ): SavedObjectSanitizedDoc => { - return addScopeToSO(doc); + ): SavedObjectSanitizedDoc => { + return addOwnerToSO(doc); }, }; @@ -158,8 +158,8 @@ export const configureMigrations = { }, '7.13.0': ( doc: SavedObjectUnsanitizedDoc> - ): SavedObjectSanitizedDoc => { - return addScopeToSO(doc); + ): SavedObjectSanitizedDoc => { + return addOwnerToSO(doc); }, }; @@ -204,8 +204,8 @@ export const userActionsMigrations = { }, '7.13.0': ( doc: SavedObjectUnsanitizedDoc> - ): SavedObjectSanitizedDoc => { - return addScopeToSO(doc); + ): SavedObjectSanitizedDoc => { + return addOwnerToSO(doc); }, }; @@ -259,23 +259,23 @@ export const commentsMigrations = { }, '7.13.0': ( doc: SavedObjectUnsanitizedDoc> - ): SavedObjectSanitizedDoc => { - return addScopeToSO(doc); + ): SavedObjectSanitizedDoc => { + return addOwnerToSO(doc); }, }; export const connectorMappingsMigrations = { '7.13.0': ( doc: SavedObjectUnsanitizedDoc> - ): SavedObjectSanitizedDoc => { - return addScopeToSO(doc); + ): SavedObjectSanitizedDoc => { + return addOwnerToSO(doc); }, }; export const subCasesMigrations = { '7.13.0': ( doc: SavedObjectUnsanitizedDoc> - ): SavedObjectSanitizedDoc => { - return addScopeToSO(doc); + ): SavedObjectSanitizedDoc => { + return addOwnerToSO(doc); }, }; diff --git a/x-pack/plugins/cases/server/saved_object_types/sub_case.ts b/x-pack/plugins/cases/server/saved_object_types/sub_case.ts index f7d3264ddd8974..471dfebe74ae1d 100644 --- a/x-pack/plugins/cases/server/saved_object_types/sub_case.ts +++ b/x-pack/plugins/cases/server/saved_object_types/sub_case.ts @@ -47,7 +47,7 @@ export const subCaseSavedObjectType: SavedObjectsType = { }, }, }, - scope: { + owner: { type: 'keyword', }, status: { diff --git a/x-pack/plugins/cases/server/saved_object_types/user_actions.ts b/x-pack/plugins/cases/server/saved_object_types/user_actions.ts index 44c3029bbff1cf..55a79f56f84da9 100644 --- a/x-pack/plugins/cases/server/saved_object_types/user_actions.ts +++ b/x-pack/plugins/cases/server/saved_object_types/user_actions.ts @@ -43,7 +43,7 @@ export const caseUserActionSavedObjectType: SavedObjectsType = { old_value: { type: 'text', }, - scope: { + owner: { type: 'keyword', }, }, diff --git a/x-pack/plugins/security/server/authorization/actions/cases.test.ts b/x-pack/plugins/security/server/authorization/actions/cases.test.ts index 877f59112fd348..3981f49a4fe11d 100644 --- a/x-pack/plugins/security/server/authorization/actions/cases.test.ts +++ b/x-pack/plugins/security/server/authorization/actions/cases.test.ts @@ -20,23 +20,23 @@ describe('#get', () => { ${{}} `(`operation of ${JSON.stringify('$operation')}`, ({ operation }) => { const actions = new CasesActions(version); - expect(() => actions.get('scope', operation)).toThrowErrorMatchingSnapshot(); + expect(() => actions.get('owner', operation)).toThrowErrorMatchingSnapshot(); }); it.each` - scope + owner ${null} ${undefined} ${''} ${1} ${true} ${{}} - `(`scope of ${JSON.stringify('$scope')}`, ({ scope }) => { + `(`owner of ${JSON.stringify('$owner')}`, ({ owner }) => { const actions = new CasesActions(version); - expect(() => actions.get(scope, 'operation')).toThrowErrorMatchingSnapshot(); + expect(() => actions.get(owner, 'operation')).toThrowErrorMatchingSnapshot(); }); - it('returns `cases:${scope}/${operation}`', () => { + it('returns `cases:${owner}/${operation}`', () => { const alertingActions = new CasesActions(version); expect(alertingActions.get('security', 'bar-operation')).toBe( 'cases:1.0.0-zeta1:security/bar-operation' diff --git a/x-pack/plugins/security/server/authorization/actions/cases.ts b/x-pack/plugins/security/server/authorization/actions/cases.ts index 622c732513e031..63955ea9023ed2 100644 --- a/x-pack/plugins/security/server/authorization/actions/cases.ts +++ b/x-pack/plugins/security/server/authorization/actions/cases.ts @@ -14,15 +14,15 @@ export class CasesActions { this.prefix = `cases:${versionNumber}:`; } - public get(scope: string, operation: string): string { + public get(owner: string, operation: string): string { if (!operation || !isString(operation)) { throw new Error('operation is required and must be a string'); } - if (!scope || !isString(scope)) { - throw new Error('scope is required and must be a string'); + if (!owner || !isString(owner)) { + throw new Error('owner is required and must be a string'); } - return `${this.prefix}${scope}/${operation}`; + return `${this.prefix}${owner}/${operation}`; } } 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 3cdbc8278ac719..aacff3082fbca2 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 @@ -19,9 +19,9 @@ export class FeaturePrivilegeCasesBuilder extends BaseFeaturePrivilegeBuilder { privilegeDefinition: FeatureKibanaPrivileges, feature: KibanaFeature ): string[] { - const getCasesPrivilege = (operations: string[], scopes: readonly string[]) => { - return scopes.flatMap((scope) => - operations.map((operation) => this.actions.cases.get(scope, operation)) + const getCasesPrivilege = (operations: string[], owners: readonly string[]) => { + return owners.flatMap((owner) => + operations.map((operation) => this.actions.cases.get(owner, operation)) ); }; diff --git a/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx b/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx index e098321829d8aa..6c1abb516dd49c 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx @@ -84,7 +84,7 @@ export const FormContext: React.FC = ({ connector: connectorToUpdate, settings: { syncAlerts }, // TODO: need to replace this with the value that the plugin registers in the feature registration - scope: 'securitySolution', + owner: 'securitySolution', }); if (afterCaseCreated && updatedCase) { diff --git a/x-pack/plugins/security_solution/public/cases/components/create/schema.tsx b/x-pack/plugins/security_solution/public/cases/components/create/schema.tsx index da475e7046bf25..f5b7d38acde842 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/schema.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/schema.tsx @@ -19,8 +19,8 @@ export const schemaTags = { labelAppend: OptionalFieldLabel, }; -// TODO: remove scope from here? -export type FormProps = Omit & { +// TODO: remove owner from here? +export type FormProps = Omit & { connectorId: string; fields: ConnectorTypeFields['fields']; syncAlerts: boolean; From 0a95e55c2c9ca87a3693104455224761150a9c63 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 2 Apr 2021 19:07:20 +0300 Subject: [PATCH 042/113] [Cases] RBAC: Create & Find integration tests (#95511) --- x-pack/plugins/cases/common/api/cases/case.ts | 4 +- .../plugins/cases/common/api/runtime_types.ts | 4 +- x-pack/plugins/cases/common/constants.ts | 5 +- .../cases/server/authorization/mock.ts | 20 ++ .../cases/server/client/cases/create.ts | 3 +- .../plugins/cases/server/client/cases/find.ts | 13 +- .../cases/server/client/cases/update.ts | 15 +- x-pack/plugins/cases/server/client/client.ts | 3 +- .../cases/server/client/comments/add.ts | 6 +- .../plugins/cases/server/client/index.test.ts | 3 + x-pack/plugins/cases/server/client/types.ts | 3 +- .../plugins/cases/server/common/utils.test.ts | 32 +-- .../server/connectors/case/index.test.ts | 3 + .../api/__fixtures__/mock_saved_objects.ts | 4 + .../routes/api/__mocks__/request_responses.ts | 1 + .../api/cases/comments/find_comments.ts | 5 +- .../server/routes/api/cases/find_cases.ts | 2 +- .../cases/server/routes/api/cases/helpers.ts | 79 +++++-- .../api/cases/sub_case/patch_sub_cases.ts | 6 +- x-pack/plugins/cases/server/services/index.ts | 50 ++-- .../feature_privilege_builder/index.ts | 2 + .../public/cases/components/create/mock.ts | 1 + .../public/cases/containers/api.test.tsx | 1 + .../public/cases/containers/mock.ts | 1 + .../cases/containers/use_post_case.test.tsx | 1 + x-pack/scripts/functional_tests.js | 3 +- .../case_api_integration/common/config.ts | 50 ++-- .../plugins/observability/kibana.json | 10 + .../plugins/observability/package.json | 14 ++ .../plugins/observability/server/index.ts | 10 + .../plugins/observability/server/plugin.ts | 61 +++++ .../plugins/security_solution/kibana.json | 10 + .../plugins/security_solution/package.json | 14 ++ .../plugins/security_solution/server/index.ts | 10 + .../security_solution/server/plugin.ts | 61 +++++ .../common/lib/authentication/roles.ts | 58 ++++- .../common/lib/authentication/users.ts | 59 ++++- .../case_api_integration/common/lib/mock.ts | 9 + .../case_api_integration/common/lib/utils.ts | 63 +++++- .../tests/common/cases/find_cases.ts | 214 +++++++++++++++++- .../tests/common/cases/post_case.ts | 94 ++++++-- .../common/cases/sub_cases/find_sub_cases.ts | 2 +- 42 files changed, 851 insertions(+), 158 deletions(-) create mode 100644 x-pack/plugins/cases/server/authorization/mock.ts create mode 100644 x-pack/test/case_api_integration/common/fixtures/plugins/observability/kibana.json create mode 100644 x-pack/test/case_api_integration/common/fixtures/plugins/observability/package.json create mode 100644 x-pack/test/case_api_integration/common/fixtures/plugins/observability/server/index.ts create mode 100644 x-pack/test/case_api_integration/common/fixtures/plugins/observability/server/plugin.ts create mode 100644 x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/kibana.json create mode 100644 x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/package.json create mode 100644 x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/server/index.ts create mode 100644 x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/server/plugin.ts diff --git a/x-pack/plugins/cases/common/api/cases/case.ts b/x-pack/plugins/cases/common/api/cases/case.ts index 4050b217556d3c..a8b0717104304e 100644 --- a/x-pack/plugins/cases/common/api/cases/case.ts +++ b/x-pack/plugins/cases/common/api/cases/case.ts @@ -112,10 +112,10 @@ export const CasesFindRequestRt = rt.partial({ page: NumberFromString, perPage: NumberFromString, search: rt.string, - searchFields: rt.array(rt.string), + searchFields: rt.union([rt.array(rt.string), rt.string]), sortField: rt.string, sortOrder: rt.union([rt.literal('desc'), rt.literal('asc')]), - class: rt.string, + owner: rt.union([rt.array(rt.string), rt.string]), }); export const CaseResponseRt = rt.intersection([ diff --git a/x-pack/plugins/cases/common/api/runtime_types.ts b/x-pack/plugins/cases/common/api/runtime_types.ts index b2ff763838287e..9785c0f4107440 100644 --- a/x-pack/plugins/cases/common/api/runtime_types.ts +++ b/x-pack/plugins/cases/common/api/runtime_types.ts @@ -58,7 +58,9 @@ const getExcessProps = (props: rt.Props, r: Record): string[] = return ex; }; -export function excess>(codec: C): C { +export function excess | rt.PartialType>( + codec: C +): C { const r = new rt.InterfaceType( codec.name, codec.is, diff --git a/x-pack/plugins/cases/common/constants.ts b/x-pack/plugins/cases/common/constants.ts index c6715f28f13f4c..8489787bc5a6f9 100644 --- a/x-pack/plugins/cases/common/constants.ts +++ b/x-pack/plugins/cases/common/constants.ts @@ -17,7 +17,6 @@ export const CASE_CONFIGURE_SAVED_OBJECT = 'cases-configure'; export const SAVED_OBJECT_TYPES = [ CASE_SAVED_OBJECT, CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, - SUB_CASE_SAVED_OBJECT, CASE_USER_ACTION_SAVED_OBJECT, CASE_COMMENT_SAVED_OBJECT, CASE_CONFIGURE_SAVED_OBJECT, @@ -82,3 +81,7 @@ export const SECURITY_SOLUTION_OWNER = 'securitySolution'; * This flag governs enabling the case as a connector feature. It is disabled by default as the feature is not complete. */ export const ENABLE_CASE_CONNECTOR = false; + +if (ENABLE_CASE_CONNECTOR) { + SAVED_OBJECT_TYPES.push(SUB_CASE_SAVED_OBJECT); +} diff --git a/x-pack/plugins/cases/server/authorization/mock.ts b/x-pack/plugins/cases/server/authorization/mock.ts new file mode 100644 index 00000000000000..1fc3395c8e43f9 --- /dev/null +++ b/x-pack/plugins/cases/server/authorization/mock.ts @@ -0,0 +1,20 @@ +/* + * 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 type { PublicMethodsOf } from '@kbn/utility-types'; +import { Authorization } from './authorization'; + +type Schema = PublicMethodsOf; +export type AuthorizationMock = jest.Mocked; + +export const createAuthorizationMock = () => { + const mocked: AuthorizationMock = { + ensureAuthorized: jest.fn(), + getFindAuthorizationFilter: jest.fn(), + }; + return mocked; +}; diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index a03bef06ddb1ad..34fdb7aff14a21 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -10,6 +10,7 @@ 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 } from 'src/core/server'; import { flattenCaseSavedObject, transformNewCase } from '../../routes/api/utils'; @@ -47,7 +48,7 @@ interface CreateCaseArgs { userActionService: CaseUserActionServiceSetup; theCase: CasePostRequest; logger: Logger; - auth: Authorization; + auth: PublicMethodsOf; } /** diff --git a/x-pack/plugins/cases/server/client/cases/find.ts b/x-pack/plugins/cases/server/client/cases/find.ts index 8907a7f2dacf1f..24e8cb6ec5f886 100644 --- a/x-pack/plugins/cases/server/client/cases/find.ts +++ b/x-pack/plugins/cases/server/client/cases/find.ts @@ -11,6 +11,7 @@ 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, @@ -18,6 +19,7 @@ import { throwErrors, caseStatuses, CasesFindResponseRt, + excess, } from '../../../common/api'; import { CASE_SAVED_OBJECT } from '../../../common/constants'; @@ -32,7 +34,7 @@ interface FindParams { savedObjectsClient: SavedObjectsClientContract; caseService: CaseServiceSetup; logger: Logger; - auth: Authorization; + auth: PublicMethodsOf; options: CasesFindRequest; } @@ -48,7 +50,7 @@ export const find = async ({ }: FindParams): Promise => { try { const queryParams = pipe( - CasesFindRequestRt.decode(options), + excess(CasesFindRequestRt).decode(options), fold(throwErrors(Boom.badRequest), identity) ); @@ -64,6 +66,7 @@ export const find = async ({ sortByField: queryParams.sortField, status: queryParams.status, caseType: queryParams.type, + owner: queryParams.owner, }; const caseQueries = constructQueryOptions({ ...queryArgs, authorizationFilter }); @@ -72,6 +75,12 @@ export const find = async ({ caseOptions: { ...queryParams, ...caseQueries.case, + searchFields: + queryParams.searchFields != null + ? Array.isArray(queryParams.searchFields) + ? queryParams.searchFields + : [queryParams.searchFields] + : queryParams.searchFields, fields: queryParams.fields ? includeFieldsRequiredForAuthentication(queryParams.fields) : queryParams.fields, diff --git a/x-pack/plugins/cases/server/client/cases/update.ts b/x-pack/plugins/cases/server/client/cases/update.ts index 5f5a2b16f43325..fa9df2060ac5b7 100644 --- a/x-pack/plugins/cases/server/client/cases/update.ts +++ b/x-pack/plugins/cases/server/client/cases/update.ts @@ -17,6 +17,8 @@ import { SavedObjectsFindResult, Logger, } from 'kibana/server'; + +import { nodeBuilder } from '../../../../../../src/plugins/data/common'; import { flattenCaseSavedObject, isCommentRequestTypeAlertOrGenAlert, @@ -134,7 +136,13 @@ async function throwIfInvalidUpdateOfTypeWithAlerts({ options: { fields: [], // there should never be generated alerts attached to an individual case but we'll check anyway - filter: `${CASE_COMMENT_SAVED_OBJECT}.attributes.type: ${CommentType.alert} OR ${CASE_COMMENT_SAVED_OBJECT}.attributes.type: ${CommentType.generatedAlert}`, + filter: nodeBuilder.or([ + nodeBuilder.is(`${CASE_COMMENT_SAVED_OBJECT}.attributes.type`, CommentType.alert), + nodeBuilder.is( + `${CASE_COMMENT_SAVED_OBJECT}.attributes.type`, + CommentType.generatedAlert + ), + ]), page: 1, perPage: 1, }, @@ -191,7 +199,10 @@ async function getAlertComments({ id: idsOfCasesToSync, includeSubCaseComments: true, options: { - filter: `${CASE_COMMENT_SAVED_OBJECT}.attributes.type: ${CommentType.alert} OR ${CASE_COMMENT_SAVED_OBJECT}.attributes.type: ${CommentType.generatedAlert}`, + filter: nodeBuilder.or([ + nodeBuilder.is(`${CASE_COMMENT_SAVED_OBJECT}.attributes.type`, CommentType.alert), + nodeBuilder.is(`${CASE_COMMENT_SAVED_OBJECT}.attributes.type`, CommentType.generatedAlert), + ]), }, }); } diff --git a/x-pack/plugins/cases/server/client/client.ts b/x-pack/plugins/cases/server/client/client.ts index c0da5b7bc6bb5a..e9bfd1ef754b05 100644 --- a/x-pack/plugins/cases/server/client/client.ts +++ b/x-pack/plugins/cases/server/client/client.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { PublicMethodsOf } from '@kbn/utility-types'; import { ElasticsearchClient, SavedObjectsClientContract, Logger } from 'src/core/server'; import { CasesClientConstructorArguments, @@ -53,7 +54,7 @@ export class CasesClientHandler implements CasesClient { private readonly _userActionService: CaseUserActionServiceSetup; private readonly _alertsService: AlertServiceContract; private readonly logger: Logger; - private readonly authorization: Authorization; + private readonly authorization: PublicMethodsOf; constructor(clientArgs: CasesClientConstructorArguments) { this._scopedClusterClient = clientArgs.scopedClusterClient; diff --git a/x-pack/plugins/cases/server/client/comments/add.ts b/x-pack/plugins/cases/server/client/comments/add.ts index d8fe985b6c1ea1..f077571019f600 100644 --- a/x-pack/plugins/cases/server/client/comments/add.ts +++ b/x-pack/plugins/cases/server/client/comments/add.ts @@ -11,6 +11,7 @@ import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { SavedObject, SavedObjectsClientContract, Logger } from 'src/core/server'; +import { nodeBuilder } from '../../../../../../src/plugins/data/common'; import { decodeCommentRequest, isCommentRequestTypeGenAlert } from '../../routes/api/utils'; import { @@ -63,7 +64,10 @@ async function getSubCase({ id: mostRecentSubCase.id, options: { fields: [], - filter: `${CASE_COMMENT_SAVED_OBJECT}.attributes.type: ${CommentType.generatedAlert}`, + filter: nodeBuilder.is( + `${CASE_COMMENT_SAVED_OBJECT}.attributes.type`, + CommentType.generatedAlert + ), page: 1, perPage: 1, }, diff --git a/x-pack/plugins/cases/server/client/index.test.ts b/x-pack/plugins/cases/server/client/index.test.ts index cfb30d6d5bcb6c..455e4ae1066889 100644 --- a/x-pack/plugins/cases/server/client/index.test.ts +++ b/x-pack/plugins/cases/server/client/index.test.ts @@ -18,6 +18,7 @@ import { createUserActionServiceMock, createAlertServiceMock, } from '../services/mocks'; +import { createAuthorizationMock } from '../authorization/mock'; jest.mock('./client'); import { CasesClientHandler } from './client'; @@ -31,6 +32,7 @@ const caseService = createCaseServiceMock(); const connectorMappingsService = connectorMappingsServiceMock(); const savedObjectsClient = savedObjectsClientMock.create(); const userActionService = createUserActionServiceMock(); +const authorization = createAuthorizationMock(); describe('createExternalCasesClient()', () => { test('it creates the client correctly', async () => { @@ -44,6 +46,7 @@ describe('createExternalCasesClient()', () => { savedObjectsClient, userActionService, logger, + authorization, }); expect(CasesClientHandler).toHaveBeenCalledTimes(1); }); diff --git a/x-pack/plugins/cases/server/client/types.ts b/x-pack/plugins/cases/server/client/types.ts index 1e3251df91aba0..b0276fc10aadae 100644 --- a/x-pack/plugins/cases/server/client/types.ts +++ b/x-pack/plugins/cases/server/client/types.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { PublicMethodsOf } from '@kbn/utility-types'; import { ElasticsearchClient, SavedObjectsClientContract, Logger } from 'kibana/server'; import { ActionsClient } from '../../../actions/server'; import { @@ -78,7 +79,7 @@ export interface CasesClientConstructorArguments { userActionService: CaseUserActionServiceSetup; alertsService: AlertServiceContract; logger: Logger; - authorization: Authorization; + authorization: PublicMethodsOf; } export interface ConfigureFields { diff --git a/x-pack/plugins/cases/server/common/utils.test.ts b/x-pack/plugins/cases/server/common/utils.test.ts index 5e6a86358de256..46e73c8b5d79cf 100644 --- a/x-pack/plugins/cases/server/common/utils.test.ts +++ b/x-pack/plugins/cases/server/common/utils.test.ts @@ -8,7 +8,7 @@ import { SavedObjectsFindResponse } from 'kibana/server'; import { AssociationType, CommentAttributes, CommentRequest, CommentType } from '../../common/api'; import { transformNewComment } from '../routes/api/utils'; -import { combineFilters, countAlerts, countAlertsForID, groupTotalAlertsByID } from './utils'; +import { countAlerts, countAlertsForID, groupTotalAlertsByID } from './utils'; interface CommentReference { ids: string[]; @@ -47,36 +47,6 @@ function createCommentFindResponse( } describe('common utils', () => { - describe('combineFilters', () => { - it("creates a filter string with two values and'd together", () => { - expect(combineFilters(['a', 'b'], 'AND')).toBe('(a AND b)'); - }); - - it('creates a filter string with three values or together', () => { - expect(combineFilters(['a', 'b', 'c'], 'OR')).toBe('(a OR b OR c)'); - }); - - it('ignores empty strings', () => { - expect(combineFilters(['', 'a', '', 'b'], 'AND')).toBe('(a AND b)'); - }); - - it('returns an empty string if all filters are empty strings', () => { - expect(combineFilters(['', ''], 'OR')).toBe(''); - }); - - it('returns an empty string if the filters are undefined', () => { - expect(combineFilters(undefined, 'OR')).toBe(''); - }); - - it('returns a value without parenthesis when only a single filter is provided', () => { - expect(combineFilters(['a'], 'OR')).toBe('a'); - }); - - it('returns a string without parenthesis when only a single non empty filter is provided', () => { - expect(combineFilters(['', ''], 'AND')).toBe(''); - }); - }); - describe('countAlerts', () => { it('returns 0 when no alerts are found', () => { expect( diff --git a/x-pack/plugins/cases/server/connectors/case/index.test.ts b/x-pack/plugins/cases/server/connectors/case/index.test.ts index c21761dba0acba..95fe562d9e140d 100644 --- a/x-pack/plugins/cases/server/connectors/case/index.test.ts +++ b/x-pack/plugins/cases/server/connectors/case/index.test.ts @@ -981,6 +981,7 @@ describe('case connector', () => { settings: { syncAlerts: true, }, + owner: 'securitySolution', }; mockCasesClient.create.mockReturnValue(Promise.resolve(createReturn)); @@ -1077,6 +1078,7 @@ describe('case connector', () => { settings: { syncAlerts: true, }, + owner: 'securitySolution', }, ]; @@ -1168,6 +1170,7 @@ describe('case connector', () => { settings: { syncAlerts: true, }, + owner: 'securitySolution', }; mockCasesClient.addComment.mockReturnValue(Promise.resolve(commentReturn)); 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 e37b3a2ac257b9..bb4e529192df33 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 @@ -58,6 +58,7 @@ export const mockCases: Array> = [ settings: { syncAlerts: true, }, + owner: 'securitySolution', }, references: [], updated_at: '2019-11-25T21:54:48.952Z', @@ -96,6 +97,7 @@ export const mockCases: Array> = [ settings: { syncAlerts: true, }, + owner: 'securitySolution', }, references: [], updated_at: '2019-11-25T22:32:00.900Z', @@ -138,6 +140,7 @@ export const mockCases: Array> = [ settings: { syncAlerts: true, }, + owner: 'securitySolution', }, references: [], updated_at: '2019-11-25T22:32:17.947Z', @@ -184,6 +187,7 @@ export const mockCases: Array> = [ settings: { syncAlerts: true, }, + owner: 'securitySolution', }, references: [], updated_at: '2019-11-25T22:32:17.947Z', diff --git a/x-pack/plugins/cases/server/routes/api/__mocks__/request_responses.ts b/x-pack/plugins/cases/server/routes/api/__mocks__/request_responses.ts index ae14b44e7dffe8..7419452f27c0ad 100644 --- a/x-pack/plugins/cases/server/routes/api/__mocks__/request_responses.ts +++ b/x-pack/plugins/cases/server/routes/api/__mocks__/request_responses.ts @@ -27,6 +27,7 @@ export const newCase: CasePostRequest = { settings: { syncAlerts: true, }, + owner: 'securitySolution', }; export const getActions = (): FindActionResult[] => [ diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/find_comments.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/find_comments.ts index f7ae8db4d96aa1..9e23a28c0725b2 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/find_comments.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/find_comments.ts @@ -14,6 +14,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; +import { esKuery } from '../../../../../../../../src/plugins/data/server'; import { AssociationType, CommentsResponseRt, @@ -63,6 +64,7 @@ export function initFindCaseCommentsApi({ caseService, router, logger }: RouteDe const id = query.subCaseId ?? request.params.case_id; const associationType = query.subCaseId ? AssociationType.subCase : AssociationType.case; + const { filter, ...queryWithoutFilter } = query; const args = query ? { caseService, @@ -75,7 +77,8 @@ export function initFindCaseCommentsApi({ caseService, router, logger }: RouteDe page: defaultPage, perPage: defaultPerPage, sortField: 'created_at', - ...query, + filter: filter != null ? esKuery.fromKueryExpression(filter) : filter, + ...queryWithoutFilter, }, associationType, } diff --git a/x-pack/plugins/cases/server/routes/api/cases/find_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/find_cases.ts index 1c7eed480eedf9..7bee574894d39a 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/find_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/find_cases.ts @@ -24,7 +24,7 @@ export function initFindCasesApi({ caseService, router, logger }: RouteDeps) { return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); } const casesClient = await context.cases.getCasesClient(); - const options = request.body as CasesFindRequest; + const options = request.query as CasesFindRequest; return response.ok({ body: await casesClient.find({ ...options }), diff --git a/x-pack/plugins/cases/server/routes/api/cases/helpers.ts b/x-pack/plugins/cases/server/routes/api/cases/helpers.ts index 7717a5241fe94c..697b4d5df7ad1d 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/helpers.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/helpers.ts @@ -31,20 +31,18 @@ export const addStatusFilter = ({ appendFilter, type = CASE_SAVED_OBJECT, }: { - status?: CaseStatuses; + status: CaseStatuses; appendFilter?: KueryNode; type?: string; }): KueryNode => { const filters: KueryNode[] = []; - if (status) { - filters.push(nodeBuilder.is(`${type}.attributes.status`, status)); - } + filters.push(nodeBuilder.is(`${type}.attributes.status`, status)); if (appendFilter) { filters.push(appendFilter); } - return nodeBuilder.and(filters); + return filters.length > 1 ? nodeBuilder.and(filters) : filters[0]; }; export const buildFilter = ({ @@ -53,12 +51,17 @@ export const buildFilter = ({ operator, type = CASE_SAVED_OBJECT, }: { - filters: string | string[] | undefined; + filters: string | string[]; field: string; operator: 'or' | 'and'; type?: string; -}): KueryNode => { - const filtersAsArray = Array.isArray(filters) ? filters : filters != null ? [filters] : []; +}): KueryNode | null => { + const filtersAsArray = Array.isArray(filters) ? filters : [filters]; + + if (filtersAsArray.length === 0) { + return null; + } + return nodeBuilder[operator]( filtersAsArray.map((filter) => nodeBuilder.is(`${type}.attributes.${field}`, filter)) ); @@ -96,6 +99,7 @@ export const constructQueryOptions = ({ status, sortByField, caseType, + owner, authorizationFilter, }: { tags?: string | string[]; @@ -103,15 +107,20 @@ export const constructQueryOptions = ({ status?: CaseStatuses; sortByField?: string; caseType?: CaseType; + owner?: string | string[]; authorizationFilter?: KueryNode; }): { case: SavedObjectFindOptionsKueryNode; subCase?: SavedObjectFindOptionsKueryNode } => { - const tagsFilter = buildFilter({ filters: tags, field: 'tags', operator: 'or' }); + const kueryNodeExists = (filter: KueryNode | null | undefined): filter is KueryNode => + filter != null; + + const tagsFilter = buildFilter({ filters: tags ?? [], field: 'tags', operator: 'or' }); const reportersFilter = buildFilter({ - filters: reporters, + filters: reporters ?? [], field: 'created_by.username', operator: 'or', }); const sortField = sortToSnake(sortByField); + const ownerFilter = buildFilter({ filters: owner ?? [], field: 'owner', operator: 'or' }); switch (caseType) { case CaseType.individual: { @@ -123,15 +132,23 @@ export const constructQueryOptions = ({ `${CASE_SAVED_OBJECT}.attributes.type`, CaseType.individual ); - const caseFilters = addStatusFilter({ - status, - appendFilter: nodeBuilder.and([tagsFilter, reportersFilter, typeFilter]), - }); + + const filters: KueryNode[] = [typeFilter, tagsFilter, reportersFilter, ownerFilter].filter( + kueryNodeExists + ); + + const caseFilters = + status != null + ? addStatusFilter({ + status, + appendFilter: filters.length > 1 ? nodeBuilder.and(filters) : filters[0], + }) + : undefined; return { case: { filter: - authorizationFilter != null + authorizationFilter != null && caseFilters != null ? combineFilterWithAuthorizationFilter(caseFilters, authorizationFilter) : caseFilters, sortField, @@ -145,8 +162,13 @@ export const constructQueryOptions = ({ `${CASE_SAVED_OBJECT}.attributes.type`, CaseType.collection ); - const caseFilters = nodeBuilder.and([tagsFilter, reportersFilter, typeFilter]); - const subCaseFilters = addStatusFilter({ status, type: SUB_CASE_SAVED_OBJECT }); + + const filters: KueryNode[] = [typeFilter, tagsFilter, reportersFilter, ownerFilter].filter( + kueryNodeExists + ); + const caseFilters = filters.length > 1 ? nodeBuilder.and(filters) : filters[0]; + const subCaseFilters = + status != null ? addStatusFilter({ status, type: SUB_CASE_SAVED_OBJECT }) : undefined; return { case: { @@ -158,8 +180,8 @@ export const constructQueryOptions = ({ }, subCase: { filter: - authorizationFilter != null - ? combineFilterWithAuthorizationFilter(caseFilters, authorizationFilter) + authorizationFilter != null && subCaseFilters != null + ? combineFilterWithAuthorizationFilter(subCaseFilters, authorizationFilter) : subCaseFilters, sortField, }, @@ -185,10 +207,19 @@ export const constructQueryOptions = ({ CaseType.collection ); - const statusFilter = nodeBuilder.and([addStatusFilter({ status }), typeIndividual]); + const statusFilter = + status != null + ? nodeBuilder.and([addStatusFilter({ status }), typeIndividual]) + : typeIndividual; const statusAndType = nodeBuilder.or([statusFilter, typeParent]); - const caseFilters = nodeBuilder.and([statusAndType, tagsFilter, reportersFilter]); - const subCaseFilters = addStatusFilter({ status, type: SUB_CASE_SAVED_OBJECT }); + + const filters: KueryNode[] = [statusAndType, tagsFilter, reportersFilter, ownerFilter].filter( + kueryNodeExists + ); + + const caseFilters = filters.length > 1 ? nodeBuilder.and(filters) : filters[0]; + const subCaseFilters = + status != null ? addStatusFilter({ status, type: SUB_CASE_SAVED_OBJECT }) : undefined; return { case: { @@ -200,8 +231,8 @@ export const constructQueryOptions = ({ }, subCase: { filter: - authorizationFilter != null - ? combineFilterWithAuthorizationFilter(caseFilters, authorizationFilter) + authorizationFilter != null && subCaseFilters != null + ? combineFilterWithAuthorizationFilter(subCaseFilters, authorizationFilter) : subCaseFilters, sortField, }, 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/cases/sub_case/patch_sub_cases.ts index 3808cd3dc45ddc..5b623815f027fa 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/sub_case/patch_sub_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/sub_case/patch_sub_cases.ts @@ -17,6 +17,7 @@ import { Logger, } from 'kibana/server'; +import { nodeBuilder } from '../../../../../../../../src/plugins/data/common'; import { CasesClient } from '../../../../client'; import { CaseServiceSetup, CaseUserActionServiceSetup } from '../../../../services'; import { @@ -209,7 +210,10 @@ async function getAlertComments({ client, id: ids, options: { - filter: `${CASE_COMMENT_SAVED_OBJECT}.attributes.type: ${CommentType.alert} OR ${CASE_COMMENT_SAVED_OBJECT}.attributes.type: ${CommentType.generatedAlert}`, + filter: nodeBuilder.or([ + nodeBuilder.is(`${CASE_COMMENT_SAVED_OBJECT}.attributes.type`, CommentType.alert), + nodeBuilder.is(`${CASE_COMMENT_SAVED_OBJECT}.attributes.type`, CommentType.generatedAlert), + ]), }, }); } diff --git a/x-pack/plugins/cases/server/services/index.ts b/x-pack/plugins/cases/server/services/index.ts index ccbd806d439848..cb275b3f5d44d3 100644 --- a/x-pack/plugins/cases/server/services/index.ts +++ b/x-pack/plugins/cases/server/services/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { cloneDeep } from 'lodash'; import { KibanaRequest, Logger, @@ -25,7 +26,6 @@ import { AuthenticatedUser, SecurityPluginSetup } from '../../../security/server import { ESCaseAttributes, CommentAttributes, - SavedObjectFindOptions, User, CommentPatchAttributes, SubCaseAttributes, @@ -82,20 +82,20 @@ interface GetSubCasesArgs extends ClientArgs { interface FindCommentsArgs { client: SavedObjectsClientContract; id: string | string[]; - options?: SavedObjectFindOptions; + options?: SavedObjectFindOptionsKueryNode; } interface FindCaseCommentsArgs { client: SavedObjectsClientContract; id: string | string[]; - options?: SavedObjectFindOptions; + options?: SavedObjectFindOptionsKueryNode; includeSubCaseComments?: boolean; } interface FindSubCaseCommentsArgs { client: SavedObjectsClientContract; id: string | string[]; - options?: SavedObjectFindOptions; + options?: SavedObjectFindOptionsKueryNode; } interface FindCasesArgs extends ClientArgs { @@ -186,7 +186,7 @@ interface FindCommentsByAssociationArgs { client: SavedObjectsClientContract; id: string | string[]; associationType: AssociationType; - options?: SavedObjectFindOptions; + options?: SavedObjectFindOptionsKueryNode; } interface Collection { @@ -419,7 +419,7 @@ export class CaseService implements CaseServiceSetup { if (ENABLE_CASE_CONNECTOR && subCaseOptions) { subCasesTotal = await this.findSubCaseStatusStats({ client, - options: subCaseOptions, + options: cloneDeep(subCaseOptions), ids: caseIds, }); } @@ -493,7 +493,13 @@ export class CaseService implements CaseServiceSetup { associationType, id: ids, options: { - filter: `(${CASE_COMMENT_SAVED_OBJECT}.attributes.type: ${CommentType.alert} OR ${CASE_COMMENT_SAVED_OBJECT}.attributes.type: ${CommentType.generatedAlert})`, + filter: nodeBuilder.or([ + nodeBuilder.is(`${CASE_COMMENT_SAVED_OBJECT}.attributes.type`, CommentType.alert), + nodeBuilder.is( + `${CASE_COMMENT_SAVED_OBJECT}.attributes.type`, + CommentType.generatedAlert + ), + ]), }, }); @@ -768,7 +774,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to find cases`); return await client.find({ sortField: defaultSortField, - ...options, + ...cloneDeep(options), type: CASE_SAVED_OBJECT, }); } catch (error) { @@ -788,7 +794,7 @@ export class CaseService implements CaseServiceSetup { if (options?.page !== undefined || options?.perPage !== undefined) { return client.find({ sortField: defaultSortField, - ...options, + ...cloneDeep(options), type: SUB_CASE_SAVED_OBJECT, }); } @@ -798,14 +804,14 @@ export class CaseService implements CaseServiceSetup { page: 1, perPage: 1, sortField: defaultSortField, - ...options, + ...cloneDeep(options), type: SUB_CASE_SAVED_OBJECT, }); return client.find({ page: 1, perPage: stats.total, sortField: defaultSortField, - ...options, + ...cloneDeep(options), type: SUB_CASE_SAVED_OBJECT, }); } catch (error) { @@ -875,7 +881,7 @@ export class CaseService implements CaseServiceSetup { return client.find({ type: CASE_COMMENT_SAVED_OBJECT, sortField: defaultSortField, - ...options, + ...cloneDeep(options), }); } // get the total number of comments that are in ES then we'll grab them all in one go @@ -886,7 +892,7 @@ export class CaseService implements CaseServiceSetup { perPage: 1, sortField: defaultSortField, // spread the options after so the caller can override the default behavior if they want - ...options, + ...cloneDeep(options), }); return client.find({ @@ -894,7 +900,7 @@ export class CaseService implements CaseServiceSetup { page: 1, perPage: stats.total, sortField: defaultSortField, - ...options, + ...cloneDeep(options), }); } catch (error) { this.log.error(`Error on GET all comments for ${JSON.stringify(id)}: ${error}`); @@ -929,13 +935,15 @@ export class CaseService implements CaseServiceSetup { let filter: KueryNode | undefined; if (!includeSubCaseComments) { // if other filters were passed in then combine them to filter out sub case comments - filter = nodeBuilder.and([ - options?.filter, - nodeBuilder.is( - `${CASE_COMMENT_SAVED_OBJECT}.attributes.associationType`, - AssociationType.case - ), - ]); + const associationFilter = nodeBuilder.is( + `${CASE_COMMENT_SAVED_OBJECT}.attributes.associationType`, + AssociationType.case + ); + + filter = + options?.filter != null + ? nodeBuilder.and([options?.filter, associationFilter]) + : associationFilter; } this.log.debug(`Attempting to GET all comments for case caseID ${JSON.stringify(id)}`); diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/index.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/index.ts index 21cf2421ce1b20..81d13390523014 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/index.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/index.ts @@ -12,6 +12,7 @@ import type { Actions } from '../../actions'; import { FeaturePrivilegeAlertingBuilder } from './alerting'; import { FeaturePrivilegeApiBuilder } from './api'; import { FeaturePrivilegeAppBuilder } from './app'; +import { FeaturePrivilegeCasesBuilder } from './cases'; import { FeaturePrivilegeCatalogueBuilder } from './catalogue'; import { FeaturePrivilegeBuilder } from './feature_privilege_builder'; import { FeaturePrivilegeManagementBuilder } from './management'; @@ -31,6 +32,7 @@ export const featurePrivilegeBuilderFactory = (actions: Actions): FeaturePrivile new FeaturePrivilegeSavedObjectBuilder(actions), new FeaturePrivilegeUIBuilder(actions), new FeaturePrivilegeAlertingBuilder(actions), + new FeaturePrivilegeCasesBuilder(actions), ]; return { diff --git a/x-pack/plugins/security_solution/public/cases/components/create/mock.ts b/x-pack/plugins/security_solution/public/cases/components/create/mock.ts index 6e17be8d53e5a7..277e51f886ab03 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/mock.ts +++ b/x-pack/plugins/security_solution/public/cases/components/create/mock.ts @@ -24,6 +24,7 @@ export const sampleData: CasePostRequest = { settings: { syncAlerts: true, }, + owner: 'securitySolution', }; export const sampleConnectorData = { loading: false, connectors: [] }; diff --git a/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx index e6ecf45097a1a3..8f0fb3ea5a1d0e 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx @@ -395,6 +395,7 @@ describe('Case Configuration API', () => { settings: { syncAlerts: true, }, + owner: 'securitySolution', }; test('check url, method, signal', async () => { diff --git a/x-pack/plugins/security_solution/public/cases/containers/mock.ts b/x-pack/plugins/security_solution/public/cases/containers/mock.ts index 6e937fe7760cd7..4559f6000493f4 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/mock.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/mock.ts @@ -254,6 +254,7 @@ export const basicCaseSnake: CaseResponse = { external_service: null, updated_at: basicUpdatedAt, updated_by: elasticUserSnake, + owner: 'securitySolution', } as CaseResponse; export const casesStatusSnake: CasesStatusResponse = { diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_case.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_case.test.tsx index 3731af4d73db5a..5cbbf75d80f39f 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_post_case.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_case.test.tsx @@ -28,6 +28,7 @@ describe('usePostCase', () => { settings: { syncAlerts: true, }, + owner: 'securitySolution', }; beforeEach(() => { jest.clearAllMocks(); diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 90306466a97535..c6945282e07420 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -31,7 +31,8 @@ const onlyNotInCoverageTests = [ require.resolve('../test/alerting_api_integration/basic/config.ts'), require.resolve('../test/alerting_api_integration/spaces_only/config.ts'), require.resolve('../test/alerting_api_integration/security_and_spaces/config.ts'), - require.resolve('../test/case_api_integration/basic/config.ts'), + require.resolve('../test/case_api_integration/security_and_spaces/config_basic.ts'), + require.resolve('../test/case_api_integration/security_and_spaces/config_trial.ts'), require.resolve('../test/apm_api_integration/basic/config.ts'), require.resolve('../test/apm_api_integration/trial/config.ts'), require.resolve('../test/detection_engine_api_integration/security_and_spaces/config.ts'), diff --git a/x-pack/test/case_api_integration/common/config.ts b/x-pack/test/case_api_integration/common/config.ts index fe663cfa8dc073..9b6c066c3f813b 100644 --- a/x-pack/test/case_api_integration/common/config.ts +++ b/x-pack/test/case_api_integration/common/config.ts @@ -56,32 +56,10 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) }, }; - const allFiles = fs.readdirSync( - path.resolve( - __dirname, - '..', - '..', - 'alerting_api_integration', - 'common', - 'fixtures', - 'plugins' - ) - ); + // Find all folders in ./fixtures/plugins + const allFiles = fs.readdirSync(path.resolve(__dirname, 'fixtures', 'plugins')); const plugins = allFiles.filter((file) => - fs - .statSync( - path.resolve( - __dirname, - '..', - '..', - 'alerting_api_integration', - 'common', - 'fixtures', - 'plugins', - file - ) - ) - .isDirectory() + fs.statSync(path.resolve(__dirname, 'fixtures', 'plugins', file)).isDirectory() ); return { @@ -109,20 +87,22 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) ...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'), `--xpack.actions.allowedHosts=${JSON.stringify(['localhost', 'some.non.existent.com'])}`, `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, + '--xpack.cases.enableAuthorization=true', '--xpack.eventLog.logEntries=true', ...disabledPlugins.map((key) => `--xpack.${key}.enabled=false`), + // Actions simulators plugin. Needed for testing push to external services. + `--plugin-path=${path.resolve( + __dirname, + '..', + '..', + 'alerting_api_integration', + 'common', + 'fixtures', + 'plugins' + )}`, ...plugins.map( (pluginDir) => - `--plugin-path=${path.resolve( - __dirname, - '..', - '..', - 'alerting_api_integration', - 'common', - 'fixtures', - 'plugins', - pluginDir - )}` + `--plugin-path=${path.resolve(__dirname, 'fixtures', 'plugins', pluginDir)}` ), `--server.xsrf.whitelist=${JSON.stringify(getAllExternalServiceSimulatorPaths())}`, ...(ssl diff --git a/x-pack/test/case_api_integration/common/fixtures/plugins/observability/kibana.json b/x-pack/test/case_api_integration/common/fixtures/plugins/observability/kibana.json new file mode 100644 index 00000000000000..b4b540fc9a821d --- /dev/null +++ b/x-pack/test/case_api_integration/common/fixtures/plugins/observability/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "observabilityFixtures", + "version": "1.0.0", + "kibanaVersion": "kibana", + "configPath": ["xpack"], + "requiredPlugins": ["features"], + "optionalPlugins": ["security", "spaces"], + "server": true, + "ui": false +} diff --git a/x-pack/test/case_api_integration/common/fixtures/plugins/observability/package.json b/x-pack/test/case_api_integration/common/fixtures/plugins/observability/package.json new file mode 100644 index 00000000000000..4d199ccd1badc7 --- /dev/null +++ b/x-pack/test/case_api_integration/common/fixtures/plugins/observability/package.json @@ -0,0 +1,14 @@ +{ + "name": "observability-fixtures", + "version": "1.0.0", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "main": "target/test/plugin_api_integration/plugins/observability_fixtures", + "scripts": { + "kbn": "node ../../../../../../../scripts/kbn.js", + "build": "rm -rf './target' && ../../../../../../../node_modules/.bin/tsc" + }, + "license": "Elastic License 2.0" +} \ No newline at end of file diff --git a/x-pack/test/case_api_integration/common/fixtures/plugins/observability/server/index.ts b/x-pack/test/case_api_integration/common/fixtures/plugins/observability/server/index.ts new file mode 100644 index 00000000000000..700aee6bfd49d6 --- /dev/null +++ b/x-pack/test/case_api_integration/common/fixtures/plugins/observability/server/index.ts @@ -0,0 +1,10 @@ +/* + * 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 { FixturePlugin } from './plugin'; + +export const plugin = () => new FixturePlugin(); diff --git a/x-pack/test/case_api_integration/common/fixtures/plugins/observability/server/plugin.ts b/x-pack/test/case_api_integration/common/fixtures/plugins/observability/server/plugin.ts new file mode 100644 index 00000000000000..802c823202b76e --- /dev/null +++ b/x-pack/test/case_api_integration/common/fixtures/plugins/observability/server/plugin.ts @@ -0,0 +1,61 @@ +/* + * 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 { Plugin, CoreSetup } from 'kibana/server'; + +import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../../plugins/features/server'; +import { SpacesPluginStart } from '../../../../../../../plugins/spaces/server'; +import { SecurityPluginStart } from '../../../../../../../plugins/security/server'; +import { SAVED_OBJECT_TYPES as casesSavedObjectTypes } from '../../../../../../../plugins/cases/common/constants'; + +export interface FixtureSetupDeps { + features: FeaturesPluginSetup; +} + +export interface FixtureStartDeps { + security?: SecurityPluginStart; + spaces?: SpacesPluginStart; +} + +export class FixturePlugin implements Plugin { + public setup(core: CoreSetup, deps: FixtureSetupDeps) { + const { features } = deps; + features.registerKibanaFeature({ + id: 'observabilityFixture', + name: 'ObservabilityFixture', + app: ['kibana'], + category: { id: 'cases-fixtures', label: 'Cases Fixtures' }, + cases: ['observabilityFixture'], + privileges: { + all: { + app: ['kibana'], + cases: { + all: ['observabilityFixture'], + }, + savedObject: { + all: ['alert', ...casesSavedObjectTypes], + read: [], + }, + ui: [], + }, + read: { + app: ['kibana'], + cases: { + read: ['observabilityFixture'], + }, + savedObject: { + all: [], + read: [...casesSavedObjectTypes], + }, + ui: [], + }, + }, + }); + } + public start() {} + public stop() {} +} diff --git a/x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/kibana.json b/x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/kibana.json new file mode 100644 index 00000000000000..000848e771af31 --- /dev/null +++ b/x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "securitySolutionFixtures", + "version": "1.0.0", + "kibanaVersion": "kibana", + "configPath": ["xpack"], + "requiredPlugins": ["features"], + "optionalPlugins": ["security", "spaces"], + "server": true, + "ui": false +} diff --git a/x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/package.json b/x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/package.json new file mode 100644 index 00000000000000..9a852dc1f0c496 --- /dev/null +++ b/x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/package.json @@ -0,0 +1,14 @@ +{ + "name": "security-solution-fixtures", + "version": "1.0.0", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "main": "target/test/plugin_api_integration/plugins/security_solution_fixtures", + "scripts": { + "kbn": "node ../../../../../../../scripts/kbn.js", + "build": "rm -rf './target' && ../../../../../../../node_modules/.bin/tsc" + }, + "license": "Elastic License 2.0" +} \ No newline at end of file diff --git a/x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/server/index.ts b/x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/server/index.ts new file mode 100644 index 00000000000000..700aee6bfd49d6 --- /dev/null +++ b/x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/server/index.ts @@ -0,0 +1,10 @@ +/* + * 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 { FixturePlugin } from './plugin'; + +export const plugin = () => new FixturePlugin(); diff --git a/x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/server/plugin.ts b/x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/server/plugin.ts new file mode 100644 index 00000000000000..46432a2507cb69 --- /dev/null +++ b/x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/server/plugin.ts @@ -0,0 +1,61 @@ +/* + * 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 { Plugin, CoreSetup } from 'kibana/server'; + +import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../../plugins/features/server'; +import { SpacesPluginStart } from '../../../../../../../plugins/spaces/server'; +import { SecurityPluginStart } from '../../../../../../../plugins/security/server'; +import { SAVED_OBJECT_TYPES as casesSavedObjectTypes } from '../../../../../../../plugins/cases/common/constants'; + +export interface FixtureSetupDeps { + features: FeaturesPluginSetup; +} + +export interface FixtureStartDeps { + security?: SecurityPluginStart; + spaces?: SpacesPluginStart; +} + +export class FixturePlugin implements Plugin { + public setup(core: CoreSetup, deps: FixtureSetupDeps) { + const { features } = deps; + features.registerKibanaFeature({ + id: 'securitySolutionFixture', + name: 'SecuritySolutionFixture', + app: ['kibana'], + category: { id: 'cases-fixtures', label: 'Cases Fixtures' }, + cases: ['securitySolutionFixture'], + privileges: { + all: { + app: ['kibana'], + cases: { + all: ['securitySolutionFixture'], + }, + savedObject: { + all: ['alert', ...casesSavedObjectTypes], + read: [], + }, + ui: [], + }, + read: { + app: ['kibana'], + cases: { + read: ['securitySolutionFixture'], + }, + savedObject: { + all: [], + read: [...casesSavedObjectTypes], + }, + ui: [], + }, + }, + }); + } + public start() {} + public stop() {} +} diff --git a/x-pack/test/case_api_integration/common/lib/authentication/roles.ts b/x-pack/test/case_api_integration/common/lib/authentication/roles.ts index e711a59229e773..cf21b01c3967e4 100644 --- a/x-pack/test/case_api_integration/common/lib/authentication/roles.ts +++ b/x-pack/test/case_api_integration/common/lib/authentication/roles.ts @@ -35,7 +35,8 @@ export const globalRead: Role = { kibana: [ { feature: { - cases: ['read'], + securitySolutionFixture: ['read'], + observabilityFixture: ['all'], }, spaces: ['*'], }, @@ -57,7 +58,29 @@ export const securitySolutionOnlyAll: Role = { kibana: [ { feature: { - siem: ['all'], + securitySolutionFixture: ['all'], + }, + spaces: ['space1'], + }, + ], + }, +}; + +export const securitySolutionOnlyRead: Role = { + name: 'sec_only_read', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + securitySolutionFixture: ['read'], }, spaces: ['space1'], }, @@ -66,7 +89,29 @@ export const securitySolutionOnlyAll: Role = { }; export const observabilityOnlyAll: Role = { - name: 'sec_only_all', + name: 'obs_only_all', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + observabilityFixture: ['all'], + }, + spaces: ['space1'], + }, + ], + }, +}; + +export const observabilityOnlyRead: Role = { + name: 'obs_only_read', privileges: { elasticsearch: { indices: [ @@ -79,10 +124,7 @@ export const observabilityOnlyAll: Role = { kibana: [ { feature: { - logs: ['all'], - infrastructure: ['all'], - apm: ['all'], - uptime: ['all'], + observabilityFixture: ['read'], }, spaces: ['space1'], }, @@ -94,5 +136,7 @@ export const roles = [ noKibanaPrivileges, globalRead, securitySolutionOnlyAll, + securitySolutionOnlyRead, observabilityOnlyAll, + observabilityOnlyRead, ]; diff --git a/x-pack/test/case_api_integration/common/lib/authentication/users.ts b/x-pack/test/case_api_integration/common/lib/authentication/users.ts index 43e21b79ee4b61..06add9ae007933 100644 --- a/x-pack/test/case_api_integration/common/lib/authentication/users.ts +++ b/x-pack/test/case_api_integration/common/lib/authentication/users.ts @@ -5,31 +5,78 @@ * 2.0. */ -import { securitySolutionOnlyAll, observabilityOnlyAll } from './roles'; +import { + securitySolutionOnlyAll, + observabilityOnlyAll, + securitySolutionOnlyRead, + observabilityOnlyRead, + globalRead as globalReadRole, + noKibanaPrivileges as noKibanaPrivilegesRole, +} from './roles'; import { User } from './types'; -const superUser: User = { +export const superUser: User = { username: 'superuser', password: 'superuser', roles: ['superuser'], }; -const secOnly: User = { +export const secOnly: User = { username: 'sec_only', password: 'sec_only', roles: [securitySolutionOnlyAll.name], }; -const obsOnly: User = { +export const secOnlyRead: User = { + username: 'sec_only_read', + password: 'sec_only_read', + roles: [securitySolutionOnlyRead.name], +}; + +export const obsOnly: User = { username: 'obs_only', password: 'obs_only', roles: [observabilityOnlyAll.name], }; -const obsSec: User = { +export const obsOnlyRead: User = { + username: 'obs_only_read', + password: 'obs_only_read', + roles: [observabilityOnlyRead.name], +}; + +export const obsSec: User = { username: 'obs_sec', password: 'obs_sec', roles: [securitySolutionOnlyAll.name, observabilityOnlyAll.name], }; -export const users = [superUser, secOnly, obsOnly, obsSec]; +export const obsSecRead: User = { + username: 'obs_sec_read', + password: 'obs_sec_read', + roles: [securitySolutionOnlyRead.name, observabilityOnlyRead.name], +}; + +export const globalRead: User = { + username: 'global_read', + password: 'global_read', + roles: [globalReadRole.name], +}; + +export const noKibanaPrivileges: User = { + username: 'no_kibana_privileges', + password: 'no_kibana_privileges', + roles: [noKibanaPrivilegesRole.name], +}; + +export const users = [ + superUser, + secOnly, + secOnlyRead, + obsOnly, + obsOnlyRead, + obsSec, + obsSecRead, + globalRead, + noKibanaPrivileges, +]; diff --git a/x-pack/test/case_api_integration/common/lib/mock.ts b/x-pack/test/case_api_integration/common/lib/mock.ts index 53dd6440a47df9..f1f088e5c50425 100644 --- a/x-pack/test/case_api_integration/common/lib/mock.ts +++ b/x-pack/test/case_api_integration/common/lib/mock.ts @@ -44,8 +44,17 @@ export const postCaseReq: CasePostRequest = { settings: { syncAlerts: true, }, + owner: 'securitySolutionFixture', }; +/** + * Return a request for creating a case. + */ +export const getPostCaseRequest = (req?: Partial): CasePostRequest => ({ + ...postCaseReq, + ...req, +}); + /** * The fields for creating a collection style case. */ 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 f7ff49727df333..82189c9d7abe3f 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -23,11 +23,13 @@ import { CaseStatuses, SubCasesResponse, CasesResponse, + CasesFindResponse, } from '../../../../plugins/cases/common/api'; -import { postCollectionReq, postCommentGenAlertReq } from './mock'; +import { getPostCaseRequest, 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 { User } from './authentication/types'; function toArray(input: T | T[]): T[] { if (Array.isArray(input)) { @@ -407,3 +409,62 @@ export const deleteConfiguration = async (es: KibanaClient): Promise => { body: {}, }); }; + +export const getSpaceUrlPrefix = (spaceId: string) => { + 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; +}; + +export const ensureSavedObjectIsAuthorized = ( + cases: CaseResponse[], + numberOfExpectedCases: number, + owners: string[] +) => { + expect(cases.length).to.eql(numberOfExpectedCases); + cases.forEach((theCase) => expect(owners.includes(theCase.owner)).to.be(true)); +}; 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 f889887d40381d..195ada335e0861 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 @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import supertestAsPromised from 'supertest-as-promised'; import type { ApiResponse, estypes } from '@elastic/elasticsearch'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { CASES_URL, @@ -22,12 +22,26 @@ import { CreateSubCaseResp, createCaseAction, deleteCaseAction, + createCaseAsUser, + ensureSavedObjectIsAuthorized, + findCasesAsUser, } from '../../../../common/lib/utils'; import { CasesFindResponse, CaseStatuses, CaseType, } from '../../../../../../plugins/cases/common/api'; +import { + obsOnly, + secOnly, + obsOnlyRead, + secOnlyRead, + noKibanaPrivileges, + superUser, + globalRead, + obsSecRead, + obsSec, +} from '../../../../common/lib/authentication/users'; interface CaseAttributes { cases: { @@ -39,6 +53,8 @@ interface CaseAttributes { export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + describe('find_cases', () => { describe('basic tests', () => { afterEach(async () => { @@ -670,5 +686,201 @@ export default ({ getService }: FtrProviderContext): void => { expect(body.count_in_progress_cases).to.eql(0); }); }); + + describe('rbac', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should return the correct cases', async () => { + await Promise.all([ + // Create case owned by the security solution user + await createCaseAsUser({ + supertestWithoutAuth, + user: secOnly, + space: 'space1', + owner: 'securitySolutionFixture', + }), + // Create case owned by the observability user + await createCaseAsUser({ + supertestWithoutAuth, + user: obsOnly, + space: 'space1', + owner: 'observabilityFixture', + }), + ]); + + 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 res = await findCasesAsUser({ + supertestWithoutAuth, + user: scenario.user, + space: 'space1', + }); + + ensureSavedObjectIsAuthorized(res.cases, 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`, async () => { + // super user creates a case at the appropriate space + await createCaseAsUser({ + supertestWithoutAuth, + user: superUser, + space: scenario.space, + owner: 'securitySolutionFixture', + }); + + // user should not be able to read cases at the appropriate space + await findCasesAsUser({ + supertestWithoutAuth, + user: scenario.user, + space: scenario.space, + expectedHttpCode: 403, + }); + }); + } + + 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({ + supertestWithoutAuth, + user: superUser, + space: 'space1', + owner: 'securitySolutionFixture', + }), + // super user creates a case with owner observabilityFixture + await createCaseAsUser({ + supertestWithoutAuth, + user: superUser, + space: 'space1', + owner: 'observabilityFixture', + }), + ]); + + const res = await findCasesAsUser({ + supertestWithoutAuth, + user: secOnly, + space: 'space1', + appendToUrl: 'search=securitySolutionFixture+observabilityFixture&searchFields=owner', + }); + + ensureSavedObjectIsAuthorized(res.cases, 1, ['securitySolutionFixture']); + }); + + // This test is to prevent a future developer to add the filter attribute without taking into consideration + // the authorizationFilter produced by the cases authorization class + it('should NOT allow to pass a filter query parameter', async () => { + await supertest + .get( + `${CASES_URL}/_find?sortOrder=asc&filter=cases.attributes.owner=observabilityFixture` + ) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + }); + + // This test ensures that the user is not allowed to define the namespaces query param + // so she cannot search across spaces + it('should NOT allow to pass a namespaces query parameter', async () => { + await supertest + .get(`${CASES_URL}/_find?sortOrder=asc&namespaces[0]=*`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + + await supertest + .get(`${CASES_URL}/_find?sortOrder=asc&namespaces=*`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + }); + + it('should NOT allow to pass a non supported query parameter', async () => { + await supertest + .get(`${CASES_URL}/_find?notExists=papa`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + }); + + it('should respect the owner filter when having permissions', async () => { + await Promise.all([ + await createCaseAsUser({ + supertestWithoutAuth, + user: obsSec, + space: 'space1', + owner: 'securitySolutionFixture', + }), + await createCaseAsUser({ + supertestWithoutAuth, + user: obsSec, + space: 'space1', + owner: 'observabilityFixture', + }), + ]); + + const res = await findCasesAsUser({ + supertestWithoutAuth, + user: obsSec, + space: 'space1', + appendToUrl: 'owner=securitySolutionFixture', + }); + + ensureSavedObjectIsAuthorized(res.cases, 1, ['securitySolutionFixture']); + }); + + it('should return the correct cases when trying to exploit RBAC through the owner query parameter', async () => { + await Promise.all([ + await createCaseAsUser({ + supertestWithoutAuth, + user: obsSec, + space: 'space1', + owner: 'securitySolutionFixture', + }), + await createCaseAsUser({ + supertestWithoutAuth, + user: obsSec, + space: 'space1', + owner: 'observabilityFixture', + }), + ]); + + // 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', + }); + + // Only security solution cases are being returned + ensureSavedObjectIsAuthorized(res.cases, 1, ['securitySolutionFixture']); + }); + }); }); }; 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 afcc36d041c111..2249587620d5fd 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 @@ -6,20 +6,33 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; import { - postCaseReq, + ConnectorTypes, + ConnectorJiraTypeFields, +} from '../../../../../../plugins/cases/common/api'; +import { + getPostCaseRequest, postCaseResp, removeServerGeneratedPropertiesFromCase, } from '../../../../common/lib/mock'; -import { deleteCases } from '../../../../common/lib/utils'; +import { createCaseAsUser, deleteCases } from '../../../../common/lib/utils'; +import { + secOnly, + secOnlyRead, + globalRead, + obsOnlyRead, + obsSecRead, + noKibanaPrivileges, +} from '../../../../common/lib/authentication/users'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); describe('post_case', () => { afterEach(async () => { @@ -30,7 +43,7 @@ export default ({ getService }: FtrProviderContext): void => { const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') - .send(postCaseReq) + .send(getPostCaseRequest()) .expect(200); const data = removeServerGeneratedPropertiesFromCase(postedCase); @@ -41,12 +54,13 @@ export default ({ getService }: FtrProviderContext): void => { await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') - .send({ ...postCaseReq, badKey: true }) + // @ts-expect-error + .send({ ...getPostCaseRequest({ badKey: true }) }) .expect(400); }); it('unhappy path - 400s when connector is not supplied', async () => { - const { connector, ...caseWithoutConnector } = postCaseReq; + const { connector, ...caseWithoutConnector } = getPostCaseRequest(); await supertest .post(CASES_URL) @@ -60,8 +74,10 @@ export default ({ getService }: FtrProviderContext): void => { .post(CASES_URL) .set('kbn-xsrf', 'true') .send({ - ...postCaseReq, - connector: { id: 'wrong', name: 'wrong', type: '.not-exists', fields: null }, + ...getPostCaseRequest({ + // @ts-expect-error + connector: { id: 'wrong', name: 'wrong', type: '.not-exists', fields: null }, + }), }) .expect(400); }); @@ -71,15 +87,63 @@ export default ({ getService }: FtrProviderContext): void => { .post(CASES_URL) .set('kbn-xsrf', 'true') .send({ - ...postCaseReq, - connector: { - id: 'wrong', - name: 'wrong', - type: '.jira', - fields: { unsupported: 'value' }, - }, + ...getPostCaseRequest({ + // @ts-expect-error + connector: { + id: 'wrong', + name: 'wrong', + type: ConnectorTypes.jira, + fields: { unsupported: 'value' }, + } as ConnectorJiraTypeFields, + }), }) .expect(400); }); + + describe('rbac', () => { + it('User: security solution only - should create a case', async () => { + const theCase = await createCaseAsUser({ + supertestWithoutAuth, + user: secOnly, + space: 'space1', + owner: 'securitySolutionFixture', + }); + expect(theCase.owner).to.eql('securitySolutionFixture'); + }); + + it('User: security solution only - should NOT create a case of different owner', async () => { + await createCaseAsUser({ + supertestWithoutAuth, + user: secOnly, + space: 'space1', + owner: 'observabilityFixture', + expectedHttpCode: 403, + }); + }); + + 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({ + supertestWithoutAuth, + user, + space: 'space1', + owner: 'securitySolutionFixture', + expectedHttpCode: 403, + }); + }); + } + + it('should NOT create a case in a space with no permissions', async () => { + await createCaseAsUser({ + supertestWithoutAuth, + user: secOnly, + space: 'space2', + owner: 'securitySolutionFixture', + expectedHttpCode: 403, + }); + }); + }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/sub_cases/find_sub_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/sub_cases/find_sub_cases.ts index 43a0d6bf6203b0..466eca95b0d727 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/sub_cases/find_sub_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/sub_cases/find_sub_cases.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import type { ApiResponse, estypes } from '@elastic/elasticsearch'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { findSubCasesResp, postCollectionReq } from '../../../../../common/lib/mock'; import { From 4f3c37ee4d3934813d616b062b2c56bbaa79f689 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 6 Apr 2021 21:02:15 +0300 Subject: [PATCH 043/113] [Cases] Cases client enchantment (#95923) --- .../cases/server/authorization/utils.ts | 3 +- .../cases/server/client/alerts/client.ts | 58 + .../client/alerts/update_status.test.ts | 2 +- .../server/client/alerts/update_status.ts | 4 +- .../{comments => attachments}/add.test.ts | 0 .../client/{comments => attachments}/add.ts | 109 +- .../cases/server/client/attachments/client.ts | 37 + .../cases/server/client/cases/client.ts | 115 ++ .../cases/server/client/cases/create.ts | 20 +- .../plugins/cases/server/client/cases/find.ts | 8 +- .../plugins/cases/server/client/cases/get.ts | 12 +- .../plugins/cases/server/client/cases/push.ts | 47 +- .../cases/server/client/cases/update.ts | 68 +- x-pack/plugins/cases/server/client/client.ts | 285 +--- .../cases/server/client/client_internal.ts | 32 + .../cases/server/client/configure/client.ts | 51 + .../server/client/configure/get_fields.ts | 4 +- .../server/client/configure/get_mappings.ts | 16 +- x-pack/plugins/cases/server/client/factory.ts | 27 +- x-pack/plugins/cases/server/client/index.ts | 20 +- x-pack/plugins/cases/server/client/mocks.ts | 4 +- x-pack/plugins/cases/server/client/types.ts | 124 +- .../server/client/user_actions/client.ts | 34 + .../cases/server/client/user_actions/get.ts | 8 +- .../server/common/models/commentable_case.ts | 53 +- x-pack/plugins/cases/server/common/utils.ts | 2 +- .../cases/server/connectors/case/index.ts | 6 +- x-pack/plugins/cases/server/plugin.ts | 25 +- .../routes/api/__fixtures__/route_contexts.ts | 2 +- .../api/cases/comments/delete_all_comments.ts | 15 +- .../api/cases/comments/delete_comment.ts | 19 +- .../api/cases/comments/find_comments.ts | 6 +- .../api/cases/comments/get_all_comment.ts | 6 +- .../routes/api/cases/comments/get_comment.ts | 10 +- .../api/cases/comments/patch_comment.ts | 62 +- .../routes/api/cases/comments/post_comment.ts | 2 +- .../api/cases/configure/get_configure.ts | 6 +- .../api/cases/configure/patch_configure.ts | 8 +- .../api/cases/configure/post_configure.ts | 14 +- .../server/routes/api/cases/delete_cases.ts | 49 +- .../server/routes/api/cases/find_cases.ts | 2 +- .../cases/server/routes/api/cases/get_case.ts | 2 +- .../cases/server/routes/api/cases/helpers.ts | 3 +- .../server/routes/api/cases/patch_cases.ts | 2 +- .../server/routes/api/cases/post_case.ts | 2 +- .../server/routes/api/cases/push_case.ts | 2 +- .../api/cases/reporters/get_reporters.ts | 4 +- .../routes/api/cases/status/get_status.ts | 4 +- .../api/cases/sub_case/delete_sub_cases.ts | 15 +- .../api/cases/sub_case/find_sub_cases.ts | 6 +- .../routes/api/cases/sub_case/get_sub_case.ts | 6 +- .../api/cases/sub_case/patch_sub_cases.ts | 56 +- .../server/routes/api/cases/tags/get_tags.ts | 4 +- .../user_actions/get_all_user_actions.ts | 4 +- .../plugins/cases/server/routes/api/types.ts | 18 +- .../cases/server/services/alerts/index.ts | 2 +- .../server/services/attachments/index.ts | 116 ++ .../cases/server/services/cases/index.ts | 1015 +++++++++++++++ .../{reporters => cases}/read_reporters.ts | 8 +- .../services/{tags => cases}/read_tags.ts | 14 +- .../cases/server/services/configure/index.ts | 131 +- .../services/connector_mappings/index.ts | 64 +- x-pack/plugins/cases/server/services/index.ts | 1150 +---------------- x-pack/plugins/cases/server/services/mocks.ts | 16 +- .../server/services/user_actions/index.ts | 91 +- 65 files changed, 2074 insertions(+), 2036 deletions(-) create mode 100644 x-pack/plugins/cases/server/client/alerts/client.ts rename x-pack/plugins/cases/server/client/{comments => attachments}/add.test.ts (100%) rename x-pack/plugins/cases/server/client/{comments => attachments}/add.ts (82%) create mode 100644 x-pack/plugins/cases/server/client/attachments/client.ts create mode 100644 x-pack/plugins/cases/server/client/cases/client.ts create mode 100644 x-pack/plugins/cases/server/client/client_internal.ts create mode 100644 x-pack/plugins/cases/server/client/configure/client.ts create mode 100644 x-pack/plugins/cases/server/client/user_actions/client.ts create mode 100644 x-pack/plugins/cases/server/services/attachments/index.ts create mode 100644 x-pack/plugins/cases/server/services/cases/index.ts rename x-pack/plugins/cases/server/services/{reporters => cases}/read_reporters.ts (89%) rename x-pack/plugins/cases/server/services/{tags => cases}/read_tags.ts (87%) diff --git a/x-pack/plugins/cases/server/authorization/utils.ts b/x-pack/plugins/cases/server/authorization/utils.ts index b44c94d21fb5ba..a7e210d07d214f 100644 --- a/x-pack/plugins/cases/server/authorization/utils.ts +++ b/x-pack/plugins/cases/server/authorization/utils.ts @@ -6,8 +6,7 @@ */ import { remove, uniq } from 'lodash'; -import { nodeBuilder } from '../../../../../src/plugins/data/common'; -import { KueryNode } from '../../../../../src/plugins/data/server'; +import { nodeBuilder, KueryNode } from '../../../../../src/plugins/data/common'; export const getOwnersFilter = (savedObjectType: string, owners: string[]): KueryNode => { return nodeBuilder.or( diff --git a/x-pack/plugins/cases/server/client/alerts/client.ts b/x-pack/plugins/cases/server/client/alerts/client.ts new file mode 100644 index 00000000000000..dfa06c0277bda2 --- /dev/null +++ b/x-pack/plugins/cases/server/client/alerts/client.ts @@ -0,0 +1,58 @@ +/* + * 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 { CaseStatuses } from '../../../common/api'; +import { AlertInfo } from '../../common'; +import { CasesClientGetAlertsResponse } from './types'; +import { get } from './get'; +import { updateStatus } from './update_status'; +import { CasesClientArgs } from '../types'; + +/** + * Defines the fields necessary to update an alert's status. + */ +export interface UpdateAlertRequest { + id: string; + index: string; + status: CaseStatuses; +} + +export interface AlertUpdateStatus { + alerts: UpdateAlertRequest[]; +} + +export interface AlertGet { + alertsInfo: AlertInfo[]; +} + +export interface AlertSubClient { + get(args: AlertGet): Promise; + updateStatus(args: AlertUpdateStatus): Promise; +} + +export const createAlertsSubClient = (args: CasesClientArgs): AlertSubClient => { + const { alertsService, scopedClusterClient, logger } = args; + + const alertsSubClient: AlertSubClient = { + get: (params: AlertGet) => + get({ + ...params, + alertsService, + scopedClusterClient, + logger, + }), + updateStatus: (params: AlertUpdateStatus) => + updateStatus({ + ...params, + alertsService, + scopedClusterClient, + logger, + }), + }; + + return Object.freeze(alertsSubClient); +}; diff --git a/x-pack/plugins/cases/server/client/alerts/update_status.test.ts b/x-pack/plugins/cases/server/client/alerts/update_status.test.ts index 5dfe6060da1db8..44d6fc244270ab 100644 --- a/x-pack/plugins/cases/server/client/alerts/update_status.test.ts +++ b/x-pack/plugins/cases/server/client/alerts/update_status.test.ts @@ -14,7 +14,7 @@ describe('updateAlertsStatus', () => { const savedObjectsClient = createMockSavedObjectsRepository(); const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - await casesClient.client.updateAlertsStatus({ + await casesClient.client.updateStatus({ alerts: [{ id: 'alert-id-1', index: '.siem-signals', status: CaseStatuses.closed }], }); 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 cd6f97273d6d7e..e02a98c396e0a9 100644 --- a/x-pack/plugins/cases/server/client/alerts/update_status.ts +++ b/x-pack/plugins/cases/server/client/alerts/update_status.ts @@ -7,7 +7,7 @@ import { ElasticsearchClient, Logger } from 'src/core/server'; import { AlertServiceContract } from '../../services'; -import { UpdateAlertRequest } from '../types'; +import { UpdateAlertRequest } from './client'; interface UpdateAlertsStatusArgs { alertsService: AlertServiceContract; @@ -16,7 +16,7 @@ interface UpdateAlertsStatusArgs { logger: Logger; } -export const updateAlertsStatus = async ({ +export const updateStatus = async ({ alertsService, alerts, scopedClusterClient, diff --git a/x-pack/plugins/cases/server/client/comments/add.test.ts b/x-pack/plugins/cases/server/client/attachments/add.test.ts similarity index 100% rename from x-pack/plugins/cases/server/client/comments/add.test.ts rename to x-pack/plugins/cases/server/client/attachments/add.test.ts diff --git a/x-pack/plugins/cases/server/client/comments/add.ts b/x-pack/plugins/cases/server/client/attachments/add.ts similarity index 82% rename from x-pack/plugins/cases/server/client/comments/add.ts rename to x-pack/plugins/cases/server/client/attachments/add.ts index f077571019f600..659ff14418d05f 100644 --- a/x-pack/plugins/cases/server/client/comments/add.ts +++ b/x-pack/plugins/cases/server/client/attachments/add.ts @@ -32,9 +32,9 @@ import { buildCommentUserActionItem, } from '../../services/user_actions/helpers'; -import { CaseServiceSetup, CaseUserActionServiceSetup } from '../../services'; +import { AttachmentService, CaseService, CaseUserActionService } from '../../services'; import { CommentableCase, createAlertUpdateRequest } from '../../common'; -import { CasesClientHandler } from '..'; +import { CasesClientArgs, CasesClientInternal } from '..'; import { createCaseError } from '../../common/error'; import { MAX_GENERATED_ALERTS_PER_SUB_CASE, @@ -50,17 +50,17 @@ async function getSubCase({ userActionService, user, }: { - caseService: CaseServiceSetup; + caseService: CaseService; savedObjectsClient: SavedObjectsClientContract; caseId: string; createdAt: string; - userActionService: CaseUserActionServiceSetup; + userActionService: CaseUserActionService; user: User; }): Promise> { const mostRecentSubCase = await caseService.getMostRecentSubCase(savedObjectsClient, caseId); if (mostRecentSubCase && mostRecentSubCase.attributes.status !== CaseStatuses.closed) { const subCaseAlertsAttachement = await caseService.getAllSubCaseComments({ - client: savedObjectsClient, + soClient: savedObjectsClient, id: mostRecentSubCase.id, options: { fields: [], @@ -79,13 +79,13 @@ async function getSubCase({ } const newSubCase = await caseService.createSubCase({ - client: savedObjectsClient, + soClient: savedObjectsClient, createdAt, caseId, createdBy: user, }); - await userActionService.postUserActions({ - client: savedObjectsClient, + await userActionService.bulkCreate({ + soClient: savedObjectsClient, actions: [ buildCaseUserActionItem({ action: 'create', @@ -102,20 +102,22 @@ async function getSubCase({ } interface AddCommentFromRuleArgs { - casesClient: CasesClientHandler; + casesClientInternal: CasesClientInternal; caseId: string; comment: CommentRequestAlertType; savedObjectsClient: SavedObjectsClientContract; - caseService: CaseServiceSetup; - userActionService: CaseUserActionServiceSetup; + attachmentService: AttachmentService; + caseService: CaseService; + userActionService: CaseUserActionService; logger: Logger; } const addGeneratedAlerts = async ({ savedObjectsClient, + attachmentService, caseService, userActionService, - casesClient, + casesClientInternal, caseId, comment, logger, @@ -136,7 +138,7 @@ const addGeneratedAlerts = async ({ const createdDate = new Date().toISOString(); const caseInfo = await caseService.getCase({ - client: savedObjectsClient, + soClient: savedObjectsClient, id: caseId, }); @@ -167,7 +169,8 @@ const addGeneratedAlerts = async ({ collection: caseInfo, subCase, soClient: savedObjectsClient, - service: caseService, + caseService, + attachmentService, }); const { @@ -184,13 +187,13 @@ const addGeneratedAlerts = async ({ comment: query, status: subCase.attributes.status, }); - await casesClient.updateAlertsStatus({ + await casesClientInternal.alerts.updateStatus({ alerts: alertsToUpdate, }); } - await userActionService.postUserActions({ - client: savedObjectsClient, + await userActionService.bulkCreate({ + soClient: savedObjectsClient, actions: [ buildCommentUserActionItem({ action: 'create', @@ -216,25 +219,27 @@ const addGeneratedAlerts = async ({ }; async function getCombinedCase({ - service, - client, + caseService, + attachmentService, + soClient, id, logger, }: { - service: CaseServiceSetup; - client: SavedObjectsClientContract; + caseService: CaseService; + attachmentService: AttachmentService; + soClient: SavedObjectsClientContract; id: string; logger: Logger; }): Promise { const [casePromise, subCasePromise] = await Promise.allSettled([ - service.getCase({ - client, + caseService.getCase({ + soClient, id, }), ...(ENABLE_CASE_CONNECTOR ? [ - service.getSubCase({ - client, + caseService.getSubCase({ + soClient, id, }), ] @@ -243,16 +248,17 @@ async function getCombinedCase({ if (subCasePromise.status === 'fulfilled') { if (subCasePromise.value.references.length > 0) { - const caseValue = await service.getCase({ - client, + const caseValue = await caseService.getCase({ + soClient, id: subCasePromise.value.references[0].id, }); return new CommentableCase({ logger, collection: caseValue, subCase: subCasePromise.value, - service, - soClient: client, + caseService, + attachmentService, + soClient, }); } else { throw Boom.badRequest('Sub case found without reference to collection'); @@ -265,38 +271,39 @@ async function getCombinedCase({ return new CommentableCase({ logger, collection: casePromise.value, - service, - soClient: client, + caseService, + attachmentService, + soClient, }); } } interface AddCommentArgs { - casesClient: CasesClientHandler; caseId: string; comment: CommentRequest; - savedObjectsClient: SavedObjectsClientContract; - caseService: CaseServiceSetup; - userActionService: CaseUserActionServiceSetup; - user: User; - logger: Logger; + casesClientInternal: CasesClientInternal; } export const addComment = async ({ - savedObjectsClient, - caseService, - userActionService, - casesClient, caseId, comment, - user, - logger, -}: AddCommentArgs): Promise => { + casesClientInternal, + ...rest +}: AddCommentArgs & CasesClientArgs): Promise => { const query = pipe( CommentRequestRt.decode(comment), fold(throwErrors(Boom.badRequest), identity) ); + const { + savedObjectsClient, + caseService, + userActionService, + attachmentService, + user, + logger, + } = rest; + if (isCommentRequestTypeGenAlert(comment)) { if (!ENABLE_CASE_CONNECTOR) { throw Boom.badRequest( @@ -307,10 +314,11 @@ export const addComment = async ({ return addGeneratedAlerts({ caseId, comment, - casesClient, + casesClientInternal, savedObjectsClient, userActionService, caseService, + attachmentService, logger, }); } @@ -320,8 +328,9 @@ export const addComment = async ({ const createdDate = new Date().toISOString(); const combinedCase = await getCombinedCase({ - service: caseService, - client: savedObjectsClient, + caseService, + attachmentService, + soClient: savedObjectsClient, id: caseId, logger, }); @@ -346,13 +355,13 @@ export const addComment = async ({ status: updatedCase.status, }); - await casesClient.updateAlertsStatus({ + await casesClientInternal.alerts.updateStatus({ alerts: alertsToUpdate, }); } - await userActionService.postUserActions({ - client: savedObjectsClient, + await userActionService.bulkCreate({ + soClient: savedObjectsClient, actions: [ buildCommentUserActionItem({ action: 'create', diff --git a/x-pack/plugins/cases/server/client/attachments/client.ts b/x-pack/plugins/cases/server/client/attachments/client.ts new file mode 100644 index 00000000000000..f3ee3098a3153f --- /dev/null +++ b/x-pack/plugins/cases/server/client/attachments/client.ts @@ -0,0 +1,37 @@ +/* + * 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 { CaseResponse, CommentRequest as AttachmentsRequest } from '../../../common/api'; +import { CasesClientInternal } from '../client_internal'; +import { CasesClientArgs } from '../types'; +import { addComment } from './add'; + +export interface AttachmentsAdd { + caseId: string; + comment: AttachmentsRequest; +} + +export interface AttachmentsSubClient { + add(args: AttachmentsAdd): Promise; +} + +export const createAttachmentsSubClient = ( + args: CasesClientArgs, + casesClientInternal: CasesClientInternal +): AttachmentsSubClient => { + const attachmentSubClient: AttachmentsSubClient = { + add: ({ caseId, comment }: AttachmentsAdd) => + addComment({ + ...args, + casesClientInternal, + caseId, + comment, + }), + }; + + 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 new file mode 100644 index 00000000000000..9c9bf1fa7641d2 --- /dev/null +++ b/x-pack/plugins/cases/server/client/cases/client.ts @@ -0,0 +1,115 @@ +/* + * 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 { ActionsClient } from '../../../../actions/server'; +import { + CasePostRequest, + CaseResponse, + CasesPatchRequest, + CasesResponse, + CasesFindRequest, + CasesFindResponse, +} from '../../../common/api'; +import { CasesClient } from '../client'; +import { CasesClientInternal } from '../client_internal'; +import { CasesClientArgs } from '../types'; +import { create } from './create'; +import { find } from './find'; +import { get } from './get'; +import { push } from './push'; +import { update } from './update'; + +export interface CaseGet { + id: string; + includeComments?: boolean; + includeSubCaseComments?: boolean; +} + +export interface CasePush { + actionsClient: ActionsClient; + caseId: string; + connectorId: string; +} + +export interface CasesSubClient { + create(theCase: CasePostRequest): Promise; + find(args: CasesFindRequest): Promise; + get(args: CaseGet): Promise; + push(args: CasePush): Promise; + update(args: CasesPatchRequest): Promise; +} + +export const createCasesSubClient = ( + args: CasesClientArgs, + casesClient: CasesClient, + casesClientInternal: CasesClientInternal +): CasesSubClient => { + const { + attachmentService, + caseConfigureService, + caseService, + user, + savedObjectsClient, + userActionService, + logger, + authorization, + } = args; + + const casesSubClient: CasesSubClient = { + create: (theCase: CasePostRequest) => + create({ + savedObjectsClient, + caseService, + caseConfigureService, + userActionService, + user, + theCase, + logger, + auth: authorization, + }), + find: (options: CasesFindRequest) => + find({ + savedObjectsClient, + caseService, + logger, + auth: authorization, + options, + }), + 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, + }), + }; + + 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 34fdb7aff14a21..935ca6d3199d2f 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -30,22 +30,18 @@ import { transformCaseConnectorToEsConnector, } from '../../routes/api/cases/helpers'; -import { - CaseConfigureServiceSetup, - CaseServiceSetup, - CaseUserActionServiceSetup, -} from '../../services'; +import { CaseConfigureService, CaseService, CaseUserActionService } from '../../services'; import { createCaseError } from '../../common/error'; import { Authorization } from '../../authorization/authorization'; import { WriteOperations } from '../../authorization/types'; import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; interface CreateCaseArgs { - caseConfigureService: CaseConfigureServiceSetup; - caseService: CaseServiceSetup; + caseConfigureService: CaseConfigureService; + caseService: CaseService; user: User; savedObjectsClient: SavedObjectsClientContract; - userActionService: CaseUserActionServiceSetup; + userActionService: CaseUserActionService; theCase: CasePostRequest; logger: Logger; auth: PublicMethodsOf; @@ -93,11 +89,11 @@ export const create = async ({ // eslint-disable-next-line @typescript-eslint/naming-convention const { username, full_name, email } = user; const createdDate = new Date().toISOString(); - const myCaseConfigure = await caseConfigureService.find({ client: savedObjectsClient }); + const myCaseConfigure = await caseConfigureService.find({ soClient: savedObjectsClient }); const caseConfigureConnector = getConnectorFromConfiguration(myCaseConfigure); const newCase = await caseService.postNewCase({ - client: savedObjectsClient, + soClient: savedObjectsClient, attributes: transformNewCase({ createdDate, newCase: query, @@ -108,8 +104,8 @@ export const create = async ({ }), }); - await userActionService.postUserActions({ - client: savedObjectsClient, + await userActionService.bulkCreate({ + soClient: savedObjectsClient, actions: [ buildCaseUserActionItem({ action: 'create', diff --git a/x-pack/plugins/cases/server/client/cases/find.ts b/x-pack/plugins/cases/server/client/cases/find.ts index 24e8cb6ec5f886..33545a39258893 100644 --- a/x-pack/plugins/cases/server/client/cases/find.ts +++ b/x-pack/plugins/cases/server/client/cases/find.ts @@ -23,7 +23,7 @@ import { } from '../../../common/api'; import { CASE_SAVED_OBJECT } from '../../../common/constants'; -import { CaseServiceSetup } from '../../services'; +import { CaseService } from '../../services'; import { createCaseError } from '../../common/error'; import { constructQueryOptions } from '../../routes/api/cases/helpers'; import { transformCases } from '../../routes/api/utils'; @@ -32,7 +32,7 @@ import { includeFieldsRequiredForAuthentication } from '../../authorization/util interface FindParams { savedObjectsClient: SavedObjectsClientContract; - caseService: CaseServiceSetup; + caseService: CaseService; logger: Logger; auth: PublicMethodsOf; options: CasesFindRequest; @@ -71,7 +71,7 @@ export const find = async ({ const caseQueries = constructQueryOptions({ ...queryArgs, authorizationFilter }); const cases = await caseService.findCasesGroupedByID({ - client: savedObjectsClient, + soClient: savedObjectsClient, caseOptions: { ...queryParams, ...caseQueries.case, @@ -97,7 +97,7 @@ export const find = async ({ ...caseStatuses.map((status) => { const statusQuery = constructQueryOptions({ ...queryArgs, status, authorizationFilter }); return caseService.findCaseStatusStats({ - client: savedObjectsClient, + soClient: savedObjectsClient, caseOptions: statusQuery.case, subCaseOptions: statusQuery.subCase, }); diff --git a/x-pack/plugins/cases/server/client/cases/get.ts b/x-pack/plugins/cases/server/client/cases/get.ts index 50725879278e4b..ccef35007118f8 100644 --- a/x-pack/plugins/cases/server/client/cases/get.ts +++ b/x-pack/plugins/cases/server/client/cases/get.ts @@ -8,14 +8,14 @@ import { SavedObjectsClientContract, Logger, SavedObject } from 'kibana/server'; import { flattenCaseSavedObject } from '../../routes/api/utils'; import { CaseResponseRt, CaseResponse, ESCaseAttributes } from '../../../common/api'; -import { CaseServiceSetup } from '../../services'; +import { CaseService } from '../../services'; import { countAlertsForID } from '../../common'; import { createCaseError } from '../../common/error'; import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; interface GetParams { savedObjectsClient: SavedObjectsClientContract; - caseService: CaseServiceSetup; + caseService: CaseService; id: string; includeComments?: boolean; includeSubCaseComments?: boolean; @@ -40,17 +40,17 @@ export const get = async ({ if (ENABLE_CASE_CONNECTOR) { const [caseInfo, subCasesForCaseId] = await Promise.all([ caseService.getCase({ - client: savedObjectsClient, + soClient: savedObjectsClient, id, }), - caseService.findSubCasesByCaseId({ client: savedObjectsClient, ids: [id] }), + caseService.findSubCasesByCaseId({ soClient: savedObjectsClient, ids: [id] }), ]); theCase = caseInfo; subCaseIds = subCasesForCaseId.saved_objects.map((so) => so.id); } else { theCase = await caseService.getCase({ - client: savedObjectsClient, + soClient: savedObjectsClient, id, }); } @@ -64,7 +64,7 @@ export const get = async ({ ); } const theComments = await caseService.getAllCaseComments({ - client: savedObjectsClient, + soClient: savedObjectsClient, id, options: { sortField: 'created_at', diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index 216ef109534fbd..c2c4d11da991dc 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -34,13 +34,14 @@ import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; import { createIncident, getCommentContextFromAttributes } from './utils'; import { - CaseConfigureServiceSetup, - CaseServiceSetup, - CaseUserActionServiceSetup, + CaseConfigureService, + CaseService, + CaseUserActionService, + AttachmentService, } from '../../services'; -import { CasesClientHandler } from '../client'; import { createCaseError } from '../../common/error'; import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; +import { CasesClient, CasesClientInternal } from '..'; /** * Returns true if the case should be closed based on the configuration settings and whether the case @@ -60,23 +61,27 @@ function shouldCloseByPush( interface PushParams { savedObjectsClient: SavedObjectsClientContract; - caseService: CaseServiceSetup; - caseConfigureService: CaseConfigureServiceSetup; - userActionService: CaseUserActionServiceSetup; + caseService: CaseService; + caseConfigureService: CaseConfigureService; + userActionService: CaseUserActionService; + attachmentService: AttachmentService; user: User; caseId: string; connectorId: string; - casesClient: CasesClientHandler; + casesClient: CasesClient; + casesClientInternal: CasesClientInternal; actionsClient: ActionsClient; logger: Logger; } export const push = async ({ savedObjectsClient, + attachmentService, caseService, caseConfigureService, userActionService, casesClient, + casesClientInternal, actionsClient, connectorId, caseId, @@ -93,13 +98,13 @@ export const push = async ({ try { [theCase, connector, userActions] = await Promise.all([ - casesClient.get({ + casesClient.cases.get({ id: caseId, includeComments: true, includeSubCaseComments: ENABLE_CASE_CONNECTOR, }), actionsClient.get({ id: connectorId }), - casesClient.getUserActions({ caseId }), + casesClient.userActions.getAll({ caseId }), ]); } catch (e) { const message = `Error getting case and/or connector and/or user actions: ${e.message}`; @@ -116,7 +121,7 @@ export const push = async ({ const alertsInfo = getAlertInfoFromComments(theCase?.comments); try { - alerts = await casesClient.getAlerts({ + alerts = await casesClientInternal.alerts.get({ alertsInfo, }); } catch (e) { @@ -128,7 +133,7 @@ export const push = async ({ } try { - connectorMappings = await casesClient.getMappings({ + connectorMappings = await casesClientInternal.configuration.getMappings({ actionsClient, connectorId: connector.id, connectorType: connector.actionTypeId, @@ -176,12 +181,12 @@ export const push = async ({ try { [myCase, myCaseConfigure, comments] = await Promise.all([ caseService.getCase({ - client: savedObjectsClient, + soClient: savedObjectsClient, id: caseId, }), - caseConfigureService.find({ client: savedObjectsClient }), + caseConfigureService.find({ soClient: savedObjectsClient }), caseService.getAllCaseComments({ - client: savedObjectsClient, + soClient: savedObjectsClient, id: caseId, options: { fields: [], @@ -219,7 +224,7 @@ export const push = async ({ try { [updatedCase, updatedComments] = await Promise.all([ caseService.patchCase({ - client: savedObjectsClient, + soClient: savedObjectsClient, caseId, updatedAttributes: { ...(shouldMarkAsClosed @@ -236,12 +241,12 @@ export const push = async ({ version: myCase.version, }), - caseService.patchComments({ - client: savedObjectsClient, + attachmentService.bulkUpdate({ + soClient: savedObjectsClient, comments: comments.saved_objects .filter((comment) => comment.attributes.pushed_at == null) .map((comment) => ({ - commentId: comment.id, + attachmentId: comment.id, updatedAttributes: { pushed_at: pushedDate, pushed_by: { username, full_name, email }, @@ -250,8 +255,8 @@ export const push = async ({ })), }), - userActionService.postUserActions({ - client: savedObjectsClient, + userActionService.bulkCreate({ + soClient: savedObjectsClient, actions: [ ...(shouldMarkAsClosed ? [ diff --git a/x-pack/plugins/cases/server/client/cases/update.ts b/x-pack/plugins/cases/server/client/cases/update.ts index fa9df2060ac5b7..52674e4c1b461c 100644 --- a/x-pack/plugins/cases/server/client/cases/update.ts +++ b/x-pack/plugins/cases/server/client/cases/update.ts @@ -47,17 +47,17 @@ import { transformCaseConnectorToEsConnector, } from '../../routes/api/cases/helpers'; -import { CaseServiceSetup, CaseUserActionServiceSetup } from '../../services'; +import { CaseService, CaseUserActionService } from '../../services'; import { CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT, } from '../../../common/constants'; -import { CasesClientHandler } from '..'; import { createAlertUpdateRequest } from '../../common'; -import { UpdateAlertRequest } from '../types'; import { createCaseError } from '../../common/error'; import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; +import { UpdateAlertRequest } from '../alerts/client'; +import { CasesClientInternal } from '../client_internal'; /** * Throws an error if any of the requests attempt to update a collection style cases' status field. @@ -123,15 +123,15 @@ function throwIfUpdateType(requests: ESCasePatchRequest[]) { async function throwIfInvalidUpdateOfTypeWithAlerts({ requests, caseService, - client, + soClient, }: { requests: ESCasePatchRequest[]; - caseService: CaseServiceSetup; - client: SavedObjectsClientContract; + caseService: CaseService; + soClient: SavedObjectsClientContract; }) { const getAlertsForID = async (caseToUpdate: ESCasePatchRequest) => { const alerts = await caseService.getAllCaseComments({ - client, + soClient, id: caseToUpdate.id, options: { fields: [], @@ -185,17 +185,17 @@ function getID( async function getAlertComments({ casesToSync, caseService, - client, + soClient, }: { casesToSync: ESCasePatchRequest[]; - caseService: CaseServiceSetup; - client: SavedObjectsClientContract; + caseService: CaseService; + soClient: SavedObjectsClientContract; }): Promise> { const idsOfCasesToSync = casesToSync.map((casePatchReq) => casePatchReq.id); // getAllCaseComments will by default get all the comments, unless page or perPage fields are set return caseService.getAllCaseComments({ - client, + soClient, id: idsOfCasesToSync, includeSubCaseComments: true, options: { @@ -214,11 +214,11 @@ async function getAlertComments({ async function getSubCasesToStatus({ totalAlerts, caseService, - client, + soClient, }: { totalAlerts: SavedObjectsFindResponse; - caseService: CaseServiceSetup; - client: SavedObjectsClientContract; + caseService: CaseService; + soClient: SavedObjectsClientContract; }): Promise> { const subCasesToRetrieve = totalAlerts.saved_objects.reduce((acc, alertComment) => { if ( @@ -235,7 +235,7 @@ async function getSubCasesToStatus({ const subCases = await caseService.getSubCases({ ids: Array.from(subCasesToRetrieve.values()), - client, + soClient, }); return subCases.saved_objects.reduce((acc, subCase) => { @@ -281,15 +281,15 @@ async function updateAlerts({ casesWithStatusChangedAndSynced, casesMap, caseService, - client, - casesClient, + soClient, + casesClientInternal, }: { casesWithSyncSettingChangedToOn: ESCasePatchRequest[]; casesWithStatusChangedAndSynced: ESCasePatchRequest[]; casesMap: Map>; - caseService: CaseServiceSetup; - client: SavedObjectsClientContract; - casesClient: CasesClientHandler; + caseService: CaseService; + soClient: SavedObjectsClientContract; + casesClientInternal: CasesClientInternal; }) { /** * It's possible that a case ID can appear multiple times in each array. I'm intentionally placing the status changes @@ -313,11 +313,11 @@ async function updateAlerts({ const totalAlerts = await getAlertComments({ casesToSync, caseService, - client, + soClient, }); // get a map of sub case id to the sub case status - const subCasesToStatus = await getSubCasesToStatus({ totalAlerts, client, caseService }); + const subCasesToStatus = await getSubCasesToStatus({ totalAlerts, soClient, caseService }); // create an array of requests that indicate the id, index, and status to update an alert const alertsToUpdate = totalAlerts.saved_objects.reduce( @@ -337,15 +337,15 @@ async function updateAlerts({ [] ); - await casesClient.updateAlertsStatus({ alerts: alertsToUpdate }); + await casesClientInternal.alerts.updateStatus({ alerts: alertsToUpdate }); } interface UpdateArgs { savedObjectsClient: SavedObjectsClientContract; - caseService: CaseServiceSetup; - userActionService: CaseUserActionServiceSetup; + caseService: CaseService; + userActionService: CaseUserActionService; user: User; - casesClient: CasesClientHandler; + casesClientInternal: CasesClientInternal; cases: CasesPatchRequest; logger: Logger; } @@ -355,7 +355,7 @@ export const update = async ({ caseService, userActionService, user, - casesClient, + casesClientInternal, cases, logger, }: UpdateArgs): Promise => { @@ -366,7 +366,7 @@ export const update = async ({ try { const myCases = await caseService.getCases({ - client: savedObjectsClient, + soClient: savedObjectsClient, caseIds: query.cases.map((q) => q.id), }); @@ -433,14 +433,14 @@ export const update = async ({ await throwIfInvalidUpdateOfTypeWithAlerts({ requests: updateFilterCases, caseService, - client: savedObjectsClient, + soClient: savedObjectsClient, }); // eslint-disable-next-line @typescript-eslint/naming-convention const { username, full_name, email } = user; const updatedDt = new Date().toISOString(); const updatedCases = await caseService.patchCases({ - client: savedObjectsClient, + soClient: savedObjectsClient, cases: updateFilterCases.map((thisCase) => { const { id: caseId, version, ...updateCaseAttributes } = thisCase; let closedInfo = {}; @@ -501,8 +501,8 @@ export const update = async ({ casesWithStatusChangedAndSynced, casesWithSyncSettingChangedToOn, caseService, - client: savedObjectsClient, - casesClient, + soClient: savedObjectsClient, + casesClientInternal, casesMap, }); @@ -523,8 +523,8 @@ export const update = async ({ }); }); - await userActionService.postUserActions({ - client: savedObjectsClient, + await userActionService.bulkCreate({ + soClient: savedObjectsClient, actions: buildCaseUserActions({ originalCases: myCases.saved_objects, updatedCases: updatedCases.saved_objects, diff --git a/x-pack/plugins/cases/server/client/client.ts b/x-pack/plugins/cases/server/client/client.ts index e9bfd1ef754b05..5f6cb8851c34cf 100644 --- a/x-pack/plugins/cases/server/client/client.ts +++ b/x-pack/plugins/cases/server/client/client.ts @@ -5,272 +5,43 @@ * 2.0. */ -import type { PublicMethodsOf } from '@kbn/utility-types'; -import { ElasticsearchClient, SavedObjectsClientContract, Logger } from 'src/core/server'; -import { - CasesClientConstructorArguments, - CasesClient, - ConfigureFields, - MappingsClient, - CasesClientUpdateAlertsStatus, - CasesClientAddComment, - CasesClientGet, - CasesClientGetUserActions, - CasesClientGetAlerts, - CasesClientPush, -} from './types'; -import { create } from './cases/create'; -import { update } from './cases/update'; -import { addComment } from './comments/add'; -import { getFields } from './configure/get_fields'; -import { getMappings } from './configure/get_mappings'; -import { updateAlertsStatus } from './alerts/update_status'; -import { - CaseConfigureServiceSetup, - CaseServiceSetup, - ConnectorMappingsServiceSetup, - CaseUserActionServiceSetup, - AlertServiceContract, -} from '../services'; -import { CasesPatchRequest, CasePostRequest, User, CasesFindRequest } from '../../common/api'; -import { get } from './cases/get'; -import { get as getUserActions } from './user_actions/get'; -import { get as getAlerts } from './alerts/get'; -import { push } from './cases/push'; -import { createCaseError } from '../common/error'; -import { Authorization } from '../authorization/authorization'; -import { find } from './cases/find'; +import { CasesClientArgs } from './types'; +import { CasesSubClient, createCasesSubClient } from './cases/client'; +import { AttachmentsSubClient, createAttachmentsSubClient } from './attachments/client'; +import { UserActionsSubClient, createUserActionsSubClient } from './user_actions/client'; +import { CasesClientInternal, createCasesClientInternal } from './client_internal'; -/** - * This class is a pass through for common case functionality (like creating, get a case). - */ -export class CasesClientHandler implements CasesClient { - private readonly _scopedClusterClient: ElasticsearchClient; - private readonly _caseConfigureService: CaseConfigureServiceSetup; - private readonly _caseService: CaseServiceSetup; - private readonly _connectorMappingsService: ConnectorMappingsServiceSetup; - private readonly user: User; - private readonly _savedObjectsClient: SavedObjectsClientContract; - private readonly _userActionService: CaseUserActionServiceSetup; - private readonly _alertsService: AlertServiceContract; - private readonly logger: Logger; - private readonly authorization: PublicMethodsOf; - - constructor(clientArgs: CasesClientConstructorArguments) { - this._scopedClusterClient = clientArgs.scopedClusterClient; - this._caseConfigureService = clientArgs.caseConfigureService; - this._caseService = clientArgs.caseService; - this._connectorMappingsService = clientArgs.connectorMappingsService; - this.user = clientArgs.user; - this._savedObjectsClient = clientArgs.savedObjectsClient; - this._userActionService = clientArgs.userActionService; - this._alertsService = clientArgs.alertsService; - this.logger = clientArgs.logger; - this.authorization = clientArgs.authorization; - } - - public async create(caseInfo: CasePostRequest) { - try { - // TODO: authorize the user - return create({ - savedObjectsClient: this._savedObjectsClient, - caseService: this._caseService, - caseConfigureService: this._caseConfigureService, - userActionService: this._userActionService, - user: this.user, - theCase: caseInfo, - logger: this.logger, - auth: this.authorization, - }); - } catch (error) { - throw createCaseError({ - message: `Failed to create a new case using client: ${error}`, - error, - logger: this.logger, - }); - } - } - - public async find(options: CasesFindRequest) { - try { - // TODO: authorize the user - return find({ - savedObjectsClient: this._savedObjectsClient, - caseService: this._caseService, - logger: this.logger, - auth: this.authorization, - options, - }); - } catch (error) { - throw createCaseError({ - message: `Failed to find cases using client: ${error}`, - error, - logger: this.logger, - }); - } - } - - public async update(cases: CasesPatchRequest) { - try { - return update({ - savedObjectsClient: this._savedObjectsClient, - caseService: this._caseService, - userActionService: this._userActionService, - user: this.user, - cases, - casesClient: this, - logger: this.logger, - }); - } catch (error) { - const caseIDVersions = cases.cases.map((caseInfo) => ({ - id: caseInfo.id, - version: caseInfo.version, - })); - throw createCaseError({ - message: `Failed to update cases using client: ${JSON.stringify(caseIDVersions)}: ${error}`, - error, - logger: this.logger, - }); - } - } - - public async addComment({ caseId, comment }: CasesClientAddComment) { - try { - return addComment({ - savedObjectsClient: this._savedObjectsClient, - caseService: this._caseService, - userActionService: this._userActionService, - casesClient: this, - caseId, - comment, - user: this.user, - logger: this.logger, - }); - } catch (error) { - throw createCaseError({ - message: `Failed to add comment using client case id: ${caseId}: ${error}`, - error, - logger: this.logger, - }); - } - } - - public async getFields(fields: ConfigureFields) { - try { - return getFields(fields); - } catch (error) { - throw createCaseError({ - message: `Failed to retrieve fields using client: ${error}`, - error, - logger: this.logger, - }); - } - } - - public async getMappings(args: MappingsClient) { - try { - return getMappings({ - ...args, - savedObjectsClient: this._savedObjectsClient, - connectorMappingsService: this._connectorMappingsService, - casesClient: this, - logger: this.logger, - }); - } catch (error) { - throw createCaseError({ - message: `Failed to get mappings using client: ${error}`, - error, - logger: this.logger, - }); - } - } +export class CasesClient { + private readonly _casesClientInternal: CasesClientInternal; + private readonly _cases: CasesSubClient; + private readonly _attachments: AttachmentsSubClient; + private readonly _userActions: UserActionsSubClient; - public async updateAlertsStatus(args: CasesClientUpdateAlertsStatus) { - try { - return updateAlertsStatus({ - ...args, - alertsService: this._alertsService, - scopedClusterClient: this._scopedClusterClient, - logger: this.logger, - }); - } catch (error) { - throw createCaseError({ - message: `Failed to update alerts status using client alerts: ${JSON.stringify( - args.alerts - )}: ${error}`, - error, - logger: this.logger, - }); - } + constructor(args: CasesClientArgs) { + this._casesClientInternal = createCasesClientInternal(args); + this._cases = createCasesSubClient(args, this, this._casesClientInternal); + this._attachments = createAttachmentsSubClient(args, this._casesClientInternal); + this._userActions = createUserActionsSubClient(args); } - public async get(args: CasesClientGet) { - try { - return get({ - ...args, - caseService: this._caseService, - savedObjectsClient: this._savedObjectsClient, - logger: this.logger, - }); - } catch (error) { - this.logger.error(`Failed to get case using client id: ${args.id}: ${error}`); - throw error; - } + public get cases() { + return this._cases; } - public async getUserActions(args: CasesClientGetUserActions) { - try { - return getUserActions({ - ...args, - savedObjectsClient: this._savedObjectsClient, - userActionService: this._userActionService, - }); - } catch (error) { - throw createCaseError({ - message: `Failed to get user actions using client id: ${args.caseId}: ${error}`, - error, - logger: this.logger, - }); - } + public get attachments() { + return this._attachments; } - public async getAlerts(args: CasesClientGetAlerts) { - try { - return getAlerts({ - ...args, - alertsService: this._alertsService, - scopedClusterClient: this._scopedClusterClient, - logger: this.logger, - }); - } catch (error) { - throw createCaseError({ - message: `Failed to get alerts using client requested alerts: ${JSON.stringify( - args.alertsInfo - )}: ${error}`, - error, - logger: this.logger, - }); - } + public get userActions() { + return this._userActions; } - public async push(args: CasesClientPush) { - try { - return push({ - ...args, - savedObjectsClient: this._savedObjectsClient, - caseService: this._caseService, - userActionService: this._userActionService, - user: this.user, - casesClient: this, - caseConfigureService: this._caseConfigureService, - logger: this.logger, - }); - } catch (error) { - throw createCaseError({ - message: `Failed to push case using client id: ${args.caseId}: ${error}`, - error, - logger: this.logger, - }); - } + // TODO: Remove it when all routes will be moved to the cases client. + public get casesClientInternal() { + return this._casesClientInternal; } } + +export const createCasesClient = (args: CasesClientArgs): CasesClient => { + return new CasesClient(args); +}; diff --git a/x-pack/plugins/cases/server/client/client_internal.ts b/x-pack/plugins/cases/server/client/client_internal.ts new file mode 100644 index 00000000000000..79f107e17af35d --- /dev/null +++ b/x-pack/plugins/cases/server/client/client_internal.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CasesClientArgs } from './types'; +import { AlertSubClient, createAlertsSubClient } from './alerts/client'; +import { ConfigureSubClient, createConfigurationSubClient } from './configure/client'; + +export class CasesClientInternal { + private readonly _alerts: AlertSubClient; + private readonly _configuration: ConfigureSubClient; + + constructor(args: CasesClientArgs) { + this._alerts = createAlertsSubClient(args); + this._configuration = createConfigurationSubClient(args, this); + } + + public get alerts() { + return this._alerts; + } + + public get configuration() { + return this._configuration; + } +} + +export const createCasesClientInternal = (args: CasesClientArgs): CasesClientInternal => { + return new CasesClientInternal(args); +}; diff --git a/x-pack/plugins/cases/server/client/configure/client.ts b/x-pack/plugins/cases/server/client/configure/client.ts new file mode 100644 index 00000000000000..8ea91415fd1635 --- /dev/null +++ b/x-pack/plugins/cases/server/client/configure/client.ts @@ -0,0 +1,51 @@ +/* + * 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 { ActionsClient } from '../../../../actions/server'; +import { ConnectorMappingsAttributes, GetFieldsResponse } from '../../../common/api'; +import { CasesClientInternal } from '../client_internal'; +import { CasesClientArgs } from '../types'; +import { getFields } from './get_fields'; +import { getMappings } from './get_mappings'; + +export interface ConfigurationGetFields { + actionsClient: ActionsClient; + connectorId: string; + connectorType: string; +} + +export interface ConfigurationGetMappings { + actionsClient: ActionsClient; + connectorId: string; + connectorType: string; +} + +export interface ConfigureSubClient { + getFields(args: ConfigurationGetFields): Promise; + getMappings(args: ConfigurationGetMappings): Promise; +} + +export const createConfigurationSubClient = ( + args: CasesClientArgs, + casesClientInternal: CasesClientInternal +): ConfigureSubClient => { + const { savedObjectsClient, connectorMappingsService, logger } = args; + + const configureSubClient: ConfigureSubClient = { + getFields: (fields: ConfigurationGetFields) => getFields(fields), + getMappings: (params: ConfigurationGetMappings) => + getMappings({ + ...params, + savedObjectsClient, + connectorMappingsService, + casesClientInternal, + logger, + }), + }; + + return Object.freeze(configureSubClient); +}; 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 deabae33810b2e..799f50845dda6b 100644 --- a/x-pack/plugins/cases/server/client/configure/get_fields.ts +++ b/x-pack/plugins/cases/server/client/configure/get_fields.ts @@ -8,14 +8,14 @@ import Boom from '@hapi/boom'; import { GetFieldsResponse } from '../../../common/api'; -import { ConfigureFields } from '../types'; +import { ConfigurationGetFields } from './client'; import { createDefaultMapping, formatFields } from './utils'; export const getFields = async ({ actionsClient, connectorType, connectorId, -}: ConfigureFields): Promise => { +}: ConfigurationGetFields): Promise => { 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 558c961f89e5bd..c157252909f669 100644 --- a/x-pack/plugins/cases/server/client/configure/get_mappings.ts +++ b/x-pack/plugins/cases/server/client/configure/get_mappings.ts @@ -10,15 +10,15 @@ import { ActionsClient } from '../../../../actions/server'; 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 { ConnectorMappingsServiceSetup } from '../../services'; -import { CasesClientHandler } from '..'; +import { ConnectorMappingsService } from '../../services'; +import { CasesClientInternal } from '..'; import { createCaseError } from '../../common/error'; interface GetMappingsArgs { savedObjectsClient: SavedObjectsClientContract; - connectorMappingsService: ConnectorMappingsServiceSetup; + connectorMappingsService: ConnectorMappingsService; actionsClient: ActionsClient; - casesClient: CasesClientHandler; + casesClientInternal: CasesClientInternal; connectorType: string; connectorId: string; logger: Logger; @@ -28,7 +28,7 @@ export const getMappings = async ({ savedObjectsClient, connectorMappingsService, actionsClient, - casesClient, + casesClientInternal, connectorType, connectorId, logger, @@ -38,7 +38,7 @@ export const getMappings = async ({ return []; } const myConnectorMappings = await connectorMappingsService.find({ - client: savedObjectsClient, + soClient: savedObjectsClient, options: { hasReference: { type: ACTION_SAVED_OBJECT_TYPE, @@ -49,13 +49,13 @@ export const getMappings = async ({ let theMapping; // Create connector mappings if there are none if (myConnectorMappings.total === 0) { - const res = await casesClient.getFields({ + const res = await casesClientInternal.configuration.getFields({ actionsClient, connectorId, connectorType, }); theMapping = await connectorMappingsService.post({ - client: savedObjectsClient, + soClient: savedObjectsClient, attributes: { mappings: res.defaultMappings, }, diff --git a/x-pack/plugins/cases/server/client/factory.ts b/x-pack/plugins/cases/server/client/factory.ts index 89ee0cdf78c75a..d622861ac65b40 100644 --- a/x-pack/plugins/cases/server/client/factory.ts +++ b/x-pack/plugins/cases/server/client/factory.ts @@ -17,20 +17,22 @@ import { Authorization } from '../authorization/authorization'; import { GetSpaceFn } from '../authorization/types'; import { AlertServiceContract, - CaseConfigureServiceSetup, - CaseServiceSetup, - CaseUserActionServiceSetup, - ConnectorMappingsServiceSetup, + CaseConfigureService, + CaseService, + CaseUserActionService, + ConnectorMappingsService, + AttachmentService, } from '../services'; -import { CasesClientHandler } from './client'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; +import { CasesClient, createCasesClient } from '.'; interface CasesClientFactoryArgs { - caseConfigureService: CaseConfigureServiceSetup; - caseService: CaseServiceSetup; - connectorMappingsService: ConnectorMappingsServiceSetup; - userActionService: CaseUserActionServiceSetup; + caseConfigureService: CaseConfigureService; + caseService: CaseService; + connectorMappingsService: ConnectorMappingsService; + userActionService: CaseUserActionService; alertsService: AlertServiceContract; + attachmentService: AttachmentService; securityPluginSetup?: SecurityPluginSetup; securityPluginStart?: SecurityPluginStart; getSpace: GetSpaceFn; @@ -39,7 +41,7 @@ interface CasesClientFactoryArgs { } /** - * This class handles the logic for creating a CasesClientHandler. We need this because some of the member variables + * This class handles the logic for creating a CasesClient. We need this because some of the member variables * can't be initialized until a plugin's start() method but we need to register the case context in the setup() method. */ export class CasesClientFactory { @@ -71,7 +73,7 @@ export class CasesClientFactory { request?: KibanaRequest; savedObjectsService?: SavedObjectsServiceStart; scopedClusterClient: ElasticsearchClient; - }): Promise { + }): Promise { if (!this.isInitialized || !this.options) { throw new Error('CasesClientFactory must be initialized before calling create'); } @@ -93,7 +95,7 @@ export class CasesClientFactory { const user = this.options.caseService.getUser({ request }); - return new CasesClientHandler({ + return createCasesClient({ alertsService: this.options.alertsService, scopedClusterClient, savedObjectsClient: savedObjectsService.getScopedClient(request, { @@ -104,6 +106,7 @@ export class CasesClientFactory { caseConfigureService: this.options.caseConfigureService, connectorMappingsService: this.options.connectorMappingsService, userActionService: this.options.userActionService, + attachmentService: this.options.attachmentService, logger: this.logger, authorization: auth, }); diff --git a/x-pack/plugins/cases/server/client/index.ts b/x-pack/plugins/cases/server/client/index.ts index 39c7f6f98c2595..7904e65ca62766 100644 --- a/x-pack/plugins/cases/server/client/index.ts +++ b/x-pack/plugins/cases/server/client/index.ts @@ -5,18 +5,8 @@ * 2.0. */ -import { CasesClientConstructorArguments, CasesClient } from './types'; -import { CasesClientHandler } from './client'; - -export { CasesClientHandler } from './client'; -export { CasesClient } from './types'; - -/** - * Create a CasesClientHandler to external services (other plugins). - */ -export const createExternalCasesClient = ( - clientArgs: CasesClientConstructorArguments -): CasesClient => { - const client = new CasesClientHandler(clientArgs); - return client; -}; +export { CasesClient } from './client'; +export { CasesClientInternal } from './client_internal'; +export { CasesClientArgs } from './types'; +export { createCasesClient } from './client'; +export { createCasesClientInternal } from './client_internal'; diff --git a/x-pack/plugins/cases/server/client/mocks.ts b/x-pack/plugins/cases/server/client/mocks.ts index 84aa566086663f..174904c1f66be6 100644 --- a/x-pack/plugins/cases/server/client/mocks.ts +++ b/x-pack/plugins/cases/server/client/mocks.ts @@ -16,7 +16,7 @@ import { AlertServiceContract, CaseConfigureService, CaseService, - CaseUserActionServiceSetup, + CaseUserActionService, ConnectorMappingsService, } from '../services'; import { CasesClient } from './types'; @@ -51,7 +51,7 @@ export const createCasesClientWithMockSavedObjectsClient = async ({ }): Promise<{ client: CasesClient; services: { - userActionService: jest.Mocked; + userActionService: jest.Mocked; alertsService: jest.Mocked; }; esClient: DeeplyMockedKeys; diff --git a/x-pack/plugins/cases/server/client/types.ts b/x-pack/plugins/cases/server/client/types.ts index b0276fc10aadae..0592dd321819de 100644 --- a/x-pack/plugins/cases/server/client/types.ts +++ b/x-pack/plugins/cases/server/client/types.ts @@ -7,115 +7,27 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import { ElasticsearchClient, SavedObjectsClientContract, Logger } from 'kibana/server'; -import { ActionsClient } from '../../../actions/server'; -import { - CasePostRequest, - CaseResponse, - CasesPatchRequest, - CasesResponse, - CaseStatuses, - CommentRequest, - ConnectorMappingsAttributes, - GetFieldsResponse, - CaseUserActionsResponse, - User, - CasesFindRequest, - CasesFindResponse, -} from '../../common/api'; +import { User } from '../../common/api'; import { Authorization } from '../authorization/authorization'; -import { AlertInfo } from '../common'; import { - CaseConfigureServiceSetup, - CaseServiceSetup, - CaseUserActionServiceSetup, AlertServiceContract, + CaseConfigureService, + CaseService, + CaseUserActionService, + ConnectorMappingsService, + AttachmentService, } from '../services'; -import { ConnectorMappingsServiceSetup } from '../services/connector_mappings'; -import { CasesClientGetAlertsResponse } from './alerts/types'; - -export interface CasesClientGet { - id: string; - includeComments?: boolean; - includeSubCaseComments?: boolean; -} - -export interface CasesClientPush { - actionsClient: ActionsClient; - caseId: string; - connectorId: string; -} - -export interface CasesClientAddComment { - caseId: string; - comment: CommentRequest; -} - -export interface CasesClientUpdateAlertsStatus { - alerts: UpdateAlertRequest[]; -} - -export interface CasesClientGetAlerts { - alertsInfo: AlertInfo[]; -} - -export interface CasesClientGetUserActions { - caseId: string; - subCaseId?: string; -} - -export interface MappingsClient { - actionsClient: ActionsClient; - connectorId: string; - connectorType: string; -} - -export interface CasesClientConstructorArguments { - scopedClusterClient: ElasticsearchClient; - caseConfigureService: CaseConfigureServiceSetup; - caseService: CaseServiceSetup; - connectorMappingsService: ConnectorMappingsServiceSetup; - user: User; - savedObjectsClient: SavedObjectsClientContract; - userActionService: CaseUserActionServiceSetup; - alertsService: AlertServiceContract; - logger: Logger; - authorization: PublicMethodsOf; -} - -export interface ConfigureFields { - actionsClient: ActionsClient; - connectorId: string; - connectorType: string; -} - -/** - * Defines the fields necessary to update an alert's status. - */ -export interface UpdateAlertRequest { - id: string; - index: string; - status: CaseStatuses; -} - -/** - * This represents the interface that other plugins can access. - */ -export interface CasesClient { - addComment(args: CasesClientAddComment): Promise; - create(theCase: CasePostRequest): Promise; - get(args: CasesClientGet): Promise; - getAlerts(args: CasesClientGetAlerts): Promise; - getFields(args: ConfigureFields): Promise; - getMappings(args: MappingsClient): Promise; - getUserActions(args: CasesClientGetUserActions): Promise; - find(args: CasesFindRequest): Promise; - push(args: CasesClientPush): Promise; - update(args: CasesPatchRequest): Promise; - updateAlertsStatus(args: CasesClientUpdateAlertsStatus): Promise; -} -export interface MappingsClient { - actionsClient: ActionsClient; - connectorId: string; - connectorType: string; +export interface CasesClientArgs { + readonly scopedClusterClient: ElasticsearchClient; + readonly caseConfigureService: CaseConfigureService; + readonly caseService: CaseService; + readonly connectorMappingsService: ConnectorMappingsService; + readonly user: User; + readonly savedObjectsClient: SavedObjectsClientContract; + readonly userActionService: CaseUserActionService; + readonly alertsService: AlertServiceContract; + readonly attachmentService: AttachmentService; + readonly logger: Logger; + readonly authorization: PublicMethodsOf; } diff --git a/x-pack/plugins/cases/server/client/user_actions/client.ts b/x-pack/plugins/cases/server/client/user_actions/client.ts new file mode 100644 index 00000000000000..50d9270440e43b --- /dev/null +++ b/x-pack/plugins/cases/server/client/user_actions/client.ts @@ -0,0 +1,34 @@ +/* + * 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 { CaseUserActionsResponse } from '../../../common/api'; +import { CasesClientArgs } from '../types'; +import { get } from './get'; + +export interface UserActionGet { + caseId: string; + subCaseId?: string; +} + +export interface UserActionsSubClient { + getAll(args: UserActionGet): Promise; +} + +export const createUserActionsSubClient = (args: CasesClientArgs): UserActionsSubClient => { + const { savedObjectsClient, userActionService } = args; + + const attachmentSubClient: UserActionsSubClient = { + getAll: (params: UserActionGet) => + get({ + ...params, + savedObjectsClient, + userActionService, + }), + }; + + 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 92c67b0d1591d5..cebd3da1b6f7e0 100644 --- a/x-pack/plugins/cases/server/client/user_actions/get.ts +++ b/x-pack/plugins/cases/server/client/user_actions/get.ts @@ -12,11 +12,11 @@ import { CASE_COMMENT_SAVED_OBJECT, } from '../../../common/constants'; import { CaseUserActionsResponseRt, CaseUserActionsResponse } from '../../../common/api'; -import { CaseUserActionServiceSetup } from '../../services'; +import { CaseUserActionService } from '../../services'; interface GetParams { savedObjectsClient: SavedObjectsClientContract; - userActionService: CaseUserActionServiceSetup; + userActionService: CaseUserActionService; caseId: string; subCaseId?: string; } @@ -27,8 +27,8 @@ export const get = async ({ caseId, subCaseId, }: GetParams): Promise => { - const userActions = await userActionService.getUserActions({ - client: savedObjectsClient, + const userActions = await userActionService.getAll({ + soClient: savedObjectsClient, caseId, subCaseId, }); diff --git a/x-pack/plugins/cases/server/common/models/commentable_case.ts b/x-pack/plugins/cases/server/common/models/commentable_case.ts index 527d851631583d..fb34c5fecea394 100644 --- a/x-pack/plugins/cases/server/common/models/commentable_case.ts +++ b/x-pack/plugins/cases/server/common/models/commentable_case.ts @@ -35,7 +35,7 @@ import { transformNewComment, } from '../../routes/api/utils'; import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../common/constants'; -import { CaseServiceSetup } from '../../services'; +import { AttachmentService, CaseService } from '../../services'; import { createCaseError } from '../error'; import { countAlertsForID } from '../index'; @@ -53,7 +53,8 @@ interface CommentableCaseParams { collection: SavedObject; subCase?: SavedObject; soClient: SavedObjectsClientContract; - service: CaseServiceSetup; + caseService: CaseService; + attachmentService: AttachmentService; logger: Logger; } @@ -65,14 +66,23 @@ export class CommentableCase { private readonly collection: SavedObject; private readonly subCase?: SavedObject; private readonly soClient: SavedObjectsClientContract; - private readonly service: CaseServiceSetup; + private readonly caseService: CaseService; + private readonly attachmentService: AttachmentService; private readonly logger: Logger; - constructor({ collection, subCase, soClient, service, logger }: CommentableCaseParams) { + constructor({ + collection, + subCase, + soClient, + caseService, + attachmentService, + logger, + }: CommentableCaseParams) { this.collection = collection; this.subCase = subCase; this.soClient = soClient; - this.service = service; + this.caseService = caseService; + this.attachmentService = attachmentService; this.logger = logger; } @@ -129,8 +139,8 @@ export class CommentableCase { let updatedSubCaseAttributes: SavedObject | undefined; if (this.subCase) { - const updatedSubCase = await this.service.patchSubCase({ - client: this.soClient, + const updatedSubCase = await this.caseService.patchSubCase({ + soClient: this.soClient, subCaseId: this.subCase.id, updatedAttributes: { updated_at: date, @@ -151,8 +161,8 @@ export class CommentableCase { }; } - const updatedCase = await this.service.patchCase({ - client: this.soClient, + const updatedCase = await this.caseService.patchCase({ + soClient: this.soClient, caseId: this.collection.id, updatedAttributes: { updated_at: date, @@ -173,7 +183,8 @@ export class CommentableCase { }, subCase: updatedSubCaseAttributes, soClient: this.soClient, - service: this.service, + caseService: this.caseService, + attachmentService: this.attachmentService, logger: this.logger, }); } catch (error) { @@ -201,9 +212,9 @@ export class CommentableCase { const { id, version, ...queryRestAttributes } = updateRequest; const [comment, commentableCase] = await Promise.all([ - this.service.patchComment({ - client: this.soClient, - commentId: id, + this.attachmentService.update({ + soClient: this.soClient, + attachmentId: id, updatedAttributes: { ...queryRestAttributes, updated_at: updatedAt, @@ -250,8 +261,8 @@ export class CommentableCase { } const [comment, commentableCase] = await Promise.all([ - this.service.postNewComment({ - client: this.soClient, + this.attachmentService.create({ + soClient: this.soClient, attributes: transformNewComment({ associationType: this.subCase ? AssociationType.subCase : AssociationType.case, createdDate, @@ -287,8 +298,8 @@ export class CommentableCase { public async encode(): Promise { try { - const collectionCommentStats = await this.service.getAllCaseComments({ - client: this.soClient, + const collectionCommentStats = await this.caseService.getAllCaseComments({ + soClient: this.soClient, id: this.collection.id, options: { fields: [], @@ -297,8 +308,8 @@ export class CommentableCase { }, }); - const collectionComments = await this.service.getAllCaseComments({ - client: this.soClient, + const collectionComments = await this.caseService.getAllCaseComments({ + soClient: this.soClient, id: this.collection.id, options: { fields: [], @@ -317,8 +328,8 @@ export class CommentableCase { }; if (this.subCase) { - const subCaseComments = await this.service.getAllSubCaseComments({ - client: this.soClient, + const subCaseComments = await this.caseService.getAllSubCaseComments({ + soClient: this.soClient, id: this.subCase.id, }); const totalAlerts = diff --git a/x-pack/plugins/cases/server/common/utils.ts b/x-pack/plugins/cases/server/common/utils.ts index 88cce82389c4df..36f5dc9cbb00a8 100644 --- a/x-pack/plugins/cases/server/common/utils.ts +++ b/x-pack/plugins/cases/server/common/utils.ts @@ -13,7 +13,7 @@ import { CommentType, User, } from '../../common/api'; -import { UpdateAlertRequest } from '../client/types'; +import { UpdateAlertRequest } from '../client/alerts/client'; import { getAlertInfoFromComments } from '../routes/api/utils'; /** diff --git a/x-pack/plugins/cases/server/connectors/case/index.ts b/x-pack/plugins/cases/server/connectors/case/index.ts index 5c069135b92f64..6f8132d77a05fb 100644 --- a/x-pack/plugins/cases/server/connectors/case/index.ts +++ b/x-pack/plugins/cases/server/connectors/case/index.ts @@ -76,7 +76,7 @@ async function executor( if (subAction === 'create') { try { - data = await casesClient.create({ + data = await casesClient.cases.create({ ...(subActionParams as CasePostRequest), }); } catch (error) { @@ -98,7 +98,7 @@ async function executor( ); try { - data = await casesClient.update({ cases: [updateParamsWithoutNullValues] }); + data = await casesClient.cases.update({ cases: [updateParamsWithoutNullValues] }); } catch (error) { throw createCaseError({ message: `Failed to update case using connector id: ${updateParamsWithoutNullValues?.id} version: ${updateParamsWithoutNullValues?.version}: ${error}`, @@ -112,7 +112,7 @@ async function executor( const { caseId, comment } = subActionParams as ExecutorSubActionAddCommentParams; try { const formattedComment = transformConnectorComment(comment, logger); - data = await casesClient.addComment({ caseId, comment: formattedComment }); + data = await casesClient.attachments.add({ caseId, comment: formattedComment }); } catch (error) { throw createCaseError({ message: `Failed to create comment using connector case id: ${caseId}: ${error}`, diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index d641e581c22049..2ccc362280b9f7 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -24,13 +24,9 @@ import { } from './saved_object_types'; import { CaseConfigureService, - CaseConfigureServiceSetup, CaseService, - CaseServiceSetup, CaseUserActionService, - CaseUserActionServiceSetup, ConnectorMappingsService, - ConnectorMappingsServiceSetup, AlertService, } from './services'; import { CasesClient } from './client'; @@ -39,6 +35,7 @@ import type { CasesRequestHandlerContext } from './types'; import { CasesClientFactory } from './client/factory'; import { SpacesPluginStart } from '../../spaces/server'; import { PluginStartContract as FeaturesPluginStart } from '../../features/server'; +import { AttachmentService } from './services/attachments'; function createConfig(context: PluginInitializerContext) { return context.config.get(); @@ -57,17 +54,18 @@ export interface PluginsStart { export class CasePlugin { private readonly log: Logger; - private caseConfigureService?: CaseConfigureServiceSetup; - private caseService?: CaseServiceSetup; - private connectorMappingsService?: ConnectorMappingsServiceSetup; - private userActionService?: CaseUserActionServiceSetup; + private caseConfigureService?: CaseConfigureService; + private caseService?: CaseService; + private connectorMappingsService?: ConnectorMappingsService; + private userActionService?: CaseUserActionService; private alertsService?: AlertService; + private attachmentService?: AttachmentService; private clientFactory: CasesClientFactory; private securityPluginSetup?: SecurityPluginSetup; private config?: ConfigType; constructor(private readonly initializerContext: PluginInitializerContext) { - this.log = this.initializerContext.logger.get(); + this.log = this.initializerContext.logger.get('plugins', 'cases'); this.clientFactory = new CasesClientFactory(this.log); } @@ -98,10 +96,11 @@ export class CasePlugin { this.log, plugins.security != null ? plugins.security.authc : undefined ); - this.caseConfigureService = await new CaseConfigureService(this.log).setup(); - this.connectorMappingsService = await new ConnectorMappingsService(this.log).setup(); - this.userActionService = await new CaseUserActionService(this.log).setup(); + this.caseConfigureService = new CaseConfigureService(this.log); + this.connectorMappingsService = new ConnectorMappingsService(this.log); + this.userActionService = new CaseUserActionService(this.log); this.alertsService = new AlertService(); + this.attachmentService = new AttachmentService(this.log); core.http.registerRouteHandlerContext( APP_ID, @@ -117,6 +116,7 @@ export class CasePlugin { caseConfigureService: this.caseConfigureService, connectorMappingsService: this.connectorMappingsService, userActionService: this.userActionService, + attachmentService: this.attachmentService, router, }); @@ -139,6 +139,7 @@ export class CasePlugin { caseService: this.caseService!, connectorMappingsService: this.connectorMappingsService!, userActionService: this.userActionService!, + attachmentService: this.attachmentService!, securityPluginSetup: this.securityPluginSetup, securityPluginStart: plugins.security, getSpace: async (request: KibanaRequest) => { diff --git a/x-pack/plugins/cases/server/routes/api/__fixtures__/route_contexts.ts b/x-pack/plugins/cases/server/routes/api/__fixtures__/route_contexts.ts index a1f1a7fe47eed7..3306712c1e550f 100644 --- a/x-pack/plugins/cases/server/routes/api/__fixtures__/route_contexts.ts +++ b/x-pack/plugins/cases/server/routes/api/__fixtures__/route_contexts.ts @@ -51,7 +51,7 @@ export const createRouteContext = async (client: any, badAuth = false) => { savedObjectsService.getScopedClient.mockReturnValue(client); const contextMock = xpackMocks.createRequestHandlerContext(); - // The tests check the calls on the saved object client, so we need to make sure it is the same one returned by + // The tests check the calls on the saved object soClient, so we need to make sure it is the same one returned by // getScopedClient and .client contextMock.core.savedObjects.getClient = jest.fn(() => client); contextMock.core.savedObjects.client = client; diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/delete_all_comments.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/delete_all_comments.ts index 62a372f6d69e15..4439b215599a9c 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/delete_all_comments.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/delete_all_comments.ts @@ -18,6 +18,7 @@ import { import { AssociationType } from '../../../../../common/api'; export function initDeleteAllCommentsApi({ + attachmentService, caseService, router, userActionService, @@ -45,7 +46,7 @@ export function initDeleteAllCommentsApi({ ); } - const client = context.core.savedObjects.getClient({ + const soClient = context.core.savedObjects.getClient({ includedHiddenTypes: SAVED_OBJECT_TYPES, }); // eslint-disable-next-line @typescript-eslint/naming-convention @@ -55,22 +56,22 @@ export function initDeleteAllCommentsApi({ const subCaseId = request.query?.subCaseId; const id = subCaseId ?? request.params.case_id; const comments = await caseService.getCommentsByAssociation({ - client, + soClient, id, associationType: subCaseId ? AssociationType.subCase : AssociationType.case, }); await Promise.all( comments.saved_objects.map((comment) => - caseService.deleteComment({ - client, - commentId: comment.id, + attachmentService.delete({ + soClient, + attachmentId: comment.id, }) ) ); - await userActionService.postUserActions({ - client, + await userActionService.bulkCreate({ + soClient, actions: comments.saved_objects.map((comment) => buildCommentUserActionItem({ action: 'delete', diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.ts index 189dbc684cb821..4818ec607cc26d 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.ts @@ -20,6 +20,7 @@ import { } from '../../../../../common/constants'; export function initDeleteCommentApi({ + attachmentService, caseService, router, userActionService, @@ -48,16 +49,16 @@ export function initDeleteCommentApi({ ); } - const client = context.core.savedObjects.getClient({ + const soClient = context.core.savedObjects.getClient({ includedHiddenTypes: SAVED_OBJECT_TYPES, }); // eslint-disable-next-line @typescript-eslint/naming-convention const { username, full_name, email } = caseService.getUser({ request }); const deleteDate = new Date().toISOString(); - const myComment = await caseService.getComment({ - client, - commentId: request.params.comment_id, + const myComment = await attachmentService.get({ + soClient, + attachmentId: request.params.comment_id, }); if (myComment == null) { @@ -74,13 +75,13 @@ export function initDeleteCommentApi({ ); } - await caseService.deleteComment({ - client, - commentId: request.params.comment_id, + await attachmentService.delete({ + soClient, + attachmentId: request.params.comment_id, }); - await userActionService.postUserActions({ - client, + await userActionService.bulkCreate({ + soClient, actions: [ buildCommentUserActionItem({ action: 'delete', diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/find_comments.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/find_comments.ts index 9e23a28c0725b2..988d0324ec02a5 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/find_comments.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/find_comments.ts @@ -48,7 +48,7 @@ export function initFindCaseCommentsApi({ caseService, router, logger }: RouteDe }, async (context, request, response) => { try { - const client = context.core.savedObjects.getClient({ + const soClient = context.core.savedObjects.getClient({ includedHiddenTypes: SAVED_OBJECT_TYPES, }); const query = pipe( @@ -68,7 +68,7 @@ export function initFindCaseCommentsApi({ caseService, router, logger }: RouteDe const args = query ? { caseService, - client, + soClient, id, options: { // We need this because the default behavior of getAllCaseComments is to return all the comments @@ -84,7 +84,7 @@ export function initFindCaseCommentsApi({ caseService, router, logger }: RouteDe } : { caseService, - client, + soClient, id, options: { page: defaultPage, diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts index 8f48dbbf0348ca..af87cbccb3bf34 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts @@ -37,7 +37,7 @@ export function initGetAllCommentsApi({ caseService, router, logger }: RouteDeps }, async (context, request, response) => { try { - const client = context.core.savedObjects.getClient({ + const soClient = context.core.savedObjects.getClient({ includedHiddenTypes: SAVED_OBJECT_TYPES, }); let comments: SavedObjectsFindResponse; @@ -54,7 +54,7 @@ export function initGetAllCommentsApi({ caseService, router, logger }: RouteDeps if (request.query?.subCaseId) { comments = await caseService.getAllSubCaseComments({ - client, + soClient, id: request.query.subCaseId, options: { sortField: defaultSortField, @@ -62,7 +62,7 @@ export function initGetAllCommentsApi({ caseService, router, logger }: RouteDeps }); } else { comments = await caseService.getAllCaseComments({ - client, + soClient, id: request.params.case_id, includeSubCaseComments: request.query?.includeSubCaseComments, options: { diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/get_comment.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/get_comment.ts index f188a67417f6d1..a03ed4a66e805c 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/get_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/get_comment.ts @@ -12,7 +12,7 @@ import { RouteDeps } from '../../types'; import { flattenCommentSavedObject, wrapError } from '../../utils'; import { CASE_COMMENT_DETAILS_URL, SAVED_OBJECT_TYPES } from '../../../../../common/constants'; -export function initGetCommentApi({ caseService, router, logger }: RouteDeps) { +export function initGetCommentApi({ attachmentService, router, logger }: RouteDeps) { router.get( { path: CASE_COMMENT_DETAILS_URL, @@ -25,13 +25,13 @@ export function initGetCommentApi({ caseService, router, logger }: RouteDeps) { }, async (context, request, response) => { try { - const client = context.core.savedObjects.getClient({ + const soClient = context.core.savedObjects.getClient({ includedHiddenTypes: SAVED_OBJECT_TYPES, }); - const comment = await caseService.getComment({ - client, - commentId: request.params.comment_id, + const comment = await attachmentService.get({ + soClient, + attachmentId: request.params.comment_id, }); return response.ok({ body: CommentResponseRt.encode(flattenCommentSavedObject(comment)), diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/patch_comment.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/patch_comment.ts index 06c28513c2d6c1..b9755cae411332 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/patch_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/patch_comment.ts @@ -25,51 +25,66 @@ import { ENABLE_CASE_CONNECTOR, } from '../../../../../common/constants'; import { CommentPatchRequestRt, throwErrors, User } from '../../../../../common/api'; -import { CaseServiceSetup } from '../../../../services'; +import { CaseService, AttachmentService } from '../../../../services'; interface CombinedCaseParams { - service: CaseServiceSetup; - client: SavedObjectsClientContract; + attachmentService: AttachmentService; + caseService: CaseService; + soClient: SavedObjectsClientContract; caseID: string; logger: Logger; subCaseId?: string; } async function getCommentableCase({ - service, - client, + attachmentService, + caseService, + soClient, caseID, subCaseId, logger, }: CombinedCaseParams) { if (subCaseId) { const [caseInfo, subCase] = await Promise.all([ - service.getCase({ - client, + caseService.getCase({ + soClient, id: caseID, }), - service.getSubCase({ - client, + caseService.getSubCase({ + soClient, id: subCaseId, }), ]); return new CommentableCase({ + attachmentService, + caseService, collection: caseInfo, - service, subCase, - soClient: client, + soClient, logger, }); } else { - const caseInfo = await service.getCase({ - client, + const caseInfo = await caseService.getCase({ + soClient, id: caseID, }); - return new CommentableCase({ collection: caseInfo, service, soClient: client, logger }); + return new CommentableCase({ + attachmentService, + caseService, + collection: caseInfo, + soClient, + logger, + }); } } -export function initPatchCommentApi({ caseService, router, userActionService, logger }: RouteDeps) { +export function initPatchCommentApi({ + attachmentService, + caseService, + router, + userActionService, + logger, +}: RouteDeps) { router.patch( { path: CASE_COMMENTS_URL, @@ -93,7 +108,7 @@ export function initPatchCommentApi({ caseService, router, userActionService, lo ); } - const client = context.core.savedObjects.getClient({ + const soClient = context.core.savedObjects.getClient({ includedHiddenTypes: SAVED_OBJECT_TYPES, }); const query = pipe( @@ -105,16 +120,17 @@ export function initPatchCommentApi({ caseService, router, userActionService, lo decodeCommentRequest(queryRestAttributes); const commentableCase = await getCommentableCase({ - service: caseService, - client, + attachmentService, + caseService, + soClient, caseID: request.params.case_id, subCaseId: request.query?.subCaseId, logger, }); - const myComment = await caseService.getComment({ - client, - commentId: queryCommentId, + const myComment = await attachmentService.get({ + soClient, + attachmentId: queryCommentId, }); if (myComment == null) { @@ -158,8 +174,8 @@ export function initPatchCommentApi({ caseService, router, userActionService, lo user: userInfo, }); - await userActionService.postUserActions({ - client, + await userActionService.bulkCreate({ + soClient, actions: [ buildCommentUserActionItem({ action: 'update', diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/post_comment.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/post_comment.ts index e3b42943ebc2aa..7dbfb2a62c46fa 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/post_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/post_comment.ts @@ -45,7 +45,7 @@ export function initPostCommentApi({ router, logger }: RouteDeps) { const comment = request.body as CommentRequest; return response.ok({ - body: await casesClient.addComment({ caseId, comment }), + body: await casesClient.attachments.add({ caseId, comment }), }); } catch (error) { logger.error( diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.ts b/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.ts index 663595b60b8bac..fa97796228bd1b 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.ts @@ -21,11 +21,11 @@ export function initGetCaseConfigure({ caseConfigureService, router, logger }: R async (context, request, response) => { try { let error = null; - const client = context.core.savedObjects.getClient({ + const soClient = context.core.savedObjects.getClient({ includedHiddenTypes: SAVED_OBJECT_TYPES, }); - const myCaseConfigure = await caseConfigureService.find({ client }); + const myCaseConfigure = await caseConfigureService.find({ soClient }); const { connector, ...caseConfigureWithoutConnector } = myCaseConfigure.saved_objects[0] ?.attributes ?? { connector: null }; @@ -40,7 +40,7 @@ export function initGetCaseConfigure({ caseConfigureService, router, logger }: R throw Boom.notFound('Action client not found'); } try { - mappings = await casesClient.getMappings({ + mappings = await casesClient.casesClientInternal.configuration.getMappings({ actionsClient, connectorId: connector.id, connectorType: connector.type, diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.ts b/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.ts index ed3c2e98d25798..61f3e4719520a4 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.ts @@ -40,7 +40,7 @@ export function initPatchCaseConfigure({ async (context, request, response) => { try { let error = null; - const client = context.core.savedObjects.getClient({ + const soClient = context.core.savedObjects.getClient({ includedHiddenTypes: SAVED_OBJECT_TYPES, }); const query = pipe( @@ -48,7 +48,7 @@ export function initPatchCaseConfigure({ fold(throwErrors(Boom.badRequest), identity) ); - const myCaseConfigure = await caseConfigureService.find({ client }); + const myCaseConfigure = await caseConfigureService.find({ soClient }); const { version, connector, ...queryWithoutVersion } = query; if (myCaseConfigure.saved_objects.length === 0) { throw Boom.conflict( @@ -78,7 +78,7 @@ export function initPatchCaseConfigure({ throw Boom.notFound('Action client have not been found'); } try { - mappings = await casesClient.getMappings({ + mappings = await casesClient.casesClientInternal.configuration.getMappings({ actionsClient, connectorId: connector.id, connectorType: connector.type, @@ -90,7 +90,7 @@ export function initPatchCaseConfigure({ } } const patch = await caseConfigureService.patch({ - client, + soClient, caseConfigureId: myCaseConfigure.saved_objects[0].id, updatedAttributes: { ...queryWithoutVersion, diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.ts b/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.ts index d8e6b2a8ecf75d..62fa7cad324fce 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.ts @@ -43,24 +43,28 @@ export function initPostCaseConfigure({ if (!context.cases) { throw Boom.badRequest('RouteHandlerContext is not registered for cases'); } + const casesClient = await context.cases.getCasesClient(); const actionsClient = context.actions?.getActionsClient(); + if (actionsClient == null) { throw Boom.notFound('Action client not found'); } - const client = context.core.savedObjects.getClient({ + + const soClient = context.core.savedObjects.getClient({ includedHiddenTypes: SAVED_OBJECT_TYPES, }); + const query = pipe( CasesConfigureRequestRt.decode(request.body), fold(throwErrors(Boom.badRequest), identity) ); - const myCaseConfigure = await caseConfigureService.find({ client }); + const myCaseConfigure = await caseConfigureService.find({ soClient }); if (myCaseConfigure.saved_objects.length > 0) { await Promise.all( myCaseConfigure.saved_objects.map((cc) => - caseConfigureService.delete({ client, caseConfigureId: cc.id }) + caseConfigureService.delete({ soClient, caseConfigureId: cc.id }) ) ); } @@ -70,7 +74,7 @@ export function initPostCaseConfigure({ const creationDate = new Date().toISOString(); let mappings: ConnectorMappingsAttributes[] = []; try { - mappings = await casesClient.getMappings({ + mappings = await casesClient.casesClientInternal.configuration.getMappings({ actionsClient, connectorId: query.connector.id, connectorType: query.connector.type, @@ -81,7 +85,7 @@ export function initPostCaseConfigure({ : `Error connecting to ${query.connector.name} instance`; } const post = await caseConfigureService.post({ - client, + soClient, attributes: { ...query, connector: transformCaseConnectorToEsConnector(query.connector), diff --git a/x-pack/plugins/cases/server/routes/api/cases/delete_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/delete_cases.ts index 645e6333300265..a9be4a314adeb5 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/delete_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/delete_cases.ts @@ -12,22 +12,24 @@ import { buildCaseUserActionItem } from '../../../services/user_actions/helpers' import { RouteDeps } from '../types'; import { wrapError } from '../utils'; import { CASES_URL, SAVED_OBJECT_TYPES, ENABLE_CASE_CONNECTOR } from '../../../../common/constants'; -import { CaseServiceSetup } from '../../../services'; +import { CaseService, AttachmentService } from '../../../services'; async function deleteSubCases({ + attachmentService, caseService, - client, + soClient, caseIds, }: { - caseService: CaseServiceSetup; - client: SavedObjectsClientContract; + attachmentService: AttachmentService; + caseService: CaseService; + soClient: SavedObjectsClientContract; caseIds: string[]; }) { - const subCasesForCaseIds = await caseService.findSubCasesByCaseId({ client, ids: caseIds }); + const subCasesForCaseIds = await caseService.findSubCasesByCaseId({ soClient, ids: caseIds }); const subCaseIDs = subCasesForCaseIds.saved_objects.map((subCase) => subCase.id); const commentsForSubCases = await caseService.getAllSubCaseComments({ - client, + soClient, id: subCaseIDs, }); @@ -35,18 +37,24 @@ async function deleteSubCases({ // per case ID await Promise.all( commentsForSubCases.saved_objects.map((commentSO) => - caseService.deleteComment({ client, commentId: commentSO.id }) + attachmentService.delete({ soClient, attachmentId: commentSO.id }) ) ); await Promise.all( subCasesForCaseIds.saved_objects.map((subCaseSO) => - caseService.deleteSubCase(client, subCaseSO.id) + caseService.deleteSubCase(soClient, subCaseSO.id) ) ); } -export function initDeleteCasesApi({ caseService, router, userActionService, logger }: RouteDeps) { +export function initDeleteCasesApi({ + attachmentService, + caseService, + router, + userActionService, + logger, +}: RouteDeps) { router.delete( { path: CASES_URL, @@ -58,13 +66,13 @@ export function initDeleteCasesApi({ caseService, router, userActionService, log }, async (context, request, response) => { try { - const client = context.core.savedObjects.getClient({ + const soClient = context.core.savedObjects.getClient({ includedHiddenTypes: SAVED_OBJECT_TYPES, }); await Promise.all( request.query.ids.map((id) => caseService.deleteCase({ - client, + soClient, id, }) ) @@ -72,7 +80,7 @@ export function initDeleteCasesApi({ caseService, router, userActionService, log const comments = await Promise.all( request.query.ids.map((id) => caseService.getAllCaseComments({ - client, + soClient, id, }) ) @@ -83,9 +91,9 @@ export function initDeleteCasesApi({ caseService, router, userActionService, log comments.map((c) => Promise.all( c.saved_objects.map(({ id }) => - caseService.deleteComment({ - client, - commentId: id, + attachmentService.delete({ + soClient, + attachmentId: id, }) ) ) @@ -94,15 +102,20 @@ export function initDeleteCasesApi({ caseService, router, userActionService, log } if (ENABLE_CASE_CONNECTOR) { - await deleteSubCases({ caseService, client, caseIds: request.query.ids }); + await deleteSubCases({ + attachmentService, + caseService, + soClient, + caseIds: request.query.ids, + }); } // eslint-disable-next-line @typescript-eslint/naming-convention const { username, full_name, email } = await caseService.getUser({ request }); const deleteDate = new Date().toISOString(); - await userActionService.postUserActions({ - client, + await userActionService.bulkCreate({ + soClient, actions: request.query.ids.map((id) => buildCaseUserActionItem({ action: 'create', diff --git a/x-pack/plugins/cases/server/routes/api/cases/find_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/find_cases.ts index 7bee574894d39a..c6ec5245ebd8ae 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/find_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/find_cases.ts @@ -27,7 +27,7 @@ export function initFindCasesApi({ caseService, router, logger }: RouteDeps) { const options = request.query as CasesFindRequest; return response.ok({ - body: await casesClient.find({ ...options }), + body: await casesClient.cases.find({ ...options }), }); } catch (error) { logger.error(`Failed to find cases in route: ${error}`); diff --git a/x-pack/plugins/cases/server/routes/api/cases/get_case.ts b/x-pack/plugins/cases/server/routes/api/cases/get_case.ts index 1f39762d5512bd..e48806567e5745 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/get_case.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/get_case.ts @@ -37,7 +37,7 @@ export function initGetCaseApi({ router, logger }: RouteDeps) { const id = request.params.case_id; return response.ok({ - body: await casesClient.get({ + body: await casesClient.cases.get({ id, includeComments: request.query.includeComments, includeSubCaseComments: request.query.includeSubCaseComments, diff --git a/x-pack/plugins/cases/server/routes/api/cases/helpers.ts b/x-pack/plugins/cases/server/routes/api/cases/helpers.ts index 697b4d5df7ad1d..f6570bb5c88cd8 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/helpers.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/helpers.ts @@ -9,8 +9,7 @@ import { get, isPlainObject } from 'lodash'; import deepEqual from 'fast-deep-equal'; import { SavedObjectsFindResponse } from 'kibana/server'; -import { nodeBuilder } from '../../../../../../../src/plugins/data/common'; -import { KueryNode } from '../../../../../../../src/plugins/data/server'; +import { nodeBuilder, KueryNode } from '../../../../../../../src/plugins/data/common'; import { CaseConnector, ESCaseConnector, diff --git a/x-pack/plugins/cases/server/routes/api/cases/patch_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/patch_cases.ts index 5c417a3d98b938..244ab1a8f16aee 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/patch_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/patch_cases.ts @@ -28,7 +28,7 @@ export function initPatchCasesApi({ router, logger }: RouteDeps) { const cases = request.body as CasesPatchRequest; return response.ok({ - body: await casesClient.update(cases), + body: await casesClient.cases.update(cases), }); } catch (error) { logger.error(`Failed to patch cases in route: ${error}`); diff --git a/x-pack/plugins/cases/server/routes/api/cases/post_case.ts b/x-pack/plugins/cases/server/routes/api/cases/post_case.ts index d5f38c76fae3f4..391310cb810100 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/post_case.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/post_case.ts @@ -28,7 +28,7 @@ export function initPostCaseApi({ router, logger }: RouteDeps) { const theCase = request.body as CasePostRequest; return response.ok({ - body: await casesClient.create({ ...theCase }), + body: await casesClient.cases.create({ ...theCase }), }); } catch (error) { logger.error(`Failed to post case in route: ${error}`); diff --git a/x-pack/plugins/cases/server/routes/api/cases/push_case.ts b/x-pack/plugins/cases/server/routes/api/cases/push_case.ts index 02423943c05572..9818c97d883c45 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/push_case.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/push_case.ts @@ -44,7 +44,7 @@ export function initPushCaseApi({ router, logger }: RouteDeps) { ); return response.ok({ - body: await casesClient.push({ + body: await casesClient.cases.push({ actionsClient, caseId: params.case_id, connectorId: params.connector_id, 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 bbb21da1b71f40..1ce60442ee9c9e 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 @@ -18,11 +18,11 @@ export function initGetReportersApi({ caseService, router, logger }: RouteDeps) }, async (context, request, response) => { try { - const client = context.core.savedObjects.getClient({ + const soClient = context.core.savedObjects.getClient({ includedHiddenTypes: SAVED_OBJECT_TYPES, }); const reporters = await caseService.getReporters({ - client, + soClient, }); return response.ok({ body: UsersRt.encode(reporters) }); } catch (error) { diff --git a/x-pack/plugins/cases/server/routes/api/cases/status/get_status.ts b/x-pack/plugins/cases/server/routes/api/cases/status/get_status.ts index 27f5e0e0177376..ddfa5e39c01b00 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/status/get_status.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/status/get_status.ts @@ -20,7 +20,7 @@ export function initGetCasesStatusApi({ caseService, router, logger }: RouteDeps }, async (context, request, response) => { try { - const client = context.core.savedObjects.getClient({ + const soClient = context.core.savedObjects.getClient({ includedHiddenTypes: SAVED_OBJECT_TYPES, }); @@ -28,7 +28,7 @@ export function initGetCasesStatusApi({ caseService, router, logger }: RouteDeps ...caseStatuses.map((status) => { const statusQuery = constructQueryOptions({ status }); return caseService.findCaseStatusStats({ - client, + soClient, caseOptions: statusQuery.case, subCaseOptions: statusQuery.subCase, }); 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/cases/sub_case/delete_sub_cases.ts index 77e94f9eb7e8f0..15eb5a421358b6 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/sub_case/delete_sub_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/sub_case/delete_sub_cases.ts @@ -17,6 +17,7 @@ import { } from '../../../../../common/constants'; export function initDeleteSubCasesApi({ + attachmentService, caseService, router, userActionService, @@ -33,13 +34,13 @@ export function initDeleteSubCasesApi({ }, async (context, request, response) => { try { - const client = context.core.savedObjects.getClient({ + const soClient = context.core.savedObjects.getClient({ includedHiddenTypes: SAVED_OBJECT_TYPES, }); const [comments, subCases] = await Promise.all([ - caseService.getAllSubCaseComments({ client, id: request.query.ids }), - caseService.getSubCases({ client, ids: request.query.ids }), + caseService.getAllSubCaseComments({ soClient, id: request.query.ids }), + caseService.getSubCases({ soClient, ids: request.query.ids }), ]); const subCaseErrors = subCases.saved_objects.filter( @@ -62,18 +63,18 @@ export function initDeleteSubCasesApi({ await Promise.all( comments.saved_objects.map((comment) => - caseService.deleteComment({ client, commentId: comment.id }) + attachmentService.delete({ soClient, attachmentId: comment.id }) ) ); - await Promise.all(request.query.ids.map((id) => caseService.deleteSubCase(client, id))); + await Promise.all(request.query.ids.map((id) => caseService.deleteSubCase(soClient, id))); // eslint-disable-next-line @typescript-eslint/naming-convention const { username, full_name, email } = await caseService.getUser({ request }); const deleteDate = new Date().toISOString(); - await userActionService.postUserActions({ - client, + await userActionService.bulkCreate({ + soClient, actions: request.query.ids.map((id) => buildCaseUserActionItem({ action: '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/cases/sub_case/find_sub_cases.ts index fd1e84e8a012c1..f9d077cbe3b122 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/sub_case/find_sub_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/sub_case/find_sub_cases.ts @@ -37,7 +37,7 @@ export function initFindSubCasesApi({ caseService, router, logger }: RouteDeps) }, async (context, request, response) => { try { - const client = context.core.savedObjects.getClient({ + const soClient = context.core.savedObjects.getClient({ includedHiddenTypes: SAVED_OBJECT_TYPES, }); const queryParams = pipe( @@ -52,7 +52,7 @@ export function initFindSubCasesApi({ caseService, router, logger }: RouteDeps) }); const subCases = await caseService.findSubCasesGroupByCase({ - client, + soClient, ids, options: { sortField: 'created_at', @@ -70,7 +70,7 @@ export function initFindSubCasesApi({ caseService, router, logger }: RouteDeps) sortByField: queryParams.sortField, }); return caseService.findSubCaseStatusStats({ - client, + soClient, options: statusQueryOptions ?? {}, ids, }); 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/cases/sub_case/get_sub_case.ts index 093165a7281842..afeaef639326d0 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/sub_case/get_sub_case.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/sub_case/get_sub_case.ts @@ -29,13 +29,13 @@ export function initGetSubCaseApi({ caseService, router, logger }: RouteDeps) { }, async (context, request, response) => { try { - const client = context.core.savedObjects.getClient({ + const soClient = context.core.savedObjects.getClient({ includedHiddenTypes: SAVED_OBJECT_TYPES, }); const includeComments = request.query.includeComments; const subCase = await caseService.getSubCase({ - client, + soClient, id: request.params.sub_case_id, }); @@ -50,7 +50,7 @@ export function initGetSubCaseApi({ caseService, router, logger }: RouteDeps) { } const theComments = await caseService.getAllSubCaseComments({ - client, + soClient, id: request.params.sub_case_id, options: { sortField: 'created_at', 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/cases/sub_case/patch_sub_cases.ts index 5b623815f027fa..4a407fc261a9b6 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/sub_case/patch_sub_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/sub_case/patch_sub_cases.ts @@ -19,7 +19,7 @@ import { import { nodeBuilder } from '../../../../../../../../src/plugins/data/common'; import { CasesClient } from '../../../../client'; -import { CaseServiceSetup, CaseUserActionServiceSetup } from '../../../../services'; +import { CaseService, CaseUserActionService } from '../../../../services'; import { CaseStatuses, SubCasesPatchRequest, @@ -51,13 +51,13 @@ import { import { getCaseToUpdate } from '../helpers'; import { buildSubCaseUserActions } from '../../../../services/user_actions/helpers'; import { createAlertUpdateRequest } from '../../../../common'; -import { UpdateAlertRequest } from '../../../../client/types'; import { createCaseError } from '../../../../common/error'; +import { UpdateAlertRequest } from '../../../../client/alerts/client'; interface UpdateArgs { - client: SavedObjectsClientContract; - caseService: CaseServiceSetup; - userActionService: CaseUserActionServiceSetup; + soClient: SavedObjectsClientContract; + caseService: CaseService; + userActionService: CaseUserActionService; request: KibanaRequest; casesClient: CasesClient; subCases: SubCasesPatchRequest; @@ -132,19 +132,19 @@ function getParentIDs({ async function getParentCases({ caseService, - client, + soClient, subCaseIDs, subCasesMap, }: { - caseService: CaseServiceSetup; - client: SavedObjectsClientContract; + caseService: CaseService; + soClient: SavedObjectsClientContract; subCaseIDs: string[]; subCasesMap: Map>; }): Promise>> { const parentIDInfo = getParentIDs({ subCaseIDs, subCasesMap }); const parentCases = await caseService.getCases({ - client, + soClient, caseIds: parentIDInfo.ids, }); @@ -199,15 +199,15 @@ function getID(comment: SavedObject): string | undefined { async function getAlertComments({ subCasesToSync, caseService, - client, + soClient, }: { subCasesToSync: SubCasePatchRequest[]; - caseService: CaseServiceSetup; - client: SavedObjectsClientContract; + caseService: CaseService; + soClient: SavedObjectsClientContract; }): Promise> { const ids = subCasesToSync.map((subCase) => subCase.id); return caseService.getAllSubCaseComments({ - client, + soClient, id: ids, options: { filter: nodeBuilder.or([ @@ -222,17 +222,17 @@ async function getAlertComments({ * Updates the status of alerts for the specified sub cases. */ async function updateAlerts({ - subCasesToSync, caseService, - client, + soClient, casesClient, logger, + subCasesToSync, }: { - subCasesToSync: SubCasePatchRequest[]; - caseService: CaseServiceSetup; - client: SavedObjectsClientContract; + caseService: CaseService; + soClient: SavedObjectsClientContract; casesClient: CasesClient; logger: Logger; + subCasesToSync: SubCasePatchRequest[]; }) { try { const subCasesToSyncMap = subCasesToSync.reduce((acc, subCase) => { @@ -240,7 +240,7 @@ async function updateAlerts({ return acc; }, new Map()); // get all the alerts for all sub cases that need to be synced - const totalAlerts = await getAlertComments({ caseService, client, subCasesToSync }); + const totalAlerts = await getAlertComments({ caseService, soClient, subCasesToSync }); // create a map of the status (open, closed, etc) to alert info that needs to be updated const alertsToUpdate = totalAlerts.saved_objects.reduce( (acc: UpdateAlertRequest[], alertComment) => { @@ -258,7 +258,7 @@ async function updateAlerts({ [] ); - await casesClient.updateAlertsStatus({ alerts: alertsToUpdate }); + await casesClient.casesClientInternal.alerts.updateStatus({ alerts: alertsToUpdate }); } catch (error) { throw createCaseError({ message: `Failed to update alert status while updating sub cases: ${JSON.stringify( @@ -271,7 +271,7 @@ async function updateAlerts({ } async function update({ - client, + soClient, caseService, userActionService, request, @@ -286,7 +286,7 @@ async function update({ try { const bulkSubCases = await caseService.getSubCases({ - client, + soClient, ids: query.subCases.map((q) => q.id), }); @@ -304,7 +304,7 @@ async function update({ } const subIDToParentCase = await getParentCases({ - client, + soClient, caseService, subCaseIDs: nonEmptySubCaseRequests.map((subCase) => subCase.id), subCasesMap, @@ -314,7 +314,7 @@ async function update({ const { username, full_name, email } = await caseService.getUser({ request }); const updatedAt = new Date().toISOString(); const updatedCases = await caseService.patchSubCases({ - client, + soClient, subCases: nonEmptySubCaseRequests.map((thisCase) => { const { id: subCaseId, version, ...updateSubCaseAttributes } = thisCase; let closedInfo: { closed_at: string | null; closed_by: User | null } = { @@ -366,7 +366,7 @@ async function update({ await updateAlerts({ caseService, - client, + soClient, casesClient, subCasesToSync: subCasesToSyncAlertsFor, logger, @@ -393,8 +393,8 @@ async function update({ [] ); - await userActionService.postUserActions({ - client, + await userActionService.bulkCreate({ + soClient, actions: buildSubCaseUserActions({ originalSubCases: bulkSubCases.saved_objects, updatedSubCases: updatedCases.saved_objects, @@ -440,7 +440,7 @@ export function initPatchSubCasesApi({ request, subCases, casesClient, - client: context.core.savedObjects.client, + soClient: context.core.savedObjects.client, caseService, userActionService, logger, 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 18231edd16353b..10c15d2518f349 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 @@ -17,11 +17,11 @@ export function initGetTagsApi({ caseService, router }: RouteDeps) { }, async (context, request, response) => { try { - const client = context.core.savedObjects.getClient({ + const soClient = context.core.savedObjects.getClient({ includedHiddenTypes: SAVED_OBJECT_TYPES, }); const tags = await caseService.getTags({ - client, + soClient, }); return response.ok({ body: tags }); } catch (error) { 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/cases/user_actions/get_all_user_actions.ts index ce0b4636130d73..07f1353f19854b 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/cases/user_actions/get_all_user_actions.ts @@ -31,7 +31,7 @@ export function initGetAllCaseUserActionsApi({ router, logger }: RouteDeps) { const caseId = request.params.case_id; return response.ok({ - body: await casesClient.getUserActions({ caseId }), + body: await casesClient.userActions.getAll({ caseId }), }); } catch (error) { logger.error( @@ -65,7 +65,7 @@ export function initGetAllSubCaseUserActionsApi({ router, logger }: RouteDeps) { const subCaseId = request.params.sub_case_id; return response.ok({ - body: await casesClient.getUserActions({ caseId, subCaseId }), + body: await casesClient.userActions.getAll({ caseId, subCaseId }), }); } catch (error) { logger.error( diff --git a/x-pack/plugins/cases/server/routes/api/types.ts b/x-pack/plugins/cases/server/routes/api/types.ts index 6ce40e01c77520..76fad3fcc33bc6 100644 --- a/x-pack/plugins/cases/server/routes/api/types.ts +++ b/x-pack/plugins/cases/server/routes/api/types.ts @@ -8,20 +8,22 @@ import type { Logger } from 'kibana/server'; import type { - CaseConfigureServiceSetup, - CaseServiceSetup, - CaseUserActionServiceSetup, - ConnectorMappingsServiceSetup, + CaseConfigureService, + CaseService, + CaseUserActionService, + ConnectorMappingsService, + AttachmentService, } from '../../services'; import type { CasesRouter } from '../../types'; export interface RouteDeps { - caseConfigureService: CaseConfigureServiceSetup; - caseService: CaseServiceSetup; - connectorMappingsService: ConnectorMappingsServiceSetup; + caseConfigureService: CaseConfigureService; + caseService: CaseService; + connectorMappingsService: ConnectorMappingsService; router: CasesRouter; - userActionService: CaseUserActionServiceSetup; + userActionService: CaseUserActionService; + attachmentService: AttachmentService; logger: Logger; } diff --git a/x-pack/plugins/cases/server/services/alerts/index.ts b/x-pack/plugins/cases/server/services/alerts/index.ts index db8e841f45ee4a..e7b331138d73ca 100644 --- a/x-pack/plugins/cases/server/services/alerts/index.ts +++ b/x-pack/plugins/cases/server/services/alerts/index.ts @@ -11,9 +11,9 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import { ElasticsearchClient, Logger } from 'kibana/server'; import { MAX_ALERTS_PER_SUB_CASE } from '../../../common/constants'; -import { UpdateAlertRequest } from '../../client/types'; import { AlertInfo } from '../../common'; import { createCaseError } from '../../common/error'; +import { UpdateAlertRequest } from '../../client/alerts/client'; export type AlertServiceContract = PublicMethodsOf; diff --git a/x-pack/plugins/cases/server/services/attachments/index.ts b/x-pack/plugins/cases/server/services/attachments/index.ts new file mode 100644 index 00000000000000..fdfa722d18defb --- /dev/null +++ b/x-pack/plugins/cases/server/services/attachments/index.ts @@ -0,0 +1,116 @@ +/* + * 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 { Logger, SavedObject, SavedObjectReference } from 'kibana/server'; + +import { + CommentAttributes as AttachmentAttributes, + CommentPatchAttributes as AttachmentPatchAttributes, +} from '../../../common/api'; +import { CASE_COMMENT_SAVED_OBJECT } from '../../../common/constants'; +import { ClientArgs } from '..'; + +interface GetAttachmentArgs extends ClientArgs { + attachmentId: string; +} + +interface CreateAttachmentArgs extends ClientArgs { + attributes: AttachmentAttributes; + references: SavedObjectReference[]; +} + +interface UpdateArgs { + attachmentId: string; + updatedAttributes: AttachmentPatchAttributes; + version?: string; +} + +type UpdateAttachmentArgs = UpdateArgs & ClientArgs; + +interface BulkUpdateAttachmentArgs extends ClientArgs { + comments: UpdateArgs[]; +} + +export class AttachmentService { + constructor(private readonly log: Logger) {} + + public async get({ + soClient, + attachmentId, + }: GetAttachmentArgs): Promise> { + try { + this.log.debug(`Attempting to GET attachment ${attachmentId}`); + return await soClient.get(CASE_COMMENT_SAVED_OBJECT, attachmentId); + } catch (error) { + this.log.error(`Error on GET attachment ${attachmentId}: ${error}`); + throw error; + } + } + + public async delete({ soClient, attachmentId }: GetAttachmentArgs) { + try { + this.log.debug(`Attempting to GET attachment ${attachmentId}`); + return await soClient.delete(CASE_COMMENT_SAVED_OBJECT, attachmentId); + } catch (error) { + this.log.error(`Error on GET attachment ${attachmentId}: ${error}`); + throw error; + } + } + + public async create({ soClient, attributes, references }: CreateAttachmentArgs) { + try { + this.log.debug(`Attempting to POST a new comment`); + return await soClient.create(CASE_COMMENT_SAVED_OBJECT, attributes, { + references, + }); + } catch (error) { + this.log.error(`Error on POST a new comment: ${error}`); + throw error; + } + } + + public async update({ + soClient, + attachmentId, + updatedAttributes, + version, + }: UpdateAttachmentArgs) { + try { + this.log.debug(`Attempting to UPDATE comment ${attachmentId}`); + return await soClient.update( + CASE_COMMENT_SAVED_OBJECT, + attachmentId, + updatedAttributes, + { version } + ); + } catch (error) { + this.log.error(`Error on UPDATE comment ${attachmentId}: ${error}`); + throw error; + } + } + + public async bulkUpdate({ soClient, comments }: BulkUpdateAttachmentArgs) { + try { + this.log.debug( + `Attempting to UPDATE comments ${comments.map((c) => c.attachmentId).join(', ')}` + ); + return await soClient.bulkUpdate( + comments.map((c) => ({ + type: CASE_COMMENT_SAVED_OBJECT, + id: c.attachmentId, + attributes: c.updatedAttributes, + version: c.version, + })) + ); + } catch (error) { + this.log.error( + `Error on UPDATE comments ${comments.map((c) => c.attachmentId).join(', ')}: ${error}` + ); + throw error; + } + } +} diff --git a/x-pack/plugins/cases/server/services/cases/index.ts b/x-pack/plugins/cases/server/services/cases/index.ts new file mode 100644 index 00000000000000..bbb82214d70a54 --- /dev/null +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -0,0 +1,1015 @@ +/* + * 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 { cloneDeep } from 'lodash'; +import { + KibanaRequest, + Logger, + SavedObject, + SavedObjectsClientContract, + SavedObjectsFindResponse, + SavedObjectsBulkResponse, + SavedObjectsFindResult, +} from 'kibana/server'; + +import { nodeBuilder, KueryNode } from '../../../../../../src/plugins/data/common'; + +import { SecurityPluginSetup } from '../../../../security/server'; +import { + ESCaseAttributes, + CommentAttributes, + User, + SubCaseAttributes, + AssociationType, + SubCaseResponse, + CommentType, + CaseType, + CaseResponse, + caseTypeField, + CasesFindRequest, +} from '../../../common/api'; +import { + defaultSortField, + groupTotalAlertsByID, + SavedObjectFindOptionsKueryNode, +} from '../../common'; +import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; +import { defaultPage, defaultPerPage } from '../../routes/api'; +import { + flattenCaseSavedObject, + flattenSubCaseSavedObject, + transformNewSubCase, +} from '../../routes/api/utils'; +import { + CASE_SAVED_OBJECT, + 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 { + pushed_at: string; + pushed_by: User; +} + +interface GetCaseArgs extends ClientArgs { + id: string; +} + +interface GetCasesArgs extends ClientArgs { + caseIds: string[]; +} + +interface GetSubCasesArgs extends ClientArgs { + ids: string[]; +} + +interface FindCommentsArgs { + soClient: SavedObjectsClientContract; + id: string | string[]; + options?: SavedObjectFindOptionsKueryNode; +} + +interface FindCaseCommentsArgs { + soClient: SavedObjectsClientContract; + id: string | string[]; + options?: SavedObjectFindOptionsKueryNode; + includeSubCaseComments?: boolean; +} + +interface FindSubCaseCommentsArgs { + soClient: SavedObjectsClientContract; + id: string | string[]; + options?: SavedObjectFindOptionsKueryNode; +} + +interface FindCasesArgs extends ClientArgs { + options?: SavedObjectFindOptionsKueryNode; +} + +interface FindSubCasesByIDArgs extends FindCasesArgs { + ids: string[]; +} + +interface FindSubCasesStatusStats { + soClient: SavedObjectsClientContract; + options: SavedObjectFindOptionsKueryNode; + ids: string[]; +} + +interface PostCaseArgs extends ClientArgs { + attributes: ESCaseAttributes; +} + +interface CreateSubCaseArgs extends ClientArgs { + createdAt: string; + caseId: string; + createdBy: User; +} + +interface PatchCase { + caseId: string; + updatedAttributes: Partial; + version?: string; +} +type PatchCaseArgs = PatchCase & ClientArgs; + +interface PatchCasesArgs extends ClientArgs { + cases: PatchCase[]; +} + +interface PatchSubCase { + soClient: SavedObjectsClientContract; + subCaseId: string; + updatedAttributes: Partial; + version?: string; +} + +interface PatchSubCases { + soClient: SavedObjectsClientContract; + subCases: Array>; +} + +interface GetUserArgs { + request: KibanaRequest; +} + +interface SubCasesMapWithPageInfo { + subCasesMap: Map; + page: number; + perPage: number; + total: number; +} + +interface CaseCommentStats { + commentTotals: Map; + alertTotals: Map; +} + +interface FindCommentsByAssociationArgs { + soClient: SavedObjectsClientContract; + id: string | string[]; + associationType: AssociationType; + options?: SavedObjectFindOptionsKueryNode; +} + +interface Collection { + case: SavedObjectsFindResult; + subCases?: SubCaseResponse[]; +} + +interface CasesMapWithPageInfo { + casesMap: Map; + page: number; + perPage: number; + total: number; +} + +type FindCaseOptions = CasesFindRequest & SavedObjectFindOptionsKueryNode; + +export class CaseService { + constructor( + private readonly log: Logger, + private readonly authentication?: SecurityPluginSetup['authc'] + ) {} + + /** + * Returns a map of all cases combined with their sub cases if they are collections. + */ + public async findCasesGroupedByID({ + soClient, + caseOptions, + subCaseOptions, + }: { + soClient: SavedObjectsClientContract; + caseOptions: FindCaseOptions; + subCaseOptions?: SavedObjectFindOptionsKueryNode; + }): Promise { + const cases = await this.findCases({ + soClient, + options: caseOptions, + }); + + const subCasesResp = ENABLE_CASE_CONNECTOR + ? await this.findSubCasesGroupByCase({ + soClient, + options: subCaseOptions, + ids: cases.saved_objects + .filter((caseInfo) => caseInfo.attributes.type === CaseType.collection) + .map((caseInfo) => caseInfo.id), + }) + : { subCasesMap: new Map(), page: 0, perPage: 0 }; + + const casesMap = cases.saved_objects.reduce((accMap, caseInfo) => { + const subCasesForCase = subCasesResp.subCasesMap.get(caseInfo.id); + + /** + * If this case is an individual add it to the return map + * If it is a collection and it has sub cases add it to the return map + * If it is a collection and it does not have sub cases, check and see if we're filtering on a status, + * if we're filtering on a status then exclude the empty collection from the results + * if we're not filtering on a status then include the empty collection (that way we can display all the collections + * when the UI isn't doing any filtering) + */ + if ( + caseInfo.attributes.type === CaseType.individual || + subCasesForCase !== undefined || + !caseOptions.status + ) { + accMap.set(caseInfo.id, { case: caseInfo, subCases: subCasesForCase }); + } + return accMap; + }, new Map()); + + /** + * One potential optimization here is to get all comment stats for individual cases, parent cases, and sub cases + * in a single request. This can be done because comments that are for sub cases have a reference to both the sub case + * and the parent. The associationType field allows us to determine which type of case the comment is attached to. + * + * So we could use the ids for all the valid cases (individual cases and parents with sub cases) to grab everything. + * Once we have it we can build the maps. + * + * Currently we get all comment stats for all sub cases in one go and we get all comment stats for cases (individual and parent) + * in another request (the one below this comment). + */ + const totalCommentsForCases = await this.getCaseCommentStats({ + soClient, + ids: Array.from(casesMap.keys()), + associationType: AssociationType.case, + }); + + const casesWithComments = new Map(); + for (const [id, caseInfo] of casesMap.entries()) { + casesWithComments.set( + id, + flattenCaseSavedObject({ + savedObject: caseInfo.case, + totalComment: totalCommentsForCases.commentTotals.get(id) ?? 0, + totalAlerts: totalCommentsForCases.alertTotals.get(id) ?? 0, + subCases: caseInfo.subCases, + }) + ); + } + + return { + casesMap: casesWithComments, + page: cases.page, + perPage: cases.per_page, + total: cases.total, + }; + } + + /** + * Retrieves the number of cases that exist with a given status (open, closed, etc). + * This also counts sub cases. Parent cases are excluded from the statistics. + */ + public async findCaseStatusStats({ + soClient, + caseOptions, + subCaseOptions, + }: { + soClient: SavedObjectsClientContract; + caseOptions: SavedObjectFindOptionsKueryNode; + subCaseOptions?: SavedObjectFindOptionsKueryNode; + }): Promise { + const casesStats = await this.findCases({ + soClient, + options: { + ...caseOptions, + fields: [], + page: 1, + perPage: 1, + }, + }); + + /** + * This could be made more performant. What we're doing here is retrieving all cases + * that match the API request's filters instead of just counts. This is because we need to grab + * the ids for the parent cases that match those filters. Then we use those IDS to count how many + * sub cases those parents have to calculate the total amount of cases that are open, closed, or in-progress. + * + * Another solution would be to store ALL filterable fields on both a case and sub case. That we could do a single + * query for each type to calculate the totals using the filters. This has drawbacks though: + * + * We'd have to sync up the parent case's editable attributes with the sub case any time they were change to avoid + * them getting out of sync and causing issues when we do these types of stats aggregations. This would result in a lot + * of update requests if the user is editing their case details often. Which could potentially cause conflict failures. + * + * Another option is to prevent the ability from update the parent case's details all together once it's created. A user + * could instead modify the sub case details directly. This could be weird though because individual sub cases for the same + * parent would have different titles, tags, etc. + * + * Another potential issue with this approach is when you push a case and all its sub case information. If the sub cases + * don't have the same title and tags, we'd need to account for that as well. + */ + const cases = await this.findCases({ + soClient, + options: { + ...caseOptions, + fields: [caseTypeField], + page: 1, + perPage: casesStats.total, + }, + }); + + const caseIds = cases.saved_objects + .filter((caseInfo) => caseInfo.attributes.type === CaseType.collection) + .map((caseInfo) => caseInfo.id); + + let subCasesTotal = 0; + + if (ENABLE_CASE_CONNECTOR && subCaseOptions) { + subCasesTotal = await this.findSubCaseStatusStats({ + soClient, + options: cloneDeep(subCaseOptions), + ids: caseIds, + }); + } + + const total = + cases.saved_objects.filter((caseInfo) => caseInfo.attributes.type !== CaseType.collection) + .length + subCasesTotal; + + return total; + } + + /** + * Retrieves the comments attached to a case or sub case. + */ + public async getCommentsByAssociation({ + soClient, + id, + associationType, + options, + }: FindCommentsByAssociationArgs): Promise> { + if (associationType === AssociationType.subCase) { + return this.getAllSubCaseComments({ + soClient, + id, + options, + }); + } else { + return this.getAllCaseComments({ + soClient, + id, + options, + }); + } + } + + /** + * Returns the number of total comments and alerts for a case (or sub case) + */ + public async getCaseCommentStats({ + soClient, + ids, + associationType, + }: { + soClient: SavedObjectsClientContract; + ids: string[]; + associationType: AssociationType; + }): Promise { + if (ids.length <= 0) { + return { + commentTotals: new Map(), + alertTotals: new Map(), + }; + } + + const refType = + associationType === AssociationType.case ? CASE_SAVED_OBJECT : SUB_CASE_SAVED_OBJECT; + + const allComments = await Promise.all( + ids.map((id) => + this.getCommentsByAssociation({ + soClient, + associationType, + id, + options: { page: 1, perPage: 1 }, + }) + ) + ); + + const alerts = await this.getCommentsByAssociation({ + soClient, + associationType, + id: ids, + options: { + filter: nodeBuilder.or([ + nodeBuilder.is(`${CASE_COMMENT_SAVED_OBJECT}.attributes.type`, CommentType.alert), + nodeBuilder.is( + `${CASE_COMMENT_SAVED_OBJECT}.attributes.type`, + CommentType.generatedAlert + ), + ]), + }, + }); + + const getID = (comments: SavedObjectsFindResponse) => { + return comments.saved_objects.length > 0 + ? comments.saved_objects[0].references.find((ref) => ref.type === refType)?.id + : undefined; + }; + + const groupedComments = allComments.reduce((acc, comments) => { + const id = getID(comments); + if (id) { + acc.set(id, comments.total); + } + return acc; + }, new Map()); + + const groupedAlerts = groupTotalAlertsByID({ comments: alerts }); + return { commentTotals: groupedComments, alertTotals: groupedAlerts }; + } + + /** + * Returns all the sub cases for a set of case IDs. Comment statistics are also returned. + */ + public async findSubCasesGroupByCase({ + soClient, + options, + ids, + }: { + soClient: SavedObjectsClientContract; + options?: SavedObjectFindOptionsKueryNode; + ids: string[]; + }): Promise { + const getCaseID = (subCase: SavedObjectsFindResult): string | undefined => { + return subCase.references.length > 0 ? subCase.references[0].id : undefined; + }; + + const emptyResponse = { + subCasesMap: new Map(), + page: 0, + perPage: 0, + total: 0, + }; + + if (!options) { + return emptyResponse; + } + + if (ids.length <= 0) { + return emptyResponse; + } + + const subCases = await this.findSubCases({ + soClient, + options: { + ...options, + hasReference: ids.map((id) => { + return { + id, + type: CASE_SAVED_OBJECT, + }; + }), + }, + }); + + const subCaseComments = await this.getCaseCommentStats({ + soClient, + ids: subCases.saved_objects.map((subCase) => subCase.id), + associationType: AssociationType.subCase, + }); + + const subCasesMap = subCases.saved_objects.reduce((accMap, subCase) => { + const parentCaseID = getCaseID(subCase); + if (parentCaseID) { + const subCaseFromMap = accMap.get(parentCaseID); + + if (subCaseFromMap === undefined) { + const subCasesForID = [ + flattenSubCaseSavedObject({ + savedObject: subCase, + totalComment: subCaseComments.commentTotals.get(subCase.id) ?? 0, + totalAlerts: subCaseComments.alertTotals.get(subCase.id) ?? 0, + }), + ]; + accMap.set(parentCaseID, subCasesForID); + } else { + subCaseFromMap.push( + flattenSubCaseSavedObject({ + savedObject: subCase, + totalComment: subCaseComments.commentTotals.get(subCase.id) ?? 0, + totalAlerts: subCaseComments.alertTotals.get(subCase.id) ?? 0, + }) + ); + } + } + return accMap; + }, new Map()); + + return { subCasesMap, page: subCases.page, perPage: subCases.per_page, total: subCases.total }; + } + + /** + * Calculates the number of sub cases for a given set of options for a set of case IDs. + */ + public async findSubCaseStatusStats({ + soClient, + options, + ids, + }: FindSubCasesStatusStats): Promise { + if (ids.length <= 0) { + return 0; + } + + const subCases = await this.findSubCases({ + soClient, + options: { + ...options, + page: 1, + perPage: 1, + fields: [], + hasReference: ids.map((id) => { + return { + id, + type: CASE_SAVED_OBJECT, + }; + }), + }, + }); + + return subCases.total; + } + + public async createSubCase({ + soClient, + createdAt, + caseId, + createdBy, + }: CreateSubCaseArgs): Promise> { + try { + this.log.debug(`Attempting to POST a new sub case`); + return soClient.create( + SUB_CASE_SAVED_OBJECT, + transformNewSubCase({ createdAt, createdBy }), + { + references: [ + { + type: CASE_SAVED_OBJECT, + name: `associated-${CASE_SAVED_OBJECT}`, + id: caseId, + }, + ], + } + ); + } catch (error) { + this.log.error(`Error on POST a new sub case for id ${caseId}: ${error}`); + throw error; + } + } + + public async getMostRecentSubCase(soClient: SavedObjectsClientContract, caseId: string) { + try { + this.log.debug(`Attempting to find most recent sub case for caseID: ${caseId}`); + const subCases = await soClient.find({ + perPage: 1, + sortField: 'created_at', + sortOrder: 'desc', + type: SUB_CASE_SAVED_OBJECT, + hasReference: { type: CASE_SAVED_OBJECT, id: caseId }, + }); + if (subCases.saved_objects.length <= 0) { + return; + } + + return subCases.saved_objects[0]; + } catch (error) { + this.log.error(`Error finding the most recent sub case for case: ${caseId}: ${error}`); + throw error; + } + } + + public async deleteSubCase(soClient: SavedObjectsClientContract, id: string) { + try { + this.log.debug(`Attempting to DELETE sub case ${id}`); + return await soClient.delete(SUB_CASE_SAVED_OBJECT, id); + } catch (error) { + this.log.error(`Error on DELETE sub case ${id}: ${error}`); + throw error; + } + } + + public async deleteCase({ soClient, id: caseId }: GetCaseArgs) { + try { + this.log.debug(`Attempting to DELETE case ${caseId}`); + return await soClient.delete(CASE_SAVED_OBJECT, caseId); + } catch (error) { + this.log.error(`Error on DELETE case ${caseId}: ${error}`); + throw error; + } + } + + public async getCase({ + soClient, + id: caseId, + }: GetCaseArgs): Promise> { + try { + this.log.debug(`Attempting to GET case ${caseId}`); + return await soClient.get(CASE_SAVED_OBJECT, caseId); + } catch (error) { + this.log.error(`Error on GET case ${caseId}: ${error}`); + throw error; + } + } + public async getSubCase({ soClient, id }: GetCaseArgs): Promise> { + try { + this.log.debug(`Attempting to GET sub case ${id}`); + return await soClient.get(SUB_CASE_SAVED_OBJECT, id); + } catch (error) { + this.log.error(`Error on GET sub case ${id}: ${error}`); + throw error; + } + } + + public async getSubCases({ + soClient, + ids, + }: GetSubCasesArgs): Promise> { + try { + this.log.debug(`Attempting to GET sub cases ${ids.join(', ')}`); + return await soClient.bulkGet( + ids.map((id) => ({ type: SUB_CASE_SAVED_OBJECT, id })) + ); + } catch (error) { + this.log.error(`Error on GET cases ${ids.join(', ')}: ${error}`); + throw error; + } + } + + public async getCases({ + soClient, + caseIds, + }: GetCasesArgs): Promise> { + try { + this.log.debug(`Attempting to GET cases ${caseIds.join(', ')}`); + return await soClient.bulkGet( + caseIds.map((caseId) => ({ type: CASE_SAVED_OBJECT, id: caseId })) + ); + } catch (error) { + this.log.error(`Error on GET cases ${caseIds.join(', ')}: ${error}`); + throw error; + } + } + + public async findCases({ + soClient, + options, + }: FindCasesArgs): Promise> { + try { + this.log.debug(`Attempting to find cases`); + return await soClient.find({ + sortField: defaultSortField, + ...cloneDeep(options), + type: CASE_SAVED_OBJECT, + }); + } catch (error) { + this.log.error(`Error on find cases: ${error}`); + throw error; + } + } + + public async findSubCases({ + soClient, + options, + }: FindCasesArgs): Promise> { + try { + this.log.debug(`Attempting to find sub cases`); + // if the page or perPage options are set then respect those instead of trying to + // grab all sub cases + if (options?.page !== undefined || options?.perPage !== undefined) { + return soClient.find({ + sortField: defaultSortField, + ...cloneDeep(options), + type: SUB_CASE_SAVED_OBJECT, + }); + } + + const stats = await soClient.find({ + fields: [], + page: 1, + perPage: 1, + sortField: defaultSortField, + ...cloneDeep(options), + type: SUB_CASE_SAVED_OBJECT, + }); + return soClient.find({ + page: 1, + perPage: stats.total, + sortField: defaultSortField, + ...cloneDeep(options), + type: SUB_CASE_SAVED_OBJECT, + }); + } catch (error) { + this.log.error(`Error on find sub cases: ${error}`); + throw error; + } + } + + /** + * Find sub cases using a collection's ID. This would try to retrieve the maximum amount of sub cases + * by default. + * + * @param id the saved object ID of the parent collection to find sub cases for. + */ + public async findSubCasesByCaseId({ + soClient, + ids, + options, + }: FindSubCasesByIDArgs): Promise> { + if (ids.length <= 0) { + return { + total: 0, + saved_objects: [], + page: options?.page ?? defaultPage, + per_page: options?.perPage ?? defaultPerPage, + }; + } + + try { + this.log.debug(`Attempting to GET sub cases for case collection id ${ids.join(', ')}`); + return this.findSubCases({ + soClient, + options: { + ...options, + hasReference: ids.map((id) => ({ + type: CASE_SAVED_OBJECT, + id, + })), + }, + }); + } catch (error) { + this.log.error( + `Error on GET all sub cases for case collection id ${ids.join(', ')}: ${error}` + ); + throw error; + } + } + + private asArray(id: string | string[] | undefined): string[] { + if (id === undefined) { + return []; + } else if (Array.isArray(id)) { + return id; + } else { + return [id]; + } + } + + private async getAllComments({ + soClient, + id, + options, + }: FindCommentsArgs): Promise> { + try { + this.log.debug(`Attempting to GET all comments for id ${JSON.stringify(id)}`); + if (options?.page !== undefined || options?.perPage !== undefined) { + return soClient.find({ + type: CASE_COMMENT_SAVED_OBJECT, + sortField: defaultSortField, + ...cloneDeep(options), + }); + } + // get the total number of comments that are in ES then we'll grab them all in one go + const stats = await soClient.find({ + type: CASE_COMMENT_SAVED_OBJECT, + fields: [], + page: 1, + perPage: 1, + sortField: defaultSortField, + // spread the options after so the caller can override the default behavior if they want + ...cloneDeep(options), + }); + + return soClient.find({ + type: CASE_COMMENT_SAVED_OBJECT, + page: 1, + perPage: stats.total, + sortField: defaultSortField, + ...cloneDeep(options), + }); + } catch (error) { + this.log.error(`Error on GET all comments for ${JSON.stringify(id)}: ${error}`); + throw error; + } + } + + /** + * Default behavior is to retrieve all comments that adhere to a given filter (if one is included). + * to override this pass in the either the page or perPage options. + * + * @param includeSubCaseComments is a flag to indicate that sub case comments should be included as well, by default + * sub case comments are excluded. If the `filter` field is included in the options, it will override this behavior + */ + public async getAllCaseComments({ + soClient, + id, + options, + includeSubCaseComments = false, + }: FindCaseCommentsArgs): Promise> { + try { + const refs = this.asArray(id).map((caseID) => ({ type: CASE_SAVED_OBJECT, id: caseID })); + if (refs.length <= 0) { + return { + saved_objects: [], + total: 0, + per_page: options?.perPage ?? defaultPerPage, + page: options?.page ?? defaultPage, + }; + } + + let filter: KueryNode | undefined; + if (!includeSubCaseComments) { + // if other filters were passed in then combine them to filter out sub case comments + const associationTypeFilter = nodeBuilder.is( + `${CASE_COMMENT_SAVED_OBJECT}.attributes.associationType`, + AssociationType.case + ); + + filter = + options?.filter != null + ? nodeBuilder.and([options.filter, associationTypeFilter]) + : associationTypeFilter; + } + + this.log.debug(`Attempting to GET all comments for case caseID ${JSON.stringify(id)}`); + return this.getAllComments({ + soClient, + id, + options: { + hasReferenceOperator: 'OR', + hasReference: refs, + filter, + ...options, + }, + }); + } catch (error) { + this.log.error(`Error on GET all comments for case ${JSON.stringify(id)}: ${error}`); + throw error; + } + } + + public async getAllSubCaseComments({ + soClient, + id, + options, + }: FindSubCaseCommentsArgs): Promise> { + try { + const refs = this.asArray(id).map((caseID) => ({ type: SUB_CASE_SAVED_OBJECT, id: caseID })); + if (refs.length <= 0) { + return { + saved_objects: [], + total: 0, + per_page: options?.perPage ?? defaultPerPage, + page: options?.page ?? defaultPage, + }; + } + + this.log.debug(`Attempting to GET all comments for sub case caseID ${JSON.stringify(id)}`); + return this.getAllComments({ + soClient, + id, + options: { + hasReferenceOperator: 'OR', + hasReference: refs, + ...options, + }, + }); + } catch (error) { + this.log.error(`Error on GET all comments for sub case ${JSON.stringify(id)}: ${error}`); + throw error; + } + } + + public async getReporters({ soClient }: ClientArgs) { + try { + this.log.debug(`Attempting to GET all reporters`); + return await readReporters({ soClient }); + } catch (error) { + this.log.error(`Error on GET all reporters: ${error}`); + throw error; + } + } + public async getTags({ soClient }: ClientArgs) { + try { + this.log.debug(`Attempting to GET all cases`); + return await readTags({ soClient }); + } catch (error) { + this.log.error(`Error on GET cases: ${error}`); + throw error; + } + } + + public getUser({ request }: GetUserArgs) { + try { + this.log.debug(`Attempting to authenticate a user`); + if (this.authentication != null) { + const user = this.authentication.getCurrentUser(request); + if (!user) { + return { + username: null, + full_name: null, + email: null, + }; + } + return user; + } + return { + username: null, + full_name: null, + email: null, + }; + } catch (error) { + this.log.error(`Error on GET cases: ${error}`); + throw error; + } + } + + public async postNewCase({ soClient, attributes }: PostCaseArgs) { + try { + this.log.debug(`Attempting to POST a new case`); + return await soClient.create(CASE_SAVED_OBJECT, { + ...attributes, + }); + } catch (error) { + this.log.error(`Error on POST a new case: ${error}`); + throw error; + } + } + + public async patchCase({ soClient, caseId, updatedAttributes, version }: PatchCaseArgs) { + try { + this.log.debug(`Attempting to UPDATE case ${caseId}`); + return await soClient.update( + CASE_SAVED_OBJECT, + caseId, + { ...updatedAttributes }, + { version } + ); + } catch (error) { + this.log.error(`Error on UPDATE case ${caseId}: ${error}`); + throw error; + } + } + + public async patchCases({ soClient, cases }: PatchCasesArgs) { + try { + this.log.debug(`Attempting to UPDATE case ${cases.map((c) => c.caseId).join(', ')}`); + return await soClient.bulkUpdate( + cases.map((c) => ({ + type: CASE_SAVED_OBJECT, + id: c.caseId, + attributes: c.updatedAttributes, + version: c.version, + })) + ); + } catch (error) { + this.log.error(`Error on UPDATE case ${cases.map((c) => c.caseId).join(', ')}: ${error}`); + throw error; + } + } + + public async patchSubCase({ soClient, subCaseId, updatedAttributes, version }: PatchSubCase) { + try { + this.log.debug(`Attempting to UPDATE sub case ${subCaseId}`); + return await soClient.update( + SUB_CASE_SAVED_OBJECT, + subCaseId, + { ...updatedAttributes }, + { version } + ); + } catch (error) { + this.log.error(`Error on UPDATE sub case ${subCaseId}: ${error}`); + throw error; + } + } + + public async patchSubCases({ soClient, subCases }: PatchSubCases) { + try { + this.log.debug( + `Attempting to UPDATE sub case ${subCases.map((c) => c.subCaseId).join(', ')}` + ); + return await soClient.bulkUpdate( + subCases.map((c) => ({ + type: SUB_CASE_SAVED_OBJECT, + id: c.subCaseId, + attributes: c.updatedAttributes, + version: c.version, + })) + ); + } catch (error) { + this.log.error( + `Error on UPDATE sub case ${subCases.map((c) => c.subCaseId).join(', ')}: ${error}` + ); + throw error; + } + } +} diff --git a/x-pack/plugins/cases/server/services/reporters/read_reporters.ts b/x-pack/plugins/cases/server/services/cases/read_reporters.ts similarity index 89% rename from x-pack/plugins/cases/server/services/reporters/read_reporters.ts rename to x-pack/plugins/cases/server/services/cases/read_reporters.ts index e6dea6b6ee1e8f..f7e88c2649ae68 100644 --- a/x-pack/plugins/cases/server/services/reporters/read_reporters.ts +++ b/x-pack/plugins/cases/server/services/cases/read_reporters.ts @@ -26,18 +26,18 @@ export const convertToReporters = (caseObjects: Array => { - const firstReporters = await client.find({ + const firstReporters = await soClient.find({ type: CASE_SAVED_OBJECT, fields: ['created_by'], page: 1, perPage: 1, }); - const reporters = await client.find({ + const reporters = await soClient.find({ type: CASE_SAVED_OBJECT, fields: ['created_by'], page: 1, diff --git a/x-pack/plugins/cases/server/services/tags/read_tags.ts b/x-pack/plugins/cases/server/services/cases/read_tags.ts similarity index 87% rename from x-pack/plugins/cases/server/services/tags/read_tags.ts rename to x-pack/plugins/cases/server/services/cases/read_tags.ts index 7ac4ff41e0aa8d..a977c473327f86 100644 --- a/x-pack/plugins/cases/server/services/tags/read_tags.ts +++ b/x-pack/plugins/cases/server/services/cases/read_tags.ts @@ -29,27 +29,27 @@ export const convertTagsToSet = (tagObjects: Array>) // 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 ({ - client, + soClient, }: { - client: SavedObjectsClientContract; + soClient: SavedObjectsClientContract; perPage?: number; }): Promise => { - const tags = await readRawTags({ client }); + const tags = await readRawTags({ soClient }); return tags; }; export const readRawTags = async ({ - client, + soClient, }: { - client: SavedObjectsClientContract; + soClient: SavedObjectsClientContract; }): Promise => { - const firstTags = await client.find({ + const firstTags = await soClient.find({ type: CASE_SAVED_OBJECT, fields: ['tags'], page: 1, perPage: 1, }); - const tags = await client.find({ + const tags = await soClient.find({ type: CASE_SAVED_OBJECT, fields: ['tags'], page: 1, diff --git a/x-pack/plugins/cases/server/services/configure/index.ts b/x-pack/plugins/cases/server/services/configure/index.ts index 74ad23dd93ba01..45a9cd714145ff 100644 --- a/x-pack/plugins/cases/server/services/configure/index.ts +++ b/x-pack/plugins/cases/server/services/configure/index.ts @@ -5,19 +5,13 @@ * 2.0. */ -import { - Logger, - SavedObject, - SavedObjectsClientContract, - SavedObjectsFindResponse, - SavedObjectsUpdateResponse, -} from 'kibana/server'; +import { Logger, SavedObjectsClientContract } from 'kibana/server'; import { ESCasesConfigureAttributes, SavedObjectFindOptions } from '../../../common/api'; import { CASE_CONFIGURE_SAVED_OBJECT } from '../../../common/constants'; interface ClientArgs { - client: SavedObjectsClientContract; + soClient: SavedObjectsClientContract; } interface GetCaseConfigureArgs extends ClientArgs { @@ -36,65 +30,70 @@ interface PatchCaseConfigureArgs extends ClientArgs { updatedAttributes: Partial; } -export interface CaseConfigureServiceSetup { - delete(args: GetCaseConfigureArgs): Promise<{}>; - get(args: GetCaseConfigureArgs): Promise>; - find(args: FindCaseConfigureArgs): Promise>; - patch( - args: PatchCaseConfigureArgs - ): Promise>; - post(args: PostCaseConfigureArgs): Promise>; -} - export class CaseConfigureService { constructor(private readonly log: Logger) {} - public setup = async (): Promise => ({ - delete: async ({ client, caseConfigureId }: GetCaseConfigureArgs) => { - try { - this.log.debug(`Attempting to DELETE case configure ${caseConfigureId}`); - return await client.delete(CASE_CONFIGURE_SAVED_OBJECT, caseConfigureId); - } catch (error) { - this.log.debug(`Error on DELETE case configure ${caseConfigureId}: ${error}`); - throw error; - } - }, - get: async ({ client, caseConfigureId }: GetCaseConfigureArgs) => { - try { - this.log.debug(`Attempting to GET case configuration ${caseConfigureId}`); - return await client.get(CASE_CONFIGURE_SAVED_OBJECT, caseConfigureId); - } catch (error) { - this.log.debug(`Error on GET case configuration ${caseConfigureId}: ${error}`); - throw error; - } - }, - find: async ({ client, options }: FindCaseConfigureArgs) => { - try { - this.log.debug(`Attempting to find all case configuration`); - return await client.find({ ...options, type: CASE_CONFIGURE_SAVED_OBJECT }); - } catch (error) { - this.log.debug(`Attempting to find all case configuration`); - throw error; - } - }, - post: async ({ client, attributes }: PostCaseConfigureArgs) => { - try { - this.log.debug(`Attempting to POST a new case configuration`); - return await client.create(CASE_CONFIGURE_SAVED_OBJECT, { ...attributes }); - } catch (error) { - this.log.debug(`Error on POST a new case configuration: ${error}`); - throw error; - } - }, - patch: async ({ client, caseConfigureId, updatedAttributes }: PatchCaseConfigureArgs) => { - try { - this.log.debug(`Attempting to UPDATE case configuration ${caseConfigureId}`); - return await client.update(CASE_CONFIGURE_SAVED_OBJECT, caseConfigureId, { + + public async delete({ soClient, caseConfigureId }: GetCaseConfigureArgs) { + try { + this.log.debug(`Attempting to DELETE case configure ${caseConfigureId}`); + return await soClient.delete(CASE_CONFIGURE_SAVED_OBJECT, caseConfigureId); + } catch (error) { + this.log.debug(`Error on DELETE case configure ${caseConfigureId}: ${error}`); + throw error; + } + } + + public async get({ soClient, caseConfigureId }: GetCaseConfigureArgs) { + try { + this.log.debug(`Attempting to GET case configuration ${caseConfigureId}`); + return await soClient.get( + CASE_CONFIGURE_SAVED_OBJECT, + caseConfigureId + ); + } catch (error) { + this.log.debug(`Error on GET case configuration ${caseConfigureId}: ${error}`); + throw error; + } + } + + public async find({ soClient, options }: FindCaseConfigureArgs) { + try { + this.log.debug(`Attempting to find all case configuration`); + return await soClient.find({ + ...options, + type: CASE_CONFIGURE_SAVED_OBJECT, + }); + } catch (error) { + this.log.debug(`Attempting to find all case configuration`); + throw error; + } + } + + public async post({ soClient, attributes }: PostCaseConfigureArgs) { + try { + this.log.debug(`Attempting to POST a new case configuration`); + return await soClient.create(CASE_CONFIGURE_SAVED_OBJECT, { + ...attributes, + }); + } catch (error) { + this.log.debug(`Error on POST a new case configuration: ${error}`); + throw error; + } + } + + public async patch({ soClient, caseConfigureId, updatedAttributes }: PatchCaseConfigureArgs) { + try { + this.log.debug(`Attempting to UPDATE case configuration ${caseConfigureId}`); + return await soClient.update( + CASE_CONFIGURE_SAVED_OBJECT, + caseConfigureId, + { ...updatedAttributes, - }); - } catch (error) { - this.log.debug(`Error on UPDATE case configuration ${caseConfigureId}: ${error}`); - throw error; - } - }, - }); + } + ); + } catch (error) { + this.log.debug(`Error on UPDATE case configuration ${caseConfigureId}: ${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 5cb338e17bf75b..0d51e12a55ac76 100644 --- a/x-pack/plugins/cases/server/services/connector_mappings/index.ts +++ b/x-pack/plugins/cases/server/services/connector_mappings/index.ts @@ -5,19 +5,13 @@ * 2.0. */ -import { - Logger, - SavedObject, - SavedObjectReference, - SavedObjectsClientContract, - SavedObjectsFindResponse, -} from 'kibana/server'; +import { Logger, SavedObjectReference, SavedObjectsClientContract } from 'kibana/server'; import { ConnectorMappings, SavedObjectFindOptions } from '../../../common/api'; import { CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT } from '../../../common/constants'; interface ClientArgs { - client: SavedObjectsClientContract; + soClient: SavedObjectsClientContract; } interface FindConnectorMappingsArgs extends ClientArgs { options?: SavedObjectFindOptions; @@ -28,33 +22,35 @@ interface PostConnectorMappingsArgs extends ClientArgs { references: SavedObjectReference[]; } -export interface ConnectorMappingsServiceSetup { - find(args: FindConnectorMappingsArgs): Promise>; - post(args: PostConnectorMappingsArgs): Promise>; -} - export class ConnectorMappingsService { constructor(private readonly log: Logger) {} - public setup = async (): Promise => ({ - find: async ({ client, options }: FindConnectorMappingsArgs) => { - try { - this.log.debug(`Attempting to find all connector mappings`); - return await client.find({ ...options, type: CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT }); - } catch (error) { - this.log.error(`Attempting to find all connector mappings: ${error}`); - throw error; - } - }, - post: async ({ client, attributes, references }: PostConnectorMappingsArgs) => { - try { - this.log.debug(`Attempting to POST a new connector mappings`); - return await client.create(CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, attributes, { + + public async find({ soClient, options }: FindConnectorMappingsArgs) { + try { + this.log.debug(`Attempting to find all connector mappings`); + return await soClient.find({ + ...options, + type: CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, + }); + } catch (error) { + this.log.error(`Attempting to find all connector mappings: ${error}`); + throw error; + } + } + + public async post({ soClient, attributes, references }: PostConnectorMappingsArgs) { + try { + this.log.debug(`Attempting to POST a new connector mappings`); + return await soClient.create( + CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, + attributes, + { references, - }); - } catch (error) { - this.log.error(`Error on POST a new connector mappings: ${error}`); - throw error; - } - }, - }); + } + ); + } catch (error) { + this.log.error(`Error on POST a new connector mappings: ${error}`); + throw error; + } + } } diff --git a/x-pack/plugins/cases/server/services/index.ts b/x-pack/plugins/cases/server/services/index.ts index cb275b3f5d44d3..cffe7df91743fc 100644 --- a/x-pack/plugins/cases/server/services/index.ts +++ b/x-pack/plugins/cases/server/services/index.ts @@ -5,1151 +5,15 @@ * 2.0. */ -import { cloneDeep } from 'lodash'; -import { - KibanaRequest, - Logger, - SavedObject, - SavedObjectsClientContract, - SavedObjectsFindResponse, - SavedObjectsUpdateResponse, - SavedObjectReference, - SavedObjectsBulkUpdateResponse, - SavedObjectsBulkResponse, - SavedObjectsFindResult, -} from 'kibana/server'; +import { SavedObjectsClientContract } from 'kibana/server'; -import { nodeBuilder } from '../../../../../src/plugins/data/common'; -import { KueryNode } from '../../../../../src/plugins/data/server'; - -import { AuthenticatedUser, SecurityPluginSetup } from '../../../security/server'; -import { - ESCaseAttributes, - CommentAttributes, - User, - CommentPatchAttributes, - SubCaseAttributes, - AssociationType, - SubCaseResponse, - CommentType, - CaseType, - CaseResponse, - caseTypeField, - CasesFindRequest, -} from '../../common/api'; -import { defaultSortField, groupTotalAlertsByID, SavedObjectFindOptionsKueryNode } from '../common'; -import { ENABLE_CASE_CONNECTOR } from '../../common/constants'; -import { defaultPage, defaultPerPage } from '../routes/api'; -import { - flattenCaseSavedObject, - flattenSubCaseSavedObject, - transformNewSubCase, -} from '../routes/api/utils'; -import { - CASE_SAVED_OBJECT, - CASE_COMMENT_SAVED_OBJECT, - SUB_CASE_SAVED_OBJECT, -} from '../../common/constants'; -import { readReporters } from './reporters/read_reporters'; -import { readTags } from './tags/read_tags'; - -export { CaseConfigureService, CaseConfigureServiceSetup } from './configure'; -export { CaseUserActionService, CaseUserActionServiceSetup } from './user_actions'; -export { ConnectorMappingsService, ConnectorMappingsServiceSetup } from './connector_mappings'; +export { CaseService } from './cases'; +export { CaseConfigureService } from './configure'; +export { CaseUserActionService } from './user_actions'; +export { ConnectorMappingsService } from './connector_mappings'; export { AlertService, AlertServiceContract } from './alerts'; +export { AttachmentService } from './attachments'; export interface ClientArgs { - client: SavedObjectsClientContract; -} - -interface PushedArgs { - pushed_at: string; - pushed_by: User; -} - -interface GetCaseArgs extends ClientArgs { - id: string; -} - -interface GetCasesArgs extends ClientArgs { - caseIds: string[]; -} - -interface GetSubCasesArgs extends ClientArgs { - ids: string[]; -} - -interface FindCommentsArgs { - client: SavedObjectsClientContract; - id: string | string[]; - options?: SavedObjectFindOptionsKueryNode; -} - -interface FindCaseCommentsArgs { - client: SavedObjectsClientContract; - id: string | string[]; - options?: SavedObjectFindOptionsKueryNode; - includeSubCaseComments?: boolean; -} - -interface FindSubCaseCommentsArgs { - client: SavedObjectsClientContract; - id: string | string[]; - options?: SavedObjectFindOptionsKueryNode; -} - -interface FindCasesArgs extends ClientArgs { - options?: SavedObjectFindOptionsKueryNode; -} - -interface FindSubCasesByIDArgs extends FindCasesArgs { - ids: string[]; -} - -interface FindSubCasesStatusStats { - client: SavedObjectsClientContract; - options: SavedObjectFindOptionsKueryNode; - ids: string[]; -} - -interface GetCommentArgs extends ClientArgs { - commentId: string; -} - -interface PostCaseArgs extends ClientArgs { - attributes: ESCaseAttributes; -} - -interface CreateSubCaseArgs extends ClientArgs { - createdAt: string; - caseId: string; - createdBy: User; -} - -interface PostCommentArgs extends ClientArgs { - attributes: CommentAttributes; - references: SavedObjectReference[]; -} - -interface PatchCase { - caseId: string; - updatedAttributes: Partial; - version?: string; -} -type PatchCaseArgs = PatchCase & ClientArgs; - -interface PatchCasesArgs extends ClientArgs { - cases: PatchCase[]; -} - -interface PatchComment { - commentId: string; - updatedAttributes: CommentPatchAttributes; - version?: string; -} - -type UpdateCommentArgs = PatchComment & ClientArgs; - -interface PatchComments extends ClientArgs { - comments: PatchComment[]; -} - -interface PatchSubCase { - client: SavedObjectsClientContract; - subCaseId: string; - updatedAttributes: Partial; - version?: string; -} - -interface PatchSubCases { - client: SavedObjectsClientContract; - subCases: Array>; -} - -interface GetUserArgs { - request: KibanaRequest; -} - -interface SubCasesMapWithPageInfo { - subCasesMap: Map; - page: number; - perPage: number; - total: number; -} - -interface CaseCommentStats { - commentTotals: Map; - alertTotals: Map; -} - -interface FindCommentsByAssociationArgs { - client: SavedObjectsClientContract; - id: string | string[]; - associationType: AssociationType; - options?: SavedObjectFindOptionsKueryNode; -} - -interface Collection { - case: SavedObjectsFindResult; - subCases?: SubCaseResponse[]; -} - -interface CasesMapWithPageInfo { - casesMap: Map; - page: number; - perPage: number; - total: number; -} - -type FindCaseOptions = CasesFindRequest & SavedObjectFindOptionsKueryNode; - -export interface CaseServiceSetup { - deleteCase(args: GetCaseArgs): Promise<{}>; - deleteComment(args: GetCommentArgs): Promise<{}>; - deleteSubCase(client: SavedObjectsClientContract, id: string): Promise<{}>; - findCases(args: FindCasesArgs): Promise>; - findSubCases(args: FindCasesArgs): Promise>; - findSubCasesByCaseId( - args: FindSubCasesByIDArgs - ): Promise>; - getAllCaseComments( - args: FindCaseCommentsArgs - ): Promise>; - getAllSubCaseComments( - args: FindSubCaseCommentsArgs - ): Promise>; - getCase(args: GetCaseArgs): Promise>; - getSubCase(args: GetCaseArgs): Promise>; - getSubCases(args: GetSubCasesArgs): Promise>; - getCases(args: GetCasesArgs): Promise>; - getComment(args: GetCommentArgs): Promise>; - getTags(args: ClientArgs): Promise; - getReporters(args: ClientArgs): Promise; - getUser(args: GetUserArgs): AuthenticatedUser | User; - postNewCase(args: PostCaseArgs): Promise>; - postNewComment(args: PostCommentArgs): Promise>; - patchCase(args: PatchCaseArgs): Promise>; - patchCases(args: PatchCasesArgs): Promise>; - patchComment(args: UpdateCommentArgs): Promise>; - patchComments(args: PatchComments): Promise>; - getMostRecentSubCase( - client: SavedObjectsClientContract, - caseId: string - ): Promise | undefined>; - createSubCase(args: CreateSubCaseArgs): Promise>; - patchSubCase(args: PatchSubCase): Promise>; - patchSubCases(args: PatchSubCases): Promise>; - findSubCaseStatusStats(args: FindSubCasesStatusStats): Promise; - getCommentsByAssociation( - args: FindCommentsByAssociationArgs - ): Promise>; - getCaseCommentStats(args: { - client: SavedObjectsClientContract; - ids: string[]; - associationType: AssociationType; - }): Promise; - findSubCasesGroupByCase(args: { - client: SavedObjectsClientContract; - options?: SavedObjectFindOptionsKueryNode; - ids: string[]; - }): Promise; - findCaseStatusStats(args: { - client: SavedObjectsClientContract; - caseOptions: SavedObjectFindOptionsKueryNode; - subCaseOptions?: SavedObjectFindOptionsKueryNode; - }): Promise; - findCasesGroupedByID(args: { - client: SavedObjectsClientContract; - caseOptions: SavedObjectFindOptionsKueryNode; - subCaseOptions?: SavedObjectFindOptionsKueryNode; - }): Promise; -} - -export class CaseService implements CaseServiceSetup { - constructor( - private readonly log: Logger, - private readonly authentication?: SecurityPluginSetup['authc'] - ) {} - - /** - * Returns a map of all cases combined with their sub cases if they are collections. - */ - public async findCasesGroupedByID({ - client, - caseOptions, - subCaseOptions, - }: { - client: SavedObjectsClientContract; - caseOptions: FindCaseOptions; - subCaseOptions?: SavedObjectFindOptionsKueryNode; - }): Promise { - const cases = await this.findCases({ - client, - options: caseOptions, - }); - - const subCasesResp = ENABLE_CASE_CONNECTOR - ? await this.findSubCasesGroupByCase({ - client, - options: subCaseOptions, - ids: cases.saved_objects - .filter((caseInfo) => caseInfo.attributes.type === CaseType.collection) - .map((caseInfo) => caseInfo.id), - }) - : { subCasesMap: new Map(), page: 0, perPage: 0 }; - - const casesMap = cases.saved_objects.reduce((accMap, caseInfo) => { - const subCasesForCase = subCasesResp.subCasesMap.get(caseInfo.id); - - /** - * If this case is an individual add it to the return map - * If it is a collection and it has sub cases add it to the return map - * If it is a collection and it does not have sub cases, check and see if we're filtering on a status, - * if we're filtering on a status then exclude the empty collection from the results - * if we're not filtering on a status then include the empty collection (that way we can display all the collections - * when the UI isn't doing any filtering) - */ - if ( - caseInfo.attributes.type === CaseType.individual || - subCasesForCase !== undefined || - !caseOptions.status - ) { - accMap.set(caseInfo.id, { case: caseInfo, subCases: subCasesForCase }); - } - return accMap; - }, new Map()); - - /** - * One potential optimization here is to get all comment stats for individual cases, parent cases, and sub cases - * in a single request. This can be done because comments that are for sub cases have a reference to both the sub case - * and the parent. The associationType field allows us to determine which type of case the comment is attached to. - * - * So we could use the ids for all the valid cases (individual cases and parents with sub cases) to grab everything. - * Once we have it we can build the maps. - * - * Currently we get all comment stats for all sub cases in one go and we get all comment stats for cases (individual and parent) - * in another request (the one below this comment). - */ - const totalCommentsForCases = await this.getCaseCommentStats({ - client, - ids: Array.from(casesMap.keys()), - associationType: AssociationType.case, - }); - - const casesWithComments = new Map(); - for (const [id, caseInfo] of casesMap.entries()) { - casesWithComments.set( - id, - flattenCaseSavedObject({ - savedObject: caseInfo.case, - totalComment: totalCommentsForCases.commentTotals.get(id) ?? 0, - totalAlerts: totalCommentsForCases.alertTotals.get(id) ?? 0, - subCases: caseInfo.subCases, - }) - ); - } - - return { - casesMap: casesWithComments, - page: cases.page, - perPage: cases.per_page, - total: cases.total, - }; - } - - /** - * Retrieves the number of cases that exist with a given status (open, closed, etc). - * This also counts sub cases. Parent cases are excluded from the statistics. - */ - public async findCaseStatusStats({ - client, - caseOptions, - subCaseOptions, - }: { - client: SavedObjectsClientContract; - caseOptions: SavedObjectFindOptionsKueryNode; - subCaseOptions?: SavedObjectFindOptionsKueryNode; - }): Promise { - const casesStats = await this.findCases({ - client, - options: { - ...caseOptions, - fields: [], - page: 1, - perPage: 1, - }, - }); - - /** - * This could be made more performant. What we're doing here is retrieving all cases - * that match the API request's filters instead of just counts. This is because we need to grab - * the ids for the parent cases that match those filters. Then we use those IDS to count how many - * sub cases those parents have to calculate the total amount of cases that are open, closed, or in-progress. - * - * Another solution would be to store ALL filterable fields on both a case and sub case. That we could do a single - * query for each type to calculate the totals using the filters. This has drawbacks though: - * - * We'd have to sync up the parent case's editable attributes with the sub case any time they were change to avoid - * them getting out of sync and causing issues when we do these types of stats aggregations. This would result in a lot - * of update requests if the user is editing their case details often. Which could potentially cause conflict failures. - * - * Another option is to prevent the ability from update the parent case's details all together once it's created. A user - * could instead modify the sub case details directly. This could be weird though because individual sub cases for the same - * parent would have different titles, tags, etc. - * - * Another potential issue with this approach is when you push a case and all its sub case information. If the sub cases - * don't have the same title and tags, we'd need to account for that as well. - */ - const cases = await this.findCases({ - client, - options: { - ...caseOptions, - fields: [caseTypeField], - page: 1, - perPage: casesStats.total, - }, - }); - - const caseIds = cases.saved_objects - .filter((caseInfo) => caseInfo.attributes.type === CaseType.collection) - .map((caseInfo) => caseInfo.id); - - let subCasesTotal = 0; - - if (ENABLE_CASE_CONNECTOR && subCaseOptions) { - subCasesTotal = await this.findSubCaseStatusStats({ - client, - options: cloneDeep(subCaseOptions), - ids: caseIds, - }); - } - - const total = - cases.saved_objects.filter((caseInfo) => caseInfo.attributes.type !== CaseType.collection) - .length + subCasesTotal; - - return total; - } - - /** - * Retrieves the comments attached to a case or sub case. - */ - public async getCommentsByAssociation({ - client, - id, - associationType, - options, - }: FindCommentsByAssociationArgs): Promise> { - if (associationType === AssociationType.subCase) { - return this.getAllSubCaseComments({ - client, - id, - options, - }); - } else { - return this.getAllCaseComments({ - client, - id, - options, - }); - } - } - - /** - * Returns the number of total comments and alerts for a case (or sub case) - */ - public async getCaseCommentStats({ - client, - ids, - associationType, - }: { - client: SavedObjectsClientContract; - ids: string[]; - associationType: AssociationType; - }): Promise { - if (ids.length <= 0) { - return { - commentTotals: new Map(), - alertTotals: new Map(), - }; - } - - const refType = - associationType === AssociationType.case ? CASE_SAVED_OBJECT : SUB_CASE_SAVED_OBJECT; - - const allComments = await Promise.all( - ids.map((id) => - this.getCommentsByAssociation({ - client, - associationType, - id, - options: { page: 1, perPage: 1 }, - }) - ) - ); - - const alerts = await this.getCommentsByAssociation({ - client, - associationType, - id: ids, - options: { - filter: nodeBuilder.or([ - nodeBuilder.is(`${CASE_COMMENT_SAVED_OBJECT}.attributes.type`, CommentType.alert), - nodeBuilder.is( - `${CASE_COMMENT_SAVED_OBJECT}.attributes.type`, - CommentType.generatedAlert - ), - ]), - }, - }); - - const getID = (comments: SavedObjectsFindResponse) => { - return comments.saved_objects.length > 0 - ? comments.saved_objects[0].references.find((ref) => ref.type === refType)?.id - : undefined; - }; - - const groupedComments = allComments.reduce((acc, comments) => { - const id = getID(comments); - if (id) { - acc.set(id, comments.total); - } - return acc; - }, new Map()); - - const groupedAlerts = groupTotalAlertsByID({ comments: alerts }); - return { commentTotals: groupedComments, alertTotals: groupedAlerts }; - } - - /** - * Returns all the sub cases for a set of case IDs. Comment statistics are also returned. - */ - public async findSubCasesGroupByCase({ - client, - options, - ids, - }: { - client: SavedObjectsClientContract; - options?: SavedObjectFindOptionsKueryNode; - ids: string[]; - }): Promise { - const getCaseID = (subCase: SavedObjectsFindResult): string | undefined => { - return subCase.references.length > 0 ? subCase.references[0].id : undefined; - }; - - const emptyResponse = { - subCasesMap: new Map(), - page: 0, - perPage: 0, - total: 0, - }; - - if (!options) { - return emptyResponse; - } - - if (ids.length <= 0) { - return emptyResponse; - } - - const subCases = await this.findSubCases({ - client, - options: { - ...options, - hasReference: ids.map((id) => { - return { - id, - type: CASE_SAVED_OBJECT, - }; - }), - }, - }); - - const subCaseComments = await this.getCaseCommentStats({ - client, - ids: subCases.saved_objects.map((subCase) => subCase.id), - associationType: AssociationType.subCase, - }); - - const subCasesMap = subCases.saved_objects.reduce((accMap, subCase) => { - const parentCaseID = getCaseID(subCase); - if (parentCaseID) { - const subCaseFromMap = accMap.get(parentCaseID); - - if (subCaseFromMap === undefined) { - const subCasesForID = [ - flattenSubCaseSavedObject({ - savedObject: subCase, - totalComment: subCaseComments.commentTotals.get(subCase.id) ?? 0, - totalAlerts: subCaseComments.alertTotals.get(subCase.id) ?? 0, - }), - ]; - accMap.set(parentCaseID, subCasesForID); - } else { - subCaseFromMap.push( - flattenSubCaseSavedObject({ - savedObject: subCase, - totalComment: subCaseComments.commentTotals.get(subCase.id) ?? 0, - totalAlerts: subCaseComments.alertTotals.get(subCase.id) ?? 0, - }) - ); - } - } - return accMap; - }, new Map()); - - return { subCasesMap, page: subCases.page, perPage: subCases.per_page, total: subCases.total }; - } - - /** - * Calculates the number of sub cases for a given set of options for a set of case IDs. - */ - public async findSubCaseStatusStats({ - client, - options, - ids, - }: FindSubCasesStatusStats): Promise { - if (ids.length <= 0) { - return 0; - } - - const subCases = await this.findSubCases({ - client, - options: { - ...options, - page: 1, - perPage: 1, - fields: [], - hasReference: ids.map((id) => { - return { - id, - type: CASE_SAVED_OBJECT, - }; - }), - }, - }); - - return subCases.total; - } - - public async createSubCase({ - client, - createdAt, - caseId, - createdBy, - }: CreateSubCaseArgs): Promise> { - try { - this.log.debug(`Attempting to POST a new sub case`); - return client.create(SUB_CASE_SAVED_OBJECT, transformNewSubCase({ createdAt, createdBy }), { - references: [ - { - type: CASE_SAVED_OBJECT, - name: `associated-${CASE_SAVED_OBJECT}`, - id: caseId, - }, - ], - }); - } catch (error) { - this.log.error(`Error on POST a new sub case for id ${caseId}: ${error}`); - throw error; - } - } - - public async getMostRecentSubCase(client: SavedObjectsClientContract, caseId: string) { - try { - this.log.debug(`Attempting to find most recent sub case for caseID: ${caseId}`); - const subCases: SavedObjectsFindResponse = await client.find({ - perPage: 1, - sortField: 'created_at', - sortOrder: 'desc', - type: SUB_CASE_SAVED_OBJECT, - hasReference: { type: CASE_SAVED_OBJECT, id: caseId }, - }); - if (subCases.saved_objects.length <= 0) { - return; - } - - return subCases.saved_objects[0]; - } catch (error) { - this.log.error(`Error finding the most recent sub case for case: ${caseId}: ${error}`); - throw error; - } - } - - public async deleteSubCase(client: SavedObjectsClientContract, id: string) { - try { - this.log.debug(`Attempting to DELETE sub case ${id}`); - return await client.delete(SUB_CASE_SAVED_OBJECT, id); - } catch (error) { - this.log.error(`Error on DELETE sub case ${id}: ${error}`); - throw error; - } - } - - public async deleteCase({ client, id: caseId }: GetCaseArgs) { - try { - this.log.debug(`Attempting to DELETE case ${caseId}`); - return await client.delete(CASE_SAVED_OBJECT, caseId); - } catch (error) { - this.log.error(`Error on DELETE case ${caseId}: ${error}`); - throw error; - } - } - public async deleteComment({ client, commentId }: GetCommentArgs) { - try { - this.log.debug(`Attempting to GET comment ${commentId}`); - return await client.delete(CASE_COMMENT_SAVED_OBJECT, commentId); - } catch (error) { - this.log.error(`Error on GET comment ${commentId}: ${error}`); - throw error; - } - } - public async getCase({ - client, - id: caseId, - }: GetCaseArgs): Promise> { - try { - this.log.debug(`Attempting to GET case ${caseId}`); - return await client.get(CASE_SAVED_OBJECT, caseId); - } catch (error) { - this.log.error(`Error on GET case ${caseId}: ${error}`); - throw error; - } - } - public async getSubCase({ client, id }: GetCaseArgs): Promise> { - try { - this.log.debug(`Attempting to GET sub case ${id}`); - return await client.get(SUB_CASE_SAVED_OBJECT, id); - } catch (error) { - this.log.error(`Error on GET sub case ${id}: ${error}`); - throw error; - } - } - - public async getSubCases({ - client, - ids, - }: GetSubCasesArgs): Promise> { - try { - this.log.debug(`Attempting to GET sub cases ${ids.join(', ')}`); - return await client.bulkGet(ids.map((id) => ({ type: SUB_CASE_SAVED_OBJECT, id }))); - } catch (error) { - this.log.error(`Error on GET cases ${ids.join(', ')}: ${error}`); - throw error; - } - } - - public async getCases({ - client, - caseIds, - }: GetCasesArgs): Promise> { - try { - this.log.debug(`Attempting to GET cases ${caseIds.join(', ')}`); - return await client.bulkGet( - caseIds.map((caseId) => ({ type: CASE_SAVED_OBJECT, id: caseId })) - ); - } catch (error) { - this.log.error(`Error on GET cases ${caseIds.join(', ')}: ${error}`); - throw error; - } - } - public async getComment({ - client, - commentId, - }: GetCommentArgs): Promise> { - try { - this.log.debug(`Attempting to GET comment ${commentId}`); - return await client.get(CASE_COMMENT_SAVED_OBJECT, commentId); - } catch (error) { - this.log.error(`Error on GET comment ${commentId}: ${error}`); - throw error; - } - } - - public async findCases({ - client, - options, - }: FindCasesArgs): Promise> { - try { - this.log.debug(`Attempting to find cases`); - return await client.find({ - sortField: defaultSortField, - ...cloneDeep(options), - type: CASE_SAVED_OBJECT, - }); - } catch (error) { - this.log.error(`Error on find cases: ${error}`); - throw error; - } - } - - public async findSubCases({ - client, - options, - }: FindCasesArgs): Promise> { - try { - this.log.debug(`Attempting to find sub cases`); - // if the page or perPage options are set then respect those instead of trying to - // grab all sub cases - if (options?.page !== undefined || options?.perPage !== undefined) { - return client.find({ - sortField: defaultSortField, - ...cloneDeep(options), - type: SUB_CASE_SAVED_OBJECT, - }); - } - - const stats = await client.find({ - fields: [], - page: 1, - perPage: 1, - sortField: defaultSortField, - ...cloneDeep(options), - type: SUB_CASE_SAVED_OBJECT, - }); - return client.find({ - page: 1, - perPage: stats.total, - sortField: defaultSortField, - ...cloneDeep(options), - type: SUB_CASE_SAVED_OBJECT, - }); - } catch (error) { - this.log.error(`Error on find sub cases: ${error}`); - throw error; - } - } - - /** - * Find sub cases using a collection's ID. This would try to retrieve the maximum amount of sub cases - * by default. - * - * @param id the saved object ID of the parent collection to find sub cases for. - */ - public async findSubCasesByCaseId({ - client, - ids, - options, - }: FindSubCasesByIDArgs): Promise> { - if (ids.length <= 0) { - return { - total: 0, - saved_objects: [], - page: options?.page ?? defaultPage, - per_page: options?.perPage ?? defaultPerPage, - }; - } - - try { - this.log.debug(`Attempting to GET sub cases for case collection id ${ids.join(', ')}`); - return this.findSubCases({ - client, - options: { - ...options, - hasReference: ids.map((id) => ({ - type: CASE_SAVED_OBJECT, - id, - })), - }, - }); - } catch (error) { - this.log.error( - `Error on GET all sub cases for case collection id ${ids.join(', ')}: ${error}` - ); - throw error; - } - } - - private asArray(id: string | string[] | undefined): string[] { - if (id === undefined) { - return []; - } else if (Array.isArray(id)) { - return id; - } else { - return [id]; - } - } - - private async getAllComments({ - client, - id, - options, - }: FindCommentsArgs): Promise> { - try { - this.log.debug(`Attempting to GET all comments for id ${JSON.stringify(id)}`); - if (options?.page !== undefined || options?.perPage !== undefined) { - return client.find({ - type: CASE_COMMENT_SAVED_OBJECT, - sortField: defaultSortField, - ...cloneDeep(options), - }); - } - // get the total number of comments that are in ES then we'll grab them all in one go - const stats = await client.find({ - type: CASE_COMMENT_SAVED_OBJECT, - fields: [], - page: 1, - perPage: 1, - sortField: defaultSortField, - // spread the options after so the caller can override the default behavior if they want - ...cloneDeep(options), - }); - - return client.find({ - type: CASE_COMMENT_SAVED_OBJECT, - page: 1, - perPage: stats.total, - sortField: defaultSortField, - ...cloneDeep(options), - }); - } catch (error) { - this.log.error(`Error on GET all comments for ${JSON.stringify(id)}: ${error}`); - throw error; - } - } - - /** - * Default behavior is to retrieve all comments that adhere to a given filter (if one is included). - * to override this pass in the either the page or perPage options. - * - * @param includeSubCaseComments is a flag to indicate that sub case comments should be included as well, by default - * sub case comments are excluded. If the `filter` field is included in the options, it will override this behavior - */ - public async getAllCaseComments({ - client, - id, - options, - includeSubCaseComments = false, - }: FindCaseCommentsArgs): Promise> { - try { - const refs = this.asArray(id).map((caseID) => ({ type: CASE_SAVED_OBJECT, id: caseID })); - if (refs.length <= 0) { - return { - saved_objects: [], - total: 0, - per_page: options?.perPage ?? defaultPerPage, - page: options?.page ?? defaultPage, - }; - } - - let filter: KueryNode | undefined; - if (!includeSubCaseComments) { - // if other filters were passed in then combine them to filter out sub case comments - const associationFilter = nodeBuilder.is( - `${CASE_COMMENT_SAVED_OBJECT}.attributes.associationType`, - AssociationType.case - ); - - filter = - options?.filter != null - ? nodeBuilder.and([options?.filter, associationFilter]) - : associationFilter; - } - - this.log.debug(`Attempting to GET all comments for case caseID ${JSON.stringify(id)}`); - return this.getAllComments({ - client, - id, - options: { - hasReferenceOperator: 'OR', - hasReference: refs, - filter, - ...options, - }, - }); - } catch (error) { - this.log.error(`Error on GET all comments for case ${JSON.stringify(id)}: ${error}`); - throw error; - } - } - - public async getAllSubCaseComments({ - client, - id, - options, - }: FindSubCaseCommentsArgs): Promise> { - try { - const refs = this.asArray(id).map((caseID) => ({ type: SUB_CASE_SAVED_OBJECT, id: caseID })); - if (refs.length <= 0) { - return { - saved_objects: [], - total: 0, - per_page: options?.perPage ?? defaultPerPage, - page: options?.page ?? defaultPage, - }; - } - - this.log.debug(`Attempting to GET all comments for sub case caseID ${JSON.stringify(id)}`); - return this.getAllComments({ - client, - id, - options: { - hasReferenceOperator: 'OR', - hasReference: refs, - ...options, - }, - }); - } catch (error) { - this.log.error(`Error on GET all comments for sub case ${JSON.stringify(id)}: ${error}`); - throw error; - } - } - - public async getReporters({ client }: ClientArgs) { - try { - this.log.debug(`Attempting to GET all reporters`); - return await readReporters({ client }); - } catch (error) { - this.log.error(`Error on GET all reporters: ${error}`); - throw error; - } - } - public async getTags({ client }: ClientArgs) { - try { - this.log.debug(`Attempting to GET all cases`); - return await readTags({ client }); - } catch (error) { - this.log.error(`Error on GET cases: ${error}`); - throw error; - } - } - - public getUser({ request }: GetUserArgs) { - try { - this.log.debug(`Attempting to authenticate a user`); - if (this.authentication != null) { - const user = this.authentication.getCurrentUser(request); - if (!user) { - return { - username: null, - full_name: null, - email: null, - }; - } - return user; - } - return { - username: null, - full_name: null, - email: null, - }; - } catch (error) { - this.log.error(`Error on GET cases: ${error}`); - throw error; - } - } - public async postNewCase({ client, attributes }: PostCaseArgs) { - try { - this.log.debug(`Attempting to POST a new case`); - return await client.create(CASE_SAVED_OBJECT, { ...attributes }); - } catch (error) { - this.log.error(`Error on POST a new case: ${error}`); - throw error; - } - } - public async postNewComment({ client, attributes, references }: PostCommentArgs) { - try { - this.log.debug(`Attempting to POST a new comment`); - return await client.create(CASE_COMMENT_SAVED_OBJECT, attributes, { references }); - } catch (error) { - this.log.error(`Error on POST a new comment: ${error}`); - throw error; - } - } - public async patchCase({ client, caseId, updatedAttributes, version }: PatchCaseArgs) { - try { - this.log.debug(`Attempting to UPDATE case ${caseId}`); - return await client.update(CASE_SAVED_OBJECT, caseId, { ...updatedAttributes }, { version }); - } catch (error) { - this.log.error(`Error on UPDATE case ${caseId}: ${error}`); - throw error; - } - } - public async patchCases({ client, cases }: PatchCasesArgs) { - try { - this.log.debug(`Attempting to UPDATE case ${cases.map((c) => c.caseId).join(', ')}`); - return await client.bulkUpdate( - cases.map((c) => ({ - type: CASE_SAVED_OBJECT, - id: c.caseId, - attributes: c.updatedAttributes, - version: c.version, - })) - ); - } catch (error) { - this.log.error(`Error on UPDATE case ${cases.map((c) => c.caseId).join(', ')}: ${error}`); - throw error; - } - } - public async patchComment({ client, commentId, updatedAttributes, version }: UpdateCommentArgs) { - try { - this.log.debug(`Attempting to UPDATE comment ${commentId}`); - return await client.update( - CASE_COMMENT_SAVED_OBJECT, - commentId, - { - ...updatedAttributes, - }, - { version } - ); - } catch (error) { - this.log.error(`Error on UPDATE comment ${commentId}: ${error}`); - throw error; - } - } - public async patchComments({ client, comments }: PatchComments) { - try { - this.log.debug( - `Attempting to UPDATE comments ${comments.map((c) => c.commentId).join(', ')}` - ); - return await client.bulkUpdate( - comments.map((c) => ({ - type: CASE_COMMENT_SAVED_OBJECT, - id: c.commentId, - attributes: c.updatedAttributes, - version: c.version, - })) - ); - } catch (error) { - this.log.error( - `Error on UPDATE comments ${comments.map((c) => c.commentId).join(', ')}: ${error}` - ); - throw error; - } - } - public async patchSubCase({ client, subCaseId, updatedAttributes, version }: PatchSubCase) { - try { - this.log.debug(`Attempting to UPDATE sub case ${subCaseId}`); - return await client.update( - SUB_CASE_SAVED_OBJECT, - subCaseId, - { ...updatedAttributes }, - { version } - ); - } catch (error) { - this.log.error(`Error on UPDATE sub case ${subCaseId}: ${error}`); - throw error; - } - } - - public async patchSubCases({ client, subCases }: PatchSubCases) { - try { - this.log.debug( - `Attempting to UPDATE sub case ${subCases.map((c) => c.subCaseId).join(', ')}` - ); - return await client.bulkUpdate( - subCases.map((c) => ({ - type: SUB_CASE_SAVED_OBJECT, - id: c.subCaseId, - attributes: c.updatedAttributes, - version: c.version, - })) - ); - } catch (error) { - this.log.error( - `Error on UPDATE sub case ${subCases.map((c) => c.subCaseId).join(', ')}: ${error}` - ); - throw error; - } - } + soClient: SavedObjectsClientContract; } diff --git a/x-pack/plugins/cases/server/services/mocks.ts b/x-pack/plugins/cases/server/services/mocks.ts index 51eb0bbb1a7e43..77129e45348b1f 100644 --- a/x-pack/plugins/cases/server/services/mocks.ts +++ b/x-pack/plugins/cases/server/services/mocks.ts @@ -7,16 +7,16 @@ import { AlertServiceContract, - CaseConfigureServiceSetup, - CaseServiceSetup, - CaseUserActionServiceSetup, - ConnectorMappingsServiceSetup, + CaseConfigureService, + CaseService, + CaseUserActionService, + ConnectorMappingsService, } from '.'; -export type CaseServiceMock = jest.Mocked; -export type CaseConfigureServiceMock = jest.Mocked; -export type ConnectorMappingsServiceMock = jest.Mocked; -export type CaseUserActionServiceMock = jest.Mocked; +export type CaseServiceMock = jest.Mocked; +export type CaseConfigureServiceMock = jest.Mocked; +export type ConnectorMappingsServiceMock = jest.Mocked; +export type CaseUserActionServiceMock = jest.Mocked; export type AlertServiceMock = jest.Mocked; export const createCaseServiceMock = (): CaseServiceMock => ({ diff --git a/x-pack/plugins/cases/server/services/user_actions/index.ts b/x-pack/plugins/cases/server/services/user_actions/index.ts index 192ab9341e4ee8..0b65657092469b 100644 --- a/x-pack/plugins/cases/server/services/user_actions/index.ts +++ b/x-pack/plugins/cases/server/services/user_actions/index.ts @@ -5,12 +5,7 @@ * 2.0. */ -import { - SavedObjectsFindResponse, - Logger, - SavedObjectsBulkResponse, - SavedObjectReference, -} from 'kibana/server'; +import { Logger, SavedObjectReference } from 'kibana/server'; import { CaseUserActionAttributes } from '../../../common/api'; import { @@ -34,52 +29,44 @@ interface PostCaseUserActionArgs extends ClientArgs { actions: UserActionItem[]; } -export interface CaseUserActionServiceSetup { - getUserActions( - args: GetCaseUserActionArgs - ): Promise>; - postUserActions( - args: PostCaseUserActionArgs - ): Promise>; -} - export class CaseUserActionService { constructor(private readonly log: Logger) {} - public setup = async (): Promise => ({ - getUserActions: async ({ client, caseId, subCaseId }: GetCaseUserActionArgs) => { - try { - const id = subCaseId ?? caseId; - const type = subCaseId ? SUB_CASE_SAVED_OBJECT : CASE_SAVED_OBJECT; - const caseUserActionInfo = await client.find({ - type: CASE_USER_ACTION_SAVED_OBJECT, - fields: [], - hasReference: { type, id }, - page: 1, - perPage: 1, - }); - return await client.find({ - type: CASE_USER_ACTION_SAVED_OBJECT, - hasReference: { type, id }, - page: 1, - perPage: caseUserActionInfo.total, - sortField: 'action_at', - sortOrder: 'asc', - }); - } catch (error) { - this.log.error(`Error on GET case user action case id: ${caseId}: ${error}`); - throw error; - } - }, - postUserActions: async ({ client, actions }: PostCaseUserActionArgs) => { - try { - this.log.debug(`Attempting to POST a new case user action`); - return await client.bulkCreate( - actions.map((action) => ({ type: CASE_USER_ACTION_SAVED_OBJECT, ...action })) - ); - } catch (error) { - this.log.error(`Error on POST a new case user action: ${error}`); - throw error; - } - }, - }); + + public async getAll({ soClient, caseId, subCaseId }: GetCaseUserActionArgs) { + try { + const id = subCaseId ?? caseId; + const type = subCaseId ? SUB_CASE_SAVED_OBJECT : CASE_SAVED_OBJECT; + const caseUserActionInfo = await soClient.find({ + type: CASE_USER_ACTION_SAVED_OBJECT, + fields: [], + hasReference: { type, id }, + page: 1, + perPage: 1, + }); + + return await soClient.find({ + type: CASE_USER_ACTION_SAVED_OBJECT, + hasReference: { type, id }, + page: 1, + perPage: caseUserActionInfo.total, + sortField: 'action_at', + sortOrder: 'asc', + }); + } catch (error) { + this.log.error(`Error on GET case user action case id: ${caseId}: ${error}`); + throw error; + } + } + + public async bulkCreate({ soClient, actions }: PostCaseUserActionArgs) { + try { + this.log.debug(`Attempting to POST a new case user action`); + return await soClient.bulkCreate( + actions.map((action) => ({ type: CASE_USER_ACTION_SAVED_OBJECT, ...action })) + ); + } catch (error) { + this.log.error(`Error on POST a new case user action: ${error}`); + throw error; + } + } } From 36781db1d940946b49aa82235665afaccac765b8 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Tue, 6 Apr 2021 16:47:16 -0400 Subject: [PATCH 044/113] [Cases] Authorization and Client Audit Logger (#95477) * Starting audit logger * Finishing auth audit logger * Fixing tests and types * Adding audit event creator * Renaming class to scope * Adding audit logger messages to create and find * Adding comments and fixing import issue * Fixing type errors * Fixing tests and adding username to message * Addressing PR feedback * Removing unneccessary log and generating id * Fixing module issue and remove expect.anything --- .../server/authorization/audit_logger.ts | 131 ++++++++++++++++++ .../server/authorization/authorization.ts | 87 +++++------- .../cases/server/authorization/index.ts | 85 ++++++++++++ .../cases/server/authorization/types.ts | 42 +++++- .../cases/server/client/cases/client.ts | 3 + .../cases/server/client/cases/create.test.ts | 23 +-- .../cases/server/client/cases/create.ts | 30 +++- .../plugins/cases/server/client/cases/find.ts | 32 ++++- .../cases/server/client/cases/update.test.ts | 4 + x-pack/plugins/cases/server/client/factory.ts | 7 +- x-pack/plugins/cases/server/client/mocks.ts | 15 +- x-pack/plugins/cases/server/client/types.ts | 2 + x-pack/plugins/cases/server/common/utils.ts | 48 +++++++ x-pack/plugins/cases/server/config.ts | 2 - .../server/connectors/case/index.test.ts | 2 - x-pack/plugins/cases/server/plugin.ts | 5 - .../routes/api/__fixtures__/route_contexts.ts | 20 +-- .../api/cases/configure/get_configure.test.ts | 7 +- .../cases/configure/patch_configure.test.ts | 5 +- .../cases/configure/post_configure.test.ts | 5 +- .../routes/api/cases/patch_cases.test.ts | 3 + .../server/routes/api/cases/post_case.test.ts | 12 +- .../api/cases/status/get_status.test.ts | 13 +- .../cases/server/routes/api/utils.test.ts | 10 ++ .../cases/server/services/cases/index.ts | 7 +- .../actions/__snapshots__/cases.test.ts.snap | 24 ++-- .../feature_privilege_builder/cases.test.ts | 56 ++++---- .../feature_privilege_builder/cases.ts | 6 +- x-pack/plugins/security/server/plugin.test.ts | 6 + .../cases/components/all_cases/index.test.tsx | 1 + .../public/cases/containers/mock.ts | 2 + .../public/cases/containers/types.ts | 1 + .../case_api_integration/common/config.ts | 1 - 33 files changed, 533 insertions(+), 164 deletions(-) create mode 100644 x-pack/plugins/cases/server/authorization/audit_logger.ts create mode 100644 x-pack/plugins/cases/server/authorization/index.ts diff --git a/x-pack/plugins/cases/server/authorization/audit_logger.ts b/x-pack/plugins/cases/server/authorization/audit_logger.ts new file mode 100644 index 00000000000000..3c890a2c7ad5be --- /dev/null +++ b/x-pack/plugins/cases/server/authorization/audit_logger.ts @@ -0,0 +1,131 @@ +/* + * 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 { OperationDetails } from '.'; +import { AuditLogger, EventCategory, EventOutcome } from '../../../security/server'; + +enum AuthorizationResult { + Unauthorized = 'Unauthorized', + Authorized = 'Authorized', +} + +export class AuthorizationAuditLogger { + private readonly auditLogger?: AuditLogger; + + constructor(logger: AuditLogger | undefined) { + this.auditLogger = logger; + } + + private createMessage({ + result, + owner, + operation, + }: { + result: AuthorizationResult; + owner?: string; + operation: OperationDetails; + }): string { + const ownerMsg = owner == null ? 'of any owner' : `with "${owner}" as the owner`; + /** + * This will take the form: + * `Unauthorized to create case with "securitySolution" as the owner` + * `Unauthorized to find cases of any owner`. + */ + return `${result} to ${operation.verbs.present} ${operation.docType} ${ownerMsg}`; + } + + private logSuccessEvent({ + message, + operation, + username, + }: { + message: string; + operation: OperationDetails; + username?: string; + }) { + this.auditLogger?.log({ + message: `${username ?? 'unknown user'} ${message}`, + event: { + action: operation.action, + category: EventCategory.DATABASE, + type: operation.type, + outcome: EventOutcome.SUCCESS, + }, + ...(username != null && { + user: { + name: username, + }, + }), + }); + } + + public failure({ + username, + owner, + operation, + }: { + username?: string; + owner?: string; + operation: OperationDetails; + }): string { + const message = this.createMessage({ + result: AuthorizationResult.Unauthorized, + owner, + operation, + }); + this.auditLogger?.log({ + message: `${username ?? 'unknown user'} ${message}`, + event: { + action: operation.action, + category: EventCategory.DATABASE, + type: operation.type, + outcome: EventOutcome.FAILURE, + }, + // add the user information if we have it + ...(username != null && { + user: { + name: username, + }, + }), + }); + return message; + } + + public success({ + username, + operation, + owner, + }: { + username: string; + owner: string; + operation: OperationDetails; + }): string { + const message = this.createMessage({ + result: AuthorizationResult.Authorized, + owner, + operation, + }); + this.logSuccessEvent({ message, operation, username }); + return message; + } + + public bulkSuccess({ + username, + operation, + owners, + }: { + username?: string; + owners: string[]; + operation: OperationDetails; + }): string { + const message = `${AuthorizationResult.Authorized} to ${operation.verbs.present} ${ + operation.docType + } of owner: ${owners.join(', ')}`; + this.logSuccessEvent({ message, operation, username }); + return message; + } +} diff --git a/x-pack/plugins/cases/server/authorization/authorization.ts b/x-pack/plugins/cases/server/authorization/authorization.ts index ab6f9c0f6fef23..5a1d6af0f4a061 100644 --- a/x-pack/plugins/cases/server/authorization/authorization.ts +++ b/x-pack/plugins/cases/server/authorization/authorization.ts @@ -7,11 +7,11 @@ import { KibanaRequest } from 'kibana/server'; import Boom from '@hapi/boom'; -import { KueryNode } from '../../../../../src/plugins/data/server'; import { SecurityPluginStart } from '../../../security/server'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; -import { GetSpaceFn, ReadOperations, WriteOperations } from './types'; +import { AuthorizationFilter, GetSpaceFn } from './types'; import { getOwnersFilter } from './utils'; +import { AuthorizationAuditLogger, OperationDetails, Operations } from '.'; /** * This class handles ensuring that the user making a request has the correct permissions @@ -21,25 +21,23 @@ export class Authorization { private readonly request: KibanaRequest; private readonly securityAuth: SecurityPluginStart['authz'] | undefined; private readonly featureCaseOwners: Set; - private readonly isAuthEnabled: boolean; - // TODO: create this - // private readonly auditLogger: AuthorizationAuditLogger; + private readonly auditLogger: AuthorizationAuditLogger; private constructor({ request, securityAuth, caseOwners, - isAuthEnabled, + auditLogger, }: { request: KibanaRequest; securityAuth?: SecurityPluginStart['authz']; caseOwners: Set; - isAuthEnabled: boolean; + auditLogger: AuthorizationAuditLogger; }) { this.request = request; this.securityAuth = securityAuth; this.featureCaseOwners = caseOwners; - this.isAuthEnabled = isAuthEnabled; + this.auditLogger = auditLogger; } /** @@ -50,13 +48,13 @@ export class Authorization { securityAuth, getSpace, features, - isAuthEnabled, + auditLogger, }: { request: KibanaRequest; securityAuth?: SecurityPluginStart['authz']; getSpace: GetSpaceFn; features: FeaturesPluginStart; - isAuthEnabled: boolean; + auditLogger: AuthorizationAuditLogger; }): Promise { // Since we need to do async operations, this static method handles that before creating the Auth class let caseOwners: Set; @@ -74,34 +72,26 @@ export class Authorization { caseOwners = new Set(); } - return new Authorization({ request, securityAuth, caseOwners, isAuthEnabled }); + return new Authorization({ request, securityAuth, caseOwners, auditLogger }); } private shouldCheckAuthorization(): boolean { return this.securityAuth?.mode?.useRbacForRequest(this.request) ?? false; } - public async ensureAuthorized(owner: string, operation: ReadOperations | WriteOperations) { - // TODO: remove - if (!this.isAuthEnabled) { - return; - } - + public async ensureAuthorized(owner: string, operation: OperationDetails) { const { securityAuth } = this; const isOwnerAvailable = this.featureCaseOwners.has(owner); - // TODO: throw if the request is not authorized if (securityAuth && this.shouldCheckAuthorization()) { - // TODO: implement ensure logic - const requiredPrivileges: string[] = [securityAuth.actions.cases.get(owner, operation)]; + const requiredPrivileges: string[] = [securityAuth.actions.cases.get(owner, operation.name)]; const checkPrivileges = securityAuth.checkPrivilegesDynamicallyWithRequest(this.request); - const { hasAllRequested, username, privileges } = await checkPrivileges({ + const { hasAllRequested, username } = await checkPrivileges({ kibana: requiredPrivileges, }); if (!isOwnerAvailable) { - // TODO: throw if any of the owner are not available /** * Under most circumstances this would have been caught by `checkPrivileges` as * a user can't have Privileges to an unknown owner, but super users @@ -109,67 +99,54 @@ export class Authorization { * as Privileged. * This check will ensure we don't accidentally let these through */ - // TODO: audit log using `username` - throw Boom.forbidden('User does not have permissions for this owner'); + throw Boom.forbidden(this.auditLogger.failure({ username, owner, operation })); } if (hasAllRequested) { - // TODO: user authorized. log success + this.auditLogger.success({ username, operation, owner }); } else { - const authorizedPrivileges = privileges.kibana.reduce((acc, privilege) => { - if (privilege.authorized) { - return [...acc, privilege.privilege]; - } - return acc; - }, []); - - const unauthorizedPrivilages = requiredPrivileges.filter( - (privilege) => !authorizedPrivileges.includes(privilege) - ); - - // TODO: audit log - // TODO: User unauthorized. throw an error. authorizedPrivileges & unauthorizedPrivilages are needed for logging. - throw Boom.forbidden('Not authorized for this owner'); + throw Boom.forbidden(this.auditLogger.failure({ owner, operation, username })); } } else if (!isOwnerAvailable) { - // TODO: throw an error - throw Boom.forbidden('Security is disabled but no owner was found'); + throw Boom.forbidden(this.auditLogger.failure({ owner, operation })); } // else security is disabled so let the operation proceed } - public async getFindAuthorizationFilter( - savedObjectType: string - ): Promise<{ - filter?: KueryNode; - ensureSavedObjectIsAuthorized: (owner: string) => void; - }> { + public async getFindAuthorizationFilter(savedObjectType: string): Promise { const { securityAuth } = this; + const operation = Operations.findCases; if (securityAuth && this.shouldCheckAuthorization()) { - const { authorizedOwners } = await this.getAuthorizedOwners([ReadOperations.Find]); + const { username, authorizedOwners } = await this.getAuthorizedOwners([operation]); if (!authorizedOwners.length) { - // TODO: Better error message, log error - throw Boom.forbidden('Not authorized for this owner'); + throw Boom.forbidden(this.auditLogger.failure({ username, operation })); } return { filter: getOwnersFilter(savedObjectType, authorizedOwners), ensureSavedObjectIsAuthorized: (owner: string) => { if (!authorizedOwners.includes(owner)) { - // TODO: log error - throw Boom.forbidden('Not authorized for this owner'); + throw Boom.forbidden(this.auditLogger.failure({ username, operation, owner })); + } + }, + logSuccessfulAuthorization: () => { + if (authorizedOwners.length) { + this.auditLogger.bulkSuccess({ username, owners: authorizedOwners, operation }); } }, }; } - return { ensureSavedObjectIsAuthorized: (owner: string) => {} }; + return { + ensureSavedObjectIsAuthorized: (owner: string) => {}, + logSuccessfulAuthorization: () => {}, + }; } private async getAuthorizedOwners( - operations: Array + operations: OperationDetails[] ): Promise<{ username?: string; hasAllRequested: boolean; @@ -182,7 +159,7 @@ export class Authorization { for (const owner of featureCaseOwners) { for (const operation of operations) { - requiredPrivileges.set(securityAuth.actions.cases.get(owner, operation), [owner]); + requiredPrivileges.set(securityAuth.actions.cases.get(owner, operation.name), [owner]); } } diff --git a/x-pack/plugins/cases/server/authorization/index.ts b/x-pack/plugins/cases/server/authorization/index.ts new file mode 100644 index 00000000000000..3203398ff51a55 --- /dev/null +++ b/x-pack/plugins/cases/server/authorization/index.ts @@ -0,0 +1,85 @@ +/* + * 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 { EventType } from '../../../security/server'; +import { CASE_SAVED_OBJECT } from '../../common/constants'; +import { Verbs, ReadOperations, WriteOperations, OperationDetails } from './types'; + +export * from './authorization'; +export * from './audit_logger'; +export * from './types'; + +const createVerbs: Verbs = { + present: 'create', + progressive: 'creating', + past: 'created', +}; + +const accessVerbs: Verbs = { + present: 'access', + progressive: 'accessing', + past: 'accessed', +}; + +const updateVerbs: Verbs = { + present: 'update', + progressive: 'updating', + past: 'updated', +}; + +const deleteVerbs: Verbs = { + present: 'delete', + progressive: 'deleting', + past: 'deleted', +}; + +/** + * Definition of all APIs within the cases backend. + */ +export const Operations: Record = { + // case operations + [WriteOperations.CreateCase]: { + type: EventType.CREATION, + name: WriteOperations.CreateCase, + action: 'create-case', + verbs: createVerbs, + docType: 'case', + savedObjectType: CASE_SAVED_OBJECT, + }, + [WriteOperations.DeleteCase]: { + type: EventType.DELETION, + name: WriteOperations.DeleteCase, + action: 'delete-case', + verbs: deleteVerbs, + docType: 'case', + savedObjectType: CASE_SAVED_OBJECT, + }, + [WriteOperations.UpdateCase]: { + type: EventType.CHANGE, + name: WriteOperations.UpdateCase, + action: 'update-case', + verbs: updateVerbs, + docType: 'case', + savedObjectType: CASE_SAVED_OBJECT, + }, + [ReadOperations.GetCase]: { + type: EventType.ACCESS, + name: ReadOperations.GetCase, + action: 'get-case', + verbs: accessVerbs, + docType: 'case', + savedObjectType: CASE_SAVED_OBJECT, + }, + [ReadOperations.FindCases]: { + type: EventType.ACCESS, + name: ReadOperations.FindCases, + action: 'find-cases', + verbs: accessVerbs, + docType: 'cases', + savedObjectType: CASE_SAVED_OBJECT, + }, +}; diff --git a/x-pack/plugins/cases/server/authorization/types.ts b/x-pack/plugins/cases/server/authorization/types.ts index 07249d858c1872..91b7c0f1180d93 100644 --- a/x-pack/plugins/cases/server/authorization/types.ts +++ b/x-pack/plugins/cases/server/authorization/types.ts @@ -6,20 +6,52 @@ */ import { KibanaRequest } from 'kibana/server'; +import { KueryNode } from 'src/plugins/data/common'; +import { EventType } from '../../../security/server'; import { Space } from '../../../spaces/server'; +/** + * The tenses for describing the action performed by a API route + */ +export interface Verbs { + present: string; + progressive: string; + past: string; +} + export type GetSpaceFn = (request: KibanaRequest) => 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? export enum ReadOperations { - Get = 'get', - Find = 'find', + GetCase = 'getCase', + FindCases = 'findCases', } // TODO: comments export enum WriteOperations { - Create = 'create', - Delete = 'delete', - Update = 'update', + CreateCase = 'createCase', + DeleteCase = 'deleteCase', + UpdateCase = 'updateCase', +} + +/** + * Defines the structure for a case API route. + */ +export interface OperationDetails { + type: EventType; + name: ReadOperations | WriteOperations; + action: string; + verbs: Verbs; + docType: string; + savedObjectType: string; +} + +/** + * Defines the helper methods and necessary information for authorizing the find API's request. + */ +export interface AuthorizationFilter { + filter?: KueryNode; + ensureSavedObjectIsAuthorized: (owner: string) => void; + logSuccessfulAuthorization: () => void; } diff --git a/x-pack/plugins/cases/server/client/cases/client.ts b/x-pack/plugins/cases/server/client/cases/client.ts index 9c9bf1fa7641d2..a77bfa01e6ec8d 100644 --- a/x-pack/plugins/cases/server/client/cases/client.ts +++ b/x-pack/plugins/cases/server/client/cases/client.ts @@ -57,6 +57,7 @@ export const createCasesSubClient = ( userActionService, logger, authorization, + auditLogger, } = args; const casesSubClient: CasesSubClient = { @@ -70,6 +71,7 @@ export const createCasesSubClient = ( theCase, logger, auth: authorization, + auditLogger, }), find: (options: CasesFindRequest) => find({ @@ -78,6 +80,7 @@ export const createCasesSubClient = ( logger, auth: authorization, options, + auditLogger, }), get: (params: CaseGet) => get({ diff --git a/x-pack/plugins/cases/server/client/cases/create.test.ts b/x-pack/plugins/cases/server/client/cases/create.test.ts index bd9f4da2b0131c..1542b025ab96cd 100644 --- a/x-pack/plugins/cases/server/client/cases/create.test.ts +++ b/x-pack/plugins/cases/server/client/cases/create.test.ts @@ -26,6 +26,11 @@ describe('create', () => { const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; spyOnDate.mockImplementation(() => ({ toISOString: jest.fn().mockReturnValue('2019-11-25T21:54:48.952Z'), + // when we create a case we generate an ID that is used for the saved object. Internally the ID generation code + // calls Date.getTime so we need it to return something even though the inject saved object client is going to + // override it with a different ID anyway + // Otherwise we'll get an error when the function is called + getTime: jest.fn().mockReturnValue(1), })); }); @@ -45,7 +50,7 @@ describe('create', () => { settings: { syncAlerts: true, }, - owner: 'awesome', + owner: 'securitySolution', }; const savedObjectsClient = createMockSavedObjectsRepository({ @@ -57,7 +62,6 @@ describe('create', () => { expect(res).toMatchInlineSnapshot(` Object { - "owner": "awesome", "closed_at": null, "closed_by": null, "comments": Array [], @@ -80,6 +84,7 @@ describe('create', () => { "description": "This is a brand new case of a bad meanie defacing data", "external_service": null, "id": "mock-it", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, @@ -121,7 +126,7 @@ describe('create', () => { "connector", "settings", ], - "new_value": "{\\"type\\":\\"individual\\",\\"description\\":\\"This is a brand new case of a bad meanie defacing data\\",\\"title\\":\\"Super Bad Security Issue\\",\\"tags\\":[\\"defacement\\"],\\"connector\\":{\\"id\\":\\"123\\",\\"name\\":\\"Jira\\",\\"type\\":\\".jira\\",\\"fields\\":{\\"issueType\\":\\"Task\\",\\"priority\\":\\"High\\",\\"parent\\":null}},\\"settings\\":{\\"syncAlerts\\":true},\\"owner\\":\\"awesome\\"}", + "new_value": "{\\"type\\":\\"individual\\",\\"description\\":\\"This is a brand new case of a bad meanie defacing data\\",\\"title\\":\\"Super Bad Security Issue\\",\\"tags\\":[\\"defacement\\"],\\"connector\\":{\\"id\\":\\"123\\",\\"name\\":\\"Jira\\",\\"type\\":\\".jira\\",\\"fields\\":{\\"issueType\\":\\"Task\\",\\"priority\\":\\"High\\",\\"parent\\":null}},\\"settings\\":{\\"syncAlerts\\":true},\\"owner\\":\\"securitySolution\\"}", "old_value": null, }, "references": Array [ @@ -151,7 +156,7 @@ describe('create', () => { settings: { syncAlerts: true, }, - owner: 'awesome', + owner: 'securitySolution', }; const savedObjectsClient = createMockSavedObjectsRepository({ @@ -162,7 +167,6 @@ describe('create', () => { expect(res).toMatchInlineSnapshot(` Object { - "owner": "awesome", "closed_at": null, "closed_by": null, "comments": Array [], @@ -181,6 +185,7 @@ describe('create', () => { "description": "This is a brand new case of a bad meanie defacing data", "external_service": null, "id": "mock-it", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, @@ -216,7 +221,7 @@ describe('create', () => { settings: { syncAlerts: true, }, - owner: 'awesome', + owner: 'securitySolution', }; const savedObjectsClient = createMockSavedObjectsRepository({ @@ -230,7 +235,6 @@ describe('create', () => { expect(res).toMatchInlineSnapshot(` Object { - "owner": "awesome", "closed_at": null, "closed_by": null, "comments": Array [], @@ -249,6 +253,7 @@ describe('create', () => { "description": "This is a brand new case of a bad meanie defacing data", "external_service": null, "id": "mock-it", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, @@ -429,7 +434,7 @@ describe('create', () => { settings: { syncAlerts: true, }, - owner: 'awesome', + owner: 'securitySolution', }; const savedObjectsClient = createMockSavedObjectsRepository({ @@ -458,7 +463,7 @@ describe('create', () => { settings: { syncAlerts: true, }, - owner: 'awesome', + owner: 'securitySolution', }; const savedObjectsClient = createMockSavedObjectsRepository({ caseSavedObject: mockCases, diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index 935ca6d3199d2f..61f36050758502 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -9,9 +9,14 @@ 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 } from 'src/core/server'; + +import { + SavedObjectsClientContract, + Logger, + SavedObjectsUtils, +} from '../../../../../../src/core/server'; + import { flattenCaseSavedObject, transformNewCase } from '../../routes/api/utils'; import { @@ -33,8 +38,10 @@ import { import { CaseConfigureService, CaseService, CaseUserActionService } from '../../services'; import { createCaseError } from '../../common/error'; import { Authorization } from '../../authorization/authorization'; -import { WriteOperations } from '../../authorization/types'; +import { Operations } from '../../authorization'; +import { AuditLogger, EventOutcome } from '../../../../security/server'; import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; +import { createAuditMsg } from '../../common'; interface CreateCaseArgs { caseConfigureService: CaseConfigureService; @@ -45,6 +52,7 @@ interface CreateCaseArgs { theCase: CasePostRequest; logger: Logger; auth: PublicMethodsOf; + auditLogger?: AuditLogger; } /** @@ -59,6 +67,7 @@ export const create = async ({ theCase, logger, auth, + auditLogger, }: CreateCaseArgs): Promise => { // default to an individual case if the type is not defined. const { type = CaseType.individual, ...nonTypeCaseFields } = theCase; @@ -79,13 +88,23 @@ export const create = async ({ ); try { + const savedObjectID = SavedObjectsUtils.generateId(); try { - await auth.ensureAuthorized(query.owner, WriteOperations.Create); + await auth.ensureAuthorized(query.owner, Operations.createCase); } catch (error) { - // TODO: log error using audit logger + auditLogger?.log(createAuditMsg({ operation: Operations.createCase, error, savedObjectID })); throw error; } + // log that we're attempting to create a case + auditLogger?.log( + createAuditMsg({ + operation: Operations.createCase, + outcome: EventOutcome.UNKNOWN, + savedObjectID, + }) + ); + // eslint-disable-next-line @typescript-eslint/naming-convention const { username, full_name, email } = user; const createdDate = new Date().toISOString(); @@ -102,6 +121,7 @@ export const create = async ({ email, connector: transformCaseConnectorToEsConnector(query.connector ?? caseConfigureConnector), }), + id: savedObjectID, }); await userActionService.bulkCreate({ diff --git a/x-pack/plugins/cases/server/client/cases/find.ts b/x-pack/plugins/cases/server/client/cases/find.ts index 33545a39258893..aebecb821b4498 100644 --- a/x-pack/plugins/cases/server/client/cases/find.ts +++ b/x-pack/plugins/cases/server/client/cases/find.ts @@ -29,6 +29,9 @@ import { constructQueryOptions } from '../../routes/api/cases/helpers'; import { transformCases } from '../../routes/api/utils'; import { Authorization } from '../../authorization/authorization'; import { includeFieldsRequiredForAuthentication } from '../../authorization/utils'; +import { AuthorizationFilter, Operations } from '../../authorization'; +import { AuditLogger } from '../../../../security/server'; +import { createAuditMsg } from '../../common'; interface FindParams { savedObjectsClient: SavedObjectsClientContract; @@ -36,6 +39,7 @@ interface FindParams { logger: Logger; auth: PublicMethodsOf; options: CasesFindRequest; + auditLogger?: AuditLogger; } /** @@ -46,6 +50,7 @@ export const find = async ({ caseService, logger, auth, + auditLogger, options, }: FindParams): Promise => { try { @@ -54,11 +59,19 @@ export const find = async ({ fold(throwErrors(Boom.badRequest), identity) ); - // TODO: Maybe surround it with try/catch + let authFindHelpers: AuthorizationFilter; + try { + authFindHelpers = await auth.getFindAuthorizationFilter(CASE_SAVED_OBJECT); + } catch (error) { + auditLogger?.log(createAuditMsg({ operation: Operations.findCases, error })); + throw error; + } + const { filter: authorizationFilter, ensureSavedObjectIsAuthorized, - } = await auth.getFindAuthorizationFilter(CASE_SAVED_OBJECT); + logSuccessfulAuthorization, + } = authFindHelpers; const queryArgs = { tags: queryParams.tags, @@ -89,7 +102,18 @@ export const find = async ({ }); for (const theCase of cases.casesMap.values()) { - ensureSavedObjectIsAuthorized(theCase.owner); + try { + ensureSavedObjectIsAuthorized(theCase.owner); + // log each of the found cases + auditLogger?.log( + createAuditMsg({ operation: Operations.findCases, savedObjectID: theCase.id }) + ); + } catch (error) { + auditLogger?.log( + createAuditMsg({ operation: Operations.findCases, error, savedObjectID: theCase.id }) + ); + throw error; + } } // TODO: Make sure we do not leak information when authorization is on @@ -104,6 +128,8 @@ export const find = async ({ }), ]); + logSuccessfulAuthorization(); + return CasesFindResponseRt.encode( transformCases({ casesMap: cases.casesMap, diff --git a/x-pack/plugins/cases/server/client/cases/update.test.ts b/x-pack/plugins/cases/server/client/cases/update.test.ts index 79c3b2838c3b20..1269545bf485c3 100644 --- a/x-pack/plugins/cases/server/client/cases/update.test.ts +++ b/x-pack/plugins/cases/server/client/cases/update.test.ts @@ -68,6 +68,7 @@ describe('update', () => { "description": "This is a brand new case of a bad meanie defacing data", "external_service": null, "id": "mock-id-1", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, @@ -164,6 +165,7 @@ describe('update', () => { "description": "This is a brand new case of a bad meanie defacing data", "external_service": null, "id": "mock-id-1", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, @@ -232,6 +234,7 @@ describe('update', () => { "description": "Oh no, a bad meanie going LOLBins all over the place!", "external_service": null, "id": "mock-id-4", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, @@ -372,6 +375,7 @@ describe('update', () => { "description": "Oh no, a bad meanie going LOLBins all over the place!", "external_service": null, "id": "mock-id-3", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, diff --git a/x-pack/plugins/cases/server/client/factory.ts b/x-pack/plugins/cases/server/client/factory.ts index d622861ac65b40..87a2b9583dac07 100644 --- a/x-pack/plugins/cases/server/client/factory.ts +++ b/x-pack/plugins/cases/server/client/factory.ts @@ -24,6 +24,7 @@ import { AttachmentService, } from '../services'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; +import { AuthorizationAuditLogger } from '../authorization'; import { CasesClient, createCasesClient } from '.'; interface CasesClientFactoryArgs { @@ -37,7 +38,6 @@ interface CasesClientFactoryArgs { securityPluginStart?: SecurityPluginStart; getSpace: GetSpaceFn; featuresPluginStart: FeaturesPluginStart; - isAuthEnabled: boolean; } /** @@ -85,12 +85,14 @@ export class CasesClientFactory { ); } + const auditLogger = this.options.securityPluginSetup?.audit.asScoped(request); + const auth = await Authorization.create({ request, securityAuth: this.options.securityPluginStart?.authz, getSpace: this.options.getSpace, features: this.options.featuresPluginStart, - isAuthEnabled: this.options.isAuthEnabled, + auditLogger: new AuthorizationAuditLogger(auditLogger), }); const user = this.options.caseService.getUser({ request }); @@ -109,6 +111,7 @@ export class CasesClientFactory { attachmentService: this.options.attachmentService, logger: this.logger, authorization: auth, + auditLogger, }); } } diff --git a/x-pack/plugins/cases/server/client/mocks.ts b/x-pack/plugins/cases/server/client/mocks.ts index 174904c1f66be6..cf964e5e53c4fe 100644 --- a/x-pack/plugins/cases/server/client/mocks.ts +++ b/x-pack/plugins/cases/server/client/mocks.ts @@ -22,8 +22,8 @@ import { import { CasesClient } from './types'; import { authenticationMock } from '../routes/api/__fixtures__'; import { featuresPluginMock } from '../../../features/server/mocks'; -import { securityMock } from '../../../security/server/mocks'; import { CasesClientFactory } from './factory'; +import { KibanaFeature } from '../../../features/common'; export type CasesClientPluginContractMock = jest.Mocked; export const createExternalCasesClientMock = (): CasesClientPluginContractMock => ({ @@ -83,6 +83,13 @@ export const createCasesClientWithMockSavedObjectsClient = async ({ const savedObjectsService = savedObjectsServiceMock.createStartContract(); savedObjectsService.getScopedClient.mockReturnValue(savedObjectsClient); + // create a fake feature + const featureStart = featuresPluginMock.createStart(); + featureStart.getKibanaFeatures.mockReturnValue([ + // all the authorization class cares about is the `cases` field in the kibana feature so just cast it to that + ({ cases: ['securitySolution'] } as unknown) as KibanaFeature, + ]); + const factory = new CasesClientFactory(log); factory.initialize({ alertsService, @@ -90,11 +97,9 @@ export const createCasesClientWithMockSavedObjectsClient = async ({ caseService, connectorMappingsService, userActionService, - featuresPluginStart: featuresPluginMock.createStart(), + featuresPluginStart: featureStart, getSpace: async (req: KibanaRequest) => undefined, - isAuthEnabled: false, - securityPluginSetup: securityMock.createSetup(), - securityPluginStart: securityMock.createStart(), + // intentionally not passing the security plugin so that security will be disabled }); // create a single reference to the caseClient so we can mock its methods diff --git a/x-pack/plugins/cases/server/client/types.ts b/x-pack/plugins/cases/server/client/types.ts index 0592dd321819de..7d50fdbb533826 100644 --- a/x-pack/plugins/cases/server/client/types.ts +++ b/x-pack/plugins/cases/server/client/types.ts @@ -8,6 +8,7 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import { ElasticsearchClient, SavedObjectsClientContract, Logger } from 'kibana/server'; import { User } from '../../common/api'; +import { AuditLogger } from '../../../security/server'; import { Authorization } from '../authorization/authorization'; import { AlertServiceContract, @@ -30,4 +31,5 @@ export interface CasesClientArgs { readonly attachmentService: AttachmentService; readonly logger: Logger; readonly authorization: PublicMethodsOf; + readonly auditLogger?: AuditLogger; } diff --git a/x-pack/plugins/cases/server/common/utils.ts b/x-pack/plugins/cases/server/common/utils.ts index 36f5dc9cbb00a8..af638c39d66093 100644 --- a/x-pack/plugins/cases/server/common/utils.ts +++ b/x-pack/plugins/cases/server/common/utils.ts @@ -6,6 +6,7 @@ */ import { SavedObjectsFindResult, SavedObjectsFindResponse } from 'kibana/server'; +import { AuditEvent, EventCategory, EventOutcome } from '../../../security/server'; import { CaseStatuses, CommentAttributes, @@ -13,6 +14,7 @@ import { CommentType, User, } from '../../common/api'; +import { OperationDetails } from '../authorization'; import { UpdateAlertRequest } from '../client/alerts/client'; import { getAlertInfoFromComments } from '../routes/api/utils'; @@ -97,3 +99,49 @@ export const countAlertsForID = ({ }): number | undefined => { return groupTotalAlertsByID({ comments }).get(id); }; + +/** + * Creates an AuditEvent describing the state of a request. + */ +export function createAuditMsg({ + operation, + outcome, + error, + savedObjectID, +}: { + operation: OperationDetails; + savedObjectID?: string; + outcome?: EventOutcome; + error?: Error; +}): AuditEvent { + const doc = + savedObjectID != null + ? `${operation.savedObjectType} [id=${savedObjectID}]` + : `a ${operation.docType}`; + const message = error + ? `Failed attempt to ${operation.verbs.present} ${doc}` + : outcome === EventOutcome.UNKNOWN + ? `User is ${operation.verbs.progressive} ${doc}` + : `User has ${operation.verbs.past} ${doc}`; + + return { + message, + event: { + action: operation.action, + category: EventCategory.DATABASE, + type: operation.type, + outcome: outcome ?? (error ? EventOutcome.FAILURE : EventOutcome.SUCCESS), + }, + ...(savedObjectID != null && { + kibana: { + saved_object: { type: operation.savedObjectType, id: savedObjectID }, + }, + }), + ...(error != null && { + error: { + code: error.name, + message: error.message, + }, + }), + }; +} diff --git a/x-pack/plugins/cases/server/config.ts b/x-pack/plugins/cases/server/config.ts index c4dca0f9ff9559..7679a5a389051c 100644 --- a/x-pack/plugins/cases/server/config.ts +++ b/x-pack/plugins/cases/server/config.ts @@ -9,8 +9,6 @@ import { schema, TypeOf } from '@kbn/config-schema'; export const ConfigSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), - // TODO: remove once authorization is complete - enableAuthorization: schema.boolean({ defaultValue: false }), }); export type ConfigType = TypeOf; diff --git a/x-pack/plugins/cases/server/connectors/case/index.test.ts b/x-pack/plugins/cases/server/connectors/case/index.test.ts index 95fe562d9e140d..edf7e3d3fdbf17 100644 --- a/x-pack/plugins/cases/server/connectors/case/index.test.ts +++ b/x-pack/plugins/cases/server/connectors/case/index.test.ts @@ -61,7 +61,6 @@ describe('case connector', () => { userActionService, featuresPluginStart: featuresPluginMock.createStart(), getSpace: async (req: KibanaRequest) => undefined, - isAuthEnabled: true, securityPluginSetup: securityMock.createSetup(), securityPluginStart: securityMock.createStart(), }); @@ -1130,7 +1129,6 @@ describe('case connector', () => { totalComment: 0, totalAlerts: 0, version: 'WzksMV0=', - closed_at: null, closed_by: null, connector: { id: 'none', name: 'none', type: ConnectorTypes.none, fields: null }, diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index 2ccc362280b9f7..8a504ce73dee8b 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -62,7 +62,6 @@ export class CasePlugin { private attachmentService?: AttachmentService; private clientFactory: CasesClientFactory; private securityPluginSetup?: SecurityPluginSetup; - private config?: ConfigType; constructor(private readonly initializerContext: PluginInitializerContext) { this.log = this.initializerContext.logger.get('plugins', 'cases'); @@ -76,8 +75,6 @@ export class CasePlugin { return; } - // save instance variables for the client factor initialization call - this.config = config; this.securityPluginSetup = plugins.security; core.savedObjects.registerType(caseCommentSavedObjectType); @@ -146,8 +143,6 @@ export class CasePlugin { return plugins.spaces?.spacesService.getActiveSpace(request); }, featuresPluginStart: plugins.features, - // we'll be removing this eventually but let's just default it to false if it wasn't specified explicitly in the config file - isAuthEnabled: this.config?.enableAuthorization ?? false, }); const getCasesClientWithRequestAndContext = async ( diff --git a/x-pack/plugins/cases/server/routes/api/__fixtures__/route_contexts.ts b/x-pack/plugins/cases/server/routes/api/__fixtures__/route_contexts.ts index 3306712c1e550f..284b01ce993258 100644 --- a/x-pack/plugins/cases/server/routes/api/__fixtures__/route_contexts.ts +++ b/x-pack/plugins/cases/server/routes/api/__fixtures__/route_contexts.ts @@ -20,12 +20,11 @@ import { CaseUserActionService, } from '../../../services'; import { authenticationMock } from '../__fixtures__'; -import type { CasesRequestHandlerContext } from '../../../types'; import { createActionsClient } from './mock_actions_client'; import { featuresPluginMock } from '../../../../../features/server/mocks'; -import { securityMock } from '../../../../../security/server/mocks'; import { CasesClientFactory } from '../../../client/factory'; import { xpackMocks } from '../../../../../../mocks'; +import { KibanaFeature } from '../../../../../features/common'; export const createRouteContext = async (client: any, badAuth = false) => { const actionsMock = createActionsClient(); @@ -56,6 +55,13 @@ export const createRouteContext = async (client: any, badAuth = false) => { contextMock.core.savedObjects.getClient = jest.fn(() => client); contextMock.core.savedObjects.client = client; + // create a fake feature + const featureStart = featuresPluginMock.createStart(); + featureStart.getKibanaFeatures.mockReturnValue([ + // all the authorization class cares about is the `cases` field in the kibana feature so just cast it to that + ({ cases: ['securitySolution'] } as unknown) as KibanaFeature, + ]); + const factory = new CasesClientFactory(log); factory.initialize({ alertsService, @@ -63,11 +69,9 @@ export const createRouteContext = async (client: any, badAuth = false) => { caseService, connectorMappingsService, userActionService, - featuresPluginStart: featuresPluginMock.createStart(), + featuresPluginStart: featureStart, getSpace: async (req: KibanaRequest) => undefined, - isAuthEnabled: false, - securityPluginSetup: securityMock.createSetup(), - securityPluginStart: securityMock.createStart(), + // intentionally not passing the security plugin so that security will be disabled }); // create a single reference to the caseClient so we can mock its methods @@ -79,13 +83,13 @@ export const createRouteContext = async (client: any, badAuth = false) => { scopedClusterClient: esClient, }); - const context = ({ + const context = { ...contextMock, actions: { getActionsClient: () => actionsMock }, cases: { getCasesClient: async () => caseClient, }, - } as unknown) as CasesRequestHandlerContext; + }; return { context, services: { userActionService } }; }; diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.test.ts b/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.test.ts index 0735671384845b..5f6e25f6c8a6d6 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.test.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.test.ts @@ -144,12 +144,13 @@ describe('GET configuration', () => { cases: { ...context.cases, getCasesClient: async () => { - return { + return ({ ...(await context?.cases?.getCasesClient()), - getMappings: () => { + getMappings: async () => { throw new Error(); }, - } as CasesClient; + // This avoids ts errors with overriding getMappings + } as unknown) as CasesClient; }, }, }; diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.test.ts b/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.test.ts index a131061f2ba86d..f94d2e462a336f 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.test.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.test.ts @@ -164,12 +164,13 @@ describe('PATCH configuration', () => { cases: { ...context.cases, getCasesClient: async () => { - return { + return ({ ...(await context?.cases?.getCasesClient()), getMappings: () => { throw new Error(); }, - } as CasesClient; + // This avoids ts errors with overriding getMappings + } as unknown) as CasesClient; }, }, }; diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.test.ts b/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.test.ts index db0488d87dc5cb..e690d9f870c343 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.test.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.test.ts @@ -84,12 +84,13 @@ describe('POST configuration', () => { cases: { ...context.cases, getCasesClient: async () => { - return { + return ({ ...(await context?.cases?.getCasesClient()), getMappings: () => { throw new Error(); }, - } as CasesClient; + // This avoids ts errors with overriding getMappings + } as unknown) as CasesClient; }, }, }; diff --git a/x-pack/plugins/cases/server/routes/api/cases/patch_cases.test.ts b/x-pack/plugins/cases/server/routes/api/cases/patch_cases.test.ts index b3f87211c95475..073c447460875f 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/patch_cases.test.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/patch_cases.test.ts @@ -77,6 +77,7 @@ describe('PATCH cases', () => { "description": "This is a brand new case of a bad meanie defacing data", "external_service": null, "id": "mock-id-1", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, @@ -151,6 +152,7 @@ describe('PATCH cases', () => { "description": "Oh no, a bad meanie going LOLBins all over the place!", "external_service": null, "id": "mock-id-4", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, @@ -220,6 +222,7 @@ describe('PATCH cases', () => { "description": "This is a brand new case of a bad meanie defacing data", "external_service": null, "id": "mock-id-1", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, diff --git a/x-pack/plugins/cases/server/routes/api/cases/post_case.test.ts b/x-pack/plugins/cases/server/routes/api/cases/post_case.test.ts index d75dcada0a9638..3991340612c745 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/post_case.test.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/post_case.test.ts @@ -46,7 +46,7 @@ describe('POST cases', () => { settings: { syncAlerts: true, }, - owner: 'awesome', + owner: 'securitySolution', }, }); @@ -86,7 +86,7 @@ describe('POST cases', () => { settings: { syncAlerts: true, }, - owner: 'awesome', + owner: 'securitySolution', }, }); @@ -120,7 +120,7 @@ describe('POST cases', () => { settings: { syncAlerts: true, }, - owner: 'awesome', + owner: 'securitySolution', }, }); @@ -146,7 +146,7 @@ describe('POST cases', () => { settings: { syncAlerts: true, }, - owner: 'awesome', + owner: 'securitySolution', }, }); @@ -180,7 +180,7 @@ describe('POST cases', () => { settings: { syncAlerts: true, }, - owner: 'awesome', + owner: 'securitySolution', }, }); @@ -196,7 +196,6 @@ describe('POST cases', () => { expect(response.status).toEqual(200); expect(response.payload).toMatchInlineSnapshot(` Object { - "owner": "awesome", "closed_at": null, "closed_by": null, "comments": Array [], @@ -215,6 +214,7 @@ describe('POST cases', () => { "description": "This is a brand new case of a bad meanie defacing data", "external_service": null, "id": "mock-it", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, diff --git a/x-pack/plugins/cases/server/routes/api/cases/status/get_status.test.ts b/x-pack/plugins/cases/server/routes/api/cases/status/get_status.test.ts index 1c399a415e4704..ca12ed9c92831b 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/status/get_status.test.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/status/get_status.test.ts @@ -16,6 +16,7 @@ import { } from '../../__fixtures__'; import { initGetCasesStatusApi } from './get_status'; import { CASE_STATUS_URL } from '../../../../../common/constants'; +import { esKuery } from 'src/plugins/data/server'; import { CaseType } from '../../../../../common/api'; describe('GET status', () => { @@ -47,17 +48,23 @@ describe('GET status', () => { const response = await routeHandler(context, request, kibanaResponseFactory); expect(context.core.savedObjects.client.find).toHaveBeenNthCalledWith(1, { ...findArgs, - filter: `((cases.attributes.status: open AND cases.attributes.type: individual) OR cases.attributes.type: ${CaseType.collection})`, + filter: esKuery.fromKueryExpression( + `((cases.attributes.status: open AND cases.attributes.type: individual) OR cases.attributes.type: ${CaseType.collection})` + ), }); expect(context.core.savedObjects.client.find).toHaveBeenNthCalledWith(2, { ...findArgs, - filter: `((cases.attributes.status: in-progress AND cases.attributes.type: individual) OR cases.attributes.type: ${CaseType.collection})`, + filter: esKuery.fromKueryExpression( + `((cases.attributes.status: in-progress AND cases.attributes.type: individual) OR cases.attributes.type: ${CaseType.collection})` + ), }); expect(context.core.savedObjects.client.find).toHaveBeenNthCalledWith(3, { ...findArgs, - filter: `((cases.attributes.status: closed AND cases.attributes.type: individual) OR cases.attributes.type: ${CaseType.collection})`, + filter: esKuery.fromKueryExpression( + `((cases.attributes.status: closed AND cases.attributes.type: individual) OR cases.attributes.type: ${CaseType.collection})` + ), }); expect(response.payload).toEqual({ diff --git a/x-pack/plugins/cases/server/routes/api/utils.test.ts b/x-pack/plugins/cases/server/routes/api/utils.test.ts index f6bc1e4f718971..99d2c1509538cc 100644 --- a/x-pack/plugins/cases/server/routes/api/utils.test.ts +++ b/x-pack/plugins/cases/server/routes/api/utils.test.ts @@ -87,6 +87,7 @@ describe('Utils', () => { }, "description": "A description", "external_service": null, + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, @@ -143,6 +144,7 @@ describe('Utils', () => { }, "description": "A description", "external_service": null, + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, @@ -202,6 +204,7 @@ describe('Utils', () => { }, "description": "A description", "external_service": null, + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, @@ -397,6 +400,7 @@ describe('Utils', () => { "description": "This is a brand new case of a bad meanie defacing data", "external_service": null, "id": "mock-id-1", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, @@ -437,6 +441,7 @@ describe('Utils', () => { "description": "Oh no, a bad meanie destroying data!", "external_service": null, "id": "mock-id-2", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, @@ -481,6 +486,7 @@ describe('Utils', () => { "description": "Oh no, a bad meanie going LOLBins all over the place!", "external_service": null, "id": "mock-id-3", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, @@ -529,6 +535,7 @@ describe('Utils', () => { "description": "Oh no, a bad meanie going LOLBins all over the place!", "external_service": null, "id": "mock-id-4", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, @@ -594,6 +601,7 @@ describe('Utils', () => { "description": "Oh no, a bad meanie going LOLBins all over the place!", "external_service": null, "id": "mock-id-3", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, @@ -650,6 +658,7 @@ describe('Utils', () => { "description": "Oh no, a bad meanie going LOLBins all over the place!", "external_service": null, "id": "mock-id-3", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, @@ -729,6 +738,7 @@ describe('Utils', () => { "description": "Oh no, a bad meanie going LOLBins all over the place!", "external_service": null, "id": "mock-id-3", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, diff --git a/x-pack/plugins/cases/server/services/cases/index.ts b/x-pack/plugins/cases/server/services/cases/index.ts index bbb82214d70a54..99d6129dc54b3e 100644 --- a/x-pack/plugins/cases/server/services/cases/index.ts +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -105,6 +105,7 @@ interface FindSubCasesStatusStats { interface PostCaseArgs extends ClientArgs { attributes: ESCaseAttributes; + id: string; } interface CreateSubCaseArgs extends ClientArgs { @@ -933,12 +934,10 @@ export class CaseService { } } - public async postNewCase({ soClient, attributes }: PostCaseArgs) { + public async postNewCase({ soClient, attributes, id }: PostCaseArgs) { try { this.log.debug(`Attempting to POST a new case`); - return await soClient.create(CASE_SAVED_OBJECT, { - ...attributes, - }); + return await soClient.create(CASE_SAVED_OBJECT, attributes, { id }); } catch (error) { this.log.error(`Error on POST a new case: ${error}`); throw error; diff --git a/x-pack/plugins/security/server/authorization/actions/__snapshots__/cases.test.ts.snap b/x-pack/plugins/security/server/authorization/actions/__snapshots__/cases.test.ts.snap index 2208105694fe9f..33140f180ad0ad 100644 --- a/x-pack/plugins/security/server/authorization/actions/__snapshots__/cases.test.ts.snap +++ b/x-pack/plugins/security/server/authorization/actions/__snapshots__/cases.test.ts.snap @@ -1,17 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`#get class of "" 1`] = `"class is required and must be a string"`; - -exports[`#get class of "{}" 1`] = `"class is required and must be a string"`; - -exports[`#get class of "1" 1`] = `"class is required and must be a string"`; - -exports[`#get class of "null" 1`] = `"class is required and must be a string"`; - -exports[`#get class of "true" 1`] = `"class is required and must be a string"`; - -exports[`#get class of "undefined" 1`] = `"class is required and must be a string"`; - exports[`#get operation of "" 1`] = `"operation is required and must be a string"`; exports[`#get operation of "{}" 1`] = `"operation is required and must be a string"`; @@ -23,3 +11,15 @@ exports[`#get operation of "null" 1`] = `"operation is required and must be a st exports[`#get operation of "true" 1`] = `"operation is required and must be a string"`; exports[`#get operation of "undefined" 1`] = `"operation is required and must be a string"`; + +exports[`#get owner of "" 1`] = `"owner is required and must be a string"`; + +exports[`#get owner of "{}" 1`] = `"owner is required and must be a string"`; + +exports[`#get owner of "1" 1`] = `"owner is required and must be a string"`; + +exports[`#get owner of "null" 1`] = `"owner is required and must be a string"`; + +exports[`#get owner of "true" 1`] = `"owner is required and must be a string"`; + +exports[`#get owner of "undefined" 1`] = `"owner is required and must be a string"`; 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 55920aabe993d8..1b1932f8640906 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 @@ -70,8 +70,8 @@ describe(`cases`, () => { expect(casesFeaturePrivilege.getActions(privilege, feature)).toMatchInlineSnapshot(` Array [ - "cases:1.0.0-zeta1:observability/get", - "cases:1.0.0-zeta1:observability/find", + "cases:1.0.0-zeta1:observability/getCase", + "cases:1.0.0-zeta1:observability/findCases", ] `); }); @@ -105,11 +105,11 @@ describe(`cases`, () => { expect(casesFeaturePrivilege.getActions(privilege, feature)).toMatchInlineSnapshot(` Array [ - "cases:1.0.0-zeta1:security/get", - "cases:1.0.0-zeta1:security/find", - "cases:1.0.0-zeta1:security/create", - "cases:1.0.0-zeta1:security/delete", - "cases:1.0.0-zeta1:security/update", + "cases:1.0.0-zeta1:security/getCase", + "cases:1.0.0-zeta1:security/findCases", + "cases:1.0.0-zeta1:security/createCase", + "cases:1.0.0-zeta1:security/deleteCase", + "cases:1.0.0-zeta1:security/updateCase", ] `); }); @@ -144,13 +144,13 @@ describe(`cases`, () => { expect(casesFeaturePrivilege.getActions(privilege, feature)).toMatchInlineSnapshot(` Array [ - "cases:1.0.0-zeta1:security/get", - "cases:1.0.0-zeta1:security/find", - "cases:1.0.0-zeta1:security/create", - "cases:1.0.0-zeta1:security/delete", - "cases:1.0.0-zeta1:security/update", - "cases:1.0.0-zeta1:obs/get", - "cases:1.0.0-zeta1:obs/find", + "cases:1.0.0-zeta1:security/getCase", + "cases:1.0.0-zeta1:security/findCases", + "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:obs/getCase", + "cases:1.0.0-zeta1:obs/findCases", ] `); }); @@ -185,20 +185,20 @@ describe(`cases`, () => { expect(casesFeaturePrivilege.getActions(privilege, feature)).toMatchInlineSnapshot(` Array [ - "cases:1.0.0-zeta1:security/get", - "cases:1.0.0-zeta1:security/find", - "cases:1.0.0-zeta1:security/create", - "cases:1.0.0-zeta1:security/delete", - "cases:1.0.0-zeta1:security/update", - "cases:1.0.0-zeta1:other-security/get", - "cases:1.0.0-zeta1:other-security/find", - "cases:1.0.0-zeta1:other-security/create", - "cases:1.0.0-zeta1:other-security/delete", - "cases:1.0.0-zeta1:other-security/update", - "cases:1.0.0-zeta1:obs/get", - "cases:1.0.0-zeta1:obs/find", - "cases:1.0.0-zeta1:other-obs/get", - "cases:1.0.0-zeta1:other-obs/find", + "cases:1.0.0-zeta1:security/getCase", + "cases:1.0.0-zeta1:security/findCases", + "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:other-security/getCase", + "cases:1.0.0-zeta1:other-security/findCases", + "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:obs/getCase", + "cases:1.0.0-zeta1:obs/findCases", + "cases:1.0.0-zeta1:other-obs/getCase", + "cases:1.0.0-zeta1:other-obs/findCases", ] `); }); 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 aacff3082fbca2..8608653c41b345 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 @@ -10,8 +10,10 @@ import { uniq } from 'lodash'; import type { FeatureKibanaPrivileges, KibanaFeature } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; -const readOperations: string[] = ['get', 'find']; -const writeOperations: string[] = ['create', 'delete', 'update']; +// 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 allOperations: string[] = [...readOperations, ...writeOperations]; export class FeaturePrivilegeCasesBuilder extends BaseFeaturePrivilegeBuilder { diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 0fa6c553c2e80b..574e37fdd18413 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -83,6 +83,9 @@ describe('Security Plugin', () => { "app": AppActions { "prefix": "app:version:", }, + "cases": CasesActions { + "prefix": "cases:version:", + }, "login": "login:", "savedObject": SavedObjectActions { "prefix": "saved_object:version:", @@ -150,6 +153,9 @@ describe('Security Plugin', () => { "app": AppActions { "prefix": "app:version:", }, + "cases": CasesActions { + "prefix": "cases:version:", + }, "login": "login:", "savedObject": SavedObjectActions { "prefix": "saved_object:version:", diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx index 3ac0084e96fb3b..27f702431e8982 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx @@ -769,6 +769,7 @@ describe('AllCases', () => { }, }, id: '1', + owner: 'securitySolution', status: 'open', subCaseIds: [], tags: ['coke', 'pepsi'], diff --git a/x-pack/plugins/security_solution/public/cases/containers/mock.ts b/x-pack/plugins/security_solution/public/cases/containers/mock.ts index 4559f6000493f4..947de140ccbb0d 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/mock.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/mock.ts @@ -75,6 +75,7 @@ export const alertComment: Comment = { export const basicCase: Case = { type: CaseType.individual, + owner: 'securitySolution', closedAt: null, closedBy: null, id: basicCaseId, @@ -105,6 +106,7 @@ export const basicCase: Case = { export const collectionCase: Case = { type: CaseType.collection, + owner: 'securitySolution', closedAt: null, closedBy: null, id: 'collection-id', diff --git a/x-pack/plugins/security_solution/public/cases/containers/types.ts b/x-pack/plugins/security_solution/public/cases/containers/types.ts index 6feb5a1501a76b..66636d2e547041 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/types.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/types.ts @@ -56,6 +56,7 @@ export interface CaseExternalService { interface BasicCase { id: string; + owner: string; closedAt: string | null; closedBy: ElasticUser | null; comments: Comment[]; diff --git a/x-pack/test/case_api_integration/common/config.ts b/x-pack/test/case_api_integration/common/config.ts index 9b6c066c3f813b..0d9a1030d68088 100644 --- a/x-pack/test/case_api_integration/common/config.ts +++ b/x-pack/test/case_api_integration/common/config.ts @@ -87,7 +87,6 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) ...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'), `--xpack.actions.allowedHosts=${JSON.stringify(['localhost', 'some.non.existent.com'])}`, `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, - '--xpack.cases.enableAuthorization=true', '--xpack.eventLog.logEntries=true', ...disabledPlugins.map((key) => `--xpack.${key}.enabled=false`), // Actions simulators plugin. Needed for testing push to external services. From 73a4bfc86e608d4145a82a043747bd7e5c5195aa Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Thu, 8 Apr 2021 11:16:35 -0400 Subject: [PATCH 045/113] [Cases] Migrate sub cases routes to a client (#96461) * Adding sub cases client * Move sub case routes to case client * Throw when attempting to access the sub cases client * Fixing throw and removing user ans soclients --- .../cases/common/api/cases/sub_case.ts | 1 + x-pack/plugins/cases/server/client/client.ts | 12 + .../cases/server/client/sub_cases/client.ts | 237 ++++++++++ .../cases/server/client/sub_cases/update.ts | 400 ++++++++++++++++ .../api/cases/sub_case/delete_sub_cases.ts | 72 +-- .../api/cases/sub_case/find_sub_cases.ts | 64 +-- .../routes/api/cases/sub_case/get_sub_case.ts | 52 +-- .../api/cases/sub_case/patch_sub_cases.ts | 431 +----------------- 8 files changed, 675 insertions(+), 594 deletions(-) create mode 100644 x-pack/plugins/cases/server/client/sub_cases/client.ts create mode 100644 x-pack/plugins/cases/server/client/sub_cases/update.ts diff --git a/x-pack/plugins/cases/common/api/cases/sub_case.ts b/x-pack/plugins/cases/common/api/cases/sub_case.ts index 4bbdfd5b7d3688..ba6cd6a8affa44 100644 --- a/x-pack/plugins/cases/common/api/cases/sub_case.ts +++ b/x-pack/plugins/cases/common/api/cases/sub_case.ts @@ -79,3 +79,4 @@ export type SubCasesResponse = rt.TypeOf; export type SubCasesFindResponse = rt.TypeOf; export type SubCasePatchRequest = rt.TypeOf; export type SubCasesPatchRequest = rt.TypeOf; +export type SubCasesFindRequest = rt.TypeOf; diff --git a/x-pack/plugins/cases/server/client/client.ts b/x-pack/plugins/cases/server/client/client.ts index 5f6cb8851c34cf..702329f7bcca21 100644 --- a/x-pack/plugins/cases/server/client/client.ts +++ b/x-pack/plugins/cases/server/client/client.ts @@ -4,24 +4,29 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import Boom from '@hapi/boom'; import { CasesClientArgs } from './types'; import { CasesSubClient, createCasesSubClient } from './cases/client'; import { AttachmentsSubClient, createAttachmentsSubClient } from './attachments/client'; import { UserActionsSubClient, createUserActionsSubClient } from './user_actions/client'; import { CasesClientInternal, createCasesClientInternal } from './client_internal'; +import { createSubCasesClient, SubCasesClient } from './sub_cases/client'; +import { ENABLE_CASE_CONNECTOR } from '../../common/constants'; export class CasesClient { private readonly _casesClientInternal: CasesClientInternal; private readonly _cases: CasesSubClient; private readonly _attachments: AttachmentsSubClient; private readonly _userActions: UserActionsSubClient; + private readonly _subCases: SubCasesClient; constructor(args: CasesClientArgs) { this._casesClientInternal = createCasesClientInternal(args); this._cases = createCasesSubClient(args, this, this._casesClientInternal); this._attachments = createAttachmentsSubClient(args, this._casesClientInternal); this._userActions = createUserActionsSubClient(args); + this._subCases = createSubCasesClient(args, this); } public get cases() { @@ -36,6 +41,13 @@ export class CasesClient { return this._userActions; } + public get subCases() { + if (!ENABLE_CASE_CONNECTOR) { + throw new Error('The case connector feature is disabled'); + } + return this._subCases; + } + // TODO: Remove it when all routes will be moved to the cases client. public get casesClientInternal() { return this._casesClientInternal; diff --git a/x-pack/plugins/cases/server/client/sub_cases/client.ts b/x-pack/plugins/cases/server/client/sub_cases/client.ts new file mode 100644 index 00000000000000..aef780ecb3ac95 --- /dev/null +++ b/x-pack/plugins/cases/server/client/sub_cases/client.ts @@ -0,0 +1,237 @@ +/* + * 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 Boom from '@hapi/boom'; + +import { + caseStatuses, + SubCaseResponse, + SubCaseResponseRt, + SubCasesFindRequest, + SubCasesFindResponse, + SubCasesFindResponseRt, + SubCasesPatchRequest, + SubCasesResponse, +} from '../../../common/api'; +import { CasesClientArgs } from '..'; +import { flattenSubCaseSavedObject, transformSubCases } from '../../routes/api/utils'; +import { countAlertsForID } from '../../common'; +import { createCaseError } from '../../common/error'; +import { CASE_SAVED_OBJECT } from '../../../common/constants'; +import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; +import { constructQueryOptions } from '../../routes/api/cases/helpers'; +import { defaultPage, defaultPerPage } from '../../routes/api'; +import { CasesClient } from '../client'; +import { update } from './update'; + +interface FindArgs { + caseID: string; + queryParams: SubCasesFindRequest; +} + +interface GetArgs { + includeComments: boolean; + id: string; +} + +/** + * The API routes for interacting with sub cases. + */ +export interface SubCasesClient { + delete(ids: string[]): Promise; + find(findArgs: FindArgs): Promise; + get(getArgs: GetArgs): Promise; + update(subCases: SubCasesPatchRequest): Promise; +} + +/** + * Creates a client for handling the different exposed API routes for interacting with sub cases. + */ +export function createSubCasesClient( + clientArgs: CasesClientArgs, + casesClient: CasesClient +): SubCasesClient { + return Object.freeze({ + delete: (ids: string[]) => deleteSubCase(ids, clientArgs), + find: (findArgs: FindArgs) => find(findArgs, clientArgs), + get: (getArgs: GetArgs) => get(getArgs, clientArgs), + update: (subCases: SubCasesPatchRequest) => update(subCases, clientArgs, casesClient), + }); +} + +async function deleteSubCase(ids: string[], clientArgs: CasesClientArgs): Promise { + try { + const { + savedObjectsClient: soClient, + user, + userActionService, + caseService, + attachmentService, + } = clientArgs; + + const [comments, subCases] = await Promise.all([ + caseService.getAllSubCaseComments({ soClient, id: ids }), + caseService.getSubCases({ soClient, ids }), + ]); + + const subCaseErrors = subCases.saved_objects.filter((subCase) => subCase.error !== undefined); + + if (subCaseErrors.length > 0) { + throw Boom.notFound( + `These sub cases ${subCaseErrors + .map((c) => c.id) + .join(', ')} do not exist. Please check you have the correct ids.` + ); + } + + const subCaseIDToParentID = subCases.saved_objects.reduce((acc, subCase) => { + const parentID = subCase.references.find((ref) => ref.type === CASE_SAVED_OBJECT); + acc.set(subCase.id, parentID?.id); + return acc; + }, new Map()); + + await Promise.all( + comments.saved_objects.map((comment) => + attachmentService.delete({ soClient, attachmentId: comment.id }) + ) + ); + + await Promise.all(ids.map((id) => caseService.deleteSubCase(soClient, id))); + + const deleteDate = new Date().toISOString(); + + await userActionService.bulkCreate({ + soClient, + actions: ids.map((id) => + buildCaseUserActionItem({ + action: 'delete', + actionAt: deleteDate, + actionBy: user, + // if for some reason the sub case didn't have a reference to its parent, we'll still log a user action + // but we won't have the case ID + caseId: subCaseIDToParentID.get(id) ?? '', + subCaseId: id, + fields: ['sub_case', 'comment', 'status'], + }) + ), + }); + } catch (error) { + throw createCaseError({ + message: `Failed to delete sub cases ids: ${JSON.stringify(ids)}: ${error}`, + error, + logger: clientArgs.logger, + }); + } +} + +async function find( + { caseID, queryParams }: FindArgs, + clientArgs: CasesClientArgs +): Promise { + try { + const { savedObjectsClient: soClient, caseService } = clientArgs; + + const ids = [caseID]; + const { subCase: subCaseQueryOptions } = constructQueryOptions({ + status: queryParams.status, + sortByField: queryParams.sortField, + }); + + const subCases = await caseService.findSubCasesGroupByCase({ + soClient, + ids, + options: { + sortField: 'created_at', + page: defaultPage, + perPage: defaultPerPage, + ...queryParams, + ...subCaseQueryOptions, + }, + }); + + const [open, inProgress, closed] = await Promise.all([ + ...caseStatuses.map((status) => { + const { subCase: statusQueryOptions } = constructQueryOptions({ + status, + sortByField: queryParams.sortField, + }); + return caseService.findSubCaseStatusStats({ + soClient, + options: statusQueryOptions ?? {}, + ids, + }); + }), + ]); + + return SubCasesFindResponseRt.encode( + transformSubCases({ + page: subCases.page, + perPage: subCases.perPage, + total: subCases.total, + subCasesMap: subCases.subCasesMap, + open, + inProgress, + closed, + }) + ); + } catch (error) { + throw createCaseError({ + message: `Failed to find sub cases for case id: ${caseID}: ${error}`, + error, + logger: clientArgs.logger, + }); + } +} + +async function get( + { includeComments, id }: GetArgs, + clientArgs: CasesClientArgs +): Promise { + try { + const { savedObjectsClient: soClient, caseService } = clientArgs; + + const subCase = await caseService.getSubCase({ + soClient, + id, + }); + + if (!includeComments) { + return SubCaseResponseRt.encode( + flattenSubCaseSavedObject({ + savedObject: subCase, + }) + ); + } + + const theComments = await caseService.getAllSubCaseComments({ + soClient, + id, + options: { + sortField: 'created_at', + sortOrder: 'asc', + }, + }); + + return SubCaseResponseRt.encode( + flattenSubCaseSavedObject({ + savedObject: subCase, + comments: theComments.saved_objects, + totalComment: theComments.total, + totalAlerts: countAlertsForID({ + comments: theComments, + id, + }), + }) + ); + } catch (error) { + throw createCaseError({ + message: `Failed to get sub case id: ${id}: ${error}`, + error, + logger: clientArgs.logger, + }); + } +} diff --git a/x-pack/plugins/cases/server/client/sub_cases/update.ts b/x-pack/plugins/cases/server/client/sub_cases/update.ts new file mode 100644 index 00000000000000..27e6e1261c0af5 --- /dev/null +++ b/x-pack/plugins/cases/server/client/sub_cases/update.ts @@ -0,0 +1,400 @@ +/* + * 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 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, + SavedObject, + SavedObjectsFindResponse, + Logger, +} from 'kibana/server'; + +import { nodeBuilder } from '../../../../../../src/plugins/data/common'; +import { CasesClient } from '../../client'; +import { CaseService } from '../../services'; +import { + CaseStatuses, + SubCasesPatchRequest, + SubCasesPatchRequestRt, + CommentType, + excess, + throwErrors, + SubCasesResponse, + SubCasePatchRequest, + SubCaseAttributes, + ESCaseAttributes, + SubCaseResponse, + SubCasesResponseRt, + User, + CommentAttributes, +} from '../../../common/api'; +import { CASE_COMMENT_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../common/constants'; +import { + flattenSubCaseSavedObject, + isCommentRequestTypeAlertOrGenAlert, +} from '../../routes/api/utils'; +import { getCaseToUpdate } from '../../routes/api/cases/helpers'; +import { buildSubCaseUserActions } from '../../services/user_actions/helpers'; +import { createAlertUpdateRequest } from '../../common'; +import { createCaseError } from '../../common/error'; +import { UpdateAlertRequest } from '../../client/alerts/client'; +import { CasesClientArgs } from '../types'; + +function checkNonExistingOrConflict( + toUpdate: SubCasePatchRequest[], + fromStorage: Map> +) { + const nonExistingSubCases: SubCasePatchRequest[] = []; + const conflictedSubCases: SubCasePatchRequest[] = []; + for (const subCaseToUpdate of toUpdate) { + const bulkEntry = fromStorage.get(subCaseToUpdate.id); + + if (bulkEntry && bulkEntry.error) { + nonExistingSubCases.push(subCaseToUpdate); + } + + if (!bulkEntry || bulkEntry.version !== subCaseToUpdate.version) { + conflictedSubCases.push(subCaseToUpdate); + } + } + + if (nonExistingSubCases.length > 0) { + throw Boom.notFound( + `These sub cases ${nonExistingSubCases + .map((c) => c.id) + .join(', ')} do not exist. Please check you have the correct ids.` + ); + } + + if (conflictedSubCases.length > 0) { + throw Boom.conflict( + `These sub cases ${conflictedSubCases + .map((c) => c.id) + .join(', ')} has been updated. Please refresh before saving additional updates.` + ); + } +} + +interface GetParentIDsResult { + ids: string[]; + parentIDToSubID: Map; +} + +function getParentIDs({ + subCasesMap, + subCaseIDs, +}: { + subCasesMap: Map>; + subCaseIDs: string[]; +}): GetParentIDsResult { + return subCaseIDs.reduce( + (acc, id) => { + const subCase = subCasesMap.get(id); + if (subCase && subCase.references.length > 0) { + const parentID = subCase.references[0].id; + acc.ids.push(parentID); + let subIDs = acc.parentIDToSubID.get(parentID); + if (subIDs === undefined) { + subIDs = []; + } + subIDs.push(id); + acc.parentIDToSubID.set(parentID, subIDs); + } + return acc; + }, + { ids: [], parentIDToSubID: new Map() } + ); +} + +async function getParentCases({ + caseService, + soClient, + subCaseIDs, + subCasesMap, +}: { + caseService: CaseService; + soClient: SavedObjectsClientContract; + subCaseIDs: string[]; + subCasesMap: Map>; +}): Promise>> { + const parentIDInfo = getParentIDs({ subCaseIDs, subCasesMap }); + + const parentCases = await caseService.getCases({ + soClient, + caseIds: parentIDInfo.ids, + }); + + const parentCaseErrors = parentCases.saved_objects.filter((so) => so.error !== undefined); + + if (parentCaseErrors.length > 0) { + throw Boom.badRequest( + `Unable to find parent cases: ${parentCaseErrors + .map((c) => c.id) + .join(', ')} for sub cases: ${subCaseIDs.join(', ')}` + ); + } + + return parentCases.saved_objects.reduce((acc, so) => { + const subCaseIDsWithParent = parentIDInfo.parentIDToSubID.get(so.id); + subCaseIDsWithParent?.forEach((subCaseId) => { + acc.set(subCaseId, so); + }); + return acc; + }, new Map>()); +} + +function getValidUpdateRequests( + toUpdate: SubCasePatchRequest[], + subCasesMap: Map> +): SubCasePatchRequest[] { + const validatedSubCaseAttributes: SubCasePatchRequest[] = toUpdate.map((updateCase) => { + const currentCase = subCasesMap.get(updateCase.id); + return currentCase != null + ? getCaseToUpdate(currentCase.attributes, { + ...updateCase, + }) + : { id: updateCase.id, version: updateCase.version }; + }); + + return validatedSubCaseAttributes.filter((updateCase: SubCasePatchRequest) => { + const { id, version, ...updateCaseAttributes } = updateCase; + return Object.keys(updateCaseAttributes).length > 0; + }); +} + +/** + * Get the id from a reference in a comment for a sub case + */ +function getID(comment: SavedObject): string | undefined { + return comment.references.find((ref) => ref.type === SUB_CASE_SAVED_OBJECT)?.id; +} + +/** + * Get all the alert comments for a set of sub cases + */ +async function getAlertComments({ + subCasesToSync, + caseService, + soClient, +}: { + subCasesToSync: SubCasePatchRequest[]; + caseService: CaseService; + soClient: SavedObjectsClientContract; +}): Promise> { + const ids = subCasesToSync.map((subCase) => subCase.id); + return caseService.getAllSubCaseComments({ + soClient, + id: ids, + options: { + filter: nodeBuilder.or([ + nodeBuilder.is(`${CASE_COMMENT_SAVED_OBJECT}.attributes.type`, CommentType.alert), + nodeBuilder.is(`${CASE_COMMENT_SAVED_OBJECT}.attributes.type`, CommentType.generatedAlert), + ]), + }, + }); +} + +/** + * Updates the status of alerts for the specified sub cases. + */ +async function updateAlerts({ + caseService, + soClient, + casesClient, + logger, + subCasesToSync, +}: { + caseService: CaseService; + soClient: SavedObjectsClientContract; + casesClient: CasesClient; + logger: Logger; + subCasesToSync: SubCasePatchRequest[]; +}) { + try { + const subCasesToSyncMap = subCasesToSync.reduce((acc, subCase) => { + acc.set(subCase.id, subCase); + return acc; + }, new Map()); + // get all the alerts for all sub cases that need to be synced + const totalAlerts = await getAlertComments({ caseService, soClient, subCasesToSync }); + // create a map of the status (open, closed, etc) to alert info that needs to be updated + const alertsToUpdate = totalAlerts.saved_objects.reduce( + (acc: UpdateAlertRequest[], alertComment) => { + if (isCommentRequestTypeAlertOrGenAlert(alertComment.attributes)) { + const id = getID(alertComment); + const status = + id !== undefined + ? subCasesToSyncMap.get(id)?.status ?? CaseStatuses.open + : CaseStatuses.open; + + acc.push(...createAlertUpdateRequest({ comment: alertComment.attributes, status })); + } + return acc; + }, + [] + ); + + await casesClient.casesClientInternal.alerts.updateStatus({ alerts: alertsToUpdate }); + } catch (error) { + throw createCaseError({ + message: `Failed to update alert status while updating sub cases: ${JSON.stringify( + subCasesToSync + )}: ${error}`, + logger, + error, + }); + } +} + +/** + * Handles updating the fields in a sub case. + */ +export async function update( + subCases: SubCasesPatchRequest, + clientArgs: CasesClientArgs, + casesClient: CasesClient +): Promise { + const query = pipe( + excess(SubCasesPatchRequestRt).decode(subCases), + fold(throwErrors(Boom.badRequest), identity) + ); + + try { + const { savedObjectsClient: soClient, user, caseService, userActionService } = clientArgs; + + const bulkSubCases = await caseService.getSubCases({ + soClient, + ids: query.subCases.map((q) => q.id), + }); + + const subCasesMap = bulkSubCases.saved_objects.reduce((acc, so) => { + acc.set(so.id, so); + return acc; + }, new Map>()); + + checkNonExistingOrConflict(query.subCases, subCasesMap); + + const nonEmptySubCaseRequests = getValidUpdateRequests(query.subCases, subCasesMap); + + if (nonEmptySubCaseRequests.length <= 0) { + throw Boom.notAcceptable('All update fields are identical to current version.'); + } + + const subIDToParentCase = await getParentCases({ + soClient, + caseService, + subCaseIDs: nonEmptySubCaseRequests.map((subCase) => subCase.id), + subCasesMap, + }); + + const updatedAt = new Date().toISOString(); + const updatedCases = await caseService.patchSubCases({ + soClient, + subCases: nonEmptySubCaseRequests.map((thisCase) => { + const { id: subCaseId, version, ...updateSubCaseAttributes } = thisCase; + let closedInfo: { closed_at: string | null; closed_by: User | null } = { + closed_at: null, + closed_by: null, + }; + + if ( + updateSubCaseAttributes.status && + updateSubCaseAttributes.status === CaseStatuses.closed + ) { + closedInfo = { + closed_at: updatedAt, + closed_by: user, + }; + } else if ( + updateSubCaseAttributes.status && + (updateSubCaseAttributes.status === CaseStatuses.open || + updateSubCaseAttributes.status === CaseStatuses['in-progress']) + ) { + closedInfo = { + closed_at: null, + closed_by: null, + }; + } + return { + subCaseId, + updatedAttributes: { + ...updateSubCaseAttributes, + ...closedInfo, + updated_at: updatedAt, + updated_by: user, + }, + version, + }; + }), + }); + + const subCasesToSyncAlertsFor = nonEmptySubCaseRequests.filter((subCaseToUpdate) => { + const storedSubCase = subCasesMap.get(subCaseToUpdate.id); + const parentCase = subIDToParentCase.get(subCaseToUpdate.id); + return ( + storedSubCase !== undefined && + subCaseToUpdate.status !== undefined && + storedSubCase.attributes.status !== subCaseToUpdate.status && + parentCase?.attributes.settings.syncAlerts + ); + }); + + await updateAlerts({ + caseService, + soClient, + casesClient, + subCasesToSync: subCasesToSyncAlertsFor, + logger: clientArgs.logger, + }); + + const returnUpdatedSubCases = updatedCases.saved_objects.reduce( + (acc, updatedSO) => { + const originalSubCase = subCasesMap.get(updatedSO.id); + if (originalSubCase) { + acc.push( + flattenSubCaseSavedObject({ + savedObject: { + ...originalSubCase, + ...updatedSO, + attributes: { ...originalSubCase.attributes, ...updatedSO.attributes }, + references: originalSubCase.references, + version: updatedSO.version ?? originalSubCase.version, + }, + }) + ); + } + return acc; + }, + [] + ); + + await userActionService.bulkCreate({ + soClient, + actions: buildSubCaseUserActions({ + originalSubCases: bulkSubCases.saved_objects, + updatedSubCases: updatedCases.saved_objects, + actionDate: updatedAt, + actionBy: user, + }), + }); + + return SubCasesResponseRt.encode(returnUpdatedSubCases); + } catch (error) { + const idVersions = query.subCases.map((subCase) => ({ + id: subCase.id, + version: subCase.version, + })); + throw createCaseError({ + message: `Failed to update sub cases: ${JSON.stringify(idVersions)}: ${error}`, + error, + logger: clientArgs.logger, + }); + } +} 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/cases/sub_case/delete_sub_cases.ts index 15eb5a421358b6..4f4870496f77ff 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/sub_case/delete_sub_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/sub_case/delete_sub_cases.ts @@ -5,24 +5,12 @@ * 2.0. */ -import Boom from '@hapi/boom'; import { schema } from '@kbn/config-schema'; -import { buildCaseUserActionItem } from '../../../../services/user_actions/helpers'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; -import { - SUB_CASES_PATCH_DEL_URL, - SAVED_OBJECT_TYPES, - CASE_SAVED_OBJECT, -} from '../../../../../common/constants'; +import { SUB_CASES_PATCH_DEL_URL } from '../../../../../common/constants'; -export function initDeleteSubCasesApi({ - attachmentService, - caseService, - router, - userActionService, - logger, -}: RouteDeps) { +export function initDeleteSubCasesApi({ caseService, router, logger }: RouteDeps) { router.delete( { path: SUB_CASES_PATCH_DEL_URL, @@ -34,60 +22,8 @@ export function initDeleteSubCasesApi({ }, async (context, request, response) => { try { - const soClient = context.core.savedObjects.getClient({ - includedHiddenTypes: SAVED_OBJECT_TYPES, - }); - - const [comments, subCases] = await Promise.all([ - caseService.getAllSubCaseComments({ soClient, id: request.query.ids }), - caseService.getSubCases({ soClient, ids: request.query.ids }), - ]); - - const subCaseErrors = subCases.saved_objects.filter( - (subCase) => subCase.error !== undefined - ); - - if (subCaseErrors.length > 0) { - throw Boom.notFound( - `These sub cases ${subCaseErrors - .map((c) => c.id) - .join(', ')} do not exist. Please check you have the correct ids.` - ); - } - - const subCaseIDToParentID = subCases.saved_objects.reduce((acc, subCase) => { - const parentID = subCase.references.find((ref) => ref.type === CASE_SAVED_OBJECT); - acc.set(subCase.id, parentID?.id); - return acc; - }, new Map()); - - await Promise.all( - comments.saved_objects.map((comment) => - attachmentService.delete({ soClient, attachmentId: comment.id }) - ) - ); - - await Promise.all(request.query.ids.map((id) => caseService.deleteSubCase(soClient, id))); - - // eslint-disable-next-line @typescript-eslint/naming-convention - const { username, full_name, email } = await caseService.getUser({ request }); - const deleteDate = new Date().toISOString(); - - await userActionService.bulkCreate({ - soClient, - actions: request.query.ids.map((id) => - buildCaseUserActionItem({ - action: 'delete', - actionAt: deleteDate, - actionBy: { username, full_name, email }, - // if for some reason the sub case didn't have a reference to its parent, we'll still log a user action - // but we won't have the case ID - caseId: subCaseIDToParentID.get(id) ?? '', - subCaseId: id, - fields: ['sub_case', 'comment', 'status'], - }) - ), - }); + const client = await context.cases.getCasesClient(); + await client.subCases.delete(request.query.ids); return response.noContent(); } catch (error) { 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/cases/sub_case/find_sub_cases.ts index f9d077cbe3b122..80cfbbd6b584f8 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/sub_case/find_sub_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/sub_case/find_sub_cases.ts @@ -12,17 +12,10 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { - caseStatuses, - SubCasesFindRequestRt, - SubCasesFindResponseRt, - throwErrors, -} from '../../../../../common/api'; +import { SubCasesFindRequestRt, throwErrors } from '../../../../../common/api'; import { RouteDeps } from '../../types'; -import { escapeHatch, transformSubCases, wrapError } from '../../utils'; -import { SUB_CASES_URL, SAVED_OBJECT_TYPES } from '../../../../../common/constants'; -import { constructQueryOptions } from '../helpers'; -import { defaultPage, defaultPerPage } from '../..'; +import { escapeHatch, wrapError } from '../../utils'; +import { SUB_CASES_URL } from '../../../../../common/constants'; export function initFindSubCasesApi({ caseService, router, logger }: RouteDeps) { router.get( @@ -37,58 +30,17 @@ export function initFindSubCasesApi({ caseService, router, logger }: RouteDeps) }, async (context, request, response) => { try { - const soClient = context.core.savedObjects.getClient({ - includedHiddenTypes: SAVED_OBJECT_TYPES, - }); const queryParams = pipe( SubCasesFindRequestRt.decode(request.query), fold(throwErrors(Boom.badRequest), identity) ); - const ids = [request.params.case_id]; - const { subCase: subCaseQueryOptions } = constructQueryOptions({ - status: queryParams.status, - sortByField: queryParams.sortField, - }); - - const subCases = await caseService.findSubCasesGroupByCase({ - soClient, - ids, - options: { - sortField: 'created_at', - page: defaultPage, - perPage: defaultPerPage, - ...queryParams, - ...subCaseQueryOptions, - }, - }); - - const [open, inProgress, closed] = await Promise.all([ - ...caseStatuses.map((status) => { - const { subCase: statusQueryOptions } = constructQueryOptions({ - status, - sortByField: queryParams.sortField, - }); - return caseService.findSubCaseStatusStats({ - soClient, - options: statusQueryOptions ?? {}, - ids, - }); - }), - ]); - + const client = await context.cases.getCasesClient(); return response.ok({ - body: SubCasesFindResponseRt.encode( - transformSubCases({ - page: subCases.page, - perPage: subCases.perPage, - total: subCases.total, - subCasesMap: subCases.subCasesMap, - open, - inProgress, - closed, - }) - ), + body: await client.subCases.find({ + caseID: request.params.case_id, + queryParams, + }), }); } catch (error) { logger.error( 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/cases/sub_case/get_sub_case.ts index afeaef639326d0..44ec5d68e9653f 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/sub_case/get_sub_case.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/sub_case/get_sub_case.ts @@ -7,13 +7,11 @@ import { schema } from '@kbn/config-schema'; -import { SubCaseResponseRt } from '../../../../../common/api'; import { RouteDeps } from '../../types'; -import { flattenSubCaseSavedObject, wrapError } from '../../utils'; -import { SUB_CASE_DETAILS_URL, SAVED_OBJECT_TYPES } from '../../../../../common/constants'; -import { countAlertsForID } from '../../../../common'; +import { wrapError } from '../../utils'; +import { SUB_CASE_DETAILS_URL } from '../../../../../common/constants'; -export function initGetSubCaseApi({ caseService, router, logger }: RouteDeps) { +export function initGetSubCaseApi({ router, logger }: RouteDeps) { router.get( { path: SUB_CASE_DETAILS_URL, @@ -29,47 +27,13 @@ export function initGetSubCaseApi({ caseService, router, logger }: RouteDeps) { }, async (context, request, response) => { try { - const soClient = context.core.savedObjects.getClient({ - includedHiddenTypes: SAVED_OBJECT_TYPES, - }); - const includeComments = request.query.includeComments; - - const subCase = await caseService.getSubCase({ - soClient, - id: request.params.sub_case_id, - }); - - if (!includeComments) { - return response.ok({ - body: SubCaseResponseRt.encode( - flattenSubCaseSavedObject({ - savedObject: subCase, - }) - ), - }); - } - - const theComments = await caseService.getAllSubCaseComments({ - soClient, - id: request.params.sub_case_id, - options: { - sortField: 'created_at', - sortOrder: 'asc', - }, - }); + const client = await context.cases.getCasesClient(); return response.ok({ - body: SubCaseResponseRt.encode( - flattenSubCaseSavedObject({ - savedObject: subCase, - comments: theComments.saved_objects, - totalComment: theComments.total, - totalAlerts: countAlertsForID({ - comments: theComments, - id: request.params.sub_case_id, - }), - }) - ), + body: await client.subCases.get({ + id: request.params.sub_case_id, + includeComments: request.query.includeComments, + }), }); } catch (error) { logger.error( 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/cases/sub_case/patch_sub_cases.ts index 4a407fc261a9b6..c1cd4b317da9bb 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/sub_case/patch_sub_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/sub_case/patch_sub_cases.ts @@ -5,424 +5,12 @@ * 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, - KibanaRequest, - SavedObject, - SavedObjectsFindResponse, - Logger, -} from 'kibana/server'; - -import { nodeBuilder } from '../../../../../../../../src/plugins/data/common'; -import { CasesClient } from '../../../../client'; -import { CaseService, CaseUserActionService } from '../../../../services'; -import { - CaseStatuses, - SubCasesPatchRequest, - SubCasesPatchRequestRt, - CommentType, - excess, - throwErrors, - SubCasesResponse, - SubCasePatchRequest, - SubCaseAttributes, - ESCaseAttributes, - SubCaseResponse, - SubCasesResponseRt, - User, - CommentAttributes, -} from '../../../../../common/api'; -import { - SUB_CASES_PATCH_DEL_URL, - CASE_COMMENT_SAVED_OBJECT, - SUB_CASE_SAVED_OBJECT, -} from '../../../../../common/constants'; +import { SubCasesPatchRequest } from '../../../../../common/api'; +import { SUB_CASES_PATCH_DEL_URL } from '../../../../../common/constants'; import { RouteDeps } from '../../types'; -import { - escapeHatch, - flattenSubCaseSavedObject, - isCommentRequestTypeAlertOrGenAlert, - wrapError, -} from '../../utils'; -import { getCaseToUpdate } from '../helpers'; -import { buildSubCaseUserActions } from '../../../../services/user_actions/helpers'; -import { createAlertUpdateRequest } from '../../../../common'; -import { createCaseError } from '../../../../common/error'; -import { UpdateAlertRequest } from '../../../../client/alerts/client'; - -interface UpdateArgs { - soClient: SavedObjectsClientContract; - caseService: CaseService; - userActionService: CaseUserActionService; - request: KibanaRequest; - casesClient: CasesClient; - subCases: SubCasesPatchRequest; - logger: Logger; -} - -function checkNonExistingOrConflict( - toUpdate: SubCasePatchRequest[], - fromStorage: Map> -) { - const nonExistingSubCases: SubCasePatchRequest[] = []; - const conflictedSubCases: SubCasePatchRequest[] = []; - for (const subCaseToUpdate of toUpdate) { - const bulkEntry = fromStorage.get(subCaseToUpdate.id); - - if (bulkEntry && bulkEntry.error) { - nonExistingSubCases.push(subCaseToUpdate); - } - - if (!bulkEntry || bulkEntry.version !== subCaseToUpdate.version) { - conflictedSubCases.push(subCaseToUpdate); - } - } - - if (nonExistingSubCases.length > 0) { - throw Boom.notFound( - `These sub cases ${nonExistingSubCases - .map((c) => c.id) - .join(', ')} do not exist. Please check you have the correct ids.` - ); - } - - if (conflictedSubCases.length > 0) { - throw Boom.conflict( - `These sub cases ${conflictedSubCases - .map((c) => c.id) - .join(', ')} has been updated. Please refresh before saving additional updates.` - ); - } -} - -interface GetParentIDsResult { - ids: string[]; - parentIDToSubID: Map; -} - -function getParentIDs({ - subCasesMap, - subCaseIDs, -}: { - subCasesMap: Map>; - subCaseIDs: string[]; -}): GetParentIDsResult { - return subCaseIDs.reduce( - (acc, id) => { - const subCase = subCasesMap.get(id); - if (subCase && subCase.references.length > 0) { - const parentID = subCase.references[0].id; - acc.ids.push(parentID); - let subIDs = acc.parentIDToSubID.get(parentID); - if (subIDs === undefined) { - subIDs = []; - } - subIDs.push(id); - acc.parentIDToSubID.set(parentID, subIDs); - } - return acc; - }, - { ids: [], parentIDToSubID: new Map() } - ); -} - -async function getParentCases({ - caseService, - soClient, - subCaseIDs, - subCasesMap, -}: { - caseService: CaseService; - soClient: SavedObjectsClientContract; - subCaseIDs: string[]; - subCasesMap: Map>; -}): Promise>> { - const parentIDInfo = getParentIDs({ subCaseIDs, subCasesMap }); - - const parentCases = await caseService.getCases({ - soClient, - caseIds: parentIDInfo.ids, - }); - - const parentCaseErrors = parentCases.saved_objects.filter((so) => so.error !== undefined); - - if (parentCaseErrors.length > 0) { - throw Boom.badRequest( - `Unable to find parent cases: ${parentCaseErrors - .map((c) => c.id) - .join(', ')} for sub cases: ${subCaseIDs.join(', ')}` - ); - } - - return parentCases.saved_objects.reduce((acc, so) => { - const subCaseIDsWithParent = parentIDInfo.parentIDToSubID.get(so.id); - subCaseIDsWithParent?.forEach((subCaseId) => { - acc.set(subCaseId, so); - }); - return acc; - }, new Map>()); -} - -function getValidUpdateRequests( - toUpdate: SubCasePatchRequest[], - subCasesMap: Map> -): SubCasePatchRequest[] { - const validatedSubCaseAttributes: SubCasePatchRequest[] = toUpdate.map((updateCase) => { - const currentCase = subCasesMap.get(updateCase.id); - return currentCase != null - ? getCaseToUpdate(currentCase.attributes, { - ...updateCase, - }) - : { id: updateCase.id, version: updateCase.version }; - }); - - return validatedSubCaseAttributes.filter((updateCase: SubCasePatchRequest) => { - const { id, version, ...updateCaseAttributes } = updateCase; - return Object.keys(updateCaseAttributes).length > 0; - }); -} - -/** - * Get the id from a reference in a comment for a sub case - */ -function getID(comment: SavedObject): string | undefined { - return comment.references.find((ref) => ref.type === SUB_CASE_SAVED_OBJECT)?.id; -} +import { escapeHatch, wrapError } from '../../utils'; -/** - * Get all the alert comments for a set of sub cases - */ -async function getAlertComments({ - subCasesToSync, - caseService, - soClient, -}: { - subCasesToSync: SubCasePatchRequest[]; - caseService: CaseService; - soClient: SavedObjectsClientContract; -}): Promise> { - const ids = subCasesToSync.map((subCase) => subCase.id); - return caseService.getAllSubCaseComments({ - soClient, - id: ids, - options: { - filter: nodeBuilder.or([ - nodeBuilder.is(`${CASE_COMMENT_SAVED_OBJECT}.attributes.type`, CommentType.alert), - nodeBuilder.is(`${CASE_COMMENT_SAVED_OBJECT}.attributes.type`, CommentType.generatedAlert), - ]), - }, - }); -} - -/** - * Updates the status of alerts for the specified sub cases. - */ -async function updateAlerts({ - caseService, - soClient, - casesClient, - logger, - subCasesToSync, -}: { - caseService: CaseService; - soClient: SavedObjectsClientContract; - casesClient: CasesClient; - logger: Logger; - subCasesToSync: SubCasePatchRequest[]; -}) { - try { - const subCasesToSyncMap = subCasesToSync.reduce((acc, subCase) => { - acc.set(subCase.id, subCase); - return acc; - }, new Map()); - // get all the alerts for all sub cases that need to be synced - const totalAlerts = await getAlertComments({ caseService, soClient, subCasesToSync }); - // create a map of the status (open, closed, etc) to alert info that needs to be updated - const alertsToUpdate = totalAlerts.saved_objects.reduce( - (acc: UpdateAlertRequest[], alertComment) => { - if (isCommentRequestTypeAlertOrGenAlert(alertComment.attributes)) { - const id = getID(alertComment); - const status = - id !== undefined - ? subCasesToSyncMap.get(id)?.status ?? CaseStatuses.open - : CaseStatuses.open; - - acc.push(...createAlertUpdateRequest({ comment: alertComment.attributes, status })); - } - return acc; - }, - [] - ); - - await casesClient.casesClientInternal.alerts.updateStatus({ alerts: alertsToUpdate }); - } catch (error) { - throw createCaseError({ - message: `Failed to update alert status while updating sub cases: ${JSON.stringify( - subCasesToSync - )}: ${error}`, - logger, - error, - }); - } -} - -async function update({ - soClient, - caseService, - userActionService, - request, - casesClient, - subCases, - logger, -}: UpdateArgs): Promise { - const query = pipe( - excess(SubCasesPatchRequestRt).decode(subCases), - fold(throwErrors(Boom.badRequest), identity) - ); - - try { - const bulkSubCases = await caseService.getSubCases({ - soClient, - ids: query.subCases.map((q) => q.id), - }); - - const subCasesMap = bulkSubCases.saved_objects.reduce((acc, so) => { - acc.set(so.id, so); - return acc; - }, new Map>()); - - checkNonExistingOrConflict(query.subCases, subCasesMap); - - const nonEmptySubCaseRequests = getValidUpdateRequests(query.subCases, subCasesMap); - - if (nonEmptySubCaseRequests.length <= 0) { - throw Boom.notAcceptable('All update fields are identical to current version.'); - } - - const subIDToParentCase = await getParentCases({ - soClient, - caseService, - subCaseIDs: nonEmptySubCaseRequests.map((subCase) => subCase.id), - subCasesMap, - }); - - // eslint-disable-next-line @typescript-eslint/naming-convention - const { username, full_name, email } = await caseService.getUser({ request }); - const updatedAt = new Date().toISOString(); - const updatedCases = await caseService.patchSubCases({ - soClient, - subCases: nonEmptySubCaseRequests.map((thisCase) => { - const { id: subCaseId, version, ...updateSubCaseAttributes } = thisCase; - let closedInfo: { closed_at: string | null; closed_by: User | null } = { - closed_at: null, - closed_by: null, - }; - - if ( - updateSubCaseAttributes.status && - updateSubCaseAttributes.status === CaseStatuses.closed - ) { - closedInfo = { - closed_at: updatedAt, - closed_by: { email, full_name, username }, - }; - } else if ( - updateSubCaseAttributes.status && - (updateSubCaseAttributes.status === CaseStatuses.open || - updateSubCaseAttributes.status === CaseStatuses['in-progress']) - ) { - closedInfo = { - closed_at: null, - closed_by: null, - }; - } - return { - subCaseId, - updatedAttributes: { - ...updateSubCaseAttributes, - ...closedInfo, - updated_at: updatedAt, - updated_by: { email, full_name, username }, - }, - version, - }; - }), - }); - - const subCasesToSyncAlertsFor = nonEmptySubCaseRequests.filter((subCaseToUpdate) => { - const storedSubCase = subCasesMap.get(subCaseToUpdate.id); - const parentCase = subIDToParentCase.get(subCaseToUpdate.id); - return ( - storedSubCase !== undefined && - subCaseToUpdate.status !== undefined && - storedSubCase.attributes.status !== subCaseToUpdate.status && - parentCase?.attributes.settings.syncAlerts - ); - }); - - await updateAlerts({ - caseService, - soClient, - casesClient, - subCasesToSync: subCasesToSyncAlertsFor, - logger, - }); - - const returnUpdatedSubCases = updatedCases.saved_objects.reduce( - (acc, updatedSO) => { - const originalSubCase = subCasesMap.get(updatedSO.id); - if (originalSubCase) { - acc.push( - flattenSubCaseSavedObject({ - savedObject: { - ...originalSubCase, - ...updatedSO, - attributes: { ...originalSubCase.attributes, ...updatedSO.attributes }, - references: originalSubCase.references, - version: updatedSO.version ?? originalSubCase.version, - }, - }) - ); - } - return acc; - }, - [] - ); - - await userActionService.bulkCreate({ - soClient, - actions: buildSubCaseUserActions({ - originalSubCases: bulkSubCases.saved_objects, - updatedSubCases: updatedCases.saved_objects, - actionDate: updatedAt, - actionBy: { email, full_name, username }, - }), - }); - - return SubCasesResponseRt.encode(returnUpdatedSubCases); - } catch (error) { - const idVersions = query.subCases.map((subCase) => ({ - id: subCase.id, - version: subCase.version, - })); - throw createCaseError({ - message: `Failed to update sub cases: ${JSON.stringify(idVersions)}: ${error}`, - error, - logger, - }); - } -} - -export function initPatchSubCasesApi({ - router, - caseService, - userActionService, - logger, -}: RouteDeps) { +export function initPatchSubCasesApi({ router, caseService, logger }: RouteDeps) { router.patch( { path: SUB_CASES_PATCH_DEL_URL, @@ -434,17 +22,8 @@ export function initPatchSubCasesApi({ try { const casesClient = await context.cases.getCasesClient(); const subCases = request.body as SubCasesPatchRequest; - return response.ok({ - body: await update({ - request, - subCases, - casesClient, - soClient: context.core.savedObjects.client, - caseService, - userActionService, - logger, - }), + body: await casesClient.subCases.update(subCases), }); } catch (error) { logger.error(`Failed to patch sub cases in route: ${error}`); From 34f2d86ad329d167540b94cebfc2404b65689730 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 14 Apr 2021 16:41:31 +0300 Subject: [PATCH 046/113] [Cases] RBAC: Migrate routes' unit tests to integration tests (#96374) Co-authored-by: Jonathan Buttner --- .../plugins/cases/common/api/cases/comment.ts | 1 + .../cases/common/api/cases/user_actions.ts | 1 + .../client/alerts/update_status.test.ts | 27 - .../server/client/attachments/add.test.ts | 593 --------- .../cases/server/client/cases/create.test.ts | 482 -------- .../cases/server/client/cases/create.ts | 2 +- .../cases/server/client/cases/update.test.ts | 772 ------------ x-pack/plugins/cases/server/client/client.ts | 1 - .../client/configure/get_fields.test.ts | 61 - .../client/configure/get_mappings.test.ts | 54 - .../plugins/cases/server/client/index.test.ts | 53 - x-pack/plugins/cases/server/client/mocks.ts | 170 ++- .../server/connectors/case/index.test.ts | 60 +- .../__fixtures__/create_mock_so_repository.ts | 305 ----- .../server/routes/api/__fixtures__/index.ts | 4 - .../api/__fixtures__/mock_actions_client.ts | 34 - .../routes/api/__fixtures__/mock_router.ts | 42 - .../routes/api/__fixtures__/route_contexts.ts | 95 -- .../routes/api/__mocks__/request_responses.ts | 140 +-- .../api/cases/comments/delete_comment.test.ts | 66 - .../api/cases/comments/delete_comment.ts | 4 +- .../api/cases/comments/get_comment.test.ts | 71 -- .../api/cases/comments/patch_comment.test.ts | 378 ------ .../api/cases/comments/post_comment.test.ts | 326 ----- .../api/cases/configure/get_configure.test.ts | 167 --- .../cases/configure/get_connectors.test.ts | 142 --- .../cases/configure/patch_configure.test.ts | 262 ---- .../cases/configure/post_configure.test.ts | 475 -------- .../routes/api/cases/delete_cases.test.ts | 114 -- .../routes/api/cases/find_cases.test.ts | 99 -- .../server/routes/api/cases/get_case.test.ts | 222 ---- .../routes/api/cases/patch_cases.test.ts | 415 ------- .../server/routes/api/cases/post_case.test.ts | 237 ---- .../server/routes/api/cases/push_case.test.ts | 466 -------- .../api/cases/status/get_status.test.ts | 92 -- x-pack/plugins/cases/server/services/mocks.ts | 126 +- .../case_api_integration/common/config.ts | 52 +- .../case_api_integration/common/lib/mock.ts | 58 +- .../case_api_integration/common/lib/utils.ts | 425 ++++++- .../tests/basic/cases/push_case.ts | 79 +- .../common/cases/comments/post_comment.ts | 422 ------- .../tests/common/cases/delete_cases.ts | 79 +- .../tests/common/cases/find_cases.ts | 547 +++------ .../tests/common/cases/get_case.ts | 54 +- .../tests/common/cases/patch_cases.ts | 1060 ++++++++--------- .../tests/common/cases/post_case.ts | 248 +++- .../common/cases/reporters/get_reporters.ts | 4 +- .../tests/common/cases/status/get_status.ts | 78 +- .../tests/common/cases/tags/get_tags.ts | 4 +- .../{cases => }/comments/delete_comment.ts | 125 +- .../{cases => }/comments/find_comments.ts | 14 +- .../{cases => }/comments/get_all_comments.ts | 40 +- .../{cases => }/comments/get_comment.ts | 45 +- .../common/{cases => }/comments/migrations.ts | 4 +- .../{cases => }/comments/patch_comment.ts | 382 +++--- .../tests/common/comments/post_comment.ts | 457 +++++++ .../tests/common/configure/get_configure.ts | 100 +- .../tests/common/configure/get_connectors.ts | 75 +- .../tests/common/configure/patch_configure.ts | 165 ++- .../tests/common/configure/post_configure.ts | 157 ++- .../tests/common/connectors/case.ts | 95 +- .../security_and_spaces/tests/common/index.ts | 24 +- .../{cases => }/sub_cases/delete_sub_cases.ts | 12 +- .../{cases => }/sub_cases/find_sub_cases.ts | 14 +- .../{cases => }/sub_cases/get_sub_case.ts | 18 +- .../{cases => }/sub_cases/patch_sub_cases.ts | 14 +- .../user_actions/get_all_user_actions.ts | 18 +- .../{cases => }/user_actions/migrations.ts | 4 +- .../tests/trial/cases/push_case.ts | 407 ++----- .../user_actions/get_all_user_actions.ts | 16 +- .../tests/trial/configure/get_connectors.ts | 8 +- 71 files changed, 2973 insertions(+), 8890 deletions(-) delete mode 100644 x-pack/plugins/cases/server/client/alerts/update_status.test.ts delete mode 100644 x-pack/plugins/cases/server/client/attachments/add.test.ts delete mode 100644 x-pack/plugins/cases/server/client/cases/create.test.ts delete mode 100644 x-pack/plugins/cases/server/client/cases/update.test.ts delete mode 100644 x-pack/plugins/cases/server/client/configure/get_fields.test.ts delete mode 100644 x-pack/plugins/cases/server/client/configure/get_mappings.test.ts delete mode 100644 x-pack/plugins/cases/server/client/index.test.ts delete mode 100644 x-pack/plugins/cases/server/routes/api/__fixtures__/create_mock_so_repository.ts delete mode 100644 x-pack/plugins/cases/server/routes/api/__fixtures__/mock_actions_client.ts delete mode 100644 x-pack/plugins/cases/server/routes/api/__fixtures__/mock_router.ts delete mode 100644 x-pack/plugins/cases/server/routes/api/__fixtures__/route_contexts.ts delete mode 100644 x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.test.ts delete mode 100644 x-pack/plugins/cases/server/routes/api/cases/comments/get_comment.test.ts delete mode 100644 x-pack/plugins/cases/server/routes/api/cases/comments/patch_comment.test.ts delete mode 100644 x-pack/plugins/cases/server/routes/api/cases/comments/post_comment.test.ts delete mode 100644 x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.test.ts delete mode 100644 x-pack/plugins/cases/server/routes/api/cases/configure/get_connectors.test.ts delete mode 100644 x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.test.ts delete mode 100644 x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.test.ts delete mode 100644 x-pack/plugins/cases/server/routes/api/cases/delete_cases.test.ts delete mode 100644 x-pack/plugins/cases/server/routes/api/cases/find_cases.test.ts delete mode 100644 x-pack/plugins/cases/server/routes/api/cases/get_case.test.ts delete mode 100644 x-pack/plugins/cases/server/routes/api/cases/patch_cases.test.ts delete mode 100644 x-pack/plugins/cases/server/routes/api/cases/post_case.test.ts delete mode 100644 x-pack/plugins/cases/server/routes/api/cases/push_case.test.ts delete mode 100644 x-pack/plugins/cases/server/routes/api/cases/status/get_status.test.ts delete mode 100644 x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/post_comment.ts rename x-pack/test/case_api_integration/security_and_spaces/tests/common/{cases => }/comments/delete_comment.ts (52%) rename x-pack/test/case_api_integration/security_and_spaces/tests/common/{cases => }/comments/find_comments.ts (92%) rename x-pack/test/case_api_integration/security_and_spaces/tests/common/{cases => }/comments/get_all_comments.ts (77%) rename x-pack/test/case_api_integration/security_and_spaces/tests/common/{cases => }/comments/get_comment.ts (51%) rename x-pack/test/case_api_integration/security_and_spaces/tests/common/{cases => }/comments/migrations.ts (86%) rename x-pack/test/case_api_integration/security_and_spaces/tests/common/{cases => }/comments/patch_comment.ts (51%) create mode 100644 x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts rename x-pack/test/case_api_integration/security_and_spaces/tests/common/{cases => }/sub_cases/delete_sub_cases.ts (89%) rename x-pack/test/case_api_integration/security_and_spaces/tests/common/{cases => }/sub_cases/find_sub_cases.ts (97%) rename x-pack/test/case_api_integration/security_and_spaces/tests/common/{cases => }/sub_cases/get_sub_case.ts (91%) rename x-pack/test/case_api_integration/security_and_spaces/tests/common/{cases => }/sub_cases/patch_sub_cases.ts (96%) rename x-pack/test/case_api_integration/security_and_spaces/tests/common/{cases => }/user_actions/get_all_user_actions.ts (95%) rename x-pack/test/case_api_integration/security_and_spaces/tests/common/{cases => }/user_actions/migrations.ts (90%) diff --git a/x-pack/plugins/cases/common/api/cases/comment.ts b/x-pack/plugins/cases/common/api/cases/comment.ts index 41ad0e87f14d2f..4eb2ad1eadd6cb 100644 --- a/x-pack/plugins/cases/common/api/cases/comment.ts +++ b/x-pack/plugins/cases/common/api/cases/comment.ts @@ -113,6 +113,7 @@ export const CommentsResponseRt = rt.type({ export const AllCommentsResponseRt = rt.array(CommentResponseRt); export type AttributesTypeAlerts = rt.TypeOf; +export type AttributesTypeUser = rt.TypeOf; export type CommentAttributes = rt.TypeOf; export type CommentRequest = rt.TypeOf; export type CommentResponse = rt.TypeOf; diff --git a/x-pack/plugins/cases/common/api/cases/user_actions.ts b/x-pack/plugins/cases/common/api/cases/user_actions.ts index 7188ee44efa933..55dfac391f3be3 100644 --- a/x-pack/plugins/cases/common/api/cases/user_actions.ts +++ b/x-pack/plugins/cases/common/api/cases/user_actions.ts @@ -58,6 +58,7 @@ export const CaseUserActionsResponseRt = rt.array(CaseUserActionResponseRT); export type CaseUserActionAttributes = rt.TypeOf; export type CaseUserActionsResponse = rt.TypeOf; +export type CaseUserActionResponse = rt.TypeOf; export type UserAction = rt.TypeOf; export type UserActionField = rt.TypeOf; diff --git a/x-pack/plugins/cases/server/client/alerts/update_status.test.ts b/x-pack/plugins/cases/server/client/alerts/update_status.test.ts deleted file mode 100644 index 44d6fc244270ab..00000000000000 --- a/x-pack/plugins/cases/server/client/alerts/update_status.test.ts +++ /dev/null @@ -1,27 +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 { CaseStatuses } from '../../../common/api'; -import { createMockSavedObjectsRepository } from '../../routes/api/__fixtures__'; -import { createCasesClientWithMockSavedObjectsClient } from '../mocks'; - -describe('updateAlertsStatus', () => { - it('updates the status of the alert correctly', async () => { - const savedObjectsClient = createMockSavedObjectsRepository(); - - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - await casesClient.client.updateStatus({ - alerts: [{ id: 'alert-id-1', index: '.siem-signals', status: CaseStatuses.closed }], - }); - - expect(casesClient.services.alertsService.updateAlertsStatus).toHaveBeenCalledWith({ - logger: expect.anything(), - scopedClusterClient: expect.anything(), - alerts: [{ id: 'alert-id-1', index: '.siem-signals', status: CaseStatuses.closed }], - }); - }); -}); diff --git a/x-pack/plugins/cases/server/client/attachments/add.test.ts b/x-pack/plugins/cases/server/client/attachments/add.test.ts deleted file mode 100644 index 23b7bc37dc814f..00000000000000 --- a/x-pack/plugins/cases/server/client/attachments/add.test.ts +++ /dev/null @@ -1,593 +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 { omit } from 'lodash/fp'; -import { CommentType } from '../../../common/api'; -import { isCaseError } from '../../common/error'; -import { - createMockSavedObjectsRepository, - mockCaseComments, - mockCases, -} from '../../routes/api/__fixtures__'; -import { createCasesClientWithMockSavedObjectsClient } from '../mocks'; - -type AlertComment = CommentType.alert | CommentType.generatedAlert; - -describe('addComment', () => { - beforeEach(async () => { - jest.restoreAllMocks(); - const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; - spyOnDate.mockImplementation(() => ({ - toISOString: jest.fn().mockReturnValue('2020-10-23T21:54:48.952Z'), - })); - }); - - describe('happy path', () => { - test('it adds a comment correctly', async () => { - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }); - - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - const res = await casesClient.client.addComment({ - caseId: 'mock-id-1', - comment: { - comment: 'Wow, good luck catching that bad meanie!', - type: CommentType.user, - }, - }); - - expect(res.id).toEqual('mock-id-1'); - expect(res.totalComment).toEqual(res.comments!.length); - expect(res.comments![res.comments!.length - 1]).toMatchInlineSnapshot(` - Object { - "associationType": "case", - "comment": "Wow, good luck catching that bad meanie!", - "created_at": "2020-10-23T21:54:48.952Z", - "created_by": Object { - "email": "d00d@awesome.com", - "full_name": "Awesome D00d", - "username": "awesome", - }, - "id": "mock-comment", - "pushed_at": null, - "pushed_by": null, - "type": "user", - "updated_at": null, - "updated_by": null, - "version": "WzksMV0=", - } - `); - }); - - test('it adds a comment of type alert correctly', async () => { - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }); - - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - const res = await casesClient.client.addComment({ - caseId: 'mock-id-1', - comment: { - type: CommentType.alert, - alertId: 'test-id', - index: 'test-index', - rule: { - id: 'test-rule1', - name: 'test-rule', - }, - }, - }); - - expect(res.id).toEqual('mock-id-1'); - expect(res.totalComment).toEqual(res.comments!.length); - expect(res.comments![res.comments!.length - 1]).toMatchInlineSnapshot(` - Object { - "alertId": "test-id", - "associationType": "case", - "created_at": "2020-10-23T21:54:48.952Z", - "created_by": Object { - "email": "d00d@awesome.com", - "full_name": "Awesome D00d", - "username": "awesome", - }, - "id": "mock-comment", - "index": "test-index", - "pushed_at": null, - "pushed_by": null, - "rule": Object { - "id": "test-rule1", - "name": "test-rule", - }, - "type": "alert", - "updated_at": null, - "updated_by": null, - "version": "WzksMV0=", - } - `); - }); - - test('it updates the case correctly after adding a comment', async () => { - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }); - - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - const res = await casesClient.client.addComment({ - caseId: 'mock-id-1', - comment: { - comment: 'Wow, good luck catching that bad meanie!', - type: CommentType.user, - }, - }); - - expect(res.updated_at).toEqual('2020-10-23T21:54:48.952Z'); - expect(res.updated_by).toEqual({ - email: 'd00d@awesome.com', - full_name: 'Awesome D00d', - username: 'awesome', - }); - }); - - test('it creates a user action', async () => { - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }); - - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - await casesClient.client.addComment({ - caseId: 'mock-id-1', - comment: { - comment: 'Wow, good luck catching that bad meanie!', - type: CommentType.user, - }, - }); - - expect( - casesClient.services.userActionService.postUserActions.mock.calls[0][0].actions - ).toEqual([ - { - attributes: { - action: 'create', - action_at: '2020-10-23T21:54:48.952Z', - action_by: { - email: 'd00d@awesome.com', - full_name: 'Awesome D00d', - username: 'awesome', - }, - action_field: ['comment'], - new_value: '{"comment":"Wow, good luck catching that bad meanie!","type":"user"}', - old_value: null, - }, - references: [ - { - id: 'mock-id-1', - name: 'associated-cases', - type: 'cases', - }, - { - id: 'mock-comment', - name: 'associated-cases-comments', - type: 'cases-comments', - }, - ], - }, - ]); - }); - - test('it allow user to create comments without authentications', async () => { - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }); - - const casesClient = await createCasesClientWithMockSavedObjectsClient({ - savedObjectsClient, - badAuth: true, - }); - const res = await casesClient.client.addComment({ - caseId: 'mock-id-1', - comment: { - comment: 'Wow, good luck catching that bad meanie!', - type: CommentType.user, - }, - }); - - expect(res.id).toEqual('mock-id-1'); - expect(res.comments![res.comments!.length - 1]).toMatchInlineSnapshot(` - Object { - "associationType": "case", - "comment": "Wow, good luck catching that bad meanie!", - "created_at": "2020-10-23T21:54:48.952Z", - "created_by": Object { - "email": null, - "full_name": null, - "username": null, - }, - "id": "mock-comment", - "pushed_at": null, - "pushed_by": null, - "type": "user", - "updated_at": null, - "updated_by": null, - "version": "WzksMV0=", - } - `); - }); - - test('it update the status of the alert if the case is synced with alerts', async () => { - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }); - - const casesClient = await createCasesClientWithMockSavedObjectsClient({ - savedObjectsClient, - badAuth: true, - }); - - casesClient.client.updateAlertsStatus = jest.fn(); - - await casesClient.client.addComment({ - caseId: 'mock-id-1', - comment: { - type: CommentType.alert, - alertId: 'test-alert', - index: 'test-index', - rule: { - id: 'test-rule1', - name: 'test-rule', - }, - }, - }); - - expect(casesClient.client.updateAlertsStatus).toHaveBeenCalledWith({ - alerts: [{ id: 'test-alert', index: 'test-index', status: 'open' }], - }); - }); - - test('it should NOT update the status of the alert if the case is NOT synced with alerts', async () => { - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: [ - { - ...mockCases[0], - attributes: { ...mockCases[0].attributes, settings: { syncAlerts: false } }, - }, - ], - caseCommentSavedObject: mockCaseComments, - }); - - const casesClient = await createCasesClientWithMockSavedObjectsClient({ - savedObjectsClient, - badAuth: true, - }); - - casesClient.client.updateAlertsStatus = jest.fn(); - - await casesClient.client.addComment({ - caseId: 'mock-id-1', - comment: { - type: CommentType.alert, - alertId: 'test-alert', - index: 'test-index', - rule: { - id: 'test-rule1', - name: 'test-rule', - }, - }, - }); - - expect(casesClient.client.updateAlertsStatus).not.toHaveBeenCalled(); - }); - }); - - describe('unhappy path', () => { - test('it throws when missing type', async () => { - expect.assertions(3); - - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }); - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - return casesClient.client - .addComment({ - caseId: 'mock-id-1', - // @ts-expect-error - comment: {}, - }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); - }); - }); - - test('it throws when missing attributes: type user', async () => { - expect.assertions(3); - - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }); - - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - const allRequestAttributes = { - type: CommentType.user, - comment: 'a comment', - }; - - ['comment'].forEach((attribute) => { - const requestAttributes = omit(attribute, allRequestAttributes); - return casesClient.client - .addComment({ - caseId: 'mock-id-1', - // @ts-expect-error - comment: { - ...requestAttributes, - }, - }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); - }); - }); - }); - - test('it throws when excess attributes are provided: type user', async () => { - expect.assertions(6); - - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }); - - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - - ['alertId', 'index'].forEach((attribute) => { - return casesClient.client - .addComment({ - caseId: 'mock-id-1', - comment: { - [attribute]: attribute, - comment: 'a comment', - type: CommentType.user, - }, - }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); - }); - }); - }); - - test('it throws when missing attributes: type alert', async () => { - expect.assertions(6); - - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }); - - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - const allRequestAttributes = { - type: CommentType.alert, - index: 'test-index', - alertId: 'test-id', - }; - - ['alertId', 'index'].forEach((attribute) => { - const requestAttributes = omit(attribute, allRequestAttributes); - return casesClient.client - .addComment({ - caseId: 'mock-id-1', - // @ts-expect-error - comment: { - ...requestAttributes, - }, - }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); - }); - }); - }); - - test('it throws when excess attributes are provided: type alert', async () => { - expect.assertions(3); - - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }); - - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - - ['comment'].forEach((attribute) => { - return casesClient.client - .addComment({ - caseId: 'mock-id-1', - comment: { - [attribute]: attribute, - type: CommentType.alert, - index: 'test-index', - alertId: 'test-id', - rule: { - id: 'test-rule1', - name: 'test-rule', - }, - }, - }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); - }); - }); - }); - - test('it throws when the case does not exists', async () => { - expect.assertions(4); - - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }); - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - return casesClient.client - .addComment({ - caseId: 'not-exists', - comment: { - comment: 'Wow, good luck catching that bad meanie!', - type: CommentType.user, - }, - }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(isCaseError(e)).toBeTruthy(); - const boomErr = e.boomify(); - expect(boomErr.isBoom).toBe(true); - expect(boomErr.output.statusCode).toBe(404); - }); - }); - - test('it throws when postNewCase throws', async () => { - expect.assertions(4); - - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }); - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - return casesClient.client - .addComment({ - caseId: 'mock-id-1', - comment: { - comment: 'Throw an error', - type: CommentType.user, - }, - }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(isCaseError(e)).toBeTruthy(); - const boomErr = e.boomify(); - expect(boomErr.isBoom).toBe(true); - expect(boomErr.output.statusCode).toBe(400); - }); - }); - - test('it throws when the case is closed and the comment is of type alert', async () => { - expect.assertions(4); - - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }); - - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - return casesClient.client - .addComment({ - caseId: 'mock-id-4', - comment: { - type: CommentType.alert, - alertId: 'test-alert', - index: 'test-index', - rule: { - id: 'test-rule1', - name: 'test-rule', - }, - }, - }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(isCaseError(e)).toBeTruthy(); - const boomErr = e.boomify(); - expect(boomErr.isBoom).toBe(true); - expect(boomErr.output.statusCode).toBe(400); - }); - }); - - describe('alert format', () => { - it.each([ - ['1', ['index1', 'index2'], CommentType.alert], - [['1', '2'], 'index', CommentType.alert], - ['1', ['index1', 'index2'], CommentType.generatedAlert], - [['1', '2'], 'index', CommentType.generatedAlert], - ])( - 'throws an error with an alert comment with contents id: %p indices: %p type: %s', - async (alertId, index, type) => { - expect.assertions(1); - - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }); - - const casesClient = await createCasesClientWithMockSavedObjectsClient({ - savedObjectsClient, - }); - await expect( - casesClient.client.addComment({ - caseId: 'mock-id-4', - comment: { - // casting because type must be either alert or generatedAlert but type is CommentType - type: type as AlertComment, - alertId, - index, - rule: { - id: 'test-rule1', - name: 'test-rule', - }, - }, - }) - ).rejects.toThrow(); - } - ); - - it.each([ - ['1', ['index1'], CommentType.alert], - [['1', '2'], ['index', 'other-index'], CommentType.alert], - ])( - 'does not throw an error with an alert comment with contents id: %p indices: %p type: %s', - async (alertId, index, type) => { - expect.assertions(1); - - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }); - - const casesClient = await createCasesClientWithMockSavedObjectsClient({ - savedObjectsClient, - }); - await expect( - casesClient.client.addComment({ - caseId: 'mock-id-1', - comment: { - // casting because type must be either alert or generatedAlert but type is CommentType - type: type as AlertComment, - alertId, - index, - rule: { - id: 'test-rule1', - name: 'test-rule', - }, - }, - }) - ).resolves.not.toBeUndefined(); - } - ); - }); - }); -}); diff --git a/x-pack/plugins/cases/server/client/cases/create.test.ts b/x-pack/plugins/cases/server/client/cases/create.test.ts deleted file mode 100644 index 1542b025ab96cd..00000000000000 --- a/x-pack/plugins/cases/server/client/cases/create.test.ts +++ /dev/null @@ -1,482 +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 { - ConnectorTypes, - CaseStatuses, - CaseType, - CasesClientPostRequest, -} from '../../../common/api'; -import { isCaseError } from '../../common/error'; - -import { - createMockSavedObjectsRepository, - mockCaseConfigure, - mockCases, -} from '../../routes/api/__fixtures__'; -import { createCasesClientWithMockSavedObjectsClient } from '../mocks'; - -describe('create', () => { - beforeEach(async () => { - jest.restoreAllMocks(); - const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; - spyOnDate.mockImplementation(() => ({ - toISOString: jest.fn().mockReturnValue('2019-11-25T21:54:48.952Z'), - // when we create a case we generate an ID that is used for the saved object. Internally the ID generation code - // calls Date.getTime so we need it to return something even though the inject saved object client is going to - // override it with a different ID anyway - // Otherwise we'll get an error when the function is called - getTime: jest.fn().mockReturnValue(1), - })); - }); - - describe('happy path', () => { - test('it creates the case correctly', async () => { - const postCase: CasesClientPostRequest = { - description: 'This is a brand new case of a bad meanie defacing data', - title: 'Super Bad Security Issue', - tags: ['defacement'], - type: CaseType.individual, - connector: { - id: '123', - name: 'Jira', - type: ConnectorTypes.jira, - fields: { issueType: 'Task', priority: 'High', parent: null }, - }, - settings: { - syncAlerts: true, - }, - owner: 'securitySolution', - }; - - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseConfigureSavedObject: mockCaseConfigure, - }); - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - const res = await casesClient.client.create(postCase); - - expect(res).toMatchInlineSnapshot(` - Object { - "closed_at": null, - "closed_by": null, - "comments": Array [], - "connector": Object { - "fields": Object { - "issueType": "Task", - "parent": null, - "priority": "High", - }, - "id": "123", - "name": "Jira", - "type": ".jira", - }, - "created_at": "2019-11-25T21:54:48.952Z", - "created_by": Object { - "email": "d00d@awesome.com", - "full_name": "Awesome D00d", - "username": "awesome", - }, - "description": "This is a brand new case of a bad meanie defacing data", - "external_service": null, - "id": "mock-it", - "owner": "securitySolution", - "settings": Object { - "syncAlerts": true, - }, - "status": "open", - "subCaseIds": undefined, - "subCases": undefined, - "tags": Array [ - "defacement", - ], - "title": "Super Bad Security Issue", - "totalAlerts": 0, - "totalComment": 0, - "type": "individual", - "updated_at": null, - "updated_by": null, - "version": "WzksMV0=", - } - `); - - expect( - casesClient.services.userActionService.postUserActions.mock.calls[0][0].actions - // using a snapshot here so we don't have to update the text field manually each time it changes - ).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object { - "action": "create", - "action_at": "2019-11-25T21:54:48.952Z", - "action_by": Object { - "email": "d00d@awesome.com", - "full_name": "Awesome D00d", - "username": "awesome", - }, - "action_field": Array [ - "description", - "status", - "tags", - "title", - "connector", - "settings", - ], - "new_value": "{\\"type\\":\\"individual\\",\\"description\\":\\"This is a brand new case of a bad meanie defacing data\\",\\"title\\":\\"Super Bad Security Issue\\",\\"tags\\":[\\"defacement\\"],\\"connector\\":{\\"id\\":\\"123\\",\\"name\\":\\"Jira\\",\\"type\\":\\".jira\\",\\"fields\\":{\\"issueType\\":\\"Task\\",\\"priority\\":\\"High\\",\\"parent\\":null}},\\"settings\\":{\\"syncAlerts\\":true},\\"owner\\":\\"securitySolution\\"}", - "old_value": null, - }, - "references": Array [ - Object { - "id": "mock-it", - "name": "associated-cases", - "type": "cases", - }, - ], - }, - ] - `); - }); - - test('it creates the case without connector in the configuration', async () => { - const postCase: CasesClientPostRequest = { - description: 'This is a brand new case of a bad meanie defacing data', - title: 'Super Bad Security Issue', - tags: ['defacement'], - type: CaseType.individual, - connector: { - id: 'none', - name: 'none', - type: ConnectorTypes.none, - fields: null, - }, - settings: { - syncAlerts: true, - }, - owner: 'securitySolution', - }; - - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }); - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - const res = await casesClient.client.create(postCase); - - expect(res).toMatchInlineSnapshot(` - Object { - "closed_at": null, - "closed_by": null, - "comments": Array [], - "connector": Object { - "fields": null, - "id": "none", - "name": "none", - "type": ".none", - }, - "created_at": "2019-11-25T21:54:48.952Z", - "created_by": Object { - "email": "d00d@awesome.com", - "full_name": "Awesome D00d", - "username": "awesome", - }, - "description": "This is a brand new case of a bad meanie defacing data", - "external_service": null, - "id": "mock-it", - "owner": "securitySolution", - "settings": Object { - "syncAlerts": true, - }, - "status": "open", - "subCaseIds": undefined, - "subCases": undefined, - "tags": Array [ - "defacement", - ], - "title": "Super Bad Security Issue", - "totalAlerts": 0, - "totalComment": 0, - "type": "individual", - "updated_at": null, - "updated_by": null, - "version": "WzksMV0=", - } - `); - }); - - test('Allow user to create case without authentication', async () => { - const postCase: CasesClientPostRequest = { - description: 'This is a brand new case of a bad meanie defacing data', - title: 'Super Bad Security Issue', - tags: ['defacement'], - type: CaseType.individual, - connector: { - id: 'none', - name: 'none', - type: ConnectorTypes.none, - fields: null, - }, - settings: { - syncAlerts: true, - }, - owner: 'securitySolution', - }; - - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }); - const casesClient = await createCasesClientWithMockSavedObjectsClient({ - savedObjectsClient, - badAuth: true, - }); - const res = await casesClient.client.create(postCase); - - expect(res).toMatchInlineSnapshot(` - Object { - "closed_at": null, - "closed_by": null, - "comments": Array [], - "connector": Object { - "fields": null, - "id": "none", - "name": "none", - "type": ".none", - }, - "created_at": "2019-11-25T21:54:48.952Z", - "created_by": Object { - "email": null, - "full_name": null, - "username": null, - }, - "description": "This is a brand new case of a bad meanie defacing data", - "external_service": null, - "id": "mock-it", - "owner": "securitySolution", - "settings": Object { - "syncAlerts": true, - }, - "status": "open", - "subCaseIds": undefined, - "subCases": undefined, - "tags": Array [ - "defacement", - ], - "title": "Super Bad Security Issue", - "totalAlerts": 0, - "totalComment": 0, - "type": "individual", - "updated_at": null, - "updated_by": null, - "version": "WzksMV0=", - } - `); - }); - }); - - describe('unhappy path', () => { - test('it throws when missing title', async () => { - expect.assertions(3); - const postCase = { - description: 'This is a brand new case of a bad meanie defacing data', - tags: ['defacement'], - connector: { - id: 'none', - name: 'none', - type: ConnectorTypes.none, - fields: null, - }, - }; - - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }); - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - return ( - casesClient.client - // @ts-expect-error - .create({ theCase: postCase }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); - }) - ); - }); - - test('it throws when missing description', async () => { - expect.assertions(3); - const postCase = { - title: 'a title', - tags: ['defacement'], - connector: { - id: 'none', - name: 'none', - type: ConnectorTypes.none, - fields: null, - }, - }; - - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }); - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - return ( - casesClient.client - // @ts-expect-error - .create({ theCase: postCase }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); - }) - ); - }); - - test('it throws when missing tags', async () => { - expect.assertions(3); - const postCase = { - title: 'a title', - description: 'This is a brand new case of a bad meanie defacing data', - connector: { - id: 'none', - name: 'none', - type: ConnectorTypes.none, - fields: null, - }, - }; - - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }); - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - return ( - casesClient.client - // @ts-expect-error - .create({ theCase: postCase }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); - }) - ); - }); - - test('it throws when missing connector ', async () => { - expect.assertions(3); - const postCase = { - title: 'a title', - description: 'This is a brand new case of a bad meanie defacing data', - tags: ['defacement'], - }; - - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }); - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - return ( - casesClient.client - // @ts-expect-error - .create({ theCase: postCase }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); - }) - ); - }); - - test('it throws when connector missing the right fields', async () => { - expect.assertions(3); - const postCase = { - title: 'a title', - description: 'This is a brand new case of a bad meanie defacing data', - tags: ['defacement'], - connector: { - id: '123', - name: 'Jira', - type: ConnectorTypes.jira, - fields: {}, - }, - }; - - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }); - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - return ( - casesClient.client - // @ts-expect-error - .create({ theCase: postCase }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); - }) - ); - }); - - test('it throws if you passing status for a new case', async () => { - expect.assertions(3); - const postCase = { - title: 'a title', - description: 'This is a brand new case of a bad meanie defacing data', - tags: ['defacement'], - type: CaseType.individual, - status: CaseStatuses.closed, - connector: { - id: 'none', - name: 'none', - type: ConnectorTypes.none, - fields: null, - }, - settings: { - syncAlerts: true, - }, - owner: 'securitySolution', - }; - - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }); - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - return casesClient.client.create(postCase).catch((e) => { - expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); - }); - }); - - it(`Returns an error if postNewCase throws`, async () => { - const postCase: CasesClientPostRequest = { - description: 'Throw an error', - title: 'Super Bad Security Issue', - tags: ['error'], - type: CaseType.individual, - connector: { - id: 'none', - name: 'none', - type: ConnectorTypes.none, - fields: null, - }, - settings: { - syncAlerts: true, - }, - owner: 'securitySolution', - }; - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }); - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - - return casesClient.client.create(postCase).catch((e) => { - expect(e).not.toBeNull(); - expect(isCaseError(e)).toBeTruthy(); - const boomErr = e.boomify(); - expect(boomErr.isBoom).toBe(true); - expect(boomErr.output.statusCode).toBe(400); - }); - }); - }); -}); diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index 61f36050758502..67496599d225da 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -132,7 +132,7 @@ export const create = async ({ actionAt: createdDate, actionBy: { username, full_name, email }, caseId: newCase.id, - fields: ['description', 'status', 'tags', 'title', 'connector', 'settings'], + fields: ['description', 'status', 'tags', 'title', 'connector', 'settings', 'owner'], newValue: JSON.stringify(query), }), ], diff --git a/x-pack/plugins/cases/server/client/cases/update.test.ts b/x-pack/plugins/cases/server/client/cases/update.test.ts deleted file mode 100644 index 1269545bf485c3..00000000000000 --- a/x-pack/plugins/cases/server/client/cases/update.test.ts +++ /dev/null @@ -1,772 +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 { ConnectorTypes, CasesPatchRequest, CaseStatuses } from '../../../common/api'; -import { isCaseError } from '../../common/error'; -import { - createMockSavedObjectsRepository, - mockCaseNoConnectorId, - mockCases, - mockCaseComments, -} from '../../routes/api/__fixtures__'; -import { createCasesClientWithMockSavedObjectsClient } from '../mocks'; - -describe('update', () => { - beforeEach(async () => { - jest.restoreAllMocks(); - const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; - spyOnDate.mockImplementation(() => ({ - toISOString: jest.fn().mockReturnValue('2019-11-25T21:54:48.952Z'), - })); - }); - - describe('happy path', () => { - test('it closes the case correctly', async () => { - const patchCases = { - cases: [ - { - id: 'mock-id-1', - status: CaseStatuses.closed, - version: 'WzAsMV0=', - }, - ], - }; - - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }); - - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - const res = await casesClient.client.update(patchCases); - - expect(res).toMatchInlineSnapshot(` - Array [ - Object { - "closed_at": "2019-11-25T21:54:48.952Z", - "closed_by": Object { - "email": "d00d@awesome.com", - "full_name": "Awesome D00d", - "username": "awesome", - }, - "comments": Array [], - "connector": Object { - "fields": null, - "id": "none", - "name": "none", - "type": ".none", - }, - "created_at": "2019-11-25T21:54:48.952Z", - "created_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - "description": "This is a brand new case of a bad meanie defacing data", - "external_service": null, - "id": "mock-id-1", - "owner": "securitySolution", - "settings": Object { - "syncAlerts": true, - }, - "status": "closed", - "subCaseIds": undefined, - "subCases": undefined, - "tags": Array [ - "defacement", - ], - "title": "Super Bad Security Issue", - "totalAlerts": 0, - "totalComment": 0, - "type": "individual", - "updated_at": "2019-11-25T21:54:48.952Z", - "updated_by": Object { - "email": "d00d@awesome.com", - "full_name": "Awesome D00d", - "username": "awesome", - }, - "version": "WzE3LDFd", - }, - ] - `); - - expect( - casesClient.services.userActionService.postUserActions.mock.calls[0][0].actions - ).toEqual([ - { - attributes: { - action: 'update', - action_at: '2019-11-25T21:54:48.952Z', - action_by: { - email: 'd00d@awesome.com', - full_name: 'Awesome D00d', - username: 'awesome', - }, - action_field: ['status'], - new_value: CaseStatuses.closed, - old_value: CaseStatuses.open, - }, - references: [ - { - id: 'mock-id-1', - name: 'associated-cases', - type: 'cases', - }, - ], - }, - ]); - }); - - test('it opens the case correctly', async () => { - const patchCases = { - cases: [ - { - id: 'mock-id-1', - status: CaseStatuses.open, - version: 'WzAsMV0=', - }, - ], - }; - - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: [ - { - ...mockCases[0], - attributes: { ...mockCases[0].attributes, status: CaseStatuses.closed }, - }, - ...mockCases.slice(1), - ], - }); - - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - const res = await casesClient.client.update(patchCases); - - expect(res).toMatchInlineSnapshot(` - Array [ - Object { - "closed_at": null, - "closed_by": null, - "comments": Array [], - "connector": Object { - "fields": null, - "id": "none", - "name": "none", - "type": ".none", - }, - "created_at": "2019-11-25T21:54:48.952Z", - "created_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - "description": "This is a brand new case of a bad meanie defacing data", - "external_service": null, - "id": "mock-id-1", - "owner": "securitySolution", - "settings": Object { - "syncAlerts": true, - }, - "status": "open", - "subCaseIds": undefined, - "subCases": undefined, - "tags": Array [ - "defacement", - ], - "title": "Super Bad Security Issue", - "totalAlerts": 0, - "totalComment": 0, - "type": "individual", - "updated_at": "2019-11-25T21:54:48.952Z", - "updated_by": Object { - "email": "d00d@awesome.com", - "full_name": "Awesome D00d", - "username": "awesome", - }, - "version": "WzE3LDFd", - }, - ] - `); - }); - - test('it change the status of case to in-progress correctly', async () => { - const patchCases = { - cases: [ - { - id: 'mock-id-4', - status: CaseStatuses['in-progress'], - version: 'WzUsMV0=', - }, - ], - }; - - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }); - - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - const res = await casesClient.client.update(patchCases); - - expect(res).toMatchInlineSnapshot(` - Array [ - Object { - "closed_at": null, - "closed_by": null, - "comments": Array [], - "connector": Object { - "fields": Object { - "issueType": "Task", - "parent": null, - "priority": "High", - }, - "id": "123", - "name": "My connector", - "type": ".jira", - }, - "created_at": "2019-11-25T22:32:17.947Z", - "created_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - "description": "Oh no, a bad meanie going LOLBins all over the place!", - "external_service": null, - "id": "mock-id-4", - "owner": "securitySolution", - "settings": Object { - "syncAlerts": true, - }, - "status": "in-progress", - "subCaseIds": undefined, - "subCases": undefined, - "tags": Array [ - "LOLBins", - ], - "title": "Another bad one", - "totalAlerts": 0, - "totalComment": 0, - "type": "individual", - "updated_at": "2019-11-25T21:54:48.952Z", - "updated_by": Object { - "email": "d00d@awesome.com", - "full_name": "Awesome D00d", - "username": "awesome", - }, - "version": "WzE3LDFd", - }, - ] - `); - }); - - test('it updates a case without a connector.id', async () => { - const patchCases = { - cases: [ - { - id: 'mock-no-connector_id', - status: CaseStatuses.closed, - version: 'WzAsMV0=', - }, - ], - }; - - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: [mockCaseNoConnectorId], - }); - - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - const res = await casesClient.client.update(patchCases); - - expect(res).toMatchInlineSnapshot(` - Array [ - Object { - "closed_at": "2019-11-25T21:54:48.952Z", - "closed_by": Object { - "email": "d00d@awesome.com", - "full_name": "Awesome D00d", - "username": "awesome", - }, - "comments": Array [], - "connector": Object { - "fields": null, - "id": "none", - "name": "none", - "type": ".none", - }, - "created_at": "2019-11-25T21:54:48.952Z", - "created_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - "description": "This is a brand new case of a bad meanie defacing data", - "external_service": null, - "id": "mock-no-connector_id", - "settings": Object { - "syncAlerts": true, - }, - "status": "closed", - "subCaseIds": undefined, - "subCases": undefined, - "tags": Array [ - "defacement", - ], - "title": "Super Bad Security Issue", - "totalAlerts": 0, - "totalComment": 0, - "updated_at": "2019-11-25T21:54:48.952Z", - "updated_by": Object { - "email": "d00d@awesome.com", - "full_name": "Awesome D00d", - "username": "awesome", - }, - "version": "WzE3LDFd", - }, - ] - `); - }); - - test('it updates the connector correctly', async () => { - const patchCases = ({ - cases: [ - { - id: 'mock-id-3', - connector: { - id: '456', - name: 'My connector 2', - type: ConnectorTypes.jira, - fields: { issueType: 'Bug', priority: 'Low', parent: null }, - }, - version: 'WzUsMV0=', - }, - ], - } as unknown) as CasesPatchRequest; - - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }); - - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - const res = await casesClient.client.update(patchCases); - - expect(res).toMatchInlineSnapshot(` - Array [ - Object { - "closed_at": null, - "closed_by": null, - "comments": Array [], - "connector": Object { - "fields": Object { - "issueType": "Bug", - "parent": null, - "priority": "Low", - }, - "id": "456", - "name": "My connector 2", - "type": ".jira", - }, - "created_at": "2019-11-25T22:32:17.947Z", - "created_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - "description": "Oh no, a bad meanie going LOLBins all over the place!", - "external_service": null, - "id": "mock-id-3", - "owner": "securitySolution", - "settings": Object { - "syncAlerts": true, - }, - "status": "open", - "subCaseIds": undefined, - "subCases": undefined, - "tags": Array [ - "LOLBins", - ], - "title": "Another bad one", - "totalAlerts": 0, - "totalComment": 0, - "type": "individual", - "updated_at": "2019-11-25T21:54:48.952Z", - "updated_by": Object { - "email": "d00d@awesome.com", - "full_name": "Awesome D00d", - "username": "awesome", - }, - "version": "WzE3LDFd", - }, - ] - `); - }); - - test('it updates alert status when the status is updated and syncAlerts=true', async () => { - const patchCases = { - cases: [ - { - id: 'mock-id-1', - status: CaseStatuses.closed, - version: 'WzAsMV0=', - }, - ], - }; - - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: [ - { - ...mockCaseComments[3], - references: [ - { - type: 'cases', - name: 'associated-cases', - id: 'mock-id-1', - }, - ], - }, - ], - }); - - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - casesClient.client.updateAlertsStatus = jest.fn(); - - await casesClient.client.update(patchCases); - - expect(casesClient.client.updateAlertsStatus).toHaveBeenCalledWith({ - alerts: [ - { - id: 'test-id', - index: 'test-index', - status: 'closed', - }, - ], - }); - }); - - test('it does NOT updates alert status when the status is updated and syncAlerts=false', async () => { - const patchCases = { - cases: [ - { - id: 'mock-id-1', - status: CaseStatuses.closed, - version: 'WzAsMV0=', - }, - ], - }; - - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: [ - { - ...mockCases[0], - attributes: { ...mockCases[0].attributes, settings: { syncAlerts: false } }, - }, - ], - caseCommentSavedObject: [{ ...mockCaseComments[3] }], - }); - - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - - await casesClient.client.update(patchCases); - - expect(casesClient.esClient.bulk).not.toHaveBeenCalled(); - }); - - test('it updates alert status when syncAlerts is turned on', async () => { - const patchCases = { - cases: [ - { - id: 'mock-id-1', - settings: { syncAlerts: true }, - version: 'WzAsMV0=', - }, - ], - }; - - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: [ - { - ...mockCases[0], - attributes: { ...mockCases[0].attributes, settings: { syncAlerts: false } }, - }, - ], - caseCommentSavedObject: [{ ...mockCaseComments[3] }], - }); - - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - casesClient.client.updateAlertsStatus = jest.fn(); - - await casesClient.client.update(patchCases); - - expect(casesClient.client.updateAlertsStatus).toHaveBeenCalledWith({ - alerts: [{ id: 'test-id', index: 'test-index', status: 'open' }], - }); - }); - - test('it does NOT updates alert status when syncAlerts is turned off', async () => { - const patchCases = { - cases: [ - { - id: 'mock-id-1', - settings: { syncAlerts: false }, - version: 'WzAsMV0=', - }, - ], - }; - - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: [{ ...mockCaseComments[3] }], - }); - - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - - await casesClient.client.update(patchCases); - - expect(casesClient.esClient.bulk).not.toHaveBeenCalled(); - }); - - test('it updates alert status for multiple cases', async () => { - const patchCases = { - cases: [ - { - id: 'mock-id-1', - settings: { syncAlerts: true }, - version: 'WzAsMV0=', - }, - { - id: 'mock-id-2', - status: CaseStatuses.closed, - version: 'WzQsMV0=', - }, - ], - }; - - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: [ - { - ...mockCases[0], - attributes: { ...mockCases[0].attributes, settings: { syncAlerts: false } }, - }, - { - ...mockCases[1], - }, - ], - caseCommentSavedObject: [ - { - ...mockCaseComments[3], - references: [ - { - type: 'cases', - name: 'associated-cases', - id: 'mock-id-1', - }, - ], - }, - { - ...mockCaseComments[4], - references: [ - { - type: 'cases', - name: 'associated-cases', - id: 'mock-id-2', - }, - ], - }, - ], - }); - - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - casesClient.client.updateAlertsStatus = jest.fn(); - - await casesClient.client.update(patchCases); - - expect(casesClient.client.updateAlertsStatus).toHaveBeenCalledWith({ - alerts: [ - { id: 'test-id', index: 'test-index', status: 'open' }, - { id: 'test-id-2', index: 'test-index-2', status: 'closed' }, - ], - }); - }); - - test('it does NOT call updateAlertsStatus when there is no comments of type alerts', async () => { - const patchCases = { - cases: [ - { - id: 'mock-id-1', - status: CaseStatuses.closed, - version: 'WzAsMV0=', - }, - ], - }; - - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }); - - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - - await casesClient.client.update(patchCases); - - expect(casesClient.esClient.bulk).not.toHaveBeenCalled(); - }); - }); - - describe('unhappy path', () => { - test('it throws when missing id', async () => { - expect.assertions(3); - const patchCases = { - cases: [ - { - connector: { - id: 'none', - name: 'none', - type: ConnectorTypes.none, - fields: null, - }, - version: 'WzUsMV0=', - }, - ], - }; - - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }); - - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - return ( - casesClient.client - // @ts-expect-error - .update({ cases: patchCases }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); - }) - ); - }); - - test('it throws when missing version', async () => { - expect.assertions(3); - const patchCases = { - cases: [ - { - id: 'mock-id-3', - connector: { - id: 'none', - name: 'none', - type: ConnectorTypes.none, - fields: null, - }, - }, - ], - }; - - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }); - - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - return ( - casesClient.client - // @ts-expect-error - .update({ cases: patchCases }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); - }) - ); - }); - - test('it throws when fields are identical', async () => { - expect.assertions(5); - const patchCases = { - cases: [ - { - id: 'mock-id-1', - status: CaseStatuses.open, - version: 'WzAsMV0=', - }, - ], - }; - - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }); - - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - return casesClient.client.update(patchCases).catch((e) => { - expect(e).not.toBeNull(); - expect(isCaseError(e)).toBeTruthy(); - const boomErr = e.boomify(); - expect(boomErr.isBoom).toBe(true); - expect(boomErr.output.statusCode).toBe(406); - expect(boomErr.message).toContain('All update fields are identical to current version.'); - }); - }); - - test('it throws when case does not exist', async () => { - expect.assertions(5); - const patchCases = { - cases: [ - { - id: 'not-exists', - connector: { - id: 'none', - name: 'none', - type: ConnectorTypes.none, - fields: null, - }, - version: 'WzUsMV0=', - }, - ], - }; - - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }); - - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - return casesClient.client.update(patchCases).catch((e) => { - expect(e).not.toBeNull(); - expect(isCaseError(e)).toBeTruthy(); - const boomErr = e.boomify(); - expect(boomErr.isBoom).toBe(true); - expect(boomErr.output.statusCode).toBe(404); - expect(boomErr.message).toContain( - 'These cases not-exists do not exist. Please check you have the correct ids.' - ); - }); - }); - - test('it throws when cases conflicts', async () => { - expect.assertions(5); - const patchCases = { - cases: [ - { - id: 'mock-id-1', - version: 'WzAsMV1=', - title: 'Super Bad Security Issue', - }, - ], - }; - - const savedObjectsClient = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }); - - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - return casesClient.client.update(patchCases).catch((e) => { - expect(e).not.toBeNull(); - expect(isCaseError(e)).toBeTruthy(); - const boomErr = e.boomify(); - expect(boomErr.isBoom).toBe(true); - expect(boomErr.output.statusCode).toBe(409); - expect(boomErr.message).toContain( - 'These cases mock-id-1 has been updated. Please refresh before saving additional updates.' - ); - }); - }); - }); -}); diff --git a/x-pack/plugins/cases/server/client/client.ts b/x-pack/plugins/cases/server/client/client.ts index 702329f7bcca21..cb2201b8721f2d 100644 --- a/x-pack/plugins/cases/server/client/client.ts +++ b/x-pack/plugins/cases/server/client/client.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import Boom from '@hapi/boom'; import { CasesClientArgs } from './types'; import { CasesSubClient, createCasesSubClient } from './cases/client'; diff --git a/x-pack/plugins/cases/server/client/configure/get_fields.test.ts b/x-pack/plugins/cases/server/client/configure/get_fields.test.ts deleted file mode 100644 index 2e2973516d0fd3..00000000000000 --- a/x-pack/plugins/cases/server/client/configure/get_fields.test.ts +++ /dev/null @@ -1,61 +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 { ConnectorTypes } from '../../../common/api'; - -import { createMockSavedObjectsRepository, mockCaseMappings } from '../../routes/api/__fixtures__'; -import { createCasesClientWithMockSavedObjectsClient } from '../mocks'; -import { actionsClientMock } from '../../../../actions/server/actions_client.mock'; -import { actionsErrResponse, mappings, mockGetFieldsResponse } from './mock'; -describe('get_fields', () => { - const execute = jest.fn().mockReturnValue(mockGetFieldsResponse); - const actionsMock = { ...actionsClientMock.create(), execute }; - beforeEach(async () => { - jest.clearAllMocks(); - }); - - describe('happy path', () => { - test('it gets fields', async () => { - const savedObjectsClient = createMockSavedObjectsRepository({ - caseMappingsSavedObject: mockCaseMappings, - }); - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - const res = await casesClient.client.getFields({ - actionsClient: actionsMock, - connectorType: ConnectorTypes.jira, - connectorId: '123', - }); - expect(res).toEqual({ - fields: [ - { id: 'summary', name: 'Summary', required: true, type: 'text' }, - { id: 'description', name: 'Description', required: false, type: 'text' }, - ], - defaultMappings: mappings[ConnectorTypes.jira], - }); - }); - }); - - describe('unhappy path', () => { - test('it throws error', async () => { - const savedObjectsClient = createMockSavedObjectsRepository({ - caseMappingsSavedObject: mockCaseMappings, - }); - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - await casesClient.client - .getFields({ - actionsClient: { ...actionsMock, execute: jest.fn().mockReturnValue(actionsErrResponse) }, - connectorType: ConnectorTypes.jira, - connectorId: '123', - }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(424); - }); - }); - }); -}); diff --git a/x-pack/plugins/cases/server/client/configure/get_mappings.test.ts b/x-pack/plugins/cases/server/client/configure/get_mappings.test.ts deleted file mode 100644 index 0ec2fc8b4621dc..00000000000000 --- a/x-pack/plugins/cases/server/client/configure/get_mappings.test.ts +++ /dev/null @@ -1,54 +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 { ConnectorTypes } from '../../../common/api'; - -import { createMockSavedObjectsRepository, mockCaseMappings } from '../../routes/api/__fixtures__'; -import { createCasesClientWithMockSavedObjectsClient } from '../mocks'; -import { actionsClientMock } from '../../../../actions/server/actions_client.mock'; -import { mappings, mockGetFieldsResponse } from './mock'; - -describe('get_mappings', () => { - const execute = jest.fn().mockReturnValue(mockGetFieldsResponse); - const actionsMock = { ...actionsClientMock.create(), execute }; - beforeEach(async () => { - jest.restoreAllMocks(); - const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; - spyOnDate.mockImplementation(() => ({ - toISOString: jest.fn().mockReturnValue('2019-11-25T21:54:48.952Z'), - })); - }); - - describe('happy path', () => { - test('it gets existing mappings', async () => { - const savedObjectsClient = createMockSavedObjectsRepository({ - caseMappingsSavedObject: mockCaseMappings, - }); - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - const res = await casesClient.client.getMappings({ - actionsClient: actionsMock, - connectorType: ConnectorTypes.jira, - connectorId: '123', - }); - - expect(res).toEqual(mappings[ConnectorTypes.jira]); - }); - test('it creates new mappings', async () => { - const savedObjectsClient = createMockSavedObjectsRepository({ - caseMappingsSavedObject: [], - }); - const casesClient = await createCasesClientWithMockSavedObjectsClient({ savedObjectsClient }); - const res = await casesClient.client.getMappings({ - actionsClient: actionsMock, - connectorType: ConnectorTypes.jira, - connectorId: '123', - }); - - expect(res).toEqual(mappings[ConnectorTypes.jira]); - }); - }); -}); diff --git a/x-pack/plugins/cases/server/client/index.test.ts b/x-pack/plugins/cases/server/client/index.test.ts deleted file mode 100644 index 455e4ae1066889..00000000000000 --- a/x-pack/plugins/cases/server/client/index.test.ts +++ /dev/null @@ -1,53 +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 { - elasticsearchServiceMock, - loggingSystemMock, - savedObjectsClientMock, -} from '../../../../../src/core/server/mocks'; -import { nullUser } from '../common'; -import { - connectorMappingsServiceMock, - createCaseServiceMock, - createConfigureServiceMock, - createUserActionServiceMock, - createAlertServiceMock, -} from '../services/mocks'; -import { createAuthorizationMock } from '../authorization/mock'; - -jest.mock('./client'); -import { CasesClientHandler } from './client'; -import { createExternalCasesClient } from './index'; - -const logger = loggingSystemMock.create().get('case'); -const esClient = elasticsearchServiceMock.createElasticsearchClient(); -const caseConfigureService = createConfigureServiceMock(); -const alertsService = createAlertServiceMock(); -const caseService = createCaseServiceMock(); -const connectorMappingsService = connectorMappingsServiceMock(); -const savedObjectsClient = savedObjectsClientMock.create(); -const userActionService = createUserActionServiceMock(); -const authorization = createAuthorizationMock(); - -describe('createExternalCasesClient()', () => { - test('it creates the client correctly', async () => { - createExternalCasesClient({ - scopedClusterClient: esClient, - alertsService, - caseConfigureService, - caseService, - connectorMappingsService, - user: nullUser, - savedObjectsClient, - userActionService, - logger, - authorization, - }); - expect(CasesClientHandler).toHaveBeenCalledTimes(1); - }); -}); diff --git a/x-pack/plugins/cases/server/client/mocks.ts b/x-pack/plugins/cases/server/client/mocks.ts index cf964e5e53c4fe..03ad31fc2c1bbc 100644 --- a/x-pack/plugins/cases/server/client/mocks.ts +++ b/x-pack/plugins/cases/server/client/mocks.ts @@ -5,115 +5,81 @@ * 2.0. */ -import { ElasticsearchClient, KibanaRequest } from 'kibana/server'; -import { DeeplyMockedKeys } from 'packages/kbn-utility-types/target/jest'; -import { - loggingSystemMock, - elasticsearchServiceMock, - savedObjectsServiceMock, -} from '../../../../../src/core/server/mocks'; -import { - AlertServiceContract, - CaseConfigureService, - CaseService, - CaseUserActionService, - ConnectorMappingsService, -} from '../services'; -import { CasesClient } from './types'; -import { authenticationMock } from '../routes/api/__fixtures__'; -import { featuresPluginMock } from '../../../features/server/mocks'; +import { PublicContract, PublicMethodsOf } from '@kbn/utility-types'; + +import { CasesClient, CasesClientInternal } from '.'; +import { AttachmentsSubClient } from './attachments/client'; +import { CasesSubClient } from './cases/client'; import { CasesClientFactory } from './factory'; -import { KibanaFeature } from '../../../features/common'; - -export type CasesClientPluginContractMock = jest.Mocked; -export const createExternalCasesClientMock = (): CasesClientPluginContractMock => ({ - addComment: jest.fn(), - create: jest.fn(), - get: jest.fn(), - push: jest.fn(), - getAlerts: jest.fn(), - getFields: jest.fn(), - getMappings: jest.fn(), - getUserActions: jest.fn(), - update: jest.fn(), - updateAlertsStatus: jest.fn(), - find: jest.fn(), -}); - -export const createCasesClientWithMockSavedObjectsClient = async ({ - savedObjectsClient, - badAuth = false, - omitFromContext = [], -}: { - savedObjectsClient: any; - badAuth?: boolean; - omitFromContext?: string[]; -}): Promise<{ - client: CasesClient; - services: { - userActionService: jest.Mocked; - alertsService: jest.Mocked; +import { SubCasesClient } from './sub_cases/client'; +import { UserActionsSubClient } from './user_actions/client'; + +type CasesSubClientMock = jest.Mocked; + +const createCasesSubClientMock = (): CasesSubClientMock => { + return { + create: jest.fn(), + find: jest.fn(), + get: jest.fn(), + push: jest.fn(), + update: jest.fn(), }; - esClient: DeeplyMockedKeys; -}> => { - const esClient = elasticsearchServiceMock.createElasticsearchClient(); - const log = loggingSystemMock.create().get('case'); - - const auth = badAuth ? authenticationMock.createInvalid() : authenticationMock.create(); - const caseService = new CaseService(log, auth); - const caseConfigureServicePlugin = new CaseConfigureService(log); - const connectorMappingsServicePlugin = new ConnectorMappingsService(log); - - const caseConfigureService = await caseConfigureServicePlugin.setup(); - - const connectorMappingsService = await connectorMappingsServicePlugin.setup(); - const userActionService = { - getUserActions: jest.fn(), - postUserActions: jest.fn(), +}; + +type AttachmentsSubClientMock = jest.Mocked; + +const createAttachmentsSubClientMock = (): AttachmentsSubClientMock => { + return { + add: jest.fn(), }; +}; - const alertsService = { - initialize: jest.fn(), - updateAlertsStatus: jest.fn(), - getAlerts: jest.fn(), +type UserActionsSubClientMock = jest.Mocked; + +const createUserActionsSubClientMock = (): UserActionsSubClientMock => { + return { + getAll: jest.fn(), }; +}; - // since the cases saved objects are hidden we need to use getScopedClient(), we'll just have it return the mock client - // that is passed in to createRouteContext - const savedObjectsService = savedObjectsServiceMock.createStartContract(); - savedObjectsService.getScopedClient.mockReturnValue(savedObjectsClient); - - // create a fake feature - const featureStart = featuresPluginMock.createStart(); - featureStart.getKibanaFeatures.mockReturnValue([ - // all the authorization class cares about is the `cases` field in the kibana feature so just cast it to that - ({ cases: ['securitySolution'] } as unknown) as KibanaFeature, - ]); - - const factory = new CasesClientFactory(log); - factory.initialize({ - alertsService, - caseConfigureService, - caseService, - connectorMappingsService, - userActionService, - featuresPluginStart: featureStart, - getSpace: async (req: KibanaRequest) => undefined, - // intentionally not passing the security plugin so that security will be disabled - }); - - // create a single reference to the caseClient so we can mock its methods - const casesClient = await factory.create({ - savedObjectsService, - // Since authorization is disabled for these unit tests we don't need any information from the request object - // so just pass in an empty one - request: {} as KibanaRequest, - scopedClusterClient: esClient, - }); +type SubCasesClientMock = jest.Mocked; +const createSubCasesClientMock = (): SubCasesClientMock => { return { - client: casesClient, - services: { userActionService, alertsService }, - esClient, + delete: jest.fn(), + find: jest.fn(), + get: jest.fn(), + update: jest.fn(), + }; +}; + +type CasesClientInternalMock = jest.Mocked; + +export interface CasesClientMock extends CasesClient { + cases: CasesSubClientMock; + attachments: AttachmentsSubClientMock; + userActions: UserActionsSubClientMock; + subCases: SubCasesClientMock; +} + +export const createCasesClientMock = (): CasesClientMock => { + const client: PublicContract = { + casesClientInternal: (jest.fn() as unknown) as CasesClientInternalMock, + cases: createCasesSubClientMock(), + attachments: createAttachmentsSubClientMock(), + userActions: createUserActionsSubClientMock(), + subCases: createSubCasesClientMock(), }; + return (client as unknown) as CasesClientMock; +}; + +export type CasesClientFactoryMock = jest.Mocked; + +export const createCasesClientFactory = (): CasesClientFactoryMock => { + const factory: PublicMethodsOf = { + initialize: jest.fn(), + create: jest.fn(), + }; + + return (factory as unknown) as CasesClientFactoryMock; }; diff --git a/x-pack/plugins/cases/server/connectors/case/index.test.ts b/x-pack/plugins/cases/server/connectors/case/index.test.ts index edf7e3d3fdbf17..876b8909b9317c 100644 --- a/x-pack/plugins/cases/server/connectors/case/index.test.ts +++ b/x-pack/plugins/cases/server/connectors/case/index.test.ts @@ -6,7 +6,7 @@ */ import { omit } from 'lodash/fp'; -import { KibanaRequest, Logger } from '../../../../../../src/core/server'; +import { Logger } from '../../../../../../src/core/server'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { actionsMock } from '../../../../actions/server/mocks'; import { validateParams } from '../../../../actions/server/lib'; @@ -19,52 +19,28 @@ import { CaseResponse, CasesResponse, } from '../../../common/api'; -import { - connectorMappingsServiceMock, - createCaseServiceMock, - createConfigureServiceMock, - createUserActionServiceMock, - createAlertServiceMock, -} from '../../services/mocks'; import { CaseActionType, CaseActionTypeExecutorOptions, CaseExecutorParams } from './types'; import { getActionType } from '.'; -import { createExternalCasesClientMock } from '../../client/mocks'; -import { CasesClientFactory } from '../../client/factory'; -import { featuresPluginMock } from '../../../../features/server/mocks'; -import { securityMock } from '../../../../security/server/mocks'; - -const mockCasesClient = createExternalCasesClientMock(); -jest.mock('../../client', () => ({ - createExternalCasesClient: () => mockCasesClient, -})); +import { + CasesClientMock, + createCasesClientFactory, + createCasesClientMock, +} from '../../client/mocks'; const services = actionsMock.createServices(); let caseActionType: CaseActionType; describe('case connector', () => { + let mockCasesClient: CasesClientMock; + beforeEach(() => { - jest.resetAllMocks(); const logger = loggingSystemMock.create().get() as jest.Mocked; - const caseService = createCaseServiceMock(); - const caseConfigureService = createConfigureServiceMock(); - const connectorMappingsService = connectorMappingsServiceMock(); - const userActionService = createUserActionServiceMock(); - const alertsService = createAlertServiceMock(); - const factory = new CasesClientFactory(logger); - - factory.initialize({ - alertsService, - caseConfigureService, - caseService, - connectorMappingsService, - userActionService, - featuresPluginStart: featuresPluginMock.createStart(), - getSpace: async (req: KibanaRequest) => undefined, - securityPluginSetup: securityMock.createSetup(), - securityPluginStart: securityMock.createStart(), - }); + mockCasesClient = createCasesClientMock(); + + const factory = createCasesClientFactory(); + factory.create.mockReturnValue(Promise.resolve(mockCasesClient)); caseActionType = getActionType({ logger, factory, @@ -983,7 +959,7 @@ describe('case connector', () => { owner: 'securitySolution', }; - mockCasesClient.create.mockReturnValue(Promise.resolve(createReturn)); + mockCasesClient.cases.create.mockReturnValue(Promise.resolve(createReturn)); const actionId = 'some-id'; const params: CaseExecutorParams = { @@ -1019,7 +995,7 @@ describe('case connector', () => { const result = await caseActionType.executor(executorOptions); expect(result).toEqual({ actionId, status: 'ok', data: createReturn }); - expect(mockCasesClient.create).toHaveBeenCalledWith({ + expect(mockCasesClient.cases.create).toHaveBeenCalledWith({ ...params.subActionParams, connector: { id: 'jira', @@ -1081,7 +1057,7 @@ describe('case connector', () => { }, ]; - mockCasesClient.update.mockReturnValue(Promise.resolve(updateReturn)); + mockCasesClient.cases.update.mockReturnValue(Promise.resolve(updateReturn)); const actionId = 'some-id'; const params: CaseExecutorParams = { @@ -1109,7 +1085,7 @@ describe('case connector', () => { const result = await caseActionType.executor(executorOptions); expect(result).toEqual({ actionId, status: 'ok', data: updateReturn }); - expect(mockCasesClient.update).toHaveBeenCalledWith({ + expect(mockCasesClient.cases.update).toHaveBeenCalledWith({ // Null values have been striped out. cases: [ { @@ -1171,7 +1147,7 @@ describe('case connector', () => { owner: 'securitySolution', }; - mockCasesClient.addComment.mockReturnValue(Promise.resolve(commentReturn)); + mockCasesClient.attachments.add.mockReturnValue(Promise.resolve(commentReturn)); const actionId = 'some-id'; const params: CaseExecutorParams = { @@ -1196,7 +1172,7 @@ describe('case connector', () => { const result = await caseActionType.executor(executorOptions); expect(result).toEqual({ actionId, status: 'ok', data: commentReturn }); - expect(mockCasesClient.addComment).toHaveBeenCalledWith({ + expect(mockCasesClient.attachments.add).toHaveBeenCalledWith({ caseId: 'case-id', comment: { comment: 'a comment', diff --git a/x-pack/plugins/cases/server/routes/api/__fixtures__/create_mock_so_repository.ts b/x-pack/plugins/cases/server/routes/api/__fixtures__/create_mock_so_repository.ts deleted file mode 100644 index a6acd917e4eea4..00000000000000 --- a/x-pack/plugins/cases/server/routes/api/__fixtures__/create_mock_so_repository.ts +++ /dev/null @@ -1,305 +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 { - SavedObjectsClientContract, - SavedObjectsErrorHelpers, - SavedObjectsBulkGetObject, - SavedObjectsBulkUpdateObject, - SavedObjectsFindOptions, -} from 'src/core/server'; - -import { - CASE_COMMENT_SAVED_OBJECT, - CASE_SAVED_OBJECT, - CASE_CONFIGURE_SAVED_OBJECT, - CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, - SUB_CASE_SAVED_OBJECT, - CASE_USER_ACTION_SAVED_OBJECT, -} from '../../../../common/constants'; - -export const createMockSavedObjectsRepository = ({ - caseSavedObject = [], - caseCommentSavedObject = [], - caseConfigureSavedObject = [], - caseMappingsSavedObject = [], - caseUserActionsSavedObject = [], -}: { - caseSavedObject?: any[]; - caseCommentSavedObject?: any[]; - caseConfigureSavedObject?: any[]; - caseMappingsSavedObject?: any[]; - caseUserActionsSavedObject?: any[]; -} = {}) => { - const mockSavedObjectsClientContract = ({ - bulkGet: jest.fn((objects: SavedObjectsBulkGetObject[]) => { - return { - saved_objects: objects.map(({ id, type }) => { - if (type === CASE_COMMENT_SAVED_OBJECT) { - const result = caseCommentSavedObject.filter((s) => s.id === id); - if (!result.length) { - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } - return result; - } - const result = caseSavedObject.filter((s) => s.id === id); - if (!result.length) { - return { - id, - type, - error: { - statusCode: 404, - error: 'Not Found', - message: 'Saved object [cases/not-exist] not found', - }, - }; - } - return result[0]; - }), - }; - }), - bulkCreate: jest.fn(), - bulkUpdate: jest.fn((objects: Array>) => { - return { - saved_objects: objects.map(({ id, type, attributes }) => { - if (type === CASE_COMMENT_SAVED_OBJECT) { - if (!caseCommentSavedObject.find((s) => s.id === id)) { - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } - } else if (type === CASE_SAVED_OBJECT) { - if (!caseSavedObject.find((s) => s.id === id)) { - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } - } - - return { - id, - type, - updated_at: '2019-11-22T22:50:55.191Z', - version: 'WzE3LDFd', - attributes, - }; - }), - }; - }), - get: jest.fn((type, id) => { - if (type === CASE_COMMENT_SAVED_OBJECT) { - const result = caseCommentSavedObject.filter((s) => s.id === id); - if (!result.length) { - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } - return result[0]; - } else if (type === CASE_SAVED_OBJECT) { - const result = caseSavedObject.filter((s) => s.id === id); - if (!result.length) { - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } - return result[0]; - } else { - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } - }), - find: jest.fn((findArgs: SavedObjectsFindOptions) => { - // References can be an array so we need to loop through it looking for the bad-guy - const hasReferenceIncludeBadGuy = (args: SavedObjectsFindOptions) => { - const references = args.hasReference; - if (references) { - return Array.isArray(references) - ? references.some((ref) => ref.id === 'bad-guy') - : references.id === 'bad-guy'; - } else { - return false; - } - }; - if (hasReferenceIncludeBadGuy(findArgs)) { - throw SavedObjectsErrorHelpers.createBadRequestError('Error thrown for testing'); - } - - if ( - (findArgs.type === CASE_CONFIGURE_SAVED_OBJECT && - caseConfigureSavedObject[0] && - caseConfigureSavedObject[0].id === 'throw-error-find') || - (findArgs.type === CASE_SAVED_OBJECT && - caseSavedObject[0] && - caseSavedObject[0].id === 'throw-error-find') - ) { - throw SavedObjectsErrorHelpers.createGenericNotFoundError('Error thrown for testing'); - } - if (findArgs.type === CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT && caseMappingsSavedObject[0]) { - return { - page: 1, - per_page: 5, - total: 1, - saved_objects: caseMappingsSavedObject, - }; - } - - if (findArgs.type === CASE_CONFIGURE_SAVED_OBJECT) { - return { - page: 1, - per_page: 5, - total: caseConfigureSavedObject.length, - saved_objects: caseConfigureSavedObject, - }; - } - - if (findArgs.type === CASE_COMMENT_SAVED_OBJECT) { - return { - page: 1, - per_page: 5, - total: caseCommentSavedObject.length, - saved_objects: caseCommentSavedObject, - }; - } - - // Currently not supporting sub cases in this mock library - if (findArgs.type === SUB_CASE_SAVED_OBJECT) { - return { - page: 1, - per_page: 0, - total: 0, - saved_objects: [], - }; - } - - if (findArgs.type === CASE_USER_ACTION_SAVED_OBJECT) { - return { - page: 1, - per_page: 5, - total: caseUserActionsSavedObject.length, - saved_objects: caseUserActionsSavedObject, - }; - } - - return { - page: 1, - per_page: 5, - total: caseSavedObject.length, - saved_objects: caseSavedObject, - }; - }), - create: jest.fn((type, attributes, references) => { - if (attributes.description === 'Throw an error' || attributes.comment === 'Throw an error') { - throw SavedObjectsErrorHelpers.createBadRequestError('Error thrown for testing'); - } - - if ( - type === CASE_CONFIGURE_SAVED_OBJECT && - attributes.connector.id === 'throw-error-create' - ) { - throw SavedObjectsErrorHelpers.createBadRequestError('Error thrown for testing'); - } - - if (type === CASE_COMMENT_SAVED_OBJECT) { - const newCommentObj = { - type, - id: 'mock-comment', - attributes, - ...references, - updated_at: '2019-12-02T22:48:08.327Z', - version: 'WzksMV0=', - }; - caseCommentSavedObject = [...caseCommentSavedObject, newCommentObj]; - return newCommentObj; - } - - if (type === CASE_CONFIGURE_SAVED_OBJECT) { - const newConfiguration = { - type, - id: 'mock-configuration', - attributes, - updated_at: '2020-04-09T09:43:51.778Z', - version: attributes.connector.id === 'no-version' ? undefined : 'WzksMV0=', - }; - - caseConfigureSavedObject = [newConfiguration]; - return newConfiguration; - } - - return { - type, - id: 'mock-it', - attributes, - references: [], - updated_at: '2019-12-02T22:48:08.327Z', - version: 'WzksMV0=', - }; - }), - update: jest.fn((type, id, attributes) => { - if (type === CASE_COMMENT_SAVED_OBJECT) { - const foundComment = caseCommentSavedObject.findIndex((s: { id: string }) => s.id === id); - if (foundComment === -1) { - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } - const comment = caseCommentSavedObject[foundComment]; - caseCommentSavedObject.splice(foundComment, 1, { - ...comment, - id, - type, - updated_at: '2019-11-22T22:50:55.191Z', - version: 'WzE3LDFd', - attributes: { - ...comment.attributes, - ...attributes, - }, - }); - } else if (type === CASE_SAVED_OBJECT) { - if (!caseSavedObject.find((s) => s.id === id)) { - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } - } - - if (type === CASE_CONFIGURE_SAVED_OBJECT) { - return { - id, - type, - updated_at: '2019-11-22T22:50:55.191Z', - attributes, - version: attributes.connector?.id === 'no-version' ? undefined : 'WzE3LDFd', - }; - } - - return { - id, - type, - updated_at: '2019-11-22T22:50:55.191Z', - version: 'WzE3LDFd', - attributes, - }; - }), - delete: jest.fn((type: string, id: string) => { - let result = caseSavedObject.filter((s) => s.id === id); - - if (type === CASE_COMMENT_SAVED_OBJECT) { - result = caseCommentSavedObject.filter((s) => s.id === id); - } - - if (type === CASE_CONFIGURE_SAVED_OBJECT) { - result = caseConfigureSavedObject.filter((s) => s.id === id); - } - - if (type === CASE_COMMENT_SAVED_OBJECT && id === 'bad-guy') { - throw SavedObjectsErrorHelpers.createBadRequestError('Error thrown for testing'); - } - - if (!result.length) { - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } - - if ( - type === CASE_CONFIGURE_SAVED_OBJECT && - caseConfigureSavedObject[0].id === 'throw-error-delete' - ) { - throw new Error('Error thrown for testing'); - } - return {}; - }), - deleteByNamespace: jest.fn(), - } as unknown) as jest.Mocked; - - return mockSavedObjectsClientContract; -}; diff --git a/x-pack/plugins/cases/server/routes/api/__fixtures__/index.ts b/x-pack/plugins/cases/server/routes/api/__fixtures__/index.ts index 1abd44aec15520..25f9b05471a0db 100644 --- a/x-pack/plugins/cases/server/routes/api/__fixtures__/index.ts +++ b/x-pack/plugins/cases/server/routes/api/__fixtures__/index.ts @@ -6,8 +6,4 @@ */ export * from './mock_saved_objects'; -export { createMockSavedObjectsRepository } from './create_mock_so_repository'; -export { createRouteContext } from './route_contexts'; export { authenticationMock } from './authc_mock'; -export { createRoute } from './mock_router'; -export { createActionsClient } from './mock_actions_client'; diff --git a/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_actions_client.ts b/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_actions_client.ts deleted file mode 100644 index d153c328cbb91b..00000000000000 --- a/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_actions_client.ts +++ /dev/null @@ -1,34 +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 { SavedObjectsErrorHelpers } from 'src/core/server'; -import { actionsClientMock } from '../../../../../actions/server/mocks'; -import { - getActions, - getActionTypes, - getActionExecuteResults, -} from '../__mocks__/request_responses'; - -export const createActionsClient = () => { - const actionsMock = actionsClientMock.create(); - actionsMock.getAll.mockImplementation(() => Promise.resolve(getActions())); - actionsMock.listTypes.mockImplementation(() => Promise.resolve(getActionTypes())); - actionsMock.get.mockImplementation(({ id }) => { - const actions = getActions(); - const action = actions.find((a) => a.id === id); - if (action) { - return Promise.resolve(action); - } else { - return Promise.reject(SavedObjectsErrorHelpers.createGenericNotFoundError('action', id)); - } - }); - actionsMock.execute.mockImplementation(({ actionId }) => - Promise.resolve(getActionExecuteResults(actionId)) - ); - - return actionsMock; -}; diff --git a/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_router.ts b/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_router.ts deleted file mode 100644 index 18cce1b087e5d0..00000000000000 --- a/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_router.ts +++ /dev/null @@ -1,42 +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 { loggingSystemMock, httpServiceMock } from '../../../../../../../src/core/server/mocks'; -import { CaseService, CaseConfigureService, ConnectorMappingsService } from '../../../services'; -import { authenticationMock } from '../__fixtures__'; -import { RouteDeps } from '../types'; - -export const createRoute = async ( - api: (deps: RouteDeps) => void, - method: 'get' | 'post' | 'delete' | 'patch', - badAuth = false -) => { - const httpService = httpServiceMock.createSetupContract(); - const router = httpService.createRouter(); - - const log = loggingSystemMock.create().get('cases'); - const auth = badAuth ? authenticationMock.createInvalid() : authenticationMock.create(); - const caseService = new CaseService(log, auth); - const caseConfigureServicePlugin = new CaseConfigureService(log); - const connectorMappingsServicePlugin = new ConnectorMappingsService(log); - const caseConfigureService = await caseConfigureServicePlugin.setup(); - const connectorMappingsService = await connectorMappingsServicePlugin.setup(); - - api({ - caseConfigureService, - caseService, - connectorMappingsService, - router, - userActionService: { - postUserActions: jest.fn(), - getUserActions: jest.fn(), - }, - logger: log, - }); - - return router[method].mock.calls[0][1]; -}; diff --git a/x-pack/plugins/cases/server/routes/api/__fixtures__/route_contexts.ts b/x-pack/plugins/cases/server/routes/api/__fixtures__/route_contexts.ts deleted file mode 100644 index 284b01ce993258..00000000000000 --- a/x-pack/plugins/cases/server/routes/api/__fixtures__/route_contexts.ts +++ /dev/null @@ -1,95 +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 { - elasticsearchServiceMock, - loggingSystemMock, - savedObjectsServiceMock, -} from 'src/core/server/mocks'; - -import { KibanaRequest } from 'kibana/server'; -import { - AlertService, - CaseService, - CaseConfigureService, - ConnectorMappingsService, - CaseUserActionService, -} from '../../../services'; -import { authenticationMock } from '../__fixtures__'; -import { createActionsClient } from './mock_actions_client'; -import { featuresPluginMock } from '../../../../../features/server/mocks'; -import { CasesClientFactory } from '../../../client/factory'; -import { xpackMocks } from '../../../../../../mocks'; -import { KibanaFeature } from '../../../../../features/common'; - -export const createRouteContext = async (client: any, badAuth = false) => { - const actionsMock = createActionsClient(); - - const log = loggingSystemMock.create().get('case'); - const esClient = elasticsearchServiceMock.createElasticsearchClient(); - - const authc = badAuth ? authenticationMock.createInvalid() : authenticationMock.create(); - - const caseService = new CaseService(log, authc); - const caseConfigureServicePlugin = new CaseConfigureService(log); - const connectorMappingsServicePlugin = new ConnectorMappingsService(log); - const caseUserActionsServicePlugin = new CaseUserActionService(log); - - const connectorMappingsService = await connectorMappingsServicePlugin.setup(); - const caseConfigureService = await caseConfigureServicePlugin.setup(); - const userActionService = await caseUserActionsServicePlugin.setup(); - const alertsService = new AlertService(); - - // since the cases saved objects are hidden we need to use getScopedClient(), we'll just have it return the mock client - // that is passed in to createRouteContext - const savedObjectsService = savedObjectsServiceMock.createStartContract(); - savedObjectsService.getScopedClient.mockReturnValue(client); - - const contextMock = xpackMocks.createRequestHandlerContext(); - // The tests check the calls on the saved object soClient, so we need to make sure it is the same one returned by - // getScopedClient and .client - contextMock.core.savedObjects.getClient = jest.fn(() => client); - contextMock.core.savedObjects.client = client; - - // create a fake feature - const featureStart = featuresPluginMock.createStart(); - featureStart.getKibanaFeatures.mockReturnValue([ - // all the authorization class cares about is the `cases` field in the kibana feature so just cast it to that - ({ cases: ['securitySolution'] } as unknown) as KibanaFeature, - ]); - - const factory = new CasesClientFactory(log); - factory.initialize({ - alertsService, - caseConfigureService, - caseService, - connectorMappingsService, - userActionService, - featuresPluginStart: featureStart, - getSpace: async (req: KibanaRequest) => undefined, - // intentionally not passing the security plugin so that security will be disabled - }); - - // create a single reference to the caseClient so we can mock its methods - const caseClient = await factory.create({ - savedObjectsService, - // Since authorization is disabled for these unit tests we don't need any information from the request object - // so just pass in an empty one - request: {} as KibanaRequest, - scopedClusterClient: esClient, - }); - - const context = { - ...contextMock, - actions: { getActionsClient: () => actionsMock }, - cases: { - getCasesClient: async () => caseClient, - }, - }; - - return { context, services: { userActionService } }; -}; diff --git a/x-pack/plugins/cases/server/routes/api/__mocks__/request_responses.ts b/x-pack/plugins/cases/server/routes/api/__mocks__/request_responses.ts index 7419452f27c0ad..32e42fea5c2072 100644 --- a/x-pack/plugins/cases/server/routes/api/__mocks__/request_responses.ts +++ b/x-pack/plugins/cases/server/routes/api/__mocks__/request_responses.ts @@ -5,14 +5,7 @@ * 2.0. */ -import { - ActionTypeConnector, - CasePostRequest, - CasesConfigureRequest, - ConnectorTypes, -} from '../../../../common/api'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { FindActionResult } from '../../../../../actions/server/types'; +import { CasePostRequest, ConnectorTypes } from '../../../../common/api'; export const newCase: CasePostRequest = { title: 'My new case', @@ -29,134 +22,3 @@ export const newCase: CasePostRequest = { }, owner: 'securitySolution', }; - -export const getActions = (): FindActionResult[] => [ - { - id: 'e90075a5-c386-41e3-ae21-ba4e61510695', - actionTypeId: '.webhook', - name: 'Test', - config: { - method: 'post', - url: 'https://example.com', - headers: null, - }, - isPreconfigured: false, - referencedByCount: 0, - }, - { - id: '123', - actionTypeId: '.servicenow', - name: 'ServiceNow', - config: { - apiUrl: 'https://dev102283.service-now.com', - }, - isPreconfigured: false, - referencedByCount: 0, - }, - { - id: '456', - actionTypeId: '.jira', - name: 'Connector without isCaseOwned', - config: { - apiUrl: 'https://elastic.jira.com', - }, - isPreconfigured: false, - referencedByCount: 0, - }, - { - id: '789', - actionTypeId: '.resilient', - name: 'Connector without mapping', - config: { - apiUrl: 'https://elastic.resilient.com', - }, - isPreconfigured: false, - referencedByCount: 0, - }, - { - id: 'for-mock-case-id-3', - actionTypeId: '.jira', - name: 'For mock case id 3', - config: { - apiUrl: 'https://elastic.jira.com', - }, - isPreconfigured: false, - referencedByCount: 0, - }, -]; - -export const getActionTypes = (): ActionTypeConnector[] => [ - { - id: '.email', - name: 'Email', - minimumLicenseRequired: 'gold', - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - }, - { - id: '.index', - name: 'Index', - minimumLicenseRequired: 'basic', - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - }, - { - id: '.servicenow', - name: 'ServiceNow', - minimumLicenseRequired: 'platinum', - enabled: false, - enabledInConfig: true, - enabledInLicense: true, - }, - { - id: '.jira', - name: 'Jira', - minimumLicenseRequired: 'gold', - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - }, - { - id: '.resilient', - name: 'IBM Resilient', - minimumLicenseRequired: 'platinum', - enabled: false, - enabledInConfig: true, - enabledInLicense: true, - }, -]; - -export const getActionExecuteResults = (actionId = '123') => ({ - status: 'ok' as const, - data: { - title: 'RJ2-200', - id: '10663', - pushedDate: '2020-12-17T00:32:40.738Z', - url: 'https://siem-kibana.atlassian.net/browse/RJ2-200', - comments: [], - }, - actionId, -}); - -export const newConfiguration: CasesConfigureRequest = { - connector: { - id: '456', - name: 'My connector 2', - type: ConnectorTypes.jira, - fields: null, - }, - closure_type: 'close-by-pushing', -}; - -export const executePushResponse = { - status: 'ok', - data: { - title: 'RJ2-200', - id: '10663', - pushedDate: '2020-12-17T00:32:40.738Z', - url: 'https://siem-kibana.atlassian.net/browse/RJ2-200', - comments: [], - }, -}; diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.test.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.test.ts deleted file mode 100644 index dcbcd7b9e246d3..00000000000000 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.test.ts +++ /dev/null @@ -1,66 +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 { kibanaResponseFactory, RequestHandler } from 'src/core/server'; -import { httpServerMock } from 'src/core/server/mocks'; - -import { - createMockSavedObjectsRepository, - createRoute, - createRouteContext, - mockCases, - mockCaseComments, -} from '../../__fixtures__'; -import { initDeleteCommentApi } from './delete_comment'; -import { CASE_COMMENT_DETAILS_URL } from '../../../../../common/constants'; - -describe('DELETE comment', () => { - let routeHandler: RequestHandler; - beforeAll(async () => { - routeHandler = await createRoute(initDeleteCommentApi, 'delete'); - }); - it(`deletes the comment. responds with 204`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: CASE_COMMENT_DETAILS_URL, - method: 'delete', - params: { - case_id: 'mock-id-1', - comment_id: 'mock-comment-1', - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(204); - }); - it(`returns an error when thrown from deleteComment service`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: CASE_COMMENT_DETAILS_URL, - method: 'delete', - params: { - case_id: 'mock-id-1', - comment_id: 'bad-guy', - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(404); - }); -}); diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.ts index 4818ec607cc26d..da4064f64be77f 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.ts @@ -70,9 +70,7 @@ export function initDeleteCommentApi({ const caseRef = myComment.references.find((c) => c.type === type); if (caseRef == null || (caseRef != null && caseRef.id !== id)) { - throw Boom.notFound( - `This comment ${request.params.comment_id} does not exist in ${id}).` - ); + throw Boom.notFound(`This comment ${request.params.comment_id} does not exist in ${id}.`); } await attachmentService.delete({ diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/get_comment.test.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/get_comment.test.ts deleted file mode 100644 index 8ee43eaba8a827..00000000000000 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/get_comment.test.ts +++ /dev/null @@ -1,71 +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 { kibanaResponseFactory, RequestHandler } from 'src/core/server'; -import { httpServerMock } from 'src/core/server/mocks'; - -import { - createMockSavedObjectsRepository, - createRoute, - createRouteContext, - mockCaseComments, - mockCases, -} from '../../__fixtures__'; -import { flattenCommentSavedObject } from '../../utils'; -import { initGetCommentApi } from './get_comment'; -import { CASE_COMMENT_DETAILS_URL } from '../../../../../common/constants'; - -describe('GET comment', () => { - let routeHandler: RequestHandler; - beforeAll(async () => { - routeHandler = await createRoute(initGetCommentApi, 'get'); - }); - it(`returns the comment`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: CASE_COMMENT_DETAILS_URL, - method: 'get', - params: { - case_id: 'mock-id-1', - comment_id: 'mock-comment-1', - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(200); - const myPayload = mockCaseComments.find((s) => s.id === 'mock-comment-1'); - expect(myPayload).not.toBeUndefined(); - if (myPayload != null) { - expect(response.payload).toEqual(flattenCommentSavedObject(myPayload)); - } - }); - it(`returns an error when getComment throws`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: CASE_COMMENT_DETAILS_URL, - method: 'get', - params: { - case_id: 'mock-id-1', - comment_id: 'not-real', - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseCommentSavedObject: mockCaseComments, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(404); - }); -}); diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/patch_comment.test.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/patch_comment.test.ts deleted file mode 100644 index 9cc0575f9bb94a..00000000000000 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/patch_comment.test.ts +++ /dev/null @@ -1,378 +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 { omit } from 'lodash/fp'; -import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; -import { httpServerMock } from 'src/core/server/mocks'; - -import { - createMockSavedObjectsRepository, - createRoute, - createRouteContext, - mockCaseComments, - mockCases, -} from '../../__fixtures__'; -import { initPatchCommentApi } from './patch_comment'; -import { CASE_COMMENTS_URL } from '../../../../../common/constants'; -import { CommentType } from '../../../../../common/api'; - -describe('PATCH comment', () => { - let routeHandler: RequestHandler; - beforeAll(async () => { - routeHandler = await createRoute(initPatchCommentApi, 'patch'); - }); - - it(`Patch a comment`, async () => { - const commentID = 'mock-comment-1'; - const request = httpServerMock.createKibanaRequest({ - path: CASE_COMMENTS_URL, - method: 'patch', - params: { - case_id: 'mock-id-1', - }, - body: { - type: CommentType.user, - comment: 'Update my comment', - id: commentID, - version: 'WzEsMV0=', - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(200); - const updatedComment = response.payload.comments.find( - (comment: { id: string }) => comment.id === commentID - ); - expect(updatedComment.comment).toEqual('Update my comment'); - }); - - it(`Patch an alert`, async () => { - const commentID = 'mock-comment-4'; - const request = httpServerMock.createKibanaRequest({ - path: CASE_COMMENTS_URL, - method: 'patch', - params: { - case_id: 'mock-id-4', - }, - body: { - type: CommentType.alert, - alertId: 'new-id', - index: 'test-index', - rule: { - id: 'rule-id', - name: 'rule', - }, - id: commentID, - version: 'WzYsMV0=', - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(200); - const updatedComment = response.payload.comments.find( - (comment: { id: string }) => comment.id === commentID - ); - expect(updatedComment.alertId).toEqual('new-id'); - }); - - it(`it throws when missing attributes: type user`, async () => { - const allRequestAttributes = { - type: CommentType.user, - comment: 'a comment', - }; - - for (const attribute of ['comment']) { - const requestAttributes = omit(attribute, allRequestAttributes); - const request = httpServerMock.createKibanaRequest({ - path: CASE_COMMENTS_URL, - method: 'post', - params: { - case_id: 'mock-id-1', - }, - body: requestAttributes, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(400); - expect(response.payload.isBoom).toEqual(true); - } - }); - - it(`it throws when excess attributes are provided: type user`, async () => { - for (const attribute of ['alertId', 'index']) { - const request = httpServerMock.createKibanaRequest({ - path: CASE_COMMENTS_URL, - method: 'post', - params: { - case_id: 'mock-id-1', - }, - body: { - [attribute]: attribute, - comment: 'a comment', - type: CommentType.user, - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(400); - expect(response.payload.isBoom).toEqual(true); - } - }); - - it(`it throws when missing attributes: type alert`, async () => { - const allRequestAttributes = { - type: CommentType.alert, - index: 'test-index', - alertId: 'test-id', - }; - - for (const attribute of ['alertId', 'index']) { - const requestAttributes = omit(attribute, allRequestAttributes); - const request = httpServerMock.createKibanaRequest({ - path: CASE_COMMENTS_URL, - method: 'post', - params: { - case_id: 'mock-id-1', - }, - body: requestAttributes, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(400); - expect(response.payload.isBoom).toEqual(true); - } - }); - - it(`it throws when excess attributes are provided: type alert`, async () => { - for (const attribute of ['comment']) { - const request = httpServerMock.createKibanaRequest({ - path: CASE_COMMENTS_URL, - method: 'post', - params: { - case_id: 'mock-id-1', - }, - body: { - [attribute]: attribute, - type: CommentType.alert, - index: 'test-index', - alertId: 'test-id', - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(400); - expect(response.payload.isBoom).toEqual(true); - } - }); - - it(`it fails to change the type of the comment`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: CASE_COMMENTS_URL, - method: 'patch', - params: { - case_id: 'mock-id-1', - }, - body: { - type: CommentType.alert, - alertId: 'test-id', - index: 'test-index', - rule: { - id: 'rule-id', - name: 'rule', - }, - id: 'mock-comment-1', - version: 'WzEsMV0=', - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(400); - expect(response.payload.isBoom).toEqual(true); - expect(response.payload.message).toEqual('You cannot change the type of the comment.'); - }); - - it(`Fails with 409 if version does not match`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: CASE_COMMENTS_URL, - method: 'patch', - params: { - case_id: 'mock-id-1', - }, - body: { - type: CommentType.user, - id: 'mock-comment-1', - comment: 'Update my comment', - version: 'badv=', - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(409); - }); - - it(`Returns an error if updateComment throws`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: CASE_COMMENTS_URL, - method: 'patch', - params: { - case_id: 'mock-id-1', - }, - body: { - type: CommentType.user, - comment: 'Update my comment', - id: 'mock-comment-does-not-exist', - version: 'WzEsMV0=', - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(404); - expect(response.payload.isBoom).toEqual(true); - }); - - describe('alert format', () => { - it.each([ - ['1', ['index1', 'index2'], CommentType.alert, 'mock-comment-4'], - [['1', '2'], 'index', CommentType.alert, 'mock-comment-4'], - ['1', ['index1', 'index2'], CommentType.generatedAlert, 'mock-comment-6'], - [['1', '2'], 'index', CommentType.generatedAlert, 'mock-comment-6'], - ])( - 'returns an error with an alert comment with contents id: %p indices: %p type: %s comment id: %s', - async (alertId, index, type, commentID) => { - const request = httpServerMock.createKibanaRequest({ - path: CASE_COMMENTS_URL, - method: 'patch', - params: { - case_id: 'mock-id-4', - }, - body: { - type, - alertId, - index, - rule: { - id: 'rule-id', - name: 'rule', - }, - id: commentID, - version: 'WzYsMV0=', - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(400); - } - ); - - it.each([ - ['1', ['index1'], CommentType.alert], - [['1', '2'], ['index', 'other-index'], CommentType.alert], - ])( - 'does not return an error with an alert comment with contents id: %p indices: %p type: %s', - async (alertId, index, type) => { - const request = httpServerMock.createKibanaRequest({ - path: CASE_COMMENTS_URL, - method: 'patch', - params: { - case_id: 'mock-id-4', - }, - body: { - type, - alertId, - index, - rule: { - id: 'rule-id', - name: 'rule', - }, - id: 'mock-comment-4', - // this version is different than the one in mockCaseComments because it gets updated in place - version: 'WzE3LDFd', - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(200); - } - ); - }); -}); diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/post_comment.test.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/post_comment.test.ts deleted file mode 100644 index 807ec0d089a524..00000000000000 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/post_comment.test.ts +++ /dev/null @@ -1,326 +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 { omit } from 'lodash/fp'; -import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; -import { httpServerMock } from 'src/core/server/mocks'; - -import { - createMockSavedObjectsRepository, - createRoute, - createRouteContext, - mockCases, - mockCaseComments, -} from '../../__fixtures__'; -import { initPostCommentApi } from './post_comment'; -import { CASE_COMMENTS_URL } from '../../../../../common/constants'; -import { CommentType } from '../../../../../common/api'; - -describe('POST comment', () => { - let routeHandler: RequestHandler; - beforeAll(async () => { - routeHandler = await createRoute(initPostCommentApi, 'post'); - const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; - spyOnDate.mockImplementation(() => ({ - toISOString: jest.fn().mockReturnValue('2019-11-25T21:54:48.952Z'), - })); - }); - - it(`Posts a new comment`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: CASE_COMMENTS_URL, - method: 'post', - params: { - case_id: 'mock-id-1', - }, - body: { - comment: 'Wow, good luck catching that bad meanie!', - type: CommentType.user, - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(200); - expect(response.payload.comments[response.payload.comments.length - 1].id).toEqual( - 'mock-comment' - ); - }); - - it(`Posts a new comment of type alert`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: CASE_COMMENTS_URL, - method: 'post', - params: { - case_id: 'mock-id-1', - }, - body: { - type: CommentType.alert, - alertId: 'test-id', - index: 'test-index', - rule: { - id: 'rule-id', - name: 'rule-name', - }, - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(200); - expect(response.payload.comments[response.payload.comments.length - 1].id).toEqual( - 'mock-comment' - ); - }); - - it(`it throws when missing type`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: CASE_COMMENTS_URL, - method: 'post', - params: { - case_id: 'mock-id-1', - }, - body: {}, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(400); - expect(response.payload.isBoom).toEqual(true); - }); - - it(`it throws when missing attributes: type user`, async () => { - const allRequestAttributes = { - type: CommentType.user, - comment: 'a comment', - }; - - for (const attribute of ['comment']) { - const requestAttributes = omit(attribute, allRequestAttributes); - const request = httpServerMock.createKibanaRequest({ - path: CASE_COMMENTS_URL, - method: 'post', - params: { - case_id: 'mock-id-1', - }, - body: requestAttributes, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(400); - expect(response.payload.isBoom).toEqual(true); - } - }); - - it(`it throws when excess attributes are provided: type user`, async () => { - for (const attribute of ['alertId', 'index']) { - const request = httpServerMock.createKibanaRequest({ - path: CASE_COMMENTS_URL, - method: 'post', - params: { - case_id: 'mock-id-1', - }, - body: { - [attribute]: attribute, - comment: 'a comment', - type: CommentType.user, - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(400); - expect(response.payload.isBoom).toEqual(true); - } - }); - - it(`it throws when missing attributes: type alert`, async () => { - const allRequestAttributes = { - type: CommentType.alert, - index: 'test-index', - alertId: 'test-id', - }; - - for (const attribute of ['alertId', 'index']) { - const requestAttributes = omit(attribute, allRequestAttributes); - const request = httpServerMock.createKibanaRequest({ - path: CASE_COMMENTS_URL, - method: 'post', - params: { - case_id: 'mock-id-1', - }, - body: requestAttributes, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(400); - expect(response.payload.isBoom).toEqual(true); - } - }); - - it(`it throws when excess attributes are provided: type alert`, async () => { - for (const attribute of ['comment']) { - const request = httpServerMock.createKibanaRequest({ - path: CASE_COMMENTS_URL, - method: 'post', - params: { - case_id: 'mock-id-1', - }, - body: { - [attribute]: attribute, - type: CommentType.alert, - index: 'test-index', - alertId: 'test-id', - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(400); - expect(response.payload.isBoom).toEqual(true); - } - }); - - it(`Returns an error if the case does not exist`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: CASE_COMMENTS_URL, - method: 'post', - params: { - case_id: 'this-is-not-real', - }, - body: { - comment: 'Wow, good luck catching that bad meanie!', - type: CommentType.user, - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(404); - expect(response.payload.isBoom).toEqual(true); - }); - - it(`Returns an error if postNewCase throws`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: CASE_COMMENTS_URL, - method: 'post', - params: { - case_id: 'mock-id-1', - }, - body: { - comment: 'Throw an error', - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(400); - expect(response.payload.isBoom).toEqual(true); - }); - - it(`Allow user to create comments without authentications`, async () => { - routeHandler = await createRoute(initPostCommentApi, 'post', true); - - const request = httpServerMock.createKibanaRequest({ - path: CASE_COMMENTS_URL, - method: 'post', - params: { - case_id: 'mock-id-1', - }, - body: { - comment: 'Wow, good luck catching that bad meanie!', - type: CommentType.user, - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }), - true - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(200); - expect(response.payload.comments[response.payload.comments.length - 1]).toMatchInlineSnapshot(` - Object { - "associationType": "case", - "comment": "Wow, good luck catching that bad meanie!", - "created_at": "2019-11-25T21:54:48.952Z", - "created_by": Object { - "email": null, - "full_name": null, - "username": null, - }, - "id": "mock-comment", - "pushed_at": null, - "pushed_by": null, - "type": "user", - "updated_at": null, - "updated_by": null, - "version": "WzksMV0=", - } - `); - }); -}); diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.test.ts b/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.test.ts deleted file mode 100644 index 5f6e25f6c8a6d6..00000000000000 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.test.ts +++ /dev/null @@ -1,167 +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 { kibanaResponseFactory, RequestHandler } from 'src/core/server'; -import { httpServerMock } from 'src/core/server/mocks'; - -import { - createMockSavedObjectsRepository, - createRoute, - createRouteContext, - mockCaseConfigure, - mockCaseMappings, -} from '../../__fixtures__'; - -import { initGetCaseConfigure } from './get_configure'; -import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; -import { mappings } from '../../../../client/configure/mock'; -import { ConnectorTypes } from '../../../../../common/api/connectors'; -import { CasesClient } from '../../../../client'; - -describe('GET configuration', () => { - let routeHandler: RequestHandler; - beforeAll(async () => { - routeHandler = await createRoute(initGetCaseConfigure, 'get'); - }); - - it('returns the configuration', async () => { - const req = httpServerMock.createKibanaRequest({ - path: CASE_CONFIGURE_URL, - method: 'get', - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseConfigureSavedObject: mockCaseConfigure, - caseMappingsSavedObject: mockCaseMappings, - }) - ); - - const res = await routeHandler(context, req, kibanaResponseFactory); - expect(res.status).toEqual(200); - expect(res.payload).toEqual({ - ...mockCaseConfigure[0].attributes, - error: null, - mappings: mappings[ConnectorTypes.jira], - version: mockCaseConfigure[0].version, - }); - }); - - it('handles undefined version correctly', async () => { - const req = httpServerMock.createKibanaRequest({ - path: CASE_CONFIGURE_URL, - method: 'get', - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseConfigureSavedObject: [{ ...mockCaseConfigure[0], version: undefined }], - caseMappingsSavedObject: mockCaseMappings, - }) - ); - - const res = await routeHandler(context, req, kibanaResponseFactory); - expect(res.status).toEqual(200); - expect(res.payload).toEqual({ - connector: { - id: '789', - name: 'My connector 3', - type: '.jira', - fields: null, - }, - closure_type: 'close-by-user', - created_at: '2020-04-09T09:43:51.778Z', - created_by: { - full_name: 'elastic', - email: 'testemail@elastic.co', - username: 'elastic', - }, - error: null, - mappings: mappings[ConnectorTypes.jira], - updated_at: '2020-04-09T09:43:51.778Z', - updated_by: { - full_name: 'elastic', - email: 'testemail@elastic.co', - username: 'elastic', - }, - version: '', - }); - }); - - it('returns an empty object when there is no configuration', async () => { - const req = httpServerMock.createKibanaRequest({ - path: CASE_CONFIGURE_URL, - method: 'get', - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseConfigureSavedObject: [], - }) - ); - - const res = await routeHandler(context, req, kibanaResponseFactory); - expect(res.status).toEqual(200); - - expect(res.payload).toEqual({}); - }); - - it('returns an error if find throws an error', async () => { - const req = httpServerMock.createKibanaRequest({ - path: CASE_CONFIGURE_URL, - method: 'get', - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseConfigureSavedObject: [{ ...mockCaseConfigure[0], id: 'throw-error-find' }], - }) - ); - - const res = await routeHandler(context, req, kibanaResponseFactory); - expect(res.status).toEqual(404); - expect(res.payload.isBoom).toEqual(true); - }); - - it('returns an error when mappings request throws', async () => { - const req = httpServerMock.createKibanaRequest({ - path: CASE_CONFIGURE_URL, - method: 'get', - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseConfigureSavedObject: mockCaseConfigure, - caseMappingsSavedObject: [], - }) - ); - const mockThrowContext = { - ...context, - cases: { - ...context.cases, - getCasesClient: async () => { - return ({ - ...(await context?.cases?.getCasesClient()), - getMappings: async () => { - throw new Error(); - }, - // This avoids ts errors with overriding getMappings - } as unknown) as CasesClient; - }, - }, - }; - - const res = await routeHandler(mockThrowContext, req, kibanaResponseFactory); - expect(res.status).toEqual(200); - expect(res.payload).toEqual({ - ...mockCaseConfigure[0].attributes, - error: 'Error connecting to My connector 3 instance', - mappings: [], - version: mockCaseConfigure[0].version, - }); - }); -}); diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/get_connectors.test.ts b/x-pack/plugins/cases/server/routes/api/cases/configure/get_connectors.test.ts deleted file mode 100644 index 3fa0fe2f83f79a..00000000000000 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/get_connectors.test.ts +++ /dev/null @@ -1,142 +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 { kibanaResponseFactory, RequestHandler } from 'src/core/server'; -import { httpServerMock } from 'src/core/server/mocks'; - -import { - createMockSavedObjectsRepository, - createRoute, - createRouteContext, - mockCaseConfigure, - mockCaseMappings, -} from '../../__fixtures__'; - -import { initCaseConfigureGetActionConnector } from './get_connectors'; -import { CASE_CONFIGURE_CONNECTORS_URL } from '../../../../../common/constants'; -import { getActions } from '../../__mocks__/request_responses'; - -describe('GET connectors', () => { - let routeHandler: RequestHandler; - beforeAll(async () => { - routeHandler = await createRoute(initCaseConfigureGetActionConnector, 'get'); - }); - - it('returns case owned connectors', async () => { - const req = httpServerMock.createKibanaRequest({ - path: `${CASE_CONFIGURE_CONNECTORS_URL}/_find`, - method: 'get', - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseConfigureSavedObject: mockCaseConfigure, - caseMappingsSavedObject: mockCaseMappings, - }) - ); - - const res = await routeHandler(context, req, kibanaResponseFactory); - expect(res.status).toEqual(200); - - const expected = getActions(); - // The first connector returned by getActions is of type .webhook and we expect to be filtered - expected.shift(); - expect(res.payload).toEqual(expected); - }); - - it('filters out connectors that are not enabled in license', async () => { - const req = httpServerMock.createKibanaRequest({ - path: `${CASE_CONFIGURE_CONNECTORS_URL}/_find`, - method: 'get', - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseConfigureSavedObject: mockCaseConfigure, - caseMappingsSavedObject: mockCaseMappings, - }) - ); - - const actionsClient = context.actions.getActionsClient(); - (actionsClient.listTypes as jest.Mock).mockImplementation(() => - Promise.resolve([ - { - id: '.servicenow', - name: 'ServiceNow', - minimumLicenseRequired: 'platinum', - enabled: false, - enabledInConfig: true, - // User does not have a platinum license - enabledInLicense: false, - }, - { - id: '.jira', - name: 'Jira', - minimumLicenseRequired: 'gold', - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - }, - { - id: '.resilient', - name: 'IBM Resilient', - minimumLicenseRequired: 'platinum', - enabled: false, - enabledInConfig: true, - // User does not have a platinum license - enabledInLicense: false, - }, - ]) - ); - - const res = await routeHandler(context, req, kibanaResponseFactory); - expect(res.status).toEqual(200); - expect(res.payload).toEqual([ - { - id: '456', - actionTypeId: '.jira', - name: 'Connector without isCaseOwned', - config: { - apiUrl: 'https://elastic.jira.com', - }, - isPreconfigured: false, - referencedByCount: 0, - }, - { - id: 'for-mock-case-id-3', - actionTypeId: '.jira', - name: 'For mock case id 3', - config: { - apiUrl: 'https://elastic.jira.com', - }, - isPreconfigured: false, - referencedByCount: 0, - }, - ]); - }); - - it('it throws an error when actions client is null', async () => { - const req = httpServerMock.createKibanaRequest({ - path: `${CASE_CONFIGURE_CONNECTORS_URL}/_find`, - method: 'get', - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseConfigureSavedObject: mockCaseConfigure, - caseMappingsSavedObject: mockCaseMappings, - }) - ); - - // @ts-expect-error - context.actions = undefined; - - const res = await routeHandler(context, req, kibanaResponseFactory); - expect(res.status).toEqual(404); - expect(res.payload.isBoom).toEqual(true); - }); -}); diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.test.ts b/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.test.ts deleted file mode 100644 index f94d2e462a336f..00000000000000 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.test.ts +++ /dev/null @@ -1,262 +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 { kibanaResponseFactory, RequestHandler } from 'src/core/server'; -import { httpServerMock } from 'src/core/server/mocks'; - -import { - createMockSavedObjectsRepository, - createRoute, - createRouteContext, - mockCaseMappings, -} from '../../__fixtures__'; - -import { mockCaseConfigure } from '../../__fixtures__/mock_saved_objects'; -import { initPatchCaseConfigure } from './patch_configure'; -import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; -import { ConnectorTypes } from '../../../../../common/api/connectors'; -import { CasesClient } from '../../../../client'; - -describe('PATCH configuration', () => { - let routeHandler: RequestHandler; - - beforeAll(async () => { - routeHandler = await createRoute(initPatchCaseConfigure, 'patch'); - const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; - spyOnDate.mockImplementation(() => ({ - toISOString: jest.fn().mockReturnValue('2020-04-09T09:43:51.778Z'), - })); - }); - - it('patch configuration', async () => { - const req = httpServerMock.createKibanaRequest({ - path: CASE_CONFIGURE_URL, - method: 'patch', - body: { - closure_type: 'close-by-pushing', - version: mockCaseConfigure[0].version, - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseConfigureSavedObject: mockCaseConfigure, - caseMappingsSavedObject: mockCaseMappings, - }) - ); - - const res = await routeHandler(context, req, kibanaResponseFactory); - - expect(res.status).toEqual(200); - expect(res.payload).toEqual( - expect.objectContaining({ - ...mockCaseConfigure[0].attributes, - connector: { fields: null, id: '789', name: 'My connector 3', type: '.jira' }, - closure_type: 'close-by-pushing', - updated_at: '2020-04-09T09:43:51.778Z', - updated_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' }, - version: 'WzE3LDFd', - }) - ); - }); - - it('patch configuration without authentication', async () => { - routeHandler = await createRoute(initPatchCaseConfigure, 'patch', true); - - const req = httpServerMock.createKibanaRequest({ - path: CASE_CONFIGURE_URL, - method: 'patch', - body: { - closure_type: 'close-by-pushing', - version: mockCaseConfigure[0].version, - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseConfigureSavedObject: mockCaseConfigure, - caseMappingsSavedObject: mockCaseMappings, - }) - ); - - const res = await routeHandler(context, req, kibanaResponseFactory); - - expect(res.status).toEqual(200); - expect(res.payload).toEqual( - expect.objectContaining({ - ...mockCaseConfigure[0].attributes, - connector: { fields: null, id: '789', name: 'My connector 3', type: '.jira' }, - closure_type: 'close-by-pushing', - updated_at: '2020-04-09T09:43:51.778Z', - updated_by: { email: null, full_name: null, username: null }, - version: 'WzE3LDFd', - }) - ); - }); - - it('patch configuration - connector', async () => { - routeHandler = await createRoute(initPatchCaseConfigure, 'patch'); - - const req = httpServerMock.createKibanaRequest({ - path: CASE_CONFIGURE_URL, - method: 'patch', - body: { - connector: { - id: 'connector-new', - name: 'New connector', - type: '.jira', - fields: null, - }, - version: mockCaseConfigure[0].version, - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseConfigureSavedObject: mockCaseConfigure, - caseMappingsSavedObject: mockCaseMappings, - }) - ); - - const res = await routeHandler(context, req, kibanaResponseFactory); - - expect(res.status).toEqual(200); - expect(res.payload).toEqual( - expect.objectContaining({ - ...mockCaseConfigure[0].attributes, - connector: { id: 'connector-new', name: 'New connector', type: '.jira', fields: null }, - closure_type: 'close-by-user', - updated_at: '2020-04-09T09:43:51.778Z', - updated_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' }, - version: 'WzE3LDFd', - }) - ); - }); - - it('patch configuration with error message for getMappings throw', async () => { - const req = httpServerMock.createKibanaRequest({ - path: CASE_CONFIGURE_URL, - method: 'patch', - body: { - closure_type: 'close-by-pushing', - connector: { - id: 'connector-new', - name: 'New connector', - type: '.jira', - fields: null, - }, - version: mockCaseConfigure[0].version, - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseConfigureSavedObject: mockCaseConfigure, - caseMappingsSavedObject: [], - }) - ); - const mockThrowContext = { - ...context, - cases: { - ...context.cases, - getCasesClient: async () => { - return ({ - ...(await context?.cases?.getCasesClient()), - getMappings: () => { - throw new Error(); - }, - // This avoids ts errors with overriding getMappings - } as unknown) as CasesClient; - }, - }, - }; - - const res = await routeHandler(mockThrowContext, req, kibanaResponseFactory); - - expect(res.status).toEqual(200); - expect(res.payload).toEqual( - expect.objectContaining({ - mappings: [], - error: 'Error connecting to New connector instance', - }) - ); - }); - it('throw error when configuration have not being created', async () => { - const req = httpServerMock.createKibanaRequest({ - path: CASE_CONFIGURE_URL, - method: 'patch', - body: { - closure_type: 'close-by-pushing', - version: mockCaseConfigure[0].version, - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseConfigureSavedObject: [], - }) - ); - - const res = await routeHandler(context, req, kibanaResponseFactory); - - expect(res.status).toEqual(409); - expect(res.payload.isBoom).toEqual(true); - }); - - it('throw error when the versions are different', async () => { - const req = httpServerMock.createKibanaRequest({ - path: CASE_CONFIGURE_URL, - method: 'patch', - body: { - closure_type: 'close-by-pushing', - version: 'different-version', - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseConfigureSavedObject: mockCaseConfigure, - caseMappingsSavedObject: mockCaseMappings, - }) - ); - - const res = await routeHandler(context, req, kibanaResponseFactory); - - expect(res.status).toEqual(409); - expect(res.payload.isBoom).toEqual(true); - }); - - it('handles undefined version correctly', async () => { - const req = httpServerMock.createKibanaRequest({ - path: CASE_CONFIGURE_URL, - method: 'patch', - body: { - connector: { - id: 'no-version', - name: 'no version', - type: ConnectorTypes.none, - fields: null, - }, - version: mockCaseConfigure[0].version, - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseConfigureSavedObject: mockCaseConfigure, - caseMappingsSavedObject: mockCaseMappings, - }) - ); - - const res = await routeHandler(context, req, kibanaResponseFactory); - expect(res.payload).toEqual( - expect.objectContaining({ - version: '', - }) - ); - }); -}); diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.test.ts b/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.test.ts deleted file mode 100644 index e690d9f870c343..00000000000000 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.test.ts +++ /dev/null @@ -1,475 +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 { kibanaResponseFactory, RequestHandler } from 'src/core/server'; -import { httpServerMock } from 'src/core/server/mocks'; - -import { - createMockSavedObjectsRepository, - createRoute, - createRouteContext, - mockCaseConfigure, - mockCaseMappings, -} from '../../__fixtures__'; - -import { initPostCaseConfigure } from './post_configure'; -import { newConfiguration } from '../../__mocks__/request_responses'; -import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; -import { ConnectorTypes } from '../../../../../common/api/connectors'; -import { CasesClient } from '../../../../client'; - -describe('POST configuration', () => { - let routeHandler: RequestHandler; - - beforeAll(async () => { - routeHandler = await createRoute(initPostCaseConfigure, 'post'); - const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; - spyOnDate.mockImplementation(() => ({ - toISOString: jest.fn().mockReturnValue('2020-04-09T09:43:51.778Z'), - })); - }); - - it('create configuration', async () => { - const req = httpServerMock.createKibanaRequest({ - path: CASE_CONFIGURE_URL, - method: 'post', - body: newConfiguration, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseConfigureSavedObject: mockCaseConfigure, - caseMappingsSavedObject: mockCaseMappings, - }) - ); - - const res = await routeHandler(context, req, kibanaResponseFactory); - - expect(res.status).toEqual(200); - expect(res.payload).toEqual( - expect.objectContaining({ - connector: { - id: '456', - name: 'My connector 2', - type: '.jira', - fields: null, - }, - closure_type: 'close-by-pushing', - created_at: '2020-04-09T09:43:51.778Z', - created_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' }, - updated_at: null, - updated_by: null, - }) - ); - }); - it('create configuration with error message for getMappings throw', async () => { - const req = httpServerMock.createKibanaRequest({ - path: CASE_CONFIGURE_URL, - method: 'post', - body: newConfiguration, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseConfigureSavedObject: mockCaseConfigure, - caseMappingsSavedObject: [], - }) - ); - const mockThrowContext = { - ...context, - cases: { - ...context.cases, - getCasesClient: async () => { - return ({ - ...(await context?.cases?.getCasesClient()), - getMappings: () => { - throw new Error(); - }, - // This avoids ts errors with overriding getMappings - } as unknown) as CasesClient; - }, - }, - }; - - const res = await routeHandler(mockThrowContext, req, kibanaResponseFactory); - - expect(res.status).toEqual(200); - expect(res.payload).toEqual( - expect.objectContaining({ - mappings: [], - error: 'Error connecting to My connector 2 instance', - }) - ); - }); - - it('create configuration without authentication', async () => { - routeHandler = await createRoute(initPostCaseConfigure, 'post', true); - - const req = httpServerMock.createKibanaRequest({ - path: CASE_CONFIGURE_URL, - method: 'post', - body: newConfiguration, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseConfigureSavedObject: mockCaseConfigure, - caseMappingsSavedObject: mockCaseMappings, - }) - ); - - const res = await routeHandler(context, req, kibanaResponseFactory); - - expect(res.status).toEqual(200); - expect(res.payload).toEqual( - expect.objectContaining({ - connector: { - id: '456', - name: 'My connector 2', - type: '.jira', - fields: null, - }, - closure_type: 'close-by-pushing', - created_at: '2020-04-09T09:43:51.778Z', - created_by: { email: null, full_name: null, username: null }, - updated_at: null, - updated_by: null, - }) - ); - }); - - it('throws when missing connector.id', async () => { - const req = httpServerMock.createKibanaRequest({ - path: CASE_CONFIGURE_URL, - method: 'post', - body: { - connector: { - name: 'My connector 2', - type: '.jira', - fields: null, - }, - closure_type: 'close-by-pushing', - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseConfigureSavedObject: mockCaseConfigure, - caseMappingsSavedObject: mockCaseMappings, - }) - ); - - const res = await routeHandler(context, req, kibanaResponseFactory); - expect(res.status).toEqual(400); - expect(res.payload.isBoom).toEqual(true); - }); - - it('throws when missing connector.name', async () => { - const req = httpServerMock.createKibanaRequest({ - path: CASE_CONFIGURE_URL, - method: 'post', - body: { - connector: { - id: '456', - type: '.jira', - fields: null, - }, - closure_type: 'close-by-pushing', - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseConfigureSavedObject: mockCaseConfigure, - caseMappingsSavedObject: mockCaseMappings, - }) - ); - - const res = await routeHandler(context, req, kibanaResponseFactory); - expect(res.status).toEqual(400); - expect(res.payload.isBoom).toEqual(true); - }); - - it('throws when missing connector.type', async () => { - const req = httpServerMock.createKibanaRequest({ - path: CASE_CONFIGURE_URL, - method: 'post', - body: { - connector: { - id: '456', - name: 'My connector 2', - fields: null, - }, - closure_type: 'close-by-pushing', - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseConfigureSavedObject: mockCaseConfigure, - caseMappingsSavedObject: mockCaseMappings, - }) - ); - - const res = await routeHandler(context, req, kibanaResponseFactory); - expect(res.status).toEqual(400); - expect(res.payload.isBoom).toEqual(true); - }); - - it('throws when missing connector.fields', async () => { - const req = httpServerMock.createKibanaRequest({ - path: CASE_CONFIGURE_URL, - method: 'post', - body: { - connector: { - id: '456', - name: 'My connector 2', - type: ConnectorTypes.none, - }, - closure_type: 'close-by-pushing', - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseConfigureSavedObject: mockCaseConfigure, - caseMappingsSavedObject: mockCaseMappings, - }) - ); - - const res = await routeHandler(context, req, kibanaResponseFactory); - expect(res.status).toEqual(400); - expect(res.payload.isBoom).toEqual(true); - }); - - it('throws when missing closure_type', async () => { - const req = httpServerMock.createKibanaRequest({ - path: CASE_CONFIGURE_URL, - method: 'post', - body: { - connector: { - id: '456', - name: 'My connector 2', - type: '.jira', - fields: null, - }, - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseConfigureSavedObject: mockCaseConfigure, - caseMappingsSavedObject: mockCaseMappings, - }) - ); - - const res = await routeHandler(context, req, kibanaResponseFactory); - expect(res.status).toEqual(400); - expect(res.payload.isBoom).toEqual(true); - }); - - it('it deletes the previous configuration', async () => { - const req = httpServerMock.createKibanaRequest({ - path: CASE_CONFIGURE_URL, - method: 'post', - body: newConfiguration, - }); - - const savedObjectRepository = createMockSavedObjectsRepository({ - caseConfigureSavedObject: mockCaseConfigure, - caseMappingsSavedObject: mockCaseMappings, - }); - - const { context } = await createRouteContext(savedObjectRepository); - - const res = await routeHandler(context, req, kibanaResponseFactory); - - expect(res.status).toEqual(200); - expect(savedObjectRepository.delete.mock.calls[0][1]).toBe(mockCaseConfigure[0].id); - }); - - it('it does NOT delete when not found', async () => { - const req = httpServerMock.createKibanaRequest({ - path: CASE_CONFIGURE_URL, - method: 'post', - body: newConfiguration, - }); - - const savedObjectRepository = createMockSavedObjectsRepository({ - caseConfigureSavedObject: [], - caseMappingsSavedObject: mockCaseMappings, - }); - - const { context } = await createRouteContext(savedObjectRepository); - - const res = await routeHandler(context, req, kibanaResponseFactory); - - expect(res.status).toEqual(200); - expect(savedObjectRepository.delete).not.toHaveBeenCalled(); - }); - - it('it deletes all configuration', async () => { - const req = httpServerMock.createKibanaRequest({ - path: CASE_CONFIGURE_URL, - method: 'post', - body: newConfiguration, - }); - - const savedObjectRepository = createMockSavedObjectsRepository({ - caseConfigureSavedObject: [ - mockCaseConfigure[0], - { ...mockCaseConfigure[0], id: 'mock-configuration-2' }, - ], - caseMappingsSavedObject: mockCaseMappings, - }); - - const { context } = await createRouteContext(savedObjectRepository); - - const res = await routeHandler(context, req, kibanaResponseFactory); - - expect(res.status).toEqual(200); - expect(savedObjectRepository.delete.mock.calls[0][1]).toBe(mockCaseConfigure[0].id); - expect(savedObjectRepository.delete.mock.calls[1][1]).toBe('mock-configuration-2'); - }); - - it('returns an error if find throws an error', async () => { - const req = httpServerMock.createKibanaRequest({ - path: CASE_CONFIGURE_URL, - method: 'post', - body: newConfiguration, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseConfigureSavedObject: [{ ...mockCaseConfigure[0], id: 'throw-error-find' }], - }) - ); - - const res = await routeHandler(context, req, kibanaResponseFactory); - expect(res.status).toEqual(404); - expect(res.payload.isBoom).toEqual(true); - }); - - it('returns an error if delete throws an error', async () => { - const req = httpServerMock.createKibanaRequest({ - path: CASE_CONFIGURE_URL, - method: 'post', - body: newConfiguration, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseConfigureSavedObject: [{ ...mockCaseConfigure[0], id: 'throw-error-delete' }], - }) - ); - - const res = await routeHandler(context, req, kibanaResponseFactory); - expect(res.status).toEqual(500); - expect(res.payload.isBoom).toEqual(true); - }); - - it('returns an error if post throws an error', async () => { - const req = httpServerMock.createKibanaRequest({ - path: CASE_CONFIGURE_URL, - method: 'post', - body: { - connector: { - id: 'throw-error-create', - name: 'My connector 2', - fields: null, - }, - closure_type: 'close-by-pushing', - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseConfigureSavedObject: mockCaseConfigure, - caseMappingsSavedObject: mockCaseMappings, - }) - ); - - const res = await routeHandler(context, req, kibanaResponseFactory); - expect(res.status).toEqual(400); - expect(res.payload.isBoom).toEqual(true); - }); - - it('handles undefined version correctly', async () => { - const req = httpServerMock.createKibanaRequest({ - path: CASE_CONFIGURE_URL, - method: 'post', - body: { - ...newConfiguration, - connector: { - id: 'no-version', - name: 'no version', - type: ConnectorTypes.none, - fields: null, - }, - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseConfigureSavedObject: mockCaseConfigure, - caseMappingsSavedObject: mockCaseMappings, - }) - ); - - const res = await routeHandler(context, req, kibanaResponseFactory); - expect(res.status).toEqual(200); - expect(res.payload).toEqual( - expect.objectContaining({ - version: '', - }) - ); - }); - - it('returns an error if fields are not null', async () => { - const req = httpServerMock.createKibanaRequest({ - path: CASE_CONFIGURE_URL, - method: 'post', - body: { - ...newConfiguration, - connector: { id: 'not-null', name: 'not-null', type: ConnectorTypes.none, fields: {} }, - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseConfigureSavedObject: mockCaseConfigure, - caseMappingsSavedObject: mockCaseMappings, - }) - ); - - const res = await routeHandler(context, req, kibanaResponseFactory); - expect(res.status).toEqual(400); - expect(res.payload.isBoom).toEqual(true); - }); - - it('returns an error if the type of the connector does not exists', async () => { - const req = httpServerMock.createKibanaRequest({ - path: CASE_CONFIGURE_URL, - method: 'post', - body: { - ...newConfiguration, - connector: { id: 'not-exists', name: 'not-exist', type: '.not-exists', fields: null }, - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseConfigureSavedObject: mockCaseConfigure, - caseMappingsSavedObject: mockCaseMappings, - }) - ); - - const res = await routeHandler(context, req, kibanaResponseFactory); - expect(res.status).toEqual(400); - expect(res.payload.isBoom).toEqual(true); - }); -}); diff --git a/x-pack/plugins/cases/server/routes/api/cases/delete_cases.test.ts b/x-pack/plugins/cases/server/routes/api/cases/delete_cases.test.ts deleted file mode 100644 index a441a027769bfc..00000000000000 --- a/x-pack/plugins/cases/server/routes/api/cases/delete_cases.test.ts +++ /dev/null @@ -1,114 +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 { kibanaResponseFactory, RequestHandler } from 'src/core/server'; -import { httpServerMock } from 'src/core/server/mocks'; - -import { - createMockSavedObjectsRepository, - createRoute, - createRouteContext, - mockCases, - mockCasesErrorTriggerData, - mockCaseComments, -} from '../__fixtures__'; -import { initDeleteCasesApi } from './delete_cases'; -import { CASES_URL } from '../../../../common/constants'; - -describe('DELETE case', () => { - let routeHandler: RequestHandler; - beforeAll(async () => { - routeHandler = await createRoute(initDeleteCasesApi, 'delete'); - }); - it(`deletes the case. responds with 204`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: CASES_URL, - method: 'delete', - query: { - ids: ['mock-id-1'], - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(204); - }); - it(`returns an error when thrown from deleteCase service`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: CASES_URL, - method: 'delete', - query: { - ids: ['not-real'], - }, - }); - - const mockSO = createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }); - // Adding this because the delete API needs to get all the cases first to determine if they are removable or not - // so it makes a call to bulkGet first - mockSO.bulkGet.mockImplementation(async () => ({ saved_objects: [] })); - - const { context } = await createRouteContext(mockSO); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(404); - }); - it(`returns an error when thrown from getAllCaseComments service`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: CASES_URL, - method: 'delete', - query: { - ids: ['bad-guy'], - }, - }); - - const mockSO = createMockSavedObjectsRepository({ - caseSavedObject: mockCasesErrorTriggerData, - caseCommentSavedObject: mockCaseComments, - }); - - // Adding this because the delete API needs to get all the cases first to determine if they are removable or not - // so it makes a call to bulkGet first - mockSO.bulkGet.mockImplementation(async () => ({ saved_objects: [] })); - - const { context } = await createRouteContext(mockSO); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(400); - }); - it(`returns an error when thrown from deleteComment service`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: CASES_URL, - method: 'delete', - query: { - ids: ['valid-id'], - }, - }); - - const mockSO = createMockSavedObjectsRepository({ - caseSavedObject: mockCasesErrorTriggerData, - caseCommentSavedObject: mockCasesErrorTriggerData, - }); - - // Adding this because the delete API needs to get all the cases first to determine if they are removable or not - // so it makes a call to bulkGet first - mockSO.bulkGet.mockImplementation(async () => ({ saved_objects: [] })); - - const { context } = await createRouteContext(mockSO); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(400); - }); -}); diff --git a/x-pack/plugins/cases/server/routes/api/cases/find_cases.test.ts b/x-pack/plugins/cases/server/routes/api/cases/find_cases.test.ts deleted file mode 100644 index ca9f731ca50107..00000000000000 --- a/x-pack/plugins/cases/server/routes/api/cases/find_cases.test.ts +++ /dev/null @@ -1,99 +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 { kibanaResponseFactory, RequestHandler } from 'src/core/server'; -import { httpServerMock } from 'src/core/server/mocks'; - -import { - createMockSavedObjectsRepository, - createRoute, - createRouteContext, - mockCases, -} from '../__fixtures__'; -import { initFindCasesApi } from './find_cases'; -import { CASES_URL } from '../../../../common/constants'; -import { mockCaseConfigure, mockCaseNoConnectorId } from '../__fixtures__/mock_saved_objects'; - -describe('FIND all cases', () => { - let routeHandler: RequestHandler; - beforeAll(async () => { - routeHandler = await createRoute(initFindCasesApi, 'get'); - }); - - it(`gets all the cases`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: `${CASES_URL}/_find`, - method: 'get', - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(200); - expect(response.payload.cases).toHaveLength(4); - // mockSavedObjectsRepository do not support filters and returns all cases every time. - expect(response.payload.count_open_cases).toEqual(4); - expect(response.payload.count_closed_cases).toEqual(4); - expect(response.payload.count_in_progress_cases).toEqual(4); - }); - - it(`has proper connector id on cases with configured connector`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: `${CASES_URL}/_find`, - method: 'get', - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(200); - expect(response.payload.cases[2].connector.id).toEqual('123'); - }); - - it(`adds 'none' connector id to cases without when 3rd party unconfigured`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: `${CASES_URL}/_find`, - method: 'get', - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: [mockCaseNoConnectorId], - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(200); - expect(response.payload.cases[0].connector.id).toEqual('none'); - }); - - it(`adds default connector id to cases without when 3rd party configured`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: `${CASES_URL}/_find`, - method: 'get', - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: [mockCaseNoConnectorId], - caseConfigureSavedObject: mockCaseConfigure, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(200); - expect(response.payload.cases[0].connector.id).toEqual('none'); - }); -}); diff --git a/x-pack/plugins/cases/server/routes/api/cases/get_case.test.ts b/x-pack/plugins/cases/server/routes/api/cases/get_case.test.ts deleted file mode 100644 index b9312331b4df28..00000000000000 --- a/x-pack/plugins/cases/server/routes/api/cases/get_case.test.ts +++ /dev/null @@ -1,222 +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 { kibanaResponseFactory, RequestHandler, SavedObject } from 'src/core/server'; -import { httpServerMock } from 'src/core/server/mocks'; - -import { ConnectorTypes, ESCaseAttributes } from '../../../../common/api'; -import { - createMockSavedObjectsRepository, - createRoute, - createRouteContext, - mockCases, - mockCasesErrorTriggerData, - mockCaseComments, - mockCaseNoConnectorId, - mockCaseConfigure, -} from '../__fixtures__'; -import { flattenCaseSavedObject } from '../utils'; -import { initGetCaseApi } from './get_case'; -import { CASE_DETAILS_URL } from '../../../../common/constants'; - -describe('GET case', () => { - let routeHandler: RequestHandler; - beforeAll(async () => { - routeHandler = await createRoute(initGetCaseApi, 'get'); - }); - it(`returns the case with empty case comments when includeComments is false`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: CASE_DETAILS_URL, - method: 'get', - params: { - case_id: 'mock-id-1', - }, - query: { - includeComments: false, - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - const savedObject = (mockCases.find( - (s) => s.id === 'mock-id-1' - ) as unknown) as SavedObject; - expect(response.status).toEqual(200); - expect(response.payload).toEqual( - flattenCaseSavedObject({ - savedObject, - }) - ); - expect(response.payload.comments).toEqual([]); - }); - - it(`returns an error when thrown from getCase`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: CASE_DETAILS_URL, - method: 'get', - params: { - case_id: 'abcdefg', - }, - query: { - includeComments: false, - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - - expect(response.status).toEqual(404); - expect(response.payload.isBoom).toEqual(true); - }); - - it(`returns the case with case comments when includeComments is true`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: CASE_DETAILS_URL, - method: 'get', - params: { - case_id: 'mock-id-1', - }, - query: { - includeComments: true, - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - - expect(response.status).toEqual(200); - expect(response.payload.comments).toHaveLength(6); - }); - - it(`returns an error when thrown from getAllCaseComments`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: CASE_DETAILS_URL, - method: 'get', - params: { - case_id: 'bad-guy', - }, - query: { - includeComments: true, - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCasesErrorTriggerData, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - - expect(response.status).toEqual(400); - }); - - it(`case w/o connector.id - returns the case with connector id when 3rd party unconfigured`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: CASE_DETAILS_URL, - method: 'get', - params: { - case_id: 'mock-no-connector_id', - }, - query: { - includeComments: false, - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: [mockCaseNoConnectorId], - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - - expect(response.status).toEqual(200); - expect(response.payload.connector).toEqual({ - fields: null, - id: 'none', - name: 'none', - type: ConnectorTypes.none, - }); - }); - - it(`case w/o connector.id - returns the case with connector id when 3rd party configured`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: CASE_DETAILS_URL, - method: 'get', - params: { - case_id: 'mock-no-connector_id', - }, - query: { - includeComments: false, - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: [mockCaseNoConnectorId], - caseConfigureSavedObject: mockCaseConfigure, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - - expect(response.status).toEqual(200); - expect(response.payload.connector).toEqual({ - fields: null, - id: 'none', - name: 'none', - type: '.none', - }); - }); - - it(`case w/ connector.id - returns the case with connector id when case already has connectorId`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: CASE_DETAILS_URL, - method: 'get', - params: { - case_id: 'mock-id-3', - }, - query: { - includeComments: false, - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseConfigureSavedObject: mockCaseConfigure, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - - expect(response.status).toEqual(200); - expect(response.payload.connector).toEqual({ - fields: { issueType: 'Task', priority: 'High', parent: null }, - id: '123', - name: 'My connector', - type: '.jira', - }); - }); -}); diff --git a/x-pack/plugins/cases/server/routes/api/cases/patch_cases.test.ts b/x-pack/plugins/cases/server/routes/api/cases/patch_cases.test.ts deleted file mode 100644 index 073c447460875f..00000000000000 --- a/x-pack/plugins/cases/server/routes/api/cases/patch_cases.test.ts +++ /dev/null @@ -1,415 +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 { kibanaResponseFactory, RequestHandler } from 'src/core/server'; -import { httpServerMock } from 'src/core/server/mocks'; - -import { - createMockSavedObjectsRepository, - createRoute, - createRouteContext, - mockCases, - mockCaseComments, -} from '../__fixtures__'; -import { initPatchCasesApi } from './patch_cases'; -import { mockCaseConfigure, mockCaseNoConnectorId } from '../__fixtures__/mock_saved_objects'; -import { CaseStatuses } from '../../../../common/api'; - -describe('PATCH cases', () => { - let routeHandler: RequestHandler; - beforeAll(async () => { - routeHandler = await createRoute(initPatchCasesApi, 'patch'); - const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; - spyOnDate.mockImplementation(() => ({ - toISOString: jest.fn().mockReturnValue('2019-11-25T21:54:48.952Z'), - })); - }); - - it(`Close a case`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: '/api/cases', - method: 'patch', - body: { - cases: [ - { - id: 'mock-id-1', - status: CaseStatuses.closed, - version: 'WzAsMV0=', - }, - ], - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(200); - expect(response.payload).toMatchInlineSnapshot(` - Array [ - Object { - "closed_at": "2019-11-25T21:54:48.952Z", - "closed_by": Object { - "email": "d00d@awesome.com", - "full_name": "Awesome D00d", - "username": "awesome", - }, - "comments": Array [], - "connector": Object { - "fields": null, - "id": "none", - "name": "none", - "type": ".none", - }, - "created_at": "2019-11-25T21:54:48.952Z", - "created_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - "description": "This is a brand new case of a bad meanie defacing data", - "external_service": null, - "id": "mock-id-1", - "owner": "securitySolution", - "settings": Object { - "syncAlerts": true, - }, - "status": "closed", - "subCaseIds": undefined, - "subCases": undefined, - "tags": Array [ - "defacement", - ], - "title": "Super Bad Security Issue", - "totalAlerts": 0, - "totalComment": 0, - "type": "individual", - "updated_at": "2019-11-25T21:54:48.952Z", - "updated_by": Object { - "email": "d00d@awesome.com", - "full_name": "Awesome D00d", - "username": "awesome", - }, - "version": "WzE3LDFd", - }, - ] - `); - }); - - it(`Open a case`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: '/api/cases', - method: 'patch', - body: { - cases: [ - { - id: 'mock-id-4', - status: CaseStatuses.open, - version: 'WzUsMV0=', - }, - ], - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseConfigureSavedObject: mockCaseConfigure, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(200); - expect(response.payload).toMatchInlineSnapshot(` - Array [ - Object { - "closed_at": null, - "closed_by": null, - "comments": Array [], - "connector": Object { - "fields": Object { - "issueType": "Task", - "parent": null, - "priority": "High", - }, - "id": "123", - "name": "My connector", - "type": ".jira", - }, - "created_at": "2019-11-25T22:32:17.947Z", - "created_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - "description": "Oh no, a bad meanie going LOLBins all over the place!", - "external_service": null, - "id": "mock-id-4", - "owner": "securitySolution", - "settings": Object { - "syncAlerts": true, - }, - "status": "open", - "subCaseIds": undefined, - "subCases": undefined, - "tags": Array [ - "LOLBins", - ], - "title": "Another bad one", - "totalAlerts": 0, - "totalComment": 0, - "type": "individual", - "updated_at": "2019-11-25T21:54:48.952Z", - "updated_by": Object { - "email": "d00d@awesome.com", - "full_name": "Awesome D00d", - "username": "awesome", - }, - "version": "WzE3LDFd", - }, - ] - `); - }); - - it(`Change case to in-progress`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: '/api/cases', - method: 'patch', - body: { - cases: [ - { - id: 'mock-id-1', - status: CaseStatuses['in-progress'], - version: 'WzAsMV0=', - }, - ], - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(200); - expect(response.payload).toMatchInlineSnapshot(` - Array [ - Object { - "closed_at": null, - "closed_by": null, - "comments": Array [], - "connector": Object { - "fields": null, - "id": "none", - "name": "none", - "type": ".none", - }, - "created_at": "2019-11-25T21:54:48.952Z", - "created_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - "description": "This is a brand new case of a bad meanie defacing data", - "external_service": null, - "id": "mock-id-1", - "owner": "securitySolution", - "settings": Object { - "syncAlerts": true, - }, - "status": "in-progress", - "subCaseIds": undefined, - "subCases": undefined, - "tags": Array [ - "defacement", - ], - "title": "Super Bad Security Issue", - "totalAlerts": 0, - "totalComment": 0, - "type": "individual", - "updated_at": "2019-11-25T21:54:48.952Z", - "updated_by": Object { - "email": "d00d@awesome.com", - "full_name": "Awesome D00d", - "username": "awesome", - }, - "version": "WzE3LDFd", - }, - ] - `); - }); - - it(`Patches a case without a connector.id`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: '/api/cases', - method: 'patch', - body: { - cases: [ - { - id: 'mock-no-connector_id', - status: CaseStatuses.closed, - version: 'WzAsMV0=', - }, - ], - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: [mockCaseNoConnectorId], - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(200); - expect(response.payload[0].connector.id).toEqual('none'); - }); - - it(`Patches a case with a connector.id`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: '/api/cases', - method: 'patch', - body: { - cases: [ - { - id: 'mock-id-3', - status: CaseStatuses.closed, - version: 'WzUsMV0=', - }, - ], - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(200); - expect(response.payload[0].connector.id).toEqual('123'); - }); - - it(`Change connector`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: '/api/cases', - method: 'patch', - body: { - cases: [ - { - id: 'mock-id-3', - connector: { - id: '456', - name: 'My connector 2', - type: '.jira', - fields: { issueType: 'Bug', priority: 'Low', parent: null }, - }, - version: 'WzUsMV0=', - }, - ], - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(200); - expect(response.payload[0].connector).toEqual({ - id: '456', - name: 'My connector 2', - type: '.jira', - fields: { issueType: 'Bug', priority: 'Low', parent: null }, - }); - }); - - it(`Fails with 409 if version does not match`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: '/api/cases', - method: 'patch', - body: { - cases: [ - { - id: 'mock-id-1', - case: { status: CaseStatuses.closed }, - version: 'badv=', - }, - ], - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(409); - }); - - it(`Fails with 406 if updated field is unchanged`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: '/api/cases', - method: 'patch', - body: { - cases: [ - { - id: 'mock-id-1', - case: { status: CaseStatuses.open }, - version: 'WzAsMV0=', - }, - ], - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseCommentSavedObject: mockCaseComments, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(406); - }); - - it(`Returns an error if updateCase throws`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: '/api/cases', - method: 'patch', - body: { - cases: [ - { - id: 'mock-id-does-not-exist', - status: CaseStatuses.closed, - version: 'WzAsMV0=', - }, - ], - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(404); - expect(response.payload.isBoom).toEqual(true); - }); -}); diff --git a/x-pack/plugins/cases/server/routes/api/cases/post_case.test.ts b/x-pack/plugins/cases/server/routes/api/cases/post_case.test.ts deleted file mode 100644 index 3991340612c745..00000000000000 --- a/x-pack/plugins/cases/server/routes/api/cases/post_case.test.ts +++ /dev/null @@ -1,237 +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 { kibanaResponseFactory, RequestHandler } from 'src/core/server'; -import { httpServerMock } from 'src/core/server/mocks'; - -import { - createMockSavedObjectsRepository, - createRoute, - createRouteContext, - mockCases, -} from '../__fixtures__'; -import { initPostCaseApi } from './post_case'; -import { CASES_URL } from '../../../../common/constants'; -import { mockCaseConfigure } from '../__fixtures__/mock_saved_objects'; -import { ConnectorTypes, CaseStatuses } from '../../../../common/api'; - -describe('POST cases', () => { - let routeHandler: RequestHandler; - beforeAll(async () => { - routeHandler = await createRoute(initPostCaseApi, 'post'); - const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; - spyOnDate.mockImplementation(() => ({ - toISOString: jest.fn().mockReturnValue('2019-11-25T21:54:48.952Z'), - })); - }); - - it(`Posts a new case, no connector configured`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: CASES_URL, - method: 'post', - body: { - description: 'This is a brand new case of a bad meanie defacing data', - title: 'Super Bad Security Issue', - tags: ['defacement'], - connector: { - id: 'none', - name: 'none', - type: ConnectorTypes.none, - fields: null, - }, - settings: { - syncAlerts: true, - }, - owner: 'securitySolution', - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(200); - expect(response.payload.id).toEqual('mock-it'); - expect(response.payload.status).toEqual('open'); - expect(response.payload.created_by.username).toEqual('awesome'); - expect(response.payload.connector).toEqual({ - id: 'none', - name: 'none', - type: ConnectorTypes.none, - fields: null, - }); - }); - - it(`Posts a new case, connector provided`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: CASES_URL, - method: 'post', - body: { - description: 'This is a brand new case of a bad meanie defacing data', - title: 'Super Bad Security Issue', - tags: ['defacement'], - connector: { - id: '123', - name: 'Jira', - type: '.jira', - fields: { issueType: 'Task', priority: 'High', parent: null }, - }, - settings: { - syncAlerts: true, - }, - owner: 'securitySolution', - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseConfigureSavedObject: mockCaseConfigure, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(200); - expect(response.payload.connector).toEqual({ - id: '123', - name: 'Jira', - type: '.jira', - fields: { issueType: 'Task', priority: 'High', parent: null }, - }); - }); - - it(`Error if you passing status for a new case`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: CASES_URL, - method: 'post', - body: { - description: 'This is a brand new case of a bad meanie defacing data', - title: 'Super Bad Security Issue', - status: CaseStatuses.open, - tags: ['defacement'], - connector: null, - settings: { - syncAlerts: true, - }, - owner: 'securitySolution', - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(400); - }); - - it(`Returns an error if postNewCase throws`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: CASES_URL, - method: 'post', - body: { - description: 'Throw an error', - title: 'Super Bad Security Issue', - tags: ['error'], - connector: null, - settings: { - syncAlerts: true, - }, - owner: 'securitySolution', - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(400); - expect(response.payload.isBoom).toEqual(true); - }); - - it(`Allow user to create case without authentication`, async () => { - routeHandler = await createRoute(initPostCaseApi, 'post', true); - - const request = httpServerMock.createKibanaRequest({ - path: CASES_URL, - method: 'post', - body: { - description: 'This is a brand new case of a bad meanie defacing data', - title: 'Super Bad Security Issue', - tags: ['defacement'], - connector: { - id: 'none', - name: 'none', - type: ConnectorTypes.none, - fields: null, - }, - settings: { - syncAlerts: true, - }, - owner: 'securitySolution', - }, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseConfigureSavedObject: mockCaseConfigure, - }), - true - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(200); - expect(response.payload).toMatchInlineSnapshot(` - Object { - "closed_at": null, - "closed_by": null, - "comments": Array [], - "connector": Object { - "fields": null, - "id": "none", - "name": "none", - "type": ".none", - }, - "created_at": "2019-11-25T21:54:48.952Z", - "created_by": Object { - "email": null, - "full_name": null, - "username": null, - }, - "description": "This is a brand new case of a bad meanie defacing data", - "external_service": null, - "id": "mock-it", - "owner": "securitySolution", - "settings": Object { - "syncAlerts": true, - }, - "status": "open", - "subCaseIds": undefined, - "subCases": undefined, - "tags": Array [ - "defacement", - ], - "title": "Super Bad Security Issue", - "totalAlerts": 0, - "totalComment": 0, - "type": "individual", - "updated_at": null, - "updated_by": null, - "version": "WzksMV0=", - } - `); - }); -}); diff --git a/x-pack/plugins/cases/server/routes/api/cases/push_case.test.ts b/x-pack/plugins/cases/server/routes/api/cases/push_case.test.ts deleted file mode 100644 index adac2c9f7ee382..00000000000000 --- a/x-pack/plugins/cases/server/routes/api/cases/push_case.test.ts +++ /dev/null @@ -1,466 +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 { kibanaResponseFactory, RequestHandler } from 'src/core/server'; -import { httpServerMock } from 'src/core/server/mocks'; - -import { - createMockSavedObjectsRepository, - createRoute, - createRouteContext, - mockCases, - mockCaseConfigure, - mockCaseMappings, - mockUserActions, - mockCaseComments, -} from '../__fixtures__'; -import { initPushCaseApi } from './push_case'; -import { CasesRequestHandlerContext } from '../../../types'; -import { getCasePushUrl } from '../../../../common/api/helpers'; - -describe('Push case', () => { - let routeHandler: RequestHandler; - const mockDate = '2019-11-25T21:54:48.952Z'; - const caseId = 'mock-id-3'; - const connectorId = '123'; - const path = getCasePushUrl(caseId, connectorId); - - beforeAll(async () => { - routeHandler = await createRoute(initPushCaseApi, 'post'); - const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; - spyOnDate.mockImplementation(() => ({ - toISOString: jest.fn().mockReturnValue(mockDate), - })); - }); - - it(`Pushes a case`, async () => { - const request = httpServerMock.createKibanaRequest({ - path, - method: 'post', - params: { - case_id: caseId, - connector_id: connectorId, - }, - body: {}, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseMappingsSavedObject: mockCaseMappings, - caseConfigureSavedObject: mockCaseConfigure, - caseUserActionsSavedObject: mockUserActions, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(200); - expect(response.payload.external_service).toEqual({ - connector_id: connectorId, - connector_name: 'ServiceNow', - external_id: '10663', - external_title: 'RJ2-200', - external_url: 'https://siem-kibana.atlassian.net/browse/RJ2-200', - pushed_at: mockDate, - pushed_by: { - email: 'd00d@awesome.com', - full_name: 'Awesome D00d', - username: 'awesome', - }, - }); - }); - - it(`Pushes a case with comments`, async () => { - const request = httpServerMock.createKibanaRequest({ - path, - method: 'post', - params: { - case_id: caseId, - connector_id: connectorId, - }, - body: {}, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseMappingsSavedObject: mockCaseMappings, - caseConfigureSavedObject: mockCaseConfigure, - caseUserActionsSavedObject: mockUserActions, - caseCommentSavedObject: [mockCaseComments[0]], - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(200); - expect(response.payload.comments[0].pushed_at).toEqual(mockDate); - expect(response.payload.comments[0].pushed_by).toEqual({ - email: 'd00d@awesome.com', - full_name: 'Awesome D00d', - username: 'awesome', - }); - }); - - it(`Filters comments with type alert correctly`, async () => { - const request = httpServerMock.createKibanaRequest({ - path, - method: 'post', - params: { - case_id: caseId, - connector_id: connectorId, - }, - body: {}, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseMappingsSavedObject: mockCaseMappings, - caseConfigureSavedObject: mockCaseConfigure, - caseUserActionsSavedObject: mockUserActions, - caseCommentSavedObject: [mockCaseComments[0], mockCaseComments[3]], - }) - ); - - const casesClient = await context.cases.getCasesClient(); - casesClient.getAlerts = jest.fn().mockResolvedValue([]); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(200); - expect(casesClient.getAlerts).toHaveBeenCalledWith({ - alertsInfo: [{ id: 'test-id', index: 'test-index' }], - }); - }); - - it(`Calls execute with correct arguments`, async () => { - const request = httpServerMock.createKibanaRequest({ - path, - method: 'post', - params: { - case_id: caseId, - connector_id: 'for-mock-case-id-3', - }, - body: {}, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseMappingsSavedObject: mockCaseMappings, - caseConfigureSavedObject: mockCaseConfigure, - caseUserActionsSavedObject: mockUserActions, - }) - ); - - const actionsClient = context.actions.getActionsClient(); - - await routeHandler(context, request, kibanaResponseFactory); - expect(actionsClient.execute).toHaveBeenCalledWith({ - actionId: 'for-mock-case-id-3', - params: { - subAction: 'pushToService', - subActionParams: { - incident: { - issueType: 'Task', - parent: null, - priority: 'High', - labels: ['LOLBins'], - summary: 'Another bad one', - description: - 'Oh no, a bad meanie going LOLBins all over the place! (created at 2019-11-25T22:32:17.947Z by elastic)', - externalId: null, - }, - comments: [], - }, - }, - }); - }); - - it(`Pushes a case and closes when closure_type: 'close-by-pushing'`, async () => { - const request = httpServerMock.createKibanaRequest({ - path, - method: 'post', - params: { - case_id: caseId, - connector_id: connectorId, - }, - body: {}, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseMappingsSavedObject: mockCaseMappings, - caseUserActionsSavedObject: mockUserActions, - caseConfigureSavedObject: [ - { - ...mockCaseConfigure[0], - attributes: { - ...mockCaseConfigure[0].attributes, - closure_type: 'close-by-pushing', - }, - }, - ], - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(200); - expect(response.payload.closed_at).toEqual(mockDate); - }); - - it(`post the correct user action`, async () => { - const request = httpServerMock.createKibanaRequest({ - path, - method: 'post', - params: { - case_id: caseId, - connector_id: connectorId, - }, - body: {}, - }); - - const { context, services } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseMappingsSavedObject: mockCaseMappings, - caseConfigureSavedObject: mockCaseConfigure, - caseUserActionsSavedObject: mockUserActions, - }) - ); - - services.userActionService.postUserActions = jest.fn(); - const postUserActions = services.userActionService.postUserActions as jest.Mock; - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(200); - expect(postUserActions.mock.calls[0][0].actions[0].attributes).toEqual({ - action: 'push-to-service', - action_at: '2019-11-25T21:54:48.952Z', - action_by: { - email: 'd00d@awesome.com', - full_name: 'Awesome D00d', - username: 'awesome', - }, - action_field: ['pushed'], - new_value: - '{"pushed_at":"2019-11-25T21:54:48.952Z","pushed_by":{"username":"awesome","full_name":"Awesome D00d","email":"d00d@awesome.com"},"connector_id":"123","connector_name":"ServiceNow","external_id":"10663","external_title":"RJ2-200","external_url":"https://siem-kibana.atlassian.net/browse/RJ2-200"}', - old_value: null, - }); - }); - - it('Unhappy path - case id is missing', async () => { - const request = httpServerMock.createKibanaRequest({ - path, - method: 'post', - params: { - connector_id: connectorId, - }, - body: {}, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseMappingsSavedObject: mockCaseMappings, - caseConfigureSavedObject: mockCaseConfigure, - caseUserActionsSavedObject: mockUserActions, - }) - ); - - const res = await routeHandler(context, request, kibanaResponseFactory); - expect(res.status).toEqual(400); - }); - - it('Unhappy path - connector id is missing', async () => { - const request = httpServerMock.createKibanaRequest({ - path, - method: 'post', - params: { - case_id: caseId, - }, - body: {}, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseMappingsSavedObject: mockCaseMappings, - caseConfigureSavedObject: mockCaseConfigure, - caseUserActionsSavedObject: mockUserActions, - }) - ); - - const res = await routeHandler(context, request, kibanaResponseFactory); - expect(res.status).toEqual(400); - }); - - it('Unhappy path - case does not exists', async () => { - const request = httpServerMock.createKibanaRequest({ - path, - method: 'post', - params: { - case_id: 'not-exist', - connector_id: connectorId, - }, - body: {}, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseMappingsSavedObject: mockCaseMappings, - caseConfigureSavedObject: mockCaseConfigure, - caseUserActionsSavedObject: mockUserActions, - }) - ); - - const res = await routeHandler(context, request, kibanaResponseFactory); - expect(res.status).toEqual(404); - }); - - it('Unhappy path - connector does not exists', async () => { - const request = httpServerMock.createKibanaRequest({ - path, - method: 'post', - params: { - case_id: caseId, - connector_id: 'not-exists', - }, - body: {}, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseMappingsSavedObject: mockCaseMappings, - caseConfigureSavedObject: mockCaseConfigure, - caseUserActionsSavedObject: mockUserActions, - }) - ); - - const res = await routeHandler(context, request, kibanaResponseFactory); - expect(res.status).toEqual(404); - }); - - it('Unhappy path - cannot push to a closed case', async () => { - const request = httpServerMock.createKibanaRequest({ - path, - method: 'post', - params: { - case_id: 'mock-id-4', - connector_id: connectorId, - }, - body: {}, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseMappingsSavedObject: mockCaseMappings, - caseConfigureSavedObject: mockCaseConfigure, - caseUserActionsSavedObject: mockUserActions, - }) - ); - - const res = await routeHandler(context, request, kibanaResponseFactory); - expect(res.status).toEqual(409); - expect(res.payload.output.payload.message).toBe( - 'This case Another bad one is closed. You can not pushed if the case is closed.' - ); - }); - - it('Unhappy path - throws when external service returns an error', async () => { - const request = httpServerMock.createKibanaRequest({ - path, - method: 'post', - params: { - case_id: caseId, - connector_id: connectorId, - }, - body: {}, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseMappingsSavedObject: mockCaseMappings, - caseConfigureSavedObject: mockCaseConfigure, - caseUserActionsSavedObject: mockUserActions, - }) - ); - - const actionsClient = context.actions.getActionsClient(); - (actionsClient.execute as jest.Mock).mockResolvedValue({ - status: 'error', - }); - - const res = await routeHandler(context, request, kibanaResponseFactory); - expect(res.status).toEqual(424); - expect(res.payload.output.payload.message).toBe('Error pushing to service'); - }); - - it('Unhappy path - context case missing', async () => { - const request = httpServerMock.createKibanaRequest({ - path, - method: 'post', - params: { - case_id: caseId, - connector_id: connectorId, - }, - body: {}, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseMappingsSavedObject: mockCaseMappings, - caseConfigureSavedObject: mockCaseConfigure, - caseUserActionsSavedObject: mockUserActions, - }) - ); - - const betterContext = ({ - ...context, - cases: null, - } as unknown) as CasesRequestHandlerContext; - - const res = await routeHandler(betterContext, request, kibanaResponseFactory); - expect(res.status).toEqual(400); - expect(res.payload).toEqual('RouteHandlerContext is not registered for cases'); - }); - - it('Unhappy path - context actions missing', async () => { - const request = httpServerMock.createKibanaRequest({ - path, - method: 'post', - params: { - case_id: caseId, - connector_id: connectorId, - }, - body: {}, - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - caseMappingsSavedObject: mockCaseMappings, - caseConfigureSavedObject: mockCaseConfigure, - caseUserActionsSavedObject: mockUserActions, - }) - ); - - const betterContext = ({ - ...context, - actions: null, - } as unknown) as CasesRequestHandlerContext; - - const res = await routeHandler(betterContext, request, kibanaResponseFactory); - expect(res.status).toEqual(400); - expect(res.payload).toEqual('Action client not found'); - }); -}); diff --git a/x-pack/plugins/cases/server/routes/api/cases/status/get_status.test.ts b/x-pack/plugins/cases/server/routes/api/cases/status/get_status.test.ts deleted file mode 100644 index ca12ed9c92831b..00000000000000 --- a/x-pack/plugins/cases/server/routes/api/cases/status/get_status.test.ts +++ /dev/null @@ -1,92 +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 { kibanaResponseFactory, RequestHandler } from 'src/core/server'; -import { httpServerMock } from 'src/core/server/mocks'; - -import { - createMockSavedObjectsRepository, - createRoute, - createRouteContext, - mockCases, -} from '../../__fixtures__'; -import { initGetCasesStatusApi } from './get_status'; -import { CASE_STATUS_URL } from '../../../../../common/constants'; -import { esKuery } from 'src/plugins/data/server'; -import { CaseType } from '../../../../../common/api'; - -describe('GET status', () => { - let routeHandler: RequestHandler; - const findArgs = { - fields: [], - page: 1, - perPage: 1, - type: 'cases', - sortField: 'created_at', - }; - - beforeAll(async () => { - routeHandler = await createRoute(initGetCasesStatusApi, 'get'); - }); - - it(`returns the status`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: CASE_STATUS_URL, - method: 'get', - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: mockCases, - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(context.core.savedObjects.client.find).toHaveBeenNthCalledWith(1, { - ...findArgs, - filter: esKuery.fromKueryExpression( - `((cases.attributes.status: open AND cases.attributes.type: individual) OR cases.attributes.type: ${CaseType.collection})` - ), - }); - - expect(context.core.savedObjects.client.find).toHaveBeenNthCalledWith(2, { - ...findArgs, - filter: esKuery.fromKueryExpression( - `((cases.attributes.status: in-progress AND cases.attributes.type: individual) OR cases.attributes.type: ${CaseType.collection})` - ), - }); - - expect(context.core.savedObjects.client.find).toHaveBeenNthCalledWith(3, { - ...findArgs, - filter: esKuery.fromKueryExpression( - `((cases.attributes.status: closed AND cases.attributes.type: individual) OR cases.attributes.type: ${CaseType.collection})` - ), - }); - - expect(response.payload).toEqual({ - count_open_cases: 4, - count_in_progress_cases: 4, - count_closed_cases: 4, - }); - }); - - it(`returns an error when findCases throws`, async () => { - const request = httpServerMock.createKibanaRequest({ - path: CASE_STATUS_URL, - method: 'get', - }); - - const { context } = await createRouteContext( - createMockSavedObjectsRepository({ - caseSavedObject: [{ ...mockCases[0], id: 'throw-error-find' }], - }) - ); - - const response = await routeHandler(context, request, kibanaResponseFactory); - expect(response.status).toEqual(404); - }); -}); diff --git a/x-pack/plugins/cases/server/services/mocks.ts b/x-pack/plugins/cases/server/services/mocks.ts index 77129e45348b1f..5e5b4ff31309e6 100644 --- a/x-pack/plugins/cases/server/services/mocks.ts +++ b/x-pack/plugins/cases/server/services/mocks.ts @@ -5,12 +5,14 @@ * 2.0. */ +import { PublicMethodsOf } from '@kbn/utility-types'; import { AlertServiceContract, CaseConfigureService, CaseService, CaseUserActionService, ConnectorMappingsService, + AttachmentService, } from '.'; export type CaseServiceMock = jest.Mocked; @@ -18,61 +20,87 @@ export type CaseConfigureServiceMock = jest.Mocked; export type ConnectorMappingsServiceMock = jest.Mocked; export type CaseUserActionServiceMock = jest.Mocked; export type AlertServiceMock = jest.Mocked; +export type AttachmentServiceMock = jest.Mocked; -export const createCaseServiceMock = (): CaseServiceMock => ({ - createSubCase: jest.fn(), - deleteCase: jest.fn(), - deleteComment: jest.fn(), - deleteSubCase: jest.fn(), - findCases: jest.fn(), - findSubCases: jest.fn(), - findSubCasesByCaseId: jest.fn(), - getAllCaseComments: jest.fn(), - getAllSubCaseComments: jest.fn(), - getCase: jest.fn(), - getCases: jest.fn(), - getComment: jest.fn(), - getMostRecentSubCase: jest.fn(), - getSubCase: jest.fn(), - getSubCases: jest.fn(), - getTags: jest.fn(), - getReporters: jest.fn(), - getUser: jest.fn(), - postNewCase: jest.fn(), - postNewComment: jest.fn(), - patchCase: jest.fn(), - patchCases: jest.fn(), - patchComment: jest.fn(), - patchComments: jest.fn(), - patchSubCase: jest.fn(), - patchSubCases: jest.fn(), - findSubCaseStatusStats: jest.fn(), - getCommentsByAssociation: jest.fn(), - getCaseCommentStats: jest.fn(), - findSubCasesGroupByCase: jest.fn(), - findCaseStatusStats: jest.fn(), - findCasesGroupedByID: jest.fn(), -}); +export const createCaseServiceMock = (): CaseServiceMock => { + const service: PublicMethodsOf = { + createSubCase: jest.fn(), + deleteCase: jest.fn(), + deleteSubCase: jest.fn(), + findCases: jest.fn(), + findSubCases: jest.fn(), + findSubCasesByCaseId: jest.fn(), + getAllCaseComments: jest.fn(), + getAllSubCaseComments: jest.fn(), + getCase: jest.fn(), + getCases: jest.fn(), + getMostRecentSubCase: jest.fn(), + getSubCase: jest.fn(), + getSubCases: jest.fn(), + getTags: jest.fn(), + getReporters: jest.fn(), + getUser: jest.fn(), + postNewCase: jest.fn(), + patchCase: jest.fn(), + patchCases: jest.fn(), + patchSubCase: jest.fn(), + patchSubCases: jest.fn(), + findSubCaseStatusStats: jest.fn(), + getCommentsByAssociation: jest.fn(), + getCaseCommentStats: jest.fn(), + findSubCasesGroupByCase: jest.fn(), + findCaseStatusStats: jest.fn(), + findCasesGroupedByID: jest.fn(), + }; -export const createConfigureServiceMock = (): CaseConfigureServiceMock => ({ - delete: jest.fn(), - get: jest.fn(), - find: jest.fn(), - patch: jest.fn(), - post: 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 CaseServiceMock; +}; -export const connectorMappingsServiceMock = (): ConnectorMappingsServiceMock => ({ - find: jest.fn(), - post: jest.fn(), -}); +export const createConfigureServiceMock = (): CaseConfigureServiceMock => { + const service: PublicMethodsOf = { + delete: jest.fn(), + get: jest.fn(), + find: jest.fn(), + patch: jest.fn(), + post: jest.fn(), + }; -export const createUserActionServiceMock = (): CaseUserActionServiceMock => ({ - getUserActions: jest.fn(), - postUserActions: 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 CaseConfigureServiceMock; +}; + +export const connectorMappingsServiceMock = (): ConnectorMappingsServiceMock => { + const service: PublicMethodsOf = { find: jest.fn(), post: 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; +}; + +export const createUserActionServiceMock = (): CaseUserActionServiceMock => { + const service: PublicMethodsOf = { + getAll: jest.fn(), + bulkCreate: 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 CaseUserActionServiceMock; +}; export const createAlertServiceMock = (): AlertServiceMock => ({ updateAlertsStatus: jest.fn(), getAlerts: jest.fn(), }); + +export const createAttachmentServiceMock = (): AttachmentServiceMock => { + const service: PublicMethodsOf = { + get: jest.fn(), + delete: jest.fn(), + create: jest.fn(), + update: jest.fn(), + bulkUpdate: 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 AttachmentServiceMock; +}; diff --git a/x-pack/test/case_api_integration/common/config.ts b/x-pack/test/case_api_integration/common/config.ts index 0d9a1030d68088..d9dacc649c9f5a 100644 --- a/x-pack/test/case_api_integration/common/config.ts +++ b/x-pack/test/case_api_integration/common/config.ts @@ -62,6 +62,36 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) fs.statSync(path.resolve(__dirname, 'fixtures', 'plugins', file)).isDirectory() ); + // This is needed so that we can correctly use the alerting test frameworks mock implementation for the connectors. + const alertingAllFiles = fs.readdirSync( + path.resolve( + __dirname, + '..', + '..', + 'alerting_api_integration', + 'common', + 'fixtures', + 'plugins' + ) + ); + + const alertingPlugins = alertingAllFiles.filter((file) => + fs + .statSync( + path.resolve( + __dirname, + '..', + '..', + 'alerting_api_integration', + 'common', + 'fixtures', + 'plugins', + file + ) + ) + .isDirectory() + ); + return { testFiles: testFiles ? testFiles : [require.resolve('../tests/common')], servers, @@ -90,15 +120,19 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) '--xpack.eventLog.logEntries=true', ...disabledPlugins.map((key) => `--xpack.${key}.enabled=false`), // Actions simulators plugin. Needed for testing push to external services. - `--plugin-path=${path.resolve( - __dirname, - '..', - '..', - 'alerting_api_integration', - 'common', - 'fixtures', - 'plugins' - )}`, + ...alertingPlugins.map( + (pluginDir) => + `--plugin-path=${path.resolve( + __dirname, + '..', + '..', + 'alerting_api_integration', + 'common', + 'fixtures', + 'plugins', + pluginDir + )}` + ), ...plugins.map( (pluginDir) => `--plugin-path=${path.resolve(__dirname, 'fixtures', 'plugins', pluginDir)}` diff --git a/x-pack/test/case_api_integration/common/lib/mock.ts b/x-pack/test/case_api_integration/common/lib/mock.ts index f1f088e5c50425..c3a6cb87141155 100644 --- a/x-pack/test/case_api_integration/common/lib/mock.ts +++ b/x-pack/test/case_api_integration/common/lib/mock.ts @@ -92,11 +92,11 @@ export const postCommentGenAlertReq: ContextTypeGeneratedAlertType = { }; export const postCaseResp = ( - id: string, + id?: string | null, req: CasePostRequest = postCaseReq ): Partial => ({ ...req, - id, + ...(id != null ? { id } : {}), comments: [], totalAlerts: 0, totalComment: 0, @@ -165,60 +165,6 @@ export const subCaseResp = ({ updated_by: defaultUser, }); -interface FormattedCollectionResponse { - caseInfo: Partial; - subCases?: Array>; - comments?: Array>; -} - -export const formatCollectionResponse = (caseInfo: CaseResponse): FormattedCollectionResponse => { - const subCase = removeServerGeneratedPropertiesFromSubCase(caseInfo.subCases?.[0]); - return { - caseInfo: removeServerGeneratedPropertiesFromCaseCollection(caseInfo), - subCases: subCase ? [subCase] : undefined, - comments: removeServerGeneratedPropertiesFromComments( - caseInfo.subCases?.[0].comments ?? caseInfo.comments - ), - }; -}; - -export const removeServerGeneratedPropertiesFromSubCase = ( - subCase: Partial | undefined -): Partial | undefined => { - if (!subCase) { - return; - } - // eslint-disable-next-line @typescript-eslint/naming-convention - const { closed_at, created_at, updated_at, version, comments, ...rest } = subCase; - return rest; -}; - -export const removeServerGeneratedPropertiesFromCaseCollection = ( - config: Partial -): Partial => { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { closed_at, created_at, updated_at, version, subCases, ...rest } = config; - return rest; -}; - -export const removeServerGeneratedPropertiesFromCase = ( - config: Partial -): Partial => { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { closed_at, created_at, updated_at, version, ...rest } = config; - return rest; -}; - -export const removeServerGeneratedPropertiesFromComments = ( - comments: CommentResponse[] | undefined -): Array> | undefined => { - return comments?.map((comment) => { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { created_at, updated_at, version, ...rest } = comment; - return rest; - }); -}; - const findCommon = { page: 1, per_page: 20, 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 82189c9d7abe3f..32094e60832a97 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -4,14 +4,21 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import expect from '@kbn/expect'; +import { omit } from 'lodash'; +import expect from '@kbn/expect'; import type { ApiResponse, estypes } from '@elastic/elasticsearch'; import type { KibanaClient } from '@elastic/elasticsearch/api/kibana'; import * as st from 'supertest'; import supertestAsPromised from 'supertest-as-promised'; -import { CASES_URL, SUB_CASES_PATCH_DEL_URL } from '../../../../plugins/cases/common/constants'; +import { + CASES_URL, + CASE_CONFIGURE_CONNECTORS_URL, + CASE_CONFIGURE_URL, + CASE_STATUS_URL, + SUB_CASES_PATCH_DEL_URL, +} from '../../../../plugins/cases/common/constants'; import { CasesConfigureRequest, CasesConfigureResponse, @@ -24,11 +31,21 @@ import { SubCasesResponse, CasesResponse, CasesFindResponse, + CommentRequest, + CaseUserActionResponse, + SubCaseResponse, + CommentResponse, + CasesPatchRequest, + AllCommentsResponse, + CommentPatchRequest, + CasesConfigurePatch, + CasesStatusResponse, } from '../../../../plugins/cases/common/api'; import { getPostCaseRequest, 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'; function toArray(input: T | T[]): T[] { @@ -146,11 +163,11 @@ export const createSubCase = async (args: { */ export const createCaseAction = async (supertest: st.SuperTest) => { const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A case connector', - actionTypeId: '.case', + connector_type_id: '.case', config: {}, }) .expect(200); @@ -164,7 +181,7 @@ export const deleteCaseAction = async ( supertest: st.SuperTest, id: string ) => { - await supertest.delete(`/api/actions/action/${id}`).set('kbn-xsrf', 'foo'); + await supertest.delete(`/api/actions/connector/${id}`).set('kbn-xsrf', 'foo'); }; /** @@ -233,7 +250,7 @@ export const createSubCaseComment = async ({ } const caseConnector = await supertest - .post(`/api/actions/action/${actionIDToUse}/_execute`) + .post(`/api/actions/connector/${actionIDToUse}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -250,7 +267,7 @@ export const createSubCaseComment = async ({ return { newSubCaseInfo: caseConnector.body.data, modifiedSubCases: closedSubCases }; }; -export const getConfiguration = ({ +export const getConfigurationRequest = ({ id = 'none', name = 'none', type = ConnectorTypes.none, @@ -267,19 +284,23 @@ export const getConfiguration = ({ }; }; -export const getConfigurationOutput = (update = false): Partial => { +export const getConfigurationOutput = ( + update = false, + overwrite = {} +): Partial => { return { - ...getConfiguration(), + ...getConfigurationRequest(), error: null, mappings: [], created_by: { email: null, full_name: null, username: 'elastic' }, updated_by: update ? { email: null, full_name: null, username: 'elastic' } : null, + ...overwrite, }; }; export const getServiceNowConnector = () => ({ name: 'ServiceNow Connector', - actionTypeId: '.servicenow', + connector_type_id: '.servicenow', secrets: { username: 'admin', password: 'password', @@ -291,7 +312,7 @@ export const getServiceNowConnector = () => ({ export const getJiraConnector = () => ({ name: 'Jira Connector', - actionTypeId: '.jira', + connector_type_id: '.jira', secrets: { email: 'elastic@elastic.co', apiToken: 'token', @@ -322,7 +343,7 @@ export const getMappings = () => [ export const getResilientConnector = () => ({ name: 'Resilient Connector', - actionTypeId: '.resilient', + connector_type_id: '.resilient', secrets: { apiKeyId: 'id', apiKeySecret: 'secret', @@ -333,21 +354,106 @@ export const getResilientConnector = () => ({ }, }); -export const removeServerGeneratedPropertiesFromConfigure = ( - config: Partial -): Partial => { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { created_at, updated_at, version, ...rest } = config; - return rest; +export const getServiceNowSIRConnector = () => ({ + name: 'ServiceNow Connector', + connector_type_id: '.servicenow-sir', + secrets: { + username: 'admin', + password: 'password', + }, + config: { + apiUrl: 'http://some.non.existent.com', + }, +}); + +export const getWebhookConnector = () => ({ + name: 'A generic Webhook action', + connector_type_id: '.webhook', + secrets: { + user: 'user', + password: 'password', + }, + config: { + headers: { + 'Content-Type': 'text/plain', + }, + url: 'http://some.non.existent.com', + }, +}); + +interface CommonSavedObjectAttributes { + id?: string | null; + created_at?: string | null; + updated_at?: string | null; + version?: string | null; + [key: string]: unknown; +} + +const savedObjectCommonAttributes = ['created_at', 'updated_at', 'version', 'id']; + +const removeServerGeneratedPropertiesFromObject = ( + object: T, + keys: K[] +): Omit => { + return omit(object, keys); +}; +export const removeServerGeneratedPropertiesFromSavedObject = < + T extends CommonSavedObjectAttributes +>( + attributes: T, + keys: Array = [] +): Omit => { + return removeServerGeneratedPropertiesFromObject(attributes, [ + ...savedObjectCommonAttributes, + ...keys, + ]); +}; + +export const removeServerGeneratedPropertiesFromUserAction = ( + attributes: CaseUserActionResponse +) => { + const keysToRemove: Array = ['action_id', 'action_at']; + return removeServerGeneratedPropertiesFromObject< + CaseUserActionResponse, + typeof keysToRemove[number] + >(attributes, keysToRemove); +}; + +export const removeServerGeneratedPropertiesFromSubCase = ( + subCase: SubCaseResponse | undefined +) => { + if (!subCase) { + return; + } + + return removeServerGeneratedPropertiesFromSavedObject(subCase, [ + 'closed_at', + 'comments', + ]); +}; + +export const removeServerGeneratedPropertiesFromCase = ( + theCase: CaseResponse +): Partial => { + return removeServerGeneratedPropertiesFromSavedObject(theCase, ['closed_at']); +}; + +export const removeServerGeneratedPropertiesFromComments = ( + comments: CommentResponse[] | undefined +): Array> | undefined => { + return comments?.map((comment) => { + return removeServerGeneratedPropertiesFromSavedObject(comment, []); + }); }; export const deleteAllCaseItems = async (es: KibanaClient) => { await Promise.all([ - deleteCases(es), + deleteCasesByESQuery(es), deleteSubCases(es), deleteCasesUserActions(es), deleteComments(es), deleteConfiguration(es), + deleteMappings(es), ]); }; @@ -362,7 +468,7 @@ export const deleteCasesUserActions = async (es: KibanaClient): Promise => }); }; -export const deleteCases = async (es: KibanaClient): Promise => { +export const deleteCasesByESQuery = async (es: KibanaClient): Promise => { await es.deleteByQuery({ index: '.kibana', // @ts-expect-error @elastic/elasticsearch DeleteByQueryRequest doesn't accept q parameter @@ -410,6 +516,17 @@ export const deleteConfiguration = async (es: KibanaClient): Promise => { }); }; +export const deleteMappings = async (es: KibanaClient): Promise => { + await es.deleteByQuery({ + index: '.kibana', + // @ts-expect-error @elastic/elasticsearch DeleteByQueryRequest doesn't accept q parameter + q: 'type:cases-connector-mappings', + wait_for_completion: true, + refresh: true, + body: {}, + }); +}; + export const getSpaceUrlPrefix = (spaceId: string) => { return spaceId && spaceId !== 'default' ? `/s/${spaceId}` : ``; }; @@ -468,3 +585,271 @@ export const ensureSavedObjectIsAuthorized = ( expect(cases.length).to.eql(numberOfExpectedCases); cases.forEach((theCase) => expect(owners.includes(theCase.owner)).to.be(true)); }; + +export const createCase = async ( + supertest: st.SuperTest, + params: CasePostRequest, + expectedHttpCode: number = 200 +): Promise => { + const { body: theCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(params) + .expect(expectedHttpCode); + + return theCase; +}; + +/** + * Sends a delete request for the specified case IDs. + */ +export const deleteCases = async ({ + supertest, + caseIDs, + expectedHttpCode = 204, +}: { + supertest: st.SuperTest; + caseIDs: string[]; + expectedHttpCode?: number; +}) => { + const { body } = await supertest + .delete(`${CASES_URL}`) + .query({ ids: caseIDs }) + .set('kbn-xsrf', 'true') + .send() + .expect(expectedHttpCode); + + return body; +}; + +export const createComment = async ( + supertest: st.SuperTest, + caseId: string, + params: CommentRequest, + expectedHttpCode: number = 200 +): Promise => { + const { body: theCase } = await supertest + .post(`${CASES_URL}/${caseId}/comments`) + .set('kbn-xsrf', 'true') + .send(params) + .expect(expectedHttpCode); + + return theCase; +}; + +export const getAllUserAction = async ( + supertest: st.SuperTest, + caseId: string, + expectedHttpCode: number = 200 +): Promise => { + const { body: userActions } = await supertest + .get(`${CASES_URL}/${caseId}/user_actions`) + .set('kbn-xsrf', 'true') + .send() + .expect(expectedHttpCode); + + return userActions; +}; + +export const updateCase = async ( + supertest: st.SuperTest, + params: CasesPatchRequest, + expectedHttpCode: number = 200 +): Promise => { + const { body: cases } = await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send(params) + .expect(expectedHttpCode); + + return cases; +}; + +export const deleteComment = async ( + supertest: st.SuperTest, + caseId: string, + commentId: string, + expectedHttpCode: number = 204 +): Promise<{} | Error> => { + const { body: comment } = await supertest + .delete(`${CASES_URL}/${caseId}/comments/${commentId}`) + .set('kbn-xsrf', 'true') + .expect(expectedHttpCode) + .send(); + + return comment; +}; + +export const getAllComments = async ( + supertest: st.SuperTest, + caseId: string, + expectedHttpCode: number = 200 +): Promise => { + const { body: comments } = await supertest + .get(`${CASES_URL}/${caseId}/comments`) + .set('kbn-xsrf', 'true') + .send() + .expect(expectedHttpCode); + + return comments; +}; + +export const getComment = async ( + supertest: st.SuperTest, + caseId: string, + commentId: string, + expectedHttpCode: number = 200 +): Promise => { + const { body: comment } = await supertest + .get(`${CASES_URL}/${caseId}/comments/${commentId}`) + .set('kbn-xsrf', 'true') + .send() + .expect(expectedHttpCode); + + return comment; +}; + +export const updateComment = async ( + supertest: st.SuperTest, + caseId: string, + req: CommentPatchRequest, + expectedHttpCode: number = 200 +): Promise => { + const { body: res } = await supertest + .patch(`${CASES_URL}/${caseId}/comments`) + .set('kbn-xsrf', 'true') + .send(req) + .expect(expectedHttpCode); + + return res; +}; + +export const getConfiguration = async ( + supertest: st.SuperTest, + expectedHttpCode: number = 200 +): Promise => { + const { body: configuration } = await supertest + .get(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(expectedHttpCode); + + return configuration; +}; + +export const createConfiguration = async ( + supertest: st.SuperTest, + req: CasesConfigureRequest = getConfigurationRequest(), + expectedHttpCode: number = 200 +): Promise => { + const { body: configuration } = await supertest + .post(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send(req) + .expect(expectedHttpCode); + + return configuration; +}; + +export type CreateConnectorResponse = Omit & { + connector_type_id: string; +}; + +export const createConnector = async ( + supertest: st.SuperTest, + req: Record, + expectedHttpCode: number = 200 +): Promise => { + const { body: connector } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'true') + .send(req) + .expect(expectedHttpCode); + + return connector; +}; + +export const getCaseConnectors = async ( + supertest: st.SuperTest, + expectedHttpCode: number = 200 +): Promise => { + const { body: connectors } = await supertest + .get(`${CASE_CONFIGURE_CONNECTORS_URL}/_find`) + .set('kbn-xsrf', 'true') + .send() + .expect(expectedHttpCode); + + return connectors; +}; + +export const updateConfiguration = async ( + supertest: st.SuperTest, + req: CasesConfigurePatch, + expectedHttpCode: number = 200 +): Promise => { + const { body: configuration } = await supertest + .patch(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send(req) + .expect(expectedHttpCode); + + return configuration; +}; + +export const getAllCasesStatuses = async ( + supertest: st.SuperTest, + expectedHttpCode: number = 200 +): Promise => { + 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 => { + const { body: theCase } = await supertest + .get(`${CASES_URL}/${caseId}?includeComments=${includeComments}`) + .set('kbn-xsrf', 'true') + .send() + .expect(expectedHttpCode); + + return theCase; +}; + +export const findCases = async ( + supertest: st.SuperTest, + query: Record = {}, + expectedHttpCode: number = 200 +): Promise => { + const { body: res } = await supertest + .get(`${CASES_URL}/_find`) + .query({ sortOrder: 'asc', ...query }) + .set('kbn-xsrf', 'true') + .send() + .expect(expectedHttpCode); + + return res; +}; + +export const pushCase = async ( + supertest: st.SuperTest, + caseId: string, + connectorId: string, + expectedHttpCode: number = 200 +): Promise => { + const { body: res } = await supertest + .post(`${CASES_URL}/${caseId}/connector/${connectorId}/_push`) + .set('kbn-xsrf', 'true') + .send({}) + .expect(expectedHttpCode); + + return res; +}; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/basic/cases/push_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/cases/push_case.ts index 067171cef30a44..f964ef3ee85929 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/basic/cases/push_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/cases/push_case.ts @@ -7,16 +7,18 @@ import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { CASE_CONFIGURE_URL, CASES_URL } from '../../../../../../plugins/cases/common/constants'; - import { postCaseReq } from '../../../../common/lib/mock'; import { - deleteCases, + deleteCasesByESQuery, deleteCasesUserActions, deleteComments, deleteConfiguration, - getConfiguration, + getConfigurationRequest, getServiceNowConnector, + createConnector, + createConfiguration, + createCase, + pushCase, } from '../../../../common/lib/utils'; import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; @@ -27,60 +29,43 @@ export default ({ getService }: FtrProviderContext): void => { describe('push_case', () => { afterEach(async () => { - await deleteCases(es); + await deleteCasesByESQuery(es); await deleteComments(es); await deleteConfiguration(es); await deleteCasesUserActions(es); }); it('should get 403 when trying to create a connector', async () => { - await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'true') - .send({ - ...getServiceNowConnector(), - }) - .expect(403); + await createConnector(supertest, getServiceNowConnector(), 403); }); it('should get 404 when trying to push to a case without a valid connector id', async () => { - await supertest - .post(CASE_CONFIGURE_URL) - .set('kbn-xsrf', 'true') - .send( - getConfiguration({ - id: 'not-exist', - name: 'Not exist', - type: ConnectorTypes.serviceNowITSM, - }) - ) - .expect(200); - - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - ...postCaseReq, - connector: getConfiguration({ - id: 'not-exist', - name: 'Not exist', - type: ConnectorTypes.serviceNowITSM, - fields: { - urgency: '2', - impact: '2', - severity: '2', - category: 'software', - subcategory: 'os', - }, - }).connector, + await createConfiguration( + supertest, + getConfigurationRequest({ + id: 'not-exist', + name: 'Not exist', + type: ConnectorTypes.serviceNowITSM, }) - .expect(200); + ); + + const postedCase = await createCase(supertest, { + ...postCaseReq, + connector: { + id: 'not-exist', + name: 'Not exist', + type: ConnectorTypes.serviceNowITSM, + fields: { + urgency: '2', + impact: '2', + severity: '2', + category: 'software', + subcategory: 'os', + }, + }, + }); - await supertest - .post(`${CASES_URL}/${postedCase.id}/connector/not-exist/_push`) - .set('kbn-xsrf', 'true') - .send({}) - .expect(404); + await pushCase(supertest, postedCase.id, 'not-exist', 404); }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/post_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/post_comment.ts deleted file mode 100644 index cb7dd74d0f7144..00000000000000 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/post_comment.ts +++ /dev/null @@ -1,422 +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 { omit } from 'lodash/fp'; -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; - -import { CASES_URL } from '../../../../../../../plugins/cases/common/constants'; -import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../../../plugins/security_solution/common/constants'; -import { CommentsResponse, CommentType } from '../../../../../../../plugins/cases/common/api'; -import { - defaultUser, - postCaseReq, - postCommentUserReq, - postCommentAlertReq, - postCollectionReq, - postCommentGenAlertReq, -} from '../../../../../common/lib/mock'; -import { - createCaseAction, - createSubCase, - deleteAllCaseItems, - deleteCaseAction, - deleteCases, - deleteCasesUserActions, - deleteComments, -} from '../../../../../common/lib/utils'; -import { - createSignalsIndex, - deleteSignalsIndex, - deleteAllAlerts, - getRuleForSignalTesting, - waitForRuleSuccessOrStatus, - waitForSignalsToBePresent, - getSignalsByIds, - createRule, - getQuerySignalIds, -} from '../../../../../../detection_engine_api_integration/utils'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - const es = getService('es'); - - describe('post_comment', () => { - afterEach(async () => { - await deleteCases(es); - await deleteComments(es); - await deleteCasesUserActions(es); - }); - - it('should post a comment', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const { body: patchedCase } = await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - expect(patchedCase.comments[0].type).to.eql(postCommentUserReq.type); - expect(patchedCase.comments[0].comment).to.eql(postCommentUserReq.comment); - expect(patchedCase.updated_by).to.eql(defaultUser); - }); - - it('should post an alert', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const { body: patchedCase } = await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentAlertReq) - .expect(200); - - expect(patchedCase.comments[0].type).to.eql(postCommentAlertReq.type); - expect(patchedCase.comments[0].alertId).to.eql(postCommentAlertReq.alertId); - expect(patchedCase.comments[0].index).to.eql(postCommentAlertReq.index); - expect(patchedCase.updated_by).to.eql(defaultUser); - }); - - it('unhappy path - 400s when type is missing', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send({ - bad: 'comment', - }) - .expect(400); - }); - - it('unhappy path - 400s when missing attributes for type user', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send({ type: CommentType.user }) - .expect(400); - }); - - it('unhappy path - 400s when adding excess attributes for type user', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - for (const attribute of ['alertId', 'index']) { - await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send({ type: CommentType.user, [attribute]: attribute, comment: 'a comment' }) - .expect(400); - } - }); - - it('unhappy path - 400s when missing attributes for type alert', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const allRequestAttributes = { - type: CommentType.alert, - index: 'test-index', - alertId: 'test-id', - rule: { - id: 'id', - name: 'name', - }, - }; - - for (const attribute of ['alertId', 'index']) { - const requestAttributes = omit(attribute, allRequestAttributes); - await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(requestAttributes) - .expect(400); - } - }); - - it('unhappy path - 400s when adding excess attributes for type alert', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - for (const attribute of ['comment']) { - await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send({ - type: CommentType.alert, - [attribute]: attribute, - alertId: 'test-id', - index: 'test-index', - rule: { - id: 'id', - name: 'name', - }, - }) - .expect(400); - } - }); - - it('unhappy path - 400s when case is missing', async () => { - await supertest - .post(`${CASES_URL}/not-exists/comments`) - .set('kbn-xsrf', 'true') - .send({ - bad: 'comment', - }) - .expect(400); - }); - - it('unhappy path - 400s when adding an alert to a closed case', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: 'closed', - }, - ], - }) - .expect(200); - - await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentAlertReq) - .expect(400); - }); - - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - it.skip('400s when adding an alert to a collection case', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCollectionReq) - .expect(200); - - await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentAlertReq) - .expect(400); - }); - - it('400s when adding a generated alert to an individual case', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentGenAlertReq) - .expect(400); - }); - - describe('alerts', () => { - beforeEach(async () => { - await esArchiver.load('auditbeat/hosts'); - await createSignalsIndex(supertest); - }); - - afterEach(async () => { - await deleteSignalsIndex(supertest); - await deleteAllAlerts(supertest); - await esArchiver.unload('auditbeat/hosts'); - }); - - it('should change the status of the alert if sync alert is on', async () => { - const rule = getRuleForSignalTesting(['auditbeat-*']); - - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: 'in-progress', - }, - ], - }) - .expect(200); - - const { id } = await createRule(supertest, rule); - await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 1, [id]); - const signals = await getSignalsByIds(supertest, [id]); - - const alert = signals.hits.hits[0]; - expect(alert._source.signal.status).eql('open'); - - await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send({ - alertId: alert._id, - index: alert._index, - rule: { - id: 'id', - name: 'name', - }, - type: CommentType.alert, - }) - .expect(200); - - const { body: updatedAlert } = await supertest - .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) - .set('kbn-xsrf', 'true') - .send(getQuerySignalIds([alert._id])) - .expect(200); - - expect(updatedAlert.hits.hits[0]._source.signal.status).eql('in-progress'); - }); - - it('should NOT change the status of the alert if sync alert is off', async () => { - const rule = getRuleForSignalTesting(['auditbeat-*']); - - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ ...postCaseReq, settings: { syncAlerts: false } }) - .expect(200); - - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: 'in-progress', - }, - ], - }) - .expect(200); - - const { id } = await createRule(supertest, rule); - await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 1, [id]); - const signals = await getSignalsByIds(supertest, [id]); - - const alert = signals.hits.hits[0]; - expect(alert._source.signal.status).eql('open'); - - await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send({ - alertId: alert._id, - index: alert._index, - rule: { - id: 'id', - name: 'name', - }, - type: CommentType.alert, - }) - .expect(200); - - const { body: updatedAlert } = await supertest - .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) - .set('kbn-xsrf', 'true') - .send(getQuerySignalIds([alert._id])) - .expect(200); - - expect(updatedAlert.hits.hits[0]._source.signal.status).eql('open'); - }); - }); - - it('should return a 400 when passing the subCaseId', async () => { - const { body } = await supertest - .post(`${CASES_URL}/case-id/comments?subCaseId=value`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(400); - expect(body.message).to.contain('subCaseId'); - }); - - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - describe.skip('sub case comments', () => { - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); - }); - after(async () => { - await deleteCaseAction(supertest, actionID); - }); - afterEach(async () => { - await deleteAllCaseItems(es); - }); - - it('posts a new comment for a sub case', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - // create another sub case just to make sure we get the right comments - await createSubCase({ supertest, actionID }); - await supertest - .post(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - const { body: subCaseComments }: { body: CommentsResponse } = await supertest - .get(`${CASES_URL}/${caseInfo.id}/comments/_find?subCaseId=${caseInfo.subCases![0].id}`) - .send() - .expect(200); - expect(subCaseComments.total).to.be(2); - expect(subCaseComments.comments[0].type).to.be(CommentType.generatedAlert); - expect(subCaseComments.comments[1].type).to.be(CommentType.user); - }); - }); - }); -}; 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 e835e4da6c8dd5..2c50ac8a453f9b 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 @@ -9,15 +9,19 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; -import { postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; +import { getPostCaseRequest, postCommentUserReq } from '../../../../common/lib/mock'; import { createCaseAction, createSubCase, deleteAllCaseItems, deleteCaseAction, - deleteCases, + deleteCasesByESQuery, deleteCasesUserActions, deleteComments, + createCase, + deleteCases, + createComment, + getComment, } from '../../../../common/lib/utils'; import { getSubCaseDetailsUrl } from '../../../../../../plugins/cases/common/api/helpers'; import { CaseResponse } from '../../../../../../plugins/cases/common/api'; @@ -29,65 +33,32 @@ export default ({ getService }: FtrProviderContext): void => { describe('delete_cases', () => { afterEach(async () => { - await deleteCases(es); + await deleteCasesByESQuery(es); await deleteComments(es); await deleteCasesUserActions(es); }); it('should delete a case', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const { body } = await supertest - .delete(`${CASES_URL}?ids=["${postedCase.id}"]`) - .set('kbn-xsrf', 'true') - .send() - .expect(204); + const postedCase = await createCase(supertest, getPostCaseRequest()); + const body = await deleteCases({ supertest, caseIDs: [postedCase.id] }); expect(body).to.eql({}); }); it(`should delete a case's comments when that case gets deleted`, async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const { body: patchedCase } = await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - await supertest - .get(`${CASES_URL}/${postedCase.id}/comments/${patchedCase.comments[0].id}`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - await supertest - .delete(`${CASES_URL}?ids=["${postedCase.id}"]`) - .set('kbn-xsrf', 'true') - .send() - .expect(204); - - await supertest - .get(`${CASES_URL}/${postedCase.id}/comments/${patchedCase.comments[0].id}`) - .set('kbn-xsrf', 'true') - .send() - .expect(404); + const postedCase = await createCase(supertest, getPostCaseRequest()); + const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); + // ensure that we can get the comment before deleting the case + await getComment(supertest, postedCase.id, patchedCase.comments![0].id); + + await deleteCases({ supertest, caseIDs: [postedCase.id] }); + + // make sure the comment is now gone + await getComment(supertest, postedCase.id, patchedCase.comments![0].id, 404); }); it('unhappy path - 404s when case is not there', async () => { - await supertest - .delete(`${CASES_URL}?ids=["fake-id"]`) - .set('kbn-xsrf', 'true') - .send() - .expect(404); + await deleteCases({ supertest, caseIDs: ['fake-id'], expectedHttpCode: 404 }); }); // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests @@ -107,11 +78,7 @@ export default ({ getService }: FtrProviderContext): void => { const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); expect(caseInfo.subCases![0].id).to.not.eql(undefined); - const { body } = await supertest - .delete(`${CASES_URL}?ids=["${caseInfo.id}"]`) - .set('kbn-xsrf', 'true') - .send() - .expect(204); + const body = await deleteCases({ supertest, caseIDs: [caseInfo.id] }); expect(body).to.eql({}); await supertest @@ -138,11 +105,7 @@ export default ({ getService }: FtrProviderContext): void => { // make sure we can get the second comment await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(200); - await supertest - .delete(`${CASES_URL}?ids=["${caseInfo.id}"]`) - .set('kbn-xsrf', 'true') - .send() - .expect(204); + await deleteCases({ supertest, caseIDs: [caseInfo.id] }); await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(404); }); 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 195ada335e0861..ca3b0201c14545 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 @@ -6,7 +6,6 @@ */ import expect from '@kbn/expect'; -import supertestAsPromised from 'supertest-as-promised'; import type { ApiResponse, estypes } from '@elastic/elasticsearch'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; @@ -25,12 +24,12 @@ import { createCaseAsUser, ensureSavedObjectIsAuthorized, findCasesAsUser, + findCases, + createCase, + updateCase, + createComment, } from '../../../../common/lib/utils'; -import { - CasesFindResponse, - CaseStatuses, - CaseType, -} from '../../../../../../plugins/cases/common/api'; +import { CaseResponse, CaseStatuses, CaseType } from '../../../../../../plugins/cases/common/api'; import { obsOnly, secOnly, @@ -62,41 +61,18 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should return empty response', async () => { - const { body } = await supertest - .get(`${CASES_URL}/_find`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(body).to.eql(findCasesResp); + const cases = await findCases(supertest); + expect(cases).to.eql(findCasesResp); }); it('should return cases', async () => { - const { body: a } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); + const a = await createCase(supertest, postCaseReq); + const b = await createCase(supertest, postCaseReq); + const c = await createCase(supertest, postCaseReq); - const { body: b } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); + const cases = await findCases(supertest); - const { body: c } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const { body } = await supertest - .get(`${CASES_URL}/_find?sortOrder=asc`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(body).to.eql({ + expect(cases).to.eql({ ...findCasesResp, total: 3, cases: [a, b, c], @@ -105,20 +81,11 @@ export default ({ getService }: FtrProviderContext): void => { }); it('filters by tags', async () => { - await supertest.post(CASES_URL).set('kbn-xsrf', 'true').send(postCaseReq); - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ ...postCaseReq, tags: ['unique'] }) - .expect(200); - - const { body } = await supertest - .get(`${CASES_URL}/_find?sortOrder=asc&tags=unique`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); + await createCase(supertest, postCaseReq); + const postedCase = await createCase(supertest, { ...postCaseReq, tags: ['unique'] }); + const cases = await findCases(supertest, { tags: ['unique'] }); - expect(body).to.eql({ + expect(cases).to.eql({ ...findCasesResp, total: 1, cases: [postedCase], @@ -127,40 +94,24 @@ export default ({ getService }: FtrProviderContext): void => { }); it('filters by status', async () => { - const { body: openCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq); - - const { body: toCloseCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq); - - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: toCloseCase.id, - version: toCloseCase.version, - status: 'closed', - }, - ], - }) - .expect(200); + await createCase(supertest, postCaseReq); + const toCloseCase = await createCase(supertest, postCaseReq); + const patchedCase = await updateCase(supertest, { + cases: [ + { + id: toCloseCase.id, + version: toCloseCase.version, + status: CaseStatuses.closed, + }, + ], + }); - const { body } = await supertest - .get(`${CASES_URL}/_find?sortOrder=asc&status=open`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); + const cases = await findCases(supertest, { status: CaseStatuses.closed }); - expect(body).to.eql({ + expect(cases).to.eql({ ...findCasesResp, total: 1, - cases: [openCase], + cases: [patchedCase[0]], count_open_cases: 1, count_closed_cases: 1, count_in_progress_cases: 0, @@ -168,18 +119,10 @@ export default ({ getService }: FtrProviderContext): void => { }); it('filters by reporters', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq); - - const { body } = await supertest - .get(`${CASES_URL}/_find?sortOrder=asc&reporters=elastic`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); + const postedCase = await createCase(supertest, postCaseReq); + const cases = await findCases(supertest, { reporters: 'elastic' }); - expect(body).to.eql({ + expect(cases).to.eql({ ...findCasesResp, total: 1, cases: [postedCase], @@ -188,32 +131,14 @@ export default ({ getService }: FtrProviderContext): void => { }); it('correctly counts comments', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); + const postedCase = await createCase(supertest, postCaseReq); // post 2 comments - await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - const { body: patchedCase } = await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - const { body } = await supertest - .get(`${CASES_URL}/_find?sortOrder=asc`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); + await createComment(supertest, postedCase.id, postCommentUserReq); + const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); - expect(body).to.eql({ + const cases = await findCases(supertest); + expect(cases).to.eql({ ...findCasesResp, total: 1, cases: [ @@ -228,64 +153,38 @@ export default ({ getService }: FtrProviderContext): void => { }); it('correctly counts open/closed/in-progress', async () => { - await supertest.post(CASES_URL).set('kbn-xsrf', 'true').send(postCaseReq); - - const { body: inProgreeCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq); - - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: 'closed', - }, - ], - }) - .expect(200); + await createCase(supertest, postCaseReq); + const inProgressCase = await createCase(supertest, postCaseReq); + const postedCase = await createCase(supertest, postCaseReq); - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: inProgreeCase.id, - version: inProgreeCase.version, - status: 'in-progress', - }, - ], - }) - .expect(200); + await updateCase(supertest, { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses.closed, + }, + ], + }); - const { body } = await supertest - .get(`${CASES_URL}/_find?sortOrder=asc`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); + await updateCase(supertest, { + cases: [ + { + id: inProgressCase.id, + version: inProgressCase.version, + status: CaseStatuses['in-progress'], + }, + ], + }); - expect(body.count_open_cases).to.eql(1); - expect(body.count_closed_cases).to.eql(1); - expect(body.count_in_progress_cases).to.eql(1); + 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 supertest - .get(`${CASES_URL}/_find?perPage=true`) - .set('kbn-xsrf', 'true') - .send() - .expect(400); + await findCases(supertest, { perPage: true }, 400); }); // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests @@ -334,75 +233,66 @@ export default ({ getService }: FtrProviderContext): void => { }); }); it('correctly counts stats without using a filter', async () => { - const { body }: { body: CasesFindResponse } = await supertest - .get(`${CASES_URL}/_find?sortOrder=asc`) - .expect(200); - - expect(body.total).to.eql(3); - expect(body.count_closed_cases).to.eql(1); - expect(body.count_open_cases).to.eql(1); - expect(body.count_in_progress_cases).to.eql(1); + const cases = await findCases(supertest); + + expect(cases.total).to.eql(3); + expect(cases.count_closed_cases).to.eql(1); + expect(cases.count_open_cases).to.eql(1); + expect(cases.count_in_progress_cases).to.eql(1); }); it('correctly counts stats with a filter for open cases', async () => { - const { body }: { body: CasesFindResponse } = await supertest - .get(`${CASES_URL}/_find?sortOrder=asc&status=open`) - .expect(200); + const cases = await findCases(supertest, { status: CaseStatuses.open }); - expect(body.cases.length).to.eql(1); + expect(cases.cases.length).to.eql(1); // since we're filtering on status and the collection only has an in-progress case, it should only return the // individual case that has the open status and no collections // ENABLE_CASE_CONNECTOR: this value is not correct because it includes a collection // that does not have an open case. This is a known issue and will need to be resolved // when this issue is addressed: https://github.com/elastic/kibana/issues/94115 - expect(body.total).to.eql(2); - expect(body.count_closed_cases).to.eql(1); - expect(body.count_open_cases).to.eql(1); - expect(body.count_in_progress_cases).to.eql(1); + expect(cases.total).to.eql(2); + expect(cases.count_closed_cases).to.eql(1); + expect(cases.count_open_cases).to.eql(1); + expect(cases.count_in_progress_cases).to.eql(1); }); it('correctly counts stats with a filter for individual cases', async () => { - const { body }: { body: CasesFindResponse } = await supertest - .get(`${CASES_URL}/_find?sortOrder=asc&type=${CaseType.individual}`) - .expect(200); - - expect(body.total).to.eql(2); - expect(body.count_closed_cases).to.eql(1); - expect(body.count_open_cases).to.eql(1); - expect(body.count_in_progress_cases).to.eql(0); + const cases = await findCases(supertest, { type: CaseType.individual }); + + expect(cases.total).to.eql(2); + expect(cases.count_closed_cases).to.eql(1); + expect(cases.count_open_cases).to.eql(1); + expect(cases.count_in_progress_cases).to.eql(0); }); it('correctly counts stats with a filter for collection cases with multiple sub cases', async () => { // 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 { body }: { body: CasesFindResponse } = await supertest - .get(`${CASES_URL}/_find?sortOrder=asc&type=${CaseType.collection}`) - .expect(200); - - expect(body.total).to.eql(1); - expect(body.cases[0].subCases?.length).to.eql(2); - expect(body.count_closed_cases).to.eql(1); - expect(body.count_open_cases).to.eql(1); - expect(body.count_in_progress_cases).to.eql(0); + const cases = await findCases(supertest, { type: CaseType.collection }); + + expect(cases.total).to.eql(1); + expect(cases.cases[0].subCases?.length).to.eql(2); + expect(cases.count_closed_cases).to.eql(1); + expect(cases.count_open_cases).to.eql(1); + expect(cases.count_in_progress_cases).to.eql(0); }); it('correctly counts stats with a filter for collection and open cases with multiple sub cases', async () => { // 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 { body }: { body: CasesFindResponse } = await supertest - .get( - `${CASES_URL}/_find?sortOrder=asc&type=${CaseType.collection}&status=${CaseStatuses.open}` - ) - .expect(200); + const cases = await findCases(supertest, { + type: CaseType.collection, + status: CaseStatuses.open, + }); - expect(body.total).to.eql(1); - expect(body.cases[0].subCases?.length).to.eql(1); - expect(body.count_closed_cases).to.eql(1); - expect(body.count_open_cases).to.eql(1); - expect(body.count_in_progress_cases).to.eql(0); + expect(cases.total).to.eql(1); + expect(cases.cases[0].subCases?.length).to.eql(1); + expect(cases.count_closed_cases).to.eql(1); + expect(cases.count_open_cases).to.eql(1); + expect(cases.count_in_progress_cases).to.eql(0); }); it('correctly counts stats including a collection without sub cases when not filtering on status', async () => { @@ -415,15 +305,13 @@ export default ({ getService }: FtrProviderContext): void => { .send() .expect(204); - const { body }: { body: CasesFindResponse } = await supertest - .get(`${CASES_URL}/_find?sortOrder=asc`) - .expect(200); + const cases = await findCases(supertest, { type: CaseType.collection }); // it should include the collection without sub cases because we did not pass in a filter on status - expect(body.total).to.eql(3); - expect(body.count_closed_cases).to.eql(1); - expect(body.count_open_cases).to.eql(1); - expect(body.count_in_progress_cases).to.eql(0); + expect(cases.total).to.eql(3); + expect(cases.count_closed_cases).to.eql(1); + expect(cases.count_open_cases).to.eql(1); + expect(cases.count_in_progress_cases).to.eql(0); }); it('correctly counts stats including a collection without sub cases when filtering on tags', async () => { @@ -436,31 +324,27 @@ export default ({ getService }: FtrProviderContext): void => { .send() .expect(204); - const { body }: { body: CasesFindResponse } = await supertest - .get(`${CASES_URL}/_find?sortOrder=asc&tags=defacement`) - .expect(200); + const cases = await findCases(supertest, { tags: ['defacement'] }); // it should include the collection without sub cases because we did not pass in a filter on status - expect(body.total).to.eql(3); - expect(body.count_closed_cases).to.eql(1); - expect(body.count_open_cases).to.eql(1); - expect(body.count_in_progress_cases).to.eql(0); + expect(cases.total).to.eql(3); + expect(cases.count_closed_cases).to.eql(1); + expect(cases.count_open_cases).to.eql(1); + expect(cases.count_in_progress_cases).to.eql(0); }); it('does not return collections without sub cases matching the requested status', async () => { - const { body }: { body: CasesFindResponse } = await supertest - .get(`${CASES_URL}/_find?sortOrder=asc&status=closed`) - .expect(200); + const cases = await findCases(supertest, { status: CaseStatuses.closed }); - expect(body.cases.length).to.eql(1); + expect(cases.cases.length).to.eql(1); // it should not include the collection that has a sub case as in-progress // ENABLE_CASE_CONNECTOR: this value is not correct because it includes collections. This short term // fix for when sub cases are not enabled. When the feature is completed the _find API // will need to be fixed as explained in this ticket: https://github.com/elastic/kibana/issues/94115 - expect(body.total).to.eql(2); - expect(body.count_closed_cases).to.eql(1); - expect(body.count_open_cases).to.eql(1); - expect(body.count_in_progress_cases).to.eql(1); + expect(cases.total).to.eql(2); + expect(cases.count_closed_cases).to.eql(1); + expect(cases.count_open_cases).to.eql(1); + expect(cases.count_in_progress_cases).to.eql(1); }); it('does not return empty collections when filtering on status', async () => { @@ -473,19 +357,17 @@ export default ({ getService }: FtrProviderContext): void => { .send() .expect(204); - const { body }: { body: CasesFindResponse } = await supertest - .get(`${CASES_URL}/_find?sortOrder=asc&status=closed`) - .expect(200); + const cases = await findCases(supertest, { status: CaseStatuses.closed }); - expect(body.cases.length).to.eql(1); + expect(cases.cases.length).to.eql(1); // ENABLE_CASE_CONNECTOR: this value is not correct because it includes collections. This short term // fix for when sub cases are not enabled. When the feature is completed the _find API // will need to be fixed as explained in this ticket: https://github.com/elastic/kibana/issues/94115 - expect(body.total).to.eql(2); - expect(body.count_closed_cases).to.eql(1); - expect(body.count_open_cases).to.eql(1); - expect(body.count_in_progress_cases).to.eql(0); + expect(cases.total).to.eql(2); + expect(cases.count_closed_cases).to.eql(1); + expect(cases.count_open_cases).to.eql(1); + expect(cases.count_in_progress_cases).to.eql(0); }); }); }); @@ -500,22 +382,17 @@ export default ({ getService }: FtrProviderContext): void => { await deleteAllCaseItems(es); }); - const createCasesWithTitleAsNumber = async (total: number) => { - const responsePromises: supertestAsPromised.Test[] = []; + const createCasesWithTitleAsNumber = async (total: number): Promise => { + const responsePromises = []; for (let i = 0; i < total; i++) { // this doesn't guarantee that the cases will be created in order that the for-loop executes, // for example case with title '2', could be created before the case with title '1' since we're doing a promise all here // A promise all is just much faster than doing it one by one which would have guaranteed that the cases are // created in the order that the for-loop executes - responsePromises.push( - supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ ...postCaseReq, title: `${i}` }) - ); + responsePromises.push(createCase(supertest, { ...postCaseReq, title: `${i}` })); } const responses = await Promise.all(responsePromises); - return responses.map((response) => response.body); + return responses; }; /** @@ -541,88 +418,68 @@ export default ({ getService }: FtrProviderContext): void => { }; it('returns the correct total when perPage is less than the total', async () => { - const { body }: { body: CasesFindResponse } = await supertest - .get(`${CASES_URL}/_find`) - .query({ - sortOrder: 'asc', - page: 1, - perPage: 5, - }) - .set('kbn-xsrf', 'true') - .expect(200); - - expect(body.cases.length).to.eql(5); - expect(body.total).to.eql(10); - expect(body.page).to.eql(1); - expect(body.per_page).to.eql(5); - expect(body.count_open_cases).to.eql(10); - expect(body.count_closed_cases).to.eql(0); - expect(body.count_in_progress_cases).to.eql(0); + const cases = await findCases(supertest, { + page: 1, + perPage: 5, + }); + + expect(cases.cases.length).to.eql(5); + expect(cases.total).to.eql(10); + expect(cases.page).to.eql(1); + expect(cases.per_page).to.eql(5); + expect(cases.count_open_cases).to.eql(10); + expect(cases.count_closed_cases).to.eql(0); + expect(cases.count_in_progress_cases).to.eql(0); }); it('returns the correct total when perPage is greater than the total', async () => { - const { body }: { body: CasesFindResponse } = await supertest - .get(`${CASES_URL}/_find`) - .query({ - sortOrder: 'asc', - page: 1, - perPage: 11, - }) - .set('kbn-xsrf', 'true') - .expect(200); - - expect(body.total).to.eql(10); - expect(body.page).to.eql(1); - expect(body.per_page).to.eql(11); - expect(body.cases.length).to.eql(10); - expect(body.count_open_cases).to.eql(10); - expect(body.count_closed_cases).to.eql(0); - expect(body.count_in_progress_cases).to.eql(0); + const cases = await findCases(supertest, { + page: 1, + perPage: 11, + }); + + expect(cases.total).to.eql(10); + expect(cases.page).to.eql(1); + expect(cases.per_page).to.eql(11); + expect(cases.cases.length).to.eql(10); + expect(cases.count_open_cases).to.eql(10); + expect(cases.count_closed_cases).to.eql(0); + expect(cases.count_in_progress_cases).to.eql(0); }); it('returns the correct total when perPage is equal to the total', async () => { - const { body }: { body: CasesFindResponse } = await supertest - .get(`${CASES_URL}/_find`) - .query({ - sortOrder: 'asc', - page: 1, - perPage: 10, - }) - .set('kbn-xsrf', 'true') - .expect(200); - - expect(body.total).to.eql(10); - expect(body.page).to.eql(1); - expect(body.per_page).to.eql(10); - expect(body.cases.length).to.eql(10); - expect(body.count_open_cases).to.eql(10); - expect(body.count_closed_cases).to.eql(0); - expect(body.count_in_progress_cases).to.eql(0); + const cases = await findCases(supertest, { + page: 1, + perPage: 10, + }); + + expect(cases.total).to.eql(10); + expect(cases.page).to.eql(1); + expect(cases.per_page).to.eql(10); + expect(cases.cases.length).to.eql(10); + expect(cases.count_open_cases).to.eql(10); + expect(cases.count_closed_cases).to.eql(0); + expect(cases.count_in_progress_cases).to.eql(0); }); it('returns the second page of results', async () => { const perPage = 5; - const { body }: { body: CasesFindResponse } = await supertest - .get(`${CASES_URL}/_find`) - .query({ - sortOrder: 'asc', - page: 2, - perPage, - }) - .set('kbn-xsrf', 'true') - .expect(200); + const cases = await findCases(supertest, { + page: 2, + perPage, + }); - expect(body.total).to.eql(10); - expect(body.page).to.eql(2); - expect(body.per_page).to.eql(5); - expect(body.cases.length).to.eql(5); - expect(body.count_open_cases).to.eql(10); - expect(body.count_closed_cases).to.eql(0); - expect(body.count_in_progress_cases).to.eql(0); + expect(cases.total).to.eql(10); + expect(cases.page).to.eql(2); + expect(cases.per_page).to.eql(5); + expect(cases.cases.length).to.eql(5); + expect(cases.count_open_cases).to.eql(10); + expect(cases.count_closed_cases).to.eql(0); + expect(cases.count_in_progress_cases).to.eql(0); const allCases = await getAllCasesSortedByCreatedAtAsc(); - body.cases.map((caseInfo, index) => { + cases.cases.map((caseInfo, index) => { // we started on the second page of 10 cases with a perPage of 5, so the first case should 0 + 5 (index + perPage) expect(caseInfo.title).to.eql(allCases[index + perPage]?.cases.title); }); @@ -635,27 +492,22 @@ 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 { body }: { body: CasesFindResponse } = await supertest - .get(`${CASES_URL}/_find`) - .query({ - sortOrder: 'asc', - page: currentPage, - perPage, - }) - .set('kbn-xsrf', 'true') - .expect(200); + const cases = await findCases(supertest, { + page: currentPage, + perPage, + }); - expect(body.total).to.eql(total); - expect(body.page).to.eql(currentPage); - expect(body.per_page).to.eql(perPage); - expect(body.cases.length).to.eql(perPage); - expect(body.count_open_cases).to.eql(total); - expect(body.count_closed_cases).to.eql(0); - expect(body.count_in_progress_cases).to.eql(0); + expect(cases.total).to.eql(total); + expect(cases.page).to.eql(currentPage); + expect(cases.per_page).to.eql(perPage); + expect(cases.cases.length).to.eql(perPage); + expect(cases.count_open_cases).to.eql(total); + expect(cases.count_closed_cases).to.eql(0); + expect(cases.count_in_progress_cases).to.eql(0); const allCases = await getAllCasesSortedByCreatedAtAsc(); - body.cases.map((caseInfo, index) => { + cases.cases.map((caseInfo, index) => { // for page 1, the cases tiles should be 0,1,2 for page 2: 3,4,5 etc (assuming the titles were sorted // correctly) expect(caseInfo.title).to.eql( @@ -666,24 +518,19 @@ export default ({ getService }: FtrProviderContext): void => { }); it('retrieves the last three cases', async () => { - const { body }: { body: CasesFindResponse } = await supertest - .get(`${CASES_URL}/_find`) - .query({ - sortOrder: 'asc', - // this should skip the first 7 cases and only return the last 3 - page: 2, - perPage: 7, - }) - .set('kbn-xsrf', 'true') - .expect(200); - - expect(body.total).to.eql(10); - expect(body.page).to.eql(2); - expect(body.per_page).to.eql(7); - expect(body.cases.length).to.eql(3); - expect(body.count_open_cases).to.eql(10); - expect(body.count_closed_cases).to.eql(0); - expect(body.count_in_progress_cases).to.eql(0); + const cases = await findCases(supertest, { + // this should skip the first 7 cases and only return the last 3 + page: 2, + perPage: 7, + }); + + expect(cases.total).to.eql(10); + expect(cases.page).to.eql(2); + expect(cases.per_page).to.eql(7); + expect(cases.cases.length).to.eql(3); + expect(cases.count_open_cases).to.eql(10); + expect(cases.count_closed_cases).to.eql(0); + expect(cases.count_in_progress_cases).to.eql(0); }); }); 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 e8bed3c5a3116f..ca16416991cbf3 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 @@ -8,13 +8,22 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { AttributesTypeUser } from '../../../../../../plugins/cases/common/api'; import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; import { + defaultUser, postCaseReq, postCaseResp, - removeServerGeneratedPropertiesFromCase, + postCommentUserReq, } from '../../../../common/lib/mock'; -import { deleteCases } from '../../../../common/lib/utils'; +import { + deleteCasesByESQuery, + createCase, + getCase, + createComment, + removeServerGeneratedPropertiesFromCase, + removeServerGeneratedPropertiesFromSavedObject, +} from '../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -23,24 +32,37 @@ export default ({ getService }: FtrProviderContext): void => { describe('get_case', () => { afterEach(async () => { - await deleteCases(es); + await deleteCasesByESQuery(es); }); - it('should return a case', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); + it('should return a case with no comments', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const theCase = await getCase(supertest, postedCase.id, true); - const { body } = await supertest - .get(`${CASES_URL}/${postedCase.id}`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); + const data = removeServerGeneratedPropertiesFromCase(theCase); + expect(data).to.eql(postCaseResp()); + expect(data.comments?.length).to.eql(0); + }); + + 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 comment = removeServerGeneratedPropertiesFromSavedObject( + theCase.comments![0] as AttributesTypeUser + ); - const data = removeServerGeneratedPropertiesFromCase(body); - expect(data).to.eql(postCaseResp(postedCase.id)); + expect(theCase.comments?.length).to.eql(1); + expect(comment).to.eql({ + type: postCommentUserReq.type, + comment: postCommentUserReq.comment, + associationType: 'case', + created_by: defaultUser, + pushed_at: null, + pushed_by: null, + updated_by: null, + }); }); it('should return a 400 when passing the includeSubCaseComments', async () => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts index c9abaf4730d366..1d7baabaf93b04 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts @@ -8,13 +8,13 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../../plugins/security_solution/common/constants'; import { CasesResponse, CaseStatuses, CaseType, CommentType, + ConnectorTypes, } from '../../../../../../plugins/cases/common/api'; import { defaultUser, @@ -23,9 +23,18 @@ import { postCollectionReq, postCommentAlertReq, postCommentUserReq, - removeServerGeneratedPropertiesFromCase, } from '../../../../common/lib/mock'; -import { deleteAllCaseItems, getSignalsWithES, setStatus } from '../../../../common/lib/utils'; +import { + deleteAllCaseItems, + getSignalsWithES, + setStatus, + createCase, + createComment, + updateCase, + getAllUserAction, + removeServerGeneratedPropertiesFromCase, + removeServerGeneratedPropertiesFromUserAction, +} from '../../../../common/lib/utils'; import { createSignalsIndex, deleteSignalsIndex, @@ -49,160 +58,131 @@ export default ({ getService }: FtrProviderContext): void => { await deleteAllCaseItems(es); }); - it('should patch a case', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const { body: patchedCases } = await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ + describe('happy path', () => { + it('should patch a case', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCases = await updateCase(supertest, { cases: [ { id: postedCase.id, version: postedCase.version, - status: 'closed', + title: 'new title', }, ], - }) - .expect(200); - - const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); - expect(data).to.eql({ - ...postCaseResp(postedCase.id), - closed_by: defaultUser, - status: 'closed', - updated_by: defaultUser, + }); + + const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); + expect(data).to.eql({ + ...postCaseResp(), + title: 'new title', + updated_by: defaultUser, + }); }); - }); - it('should patch a case with new connector', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const { body: patchedCases } = await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ + it('should closes the case correctly', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCases = await updateCase(supertest, { cases: [ { id: postedCase.id, version: postedCase.version, - connector: { - id: 'jira', - name: 'Jira', - type: '.jira', - fields: { issueType: 'Task', priority: null, parent: null }, - }, + status: CaseStatuses.closed, }, ], - }) - .expect(200); - - const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); - expect(data).to.eql({ - ...postCaseResp(postedCase.id), - connector: { - id: 'jira', - name: 'Jira', - type: '.jira', - fields: { issueType: 'Task', priority: null, parent: null }, - }, - updated_by: defaultUser, + }); + + const userActions = await getAllUserAction(supertest, postedCase.id); + const statusUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); + const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); + + expect(data).to.eql({ + ...postCaseResp(), + status: CaseStatuses.closed, + closed_by: defaultUser, + updated_by: defaultUser, + }); + + expect(statusUserAction).to.eql({ + action_field: ['status'], + action: 'update', + action_by: defaultUser, + new_value: CaseStatuses.closed, + old_value: CaseStatuses.open, + case_id: `${postedCase.id}`, + comment_id: null, + sub_case_id: '', + }); }); - }); - it('unhappy path - 404s when case is not there', async () => { - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ + it('should change the status of case to in-progress correctly', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCases = await updateCase(supertest, { cases: [ { - id: 'not-real', - version: 'version', - status: 'closed', + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses['in-progress'], }, ], - }) - .expect(404); - }); + }); + + const userActions = await getAllUserAction(supertest, postedCase.id); + const statusUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); + const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); + + expect(data).to.eql({ + ...postCaseResp(), + status: CaseStatuses['in-progress'], + updated_by: defaultUser, + }); - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - it.skip('should 400 and not allow converting a collection back to an individual case', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCollectionReq) - .expect(200); - - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ + expect(statusUserAction).to.eql({ + action_field: ['status'], + action: 'update', + action_by: defaultUser, + new_value: CaseStatuses['in-progress'], + old_value: CaseStatuses.open, + case_id: `${postedCase.id}`, + comment_id: null, + sub_case_id: '', + }); + }); + + it('should patch a case with new connector', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCases = await updateCase(supertest, { cases: [ { id: postedCase.id, version: postedCase.version, - type: CaseType.individual, + connector: { + id: 'jira', + name: 'Jira', + type: ConnectorTypes.jira, + fields: { issueType: 'Task', priority: null, parent: null }, + }, }, ], - }) - .expect(400); - }); + }); - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - it.skip('should allow converting an individual case to a collection when it does not have alerts', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const { body: patchedCase } = await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: patchedCase.id, - version: patchedCase.version, - type: CaseType.collection, - }, - ], - }) - .expect(200); - }); + const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); + expect(data).to.eql({ + ...postCaseResp(), + connector: { + id: 'jira', + name: 'Jira', + type: '.jira', + fields: { issueType: 'Task', priority: null, parent: null }, + }, + updated_by: defaultUser, + }); + }); - it('should 400 when attempting to update an individual case to a collection when it has alerts attached to it', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const { body: patchedCase } = await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentAlertReq) - .expect(200); - - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + it.skip('should allow converting an individual case to a collection when it does not have alerts', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); + await updateCase(supertest, { cases: [ { id: patchedCase.id, @@ -210,190 +190,261 @@ export default ({ getService }: FtrProviderContext): void => { type: CaseType.collection, }, ], - }) - .expect(400); + }); + }); }); - it('should 400 when attempting to update the case type when the case connector feature is disabled', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: postedCase.id, - version: postedCase.version, - type: CaseType.collection, - }, - ], - }) - .expect(400); - }); + describe('unhappy path', () => { + it('404s when case is not there', async () => { + await updateCase( + supertest, + { + cases: [ + { + id: 'not-real', + version: 'version', + status: CaseStatuses.closed, + }, + ], + }, + 404 + ); + }); - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - it.skip("should 400 when attempting to update a collection case's status", async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCollectionReq) - .expect(200); - - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: 'closed', - }, - ], - }) - .expect(400); - }); + it('400s when id is missing', async () => { + await updateCase( + supertest, + { + cases: [ + // @ts-expect-error + { + version: 'version', + status: CaseStatuses.closed, + }, + ], + }, + 400 + ); + }); - it('unhappy path - 406s when excess data sent', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: postedCase.id, - version: postedCase.version, - badKey: 'closed', - }, - ], - }) - .expect(406); - }); + it('406s when fields are identical', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await updateCase( + supertest, + { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses.open, + }, + ], + }, + 406 + ); + }); - it('unhappy path - 400s when bad data sent', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: true, - }, - ], - }) - .expect(400); - }); + it('400s when version is missing', async () => { + await updateCase( + supertest, + { + cases: [ + // @ts-expect-error + { + id: 'not-real', + status: CaseStatuses.closed, + }, + ], + }, + 400 + ); + }); - it('unhappy path - 400s when unsupported status sent', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: 'not-supported', - }, - ], - }) - .expect(400); - }); + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + it.skip('should 400 and not allow converting a collection back to an individual case', async () => { + const postedCase = await createCase(supertest, postCollectionReq); + await updateCase( + supertest, + { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + type: CaseType.individual, + }, + ], + }, + 400 + ); + }); - it('unhappy path - 400s when bad connector type sent', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: postedCase.id, - version: postedCase.version, - connector: { id: 'none', name: 'none', type: '.not-exists', fields: null }, - }, - ], - }) - .expect(400); - }); + it('406s when excess data sent', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await updateCase( + supertest, + { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + // @ts-expect-error + badKey: 'closed', + }, + ], + }, + 406 + ); + }); - it('unhappy path - 400s when bad connector sent', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: postedCase.id, - version: postedCase.version, - connector: { - id: 'none', - name: 'none', - type: '.jira', - fields: { unsupported: 'value' }, + it('400s when bad data sent', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await updateCase( + supertest, + { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + // @ts-expect-error + status: true, }, - }, - ], - }) - .expect(400); - }); + ], + }, + 400 + ); + }); - it('unhappy path - 409s when conflict', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - await supertest - .patch(`${CASES_URL}`) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: postedCase.id, - version: 'version', - status: 'closed', - }, - ], - }) - .expect(409); + it('400s when unsupported status sent', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await updateCase( + supertest, + { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + // @ts-expect-error + status: 'not-supported', + }, + ], + }, + 400 + ); + }); + + it('400s when bad connector type sent', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await updateCase( + supertest, + { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + // @ts-expect-error + connector: { id: 'none', name: 'none', type: '.not-exists', fields: null }, + }, + ], + }, + 400 + ); + }); + + it('400s when bad connector sent', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await updateCase( + supertest, + { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + connector: { + id: 'jira', + name: 'Jira', + // @ts-expect-error + type: ConnectorTypes.jira, + // @ts-expect-error + fields: { unsupported: 'value' }, + }, + }, + ], + }, + 400 + ); + }); + + it('409s when version does not match', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await updateCase( + supertest, + { + cases: [ + { + id: postedCase.id, + version: 'version', + // @ts-expect-error + status: 'closed', + }, + ], + }, + 409 + ); + }); + + it('should 400 when attempting to update an individual case to a collection when it has alerts attached to it', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment(supertest, postedCase.id, postCommentAlertReq); + await updateCase( + supertest, + { + cases: [ + { + id: patchedCase.id, + version: patchedCase.version, + type: CaseType.collection, + }, + ], + }, + 400 + ); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed delete these tests + it('should 400 when attempting to update the case type when the case connector feature is disabled', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await updateCase( + supertest, + { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + type: CaseType.collection, + }, + ], + }, + 400 + ); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + it.skip("should 400 when attempting to update a collection case's status", async () => { + const postedCase = await createCase(supertest, postCollectionReq); + await updateCase( + supertest, + { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses.closed, + }, + ], + }, + 400 + ); + }); }); describe('alerts', () => { @@ -412,47 +463,34 @@ export default ({ getService }: FtrProviderContext): void => { const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; const signalID2 = '4d0f4b1533e46b66b43bdd0330d23f39f2cf42a7253153270e38d30cce9ff0c6'; - const { body: individualCase1 } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - ...postCaseReq, - settings: { - syncAlerts: false, - }, - }); + // does NOT updates alert status when adding comments and syncAlerts=false + const individualCase1 = await createCase(supertest, { + ...postCaseReq, + settings: { + syncAlerts: false, + }, + }); - const { body: updatedInd1WithComment } = await supertest - .post(`${CASES_URL}/${individualCase1.id}/comments`) - .set('kbn-xsrf', 'true') - .send({ - alertId: signalID, - index: defaultSignalsIndex, - rule: { id: 'test-rule-id', name: 'test-index-id' }, - type: CommentType.alert, - }) - .expect(200); + const updatedInd1WithComment = await createComment(supertest, individualCase1.id, { + alertId: signalID, + index: defaultSignalsIndex, + rule: { id: 'test-rule-id', name: 'test-index-id' }, + type: CommentType.alert, + }); - const { body: individualCase2 } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - ...postCaseReq, - settings: { - syncAlerts: false, - }, - }); + const individualCase2 = await createCase(supertest, { + ...postCaseReq, + settings: { + syncAlerts: false, + }, + }); - const { body: updatedInd2WithComment } = await supertest - .post(`${CASES_URL}/${individualCase2.id}/comments`) - .set('kbn-xsrf', 'true') - .send({ - alertId: signalID2, - index: defaultSignalsIndex, - rule: { id: 'test-rule-id', name: 'test-index-id' }, - type: CommentType.alert, - }) - .expect(200); + const updatedInd2WithComment = await createComment(supertest, individualCase2.id, { + alertId: signalID2, + index: defaultSignalsIndex, + rule: { id: 'test-rule-id', name: 'test-index-id' }, + type: CommentType.alert, + }); await es.indices.refresh({ index: defaultSignalsIndex }); @@ -470,6 +508,7 @@ export default ({ getService }: FtrProviderContext): void => { CaseStatuses.open ); + // does NOT updates alert status when the status is updated and syncAlerts=false const updatedIndWithStatus: CasesResponse = (await setStatus({ supertest, cases: [ @@ -503,18 +542,15 @@ export default ({ getService }: FtrProviderContext): void => { CaseStatuses.open ); + // it updates alert status when syncAlerts is turned on // turn on the sync settings - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: updatedIndWithStatus.map((caseInfo) => ({ - id: caseInfo.id, - version: caseInfo.version, - settings: { syncAlerts: true }, - })), - }) - .expect(200); + await updateCase(supertest, { + cases: updatedIndWithStatus.map((caseInfo) => ({ + id: caseInfo.id, + version: caseInfo.version, + settings: { syncAlerts: true }, + })), + }); await es.indices.refresh({ index: defaultSignalsIndex }); @@ -561,37 +597,26 @@ export default ({ getService }: FtrProviderContext): void => { const signalIDInSecondIndex = 'duplicate-signal-id'; const signalsIndex2 = '.siem-signals-default-000002'; - const { body: individualCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - ...postCaseReq, - settings: { - syncAlerts: false, - }, - }); + const individualCase = await createCase(supertest, { + ...postCaseReq, + settings: { + syncAlerts: false, + }, + }); - const { body: updatedIndWithComment } = await supertest - .post(`${CASES_URL}/${individualCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send({ - alertId: signalIDInFirstIndex, - index: defaultSignalsIndex, - rule: { id: 'test-rule-id', name: 'test-index-id' }, - type: CommentType.alert, - }) - .expect(200); + const updatedIndWithComment = await createComment(supertest, individualCase.id, { + alertId: signalIDInFirstIndex, + index: defaultSignalsIndex, + rule: { id: 'test-rule-id', name: 'test-index-id' }, + type: CommentType.alert, + }); - const { body: updatedIndWithComment2 } = await supertest - .post(`${CASES_URL}/${updatedIndWithComment.id}/comments`) - .set('kbn-xsrf', 'true') - .send({ - alertId: signalIDInSecondIndex, - index: signalsIndex2, - rule: { id: 'test-rule-id', name: 'test-index-id' }, - type: CommentType.alert, - }) - .expect(200); + const updatedIndWithComment2 = await createComment(supertest, updatedIndWithComment.id, { + alertId: signalIDInSecondIndex, + index: signalsIndex2, + rule: { id: 'test-rule-id', name: 'test-index-id' }, + type: CommentType.alert, + }); await es.indices.refresh({ index: defaultSignalsIndex }); @@ -629,19 +654,15 @@ export default ({ getService }: FtrProviderContext): void => { ).to.be(CaseStatuses.open); // turn on the sync settings - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: updatedIndWithStatus[0].id, - version: updatedIndWithStatus[0].version, - settings: { syncAlerts: true }, - }, - ], - }) - .expect(200); + await updateCase(supertest, { + cases: [ + { + id: updatedIndWithStatus[0].id, + version: updatedIndWithStatus[0].version, + settings: { syncAlerts: true }, + }, + ], + }); await es.indices.refresh({ index: defaultSignalsIndex }); signals = await getSignals(); @@ -675,12 +696,7 @@ export default ({ getService }: FtrProviderContext): void => { it('updates alert status when the status is updated and syncAlerts=true', async () => { const rule = getRuleForSignalTesting(['auditbeat-*']); - - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); + const postedCase = await createCase(supertest, postCaseReq); const { id } = await createRule(supertest, rule); await waitForRuleSuccessOrStatus(supertest, id); @@ -690,35 +706,26 @@ export default ({ getService }: FtrProviderContext): void => { const alert = signals.hits.hits[0]; expect(alert._source.signal.status).eql('open'); - const { body: caseUpdated } = await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send({ - alertId: alert._id, - index: alert._index, - rule: { - id: 'id', - name: 'name', - }, - type: CommentType.alert, - }) - .expect(200); + const caseUpdated = await createComment(supertest, postedCase.id, { + alertId: alert._id, + index: alert._index, + rule: { + id: 'id', + name: 'name', + }, + type: CommentType.alert, + }); await es.indices.refresh({ index: alert._index }); - - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: caseUpdated.id, - version: caseUpdated.version, - status: 'in-progress', - }, - ], - }) - .expect(200); + await updateCase(supertest, { + cases: [ + { + id: caseUpdated.id, + version: caseUpdated.version, + status: CaseStatuses['in-progress'], + }, + ], + }); // force a refresh on the index that the signal is stored in so that we can search for it and get the correct // status @@ -736,11 +743,10 @@ export default ({ getService }: FtrProviderContext): void => { it('does NOT updates alert status when the status is updated and syncAlerts=false', async () => { const rule = getRuleForSignalTesting(['auditbeat-*']); - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ ...postCaseReq, settings: { syncAlerts: false } }) - .expect(200); + const postedCase = await createCase(supertest, { + ...postCaseReq, + settings: { syncAlerts: false }, + }); const { id } = await createRule(supertest, rule); await waitForRuleSuccessOrStatus(supertest, id); @@ -750,33 +756,25 @@ export default ({ getService }: FtrProviderContext): void => { const alert = signals.hits.hits[0]; expect(alert._source.signal.status).eql('open'); - const { body: caseUpdated } = await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send({ - alertId: alert._id, - index: alert._index, - type: CommentType.alert, - rule: { - id: 'id', - name: 'name', - }, - }) - .expect(200); + const caseUpdated = await createComment(supertest, postedCase.id, { + alertId: alert._id, + index: alert._index, + type: CommentType.alert, + rule: { + id: 'id', + name: 'name', + }, + }); - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: caseUpdated.id, - version: caseUpdated.version, - status: 'in-progress', - }, - ], - }) - .expect(200); + await updateCase(supertest, { + cases: [ + { + id: caseUpdated.id, + version: caseUpdated.version, + status: CaseStatuses['in-progress'], + }, + ], + }); const { body: updatedAlert } = await supertest .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) @@ -790,11 +788,10 @@ export default ({ getService }: FtrProviderContext): void => { it('it updates alert status when syncAlerts is turned on', async () => { const rule = getRuleForSignalTesting(['auditbeat-*']); - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ ...postCaseReq, settings: { syncAlerts: false } }) - .expect(200); + const postedCase = await createCase(supertest, { + ...postCaseReq, + settings: { syncAlerts: false }, + }); const { id } = await createRule(supertest, rule); await waitForRuleSuccessOrStatus(supertest, id); @@ -804,49 +801,37 @@ export default ({ getService }: FtrProviderContext): void => { const alert = signals.hits.hits[0]; expect(alert._source.signal.status).eql('open'); - const { body: caseUpdated } = await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send({ - alertId: alert._id, - index: alert._index, - rule: { - id: 'id', - name: 'name', - }, - type: CommentType.alert, - }) - .expect(200); + const caseUpdated = await createComment(supertest, postedCase.id, { + alertId: alert._id, + index: alert._index, + rule: { + id: 'id', + name: 'name', + }, + type: CommentType.alert, + }); // Update the status of the case with sync alerts off - const { body: caseStatusUpdated } = await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: caseUpdated.id, - version: caseUpdated.version, - status: 'in-progress', - }, - ], - }) - .expect(200); + const caseStatusUpdated = await updateCase(supertest, { + cases: [ + { + id: caseUpdated.id, + version: caseUpdated.version, + status: CaseStatuses['in-progress'], + }, + ], + }); // Turn sync alerts on - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: caseStatusUpdated[0].id, - version: caseStatusUpdated[0].version, - settings: { syncAlerts: true }, - }, - ], - }) - .expect(200); + await updateCase(supertest, { + cases: [ + { + id: caseStatusUpdated[0].id, + version: caseStatusUpdated[0].version, + settings: { syncAlerts: true }, + }, + ], + }); // refresh the index because syncAlerts was set to true so the alert's status should have been updated await es.indices.refresh({ index: alert._index }); @@ -863,12 +848,7 @@ export default ({ getService }: FtrProviderContext): void => { it('it does NOT updates alert status when syncAlerts is turned off', async () => { const rule = getRuleForSignalTesting(['auditbeat-*']); - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - + const postedCase = await createCase(supertest, postCaseReq); const { id } = await createRule(supertest, rule); await waitForRuleSuccessOrStatus(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); @@ -877,49 +857,37 @@ export default ({ getService }: FtrProviderContext): void => { const alert = signals.hits.hits[0]; expect(alert._source.signal.status).eql('open'); - const { body: caseUpdated } = await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send({ - alertId: alert._id, - index: alert._index, - type: CommentType.alert, - rule: { - id: 'id', - name: 'name', - }, - }) - .expect(200); + const caseUpdated = await createComment(supertest, postedCase.id, { + alertId: alert._id, + index: alert._index, + type: CommentType.alert, + rule: { + id: 'id', + name: 'name', + }, + }); // Turn sync alerts off - const { body: caseSettingsUpdated } = await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: caseUpdated.id, - version: caseUpdated.version, - settings: { syncAlerts: false }, - }, - ], - }) - .expect(200); + const caseSettingsUpdated = await updateCase(supertest, { + cases: [ + { + id: caseUpdated.id, + version: caseUpdated.version, + settings: { syncAlerts: false }, + }, + ], + }); // Update the status of the case with sync alerts off - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: caseSettingsUpdated[0].id, - version: caseSettingsUpdated[0].version, - status: 'in-progress', - }, - ], - }) - .expect(200); + await updateCase(supertest, { + cases: [ + { + id: caseSettingsUpdated[0].id, + version: caseSettingsUpdated[0].version, + status: CaseStatuses['in-progress'], + }, + ], + }); const { body: updatedAlert } = await supertest .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) 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 2249587620d5fd..1971cb5398b526 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 @@ -5,19 +5,26 @@ * 2.0. */ +/* eslint-disable @typescript-eslint/naming-convention */ + import expect from '@kbn/expect'; import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; import { ConnectorTypes, ConnectorJiraTypeFields, + CaseStatuses, + CaseUserActionResponse, } from '../../../../../../plugins/cases/common/api'; +import { getPostCaseRequest, postCaseResp, defaultUser } from '../../../../common/lib/mock'; import { - getPostCaseRequest, - postCaseResp, + createCaseAsUser, + deleteCasesByESQuery, + createCase, removeServerGeneratedPropertiesFromCase, -} from '../../../../common/lib/mock'; -import { createCaseAsUser, deleteCases } from '../../../../common/lib/utils'; + removeServerGeneratedPropertiesFromUserAction, + getAllUserAction, +} from '../../../../common/lib/utils'; import { secOnly, secOnlyRead, @@ -36,68 +43,195 @@ export default ({ getService }: FtrProviderContext): void => { describe('post_case', () => { afterEach(async () => { - await deleteCases(es); + await deleteCasesByESQuery(es); }); - it('should post a case', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(getPostCaseRequest()) - .expect(200); + describe('happy path', () => { + it('should post a case', async () => { + const postedCase = await createCase( + supertest, + getPostCaseRequest({ + connector: { + id: '123', + name: 'Jira', + type: ConnectorTypes.jira, + fields: { issueType: 'Task', priority: 'High', parent: null }, + }, + }) + ); + const data = removeServerGeneratedPropertiesFromCase(postedCase); - const data = removeServerGeneratedPropertiesFromCase(postedCase); - expect(data).to.eql(postCaseResp(postedCase.id)); - }); + expect(data).to.eql( + postCaseResp( + null, + getPostCaseRequest({ + connector: { + id: '123', + name: 'Jira', + type: ConnectorTypes.jira, + fields: { issueType: 'Task', priority: 'High', parent: null }, + }, + }) + ) + ); + }); - it('unhappy path - 400s when bad query supplied', async () => { - await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - // @ts-expect-error - .send({ ...getPostCaseRequest({ badKey: true }) }) - .expect(400); - }); + it('should post a case: none connector', async () => { + const postedCase = await createCase( + supertest, + getPostCaseRequest({ + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + }) + ); + const data = removeServerGeneratedPropertiesFromCase(postedCase); - it('unhappy path - 400s when connector is not supplied', async () => { - const { connector, ...caseWithoutConnector } = getPostCaseRequest(); + expect(data).to.eql( + postCaseResp( + null, + getPostCaseRequest({ + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + }) + ) + ); + }); - await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(caseWithoutConnector) - .expect(400); - }); + it('should create a user action when creating a case', async () => { + const postedCase = await createCase(supertest, getPostCaseRequest()); + const userActions = await getAllUserAction(supertest, postedCase.id); + const creationUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[0]); + + const { new_value, ...rest } = creationUserAction as CaseUserActionResponse; + const parsedNewValue = JSON.parse(new_value!); - it('unhappy path - 400s when connector has wrong type', async () => { - await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - ...getPostCaseRequest({ - // @ts-expect-error - connector: { id: 'wrong', name: 'wrong', type: '.not-exists', fields: null }, - }), - }) - .expect(400); + expect(rest).to.eql({ + action_field: [ + 'description', + 'status', + 'tags', + 'title', + 'connector', + 'settings', + 'owner', + ], + action: 'create', + action_by: defaultUser, + old_value: null, + case_id: `${postedCase.id}`, + comment_id: null, + sub_case_id: '', + }); + + expect(parsedNewValue).to.eql({ + type: postedCase.type, + description: postedCase.description, + title: postedCase.title, + tags: postedCase.tags, + connector: postedCase.connector, + settings: postedCase.settings, + owner: postedCase.owner, + }); + }); + + it('creates the case without connector in the configuration', async () => { + const postedCase = await createCase(supertest, getPostCaseRequest()); + const data = removeServerGeneratedPropertiesFromCase(postedCase); + + expect(data).to.eql(postCaseResp()); + }); }); - it('unhappy path - 400s when connector has wrong fields', async () => { - await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - ...getPostCaseRequest({ - // @ts-expect-error - connector: { - id: 'wrong', - name: 'wrong', - type: ConnectorTypes.jira, - fields: { unsupported: 'value' }, - } as ConnectorJiraTypeFields, - }), - }) - .expect(400); + describe('unhappy path', () => { + it('400s when bad query supplied', async () => { + await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + // @ts-expect-error + .send({ ...getPostCaseRequest({ badKey: true }) }) + .expect(400); + }); + + it('400s when connector is not supplied', async () => { + const { connector, ...caseWithoutConnector } = getPostCaseRequest(); + + await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(caseWithoutConnector) + .expect(400); + }); + + it('400s when connector has wrong type', async () => { + await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + ...getPostCaseRequest({ + // @ts-expect-error + connector: { id: 'wrong', name: 'wrong', type: '.not-exists', fields: null }, + }), + }) + .expect(400); + }); + + it('400s when connector has wrong fields', async () => { + await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + ...getPostCaseRequest({ + // @ts-expect-error + connector: { + id: 'wrong', + name: 'wrong', + type: ConnectorTypes.jira, + fields: { unsupported: 'value' }, + } as ConnectorJiraTypeFields, + }), + }) + .expect(400); + }); + + it('400s when missing title', async () => { + const { title, ...caseWithoutTitle } = getPostCaseRequest(); + + await supertest.post(CASES_URL).set('kbn-xsrf', 'true').send(caseWithoutTitle).expect(400); + }); + + it('400s when missing description', async () => { + const { description, ...caseWithoutDescription } = getPostCaseRequest(); + + await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(caseWithoutDescription) + .expect(400); + }); + + it('400s when missing tags', async () => { + const { tags, ...caseWithoutTags } = getPostCaseRequest(); + + await supertest.post(CASES_URL).set('kbn-xsrf', 'true').send(caseWithoutTags).expect(400); + }); + + it('400s if you passing status for a new case', async () => { + const req = getPostCaseRequest(); + + await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ ...req, status: CaseStatuses.open }) + .expect(400); + }); }); describe('rbac', () => { 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 c6e84766e46382..c811c0982840e2 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 @@ -10,7 +10,7 @@ import { FtrProviderContext } from '../../../../../../common/ftr_provider_contex import { CASES_URL, CASE_REPORTERS_URL } from '../../../../../../../plugins/cases/common/constants'; import { defaultUser, postCaseReq } from '../../../../../common/lib/mock'; -import { deleteCases } from '../../../../../common/lib/utils'; +import { deleteCasesByESQuery } from '../../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -19,7 +19,7 @@ export default ({ getService }: FtrProviderContext): void => { describe('get_reporters', () => { afterEach(async () => { - await deleteCases(es); + await deleteCasesByESQuery(es); }); it('should return reporters', async () => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts index 71602f993a1d4f..b71c7105be8f2b 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts @@ -8,9 +8,14 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; -import { CASES_URL, CASE_STATUS_URL } from '../../../../../../../plugins/cases/common/constants'; +import { CaseStatuses } from '../../../../../../../plugins/cases/common/api'; import { postCaseReq } from '../../../../../common/lib/mock'; -import { deleteCases } from '../../../../../common/lib/utils'; +import { + deleteCasesByESQuery, + createCase, + updateCase, + getAllCasesStatuses, +} from '../../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -19,58 +24,37 @@ export default ({ getService }: FtrProviderContext): void => { describe('get_status', () => { afterEach(async () => { - await deleteCases(es); + await deleteCasesByESQuery(es); }); it('should return case statuses', async () => { - await supertest.post(CASES_URL).set('kbn-xsrf', 'true').send(postCaseReq); + await createCase(supertest, postCaseReq); + const inProgressCase = await createCase(supertest, postCaseReq); + const postedCase = await createCase(supertest, postCaseReq); - const { body: inProgressCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq); - - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: 'closed', - }, - ], - }) - .expect(200); + await updateCase(supertest, { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses.closed, + }, + ], + }); - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: inProgressCase.id, - version: inProgressCase.version, - status: 'in-progress', - }, - ], - }) - .expect(200); + await updateCase(supertest, { + cases: [ + { + id: inProgressCase.id, + version: inProgressCase.version, + status: CaseStatuses['in-progress'], + }, + ], + }); - const { body } = await supertest - .get(CASE_STATUS_URL) - .set('kbn-xsrf', 'true') - .send() - .expect(200); + const statuses = await getAllCasesStatuses(supertest); - expect(body).to.eql({ + expect(statuses).to.eql({ count_open_cases: 1, count_closed_cases: 1, count_in_progress_cases: 1, 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 3ca8e9b6aa3ce8..a47cf12158a34e 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 @@ -10,7 +10,7 @@ import { FtrProviderContext } from '../../../../../../common/ftr_provider_contex import { CASES_URL, CASE_TAGS_URL } from '../../../../../../../plugins/cases/common/constants'; import { postCaseReq } from '../../../../../common/lib/mock'; -import { deleteCases } from '../../../../../common/lib/utils'; +import { deleteCasesByESQuery } from '../../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -19,7 +19,7 @@ export default ({ getService }: FtrProviderContext): void => { describe('get_tags', () => { afterEach(async () => { - await deleteCases(es); + await deleteCasesByESQuery(es); }); it('should return case tags', async () => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/delete_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts similarity index 52% rename from x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/delete_comment.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts index c0e905f9ad2011..8394109ce6696f 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/delete_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts @@ -6,19 +6,22 @@ */ 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 { postCaseReq, postCommentUserReq } from '../../../../../common/lib/mock'; +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; import { createCaseAction, createSubCase, deleteAllCaseItems, deleteCaseAction, - deleteCases, + deleteCasesByESQuery, deleteCasesUserActions, deleteComments, -} from '../../../../../common/lib/utils'; + createCase, + createComment, + deleteComment, +} from '../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -27,82 +30,60 @@ export default ({ getService }: FtrProviderContext): void => { describe('delete_comment', () => { afterEach(async () => { - await deleteCases(es); + await deleteCasesByESQuery(es); await deleteComments(es); await deleteCasesUserActions(es); }); - it('should delete a comment', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const { body: patchedCase } = await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - const { body: comment } = await supertest - .delete(`${CASES_URL}/${postedCase.id}/comments/${patchedCase.comments[0].id}`) - .set('kbn-xsrf', 'true') - .expect(204) - .send(); - expect(comment).to.eql({}); - }); + describe('happy path', () => { + it('should delete a comment', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); + const comment = await deleteComment(supertest, postedCase.id, patchedCase.comments![0].id); - it('unhappy path - 404s when comment belongs to different case', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const { body: patchedCase } = await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - const { body } = await supertest - .delete(`${CASES_URL}/fake-id/comments/${patchedCase.comments[0].id}`) - .set('kbn-xsrf', 'true') - .send() - .expect(404); - - expect(body.message).to.eql( - `This comment ${patchedCase.comments[0].id} does not exist in fake-id).` - ); + expect(comment).to.eql({}); + }); }); - it('unhappy path - 404s when comment is not there', async () => { - await supertest - .delete(`${CASES_URL}/fake-id/comments/fake-id`) - .set('kbn-xsrf', 'true') - .send() - .expect(404); - }); + describe('unhappy path', () => { + it('404s when comment belongs to different case', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); + const error = (await deleteComment( + supertest, + 'fake-id', + patchedCase.comments![0].id, + 404 + )) as Error; + + expect(error.message).to.be( + `This comment ${patchedCase.comments![0].id} does not exist in fake-id.` + ); + }); - it('should return a 400 when attempting to delete all comments when passing the `subCaseId` parameter', async () => { - const { body } = await supertest - .delete(`${CASES_URL}/case-id/comments?subCaseId=value`) - .set('kbn-xsrf', 'true') - .send() - .expect(400); - // make sure the failure is because of the subCaseId - expect(body.message).to.contain('subCaseId'); - }); + it('404s when comment is not there', async () => { + await deleteComment(supertest, 'fake-id', 'fake-id', 404); + }); - it('should return a 400 when attempting to delete a single comment when passing the `subCaseId` parameter', async () => { - const { body } = await supertest - .delete(`${CASES_URL}/case-id/comments/comment-id?subCaseId=value`) - .set('kbn-xsrf', 'true') - .send() - .expect(400); - // make sure the failure is because of the subCaseId - expect(body.message).to.contain('subCaseId'); + it('should return a 400 when attempting to delete all comments when passing the `subCaseId` parameter', async () => { + const { body } = await supertest + .delete(`${CASES_URL}/case-id/comments?subCaseId=value`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + // make sure the failure is because of the subCaseId + expect(body.message).to.contain('subCaseId'); + }); + + it('should return a 400 when attempting to delete a single comment when passing the `subCaseId` parameter', async () => { + const { body } = await supertest + .delete(`${CASES_URL}/case-id/comments/comment-id?subCaseId=value`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + // make sure the failure is because of the subCaseId + expect(body.message).to.contain('subCaseId'); + }); }); // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/find_comments.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts similarity index 92% rename from x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/find_comments.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts index c3919516bb969d..95f15d1e330ffe 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/find_comments.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts @@ -6,20 +6,20 @@ */ 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 { CommentsResponse, CommentType } from '../../../../../../../plugins/cases/common/api'; -import { postCaseReq, postCommentUserReq } from '../../../../../common/lib/mock'; +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { CommentsResponse, CommentType } from '../../../../../../plugins/cases/common/api'; +import { postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; import { createCaseAction, createSubCase, deleteAllCaseItems, deleteCaseAction, - deleteCases, + deleteCasesByESQuery, deleteCasesUserActions, deleteComments, -} from '../../../../../common/lib/utils'; +} from '../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -28,7 +28,7 @@ export default ({ getService }: FtrProviderContext): void => { describe('find_comments', () => { afterEach(async () => { - await deleteCases(es); + await deleteCasesByESQuery(es); await deleteComments(es); await deleteCasesUserActions(es); }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/get_all_comments.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts similarity index 77% rename from x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/get_all_comments.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts index c91337bcbfeaf2..06eb9d0fb4174c 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/get_all_comments.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts @@ -6,17 +6,20 @@ */ 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 { postCaseReq, postCommentUserReq } from '../../../../../common/lib/mock'; +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; import { createCaseAction, createSubCase, deleteAllCaseItems, deleteCaseAction, -} from '../../../../../common/lib/utils'; -import { CommentType } from '../../../../../../../plugins/cases/common/api'; + createCase, + createComment, + getAllComments, +} from '../../../../common/lib/utils'; +import { CommentType } from '../../../../../../plugins/cases/common/api'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -29,29 +32,10 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should get multiple comments for a single case', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - const { body: comments } = await supertest - .get(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); + const postedCase = await createCase(supertest, postCaseReq); + await createComment(supertest, postedCase.id, postCommentUserReq); + await createComment(supertest, postedCase.id, postCommentUserReq); + const comments = await getAllComments(supertest, postedCase.id); expect(comments.length).to.eql(2); }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/get_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts similarity index 51% rename from x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/get_comment.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts index 1373d56c311c5c..e843b31d18dfd3 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/get_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts @@ -6,17 +6,20 @@ */ 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 { postCaseReq, postCommentUserReq } from '../../../../../common/lib/mock'; +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; import { createCaseAction, createSubCase, deleteAllCaseItems, deleteCaseAction, -} from '../../../../../common/lib/utils'; -import { CommentResponse, CommentType } from '../../../../../../../plugins/cases/common/api'; + createCase, + createComment, + getComment, +} from '../../../../common/lib/utils'; +import { CommentType } from '../../../../../../plugins/cases/common/api'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -29,33 +32,15 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should get a comment', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); + const comment = await getComment(supertest, postedCase.id, patchedCase.comments![0].id); - const { body: patchedCase } = await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - const { body: comment } = await supertest - .get(`${CASES_URL}/${postedCase.id}/comments/${patchedCase.comments[0].id}`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(comment).to.eql(patchedCase.comments[0]); + expect(comment).to.eql(patchedCase.comments![0]); }); it('unhappy path - 404s when comment is not there', async () => { - await supertest - .get(`${CASES_URL}/fake-id/comments/fake-comment`) - .set('kbn-xsrf', 'true') - .send() - .expect(404); + await getComment(supertest, 'fake-id', 'fake-id', 404); }); // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests @@ -69,9 +54,7 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should get a sub case comment', async () => { const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - const { body: comment }: { body: CommentResponse } = await supertest - .get(`${CASES_URL}/${caseInfo.id}/comments/${caseInfo.comments![0].id}`) - .expect(200); + const comment = await getComment(supertest, caseInfo.id, caseInfo.comments![0].id); expect(comment.type).to.be(CommentType.generatedAlert); }); }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/migrations.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/migrations.ts similarity index 86% rename from x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/migrations.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/migrations.ts index 8ceb81017ecdb0..50a219c5e84b30 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/migrations.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/migrations.ts @@ -6,8 +6,8 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; -import { CASES_URL } from '../../../../../../../plugins/cases/common/constants'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; // eslint-disable-next-line import/no-default-export export default function createGetTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/patch_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts similarity index 51% rename from x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/patch_comment.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts index 3c0bdd4a14cee8..b82800b6bd7a6d 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/comments/patch_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts @@ -7,25 +7,34 @@ import { omit } from 'lodash/fp'; 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 { CaseResponse, CommentType } from '../../../../../../../plugins/cases/common/api'; +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { + AttributesTypeAlerts, + AttributesTypeUser, + CaseResponse, + CommentType, +} from '../../../../../../plugins/cases/common/api'; import { defaultUser, postCaseReq, postCommentUserReq, postCommentAlertReq, -} from '../../../../../common/lib/mock'; + postCommentGenAlertReq, +} from '../../../../common/lib/mock'; import { createCaseAction, createSubCase, deleteAllCaseItems, deleteCaseAction, - deleteCases, + deleteCasesByESQuery, deleteCasesUserActions, deleteComments, -} from '../../../../../common/lib/utils'; + createCase, + createComment, + updateComment, +} from '../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -34,7 +43,7 @@ export default ({ getService }: FtrProviderContext): void => { describe('patch_comment', () => { afterEach(async () => { - await deleteCases(es); + await deleteCasesByESQuery(es); await deleteComments(es); await deleteCasesUserActions(es); }); @@ -138,121 +147,88 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should patch a comment', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const { body: patchedCase } = await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); const newComment = 'Well I decided to update my comment. So what? Deal with it.'; - const { body } = await supertest - .patch(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send({ - id: patchedCase.comments[0].id, - version: patchedCase.comments[0].version, - comment: newComment, - type: CommentType.user, - }) - .expect(200); + const updatedCase = await updateComment(supertest, postedCase.id, { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + comment: newComment, + type: CommentType.user, + }); - expect(body.comments[0].comment).to.eql(newComment); - expect(body.comments[0].type).to.eql('user'); - expect(body.updated_by).to.eql(defaultUser); + const userComment = updatedCase.comments![0] as AttributesTypeUser; + expect(userComment.comment).to.eql(newComment); + expect(userComment.type).to.eql(CommentType.user); + expect(updatedCase.updated_by).to.eql(defaultUser); }); it('should patch an alert', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const { body: patchedCase } = await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentAlertReq) - .expect(200); - - const { body } = await supertest - .patch(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send({ - id: patchedCase.comments[0].id, - version: patchedCase.comments[0].version, - type: CommentType.alert, - alertId: 'new-id', - index: postCommentAlertReq.index, - rule: { - id: 'id', - name: 'name', - }, - }) - .expect(200); + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment(supertest, postedCase.id, postCommentAlertReq); + const updatedCase = await updateComment(supertest, postedCase.id, { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + type: CommentType.alert, + alertId: 'new-id', + index: postCommentAlertReq.index, + rule: { + id: 'id', + name: 'name', + }, + }); - expect(body.comments[0].alertId).to.eql('new-id'); - expect(body.comments[0].index).to.eql(postCommentAlertReq.index); - expect(body.comments[0].type).to.eql('alert'); - expect(body.updated_by).to.eql(defaultUser); + const alertComment = updatedCase.comments![0] as AttributesTypeAlerts; + expect(alertComment.alertId).to.eql('new-id'); + expect(alertComment.index).to.eql(postCommentAlertReq.index); + expect(alertComment.type).to.eql(CommentType.alert); + expect(alertComment.rule).to.eql({ + id: 'id', + name: 'name', + }); + expect(alertComment.updated_by).to.eql(defaultUser); }); it('unhappy path - 404s when comment is not there', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - await supertest - .patch(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send({ + const postedCase = await createCase(supertest, postCaseReq); + await updateComment( + supertest, + postedCase.id, + { id: 'id', version: 'version', type: CommentType.user, comment: 'comment', - }) - .expect(404); + }, + 404 + ); }); it('unhappy path - 404s when case is not there', async () => { - await supertest - .patch(`${CASES_URL}/fake-id/comments`) - .set('kbn-xsrf', 'true') - .send({ + await updateComment( + supertest, + 'fake-id', + { id: 'id', version: 'version', type: CommentType.user, comment: 'comment', - }) - .expect(404); + }, + 404 + ); }); it('unhappy path - 400s when trying to change comment type', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const { body: patchedCase } = await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - await supertest - .patch(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send({ - id: patchedCase.comments[0].id, - version: patchedCase.comments[0].version, + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); + + await updateComment( + supertest, + postedCase.id, + { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, type: CommentType.alert, alertId: 'test-id', index: 'test-index', @@ -260,73 +236,50 @@ export default ({ getService }: FtrProviderContext): void => { id: 'id', name: 'name', }, - }) - .expect(400); + }, + 400 + ); }); it('unhappy path - 400s when missing attributes for type user', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const { body: patchedCase } = await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - await supertest - .patch(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send({ - id: patchedCase.comments[0].id, - version: patchedCase.comments[0].version, - }) - .expect(400); + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); + + await updateComment( + supertest, + postedCase.id, + // @ts-expect-error + { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + }, + 400 + ); }); it('unhappy path - 400s when adding excess attributes for type user', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const { body: patchedCase } = await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); for (const attribute of ['alertId', 'index']) { - await supertest - .patch(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send({ - id: patchedCase.comments[0].id, - version: patchedCase.comments[0].version, + await updateComment( + supertest, + postedCase.id, + { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, comment: 'a comment', type: CommentType.user, [attribute]: attribute, - }) - .expect(400); + }, + 400 + ); } }); it('unhappy path - 400s when missing attributes for type alert', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const { body: patchedCase } = await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment(supertest, postedCase.id, postCommentAlertReq); const allRequestAttributes = { type: CommentType.alert, @@ -340,38 +293,31 @@ export default ({ getService }: FtrProviderContext): void => { for (const attribute of ['alertId', 'index']) { const requestAttributes = omit(attribute, allRequestAttributes); - await supertest - .patch(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send({ - id: patchedCase.comments[0].id, - version: patchedCase.comments[0].version, + await updateComment( + supertest, + postedCase.id, + // @ts-expect-error + { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, ...requestAttributes, - }) - .expect(400); + }, + 400 + ); } }); it('unhappy path - 400s when adding excess attributes for type alert', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const { body: patchedCase } = await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment(supertest, postedCase.id, postCommentAlertReq); for (const attribute of ['comment']) { - await supertest - .patch(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send({ - id: patchedCase.comments[0].id, - version: patchedCase.comments[0].version, + await updateComment( + supertest, + postedCase.id, + { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, type: CommentType.alert, index: 'test-index', alertId: 'test-id', @@ -380,35 +326,81 @@ export default ({ getService }: FtrProviderContext): void => { name: 'name', }, [attribute]: attribute, - }) - .expect(400); + }, + 400 + ); } }); it('unhappy path - 409s when conflict', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const { body: patchedCase } = await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); const newComment = 'Well I decided to update my comment. So what? Deal with it.'; - await supertest - .patch(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send({ - id: patchedCase.comments[0].id, + await updateComment( + supertest, + postedCase.id, + { + id: patchedCase.comments![0].id, version: 'version-mismatch', type: CommentType.user, comment: newComment, - }) - .expect(409); + }, + 409 + ); + }); + + describe('alert format', () => { + type AlertComment = CommentType.alert | CommentType.generatedAlert; + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed create a test case for generated alerts here + for (const [alertId, index, type] of [ + ['1', ['index1', 'index2'], CommentType.alert], + [['1', '2'], 'index', CommentType.alert], + ]) { + it(`throws an error with an alert comment with contents id: ${alertId} indices: ${index} type: ${type}`, async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment(supertest, postedCase.id, postCommentAlertReq); + + await updateComment( + supertest, + patchedCase.id, + { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + type: type as AlertComment, + alertId, + index, + rule: postCommentAlertReq.rule, + }, + 400 + ); + }); + } + + for (const [alertId, index, type] of [ + ['1', ['index1'], CommentType.alert], + [['1', '2'], ['index', 'other-index'], CommentType.alert], + ]) { + it(`does not throw an error with an alert comment with contents id: ${alertId} indices: ${index} type: ${type}`, async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment(supertest, postedCase.id, { + ...postCommentAlertReq, + alertId, + index, + type: type as AlertComment, + }); + + await updateComment(supertest, postedCase.id, { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + type: type as AlertComment, + alertId, + index, + rule: postCommentAlertReq.rule, + }); + }); + } }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts new file mode 100644 index 00000000000000..b63e21eea201a3 --- /dev/null +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts @@ -0,0 +1,457 @@ +/* + * 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 { omit } from 'lodash/fp'; +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../../plugins/security_solution/common/constants'; +import { + CommentsResponse, + CommentType, + AttributesTypeUser, + AttributesTypeAlerts, +} from '../../../../../../plugins/cases/common/api'; +import { + defaultUser, + postCaseReq, + postCommentUserReq, + postCommentAlertReq, + postCollectionReq, + postCommentGenAlertReq, +} from '../../../../common/lib/mock'; +import { + createCaseAction, + createSubCase, + deleteAllCaseItems, + deleteCaseAction, + deleteCasesByESQuery, + deleteCasesUserActions, + deleteComments, + createCase, + createComment, + getAllUserAction, + removeServerGeneratedPropertiesFromUserAction, + removeServerGeneratedPropertiesFromSavedObject, +} from '../../../../common/lib/utils'; +import { + createSignalsIndex, + deleteSignalsIndex, + deleteAllAlerts, + getRuleForSignalTesting, + waitForRuleSuccessOrStatus, + waitForSignalsToBePresent, + getSignalsByIds, + createRule, + getQuerySignalIds, +} from '../../../../../detection_engine_api_integration/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + + describe('post_comment', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + await deleteComments(es); + await deleteCasesUserActions(es); + }); + + describe('happy path', () => { + it('should post a comment', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); + const comment = removeServerGeneratedPropertiesFromSavedObject( + patchedCase.comments![0] as AttributesTypeUser + ); + + expect(comment).to.eql({ + type: postCommentUserReq.type, + comment: postCommentUserReq.comment, + associationType: 'case', + created_by: defaultUser, + pushed_at: null, + pushed_by: null, + updated_by: null, + }); + + // updates the case correctly after adding a comment + expect(patchedCase.totalComment).to.eql(patchedCase.comments!.length); + expect(patchedCase.updated_by).to.eql(defaultUser); + }); + + it('should post an alert', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment(supertest, postedCase.id, postCommentAlertReq); + const comment = removeServerGeneratedPropertiesFromSavedObject( + patchedCase.comments![0] as AttributesTypeAlerts + ); + + expect(comment).to.eql({ + type: postCommentAlertReq.type, + alertId: postCommentAlertReq.alertId, + index: postCommentAlertReq.index, + rule: postCommentAlertReq.rule, + associationType: 'case', + created_by: defaultUser, + pushed_at: null, + pushed_by: null, + updated_by: null, + }); + + // updates the case correctly after adding a comment + expect(patchedCase.totalComment).to.eql(patchedCase.comments!.length); + expect(patchedCase.updated_by).to.eql(defaultUser); + }); + + it('creates a user action', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); + const userActions = await getAllUserAction(supertest, postedCase.id); + const commentUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); + + expect(commentUserAction).to.eql({ + action_field: ['comment'], + action: 'create', + action_by: defaultUser, + new_value: `{"comment":"${postCommentUserReq.comment}","type":"${postCommentUserReq.type}"}`, + old_value: null, + case_id: `${postedCase.id}`, + comment_id: `${patchedCase.comments![0].id}`, + sub_case_id: '', + }); + }); + }); + + describe('unhappy path', () => { + it('400s when type is missing', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await createComment( + supertest, + postedCase.id, + { + // @ts-expect-error + bad: 'comment', + }, + 400 + ); + }); + + it('400s when missing attributes for type user', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await createComment( + supertest, + postedCase.id, + // @ts-expect-error + { + type: CommentType.user, + }, + 400 + ); + }); + + it('400s when adding excess attributes for type user', async () => { + const postedCase = await createCase(supertest, postCaseReq); + + for (const attribute of ['alertId', 'index']) { + await createComment( + supertest, + postedCase.id, + { + type: CommentType.user, + [attribute]: attribute, + comment: 'a comment', + }, + 400 + ); + } + }); + + it('400s when missing attributes for type alert', async () => { + const postedCase = await createCase(supertest, postCaseReq); + + const allRequestAttributes = { + type: CommentType.alert, + index: 'test-index', + alertId: 'test-id', + rule: { + id: 'id', + name: 'name', + }, + }; + + for (const attribute of ['alertId', 'index']) { + const requestAttributes = omit(attribute, allRequestAttributes); + // @ts-expect-error + await createComment(supertest, postedCase.id, requestAttributes, 400); + } + }); + + it('400s when adding excess attributes for type alert', async () => { + const postedCase = await createCase(supertest, postCaseReq); + + for (const attribute of ['comment']) { + await createComment( + supertest, + postedCase.id, + { + type: CommentType.alert, + [attribute]: attribute, + alertId: 'test-id', + index: 'test-index', + rule: { + id: 'id', + name: 'name', + }, + }, + 400 + ); + } + }); + + it('400s when case is missing', async () => { + await createComment( + supertest, + 'not-exists', + { + // @ts-expect-error + bad: 'comment', + }, + 400 + ); + }); + + it('400s when adding an alert to a closed case', async () => { + const postedCase = await createCase(supertest, postCaseReq); + + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: 'closed', + }, + ], + }) + .expect(200); + + await createComment(supertest, postedCase.id, postCommentAlertReq, 400); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + it.skip('400s when adding an alert to a collection case', async () => { + const postedCase = await createCase(supertest, postCollectionReq); + await createComment(supertest, postedCase.id, postCommentAlertReq, 400); + }); + + it('400s when adding a generated alert to an individual case', async () => { + const postedCase = await createCase(supertest, postCaseReq); + + await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentGenAlertReq) + .expect(400); + }); + + it('should return a 400 when passing the subCaseId', async () => { + const { body } = await supertest + .post(`${CASES_URL}/case-id/comments?subCaseId=value`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(400); + expect(body.message).to.contain('subCaseId'); + }); + }); + + describe('alerts', () => { + beforeEach(async () => { + await esArchiver.load('auditbeat/hosts'); + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(supertest); + await esArchiver.unload('auditbeat/hosts'); + }); + + it('should change the status of the alert if sync alert is on', async () => { + const rule = getRuleForSignalTesting(['auditbeat-*']); + const postedCase = await createCase(supertest, postCaseReq); + + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: 'in-progress', + }, + ], + }) + .expect(200); + + const { id } = await createRule(supertest, rule); + await waitForRuleSuccessOrStatus(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signals = await getSignalsByIds(supertest, [id]); + + const alert = signals.hits.hits[0]; + expect(alert._source.signal.status).eql('open'); + + await createComment(supertest, postedCase.id, { + alertId: alert._id, + index: alert._index, + rule: { + id: 'id', + name: 'name', + }, + type: CommentType.alert, + }); + + const { body: updatedAlert } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQuerySignalIds([alert._id])) + .expect(200); + + expect(updatedAlert.hits.hits[0]._source.signal.status).eql('in-progress'); + }); + + it('should NOT change the status of the alert if sync alert is off', async () => { + const rule = getRuleForSignalTesting(['auditbeat-*']); + const postedCase = await createCase(supertest, { + ...postCaseReq, + settings: { syncAlerts: false }, + }); + + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: 'in-progress', + }, + ], + }) + .expect(200); + + const { id } = await createRule(supertest, rule); + await waitForRuleSuccessOrStatus(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signals = await getSignalsByIds(supertest, [id]); + + const alert = signals.hits.hits[0]; + expect(alert._source.signal.status).eql('open'); + + await createComment(supertest, postedCase.id, { + alertId: alert._id, + index: alert._index, + rule: { + id: 'id', + name: 'name', + }, + type: CommentType.alert, + }); + + const { body: updatedAlert } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQuerySignalIds([alert._id])) + .expect(200); + + expect(updatedAlert.hits.hits[0]._source.signal.status).eql('open'); + }); + }); + + describe('alert format', () => { + type AlertComment = CommentType.alert | CommentType.generatedAlert; + + for (const [alertId, index, type] of [ + ['1', ['index1', 'index2'], CommentType.alert], + [['1', '2'], 'index', CommentType.alert], + ['1', ['index1', 'index2'], CommentType.generatedAlert], + [['1', '2'], 'index', CommentType.generatedAlert], + ]) { + it(`throws an error with an alert comment with contents id: ${alertId} indices: ${index} type: ${type}`, async () => { + const postedCase = await createCase(supertest, postCaseReq); + await createComment( + supertest, + postedCase.id, + { ...postCommentAlertReq, alertId, index, type: type as AlertComment }, + 400 + ); + }); + } + + for (const [alertId, index, type] of [ + ['1', ['index1'], CommentType.alert], + [['1', '2'], ['index', 'other-index'], CommentType.alert], + ]) { + it(`does not throw an error with an alert comment with contents id: ${alertId} indices: ${index} type: ${type}`, async () => { + const postedCase = await createCase(supertest, postCaseReq); + await createComment( + supertest, + postedCase.id, + { + ...postCommentAlertReq, + alertId, + index, + type: type as AlertComment, + }, + 200 + ); + }); + } + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('sub case comments', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('posts a new comment for a sub case', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + // create another sub case just to make sure we get the right comments + await createSubCase({ supertest, actionID }); + await supertest + .post(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(200); + + const { body: subCaseComments }: { body: CommentsResponse } = await supertest + .get(`${CASES_URL}/${caseInfo.id}/comments/_find?subCaseId=${caseInfo.subCases![0].id}`) + .send() + .expect(200); + expect(subCaseComments.total).to.be(2); + expect(subCaseComments.comments[0].type).to.be(CommentType.generatedAlert); + expect(subCaseComments.comments[1].type).to.be(CommentType.user); + }); + }); + }); +}; 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 391cb3a4e5a2ab..1f36ecc812c5fa 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 @@ -8,49 +8,103 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { CASE_CONFIGURE_URL } from '../../../../../../plugins/cases/common/constants'; +import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; import { - getConfiguration, - removeServerGeneratedPropertiesFromConfigure, + ExternalServiceSimulator, + getExternalServiceSimulatorPath, +} from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; +import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; +import { + removeServerGeneratedPropertiesFromSavedObject, getConfigurationOutput, deleteConfiguration, + getConfiguration, + createConfiguration, + getConfigurationRequest, + createConnector, + getServiceNowConnector, } from '../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); + const kibanaServer = getService('kibanaServer'); describe('get_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 return an empty find body correctly if no configuration is loaded', async () => { - const { body } = await supertest - .get(CASE_CONFIGURE_URL) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(body).to.eql({}); + const configuration = await getConfiguration(supertest); + expect(configuration).to.eql({}); }); it('should return a configuration', async () => { - await supertest - .post(CASE_CONFIGURE_URL) - .set('kbn-xsrf', 'true') - .send(getConfiguration()) - .expect(200); - - const { body } = await supertest - .get(CASE_CONFIGURE_URL) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - const data = removeServerGeneratedPropertiesFromConfigure(body); + await createConfiguration(supertest); + const configuration = await getConfiguration(supertest); + + const data = removeServerGeneratedPropertiesFromSavedObject(configuration); expect(data).to.eql(getConfigurationOutput()); }); + + it('should return a configuration with mapping', async () => { + const connector = await createConnector(supertest, { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }); + + actionsRemover.add('default', connector.id, 'action', 'actions'); + + await createConfiguration( + supertest, + getConfigurationRequest({ + id: connector.id, + name: connector.name, + type: connector.connector_type_id as ConnectorTypes, + }) + ); + + const configuration = await getConfiguration(supertest); + const data = removeServerGeneratedPropertiesFromSavedObject(configuration); + 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, + }, + }) + ); + }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/get_connectors.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/get_connectors.ts index 1b6cf2ad56c593..cfa23a968182f7 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/get_connectors.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/get_connectors.ts @@ -8,8 +8,16 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { CASE_CONFIGURE_CONNECTORS_URL } from '../../../../../../plugins/cases/common/constants'; import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; +import { + getCaseConnectors, + createConnector, + getServiceNowConnector, + getJiraConnector, + getResilientConnector, + getServiceNowSIRConnector, + getWebhookConnector, +} from '../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -22,13 +30,66 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should return an empty find body correctly if no connectors are loaded', async () => { - const { body } = await supertest - .get(`${CASE_CONFIGURE_CONNECTORS_URL}/_find`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); + const connectors = await getCaseConnectors(supertest); + expect(connectors).to.eql([]); + }); + + it('should return case owned connectors', async () => { + const sn = await createConnector(supertest, getServiceNowConnector()); + actionsRemover.add('default', sn.id, 'action', 'actions'); + + const jira = await createConnector(supertest, getJiraConnector()); + actionsRemover.add('default', jira.id, 'action', 'actions'); + + const resilient = await createConnector(supertest, getResilientConnector()); + actionsRemover.add('default', resilient.id, 'action', 'actions'); + + const sir = await createConnector(supertest, getServiceNowSIRConnector()); + actionsRemover.add('default', sir.id, 'action', 'actions'); + + // Should not be returned when getting the connectors + const webhook = await createConnector(supertest, getWebhookConnector()); + actionsRemover.add('default', webhook.id, 'action', 'actions'); + + const connectors = await getCaseConnectors(supertest); + expect(connectors).to.eql([ + { + id: jira.id, + actionTypeId: '.jira', + name: 'Jira Connector', + config: { apiUrl: 'http://some.non.existent.com', projectKey: 'pkey' }, + isPreconfigured: false, + referencedByCount: 0, + }, + { + id: resilient.id, + actionTypeId: '.resilient', + name: 'Resilient Connector', + config: { apiUrl: 'http://some.non.existent.com', orgId: 'pkey' }, + isPreconfigured: false, + referencedByCount: 0, + }, + { + id: sn.id, + actionTypeId: '.servicenow', + name: 'ServiceNow Connector', + config: { apiUrl: 'http://some.non.existent.com' }, + isPreconfigured: false, + referencedByCount: 0, + }, + { + id: sir.id, + actionTypeId: '.servicenow-sir', + name: 'ServiceNow Connector', + config: { apiUrl: 'http://some.non.existent.com' }, + isPreconfigured: false, + referencedByCount: 0, + }, + ]); + }); - expect(body).to.eql([]); + it.skip('filters out connectors that are not enabled in license', async () => { + // TODO: Should find a way to downgrade license to gold and upgrade back to trial }); }); }; 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 1e2ef74479ffd9..8901447e37b3ae 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 @@ -7,80 +7,132 @@ import expect from '@kbn/expect'; 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 { CASE_CONFIGURE_URL } from '../../../../../../plugins/cases/common/constants'; import { - getConfiguration, - removeServerGeneratedPropertiesFromConfigure, + getConfigurationRequest, + removeServerGeneratedPropertiesFromSavedObject, getConfigurationOutput, deleteConfiguration, + createConfiguration, + updateConfiguration, + getServiceNowConnector, + createConnector, } from '../../../../common/lib/utils'; +import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); + const kibanaServer = getService('kibanaServer'); describe('patch_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 patch a configuration', async () => { - const res = await supertest - .post(CASE_CONFIGURE_URL) - .set('kbn-xsrf', 'true') - .send(getConfiguration()) - .expect(200); - - const { body } = await supertest - .patch(CASE_CONFIGURE_URL) - .set('kbn-xsrf', 'true') - .send({ closure_type: 'close-by-pushing', version: res.body.version }) - .expect(200); - - const data = removeServerGeneratedPropertiesFromConfigure(body); + const configuration = await createConfiguration(supertest); + const newConfiguration = await updateConfiguration(supertest, { + closure_type: 'close-by-pushing', + version: configuration.version, + }); + + const data = removeServerGeneratedPropertiesFromSavedObject(newConfiguration); expect(data).to.eql({ ...getConfigurationOutput(true), closure_type: 'close-by-pushing' }); }); + it('should patch a configuration: connector', async () => { + const connector = await createConnector(supertest, { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }); + + actionsRemover.add('default', connector.id, 'action', 'actions'); + + const configuration = await createConfiguration(supertest); + const newConfiguration = await updateConfiguration(supertest, { + ...getConfigurationRequest({ + id: connector.id, + name: connector.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: connector.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 supertest - .post(CASE_CONFIGURE_URL) - .set('kbn-xsrf', 'true') - .send(getConfiguration()) - .expect(200); - - await supertest - .patch(CASE_CONFIGURE_URL) - .set('kbn-xsrf', 'true') - // @ts-ignore We need it to test unsupported types - .send(getConfiguration({ type: '.unsupported' })) - .expect(400); + await createConfiguration(supertest); + await updateConfiguration( + supertest, + // @ts-expect-error + getConfigurationRequest({ type: '.unsupported' }), + 400 + ); }); it('should not patch a configuration with unsupported connector fields', async () => { - await supertest - .post(CASE_CONFIGURE_URL) - .set('kbn-xsrf', 'true') - .send(getConfiguration()) - .expect(200); - - await supertest - .patch(CASE_CONFIGURE_URL) - .set('kbn-xsrf', 'true') - // @ts-ignore We need it to test unsupported fields - .send(getConfiguration({ type: '.jira', fields: { unsupported: 'value' } })) - .expect(400); + await createConfiguration(supertest); + await updateConfiguration( + supertest, + // @ts-expect-error + getConfigurationRequest({ type: '.jira', fields: { unsupported: 'value' } }), + 400 + ); }); it('should handle patch request when there is no configuration', async () => { - const { body } = await supertest - .patch(CASE_CONFIGURE_URL) - .set('kbn-xsrf', 'true') - .send({ closure_type: 'close-by-pushing', version: 'no-version' }) - .expect(409); + const error = await updateConfiguration( + supertest, + { closure_type: 'close-by-pushing', version: 'no-version' }, + 409 + ); - expect(body).to.eql({ + expect(error).to.eql({ error: 'Conflict', message: 'You can not patch this configuration since you did not created first with a post.', @@ -89,19 +141,14 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should handle patch request when versions are different', async () => { - await supertest - .post(CASE_CONFIGURE_URL) - .set('kbn-xsrf', 'true') - .send(getConfiguration()) - .expect(200); - - const { body } = await supertest - .patch(CASE_CONFIGURE_URL) - .set('kbn-xsrf', 'true') - .send({ closure_type: 'close-by-pushing', version: 'no-version' }) - .expect(409); - - expect(body).to.eql({ + await createConfiguration(supertest); + const error = await updateConfiguration( + supertest, + { closure_type: 'close-by-pushing', version: 'no-version' }, + 409 + ); + + expect(error).to.eql({ error: 'Conflict', message: 'This configuration has been updated. Please refresh before saving additional updates.', 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 9d0fad202a5179..c74e048edcfa03 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 @@ -6,14 +6,16 @@ */ import expect from '@kbn/expect'; +import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { CASE_CONFIGURE_URL } from '../../../../../../plugins/cases/common/constants'; import { - getConfiguration, - removeServerGeneratedPropertiesFromConfigure, + getConfigurationRequest, + removeServerGeneratedPropertiesFromSavedObject, getConfigurationOutput, deleteConfiguration, + createConfiguration, + getConfiguration, } from '../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export @@ -27,55 +29,130 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should create a configuration', async () => { - const { body } = await supertest - .post(CASE_CONFIGURE_URL) - .set('kbn-xsrf', 'true') - .send(getConfiguration()) - .expect(200); + const configuration = await createConfiguration(supertest); - const data = removeServerGeneratedPropertiesFromConfigure(body); + const data = removeServerGeneratedPropertiesFromSavedObject(configuration); expect(data).to.eql(getConfigurationOutput()); }); it('should keep only the latest configuration', async () => { - await supertest - .post(CASE_CONFIGURE_URL) - .set('kbn-xsrf', 'true') - .send(getConfiguration({ id: 'connector-2' })) - .expect(200); - - await supertest - .post(CASE_CONFIGURE_URL) - .set('kbn-xsrf', 'true') - .send(getConfiguration()) - .expect(200); - - const { body } = await supertest - .get(CASE_CONFIGURE_URL) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - const data = removeServerGeneratedPropertiesFromConfigure(body); + await createConfiguration(supertest, getConfigurationRequest({ id: 'connector-2' })); + await createConfiguration(supertest); + const configuration = await getConfiguration(supertest); + + const data = removeServerGeneratedPropertiesFromSavedObject(configuration); expect(data).to.eql(getConfigurationOutput()); }); + it('should not create a configuration when missing connector.id', async () => { + await createConfiguration( + supertest, + { + // @ts-expect-error + connector: { + name: 'Connector', + type: ConnectorTypes.none, + fields: null, + }, + closure_type: 'close-by-user', + }, + 400 + ); + }); + + it('should not create a configuration when missing connector.name', async () => { + await createConfiguration( + supertest, + { + // @ts-expect-error + connector: { + id: 'test-id', + type: ConnectorTypes.none, + fields: null, + }, + closure_type: 'close-by-user', + }, + 400 + ); + }); + + it('should not create a configuration when missing connector.type', async () => { + await createConfiguration( + supertest, + { + // @ts-expect-error + connector: { + id: 'test-id', + name: 'Connector', + fields: null, + }, + closure_type: 'close-by-user', + }, + 400 + ); + }); + + it('should not create a configuration when missing connector.fields', async () => { + await createConfiguration( + supertest, + { + // @ts-expect-error + connector: { + id: 'test-id', + type: ConnectorTypes.none, + name: 'Connector', + }, + closure_type: 'close-by-user', + }, + 400 + ); + }); + + it('should not create a configuration when when missing closure_type', async () => { + await createConfiguration( + supertest, + // @ts-expect-error + { + connector: { + id: 'test-id', + type: ConnectorTypes.none, + name: 'Connector', + fields: null, + }, + }, + 400 + ); + }); + + it('should not create a configuration when when fields are not null', async () => { + await createConfiguration( + supertest, + { + connector: { + id: 'test-id', + type: ConnectorTypes.none, + name: 'Connector', + // @ts-expect-error + fields: {}, + }, + closure_type: 'close-by-user', + }, + 400 + ); + }); + it('should not create a configuration with unsupported connector type', async () => { - await supertest - .post(CASE_CONFIGURE_URL) - .set('kbn-xsrf', 'true') - // @ts-ignore We need it to test unsupported types - .send(getConfiguration({ type: '.unsupported' })) - .expect(400); + // @ts-expect-error + await createConfiguration(supertest, getConfigurationRequest({ type: '.unsupported' }), 400); }); it('should not create a configuration with unsupported connector fields', async () => { - await supertest - .post(CASE_CONFIGURE_URL) - .set('kbn-xsrf', 'true') - // @ts-ignore We need it to test unsupported types - .send(getConfiguration({ type: '.jira', fields: { unsupported: 'value' } })) - .expect(400); + await createConfiguration( + supertest, + // @ts-expect-error + getConfigurationRequest({ type: '.jira', fields: { unsupported: 'value' } }), + 400 + ); }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/connectors/case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/connectors/case.ts index 8cfd21a5af2c09..9be413015c0519 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/connectors/case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/connectors/case.ts @@ -11,12 +11,11 @@ import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; import { CommentType } from '../../../../../../plugins/cases/common/api'; +import { postCaseReq, postCaseResp } from '../../../../common/lib/mock'; import { - postCaseReq, - postCaseResp, removeServerGeneratedPropertiesFromCase, removeServerGeneratedPropertiesFromComments, -} from '../../../../common/lib/mock'; +} from '../../../../common/lib/utils'; import { createRule, createSignalsIndex, @@ -38,7 +37,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should return 400 when creating a case action', async () => { await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A case connector', @@ -51,7 +50,7 @@ export default ({ getService }: FtrProviderContext): void => { // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests it.skip('should return 200 when creating a case action successfully', async () => { const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A case connector', @@ -71,7 +70,7 @@ export default ({ getService }: FtrProviderContext): void => { }); const { body: fetchedAction } = await supertest - .get(`/api/actions/action/${createdActionId}`) + .get(`/api/actions/connector/${createdActionId}`) .expect(200); expect(fetchedAction).to.eql({ @@ -86,7 +85,7 @@ export default ({ getService }: FtrProviderContext): void => { describe.skip('create', () => { it('should respond with a 400 Bad Request when creating a case without title', async () => { const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A case connector', @@ -115,7 +114,7 @@ export default ({ getService }: FtrProviderContext): void => { }; const caseConnector = await supertest - .post(`/api/actions/action/${createdActionId}/_execute`) + .post(`/api/actions/connector/${createdActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params }) .expect(200); @@ -131,7 +130,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should respond with a 400 Bad Request when creating a case without description', async () => { const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A case connector', @@ -160,7 +159,7 @@ export default ({ getService }: FtrProviderContext): void => { }; const caseConnector = await supertest - .post(`/api/actions/action/${createdActionId}/_execute`) + .post(`/api/actions/connector/${createdActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params }) .expect(200); @@ -176,7 +175,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should respond with a 400 Bad Request when creating a case without tags', async () => { const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A case connector', @@ -205,7 +204,7 @@ export default ({ getService }: FtrProviderContext): void => { }; const caseConnector = await supertest - .post(`/api/actions/action/${createdActionId}/_execute`) + .post(`/api/actions/connector/${createdActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params }) .expect(200); @@ -221,7 +220,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should respond with a 400 Bad Request when creating a case without connector', async () => { const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A case connector', @@ -241,7 +240,7 @@ export default ({ getService }: FtrProviderContext): void => { }; const caseConnector = await supertest - .post(`/api/actions/action/${createdActionId}/_execute`) + .post(`/api/actions/connector/${createdActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params }) .expect(200); @@ -257,7 +256,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should respond with a 400 Bad Request when creating jira without issueType', async () => { const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A case connector', @@ -286,7 +285,7 @@ export default ({ getService }: FtrProviderContext): void => { }; const caseConnector = await supertest - .post(`/api/actions/action/${createdActionId}/_execute`) + .post(`/api/actions/connector/${createdActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params }) .expect(200); @@ -302,7 +301,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should respond with a 400 Bad Request when creating a connector with wrong fields', async () => { const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A case connector', @@ -332,7 +331,7 @@ export default ({ getService }: FtrProviderContext): void => { }; const caseConnector = await supertest - .post(`/api/actions/action/${createdActionId}/_execute`) + .post(`/api/actions/connector/${createdActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params }) .expect(200); @@ -348,7 +347,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should respond with a 400 Bad Request when creating a none without fields as null', async () => { const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A case connector', @@ -374,7 +373,7 @@ export default ({ getService }: FtrProviderContext): void => { }; const caseConnector = await supertest - .post(`/api/actions/action/${createdActionId}/_execute`) + .post(`/api/actions/connector/${createdActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params }) .expect(200); @@ -390,7 +389,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should create a case', async () => { const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A case connector', @@ -423,7 +422,7 @@ export default ({ getService }: FtrProviderContext): void => { }; const caseConnector = await supertest - .post(`/api/actions/action/${createdActionId}/_execute`) + .post(`/api/actions/connector/${createdActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params }) .expect(200); @@ -448,7 +447,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should create a case with connector with field as null if not provided', async () => { const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A case connector', @@ -477,7 +476,7 @@ export default ({ getService }: FtrProviderContext): void => { }; const caseConnector = await supertest - .post(`/api/actions/action/${createdActionId}/_execute`) + .post(`/api/actions/connector/${createdActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params }) .expect(200); @@ -516,7 +515,7 @@ export default ({ getService }: FtrProviderContext): void => { describe.skip('update', () => { it('should respond with a 400 Bad Request when updating a case without id', async () => { const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A case connector', @@ -535,7 +534,7 @@ export default ({ getService }: FtrProviderContext): void => { }; const caseConnector = await supertest - .post(`/api/actions/action/${createdActionId}/_execute`) + .post(`/api/actions/connector/${createdActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params }) .expect(200); @@ -551,7 +550,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should respond with a 400 Bad Request when updating a case without version', async () => { const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A case connector', @@ -570,7 +569,7 @@ export default ({ getService }: FtrProviderContext): void => { }; const caseConnector = await supertest - .post(`/api/actions/action/${createdActionId}/_execute`) + .post(`/api/actions/connector/${createdActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params }) .expect(200); @@ -586,7 +585,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should update a case', async () => { const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A case connector', @@ -613,7 +612,7 @@ export default ({ getService }: FtrProviderContext): void => { }; await supertest - .post(`/api/actions/action/${createdActionId}/_execute`) + .post(`/api/actions/connector/${createdActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params }) .expect(200); @@ -640,7 +639,7 @@ export default ({ getService }: FtrProviderContext): void => { describe.skip('addComment', () => { it('should respond with a 400 Bad Request when adding a comment to a case without caseId', async () => { const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A case connector', @@ -658,7 +657,7 @@ export default ({ getService }: FtrProviderContext): void => { }; const caseConnector = await supertest - .post(`/api/actions/action/${createdActionId}/_execute`) + .post(`/api/actions/connector/${createdActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params }) .expect(200); @@ -674,7 +673,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should respond with a 400 Bad Request when missing attributes of type user', async () => { const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A case connector', @@ -692,7 +691,7 @@ export default ({ getService }: FtrProviderContext): void => { }; const caseConnector = await supertest - .post(`/api/actions/action/${createdActionId}/_execute`) + .post(`/api/actions/connector/${createdActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params }) .expect(200); @@ -728,7 +727,7 @@ export default ({ getService }: FtrProviderContext): void => { const alert = signals.hits.hits[0]; const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A case connector', @@ -759,7 +758,7 @@ export default ({ getService }: FtrProviderContext): void => { }; const caseConnector = await supertest - .post(`/api/actions/action/${createdActionId}/_execute`) + .post(`/api/actions/connector/${createdActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params }) .expect(200); @@ -790,7 +789,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should respond with a 400 Bad Request when missing attributes of type alert', async () => { const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A case connector', @@ -818,7 +817,7 @@ export default ({ getService }: FtrProviderContext): void => { for (const attribute of ['alertId']) { const requestAttributes = omit(attribute, comment); const caseConnector = await supertest - .post(`/api/actions/action/${createdActionId}/_execute`) + .post(`/api/actions/connector/${createdActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -839,7 +838,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should respond with a 400 Bad Request when adding excess attributes for type user', async () => { const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A case connector', @@ -859,7 +858,7 @@ export default ({ getService }: FtrProviderContext): void => { for (const attribute of ['blah', 'bogus']) { const caseConnector = await supertest - .post(`/api/actions/action/${createdActionId}/_execute`) + .post(`/api/actions/connector/${createdActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -882,7 +881,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should respond with a 400 Bad Request when adding excess attributes for type alert', async () => { const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A case connector', @@ -907,7 +906,7 @@ export default ({ getService }: FtrProviderContext): void => { for (const attribute of ['comment']) { const caseConnector = await supertest - .post(`/api/actions/action/${createdActionId}/_execute`) + .post(`/api/actions/connector/${createdActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params: { @@ -931,7 +930,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should respond with a 400 Bad Request when adding a comment to a case without type', async () => { const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A case connector', @@ -950,7 +949,7 @@ export default ({ getService }: FtrProviderContext): void => { }; const caseConnector = await supertest - .post(`/api/actions/action/${createdActionId}/_execute`) + .post(`/api/actions/connector/${createdActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params }) .expect(200); @@ -966,7 +965,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should add a comment of type user', async () => { const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A case connector', @@ -992,7 +991,7 @@ export default ({ getService }: FtrProviderContext): void => { }; await supertest - .post(`/api/actions/action/${createdActionId}/_execute`) + .post(`/api/actions/connector/${createdActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params }) .expect(200); @@ -1019,7 +1018,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should add a comment of type alert', async () => { const { body: createdAction } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'foo') .send({ name: 'A case connector', @@ -1050,7 +1049,7 @@ export default ({ getService }: FtrProviderContext): void => { }; await supertest - .post(`/api/actions/action/${createdActionId}/_execute`) + .post(`/api/actions/connector/${createdActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ params }) .expect(200); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts index ba5a865b35778d..c6c68efd7a7523 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts @@ -10,12 +10,12 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default ({ loadTestFile }: FtrProviderContext): void => { describe('Common', function () { - loadTestFile(require.resolve('./cases/comments/delete_comment')); - loadTestFile(require.resolve('./cases/comments/find_comments')); - loadTestFile(require.resolve('./cases/comments/get_comment')); - loadTestFile(require.resolve('./cases/comments/get_all_comments')); - loadTestFile(require.resolve('./cases/comments/patch_comment')); - loadTestFile(require.resolve('./cases/comments/post_comment')); + loadTestFile(require.resolve('./comments/delete_comment')); + loadTestFile(require.resolve('./comments/find_comments')); + loadTestFile(require.resolve('./comments/get_comment')); + loadTestFile(require.resolve('./comments/get_all_comments')); + loadTestFile(require.resolve('./comments/patch_comment')); + loadTestFile(require.resolve('./comments/post_comment')); loadTestFile(require.resolve('./cases/delete_cases')); loadTestFile(require.resolve('./cases/find_cases')); loadTestFile(require.resolve('./cases/get_case')); @@ -24,20 +24,20 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./cases/reporters/get_reporters')); loadTestFile(require.resolve('./cases/status/get_status')); loadTestFile(require.resolve('./cases/tags/get_tags')); - loadTestFile(require.resolve('./cases/user_actions/get_all_user_actions')); + loadTestFile(require.resolve('./user_actions/get_all_user_actions')); loadTestFile(require.resolve('./configure/get_configure')); loadTestFile(require.resolve('./configure/get_connectors')); loadTestFile(require.resolve('./configure/patch_configure')); loadTestFile(require.resolve('./configure/post_configure')); loadTestFile(require.resolve('./connectors/case')); - loadTestFile(require.resolve('./cases/sub_cases/patch_sub_cases')); - loadTestFile(require.resolve('./cases/sub_cases/delete_sub_cases')); - loadTestFile(require.resolve('./cases/sub_cases/get_sub_case')); - loadTestFile(require.resolve('./cases/sub_cases/find_sub_cases')); + loadTestFile(require.resolve('./sub_cases/patch_sub_cases')); + loadTestFile(require.resolve('./sub_cases/delete_sub_cases')); + loadTestFile(require.resolve('./sub_cases/get_sub_case')); + loadTestFile(require.resolve('./sub_cases/find_sub_cases')); // Migrations loadTestFile(require.resolve('./cases/migrations')); loadTestFile(require.resolve('./configure/migrations')); - loadTestFile(require.resolve('./cases/user_actions/migrations')); + loadTestFile(require.resolve('./user_actions/migrations')); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/sub_cases/delete_sub_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/sub_cases/delete_sub_cases.ts similarity index 89% rename from x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/sub_cases/delete_sub_cases.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/sub_cases/delete_sub_cases.ts index bd3d9ff86d5400..951db263a6c784 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/sub_cases/delete_sub_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/sub_cases/delete_sub_cases.ts @@ -5,21 +5,21 @@ * 2.0. */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { CASES_URL, SUB_CASES_PATCH_DEL_URL, -} from '../../../../../../../plugins/cases/common/constants'; -import { postCommentUserReq } from '../../../../../common/lib/mock'; +} from '../../../../../../plugins/cases/common/constants'; +import { postCommentUserReq } from '../../../../common/lib/mock'; import { createCaseAction, createSubCase, deleteAllCaseItems, deleteCaseAction, -} from '../../../../../common/lib/utils'; -import { getSubCaseDetailsUrl } from '../../../../../../../plugins/cases/common/api/helpers'; -import { CaseResponse } from '../../../../../../../plugins/cases/common/api'; +} from '../../../../common/lib/utils'; +import { getSubCaseDetailsUrl } from '../../../../../../plugins/cases/common/api/helpers'; +import { CaseResponse } from '../../../../../../plugins/cases/common/api'; // eslint-disable-next-line import/no-default-export export default function ({ getService }: FtrProviderContext) { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/sub_cases/find_sub_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/sub_cases/find_sub_cases.ts similarity index 97% rename from x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/sub_cases/find_sub_cases.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/sub_cases/find_sub_cases.ts index 466eca95b0d727..14c0460c7583b2 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/sub_cases/find_sub_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/sub_cases/find_sub_cases.ts @@ -7,9 +7,9 @@ import expect from '@kbn/expect'; import type { ApiResponse, estypes } from '@elastic/elasticsearch'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { findSubCasesResp, postCollectionReq } from '../../../../../common/lib/mock'; +import { findSubCasesResp, postCollectionReq } from '../../../../common/lib/mock'; import { createCaseAction, createSubCase, @@ -17,19 +17,19 @@ import { deleteAllCaseItems, deleteCaseAction, setStatus, -} from '../../../../../common/lib/utils'; -import { getSubCasesUrl } from '../../../../../../../plugins/cases/common/api/helpers'; +} from '../../../../common/lib/utils'; +import { getSubCasesUrl } from '../../../../../../plugins/cases/common/api/helpers'; import { CaseResponse, CaseStatuses, CommentType, SubCasesFindResponse, -} from '../../../../../../../plugins/cases/common/api'; -import { CASES_URL } from '../../../../../../../plugins/cases/common/constants'; +} from '../../../../../../plugins/cases/common/api'; +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; import { ContextTypeGeneratedAlertType, createAlertsString, -} from '../../../../../../../plugins/cases/server/connectors'; +} from '../../../../../../plugins/cases/server/connectors'; interface SubCaseAttributes { 'cases-sub-case': { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/sub_cases/get_sub_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/sub_cases/get_sub_case.ts similarity index 91% rename from x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/sub_cases/get_sub_case.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/sub_cases/get_sub_case.ts index 6a0d7f4dd042e0..35ed4ba5c3c71b 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/sub_cases/get_sub_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/sub_cases/get_sub_case.ts @@ -6,31 +6,27 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { - commentsResp, - postCommentAlertReq, - removeServerGeneratedPropertiesFromComments, - removeServerGeneratedPropertiesFromSubCase, - subCaseResp, -} from '../../../../../common/lib/mock'; +import { commentsResp, postCommentAlertReq, subCaseResp } from '../../../../common/lib/mock'; import { createCaseAction, createSubCase, defaultCreateSubComment, deleteAllCaseItems, deleteCaseAction, -} from '../../../../../common/lib/utils'; + removeServerGeneratedPropertiesFromComments, + removeServerGeneratedPropertiesFromSubCase, +} from '../../../../common/lib/utils'; import { getCaseCommentsUrl, getSubCaseDetailsUrl, -} from '../../../../../../../plugins/cases/common/api/helpers'; +} from '../../../../../../plugins/cases/common/api/helpers'; import { AssociationType, CaseResponse, SubCaseResponse, -} from '../../../../../../../plugins/cases/common/api'; +} from '../../../../../../plugins/cases/common/api'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/sub_cases/patch_sub_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/sub_cases/patch_sub_cases.ts similarity index 96% rename from x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/sub_cases/patch_sub_cases.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/sub_cases/patch_sub_cases.ts index a4bd3ce187d0e9..43526bca644db6 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/sub_cases/patch_sub_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/sub_cases/patch_sub_cases.ts @@ -5,12 +5,12 @@ * 2.0. */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { CASES_URL, SUB_CASES_PATCH_DEL_URL, -} from '../../../../../../../plugins/cases/common/constants'; +} from '../../../../../../plugins/cases/common/constants'; import { createCaseAction, createSubCase, @@ -18,15 +18,15 @@ import { deleteCaseAction, getSignalsWithES, setStatus, -} from '../../../../../common/lib/utils'; -import { getSubCaseDetailsUrl } from '../../../../../../../plugins/cases/common/api/helpers'; +} from '../../../../common/lib/utils'; +import { getSubCaseDetailsUrl } from '../../../../../../plugins/cases/common/api/helpers'; import { CaseStatuses, CommentType, SubCaseResponse, -} from '../../../../../../../plugins/cases/common/api'; -import { createAlertsString } from '../../../../../../../plugins/cases/server/connectors'; -import { postCaseReq, postCollectionReq } from '../../../../../common/lib/mock'; +} from '../../../../../../plugins/cases/common/api'; +import { createAlertsString } from '../../../../../../plugins/cases/server/connectors'; +import { postCaseReq, postCollectionReq } from '../../../../common/lib/mock'; const defaultSignalsIndex = '.siem-signals-default-000001'; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts similarity index 95% rename from x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/user_actions/get_all_user_actions.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts index 8f047602acc38d..0d11edc5587d14 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/user_actions/get_all_user_actions.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts @@ -6,21 +6,17 @@ */ 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 { CommentType } from '../../../../../../../plugins/cases/common/api'; +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { CommentType } from '../../../../../../plugins/cases/common/api'; +import { userActionPostResp, postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; import { - userActionPostResp, - postCaseReq, - postCommentUserReq, -} from '../../../../../common/lib/mock'; -import { - deleteCases, + deleteCasesByESQuery, deleteCasesUserActions, deleteComments, deleteConfiguration, -} from '../../../../../common/lib/utils'; +} from '../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -29,7 +25,7 @@ export default ({ getService }: FtrProviderContext): void => { describe('get_all_user_actions', () => { afterEach(async () => { - await deleteCases(es); + await deleteCasesByESQuery(es); await deleteComments(es); await deleteConfiguration(es); await deleteCasesUserActions(es); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/user_actions/migrations.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/migrations.ts similarity index 90% rename from x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/user_actions/migrations.ts rename to x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/migrations.ts index 8bba29a56cd9d7..e198260e88a9c1 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/user_actions/migrations.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/migrations.ts @@ -6,8 +6,8 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; -import { CASES_URL } from '../../../../../../../plugins/cases/common/constants'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; // eslint-disable-next-line import/no-default-export export default function createGetTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts index f759579510d49c..67773067ad2d49 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts @@ -5,30 +5,41 @@ * 2.0. */ +/* eslint-disable @typescript-eslint/naming-convention */ + import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; -import { CASE_CONFIGURE_URL, CASES_URL } from '../../../../../../plugins/cases/common/constants'; -import { - postCaseReq, - defaultUser, - postCommentUserReq, - postCollectionReq, -} from '../../../../common/lib/mock'; +import { postCaseReq, defaultUser, postCommentUserReq } from '../../../../common/lib/mock'; import { - deleteCases, + deleteCasesByESQuery, deleteCasesUserActions, deleteComments, deleteConfiguration, - getConfiguration, + getConfigurationRequest, getServiceNowConnector, + createConnector, + createConfiguration, + createCase, + pushCase, + createComment, + CreateConnectorResponse, + updateCase, + getAllUserAction, + removeServerGeneratedPropertiesFromUserAction, } from '../../../../common/lib/utils'; import { ExternalServiceSimulator, getExternalServiceSimulatorPath, } from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; -import { CaseStatuses } from '../../../../../../plugins/cases/common/api'; +import { + CaseConnector, + CaseResponse, + CaseStatuses, + CaseUserActionResponse, + ConnectorTypes, +} from '../../../../../../plugins/cases/common/api'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -47,64 +58,58 @@ export default ({ getService }: FtrProviderContext): void => { }); afterEach(async () => { - await deleteCases(es); + await deleteCasesByESQuery(es); await deleteComments(es); await deleteConfiguration(es); await deleteCasesUserActions(es); await actionsRemover.removeAll(); }); - it('should push a case', async () => { - const { body: connector } = await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'true') - .send({ - ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, - }) - .expect(200); + const createCaseWithConnector = async ( + configureReq = {} + ): Promise<{ + postedCase: CaseResponse; + connector: CreateConnectorResponse; + }> => { + const connector = await createConnector(supertest, { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }); actionsRemover.add('default', connector.id, 'action', 'actions'); - await supertest - .post(CASE_CONFIGURE_URL) - .set('kbn-xsrf', 'true') - .send( - getConfiguration({ - id: connector.id, - name: connector.name, - type: connector.actionTypeId, - }) - ) - .expect(200); + await createConfiguration(supertest, { + ...getConfigurationRequest({ + id: connector.id, + name: connector.name, + type: connector.connector_type_id as ConnectorTypes, + }), + ...configureReq, + }); - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - ...postCaseReq, - connector: getConfiguration({ - id: connector.id, - name: connector.name, - type: connector.actionTypeId, - fields: { - urgency: '2', - impact: '2', - severity: '2', - category: 'software', - subcategory: 'os', - }, - }).connector, - }) - .expect(200); + const postedCase = await createCase(supertest, { + ...postCaseReq, + connector: { + id: connector.id, + name: connector.name, + type: connector.connector_type_id, + fields: { + urgency: '2', + impact: '2', + severity: '2', + category: 'software', + subcategory: 'os', + }, + } as CaseConnector, + }); - const { body } = await supertest - .post(`${CASES_URL}/${postedCase.id}/connector/${connector.id}/_push`) - .set('kbn-xsrf', 'true') - .send({}) - .expect(200); + return { postedCase, connector }; + }; - // eslint-disable-next-line @typescript-eslint/naming-convention - const { pushed_at, external_url, ...rest } = body.external_service; + it('should push a case', async () => { + const { postedCase, connector } = await createCaseWithConnector(); + const theCase = await pushCase(supertest, postedCase.id, connector.id); + + const { pushed_at, external_url, ...rest } = theCase.external_service!; expect(rest).to.eql({ pushed_by: defaultUser, @@ -123,259 +128,87 @@ export default ({ getService }: FtrProviderContext): void => { }); it('pushes a comment appropriately', async () => { - const { body: connector } = await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'true') - .send({ - ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, - }) - .expect(200); - - actionsRemover.add('default', connector.id, 'action', 'actions'); - - await supertest - .post(CASE_CONFIGURE_URL) - .set('kbn-xsrf', 'true') - .send( - getConfiguration({ - id: connector.id, - name: connector.name, - type: connector.actionTypeId, - }) - ) - .expect(200); - - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - ...postCaseReq, - connector: getConfiguration({ - id: connector.id, - name: connector.name, - type: connector.actionTypeId, - fields: { - urgency: '2', - impact: '2', - severity: '2', - category: 'software', - subcategory: 'os', - }, - }).connector, - }) - .expect(200); - - await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - const { body } = await supertest - .post(`${CASES_URL}/${postedCase.id}/connector/${connector.id}/_push`) - .set('kbn-xsrf', 'true') - .send({}) - .expect(200); + const { postedCase, connector } = await createCaseWithConnector(); + await createComment(supertest, postedCase.id, postCommentUserReq); + const theCase = await pushCase(supertest, postedCase.id, connector.id); - expect(body.comments[0].pushed_by).to.eql(defaultUser); + expect(theCase.comments![0].pushed_by).to.eql(defaultUser); }); it('should pushes a case and closes when closure_type: close-by-pushing', async () => { - const { body: connector } = await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'true') - .send({ - ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, - }) - .expect(200); + const { postedCase, connector } = await createCaseWithConnector({ + closure_type: 'close-by-pushing', + }); + const theCase = await pushCase(supertest, postedCase.id, connector.id); - actionsRemover.add('default', connector.id, 'action', 'actions'); - await supertest - .post(CASE_CONFIGURE_URL) - .set('kbn-xsrf', 'true') - .send({ - ...getConfiguration({ - id: connector.id, - name: connector.name, - type: connector.actionTypeId, - }), - closure_type: 'close-by-pushing', - }) - .expect(200); + expect(theCase.status).to.eql('closed'); + }); - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - ...postCaseReq, - connector: getConfiguration({ - id: connector.id, - name: connector.name, - type: connector.actionTypeId, - fields: { - urgency: '2', - impact: '2', - severity: '2', - category: 'software', - subcategory: 'os', - }, - }).connector, - }) - .expect(200); + it('should create the correct user action', async () => { + const { postedCase, connector } = await createCaseWithConnector(); + const pushedCase = await pushCase(supertest, postedCase.id, connector.id); + const userActions = await getAllUserAction(supertest, pushedCase.id); + const pushUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); - const { body } = await supertest - .post(`${CASES_URL}/${postedCase.id}/connector/${connector.id}/_push`) - .set('kbn-xsrf', 'true') - .send({}) - .expect(200); + const { new_value, ...rest } = pushUserAction as CaseUserActionResponse; + const parsedNewValue = JSON.parse(new_value!); - expect(body.status).to.eql('closed'); + expect(rest).to.eql({ + action_field: ['pushed'], + action: 'push-to-service', + action_by: defaultUser, + old_value: null, + case_id: `${postedCase.id}`, + comment_id: null, + sub_case_id: '', + }); + + expect(parsedNewValue).to.eql({ + pushed_at: pushedCase.external_service!.pushed_at, + pushed_by: defaultUser, + connector_id: connector.id, + connector_name: connector.name, + external_id: '123', + external_title: 'INC01', + external_url: `${servicenowSimulatorURL}/nav_to.do?uri=incident.do?sys_id=123`, + }); }); // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests it.skip('should push a collection case but not close it when closure_type: close-by-pushing', async () => { - const { body: connector } = await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'true') - .send({ - ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, - }) - .expect(200); - - actionsRemover.add('default', connector.id, 'action', 'actions'); - await supertest - .post(CASE_CONFIGURE_URL) - .set('kbn-xsrf', 'true') - .send({ - ...getConfiguration({ - id: connector.id, - name: connector.name, - type: connector.actionTypeId, - }), - closure_type: 'close-by-pushing', - }) - .expect(200); - - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - ...postCollectionReq, - connector: getConfiguration({ - id: connector.id, - name: connector.name, - type: connector.actionTypeId, - fields: { - urgency: '2', - impact: '2', - severity: '2', - category: 'software', - subcategory: 'os', - }, - }).connector, - }) - .expect(200); - - const { body } = await supertest - .post(`${CASES_URL}/${postedCase.id}/connector/${connector.id}/_push`) - .set('kbn-xsrf', 'true') - .send({}) - .expect(200); + const { postedCase, connector } = await createCaseWithConnector({ + closure_type: 'close-by-pushing', + }); - expect(body.status).to.eql(CaseStatuses.open); + const theCase = await pushCase(supertest, postedCase.id, connector.id); + expect(theCase.status).to.eql(CaseStatuses.open); }); it('unhappy path - 404s when case does not exist', async () => { - await supertest - .post(`${CASES_URL}/fake-id/connector/fake-connector/_push`) - .set('kbn-xsrf', 'true') - .send({}) - .expect(404); + await pushCase(supertest, 'fake-id', 'fake-connector', 404); }); it('unhappy path - 404s when connector does not exist', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - ...postCaseReq, - connector: getConfiguration().connector, - }) - .expect(200); - - await supertest - .post(`${CASES_URL}/${postedCase.id}/connector/fake-connector/_push`) - .set('kbn-xsrf', 'true') - .send({}) - .expect(404); + const postedCase = await createCase(supertest, { + ...postCaseReq, + connector: getConfigurationRequest().connector, + }); + await pushCase(supertest, postedCase.id, 'fake-connector', 404); }); it('unhappy path = 409s when case is closed', async () => { - const { body: connector } = await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'true') - .send({ - ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, - }) - .expect(200); - - actionsRemover.add('default', connector.id, 'action', 'actions'); - - await supertest - .post(CASE_CONFIGURE_URL) - .set('kbn-xsrf', 'true') - .send( - getConfiguration({ - id: connector.id, - name: connector.name, - type: connector.actionTypeId, - }) - ) - .expect(200); - - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - ...postCaseReq, - connector: getConfiguration({ - id: connector.id, - name: connector.name, - type: connector.actionTypeId, - fields: { - urgency: '2', - impact: '2', - severity: '2', - category: 'software', - subcategory: 'os', - }, - }).connector, - }) - .expect(200); - - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: 'closed', - }, - ], - }) - .expect(200); + const { postedCase, connector } = await createCaseWithConnector(); + await updateCase(supertest, { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses.closed, + }, + ], + }); - await supertest - .post(`${CASES_URL}/${postedCase.id}/connector/${connector.id}/_push`) - .set('kbn-xsrf', 'true') - .send({}) - .expect(409); + await pushCase(supertest, postedCase.id, connector.id, 409); }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/user_actions/get_all_user_actions.ts index 0b66200a3fab09..3729b20f82b301 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/user_actions/get_all_user_actions.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/user_actions/get_all_user_actions.ts @@ -11,11 +11,11 @@ import { FtrProviderContext } from '../../../../../../common/ftr_provider_contex import { CASE_CONFIGURE_URL, CASES_URL } from '../../../../../../../plugins/cases/common/constants'; import { defaultUser, postCaseReq } from '../../../../../common/lib/mock'; import { - deleteCases, + deleteCasesByESQuery, deleteCasesUserActions, deleteComments, deleteConfiguration, - getConfiguration, + getConfigurationRequest, getServiceNowConnector, } from '../../../../../common/lib/utils'; @@ -40,7 +40,7 @@ export default ({ getService }: FtrProviderContext): void => { ); }); afterEach(async () => { - await deleteCases(es); + await deleteCasesByESQuery(es); await deleteComments(es); await deleteConfiguration(es); await deleteCasesUserActions(es); @@ -49,7 +49,7 @@ export default ({ getService }: FtrProviderContext): void => { it(`on new push to service, user action: 'push-to-service' should be called with actionFields: ['pushed']`, async () => { const { body: connector } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'true') .send({ ...getServiceNowConnector(), @@ -63,10 +63,10 @@ export default ({ getService }: FtrProviderContext): void => { .post(CASE_CONFIGURE_URL) .set('kbn-xsrf', 'true') .send( - getConfiguration({ + getConfigurationRequest({ id: connector.id, name: connector.name, - type: connector.actionTypeId, + type: connector.connector_type_id, }) ) .expect(200); @@ -76,10 +76,10 @@ export default ({ getService }: FtrProviderContext): void => { .set('kbn-xsrf', 'true') .send({ ...postCaseReq, - connector: getConfiguration({ + connector: getConfigurationRequest({ id: connector.id, name: connector.name, - type: connector.actionTypeId, + type: connector.connector_type_id, fields: { urgency: '2', impact: '2', diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts index 0b6c755c79b505..75d1378260b191 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts @@ -28,13 +28,13 @@ export default ({ getService }: FtrProviderContext): void => { it('should return the correct connectors', async () => { const { body: snConnector } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'true') .send(getServiceNowConnector()) .expect(200); const { body: emailConnector } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'true') .send({ name: 'An email action', @@ -51,13 +51,13 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200); const { body: jiraConnector } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'true') .send(getJiraConnector()) .expect(200); const { body: resilientConnector } = await supertest - .post('/api/actions/action') + .post('/api/actions/connector') .set('kbn-xsrf', 'true') .send(getResilientConnector()) .expect(200); From 613e859780597182533282ea19c3b941726a9af5 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Wed, 14 Apr 2021 16:35:36 -0400 Subject: [PATCH 047/113] [Cases] Move remaining HTTP functionality to client (#96507) * Moving deletes and find for attachments * Moving rest of comment apis * Migrating configuration routes to client * Finished moving routes, starting utils refactor * Refactoring utilites and fixing integration tests * Addressing PR feedback * Fixing mocks and types * Fixing integration tests * Renaming status_stats * Fixing test type errors * Adding plugins to kibana.json * Adding cases to required plugin --- .../cases/server/client/attachments/add.ts | 9 +- .../cases/server/client/attachments/client.ts | 26 +- .../cases/server/client/attachments/delete.ts | 154 ++++ .../cases/server/client/attachments/get.ts | 185 ++++ .../cases/server/client/attachments/update.ts | 181 ++++ .../cases/server/client/cases/client.ts | 20 +- .../cases/server/client/cases/create.ts | 14 +- .../cases/server/client/cases/delete.ts | 128 +++ .../plugins/cases/server/client/cases/find.ts | 5 +- .../plugins/cases/server/client/cases/get.ts | 48 +- .../plugins/cases/server/client/cases/push.ts | 4 +- .../cases/server/client/cases/update.ts | 16 +- .../cases/server/client/cases/utils.test.ts | 2 +- .../cases/server/client/cases/utils.ts | 2 +- x-pack/plugins/cases/server/client/client.ts | 17 +- .../cases/server/client/client_internal.ts | 9 +- .../cases/server/client/configure/client.ts | 262 +++++- .../server/client/configure/get_fields.ts | 9 +- .../server/client/configure/get_mappings.ts | 4 - x-pack/plugins/cases/server/client/factory.ts | 8 +- x-pack/plugins/cases/server/client/mocks.ts | 35 +- .../cases/server/client/stats/client.ts | 54 ++ .../cases/server/client/sub_cases/client.ts | 13 +- .../cases/server/client/sub_cases/update.ts | 34 +- x-pack/plugins/cases/server/client/types.ts | 2 + .../server/client/user_actions/client.ts | 3 +- .../cases/server/client/user_actions/get.ts | 60 +- .../plugins/cases/server/client/utils.test.ts | 329 +++++++ .../api/cases/helpers.ts => client/utils.ts} | 144 ++- x-pack/plugins/cases/server/common/error.ts | 2 +- x-pack/plugins/cases/server/common/index.ts | 1 + .../server/common/models/commentable_case.ts | 4 +- .../plugins/cases/server/common/utils.test.ts | 629 ++++++++++++- x-pack/plugins/cases/server/common/utils.ts | 331 ++++++- x-pack/plugins/cases/server/plugin.ts | 7 +- .../api/cases/comments/delete_all_comments.ts | 63 +- .../api/cases/comments/delete_comment.ts | 71 +- .../api/cases/comments/find_comments.ts | 69 +- .../api/cases/comments/get_all_comment.ts | 54 +- .../routes/api/cases/comments/get_comment.ts | 20 +- .../api/cases/comments/patch_comment.ts | 170 +--- .../api/cases/configure/get_configure.ts | 50 +- .../api/cases/configure/get_connectors.ts | 33 +- .../api/cases/configure/patch_configure.ts | 88 +- .../api/cases/configure/post_configure.ts | 81 +- .../server/routes/api/cases/delete_cases.ts | 116 +-- .../cases/server/routes/api/cases/get_case.ts | 8 +- .../server/routes/api/cases/helpers.test.ts | 111 --- .../api/cases/reporters/get_reporters.ts | 15 +- .../routes/api/cases/status/get_status.ts | 27 +- .../server/routes/api/cases/tags/get_tags.ts | 15 +- .../plugins/cases/server/routes/api/types.ts | 6 - .../cases/server/routes/api/utils.test.ts | 844 +----------------- .../plugins/cases/server/routes/api/utils.ts | 392 +------- .../cases/server/services/cases/index.ts | 26 +- .../server/services/user_actions/helpers.ts | 6 +- .../plugins/observability/kibana.json | 2 +- .../plugins/security_solution/kibana.json | 2 +- .../case_api_integration/common/lib/utils.ts | 4 +- .../tests/common/cases/get_case.ts | 2 +- .../tests/common/comments/delete_comment.ts | 4 +- .../tests/common/comments/find_comments.ts | 2 +- .../tests/common/comments/get_all_comments.ts | 4 +- .../tests/common/comments/get_comment.ts | 1 - .../tests/common/comments/patch_comment.ts | 3 +- .../user_actions/get_all_user_actions.ts | 3 +- 66 files changed, 2715 insertions(+), 2328 deletions(-) create mode 100644 x-pack/plugins/cases/server/client/attachments/delete.ts create mode 100644 x-pack/plugins/cases/server/client/attachments/get.ts create mode 100644 x-pack/plugins/cases/server/client/attachments/update.ts create mode 100644 x-pack/plugins/cases/server/client/cases/delete.ts create mode 100644 x-pack/plugins/cases/server/client/stats/client.ts create mode 100644 x-pack/plugins/cases/server/client/utils.test.ts rename x-pack/plugins/cases/server/{routes/api/cases/helpers.ts => client/utils.ts} (75%) delete mode 100644 x-pack/plugins/cases/server/routes/api/cases/helpers.test.ts diff --git a/x-pack/plugins/cases/server/client/attachments/add.ts b/x-pack/plugins/cases/server/client/attachments/add.ts index 659ff14418d05f..e77115ba4e2289 100644 --- a/x-pack/plugins/cases/server/client/attachments/add.ts +++ b/x-pack/plugins/cases/server/client/attachments/add.ts @@ -12,7 +12,6 @@ import { identity } from 'fp-ts/lib/function'; import { SavedObject, SavedObjectsClientContract, Logger } from 'src/core/server'; import { nodeBuilder } from '../../../../../../src/plugins/data/common'; -import { decodeCommentRequest, isCommentRequestTypeGenAlert } from '../../routes/api/utils'; import { throwErrors, @@ -33,7 +32,11 @@ import { } from '../../services/user_actions/helpers'; import { AttachmentService, CaseService, CaseUserActionService } from '../../services'; -import { CommentableCase, createAlertUpdateRequest } from '../../common'; +import { + CommentableCase, + createAlertUpdateRequest, + isCommentRequestTypeGenAlert, +} from '../../common'; import { CasesClientArgs, CasesClientInternal } from '..'; import { createCaseError } from '../../common/error'; import { @@ -42,6 +45,8 @@ import { ENABLE_CASE_CONNECTOR, } from '../../../common/constants'; +import { decodeCommentRequest } from '../utils'; + async function getSubCase({ caseService, savedObjectsClient, diff --git a/x-pack/plugins/cases/server/client/attachments/client.ts b/x-pack/plugins/cases/server/client/attachments/client.ts index f3ee3098a3153f..27fb5e1cf61f04 100644 --- a/x-pack/plugins/cases/server/client/attachments/client.ts +++ b/x-pack/plugins/cases/server/client/attachments/client.ts @@ -5,18 +5,34 @@ * 2.0. */ -import { CaseResponse, CommentRequest as AttachmentsRequest } from '../../../common/api'; +import { + AllCommentsResponse, + CaseResponse, + CommentRequest as AttachmentsRequest, + CommentResponse, + CommentsResponse, +} from '../../../common/api'; + import { CasesClientInternal } from '../client_internal'; import { CasesClientArgs } from '../types'; import { addComment } from './add'; +import { DeleteAllArgs, deleteAll, DeleteArgs, deleteComment } from './delete'; +import { find, FindArgs, get, getAll, GetAllArgs, GetArgs } from './get'; +import { update, UpdateArgs } from './update'; -export interface AttachmentsAdd { +interface AttachmentsAdd { caseId: string; comment: AttachmentsRequest; } export interface AttachmentsSubClient { add(args: AttachmentsAdd): Promise; + deleteAll(deleteAllArgs: DeleteAllArgs): Promise; + delete(deleteArgs: DeleteArgs): Promise; + find(findArgs: FindArgs): Promise; + getAll(getAllArgs: GetAllArgs): Promise; + get(getArgs: GetArgs): Promise; + update(updateArgs: UpdateArgs): Promise; } export const createAttachmentsSubClient = ( @@ -31,6 +47,12 @@ export const createAttachmentsSubClient = ( 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), }; return Object.freeze(attachmentSubClient); diff --git a/x-pack/plugins/cases/server/client/attachments/delete.ts b/x-pack/plugins/cases/server/client/attachments/delete.ts new file mode 100644 index 00000000000000..37069b94df7cbd --- /dev/null +++ b/x-pack/plugins/cases/server/client/attachments/delete.ts @@ -0,0 +1,154 @@ +/* + * 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 Boom from '@hapi/boom'; +import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../common/constants'; + +import { AssociationType } from '../../../common/api'; +import { CasesClientArgs } from '../types'; +import { buildCommentUserActionItem } from '../../services/user_actions/helpers'; +import { createCaseError } from '../../common/error'; +import { checkEnabledCaseConnectorOrThrow } from '../../common'; + +/** + * Parameters for deleting all comments of a case or sub case. + */ +export interface DeleteAllArgs { + caseID: string; + subCaseID?: string; +} + +/** + * Parameters for deleting a single comment of a case or sub case. + */ +export interface DeleteArgs { + caseID: string; + attachmentID: string; + subCaseID?: string; +} + +/** + * Delete all comments for a case or sub case. + */ +export async function deleteAll( + { caseID, subCaseID }: DeleteAllArgs, + clientArgs: CasesClientArgs +): Promise { + const { + user, + savedObjectsClient: soClient, + caseService, + attachmentService, + userActionService, + logger, + } = clientArgs; + + try { + checkEnabledCaseConnectorOrThrow(subCaseID); + + const id = subCaseID ?? caseID; + const comments = await caseService.getCommentsByAssociation({ + soClient, + id, + associationType: subCaseID ? AssociationType.subCase : AssociationType.case, + }); + + await Promise.all( + comments.saved_objects.map((comment) => + attachmentService.delete({ + soClient, + attachmentId: comment.id, + }) + ) + ); + + const deleteDate = new Date().toISOString(); + + await userActionService.bulkCreate({ + soClient, + actions: comments.saved_objects.map((comment) => + buildCommentUserActionItem({ + action: 'delete', + actionAt: deleteDate, + actionBy: user, + caseId: caseID, + subCaseId: subCaseID, + commentId: comment.id, + fields: ['comment'], + }) + ), + }); + } catch (error) { + throw createCaseError({ + message: `Failed to delete all comments case id: ${caseID} sub case id: ${subCaseID}: ${error}`, + error, + logger, + }); + } +} + +export async function deleteComment( + { caseID, attachmentID, subCaseID }: DeleteArgs, + clientArgs: CasesClientArgs +) { + const { + user, + savedObjectsClient: soClient, + attachmentService, + userActionService, + logger, + } = clientArgs; + + try { + checkEnabledCaseConnectorOrThrow(subCaseID); + + const deleteDate = new Date().toISOString(); + + const myComment = await attachmentService.get({ + soClient, + attachmentId: attachmentID, + }); + + if (myComment == null) { + throw Boom.notFound(`This comment ${attachmentID} does not exist anymore.`); + } + + const type = subCaseID ? SUB_CASE_SAVED_OBJECT : CASE_SAVED_OBJECT; + const id = subCaseID ?? caseID; + + const caseRef = myComment.references.find((c) => c.type === type); + if (caseRef == null || (caseRef != null && caseRef.id !== id)) { + throw Boom.notFound(`This comment ${attachmentID} does not exist in ${id}.`); + } + + await attachmentService.delete({ + soClient, + attachmentId: attachmentID, + }); + + await userActionService.bulkCreate({ + soClient, + actions: [ + buildCommentUserActionItem({ + action: 'delete', + actionAt: deleteDate, + actionBy: user, + caseId: id, + subCaseId: subCaseID, + commentId: attachmentID, + fields: ['comment'], + }), + ], + }); + } catch (error) { + throw createCaseError({ + message: `Failed to delete comment in route case id: ${caseID} comment id: ${attachmentID} sub case id: ${subCaseID}: ${error}`, + error, + logger, + }); + } +} diff --git a/x-pack/plugins/cases/server/client/attachments/get.ts b/x-pack/plugins/cases/server/client/attachments/get.ts new file mode 100644 index 00000000000000..70aeb5a3df2aa9 --- /dev/null +++ b/x-pack/plugins/cases/server/client/attachments/get.ts @@ -0,0 +1,185 @@ +/* + * 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 Boom from '@hapi/boom'; +import * as rt from 'io-ts'; +import { SavedObjectsFindResponse } from 'kibana/server'; +import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; + +import { esKuery } from '../../../../../../src/plugins/data/server'; +import { + AllCommentsResponse, + AllCommentsResponseRt, + AssociationType, + CommentAttributes, + CommentResponse, + CommentResponseRt, + CommentsResponse, + CommentsResponseRt, + SavedObjectFindOptionsRt, +} from '../../../common/api'; +import { + checkEnabledCaseConnectorOrThrow, + defaultSortField, + transformComments, + flattenCommentSavedObject, + flattenCommentSavedObjects, +} from '../../common'; +import { createCaseError } from '../../common/error'; +import { defaultPage, defaultPerPage } from '../../routes/api'; +import { CasesClientArgs } from '../types'; + +const FindQueryParamsRt = rt.partial({ + ...SavedObjectFindOptionsRt.props, + subCaseId: rt.string, +}); + +type FindQueryParams = rt.TypeOf; + +export interface FindArgs { + caseID: string; + queryParams?: FindQueryParams; +} + +export interface GetAllArgs { + caseID: string; + includeSubCaseComments?: boolean; + subCaseID?: string; +} + +export interface GetArgs { + caseID: string; + attachmentID: string; +} + +/** + * Retrieves the attachments for a case entity. This support pagination. + */ +export async function find( + { caseID, queryParams }: FindArgs, + { savedObjectsClient: soClient, caseService, logger }: CasesClientArgs +): Promise { + try { + checkEnabledCaseConnectorOrThrow(queryParams?.subCaseId); + + const id = queryParams?.subCaseId ?? caseID; + const associationType = queryParams?.subCaseId ? AssociationType.subCase : AssociationType.case; + const { filter, ...queryWithoutFilter } = queryParams ?? {}; + const args = queryParams + ? { + caseService, + soClient, + id, + options: { + // We need this because the default behavior of getAllCaseComments is to return all the comments + // unless the page and/or perPage is specified. Since we're spreading the query after the request can + // still override this behavior. + page: defaultPage, + perPage: defaultPerPage, + sortField: 'created_at', + filter: filter != null ? esKuery.fromKueryExpression(filter) : filter, + ...queryWithoutFilter, + }, + associationType, + } + : { + caseService, + soClient, + id, + options: { + page: defaultPage, + perPage: defaultPerPage, + sortField: 'created_at', + }, + associationType, + }; + + const theComments = await caseService.getCommentsByAssociation(args); + return CommentsResponseRt.encode(transformComments(theComments)); + } catch (error) { + throw createCaseError({ + message: `Failed to find comments case id: ${caseID}: ${error}`, + error, + logger, + }); + } +} + +/** + * Retrieves a single attachment by its ID. + */ +export async function get( + { attachmentID, caseID }: GetArgs, + clientArgs: CasesClientArgs +): Promise { + const { attachmentService, savedObjectsClient: soClient, logger } = clientArgs; + + try { + const comment = await attachmentService.get({ + soClient, + attachmentId: attachmentID, + }); + + return CommentResponseRt.encode(flattenCommentSavedObject(comment)); + } catch (error) { + throw createCaseError({ + message: `Failed to get comment case id: ${caseID} attachment id: ${attachmentID}: ${error}`, + error, + logger, + }); + } +} + +/** + * Retrieves all the attachments for a case. The `includeSubCaseComments` can be used to include the sub case comments for + * collections. If the entity is a sub case, pass in the subCaseID. + */ +export async function getAll( + { caseID, includeSubCaseComments, subCaseID }: GetAllArgs, + clientArgs: CasesClientArgs +): Promise { + const { savedObjectsClient: soClient, caseService, logger } = clientArgs; + + try { + let comments: SavedObjectsFindResponse; + + if ( + !ENABLE_CASE_CONNECTOR && + (subCaseID !== undefined || includeSubCaseComments !== undefined) + ) { + throw Boom.badRequest( + 'The sub case id and include sub case comments fields are not supported when the case connector feature is disabled' + ); + } + + if (subCaseID) { + comments = await caseService.getAllSubCaseComments({ + soClient, + id: subCaseID, + options: { + sortField: defaultSortField, + }, + }); + } else { + comments = await caseService.getAllCaseComments({ + soClient, + id: caseID, + includeSubCaseComments, + options: { + sortField: defaultSortField, + }, + }); + } + + return AllCommentsResponseRt.encode(flattenCommentSavedObjects(comments.saved_objects)); + } catch (error) { + throw createCaseError({ + message: `Failed to get all comments case id: ${caseID} include sub case comments: ${includeSubCaseComments} sub case id: ${subCaseID}: ${error}`, + error, + logger, + }); + } +} diff --git a/x-pack/plugins/cases/server/client/attachments/update.ts b/x-pack/plugins/cases/server/client/attachments/update.ts new file mode 100644 index 00000000000000..79b1f5bfc02255 --- /dev/null +++ b/x-pack/plugins/cases/server/client/attachments/update.ts @@ -0,0 +1,181 @@ +/* + * 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 { pick } from 'lodash/fp'; +import Boom from '@hapi/boom'; + +import { SavedObjectsClientContract, Logger } from 'kibana/server'; +import { checkEnabledCaseConnectorOrThrow, CommentableCase } from '../../common'; +import { buildCommentUserActionItem } from '../../services/user_actions/helpers'; +import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../common/constants'; +import { AttachmentService, CaseService } from '../../services'; +import { CaseResponse, CommentPatchRequest } from '../../../common/api'; +import { CasesClientArgs } from '..'; +import { decodeCommentRequest } from '../utils'; +import { createCaseError } from '../../common/error'; + +export interface UpdateArgs { + caseID: string; + updateRequest: CommentPatchRequest; + subCaseID?: string; +} + +interface CombinedCaseParams { + attachmentService: AttachmentService; + caseService: CaseService; + soClient: SavedObjectsClientContract; + caseID: string; + logger: Logger; + subCaseId?: string; +} + +async function getCommentableCase({ + attachmentService, + caseService, + soClient, + caseID, + subCaseId, + logger, +}: CombinedCaseParams) { + if (subCaseId) { + const [caseInfo, subCase] = await Promise.all([ + caseService.getCase({ + soClient, + id: caseID, + }), + caseService.getSubCase({ + soClient, + id: subCaseId, + }), + ]); + return new CommentableCase({ + attachmentService, + caseService, + collection: caseInfo, + subCase, + soClient, + logger, + }); + } else { + const caseInfo = await caseService.getCase({ + soClient, + id: caseID, + }); + return new CommentableCase({ + attachmentService, + caseService, + collection: caseInfo, + soClient, + logger, + }); + } +} + +/** + * Update an attachment. + */ +export async function update( + { caseID, subCaseID, updateRequest: queryParams }: UpdateArgs, + clientArgs: CasesClientArgs +): Promise { + const { + attachmentService, + caseService, + savedObjectsClient: soClient, + logger, + user, + userActionService, + } = clientArgs; + + try { + checkEnabledCaseConnectorOrThrow(subCaseID); + + const { + id: queryCommentId, + version: queryCommentVersion, + ...queryRestAttributes + } = queryParams; + + decodeCommentRequest(queryRestAttributes); + + const commentableCase = await getCommentableCase({ + attachmentService, + caseService, + soClient, + caseID, + subCaseId: subCaseID, + logger, + }); + + const myComment = await attachmentService.get({ + soClient, + attachmentId: queryCommentId, + }); + + if (myComment == null) { + throw Boom.notFound(`This comment ${queryCommentId} does not exist anymore.`); + } + + if (myComment.attributes.type !== queryRestAttributes.type) { + throw Boom.badRequest(`You cannot change the type of the comment.`); + } + + const saveObjType = subCaseID ? SUB_CASE_SAVED_OBJECT : CASE_SAVED_OBJECT; + + const caseRef = myComment.references.find((c) => c.type === saveObjType); + if (caseRef == null || (caseRef != null && caseRef.id !== commentableCase.id)) { + throw Boom.notFound( + `This comment ${queryCommentId} does not exist in ${commentableCase.id}).` + ); + } + + if (queryCommentVersion !== myComment.version) { + throw Boom.conflict( + 'This case has been updated. Please refresh before saving additional updates.' + ); + } + + const updatedDate = new Date().toISOString(); + const { + comment: updatedComment, + commentableCase: updatedCase, + } = await commentableCase.updateComment({ + updateRequest: queryParams, + updatedAt: updatedDate, + user, + }); + + await userActionService.bulkCreate({ + soClient, + actions: [ + buildCommentUserActionItem({ + action: 'update', + actionAt: updatedDate, + actionBy: user, + caseId: caseID, + subCaseId: subCaseID, + commentId: updatedComment.id, + fields: ['comment'], + newValue: JSON.stringify(queryRestAttributes), + oldValue: JSON.stringify( + // We are interested only in ContextBasicRt attributes + // myComment.attribute contains also CommentAttributesBasicRt attributes + pick(Object.keys(queryRestAttributes), myComment.attributes) + ), + }), + ], + }); + + return await updatedCase.encode(); + } catch (error) { + throw createCaseError({ + message: `Failed to patch comment case id: ${caseID} sub case id: ${subCaseID}: ${error}`, + error, + logger, + }); + } +} diff --git a/x-pack/plugins/cases/server/client/cases/client.ts b/x-pack/plugins/cases/server/client/cases/client.ts index a77bfa01e6ec8d..423863528184a1 100644 --- a/x-pack/plugins/cases/server/client/cases/client.ts +++ b/x-pack/plugins/cases/server/client/cases/client.ts @@ -13,36 +13,47 @@ import { CasesResponse, CasesFindRequest, CasesFindResponse, + User, } from '../../../common/api'; import { CasesClient } from '../client'; import { CasesClientInternal } from '../client_internal'; import { CasesClientArgs } from '../types'; import { create } from './create'; +import { deleteCases } from './delete'; import { find } from './find'; -import { get } from './get'; +import { get, getReporters, getTags } from './get'; import { push } from './push'; import { update } from './update'; -export interface CaseGet { +interface CaseGet { id: string; includeComments?: boolean; includeSubCaseComments?: boolean; } -export interface CasePush { +interface CasePush { actionsClient: ActionsClient; caseId: string; connectorId: string; } +/** + * The public API for interacting with cases. + */ export interface CasesSubClient { create(theCase: CasePostRequest): Promise; find(args: CasesFindRequest): Promise; get(args: CaseGet): Promise; push(args: CasePush): Promise; update(args: CasesPatchRequest): Promise; + delete(ids: string[]): Promise; + getTags(): Promise; + getReporters(): Promise; } +/** + * Creates the interface for CRUD on cases objects. + */ export const createCasesSubClient = ( args: CasesClientArgs, casesClient: CasesClient, @@ -112,6 +123,9 @@ export const createCasesSubClient = ( casesClientInternal, logger, }), + delete: (ids: string[]) => deleteCases(ids, args), + getTags: () => getTags(args), + getReporters: () => getReporters(args), }; 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 67496599d225da..d4c3ba52095839 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -17,8 +17,6 @@ import { SavedObjectsUtils, } from '../../../../../../src/core/server'; -import { flattenCaseSavedObject, transformNewCase } from '../../routes/api/utils'; - import { throwErrors, excess, @@ -30,10 +28,7 @@ import { User, } from '../../../common/api'; import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; -import { - getConnectorFromConfiguration, - transformCaseConnectorToEsConnector, -} from '../../routes/api/cases/helpers'; +import { getConnectorFromConfiguration } from '../utils'; import { CaseConfigureService, CaseService, CaseUserActionService } from '../../services'; import { createCaseError } from '../../common/error'; @@ -41,7 +36,12 @@ import { Authorization } from '../../authorization/authorization'; import { Operations } from '../../authorization'; import { AuditLogger, EventOutcome } from '../../../../security/server'; import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; -import { createAuditMsg } from '../../common'; +import { + createAuditMsg, + flattenCaseSavedObject, + transformCaseConnectorToEsConnector, + transformNewCase, +} from '../../common'; interface CreateCaseArgs { caseConfigureService: CaseConfigureService; diff --git a/x-pack/plugins/cases/server/client/cases/delete.ts b/x-pack/plugins/cases/server/client/cases/delete.ts new file mode 100644 index 00000000000000..1bc94b5a0b4c85 --- /dev/null +++ b/x-pack/plugins/cases/server/client/cases/delete.ts @@ -0,0 +1,128 @@ +/* + * 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 { 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'; + +async function deleteSubCases({ + attachmentService, + caseService, + soClient, + caseIds, +}: { + attachmentService: AttachmentService; + caseService: CaseService; + soClient: SavedObjectsClientContract; + caseIds: string[]; +}) { + const subCasesForCaseIds = await caseService.findSubCasesByCaseId({ soClient, ids: caseIds }); + + const subCaseIDs = subCasesForCaseIds.saved_objects.map((subCase) => subCase.id); + const commentsForSubCases = await caseService.getAllSubCaseComments({ + soClient, + id: subCaseIDs, + }); + + // This shouldn't actually delete anything because all the comments should be deleted when comments are deleted + // per case ID + await Promise.all( + commentsForSubCases.saved_objects.map((commentSO) => + attachmentService.delete({ soClient, attachmentId: commentSO.id }) + ) + ); + + await Promise.all( + subCasesForCaseIds.saved_objects.map((subCaseSO) => + caseService.deleteSubCase(soClient, subCaseSO.id) + ) + ); +} + +export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): Promise { + const { + savedObjectsClient: soClient, + caseService, + attachmentService, + user, + userActionService, + logger, + } = clientArgs; + try { + await Promise.all( + ids.map((id) => + caseService.deleteCase({ + soClient, + id, + }) + ) + ); + const comments = await Promise.all( + ids.map((id) => + caseService.getAllCaseComments({ + soClient, + id, + }) + ) + ); + + if (comments.some((c) => c.saved_objects.length > 0)) { + await Promise.all( + comments.map((c) => + Promise.all( + c.saved_objects.map(({ id }) => + attachmentService.delete({ + soClient, + attachmentId: id, + }) + ) + ) + ) + ); + } + + if (ENABLE_CASE_CONNECTOR) { + await deleteSubCases({ + attachmentService, + caseService, + soClient, + caseIds: ids, + }); + } + + const deleteDate = new Date().toISOString(); + + await userActionService.bulkCreate({ + soClient, + actions: ids.map((id) => + buildCaseUserActionItem({ + action: 'create', + actionAt: deleteDate, + actionBy: user, + caseId: id, + fields: [ + 'comment', + 'description', + 'status', + 'tags', + 'title', + ...(ENABLE_CASE_CONNECTOR ? ['sub_case'] : []), + ], + }) + ), + }); + } catch (error) { + throw createCaseError({ + message: `Failed to delete cases ids: ${JSON.stringify(ids)}: ${error}`, + error, + logger, + }); + } +} diff --git a/x-pack/plugins/cases/server/client/cases/find.ts b/x-pack/plugins/cases/server/client/cases/find.ts index aebecb821b4498..b3c201f65f212c 100644 --- a/x-pack/plugins/cases/server/client/cases/find.ts +++ b/x-pack/plugins/cases/server/client/cases/find.ts @@ -25,13 +25,12 @@ import { import { CASE_SAVED_OBJECT } from '../../../common/constants'; import { CaseService } from '../../services'; import { createCaseError } from '../../common/error'; -import { constructQueryOptions } from '../../routes/api/cases/helpers'; -import { transformCases } from '../../routes/api/utils'; +import { constructQueryOptions } from '../utils'; import { Authorization } from '../../authorization/authorization'; import { includeFieldsRequiredForAuthentication } from '../../authorization/utils'; import { AuthorizationFilter, Operations } from '../../authorization'; import { AuditLogger } from '../../../../security/server'; -import { createAuditMsg } from '../../common'; +import { createAuditMsg, transformCases } from '../../common'; interface FindParams { savedObjectsClient: SavedObjectsClientContract; diff --git a/x-pack/plugins/cases/server/client/cases/get.ts b/x-pack/plugins/cases/server/client/cases/get.ts index ccef35007118f8..58fff0d5e435d9 100644 --- a/x-pack/plugins/cases/server/client/cases/get.ts +++ b/x-pack/plugins/cases/server/client/cases/get.ts @@ -4,14 +4,15 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import Boom from '@hapi/boom'; import { SavedObjectsClientContract, Logger, SavedObject } from 'kibana/server'; -import { flattenCaseSavedObject } from '../../routes/api/utils'; -import { CaseResponseRt, CaseResponse, ESCaseAttributes } from '../../../common/api'; +import { CaseResponseRt, CaseResponse, ESCaseAttributes, User, UsersRt } from '../../../common/api'; import { CaseService } from '../../services'; -import { countAlertsForID } from '../../common'; +import { countAlertsForID, flattenCaseSavedObject } from '../../common'; import { createCaseError } from '../../common/error'; import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; +import { CasesClientArgs } from '..'; interface GetParams { savedObjectsClient: SavedObjectsClientContract; @@ -34,6 +35,12 @@ export const get = async ({ includeSubCaseComments = false, }: GetParams): Promise => { try { + if (!ENABLE_CASE_CONNECTOR && includeSubCaseComments) { + throw Boom.badRequest( + 'The `includeSubCaseComments` is not supported when the case connector feature is disabled' + ); + } + let theCase: SavedObject; let subCaseIds: string[] = []; @@ -86,3 +93,38 @@ export const get = async ({ throw createCaseError({ message: `Failed to get case id: ${id}: ${error}`, error, logger }); } }; + +/** + * Retrieves the tags from all the cases. + */ +export async function getTags({ + savedObjectsClient: soClient, + caseService, + logger, +}: CasesClientArgs): Promise { + try { + return await caseService.getTags({ + soClient, + }); + } catch (error) { + throw createCaseError({ message: `Failed to get tags: ${error}`, error, logger }); + } +} + +/** + * Retrieves the reporters from all the cases. + */ +export async function getReporters({ + savedObjectsClient: soClient, + caseService, + logger, +}: CasesClientArgs): Promise { + try { + const reporters = await caseService.getReporters({ + soClient, + }); + return UsersRt.encode(reporters); + } 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 c2c4d11da991dc..ae690c8b6a086b 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -15,7 +15,6 @@ import { SavedObject, } from 'kibana/server'; import { ActionResult, ActionsClient } from '../../../../actions/server'; -import { flattenCaseSavedObject, getAlertInfoFromComments } from '../../routes/api/utils'; import { ActionConnector, @@ -39,7 +38,7 @@ import { CaseUserActionService, AttachmentService, } from '../../services'; -import { createCaseError } from '../../common/error'; +import { createCaseError, flattenCaseSavedObject, getAlertInfoFromComments } from '../../common'; import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; import { CasesClient, CasesClientInternal } from '..'; @@ -134,7 +133,6 @@ export const push = async ({ try { connectorMappings = await casesClientInternal.configuration.getMappings({ - actionsClient, connectorId: connector.id, connectorType: connector.actionTypeId, }); diff --git a/x-pack/plugins/cases/server/client/cases/update.ts b/x-pack/plugins/cases/server/client/cases/update.ts index 52674e4c1b461c..dcd66ebbcae260 100644 --- a/x-pack/plugins/cases/server/client/cases/update.ts +++ b/x-pack/plugins/cases/server/client/cases/update.ts @@ -19,10 +19,6 @@ import { } from 'kibana/server'; import { nodeBuilder } from '../../../../../../src/plugins/data/common'; -import { - flattenCaseSavedObject, - isCommentRequestTypeAlertOrGenAlert, -} from '../../routes/api/utils'; import { throwErrors, @@ -42,10 +38,7 @@ import { User, } from '../../../common/api'; import { buildCaseUserActions } from '../../services/user_actions/helpers'; -import { - getCaseToUpdate, - transformCaseConnectorToEsConnector, -} from '../../routes/api/cases/helpers'; +import { getCaseToUpdate } from '../utils'; import { CaseService, CaseUserActionService } from '../../services'; import { @@ -53,7 +46,12 @@ import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT, } from '../../../common/constants'; -import { createAlertUpdateRequest } from '../../common'; +import { + createAlertUpdateRequest, + transformCaseConnectorToEsConnector, + flattenCaseSavedObject, + isCommentRequestTypeAlertOrGenAlert, +} from '../../common'; import { createCaseError } from '../../common/error'; import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; import { UpdateAlertRequest } from '../alerts/client'; diff --git a/x-pack/plugins/cases/server/client/cases/utils.test.ts b/x-pack/plugins/cases/server/client/cases/utils.test.ts index 859114a5e8fb07..5f41a95d3c5017 100644 --- a/x-pack/plugins/cases/server/client/cases/utils.test.ts +++ b/x-pack/plugins/cases/server/client/cases/utils.test.ts @@ -6,7 +6,6 @@ */ import { actionsClientMock } from '../../../../actions/server/actions_client.mock'; -import { flattenCaseSavedObject } from '../../routes/api/utils'; import { mockCases } from '../../routes/api/__fixtures__'; import { BasicParams, ExternalServiceParams, Incident } from './types'; @@ -29,6 +28,7 @@ import { transformers, transformFields, } from './utils'; +import { flattenCaseSavedObject } from '../../common'; const formatComment = { commentId: commentObj.id, diff --git a/x-pack/plugins/cases/server/client/cases/utils.ts b/x-pack/plugins/cases/server/client/cases/utils.ts index 7e77bf4ac84cc8..8bac4956a9e5f9 100644 --- a/x-pack/plugins/cases/server/client/cases/utils.ts +++ b/x-pack/plugins/cases/server/client/cases/utils.ts @@ -38,7 +38,7 @@ import { TransformerArgs, TransformFieldsArgs, } from './types'; -import { getAlertIds } from '../../routes/api/utils'; +import { getAlertIds } from '../utils'; interface CreateIncidentArgs { actionsClient: ActionsClient; diff --git a/x-pack/plugins/cases/server/client/client.ts b/x-pack/plugins/cases/server/client/client.ts index cb2201b8721f2d..9d0da7018518f3 100644 --- a/x-pack/plugins/cases/server/client/client.ts +++ b/x-pack/plugins/cases/server/client/client.ts @@ -12,6 +12,8 @@ import { UserActionsSubClient, createUserActionsSubClient } from './user_actions import { CasesClientInternal, createCasesClientInternal } from './client_internal'; import { createSubCasesClient, SubCasesClient } from './sub_cases/client'; import { ENABLE_CASE_CONNECTOR } from '../../common/constants'; +import { ConfigureSubClient, createConfigurationSubClient } from './configure/client'; +import { createStatsSubClient, StatsSubClient } from './stats/client'; export class CasesClient { private readonly _casesClientInternal: CasesClientInternal; @@ -19,13 +21,17 @@ export class CasesClient { private readonly _attachments: AttachmentsSubClient; private readonly _userActions: UserActionsSubClient; private readonly _subCases: SubCasesClient; + private readonly _configure: ConfigureSubClient; + private readonly _stats: StatsSubClient; constructor(args: CasesClientArgs) { this._casesClientInternal = createCasesClientInternal(args); this._cases = createCasesSubClient(args, this, this._casesClientInternal); this._attachments = createAttachmentsSubClient(args, this._casesClientInternal); this._userActions = createUserActionsSubClient(args); - this._subCases = createSubCasesClient(args, this); + this._subCases = createSubCasesClient(args, this._casesClientInternal); + this._configure = createConfigurationSubClient(args, this._casesClientInternal); + this._stats = createStatsSubClient(args); } public get cases() { @@ -47,9 +53,12 @@ export class CasesClient { return this._subCases; } - // TODO: Remove it when all routes will be moved to the cases client. - public get casesClientInternal() { - return this._casesClientInternal; + public get configure() { + return this._configure; + } + + public get stats() { + return this._stats; } } diff --git a/x-pack/plugins/cases/server/client/client_internal.ts b/x-pack/plugins/cases/server/client/client_internal.ts index 79f107e17af35d..3623498223da72 100644 --- a/x-pack/plugins/cases/server/client/client_internal.ts +++ b/x-pack/plugins/cases/server/client/client_internal.ts @@ -7,15 +7,18 @@ import { CasesClientArgs } from './types'; import { AlertSubClient, createAlertsSubClient } from './alerts/client'; -import { ConfigureSubClient, createConfigurationSubClient } from './configure/client'; +import { + InternalConfigureSubClient, + createInternalConfigurationSubClient, +} from './configure/client'; export class CasesClientInternal { private readonly _alerts: AlertSubClient; - private readonly _configuration: ConfigureSubClient; + private readonly _configuration: InternalConfigureSubClient; constructor(args: CasesClientArgs) { this._alerts = createAlertsSubClient(args); - this._configuration = createConfigurationSubClient(args, this); + this._configuration = createInternalConfigurationSubClient(args, this); } public get alerts() { diff --git a/x-pack/plugins/cases/server/client/configure/client.ts b/x-pack/plugins/cases/server/client/configure/client.ts index 8ea91415fd1635..2b9048a4518e92 100644 --- a/x-pack/plugins/cases/server/client/configure/client.ts +++ b/x-pack/plugins/cases/server/client/configure/client.ts @@ -4,39 +4,71 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import Boom from '@hapi/boom'; -import { ActionsClient } from '../../../../actions/server'; -import { ConnectorMappingsAttributes, GetFieldsResponse } from '../../../common/api'; +import { SUPPORTED_CONNECTORS } from '../../../common/constants'; +import { + CaseConfigureResponseRt, + CasesConfigurePatch, + CasesConfigureRequest, + CasesConfigureResponse, + ConnectorMappingsAttributes, + GetFieldsResponse, +} from '../../../common/api'; +import { createCaseError } from '../../common/error'; +import { + transformCaseConnectorToEsConnector, + transformESConnectorToCaseConnector, +} from '../../common'; import { CasesClientInternal } from '../client_internal'; import { CasesClientArgs } from '../types'; import { getFields } from './get_fields'; import { getMappings } from './get_mappings'; -export interface ConfigurationGetFields { - actionsClient: ActionsClient; +// 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; } -export interface ConfigurationGetMappings { - actionsClient: ActionsClient; +interface ConfigurationGetMappings { connectorId: string; connectorType: string; } -export interface ConfigureSubClient { +/** + * Defines the internal helper functions. + */ +export interface InternalConfigureSubClient { getFields(args: ConfigurationGetFields): Promise; getMappings(args: ConfigurationGetMappings): Promise; } -export const createConfigurationSubClient = ( +/** + * This is the public API for interacting with the connector configuration for cases. + */ +export interface ConfigureSubClient { + get(): Promise; + getConnectors(): Promise; + update(configurations: CasesConfigurePatch): Promise; + create(configuration: CasesConfigureRequest): Promise; +} + +/** + * These functions should not be exposed on the plugin contract. They are for internal use to support the CRUD of + * configurations. + */ +export const createInternalConfigurationSubClient = ( args: CasesClientArgs, casesClientInternal: CasesClientInternal -): ConfigureSubClient => { - const { savedObjectsClient, connectorMappingsService, logger } = args; +): InternalConfigureSubClient => { + const { savedObjectsClient, connectorMappingsService, logger, actionsClient } = args; - const configureSubClient: ConfigureSubClient = { - getFields: (fields: ConfigurationGetFields) => getFields(fields), + const configureSubClient: InternalConfigureSubClient = { + getFields: (fields: ConfigurationGetFields) => getFields({ ...fields, actionsClient }), getMappings: (params: ConfigurationGetMappings) => getMappings({ ...params, @@ -49,3 +81,209 @@ export const createConfigurationSubClient = ( return Object.freeze(configureSubClient); }; + +export const createConfigurationSubClient = ( + clientArgs: CasesClientArgs, + casesInternalClient: CasesClientInternal +): ConfigureSubClient => { + return Object.freeze({ + get: () => get(clientArgs, casesInternalClient), + getConnectors: () => getConnectors(clientArgs), + update: (configuration: CasesConfigurePatch) => + update(configuration, clientArgs, casesInternalClient), + create: (configuration: CasesConfigureRequest) => + create(configuration, clientArgs, casesInternalClient), + }); +}; + +async function get( + clientArgs: CasesClientArgs, + casesClientInternal: CasesClientInternal +): Promise { + const { savedObjectsClient: soClient, caseConfigureService, logger } = clientArgs; + try { + let error: string | null = null; + + const myCaseConfigure = await caseConfigureService.find({ soClient }); + + 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`; + } + } + + return myCaseConfigure.saved_objects.length > 0 + ? CaseConfigureResponseRt.encode({ + ...caseConfigureWithoutConnector, + connector: transformESConnectorToCaseConnector(connector), + mappings, + version: myCaseConfigure.saved_objects[0].version ?? '', + error, + }) + : {}; + } catch (error) { + throw createCaseError({ message: `Failed to get case configure: ${error}`, error, logger }); + } +} + +async function getConnectors({ + actionsClient, + logger, +}: CasesClientArgs): Promise { + const isConnectorSupported = ( + action: FindActionResult, + actionTypes: Record + ): boolean => + SUPPORTED_CONNECTORS.includes(action.actionTypeId) && + actionTypes[action.actionTypeId]?.enabledInLicense; + + try { + const actionTypes = (await actionsClient.listTypes()).reduce( + (types, type) => ({ ...types, [type.id]: type }), + {} + ); + + return (await actionsClient.getAll()).filter((action) => + isConnectorSupported(action, actionTypes) + ); + } catch (error) { + throw createCaseError({ message: `Failed to get connectors: ${error}`, error, logger }); + } +} + +async function update( + configurations: CasesConfigurePatch, + clientArgs: CasesClientArgs, + casesClientInternal: CasesClientInternal +): Promise { + const { caseConfigureService, logger, savedObjectsClient: soClient, user } = clientArgs; + + try { + let error = null; + + 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.' + ); + } + + if (version !== myCaseConfigure.saved_objects[0].version) { + throw Boom.conflict( + 'This configuration has been updated. Please refresh before saving additional updates.' + ); + } + + 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 patch = await caseConfigureService.patch({ + soClient, + caseConfigureId: myCaseConfigure.saved_objects[0].id, + updatedAttributes: { + ...queryWithoutVersion, + ...(connector != null ? { connector: transformCaseConnectorToEsConnector(connector) } : {}), + updated_at: updateDate, + updated_by: user, + }, + }); + return CaseConfigureResponseRt.encode({ + ...myCaseConfigure.saved_objects[0].attributes, + ...patch.attributes, + connector: transformESConnectorToCaseConnector( + patch.attributes.connector ?? myCaseConfigure.saved_objects[0].attributes.connector + ), + mappings, + version: patch.version ?? '', + error, + }); + } catch (error) { + throw createCaseError({ + message: `Failed to get patch configure in route: ${error}`, + error, + logger, + }); + } +} + +async function create( + configuration: CasesConfigureRequest, + clientArgs: CasesClientArgs, + casesClientInternal: CasesClientInternal +): Promise { + const { savedObjectsClient: soClient, caseConfigureService, logger, user } = clientArgs; + try { + let error = null; + + const myCaseConfigure = await caseConfigureService.find({ soClient }); + if (myCaseConfigure.saved_objects.length > 0) { + await Promise.all( + myCaseConfigure.saved_objects.map((cc) => + caseConfigureService.delete({ soClient, caseConfigureId: cc.id }) + ) + ); + } + + const creationDate = new Date().toISOString(); + let mappings: ConnectorMappingsAttributes[] = []; + try { + mappings = await casesClientInternal.configuration.getMappings({ + connectorId: configuration.connector.id, + connectorType: configuration.connector.type, + }); + } catch (e) { + error = e.isBoom + ? e.output.payload.message + : `Error connecting to ${configuration.connector.name} instance`; + } + const post = await caseConfigureService.post({ + soClient, + attributes: { + ...configuration, + connector: transformCaseConnectorToEsConnector(configuration.connector), + created_at: creationDate, + created_by: user, + updated_at: null, + updated_by: null, + }, + }); + + return CaseConfigureResponseRt.encode({ + ...post.attributes, + // Reserve for future implementations + connector: transformESConnectorToCaseConnector(post.attributes.connector), + mappings, + version: post.version ?? '', + error, + }); + } catch (error) { + throw createCaseError({ + message: `Failed to create case configuration: ${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 799f50845dda6b..8a6b20256328fd 100644 --- a/x-pack/plugins/cases/server/client/configure/get_fields.ts +++ b/x-pack/plugins/cases/server/client/configure/get_fields.ts @@ -6,11 +6,18 @@ */ import Boom from '@hapi/boom'; +import { PublicMethodsOf } from '@kbn/utility-types'; +import { ActionsClient } from '../../../../actions/server'; import { GetFieldsResponse } from '../../../common/api'; -import { ConfigurationGetFields } from './client'; import { createDefaultMapping, formatFields } from './utils'; +interface ConfigurationGetFields { + connectorId: string; + connectorType: string; + actionsClient: PublicMethodsOf; +} + export const getFields = async ({ actionsClient, connectorType, 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 c157252909f669..4f8b8c6cbf32a5 100644 --- a/x-pack/plugins/cases/server/client/configure/get_mappings.ts +++ b/x-pack/plugins/cases/server/client/configure/get_mappings.ts @@ -6,7 +6,6 @@ */ import { SavedObjectsClientContract, Logger } from 'src/core/server'; -import { ActionsClient } from '../../../../actions/server'; 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'; @@ -17,7 +16,6 @@ import { createCaseError } from '../../common/error'; interface GetMappingsArgs { savedObjectsClient: SavedObjectsClientContract; connectorMappingsService: ConnectorMappingsService; - actionsClient: ActionsClient; casesClientInternal: CasesClientInternal; connectorType: string; connectorId: string; @@ -27,7 +25,6 @@ interface GetMappingsArgs { export const getMappings = async ({ savedObjectsClient, connectorMappingsService, - actionsClient, casesClientInternal, connectorType, connectorId, @@ -50,7 +47,6 @@ export const getMappings = async ({ // Create connector mappings if there are none if (myConnectorMappings.total === 0) { const res = await casesClientInternal.configuration.getFields({ - actionsClient, connectorId, connectorType, }); diff --git a/x-pack/plugins/cases/server/client/factory.ts b/x-pack/plugins/cases/server/client/factory.ts index 87a2b9583dac07..1202fe8c2a421d 100644 --- a/x-pack/plugins/cases/server/client/factory.ts +++ b/x-pack/plugins/cases/server/client/factory.ts @@ -24,6 +24,7 @@ import { AttachmentService, } from '../services'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; +import { PluginStartContract as ActionsPluginStart } from '../../../actions/server'; import { AuthorizationAuditLogger } from '../authorization'; import { CasesClient, createCasesClient } from '.'; @@ -38,6 +39,7 @@ interface CasesClientFactoryArgs { securityPluginStart?: SecurityPluginStart; getSpace: GetSpaceFn; featuresPluginStart: FeaturesPluginStart; + actionsPluginStart: ActionsPluginStart; } /** @@ -95,7 +97,7 @@ export class CasesClientFactory { auditLogger: new AuthorizationAuditLogger(auditLogger), }); - const user = this.options.caseService.getUser({ request }); + const userInfo = this.options.caseService.getUser({ request }); return createCasesClient({ alertsService: this.options.alertsService, @@ -103,7 +105,8 @@ export class CasesClientFactory { savedObjectsClient: savedObjectsService.getScopedClient(request, { includedHiddenTypes: SAVED_OBJECT_TYPES, }), - user, + // We only want these fields from the userInfo object + user: { username: userInfo.username, email: userInfo.email, full_name: userInfo.full_name }, caseService: this.options.caseService, caseConfigureService: this.options.caseConfigureService, connectorMappingsService: this.options.connectorMappingsService, @@ -112,6 +115,7 @@ export class CasesClientFactory { logger: this.logger, authorization: auth, auditLogger, + actionsClient: await this.options.actionsPluginStart.getActionsClientWithRequest(request), }); } } diff --git a/x-pack/plugins/cases/server/client/mocks.ts b/x-pack/plugins/cases/server/client/mocks.ts index 03ad31fc2c1bbc..7db3d62c491e7a 100644 --- a/x-pack/plugins/cases/server/client/mocks.ts +++ b/x-pack/plugins/cases/server/client/mocks.ts @@ -7,10 +7,12 @@ import { PublicContract, PublicMethodsOf } from '@kbn/utility-types'; -import { CasesClient, CasesClientInternal } from '.'; +import { CasesClient } from '.'; import { AttachmentsSubClient } from './attachments/client'; import { CasesSubClient } from './cases/client'; +import { ConfigureSubClient } from './configure/client'; import { CasesClientFactory } from './factory'; +import { StatsSubClient } from './stats/client'; import { SubCasesClient } from './sub_cases/client'; import { UserActionsSubClient } from './user_actions/client'; @@ -23,6 +25,9 @@ const createCasesSubClientMock = (): CasesSubClientMock => { get: jest.fn(), push: jest.fn(), update: jest.fn(), + delete: jest.fn(), + getTags: jest.fn(), + getReporters: jest.fn(), }; }; @@ -31,6 +36,12 @@ type AttachmentsSubClientMock = jest.Mocked; const createAttachmentsSubClientMock = (): AttachmentsSubClientMock => { return { add: jest.fn(), + deleteAll: jest.fn(), + delete: jest.fn(), + find: jest.fn(), + getAll: jest.fn(), + get: jest.fn(), + update: jest.fn(), }; }; @@ -53,7 +64,24 @@ const createSubCasesClientMock = (): SubCasesClientMock => { }; }; -type CasesClientInternalMock = jest.Mocked; +type ConfigureSubClientMock = jest.Mocked; + +const createConfigureSubClientMock = (): ConfigureSubClientMock => { + return { + get: jest.fn(), + getConnectors: jest.fn(), + update: jest.fn(), + create: jest.fn(), + }; +}; + +type StatsSubClientMock = jest.Mocked; + +const createStatsSubClientMock = (): StatsSubClientMock => { + return { + getStatusTotalsByType: jest.fn(), + }; +}; export interface CasesClientMock extends CasesClient { cases: CasesSubClientMock; @@ -64,11 +92,12 @@ export interface CasesClientMock extends CasesClient { export const createCasesClientMock = (): CasesClientMock => { const client: PublicContract = { - casesClientInternal: (jest.fn() as unknown) as CasesClientInternalMock, cases: createCasesSubClientMock(), attachments: createAttachmentsSubClientMock(), userActions: createUserActionsSubClientMock(), subCases: createSubCasesClientMock(), + configure: createConfigureSubClientMock(), + stats: createStatsSubClientMock(), }; return (client as unknown) as CasesClientMock; }; diff --git a/x-pack/plugins/cases/server/client/stats/client.ts b/x-pack/plugins/cases/server/client/stats/client.ts new file mode 100644 index 00000000000000..40ced0bfbf4bb0 --- /dev/null +++ b/x-pack/plugins/cases/server/client/stats/client.ts @@ -0,0 +1,54 @@ +/* + * 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 { CasesClientArgs } from '..'; +import { CasesStatusResponse, CasesStatusResponseRt, caseStatuses } from '../../../common/api'; +import { createCaseError } from '../../common/error'; +import { constructQueryOptions } from '../utils'; + +/** + * Statistics API contract. + */ +export interface StatsSubClient { + getStatusTotalsByType(): Promise; +} + +/** + * Creates the interface for retrieving the number of open, closed, and in progress cases. + */ +export function createStatsSubClient(clientArgs: CasesClientArgs): StatsSubClient { + return Object.freeze({ + getStatusTotalsByType: () => getStatusTotalsByType(clientArgs), + }); +} + +async function getStatusTotalsByType({ + savedObjectsClient: soClient, + caseService, + logger, +}: CasesClientArgs): Promise { + try { + const [openCases, inProgressCases, closedCases] = await Promise.all([ + ...caseStatuses.map((status) => { + const statusQuery = constructQueryOptions({ status }); + return caseService.findCaseStatusStats({ + soClient, + caseOptions: statusQuery.case, + subCaseOptions: statusQuery.subCase, + }); + }), + ]); + + return CasesStatusResponseRt.encode({ + count_open_cases: openCases, + count_in_progress_cases: inProgressCases, + count_closed_cases: closedCases, + }); + } catch (error) { + throw createCaseError({ message: `Failed to get status stats: ${error}`, error, logger }); + } +} diff --git a/x-pack/plugins/cases/server/client/sub_cases/client.ts b/x-pack/plugins/cases/server/client/sub_cases/client.ts index aef780ecb3ac95..ac390710def876 100644 --- a/x-pack/plugins/cases/server/client/sub_cases/client.ts +++ b/x-pack/plugins/cases/server/client/sub_cases/client.ts @@ -17,15 +17,13 @@ import { SubCasesPatchRequest, SubCasesResponse, } from '../../../common/api'; -import { CasesClientArgs } from '..'; -import { flattenSubCaseSavedObject, transformSubCases } from '../../routes/api/utils'; -import { countAlertsForID } from '../../common'; +import { CasesClientArgs, CasesClientInternal } from '..'; +import { countAlertsForID, flattenSubCaseSavedObject, transformSubCases } from '../../common'; import { createCaseError } from '../../common/error'; import { CASE_SAVED_OBJECT } from '../../../common/constants'; import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; -import { constructQueryOptions } from '../../routes/api/cases/helpers'; +import { constructQueryOptions } from '../utils'; import { defaultPage, defaultPerPage } from '../../routes/api'; -import { CasesClient } from '../client'; import { update } from './update'; interface FindArgs { @@ -53,13 +51,14 @@ export interface SubCasesClient { */ export function createSubCasesClient( clientArgs: CasesClientArgs, - casesClient: CasesClient + casesClientInternal: CasesClientInternal ): SubCasesClient { return Object.freeze({ delete: (ids: string[]) => deleteSubCase(ids, clientArgs), find: (findArgs: FindArgs) => find(findArgs, clientArgs), get: (getArgs: GetArgs) => get(getArgs, clientArgs), - update: (subCases: SubCasesPatchRequest) => update(subCases, clientArgs, casesClient), + update: (subCases: SubCasesPatchRequest) => + update({ subCases, clientArgs, casesClientInternal }), }); } diff --git a/x-pack/plugins/cases/server/client/sub_cases/update.ts b/x-pack/plugins/cases/server/client/sub_cases/update.ts index 27e6e1261c0af5..de7a75634d7fbd 100644 --- a/x-pack/plugins/cases/server/client/sub_cases/update.ts +++ b/x-pack/plugins/cases/server/client/sub_cases/update.ts @@ -17,7 +17,6 @@ import { } from 'kibana/server'; import { nodeBuilder } from '../../../../../../src/plugins/data/common'; -import { CasesClient } from '../../client'; import { CaseService } from '../../services'; import { CaseStatuses, @@ -36,16 +35,17 @@ import { CommentAttributes, } from '../../../common/api'; import { CASE_COMMENT_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../common/constants'; +import { getCaseToUpdate } from '../utils'; +import { buildSubCaseUserActions } from '../../services/user_actions/helpers'; import { - flattenSubCaseSavedObject, + createAlertUpdateRequest, isCommentRequestTypeAlertOrGenAlert, -} from '../../routes/api/utils'; -import { getCaseToUpdate } from '../../routes/api/cases/helpers'; -import { buildSubCaseUserActions } from '../../services/user_actions/helpers'; -import { createAlertUpdateRequest } from '../../common'; + flattenSubCaseSavedObject, +} from '../../common'; import { createCaseError } from '../../common/error'; import { UpdateAlertRequest } from '../../client/alerts/client'; import { CasesClientArgs } from '../types'; +import { CasesClientInternal } from '../client_internal'; function checkNonExistingOrConflict( toUpdate: SubCasePatchRequest[], @@ -207,13 +207,13 @@ async function getAlertComments({ async function updateAlerts({ caseService, soClient, - casesClient, + casesClientInternal, logger, subCasesToSync, }: { caseService: CaseService; soClient: SavedObjectsClientContract; - casesClient: CasesClient; + casesClientInternal: CasesClientInternal; logger: Logger; subCasesToSync: SubCasePatchRequest[]; }) { @@ -241,7 +241,7 @@ async function updateAlerts({ [] ); - await casesClient.casesClientInternal.alerts.updateStatus({ alerts: alertsToUpdate }); + await casesClientInternal.alerts.updateStatus({ alerts: alertsToUpdate }); } catch (error) { throw createCaseError({ message: `Failed to update alert status while updating sub cases: ${JSON.stringify( @@ -256,11 +256,15 @@ async function updateAlerts({ /** * Handles updating the fields in a sub case. */ -export async function update( - subCases: SubCasesPatchRequest, - clientArgs: CasesClientArgs, - casesClient: CasesClient -): Promise { +export async function update({ + subCases, + clientArgs, + casesClientInternal, +}: { + subCases: SubCasesPatchRequest; + clientArgs: CasesClientArgs; + casesClientInternal: CasesClientInternal; +}): Promise { const query = pipe( excess(SubCasesPatchRequestRt).decode(subCases), fold(throwErrors(Boom.badRequest), identity) @@ -349,7 +353,7 @@ export async function update( await updateAlerts({ caseService, soClient, - casesClient, + casesClientInternal, subCasesToSync: subCasesToSyncAlertsFor, logger: clientArgs.logger, }); diff --git a/x-pack/plugins/cases/server/client/types.ts b/x-pack/plugins/cases/server/client/types.ts index 7d50fdbb533826..5147cea0b59f0d 100644 --- a/x-pack/plugins/cases/server/client/types.ts +++ b/x-pack/plugins/cases/server/client/types.ts @@ -18,6 +18,7 @@ import { ConnectorMappingsService, AttachmentService, } from '../services'; +import { ActionsClient } from '../../../actions/server'; export interface CasesClientArgs { readonly scopedClusterClient: ElasticsearchClient; @@ -32,4 +33,5 @@ export interface CasesClientArgs { readonly logger: Logger; readonly authorization: PublicMethodsOf; readonly auditLogger?: AuditLogger; + readonly actionsClient: PublicMethodsOf; } 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 50d9270440e43b..8098714f8f9554 100644 --- a/x-pack/plugins/cases/server/client/user_actions/client.ts +++ b/x-pack/plugins/cases/server/client/user_actions/client.ts @@ -19,7 +19,7 @@ export interface UserActionsSubClient { } export const createUserActionsSubClient = (args: CasesClientArgs): UserActionsSubClient => { - const { savedObjectsClient, userActionService } = args; + const { savedObjectsClient, userActionService, logger } = args; const attachmentSubClient: UserActionsSubClient = { getAll: (params: UserActionGet) => @@ -27,6 +27,7 @@ export const createUserActionsSubClient = (args: CasesClientArgs): UserActionsSu ...params, savedObjectsClient, userActionService, + logger, }), }; 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 cebd3da1b6f7e0..4a8d1101d19cf2 100644 --- a/x-pack/plugins/cases/server/client/user_actions/get.ts +++ b/x-pack/plugins/cases/server/client/user_actions/get.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SavedObjectsClientContract } from 'kibana/server'; +import { SavedObjectsClientContract, Logger } from 'kibana/server'; import { SUB_CASE_SAVED_OBJECT, CASE_SAVED_OBJECT, @@ -13,12 +13,15 @@ import { } from '../../../common/constants'; import { CaseUserActionsResponseRt, CaseUserActionsResponse } from '../../../common/api'; import { CaseUserActionService } from '../../services'; +import { createCaseError } from '../../common/error'; +import { checkEnabledCaseConnectorOrThrow } from '../../common'; interface GetParams { savedObjectsClient: SavedObjectsClientContract; userActionService: CaseUserActionService; caseId: string; subCaseId?: string; + logger: Logger; } export const get = async ({ @@ -26,28 +29,39 @@ export const get = async ({ userActionService, caseId, subCaseId, + logger, }: GetParams): Promise => { - const userActions = await userActionService.getAll({ - soClient: savedObjectsClient, - caseId, - subCaseId, - }); + try { + checkEnabledCaseConnectorOrThrow(subCaseId); - return CaseUserActionsResponseRt.encode( - userActions.saved_objects.reduce((acc, ua) => { - if (subCaseId == null && ua.references.some((uar) => uar.type === SUB_CASE_SAVED_OBJECT)) { - return acc; - } - return [ - ...acc, - { - ...ua.attributes, - action_id: ua.id, - case_id: ua.references.find((r) => r.type === CASE_SAVED_OBJECT)?.id ?? '', - comment_id: ua.references.find((r) => r.type === CASE_COMMENT_SAVED_OBJECT)?.id ?? null, - sub_case_id: ua.references.find((r) => r.type === SUB_CASE_SAVED_OBJECT)?.id ?? '', - }, - ]; - }, []) - ); + const userActions = await userActionService.getAll({ + soClient: savedObjectsClient, + caseId, + subCaseId, + }); + + return CaseUserActionsResponseRt.encode( + userActions.saved_objects.reduce((acc, ua) => { + if (subCaseId == null && ua.references.some((uar) => uar.type === SUB_CASE_SAVED_OBJECT)) { + return acc; + } + return [ + ...acc, + { + ...ua.attributes, + action_id: ua.id, + case_id: ua.references.find((r) => r.type === CASE_SAVED_OBJECT)?.id ?? '', + comment_id: ua.references.find((r) => r.type === CASE_COMMENT_SAVED_OBJECT)?.id ?? null, + sub_case_id: ua.references.find((r) => r.type === SUB_CASE_SAVED_OBJECT)?.id ?? '', + }, + ]; + }, []) + ); + } catch (error) { + throw createCaseError({ + message: `Failed to retrieve user actions case id: ${caseId} sub case id: ${subCaseId}: ${error}`, + error, + logger, + }); + } }; diff --git a/x-pack/plugins/cases/server/client/utils.test.ts b/x-pack/plugins/cases/server/client/utils.test.ts new file mode 100644 index 00000000000000..c8ed1f4f0efa61 --- /dev/null +++ b/x-pack/plugins/cases/server/client/utils.test.ts @@ -0,0 +1,329 @@ +/* + * 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 { SavedObjectsFindResponse } from 'kibana/server'; +import { + CaseConnector, + CaseType, + ConnectorTypes, + ESCaseConnector, + ESCasesConfigureAttributes, +} from '../../common/api'; +import { mockCaseConfigure } from '../routes/api/__fixtures__'; +import { newCase } from '../routes/api/__mocks__/request_responses'; +import { + transformCaseConnectorToEsConnector, + transformESConnectorToCaseConnector, + transformNewCase, +} from '../common'; +import { getConnectorFromConfiguration, sortToSnake } from './utils'; + +describe('utils', () => { + const caseConnector: CaseConnector = { + id: '123', + name: 'Jira', + type: ConnectorTypes.jira, + fields: { issueType: 'Task', priority: 'High', parent: null }, + }; + + const esCaseConnector: ESCaseConnector = { + id: '123', + name: 'Jira', + type: ConnectorTypes.jira, + fields: [ + { key: 'issueType', value: 'Task' }, + { key: 'priority', value: 'High' }, + { key: 'parent', value: null }, + ], + }; + + const caseConfigure: SavedObjectsFindResponse = { + saved_objects: [{ ...mockCaseConfigure[0], score: 0 }], + total: 1, + per_page: 20, + page: 1, + }; + + describe('transformCaseConnectorToEsConnector', () => { + it('transform correctly', () => { + expect(transformCaseConnectorToEsConnector(caseConnector)).toEqual(esCaseConnector); + }); + + it('transform correctly with null attributes', () => { + // @ts-ignore this is case the connector does not exist for old cases object or configurations + expect(transformCaseConnectorToEsConnector(null)).toEqual({ + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: [], + }); + }); + }); + + describe('transformESConnectorToCaseConnector', () => { + it('transform correctly', () => { + expect(transformESConnectorToCaseConnector(esCaseConnector)).toEqual(caseConnector); + }); + + it('transform correctly with null attributes', () => { + // @ts-ignore this is case the connector does not exist for old cases object or configurations + expect(transformESConnectorToCaseConnector(null)).toEqual({ + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }); + }); + }); + + describe('getConnectorFromConfiguration', () => { + it('transform correctly', () => { + expect(getConnectorFromConfiguration(caseConfigure)).toEqual({ + id: '789', + name: 'My connector 3', + type: ConnectorTypes.jira, + fields: null, + }); + }); + + it('transform correctly with no connector', () => { + const caseConfigureNoConnector: SavedObjectsFindResponse = { + ...caseConfigure, + saved_objects: [ + { + ...mockCaseConfigure[0], + // @ts-ignore this is case the connector does not exist for old cases object or configurations + attributes: { ...mockCaseConfigure[0].attributes, connector: null }, + score: 0, + }, + ], + }; + + expect(getConnectorFromConfiguration(caseConfigureNoConnector)).toEqual({ + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }); + }); + }); + + describe('sortToSnake', () => { + it('it transforms status correctly', () => { + expect(sortToSnake('status')).toBe('status'); + }); + + it('it transforms createdAt correctly', () => { + expect(sortToSnake('createdAt')).toBe('created_at'); + }); + + it('it transforms created_at correctly', () => { + expect(sortToSnake('created_at')).toBe('created_at'); + }); + + it('it transforms closedAt correctly', () => { + expect(sortToSnake('closedAt')).toBe('closed_at'); + }); + + it('it transforms closed_at correctly', () => { + expect(sortToSnake('closed_at')).toBe('closed_at'); + }); + + it('it transforms default correctly', () => { + expect(sortToSnake('not-exist')).toBe('created_at'); + }); + }); + + describe('transformNewCase', () => { + const connector: ESCaseConnector = { + id: '123', + name: 'My connector', + type: ConnectorTypes.jira, + fields: [ + { key: 'issueType', value: 'Task' }, + { key: 'priority', value: 'High' }, + { key: 'parent', value: null }, + ], + }; + it('transform correctly', () => { + const myCase = { + newCase: { ...newCase, type: CaseType.individual }, + connector, + createdDate: '2020-04-09T09:43:51.778Z', + email: 'elastic@elastic.co', + full_name: 'Elastic', + username: 'elastic', + }; + + const res = transformNewCase(myCase); + + expect(res).toMatchInlineSnapshot(` + Object { + "closed_at": null, + "closed_by": null, + "connector": Object { + "fields": Array [ + Object { + "key": "issueType", + "value": "Task", + }, + Object { + "key": "priority", + "value": "High", + }, + Object { + "key": "parent", + "value": null, + }, + ], + "id": "123", + "name": "My connector", + "type": ".jira", + }, + "created_at": "2020-04-09T09:43:51.778Z", + "created_by": Object { + "email": "elastic@elastic.co", + "full_name": "Elastic", + "username": "elastic", + }, + "description": "A description", + "external_service": null, + "owner": "securitySolution", + "settings": Object { + "syncAlerts": true, + }, + "status": "open", + "tags": Array [ + "new", + "case", + ], + "title": "My new case", + "type": "individual", + "updated_at": null, + "updated_by": null, + } + `); + }); + + it('transform correctly without optional fields', () => { + const myCase = { + newCase: { ...newCase, type: CaseType.individual }, + connector, + createdDate: '2020-04-09T09:43:51.778Z', + }; + + const res = transformNewCase(myCase); + + expect(res).toMatchInlineSnapshot(` + Object { + "closed_at": null, + "closed_by": null, + "connector": Object { + "fields": Array [ + Object { + "key": "issueType", + "value": "Task", + }, + Object { + "key": "priority", + "value": "High", + }, + Object { + "key": "parent", + "value": null, + }, + ], + "id": "123", + "name": "My connector", + "type": ".jira", + }, + "created_at": "2020-04-09T09:43:51.778Z", + "created_by": Object { + "email": undefined, + "full_name": undefined, + "username": undefined, + }, + "description": "A description", + "external_service": null, + "owner": "securitySolution", + "settings": Object { + "syncAlerts": true, + }, + "status": "open", + "tags": Array [ + "new", + "case", + ], + "title": "My new case", + "type": "individual", + "updated_at": null, + "updated_by": null, + } + `); + }); + + it('transform correctly with optional fields as null', () => { + const myCase = { + newCase: { ...newCase, type: CaseType.individual }, + connector, + createdDate: '2020-04-09T09:43:51.778Z', + email: null, + full_name: null, + username: null, + }; + + const res = transformNewCase(myCase); + + expect(res).toMatchInlineSnapshot(` + Object { + "closed_at": null, + "closed_by": null, + "connector": Object { + "fields": Array [ + Object { + "key": "issueType", + "value": "Task", + }, + Object { + "key": "priority", + "value": "High", + }, + Object { + "key": "parent", + "value": null, + }, + ], + "id": "123", + "name": "My connector", + "type": ".jira", + }, + "created_at": "2020-04-09T09:43:51.778Z", + "created_by": Object { + "email": null, + "full_name": null, + "username": null, + }, + "description": "A description", + "external_service": null, + "owner": "securitySolution", + "settings": Object { + "syncAlerts": true, + }, + "status": "open", + "tags": Array [ + "new", + "case", + ], + "title": "My new case", + "type": "individual", + "updated_at": null, + "updated_by": null, + } + `); + }); + }); +}); diff --git a/x-pack/plugins/cases/server/routes/api/cases/helpers.ts b/x-pack/plugins/cases/server/client/utils.ts similarity index 75% rename from x-pack/plugins/cases/server/routes/api/cases/helpers.ts rename to x-pack/plugins/cases/server/client/utils.ts index f6570bb5c88cd8..c56e1178e96c8d 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/helpers.ts +++ b/x-pack/plugins/cases/server/client/utils.ts @@ -5,25 +5,95 @@ * 2.0. */ +import { badRequest } from '@hapi/boom'; import { get, isPlainObject } from 'lodash'; import deepEqual from 'fast-deep-equal'; +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; +import { pipe } from 'fp-ts/lib/pipeable'; import { SavedObjectsFindResponse } from 'kibana/server'; -import { nodeBuilder, KueryNode } from '../../../../../../../src/plugins/data/common'; +import { nodeBuilder, KueryNode } from '../../../../../src/plugins/data/common'; import { CaseConnector, - ESCaseConnector, ESCasesConfigureAttributes, - ConnectorTypeFields, ConnectorTypes, CaseStatuses, CaseType, - ESConnectorFields, -} from '../../../../common/api'; -import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../../common/constants'; -import { sortToSnake } from '../utils'; -import { combineFilterWithAuthorizationFilter } from '../../../authorization/utils'; -import { SavedObjectFindOptionsKueryNode } from '../../../common'; + CommentRequest, + throwErrors, + excess, + ContextTypeUserRt, + AlertCommentRequestRt, +} from '../../common/api'; +import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../common/constants'; +import { combineFilterWithAuthorizationFilter } from '../authorization/utils'; +import { + getIDsAndIndicesAsArrays, + isCommentRequestTypeAlertOrGenAlert, + isCommentRequestTypeUser, + SavedObjectFindOptionsKueryNode, +} from '../common'; + +export const decodeCommentRequest = (comment: CommentRequest) => { + if (isCommentRequestTypeUser(comment)) { + pipe(excess(ContextTypeUserRt).decode(comment), fold(throwErrors(badRequest), identity)); + } else if (isCommentRequestTypeAlertOrGenAlert(comment)) { + pipe(excess(AlertCommentRequestRt).decode(comment), fold(throwErrors(badRequest), identity)); + const { ids, indices } = getIDsAndIndicesAsArrays(comment); + + /** + * The alertId and index field must either be both of type string or they must both be string[] and be the same length. + * Having a one-to-one relationship between the id and index of an alert avoids accidentally updating or + * retrieving the wrong alert. Elasticsearch only guarantees that the _id (the field we use for alertId) to be + * unique within a single index. So if we attempt to update or get a specific alert across multiple indices we could + * update or receive the wrong one. + * + * Consider the situation where we have a alert1 with _id = '100' in index 'my-index-awesome' and also in index + * 'my-index-hi'. + * If we attempt to update the status of alert1 using an index pattern like `my-index-*` or even providing multiple + * indices, there's a chance we'll accidentally update too many alerts. + * + * This check doesn't enforce that the API request has the correct alert ID to index relationship it just guards + * against accidentally making a request like: + * { + * alertId: [1,2,3], + * index: awesome, + * } + * + * Instead this requires the requestor to provide: + * { + * alertId: [1,2,3], + * index: [awesome, awesome, awesome] + * } + * + * Ideally we'd change the format of the comment request to be an array of objects like: + * { + * alerts: [{id: 1, index: awesome}, {id: 2, index: awesome}] + * } + * + * But we'd need to also implement a migration because the saved object document currently stores the id and index + * in separate fields. + */ + if (ids.length !== indices.length) { + throw badRequest( + `Received an alert comment with ids and indices arrays of different lengths ids: ${JSON.stringify( + ids + )} indices: ${JSON.stringify(indices)}` + ); + } + } +}; + +/** + * Return the alert IDs from the comment if it is an alert style comment. Otherwise return an empty array. + */ +export const getAlertIds = (comment: CommentRequest): string[] => { + if (isCommentRequestTypeAlertOrGenAlert(comment)) { + return Array.isArray(comment.alertId) ? comment.alertId : [comment.alertId]; + } + return []; +}; export const addStatusFilter = ({ status, @@ -353,43 +423,23 @@ export const getConnectorFromConfiguration = ( return caseConnector; }; -export const transformCaseConnectorToEsConnector = (connector: CaseConnector): ESCaseConnector => ({ - id: connector?.id ?? 'none', - name: connector?.name ?? 'none', - type: connector?.type ?? '.none', - fields: - connector?.fields != null - ? Object.entries(connector.fields).reduce( - (acc, [key, value]) => [ - ...acc, - { - key, - value, - }, - ], - [] - ) - : [], -}); +enum SortFieldCase { + closedAt = 'closed_at', + createdAt = 'created_at', + status = 'status', +} -export const transformESConnectorToCaseConnector = (connector?: ESCaseConnector): CaseConnector => { - const connectorTypeField = { - type: connector?.type ?? '.none', - fields: - connector && connector.fields != null && connector.fields.length > 0 - ? connector.fields.reduce( - (fields, { key, value }) => ({ - ...fields, - [key]: value, - }), - {} - ) - : null, - } as ConnectorTypeFields; - - return { - id: connector?.id ?? 'none', - name: connector?.name ?? 'none', - ...connectorTypeField, - }; +export const sortToSnake = (sortField: string | undefined): SortFieldCase => { + switch (sortField) { + case 'status': + return SortFieldCase.status; + case 'createdAt': + case 'created_at': + return SortFieldCase.createdAt; + case 'closedAt': + case 'closed_at': + return SortFieldCase.closedAt; + default: + return SortFieldCase.createdAt; + } }; diff --git a/x-pack/plugins/cases/server/common/error.ts b/x-pack/plugins/cases/server/common/error.ts index 95b05fd612e602..1b53eb9fdb2181 100644 --- a/x-pack/plugins/cases/server/common/error.ts +++ b/x-pack/plugins/cases/server/common/error.ts @@ -28,7 +28,7 @@ class CaseError extends Error { * and data from that. */ public boomify(): Boom { - const message = this.message ?? this.wrappedError?.message; + const message = this.wrappedError?.message ?? this.message; let statusCode = 500; let data: unknown | undefined; diff --git a/x-pack/plugins/cases/server/common/index.ts b/x-pack/plugins/cases/server/common/index.ts index b07ed5d4ae2d62..324c7e7ffd1a8b 100644 --- a/x-pack/plugins/cases/server/common/index.ts +++ b/x-pack/plugins/cases/server/common/index.ts @@ -8,3 +8,4 @@ export * from './models'; export * from './utils'; export * from './types'; +export * from './error'; diff --git a/x-pack/plugins/cases/server/common/models/commentable_case.ts b/x-pack/plugins/cases/server/common/models/commentable_case.ts index fb34c5fecea394..d2276c0027ecee 100644 --- a/x-pack/plugins/cases/server/common/models/commentable_case.ts +++ b/x-pack/plugins/cases/server/common/models/commentable_case.ts @@ -28,12 +28,12 @@ import { SubCaseAttributes, User, } from '../../../common/api'; -import { transformESConnectorToCaseConnector } from '../../routes/api/cases/helpers'; import { + transformESConnectorToCaseConnector, flattenCommentSavedObjects, flattenSubCaseSavedObject, transformNewComment, -} from '../../routes/api/utils'; +} from '..'; import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../common/constants'; import { AttachmentService, CaseService } from '../../services'; import { createCaseError } from '../error'; diff --git a/x-pack/plugins/cases/server/common/utils.test.ts b/x-pack/plugins/cases/server/common/utils.test.ts index 46e73c8b5d79cf..e7dcbf0111f55e 100644 --- a/x-pack/plugins/cases/server/common/utils.test.ts +++ b/x-pack/plugins/cases/server/common/utils.test.ts @@ -6,9 +6,29 @@ */ import { SavedObjectsFindResponse } from 'kibana/server'; -import { AssociationType, CommentAttributes, CommentRequest, CommentType } from '../../common/api'; -import { transformNewComment } from '../routes/api/utils'; -import { countAlerts, countAlertsForID, groupTotalAlertsByID } from './utils'; +import { + AssociationType, + CaseResponse, + CommentAttributes, + CommentRequest, + CommentType, +} from '../../common/api'; +import { + mockCaseComments, + mockCases, + mockCaseNoConnectorId, +} from '../routes/api/__fixtures__/mock_saved_objects'; +import { + flattenCaseSavedObject, + transformNewComment, + countAlerts, + countAlertsForID, + groupTotalAlertsByID, + transformCases, + transformComments, + flattenCommentSavedObjects, + flattenCommentSavedObject, +} from './utils'; interface CommentReference { ids: string[]; @@ -47,6 +67,609 @@ function createCommentFindResponse( } describe('common utils', () => { + describe('transformCases', () => { + it('transforms correctly', () => { + const casesMap = new Map( + mockCases.map((obj) => { + return [obj.id, flattenCaseSavedObject({ savedObject: obj, totalComment: 2 })]; + }) + ); + const res = transformCases({ + casesMap, + countOpenCases: 2, + countInProgressCases: 2, + countClosedCases: 2, + page: 1, + perPage: 10, + total: casesMap.size, + }); + expect(res).toMatchInlineSnapshot(` + Object { + "cases": Array [ + Object { + "closed_at": null, + "closed_by": null, + "comments": Array [], + "connector": Object { + "fields": null, + "id": "none", + "name": "none", + "type": ".none", + }, + "created_at": "2019-11-25T21:54:48.952Z", + "created_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + "description": "This is a brand new case of a bad meanie defacing data", + "external_service": null, + "id": "mock-id-1", + "owner": "securitySolution", + "settings": Object { + "syncAlerts": true, + }, + "status": "open", + "subCaseIds": undefined, + "subCases": undefined, + "tags": Array [ + "defacement", + ], + "title": "Super Bad Security Issue", + "totalAlerts": 0, + "totalComment": 2, + "type": "individual", + "updated_at": "2019-11-25T21:54:48.952Z", + "updated_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + "version": "WzAsMV0=", + }, + Object { + "closed_at": null, + "closed_by": null, + "comments": Array [], + "connector": Object { + "fields": null, + "id": "none", + "name": "none", + "type": ".none", + }, + "created_at": "2019-11-25T22:32:00.900Z", + "created_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + "description": "Oh no, a bad meanie destroying data!", + "external_service": null, + "id": "mock-id-2", + "owner": "securitySolution", + "settings": Object { + "syncAlerts": true, + }, + "status": "open", + "subCaseIds": undefined, + "subCases": undefined, + "tags": Array [ + "Data Destruction", + ], + "title": "Damaging Data Destruction Detected", + "totalAlerts": 0, + "totalComment": 2, + "type": "individual", + "updated_at": "2019-11-25T22:32:00.900Z", + "updated_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + "version": "WzQsMV0=", + }, + Object { + "closed_at": null, + "closed_by": null, + "comments": Array [], + "connector": Object { + "fields": Object { + "issueType": "Task", + "parent": null, + "priority": "High", + }, + "id": "123", + "name": "My connector", + "type": ".jira", + }, + "created_at": "2019-11-25T22:32:17.947Z", + "created_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + "description": "Oh no, a bad meanie going LOLBins all over the place!", + "external_service": null, + "id": "mock-id-3", + "owner": "securitySolution", + "settings": Object { + "syncAlerts": true, + }, + "status": "open", + "subCaseIds": undefined, + "subCases": undefined, + "tags": Array [ + "LOLBins", + ], + "title": "Another bad one", + "totalAlerts": 0, + "totalComment": 2, + "type": "individual", + "updated_at": "2019-11-25T22:32:17.947Z", + "updated_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + "version": "WzUsMV0=", + }, + Object { + "closed_at": "2019-11-25T22:32:17.947Z", + "closed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + "comments": Array [], + "connector": Object { + "fields": Object { + "issueType": "Task", + "parent": null, + "priority": "High", + }, + "id": "123", + "name": "My connector", + "type": ".jira", + }, + "created_at": "2019-11-25T22:32:17.947Z", + "created_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + "description": "Oh no, a bad meanie going LOLBins all over the place!", + "external_service": null, + "id": "mock-id-4", + "owner": "securitySolution", + "settings": Object { + "syncAlerts": true, + }, + "status": "closed", + "subCaseIds": undefined, + "subCases": undefined, + "tags": Array [ + "LOLBins", + ], + "title": "Another bad one", + "totalAlerts": 0, + "totalComment": 2, + "type": "individual", + "updated_at": "2019-11-25T22:32:17.947Z", + "updated_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + "version": "WzUsMV0=", + }, + ], + "count_closed_cases": 2, + "count_in_progress_cases": 2, + "count_open_cases": 2, + "page": 1, + "per_page": 10, + "total": 4, + } + `); + }); + }); + + describe('flattenCaseSavedObject', () => { + it('flattens correctly', () => { + const myCase = { ...mockCases[2] }; + const res = flattenCaseSavedObject({ + savedObject: myCase, + totalComment: 2, + }); + + expect(res).toMatchInlineSnapshot(` + Object { + "closed_at": null, + "closed_by": null, + "comments": Array [], + "connector": Object { + "fields": Object { + "issueType": "Task", + "parent": null, + "priority": "High", + }, + "id": "123", + "name": "My connector", + "type": ".jira", + }, + "created_at": "2019-11-25T22:32:17.947Z", + "created_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + "description": "Oh no, a bad meanie going LOLBins all over the place!", + "external_service": null, + "id": "mock-id-3", + "owner": "securitySolution", + "settings": Object { + "syncAlerts": true, + }, + "status": "open", + "subCaseIds": undefined, + "subCases": undefined, + "tags": Array [ + "LOLBins", + ], + "title": "Another bad one", + "totalAlerts": 0, + "totalComment": 2, + "type": "individual", + "updated_at": "2019-11-25T22:32:17.947Z", + "updated_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + "version": "WzUsMV0=", + } + `); + }); + + it('flattens correctly without version', () => { + const myCase = { ...mockCases[2] }; + myCase.version = undefined; + const res = flattenCaseSavedObject({ + savedObject: myCase, + totalComment: 2, + }); + + expect(res).toMatchInlineSnapshot(` + Object { + "closed_at": null, + "closed_by": null, + "comments": Array [], + "connector": Object { + "fields": Object { + "issueType": "Task", + "parent": null, + "priority": "High", + }, + "id": "123", + "name": "My connector", + "type": ".jira", + }, + "created_at": "2019-11-25T22:32:17.947Z", + "created_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + "description": "Oh no, a bad meanie going LOLBins all over the place!", + "external_service": null, + "id": "mock-id-3", + "owner": "securitySolution", + "settings": Object { + "syncAlerts": true, + }, + "status": "open", + "subCaseIds": undefined, + "subCases": undefined, + "tags": Array [ + "LOLBins", + ], + "title": "Another bad one", + "totalAlerts": 0, + "totalComment": 2, + "type": "individual", + "updated_at": "2019-11-25T22:32:17.947Z", + "updated_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + "version": "0", + } + `); + }); + + it('flattens correctly with comments', () => { + const myCase = { ...mockCases[2] }; + const comments = [{ ...mockCaseComments[0] }]; + const res = flattenCaseSavedObject({ + savedObject: myCase, + comments, + totalComment: 2, + }); + + expect(res).toMatchInlineSnapshot(` + Object { + "closed_at": null, + "closed_by": null, + "comments": Array [ + Object { + "associationType": "case", + "comment": "Wow, good luck catching that bad meanie!", + "created_at": "2019-11-25T21:55:00.177Z", + "created_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + "id": "mock-comment-1", + "pushed_at": null, + "pushed_by": null, + "type": "user", + "updated_at": "2019-11-25T21:55:00.177Z", + "updated_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + "version": "WzEsMV0=", + }, + ], + "connector": Object { + "fields": Object { + "issueType": "Task", + "parent": null, + "priority": "High", + }, + "id": "123", + "name": "My connector", + "type": ".jira", + }, + "created_at": "2019-11-25T22:32:17.947Z", + "created_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + "description": "Oh no, a bad meanie going LOLBins all over the place!", + "external_service": null, + "id": "mock-id-3", + "owner": "securitySolution", + "settings": Object { + "syncAlerts": true, + }, + "status": "open", + "subCaseIds": undefined, + "subCases": undefined, + "tags": Array [ + "LOLBins", + ], + "title": "Another bad one", + "totalAlerts": 0, + "totalComment": 2, + "type": "individual", + "updated_at": "2019-11-25T22:32:17.947Z", + "updated_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + "version": "WzUsMV0=", + } + `); + }); + + it('inserts missing connector', () => { + const extraCaseData = { + totalComment: 2, + }; + + const res = flattenCaseSavedObject({ + // @ts-ignore this is to update old case saved objects to include connector + savedObject: mockCaseNoConnectorId, + ...extraCaseData, + }); + + expect(res).toMatchInlineSnapshot(` + Object { + "closed_at": null, + "closed_by": null, + "comments": Array [], + "connector": Object { + "fields": null, + "id": "none", + "name": "none", + "type": ".none", + }, + "created_at": "2019-11-25T21:54:48.952Z", + "created_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + "description": "This is a brand new case of a bad meanie defacing data", + "external_service": null, + "id": "mock-no-connector_id", + "settings": Object { + "syncAlerts": true, + }, + "status": "open", + "subCaseIds": undefined, + "subCases": undefined, + "tags": Array [ + "defacement", + ], + "title": "Super Bad Security Issue", + "totalAlerts": 0, + "totalComment": 2, + "updated_at": "2019-11-25T21:54:48.952Z", + "updated_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + "version": "WzAsMV0=", + } + `); + }); + }); + + describe('transformComments', () => { + it('transforms correctly', () => { + const comments = { + saved_objects: mockCaseComments.map((obj) => ({ ...obj, score: 1 })), + total: mockCaseComments.length, + per_page: 10, + page: 1, + }; + + const res = transformComments(comments); + expect(res).toEqual({ + page: 1, + per_page: 10, + total: mockCaseComments.length, + comments: flattenCommentSavedObjects(comments.saved_objects), + }); + }); + }); + + describe('flattenCommentSavedObjects', () => { + it('flattens correctly', () => { + const comments = [{ ...mockCaseComments[0] }, { ...mockCaseComments[1] }]; + const res = flattenCommentSavedObjects(comments); + expect(res).toEqual([ + flattenCommentSavedObject(comments[0]), + flattenCommentSavedObject(comments[1]), + ]); + }); + }); + + describe('flattenCommentSavedObject', () => { + it('flattens correctly', () => { + const comment = { ...mockCaseComments[0] }; + const res = flattenCommentSavedObject(comment); + expect(res).toEqual({ + id: comment.id, + version: comment.version, + ...comment.attributes, + }); + }); + + it('flattens correctly without version', () => { + const comment = { ...mockCaseComments[0] }; + comment.version = undefined; + const res = flattenCommentSavedObject(comment); + expect(res).toEqual({ + id: comment.id, + version: '0', + ...comment.attributes, + }); + }); + }); + + describe('transformNewComment', () => { + it('transforms correctly', () => { + const comment = { + comment: 'A comment', + type: CommentType.user as const, + createdDate: '2020-04-09T09:43:51.778Z', + email: 'elastic@elastic.co', + full_name: 'Elastic', + username: 'elastic', + associationType: AssociationType.case, + }; + + const res = transformNewComment(comment); + expect(res).toMatchInlineSnapshot(` + Object { + "associationType": "case", + "comment": "A comment", + "created_at": "2020-04-09T09:43:51.778Z", + "created_by": Object { + "email": "elastic@elastic.co", + "full_name": "Elastic", + "username": "elastic", + }, + "pushed_at": null, + "pushed_by": null, + "type": "user", + "updated_at": null, + "updated_by": null, + } + `); + }); + + it('transform correctly without optional fields', () => { + const comment = { + comment: 'A comment', + type: CommentType.user as const, + createdDate: '2020-04-09T09:43:51.778Z', + associationType: AssociationType.case, + }; + + const res = transformNewComment(comment); + + expect(res).toMatchInlineSnapshot(` + Object { + "associationType": "case", + "comment": "A comment", + "created_at": "2020-04-09T09:43:51.778Z", + "created_by": Object { + "email": undefined, + "full_name": undefined, + "username": undefined, + }, + "pushed_at": null, + "pushed_by": null, + "type": "user", + "updated_at": null, + "updated_by": null, + } + `); + }); + + it('transform correctly with optional fields as null', () => { + const comment = { + comment: 'A comment', + type: CommentType.user as const, + createdDate: '2020-04-09T09:43:51.778Z', + email: null, + full_name: null, + username: null, + associationType: AssociationType.case, + }; + + const res = transformNewComment(comment); + + expect(res).toMatchInlineSnapshot(` + Object { + "associationType": "case", + "comment": "A comment", + "created_at": "2020-04-09T09:43:51.778Z", + "created_by": Object { + "email": null, + "full_name": null, + "username": null, + }, + "pushed_at": null, + "pushed_by": null, + "type": "user", + "updated_at": null, + "updated_by": null, + } + `); + }); + }); + describe('countAlerts', () => { it('returns 0 when no alerts are found', () => { expect( diff --git a/x-pack/plugins/cases/server/common/utils.ts b/x-pack/plugins/cases/server/common/utils.ts index af638c39d66093..def25b8c7acec6 100644 --- a/x-pack/plugins/cases/server/common/utils.ts +++ b/x-pack/plugins/cases/server/common/utils.ts @@ -4,19 +4,38 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import Boom from '@hapi/boom'; -import { SavedObjectsFindResult, SavedObjectsFindResponse } from 'kibana/server'; +import { SavedObjectsFindResult, SavedObjectsFindResponse, SavedObject } from 'kibana/server'; +import { isEmpty } from 'lodash'; +import { AlertInfo } from '.'; import { AuditEvent, EventCategory, EventOutcome } from '../../../security/server'; import { + AssociationType, + CaseConnector, + CaseResponse, + CasesClientPostRequest, + CasesFindResponse, CaseStatuses, CommentAttributes, CommentRequest, + CommentRequestAlertType, + CommentRequestUserType, + CommentResponse, + CommentsResponse, CommentType, + ConnectorTypeFields, + ESCaseAttributes, + ESCaseConnector, + ESConnectorFields, + SubCaseAttributes, + SubCaseResponse, + SubCasesFindResponse, User, } from '../../common/api'; +import { ENABLE_CASE_CONNECTOR } from '../../common/constants'; import { OperationDetails } from '../authorization'; import { UpdateAlertRequest } from '../client/alerts/client'; -import { getAlertInfoFromComments } from '../routes/api/utils'; /** * Default sort field for querying saved objects. @@ -28,6 +47,303 @@ export const defaultSortField = 'created_at'; */ export const nullUser: User = { username: null, full_name: null, email: null }; +export const transformNewCase = ({ + connector, + createdDate, + email, + // eslint-disable-next-line @typescript-eslint/naming-convention + full_name, + newCase, + username, +}: { + connector: ESCaseConnector; + createdDate: string; + email?: string | null; + full_name?: string | null; + newCase: CasesClientPostRequest; + username?: string | null; +}): ESCaseAttributes => ({ + ...newCase, + closed_at: null, + closed_by: null, + connector, + created_at: createdDate, + created_by: { email, full_name, username }, + external_service: null, + status: CaseStatuses.open, + updated_at: null, + updated_by: null, +}); + +export const transformCases = ({ + casesMap, + countOpenCases, + countInProgressCases, + countClosedCases, + page, + perPage, + total, +}: { + casesMap: Map; + countOpenCases: number; + countInProgressCases: number; + countClosedCases: number; + page: number; + perPage: number; + total: number; +}): CasesFindResponse => ({ + page, + per_page: perPage, + total, + cases: Array.from(casesMap.values()), + count_open_cases: countOpenCases, + count_in_progress_cases: countInProgressCases, + count_closed_cases: countClosedCases, +}); + +export const transformSubCases = ({ + subCasesMap, + open, + inProgress, + closed, + page, + perPage, + total, +}: { + subCasesMap: Map; + open: number; + inProgress: number; + closed: number; + page: number; + perPage: number; + total: number; +}): SubCasesFindResponse => ({ + page, + per_page: perPage, + total, + // Squish all the entries in the map together as one array + subCases: Array.from(subCasesMap.values()).flat(), + count_open_cases: open, + count_in_progress_cases: inProgress, + count_closed_cases: closed, +}); + +export const flattenCaseSavedObject = ({ + savedObject, + comments = [], + totalComment = comments.length, + totalAlerts = 0, + subCases, + subCaseIds, +}: { + savedObject: SavedObject; + comments?: Array>; + totalComment?: number; + totalAlerts?: number; + subCases?: SubCaseResponse[]; + subCaseIds?: string[]; +}): CaseResponse => ({ + id: savedObject.id, + version: savedObject.version ?? '0', + comments: flattenCommentSavedObjects(comments), + totalComment, + totalAlerts, + ...savedObject.attributes, + connector: transformESConnectorToCaseConnector(savedObject.attributes.connector), + subCases, + subCaseIds: !isEmpty(subCaseIds) ? subCaseIds : undefined, +}); + +export const flattenSubCaseSavedObject = ({ + savedObject, + comments = [], + totalComment = comments.length, + totalAlerts = 0, +}: { + savedObject: SavedObject; + comments?: Array>; + totalComment?: number; + totalAlerts?: number; +}): SubCaseResponse => ({ + id: savedObject.id, + version: savedObject.version ?? '0', + comments: flattenCommentSavedObjects(comments), + totalComment, + totalAlerts, + ...savedObject.attributes, +}); + +export const transformComments = ( + comments: SavedObjectsFindResponse +): CommentsResponse => ({ + page: comments.page, + per_page: comments.per_page, + total: comments.total, + comments: flattenCommentSavedObjects(comments.saved_objects), +}); + +export const flattenCommentSavedObjects = ( + savedObjects: Array> +): CommentResponse[] => + savedObjects.reduce((acc: CommentResponse[], savedObject: SavedObject) => { + return [...acc, flattenCommentSavedObject(savedObject)]; + }, []); + +export const flattenCommentSavedObject = ( + savedObject: SavedObject +): CommentResponse => ({ + id: savedObject.id, + version: savedObject.version ?? '0', + ...savedObject.attributes, +}); + +export const transformCaseConnectorToEsConnector = (connector: CaseConnector): ESCaseConnector => ({ + id: connector?.id ?? 'none', + name: connector?.name ?? 'none', + type: connector?.type ?? '.none', + fields: + connector?.fields != null + ? Object.entries(connector.fields).reduce( + (acc, [key, value]) => [ + ...acc, + { + key, + value, + }, + ], + [] + ) + : [], +}); + +export const transformESConnectorToCaseConnector = (connector?: ESCaseConnector): CaseConnector => { + const connectorTypeField = { + type: connector?.type ?? '.none', + fields: + connector && connector.fields != null && connector.fields.length > 0 + ? connector.fields.reduce( + (fields, { key, value }) => ({ + ...fields, + [key]: value, + }), + {} + ) + : null, + } as ConnectorTypeFields; + + return { + id: connector?.id ?? 'none', + name: connector?.name ?? 'none', + ...connectorTypeField, + }; +}; + +export const getIDsAndIndicesAsArrays = ( + comment: CommentRequestAlertType +): { ids: string[]; indices: string[] } => { + return { + ids: Array.isArray(comment.alertId) ? comment.alertId : [comment.alertId], + indices: Array.isArray(comment.index) ? comment.index : [comment.index], + }; +}; + +/** + * This functions extracts the ids and indices from an alert comment. It enforces that the alertId and index are either + * both strings or string arrays that are the same length. If they are arrays they represent a 1-to-1 mapping of + * id existing in an index at each position in the array. This is not ideal. Ideally an alert comment request would + * accept an array of objects like this: Array<{id: string; index: string; ruleName: string ruleID: string}> instead. + * + * To reformat the alert comment request requires a migration and a breaking API change. + */ +const getAndValidateAlertInfoFromComment = (comment: CommentRequest): AlertInfo[] => { + if (!isCommentRequestTypeAlertOrGenAlert(comment)) { + return []; + } + + const { ids, indices } = getIDsAndIndicesAsArrays(comment); + + if (ids.length !== indices.length) { + return []; + } + + return ids.map((id, index) => ({ id, index: indices[index] })); +}; + +/** + * Builds an AlertInfo object accumulating the alert IDs and indices for the passed in alerts. + */ +export const getAlertInfoFromComments = (comments: CommentRequest[] | undefined): AlertInfo[] => { + if (comments === undefined) { + return []; + } + + return comments.reduce((acc: AlertInfo[], comment) => { + const alertInfo = getAndValidateAlertInfoFromComment(comment); + acc.push(...alertInfo); + return acc; + }, []); +}; + +type NewCommentArgs = CommentRequest & { + associationType: AssociationType; + createdDate: string; + email?: string | null; + full_name?: string | null; + username?: string | null; +}; + +export const transformNewComment = ({ + associationType, + createdDate, + email, + // eslint-disable-next-line @typescript-eslint/naming-convention + full_name, + username, + ...comment +}: NewCommentArgs): CommentAttributes => { + return { + associationType, + ...comment, + created_at: createdDate, + created_by: { email, full_name, username }, + pushed_at: null, + pushed_by: null, + updated_at: null, + updated_by: null, + }; +}; + +/** + * A type narrowing function for user comments. Exporting so integration tests can use it. + */ +export const isCommentRequestTypeUser = ( + context: CommentRequest +): context is CommentRequestUserType => { + return context.type === CommentType.user; +}; + +/** + * A type narrowing function for alert comments. Exporting so integration tests can use it. + */ +export const isCommentRequestTypeAlertOrGenAlert = ( + context: CommentRequest +): context is CommentRequestAlertType => { + return context.type === CommentType.alert || context.type === CommentType.generatedAlert; +}; + +/** + * This is used to test if the posted comment is an generated alert. A generated alert will have one or many alerts. + * An alert is essentially an object with a _id field. This differs from a regular attached alert because the _id is + * passed directly in the request, it won't be in an object. Internally case will strip off the outer object and store + * both a generated and user attached alert in the same structure but this function is useful to determine which + * structure the new alert in the request has. + */ +export const isCommentRequestTypeGenAlert = ( + context: CommentRequest +): context is CommentRequestAlertType => { + return context.type === CommentType.generatedAlert; +}; + /** * Adds the ids and indices to a map of statuses */ @@ -145,3 +461,14 @@ export function createAuditMsg({ }), }; } + +/** + * If subCaseID is defined and the case connector feature is disabled this throws an error. + */ +export function checkEnabledCaseConnectorOrThrow(subCaseID: string | undefined) { + if (!ENABLE_CASE_CONNECTOR && subCaseID !== undefined) { + throw Boom.badRequest( + 'The sub case parameters are not supported when the case connector feature is disabled' + ); + } +} diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index 8a504ce73dee8b..4493e04f307c49 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -9,7 +9,10 @@ import { IContextProvider, KibanaRequest, Logger, PluginInitializerContext } fro import { CoreSetup, CoreStart } from 'src/core/server'; import { SecurityPluginSetup, SecurityPluginStart } from '../../security/server'; -import { PluginSetupContract as ActionsPluginSetup } from '../../actions/server'; +import { + PluginSetupContract as ActionsPluginSetup, + PluginStartContract as ActionsPluginStart, +} from '../../actions/server'; import { APP_ID, ENABLE_CASE_CONNECTOR } from '../common/constants'; import { ConfigType } from './config'; @@ -50,6 +53,7 @@ export interface PluginsStart { security?: SecurityPluginStart; features: FeaturesPluginStart; spaces?: SpacesPluginStart; + actions: ActionsPluginStart; } export class CasePlugin { @@ -143,6 +147,7 @@ export class CasePlugin { return plugins.spaces?.spacesService.getActiveSpace(request); }, featuresPluginStart: plugins.features, + actionsPluginStart: plugins.actions, }); const getCasesClientWithRequestAndContext = async ( diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/delete_all_comments.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/delete_all_comments.ts index 4439b215599a9c..08c4491f7b1518 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/delete_all_comments.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/delete_all_comments.ts @@ -5,25 +5,12 @@ * 2.0. */ -import Boom from '@hapi/boom'; import { schema } from '@kbn/config-schema'; -import { buildCommentUserActionItem } from '../../../../services/user_actions/helpers'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; -import { - CASE_COMMENTS_URL, - ENABLE_CASE_CONNECTOR, - SAVED_OBJECT_TYPES, -} from '../../../../../common/constants'; -import { AssociationType } from '../../../../../common/api'; +import { CASE_COMMENTS_URL } from '../../../../../common/constants'; -export function initDeleteAllCommentsApi({ - attachmentService, - caseService, - router, - userActionService, - logger, -}: RouteDeps) { +export function initDeleteAllCommentsApi({ router, logger }: RouteDeps) { router.delete( { path: CASE_COMMENTS_URL, @@ -40,49 +27,11 @@ export function initDeleteAllCommentsApi({ }, async (context, request, response) => { try { - if (!ENABLE_CASE_CONNECTOR && request.query?.subCaseId !== undefined) { - throw Boom.badRequest( - 'The `subCaseId` is not supported when the case connector feature is disabled' - ); - } + const client = await context.cases.getCasesClient(); - const soClient = context.core.savedObjects.getClient({ - includedHiddenTypes: SAVED_OBJECT_TYPES, - }); - // eslint-disable-next-line @typescript-eslint/naming-convention - const { username, full_name, email } = await caseService.getUser({ request }); - const deleteDate = new Date().toISOString(); - - const subCaseId = request.query?.subCaseId; - const id = subCaseId ?? request.params.case_id; - const comments = await caseService.getCommentsByAssociation({ - soClient, - id, - associationType: subCaseId ? AssociationType.subCase : AssociationType.case, - }); - - await Promise.all( - comments.saved_objects.map((comment) => - attachmentService.delete({ - soClient, - attachmentId: comment.id, - }) - ) - ); - - await userActionService.bulkCreate({ - soClient, - actions: comments.saved_objects.map((comment) => - buildCommentUserActionItem({ - action: 'delete', - actionAt: deleteDate, - actionBy: { username, full_name, email }, - caseId: request.params.case_id, - subCaseId, - commentId: comment.id, - fields: ['comment'], - }) - ), + await client.attachments.deleteAll({ + caseID: request.params.case_id, + subCaseID: request.query?.subCaseId, }); return response.noContent(); diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.ts index da4064f64be77f..284013ff36c095 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.ts @@ -5,27 +5,13 @@ * 2.0. */ -import Boom from '@hapi/boom'; import { schema } from '@kbn/config-schema'; -import { buildCommentUserActionItem } from '../../../../services/user_actions/helpers'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; -import { - CASE_COMMENT_DETAILS_URL, - SAVED_OBJECT_TYPES, - CASE_SAVED_OBJECT, - SUB_CASE_SAVED_OBJECT, - ENABLE_CASE_CONNECTOR, -} from '../../../../../common/constants'; +import { CASE_COMMENT_DETAILS_URL } from '../../../../../common/constants'; -export function initDeleteCommentApi({ - attachmentService, - caseService, - router, - userActionService, - logger, -}: RouteDeps) { +export function initDeleteCommentApi({ router, logger }: RouteDeps) { router.delete( { path: CASE_COMMENT_DETAILS_URL, @@ -43,54 +29,11 @@ export function initDeleteCommentApi({ }, async (context, request, response) => { try { - if (!ENABLE_CASE_CONNECTOR && request.query?.subCaseId !== undefined) { - throw Boom.badRequest( - 'The `subCaseId` is not supported when the case connector feature is disabled' - ); - } - - const soClient = context.core.savedObjects.getClient({ - includedHiddenTypes: SAVED_OBJECT_TYPES, - }); - // eslint-disable-next-line @typescript-eslint/naming-convention - const { username, full_name, email } = caseService.getUser({ request }); - const deleteDate = new Date().toISOString(); - - const myComment = await attachmentService.get({ - soClient, - attachmentId: request.params.comment_id, - }); - - if (myComment == null) { - throw Boom.notFound(`This comment ${request.params.comment_id} does not exist anymore.`); - } - - const type = request.query?.subCaseId ? SUB_CASE_SAVED_OBJECT : CASE_SAVED_OBJECT; - const id = request.query?.subCaseId ?? request.params.case_id; - - const caseRef = myComment.references.find((c) => c.type === type); - if (caseRef == null || (caseRef != null && caseRef.id !== id)) { - throw Boom.notFound(`This comment ${request.params.comment_id} does not exist in ${id}.`); - } - - await attachmentService.delete({ - soClient, - attachmentId: request.params.comment_id, - }); - - await userActionService.bulkCreate({ - soClient, - actions: [ - buildCommentUserActionItem({ - action: 'delete', - actionAt: deleteDate, - actionBy: { username, full_name, email }, - caseId: id, - subCaseId: request.query?.subCaseId, - commentId: request.params.comment_id, - fields: ['comment'], - }), - ], + const client = await context.cases.getCasesClient(); + await client.attachments.delete({ + attachmentID: request.params.comment_id, + subCaseID: request.query?.subCaseId, + caseID: request.params.case_id, }); return response.noContent(); diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/find_comments.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/find_comments.ts index 988d0324ec02a5..b7b8a3b44146f7 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/find_comments.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/find_comments.ts @@ -14,28 +14,17 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { esKuery } from '../../../../../../../../src/plugins/data/server'; -import { - AssociationType, - CommentsResponseRt, - SavedObjectFindOptionsRt, - throwErrors, -} from '../../../../../common/api'; +import { SavedObjectFindOptionsRt, throwErrors } from '../../../../../common/api'; import { RouteDeps } from '../../types'; -import { escapeHatch, transformComments, wrapError } from '../../utils'; -import { - CASE_COMMENTS_URL, - SAVED_OBJECT_TYPES, - ENABLE_CASE_CONNECTOR, -} from '../../../../../common/constants'; -import { defaultPage, defaultPerPage } from '../..'; +import { escapeHatch, wrapError } from '../../utils'; +import { CASE_COMMENTS_URL } from '../../../../../common/constants'; const FindQueryParamsRt = rt.partial({ ...SavedObjectFindOptionsRt.props, subCaseId: rt.string, }); -export function initFindCaseCommentsApi({ caseService, router, logger }: RouteDeps) { +export function initFindCaseCommentsApi({ router, logger }: RouteDeps) { router.get( { path: `${CASE_COMMENTS_URL}/_find`, @@ -48,54 +37,18 @@ export function initFindCaseCommentsApi({ caseService, router, logger }: RouteDe }, async (context, request, response) => { try { - const soClient = context.core.savedObjects.getClient({ - includedHiddenTypes: SAVED_OBJECT_TYPES, - }); const query = pipe( FindQueryParamsRt.decode(request.query), fold(throwErrors(Boom.badRequest), identity) ); - if (!ENABLE_CASE_CONNECTOR && query.subCaseId !== undefined) { - throw Boom.badRequest( - 'The `subCaseId` is not supported when the case connector feature is disabled' - ); - } - - const id = query.subCaseId ?? request.params.case_id; - const associationType = query.subCaseId ? AssociationType.subCase : AssociationType.case; - const { filter, ...queryWithoutFilter } = query; - const args = query - ? { - caseService, - soClient, - id, - options: { - // We need this because the default behavior of getAllCaseComments is to return all the comments - // unless the page and/or perPage is specified. Since we're spreading the query after the request can - // still override this behavior. - page: defaultPage, - perPage: defaultPerPage, - sortField: 'created_at', - filter: filter != null ? esKuery.fromKueryExpression(filter) : filter, - ...queryWithoutFilter, - }, - associationType, - } - : { - caseService, - soClient, - id, - options: { - page: defaultPage, - perPage: defaultPerPage, - sortField: 'created_at', - }, - associationType, - }; - - const theComments = await caseService.getCommentsByAssociation(args); - return response.ok({ body: CommentsResponseRt.encode(transformComments(theComments)) }); + const client = await context.cases.getCasesClient(); + return response.ok({ + body: await client.attachments.find({ + caseID: request.params.case_id, + queryParams: query, + }), + }); } catch (error) { logger.error( `Failed to find comments in route case id: ${request.params.case_id}: ${error}` diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts index af87cbccb3bf34..7777a0b36a1f11 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts @@ -5,21 +5,13 @@ * 2.0. */ -import Boom from '@hapi/boom'; import { schema } from '@kbn/config-schema'; -import { SavedObjectsFindResponse } from 'kibana/server'; -import { AllCommentsResponseRt, CommentAttributes } from '../../../../../common/api'; import { RouteDeps } from '../../types'; -import { flattenCommentSavedObjects, wrapError } from '../../utils'; -import { - CASE_COMMENTS_URL, - SAVED_OBJECT_TYPES, - ENABLE_CASE_CONNECTOR, -} from '../../../../../common/constants'; -import { defaultSortField } from '../../../../common'; +import { wrapError } from '../../utils'; +import { CASE_COMMENTS_URL } from '../../../../../common/constants'; -export function initGetAllCommentsApi({ caseService, router, logger }: RouteDeps) { +export function initGetAllCommentsApi({ router, logger }: RouteDeps) { router.get( { path: CASE_COMMENTS_URL, @@ -37,42 +29,14 @@ export function initGetAllCommentsApi({ caseService, router, logger }: RouteDeps }, async (context, request, response) => { try { - const soClient = context.core.savedObjects.getClient({ - includedHiddenTypes: SAVED_OBJECT_TYPES, - }); - let comments: SavedObjectsFindResponse; - - if ( - !ENABLE_CASE_CONNECTOR && - (request.query?.subCaseId !== undefined || - request.query?.includeSubCaseComments !== undefined) - ) { - throw Boom.badRequest( - 'The `subCaseId` and `includeSubCaseComments` are not supported when the case connector feature is disabled' - ); - } - - if (request.query?.subCaseId) { - comments = await caseService.getAllSubCaseComments({ - soClient, - id: request.query.subCaseId, - options: { - sortField: defaultSortField, - }, - }); - } else { - comments = await caseService.getAllCaseComments({ - soClient, - id: request.params.case_id, - includeSubCaseComments: request.query?.includeSubCaseComments, - options: { - sortField: defaultSortField, - }, - }); - } + const client = await context.cases.getCasesClient(); return response.ok({ - body: AllCommentsResponseRt.encode(flattenCommentSavedObjects(comments.saved_objects)), + body: await client.attachments.getAll({ + caseID: request.params.case_id, + includeSubCaseComments: request.query?.includeSubCaseComments, + subCaseID: request.query?.subCaseId, + }), }); } catch (error) { logger.error( diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/get_comment.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/get_comment.ts index a03ed4a66e805c..cf6f7d62dcf6e0 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/get_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/get_comment.ts @@ -7,12 +7,11 @@ import { schema } from '@kbn/config-schema'; -import { CommentResponseRt } from '../../../../../common/api'; import { RouteDeps } from '../../types'; -import { flattenCommentSavedObject, wrapError } from '../../utils'; -import { CASE_COMMENT_DETAILS_URL, SAVED_OBJECT_TYPES } from '../../../../../common/constants'; +import { wrapError } from '../../utils'; +import { CASE_COMMENT_DETAILS_URL } from '../../../../../common/constants'; -export function initGetCommentApi({ attachmentService, router, logger }: RouteDeps) { +export function initGetCommentApi({ router, logger }: RouteDeps) { router.get( { path: CASE_COMMENT_DETAILS_URL, @@ -25,16 +24,13 @@ export function initGetCommentApi({ attachmentService, router, logger }: RouteDe }, async (context, request, response) => { try { - const soClient = context.core.savedObjects.getClient({ - includedHiddenTypes: SAVED_OBJECT_TYPES, - }); + const client = await context.cases.getCasesClient(); - const comment = await attachmentService.get({ - soClient, - attachmentId: request.params.comment_id, - }); return response.ok({ - body: CommentResponseRt.encode(flattenCommentSavedObject(comment)), + body: await client.attachments.get({ + attachmentID: request.params.comment_id, + caseID: request.params.case_id, + }), }); } catch (error) { logger.error( diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/patch_comment.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/patch_comment.ts index b9755cae411332..28852eca3af419 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/patch_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/patch_comment.ts @@ -5,86 +5,18 @@ * 2.0. */ -import { pick } from 'lodash/fp'; import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { schema } from '@kbn/config-schema'; import Boom from '@hapi/boom'; -import { SavedObjectsClientContract, Logger } from 'kibana/server'; -import { CommentableCase } from '../../../../common'; -import { buildCommentUserActionItem } from '../../../../services/user_actions/helpers'; import { RouteDeps } from '../../types'; -import { escapeHatch, wrapError, decodeCommentRequest } from '../../utils'; -import { - CASE_COMMENTS_URL, - SAVED_OBJECT_TYPES, - CASE_SAVED_OBJECT, - SUB_CASE_SAVED_OBJECT, - ENABLE_CASE_CONNECTOR, -} from '../../../../../common/constants'; -import { CommentPatchRequestRt, throwErrors, User } from '../../../../../common/api'; -import { CaseService, AttachmentService } from '../../../../services'; +import { escapeHatch, wrapError } from '../../utils'; +import { CASE_COMMENTS_URL } from '../../../../../common/constants'; +import { CommentPatchRequestRt, throwErrors } from '../../../../../common/api'; -interface CombinedCaseParams { - attachmentService: AttachmentService; - caseService: CaseService; - soClient: SavedObjectsClientContract; - caseID: string; - logger: Logger; - subCaseId?: string; -} - -async function getCommentableCase({ - attachmentService, - caseService, - soClient, - caseID, - subCaseId, - logger, -}: CombinedCaseParams) { - if (subCaseId) { - const [caseInfo, subCase] = await Promise.all([ - caseService.getCase({ - soClient, - id: caseID, - }), - caseService.getSubCase({ - soClient, - id: subCaseId, - }), - ]); - return new CommentableCase({ - attachmentService, - caseService, - collection: caseInfo, - subCase, - soClient, - logger, - }); - } else { - const caseInfo = await caseService.getCase({ - soClient, - id: caseID, - }); - return new CommentableCase({ - attachmentService, - caseService, - collection: caseInfo, - soClient, - logger, - }); - } -} - -export function initPatchCommentApi({ - attachmentService, - caseService, - router, - userActionService, - logger, -}: RouteDeps) { +export function initPatchCommentApi({ router, logger }: RouteDeps) { router.patch( { path: CASE_COMMENTS_URL, @@ -102,101 +34,19 @@ export function initPatchCommentApi({ }, async (context, request, response) => { try { - if (!ENABLE_CASE_CONNECTOR && request.query?.subCaseId !== undefined) { - throw Boom.badRequest( - 'The `subCaseId` is not supported when the case connector feature is disabled' - ); - } - - const soClient = context.core.savedObjects.getClient({ - includedHiddenTypes: SAVED_OBJECT_TYPES, - }); const query = pipe( CommentPatchRequestRt.decode(request.body), fold(throwErrors(Boom.badRequest), identity) ); - const { id: queryCommentId, version: queryCommentVersion, ...queryRestAttributes } = query; - decodeCommentRequest(queryRestAttributes); - - const commentableCase = await getCommentableCase({ - attachmentService, - caseService, - soClient, - caseID: request.params.case_id, - subCaseId: request.query?.subCaseId, - logger, - }); - - const myComment = await attachmentService.get({ - soClient, - attachmentId: queryCommentId, - }); - - if (myComment == null) { - throw Boom.notFound(`This comment ${queryCommentId} does not exist anymore.`); - } - - if (myComment.attributes.type !== queryRestAttributes.type) { - throw Boom.badRequest(`You cannot change the type of the comment.`); - } - - const saveObjType = request.query?.subCaseId ? SUB_CASE_SAVED_OBJECT : CASE_SAVED_OBJECT; - - const caseRef = myComment.references.find((c) => c.type === saveObjType); - if (caseRef == null || (caseRef != null && caseRef.id !== commentableCase.id)) { - throw Boom.notFound( - `This comment ${queryCommentId} does not exist in ${commentableCase.id}).` - ); - } - - if (queryCommentVersion !== myComment.version) { - throw Boom.conflict( - 'This case has been updated. Please refresh before saving additional updates.' - ); - } - - // eslint-disable-next-line @typescript-eslint/naming-convention - const { username, full_name, email } = await caseService.getUser({ request }); - const userInfo: User = { - username, - full_name, - email, - }; - - const updatedDate = new Date().toISOString(); - const { - comment: updatedComment, - commentableCase: updatedCase, - } = await commentableCase.updateComment({ - updateRequest: query, - updatedAt: updatedDate, - user: userInfo, - }); - - await userActionService.bulkCreate({ - soClient, - actions: [ - buildCommentUserActionItem({ - action: 'update', - actionAt: updatedDate, - actionBy: { username, full_name, email }, - caseId: request.params.case_id, - subCaseId: request.query?.subCaseId, - commentId: updatedComment.id, - fields: ['comment'], - newValue: JSON.stringify(queryRestAttributes), - oldValue: JSON.stringify( - // We are interested only in ContextBasicRt attributes - // myComment.attribute contains also CommentAttributesBasicRt attributes - pick(Object.keys(queryRestAttributes), myComment.attributes) - ), - }), - ], - }); + const client = await context.cases.getCasesClient(); return response.ok({ - body: await updatedCase.encode(), + body: await client.attachments.update({ + caseID: request.params.case_id, + subCaseID: request.query?.subCaseId, + updateRequest: query, + }), }); } catch (error) { logger.error( diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.ts b/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.ts index fa97796228bd1b..933a53eb8a8705 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.ts @@ -5,14 +5,11 @@ * 2.0. */ -import Boom from '@hapi/boom'; -import { CaseConfigureResponseRt, ConnectorMappingsAttributes } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; -import { CASE_CONFIGURE_URL, SAVED_OBJECT_TYPES } from '../../../../../common/constants'; -import { transformESConnectorToCaseConnector } from '../helpers'; +import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; -export function initGetCaseConfigure({ caseConfigureService, router, logger }: RouteDeps) { +export function initGetCaseConfigure({ router, logger }: RouteDeps) { router.get( { path: CASE_CONFIGURE_URL, @@ -20,49 +17,10 @@ export function initGetCaseConfigure({ caseConfigureService, router, logger }: R }, async (context, request, response) => { try { - let error = null; - const soClient = context.core.savedObjects.getClient({ - includedHiddenTypes: SAVED_OBJECT_TYPES, - }); - - const myCaseConfigure = await caseConfigureService.find({ soClient }); - - const { connector, ...caseConfigureWithoutConnector } = myCaseConfigure.saved_objects[0] - ?.attributes ?? { connector: null }; - let mappings: ConnectorMappingsAttributes[] = []; - if (connector != null) { - if (!context.cases) { - throw Boom.badRequest('RouteHandlerContext is not registered for cases'); - } - const casesClient = await context.cases.getCasesClient(); - const actionsClient = context.actions?.getActionsClient(); - if (actionsClient == null) { - throw Boom.notFound('Action client not found'); - } - try { - mappings = await casesClient.casesClientInternal.configuration.getMappings({ - actionsClient, - connectorId: connector.id, - connectorType: connector.type, - }); - } catch (e) { - error = e.isBoom - ? e.output.payload.message - : `Error connecting to ${connector.name} instance`; - } - } + const client = await context.cases.getCasesClient(); return response.ok({ - body: - myCaseConfigure.saved_objects.length > 0 - ? CaseConfigureResponseRt.encode({ - ...caseConfigureWithoutConnector, - connector: transformESConnectorToCaseConnector(connector), - mappings, - version: myCaseConfigure.saved_objects[0].version ?? '', - error, - }) - : {}, + body: await client.configure.get(), }); } 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/cases/configure/get_connectors.ts index 81ffc06355ff5e..be05d1c3b82303 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/get_connectors.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/configure/get_connectors.ts @@ -5,29 +5,14 @@ * 2.0. */ -import Boom from '@hapi/boom'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; -import { ActionType } from '../../../../../../actions/common'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { FindActionResult } from '../../../../../../actions/server/types'; -import { - CASE_CONFIGURE_CONNECTORS_URL, - SUPPORTED_CONNECTORS, -} from '../../../../../common/constants'; - -const isConnectorSupported = ( - action: FindActionResult, - actionTypes: Record -): boolean => - SUPPORTED_CONNECTORS.includes(action.actionTypeId) && - actionTypes[action.actionTypeId]?.enabledInLicense; +import { CASE_CONFIGURE_CONNECTORS_URL } from '../../../../../common/constants'; /* * Be aware that this api will only return 20 connectors */ - export function initCaseConfigureGetActionConnector({ router, logger }: RouteDeps) { router.get( { @@ -36,21 +21,9 @@ export function initCaseConfigureGetActionConnector({ router, logger }: RouteDep }, async (context, request, response) => { try { - const actionsClient = context.actions?.getActionsClient(); - - if (actionsClient == null) { - throw Boom.notFound('Action client not found'); - } - - const actionTypes = (await actionsClient.listTypes()).reduce( - (types, type) => ({ ...types, [type.id]: type }), - {} - ); + const client = await context.cases.getCasesClient(); - const results = (await actionsClient.getAll()).filter((action) => - isConnectorSupported(action, actionTypes) - ); - return response.ok({ body: results }); + return response.ok({ body: await client.configure.getConnectors() }); } catch (error) { logger.error(`Failed to get connectors in route: ${error}`); return response.customError(wrapError(error)); diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.ts b/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.ts index 61f3e4719520a4..d32c7151f6df5f 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.ts @@ -10,26 +10,12 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { - CasesConfigurePatchRt, - CaseConfigureResponseRt, - throwErrors, - ConnectorMappingsAttributes, -} from '../../../../../common/api'; +import { CasesConfigurePatchRt, throwErrors } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { wrapError, escapeHatch } from '../../utils'; -import { CASE_CONFIGURE_URL, SAVED_OBJECT_TYPES } from '../../../../../common/constants'; -import { - transformCaseConnectorToEsConnector, - transformESConnectorToCaseConnector, -} from '../helpers'; +import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; -export function initPatchCaseConfigure({ - caseConfigureService, - caseService, - router, - logger, -}: RouteDeps) { +export function initPatchCaseConfigure({ router, logger }: RouteDeps) { router.patch( { path: CASE_CONFIGURE_URL, @@ -39,79 +25,15 @@ export function initPatchCaseConfigure({ }, async (context, request, response) => { try { - let error = null; - const soClient = context.core.savedObjects.getClient({ - includedHiddenTypes: SAVED_OBJECT_TYPES, - }); const query = pipe( CasesConfigurePatchRt.decode(request.body), fold(throwErrors(Boom.badRequest), identity) ); - const myCaseConfigure = await caseConfigureService.find({ soClient }); - const { version, connector, ...queryWithoutVersion } = query; - if (myCaseConfigure.saved_objects.length === 0) { - throw Boom.conflict( - 'You can not patch this configuration since you did not created first with a post.' - ); - } - - if (version !== myCaseConfigure.saved_objects[0].version) { - throw Boom.conflict( - 'This configuration has been updated. Please refresh before saving additional updates.' - ); - } - - // eslint-disable-next-line @typescript-eslint/naming-convention - const { username, full_name, email } = await caseService.getUser({ request }); + const client = await context.cases.getCasesClient(); - const updateDate = new Date().toISOString(); - - let mappings: ConnectorMappingsAttributes[] = []; - if (connector != null) { - if (!context.cases) { - throw Boom.badRequest('RouteHandlerContext is not registered for cases'); - } - const casesClient = await context.cases.getCasesClient(); - const actionsClient = context.actions?.getActionsClient(); - if (actionsClient == null) { - throw Boom.notFound('Action client have not been found'); - } - try { - mappings = await casesClient.casesClientInternal.configuration.getMappings({ - actionsClient, - connectorId: connector.id, - connectorType: connector.type, - }); - } catch (e) { - error = e.isBoom - ? e.output.payload.message - : `Error connecting to ${connector.name} instance`; - } - } - const patch = await caseConfigureService.patch({ - soClient, - caseConfigureId: myCaseConfigure.saved_objects[0].id, - updatedAttributes: { - ...queryWithoutVersion, - ...(connector != null - ? { connector: transformCaseConnectorToEsConnector(connector) } - : {}), - updated_at: updateDate, - updated_by: { email, full_name, username }, - }, - }); return response.ok({ - body: CaseConfigureResponseRt.encode({ - ...myCaseConfigure.saved_objects[0].attributes, - ...patch.attributes, - connector: transformESConnectorToCaseConnector( - patch.attributes.connector ?? myCaseConfigure.saved_objects[0].attributes.connector - ), - mappings, - version: patch.version ?? '', - error, - }), + body: await client.configure.update(query), }); } 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/cases/configure/post_configure.ts index 62fa7cad324fce..ca25a29d6a1dee 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.ts @@ -10,26 +10,12 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { - CasesConfigureRequestRt, - CaseConfigureResponseRt, - throwErrors, - ConnectorMappingsAttributes, -} from '../../../../../common/api'; +import { CasesConfigureRequestRt, throwErrors } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { wrapError, escapeHatch } from '../../utils'; -import { CASE_CONFIGURE_URL, SAVED_OBJECT_TYPES } from '../../../../../common/constants'; -import { - transformCaseConnectorToEsConnector, - transformESConnectorToCaseConnector, -} from '../helpers'; +import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; -export function initPostCaseConfigure({ - caseConfigureService, - caseService, - router, - logger, -}: RouteDeps) { +export function initPostCaseConfigure({ router, logger }: RouteDeps) { router.post( { path: CASE_CONFIGURE_URL, @@ -39,72 +25,15 @@ export function initPostCaseConfigure({ }, async (context, request, response) => { try { - let error = null; - if (!context.cases) { - throw Boom.badRequest('RouteHandlerContext is not registered for cases'); - } - - const casesClient = await context.cases.getCasesClient(); - const actionsClient = context.actions?.getActionsClient(); - - if (actionsClient == null) { - throw Boom.notFound('Action client not found'); - } - - const soClient = context.core.savedObjects.getClient({ - includedHiddenTypes: SAVED_OBJECT_TYPES, - }); - const query = pipe( CasesConfigureRequestRt.decode(request.body), fold(throwErrors(Boom.badRequest), identity) ); - const myCaseConfigure = await caseConfigureService.find({ soClient }); - if (myCaseConfigure.saved_objects.length > 0) { - await Promise.all( - myCaseConfigure.saved_objects.map((cc) => - caseConfigureService.delete({ soClient, caseConfigureId: cc.id }) - ) - ); - } - // eslint-disable-next-line @typescript-eslint/naming-convention - const { email, full_name, username } = await caseService.getUser({ request }); - - const creationDate = new Date().toISOString(); - let mappings: ConnectorMappingsAttributes[] = []; - try { - mappings = await casesClient.casesClientInternal.configuration.getMappings({ - actionsClient, - connectorId: query.connector.id, - connectorType: query.connector.type, - }); - } catch (e) { - error = e.isBoom - ? e.output.payload.message - : `Error connecting to ${query.connector.name} instance`; - } - const post = await caseConfigureService.post({ - soClient, - attributes: { - ...query, - connector: transformCaseConnectorToEsConnector(query.connector), - created_at: creationDate, - created_by: { email, full_name, username }, - updated_at: null, - updated_by: null, - }, - }); + const client = await context.cases.getCasesClient(); return response.ok({ - body: CaseConfigureResponseRt.encode({ - ...post.attributes, - // Reserve for future implementations - connector: transformESConnectorToCaseConnector(post.attributes.connector), - mappings, - version: post.version ?? '', - error, - }), + body: await client.configure.create(query), }); } catch (error) { logger.error(`Failed to post case configure in route: ${error}`); diff --git a/x-pack/plugins/cases/server/routes/api/cases/delete_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/delete_cases.ts index a9be4a314adeb5..1784a434292cc3 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/delete_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/delete_cases.ts @@ -7,54 +7,11 @@ import { schema } from '@kbn/config-schema'; -import { SavedObjectsClientContract } from 'src/core/server'; -import { buildCaseUserActionItem } from '../../../services/user_actions/helpers'; import { RouteDeps } from '../types'; import { wrapError } from '../utils'; -import { CASES_URL, SAVED_OBJECT_TYPES, ENABLE_CASE_CONNECTOR } from '../../../../common/constants'; -import { CaseService, AttachmentService } from '../../../services'; +import { CASES_URL } from '../../../../common/constants'; -async function deleteSubCases({ - attachmentService, - caseService, - soClient, - caseIds, -}: { - attachmentService: AttachmentService; - caseService: CaseService; - soClient: SavedObjectsClientContract; - caseIds: string[]; -}) { - const subCasesForCaseIds = await caseService.findSubCasesByCaseId({ soClient, ids: caseIds }); - - const subCaseIDs = subCasesForCaseIds.saved_objects.map((subCase) => subCase.id); - const commentsForSubCases = await caseService.getAllSubCaseComments({ - soClient, - id: subCaseIDs, - }); - - // This shouldn't actually delete anything because all the comments should be deleted when comments are deleted - // per case ID - await Promise.all( - commentsForSubCases.saved_objects.map((commentSO) => - attachmentService.delete({ soClient, attachmentId: commentSO.id }) - ) - ); - - await Promise.all( - subCasesForCaseIds.saved_objects.map((subCaseSO) => - caseService.deleteSubCase(soClient, subCaseSO.id) - ) - ); -} - -export function initDeleteCasesApi({ - attachmentService, - caseService, - router, - userActionService, - logger, -}: RouteDeps) { +export function initDeleteCasesApi({ router, logger }: RouteDeps) { router.delete( { path: CASES_URL, @@ -66,73 +23,8 @@ export function initDeleteCasesApi({ }, async (context, request, response) => { try { - const soClient = context.core.savedObjects.getClient({ - includedHiddenTypes: SAVED_OBJECT_TYPES, - }); - await Promise.all( - request.query.ids.map((id) => - caseService.deleteCase({ - soClient, - id, - }) - ) - ); - const comments = await Promise.all( - request.query.ids.map((id) => - caseService.getAllCaseComments({ - soClient, - id, - }) - ) - ); - - if (comments.some((c) => c.saved_objects.length > 0)) { - await Promise.all( - comments.map((c) => - Promise.all( - c.saved_objects.map(({ id }) => - attachmentService.delete({ - soClient, - attachmentId: id, - }) - ) - ) - ) - ); - } - - if (ENABLE_CASE_CONNECTOR) { - await deleteSubCases({ - attachmentService, - caseService, - soClient, - caseIds: request.query.ids, - }); - } - - // eslint-disable-next-line @typescript-eslint/naming-convention - const { username, full_name, email } = await caseService.getUser({ request }); - const deleteDate = new Date().toISOString(); - - await userActionService.bulkCreate({ - soClient, - actions: request.query.ids.map((id) => - buildCaseUserActionItem({ - action: 'create', - actionAt: deleteDate, - actionBy: { username, full_name, email }, - caseId: id, - fields: [ - 'comment', - 'description', - 'status', - 'tags', - 'title', - ...(ENABLE_CASE_CONNECTOR ? ['sub_case'] : []), - ], - }) - ), - }); + const client = await context.cases.getCasesClient(); + await client.cases.delete(request.query.ids); return response.noContent(); } catch (error) { diff --git a/x-pack/plugins/cases/server/routes/api/cases/get_case.ts b/x-pack/plugins/cases/server/routes/api/cases/get_case.ts index e48806567e5745..9d26fbb90328c9 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/get_case.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/get_case.ts @@ -7,10 +7,9 @@ import { schema } from '@kbn/config-schema'; -import Boom from '@hapi/boom'; import { RouteDeps } from '../types'; import { wrapError } from '../utils'; -import { CASE_DETAILS_URL, ENABLE_CASE_CONNECTOR } from '../../../../common/constants'; +import { CASE_DETAILS_URL } from '../../../../common/constants'; export function initGetCaseApi({ router, logger }: RouteDeps) { router.get( @@ -28,11 +27,6 @@ export function initGetCaseApi({ router, logger }: RouteDeps) { }, async (context, request, response) => { try { - if (!ENABLE_CASE_CONNECTOR && request.query.includeSubCaseComments !== undefined) { - throw Boom.badRequest( - 'The `subCaseId` is not supported when the case connector feature is disabled' - ); - } const casesClient = await context.cases.getCasesClient(); const id = request.params.case_id; diff --git a/x-pack/plugins/cases/server/routes/api/cases/helpers.test.ts b/x-pack/plugins/cases/server/routes/api/cases/helpers.test.ts deleted file mode 100644 index f7cfebeaea749f..00000000000000 --- a/x-pack/plugins/cases/server/routes/api/cases/helpers.test.ts +++ /dev/null @@ -1,111 +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 { SavedObjectsFindResponse } from 'kibana/server'; -import { - CaseConnector, - ConnectorTypes, - ESCaseConnector, - ESCasesConfigureAttributes, -} from '../../../../common/api'; -import { mockCaseConfigure } from '../__fixtures__'; -import { - transformCaseConnectorToEsConnector, - transformESConnectorToCaseConnector, - getConnectorFromConfiguration, -} from './helpers'; - -describe('helpers', () => { - const caseConnector: CaseConnector = { - id: '123', - name: 'Jira', - type: ConnectorTypes.jira, - fields: { issueType: 'Task', priority: 'High', parent: null }, - }; - - const esCaseConnector: ESCaseConnector = { - id: '123', - name: 'Jira', - type: ConnectorTypes.jira, - fields: [ - { key: 'issueType', value: 'Task' }, - { key: 'priority', value: 'High' }, - { key: 'parent', value: null }, - ], - }; - - const caseConfigure: SavedObjectsFindResponse = { - saved_objects: [{ ...mockCaseConfigure[0], score: 0 }], - total: 1, - per_page: 20, - page: 1, - }; - - describe('transformCaseConnectorToEsConnector', () => { - it('transform correctly', () => { - expect(transformCaseConnectorToEsConnector(caseConnector)).toEqual(esCaseConnector); - }); - - it('transform correctly with null attributes', () => { - // @ts-ignore this is case the connector does not exist for old cases object or configurations - expect(transformCaseConnectorToEsConnector(null)).toEqual({ - id: 'none', - name: 'none', - type: ConnectorTypes.none, - fields: [], - }); - }); - }); - - describe('transformESConnectorToCaseConnector', () => { - it('transform correctly', () => { - expect(transformESConnectorToCaseConnector(esCaseConnector)).toEqual(caseConnector); - }); - - it('transform correctly with null attributes', () => { - // @ts-ignore this is case the connector does not exist for old cases object or configurations - expect(transformESConnectorToCaseConnector(null)).toEqual({ - id: 'none', - name: 'none', - type: ConnectorTypes.none, - fields: null, - }); - }); - }); - - describe('getConnectorFromConfiguration', () => { - it('transform correctly', () => { - expect(getConnectorFromConfiguration(caseConfigure)).toEqual({ - id: '789', - name: 'My connector 3', - type: ConnectorTypes.jira, - fields: null, - }); - }); - - it('transform correctly with no connector', () => { - const caseConfigureNoConnector: SavedObjectsFindResponse = { - ...caseConfigure, - saved_objects: [ - { - ...mockCaseConfigure[0], - // @ts-ignore this is case the connector does not exist for old cases object or configurations - attributes: { ...mockCaseConfigure[0].attributes, connector: null }, - score: 0, - }, - ], - }; - - expect(getConnectorFromConfiguration(caseConfigureNoConnector)).toEqual({ - id: 'none', - name: 'none', - type: ConnectorTypes.none, - fields: null, - }); - }); - }); -}); 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 1ce60442ee9c9e..2836c7572e810e 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 @@ -5,12 +5,11 @@ * 2.0. */ -import { UsersRt } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; -import { CASE_REPORTERS_URL, SAVED_OBJECT_TYPES } from '../../../../../common/constants'; +import { CASE_REPORTERS_URL } from '../../../../../common/constants'; -export function initGetReportersApi({ caseService, router, logger }: RouteDeps) { +export function initGetReportersApi({ router, logger }: RouteDeps) { router.get( { path: CASE_REPORTERS_URL, @@ -18,13 +17,9 @@ export function initGetReportersApi({ caseService, router, logger }: RouteDeps) }, async (context, request, response) => { try { - const soClient = context.core.savedObjects.getClient({ - includedHiddenTypes: SAVED_OBJECT_TYPES, - }); - const reporters = await caseService.getReporters({ - soClient, - }); - return response.ok({ body: UsersRt.encode(reporters) }); + const client = await context.cases.getCasesClient(); + + return response.ok({ body: await client.cases.getReporters() }); } 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/status/get_status.ts b/x-pack/plugins/cases/server/routes/api/cases/status/get_status.ts index ddfa5e39c01b00..6ba59635807823 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/status/get_status.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/status/get_status.ts @@ -8,11 +8,9 @@ import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; -import { CASE_STATUS_URL, SAVED_OBJECT_TYPES } from '../../../../../common/constants'; -import { CasesStatusResponseRt, caseStatuses } from '../../../../../common/api'; -import { constructQueryOptions } from '../helpers'; +import { CASE_STATUS_URL } from '../../../../../common/constants'; -export function initGetCasesStatusApi({ caseService, router, logger }: RouteDeps) { +export function initGetCasesStatusApi({ router, logger }: RouteDeps) { router.get( { path: CASE_STATUS_URL, @@ -20,27 +18,10 @@ export function initGetCasesStatusApi({ caseService, router, logger }: RouteDeps }, async (context, request, response) => { try { - const soClient = context.core.savedObjects.getClient({ - includedHiddenTypes: SAVED_OBJECT_TYPES, - }); - - const [openCases, inProgressCases, closedCases] = await Promise.all([ - ...caseStatuses.map((status) => { - const statusQuery = constructQueryOptions({ status }); - return caseService.findCaseStatusStats({ - soClient, - caseOptions: statusQuery.case, - subCaseOptions: statusQuery.subCase, - }); - }), - ]); + const client = await context.cases.getCasesClient(); return response.ok({ - body: CasesStatusResponseRt.encode({ - count_open_cases: openCases, - count_in_progress_cases: inProgressCases, - count_closed_cases: closedCases, - }), + body: await client.stats.getStatusTotalsByType(), }); } catch (error) { logger.error(`Failed to get status stats in route: ${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 10c15d2518f349..e13974b514c083 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 @@ -7,9 +7,9 @@ import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; -import { CASE_TAGS_URL, SAVED_OBJECT_TYPES } from '../../../../../common/constants'; +import { CASE_TAGS_URL } from '../../../../../common/constants'; -export function initGetTagsApi({ caseService, router }: RouteDeps) { +export function initGetTagsApi({ router, logger }: RouteDeps) { router.get( { path: CASE_TAGS_URL, @@ -17,14 +17,11 @@ export function initGetTagsApi({ caseService, router }: RouteDeps) { }, async (context, request, response) => { try { - const soClient = context.core.savedObjects.getClient({ - includedHiddenTypes: SAVED_OBJECT_TYPES, - }); - const tags = await caseService.getTags({ - soClient, - }); - return response.ok({ body: tags }); + const client = await context.cases.getCasesClient(); + + return response.ok({ body: await client.cases.getTags() }); } 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/types.ts b/x-pack/plugins/cases/server/routes/api/types.ts index 76fad3fcc33bc6..d41e89dae31f84 100644 --- a/x-pack/plugins/cases/server/routes/api/types.ts +++ b/x-pack/plugins/cases/server/routes/api/types.ts @@ -27,12 +27,6 @@ export interface RouteDeps { logger: Logger; } -export enum SortFieldCase { - closedAt = 'closed_at', - createdAt = 'created_at', - status = 'status', -} - export interface TotalCommentByCase { caseId: string; totalComments: number; diff --git a/x-pack/plugins/cases/server/routes/api/utils.test.ts b/x-pack/plugins/cases/server/routes/api/utils.test.ts index 99d2c1509538cc..3fce38b27446ef 100644 --- a/x-pack/plugins/cases/server/routes/api/utils.test.ts +++ b/x-pack/plugins/cases/server/routes/api/utils.test.ts @@ -5,317 +5,10 @@ * 2.0. */ -import { - transformNewCase, - transformNewComment, - wrapError, - transformCases, - flattenCaseSavedObject, - flattenCommentSavedObjects, - transformComments, - flattenCommentSavedObject, - sortToSnake, -} from './utils'; -import { newCase } from './__mocks__/request_responses'; +import { wrapError } from './utils'; import { isBoom, boomify } from '@hapi/boom'; -import { - mockCases, - mockCaseComments, - mockCaseNoConnectorId, -} from './__fixtures__/mock_saved_objects'; -import { - ConnectorTypes, - ESCaseConnector, - CommentType, - AssociationType, - CaseType, - CaseResponse, -} from '../../../common/api'; describe('Utils', () => { - describe('transformNewCase', () => { - const connector: ESCaseConnector = { - id: '123', - name: 'My connector', - type: ConnectorTypes.jira, - fields: [ - { key: 'issueType', value: 'Task' }, - { key: 'priority', value: 'High' }, - { key: 'parent', value: null }, - ], - }; - it('transform correctly', () => { - const myCase = { - newCase: { ...newCase, type: CaseType.individual }, - connector, - createdDate: '2020-04-09T09:43:51.778Z', - email: 'elastic@elastic.co', - full_name: 'Elastic', - username: 'elastic', - }; - - const res = transformNewCase(myCase); - - expect(res).toMatchInlineSnapshot(` - Object { - "closed_at": null, - "closed_by": null, - "connector": Object { - "fields": Array [ - Object { - "key": "issueType", - "value": "Task", - }, - Object { - "key": "priority", - "value": "High", - }, - Object { - "key": "parent", - "value": null, - }, - ], - "id": "123", - "name": "My connector", - "type": ".jira", - }, - "created_at": "2020-04-09T09:43:51.778Z", - "created_by": Object { - "email": "elastic@elastic.co", - "full_name": "Elastic", - "username": "elastic", - }, - "description": "A description", - "external_service": null, - "owner": "securitySolution", - "settings": Object { - "syncAlerts": true, - }, - "status": "open", - "tags": Array [ - "new", - "case", - ], - "title": "My new case", - "type": "individual", - "updated_at": null, - "updated_by": null, - } - `); - }); - - it('transform correctly without optional fields', () => { - const myCase = { - newCase: { ...newCase, type: CaseType.individual }, - connector, - createdDate: '2020-04-09T09:43:51.778Z', - }; - - const res = transformNewCase(myCase); - - expect(res).toMatchInlineSnapshot(` - Object { - "closed_at": null, - "closed_by": null, - "connector": Object { - "fields": Array [ - Object { - "key": "issueType", - "value": "Task", - }, - Object { - "key": "priority", - "value": "High", - }, - Object { - "key": "parent", - "value": null, - }, - ], - "id": "123", - "name": "My connector", - "type": ".jira", - }, - "created_at": "2020-04-09T09:43:51.778Z", - "created_by": Object { - "email": undefined, - "full_name": undefined, - "username": undefined, - }, - "description": "A description", - "external_service": null, - "owner": "securitySolution", - "settings": Object { - "syncAlerts": true, - }, - "status": "open", - "tags": Array [ - "new", - "case", - ], - "title": "My new case", - "type": "individual", - "updated_at": null, - "updated_by": null, - } - `); - }); - - it('transform correctly with optional fields as null', () => { - const myCase = { - newCase: { ...newCase, type: CaseType.individual }, - connector, - createdDate: '2020-04-09T09:43:51.778Z', - email: null, - full_name: null, - username: null, - }; - - const res = transformNewCase(myCase); - - expect(res).toMatchInlineSnapshot(` - Object { - "closed_at": null, - "closed_by": null, - "connector": Object { - "fields": Array [ - Object { - "key": "issueType", - "value": "Task", - }, - Object { - "key": "priority", - "value": "High", - }, - Object { - "key": "parent", - "value": null, - }, - ], - "id": "123", - "name": "My connector", - "type": ".jira", - }, - "created_at": "2020-04-09T09:43:51.778Z", - "created_by": Object { - "email": null, - "full_name": null, - "username": null, - }, - "description": "A description", - "external_service": null, - "owner": "securitySolution", - "settings": Object { - "syncAlerts": true, - }, - "status": "open", - "tags": Array [ - "new", - "case", - ], - "title": "My new case", - "type": "individual", - "updated_at": null, - "updated_by": null, - } - `); - }); - }); - - describe('transformNewComment', () => { - it('transforms correctly', () => { - const comment = { - comment: 'A comment', - type: CommentType.user as const, - createdDate: '2020-04-09T09:43:51.778Z', - email: 'elastic@elastic.co', - full_name: 'Elastic', - username: 'elastic', - associationType: AssociationType.case, - }; - - const res = transformNewComment(comment); - expect(res).toMatchInlineSnapshot(` - Object { - "associationType": "case", - "comment": "A comment", - "created_at": "2020-04-09T09:43:51.778Z", - "created_by": Object { - "email": "elastic@elastic.co", - "full_name": "Elastic", - "username": "elastic", - }, - "pushed_at": null, - "pushed_by": null, - "type": "user", - "updated_at": null, - "updated_by": null, - } - `); - }); - - it('transform correctly without optional fields', () => { - const comment = { - comment: 'A comment', - type: CommentType.user as const, - createdDate: '2020-04-09T09:43:51.778Z', - associationType: AssociationType.case, - }; - - const res = transformNewComment(comment); - - expect(res).toMatchInlineSnapshot(` - Object { - "associationType": "case", - "comment": "A comment", - "created_at": "2020-04-09T09:43:51.778Z", - "created_by": Object { - "email": undefined, - "full_name": undefined, - "username": undefined, - }, - "pushed_at": null, - "pushed_by": null, - "type": "user", - "updated_at": null, - "updated_by": null, - } - `); - }); - - it('transform correctly with optional fields as null', () => { - const comment = { - comment: 'A comment', - type: CommentType.user as const, - createdDate: '2020-04-09T09:43:51.778Z', - email: null, - full_name: null, - username: null, - associationType: AssociationType.case, - }; - - const res = transformNewComment(comment); - - expect(res).toMatchInlineSnapshot(` - Object { - "associationType": "case", - "comment": "A comment", - "created_at": "2020-04-09T09:43:51.778Z", - "created_by": Object { - "email": null, - "full_name": null, - "username": null, - }, - "pushed_at": null, - "pushed_by": null, - "type": "user", - "updated_at": null, - "updated_by": null, - } - `); - }); - }); - describe('wrapError', () => { it('wraps an error', () => { const error = new Error('Something happened'); @@ -361,539 +54,4 @@ describe('Utils', () => { expect(res.headers).toEqual({}); }); }); - - describe('transformCases', () => { - it('transforms correctly', () => { - const casesMap = new Map( - mockCases.map((obj) => { - return [obj.id, flattenCaseSavedObject({ savedObject: obj, totalComment: 2 })]; - }) - ); - const res = transformCases({ - casesMap, - countOpenCases: 2, - countInProgressCases: 2, - countClosedCases: 2, - page: 1, - perPage: 10, - total: casesMap.size, - }); - expect(res).toMatchInlineSnapshot(` - Object { - "cases": Array [ - Object { - "closed_at": null, - "closed_by": null, - "comments": Array [], - "connector": Object { - "fields": null, - "id": "none", - "name": "none", - "type": ".none", - }, - "created_at": "2019-11-25T21:54:48.952Z", - "created_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - "description": "This is a brand new case of a bad meanie defacing data", - "external_service": null, - "id": "mock-id-1", - "owner": "securitySolution", - "settings": Object { - "syncAlerts": true, - }, - "status": "open", - "subCaseIds": undefined, - "subCases": undefined, - "tags": Array [ - "defacement", - ], - "title": "Super Bad Security Issue", - "totalAlerts": 0, - "totalComment": 2, - "type": "individual", - "updated_at": "2019-11-25T21:54:48.952Z", - "updated_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - "version": "WzAsMV0=", - }, - Object { - "closed_at": null, - "closed_by": null, - "comments": Array [], - "connector": Object { - "fields": null, - "id": "none", - "name": "none", - "type": ".none", - }, - "created_at": "2019-11-25T22:32:00.900Z", - "created_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - "description": "Oh no, a bad meanie destroying data!", - "external_service": null, - "id": "mock-id-2", - "owner": "securitySolution", - "settings": Object { - "syncAlerts": true, - }, - "status": "open", - "subCaseIds": undefined, - "subCases": undefined, - "tags": Array [ - "Data Destruction", - ], - "title": "Damaging Data Destruction Detected", - "totalAlerts": 0, - "totalComment": 2, - "type": "individual", - "updated_at": "2019-11-25T22:32:00.900Z", - "updated_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - "version": "WzQsMV0=", - }, - Object { - "closed_at": null, - "closed_by": null, - "comments": Array [], - "connector": Object { - "fields": Object { - "issueType": "Task", - "parent": null, - "priority": "High", - }, - "id": "123", - "name": "My connector", - "type": ".jira", - }, - "created_at": "2019-11-25T22:32:17.947Z", - "created_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - "description": "Oh no, a bad meanie going LOLBins all over the place!", - "external_service": null, - "id": "mock-id-3", - "owner": "securitySolution", - "settings": Object { - "syncAlerts": true, - }, - "status": "open", - "subCaseIds": undefined, - "subCases": undefined, - "tags": Array [ - "LOLBins", - ], - "title": "Another bad one", - "totalAlerts": 0, - "totalComment": 2, - "type": "individual", - "updated_at": "2019-11-25T22:32:17.947Z", - "updated_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - "version": "WzUsMV0=", - }, - Object { - "closed_at": "2019-11-25T22:32:17.947Z", - "closed_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - "comments": Array [], - "connector": Object { - "fields": Object { - "issueType": "Task", - "parent": null, - "priority": "High", - }, - "id": "123", - "name": "My connector", - "type": ".jira", - }, - "created_at": "2019-11-25T22:32:17.947Z", - "created_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - "description": "Oh no, a bad meanie going LOLBins all over the place!", - "external_service": null, - "id": "mock-id-4", - "owner": "securitySolution", - "settings": Object { - "syncAlerts": true, - }, - "status": "closed", - "subCaseIds": undefined, - "subCases": undefined, - "tags": Array [ - "LOLBins", - ], - "title": "Another bad one", - "totalAlerts": 0, - "totalComment": 2, - "type": "individual", - "updated_at": "2019-11-25T22:32:17.947Z", - "updated_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - "version": "WzUsMV0=", - }, - ], - "count_closed_cases": 2, - "count_in_progress_cases": 2, - "count_open_cases": 2, - "page": 1, - "per_page": 10, - "total": 4, - } - `); - }); - }); - - describe('flattenCaseSavedObject', () => { - it('flattens correctly', () => { - const myCase = { ...mockCases[2] }; - const res = flattenCaseSavedObject({ - savedObject: myCase, - totalComment: 2, - }); - - expect(res).toMatchInlineSnapshot(` - Object { - "closed_at": null, - "closed_by": null, - "comments": Array [], - "connector": Object { - "fields": Object { - "issueType": "Task", - "parent": null, - "priority": "High", - }, - "id": "123", - "name": "My connector", - "type": ".jira", - }, - "created_at": "2019-11-25T22:32:17.947Z", - "created_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - "description": "Oh no, a bad meanie going LOLBins all over the place!", - "external_service": null, - "id": "mock-id-3", - "owner": "securitySolution", - "settings": Object { - "syncAlerts": true, - }, - "status": "open", - "subCaseIds": undefined, - "subCases": undefined, - "tags": Array [ - "LOLBins", - ], - "title": "Another bad one", - "totalAlerts": 0, - "totalComment": 2, - "type": "individual", - "updated_at": "2019-11-25T22:32:17.947Z", - "updated_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - "version": "WzUsMV0=", - } - `); - }); - - it('flattens correctly without version', () => { - const myCase = { ...mockCases[2] }; - myCase.version = undefined; - const res = flattenCaseSavedObject({ - savedObject: myCase, - totalComment: 2, - }); - - expect(res).toMatchInlineSnapshot(` - Object { - "closed_at": null, - "closed_by": null, - "comments": Array [], - "connector": Object { - "fields": Object { - "issueType": "Task", - "parent": null, - "priority": "High", - }, - "id": "123", - "name": "My connector", - "type": ".jira", - }, - "created_at": "2019-11-25T22:32:17.947Z", - "created_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - "description": "Oh no, a bad meanie going LOLBins all over the place!", - "external_service": null, - "id": "mock-id-3", - "owner": "securitySolution", - "settings": Object { - "syncAlerts": true, - }, - "status": "open", - "subCaseIds": undefined, - "subCases": undefined, - "tags": Array [ - "LOLBins", - ], - "title": "Another bad one", - "totalAlerts": 0, - "totalComment": 2, - "type": "individual", - "updated_at": "2019-11-25T22:32:17.947Z", - "updated_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - "version": "0", - } - `); - }); - - it('flattens correctly with comments', () => { - const myCase = { ...mockCases[2] }; - const comments = [{ ...mockCaseComments[0] }]; - const res = flattenCaseSavedObject({ - savedObject: myCase, - comments, - totalComment: 2, - }); - - expect(res).toMatchInlineSnapshot(` - Object { - "closed_at": null, - "closed_by": null, - "comments": Array [ - Object { - "associationType": "case", - "comment": "Wow, good luck catching that bad meanie!", - "created_at": "2019-11-25T21:55:00.177Z", - "created_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - "id": "mock-comment-1", - "pushed_at": null, - "pushed_by": null, - "type": "user", - "updated_at": "2019-11-25T21:55:00.177Z", - "updated_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - "version": "WzEsMV0=", - }, - ], - "connector": Object { - "fields": Object { - "issueType": "Task", - "parent": null, - "priority": "High", - }, - "id": "123", - "name": "My connector", - "type": ".jira", - }, - "created_at": "2019-11-25T22:32:17.947Z", - "created_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - "description": "Oh no, a bad meanie going LOLBins all over the place!", - "external_service": null, - "id": "mock-id-3", - "owner": "securitySolution", - "settings": Object { - "syncAlerts": true, - }, - "status": "open", - "subCaseIds": undefined, - "subCases": undefined, - "tags": Array [ - "LOLBins", - ], - "title": "Another bad one", - "totalAlerts": 0, - "totalComment": 2, - "type": "individual", - "updated_at": "2019-11-25T22:32:17.947Z", - "updated_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - "version": "WzUsMV0=", - } - `); - }); - - it('inserts missing connector', () => { - const extraCaseData = { - totalComment: 2, - }; - - const res = flattenCaseSavedObject({ - // @ts-ignore this is to update old case saved objects to include connector - savedObject: mockCaseNoConnectorId, - ...extraCaseData, - }); - - expect(res).toMatchInlineSnapshot(` - Object { - "closed_at": null, - "closed_by": null, - "comments": Array [], - "connector": Object { - "fields": null, - "id": "none", - "name": "none", - "type": ".none", - }, - "created_at": "2019-11-25T21:54:48.952Z", - "created_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - "description": "This is a brand new case of a bad meanie defacing data", - "external_service": null, - "id": "mock-no-connector_id", - "settings": Object { - "syncAlerts": true, - }, - "status": "open", - "subCaseIds": undefined, - "subCases": undefined, - "tags": Array [ - "defacement", - ], - "title": "Super Bad Security Issue", - "totalAlerts": 0, - "totalComment": 2, - "updated_at": "2019-11-25T21:54:48.952Z", - "updated_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - "version": "WzAsMV0=", - } - `); - }); - }); - - describe('transformComments', () => { - it('transforms correctly', () => { - const comments = { - saved_objects: mockCaseComments.map((obj) => ({ ...obj, score: 1 })), - total: mockCaseComments.length, - per_page: 10, - page: 1, - }; - - const res = transformComments(comments); - expect(res).toEqual({ - page: 1, - per_page: 10, - total: mockCaseComments.length, - comments: flattenCommentSavedObjects(comments.saved_objects), - }); - }); - }); - - describe('flattenCommentSavedObjects', () => { - it('flattens correctly', () => { - const comments = [{ ...mockCaseComments[0] }, { ...mockCaseComments[1] }]; - const res = flattenCommentSavedObjects(comments); - expect(res).toEqual([ - flattenCommentSavedObject(comments[0]), - flattenCommentSavedObject(comments[1]), - ]); - }); - }); - - describe('flattenCommentSavedObject', () => { - it('flattens correctly', () => { - const comment = { ...mockCaseComments[0] }; - const res = flattenCommentSavedObject(comment); - expect(res).toEqual({ - id: comment.id, - version: comment.version, - ...comment.attributes, - }); - }); - - it('flattens correctly without version', () => { - const comment = { ...mockCaseComments[0] }; - comment.version = undefined; - const res = flattenCommentSavedObject(comment); - expect(res).toEqual({ - id: comment.id, - version: '0', - ...comment.attributes, - }); - }); - }); - - describe('sortToSnake', () => { - it('it transforms status correctly', () => { - expect(sortToSnake('status')).toBe('status'); - }); - - it('it transforms createdAt correctly', () => { - expect(sortToSnake('createdAt')).toBe('created_at'); - }); - - it('it transforms created_at correctly', () => { - expect(sortToSnake('created_at')).toBe('created_at'); - }); - - it('it transforms closedAt correctly', () => { - expect(sortToSnake('closedAt')).toBe('closed_at'); - }); - - it('it transforms closed_at correctly', () => { - expect(sortToSnake('closed_at')).toBe('closed_at'); - }); - - it('it transforms default correctly', () => { - expect(sortToSnake('not-exist')).toBe('created_at'); - }); - }); }); diff --git a/x-pack/plugins/cases/server/routes/api/utils.ts b/x-pack/plugins/cases/server/routes/api/utils.ts index 8e8862f4157f1a..f7a77a5dbf3919 100644 --- a/x-pack/plugins/cases/server/routes/api/utils.ts +++ b/x-pack/plugins/cases/server/routes/api/utils.ts @@ -5,180 +5,12 @@ * 2.0. */ -import { isEmpty } from 'lodash'; -import { badRequest, Boom, boomify, isBoom } from '@hapi/boom'; -import { fold } from 'fp-ts/lib/Either'; -import { identity } from 'fp-ts/lib/function'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { schema } from '@kbn/config-schema'; -import { - CustomHttpResponseOptions, - ResponseError, - SavedObject, - SavedObjectsFindResponse, -} from 'kibana/server'; - -import { - CaseResponse, - CasesFindResponse, - CommentResponse, - CommentsResponse, - CommentAttributes, - ESCaseConnector, - ESCaseAttributes, - CommentRequest, - ContextTypeUserRt, - CommentRequestUserType, - CommentRequestAlertType, - CommentType, - excess, - throwErrors, - CaseStatuses, - CasesClientPostRequest, - AssociationType, - SubCaseAttributes, - SubCaseResponse, - SubCasesFindResponse, - User, - AlertCommentRequestRt, -} from '../../../common/api'; -import { transformESConnectorToCaseConnector } from './cases/helpers'; +import { Boom, boomify, isBoom } from '@hapi/boom'; -import { SortFieldCase } from './types'; -import { AlertInfo } from '../../common'; +import { schema } from '@kbn/config-schema'; +import { CustomHttpResponseOptions, ResponseError } from 'kibana/server'; import { isCaseError } from '../../common/error'; -export const transformNewSubCase = ({ - createdAt, - createdBy, -}: { - createdAt: string; - createdBy: User; -}): SubCaseAttributes => { - return { - closed_at: null, - closed_by: null, - created_at: createdAt, - created_by: createdBy, - status: CaseStatuses.open, - updated_at: null, - updated_by: null, - }; -}; - -export const transformNewCase = ({ - connector, - createdDate, - email, - // eslint-disable-next-line @typescript-eslint/naming-convention - full_name, - newCase, - username, -}: { - connector: ESCaseConnector; - createdDate: string; - email?: string | null; - full_name?: string | null; - newCase: CasesClientPostRequest; - username?: string | null; -}): ESCaseAttributes => ({ - ...newCase, - closed_at: null, - closed_by: null, - connector, - created_at: createdDate, - created_by: { email, full_name, username }, - external_service: null, - status: CaseStatuses.open, - updated_at: null, - updated_by: null, -}); - -type NewCommentArgs = CommentRequest & { - associationType: AssociationType; - createdDate: string; - email?: string | null; - full_name?: string | null; - username?: string | null; -}; - -/** - * Return the alert IDs from the comment if it is an alert style comment. Otherwise return an empty array. - */ -export const getAlertIds = (comment: CommentRequest): string[] => { - if (isCommentRequestTypeAlertOrGenAlert(comment)) { - return Array.isArray(comment.alertId) ? comment.alertId : [comment.alertId]; - } - return []; -}; - -const getIDsAndIndicesAsArrays = ( - comment: CommentRequestAlertType -): { ids: string[]; indices: string[] } => { - return { - ids: Array.isArray(comment.alertId) ? comment.alertId : [comment.alertId], - indices: Array.isArray(comment.index) ? comment.index : [comment.index], - }; -}; - -/** - * This functions extracts the ids and indices from an alert comment. It enforces that the alertId and index are either - * both strings or string arrays that are the same length. If they are arrays they represent a 1-to-1 mapping of - * id existing in an index at each position in the array. This is not ideal. Ideally an alert comment request would - * accept an array of objects like this: Array<{id: string; index: string; ruleName: string ruleID: string}> instead. - * - * To reformat the alert comment request requires a migration and a breaking API change. - */ -const getAndValidateAlertInfoFromComment = (comment: CommentRequest): AlertInfo[] => { - if (!isCommentRequestTypeAlertOrGenAlert(comment)) { - return []; - } - - const { ids, indices } = getIDsAndIndicesAsArrays(comment); - - if (ids.length !== indices.length) { - return []; - } - - return ids.map((id, index) => ({ id, index: indices[index] })); -}; - -/** - * Builds an AlertInfo object accumulating the alert IDs and indices for the passed in alerts. - */ -export const getAlertInfoFromComments = (comments: CommentRequest[] | undefined): AlertInfo[] => { - if (comments === undefined) { - return []; - } - - return comments.reduce((acc: AlertInfo[], comment) => { - const alertInfo = getAndValidateAlertInfoFromComment(comment); - acc.push(...alertInfo); - return acc; - }, []); -}; - -export const transformNewComment = ({ - associationType, - createdDate, - email, - // eslint-disable-next-line @typescript-eslint/naming-convention - full_name, - username, - ...comment -}: NewCommentArgs): CommentAttributes => { - return { - associationType, - ...comment, - created_at: createdDate, - created_by: { email, full_name, username }, - pushed_at: null, - pushed_by: null, - updated_at: null, - updated_by: null, - }; -}; - /** * Transforms an error into the correct format for a kibana response. */ @@ -199,222 +31,4 @@ export function wrapError(error: any): CustomHttpResponseOptions }; } -export const transformCases = ({ - casesMap, - countOpenCases, - countInProgressCases, - countClosedCases, - page, - perPage, - total, -}: { - casesMap: Map; - countOpenCases: number; - countInProgressCases: number; - countClosedCases: number; - page: number; - perPage: number; - total: number; -}): CasesFindResponse => ({ - page, - per_page: perPage, - total, - cases: Array.from(casesMap.values()), - count_open_cases: countOpenCases, - count_in_progress_cases: countInProgressCases, - count_closed_cases: countClosedCases, -}); - -export const transformSubCases = ({ - subCasesMap, - open, - inProgress, - closed, - page, - perPage, - total, -}: { - subCasesMap: Map; - open: number; - inProgress: number; - closed: number; - page: number; - perPage: number; - total: number; -}): SubCasesFindResponse => ({ - page, - per_page: perPage, - total, - // Squish all the entries in the map together as one array - subCases: Array.from(subCasesMap.values()).flat(), - count_open_cases: open, - count_in_progress_cases: inProgress, - count_closed_cases: closed, -}); - -export const flattenCaseSavedObject = ({ - savedObject, - comments = [], - totalComment = comments.length, - totalAlerts = 0, - subCases, - subCaseIds, -}: { - savedObject: SavedObject; - comments?: Array>; - totalComment?: number; - totalAlerts?: number; - subCases?: SubCaseResponse[]; - subCaseIds?: string[]; -}): CaseResponse => ({ - id: savedObject.id, - version: savedObject.version ?? '0', - comments: flattenCommentSavedObjects(comments), - totalComment, - totalAlerts, - ...savedObject.attributes, - connector: transformESConnectorToCaseConnector(savedObject.attributes.connector), - subCases, - subCaseIds: !isEmpty(subCaseIds) ? subCaseIds : undefined, -}); - -export const flattenSubCaseSavedObject = ({ - savedObject, - comments = [], - totalComment = comments.length, - totalAlerts = 0, -}: { - savedObject: SavedObject; - comments?: Array>; - totalComment?: number; - totalAlerts?: number; -}): SubCaseResponse => ({ - id: savedObject.id, - version: savedObject.version ?? '0', - comments: flattenCommentSavedObjects(comments), - totalComment, - totalAlerts, - ...savedObject.attributes, -}); - -export const transformComments = ( - comments: SavedObjectsFindResponse -): CommentsResponse => ({ - page: comments.page, - per_page: comments.per_page, - total: comments.total, - comments: flattenCommentSavedObjects(comments.saved_objects), -}); - -export const flattenCommentSavedObjects = ( - savedObjects: Array> -): CommentResponse[] => - savedObjects.reduce((acc: CommentResponse[], savedObject: SavedObject) => { - return [...acc, flattenCommentSavedObject(savedObject)]; - }, []); - -export const flattenCommentSavedObject = ( - savedObject: SavedObject -): CommentResponse => ({ - id: savedObject.id, - version: savedObject.version ?? '0', - ...savedObject.attributes, -}); - -export const sortToSnake = (sortField: string | undefined): SortFieldCase => { - switch (sortField) { - case 'status': - return SortFieldCase.status; - case 'createdAt': - case 'created_at': - return SortFieldCase.createdAt; - case 'closedAt': - case 'closed_at': - return SortFieldCase.closedAt; - default: - return SortFieldCase.createdAt; - } -}; - export const escapeHatch = schema.object({}, { unknowns: 'allow' }); - -/** - * A type narrowing function for user comments. Exporting so integration tests can use it. - */ -export const isCommentRequestTypeUser = ( - context: CommentRequest -): context is CommentRequestUserType => { - return context.type === CommentType.user; -}; - -/** - * A type narrowing function for alert comments. Exporting so integration tests can use it. - */ -export const isCommentRequestTypeAlertOrGenAlert = ( - context: CommentRequest -): context is CommentRequestAlertType => { - return context.type === CommentType.alert || context.type === CommentType.generatedAlert; -}; - -/** - * This is used to test if the posted comment is an generated alert. A generated alert will have one or many alerts. - * An alert is essentially an object with a _id field. This differs from a regular attached alert because the _id is - * passed directly in the request, it won't be in an object. Internally case will strip off the outer object and store - * both a generated and user attached alert in the same structure but this function is useful to determine which - * structure the new alert in the request has. - */ -export const isCommentRequestTypeGenAlert = ( - context: CommentRequest -): context is CommentRequestAlertType => { - return context.type === CommentType.generatedAlert; -}; - -export const decodeCommentRequest = (comment: CommentRequest) => { - if (isCommentRequestTypeUser(comment)) { - pipe(excess(ContextTypeUserRt).decode(comment), fold(throwErrors(badRequest), identity)); - } else if (isCommentRequestTypeAlertOrGenAlert(comment)) { - pipe(excess(AlertCommentRequestRt).decode(comment), fold(throwErrors(badRequest), identity)); - const { ids, indices } = getIDsAndIndicesAsArrays(comment); - - /** - * The alertId and index field must either be both of type string or they must both be string[] and be the same length. - * Having a one-to-one relationship between the id and index of an alert avoids accidentally updating or - * retrieving the wrong alert. Elasticsearch only guarantees that the _id (the field we use for alertId) to be - * unique within a single index. So if we attempt to update or get a specific alert across multiple indices we could - * update or receive the wrong one. - * - * Consider the situation where we have a alert1 with _id = '100' in index 'my-index-awesome' and also in index - * 'my-index-hi'. - * If we attempt to update the status of alert1 using an index pattern like `my-index-*` or even providing multiple - * indices, there's a chance we'll accidentally update too many alerts. - * - * This check doesn't enforce that the API request has the correct alert ID to index relationship it just guards - * against accidentally making a request like: - * { - * alertId: [1,2,3], - * index: awesome, - * } - * - * Instead this requires the requestor to provide: - * { - * alertId: [1,2,3], - * index: [awesome, awesome, awesome] - * } - * - * Ideally we'd change the format of the comment request to be an array of objects like: - * { - * alerts: [{id: 1, index: awesome}, {id: 2, index: awesome}] - * } - * - * But we'd need to also implement a migration because the saved object document currently stores the id and index - * in separate fields. - */ - if (ids.length !== indices.length) { - throw badRequest( - `Received an alert comment with ids and indices arrays of different lengths ids: ${JSON.stringify( - ids - )} indices: ${JSON.stringify(indices)}` - ); - } - } -}; diff --git a/x-pack/plugins/cases/server/services/cases/index.ts b/x-pack/plugins/cases/server/services/cases/index.ts index 99d6129dc54b3e..c7d94b3c66329d 100644 --- a/x-pack/plugins/cases/server/services/cases/index.ts +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -31,19 +31,17 @@ import { CaseResponse, caseTypeField, CasesFindRequest, + CaseStatuses, } from '../../../common/api'; import { defaultSortField, + flattenCaseSavedObject, + flattenSubCaseSavedObject, groupTotalAlertsByID, SavedObjectFindOptionsKueryNode, } from '../../common'; import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; import { defaultPage, defaultPerPage } from '../../routes/api'; -import { - flattenCaseSavedObject, - flattenSubCaseSavedObject, - transformNewSubCase, -} from '../../routes/api/utils'; import { CASE_SAVED_OBJECT, CASE_COMMENT_SAVED_OBJECT, @@ -174,6 +172,24 @@ interface CasesMapWithPageInfo { type FindCaseOptions = CasesFindRequest & SavedObjectFindOptionsKueryNode; +const transformNewSubCase = ({ + createdAt, + createdBy, +}: { + createdAt: string; + createdBy: User; +}): SubCaseAttributes => { + return { + closed_at: null, + closed_by: null, + created_at: createdAt, + created_by: createdBy, + status: CaseStatuses.open, + updated_at: null, + updated_by: null, + }; +}; + export class CaseService { constructor( private readonly log: Logger, diff --git a/x-pack/plugins/cases/server/services/user_actions/helpers.ts b/x-pack/plugins/cases/server/services/user_actions/helpers.ts index ebfdcd9792f317..e987bd1685405f 100644 --- a/x-pack/plugins/cases/server/services/user_actions/helpers.ts +++ b/x-pack/plugins/cases/server/services/user_actions/helpers.ts @@ -18,16 +18,14 @@ import { UserActionFieldType, SubCaseAttributes, } from '../../../common/api'; -import { - isTwoArraysDifference, - transformESConnectorToCaseConnector, -} from '../../routes/api/cases/helpers'; +import { isTwoArraysDifference } from '../../client/utils'; import { UserActionItem } from '.'; import { CASE_SAVED_OBJECT, CASE_COMMENT_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT, } from '../../../common/constants'; +import { transformESConnectorToCaseConnector } from '../../common'; export const transformNewUserAction = ({ actionField, diff --git a/x-pack/test/case_api_integration/common/fixtures/plugins/observability/kibana.json b/x-pack/test/case_api_integration/common/fixtures/plugins/observability/kibana.json index b4b540fc9a821d..5115f4e3a0d3ba 100644 --- a/x-pack/test/case_api_integration/common/fixtures/plugins/observability/kibana.json +++ b/x-pack/test/case_api_integration/common/fixtures/plugins/observability/kibana.json @@ -3,7 +3,7 @@ "version": "1.0.0", "kibanaVersion": "kibana", "configPath": ["xpack"], - "requiredPlugins": ["features"], + "requiredPlugins": ["features", "cases"], "optionalPlugins": ["security", "spaces"], "server": true, "ui": false diff --git a/x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/kibana.json b/x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/kibana.json index 000848e771af31..cdef22263b01e1 100644 --- a/x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/kibana.json +++ b/x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/kibana.json @@ -3,7 +3,7 @@ "version": "1.0.0", "kibanaVersion": "kibana", "configPath": ["xpack"], - "requiredPlugins": ["features"], + "requiredPlugins": ["features", "cases"], "optionalPlugins": ["security", "spaces"], "server": true, "ui": false 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 32094e60832a97..2ff5e9d71985b0 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -614,7 +614,9 @@ export const deleteCases = async ({ }) => { const { body } = await supertest .delete(`${CASES_URL}`) - .query({ ids: caseIDs }) + // 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) }) .set('kbn-xsrf', 'true') .send() .expect(expectedHttpCode); 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 ca16416991cbf3..8239cbadbaa2f7 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 @@ -72,7 +72,7 @@ export default ({ getService }: FtrProviderContext): void => { .send() .expect(400); - expect(body.message).to.contain('subCaseId'); + expect(body.message).to.contain('disabled'); }); it('unhappy path - 404s when case is not there', async () => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts index 8394109ce6696f..cd4e72f6f93159 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts @@ -72,7 +72,7 @@ export default ({ getService }: FtrProviderContext): void => { .send() .expect(400); // make sure the failure is because of the subCaseId - expect(body.message).to.contain('subCaseId'); + expect(body.message).to.contain('disabled'); }); it('should return a 400 when attempting to delete a single comment when passing the `subCaseId` parameter', async () => { @@ -82,7 +82,7 @@ export default ({ getService }: FtrProviderContext): void => { .send() .expect(400); // make sure the failure is because of the subCaseId - expect(body.message).to.contain('subCaseId'); + expect(body.message).to.contain('disabled'); }); }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts index 95f15d1e330ffe..43e128c1e41fa4 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts @@ -118,7 +118,7 @@ export default ({ getService }: FtrProviderContext): void => { .send() .expect(400); - expect(body.message).to.contain('subCaseId'); + expect(body.message).to.contain('disabled'); }); // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts index 06eb9d0fb4174c..736d04f43ed051 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts @@ -47,7 +47,7 @@ export default ({ getService }: FtrProviderContext): void => { .send() .expect(400); - expect(body.message).to.contain('subCaseId'); + expect(body.message).to.contain('disabled'); }); it('should return a 400 when passing the includeSubCaseComments parameter', async () => { @@ -57,7 +57,7 @@ export default ({ getService }: FtrProviderContext): void => { .send() .expect(400); - expect(body.message).to.contain('includeSubCaseComments'); + expect(body.message).to.contain('disabled'); }); // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts index e843b31d18dfd3..441f01843f8650 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts @@ -8,7 +8,6 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; import { postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; import { createCaseAction, diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts index b82800b6bd7a6d..b73b89d33e9c60 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts @@ -21,7 +21,6 @@ import { postCaseReq, postCommentUserReq, postCommentAlertReq, - postCommentGenAlertReq, } from '../../../../common/lib/mock'; import { createCaseAction, @@ -65,7 +64,7 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(400); - expect(body.message).to.contain('subCaseId'); + expect(body.message).to.contain('disabled'); }); // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts index 0d11edc5587d14..56a6d1b15004b8 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts @@ -31,7 +31,7 @@ export default ({ getService }: FtrProviderContext): void => { await deleteCasesUserActions(es); }); - it(`on new case, user action: 'create' should be called with actionFields: ['description', 'status', 'tags', 'title', 'connector', 'settings]`, async () => { + it(`on new case, user action: 'create' should be called with actionFields: ['description', 'status', 'tags', 'title', 'connector', 'settings, owner]`, async () => { const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') @@ -53,6 +53,7 @@ export default ({ getService }: FtrProviderContext): void => { 'title', 'connector', 'settings', + 'owner', ]); expect(body[0].action).to.eql('create'); expect(body[0].old_value).to.eql(null); From 676173ec0da6ee66915095d7dab5c7a335376b95 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Tue, 20 Apr 2021 11:32:56 -0400 Subject: [PATCH 048/113] [Cases] Refactoring authorization (#97483) * Refactoring authorization * Wrapping auth calls in helper for try catch * Reverting name change * Hardcoding the saved object types * Switching ensure to owner array --- x-pack/plugins/cases/common/constants.ts | 3 + .../server/authorization/audit_logger.ts | 44 ++---- .../server/authorization/authorization.ts | 41 +++--- .../cases/server/client/cases/create.ts | 17 +-- .../plugins/cases/server/client/cases/find.ts | 40 ++---- x-pack/plugins/cases/server/client/utils.ts | 125 ++++++++++++++++++ x-pack/plugins/cases/server/common/utils.ts | 49 +------ .../plugins/observability/server/plugin.ts | 14 +- .../security_solution/server/plugin.ts | 13 +- 9 files changed, 211 insertions(+), 135 deletions(-) diff --git a/x-pack/plugins/cases/common/constants.ts b/x-pack/plugins/cases/common/constants.ts index 8489787bc5a6f9..ed759a6c641688 100644 --- a/x-pack/plugins/cases/common/constants.ts +++ b/x-pack/plugins/cases/common/constants.ts @@ -14,6 +14,9 @@ export const CASE_USER_ACTION_SAVED_OBJECT = 'cases-user-actions'; export const CASE_COMMENT_SAVED_OBJECT = 'cases-comments'; export const CASE_CONFIGURE_SAVED_OBJECT = 'cases-configure'; +/** + * If more values are added here please also add them here: x-pack/test/case_api_integration/common/fixtures/plugins + */ export const SAVED_OBJECT_TYPES = [ CASE_SAVED_OBJECT, CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, diff --git a/x-pack/plugins/cases/server/authorization/audit_logger.ts b/x-pack/plugins/cases/server/authorization/audit_logger.ts index 3c890a2c7ad5be..2a739ea6e81067 100644 --- a/x-pack/plugins/cases/server/authorization/audit_logger.ts +++ b/x-pack/plugins/cases/server/authorization/audit_logger.ts @@ -20,19 +20,19 @@ export class AuthorizationAuditLogger { this.auditLogger = logger; } - private createMessage({ + private static createMessage({ result, - owner, + owners, operation, }: { result: AuthorizationResult; - owner?: string; + owners?: string[]; operation: OperationDetails; }): string { - const ownerMsg = owner == null ? 'of any owner' : `with "${owner}" as the owner`; + const ownerMsg = owners == null ? 'of any owner' : `with owners: "${owners.join(', ')}"`; /** * This will take the form: - * `Unauthorized to create case with "securitySolution" as the owner` + * `Unauthorized to create case with owners: "securitySolution, observability"` * `Unauthorized to find cases of any owner`. */ return `${result} to ${operation.verbs.present} ${operation.docType} ${ownerMsg}`; @@ -65,16 +65,16 @@ export class AuthorizationAuditLogger { public failure({ username, - owner, + owners, operation, }: { username?: string; - owner?: string; + owners?: string[]; operation: OperationDetails; }): string { - const message = this.createMessage({ + const message = AuthorizationAuditLogger.createMessage({ result: AuthorizationResult.Unauthorized, - owner, + owners, operation, }); this.auditLogger?.log({ @@ -96,24 +96,6 @@ export class AuthorizationAuditLogger { } public success({ - username, - operation, - owner, - }: { - username: string; - owner: string; - operation: OperationDetails; - }): string { - const message = this.createMessage({ - result: AuthorizationResult.Authorized, - owner, - operation, - }); - this.logSuccessEvent({ message, operation, username }); - return message; - } - - public bulkSuccess({ username, operation, owners, @@ -122,9 +104,11 @@ export class AuthorizationAuditLogger { owners: string[]; operation: OperationDetails; }): string { - const message = `${AuthorizationResult.Authorized} to ${operation.verbs.present} ${ - operation.docType - } of owner: ${owners.join(', ')}`; + const message = AuthorizationAuditLogger.createMessage({ + result: AuthorizationResult.Authorized, + owners, + operation, + }); this.logSuccessEvent({ message, operation, username }); return message; } diff --git a/x-pack/plugins/cases/server/authorization/authorization.ts b/x-pack/plugins/cases/server/authorization/authorization.ts index 5a1d6af0f4a061..adb684c60a1bd2 100644 --- a/x-pack/plugins/cases/server/authorization/authorization.ts +++ b/x-pack/plugins/cases/server/authorization/authorization.ts @@ -11,7 +11,7 @@ import { SecurityPluginStart } from '../../../security/server'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; import { AuthorizationFilter, GetSpaceFn } from './types'; import { getOwnersFilter } from './utils'; -import { AuthorizationAuditLogger, OperationDetails, Operations } from '.'; +import { AuthorizationAuditLogger, OperationDetails } from '.'; /** * This class handles ensuring that the user making a request has the correct permissions @@ -79,19 +79,21 @@ export class Authorization { return this.securityAuth?.mode?.useRbacForRequest(this.request) ?? false; } - public async ensureAuthorized(owner: string, operation: OperationDetails) { + public async ensureAuthorized(owners: string[], operation: OperationDetails) { const { securityAuth } = this; - const isOwnerAvailable = this.featureCaseOwners.has(owner); + const areAllOwnersAvailable = owners.every((owner) => this.featureCaseOwners.has(owner)); if (securityAuth && this.shouldCheckAuthorization()) { - const requiredPrivileges: string[] = [securityAuth.actions.cases.get(owner, operation.name)]; + const requiredPrivileges: string[] = owners.map((owner) => + securityAuth.actions.cases.get(owner, operation.name) + ); const checkPrivileges = securityAuth.checkPrivilegesDynamicallyWithRequest(this.request); const { hasAllRequested, username } = await checkPrivileges({ kibana: requiredPrivileges, }); - if (!isOwnerAvailable) { + if (!areAllOwnersAvailable) { /** * Under most circumstances this would have been caught by `checkPrivileges` as * a user can't have Privileges to an unknown owner, but super users @@ -99,24 +101,25 @@ export class Authorization { * as Privileged. * This check will ensure we don't accidentally let these through */ - throw Boom.forbidden(this.auditLogger.failure({ username, owner, operation })); + throw Boom.forbidden(this.auditLogger.failure({ username, owners, operation })); } if (hasAllRequested) { - this.auditLogger.success({ username, operation, owner }); + this.auditLogger.success({ username, operation, owners }); } else { - throw Boom.forbidden(this.auditLogger.failure({ owner, operation, username })); + throw Boom.forbidden(this.auditLogger.failure({ owners, operation, username })); } - } else if (!isOwnerAvailable) { - throw Boom.forbidden(this.auditLogger.failure({ owner, operation })); + } else if (!areAllOwnersAvailable) { + throw Boom.forbidden(this.auditLogger.failure({ owners, operation })); } // else security is disabled so let the operation proceed } - public async getFindAuthorizationFilter(savedObjectType: string): Promise { + public async getFindAuthorizationFilter( + operation: OperationDetails + ): Promise { const { securityAuth } = this; - const operation = Operations.findCases; if (securityAuth && this.shouldCheckAuthorization()) { const { username, authorizedOwners } = await this.getAuthorizedOwners([operation]); @@ -125,15 +128,17 @@ export class Authorization { } return { - filter: getOwnersFilter(savedObjectType, authorizedOwners), + filter: getOwnersFilter(operation.savedObjectType, authorizedOwners), ensureSavedObjectIsAuthorized: (owner: string) => { if (!authorizedOwners.includes(owner)) { - throw Boom.forbidden(this.auditLogger.failure({ username, operation, owner })); + throw Boom.forbidden( + this.auditLogger.failure({ username, operation, owners: [owner] }) + ); } }, logSuccessfulAuthorization: () => { if (authorizedOwners.length) { - this.auditLogger.bulkSuccess({ username, owners: authorizedOwners, operation }); + this.auditLogger.success({ username, owners: authorizedOwners, operation }); } }, }; @@ -155,11 +160,11 @@ export class Authorization { const { securityAuth, featureCaseOwners } = this; if (securityAuth && this.shouldCheckAuthorization()) { const checkPrivileges = securityAuth.checkPrivilegesDynamicallyWithRequest(this.request); - const requiredPrivileges = new Map(); + const requiredPrivileges = new Map(); for (const owner of featureCaseOwners) { for (const operation of operations) { - requiredPrivileges.set(securityAuth.actions.cases.get(owner, operation.name), [owner]); + requiredPrivileges.set(securityAuth.actions.cases.get(owner, operation.name), owner); } } @@ -174,7 +179,7 @@ export class Authorization { ? Array.from(featureCaseOwners) : privileges.kibana.reduce((authorizedOwners, { authorized, privilege }) => { if (authorized && requiredPrivileges.has(privilege)) { - const [owner] = requiredPrivileges.get(privilege)!; + const owner = requiredPrivileges.get(privilege)!; authorizedOwners.push(owner); } diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index d4c3ba52095839..2109424575ed35 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -28,7 +28,7 @@ import { User, } from '../../../common/api'; import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; -import { getConnectorFromConfiguration } from '../utils'; +import { createAuditMsg, ensureAuthorized, getConnectorFromConfiguration } from '../utils'; import { CaseConfigureService, CaseService, CaseUserActionService } from '../../services'; import { createCaseError } from '../../common/error'; @@ -37,7 +37,6 @@ import { Operations } from '../../authorization'; import { AuditLogger, EventOutcome } from '../../../../security/server'; import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; import { - createAuditMsg, flattenCaseSavedObject, transformCaseConnectorToEsConnector, transformNewCase, @@ -89,12 +88,14 @@ export const create = async ({ try { const savedObjectID = SavedObjectsUtils.generateId(); - try { - await auth.ensureAuthorized(query.owner, Operations.createCase); - } catch (error) { - auditLogger?.log(createAuditMsg({ operation: Operations.createCase, error, savedObjectID })); - throw error; - } + + await ensureAuthorized({ + operation: Operations.createCase, + owners: [query.owner], + authorization: auth, + auditLogger, + savedObjectIDs: [savedObjectID], + }); // log that we're attempting to create a case auditLogger?.log( diff --git a/x-pack/plugins/cases/server/client/cases/find.ts b/x-pack/plugins/cases/server/client/cases/find.ts index b3c201f65f212c..8334beb102cb92 100644 --- a/x-pack/plugins/cases/server/client/cases/find.ts +++ b/x-pack/plugins/cases/server/client/cases/find.ts @@ -22,15 +22,14 @@ import { excess, } from '../../../common/api'; -import { CASE_SAVED_OBJECT } from '../../../common/constants'; import { CaseService } from '../../services'; import { createCaseError } from '../../common/error'; -import { constructQueryOptions } from '../utils'; +import { constructQueryOptions, getAuthorizationFilter } from '../utils'; import { Authorization } from '../../authorization/authorization'; import { includeFieldsRequiredForAuthentication } from '../../authorization/utils'; -import { AuthorizationFilter, Operations } from '../../authorization'; +import { Operations } from '../../authorization'; import { AuditLogger } from '../../../../security/server'; -import { createAuditMsg, transformCases } from '../../common'; +import { transformCases } from '../../common'; interface FindParams { savedObjectsClient: SavedObjectsClientContract; @@ -49,8 +48,8 @@ export const find = async ({ caseService, logger, auth, - auditLogger, options, + auditLogger, }: FindParams): Promise => { try { const queryParams = pipe( @@ -58,19 +57,15 @@ export const find = async ({ fold(throwErrors(Boom.badRequest), identity) ); - let authFindHelpers: AuthorizationFilter; - try { - authFindHelpers = await auth.getFindAuthorizationFilter(CASE_SAVED_OBJECT); - } catch (error) { - auditLogger?.log(createAuditMsg({ operation: Operations.findCases, error })); - throw error; - } - const { filter: authorizationFilter, - ensureSavedObjectIsAuthorized, + ensureSavedObjectsAreAuthorized, logSuccessfulAuthorization, - } = authFindHelpers; + } = await getAuthorizationFilter({ + authorization: auth, + operation: Operations.findCases, + auditLogger, + }); const queryArgs = { tags: queryParams.tags, @@ -100,20 +95,7 @@ export const find = async ({ subCaseOptions: caseQueries.subCase, }); - for (const theCase of cases.casesMap.values()) { - try { - ensureSavedObjectIsAuthorized(theCase.owner); - // log each of the found cases - auditLogger?.log( - createAuditMsg({ operation: Operations.findCases, savedObjectID: theCase.id }) - ); - } catch (error) { - auditLogger?.log( - createAuditMsg({ operation: Operations.findCases, error, savedObjectID: theCase.id }) - ); - throw error; - } - } + ensureSavedObjectsAreAuthorized([...cases.casesMap.values()]); // TODO: Make sure we do not leak information when authorization is on const [openCases, inProgressCases, closedCases] = await Promise.all([ diff --git a/x-pack/plugins/cases/server/client/utils.ts b/x-pack/plugins/cases/server/client/utils.ts index c56e1178e96c8d..0dcbf61fa08942 100644 --- a/x-pack/plugins/cases/server/client/utils.ts +++ b/x-pack/plugins/cases/server/client/utils.ts @@ -13,6 +13,7 @@ import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; import { SavedObjectsFindResponse } from 'kibana/server'; +import { PublicMethodsOf } from '@kbn/utility-types'; import { nodeBuilder, KueryNode } from '../../../../../src/plugins/data/common'; import { CaseConnector, @@ -27,6 +28,7 @@ import { AlertCommentRequestRt, } from '../../common/api'; import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../common/constants'; +import { AuditEvent, EventCategory, EventOutcome } from '../../../security/server'; import { combineFilterWithAuthorizationFilter } from '../authorization/utils'; import { getIDsAndIndicesAsArrays, @@ -34,6 +36,8 @@ import { isCommentRequestTypeUser, SavedObjectFindOptionsKueryNode, } from '../common'; +import { Authorization, OperationDetails } from '../authorization'; +import { AuditLogger } from '../../../security/server'; export const decodeCommentRequest = (comment: CommentRequest) => { if (isCommentRequestTypeUser(comment)) { @@ -443,3 +447,124 @@ export const sortToSnake = (sortField: string | undefined): SortFieldCase => { return SortFieldCase.createdAt; } }; + +/** + * Wraps the Authorization class' ensureAuthorized call in a try/catch to handle the audit logging + * on a failure. + */ +export async function ensureAuthorized({ + owners, + operation, + savedObjectIDs, + authorization, + auditLogger, +}: { + owners: string[]; + operation: OperationDetails; + savedObjectIDs: string[]; + authorization: PublicMethodsOf; + auditLogger?: AuditLogger; +}) { + try { + return await authorization.ensureAuthorized(owners, operation); + } catch (error) { + for (const savedObjectID of savedObjectIDs) { + auditLogger?.log(createAuditMsg({ operation, error, savedObjectID })); + } + throw error; + } +} + +/** + * Describes an entity with the necessary fields to identify if the user is authorized to interact with the saved object + * returned from some find query. + */ +interface OwnerEntity { + owner: string; + id: string; +} + +/** + * Wraps the Authorization class' method for determining which found saved objects the user making the request + * is authorized to interact with. + */ +export async function getAuthorizationFilter({ + operation, + authorization, + auditLogger, +}: { + operation: OperationDetails; + authorization: PublicMethodsOf; + auditLogger?: AuditLogger; +}) { + try { + const { + filter, + ensureSavedObjectIsAuthorized, + logSuccessfulAuthorization, + } = await authorization.getFindAuthorizationFilter(operation); + return { + filter, + ensureSavedObjectsAreAuthorized: (entities: OwnerEntity[]) => { + for (const entity of entities) { + try { + ensureSavedObjectIsAuthorized(entity.owner); + auditLogger?.log(createAuditMsg({ operation, savedObjectID: entity.id })); + } catch (error) { + auditLogger?.log(createAuditMsg({ error, operation, savedObjectID: entity.id })); + } + } + }, + logSuccessfulAuthorization, + }; + } catch (error) { + auditLogger?.log(createAuditMsg({ error, operation })); + throw error; + } +} + +/** + * Creates an AuditEvent describing the state of a request. + */ +export function createAuditMsg({ + operation, + outcome, + error, + savedObjectID, +}: { + operation: OperationDetails; + savedObjectID?: string; + outcome?: EventOutcome; + error?: Error; +}): AuditEvent { + const doc = + savedObjectID != null + ? `${operation.savedObjectType} [id=${savedObjectID}]` + : `a ${operation.docType}`; + const message = error + ? `Failed attempt to ${operation.verbs.present} ${doc}` + : outcome === EventOutcome.UNKNOWN + ? `User is ${operation.verbs.progressive} ${doc}` + : `User has ${operation.verbs.past} ${doc}`; + + return { + message, + event: { + action: operation.action, + category: EventCategory.DATABASE, + type: operation.type, + outcome: outcome ?? (error ? EventOutcome.FAILURE : EventOutcome.SUCCESS), + }, + ...(savedObjectID != null && { + kibana: { + saved_object: { type: operation.savedObjectType, id: savedObjectID }, + }, + }), + ...(error != null && { + error: { + code: error.name, + message: error.message, + }, + }), + }; +} diff --git a/x-pack/plugins/cases/server/common/utils.ts b/x-pack/plugins/cases/server/common/utils.ts index def25b8c7acec6..c4cad60f4d465b 100644 --- a/x-pack/plugins/cases/server/common/utils.ts +++ b/x-pack/plugins/cases/server/common/utils.ts @@ -9,7 +9,7 @@ import Boom from '@hapi/boom'; import { SavedObjectsFindResult, SavedObjectsFindResponse, SavedObject } from 'kibana/server'; import { isEmpty } from 'lodash'; import { AlertInfo } from '.'; -import { AuditEvent, EventCategory, EventOutcome } from '../../../security/server'; + import { AssociationType, CaseConnector, @@ -34,7 +34,6 @@ import { User, } from '../../common/api'; import { ENABLE_CASE_CONNECTOR } from '../../common/constants'; -import { OperationDetails } from '../authorization'; import { UpdateAlertRequest } from '../client/alerts/client'; /** @@ -416,52 +415,6 @@ export const countAlertsForID = ({ return groupTotalAlertsByID({ comments }).get(id); }; -/** - * Creates an AuditEvent describing the state of a request. - */ -export function createAuditMsg({ - operation, - outcome, - error, - savedObjectID, -}: { - operation: OperationDetails; - savedObjectID?: string; - outcome?: EventOutcome; - error?: Error; -}): AuditEvent { - const doc = - savedObjectID != null - ? `${operation.savedObjectType} [id=${savedObjectID}]` - : `a ${operation.docType}`; - const message = error - ? `Failed attempt to ${operation.verbs.present} ${doc}` - : outcome === EventOutcome.UNKNOWN - ? `User is ${operation.verbs.progressive} ${doc}` - : `User has ${operation.verbs.past} ${doc}`; - - return { - message, - event: { - action: operation.action, - category: EventCategory.DATABASE, - type: operation.type, - outcome: outcome ?? (error ? EventOutcome.FAILURE : EventOutcome.SUCCESS), - }, - ...(savedObjectID != null && { - kibana: { - saved_object: { type: operation.savedObjectType, id: savedObjectID }, - }, - }), - ...(error != null && { - error: { - code: error.name, - message: error.message, - }, - }), - }; -} - /** * If subCaseID is defined and the case connector feature is disabled this throws an error. */ diff --git a/x-pack/test/case_api_integration/common/fixtures/plugins/observability/server/plugin.ts b/x-pack/test/case_api_integration/common/fixtures/plugins/observability/server/plugin.ts index 802c823202b76e..9ce9d0e1ae1d1e 100644 --- a/x-pack/test/case_api_integration/common/fixtures/plugins/observability/server/plugin.ts +++ b/x-pack/test/case_api_integration/common/fixtures/plugins/observability/server/plugin.ts @@ -10,7 +10,6 @@ import { Plugin, CoreSetup } from 'kibana/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../../plugins/features/server'; import { SpacesPluginStart } from '../../../../../../../plugins/spaces/server'; import { SecurityPluginStart } from '../../../../../../../plugins/security/server'; -import { SAVED_OBJECT_TYPES as casesSavedObjectTypes } from '../../../../../../../plugins/cases/common/constants'; export interface FixtureSetupDeps { features: FeaturesPluginSetup; @@ -21,6 +20,19 @@ export interface FixtureStartDeps { spaces?: SpacesPluginStart; } +/** + * These are a copy of the values here: x-pack/plugins/cases/common/constants.ts because when the plugin attempts to + * import them from the constants.ts file it gets an error. + */ +const casesSavedObjectTypes = [ + 'cases', + 'cases-connector-mappings', + 'cases-sub-case', + 'cases-user-actions', + 'cases-comments', + 'cases-configure', +]; + export class FixturePlugin implements Plugin { public setup(core: CoreSetup, deps: FixtureSetupDeps) { const { features } = deps; diff --git a/x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/server/plugin.ts b/x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/server/plugin.ts index 46432a2507cb69..f4f8510a7d9b6b 100644 --- a/x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/server/plugin.ts +++ b/x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/server/plugin.ts @@ -10,7 +10,6 @@ import { Plugin, CoreSetup } from 'kibana/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../../plugins/features/server'; import { SpacesPluginStart } from '../../../../../../../plugins/spaces/server'; import { SecurityPluginStart } from '../../../../../../../plugins/security/server'; -import { SAVED_OBJECT_TYPES as casesSavedObjectTypes } from '../../../../../../../plugins/cases/common/constants'; export interface FixtureSetupDeps { features: FeaturesPluginSetup; @@ -21,6 +20,18 @@ export interface FixtureStartDeps { spaces?: SpacesPluginStart; } +/** + * These are a copy of the values here: x-pack/plugins/cases/common/constants.ts because when the plugin attempts to + * import them from the constants.ts file it gets an error. + */ +const casesSavedObjectTypes = [ + 'cases', + 'cases-connector-mappings', + 'cases-sub-case', + 'cases-user-actions', + 'cases-comments', + 'cases-configure', +]; export class FixturePlugin implements Plugin { public setup(core: CoreSetup, deps: FixtureSetupDeps) { const { features } = deps; From 6cdfa848914da80e24e0df90fbf0dad9db156a24 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 26 Apr 2021 19:45:52 +0300 Subject: [PATCH 049/113] [Cases] Add authorization to configuration & cases routes (#97228) --- x-pack/plugins/cases/common/api/cases/case.ts | 9 + .../cases/common/api/cases/configure.ts | 21 +- .../cases/common/api/connectors/mappings.ts | 1 + .../plugins/cases/common/api/runtime_types.ts | 4 + x-pack/plugins/cases/common/constants.ts | 1 + .../cases/server/authorization/index.ts | 42 +- .../cases/server/authorization/types.ts | 8 + .../cases/server/client/alerts/client.ts | 20 +- .../plugins/cases/server/client/alerts/get.ts | 17 +- .../server/client/alerts/update_status.ts | 17 +- .../cases/server/client/attachments/add.ts | 14 +- .../cases/server/client/attachments/client.ts | 24 +- .../cases/server/client/cases/client.ts | 90 +---- .../cases/server/client/cases/create.ts | 53 +-- .../cases/server/client/cases/delete.ts | 51 ++- .../plugins/cases/server/client/cases/find.ts | 33 +- .../plugins/cases/server/client/cases/get.ts | 172 +++++++-- .../plugins/cases/server/client/cases/push.ts | 60 ++- .../cases/server/client/cases/update.ts | 30 +- .../cases/server/client/configure/client.ts | 359 ++++++++++++++---- .../client/configure/create_mappings.ts | 55 +++ .../server/client/configure/get_fields.ts | 14 +- .../server/client/configure/get_mappings.ts | 56 +-- .../cases/server/client/configure/types.ts | 24 ++ .../client/configure/update_mappings.ts | 55 +++ .../server/client/user_actions/client.ts | 14 +- .../cases/server/client/user_actions/get.ts | 19 +- x-pack/plugins/cases/server/client/utils.ts | 18 + .../api/__fixtures__/mock_saved_objects.ts | 2 + .../api/cases/reporters/get_reporters.ts | 14 +- .../server/routes/api/cases/tags/get_tags.ts | 14 +- .../comments/delete_all_comments.ts | 6 +- .../{cases => }/comments/delete_comment.ts | 6 +- .../api/{cases => }/comments/find_comments.ts | 8 +- .../{cases => }/comments/get_all_comment.ts | 6 +- .../api/{cases => }/comments/get_comment.ts | 6 +- .../api/{cases => }/comments/patch_comment.ts | 8 +- .../api/{cases => }/comments/post_comment.ts | 8 +- .../{cases => }/configure/get_configure.ts | 14 +- .../{cases => }/configure/get_connectors.ts | 6 +- .../{cases => }/configure/patch_configure.ts | 23 +- .../{cases => }/configure/post_configure.ts | 8 +- .../plugins/cases/server/routes/api/index.ts | 34 +- .../api/{cases/status => stats}/get_status.ts | 6 +- .../{cases => }/sub_case/delete_sub_cases.ts | 6 +- .../{cases => }/sub_case/find_sub_cases.ts | 8 +- .../api/{cases => }/sub_case/get_sub_case.ts | 6 +- .../{cases => }/sub_case/patch_sub_cases.ts | 8 +- .../user_actions/get_all_user_actions.ts | 6 +- .../cases/server/services/cases/index.ts | 55 ++- .../server/services/cases/read_reporters.ts | 47 --- .../cases/server/services/cases/read_tags.ts | 60 --- .../cases/server/services/configure/index.ts | 52 ++- .../services/connector_mappings/index.ts | 33 +- x-pack/plugins/cases/server/services/mocks.ts | 6 +- .../feature_privilege_builder/cases.test.ts | 32 ++ .../feature_privilege_builder/cases.ts | 16 +- .../cases/containers/configure/api.test.ts | 2 +- .../public/cases/containers/configure/mock.ts | 5 + .../cases/containers/configure/types.ts | 2 + .../containers/configure/use_configure.tsx | 2 + .../common/lib/authentication/index.ts | 22 +- .../common/lib/authentication/types.ts | 6 + .../case_api_integration/common/lib/utils.ts | 204 ++++++---- .../tests/common/cases/delete_cases.ts | 176 ++++++++- .../tests/common/cases/find_cases.ts | 287 ++++++++------ .../tests/common/cases/get_case.ts | 125 +++++- .../tests/common/cases/post_case.ts | 56 +-- .../common/cases/reporters/get_reporters.ts | 186 ++++++++- .../tests/common/cases/tags/get_tags.ts | 195 +++++++++- .../tests/common/configure/get_configure.ts | 186 ++++++++- .../tests/common/configure/patch_configure.ts | 230 ++++++++++- .../tests/common/configure/post_configure.ts | 211 +++++++++- 73 files changed, 2730 insertions(+), 950 deletions(-) create mode 100644 x-pack/plugins/cases/server/client/configure/create_mappings.ts create mode 100644 x-pack/plugins/cases/server/client/configure/types.ts create mode 100644 x-pack/plugins/cases/server/client/configure/update_mappings.ts rename x-pack/plugins/cases/server/routes/api/{cases => }/comments/delete_all_comments.ts (89%) rename x-pack/plugins/cases/server/routes/api/{cases => }/comments/delete_comment.ts (89%) rename x-pack/plugins/cases/server/routes/api/{cases => }/comments/find_comments.ts (89%) rename x-pack/plugins/cases/server/routes/api/{cases => }/comments/get_all_comment.ts (90%) rename x-pack/plugins/cases/server/routes/api/{cases => }/comments/get_comment.ts (87%) rename x-pack/plugins/cases/server/routes/api/{cases => }/comments/patch_comment.ts (90%) rename x-pack/plugins/cases/server/routes/api/{cases => }/comments/post_comment.ts (90%) rename x-pack/plugins/cases/server/routes/api/{cases => }/configure/get_configure.ts (63%) rename x-pack/plugins/cases/server/routes/api/{cases => }/configure/get_connectors.ts (84%) rename x-pack/plugins/cases/server/routes/api/{cases => }/configure/patch_configure.ts (61%) rename x-pack/plugins/cases/server/routes/api/{cases => }/configure/post_configure.ts (86%) rename x-pack/plugins/cases/server/routes/api/{cases/status => stats}/get_status.ts (84%) rename x-pack/plugins/cases/server/routes/api/{cases => }/sub_case/delete_sub_cases.ts (86%) rename x-pack/plugins/cases/server/routes/api/{cases => }/sub_case/find_sub_cases.ts (89%) rename x-pack/plugins/cases/server/routes/api/{cases => }/sub_case/get_sub_case.ts (89%) rename x-pack/plugins/cases/server/routes/api/{cases => }/sub_case/patch_sub_cases.ts (79%) rename x-pack/plugins/cases/server/routes/api/{cases => }/user_actions/get_all_user_actions.ts (95%) delete mode 100644 x-pack/plugins/cases/server/services/cases/read_reporters.ts delete mode 100644 x-pack/plugins/cases/server/services/cases/read_tags.ts diff --git a/x-pack/plugins/cases/common/api/cases/case.ts b/x-pack/plugins/cases/common/api/cases/case.ts index a8b0717104304e..389caffee1a5cc 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 b5a89efde17678..02e2cb65962308 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 3d2013af476880..e0fdd2d7e62dc4 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 9785c0f4107440..c3202cca6718f3 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 ed759a6c641688..9eb100edeee468 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 3203398ff51a55..9f30e8cf7a8da5 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 dfa06c0277bda2..19dc95982613ff 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 88298450e499a4..186f914aa2cd72 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 e02a98c396e0a9..3c7f60ecae15d0 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 e77115ba4e2289..cb0d7ef5a1e149 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 27fb5e1cf61f04..7ffbb8684f9590 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 423863528184a1..fd2f148d304abb 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 2109424575ed35..15fbd34628182c 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 1bc94b5a0b4c85..4657df2e71b301 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 8334beb102cb92..988812da0d852a 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 58fff0d5e435d9..73ca65d52e5666 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 ae690c8b6a086b..b7f416203e078f 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 dcd66ebbcae260..402e6726a71cd1 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 2b9048a4518e92..1037a2ff9d8938 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 00000000000000..73fd59e15da53a --- /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 8a6b20256328fd..78627cfaca6ed1 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 4f8b8c6cbf32a5..31435e7c7cdb28 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 00000000000000..a34251690db48d --- /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 00000000000000..d7acbbd5f74f7c --- /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 8098714f8f9554..909c5337853020 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 4a8d1101d19cf2..dac997c3fa90a2 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 0dcbf61fa08942..b61de9f2beb6ae 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 bb4e529192df33..933a59cf060165 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 2836c7572e810e..a7a0e4f8bb141b 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 e13974b514c083..a62c3247b01dff 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 08c4491f7b1518..a41d4683af2d0e 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 284013ff36c095..f145fc62efc8a9 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 b7b8a3b44146f7..c992e7d0c114cb 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 7777a0b36a1f11..b916e22c6b0ed2 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 cf6f7d62dcf6e0..09805c00cb10a0 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 28852eca3af419..aecdeb46756c07 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 7dbfb2a62c46fa..1919aef7b72b4e 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 933a53eb8a8705..8222ac8fe56909 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 be05d1c3b82303..46c110bbb8ba52 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 d32c7151f6df5f..49288c72eadeec 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 ca25a29d6a1dee..fe8ffedbc85f6b 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 c5b7aa85dc33e0..f05bd3b229256c 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 6ba59635807823..3d9dc73860ef94 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 4f4870496f77ff..45899735ddb04f 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 80cfbbd6b584f8..8243e4a9529938 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 44ec5d68e9653f..db3e29f5ed96e7 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 c1cd4b317da9bb..ce03c3bf970ab3 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 07f1353f19854b..5944ff6176d780 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 c7d94b3c66329d..2362d893739a07 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 f7e88c2649ae68..00000000000000 --- 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 a977c473327f86..00000000000000 --- 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 45a9cd714145ff..28e9af01f9d735 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 0d51e12a55ac76..44892336458213 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 5e5b4ff31309e6..2b58cd023a8ad5 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 1b1932f8640906..4ca2bd01d9a2d0 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 8608653c41b345..1ff72e9ad3fe1a 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 0c7ae422be861f..999cb8d29d7452 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 4e71c9a990eced..a76ca16d799aae 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 aa86d1bfdb0b14..b628705569bd02 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 2ec2a73363bfec..ca817747e91914 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 f7a54244b3bf58..bcc23896f85f8b 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 2b61ae992fa64b..3bf3629441f931 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 2ff5e9d71985b0..0a0151d37d3f8e 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 2c50ac8a453f9b..9ebc16f5e07aa5 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 ca3b0201c14545..c537d2477cb597 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 8239cbadbaa2f7..187c84be7c1962 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 1971cb5398b526..f2b9027cfb1f16 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 c811c0982840e2..e34d9ccad39ac6 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 a47cf12158a34e..0c7237683666fd 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 1f36ecc812c5fa..b26e8a3f3b3812 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 8901447e37b3ae..c76e5f408e4757 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 c74e048edcfa03..a47c10efe5037b 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', + ]); + }); + }); }); }; From 103388e2b9cf359a3119e17f691839936596a9ad Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Fri, 30 Apr 2021 09:29:20 -0400 Subject: [PATCH 050/113] [Cases] Attachments RBAC (#97756) * Starting rbac for comments * Adding authorization to rest of comment apis * Starting the comment rbac tests * Fixing some of the rbac tests * Adding some integration tests * Starting patch tests * Working tests for comments * Working tests * Fixing some tests * Fixing type issues from pulling in master * Fixing connector tests that only work in trial license * Attempting to fix cypress * Mock return of array for configure * Fixing cypress test * Cleaning up * Addressing PR comments * Reducing operations --- .../plugins/cases/common/api/cases/comment.ts | 10 + .../cases/common/api/cases/user_actions.ts | 1 + x-pack/plugins/cases/common/api/helpers.ts | 5 + .../server/authorization/audit_logger.ts | 16 +- .../cases/server/authorization/index.ts | 130 +++++- .../cases/server/authorization/types.ts | 51 ++- .../cases/server/authorization/utils.ts | 22 +- .../cases/server/client/attachments/add.ts | 95 +++-- .../cases/server/client/attachments/client.ts | 12 +- .../cases/server/client/attachments/delete.ts | 28 +- .../cases/server/client/attachments/get.ts | 102 ++++- .../cases/server/client/attachments/update.ts | 17 +- .../cases/server/client/cases/create.ts | 12 +- .../cases/server/client/cases/delete.ts | 14 +- .../plugins/cases/server/client/cases/find.ts | 4 +- .../plugins/cases/server/client/cases/mock.ts | 3 + .../cases/server/client/cases/utils.ts | 4 + .../cases/server/client/configure/client.ts | 20 - x-pack/plugins/cases/server/client/utils.ts | 200 +++++---- .../server/common/models/commentable_case.ts | 11 + .../plugins/cases/server/common/utils.test.ts | 20 +- x-pack/plugins/cases/server/common/utils.ts | 1 + .../server/connectors/case/index.test.ts | 4 + .../cases/server/connectors/case/index.ts | 1 + .../cases/server/connectors/case/schema.ts | 3 + x-pack/plugins/cases/server/plugin.ts | 2 +- .../api/__fixtures__/mock_saved_objects.ts | 6 + .../routes/api/comments/find_comments.ts | 11 +- .../cases/server/scripts/sub_cases/index.ts | 1 + .../server/services/attachments/index.ts | 4 +- .../cases/server/services/cases/index.ts | 8 +- .../server/services/user_actions/helpers.ts | 1 + .../feature_privilege_builder/cases.test.ts | 28 +- .../feature_privilege_builder/cases.ts | 5 +- .../integration/cases/connectors.spec.ts | 24 +- .../security_solution/cypress/objects/case.ts | 2 + .../cypress/tasks/api_calls/cases.ts | 1 + .../components/add_comment/index.test.tsx | 1 + .../cases/components/add_comment/index.tsx | 3 +- .../components/case_view/helpers.test.tsx | 2 + .../configure_cases/__mock__/index.tsx | 1 + .../add_to_case_action.test.tsx | 3 + .../timeline_actions/add_to_case_action.tsx | 2 + .../public/cases/containers/api.test.tsx | 1 + .../cases/containers/configure/api.test.ts | 6 +- .../public/cases/containers/configure/api.ts | 29 +- .../configure/use_configure.test.tsx | 2 + .../containers/configure/use_configure.tsx | 31 +- .../public/cases/containers/mock.ts | 3 + .../containers/use_post_comment.test.tsx | 1 + .../public/cases/containers/utils.ts | 10 + .../common/lib/authentication/index.ts | 18 +- .../common/lib/authentication/roles.ts | 2 +- .../case_api_integration/common/lib/mock.ts | 3 + .../case_api_integration/common/lib/utils.ts | 136 ++++-- .../tests/basic/configure/create_connector.ts | 20 + .../tests/common/cases/delete_cases.ts | 19 +- .../tests/common/cases/find_cases.ts | 26 +- .../tests/common/cases/get_case.ts | 16 +- .../tests/common/cases/patch_cases.ts | 148 ++++--- .../tests/common/comments/delete_comment.ts | 239 ++++++++++- .../tests/common/comments/find_comments.ts | 243 ++++++++++- .../tests/common/comments/get_all_comments.ts | 123 +++++- .../tests/common/comments/get_comment.ts | 119 +++++- .../tests/common/comments/patch_comment.ts | 393 ++++++++++++++---- .../tests/common/comments/post_comment.ts | 262 +++++++++--- .../tests/common/configure/get_configure.ts | 67 --- .../tests/common/configure/get_connectors.ts | 70 +--- .../tests/common/configure/migrations.ts | 7 +- .../tests/common/configure/patch_configure.ts | 122 ------ .../tests/common/configure/post_configure.ts | 61 --- .../tests/common/connectors/case.ts | 1 - .../tests/common/sub_cases/find_sub_cases.ts | 1 + .../tests/common/sub_cases/patch_sub_cases.ts | 5 + .../user_actions/get_all_user_actions.ts | 18 +- .../tests/trial/cases/push_case.ts | 2 +- .../tests/trial/configure/get_configure.ts | 95 +++++ .../tests/trial/configure/get_connectors.ts | 16 +- .../tests/trial/configure/index.ts | 18 + .../tests/trial/configure/patch_configure.ts | 162 ++++++++ .../tests/trial/configure/post_configure.ts | 95 +++++ .../security_and_spaces/tests/trial/index.ts | 1 + 82 files changed, 2594 insertions(+), 888 deletions(-) create mode 100644 x-pack/test/case_api_integration/security_and_spaces/tests/basic/configure/create_connector.ts create mode 100644 x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_configure.ts create mode 100644 x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/index.ts create mode 100644 x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/patch_configure.ts create mode 100644 x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/post_configure.ts diff --git a/x-pack/plugins/cases/common/api/cases/comment.ts b/x-pack/plugins/cases/common/api/cases/comment.ts index 4eb2ad1eadd6cb..089bba8615725a 100644 --- a/x-pack/plugins/cases/common/api/cases/comment.ts +++ b/x-pack/plugins/cases/common/api/cases/comment.ts @@ -6,6 +6,7 @@ */ import * as rt from 'io-ts'; +import { SavedObjectFindOptionsRt } from '../saved_object'; import { UserRT } from '../user'; @@ -27,6 +28,7 @@ export const CommentAttributesBasicRt = rt.type({ ]), created_at: rt.string, created_by: UserRT, + owner: rt.string, pushed_at: rt.union([rt.string, rt.null]), pushed_by: rt.union([UserRT, rt.null]), updated_at: rt.union([rt.string, rt.null]), @@ -42,6 +44,7 @@ export enum CommentType { export const ContextTypeUserRt = rt.type({ comment: rt.string, type: rt.literal(CommentType.user), + owner: rt.string, }); /** @@ -57,6 +60,7 @@ export const AlertCommentRequestRt = rt.type({ id: rt.union([rt.string, rt.null]), name: rt.union([rt.string, rt.null]), }), + owner: rt.string, }); const AttributesTypeUserRt = rt.intersection([ContextTypeUserRt, CommentAttributesBasicRt]); @@ -112,6 +116,12 @@ export const CommentsResponseRt = rt.type({ export const AllCommentsResponseRt = rt.array(CommentResponseRt); +export const FindQueryParamsRt = rt.partial({ + ...SavedObjectFindOptionsRt.props, + subCaseId: rt.string, +}); + +export type FindQueryParams = rt.TypeOf; export type AttributesTypeAlerts = rt.TypeOf; export type AttributesTypeUser = rt.TypeOf; export type CommentAttributes = rt.TypeOf; diff --git a/x-pack/plugins/cases/common/api/cases/user_actions.ts b/x-pack/plugins/cases/common/api/cases/user_actions.ts index 55dfac391f3be3..1b53adb002436d 100644 --- a/x-pack/plugins/cases/common/api/cases/user_actions.ts +++ b/x-pack/plugins/cases/common/api/cases/user_actions.ts @@ -22,6 +22,7 @@ const UserActionFieldTypeRt = rt.union([ rt.literal('status'), rt.literal('settings'), rt.literal('sub_case'), + rt.literal('owner'), ]); const UserActionFieldRt = rt.array(UserActionFieldTypeRt); const UserActionRt = rt.union([ diff --git a/x-pack/plugins/cases/common/api/helpers.ts b/x-pack/plugins/cases/common/api/helpers.ts index 43e292b91db4b7..6b5f126c74fdb7 100644 --- a/x-pack/plugins/cases/common/api/helpers.ts +++ b/x-pack/plugins/cases/common/api/helpers.ts @@ -14,6 +14,7 @@ import { SUB_CASES_URL, CASE_PUSH_URL, SUB_CASE_USER_ACTIONS_URL, + CASE_CONFIGURE_DETAILS_URL, } from '../constants'; export const getCaseDetailsUrl = (id: string): string => { @@ -47,3 +48,7 @@ export const getSubCaseUserActionUrl = (caseID: string, subCaseId: string): stri export const getCasePushUrl = (caseId: string, connectorId: string): string => { return CASE_PUSH_URL.replace('{case_id}', caseId).replace('{connector_id}', connectorId); }; + +export const getCaseConfigurationDetailsUrl = (configureID: string): string => { + return CASE_CONFIGURE_DETAILS_URL.replace('{configuration_id}', configureID); +}; diff --git a/x-pack/plugins/cases/server/authorization/audit_logger.ts b/x-pack/plugins/cases/server/authorization/audit_logger.ts index 2a739ea6e81067..216cf7d9c20e00 100644 --- a/x-pack/plugins/cases/server/authorization/audit_logger.ts +++ b/x-pack/plugins/cases/server/authorization/audit_logger.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { OperationDetails } from '.'; -import { AuditLogger, EventCategory, EventOutcome } from '../../../security/server'; +import { DATABASE_CATEGORY, ECS_OUTCOMES, OperationDetails } from '.'; +import { AuditLogger } from '../../../security/server'; enum AuthorizationResult { Unauthorized = 'Unauthorized', @@ -51,9 +51,9 @@ export class AuthorizationAuditLogger { message: `${username ?? 'unknown user'} ${message}`, event: { action: operation.action, - category: EventCategory.DATABASE, - type: operation.type, - outcome: EventOutcome.SUCCESS, + category: DATABASE_CATEGORY, + type: [operation.type], + outcome: ECS_OUTCOMES.success, }, ...(username != null && { user: { @@ -81,9 +81,9 @@ export class AuthorizationAuditLogger { message: `${username ?? 'unknown user'} ${message}`, event: { action: operation.action, - category: EventCategory.DATABASE, - type: operation.type, - outcome: EventOutcome.FAILURE, + category: DATABASE_CATEGORY, + type: [operation.type], + outcome: ECS_OUTCOMES.failure, }, // add the user information if we have it ...(username != null && { diff --git a/x-pack/plugins/cases/server/authorization/index.ts b/x-pack/plugins/cases/server/authorization/index.ts index 9f30e8cf7a8da5..be8ca55ccd262a 100644 --- a/x-pack/plugins/cases/server/authorization/index.ts +++ b/x-pack/plugins/cases/server/authorization/index.ts @@ -5,8 +5,12 @@ * 2.0. */ -import { EventType } from '../../../security/server'; -import { CASE_CONFIGURE_SAVED_OBJECT, CASE_SAVED_OBJECT } from '../../common/constants'; +import { EcsEventCategory, EcsEventOutcome, EcsEventType } from 'kibana/server'; +import { + CASE_COMMENT_SAVED_OBJECT, + CASE_CONFIGURE_SAVED_OBJECT, + CASE_SAVED_OBJECT, +} from '../../common/constants'; import { Verbs, ReadOperations, WriteOperations, OperationDetails } from './types'; export * from './authorization'; @@ -37,13 +41,44 @@ const deleteVerbs: Verbs = { past: 'deleted', }; +const EVENT_TYPES: Record = { + creation: 'creation', + deletion: 'deletion', + change: 'change', + access: 'access', +}; + +/** + * These values need to match the respective values in this file: x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts + * These are shared between find, get, get all, and delete/delete all + * There currently isn't a use case for a user to delete one comment but not all or differentiating between get, get all, + * and find operations from a privilege stand point. + */ +const DELETE_COMMENT_OPERATION = 'deleteComment'; +const ACCESS_COMMENT_OPERATION = 'getComment'; +const ACCESS_CASE_OPERATION = 'getCase'; + +/** + * Database constant for ECS category for use for audit logging. + */ +export const DATABASE_CATEGORY: EcsEventCategory[] = ['database']; + +/** + * ECS Outcomes for audit logging. + */ +export const ECS_OUTCOMES: Record = { + failure: 'failure', + success: 'success', + unknown: 'unknown', +}; + /** * Definition of all APIs within the cases backend. */ export const Operations: Record = { // case operations [WriteOperations.CreateCase]: { - type: EventType.CREATION, + type: EVENT_TYPES.creation, name: WriteOperations.CreateCase, action: 'create-case', verbs: createVerbs, @@ -51,7 +86,7 @@ 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 +/** + * Read operations for the cases APIs. + * + * NOTE: 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', + GetComment = 'getComment', + GetAllComments = 'getAllComments', + FindComments = 'findComments', GetTags = 'getTags', GetReporters = 'getReporters', FindConfigurations = 'findConfigurations', } -// TODO: comments +/** + * Write operations for the cases APIs. + * + * NOTE: 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 WriteOperations { CreateCase = 'createCase', DeleteCase = 'deleteCase', UpdateCase = 'updateCase', + CreateComment = 'createComment', + DeleteAllComments = 'deleteAllComments', + DeleteComment = 'deleteComment', + UpdateComment = 'updateComment', CreateConfiguration = 'createConfiguration', UpdateConfiguration = 'updateConfiguration', } @@ -47,11 +59,30 @@ export enum WriteOperations { * Defines the structure for a case API route. */ export interface OperationDetails { - type: EventType; - name: ReadOperations | WriteOperations; + /** + * The ECS event type that this operation should be audit logged as (creation, deletion, access, etc) + */ + type: EcsEventType; + /** + * The name of the operation to authorize against for the privilege check. + * These values need to match one of the operation strings defined here: x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts + */ + name: string; + /** + * The ECS `event.action` field, should be in the form of - e.g get-comment, find-cases + */ action: string; + /** + * The verbs that are associated with this type of operation, these should line up with the event type e.g. creating, created, create etc + */ verbs: Verbs; + /** + * The readable name of the entity being operated on e.g. case, comment, configurations (make it plural if it reads better that way etc) + */ docType: string; + /** + * The actual saved object type of the entity e.g. cases, cases-comments + */ savedObjectType: string; } diff --git a/x-pack/plugins/cases/server/authorization/utils.ts b/x-pack/plugins/cases/server/authorization/utils.ts index a7e210d07d214f..11d143eb05b2a2 100644 --- a/x-pack/plugins/cases/server/authorization/utils.ts +++ b/x-pack/plugins/cases/server/authorization/utils.ts @@ -19,10 +19,18 @@ export const getOwnersFilter = (savedObjectType: string, owners: string[]): Kuer }; export const combineFilterWithAuthorizationFilter = ( - filter: KueryNode, - authorizationFilter: KueryNode + filter?: KueryNode, + authorizationFilter?: KueryNode ) => { - return nodeBuilder.and([filter, authorizationFilter]); + if (!filter && !authorizationFilter) { + return; + } + + const kueries = [ + ...(filter !== undefined ? [filter] : []), + ...(authorizationFilter !== undefined ? [authorizationFilter] : []), + ]; + return nodeBuilder.and(kueries); }; export const ensureFieldIsSafeForQuery = (field: string, value: string): boolean => { @@ -41,5 +49,9 @@ export const ensureFieldIsSafeForQuery = (field: string, value: string): boolean return true; }; -export const includeFieldsRequiredForAuthentication = (fields: string[]): string[] => - uniq([...fields, 'owner']); +export const includeFieldsRequiredForAuthentication = (fields?: string[]): string[] | undefined => { + if (fields === undefined) { + return; + } + return uniq([...fields, 'owner']); +}; diff --git a/x-pack/plugins/cases/server/client/attachments/add.ts b/x-pack/plugins/cases/server/client/attachments/add.ts index cb0d7ef5a1e149..4cc9ca7f868ec1 100644 --- a/x-pack/plugins/cases/server/client/attachments/add.ts +++ b/x-pack/plugins/cases/server/client/attachments/add.ts @@ -10,7 +10,12 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { SavedObject, SavedObjectsClientContract, Logger } from 'src/core/server'; +import { + SavedObject, + SavedObjectsClientContract, + Logger, + SavedObjectsUtils, +} from '../../../../../../src/core/server'; import { nodeBuilder } from '../../../../../../src/plugins/data/common'; import { @@ -20,11 +25,10 @@ import { CaseStatuses, CaseType, SubCaseAttributes, - CommentRequest, CaseResponse, User, - CommentRequestAlertType, AlertCommentRequestRt, + CommentRequest, } from '../../../common/api'; import { buildCaseUserActionItem, @@ -45,7 +49,8 @@ import { ENABLE_CASE_CONNECTOR, } from '../../../common/constants'; -import { decodeCommentRequest } from '../utils'; +import { decodeCommentRequest, ensureAuthorized } from '../utils'; +import { Operations } from '../../authorization'; async function getSubCase({ caseService, @@ -106,27 +111,21 @@ async function getSubCase({ return newSubCase; } -interface AddCommentFromRuleArgs { - casesClientInternal: CasesClientInternal; - caseId: string; - comment: CommentRequestAlertType; - savedObjectsClient: SavedObjectsClientContract; - attachmentService: AttachmentService; - caseService: CaseService; - userActionService: CaseUserActionService; - logger: Logger; -} +const addGeneratedAlerts = async ( + { caseId, comment }: AddArgs, + clientArgs: CasesClientArgs, + casesClientInternal: CasesClientInternal +): Promise => { + const { + savedObjectsClient, + attachmentService, + caseService, + userActionService, + logger, + auditLogger, + authorization, + } = clientArgs; -const addGeneratedAlerts = async ({ - savedObjectsClient, - attachmentService, - caseService, - userActionService, - casesClientInternal, - caseId, - comment, - logger, -}: AddCommentFromRuleArgs): Promise => { const query = pipe( AlertCommentRequestRt.decode(comment), fold(throwErrors(Boom.badRequest), identity) @@ -141,6 +140,15 @@ const addGeneratedAlerts = async ({ try { const createdDate = new Date().toISOString(); + const savedObjectID = SavedObjectsUtils.generateId(); + + await ensureAuthorized({ + authorization, + auditLogger, + owners: [comment.owner], + savedObjectIDs: [savedObjectID], + operation: Operations.createComment, + }); const caseInfo = await caseService.getCase({ soClient: savedObjectsClient, @@ -181,7 +189,12 @@ const addGeneratedAlerts = async ({ const { comment: newComment, commentableCase: updatedCase, - } = await commentableCase.createComment({ createdDate, user: userDetails, commentReq: query }); + } = await commentableCase.createComment({ + createdDate, + user: userDetails, + commentReq: query, + id: savedObjectID, + }); if ( (newComment.attributes.type === CommentType.alert || @@ -283,16 +296,20 @@ async function getCombinedCase({ } } -interface AddCommentArgs { +/** + * The arguments needed for creating a new attachment to a case. + */ +export interface AddArgs { caseId: string; comment: CommentRequest; } export const addComment = async ( - { caseId, comment }: AddCommentArgs, + addArgs: AddArgs, clientArgs: CasesClientArgs, casesClientInternal: CasesClientInternal ): Promise => { + const { comment, caseId } = addArgs; const query = pipe( CommentRequestRt.decode(comment), fold(throwErrors(Boom.badRequest), identity) @@ -305,6 +322,8 @@ export const addComment = async ( attachmentService, user, logger, + authorization, + auditLogger, } = clientArgs; if (isCommentRequestTypeGenAlert(comment)) { @@ -314,20 +333,21 @@ export const addComment = async ( ); } - return addGeneratedAlerts({ - caseId, - comment, - casesClientInternal, - savedObjectsClient, - userActionService, - caseService, - attachmentService, - logger, - }); + return addGeneratedAlerts(addArgs, clientArgs, casesClientInternal); } decodeCommentRequest(comment); try { + const savedObjectID = SavedObjectsUtils.generateId(); + + await ensureAuthorized({ + authorization, + auditLogger, + operation: Operations.createComment, + owners: [comment.owner], + savedObjectIDs: [savedObjectID], + }); + const createdDate = new Date().toISOString(); const combinedCase = await getCombinedCase({ @@ -350,6 +370,7 @@ export const addComment = async ( createdDate, user: userInfo, commentReq: query, + id: savedObjectID, }); if (newComment.attributes.type === CommentType.alert && updatedCase.settings.syncAlerts) { diff --git a/x-pack/plugins/cases/server/client/attachments/client.ts b/x-pack/plugins/cases/server/client/attachments/client.ts index 7ffbb8684f9590..41f1db81719fc2 100644 --- a/x-pack/plugins/cases/server/client/attachments/client.ts +++ b/x-pack/plugins/cases/server/client/attachments/client.ts @@ -8,25 +8,19 @@ import { AllCommentsResponse, CaseResponse, - CommentRequest as AttachmentsRequest, CommentResponse, CommentsResponse, } from '../../../common/api'; import { CasesClientInternal } from '../client_internal'; import { CasesClientArgs } from '../types'; -import { addComment } from './add'; +import { AddArgs, addComment } from './add'; import { DeleteAllArgs, deleteAll, DeleteArgs, deleteComment } from './delete'; import { find, FindArgs, get, getAll, GetAllArgs, GetArgs } from './get'; import { update, UpdateArgs } from './update'; -interface AttachmentsAdd { - caseId: string; - comment: AttachmentsRequest; -} - export interface AttachmentsSubClient { - add(params: AttachmentsAdd): Promise; + add(params: AddArgs): Promise; deleteAll(deleteAllArgs: DeleteAllArgs): Promise; delete(deleteArgs: DeleteArgs): Promise; find(findArgs: FindArgs): Promise; @@ -40,7 +34,7 @@ export const createAttachmentsSubClient = ( casesClientInternal: CasesClientInternal ): AttachmentsSubClient => { const attachmentSubClient: AttachmentsSubClient = { - add: (params: AttachmentsAdd) => addComment(params, clientArgs, casesClientInternal), + add: (params: AddArgs) => addComment(params, clientArgs, casesClientInternal), deleteAll: (deleteAllArgs: DeleteAllArgs) => deleteAll(deleteAllArgs, clientArgs), delete: (deleteArgs: DeleteArgs) => deleteComment(deleteArgs, clientArgs), find: (findArgs: FindArgs) => find(findArgs, clientArgs), diff --git a/x-pack/plugins/cases/server/client/attachments/delete.ts b/x-pack/plugins/cases/server/client/attachments/delete.ts index 37069b94df7cbd..f600aef64d1b68 100644 --- a/x-pack/plugins/cases/server/client/attachments/delete.ts +++ b/x-pack/plugins/cases/server/client/attachments/delete.ts @@ -13,6 +13,8 @@ import { CasesClientArgs } from '../types'; import { buildCommentUserActionItem } from '../../services/user_actions/helpers'; import { createCaseError } from '../../common/error'; import { checkEnabledCaseConnectorOrThrow } from '../../common'; +import { ensureAuthorized } from '../utils'; +import { Operations } from '../../authorization'; /** * Parameters for deleting all comments of a case or sub case. @@ -45,6 +47,8 @@ export async function deleteAll( attachmentService, userActionService, logger, + authorization, + auditLogger, } = clientArgs; try { @@ -57,6 +61,18 @@ export async function deleteAll( associationType: subCaseID ? AssociationType.subCase : AssociationType.case, }); + if (comments.total <= 0) { + throw Boom.notFound(`No comments found for ${id}.`); + } + + await ensureAuthorized({ + authorization, + auditLogger, + operation: Operations.deleteAllComments, + savedObjectIDs: comments.saved_objects.map((comment) => comment.id), + owners: comments.saved_objects.map((comment) => comment.attributes.owner), + }); + await Promise.all( comments.saved_objects.map((comment) => attachmentService.delete({ @@ -101,6 +117,8 @@ export async function deleteComment( attachmentService, userActionService, logger, + authorization, + auditLogger, } = clientArgs; try { @@ -117,6 +135,14 @@ export async function deleteComment( throw Boom.notFound(`This comment ${attachmentID} does not exist anymore.`); } + await ensureAuthorized({ + authorization, + auditLogger, + owners: [myComment.attributes.owner], + savedObjectIDs: [myComment.id], + operation: Operations.deleteComment, + }); + const type = subCaseID ? SUB_CASE_SAVED_OBJECT : CASE_SAVED_OBJECT; const id = subCaseID ?? caseID; @@ -146,7 +172,7 @@ export async function deleteComment( }); } catch (error) { throw createCaseError({ - message: `Failed to delete comment in route case id: ${caseID} comment id: ${attachmentID} sub case id: ${subCaseID}: ${error}`, + message: `Failed to delete comment: ${caseID} comment id: ${attachmentID} sub case id: ${subCaseID}: ${error}`, error, logger, }); diff --git a/x-pack/plugins/cases/server/client/attachments/get.ts b/x-pack/plugins/cases/server/client/attachments/get.ts index 70aeb5a3df2aa9..f6f5bcfb4f0462 100644 --- a/x-pack/plugins/cases/server/client/attachments/get.ts +++ b/x-pack/plugins/cases/server/client/attachments/get.ts @@ -5,11 +5,9 @@ * 2.0. */ import Boom from '@hapi/boom'; -import * as rt from 'io-ts'; import { SavedObjectsFindResponse } from 'kibana/server'; import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; -import { esKuery } from '../../../../../../src/plugins/data/server'; import { AllCommentsResponse, AllCommentsResponseRt, @@ -19,7 +17,7 @@ import { CommentResponseRt, CommentsResponse, CommentsResponseRt, - SavedObjectFindOptionsRt, + FindQueryParams, } from '../../../common/api'; import { checkEnabledCaseConnectorOrThrow, @@ -31,13 +29,14 @@ import { import { createCaseError } from '../../common/error'; import { defaultPage, defaultPerPage } from '../../routes/api'; import { CasesClientArgs } from '../types'; - -const FindQueryParamsRt = rt.partial({ - ...SavedObjectFindOptionsRt.props, - subCaseId: rt.string, -}); - -type FindQueryParams = rt.TypeOf; +import { + combineFilters, + ensureAuthorized, + getAuthorizationFilter, + stringToKueryNode, +} from '../utils'; +import { Operations } from '../../authorization'; +import { includeFieldsRequiredForAuthentication } from '../../authorization/utils'; export interface FindArgs { caseID: string; @@ -60,14 +59,39 @@ export interface GetArgs { */ export async function find( { caseID, queryParams }: FindArgs, - { savedObjectsClient: soClient, caseService, logger }: CasesClientArgs + clientArgs: CasesClientArgs ): Promise { + const { + savedObjectsClient: soClient, + caseService, + logger, + authorization, + auditLogger, + } = clientArgs; + try { checkEnabledCaseConnectorOrThrow(queryParams?.subCaseId); + const { + filter: authorizationFilter, + ensureSavedObjectsAreAuthorized, + logSuccessfulAuthorization, + } = await getAuthorizationFilter({ + authorization, + auditLogger, + operation: Operations.findComments, + }); + const id = queryParams?.subCaseId ?? caseID; const associationType = queryParams?.subCaseId ? AssociationType.subCase : AssociationType.case; const { filter, ...queryWithoutFilter } = queryParams ?? {}; + + // if the fields property was defined, make sure we include the 'owner' field in the response + const fields = includeFieldsRequiredForAuthentication(queryWithoutFilter.fields); + + // combine any passed in filter property and the filter for the appropriate owner + const combinedFilter = combineFilters([stringToKueryNode(filter), authorizationFilter]); + const args = queryParams ? { caseService, @@ -80,8 +104,9 @@ export async function find( page: defaultPage, perPage: defaultPerPage, sortField: 'created_at', - filter: filter != null ? esKuery.fromKueryExpression(filter) : filter, + filter: combinedFilter, ...queryWithoutFilter, + fields, }, associationType, } @@ -93,11 +118,22 @@ export async function find( page: defaultPage, perPage: defaultPerPage, sortField: 'created_at', + filter: combinedFilter, }, associationType, }; const theComments = await caseService.getCommentsByAssociation(args); + + ensureSavedObjectsAreAuthorized( + theComments.saved_objects.map((comment) => ({ + owner: comment.attributes.owner, + id: comment.id, + })) + ); + + logSuccessfulAuthorization(); + return CommentsResponseRt.encode(transformComments(theComments)); } catch (error) { throw createCaseError({ @@ -115,7 +151,13 @@ export async function get( { attachmentID, caseID }: GetArgs, clientArgs: CasesClientArgs ): Promise { - const { attachmentService, savedObjectsClient: soClient, logger } = clientArgs; + const { + attachmentService, + savedObjectsClient: soClient, + logger, + authorization, + auditLogger, + } = clientArgs; try { const comment = await attachmentService.get({ @@ -123,6 +165,14 @@ export async function get( attachmentId: attachmentID, }); + await ensureAuthorized({ + authorization, + auditLogger, + owners: [comment.attributes.owner], + savedObjectIDs: [comment.id], + operation: Operations.getComment, + }); + return CommentResponseRt.encode(flattenCommentSavedObject(comment)); } catch (error) { throw createCaseError({ @@ -141,7 +191,13 @@ export async function getAll( { caseID, includeSubCaseComments, subCaseID }: GetAllArgs, clientArgs: CasesClientArgs ): Promise { - const { savedObjectsClient: soClient, caseService, logger } = clientArgs; + const { + savedObjectsClient: soClient, + caseService, + logger, + authorization, + auditLogger, + } = clientArgs; try { let comments: SavedObjectsFindResponse; @@ -155,11 +211,22 @@ export async function getAll( ); } + const { + filter, + ensureSavedObjectsAreAuthorized, + logSuccessfulAuthorization, + } = await getAuthorizationFilter({ + authorization, + auditLogger, + operation: Operations.getAllComments, + }); + if (subCaseID) { comments = await caseService.getAllSubCaseComments({ soClient, id: subCaseID, options: { + filter, sortField: defaultSortField, }, }); @@ -169,11 +236,18 @@ export async function getAll( id: caseID, includeSubCaseComments, options: { + filter, sortField: defaultSortField, }, }); } + ensureSavedObjectsAreAuthorized( + comments.saved_objects.map((comment) => ({ id: comment.id, owner: comment.attributes.owner })) + ); + + logSuccessfulAuthorization(); + return AllCommentsResponseRt.encode(flattenCommentSavedObjects(comments.saved_objects)); } catch (error) { throw createCaseError({ diff --git a/x-pack/plugins/cases/server/client/attachments/update.ts b/x-pack/plugins/cases/server/client/attachments/update.ts index 79b1f5bfc02255..c2c6d6800e51fb 100644 --- a/x-pack/plugins/cases/server/client/attachments/update.ts +++ b/x-pack/plugins/cases/server/client/attachments/update.ts @@ -15,8 +15,9 @@ import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../common/consta import { AttachmentService, CaseService } from '../../services'; import { CaseResponse, CommentPatchRequest } from '../../../common/api'; import { CasesClientArgs } from '..'; -import { decodeCommentRequest } from '../utils'; +import { decodeCommentRequest, ensureAuthorized } from '../utils'; import { createCaseError } from '../../common/error'; +import { Operations } from '../../authorization'; export interface UpdateArgs { caseID: string; @@ -89,6 +90,8 @@ export async function update( logger, user, userActionService, + authorization, + auditLogger, } = clientArgs; try { @@ -120,10 +123,22 @@ export async function update( throw Boom.notFound(`This comment ${queryCommentId} does not exist anymore.`); } + await ensureAuthorized({ + authorization, + auditLogger, + operation: Operations.updateComment, + savedObjectIDs: [myComment.id], + owners: [myComment.attributes.owner], + }); + if (myComment.attributes.type !== queryRestAttributes.type) { throw Boom.badRequest(`You cannot change the type of the comment.`); } + if (myComment.attributes.owner !== queryRestAttributes.owner) { + throw Boom.badRequest(`You cannot change the owner of the comment.`); + } + const saveObjType = subCaseID ? SUB_CASE_SAVED_OBJECT : CASE_SAVED_OBJECT; const caseRef = myComment.references.find((c) => c.type === saveObjType); diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index 15fbd34628182c..3f66db7281c381 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -22,11 +22,10 @@ import { CaseType, } from '../../../common/api'; import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; -import { createAuditMsg, ensureAuthorized, getConnectorFromConfiguration } from '../utils'; +import { ensureAuthorized, getConnectorFromConfiguration } from '../utils'; import { createCaseError } from '../../common/error'; import { Operations } from '../../authorization'; -import { EventOutcome } from '../../../../security/server'; import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; import { flattenCaseSavedObject, @@ -82,15 +81,6 @@ export const create = async ( savedObjectIDs: [savedObjectID], }); - // log that we're attempting to create a case - auditLogger?.log( - createAuditMsg({ - operation: Operations.createCase, - outcome: EventOutcome.UNKNOWN, - savedObjectID, - }) - ); - // eslint-disable-next-line @typescript-eslint/naming-convention const { username, full_name, email } = user; const createdDate = new Date().toISOString(); diff --git a/x-pack/plugins/cases/server/client/cases/delete.ts b/x-pack/plugins/cases/server/client/cases/delete.ts index 4657df2e71b301..100135e2992ebb 100644 --- a/x-pack/plugins/cases/server/client/cases/delete.ts +++ b/x-pack/plugins/cases/server/client/cases/delete.ts @@ -13,8 +13,7 @@ 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'; +import { ensureAuthorized } from '../utils'; async function deleteSubCases({ attachmentService, @@ -88,17 +87,6 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P 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({ diff --git a/x-pack/plugins/cases/server/client/cases/find.ts b/x-pack/plugins/cases/server/client/cases/find.ts index 988812da0d852a..53ae6a2e76b81d 100644 --- a/x-pack/plugins/cases/server/client/cases/find.ts +++ b/x-pack/plugins/cases/server/client/cases/find.ts @@ -73,9 +73,7 @@ export const find = async ( ? queryParams.searchFields : [queryParams.searchFields] : queryParams.searchFields, - fields: queryParams.fields - ? includeFieldsRequiredForAuthentication(queryParams.fields) - : queryParams.fields, + fields: includeFieldsRequiredForAuthentication(queryParams.fields), }, subCaseOptions: caseQueries.subCase, }); diff --git a/x-pack/plugins/cases/server/client/cases/mock.ts b/x-pack/plugins/cases/server/client/cases/mock.ts index 490519187f49ea..1d46f5715c4ba1 100644 --- a/x-pack/plugins/cases/server/client/cases/mock.ts +++ b/x-pack/plugins/cases/server/client/cases/mock.ts @@ -39,6 +39,7 @@ export const comment: CommentResponse = { email: 'testemail@elastic.co', username: 'elastic', }, + owner: 'securitySolution', pushed_at: null, pushed_by: null, updated_at: '2019-11-25T21:55:00.177Z', @@ -66,6 +67,7 @@ export const commentAlert: CommentResponse = { email: 'testemail@elastic.co', username: 'elastic', }, + owner: 'securitySolution', pushed_at: null, pushed_by: null, updated_at: '2019-11-25T21:55:00.177Z', @@ -83,6 +85,7 @@ export const commentAlertMultipleIds: CommentResponseAlertsType = { alertId: ['alert-id-1', 'alert-id-2'], index: 'alert-index-1', type: CommentType.alert as const, + owner: 'securitySolution', }; export const commentGeneratedAlert: CommentResponseAlertsType = { diff --git a/x-pack/plugins/cases/server/client/cases/utils.ts b/x-pack/plugins/cases/server/client/cases/utils.ts index 8bac4956a9e5f9..c45f976e680c53 100644 --- a/x-pack/plugins/cases/server/client/cases/utils.ts +++ b/x-pack/plugins/cases/server/client/cases/utils.ts @@ -329,11 +329,13 @@ export const isCommentAlertType = ( export const getCommentContextFromAttributes = ( attributes: CommentAttributes ): CommentRequestUserType | CommentRequestAlertType => { + const owner = attributes.owner; switch (attributes.type) { case CommentType.user: return { type: CommentType.user, comment: attributes.comment, + owner, }; case CommentType.generatedAlert: case CommentType.alert: @@ -342,11 +344,13 @@ export const getCommentContextFromAttributes = ( alertId: attributes.alertId, index: attributes.index, rule: attributes.rule, + owner, }; default: return { type: CommentType.user, comment: '', + owner, }; } }; diff --git a/x-pack/plugins/cases/server/client/configure/client.ts b/x-pack/plugins/cases/server/client/configure/client.ts index 1037a2ff9d8938..1e44e615626b77 100644 --- a/x-pack/plugins/cases/server/client/configure/client.ts +++ b/x-pack/plugins/cases/server/client/configure/client.ts @@ -32,7 +32,6 @@ import { transformCaseConnectorToEsConnector, transformESConnectorToCaseConnector, } from '../../common'; -import { EventOutcome } from '../../../../security/server'; import { CasesClientInternal } from '../client_internal'; import { CasesClientArgs } from '../types'; import { getFields } from './get_fields'; @@ -44,7 +43,6 @@ import { ActionType } from '../../../../actions/common'; import { Operations } from '../../authorization'; import { combineAuthorizedAndOwnerFilter, - createAuditMsg, ensureAuthorized, getAuthorizationFilter, } from '../utils'; @@ -276,15 +274,6 @@ async function update( 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.' @@ -426,15 +415,6 @@ async function create( 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[] = []; diff --git a/x-pack/plugins/cases/server/client/utils.ts b/x-pack/plugins/cases/server/client/utils.ts index b61de9f2beb6ae..eb00cce8654ef4 100644 --- a/x-pack/plugins/cases/server/client/utils.ts +++ b/x-pack/plugins/cases/server/client/utils.ts @@ -12,9 +12,10 @@ import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; -import { SavedObjectsFindResponse } from 'kibana/server'; +import { EcsEventOutcome, SavedObjectsFindResponse } from 'kibana/server'; import { PublicMethodsOf } from '@kbn/utility-types'; import { nodeBuilder, KueryNode } from '../../../../../src/plugins/data/common'; +import { esKuery } from '../../../../../src/plugins/data/server'; import { CaseConnector, ESCasesConfigureAttributes, @@ -28,7 +29,7 @@ import { AlertCommentRequestRt, } from '../../common/api'; import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../common/constants'; -import { AuditEvent, EventCategory, EventOutcome } from '../../../security/server'; +import { AuditEvent } from '../../../security/server'; import { combineFilterWithAuthorizationFilter } from '../authorization/utils'; import { getIDsAndIndicesAsArrays, @@ -36,7 +37,7 @@ import { isCommentRequestTypeUser, SavedObjectFindOptionsKueryNode, } from '../common'; -import { Authorization, OperationDetails } from '../authorization'; +import { Authorization, DATABASE_CATEGORY, ECS_OUTCOMES, OperationDetails } from '../authorization'; import { AuditLogger } from '../../../security/server'; export const decodeCommentRequest = (comment: CommentRequest) => { @@ -118,21 +119,27 @@ export const addStatusFilter = ({ return filters.length > 1 ? nodeBuilder.and(filters) : filters[0]; }; +interface FilterField { + filters?: string | string[]; + field: string; + operator: 'and' | 'or'; + type?: string; +} + export const buildFilter = ({ filters, field, operator, type = CASE_SAVED_OBJECT, -}: { - filters: string | string[]; - field: string; - operator: 'or' | 'and'; - type?: string; -}): KueryNode | null => { +}: FilterField): KueryNode | undefined => { + if (filters === undefined) { + return; + } + const filtersAsArray = Array.isArray(filters) ? filters : [filters]; if (filtersAsArray.length === 0) { - return null; + return; } return nodeBuilder[operator]( @@ -140,24 +147,47 @@ export const buildFilter = ({ ); }; +/** + * Combines the authorized filters with the requested owners. + */ 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, + filters: owner, field: 'owner', operator: 'or', type: savedObjectType, }); - return authorizationFilter != null && ownerFilter != null - ? combineFilterWithAuthorizationFilter(ownerFilter, authorizationFilter) - : authorizationFilter ?? ownerFilter ?? undefined; + return combineFilterWithAuthorizationFilter(ownerFilter, authorizationFilter); }; +/** + * Combines Kuery nodes and accepts an array with a mixture of undefined and KueryNodes. This will filter out the undefined + * filters and return a KueryNode with the filters and'd together. + */ +export function combineFilters(nodes: Array): KueryNode | undefined { + const filters = nodes.filter((node): node is KueryNode => node !== undefined); + if (filters.length <= 0) { + return; + } + return nodeBuilder.and(filters); +} + +/** + * Creates a KueryNode from a string expression. Returns undefined if the expression is undefined. + */ +export function stringToKueryNode(expression?: string): KueryNode | undefined { + if (!expression) { + return; + } + + return esKuery.fromKueryExpression(expression); +} + /** * 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 @@ -238,10 +268,7 @@ export const constructQueryOptions = ({ return { case: { - filter: - authorizationFilter != null && caseFilters != null - ? combineFilterWithAuthorizationFilter(caseFilters, authorizationFilter) - : caseFilters, + filter: combineFilterWithAuthorizationFilter(caseFilters, authorizationFilter), sortField, }, }; @@ -263,17 +290,11 @@ export const constructQueryOptions = ({ return { case: { - filter: - authorizationFilter != null - ? combineFilterWithAuthorizationFilter(caseFilters, authorizationFilter) - : caseFilters, + filter: combineFilterWithAuthorizationFilter(caseFilters, authorizationFilter), sortField, }, subCase: { - filter: - authorizationFilter != null && subCaseFilters != null - ? combineFilterWithAuthorizationFilter(subCaseFilters, authorizationFilter) - : subCaseFilters, + filter: combineFilterWithAuthorizationFilter(subCaseFilters, authorizationFilter), sortField, }, }; @@ -314,17 +335,11 @@ export const constructQueryOptions = ({ return { case: { - filter: - authorizationFilter != null - ? combineFilterWithAuthorizationFilter(caseFilters, authorizationFilter) - : caseFilters, + filter: combineFilterWithAuthorizationFilter(caseFilters, authorizationFilter), sortField, }, subCase: { - filter: - authorizationFilter != null && subCaseFilters != null - ? combineFilterWithAuthorizationFilter(subCaseFilters, authorizationFilter) - : subCaseFilters, + filter: combineFilterWithAuthorizationFilter(subCaseFilters, authorizationFilter), sortField, }, }; @@ -466,6 +481,52 @@ export const sortToSnake = (sortField: string | undefined): SortFieldCase => { } }; +/** + * Creates an AuditEvent describing the state of a request. + */ +function createAuditMsg({ + operation, + outcome, + error, + savedObjectID, +}: { + operation: OperationDetails; + savedObjectID?: string; + outcome?: EcsEventOutcome; + error?: Error; +}): AuditEvent { + const doc = + savedObjectID != null + ? `${operation.savedObjectType} [id=${savedObjectID}]` + : `a ${operation.docType}`; + const message = error + ? `Failed attempt to ${operation.verbs.present} ${doc}` + : outcome === ECS_OUTCOMES.unknown + ? `User is ${operation.verbs.progressive} ${doc}` + : `User has ${operation.verbs.past} ${doc}`; + + return { + message, + event: { + action: operation.action, + category: DATABASE_CATEGORY, + type: [operation.type], + outcome: outcome ?? (error ? ECS_OUTCOMES.failure : ECS_OUTCOMES.success), + }, + ...(savedObjectID != null && { + kibana: { + saved_object: { type: operation.savedObjectType, id: savedObjectID }, + }, + }), + ...(error != null && { + error: { + code: error.name, + message: error.message, + }, + }), + }; +} + /** * Wraps the Authorization class' ensureAuthorized call in a try/catch to handle the audit logging * on a failure. @@ -483,12 +544,19 @@ export async function ensureAuthorized({ authorization: PublicMethodsOf; auditLogger?: AuditLogger; }) { - try { - return await authorization.ensureAuthorized(owners, operation); - } catch (error) { + const logSavedObjects = ({ outcome, error }: { outcome?: EcsEventOutcome; error?: Error }) => { for (const savedObjectID of savedObjectIDs) { - auditLogger?.log(createAuditMsg({ operation, error, savedObjectID })); + auditLogger?.log(createAuditMsg({ operation, outcome, error, savedObjectID })); } + }; + + try { + await authorization.ensureAuthorized(owners, operation); + + // log that we're attempting an operation + logSavedObjects({ outcome: ECS_OUTCOMES.unknown }); + } catch (error) { + logSavedObjects({ error }); throw error; } } @@ -502,6 +570,12 @@ interface OwnerEntity { id: string; } +interface AuthFilterHelpers { + filter?: KueryNode; + ensureSavedObjectsAreAuthorized: (entities: OwnerEntity[]) => void; + logSuccessfulAuthorization: () => void; +} + /** * Wraps the Authorization class' method for determining which found saved objects the user making the request * is authorized to interact with. @@ -514,7 +588,7 @@ export async function getAuthorizationFilter({ operation: OperationDetails; authorization: PublicMethodsOf; auditLogger?: AuditLogger; -}) { +}): Promise { try { const { filter, @@ -540,49 +614,3 @@ export async function getAuthorizationFilter({ throw error; } } - -/** - * Creates an AuditEvent describing the state of a request. - */ -export function createAuditMsg({ - operation, - outcome, - error, - savedObjectID, -}: { - operation: OperationDetails; - savedObjectID?: string; - outcome?: EventOutcome; - error?: Error; -}): AuditEvent { - const doc = - savedObjectID != null - ? `${operation.savedObjectType} [id=${savedObjectID}]` - : `a ${operation.docType}`; - const message = error - ? `Failed attempt to ${operation.verbs.present} ${doc}` - : outcome === EventOutcome.UNKNOWN - ? `User is ${operation.verbs.progressive} ${doc}` - : `User has ${operation.verbs.past} ${doc}`; - - return { - message, - event: { - action: operation.action, - category: EventCategory.DATABASE, - type: operation.type, - outcome: outcome ?? (error ? EventOutcome.FAILURE : EventOutcome.SUCCESS), - }, - ...(savedObjectID != null && { - kibana: { - saved_object: { type: operation.savedObjectType, id: savedObjectID }, - }, - }), - ...(error != null && { - error: { - code: error.name, - message: error.message, - }, - }), - }; -} diff --git a/x-pack/plugins/cases/server/common/models/commentable_case.ts b/x-pack/plugins/cases/server/common/models/commentable_case.ts index d2276c0027ecee..81b5aca58f7979 100644 --- a/x-pack/plugins/cases/server/common/models/commentable_case.ts +++ b/x-pack/plugins/cases/server/common/models/commentable_case.ts @@ -119,6 +119,10 @@ export class CommentableCase { return this.subCase?.id; } + private get owner(): string { + return this.collection.attributes.owner; + } + private buildRefsToCase(): SavedObjectReference[] { const subCaseSOType = SUB_CASE_SAVED_OBJECT; const caseSOType = CASE_SAVED_OBJECT; @@ -244,10 +248,12 @@ export class CommentableCase { createdDate, user, commentReq, + id, }: { createdDate: string; user: User; commentReq: CommentRequest; + id: string; }): Promise { try { if (commentReq.type === CommentType.alert) { @@ -260,6 +266,10 @@ export class CommentableCase { } } + if (commentReq.owner !== this.owner) { + throw Boom.badRequest('The owner field of the comment must match the case'); + } + const [comment, commentableCase] = await Promise.all([ this.attachmentService.create({ soClient: this.soClient, @@ -270,6 +280,7 @@ export class CommentableCase { ...user, }), references: this.buildRefsToCase(), + id, }), this.update({ date: createdDate, user }), ]); diff --git a/x-pack/plugins/cases/server/common/utils.test.ts b/x-pack/plugins/cases/server/common/utils.test.ts index e7dcbf0111f55e..4057cf4f3f52d1 100644 --- a/x-pack/plugins/cases/server/common/utils.test.ts +++ b/x-pack/plugins/cases/server/common/utils.test.ts @@ -412,6 +412,7 @@ describe('common utils', () => { "username": "elastic", }, "id": "mock-comment-1", + "owner": "securitySolution", "pushed_at": null, "pushed_by": null, "type": "user", @@ -586,6 +587,7 @@ describe('common utils', () => { full_name: 'Elastic', username: 'elastic', associationType: AssociationType.case, + owner: 'securitySolution', }; const res = transformNewComment(comment); @@ -599,6 +601,7 @@ describe('common utils', () => { "full_name": "Elastic", "username": "elastic", }, + "owner": "securitySolution", "pushed_at": null, "pushed_by": null, "type": "user", @@ -613,6 +616,7 @@ describe('common utils', () => { comment: 'A comment', type: CommentType.user as const, createdDate: '2020-04-09T09:43:51.778Z', + owner: 'securitySolution', associationType: AssociationType.case, }; @@ -628,6 +632,7 @@ describe('common utils', () => { "full_name": undefined, "username": undefined, }, + "owner": "securitySolution", "pushed_at": null, "pushed_by": null, "type": "user", @@ -645,6 +650,7 @@ describe('common utils', () => { email: null, full_name: null, username: null, + owner: 'securitySolution', associationType: AssociationType.case, }; @@ -660,6 +666,7 @@ describe('common utils', () => { "full_name": null, "username": null, }, + "owner": "securitySolution", "pushed_at": null, "pushed_by": null, "type": "user", @@ -675,7 +682,10 @@ describe('common utils', () => { expect( countAlerts( createCommentFindResponse([ - { ids: ['1'], comments: [{ comment: '', type: CommentType.user }] }, + { + ids: ['1'], + comments: [{ comment: '', type: CommentType.user, owner: 'securitySolution' }], + }, ]).saved_objects[0] ) ).toBe(0); @@ -696,6 +706,7 @@ describe('common utils', () => { id: 'rule-id-1', name: 'rule-name-1', }, + owner: 'securitySolution', }, ], }, @@ -719,6 +730,7 @@ describe('common utils', () => { id: 'rule-id-1', name: 'rule-name-1', }, + owner: 'securitySolution', }, ], }, @@ -739,6 +751,7 @@ describe('common utils', () => { { alertId: ['a', 'b'], index: '', + owner: 'securitySolution', type: CommentType.alert, rule: { id: 'rule-id-1', @@ -747,6 +760,7 @@ describe('common utils', () => { }, { comment: '', + owner: 'securitySolution', type: CommentType.user, }, ], @@ -766,6 +780,7 @@ describe('common utils', () => { ids: ['1'], comments: [ { + owner: 'securitySolution', alertId: ['a', 'b'], index: '', type: CommentType.alert, @@ -780,6 +795,7 @@ describe('common utils', () => { ids: ['2'], comments: [ { + owner: 'securitySolution', comment: '', type: CommentType.user, }, @@ -803,6 +819,7 @@ describe('common utils', () => { ids: ['1', '2'], comments: [ { + owner: 'securitySolution', alertId: ['a', 'b'], index: '', type: CommentType.alert, @@ -834,6 +851,7 @@ describe('common utils', () => { ids: ['1', '2'], comments: [ { + owner: 'securitySolution', alertId: ['a', 'b'], index: '', type: CommentType.alert, diff --git a/x-pack/plugins/cases/server/common/utils.ts b/x-pack/plugins/cases/server/common/utils.ts index c4cad60f4d465b..7f38be2ba806dd 100644 --- a/x-pack/plugins/cases/server/common/utils.ts +++ b/x-pack/plugins/cases/server/common/utils.ts @@ -286,6 +286,7 @@ export const getAlertInfoFromComments = (comments: CommentRequest[] | undefined) type NewCommentArgs = CommentRequest & { associationType: AssociationType; createdDate: string; + owner: string; email?: string | null; full_name?: string | null; username?: string | null; diff --git a/x-pack/plugins/cases/server/connectors/case/index.test.ts b/x-pack/plugins/cases/server/connectors/case/index.test.ts index 876b8909b9317c..a2afc1df4ecf74 100644 --- a/x-pack/plugins/cases/server/connectors/case/index.test.ts +++ b/x-pack/plugins/cases/server/connectors/case/index.test.ts @@ -753,6 +753,7 @@ describe('case connector', () => { comment: { comment: 'a comment', type: CommentType.user, + owner: 'securitySolution', }, }, }; @@ -773,6 +774,7 @@ describe('case connector', () => { id: null, name: null, }, + owner: 'securitySolution', }, }, }; @@ -1134,6 +1136,7 @@ describe('case connector', () => { username: 'awesome', }, id: 'mock-comment', + owner: 'securitySolution', pushed_at: null, pushed_by: null, updated_at: null, @@ -1157,6 +1160,7 @@ describe('case connector', () => { comment: { comment: 'a comment', type: CommentType.user, + owner: 'securitySolution', }, }, }; diff --git a/x-pack/plugins/cases/server/connectors/case/index.ts b/x-pack/plugins/cases/server/connectors/case/index.ts index 6f8132d77a05fb..f647c67d286d95 100644 --- a/x-pack/plugins/cases/server/connectors/case/index.ts +++ b/x-pack/plugins/cases/server/connectors/case/index.ts @@ -178,6 +178,7 @@ export const transformConnectorComment = ( alertId: ids, index: indices, rule, + owner: comment.owner, }; } catch (e) { throw createCaseError({ diff --git a/x-pack/plugins/cases/server/connectors/case/schema.ts b/x-pack/plugins/cases/server/connectors/case/schema.ts index 1637cec7520be3..e44d9d9774c963 100644 --- a/x-pack/plugins/cases/server/connectors/case/schema.ts +++ b/x-pack/plugins/cases/server/connectors/case/schema.ts @@ -15,11 +15,13 @@ export const CaseConfigurationSchema = schema.object({}); const ContextTypeUserSchema = schema.object({ type: schema.literal(CommentType.user), comment: schema.string(), + owner: schema.string(), }); const ContextTypeAlertGroupSchema = schema.object({ type: schema.literal(CommentType.generatedAlert), alerts: schema.string(), + owner: schema.string(), }); export type ContextTypeGeneratedAlertType = typeof ContextTypeAlertGroupSchema.type; @@ -33,6 +35,7 @@ const ContextTypeAlertSchema = schema.object({ id: schema.nullable(schema.string()), name: schema.nullable(schema.string()), }), + owner: schema.string(), }); export type ContextTypeAlertSchemaType = typeof ContextTypeAlertSchema.type; diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index 4493e04f307c49..ad601e132535b7 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -68,7 +68,7 @@ export class CasePlugin { private securityPluginSetup?: SecurityPluginSetup; constructor(private readonly initializerContext: PluginInitializerContext) { - this.log = this.initializerContext.logger.get('plugins', 'cases'); + this.log = this.initializerContext.logger.get(); this.clientFactory = new CasesClientFactory(this.log); } 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 933a59cf060165..e5b826cf0ddefd 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 @@ -250,6 +250,7 @@ export const mockCaseComments: Array> = [ email: 'testemail@elastic.co', username: 'elastic', }, + owner: 'securitySolution', pushed_at: null, pushed_by: null, updated_at: '2019-11-25T21:55:00.177Z', @@ -282,6 +283,7 @@ export const mockCaseComments: Array> = [ email: 'testemail@elastic.co', username: 'elastic', }, + owner: 'securitySolution', pushed_at: null, pushed_by: null, updated_at: '2019-11-25T21:55:14.633Z', @@ -315,6 +317,7 @@ export const mockCaseComments: Array> = [ email: 'testemail@elastic.co', username: 'elastic', }, + owner: 'securitySolution', pushed_at: null, pushed_by: null, updated_at: '2019-11-25T22:32:30.608Z', @@ -348,6 +351,7 @@ export const mockCaseComments: Array> = [ email: 'testemail@elastic.co', username: 'elastic', }, + owner: 'securitySolution', pushed_at: null, pushed_by: null, rule: { @@ -385,6 +389,7 @@ export const mockCaseComments: Array> = [ email: 'testemail@elastic.co', username: 'elastic', }, + owner: 'securitySolution', pushed_at: null, pushed_by: null, rule: { @@ -422,6 +427,7 @@ export const mockCaseComments: Array> = [ email: 'testemail@elastic.co', username: 'elastic', }, + owner: 'securitySolution', pushed_at: null, pushed_by: null, rule: { diff --git a/x-pack/plugins/cases/server/routes/api/comments/find_comments.ts b/x-pack/plugins/cases/server/routes/api/comments/find_comments.ts index c992e7d0c114cb..a758805deb6efd 100644 --- a/x-pack/plugins/cases/server/routes/api/comments/find_comments.ts +++ b/x-pack/plugins/cases/server/routes/api/comments/find_comments.ts @@ -5,8 +5,6 @@ * 2.0. */ -import * as rt from 'io-ts'; - import { schema } from '@kbn/config-schema'; import Boom from '@hapi/boom'; @@ -14,16 +12,11 @@ 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 { FindQueryParamsRt, throwErrors, excess } 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, - subCaseId: rt.string, -}); - export function initFindCaseCommentsApi({ router, logger }: RouteDeps) { router.get( { @@ -38,7 +31,7 @@ export function initFindCaseCommentsApi({ router, logger }: RouteDeps) { async (context, request, response) => { try { const query = pipe( - FindQueryParamsRt.decode(request.query), + excess(FindQueryParamsRt).decode(request.query), fold(throwErrors(Boom.badRequest), identity) ); diff --git a/x-pack/plugins/cases/server/scripts/sub_cases/index.ts b/x-pack/plugins/cases/server/scripts/sub_cases/index.ts index ba3bcaa65091c3..b76c7ac06eff33 100644 --- a/x-pack/plugins/cases/server/scripts/sub_cases/index.ts +++ b/x-pack/plugins/cases/server/scripts/sub_cases/index.ts @@ -103,6 +103,7 @@ async function handleGenGroupAlerts(argv: any) { console.log('Case id: ', caseID); const comment: ContextTypeGeneratedAlertType = { + owner: 'securitySolution', type: CommentType.generatedAlert, alerts: createAlertsString( argv.ids.map((id: string) => ({ diff --git a/x-pack/plugins/cases/server/services/attachments/index.ts b/x-pack/plugins/cases/server/services/attachments/index.ts index fdfa722d18defb..2308e90320c62c 100644 --- a/x-pack/plugins/cases/server/services/attachments/index.ts +++ b/x-pack/plugins/cases/server/services/attachments/index.ts @@ -21,6 +21,7 @@ interface GetAttachmentArgs extends ClientArgs { interface CreateAttachmentArgs extends ClientArgs { attributes: AttachmentAttributes; references: SavedObjectReference[]; + id: string; } interface UpdateArgs { @@ -61,11 +62,12 @@ export class AttachmentService { } } - public async create({ soClient, attributes, references }: CreateAttachmentArgs) { + public async create({ soClient, attributes, references, id }: CreateAttachmentArgs) { try { this.log.debug(`Attempting to POST a new comment`); return await soClient.create(CASE_COMMENT_SAVED_OBJECT, attributes, { references, + id, }); } catch (error) { this.log.error(`Error on POST a new comment: ${error}`); diff --git a/x-pack/plugins/cases/server/services/cases/index.ts b/x-pack/plugins/cases/server/services/cases/index.ts index 2362d893739a07..870ba94b1ba131 100644 --- a/x-pack/plugins/cases/server/services/cases/index.ts +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -795,7 +795,7 @@ export class CaseService { options, }: FindCommentsArgs): Promise> { try { - this.log.debug(`Attempting to GET all comments for id ${JSON.stringify(id)}`); + this.log.debug(`Attempting to GET all comments internal for id ${JSON.stringify(id)}`); if (options?.page !== undefined || options?.perPage !== undefined) { return soClient.find({ type: CASE_COMMENT_SAVED_OBJECT, @@ -822,7 +822,7 @@ export class CaseService { ...cloneDeep(options), }); } catch (error) { - this.log.error(`Error on GET all comments for ${JSON.stringify(id)}: ${error}`); + this.log.error(`Error on GET all comments internal for ${JSON.stringify(id)}: ${error}`); throw error; } } @@ -866,7 +866,7 @@ export class CaseService { } this.log.debug(`Attempting to GET all comments for case caseID ${JSON.stringify(id)}`); - return this.getAllComments({ + return await this.getAllComments({ soClient, id, options: { @@ -899,7 +899,7 @@ export class CaseService { } this.log.debug(`Attempting to GET all comments for sub case caseID ${JSON.stringify(id)}`); - return this.getAllComments({ + return await this.getAllComments({ soClient, id, options: { diff --git a/x-pack/plugins/cases/server/services/user_actions/helpers.ts b/x-pack/plugins/cases/server/services/user_actions/helpers.ts index e987bd1685405f..2ab3bdb5e1cee5 100644 --- a/x-pack/plugins/cases/server/services/user_actions/helpers.ts +++ b/x-pack/plugins/cases/server/services/user_actions/helpers.ts @@ -157,6 +157,7 @@ const userActionFieldsAllowed: UserActionField = [ 'status', 'settings', 'sub_case', + 'owner', ]; interface CaseSubIDs { 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 4ca2bd01d9a2d0..ef396f75b85755 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 @@ -71,7 +71,7 @@ describe(`cases`, () => { expect(casesFeaturePrivilege.getActions(privilege, feature)).toMatchInlineSnapshot(` Array [ "cases:1.0.0-zeta1:observability/getCase", - "cases:1.0.0-zeta1:observability/findCases", + "cases:1.0.0-zeta1:observability/getComment", "cases:1.0.0-zeta1:observability/getTags", "cases:1.0.0-zeta1:observability/getReporters", "cases:1.0.0-zeta1:observability/findConfigurations", @@ -109,13 +109,16 @@ describe(`cases`, () => { expect(casesFeaturePrivilege.getActions(privilege, feature)).toMatchInlineSnapshot(` Array [ "cases:1.0.0-zeta1:security/getCase", - "cases:1.0.0-zeta1:security/findCases", + "cases:1.0.0-zeta1:security/getComment", "cases:1.0.0-zeta1:security/getTags", "cases:1.0.0-zeta1:security/getReporters", "cases:1.0.0-zeta1:security/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/createComment", + "cases:1.0.0-zeta1:security/deleteComment", + "cases:1.0.0-zeta1:security/updateComment", "cases:1.0.0-zeta1:security/createConfiguration", "cases:1.0.0-zeta1:security/updateConfiguration", ] @@ -153,17 +156,20 @@ describe(`cases`, () => { expect(casesFeaturePrivilege.getActions(privilege, feature)).toMatchInlineSnapshot(` Array [ "cases:1.0.0-zeta1:security/getCase", - "cases:1.0.0-zeta1:security/findCases", + "cases:1.0.0-zeta1:security/getComment", "cases:1.0.0-zeta1:security/getTags", "cases:1.0.0-zeta1:security/getReporters", "cases:1.0.0-zeta1:security/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/createComment", + "cases:1.0.0-zeta1:security/deleteComment", + "cases:1.0.0-zeta1:security/updateComment", "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/getComment", "cases:1.0.0-zeta1:obs/getTags", "cases:1.0.0-zeta1:obs/getReporters", "cases:1.0.0-zeta1:obs/findConfigurations", @@ -202,32 +208,38 @@ describe(`cases`, () => { expect(casesFeaturePrivilege.getActions(privilege, feature)).toMatchInlineSnapshot(` Array [ "cases:1.0.0-zeta1:security/getCase", - "cases:1.0.0-zeta1:security/findCases", + "cases:1.0.0-zeta1:security/getComment", "cases:1.0.0-zeta1:security/getTags", "cases:1.0.0-zeta1:security/getReporters", "cases:1.0.0-zeta1:security/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/createComment", + "cases:1.0.0-zeta1:security/deleteComment", + "cases:1.0.0-zeta1:security/updateComment", "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/getComment", "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/createComment", + "cases:1.0.0-zeta1:other-security/deleteComment", + "cases:1.0.0-zeta1:other-security/updateComment", "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/getComment", "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/getComment", "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 1ff72e9ad3fe1a..2643d7c6d6aafa 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 @@ -14,7 +14,7 @@ import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; // x-pack/plugins/cases/server/authorization/index.ts const readOperations: string[] = [ 'getCase', - 'findCases', + 'getComment', 'getTags', 'getReporters', 'findConfigurations', @@ -23,6 +23,9 @@ const writeOperations: string[] = [ 'createCase', 'deleteCase', 'updateCase', + 'createComment', + 'deleteComment', + 'updateComment', 'createConfiguration', 'updateConfiguration', ]; diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts index 996df2a8fe60a6..9e55067ce4ed4d 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts @@ -40,6 +40,8 @@ describe('Cases connectors', () => { { source: 'comments', target: 'comments', action_type: 'append' }, ], version: 'WzEwNCwxXQ==', + id: '123', + owner: 'securitySolution', }; beforeEach(() => { cleanKibana(); @@ -53,16 +55,18 @@ describe('Cases connectors', () => { cy.intercept('GET', '/api/cases/configure', (req) => { req.reply((res) => { const resBody = - res.body.version != null - ? { - ...res.body, - error: null, - mappings: [ - { source: 'title', target: 'short_description', action_type: 'overwrite' }, - { source: 'description', target: 'description', action_type: 'overwrite' }, - { source: 'comments', target: 'comments', action_type: 'append' }, - ], - } + res.body.length > 0 && res.body[0].version != null + ? [ + { + ...res.body[0], + error: null, + mappings: [ + { source: 'title', target: 'short_description', action_type: 'overwrite' }, + { source: 'description', target: 'description', action_type: 'overwrite' }, + { source: 'comments', target: 'comments', action_type: 'append' }, + ], + }, + ] : res.body; res.send(200, resBody); }); diff --git a/x-pack/plugins/security_solution/cypress/objects/case.ts b/x-pack/plugins/security_solution/cypress/objects/case.ts index a0135431c65439..278eab29f0a62e 100644 --- a/x-pack/plugins/security_solution/cypress/objects/case.ts +++ b/x-pack/plugins/security_solution/cypress/objects/case.ts @@ -13,6 +13,7 @@ export interface TestCase { description: string; timeline: CompleteTimeline; reporter: string; + owner: string; } export interface Connector { @@ -45,6 +46,7 @@ export const case1: TestCase = { description: 'This is the case description', timeline, reporter: 'elastic', + owner: 'securitySolution', }; export const serviceNowConnector: Connector = { diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/cases.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/cases.ts index f73b8e47066d22..798cd184d6012b 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/cases.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/cases.ts @@ -24,6 +24,7 @@ export const createCase = (newCase: TestCase) => settings: { syncAlerts: true, }, + owner: newCase.owner, }, headers: { 'kbn-xsrf': 'cypress-creds' }, }); diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx index 9c06fc032f819b..db55072090129a 100644 --- a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx @@ -45,6 +45,7 @@ const defaultPostComment = { const sampleData: CommentRequest = { comment: 'what a cool comment', type: CommentType.user, + owner: 'securitySolution', }; describe('AddComment ', () => { diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx index acd27e99a857ff..57b717c11bb354 100644 --- a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx @@ -86,7 +86,8 @@ export const AddComment = React.memo( } postComment({ caseId, - data: { ...data, type: CommentType.user }, + // TODO: get plugin name + data: { ...data, type: CommentType.user, owner: 'securitySolution' }, updateCase: onCommentPosted, subCaseId, }); diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/helpers.test.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/helpers.test.tsx index 18a76e2766d8da..d0385b1a45f529 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/helpers.test.tsx @@ -19,6 +19,7 @@ const comments: Comment[] = [ id: 'comment-id', createdAt: '2020-02-19T23:06:33.798Z', createdBy: { username: 'elastic' }, + owner: 'securitySolution', rule: { id: null, name: null, @@ -37,6 +38,7 @@ const comments: Comment[] = [ id: 'comment-id', createdAt: '2020-02-19T23:06:33.798Z', createdBy: { username: 'elastic' }, + owner: 'securitySolution', pushedAt: null, pushedBy: null, rule: { diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/__mock__/index.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/__mock__/index.tsx index ccc697a2ae84e4..63541b43461a92 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/__mock__/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/__mock__/index.tsx @@ -46,6 +46,7 @@ export const useCaseConfigureResponse: ReturnUseCaseConfigure = { setCurrentConfiguration: jest.fn(), setMappings: jest.fn(), version: '', + id: '', }; export const useConnectorsResponse: UseConnectorsResponse = { diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx index 40a202f5257a7c..58a0af0ba9cf24 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx @@ -189,6 +189,7 @@ describe('AddToCaseAction', () => { name: 'rule-name', }, type: 'alert', + owner: 'securitySolution', }); }); @@ -226,6 +227,7 @@ describe('AddToCaseAction', () => { name: 'rule-name', }, type: 'alert', + owner: 'securitySolution', }); }); @@ -257,6 +259,7 @@ describe('AddToCaseAction', () => { name: null, }, type: 'alert', + owner: 'securitySolution', }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx index 45c1355cecfa7c..09af79ba0b147d 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx @@ -91,6 +91,8 @@ const AddToCaseActionComponent: React.FC = ({ id: rule?.id != null ? rule.id[0] : null, name: rule?.name != null ? rule.name[0] : null, }, + // TODO: get plugin name + owner: 'securitySolution', }, updateCase, }); diff --git a/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx index 8f0fb3ea5a1d0e..c8b5eb5674a12a 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx @@ -420,6 +420,7 @@ describe('Case Configuration API', () => { }); const data = { comment: 'comment', + owner: 'securitySolution', type: CommentType.user as const, }; 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 999cb8d29d7452..a1ed7311ac74be 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 @@ -53,7 +53,7 @@ describe('Case Configuration API', () => { describe('fetch configuration', () => { beforeEach(() => { fetchMock.mockClear(); - fetchMock.mockResolvedValue(caseConfigurationResposeMock); + fetchMock.mockResolvedValue([caseConfigurationResposeMock]); }); test('check url, method, signal', async () => { @@ -106,13 +106,14 @@ describe('Case Configuration API', () => { test('check url, body, method, signal', async () => { await patchCaseConfigure( + '123', { connector: { id: '456', name: 'My Connector 2', type: ConnectorTypes.none, fields: null }, version: 'WzHJ12', }, abortCtrl.signal ); - expect(fetchMock).toHaveBeenCalledWith('/api/cases/configure', { + expect(fetchMock).toHaveBeenCalledWith('/api/cases/configure/123', { body: '{"connector":{"id":"456","name":"My Connector 2","type":".none","fields":null},"version":"WzHJ12"}', method: 'PATCH', @@ -122,6 +123,7 @@ describe('Case Configuration API', () => { test('happy path', async () => { const resp = await patchCaseConfigure( + '123', { connector: { id: '456', name: 'My Connector 2', type: ConnectorTypes.none, fields: null }, version: 'WzHJ12', diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/api.ts b/x-pack/plugins/security_solution/public/cases/containers/configure/api.ts index 943724ef083986..142958ae2919b5 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/configure/api.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/api.ts @@ -12,6 +12,7 @@ import { CasesConfigurePatch, CasesConfigureResponse, CasesConfigureRequest, + CasesConfigurationsResponse, } from '../../../../../cases/common/api'; import { KibanaServices } from '../../../common/lib/kibana'; @@ -22,8 +23,13 @@ import { } from '../../../../../cases/common/constants'; import { ApiProps } from '../types'; -import { convertToCamelCase, decodeCaseConfigureResponse } from '../utils'; +import { + convertToCamelCase, + decodeCaseConfigurationsResponse, + decodeCaseConfigureResponse, +} from '../utils'; import { CaseConfigure } from './types'; +import { getCaseConfigurationDetailsUrl } from '../../../../../cases/common/api/helpers'; export const fetchConnectors = async ({ signal }: ApiProps): Promise => { const response = await KibanaServices.get().http.fetch(`${CASE_CONFIGURE_CONNECTORS_URL}/_find`, { @@ -34,8 +40,9 @@ export const fetchConnectors = async ({ signal }: ApiProps): Promise => { - const response = await KibanaServices.get().http.fetch( + const response = await KibanaServices.get().http.fetch( CASE_CONFIGURE_URL, { method: 'GET', @@ -43,11 +50,16 @@ export const getCaseConfigure = async ({ signal }: ApiProps): Promise( - decodeCaseConfigureResponse(response) - ) - : null; + if (!isEmpty(response)) { + const decodedConfigs = decodeCaseConfigurationsResponse(response); + if (Array.isArray(decodedConfigs) && decodedConfigs.length > 0) { + return convertToCamelCase(decodedConfigs[0]); + } else { + return null; + } + } else { + return null; + } }; export const getConnectorMappings = async ({ signal }: ApiProps): Promise => { @@ -77,11 +89,12 @@ export const postCaseConfigure = async ( }; export const patchCaseConfigure = async ( + id: string, caseConfiguration: CasesConfigurePatch, signal: AbortSignal ): Promise => { const response = await KibanaServices.get().http.fetch( - CASE_CONFIGURE_URL, + getCaseConfigurationDetailsUrl(id), { method: 'PATCH', body: JSON.stringify(caseConfiguration), diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.test.tsx index 44a503cd089efe..267e0f337c1281 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.test.tsx @@ -84,6 +84,7 @@ describe('useConfigure', () => { setCurrentConfiguration: result.current.setCurrentConfiguration, setMappings: result.current.setMappings, version: caseConfigurationCamelCaseResponseMock.version, + id: caseConfigurationCamelCaseResponseMock.id, }); }); }); @@ -286,6 +287,7 @@ describe('useConfigure', () => { Promise.resolve({ ...caseConfigurationCamelCaseResponseMock, version: '', + id: '', }) ); const spyOnPostCaseConfigure = jest.spyOn(api, 'postCaseConfigure'); 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 ca817747e91914..21b1b6dc6392bf 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 @@ -28,6 +28,7 @@ export interface State extends ConnectorConfiguration { mappings: CaseConnectorMapping[]; persistLoading: boolean; version: string; + id: string; } export type Action = | { @@ -54,6 +55,10 @@ export type Action = type: 'setVersion'; payload: string; } + | { + type: 'setID'; + payload: string; + } | { type: 'setClosureType'; closureType: ClosureType; @@ -85,6 +90,11 @@ export const configureCasesReducer = (state: State, action: Action) => { ...state, version: action.payload, }; + case 'setID': + return { + ...state, + id: action.payload, + }; case 'setCurrentConfiguration': { return { ...state, @@ -145,6 +155,7 @@ export const initialState: State = { mappings: [], persistLoading: false, version: '', + id: '', }; export const useCaseConfigure = (): ReturnUseCaseConfigure => { @@ -206,6 +217,14 @@ export const useCaseConfigure = (): ReturnUseCaseConfigure => { }); }, []); + // TODO: refactor + const setID = useCallback((id: string) => { + dispatch({ + payload: id, + type: 'setID', + }); + }, []); + const [, dispatchToaster] = useStateToaster(); const isCancelledRefetchRef = useRef(false); const abortCtrlRefetchRef = useRef(new AbortController()); @@ -229,6 +248,7 @@ export const useCaseConfigure = (): ReturnUseCaseConfigure => { setClosureType(res.closureType); } setVersion(res.version); + setID(res.id); setMappings(res.mappings); if (!state.firstLoad) { @@ -278,14 +298,17 @@ 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 = state.version.length === 0 - ? await postCaseConfigure(connectorObj, abortCtrlPersistRef.current.signal) + ? await postCaseConfigure( + // TODO: use constant after https://github.com/elastic/kibana/pull/97646 is being merged + { ...connectorObj, owner: 'securitySolution' }, + abortCtrlPersistRef.current.signal + ) : await patchCaseConfigure( + state.id, { ...connectorObj, version: state.version, @@ -299,6 +322,7 @@ export const useCaseConfigure = (): ReturnUseCaseConfigure => { setClosureType(res.closureType); } setVersion(res.version); + setID(res.id); setMappings(res.mappings); if (setCurrentConfiguration != null) { setCurrentConfiguration({ @@ -340,6 +364,7 @@ export const useCaseConfigure = (): ReturnUseCaseConfigure => { setMappings, setPersistLoading, setVersion, + setID, state, ] ); diff --git a/x-pack/plugins/security_solution/public/cases/containers/mock.ts b/x-pack/plugins/security_solution/public/cases/containers/mock.ts index 947de140ccbb0d..6880a105b1ce61 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/mock.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/mock.ts @@ -47,6 +47,7 @@ export const basicComment: Comment = { id: basicCommentId, createdAt: basicCreatedAt, createdBy: elasticUser, + owner: 'securitySolution', pushedAt: null, pushedBy: null, updatedAt: null, @@ -62,6 +63,7 @@ export const alertComment: Comment = { id: 'alert-comment-id', createdAt: basicCreatedAt, createdBy: elasticUser, + owner: 'securitySolution', pushedAt: null, pushedBy: null, rule: { @@ -232,6 +234,7 @@ export const basicCommentSnake: CommentResponse = { id: basicCommentId, created_at: basicCreatedAt, created_by: elasticUserSnake, + owner: 'securitySolution', pushed_at: null, pushed_by: null, updated_at: null, diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.test.tsx index 4d4ac5d071fa5e..d0bab3e6f241b5 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.test.tsx @@ -19,6 +19,7 @@ describe('usePostComment', () => { const samplePost = { comment: 'a comment', type: CommentType.user as const, + owner: 'securitySolution', }; const updateCaseCallback = jest.fn(); beforeEach(() => { diff --git a/x-pack/plugins/security_solution/public/cases/containers/utils.ts b/x-pack/plugins/security_solution/public/cases/containers/utils.ts index 7c33e4481b2aab..7c291bc77c80fe 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/utils.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/utils.ts @@ -22,6 +22,8 @@ import { CasesStatusResponseRt, CasesStatusResponse, throwErrors, + CasesConfigurationsResponse, + CaseConfigurationsResponseRt, CasesConfigureResponse, CaseConfigureResponseRt, CaseUserActionsResponse, @@ -93,6 +95,14 @@ export const decodeCasesResponse = (respCase?: CasesResponse) => export const decodeCasesFindResponse = (respCases?: CasesFindResponse) => pipe(CasesFindResponseRt.decode(respCases), fold(throwErrors(createToasterPlainError), identity)); +// TODO: might need to refactor this +export const decodeCaseConfigurationsResponse = (respCase?: CasesConfigurationsResponse) => { + return pipe( + CaseConfigurationsResponseRt.decode(respCase), + fold(throwErrors(createToasterPlainError), identity) + ); +}; + export const decodeCaseConfigureResponse = (respCase?: CasesConfigureResponse) => pipe( CaseConfigureResponseRt.decode(respCase), 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 bcc23896f85f8b..a72141745e5777 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 @@ -54,18 +54,30 @@ const createUsersAndRoles = async (getService: CommonFtrProviderContext['getServ export const deleteSpaces = async (getService: CommonFtrProviderContext['getService']) => { const spacesService = getService('spaces'); for (const space of spaces) { - await spacesService.delete(space.id); + try { + await spacesService.delete(space.id); + } catch (error) { + // ignore errors because if a migration is run it will delete the .kibana index which remove the spaces and users + } } }; const deleteUsersAndRoles = async (getService: CommonFtrProviderContext['getService']) => { const security = getService('security'); for (const user of users) { - await security.user.delete(user.username); + try { + await security.user.delete(user.username); + } catch (error) { + // ignore errors because if a migration is run it will delete the .kibana index which remove the spaces and users + } } for (const role of roles) { - await security.role.delete(role.name); + try { + await security.role.delete(role.name); + } catch (error) { + // ignore errors because if a migration is run it will delete the .kibana index which remove the spaces and users + } } }; diff --git a/x-pack/test/case_api_integration/common/lib/authentication/roles.ts b/x-pack/test/case_api_integration/common/lib/authentication/roles.ts index cf21b01c3967e4..c08b68bb2721f1 100644 --- a/x-pack/test/case_api_integration/common/lib/authentication/roles.ts +++ b/x-pack/test/case_api_integration/common/lib/authentication/roles.ts @@ -36,7 +36,7 @@ export const globalRead: Role = { { feature: { securitySolutionFixture: ['read'], - observabilityFixture: ['all'], + observabilityFixture: ['read'], }, spaces: ['*'], }, diff --git a/x-pack/test/case_api_integration/common/lib/mock.ts b/x-pack/test/case_api_integration/common/lib/mock.ts index c3a6cb87141155..20511f8daab649 100644 --- a/x-pack/test/case_api_integration/common/lib/mock.ts +++ b/x-pack/test/case_api_integration/common/lib/mock.ts @@ -74,6 +74,7 @@ export const userActionPostResp: CasesClientPostRequest = { export const postCommentUserReq: CommentRequestUserType = { comment: 'This is a cool comment', type: CommentType.user, + owner: 'securitySolutionFixture', }; export const postCommentAlertReq: CommentRequestAlertType = { @@ -81,6 +82,7 @@ export const postCommentAlertReq: CommentRequestAlertType = { index: 'test-index', rule: { id: 'test-rule-id', name: 'test-index-id' }, type: CommentType.alert, + owner: 'securitySolutionFixture', }; export const postCommentGenAlertReq: ContextTypeGeneratedAlertType = { @@ -89,6 +91,7 @@ export const postCommentGenAlertReq: ContextTypeGeneratedAlertType = { { _id: 'test-id2', _index: 'test-index', ruleId: 'rule-id', ruleName: 'rule name' }, ]), type: CommentType.generatedAlert, + owner: 'securitySolutionFixture', }; export const postCaseResp = ( 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 0a0151d37d3f8e..43090df495ce93 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -470,6 +470,7 @@ export const deleteCasesUserActions = async (es: KibanaClient): Promise => wait_for_completion: true, refresh: true, body: {}, + conflicts: 'proceed', }); }; @@ -481,6 +482,7 @@ export const deleteCasesByESQuery = async (es: KibanaClient): Promise => { wait_for_completion: true, refresh: true, body: {}, + conflicts: 'proceed', }); }; @@ -496,6 +498,7 @@ export const deleteSubCases = async (es: KibanaClient): Promise => { wait_for_completion: true, refresh: true, body: {}, + conflicts: 'proceed', }); }; @@ -507,6 +510,7 @@ export const deleteComments = async (es: KibanaClient): Promise => { wait_for_completion: true, refresh: true, body: {}, + conflicts: 'proceed', }); }; @@ -518,6 +522,7 @@ export const deleteConfiguration = async (es: KibanaClient): Promise => { wait_for_completion: true, refresh: true, body: {}, + conflicts: 'proceed', }); }; @@ -529,10 +534,11 @@ export const deleteMappings = async (es: KibanaClient): Promise => { wait_for_completion: true, refresh: true, body: {}, + conflicts: 'proceed', }); }; -export const getSpaceUrlPrefix = (spaceId?: string | null) => { +export const getSpaceUrlPrefix = (spaceId: string | undefined | null) => { return spaceId && spaceId !== 'default' ? `/s/${spaceId}` : ``; }; @@ -592,13 +598,19 @@ export const deleteCases = async ({ return body; }; -export const createComment = async ( - supertest: st.SuperTest, - caseId: string, - params: CommentRequest, - expectedHttpCode: number = 200, - auth: { user: User; space: string | null } = { user: superUser, space: null } -): Promise => { +export const createComment = async ({ + supertest, + caseId, + params, + auth = { user: superUser, space: null }, + expectedHttpCode = 200, +}: { + supertest: st.SuperTest; + caseId: string; + params: CommentRequest; + auth?: { user: User; space: string | null }; + expectedHttpCode?: number; +}): Promise => { const { body: theCase } = await supertest .post(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}/${caseId}/comments`) .auth(auth.user.username, auth.user.password) @@ -636,58 +648,108 @@ export const updateCase = async ( return cases; }; -export const deleteComment = async ( - supertest: st.SuperTest, - caseId: string, - commentId: string, - expectedHttpCode: number = 204 -): Promise<{} | Error> => { +export const deleteComment = async ({ + supertest, + caseId, + commentId, + expectedHttpCode = 204, + auth = { user: superUser, space: null }, +}: { + supertest: st.SuperTest; + caseId: string; + commentId: string; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): Promise<{} | Error> => { const { body: comment } = await supertest - .delete(`${CASES_URL}/${caseId}/comments/${commentId}`) + .delete(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}/${caseId}/comments/${commentId}`) .set('kbn-xsrf', 'true') + .auth(auth.user.username, auth.user.password) .expect(expectedHttpCode) .send(); return comment; }; -export const getAllComments = async ( - supertest: st.SuperTest, - caseId: string, - expectedHttpCode: number = 200 -): Promise => { - const { body: comments } = await supertest - .get(`${CASES_URL}/${caseId}/comments`) +export const deleteAllComments = async ({ + supertest, + caseId, + expectedHttpCode = 204, + auth = { user: superUser, space: null }, +}: { + supertest: st.SuperTest; + caseId: string; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): Promise<{} | Error> => { + const { body: comment } = await supertest + .delete(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}/${caseId}/comments`) .set('kbn-xsrf', 'true') + .auth(auth.user.username, auth.user.password) + .expect(expectedHttpCode) + .send(); + + return comment; +}; + +export const getAllComments = async ({ + supertest, + caseId, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: st.SuperTest; + caseId: string; + auth?: { user: User; space: string | null }; + expectedHttpCode?: number; +}): Promise => { + const { body: comments } = await supertest + .get(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}/${caseId}/comments`) + .auth(auth.user.username, auth.user.password) .expect(expectedHttpCode); return comments; }; -export const getComment = async ( - supertest: st.SuperTest, - caseId: string, - commentId: string, - expectedHttpCode: number = 200 -): Promise => { +export const getComment = async ({ + supertest, + caseId, + commentId, + expectedHttpCode = 200, + auth = { user: superUser }, +}: { + supertest: st.SuperTest; + caseId: string; + commentId: string; + expectedHttpCode?: number; + auth?: { user: User; space?: string }; +}): Promise => { const { body: comment } = await supertest - .get(`${CASES_URL}/${caseId}/comments/${commentId}`) - .set('kbn-xsrf', 'true') + .get(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}/${caseId}/comments/${commentId}`) + .auth(auth.user.username, auth.user.password) .expect(expectedHttpCode); return comment; }; -export const updateComment = async ( - supertest: st.SuperTest, - caseId: string, - req: CommentPatchRequest, - expectedHttpCode: number = 200 -): Promise => { +export const updateComment = async ({ + supertest, + caseId, + req, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: st.SuperTest; + caseId: string; + req: CommentPatchRequest; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): Promise => { const { body: res } = await supertest - .patch(`${CASES_URL}/${caseId}/comments`) + .patch(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}/${caseId}/comments`) .set('kbn-xsrf', 'true') .send(req) + .auth(auth.user.username, auth.user.password) .expect(expectedHttpCode); return res; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/basic/configure/create_connector.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/configure/create_connector.ts new file mode 100644 index 00000000000000..a403e6d55be86d --- /dev/null +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/configure/create_connector.ts @@ -0,0 +1,20 @@ +/* + * 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 { createConnector, getServiceNowConnector } from '../../../../common/lib/utils'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function serviceNow({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('create service now action', () => { + it('should return 403 when creating a service now action', async () => { + await createConnector(supertest, getServiceNowConnector(), 403); + }); + }); +} 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 9ebc16f5e07aa5..484dca314c9cce 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 @@ -61,14 +61,27 @@ export default ({ getService }: FtrProviderContext): void => { it(`should delete a case's comments when that case gets deleted`, async () => { const postedCase = await createCase(supertest, getPostCaseRequest()); - const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); // ensure that we can get the comment before deleting the case - await getComment(supertest, postedCase.id, patchedCase.comments![0].id); + await getComment({ + supertest, + caseId: postedCase.id, + commentId: patchedCase.comments![0].id, + }); await deleteCases({ supertest, caseIDs: [postedCase.id] }); // make sure the comment is now gone - await getComment(supertest, postedCase.id, patchedCase.comments![0].id, 404); + await getComment({ + supertest, + caseId: postedCase.id, + commentId: patchedCase.comments![0].id, + expectedHttpCode: 404, + }); }); it('should create a user action when creating a case', async () => { 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 c537d2477cb597..6bcd78f98e5eb5 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 @@ -137,8 +137,12 @@ export default ({ getService }: FtrProviderContext): void => { const postedCase = await createCase(supertest, postCaseReq); // post 2 comments - await createComment(supertest, postedCase.id, postCommentUserReq); - const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); + await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq }); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); const cases = await findCases({ supertest }); expect(cases).to.eql({ @@ -566,7 +570,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should return the correct cases', async () => { await Promise.all([ // Create case owned by the security solution user - await createCase( + createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, @@ -576,7 +580,7 @@ export default ({ getService }: FtrProviderContext): void => { } ), // Create case owned by the observability user - await createCase( + createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, @@ -651,7 +655,7 @@ 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 createCase( + createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, @@ -661,7 +665,7 @@ export default ({ getService }: FtrProviderContext): void => { } ), // super user creates a case with owner observabilityFixture - await createCase( + createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, @@ -692,7 +696,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should NOT allow to pass a filter query parameter', async () => { await supertest .get( - `${CASES_URL}/_find?sortOrder=asc&filter=cases.attributes.owner=observabilityFixture` + `${CASES_URL}/_find?sortOrder=asc&filter=cases.attributes.owner:"observabilityFixture"` ) .set('kbn-xsrf', 'true') .send() @@ -725,7 +729,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should respect the owner filter when having permissions', async () => { await Promise.all([ - await createCase( + createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, @@ -734,7 +738,7 @@ export default ({ getService }: FtrProviderContext): void => { space: 'space1', } ), - await createCase( + createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, @@ -762,7 +766,7 @@ 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 createCase( + createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, @@ -771,7 +775,7 @@ export default ({ getService }: FtrProviderContext): void => { space: 'space1', } ), - await createCase( + createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, 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 187c84be7c1962..222632b41c297c 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 @@ -60,7 +60,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); + await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq }); const theCase = await getCase({ supertest, caseId: postedCase.id, includeComments: true }); const comment = removeServerGeneratedPropertiesFromSavedObject( @@ -76,6 +76,7 @@ export default ({ getService }: FtrProviderContext): void => { pushed_at: null, pushed_by: null, updated_by: null, + owner: 'securitySolutionFixture', }); }); @@ -127,9 +128,15 @@ export default ({ getService }: FtrProviderContext): void => { } ); - await createComment(supertestWithoutAuth, postedCase.id, postCommentUserReq, 200, { - user: secOnly, - space: 'space1', + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + expectedHttpCode: 200, + auth: { + user: secOnly, + space: 'space1', + }, }); const theCase = await getCase({ @@ -152,6 +159,7 @@ export default ({ getService }: FtrProviderContext): void => { pushed_at: null, pushed_by: null, updated_by: null, + owner: 'securitySolutionFixture', }); }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts index 1d7baabaf93b04..b50c18192a05b7 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts @@ -181,7 +181,11 @@ export default ({ getService }: FtrProviderContext): void => { // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests it.skip('should allow converting an individual case to a collection when it does not have alerts', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); await updateCase(supertest, { cases: [ { @@ -394,7 +398,11 @@ export default ({ getService }: FtrProviderContext): void => { it('should 400 when attempting to update an individual case to a collection when it has alerts attached to it', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment(supertest, postedCase.id, postCommentAlertReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentAlertReq, + }); await updateCase( supertest, { @@ -471,11 +479,16 @@ export default ({ getService }: FtrProviderContext): void => { }, }); - const updatedInd1WithComment = await createComment(supertest, individualCase1.id, { - alertId: signalID, - index: defaultSignalsIndex, - rule: { id: 'test-rule-id', name: 'test-index-id' }, - type: CommentType.alert, + const updatedInd1WithComment = await createComment({ + supertest, + caseId: individualCase1.id, + params: { + alertId: signalID, + index: defaultSignalsIndex, + rule: { id: 'test-rule-id', name: 'test-index-id' }, + type: CommentType.alert, + owner: 'securitySolutionFixture', + }, }); const individualCase2 = await createCase(supertest, { @@ -485,11 +498,16 @@ export default ({ getService }: FtrProviderContext): void => { }, }); - const updatedInd2WithComment = await createComment(supertest, individualCase2.id, { - alertId: signalID2, - index: defaultSignalsIndex, - rule: { id: 'test-rule-id', name: 'test-index-id' }, - type: CommentType.alert, + const updatedInd2WithComment = await createComment({ + supertest, + caseId: individualCase2.id, + params: { + alertId: signalID2, + index: defaultSignalsIndex, + rule: { id: 'test-rule-id', name: 'test-index-id' }, + type: CommentType.alert, + owner: 'securitySolutionFixture', + }, }); await es.indices.refresh({ index: defaultSignalsIndex }); @@ -604,18 +622,28 @@ export default ({ getService }: FtrProviderContext): void => { }, }); - const updatedIndWithComment = await createComment(supertest, individualCase.id, { - alertId: signalIDInFirstIndex, - index: defaultSignalsIndex, - rule: { id: 'test-rule-id', name: 'test-index-id' }, - type: CommentType.alert, + const updatedIndWithComment = await createComment({ + supertest, + caseId: individualCase.id, + params: { + alertId: signalIDInFirstIndex, + index: defaultSignalsIndex, + rule: { id: 'test-rule-id', name: 'test-index-id' }, + type: CommentType.alert, + owner: 'securitySolutionFixture', + }, }); - const updatedIndWithComment2 = await createComment(supertest, updatedIndWithComment.id, { - alertId: signalIDInSecondIndex, - index: signalsIndex2, - rule: { id: 'test-rule-id', name: 'test-index-id' }, - type: CommentType.alert, + const updatedIndWithComment2 = await createComment({ + supertest, + caseId: updatedIndWithComment.id, + params: { + alertId: signalIDInSecondIndex, + index: signalsIndex2, + rule: { id: 'test-rule-id', name: 'test-index-id' }, + type: CommentType.alert, + owner: 'securitySolutionFixture', + }, }); await es.indices.refresh({ index: defaultSignalsIndex }); @@ -706,14 +734,19 @@ export default ({ getService }: FtrProviderContext): void => { const alert = signals.hits.hits[0]; expect(alert._source.signal.status).eql('open'); - const caseUpdated = await createComment(supertest, postedCase.id, { - alertId: alert._id, - index: alert._index, - rule: { - id: 'id', - name: 'name', + const caseUpdated = await createComment({ + supertest, + caseId: postedCase.id, + params: { + alertId: alert._id, + index: alert._index, + rule: { + id: 'id', + name: 'name', + }, + type: CommentType.alert, + owner: 'securitySolutionFixture', }, - type: CommentType.alert, }); await es.indices.refresh({ index: alert._index }); @@ -756,13 +789,18 @@ export default ({ getService }: FtrProviderContext): void => { const alert = signals.hits.hits[0]; expect(alert._source.signal.status).eql('open'); - const caseUpdated = await createComment(supertest, postedCase.id, { - alertId: alert._id, - index: alert._index, - type: CommentType.alert, - rule: { - id: 'id', - name: 'name', + const caseUpdated = await createComment({ + supertest, + caseId: postedCase.id, + params: { + alertId: alert._id, + index: alert._index, + type: CommentType.alert, + rule: { + id: 'id', + name: 'name', + }, + owner: 'securitySolutionFixture', }, }); @@ -801,14 +839,19 @@ export default ({ getService }: FtrProviderContext): void => { const alert = signals.hits.hits[0]; expect(alert._source.signal.status).eql('open'); - const caseUpdated = await createComment(supertest, postedCase.id, { - alertId: alert._id, - index: alert._index, - rule: { - id: 'id', - name: 'name', + const caseUpdated = await createComment({ + supertest, + caseId: postedCase.id, + params: { + alertId: alert._id, + index: alert._index, + rule: { + id: 'id', + name: 'name', + }, + type: CommentType.alert, + owner: 'securitySolutionFixture', }, - type: CommentType.alert, }); // Update the status of the case with sync alerts off @@ -857,13 +900,18 @@ export default ({ getService }: FtrProviderContext): void => { const alert = signals.hits.hits[0]; expect(alert._source.signal.status).eql('open'); - const caseUpdated = await createComment(supertest, postedCase.id, { - alertId: alert._id, - index: alert._index, - type: CommentType.alert, - rule: { - id: 'id', - name: 'name', + const caseUpdated = await createComment({ + supertest, + caseId: postedCase.id, + params: { + alertId: alert._id, + index: alert._index, + type: CommentType.alert, + rule: { + id: 'id', + name: 'name', + }, + owner: 'securitySolutionFixture', }, }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts index cd4e72f6f93159..353974632feb88 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts @@ -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 { postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; +import { getPostCaseRequest, postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; import { createCaseAction, createSubCase, @@ -21,7 +21,18 @@ import { createCase, createComment, deleteComment, + deleteAllComments, } from '../../../../common/lib/utils'; +import { + globalRead, + noKibanaPrivileges, + obsOnly, + obsOnlyRead, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../common/lib/authentication/users'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -38,8 +49,16 @@ export default ({ getService }: FtrProviderContext): void => { describe('happy path', () => { it('should delete a comment', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); - const comment = await deleteComment(supertest, postedCase.id, patchedCase.comments![0].id); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + const comment = await deleteComment({ + supertest, + caseId: postedCase.id, + commentId: patchedCase.comments![0].id, + }); expect(comment).to.eql({}); }); @@ -48,13 +67,17 @@ export default ({ getService }: FtrProviderContext): void => { describe('unhappy path', () => { it('404s when comment belongs to different case', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); - const error = (await deleteComment( + const patchedCase = await createComment({ supertest, - 'fake-id', - patchedCase.comments![0].id, - 404 - )) as Error; + caseId: postedCase.id, + params: postCommentUserReq, + }); + const error = (await deleteComment({ + supertest, + caseId: 'fake-id', + commentId: patchedCase.comments![0].id, + expectedHttpCode: 404, + })) as Error; expect(error.message).to.be( `This comment ${patchedCase.comments![0].id} does not exist in fake-id.` @@ -62,7 +85,12 @@ export default ({ getService }: FtrProviderContext): void => { }); it('404s when comment is not there', async () => { - await deleteComment(supertest, 'fake-id', 'fake-id', 404); + await deleteComment({ + supertest, + caseId: 'fake-id', + commentId: 'fake-id', + expectedHttpCode: 404, + }); }); it('should return a 400 when attempting to delete all comments when passing the `subCaseId` parameter', async () => { @@ -150,5 +178,194 @@ export default ({ getService }: FtrProviderContext): void => { expect(allComments.length).to.eql(0); }); }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should delete a comment from the appropriate owner', async () => { + const secCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: secOnly, space: 'space1' } + ); + + const commentResp = await createComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + params: postCommentUserReq, + auth: { user: secOnly, space: 'space1' }, + }); + + await deleteComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + commentId: commentResp.comments![0].id, + auth: { user: secOnly, space: 'space1' }, + }); + }); + + it('should delete multiple comments from the appropriate owner', async () => { + const secCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: secOnly, space: 'space1' } + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + params: postCommentUserReq, + auth: { user: secOnly, space: 'space1' }, + }); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + params: postCommentUserReq, + auth: { user: secOnly, space: 'space1' }, + }); + + await deleteAllComments({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + auth: { user: secOnly, space: 'space1' }, + }); + }); + + it('should not delete a comment from a different owner', async () => { + const secCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: secOnly, space: 'space1' } + ); + + const commentResp = await createComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + params: postCommentUserReq, + auth: { user: secOnly, space: 'space1' }, + }); + + await deleteComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + commentId: commentResp.comments![0].id, + auth: { user: obsOnly, space: 'space1' }, + expectedHttpCode: 403, + }); + + await deleteAllComments({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + auth: { user: obsOnly, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT delete a comment`, async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space1' } + ); + + const commentResp = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: { user: superUser, space: 'space1' }, + }); + + await deleteComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + commentId: commentResp.comments![0].id, + auth: { user, space: 'space1' }, + expectedHttpCode: 403, + }); + + await deleteAllComments({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + auth: { user, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + } + + it('should not delete a comment with no kibana privileges', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space1' } + ); + + const commentResp = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: { user: superUser, space: 'space1' }, + }); + + await deleteComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + commentId: commentResp.comments![0].id, + auth: { user: noKibanaPrivileges, space: 'space1' }, + expectedHttpCode: 403, + }); + + await deleteAllComments({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + auth: { user: noKibanaPrivileges, space: 'space1' }, + // the find in the delete all will return no results + expectedHttpCode: 404, + }); + }); + + it('should NOT delete a comment in a space with where the user does not have permissions', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space2' } + ); + + const commentResp = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: { user: superUser, space: 'space2' }, + }); + + await deleteComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + commentId: commentResp.comments![0].id, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 404, + }); + + await deleteAllComments({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 404, + }); + }); + }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts index 43e128c1e41fa4..470c2481410ff7 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts @@ -6,21 +6,41 @@ */ 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 { CommentsResponse, CommentType } from '../../../../../../plugins/cases/common/api'; -import { postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; +import { + getPostCaseRequest, + postCaseReq, + postCommentAlertReq, + postCommentUserReq, +} from '../../../../common/lib/mock'; import { createCaseAction, + createComment, createSubCase, deleteAllCaseItems, deleteCaseAction, deleteCasesByESQuery, deleteCasesUserActions, deleteComments, + ensureSavedObjectIsAuthorized, + getSpaceUrlPrefix, + createCase, } from '../../../../common/lib/utils'; +import { + obsOnly, + secOnly, + obsOnlyRead, + secOnlyRead, + noKibanaPrivileges, + superUser, + globalRead, + obsSecRead, +} from '../../../../common/lib/authentication/users'; + // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); @@ -79,7 +99,7 @@ export default ({ getService }: FtrProviderContext): void => { const { body: patchedCase } = await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send({ comment: 'unique', type: CommentType.user }) + .send({ ...postCommentUserReq, comment: 'unique' }) .expect(200); const { body: caseComments } = await supertest @@ -151,5 +171,222 @@ export default ({ getService }: FtrProviderContext): void => { expect(subCaseComments.comments[1].type).to.be(CommentType.user); }); }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should return the correct comments', async () => { + const space1 = 'space1'; + + const [secCase, obsCase] = await Promise.all([ + // Create case owned by the security solution user + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: secOnly, space: space1 } + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { user: obsOnly, space: space1 } + ), + // Create case owned by the observability user + ]); + + await Promise.all([ + createComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + params: postCommentUserReq, + auth: { user: secOnly, space: space1 }, + }), + createComment({ + supertest: supertestWithoutAuth, + caseId: obsCase.id, + params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, + auth: { user: obsOnly, space: space1 }, + }), + ]); + + for (const scenario of [ + { + user: globalRead, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: secCase.id, + }, + { + user: globalRead, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: obsCase.id, + }, + { + user: superUser, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: secCase.id, + }, + { + user: superUser, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: obsCase.id, + }, + { + user: secOnlyRead, + numExpectedEntites: 1, + owners: ['securitySolutionFixture'], + caseID: secCase.id, + }, + { + user: obsOnlyRead, + numExpectedEntites: 1, + owners: ['observabilityFixture'], + caseID: obsCase.id, + }, + { + user: obsSecRead, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: secCase.id, + }, + { + user: obsSecRead, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: obsCase.id, + }, + ]) { + const { body: caseComments }: { body: CommentsResponse } = await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(space1)}${CASES_URL}/${scenario.caseID}/comments/_find`) + .auth(scenario.user.username, scenario.user.password) + .expect(200); + + ensureSavedObjectIsAuthorized( + caseComments.comments, + scenario.numExpectedEntites, + 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 comment`, async () => { + // super user creates a case and comment in the appropriate space + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: scenario.space } + ); + + await createComment({ + supertest: supertestWithoutAuth, + auth: { user: superUser, space: scenario.space }, + params: { ...postCommentUserReq, owner: 'securitySolutionFixture' }, + caseId: caseInfo.id, + }); + + // user should not be able to read comments + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(scenario.space)}${CASES_URL}/${caseInfo.id}/comments/_find`) + .auth(scenario.user.username, scenario.user.password) + .expect(403); + }); + } + + it('should not return any comments when trying to exploit RBAC through the search query parameter', async () => { + const obsCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { user: superUser, space: 'space1' } + ); + + await createComment({ + supertest: supertestWithoutAuth, + auth: { user: superUser, space: 'space1' }, + params: { ...postCommentUserReq, owner: 'observabilityFixture' }, + caseId: obsCase.id, + }); + + const { body: res }: { body: CommentsResponse } = await supertestWithoutAuth + .get( + `${getSpaceUrlPrefix('space1')}${CASES_URL}/${ + obsCase.id + }/comments/_find?search=securitySolutionFixture+observabilityFixture` + ) + .auth(secOnly.username, secOnly.password) + .expect(200); + + // shouldn't find any comments since they were created under the observability ownership + ensureSavedObjectIsAuthorized(res.comments, 0, ['securitySolutionFixture']); + }); + + it('should not allow retrieving unauthorized comments using the filter field', async () => { + const obsCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { user: superUser, space: 'space1' } + ); + + await createComment({ + supertest: supertestWithoutAuth, + auth: { user: superUser, space: 'space1' }, + params: { ...postCommentUserReq, owner: 'observabilityFixture' }, + caseId: obsCase.id, + }); + + const { body: res } = await supertestWithoutAuth + .get( + `${getSpaceUrlPrefix('space1')}${CASES_URL}/${ + obsCase.id + }/comments/_find?filter=cases-comments.attributes.owner:"observabilityFixture"` + ) + .auth(secOnly.username, secOnly.password) + .expect(200); + expect(res.comments.length).to.be(0); + }); + + // This test ensures that the user is not allowed to define the namespaces query param + // so she cannot search across spaces + it('should NOT allow to pass a namespaces query parameter', async () => { + const obsCase = await createCase( + supertest, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200 + ); + + await createComment({ + supertest, + params: { ...postCommentUserReq, owner: 'observabilityFixture' }, + caseId: obsCase.id, + }); + + await supertest + .get(`${CASES_URL}/${obsCase.id}/comments/_find?namespaces[0]=*`) + .expect(400); + + await supertest.get(`${CASES_URL}/${obsCase.id}/comments/_find?namespaces=*`).expect(400); + }); + + it('should NOT allow to pass a non supported query parameter', async () => { + await supertest.get(`${CASES_URL}/id/comments/_find?notExists=papa`).expect(400); + await supertest.get(`${CASES_URL}/id/comments/_find?owner=papa`).expect(400); + }); + }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts index 736d04f43ed051..2be30ed7bc02ce 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts @@ -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 { postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; +import { postCaseReq, getPostCaseRequest, postCommentUserReq } from '../../../../common/lib/mock'; import { createCaseAction, createSubCase, @@ -20,6 +20,17 @@ import { getAllComments, } from '../../../../common/lib/utils'; import { CommentType } from '../../../../../../plugins/cases/common/api'; +import { + globalRead, + noKibanaPrivileges, + obsOnly, + obsOnlyRead, + obsSec, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../common/lib/authentication/users'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -33,9 +44,17 @@ export default ({ getService }: FtrProviderContext): void => { it('should get multiple comments for a single case', async () => { const postedCase = await createCase(supertest, postCaseReq); - await createComment(supertest, postedCase.id, postCommentUserReq); - await createComment(supertest, postedCase.id, postCommentUserReq); - const comments = await getAllComments(supertest, postedCase.id); + await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + const comments = await getAllComments({ supertest, caseId: postedCase.id }); expect(comments.length).to.eql(2); }); @@ -113,5 +132,99 @@ export default ({ getService }: FtrProviderContext): void => { expect(body.length).to.eql(0); }); }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + it('should get all comments when the user has the correct permissions', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space1' } + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: { user: superUser, space: 'space1' }, + }); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: { user: superUser, space: 'space1' }, + }); + + for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { + const comments = await getAllComments({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + auth: { user, space: 'space1' }, + }); + + expect(comments.length).to.eql(2); + } + }); + + it('should not get comments when the user does not have correct permission', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space1' } + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: { user: superUser, space: 'space1' }, + }); + + for (const scenario of [ + { user: noKibanaPrivileges, returnCode: 403 }, + { user: obsOnly, returnCode: 200 }, + { user: obsOnlyRead, returnCode: 200 }, + ]) { + const comments = await getAllComments({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + auth: { user: scenario.user, space: 'space1' }, + expectedHttpCode: scenario.returnCode, + }); + + // only check the length if we get a 200 in response + if (scenario.returnCode === 200) { + expect(comments.length).to.be(0); + } + } + }); + + it('should NOT get a comment in a space with no permissions', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space2' } + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: { user: superUser, space: 'space2' }, + }); + + await getAllComments({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + auth: { user: secOnly, space: 'space2' }, + expectedHttpCode: 403, + }); + }); + }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts index 441f01843f8650..7b55d468312a1b 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts @@ -6,9 +6,9 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; +import { postCaseReq, postCommentUserReq, getPostCaseRequest } from '../../../../common/lib/mock'; import { createCaseAction, createSubCase, @@ -19,6 +19,17 @@ import { getComment, } from '../../../../common/lib/utils'; import { CommentType } from '../../../../../../plugins/cases/common/api'; +import { + globalRead, + noKibanaPrivileges, + obsOnly, + obsOnlyRead, + obsSec, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../common/lib/authentication/users'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -32,14 +43,27 @@ export default ({ getService }: FtrProviderContext): void => { it('should get a comment', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); - const comment = await getComment(supertest, postedCase.id, patchedCase.comments![0].id); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + const comment = await getComment({ + supertest, + caseId: postedCase.id, + commentId: patchedCase.comments![0].id, + }); expect(comment).to.eql(patchedCase.comments![0]); }); it('unhappy path - 404s when comment is not there', async () => { - await getComment(supertest, 'fake-id', 'fake-id', 404); + await getComment({ + supertest, + caseId: 'fake-id', + commentId: 'fake-id', + expectedHttpCode: 404, + }); }); // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests @@ -53,9 +77,92 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should get a sub case comment', async () => { const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - const comment = await getComment(supertest, caseInfo.id, caseInfo.comments![0].id); + const comment = await getComment({ + supertest, + caseId: caseInfo.id, + commentId: caseInfo.comments![0].id, + }); expect(comment.type).to.be(CommentType.generatedAlert); }); }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + it('should get a comment when the user has the correct permissions', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space1' } + ); + + const caseWithComment = await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: { user: superUser, space: 'space1' }, + }); + + for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { + await getComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + commentId: caseWithComment.comments![0].id, + auth: { user, space: 'space1' }, + }); + } + }); + + it('should not get comment when the user does not have correct permissions', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space1' } + ); + + const caseWithComment = await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: { user: superUser, space: 'space1' }, + }); + + for (const user of [noKibanaPrivileges, obsOnly, obsOnlyRead]) { + await getComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + commentId: caseWithComment.comments![0].id, + auth: { user, space: 'space1' }, + expectedHttpCode: 403, + }); + } + }); + + it('should NOT get a case in a space with no permissions', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space2' } + ); + + const caseWithComment = await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: { user: superUser, space: 'space2' }, + }); + + await getComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + commentId: caseWithComment.comments![0].id, + auth: { user: secOnly, space: 'space2' }, + expectedHttpCode: 403, + }); + }); + }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts index b73b89d33e9c60..fcaebddeb8bde7 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts @@ -7,7 +7,7 @@ import { omit } from 'lodash/fp'; 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 { @@ -21,6 +21,7 @@ import { postCaseReq, postCommentUserReq, postCommentAlertReq, + getPostCaseRequest, } from '../../../../common/lib/mock'; import { createCaseAction, @@ -34,6 +35,16 @@ import { createComment, updateComment, } from '../../../../common/lib/utils'; +import { + globalRead, + noKibanaPrivileges, + obsOnly, + obsOnlyRead, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../common/lib/authentication/users'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -61,6 +72,7 @@ export default ({ getService }: FtrProviderContext): void => { id: 'id', name: 'name', }, + owner: 'securitySolutionFixture', }) .expect(400); @@ -147,14 +159,23 @@ export default ({ getService }: FtrProviderContext): void => { it('should patch a comment', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); const newComment = 'Well I decided to update my comment. So what? Deal with it.'; - const updatedCase = await updateComment(supertest, postedCase.id, { - id: patchedCase.comments![0].id, - version: patchedCase.comments![0].version, - comment: newComment, - type: CommentType.user, + const updatedCase = await updateComment({ + supertest, + caseId: postedCase.id, + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + comment: newComment, + type: CommentType.user, + owner: 'securitySolutionFixture', + }, }); const userComment = updatedCase.comments![0] as AttributesTypeUser; @@ -165,16 +186,25 @@ export default ({ getService }: FtrProviderContext): void => { it('should patch an alert', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment(supertest, postedCase.id, postCommentAlertReq); - const updatedCase = await updateComment(supertest, postedCase.id, { - id: patchedCase.comments![0].id, - version: patchedCase.comments![0].version, - type: CommentType.alert, - alertId: 'new-id', - index: postCommentAlertReq.index, - rule: { - id: 'id', - name: 'name', + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentAlertReq, + }); + const updatedCase = await updateComment({ + supertest, + caseId: postedCase.id, + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + type: CommentType.alert, + alertId: 'new-id', + index: postCommentAlertReq.index, + rule: { + id: 'id', + name: 'name', + }, + owner: 'securitySolutionFixture', }, }); @@ -189,43 +219,71 @@ export default ({ getService }: FtrProviderContext): void => { expect(alertComment.updated_by).to.eql(defaultUser); }); + it('should not allow updating the owner of a comment', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + + await updateComment({ + supertest, + caseId: postedCase.id, + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + type: CommentType.user, + comment: postCommentUserReq.comment, + owner: 'changedOwner', + }, + expectedHttpCode: 400, + }); + }); + it('unhappy path - 404s when comment is not there', async () => { const postedCase = await createCase(supertest, postCaseReq); - await updateComment( + await updateComment({ supertest, - postedCase.id, - { + caseId: postedCase.id, + req: { id: 'id', version: 'version', type: CommentType.user, comment: 'comment', + owner: 'securitySolutionFixture', }, - 404 - ); + expectedHttpCode: 404, + }); }); it('unhappy path - 404s when case is not there', async () => { - await updateComment( + await updateComment({ supertest, - 'fake-id', - { + caseId: 'fake-id', + req: { id: 'id', version: 'version', type: CommentType.user, comment: 'comment', + owner: 'securitySolutionFixture', }, - 404 - ); + expectedHttpCode: 404, + }); }); it('unhappy path - 400s when trying to change comment type', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); - await updateComment( + await updateComment({ supertest, - postedCase.id, - { + caseId: postedCase.id, + req: { id: patchedCase.comments![0].id, version: patchedCase.comments![0].version, type: CommentType.alert, @@ -235,50 +293,64 @@ export default ({ getService }: FtrProviderContext): void => { id: 'id', name: 'name', }, + owner: 'securitySolutionFixture', }, - 400 - ); + expectedHttpCode: 400, + }); }); it('unhappy path - 400s when missing attributes for type user', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); - await updateComment( + await updateComment({ supertest, - postedCase.id, + caseId: postedCase.id, // @ts-expect-error - { + req: { id: patchedCase.comments![0].id, version: patchedCase.comments![0].version, }, - 400 - ); + expectedHttpCode: 400, + }); }); it('unhappy path - 400s when adding excess attributes for type user', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); for (const attribute of ['alertId', 'index']) { - await updateComment( + await updateComment({ supertest, - postedCase.id, - { + caseId: postedCase.id, + req: { id: patchedCase.comments![0].id, version: patchedCase.comments![0].version, comment: 'a comment', type: CommentType.user, [attribute]: attribute, + owner: 'securitySolutionFixture', }, - 400 - ); + expectedHttpCode: 400, + }); } }); it('unhappy path - 400s when missing attributes for type alert', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment(supertest, postedCase.id, postCommentAlertReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentAlertReq, + }); const allRequestAttributes = { type: CommentType.alert, @@ -292,29 +364,33 @@ export default ({ getService }: FtrProviderContext): void => { for (const attribute of ['alertId', 'index']) { const requestAttributes = omit(attribute, allRequestAttributes); - await updateComment( + await updateComment({ supertest, - postedCase.id, + caseId: postedCase.id, // @ts-expect-error - { + req: { id: patchedCase.comments![0].id, version: patchedCase.comments![0].version, ...requestAttributes, }, - 400 - ); + expectedHttpCode: 400, + }); } }); it('unhappy path - 400s when adding excess attributes for type alert', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment(supertest, postedCase.id, postCommentAlertReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentAlertReq, + }); for (const attribute of ['comment']) { - await updateComment( + await updateComment({ supertest, - postedCase.id, - { + caseId: postedCase.id, + req: { id: patchedCase.comments![0].id, version: patchedCase.comments![0].version, type: CommentType.alert, @@ -324,29 +400,35 @@ export default ({ getService }: FtrProviderContext): void => { id: 'id', name: 'name', }, + owner: 'securitySolutionFixture', [attribute]: attribute, }, - 400 - ); + expectedHttpCode: 400, + }); } }); it('unhappy path - 409s when conflict', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); const newComment = 'Well I decided to update my comment. So what? Deal with it.'; - await updateComment( + await updateComment({ supertest, - postedCase.id, - { + caseId: postedCase.id, + req: { id: patchedCase.comments![0].id, version: 'version-mismatch', type: CommentType.user, comment: newComment, + owner: 'securitySolutionFixture', }, - 409 - ); + expectedHttpCode: 409, + }); }); describe('alert format', () => { @@ -359,21 +441,26 @@ export default ({ getService }: FtrProviderContext): void => { ]) { it(`throws an error with an alert comment with contents id: ${alertId} indices: ${index} type: ${type}`, async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment(supertest, postedCase.id, postCommentAlertReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentAlertReq, + }); - await updateComment( + await updateComment({ supertest, - patchedCase.id, - { + caseId: patchedCase.id, + req: { id: patchedCase.comments![0].id, version: patchedCase.comments![0].version, type: type as AlertComment, alertId, index, + owner: 'securitySolutionFixture', rule: postCommentAlertReq.rule, }, - 400 - ); + expectedHttpCode: 400, + }); }); } @@ -383,23 +470,171 @@ export default ({ getService }: FtrProviderContext): void => { ]) { it(`does not throw an error with an alert comment with contents id: ${alertId} indices: ${index} type: ${type}`, async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment(supertest, postedCase.id, { - ...postCommentAlertReq, - alertId, - index, - type: type as AlertComment, + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: { + ...postCommentAlertReq, + alertId, + index, + owner: 'securitySolutionFixture', + type: type as AlertComment, + }, + }); + + await updateComment({ + supertest, + caseId: postedCase.id, + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + type: type as AlertComment, + alertId, + index, + owner: 'securitySolutionFixture', + rule: postCommentAlertReq.rule, + }, }); + }); + } + }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should update a comment that the user has permissions for', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space1' } + ); + + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + auth: { user: superUser, space: 'space1' }, + }); + + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; + const updatedCase = await updateComment({ + supertest, + caseId: postedCase.id, + req: { + ...postCommentUserReq, + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + comment: newComment, + }, + auth: { user: secOnly, space: 'space1' }, + }); + + const userComment = updatedCase.comments![0] as AttributesTypeUser; + expect(userComment.comment).to.eql(newComment); + expect(userComment.type).to.eql(CommentType.user); + expect(updatedCase.updated_by).to.eql(defaultUser); + expect(userComment.owner).to.eql('securitySolutionFixture'); + }); + + it('should not update a comment that has a different owner thant he user has access to', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space1' } + ); + + const patchedCase = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: { user: superUser, space: 'space1' }, + }); - await updateComment(supertest, postedCase.id, { + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; + await updateComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + req: { + ...postCommentUserReq, id: patchedCase.comments![0].id, version: patchedCase.comments![0].version, - type: type as AlertComment, - alertId, - index, - rule: postCommentAlertReq.rule, + comment: newComment, + }, + auth: { user: obsOnly, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT update a comment`, async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space1' } + ); + + const patchedCase = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: { user: superUser, space: 'space1' }, + }); + + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; + await updateComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + req: { + ...postCommentUserReq, + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + comment: newComment, + }, + auth: { user, space: 'space1' }, + expectedHttpCode: 403, }); }); } + + it('should not update a comment in a space the user does not have permissions', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space2' } + ); + + const patchedCase = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: { user: superUser, space: 'space2' }, + }); + + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; + await updateComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + req: { + ...postCommentUserReq, + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + comment: newComment, + }, + auth: { user: secOnly, space: 'space2' }, + // getting the case will fail in the saved object layer with a 403 + expectedHttpCode: 403, + }); + }); }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts index b63e21eea201a3..0e501648c512bf 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts @@ -7,7 +7,7 @@ import { omit } from 'lodash/fp'; 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 { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../../plugins/security_solution/common/constants'; @@ -24,6 +24,7 @@ import { postCommentAlertReq, postCollectionReq, postCommentGenAlertReq, + getPostCaseRequest, } from '../../../../common/lib/mock'; import { createCaseAction, @@ -50,6 +51,16 @@ import { createRule, getQuerySignalIds, } from '../../../../../detection_engine_api_integration/utils'; +import { + globalRead, + noKibanaPrivileges, + obsOnly, + obsOnlyRead, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../common/lib/authentication/users'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -67,7 +78,11 @@ export default ({ getService }: FtrProviderContext): void => { describe('happy path', () => { it('should post a comment', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); const comment = removeServerGeneratedPropertiesFromSavedObject( patchedCase.comments![0] as AttributesTypeUser ); @@ -80,6 +95,7 @@ export default ({ getService }: FtrProviderContext): void => { pushed_at: null, pushed_by: null, updated_by: null, + owner: 'securitySolutionFixture', }); // updates the case correctly after adding a comment @@ -89,7 +105,11 @@ export default ({ getService }: FtrProviderContext): void => { it('should post an alert', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment(supertest, postedCase.id, postCommentAlertReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentAlertReq, + }); const comment = removeServerGeneratedPropertiesFromSavedObject( patchedCase.comments![0] as AttributesTypeAlerts ); @@ -104,6 +124,7 @@ export default ({ getService }: FtrProviderContext): void => { pushed_at: null, pushed_by: null, updated_by: null, + owner: 'securitySolutionFixture', }); // updates the case correctly after adding a comment @@ -113,7 +134,11 @@ export default ({ getService }: FtrProviderContext): void => { it('creates a user action', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); const userActions = await getAllUserAction(supertest, postedCase.id); const commentUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); @@ -121,7 +146,7 @@ export default ({ getService }: FtrProviderContext): void => { action_field: ['comment'], action: 'create', action_by: defaultUser, - new_value: `{"comment":"${postCommentUserReq.comment}","type":"${postCommentUserReq.type}"}`, + new_value: `{"comment":"${postCommentUserReq.comment}","type":"${postCommentUserReq.type}","owner":"securitySolutionFixture"}`, old_value: null, case_id: `${postedCase.id}`, comment_id: `${patchedCase.comments![0].id}`, @@ -131,46 +156,61 @@ export default ({ getService }: FtrProviderContext): void => { }); describe('unhappy path', () => { + it('400s when attempting to create a comment with a different owner than the case', async () => { + const postedCase = await createCase( + supertest, + getPostCaseRequest({ owner: 'securitySolutionFixture' }) + ); + + await createComment({ + supertest, + caseId: postedCase.id, + params: { ...postCommentUserReq, owner: 'observabilityFixture' }, + expectedHttpCode: 400, + }); + }); + it('400s when type is missing', async () => { const postedCase = await createCase(supertest, postCaseReq); - await createComment( + await createComment({ supertest, - postedCase.id, - { + caseId: postedCase.id, + params: { // @ts-expect-error bad: 'comment', }, - 400 - ); + expectedHttpCode: 400, + }); }); it('400s when missing attributes for type user', async () => { const postedCase = await createCase(supertest, postCaseReq); - await createComment( + await createComment({ supertest, - postedCase.id, + caseId: postedCase.id, // @ts-expect-error - { + params: { type: CommentType.user, }, - 400 - ); + expectedHttpCode: 400, + }); }); it('400s when adding excess attributes for type user', async () => { const postedCase = await createCase(supertest, postCaseReq); for (const attribute of ['alertId', 'index']) { - await createComment( + await createComment({ supertest, - postedCase.id, - { + caseId: postedCase.id, + params: { type: CommentType.user, [attribute]: attribute, comment: 'a comment', + owner: 'securitySolutionFixture', }, - 400 - ); + expectedHttpCode: 400, + }); } }); @@ -185,12 +225,18 @@ export default ({ getService }: FtrProviderContext): void => { id: 'id', name: 'name', }, + owner: 'securitySolutionFixture', }; for (const attribute of ['alertId', 'index']) { const requestAttributes = omit(attribute, allRequestAttributes); - // @ts-expect-error - await createComment(supertest, postedCase.id, requestAttributes, 400); + await createComment({ + supertest, + caseId: postedCase.id, + // @ts-expect-error + params: requestAttributes, + expectedHttpCode: 400, + }); } }); @@ -198,10 +244,10 @@ export default ({ getService }: FtrProviderContext): void => { const postedCase = await createCase(supertest, postCaseReq); for (const attribute of ['comment']) { - await createComment( + await createComment({ supertest, - postedCase.id, - { + caseId: postedCase.id, + params: { type: CommentType.alert, [attribute]: attribute, alertId: 'test-id', @@ -210,22 +256,23 @@ export default ({ getService }: FtrProviderContext): void => { id: 'id', name: 'name', }, + owner: 'securitySolutionFixture', }, - 400 - ); + expectedHttpCode: 400, + }); } }); it('400s when case is missing', async () => { - await createComment( + await createComment({ supertest, - 'not-exists', - { + caseId: 'not-exists', + params: { // @ts-expect-error bad: 'comment', }, - 400 - ); + expectedHttpCode: 400, + }); }); it('400s when adding an alert to a closed case', async () => { @@ -245,13 +292,23 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - await createComment(supertest, postedCase.id, postCommentAlertReq, 400); + await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentAlertReq, + expectedHttpCode: 400, + }); }); // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests it.skip('400s when adding an alert to a collection case', async () => { const postedCase = await createCase(supertest, postCollectionReq); - await createComment(supertest, postedCase.id, postCommentAlertReq, 400); + await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentAlertReq, + expectedHttpCode: 400, + }); }); it('400s when adding a generated alert to an individual case', async () => { @@ -312,14 +369,19 @@ export default ({ getService }: FtrProviderContext): void => { const alert = signals.hits.hits[0]; expect(alert._source.signal.status).eql('open'); - await createComment(supertest, postedCase.id, { - alertId: alert._id, - index: alert._index, - rule: { - id: 'id', - name: 'name', + await createComment({ + supertest, + caseId: postedCase.id, + params: { + alertId: alert._id, + index: alert._index, + rule: { + id: 'id', + name: 'name', + }, + owner: 'securitySolutionFixture', + type: CommentType.alert, }, - type: CommentType.alert, }); const { body: updatedAlert } = await supertest @@ -360,14 +422,19 @@ export default ({ getService }: FtrProviderContext): void => { const alert = signals.hits.hits[0]; expect(alert._source.signal.status).eql('open'); - await createComment(supertest, postedCase.id, { - alertId: alert._id, - index: alert._index, - rule: { - id: 'id', - name: 'name', + await createComment({ + supertest, + caseId: postedCase.id, + params: { + alertId: alert._id, + index: alert._index, + rule: { + id: 'id', + name: 'name', + }, + owner: 'securitySolutionFixture', + type: CommentType.alert, }, - type: CommentType.alert, }); const { body: updatedAlert } = await supertest @@ -391,12 +458,12 @@ export default ({ getService }: FtrProviderContext): void => { ]) { it(`throws an error with an alert comment with contents id: ${alertId} indices: ${index} type: ${type}`, async () => { const postedCase = await createCase(supertest, postCaseReq); - await createComment( + await createComment({ supertest, - postedCase.id, - { ...postCommentAlertReq, alertId, index, type: type as AlertComment }, - 400 - ); + caseId: postedCase.id, + params: { ...postCommentAlertReq, alertId, index, type: type as AlertComment }, + expectedHttpCode: 400, + }); }); } @@ -406,17 +473,17 @@ export default ({ getService }: FtrProviderContext): void => { ]) { it(`does not throw an error with an alert comment with contents id: ${alertId} indices: ${index} type: ${type}`, async () => { const postedCase = await createCase(supertest, postCaseReq); - await createComment( + await createComment({ supertest, - postedCase.id, - { + caseId: postedCase.id, + params: { ...postCommentAlertReq, alertId, index, type: type as AlertComment, }, - 200 - ); + expectedHttpCode: 200, + }); }); } }); @@ -453,5 +520,84 @@ export default ({ getService }: FtrProviderContext): void => { expect(subCaseComments.comments[1].type).to.be(CommentType.user); }); }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should create a comment when the user has the correct permissions for that owner', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space1' } + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: { user: secOnly, space: 'space1' }, + }); + }); + + it('should not create a comment when the user does not have permissions for that owner', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { user: obsOnly, space: 'space1' } + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: { ...postCommentUserReq, owner: 'observabilityFixture' }, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should not create a comment`, async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space1' } + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: { user, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + } + + it('should not create a comment in a space the user does not have permissions for', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space2' } + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: { user: secOnly, space: 'space2' }, + expectedHttpCode: 403, + }); + }); + }); }); }; 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 b26e8a3f3b3812..279936ebbef462 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 @@ -8,12 +8,6 @@ import expect from '@kbn/expect'; 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 { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; import { removeServerGeneratedPropertiesFromSavedObject, getConfigurationOutput, @@ -21,8 +15,6 @@ import { getConfiguration, createConfiguration, getConfigurationRequest, - createConnector, - getServiceNowConnector, ensureSavedObjectIsAuthorized, } from '../../../../common/lib/utils'; import { @@ -42,21 +34,10 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); - const kibanaServer = getService('kibanaServer'); describe('get_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 return an empty find body correctly if no configuration is loaded', async () => { @@ -91,54 +72,6 @@ export default ({ getService }: FtrProviderContext): void => { expect(data).to.eql(getConfigurationOutput()); }); - it('should return a configuration with mapping', async () => { - const connector = await createConnector(supertest, { - ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, - }); - - actionsRemover.add('default', connector.id, 'action', 'actions'); - - await createConfiguration( - supertest, - getConfigurationRequest({ - id: connector.id, - name: connector.name, - type: connector.connector_type_id as ConnectorTypes, - }) - ); - - const configuration = await getConfiguration({ supertest }); - const data = removeServerGeneratedPropertiesFromSavedObject(configuration[0]); - 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, - }, - }) - ); - }); - describe('rbac', () => { it('should return the correct configuration', async () => { await createConfiguration(supertestWithoutAuth, getConfigurationRequest(), 200, { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/get_connectors.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/get_connectors.ts index cfa23a968182f7..5156b9537583f3 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/get_connectors.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/get_connectors.ts @@ -8,86 +8,18 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; -import { - getCaseConnectors, - createConnector, - getServiceNowConnector, - getJiraConnector, - getResilientConnector, - getServiceNowSIRConnector, - getWebhookConnector, -} from '../../../../common/lib/utils'; +import { getCaseConnectors } from '../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); - const actionsRemover = new ActionsRemover(supertest); describe('get_connectors', () => { - afterEach(async () => { - await actionsRemover.removeAll(); - }); - it('should return an empty find body correctly if no connectors are loaded', async () => { const connectors = await getCaseConnectors(supertest); expect(connectors).to.eql([]); }); - it('should return case owned connectors', async () => { - const sn = await createConnector(supertest, getServiceNowConnector()); - actionsRemover.add('default', sn.id, 'action', 'actions'); - - const jira = await createConnector(supertest, getJiraConnector()); - actionsRemover.add('default', jira.id, 'action', 'actions'); - - const resilient = await createConnector(supertest, getResilientConnector()); - actionsRemover.add('default', resilient.id, 'action', 'actions'); - - const sir = await createConnector(supertest, getServiceNowSIRConnector()); - actionsRemover.add('default', sir.id, 'action', 'actions'); - - // Should not be returned when getting the connectors - const webhook = await createConnector(supertest, getWebhookConnector()); - actionsRemover.add('default', webhook.id, 'action', 'actions'); - - const connectors = await getCaseConnectors(supertest); - expect(connectors).to.eql([ - { - id: jira.id, - actionTypeId: '.jira', - name: 'Jira Connector', - config: { apiUrl: 'http://some.non.existent.com', projectKey: 'pkey' }, - isPreconfigured: false, - referencedByCount: 0, - }, - { - id: resilient.id, - actionTypeId: '.resilient', - name: 'Resilient Connector', - config: { apiUrl: 'http://some.non.existent.com', orgId: 'pkey' }, - isPreconfigured: false, - referencedByCount: 0, - }, - { - id: sn.id, - actionTypeId: '.servicenow', - name: 'ServiceNow Connector', - config: { apiUrl: 'http://some.non.existent.com' }, - isPreconfigured: false, - referencedByCount: 0, - }, - { - id: sir.id, - actionTypeId: '.servicenow-sir', - name: 'ServiceNow Connector', - config: { apiUrl: 'http://some.non.existent.com' }, - isPreconfigured: false, - referencedByCount: 0, - }, - ]); - }); - it.skip('filters out connectors that are not enabled in license', async () => { // TODO: Should find a way to downgrade license to gold and upgrade back to trial }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/migrations.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/migrations.ts index fd9baf39b49f96..cc2f6c414503d5 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/migrations.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/migrations.ts @@ -30,9 +30,10 @@ export default function createGetTests({ getService }: FtrProviderContext) { .send() .expect(200); - expect(body).key('connector'); - expect(body).not.key('connector_id'); - expect(body.connector).to.eql({ + expect(body.length).to.be(1); + expect(body[0]).key('connector'); + expect(body[0]).not.key('connector_id'); + expect(body[0].connector).to.eql({ id: 'connector-1', name: 'Connector 1', type: '.none', 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 c76e5f408e4757..ced727f8e4e75e 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 @@ -8,10 +8,6 @@ import expect from '@kbn/expect'; 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, @@ -20,10 +16,7 @@ import { deleteConfiguration, createConfiguration, updateConfiguration, - getServiceNowConnector, - createConnector, } from '../../../../common/lib/utils'; -import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; import { secOnly, obsOnlyRead, @@ -39,17 +32,9 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); - const kibanaServer = getService('kibanaServer'); describe('patch_configure', () => { const actionsRemover = new ActionsRemover(supertest); - let servicenowSimulatorURL: string = ''; - - before(() => { - servicenowSimulatorURL = kibanaServer.resolveUrl( - getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) - ); - }); afterEach(async () => { await deleteConfiguration(es); @@ -67,113 +52,6 @@ export default ({ getService }: FtrProviderContext): void => { expect(data).to.eql({ ...getConfigurationOutput(true), closure_type: 'close-by-pushing' }); }); - it('should patch a configuration connector and create mappings', async () => { - const connector = await createConnector(supertest, { - ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, - }); - - 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, configuration.id, { - ...getConfigurationRequest({ - id: connector.id, - name: connector.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: connector.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 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 () => { const configuration = await createConfiguration(supertest); await updateConfiguration( 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 a47c10efe5037b..f1dae9f319109b 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 @@ -9,10 +9,6 @@ import expect from '@kbn/expect'; import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; 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, @@ -20,8 +16,6 @@ import { getConfigurationOutput, deleteConfiguration, createConfiguration, - createConnector, - getServiceNowConnector, getConfiguration, ensureSavedObjectIsAuthorized, } from '../../../../common/lib/utils'; @@ -41,17 +35,9 @@ 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); @@ -73,53 +59,6 @@ export default ({ getService }: FtrProviderContext): void => { 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, diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/connectors/case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/connectors/case.ts index 9be413015c0519..fd9ec8142b49fe 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/connectors/case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/connectors/case.ts @@ -718,7 +718,6 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should add a comment of type alert', async () => { - // TODO: don't do all this stuff const rule = getRuleForSignalTesting(['auditbeat-*']); const { id } = await createRule(supertest, rule); await waitForRuleSuccessOrStatus(supertest, id); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/sub_cases/find_sub_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/sub_cases/find_sub_cases.ts index 14c0460c7583b2..d54523bec0c4d4 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/sub_cases/find_sub_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/sub_cases/find_sub_cases.ts @@ -298,6 +298,7 @@ export default ({ getService }: FtrProviderContext): void => { { _id: `${i}`, _index: 'test-index', ruleId: 'rule-id', ruleName: 'rule name' }, ]), type: CommentType.generatedAlert, + owner: 'securitySolutionFixture', }; responses.push( await createSubCase({ diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/sub_cases/patch_sub_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/sub_cases/patch_sub_cases.ts index 43526bca644db6..442644463fa38d 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/sub_cases/patch_sub_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/sub_cases/patch_sub_cases.ts @@ -100,6 +100,7 @@ export default function ({ getService }: FtrProviderContext) { }, ]), type: CommentType.generatedAlert, + owner: 'securitySolutionFixture', }, }); @@ -156,6 +157,7 @@ export default function ({ getService }: FtrProviderContext) { }, ]), type: CommentType.generatedAlert, + owner: 'securitySolutionFixture', }, }); @@ -225,6 +227,7 @@ export default function ({ getService }: FtrProviderContext) { }, ]), type: CommentType.generatedAlert, + owner: 'securitySolutionFixture', }, }); @@ -243,6 +246,7 @@ export default function ({ getService }: FtrProviderContext) { }, ]), type: CommentType.generatedAlert, + owner: 'securitySolutionFixture', }, }); @@ -354,6 +358,7 @@ export default function ({ getService }: FtrProviderContext) { }, ]), type: CommentType.generatedAlert, + owner: 'securitySolutionFixture', }, }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts index 56a6d1b15004b8..19911890929d2d 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts @@ -293,12 +293,17 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200); const newComment = 'Well I decided to update my comment. So what? Deal with it.'; - await supertest.patch(`${CASES_URL}/${postedCase.id}/comments`).set('kbn-xsrf', 'true').send({ - id: patchedCase.comments[0].id, - version: patchedCase.comments[0].version, - comment: newComment, - type: CommentType.user, - }); + await supertest + .patch(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send({ + id: patchedCase.comments[0].id, + version: patchedCase.comments[0].version, + comment: newComment, + type: CommentType.user, + owner: 'securitySolutionFixture', + }) + .expect(200); const { body } = await supertest .get(`${CASES_URL}/${postedCase.id}/user_actions`) @@ -313,6 +318,7 @@ export default ({ getService }: FtrProviderContext): void => { expect(JSON.parse(body[2].new_value)).to.eql({ comment: newComment, type: CommentType.user, + owner: 'securitySolutionFixture', }); }); }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts index 67773067ad2d49..88f7c15f4a5fe8 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts @@ -129,7 +129,7 @@ export default ({ getService }: FtrProviderContext): void => { it('pushes a comment appropriately', async () => { const { postedCase, connector } = await createCaseWithConnector(); - await createComment(supertest, postedCase.id, postCommentUserReq); + await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq }); const theCase = await pushCase(supertest, postedCase.id, connector.id); expect(theCase.comments![0].pushed_by).to.eql(defaultUser); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_configure.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_configure.ts new file mode 100644 index 00000000000000..6d556423893d53 --- /dev/null +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_configure.ts @@ -0,0 +1,95 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +import { + ExternalServiceSimulator, + getExternalServiceSimulatorPath, +} from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; + +import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; +import { + getServiceNowConnector, + createConnector, + createConfiguration, + getConfiguration, + getConfigurationRequest, + removeServerGeneratedPropertiesFromSavedObject, + getConfigurationOutput, +} from '../../../../common/lib/utils'; +import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const actionsRemover = new ActionsRemover(supertest); + const kibanaServer = getService('kibanaServer'); + + describe('get_configure', () => { + let servicenowSimulatorURL: string = ''; + + before(() => { + servicenowSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) + ); + }); + + afterEach(async () => { + await actionsRemover.removeAll(); + }); + + it('should return a configuration with mapping', async () => { + const connector = await createConnector(supertest, { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }); + actionsRemover.add('default', connector.id, 'action', 'actions'); + + await createConfiguration( + supertest, + getConfigurationRequest({ + id: connector.id, + name: connector.name, + type: connector.connector_type_id as ConnectorTypes, + }) + ); + + const configuration = await getConfiguration({ supertest }); + + const data = removeServerGeneratedPropertiesFromSavedObject(configuration[0]); + 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, + }, + }) + ); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts index 75d1378260b191..6faea0e1789bb5 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts @@ -14,6 +14,8 @@ import { getServiceNowConnector, getJiraConnector, getResilientConnector, + createConnector, + getServiceNowSIRConnector, } from '../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export @@ -38,7 +40,7 @@ export default ({ getService }: FtrProviderContext): void => { .set('kbn-xsrf', 'true') .send({ name: 'An email action', - actionTypeId: '.email', + connector_type_id: '.email', config: { service: '__json', from: 'bob@example.com', @@ -62,6 +64,9 @@ export default ({ getService }: FtrProviderContext): void => { .send(getResilientConnector()) .expect(200); + const sir = await createConnector(supertest, getServiceNowSIRConnector()); + + actionsRemover.add('default', sir.id, 'action', 'actions'); actionsRemover.add('default', snConnector.id, 'action', 'actions'); actionsRemover.add('default', emailConnector.id, 'action', 'actions'); actionsRemover.add('default', jiraConnector.id, 'action', 'actions'); @@ -72,6 +77,7 @@ export default ({ getService }: FtrProviderContext): void => { .set('kbn-xsrf', 'true') .send() .expect(200); + expect(connectors).to.eql([ { id: jiraConnector.id, @@ -105,6 +111,14 @@ export default ({ getService }: FtrProviderContext): void => { isPreconfigured: false, referencedByCount: 0, }, + { + id: sir.id, + actionTypeId: '.servicenow-sir', + name: 'ServiceNow Connector', + config: { apiUrl: 'http://some.non.existent.com' }, + isPreconfigured: false, + referencedByCount: 0, + }, ]); }); }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/index.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/index.ts new file mode 100644 index 00000000000000..0c8c3931d15774 --- /dev/null +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('configuration tests', function () { + loadTestFile(require.resolve('./get_configure')); + loadTestFile(require.resolve('./get_connectors')); + loadTestFile(require.resolve('./patch_configure')); + loadTestFile(require.resolve('./post_configure')); + }); +}; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/patch_configure.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/patch_configure.ts new file mode 100644 index 00000000000000..9e82ce1f0c2338 --- /dev/null +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/patch_configure.ts @@ -0,0 +1,162 @@ +/* + * 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 expect from '@kbn/expect'; +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, + removeServerGeneratedPropertiesFromSavedObject, + getConfigurationOutput, + deleteConfiguration, + createConfiguration, + updateConfiguration, + getServiceNowConnector, + createConnector, +} from '../../../../common/lib/utils'; +import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const kibanaServer = getService('kibanaServer'); + + describe('patch_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 patch a configuration connector and create mappings', async () => { + const connector = await createConnector(supertest, { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }); + + actionsRemover.add('default', connector.id, 'action', 'actions'); + + // Configuration is created with no connector so the mappings are empty + const configuration = await createConfiguration(supertest); + + // the update request doesn't accept the owner field + const { owner, ...reqWithoutOwner } = getConfigurationRequest({ + id: connector.id, + name: connector.name, + type: connector.connector_type_id as ConnectorTypes, + fields: null, + }); + + const newConfiguration = await updateConfiguration(supertest, configuration.id, { + ...reqWithoutOwner, + version: configuration.version, + }); + + const data = removeServerGeneratedPropertiesFromSavedObject(newConfiguration); + expect(data).to.eql({ + ...getConfigurationOutput(true), + connector: { + id: connector.id, + name: connector.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 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, + }) + ); + + // the update request doesn't accept the owner field + const { owner, ...rest } = getConfigurationRequest({ + id: connector.id, + name: 'New name', + type: connector.connector_type_id as ConnectorTypes, + fields: null, + }); + + const newConfiguration = await updateConfiguration(supertest, configuration.id, { + ...rest, + 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', + }, + ], + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/post_configure.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/post_configure.ts new file mode 100644 index 00000000000000..503e0384859ec2 --- /dev/null +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/post_configure.ts @@ -0,0 +1,95 @@ +/* + * 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 expect from '@kbn/expect'; +import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; +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, + removeServerGeneratedPropertiesFromSavedObject, + getConfigurationOutput, + deleteConfiguration, + createConfiguration, + createConnector, + getServiceNowConnector, +} from '../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + 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 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, + }, + }) + ); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts index 6f2c3a6bb27013..5ba09dd56bd67b 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts @@ -27,5 +27,6 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => { // Trial loadTestFile(require.resolve('./cases/push_case')); + loadTestFile(require.resolve('./configure/index')); }); }; From 18e75d914c93989c409e27a9cfc9dc17225837cb Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Mon, 3 May 2021 21:11:25 -0400 Subject: [PATCH 051/113] [Cases] Add RBAC to remaining Cases APIs (#98762) * Starting rbac for comments * Adding authorization to rest of comment apis * Starting the comment rbac tests * Fixing some of the rbac tests * Adding some integration tests * Starting patch tests * Working tests for comments * Working tests * Fixing some tests * Fixing type issues from pulling in master * Fixing connector tests that only work in trial license * Attempting to fix cypress * Mock return of array for configure * Fixing cypress test * Cleaning up * Working case update tests * Addressing PR comments * Reducing operations * Working rbac push case tests * Starting stats apis * Working status tests * User action tests and fixing migration errors * Fixing type errors * including error in message * Addressing pr feedback --- x-pack/plugins/cases/common/api/cases/case.ts | 1 - .../cases/common/api/cases/configure.ts | 5 +- .../cases/common/api/cases/constants.ts | 11 + .../plugins/cases/common/api/cases/index.ts | 1 + .../cases/common/api/cases/sub_case.ts | 1 + .../cases/common/api/cases/user_actions.ts | 4 +- .../cases/server/authorization/index.ts | 27 + .../cases/server/authorization/types.ts | 3 + .../cases/server/authorization/utils.ts | 7 +- .../cases/server/client/attachments/add.ts | 3 + .../cases/server/client/attachments/delete.ts | 2 + .../cases/server/client/attachments/update.ts | 1 + .../cases/server/client/cases/create.ts | 4 +- .../cases/server/client/cases/delete.ts | 8 +- .../plugins/cases/server/client/cases/find.ts | 2 +- .../plugins/cases/server/client/cases/mock.ts | 6 + .../plugins/cases/server/client/cases/push.ts | 208 +++--- .../cases/server/client/cases/update.ts | 87 ++- .../cases/server/client/cases/utils.test.ts | 1 + .../cases/server/client/stats/client.ts | 20 +- .../cases/server/client/sub_cases/client.ts | 7 +- .../cases/server/client/user_actions/get.ts | 12 +- x-pack/plugins/cases/server/client/utils.ts | 12 +- .../api/__fixtures__/mock_saved_objects.ts | 2 + .../cases/server/services/cases/index.ts | 26 +- .../server/services/user_actions/helpers.ts | 24 +- .../feature_privilege_builder/cases.test.ts | 12 + .../feature_privilege_builder/cases.ts | 2 + .../public/cases/containers/mock.ts | 2 + .../common/lib/authentication/index.ts | 4 +- .../common/lib/authentication/roles.ts | 10 + .../case_api_integration/common/lib/utils.ts | 97 ++- .../tests/basic/cases/push_case.ts | 9 +- .../tests/basic/configure/create_connector.ts | 2 +- .../security_and_spaces/tests/basic/index.ts | 8 +- .../tests/common/cases/delete_cases.ts | 7 +- .../tests/common/cases/find_cases.ts | 57 +- .../tests/common/cases/patch_cases.ts | 615 +++++++++++++----- .../tests/common/cases/post_case.ts | 1 + .../tests/common/cases/status/get_status.ts | 160 ++++- .../tests/common/comments/delete_comment.ts | 9 +- .../tests/common/comments/find_comments.ts | 9 +- .../tests/common/comments/get_all_comments.ts | 11 +- .../tests/common/comments/get_comment.ts | 9 +- .../tests/common/comments/patch_comment.ts | 13 +- .../tests/common/comments/post_comment.ts | 6 +- .../security_and_spaces/tests/common/index.ts | 6 +- .../tests/common/migrations.ts | 18 + .../user_actions/get_all_user_actions.ts | 90 ++- .../tests/trial/cases/push_case.ts | 262 ++++++-- .../tests/trial/configure/get_configure.ts | 9 +- .../tests/trial/configure/get_connectors.ts | 2 +- .../tests/trial/configure/patch_configure.ts | 18 +- .../tests/trial/configure/post_configure.ts | 9 +- .../security_and_spaces/tests/trial/index.ts | 10 +- 55 files changed, 1428 insertions(+), 524 deletions(-) create mode 100644 x-pack/plugins/cases/common/api/cases/constants.ts create mode 100644 x-pack/test/case_api_integration/security_and_spaces/tests/common/migrations.ts diff --git a/x-pack/plugins/cases/common/api/cases/case.ts b/x-pack/plugins/cases/common/api/cases/case.ts index 389caffee1a5cc..9b184d437f281c 100644 --- a/x-pack/plugins/cases/common/api/cases/case.ts +++ b/x-pack/plugins/cases/common/api/cases/case.ts @@ -38,7 +38,6 @@ const CaseBasicRt = rt.type({ [caseTypeField]: CaseTypeRt, connector: CaseConnectorRt, settings: SettingsRt, - // TODO: should a user be able to update the owner? owner: rt.string, }); diff --git a/x-pack/plugins/cases/common/api/cases/configure.ts b/x-pack/plugins/cases/common/api/cases/configure.ts index 02e2cb65962308..eeeb9ed4ebd042 100644 --- a/x-pack/plugins/cases/common/api/cases/configure.ts +++ b/x-pack/plugins/cases/common/api/cases/configure.ts @@ -10,6 +10,7 @@ import * as rt from 'io-ts'; import { UserRT } from '../user'; import { CaseConnectorRt, ConnectorMappingsRt, ESCaseConnector } from '../connectors'; import { OmitProp } from '../runtime_types'; +import { OWNER_FIELD } from './constants'; // TODO: we will need to add this type rt.literal('close-by-third-party') const ClosureTypeRT = rt.union([rt.literal('close-by-user'), rt.literal('close-by-pushing')]); @@ -20,7 +21,9 @@ const CasesConfigureBasicRt = rt.type({ owner: rt.string, }); -const CasesConfigureBasicWithoutOwnerRt = rt.type(OmitProp(CasesConfigureBasicRt.props, 'owner')); +const CasesConfigureBasicWithoutOwnerRt = rt.type( + OmitProp(CasesConfigureBasicRt.props, OWNER_FIELD) +); export const CasesConfigureRequestRt = CasesConfigureBasicRt; export const CasesConfigurePatchRt = rt.intersection([ diff --git a/x-pack/plugins/cases/common/api/cases/constants.ts b/x-pack/plugins/cases/common/api/cases/constants.ts new file mode 100644 index 00000000000000..b8dd13c5d490ea --- /dev/null +++ b/x-pack/plugins/cases/common/api/cases/constants.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * The field used for authorization in various entities within cases. + */ +export const OWNER_FIELD = 'owner'; diff --git a/x-pack/plugins/cases/common/api/cases/index.ts b/x-pack/plugins/cases/common/api/cases/index.ts index 6e7fb818cb2b58..0f78ca9b35377d 100644 --- a/x-pack/plugins/cases/common/api/cases/index.ts +++ b/x-pack/plugins/cases/common/api/cases/index.ts @@ -11,3 +11,4 @@ export * from './comment'; export * from './status'; export * from './user_actions'; export * from './sub_case'; +export * from './constants'; diff --git a/x-pack/plugins/cases/common/api/cases/sub_case.ts b/x-pack/plugins/cases/common/api/cases/sub_case.ts index ba6cd6a8affa44..826654cab2d7f1 100644 --- a/x-pack/plugins/cases/common/api/cases/sub_case.ts +++ b/x-pack/plugins/cases/common/api/cases/sub_case.ts @@ -26,6 +26,7 @@ export const SubCaseAttributesRt = rt.intersection([ created_by: rt.union([UserRT, rt.null]), updated_at: rt.union([rt.string, rt.null]), updated_by: rt.union([UserRT, rt.null]), + owner: rt.string, }), ]); diff --git a/x-pack/plugins/cases/common/api/cases/user_actions.ts b/x-pack/plugins/cases/common/api/cases/user_actions.ts index 1b53adb002436d..03912c550d77a7 100644 --- a/x-pack/plugins/cases/common/api/cases/user_actions.ts +++ b/x-pack/plugins/cases/common/api/cases/user_actions.ts @@ -6,6 +6,7 @@ */ import * as rt from 'io-ts'; +import { OWNER_FIELD } from './constants'; import { UserRT } from '../user'; @@ -22,7 +23,7 @@ const UserActionFieldTypeRt = rt.union([ rt.literal('status'), rt.literal('settings'), rt.literal('sub_case'), - rt.literal('owner'), + rt.literal(OWNER_FIELD), ]); const UserActionFieldRt = rt.array(UserActionFieldTypeRt); const UserActionRt = rt.union([ @@ -41,6 +42,7 @@ const CaseUserActionBasicRT = rt.type({ action_by: UserRT, new_value: rt.union([rt.string, rt.null]), old_value: rt.union([rt.string, rt.null]), + owner: rt.string, }); const CaseUserActionResponseRT = rt.intersection([ diff --git a/x-pack/plugins/cases/server/authorization/index.ts b/x-pack/plugins/cases/server/authorization/index.ts index be8ca55ccd262a..3a6ec502ff72b1 100644 --- a/x-pack/plugins/cases/server/authorization/index.ts +++ b/x-pack/plugins/cases/server/authorization/index.ts @@ -10,6 +10,7 @@ import { CASE_COMMENT_SAVED_OBJECT, CASE_CONFIGURE_SAVED_OBJECT, CASE_SAVED_OBJECT, + CASE_USER_ACTION_SAVED_OBJECT, } from '../../common/constants'; import { Verbs, ReadOperations, WriteOperations, OperationDetails } from './types'; @@ -101,6 +102,14 @@ export const Operations: Record Promise; export enum ReadOperations { GetCase = 'getCase', FindCases = 'findCases', + GetCaseStatuses = 'getCaseStatuses', GetComment = 'getComment', GetAllComments = 'getAllComments', FindComments = 'findComments', GetTags = 'getTags', GetReporters = 'getReporters', FindConfigurations = 'findConfigurations', + GetUserActions = 'getUserActions', } /** @@ -47,6 +49,7 @@ export enum WriteOperations { CreateCase = 'createCase', DeleteCase = 'deleteCase', UpdateCase = 'updateCase', + PushCase = 'pushCase', CreateComment = 'createComment', DeleteAllComments = 'deleteAllComments', DeleteComment = 'deleteComment', diff --git a/x-pack/plugins/cases/server/authorization/utils.ts b/x-pack/plugins/cases/server/authorization/utils.ts index 11d143eb05b2a2..eb2dcc1a0f2e40 100644 --- a/x-pack/plugins/cases/server/authorization/utils.ts +++ b/x-pack/plugins/cases/server/authorization/utils.ts @@ -7,12 +7,13 @@ import { remove, uniq } from 'lodash'; import { nodeBuilder, KueryNode } from '../../../../../src/plugins/data/common'; +import { OWNER_FIELD } from '../../common/api'; export const getOwnersFilter = (savedObjectType: string, owners: string[]): KueryNode => { return nodeBuilder.or( owners.reduce((query, owner) => { - ensureFieldIsSafeForQuery('owner', owner); - query.push(nodeBuilder.is(`${savedObjectType}.attributes.owner`, owner)); + ensureFieldIsSafeForQuery(OWNER_FIELD, owner); + query.push(nodeBuilder.is(`${savedObjectType}.attributes.${OWNER_FIELD}`, owner)); return query; }, []) ); @@ -53,5 +54,5 @@ export const includeFieldsRequiredForAuthentication = (fields?: string[]): strin if (fields === undefined) { return; } - return uniq([...fields, 'owner']); + return uniq([...fields, OWNER_FIELD]); }; diff --git a/x-pack/plugins/cases/server/client/attachments/add.ts b/x-pack/plugins/cases/server/client/attachments/add.ts index 4cc9ca7f868ec1..9480730a3f1379 100644 --- a/x-pack/plugins/cases/server/client/attachments/add.ts +++ b/x-pack/plugins/cases/server/client/attachments/add.ts @@ -105,6 +105,7 @@ async function getSubCase({ subCaseId: newSubCase.id, fields: ['status', 'sub_case'], newValue: JSON.stringify({ status: newSubCase.attributes.status }), + owner: newSubCase.attributes.owner, }), ], }); @@ -222,6 +223,7 @@ const addGeneratedAlerts = async ( commentId: newComment.id, fields: ['comment'], newValue: JSON.stringify(query), + owner: newComment.attributes.owner, }), ], }); @@ -396,6 +398,7 @@ export const addComment = async ( commentId: newComment.id, fields: ['comment'], newValue: JSON.stringify(query), + owner: newComment.attributes.owner, }), ], }); diff --git a/x-pack/plugins/cases/server/client/attachments/delete.ts b/x-pack/plugins/cases/server/client/attachments/delete.ts index f600aef64d1b68..83df367d951ee8 100644 --- a/x-pack/plugins/cases/server/client/attachments/delete.ts +++ b/x-pack/plugins/cases/server/client/attachments/delete.ts @@ -95,6 +95,7 @@ export async function deleteAll( subCaseId: subCaseID, commentId: comment.id, fields: ['comment'], + owner: comment.attributes.owner, }) ), }); @@ -167,6 +168,7 @@ export async function deleteComment( subCaseId: subCaseID, commentId: attachmentID, fields: ['comment'], + owner: myComment.attributes.owner, }), ], }); diff --git a/x-pack/plugins/cases/server/client/attachments/update.ts b/x-pack/plugins/cases/server/client/attachments/update.ts index c2c6d6800e51fb..26c44509abce8c 100644 --- a/x-pack/plugins/cases/server/client/attachments/update.ts +++ b/x-pack/plugins/cases/server/client/attachments/update.ts @@ -181,6 +181,7 @@ export async function update( // myComment.attribute contains also CommentAttributesBasicRt attributes pick(Object.keys(queryRestAttributes), myComment.attributes) ), + owner: myComment.attributes.owner, }), ], }); diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index 3f66db7281c381..4e8a5834d68698 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -20,6 +20,7 @@ import { CasesClientPostRequestRt, CasePostRequest, CaseType, + OWNER_FIELD, } from '../../../common/api'; import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; import { ensureAuthorized, getConnectorFromConfiguration } from '../utils'; @@ -108,8 +109,9 @@ export const create = async ( actionAt: createdDate, actionBy: { username, full_name, email }, caseId: newCase.id, - fields: ['description', 'status', 'tags', 'title', 'connector', 'settings', 'owner'], + fields: ['description', 'status', 'tags', 'title', 'connector', 'settings', OWNER_FIELD], newValue: JSON.stringify(query), + owner: newCase.attributes.owner, }), ], }); diff --git a/x-pack/plugins/cases/server/client/cases/delete.ts b/x-pack/plugins/cases/server/client/cases/delete.ts index 100135e2992ebb..256a8be2ccbe02 100644 --- a/x-pack/plugins/cases/server/client/cases/delete.ts +++ b/x-pack/plugins/cases/server/client/cases/delete.ts @@ -14,6 +14,7 @@ import { AttachmentService, CaseService } from '../../services'; import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; import { Operations } from '../../authorization'; import { ensureAuthorized } from '../utils'; +import { OWNER_FIELD } from '../../../common/api'; async function deleteSubCases({ attachmentService, @@ -133,12 +134,12 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P await userActionService.bulkCreate({ soClient, - actions: ids.map((id) => + actions: cases.saved_objects.map((caseInfo) => buildCaseUserActionItem({ action: 'delete', actionAt: deleteDate, actionBy: user, - caseId: id, + caseId: caseInfo.id, fields: [ 'description', 'status', @@ -146,10 +147,11 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P 'title', 'connector', 'settings', - 'owner', + OWNER_FIELD, 'comment', ...(ENABLE_CASE_CONNECTOR ? ['sub_case'] : []), ], + owner: caseInfo.attributes.owner, }) ), }); diff --git a/x-pack/plugins/cases/server/client/cases/find.ts b/x-pack/plugins/cases/server/client/cases/find.ts index 53ae6a2e76b81d..0899cd3d0150f2 100644 --- a/x-pack/plugins/cases/server/client/cases/find.ts +++ b/x-pack/plugins/cases/server/client/cases/find.ts @@ -80,7 +80,6 @@ export const find = async ( ensureSavedObjectsAreAuthorized([...cases.casesMap.values()]); - // TODO: Make sure we do not leak information when authorization is on const [openCases, inProgressCases, closedCases] = await Promise.all([ ...caseStatuses.map((status) => { const statusQuery = constructQueryOptions({ ...queryArgs, status, authorizationFilter }); @@ -88,6 +87,7 @@ export const find = async ( soClient: savedObjectsClient, caseOptions: statusQuery.case, subCaseOptions: statusQuery.subCase, + ensureSavedObjectsAreAuthorized, }); }), ]); diff --git a/x-pack/plugins/cases/server/client/cases/mock.ts b/x-pack/plugins/cases/server/client/cases/mock.ts index 1d46f5715c4ba1..01740c9a41a935 100644 --- a/x-pack/plugins/cases/server/client/cases/mock.ts +++ b/x-pack/plugins/cases/server/client/cases/mock.ts @@ -135,6 +135,7 @@ export const userActions: CaseUserActionsResponse = [ action_id: 'fd830c60-6646-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', comment_id: null, + owner: 'securitySolution', }, { action_field: ['pushed'], @@ -151,6 +152,7 @@ export const userActions: CaseUserActionsResponse = [ action_id: '0a801750-6647-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', comment_id: null, + owner: 'securitySolution', }, { action_field: ['comment'], @@ -166,6 +168,7 @@ export const userActions: CaseUserActionsResponse = [ action_id: '7373eb60-6647-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', comment_id: 'comment-alert-1', + owner: 'securitySolution', }, { action_field: ['comment'], @@ -181,6 +184,7 @@ export const userActions: CaseUserActionsResponse = [ action_id: '7abc6410-6647-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', comment_id: 'comment-alert-2', + owner: 'securitySolution', }, { action_field: ['pushed'], @@ -197,6 +201,7 @@ export const userActions: CaseUserActionsResponse = [ action_id: '9b91d8f0-6647-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', comment_id: null, + owner: 'securitySolution', }, { action_field: ['comment'], @@ -212,5 +217,6 @@ export const userActions: CaseUserActionsResponse = [ action_id: '0818e5e0-6648-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', comment_id: 'comment-user-1', + owner: 'securitySolution', }, ]; diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index b7f416203e078f..3991a9730c440e 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -6,13 +6,7 @@ */ import Boom from '@hapi/boom'; -import { - SavedObjectsBulkUpdateResponse, - SavedObjectsUpdateResponse, - SavedObjectsFindResponse, - SavedObject, -} from 'kibana/server'; -import { ActionResult } from '../../../../actions/server'; +import { SavedObjectsFindResponse, SavedObject } from 'kibana/server'; import { ActionConnector, @@ -21,8 +15,6 @@ import { CaseStatuses, ExternalServiceResponse, ESCaseAttributes, - CommentAttributes, - CaseUserActionsResponse, ESCasesConfigureAttributes, CaseType, } from '../../../common/api'; @@ -32,6 +24,8 @@ import { createIncident, getCommentContextFromAttributes } from './utils'; import { createCaseError, flattenCaseSavedObject, getAlertInfoFromComments } from '../../common'; import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; import { CasesClient, CasesClientArgs, CasesClientInternal } from '..'; +import { ensureAuthorized } from '../utils'; +import { Operations } from '../../authorization'; /** * Returns true if the case should be closed based on the configuration settings and whether the case @@ -69,18 +63,13 @@ export const push = async ( actionsClient, user, logger, + auditLogger, + authorization, } = clientArgs; - /* Start of push to external service */ - let theCase: CaseResponse; - let connector: ActionResult; - let userActions: CaseUserActionsResponse; - let alerts; - let connectorMappings; - let externalServiceIncident; - try { - [theCase, connector, userActions] = await Promise.all([ + /* Start of push to external service */ + const [theCase, connector, userActions] = await Promise.all([ casesClient.cases.get({ id: caseId, includeComments: true, @@ -89,34 +78,29 @@ export const push = async ( actionsClient.get({ id: connectorId }), casesClient.userActions.getAll({ caseId }), ]); - } catch (e) { - const message = `Error getting case and/or connector and/or user actions: ${e.message}`; - throw createCaseError({ message, error: e, logger }); - } - // We need to change the logic when we support subcases - if (theCase?.status === CaseStatuses.closed) { - throw Boom.conflict( - `This case ${theCase.title} is closed. You can not pushed if the case is closed.` - ); - } + await ensureAuthorized({ + authorization, + auditLogger, + operation: Operations.pushCase, + savedObjectIDs: [caseId], + owners: [theCase.owner], + }); + + // We need to change the logic when we support subcases + if (theCase?.status === CaseStatuses.closed) { + throw Boom.conflict( + `The ${theCase.title} case is closed. Pushing a closed case is not allowed.` + ); + } - const alertsInfo = getAlertInfoFromComments(theCase?.comments); + const alertsInfo = getAlertInfoFromComments(theCase?.comments); - try { - alerts = await casesClientInternal.alerts.get({ + const alerts = await casesClientInternal.alerts.get({ alertsInfo, }); - } catch (e) { - throw createCaseError({ - message: `Error getting alerts for case with id ${theCase.id}: ${e.message}`, - logger, - error: e, - }); - } - try { - connectorMappings = await casesClientInternal.configuration.getMappings({ + const connectorMappings = await casesClientInternal.configuration.getMappings({ connectorId: connector.id, connectorType: connector.actionTypeId, }); @@ -124,13 +108,8 @@ export const push = async ( if (connectorMappings.length === 0) { throw new Error('Connector mapping has not been created'); } - } catch (e) { - const message = `Error getting mapping for connector with id ${connector.id}: ${e.message}`; - throw createCaseError({ message, error: e, logger }); - } - try { - externalServiceIncident = await createIncident({ + const externalServiceIncident = await createIncident({ actionsClient, theCase, userActions, @@ -138,34 +117,25 @@ export const push = async ( mappings: connectorMappings[0].attributes.mappings, alerts, }); - } catch (e) { - const message = `Error creating incident for case with id ${theCase.id}: ${e.message}`; - throw createCaseError({ error: e, message, logger }); - } - const pushRes = await actionsClient.execute({ - actionId: connector?.id ?? '', - params: { - subAction: 'pushToService', - subActionParams: externalServiceIncident, - }, - }); - - if (pushRes.status === 'error') { - throw Boom.failedDependency( - pushRes.serviceMessage ?? pushRes.message ?? 'Error pushing to service' - ); - } + const pushRes = await actionsClient.execute({ + actionId: connector?.id ?? '', + params: { + subAction: 'pushToService', + subActionParams: externalServiceIncident, + }, + }); - /* End of push to external service */ + if (pushRes.status === 'error') { + throw Boom.failedDependency( + pushRes.serviceMessage ?? pushRes.message ?? 'Error pushing to service' + ); + } - /* Start of update case with push information */ - let myCase; - let myCaseConfigure; - let comments; + /* End of push to external service */ - try { - [myCase, myCaseConfigure, comments] = await Promise.all([ + /* Start of update case with push information */ + const [myCase, myCaseConfigure, comments] = await Promise.all([ caseService.getCase({ soClient: savedObjectsClient, id: caseId, @@ -182,33 +152,25 @@ export const push = async ( includeSubCaseComments: ENABLE_CASE_CONNECTOR, }), ]); - } catch (e) { - const message = `Error getting user and/or case and/or case configuration and/or case comments: ${e.message}`; - throw createCaseError({ error: e, message, logger }); - } - - // eslint-disable-next-line @typescript-eslint/naming-convention - const { username, full_name, email } = user; - const pushedDate = new Date().toISOString(); - const externalServiceResponse = pushRes.data as ExternalServiceResponse; - const externalService = { - pushed_at: pushedDate, - pushed_by: { username, full_name, email }, - connector_id: connector.id, - connector_name: connector.name, - external_id: externalServiceResponse.id, - external_title: externalServiceResponse.title, - external_url: externalServiceResponse.url, - }; + // eslint-disable-next-line @typescript-eslint/naming-convention + const { username, full_name, email } = user; + const pushedDate = new Date().toISOString(); + const externalServiceResponse = pushRes.data as ExternalServiceResponse; - let updatedCase: SavedObjectsUpdateResponse; - let updatedComments: SavedObjectsBulkUpdateResponse; + const externalService = { + pushed_at: pushedDate, + pushed_by: { username, full_name, email }, + connector_id: connector.id, + connector_name: connector.name, + external_id: externalServiceResponse.id, + external_title: externalServiceResponse.title, + external_url: externalServiceResponse.url, + }; - const shouldMarkAsClosed = shouldCloseByPush(myCaseConfigure, myCase); + const shouldMarkAsClosed = shouldCloseByPush(myCaseConfigure, myCase); - try { - [updatedCase, updatedComments] = await Promise.all([ + const [updatedCase, updatedComments] = await Promise.all([ caseService.patchCase({ soClient: savedObjectsClient, caseId, @@ -254,6 +216,7 @@ export const push = async ( fields: ['status'], newValue: CaseStatuses.closed, oldValue: myCase.attributes.status, + owner: myCase.attributes.owner, }), ] : []), @@ -264,38 +227,39 @@ export const push = async ( caseId, fields: ['pushed'], newValue: JSON.stringify(externalService), + owner: myCase.attributes.owner, }), ], }), ]); - } catch (e) { - const message = `Error updating case and/or comments and/or creating user action: ${e.message}`; - throw createCaseError({ error: e, message, logger }); - } - /* End of update case with push information */ - return CaseResponseRt.encode( - flattenCaseSavedObject({ - savedObject: { - ...myCase, - ...updatedCase, - attributes: { ...myCase.attributes, ...updatedCase?.attributes }, - references: myCase.references, - }, - comments: comments.saved_objects.map((origComment) => { - const updatedComment = updatedComments.saved_objects.find((c) => c.id === origComment.id); - return { - ...origComment, - ...updatedComment, - attributes: { - ...origComment.attributes, - ...updatedComment?.attributes, - ...getCommentContextFromAttributes(origComment.attributes), - }, - version: updatedComment?.version ?? origComment.version, - references: origComment?.references ?? [], - }; - }), - }) - ); + /* End of update case with push information */ + + return CaseResponseRt.encode( + flattenCaseSavedObject({ + savedObject: { + ...myCase, + ...updatedCase, + attributes: { ...myCase.attributes, ...updatedCase?.attributes }, + references: myCase.references, + }, + comments: comments.saved_objects.map((origComment) => { + const updatedComment = updatedComments.saved_objects.find((c) => c.id === origComment.id); + return { + ...origComment, + ...updatedComment, + attributes: { + ...origComment.attributes, + ...updatedComment?.attributes, + ...getCommentContextFromAttributes(origComment.attributes), + }, + version: updatedComment?.version ?? origComment.version, + references: origComment?.references ?? [], + }; + }), + }) + ); + } catch (error) { + throw createCaseError({ message: `Failed to push case: ${error}`, error, logger }); + } }; diff --git a/x-pack/plugins/cases/server/client/cases/update.ts b/x-pack/plugins/cases/server/client/cases/update.ts index 402e6726a71cd1..de3c499db50984 100644 --- a/x-pack/plugins/cases/server/client/cases/update.ts +++ b/x-pack/plugins/cases/server/client/cases/update.ts @@ -36,7 +36,7 @@ import { CommentAttributes, } from '../../../common/api'; import { buildCaseUserActions } from '../../services/user_actions/helpers'; -import { getCaseToUpdate } from '../utils'; +import { ensureAuthorized, getCaseToUpdate } from '../utils'; import { CaseService } from '../../services'; import { @@ -55,6 +55,7 @@ import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; import { UpdateAlertRequest } from '../alerts/client'; import { CasesClientInternal } from '../client_internal'; import { CasesClientArgs } from '..'; +import { Operations } from '../../authorization'; /** * Throws an error if any of the requests attempt to update a collection style cases' status field. @@ -113,6 +114,18 @@ function throwIfUpdateType(requests: ESCasePatchRequest[]) { } } +/** + * Throws an error if any of the requests attempt to update the owner of a case. + */ +function throwIfUpdateOwner(requests: ESCasePatchRequest[]) { + const requestsUpdatingOwner = requests.filter((req) => req.owner !== undefined); + + if (requestsUpdatingOwner.length > 0) { + const ids = requestsUpdatingOwner.map((req) => req.id); + throw Boom.badRequest(`Updating the owner of a case is not allowed ids: [${ids.join(', ')}]`); + } +} + /** * Throws an error if any of the requests attempt to update an individual style cases' type field to a collection * when alerts are attached to the case. @@ -337,12 +350,53 @@ async function updateAlerts({ await casesClientInternal.alerts.updateStatus({ alerts: alertsToUpdate }); } +function partitionPatchRequest( + casesMap: Map>, + patchReqCases: CasePatchRequest[] +): { + nonExistingCases: CasePatchRequest[]; + conflictedCases: CasePatchRequest[]; + casesToAuthorize: Array>; +} { + const nonExistingCases: CasePatchRequest[] = []; + const conflictedCases: CasePatchRequest[] = []; + const casesToAuthorize: Array> = []; + + for (const reqCase of patchReqCases) { + const foundCase = casesMap.get(reqCase.id); + + if (!foundCase || foundCase.error) { + nonExistingCases.push(reqCase); + } else if (foundCase.version !== reqCase.version) { + conflictedCases.push(reqCase); + // let's try to authorize the conflicted case even though we'll fail after afterwards just in case + casesToAuthorize.push(foundCase); + } else { + casesToAuthorize.push(foundCase); + } + } + + return { + nonExistingCases, + conflictedCases, + casesToAuthorize, + }; +} + export const update = async ( cases: CasesPatchRequest, clientArgs: CasesClientArgs, casesClientInternal: CasesClientInternal ): Promise => { - const { savedObjectsClient, caseService, userActionService, user, logger } = clientArgs; + const { + savedObjectsClient, + caseService, + userActionService, + user, + logger, + authorization, + auditLogger, + } = clientArgs; const query = pipe( excess(CasesPatchRequestRt).decode(cases), fold(throwErrors(Boom.badRequest), identity) @@ -354,15 +408,22 @@ export const update = async ( caseIds: query.cases.map((q) => q.id), }); - let nonExistingCases: CasePatchRequest[] = []; - const conflictedCases = query.cases.filter((q) => { - const myCase = myCases.saved_objects.find((c) => c.id === q.id); + const casesMap = myCases.saved_objects.reduce((acc, so) => { + acc.set(so.id, so); + return acc; + }, new Map>()); - if (myCase && myCase.error) { - nonExistingCases = [...nonExistingCases, q]; - return false; - } - return myCase == null || myCase?.version !== q.version; + const { nonExistingCases, conflictedCases, casesToAuthorize } = partitionPatchRequest( + casesMap, + query.cases + ); + + await ensureAuthorized({ + authorization, + auditLogger, + owners: casesToAuthorize.map((caseInfo) => caseInfo.attributes.owner), + operation: Operations.updateCase, + savedObjectIDs: casesToAuthorize.map((caseInfo) => caseInfo.id), }); if (nonExistingCases.length > 0) { @@ -403,15 +464,11 @@ export const update = async ( throw Boom.notAcceptable('All update fields are identical to current version.'); } - const casesMap = myCases.saved_objects.reduce((acc, so) => { - acc.set(so.id, so); - return acc; - }, new Map>()); - if (!ENABLE_CASE_CONNECTOR) { throwIfUpdateType(updateFilterCases); } + throwIfUpdateOwner(updateFilterCases); throwIfUpdateStatusOfCollection(updateFilterCases, casesMap); throwIfUpdateTypeCollectionToIndividual(updateFilterCases, casesMap); await throwIfInvalidUpdateOfTypeWithAlerts({ diff --git a/x-pack/plugins/cases/server/client/cases/utils.test.ts b/x-pack/plugins/cases/server/client/cases/utils.test.ts index 5f41a95d3c5017..391fe5803f81f2 100644 --- a/x-pack/plugins/cases/server/client/cases/utils.test.ts +++ b/x-pack/plugins/cases/server/client/cases/utils.test.ts @@ -701,6 +701,7 @@ describe('utils', () => { action_id: '9b91d8f0-6647-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', comment_id: null, + owner: 'securitySolution', }, ]); diff --git a/x-pack/plugins/cases/server/client/stats/client.ts b/x-pack/plugins/cases/server/client/stats/client.ts index 40ced0bfbf4bb0..8c18c35e8f4fd6 100644 --- a/x-pack/plugins/cases/server/client/stats/client.ts +++ b/x-pack/plugins/cases/server/client/stats/client.ts @@ -7,8 +7,9 @@ import { CasesClientArgs } from '..'; import { CasesStatusResponse, CasesStatusResponseRt, caseStatuses } from '../../../common/api'; +import { Operations } from '../../authorization'; import { createCaseError } from '../../common/error'; -import { constructQueryOptions } from '../utils'; +import { constructQueryOptions, getAuthorizationFilter } from '../utils'; /** * Statistics API contract. @@ -30,19 +31,34 @@ async function getStatusTotalsByType({ savedObjectsClient: soClient, caseService, logger, + authorization, + auditLogger, }: CasesClientArgs): Promise { try { + const { + filter: authorizationFilter, + ensureSavedObjectsAreAuthorized, + logSuccessfulAuthorization, + } = await getAuthorizationFilter({ + authorization, + operation: Operations.getCaseStatuses, + auditLogger, + }); + const [openCases, inProgressCases, closedCases] = await Promise.all([ ...caseStatuses.map((status) => { - const statusQuery = constructQueryOptions({ status }); + const statusQuery = constructQueryOptions({ status, authorizationFilter }); return caseService.findCaseStatusStats({ soClient, caseOptions: statusQuery.case, subCaseOptions: statusQuery.subCase, + ensureSavedObjectsAreAuthorized, }); }), ]); + logSuccessfulAuthorization(); + return CasesStatusResponseRt.encode({ count_open_cases: openCases, count_in_progress_cases: inProgressCases, diff --git a/x-pack/plugins/cases/server/client/sub_cases/client.ts b/x-pack/plugins/cases/server/client/sub_cases/client.ts index ac390710def876..102cbee14a2060 100644 --- a/x-pack/plugins/cases/server/client/sub_cases/client.ts +++ b/x-pack/plugins/cases/server/client/sub_cases/client.ts @@ -105,16 +105,17 @@ async function deleteSubCase(ids: string[], clientArgs: CasesClientArgs): Promis await userActionService.bulkCreate({ soClient, - actions: ids.map((id) => + actions: subCases.saved_objects.map((subCase) => buildCaseUserActionItem({ action: 'delete', actionAt: deleteDate, actionBy: user, // if for some reason the sub case didn't have a reference to its parent, we'll still log a user action // but we won't have the case ID - caseId: subCaseIDToParentID.get(id) ?? '', - subCaseId: id, + caseId: subCaseIDToParentID.get(subCase.id) ?? '', + subCaseId: subCase.id, fields: ['sub_case', 'comment', 'status'], + owner: subCase.attributes.owner, }) ), }); diff --git a/x-pack/plugins/cases/server/client/user_actions/get.ts b/x-pack/plugins/cases/server/client/user_actions/get.ts index dac997c3fa90a2..0b03fb75614a80 100644 --- a/x-pack/plugins/cases/server/client/user_actions/get.ts +++ b/x-pack/plugins/cases/server/client/user_actions/get.ts @@ -14,6 +14,8 @@ import { CaseUserActionsResponseRt, CaseUserActionsResponse } from '../../../com import { createCaseError } from '../../common/error'; import { checkEnabledCaseConnectorOrThrow } from '../../common'; import { CasesClientArgs } from '..'; +import { ensureAuthorized } from '../utils'; +import { Operations } from '../../authorization'; interface GetParams { caseId: string; @@ -24,7 +26,7 @@ export const get = async ( { caseId, subCaseId }: GetParams, clientArgs: CasesClientArgs ): Promise => { - const { savedObjectsClient, userActionService, logger } = clientArgs; + const { savedObjectsClient, userActionService, logger, authorization, auditLogger } = clientArgs; try { checkEnabledCaseConnectorOrThrow(subCaseId); @@ -35,6 +37,14 @@ export const get = async ( subCaseId, }); + await ensureAuthorized({ + authorization, + auditLogger, + owners: userActions.saved_objects.map((userAction) => userAction.attributes.owner), + savedObjectIDs: userActions.saved_objects.map((userAction) => userAction.id), + operation: Operations.getUserActions, + }); + return CaseUserActionsResponseRt.encode( userActions.saved_objects.reduce((acc, ua) => { if (subCaseId == null && ua.references.some((uar) => uar.type === SUB_CASE_SAVED_OBJECT)) { diff --git a/x-pack/plugins/cases/server/client/utils.ts b/x-pack/plugins/cases/server/client/utils.ts index eb00cce8654ef4..931372cc1d6c95 100644 --- a/x-pack/plugins/cases/server/client/utils.ts +++ b/x-pack/plugins/cases/server/client/utils.ts @@ -27,6 +27,7 @@ import { excess, ContextTypeUserRt, AlertCommentRequestRt, + OWNER_FIELD, } from '../../common/api'; import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../common/constants'; import { AuditEvent } from '../../../security/server'; @@ -157,7 +158,7 @@ export const combineAuthorizedAndOwnerFilter = ( ): KueryNode | undefined => { const ownerFilter = buildFilter({ filters: owner, - field: 'owner', + field: OWNER_FIELD, operator: 'or', type: savedObjectType, }); @@ -241,7 +242,7 @@ export const constructQueryOptions = ({ operator: 'or', }); const sortField = sortToSnake(sortByField); - const ownerFilter = buildFilter({ filters: owner ?? [], field: 'owner', operator: 'or' }); + const ownerFilter = buildFilter({ filters: owner ?? [], field: OWNER_FIELD, operator: 'or' }); switch (caseType) { case CaseType.individual: { @@ -570,9 +571,14 @@ interface OwnerEntity { id: string; } +/** + * Function callback for making sure the found saved objects are of the authorized owner + */ +export type EnsureSOAuthCallback = (entities: OwnerEntity[]) => void; + interface AuthFilterHelpers { filter?: KueryNode; - ensureSavedObjectsAreAuthorized: (entities: OwnerEntity[]) => void; + ensureSavedObjectsAreAuthorized: EnsureSOAuthCallback; logSuccessfulAuthorization: () => void; } diff --git a/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts index e5b826cf0ddefd..f221942716d082 100644 --- a/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts +++ b/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts @@ -513,6 +513,7 @@ export const mockUserActions: Array> = [ new_value: '{"title":"A case","tags":["case"],"description":"Yeah!","connector":{"id":"connector-od","name":"My Connector","type":".servicenow-sir","fields":{"category":"Denial of Service","destIp":true,"malwareHash":true,"malwareUrl":true,"priority":"2","sourceIp":true,"subcategory":"45"}},"settings":{"syncAlerts":true}}', old_value: null, + owner: 'securitySolution', }, version: 'WzYsMV0=', references: [], @@ -532,6 +533,7 @@ export const mockUserActions: Array> = [ new_value: '{"type":"alert","alertId":"cec3da90fb37a44407145adf1593f3b0d5ad94c4654201f773d63b5d4706128e","index":".siem-signals-default-000008"}', old_value: null, + owner: 'securitySolution', }, version: 'WzYsMV0=', references: [], diff --git a/x-pack/plugins/cases/server/services/cases/index.ts b/x-pack/plugins/cases/server/services/cases/index.ts index 870ba94b1ba131..246872b0af9d41 100644 --- a/x-pack/plugins/cases/server/services/cases/index.ts +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -32,6 +32,7 @@ import { caseTypeField, CasesFindRequest, CaseStatuses, + OWNER_FIELD, } from '../../../common/api'; import { defaultSortField, @@ -48,6 +49,8 @@ import { SUB_CASE_SAVED_OBJECT, } from '../../../common/constants'; import { ClientArgs } from '..'; +import { EnsureSOAuthCallback } from '../../client/utils'; +import { includeFieldsRequiredForAuthentication } from '../../authorization/utils'; interface PushedArgs { pushed_at: string; @@ -183,9 +186,11 @@ interface GetReportersArgs { const transformNewSubCase = ({ createdAt, createdBy, + owner, }: { createdAt: string; createdBy: User; + owner: string; }): SubCaseAttributes => { return { closed_at: null, @@ -195,6 +200,7 @@ const transformNewSubCase = ({ status: CaseStatuses.open, updated_at: null, updated_by: null, + owner, }; }; @@ -298,9 +304,11 @@ export class CaseService { soClient, caseOptions, subCaseOptions, + ensureSavedObjectsAreAuthorized, }: { soClient: SavedObjectsClientContract; caseOptions: SavedObjectFindOptionsKueryNode; + ensureSavedObjectsAreAuthorized: EnsureSOAuthCallback; subCaseOptions?: SavedObjectFindOptionsKueryNode; }): Promise { const casesStats = await this.findCases({ @@ -337,12 +345,17 @@ export class CaseService { soClient, options: { ...caseOptions, - fields: [caseTypeField], + fields: includeFieldsRequiredForAuthentication([caseTypeField]), page: 1, perPage: casesStats.total, }, }); + // make sure that the retrieved cases were correctly filtered by owner + ensureSavedObjectsAreAuthorized( + cases.saved_objects.map((caseInfo) => ({ id: caseInfo.id, owner: caseInfo.attributes.owner })) + ); + const caseIds = cases.saved_objects .filter((caseInfo) => caseInfo.attributes.type === CaseType.collection) .map((caseInfo) => caseInfo.id); @@ -575,7 +588,8 @@ export class CaseService { this.log.debug(`Attempting to POST a new sub case`); return soClient.create( SUB_CASE_SAVED_OBJECT, - transformNewSubCase({ createdAt, createdBy }), + // ENABLE_CASE_CONNECTOR: populate the owner field correctly + transformNewSubCase({ createdAt, createdBy, owner: '' }), { references: [ { @@ -922,7 +936,7 @@ export class CaseService { this.log.debug(`Attempting to GET all reporters`); const firstReporters = await soClient.find({ type: CASE_SAVED_OBJECT, - fields: ['created_by', 'owner'], + fields: ['created_by', OWNER_FIELD], page: 1, perPage: 1, filter: cloneDeep(filter), @@ -930,7 +944,7 @@ export class CaseService { return await soClient.find({ type: CASE_SAVED_OBJECT, - fields: ['created_by', 'owner'], + fields: ['created_by', OWNER_FIELD], page: 1, perPage: firstReporters.total, filter: cloneDeep(filter), @@ -949,7 +963,7 @@ export class CaseService { this.log.debug(`Attempting to GET all cases`); const firstTags = await soClient.find({ type: CASE_SAVED_OBJECT, - fields: ['tags', 'owner'], + fields: ['tags', OWNER_FIELD], page: 1, perPage: 1, filter: cloneDeep(filter), @@ -957,7 +971,7 @@ export class CaseService { return await soClient.find({ type: CASE_SAVED_OBJECT, - fields: ['tags', 'owner'], + fields: ['tags', OWNER_FIELD], page: 1, perPage: firstTags.total, filter: cloneDeep(filter), diff --git a/x-pack/plugins/cases/server/services/user_actions/helpers.ts b/x-pack/plugins/cases/server/services/user_actions/helpers.ts index 2ab3bdb5e1cee5..664a9041491a19 100644 --- a/x-pack/plugins/cases/server/services/user_actions/helpers.ts +++ b/x-pack/plugins/cases/server/services/user_actions/helpers.ts @@ -17,6 +17,7 @@ import { User, UserActionFieldType, SubCaseAttributes, + OWNER_FIELD, } from '../../../common/api'; import { isTwoArraysDifference } from '../../client/utils'; import { UserActionItem } from '.'; @@ -34,6 +35,7 @@ export const transformNewUserAction = ({ email, // eslint-disable-next-line @typescript-eslint/naming-convention full_name, + owner, newValue = null, oldValue = null, username, @@ -41,6 +43,7 @@ export const transformNewUserAction = ({ actionField: UserActionField; action: UserAction; actionAt: string; + owner: string; email?: string | null; full_name?: string | null; newValue?: string | null; @@ -53,6 +56,7 @@ export const transformNewUserAction = ({ action_by: { email, full_name, username }, new_value: newValue, old_value: oldValue, + owner, }); interface BuildCaseUserAction { @@ -60,6 +64,7 @@ interface BuildCaseUserAction { actionAt: string; actionBy: User; caseId: string; + owner: string; fields: UserActionField | unknown[]; newValue?: string | unknown; oldValue?: string | unknown; @@ -80,11 +85,13 @@ export const buildCommentUserActionItem = ({ newValue, oldValue, subCaseId, + owner, }: BuildCommentUserActionItem): UserActionItem => ({ attributes: transformNewUserAction({ actionField: fields as UserActionField, action, actionAt, + owner, ...actionBy, newValue: newValue as string, oldValue: oldValue as string, @@ -121,11 +128,13 @@ export const buildCaseUserActionItem = ({ newValue, oldValue, subCaseId, + owner, }: BuildCaseUserAction): UserActionItem => ({ attributes: transformNewUserAction({ actionField: fields as UserActionField, action, actionAt, + owner, ...actionBy, newValue: newValue as string, oldValue: oldValue as string, @@ -157,7 +166,7 @@ const userActionFieldsAllowed: UserActionField = [ 'status', 'settings', 'sub_case', - 'owner', + OWNER_FIELD, ]; interface CaseSubIDs { @@ -180,7 +189,14 @@ interface Getters { getCaseAndSubID: GetCaseAndSubID; } -const buildGenericCaseUserActions = ({ +interface OwnerEntity { + owner: string; +} + +/** + * The entity associated with the user action must contain an owner field + */ +const buildGenericCaseUserActions = ({ actionDate, actionBy, originalCases, @@ -221,6 +237,7 @@ const buildGenericCaseUserActions = ({ fields: [field], newValue: updatedValue, oldValue: origValue, + owner: originalItem.attributes.owner, }), ]; } else if (Array.isArray(origValue) && Array.isArray(updatedValue)) { @@ -236,6 +253,7 @@ const buildGenericCaseUserActions = ({ subCaseId, fields: [field], newValue: compareValues.addedItems.join(', '), + owner: originalItem.attributes.owner, }), ]; } @@ -251,6 +269,7 @@ const buildGenericCaseUserActions = ({ subCaseId, fields: [field], newValue: compareValues.deletedItems.join(', '), + owner: originalItem.attributes.owner, }), ]; } @@ -270,6 +289,7 @@ const buildGenericCaseUserActions = ({ fields: [field], newValue: JSON.stringify(updatedValue), oldValue: JSON.stringify(origValue), + owner: originalItem.attributes.owner, }), ]; } diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts index ef396f75b85755..b7550a0717a281 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts @@ -74,6 +74,7 @@ describe(`cases`, () => { "cases:1.0.0-zeta1:observability/getComment", "cases:1.0.0-zeta1:observability/getTags", "cases:1.0.0-zeta1:observability/getReporters", + "cases:1.0.0-zeta1:observability/getUserActions", "cases:1.0.0-zeta1:observability/findConfigurations", ] `); @@ -112,10 +113,12 @@ describe(`cases`, () => { "cases:1.0.0-zeta1:security/getComment", "cases:1.0.0-zeta1:security/getTags", "cases:1.0.0-zeta1:security/getReporters", + "cases:1.0.0-zeta1:security/getUserActions", "cases:1.0.0-zeta1:security/findConfigurations", "cases:1.0.0-zeta1:security/createCase", "cases:1.0.0-zeta1:security/deleteCase", "cases:1.0.0-zeta1:security/updateCase", + "cases:1.0.0-zeta1:security/pushCase", "cases:1.0.0-zeta1:security/createComment", "cases:1.0.0-zeta1:security/deleteComment", "cases:1.0.0-zeta1:security/updateComment", @@ -159,10 +162,12 @@ describe(`cases`, () => { "cases:1.0.0-zeta1:security/getComment", "cases:1.0.0-zeta1:security/getTags", "cases:1.0.0-zeta1:security/getReporters", + "cases:1.0.0-zeta1:security/getUserActions", "cases:1.0.0-zeta1:security/findConfigurations", "cases:1.0.0-zeta1:security/createCase", "cases:1.0.0-zeta1:security/deleteCase", "cases:1.0.0-zeta1:security/updateCase", + "cases:1.0.0-zeta1:security/pushCase", "cases:1.0.0-zeta1:security/createComment", "cases:1.0.0-zeta1:security/deleteComment", "cases:1.0.0-zeta1:security/updateComment", @@ -172,6 +177,7 @@ describe(`cases`, () => { "cases:1.0.0-zeta1:obs/getComment", "cases:1.0.0-zeta1:obs/getTags", "cases:1.0.0-zeta1:obs/getReporters", + "cases:1.0.0-zeta1:obs/getUserActions", "cases:1.0.0-zeta1:obs/findConfigurations", ] `); @@ -211,10 +217,12 @@ describe(`cases`, () => { "cases:1.0.0-zeta1:security/getComment", "cases:1.0.0-zeta1:security/getTags", "cases:1.0.0-zeta1:security/getReporters", + "cases:1.0.0-zeta1:security/getUserActions", "cases:1.0.0-zeta1:security/findConfigurations", "cases:1.0.0-zeta1:security/createCase", "cases:1.0.0-zeta1:security/deleteCase", "cases:1.0.0-zeta1:security/updateCase", + "cases:1.0.0-zeta1:security/pushCase", "cases:1.0.0-zeta1:security/createComment", "cases:1.0.0-zeta1:security/deleteComment", "cases:1.0.0-zeta1:security/updateComment", @@ -224,10 +232,12 @@ describe(`cases`, () => { "cases:1.0.0-zeta1:other-security/getComment", "cases:1.0.0-zeta1:other-security/getTags", "cases:1.0.0-zeta1:other-security/getReporters", + "cases:1.0.0-zeta1:other-security/getUserActions", "cases:1.0.0-zeta1:other-security/findConfigurations", "cases:1.0.0-zeta1:other-security/createCase", "cases:1.0.0-zeta1:other-security/deleteCase", "cases:1.0.0-zeta1:other-security/updateCase", + "cases:1.0.0-zeta1:other-security/pushCase", "cases:1.0.0-zeta1:other-security/createComment", "cases:1.0.0-zeta1:other-security/deleteComment", "cases:1.0.0-zeta1:other-security/updateComment", @@ -237,11 +247,13 @@ describe(`cases`, () => { "cases:1.0.0-zeta1:obs/getComment", "cases:1.0.0-zeta1:obs/getTags", "cases:1.0.0-zeta1:obs/getReporters", + "cases:1.0.0-zeta1:obs/getUserActions", "cases:1.0.0-zeta1:obs/findConfigurations", "cases:1.0.0-zeta1:other-obs/getCase", "cases:1.0.0-zeta1:other-obs/getComment", "cases:1.0.0-zeta1:other-obs/getTags", "cases:1.0.0-zeta1:other-obs/getReporters", + "cases:1.0.0-zeta1:other-obs/getUserActions", "cases:1.0.0-zeta1:other-obs/findConfigurations", ] `); diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts index 2643d7c6d6aafa..4b04f98704c8f1 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts @@ -17,12 +17,14 @@ const readOperations: string[] = [ 'getComment', 'getTags', 'getReporters', + 'getUserActions', 'findConfigurations', ]; const writeOperations: string[] = [ 'createCase', 'deleteCase', 'updateCase', + 'pushCase', 'createComment', 'deleteComment', 'updateComment', diff --git a/x-pack/plugins/security_solution/public/cases/containers/mock.ts b/x-pack/plugins/security_solution/public/cases/containers/mock.ts index 6880a105b1ce61..8e29e3760c8d8a 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/mock.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/mock.ts @@ -185,6 +185,7 @@ const basicAction = { newValue: 'what a cool value', caseId: basicCaseId, commentId: null, + owner: 'securitySolution', }; export const cases: Case[] = [ @@ -317,6 +318,7 @@ const basicActionSnake = { new_value: 'what a cool value', case_id: basicCaseId, comment_id: null, + owner: 'securitySolution', }; export const getUserActionSnake = (af: UserActionField, a: UserAction) => ({ ...basicActionSnake, diff --git a/x-pack/test/case_api_integration/common/lib/authentication/index.ts b/x-pack/test/case_api_integration/common/lib/authentication/index.ts index a72141745e5777..dfd151344b40c1 100644 --- a/x-pack/test/case_api_integration/common/lib/authentication/index.ts +++ b/x-pack/test/case_api_integration/common/lib/authentication/index.ts @@ -7,7 +7,7 @@ import { FtrProviderContext as CommonFtrProviderContext } from '../../../common/ftr_provider_context'; import { Role, User, UserInfo } from './types'; -import { users } from './users'; +import { superUser, users } from './users'; import { roles } from './roles'; import { spaces } from './spaces'; @@ -90,3 +90,5 @@ export const deleteSpacesAndUsers = async (getService: CommonFtrProviderContext[ await deleteSpaces(getService); await deleteUsersAndRoles(getService); }; + +export const superUserSpace1Auth = { user: superUser, space: 'space1' }; diff --git a/x-pack/test/case_api_integration/common/lib/authentication/roles.ts b/x-pack/test/case_api_integration/common/lib/authentication/roles.ts index c08b68bb2721f1..5ddecd92061065 100644 --- a/x-pack/test/case_api_integration/common/lib/authentication/roles.ts +++ b/x-pack/test/case_api_integration/common/lib/authentication/roles.ts @@ -37,6 +37,8 @@ export const globalRead: Role = { feature: { securitySolutionFixture: ['read'], observabilityFixture: ['read'], + actions: ['read'], + actionsSimulators: ['read'], }, spaces: ['*'], }, @@ -59,6 +61,8 @@ export const securitySolutionOnlyAll: Role = { { feature: { securitySolutionFixture: ['all'], + actions: ['all'], + actionsSimulators: ['all'], }, spaces: ['space1'], }, @@ -81,6 +85,8 @@ export const securitySolutionOnlyRead: Role = { { feature: { securitySolutionFixture: ['read'], + actions: ['read'], + actionsSimulators: ['read'], }, spaces: ['space1'], }, @@ -103,6 +109,8 @@ export const observabilityOnlyAll: Role = { { feature: { observabilityFixture: ['all'], + actions: ['all'], + actionsSimulators: ['all'], }, spaces: ['space1'], }, @@ -125,6 +133,8 @@ export const observabilityOnlyRead: Role = { { feature: { observabilityFixture: ['read'], + actions: ['read'], + actionsSimulators: ['read'], }, spaces: ['space1'], }, diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts index 43090df495ce93..731ddca08a34e3 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -43,9 +43,10 @@ import { CasesConfigurePatch, CasesStatusResponse, CasesConfigurationsResponse, + CaseUserActionsResponse, } from '../../../../plugins/cases/common/api'; import { postCollectionReq, postCommentGenAlertReq } from './mock'; -import { getSubCasesUrl } from '../../../../plugins/cases/common/api/helpers'; +import { getCaseUserActionUrl, getSubCasesUrl } from '../../../../plugins/cases/common/api/helpers'; import { ContextTypeGeneratedAlertType } from '../../../../plugins/cases/server/connectors'; import { SignalHit } from '../../../../plugins/security_solution/server/lib/detection_engine/signals/types'; import { ActionResult, FindActionResult } from '../../../../plugins/actions/server/types'; @@ -634,13 +635,20 @@ export const getAllUserAction = async ( return userActions; }; -export const updateCase = async ( - supertest: st.SuperTest, - params: CasesPatchRequest, - expectedHttpCode: number = 200 -): Promise => { +export const updateCase = async ({ + supertest, + params, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: st.SuperTest; + params: CasesPatchRequest; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): Promise => { const { body: cases } = await supertest - .patch(CASES_URL) + .patch(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}`) + .auth(auth.user.username, auth.user.password) .set('kbn-xsrf', 'true') .send(params) .expect(expectedHttpCode); @@ -648,6 +656,24 @@ export const updateCase = async ( return cases; }; +export const getCaseUserActions = async ({ + supertest, + caseID, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: st.SuperTest; + caseID: string; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): Promise => { + const { body: userActions } = await supertest + .get(`${getSpaceUrlPrefix(auth.space)}${getCaseUserActionUrl(caseID)}`) + .auth(auth.user.username, auth.user.password) + .expect(expectedHttpCode); + return userActions; +}; + export const deleteComment = async ({ supertest, caseId, @@ -796,13 +822,20 @@ export type CreateConnectorResponse = Omit & { connector_type_id: string; }; -export const createConnector = async ( - supertest: st.SuperTest, - req: Record, - expectedHttpCode: number = 200 -): Promise => { +export const createConnector = async ({ + supertest, + req, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: st.SuperTest; + req: Record; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): Promise => { const { body: connector } = await supertest - .post('/api/actions/connector') + .post(`${getSpaceUrlPrefix(auth.space)}/api/actions/connector`) + .auth(auth.user.username, auth.user.password) .set('kbn-xsrf', 'true') .send(req) .expect(expectedHttpCode); @@ -839,12 +872,18 @@ export const updateConfiguration = async ( return configuration; }; -export const getAllCasesStatuses = async ( - supertest: st.SuperTest, - expectedHttpCode: number = 200 -): Promise => { +export const getAllCasesStatuses = async ({ + supertest, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: st.SuperTest; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): Promise => { const { body: statuses } = await supertest - .get(CASE_STATUS_URL) + .get(`${getSpaceUrlPrefix(auth.space)}${CASE_STATUS_URL}`) + .auth(auth.user.username, auth.user.password) .set('kbn-xsrf', 'true') .expect(expectedHttpCode); @@ -939,14 +978,22 @@ export const getReporters = async ({ return res; }; -export const pushCase = async ( - supertest: st.SuperTest, - caseId: string, - connectorId: string, - expectedHttpCode: number = 200 -): Promise => { +export const pushCase = async ({ + supertest, + caseId, + connectorId, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: st.SuperTest; + caseId: string; + connectorId: string; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): Promise => { const { body: res } = await supertest - .post(`${CASES_URL}/${caseId}/connector/${connectorId}/_push`) + .post(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}/${caseId}/connector/${connectorId}/_push`) + .auth(auth.user.username, auth.user.password) .set('kbn-xsrf', 'true') .send({}) .expect(expectedHttpCode); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/basic/cases/push_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/cases/push_case.ts index f964ef3ee85929..5285b57f3be727 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/basic/cases/push_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/cases/push_case.ts @@ -36,7 +36,7 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should get 403 when trying to create a connector', async () => { - await createConnector(supertest, getServiceNowConnector(), 403); + await createConnector({ supertest, req: getServiceNowConnector(), expectedHttpCode: 403 }); }); it('should get 404 when trying to push to a case without a valid connector id', async () => { @@ -65,7 +65,12 @@ export default ({ getService }: FtrProviderContext): void => { }, }); - await pushCase(supertest, postedCase.id, 'not-exist', 404); + await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: 'not-exist', + expectedHttpCode: 404, + }); }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/basic/configure/create_connector.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/configure/create_connector.ts index a403e6d55be86d..fe8e311b5e4f64 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/basic/configure/create_connector.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/configure/create_connector.ts @@ -14,7 +14,7 @@ export default function serviceNow({ getService }: FtrProviderContext) { describe('create service now action', () => { it('should return 403 when creating a service now action', async () => { - await createConnector(supertest, getServiceNowConnector(), 403); + await createConnector({ supertest, req: getServiceNowConnector(), expectedHttpCode: 403 }); }); }); } diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/basic/index.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/index.ts index 502c64ccce04a4..90fbb106374349 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/basic/index.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/index.ts @@ -21,10 +21,14 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => { after(async () => { await deleteSpacesAndUsers(getService); }); - // Common - loadTestFile(require.resolve('../common')); // Basic loadTestFile(require.resolve('./cases/push_case')); + + // Common + loadTestFile(require.resolve('../common')); + + // NOTE: These need to be at the end because they could delete the .kibana index and inadvertently remove the users and spaces + loadTestFile(require.resolve('../common/migrations')); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/delete_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/delete_cases.ts index 484dca314c9cce..17aac2dd7e2859 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/delete_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/delete_cases.ts @@ -39,6 +39,8 @@ import { superUser, } from '../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; + // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertestWithoutAuth = getService('supertestWithoutAuth'); @@ -108,6 +110,7 @@ export default ({ getService }: FtrProviderContext): void => { case_id: `${postedCase.id}`, comment_id: null, sub_case_id: '', + owner: 'securitySolutionFixture', }); }); @@ -237,14 +240,14 @@ export default ({ getService }: FtrProviderContext): void => { supertest: supertestWithoutAuth, caseId: caseSec.id, expectedHttpCode: 200, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); await getCase({ supertest: supertestWithoutAuth, caseId: caseObs.id, expectedHttpCode: 200, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/find_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/find_cases.ts index 6bcd78f98e5eb5..b7838dd9299bcd 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/find_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/find_cases.ts @@ -99,14 +99,17 @@ export default ({ getService }: FtrProviderContext): void => { it('filters by status', async () => { await createCase(supertest, postCaseReq); const toCloseCase = await createCase(supertest, postCaseReq); - const patchedCase = await updateCase(supertest, { - cases: [ - { - id: toCloseCase.id, - version: toCloseCase.version, - status: CaseStatuses.closed, - }, - ], + const patchedCase = await updateCase({ + supertest, + params: { + cases: [ + { + id: toCloseCase.id, + version: toCloseCase.version, + status: CaseStatuses.closed, + }, + ], + }, }); const cases = await findCases({ supertest, query: { status: CaseStatuses.closed } }); @@ -164,24 +167,30 @@ export default ({ getService }: FtrProviderContext): void => { const inProgressCase = await createCase(supertest, postCaseReq); const postedCase = await createCase(supertest, postCaseReq); - await updateCase(supertest, { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: CaseStatuses.closed, - }, - ], + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses.closed, + }, + ], + }, }); - await updateCase(supertest, { - cases: [ - { - id: inProgressCase.id, - version: inProgressCase.version, - status: CaseStatuses['in-progress'], - }, - ], + await updateCase({ + supertest, + params: { + cases: [ + { + id: inProgressCase.id, + version: inProgressCase.version, + status: CaseStatuses['in-progress'], + }, + ], + }, }); const cases = await findCases({ supertest }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts index b50c18192a05b7..674c2c68381b8c 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../../plugins/security_solution/common/constants'; import { @@ -18,6 +18,7 @@ import { } from '../../../../../../plugins/cases/common/api'; import { defaultUser, + getPostCaseRequest, postCaseReq, postCaseResp, postCollectionReq, @@ -34,6 +35,7 @@ import { getAllUserAction, removeServerGeneratedPropertiesFromCase, removeServerGeneratedPropertiesFromUserAction, + findCases, } from '../../../../common/lib/utils'; import { createSignalsIndex, @@ -46,6 +48,17 @@ import { createRule, getQuerySignalIds, } from '../../../../../detection_engine_api_integration/utils'; +import { + globalRead, + noKibanaPrivileges, + obsOnly, + obsOnlyRead, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -61,14 +74,17 @@ export default ({ getService }: FtrProviderContext): void => { describe('happy path', () => { it('should patch a case', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCases = await updateCase(supertest, { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - title: 'new title', - }, - ], + const patchedCases = await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', + }, + ], + }, }); const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); @@ -81,14 +97,17 @@ export default ({ getService }: FtrProviderContext): void => { it('should closes the case correctly', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCases = await updateCase(supertest, { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: CaseStatuses.closed, - }, - ], + const patchedCases = await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses.closed, + }, + ], + }, }); const userActions = await getAllUserAction(supertest, postedCase.id); @@ -111,19 +130,23 @@ export default ({ getService }: FtrProviderContext): void => { case_id: `${postedCase.id}`, comment_id: null, sub_case_id: '', + owner: 'securitySolutionFixture', }); }); it('should change the status of case to in-progress correctly', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCases = await updateCase(supertest, { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: CaseStatuses['in-progress'], - }, - ], + const patchedCases = await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses['in-progress'], + }, + ], + }, }); const userActions = await getAllUserAction(supertest, postedCase.id); @@ -145,24 +168,28 @@ export default ({ getService }: FtrProviderContext): void => { case_id: `${postedCase.id}`, comment_id: null, sub_case_id: '', + owner: 'securitySolutionFixture', }); }); it('should patch a case with new connector', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCases = await updateCase(supertest, { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - connector: { - id: 'jira', - name: 'Jira', - type: ConnectorTypes.jira, - fields: { issueType: 'Task', priority: null, parent: null }, + const patchedCases = await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + connector: { + id: 'jira', + name: 'Jira', + type: ConnectorTypes.jira, + fields: { issueType: 'Task', priority: null, parent: null }, + }, }, - }, - ], + ], + }, }); const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); @@ -186,23 +213,43 @@ export default ({ getService }: FtrProviderContext): void => { caseId: postedCase.id, params: postCommentUserReq, }); - await updateCase(supertest, { - cases: [ - { - id: patchedCase.id, - version: patchedCase.version, - type: CaseType.collection, - }, - ], + await updateCase({ + supertest, + params: { + cases: [ + { + id: patchedCase.id, + version: patchedCase.version, + type: CaseType.collection, + }, + ], + }, }); }); }); describe('unhappy path', () => { + it('400s when attempting to change the owner of a case', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + owner: 'observabilityFixture', + }, + ], + }, + expectedHttpCode: 400, + }); + }); + it('404s when case is not there', async () => { - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ { id: 'not-real', @@ -211,14 +258,14 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 404 - ); + expectedHttpCode: 404, + }); }); it('400s when id is missing', async () => { - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ // @ts-expect-error { @@ -227,15 +274,15 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 400 - ); + expectedHttpCode: 400, + }); }); it('406s when fields are identical', async () => { const postedCase = await createCase(supertest, postCaseReq); - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ { id: postedCase.id, @@ -244,14 +291,14 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 406 - ); + expectedHttpCode: 406, + }); }); it('400s when version is missing', async () => { - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ // @ts-expect-error { @@ -260,16 +307,16 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 400 - ); + expectedHttpCode: 400, + }); }); // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests it.skip('should 400 and not allow converting a collection back to an individual case', async () => { const postedCase = await createCase(supertest, postCollectionReq); - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ { id: postedCase.id, @@ -278,15 +325,15 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 400 - ); + expectedHttpCode: 400, + }); }); it('406s when excess data sent', async () => { const postedCase = await createCase(supertest, postCaseReq); - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ { id: postedCase.id, @@ -296,15 +343,15 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 406 - ); + expectedHttpCode: 406, + }); }); it('400s when bad data sent', async () => { const postedCase = await createCase(supertest, postCaseReq); - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ { id: postedCase.id, @@ -314,15 +361,15 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 400 - ); + expectedHttpCode: 400, + }); }); it('400s when unsupported status sent', async () => { const postedCase = await createCase(supertest, postCaseReq); - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ { id: postedCase.id, @@ -332,15 +379,15 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 400 - ); + expectedHttpCode: 400, + }); }); it('400s when bad connector type sent', async () => { const postedCase = await createCase(supertest, postCaseReq); - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ { id: postedCase.id, @@ -350,15 +397,15 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 400 - ); + expectedHttpCode: 400, + }); }); it('400s when bad connector sent', async () => { const postedCase = await createCase(supertest, postCaseReq); - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ { id: postedCase.id, @@ -374,15 +421,15 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 400 - ); + expectedHttpCode: 400, + }); }); it('409s when version does not match', async () => { const postedCase = await createCase(supertest, postCaseReq); - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ { id: postedCase.id, @@ -392,8 +439,8 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 409 - ); + expectedHttpCode: 409, + }); }); it('should 400 when attempting to update an individual case to a collection when it has alerts attached to it', async () => { @@ -403,9 +450,9 @@ export default ({ getService }: FtrProviderContext): void => { caseId: postedCase.id, params: postCommentAlertReq, }); - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ { id: patchedCase.id, @@ -414,16 +461,16 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 400 - ); + expectedHttpCode: 400, + }); }); // ENABLE_CASE_CONNECTOR: once the case connector feature is completed delete these tests it('should 400 when attempting to update the case type when the case connector feature is disabled', async () => { const postedCase = await createCase(supertest, postCaseReq); - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ { id: postedCase.id, @@ -432,16 +479,16 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 400 - ); + expectedHttpCode: 400, + }); }); // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests it.skip("should 400 when attempting to update a collection case's status", async () => { const postedCase = await createCase(supertest, postCollectionReq); - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ { id: postedCase.id, @@ -450,8 +497,8 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 400 - ); + expectedHttpCode: 400, + }); }); }); @@ -562,12 +609,15 @@ export default ({ getService }: FtrProviderContext): void => { // it updates alert status when syncAlerts is turned on // turn on the sync settings - await updateCase(supertest, { - cases: updatedIndWithStatus.map((caseInfo) => ({ - id: caseInfo.id, - version: caseInfo.version, - settings: { syncAlerts: true }, - })), + await updateCase({ + supertest, + params: { + cases: updatedIndWithStatus.map((caseInfo) => ({ + id: caseInfo.id, + version: caseInfo.version, + settings: { syncAlerts: true }, + })), + }, }); await es.indices.refresh({ index: defaultSignalsIndex }); @@ -682,14 +732,17 @@ export default ({ getService }: FtrProviderContext): void => { ).to.be(CaseStatuses.open); // turn on the sync settings - await updateCase(supertest, { - cases: [ - { - id: updatedIndWithStatus[0].id, - version: updatedIndWithStatus[0].version, - settings: { syncAlerts: true }, - }, - ], + await updateCase({ + supertest, + params: { + cases: [ + { + id: updatedIndWithStatus[0].id, + version: updatedIndWithStatus[0].version, + settings: { syncAlerts: true }, + }, + ], + }, }); await es.indices.refresh({ index: defaultSignalsIndex }); @@ -750,14 +803,17 @@ export default ({ getService }: FtrProviderContext): void => { }); await es.indices.refresh({ index: alert._index }); - await updateCase(supertest, { - cases: [ - { - id: caseUpdated.id, - version: caseUpdated.version, - status: CaseStatuses['in-progress'], - }, - ], + await updateCase({ + supertest, + params: { + cases: [ + { + id: caseUpdated.id, + version: caseUpdated.version, + status: CaseStatuses['in-progress'], + }, + ], + }, }); // force a refresh on the index that the signal is stored in so that we can search for it and get the correct @@ -804,14 +860,17 @@ export default ({ getService }: FtrProviderContext): void => { }, }); - await updateCase(supertest, { - cases: [ - { - id: caseUpdated.id, - version: caseUpdated.version, - status: CaseStatuses['in-progress'], - }, - ], + await updateCase({ + supertest, + params: { + cases: [ + { + id: caseUpdated.id, + version: caseUpdated.version, + status: CaseStatuses['in-progress'], + }, + ], + }, }); const { body: updatedAlert } = await supertest @@ -855,25 +914,31 @@ export default ({ getService }: FtrProviderContext): void => { }); // Update the status of the case with sync alerts off - const caseStatusUpdated = await updateCase(supertest, { - cases: [ - { - id: caseUpdated.id, - version: caseUpdated.version, - status: CaseStatuses['in-progress'], - }, - ], + const caseStatusUpdated = await updateCase({ + supertest, + params: { + cases: [ + { + id: caseUpdated.id, + version: caseUpdated.version, + status: CaseStatuses['in-progress'], + }, + ], + }, }); // Turn sync alerts on - await updateCase(supertest, { - cases: [ - { - id: caseStatusUpdated[0].id, - version: caseStatusUpdated[0].version, - settings: { syncAlerts: true }, - }, - ], + await updateCase({ + supertest, + params: { + cases: [ + { + id: caseStatusUpdated[0].id, + version: caseStatusUpdated[0].version, + settings: { syncAlerts: true }, + }, + ], + }, }); // refresh the index because syncAlerts was set to true so the alert's status should have been updated @@ -916,25 +981,31 @@ export default ({ getService }: FtrProviderContext): void => { }); // Turn sync alerts off - const caseSettingsUpdated = await updateCase(supertest, { - cases: [ - { - id: caseUpdated.id, - version: caseUpdated.version, - settings: { syncAlerts: false }, - }, - ], + const caseSettingsUpdated = await updateCase({ + supertest, + params: { + cases: [ + { + id: caseUpdated.id, + version: caseUpdated.version, + settings: { syncAlerts: false }, + }, + ], + }, }); // Update the status of the case with sync alerts off - await updateCase(supertest, { - cases: [ - { - id: caseSettingsUpdated[0].id, - version: caseSettingsUpdated[0].version, - status: CaseStatuses['in-progress'], - }, - ], + await updateCase({ + supertest, + params: { + cases: [ + { + id: caseSettingsUpdated[0].id, + version: caseSettingsUpdated[0].version, + status: CaseStatuses['in-progress'], + }, + ], + }, }); const { body: updatedAlert } = await supertest @@ -947,5 +1018,223 @@ export default ({ getService }: FtrProviderContext): void => { }); }); }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + it('should update a case when the user has the correct permissions', async () => { + const postedCase = await createCase(supertestWithoutAuth, postCaseReq, 200, { + user: secOnly, + space: 'space1', + }); + + const patchedCases = await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', + }, + ], + }, + auth: { user: secOnly, space: 'space1' }, + }); + + expect(patchedCases[0].owner).to.eql('securitySolutionFixture'); + }); + + it('should update multiple cases when the user has the correct permissions', async () => { + const [case1, case2, case3] = await Promise.all([ + createCase(supertestWithoutAuth, postCaseReq, 200, { + user: superUser, + space: 'space1', + }), + createCase(supertestWithoutAuth, postCaseReq, 200, { + user: superUser, + space: 'space1', + }), + createCase(supertestWithoutAuth, postCaseReq, 200, { + user: superUser, + space: 'space1', + }), + ]); + + const patchedCases = await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: case1.id, + version: case1.version, + title: 'new title', + }, + { + id: case2.id, + version: case2.version, + title: 'new title', + }, + { + id: case3.id, + version: case3.version, + title: 'new title', + }, + ], + }, + auth: { user: secOnly, space: 'space1' }, + }); + + expect(patchedCases[0].owner).to.eql('securitySolutionFixture'); + expect(patchedCases[1].owner).to.eql('securitySolutionFixture'); + expect(patchedCases[2].owner).to.eql('securitySolutionFixture'); + }); + + it('should not update a case when the user does not have the correct ownership', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { user: obsOnly, space: 'space1' } + ); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', + }, + ], + }, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + + it('should not update any cases when the user does not have the correct ownership', async () => { + const [case1, case2, case3] = await Promise.all([ + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ), + ]); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: case1.id, + version: case1.version, + title: 'new title', + }, + { + id: case2.id, + version: case2.version, + title: 'new title', + }, + { + id: case3.id, + version: case3.version, + title: 'new title', + }, + ], + }, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 403, + }); + + const resp = await findCases({ supertest, auth: superUserSpace1Auth }); + expect(resp.cases.length).to.eql(3); + // the update should have failed and none of the title should have been changed + expect(resp.cases[0].title).to.eql(postCaseReq.title); + expect(resp.cases[1].title).to.eql(postCaseReq.title); + expect(resp.cases[2].title).to.eql(postCaseReq.title); + }); + + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT update a case`, async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', + }, + ], + }, + auth: { user, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + } + + it('should NOT create a case in a space with no permissions', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space2', + } + ); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', + }, + ], + }, + auth: { user: secOnly, space: 'space2' }, + expectedHttpCode: 403, + }); + }); + }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/post_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/post_case.ts index f2b9027cfb1f16..91fb03604b3c4b 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/post_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/post_case.ts @@ -128,6 +128,7 @@ export default ({ getService }: FtrProviderContext): void => { case_id: `${postedCase.id}`, comment_id: null, sub_case_id: '', + owner: 'securitySolutionFixture', }); expect(parsedNewValue).to.eql({ diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts index b71c7105be8f2b..f58dfa1522d4a8 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts @@ -6,16 +6,26 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { CaseStatuses } from '../../../../../../../plugins/cases/common/api'; -import { postCaseReq } from '../../../../../common/lib/mock'; +import { getPostCaseRequest, postCaseReq } from '../../../../../common/lib/mock'; import { - deleteCasesByESQuery, createCase, updateCase, getAllCasesStatuses, + deleteAllCaseItems, } from '../../../../../common/lib/utils'; +import { + globalRead, + noKibanaPrivileges, + obsOnlyRead, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -24,35 +34,35 @@ export default ({ getService }: FtrProviderContext): void => { describe('get_status', () => { afterEach(async () => { - await deleteCasesByESQuery(es); + await deleteAllCaseItems(es); }); it('should return case statuses', async () => { - await createCase(supertest, postCaseReq); - const inProgressCase = await createCase(supertest, postCaseReq); - const postedCase = await createCase(supertest, postCaseReq); - - await updateCase(supertest, { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: CaseStatuses.closed, - }, - ], - }); + const [, inProgressCase, postedCase] = await Promise.all([ + createCase(supertest, postCaseReq), + createCase(supertest, postCaseReq), + createCase(supertest, postCaseReq), + ]); - await updateCase(supertest, { - cases: [ - { - id: inProgressCase.id, - version: inProgressCase.version, - status: CaseStatuses['in-progress'], - }, - ], + await updateCase({ + supertest, + params: { + cases: [ + { + id: inProgressCase.id, + version: inProgressCase.version, + status: CaseStatuses['in-progress'], + }, + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses.closed, + }, + ], + }, }); - const statuses = await getAllCasesStatuses(supertest); + const statuses = await getAllCasesStatuses({ supertest }); expect(statuses).to.eql({ count_open_cases: 1, @@ -60,5 +70,103 @@ export default ({ getService }: FtrProviderContext): void => { count_in_progress_cases: 1, }); }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + it('should return the correct status stats', async () => { + /** + * Owner: Sec + * open: 0, in-prog: 1, closed: 1 + * Owner: Obs + * open: 1, in-prog: 1 + */ + const [inProgressSec, closedSec, , inProgressObs] = await Promise.all([ + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { + user: superUser, + space: 'space1', + }), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { + user: superUser, + space: 'space1', + }), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + superUserSpace1Auth + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + superUserSpace1Auth + ), + ]); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: inProgressSec.id, + version: inProgressSec.version, + status: CaseStatuses['in-progress'], + }, + { + id: closedSec.id, + version: closedSec.version, + status: CaseStatuses.closed, + }, + { + id: inProgressObs.id, + version: inProgressObs.version, + status: CaseStatuses['in-progress'], + }, + ], + }, + auth: superUserSpace1Auth, + }); + + for (const scenario of [ + { user: globalRead, stats: { open: 1, inProgress: 2, closed: 1 } }, + { user: superUser, stats: { open: 1, inProgress: 2, closed: 1 } }, + { user: secOnlyRead, stats: { open: 0, inProgress: 1, closed: 1 } }, + { user: obsOnlyRead, stats: { open: 1, inProgress: 1, closed: 0 } }, + { user: obsSecRead, stats: { open: 1, inProgress: 2, closed: 1 } }, + ]) { + const statuses = await getAllCasesStatuses({ + supertest: supertestWithoutAuth, + auth: { user: scenario.user, space: 'space1' }, + }); + + expect(statuses).to.eql({ + count_open_cases: scenario.stats.open, + count_closed_cases: scenario.stats.closed, + count_in_progress_cases: scenario.stats.inProgress, + }); + } + }); + + for (const scenario of [ + { user: noKibanaPrivileges, space: 'space1' }, + { user: secOnly, space: 'space2' }, + ]) { + it(`should return a 403 when retrieving the statuses when the user ${ + scenario.user.username + } with role(s) ${scenario.user.roles.join()} and space ${scenario.space}`, async () => { + await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { + user: superUser, + space: scenario.space, + }); + + await getAllCasesStatuses({ + supertest: supertestWithoutAuth, + auth: { user: scenario.user, space: scenario.space }, + expectedHttpCode: 403, + }); + }); + } + }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts index 353974632feb88..73b85ef97d1194 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts @@ -33,6 +33,7 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -277,14 +278,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); const commentResp = await createComment({ supertest: supertestWithoutAuth, caseId: postedCase.id, params: postCommentUserReq, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); await deleteComment({ @@ -309,14 +310,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); const commentResp = await createComment({ supertest: supertestWithoutAuth, caseId: postedCase.id, params: postCommentUserReq, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); await deleteComment({ diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts index 470c2481410ff7..0f73b1ee7a624a 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts @@ -40,6 +40,7 @@ import { globalRead, obsSecRead, } from '../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -312,12 +313,12 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); await createComment({ supertest: supertestWithoutAuth, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, params: { ...postCommentUserReq, owner: 'observabilityFixture' }, caseId: obsCase.id, }); @@ -340,12 +341,12 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); await createComment({ supertest: supertestWithoutAuth, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, params: { ...postCommentUserReq, owner: 'observabilityFixture' }, caseId: obsCase.id, }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts index 2be30ed7bc02ce..361e72bdc79bf6 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts @@ -31,6 +31,7 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -141,21 +142,21 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); await createComment({ supertest: supertestWithoutAuth, caseId: caseInfo.id, params: postCommentUserReq, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); await createComment({ supertest: supertestWithoutAuth, caseId: caseInfo.id, params: postCommentUserReq, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { @@ -174,14 +175,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); await createComment({ supertest: supertestWithoutAuth, caseId: caseInfo.id, params: postCommentUserReq, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); for (const scenario of [ diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts index 7b55d468312a1b..98b6cc5a7a30c5 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts @@ -30,6 +30,7 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -94,14 +95,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); const caseWithComment = await createComment({ supertest: supertestWithoutAuth, caseId: caseInfo.id, params: postCommentUserReq, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { @@ -119,14 +120,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); const caseWithComment = await createComment({ supertest: supertestWithoutAuth, caseId: caseInfo.id, params: postCommentUserReq, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); for (const user of [noKibanaPrivileges, obsOnly, obsOnlyRead]) { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts index fcaebddeb8bde7..c1f37d5eb2f057 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts @@ -45,6 +45,7 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -511,14 +512,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); const patchedCase = await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); const newComment = 'Well I decided to update my comment. So what? Deal with it.'; @@ -546,14 +547,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); const patchedCase = await createComment({ supertest: supertestWithoutAuth, caseId: postedCase.id, params: postCommentUserReq, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); const newComment = 'Well I decided to update my comment. So what? Deal with it.'; @@ -579,14 +580,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); const patchedCase = await createComment({ supertest: supertestWithoutAuth, caseId: postedCase.id, params: postCommentUserReq, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); const newComment = 'Well I decided to update my comment. So what? Deal with it.'; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts index 0e501648c512bf..1fcb49ec10ad4e 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts @@ -61,6 +61,7 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -151,6 +152,7 @@ export default ({ getService }: FtrProviderContext): void => { case_id: `${postedCase.id}`, comment_id: `${patchedCase.comments![0].id}`, sub_case_id: '', + owner: 'securitySolutionFixture', }); }); }); @@ -533,7 +535,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); await createComment({ @@ -569,7 +571,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); await createComment({ diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts index c6c68efd7a7523..ff2d1b5f37aae6 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts @@ -35,9 +35,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./sub_cases/get_sub_case')); loadTestFile(require.resolve('./sub_cases/find_sub_cases')); - // Migrations - loadTestFile(require.resolve('./cases/migrations')); - loadTestFile(require.resolve('./configure/migrations')); - loadTestFile(require.resolve('./user_actions/migrations')); + // NOTE: Migrations are not included because they can inadvertently remove the .kibana indices which removes the users and spaces + // which causes errors in any tests after them that relies on those }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/migrations.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/migrations.ts new file mode 100644 index 00000000000000..17d93e76bbdda5 --- /dev/null +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/migrations.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('Common migrations', function () { + // Migrations + loadTestFile(require.resolve('./cases/migrations')); + loadTestFile(require.resolve('./configure/migrations')); + loadTestFile(require.resolve('./user_actions/migrations')); + }); +}; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts index 19911890929d2d..5cd4082bd3293b 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts @@ -9,14 +9,33 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; -import { CommentType } from '../../../../../../plugins/cases/common/api'; -import { userActionPostResp, postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; import { - deleteCasesByESQuery, - deleteCasesUserActions, - deleteComments, - deleteConfiguration, + CaseResponse, + CaseStatuses, + CommentType, +} from '../../../../../../plugins/cases/common/api'; +import { + userActionPostResp, + postCaseReq, + postCommentUserReq, + getPostCaseRequest, +} from '../../../../common/lib/mock'; +import { + deleteAllCaseItems, + createCase, + updateCase, + getCaseUserActions, } from '../../../../common/lib/utils'; +import { + globalRead, + noKibanaPrivileges, + obsSec, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -25,10 +44,7 @@ export default ({ getService }: FtrProviderContext): void => { describe('get_all_user_actions', () => { afterEach(async () => { - await deleteCasesByESQuery(es); - await deleteComments(es); - await deleteConfiguration(es); - await deleteCasesUserActions(es); + await deleteAllCaseItems(es); }); it(`on new case, user action: 'create' should be called with actionFields: ['description', 'status', 'tags', 'title', 'connector', 'settings, owner]`, async () => { @@ -321,5 +337,59 @@ export default ({ getService }: FtrProviderContext): void => { owner: 'securitySolutionFixture', }); }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + let caseInfo: CaseResponse; + beforeEach(async () => { + caseInfo = await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { + user: superUser, + space: 'space1', + }); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: caseInfo.id, + version: caseInfo.version, + status: CaseStatuses.closed, + }, + ], + }, + auth: superUserSpace1Auth, + }); + }); + + it('should get the user actions for a case when the user has the correct permissions', async () => { + for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { + const userActions = await getCaseUserActions({ + supertest: supertestWithoutAuth, + caseID: caseInfo.id, + auth: { user, space: 'space1' }, + }); + + expect(userActions.length).to.eql(2); + } + }); + + for (const scenario of [ + { user: noKibanaPrivileges, space: 'space1' }, + { user: secOnly, space: 'space2' }, + ]) { + it(`should 403 when requesting the user actions of a case with user ${ + scenario.user.username + } with role(s) ${scenario.user.roles.join()} and space ${scenario.space}`, async () => { + await getCaseUserActions({ + supertest: supertestWithoutAuth, + caseID: caseInfo.id, + auth: { user: scenario.user, space: scenario.space }, + expectedHttpCode: 403, + }); + }); + } + }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts index 88f7c15f4a5fe8..3c096cb7557c32 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts @@ -8,15 +8,18 @@ /* eslint-disable @typescript-eslint/naming-convention */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import * as st from 'supertest'; +import supertestAsPromised from 'supertest-as-promised'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; -import { postCaseReq, defaultUser, postCommentUserReq } from '../../../../common/lib/mock'; import { - deleteCasesByESQuery, - deleteCasesUserActions, - deleteComments, - deleteConfiguration, + postCaseReq, + defaultUser, + postCommentUserReq, + getPostCaseRequest, +} from '../../../../common/lib/mock'; +import { getConfigurationRequest, getServiceNowConnector, createConnector, @@ -28,6 +31,7 @@ import { updateCase, getAllUserAction, removeServerGeneratedPropertiesFromUserAction, + deleteAllCaseItems, } from '../../../../common/lib/utils'; import { ExternalServiceSimulator, @@ -35,11 +39,23 @@ import { } from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; import { CaseConnector, + CasePostRequest, CaseResponse, CaseStatuses, CaseUserActionResponse, ConnectorTypes, } from '../../../../../../plugins/cases/common/api'; +import { + globalRead, + noKibanaPrivileges, + obsOnlyRead, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../common/lib/authentication/users'; +import { User } from '../../../../common/lib/authentication/types'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -58,56 +74,79 @@ export default ({ getService }: FtrProviderContext): void => { }); afterEach(async () => { - await deleteCasesByESQuery(es); - await deleteComments(es); - await deleteConfiguration(es); - await deleteCasesUserActions(es); + await deleteAllCaseItems(es); await actionsRemover.removeAll(); }); - const createCaseWithConnector = async ( - configureReq = {} - ): Promise<{ + const createCaseWithConnector = async ({ + testAgent = supertest, + configureReq = {}, + auth = { user: superUser, space: null }, + createCaseReq = getPostCaseRequest(), + }: { + testAgent?: st.SuperTest; + configureReq?: Record; + auth?: { user: User; space: string | null }; + createCaseReq?: CasePostRequest; + } = {}): Promise<{ postedCase: CaseResponse; connector: CreateConnectorResponse; }> => { - const connector = await createConnector(supertest, { - ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, + const connector = await createConnector({ + supertest: testAgent, + req: { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }, + auth, }); - actionsRemover.add('default', connector.id, 'action', 'actions'); - await createConfiguration(supertest, { - ...getConfigurationRequest({ - id: connector.id, - name: connector.name, - type: connector.connector_type_id as ConnectorTypes, - }), - ...configureReq, - }); + actionsRemover.add(auth.space ?? 'default', connector.id, 'action', 'actions'); + await createConfiguration( + testAgent, + { + ...getConfigurationRequest({ + id: connector.id, + name: connector.name, + type: connector.connector_type_id as ConnectorTypes, + }), + ...configureReq, + }, + 200, + auth + ); - const postedCase = await createCase(supertest, { - ...postCaseReq, - connector: { - id: connector.id, - name: connector.name, - type: connector.connector_type_id, - fields: { - urgency: '2', - impact: '2', - severity: '2', - category: 'software', - subcategory: 'os', - }, - } as CaseConnector, - }); + const postedCase = await createCase( + testAgent, + { + ...createCaseReq, + connector: { + id: connector.id, + name: connector.name, + type: connector.connector_type_id, + fields: { + urgency: '2', + impact: '2', + severity: '2', + category: 'software', + subcategory: 'os', + }, + } as CaseConnector, + }, + 200, + auth + ); return { postedCase, connector }; }; it('should push a case', async () => { const { postedCase, connector } = await createCaseWithConnector(); - const theCase = await pushCase(supertest, postedCase.id, connector.id); + const theCase = await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: connector.id, + }); const { pushed_at, external_url, ...rest } = theCase.external_service!; @@ -130,23 +169,37 @@ export default ({ getService }: FtrProviderContext): void => { it('pushes a comment appropriately', async () => { const { postedCase, connector } = await createCaseWithConnector(); await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq }); - const theCase = await pushCase(supertest, postedCase.id, connector.id); + const theCase = await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: connector.id, + }); expect(theCase.comments![0].pushed_by).to.eql(defaultUser); }); it('should pushes a case and closes when closure_type: close-by-pushing', async () => { const { postedCase, connector } = await createCaseWithConnector({ - closure_type: 'close-by-pushing', + configureReq: { + closure_type: 'close-by-pushing', + }, + }); + const theCase = await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: connector.id, }); - const theCase = await pushCase(supertest, postedCase.id, connector.id); expect(theCase.status).to.eql('closed'); }); it('should create the correct user action', async () => { const { postedCase, connector } = await createCaseWithConnector(); - const pushedCase = await pushCase(supertest, postedCase.id, connector.id); + const pushedCase = await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: connector.id, + }); const userActions = await getAllUserAction(supertest, pushedCase.id); const pushUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); @@ -161,6 +214,7 @@ export default ({ getService }: FtrProviderContext): void => { case_id: `${postedCase.id}`, comment_id: null, sub_case_id: '', + owner: 'securitySolutionFixture', }); expect(parsedNewValue).to.eql({ @@ -177,15 +231,26 @@ export default ({ getService }: FtrProviderContext): void => { // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests it.skip('should push a collection case but not close it when closure_type: close-by-pushing', async () => { const { postedCase, connector } = await createCaseWithConnector({ - closure_type: 'close-by-pushing', + configureReq: { + closure_type: 'close-by-pushing', + }, }); - const theCase = await pushCase(supertest, postedCase.id, connector.id); + const theCase = await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: connector.id, + }); expect(theCase.status).to.eql(CaseStatuses.open); }); it('unhappy path - 404s when case does not exist', async () => { - await pushCase(supertest, 'fake-id', 'fake-connector', 404); + await pushCase({ + supertest, + caseId: 'fake-id', + connectorId: 'fake-connector', + expectedHttpCode: 404, + }); }); it('unhappy path - 404s when connector does not exist', async () => { @@ -193,22 +258,103 @@ export default ({ getService }: FtrProviderContext): void => { ...postCaseReq, connector: getConfigurationRequest().connector, }); - await pushCase(supertest, postedCase.id, 'fake-connector', 404); + await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: 'fake-connector', + expectedHttpCode: 404, + }); }); it('unhappy path = 409s when case is closed', async () => { const { postedCase, connector } = await createCaseWithConnector(); - await updateCase(supertest, { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: CaseStatuses.closed, - }, - ], + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses.closed, + }, + ], + }, }); - await pushCase(supertest, postedCase.id, connector.id, 409); + await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: connector.id, + expectedHttpCode: 409, + }); + }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + it('should push a case that the user has permissions for', async () => { + const { postedCase, connector } = await createCaseWithConnector({ + testAgent: supertestWithoutAuth, + auth: superUserSpace1Auth, + }); + + await pushCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + connectorId: connector.id, + auth: { user: secOnly, space: 'space1' }, + }); + }); + + it('should not push a case that the user does not have permissions for', async () => { + const { postedCase, connector } = await createCaseWithConnector({ + testAgent: supertestWithoutAuth, + auth: superUserSpace1Auth, + createCaseReq: getPostCaseRequest({ owner: 'observabilityFixture' }), + }); + + await pushCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + connectorId: connector.id, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT push a case`, async () => { + const { postedCase, connector } = await createCaseWithConnector({ + testAgent: supertestWithoutAuth, + auth: superUserSpace1Auth, + }); + + await pushCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + connectorId: connector.id, + auth: { user, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + } + + it('should not push a case in a space that the user does not have permissions for', async () => { + const { postedCase, connector } = await createCaseWithConnector({ + testAgent: supertestWithoutAuth, + auth: { user: superUser, space: 'space2' }, + }); + + await pushCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + connectorId: connector.id, + auth: { user: secOnly, space: 'space2' }, + expectedHttpCode: 403, + }); + }); }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_configure.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_configure.ts index 6d556423893d53..ff8f1cff884aff 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_configure.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_configure.ts @@ -45,9 +45,12 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should return a configuration with mapping', async () => { - const connector = await createConnector(supertest, { - ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, + const connector = await createConnector({ + supertest, + req: { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }, }); actionsRemover.add('default', connector.id, 'action', 'actions'); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts index 6faea0e1789bb5..bc27dd17a21b6c 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts @@ -64,7 +64,7 @@ export default ({ getService }: FtrProviderContext): void => { .send(getResilientConnector()) .expect(200); - const sir = await createConnector(supertest, getServiceNowSIRConnector()); + const sir = await createConnector({ supertest, req: getServiceNowSIRConnector() }); actionsRemover.add('default', sir.id, 'action', 'actions'); actionsRemover.add('default', snConnector.id, 'action', 'actions'); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/patch_configure.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/patch_configure.ts index 9e82ce1f0c2338..789b68b19beb66 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/patch_configure.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/patch_configure.ts @@ -47,9 +47,12 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should patch a configuration connector and create mappings', async () => { - const connector = await createConnector(supertest, { - ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, + const connector = await createConnector({ + supertest, + req: { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }, }); actionsRemover.add('default', connector.id, 'action', 'actions'); @@ -100,9 +103,12 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should mappings when updating the connector', async () => { - const connector = await createConnector(supertest, { - ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, + const connector = await createConnector({ + supertest, + req: { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }, }); actionsRemover.add('default', connector.id, 'action', 'actions'); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/post_configure.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/post_configure.ts index 503e0384859ec2..96ffcf4bc3f5c5 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/post_configure.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/post_configure.ts @@ -46,9 +46,12 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should create a configuration with mapping', async () => { - const connector = await createConnector(supertest, { - ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, + const connector = await createConnector({ + supertest, + req: { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }, }); actionsRemover.add('default', connector.id, 'action', 'actions'); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts index 5ba09dd56bd67b..26bc6a072450de 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts @@ -22,11 +22,15 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => { await deleteSpacesAndUsers(getService); }); - // Common - loadTestFile(require.resolve('../common')); - // Trial loadTestFile(require.resolve('./cases/push_case')); + loadTestFile(require.resolve('./cases/user_actions/get_all_user_actions')); loadTestFile(require.resolve('./configure/index')); + + // Common + loadTestFile(require.resolve('../common')); + + // NOTE: These need to be at the end because they could delete the .kibana index and inadvertently remove the users and spaces + loadTestFile(require.resolve('../common/migrations')); }); }; From b121662dd8485b759b88c33856284b06979058b9 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Tue, 4 May 2021 16:48:25 -0400 Subject: [PATCH 052/113] Fixing some type errors --- .../components/all_cases/selector_modal/index.test.tsx | 1 + .../cases/public/components/case_view/helpers.test.tsx | 2 ++ x-pack/plugins/cases/public/containers/configure/api.ts | 2 +- .../server/routes/api/__fixtures__/mock_saved_objects.ts | 1 + .../components/timeline_actions/add_to_case_action.test.tsx | 6 +----- .../components/timeline_actions/add_to_case_action.tsx | 1 - .../tests/trial/configure/get_connectors.ts | 1 + 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/cases/public/components/all_cases/selector_modal/index.test.tsx b/x-pack/plugins/cases/public/components/all_cases/selector_modal/index.test.tsx index aaec37335c6999..b2444c5ccb0ddc 100644 --- a/x-pack/plugins/cases/public/components/all_cases/selector_modal/index.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/selector_modal/index.test.tsx @@ -59,6 +59,7 @@ describe('AllCasesSelectorModal', () => { }, index: 'index-id', alertId: 'alert-id', + owner: 'securitySolution', }, disabledStatuses: [], updateCase, diff --git a/x-pack/plugins/cases/public/components/case_view/helpers.test.tsx b/x-pack/plugins/cases/public/components/case_view/helpers.test.tsx index f266c574c27da0..47ab272bdc3f86 100644 --- a/x-pack/plugins/cases/public/components/case_view/helpers.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/helpers.test.tsx @@ -28,6 +28,7 @@ const comments: Comment[] = [ updatedAt: null, updatedBy: null, version: 'WzQ3LDFc', + owner: 'securitySolution', }, { associationType: AssociationType.case, @@ -46,6 +47,7 @@ const comments: Comment[] = [ updatedAt: null, updatedBy: null, version: 'WzQ3LDFc', + owner: 'securitySolution', }, ]; diff --git a/x-pack/plugins/cases/public/containers/configure/api.ts b/x-pack/plugins/cases/public/containers/configure/api.ts index e31b5a8603bbf3..2d26e390050574 100644 --- a/x-pack/plugins/cases/public/containers/configure/api.ts +++ b/x-pack/plugins/cases/public/containers/configure/api.ts @@ -16,6 +16,7 @@ import { CasesConfigureRequest, CasesConfigureResponse, CasesConfigurationsResponse, + getCaseConfigurationDetailsUrl, } from '../../../common'; import { KibanaServices } from '../../common/lib/kibana'; @@ -26,7 +27,6 @@ import { decodeCaseConfigureResponse, } from '../utils'; import { CaseConfigure } from './types'; -import { getCaseConfigurationDetailsUrl } from '../../../../../cases/common/api/helpers'; export const fetchConnectors = async ({ signal }: ApiProps): Promise => { const response = await KibanaServices.get().http.fetch(`${CASE_CONFIGURE_CONNECTORS_URL}/_find`, { 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 53b8fce3166e46..ff188426dd96db 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 @@ -503,6 +503,7 @@ export const mockCaseMappingsResilient: Array> = id: 'mock-mappings-1', attributes: { mappings: mappings[ConnectorTypes.resilient], + owner: 'securitySolution', }, references: [], }, diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx index 4835787674eaa3..fa37fb53a54b06 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx @@ -116,6 +116,7 @@ describe('AddToCaseAction', () => { alertId: 'test-id', index: 'test-index', rule: { id: 'rule-id', name: 'rule-name' }, + owner: 'securitySolution', }); }); @@ -138,16 +139,11 @@ describe('AddToCaseAction', () => { expect(mockAllCasesModal.mock.calls[0][0].alertData).toEqual({ alertId: 'test-id', index: 'test-index', -<<<<<<< HEAD rule: { id: 'rule-id', name: null, }, - type: 'alert', owner: 'securitySolution', -======= - rule: { id: 'rule-id', name: null }, ->>>>>>> 9e2e8b9f19793ad658dc0ccea9acd27dbc1bf766 }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx index c524a10c7b833e..f7594dbb4c180a 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx @@ -49,7 +49,6 @@ interface PostCommentArg { }; updateCase?: (newCase: Case) => void; subCaseId?: string; - // TODO: refactor } const AddToCaseActionComponent: React.FC = ({ diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts index 8716c74a013823..fb922f8d102434 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts @@ -120,6 +120,7 @@ export default ({ getService }: FtrProviderContext): void => { name: 'ServiceNow Connector', config: { apiUrl: 'http://some.non.existent.com' }, isPreconfigured: false, + isMissingSecrets: false, referencedByCount: 0, }, ]); From b910889069f0735a1dbc6cb4450e12c7ad21c518 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Mon, 10 May 2021 14:19:06 -0400 Subject: [PATCH 053/113] [Cases] Add space only tests (#99409) * Starting spaces tests * Finishing space only tests * Refactoring createCaseWithConnector * Fixing spelling * Addressing PR feedback and creating alert tests * Fixing mocks --- x-pack/plugins/cases/server/client/mocks.ts | 1 + x-pack/scripts/functional_tests.js | 1 + .../common/lib/authentication/index.ts | 4 +- .../case_api_integration/common/lib/mock.ts | 5 + .../case_api_integration/common/lib/utils.ts | 113 ++++++++--- .../tests/common/cases/delete_cases.ts | 7 +- .../tests/common/cases/patch_cases.ts | 8 +- .../tests/common/cases/post_case.ts | 4 +- .../tests/common/cases/status/get_status.ts | 2 +- .../tests/common/comments/delete_comment.ts | 2 +- .../tests/common/comments/find_comments.ts | 2 +- .../tests/common/comments/get_all_comments.ts | 2 +- .../tests/common/comments/get_comment.ts | 2 +- .../tests/common/comments/patch_comment.ts | 2 +- .../tests/common/comments/post_comment.ts | 6 +- .../tests/common/configure/get_connectors.ts | 2 +- .../user_actions/get_all_user_actions.ts | 2 +- .../tests/trial/cases/push_case.ts | 131 +++++-------- .../spaces_only/config.ts | 5 +- .../tests/common/alerts/get_cases.ts | 110 +++++++++++ .../tests/common/cases/delete_cases.ts | 53 ++++++ .../tests/common/cases/find_cases.ts | 63 ++++++ .../tests/common/cases/get_case.ts | 49 +++++ .../tests/common/cases/patch_cases.ts | 74 ++++++++ .../tests/common/cases/post_case.ts | 64 +++++++ .../common/cases/reporters/get_reporters.ts | 47 +++++ .../tests/common/cases/status/get_status.ts | 92 +++++++++ .../tests/common/cases/tags/get_tags.ts | 42 ++++ .../tests/common/comments/delete_comment.ts | 72 +++++++ .../tests/common/comments/find_comments.ts | 82 ++++++++ .../tests/common/comments/get_all_comments.ts | 73 +++++++ .../tests/common/comments/get_comment.ts | 66 +++++++ .../tests/common/comments/patch_comment.ts | 90 +++++++++ .../tests/common/comments/post_comment.ts | 75 ++++++++ .../tests/common/configure/get_configure.ts | 51 +++++ .../tests/common/configure/patch_configure.ts | 77 ++++++++ .../tests/common/configure/post_configure.ts | 44 +++++ .../spaces_only/tests/common/index.ts | 33 ++++ .../user_actions/get_all_user_actions.ts | 48 +++++ .../tests/trial/cases/push_case.ts | 96 ++++++++++ .../tests/trial/configure/get_configure.ts | 135 +++++++++++++ .../tests/trial/configure/get_connectors.ts | 179 ++++++++++++++++++ .../tests/trial/configure/index.ts | 18 ++ .../tests/trial/configure/patch_configure.ts | 162 ++++++++++++++++ .../tests/trial/configure/post_configure.ts | 105 ++++++++++ .../spaces_only/tests/{ => trial}/index.ts | 11 +- 46 files changed, 2173 insertions(+), 139 deletions(-) create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/alerts/get_cases.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/cases/delete_cases.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/cases/find_cases.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/cases/get_case.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/cases/patch_cases.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/cases/post_case.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/cases/reporters/get_reporters.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/cases/status/get_status.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/cases/tags/get_tags.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/comments/delete_comment.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/comments/find_comments.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/comments/get_all_comments.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/comments/get_comment.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/comments/patch_comment.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/comments/post_comment.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/configure/get_configure.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/configure/patch_configure.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/configure/post_configure.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/index.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/user_actions/get_all_user_actions.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/trial/cases/push_case.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_configure.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_connectors.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/trial/configure/index.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/trial/configure/patch_configure.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/trial/configure/post_configure.ts rename x-pack/test/case_api_integration/spaces_only/tests/{ => trial}/index.ts (61%) diff --git a/x-pack/plugins/cases/server/client/mocks.ts b/x-pack/plugins/cases/server/client/mocks.ts index 7db3d62c491e7a..10b298995f87ac 100644 --- a/x-pack/plugins/cases/server/client/mocks.ts +++ b/x-pack/plugins/cases/server/client/mocks.ts @@ -28,6 +28,7 @@ const createCasesSubClientMock = (): CasesSubClientMock => { delete: jest.fn(), getTags: jest.fn(), getReporters: jest.fn(), + getCaseIDsByAlertID: jest.fn(), }; }; diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index ebcdef46ebd57b..e2b3c951b07222 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -36,6 +36,7 @@ const onlyNotInCoverageTests = [ require.resolve('../test/alerting_api_integration/security_and_spaces/config.ts'), require.resolve('../test/case_api_integration/security_and_spaces/config_basic.ts'), require.resolve('../test/case_api_integration/security_and_spaces/config_trial.ts'), + require.resolve('../test/case_api_integration/spaces_only/config.ts'), require.resolve('../test/apm_api_integration/basic/config.ts'), require.resolve('../test/apm_api_integration/trial/config.ts'), require.resolve('../test/apm_api_integration/rules/config.ts'), 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 dfd151344b40c1..a72141745e5777 100644 --- a/x-pack/test/case_api_integration/common/lib/authentication/index.ts +++ b/x-pack/test/case_api_integration/common/lib/authentication/index.ts @@ -7,7 +7,7 @@ import { FtrProviderContext as CommonFtrProviderContext } from '../../../common/ftr_provider_context'; import { Role, User, UserInfo } from './types'; -import { superUser, users } from './users'; +import { users } from './users'; import { roles } from './roles'; import { spaces } from './spaces'; @@ -90,5 +90,3 @@ export const deleteSpacesAndUsers = async (getService: CommonFtrProviderContext[ await deleteSpaces(getService); await deleteUsersAndRoles(getService); }; - -export const superUserSpace1Auth = { user: superUser, space: 'space1' }; diff --git a/x-pack/test/case_api_integration/common/lib/mock.ts b/x-pack/test/case_api_integration/common/lib/mock.ts index 20511f8daab649..015661b0158a1a 100644 --- a/x-pack/test/case_api_integration/common/lib/mock.ts +++ b/x-pack/test/case_api_integration/common/lib/mock.ts @@ -31,6 +31,11 @@ import { } from '../../../../plugins/cases/common/api'; export const defaultUser = { email: null, full_name: null, username: 'elastic' }; +/** + * A null filled user will occur when the security plugin is disabled + */ +export const nullUser = { email: null, full_name: null, username: null }; + export const postCaseReq: CasePostRequest = { description: 'This is a brand new case of a bad meanie defacing data', title: 'Super Bad Security Issue', 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 71d7ab9c30224d..b7a713b6316cb8 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -12,6 +12,7 @@ import type { KibanaClient } from '@elastic/elasticsearch/api/kibana'; import * as st from 'supertest'; import supertestAsPromised from 'supertest-as-promised'; +import { ObjectRemover as ActionsRemover } from '../../../alerting_api_integration/common/lib'; import { CASES_URL, CASE_CONFIGURE_CONNECTORS_URL, @@ -45,7 +46,7 @@ import { CasesConfigurationsResponse, CaseUserActionsResponse, } from '../../../../plugins/cases/common/api'; -import { postCollectionReq, postCommentGenAlertReq } from './mock'; +import { getPostCaseRequest, postCollectionReq, postCommentGenAlertReq } from './mock'; import { getCaseUserActionUrl, getSubCasesUrl } from '../../../../plugins/cases/common/api/helpers'; import { ContextTypeGeneratedAlertType } from '../../../../plugins/cases/server/connectors'; import { SignalHit } from '../../../../plugins/security_solution/server/lib/detection_engine/signals/types'; @@ -539,6 +540,16 @@ export const deleteMappings = async (es: KibanaClient): Promise => { }); }; +export const superUserSpace1Auth = getAuthWithSuperUser(); + +/** + * Returns an auth object with the specified space and user set as super user. The result can be passed to other utility + * functions. + */ +export function getAuthWithSuperUser(space: string = 'space1'): { user: User; space: string } { + return { user: superUser, space }; +} + export const getSpaceUrlPrefix = (spaceId: string | undefined | null) => { return spaceId && spaceId !== 'default' ? `/s/${spaceId}` : ``; }; @@ -556,6 +567,72 @@ export const ensureSavedObjectIsAuthorized = ( entities.forEach((entity) => expect(owners.includes(entity.owner)).to.be(true)); }; +export const createCaseWithConnector = async ({ + supertest, + configureReq = {}, + servicenowSimulatorURL, + actionsRemover, + auth = { user: superUser, space: null }, + createCaseReq = getPostCaseRequest(), +}: { + supertest: st.SuperTest; + servicenowSimulatorURL: string; + actionsRemover: ActionsRemover; + configureReq?: Record; + auth?: { user: User; space: string | null }; + createCaseReq?: CasePostRequest; +}): Promise<{ + postedCase: CaseResponse; + connector: CreateConnectorResponse; +}> => { + const connector = await createConnector({ + supertest, + req: { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }, + auth, + }); + + actionsRemover.add(auth.space ?? 'default', connector.id, 'action', 'actions'); + await createConfiguration( + supertest, + { + ...getConfigurationRequest({ + id: connector.id, + name: connector.name, + type: connector.connector_type_id as ConnectorTypes, + }), + ...configureReq, + }, + 200, + auth + ); + + const postedCase = await createCase( + supertest, + { + ...createCaseReq, + connector: { + id: connector.id, + name: connector.name, + type: connector.connector_type_id, + fields: { + urgency: '2', + impact: '2', + severity: '2', + category: 'software', + subcategory: 'os', + }, + } as CaseConnector, + }, + 200, + auth + ); + + return { postedCase, connector }; +}; + export const createCase = async ( supertest: st.SuperTest, params: CasePostRequest, @@ -622,19 +699,6 @@ export const createComment = async ({ return theCase; }; -export const getAllUserAction = async ( - supertest: st.SuperTest, - caseId: string, - expectedHttpCode: number = 200 -): Promise => { - const { body: userActions } = await supertest - .get(`${CASES_URL}/${caseId}/user_actions`) - .set('kbn-xsrf', 'true') - .expect(expectedHttpCode); - - return userActions; -}; - export const updateCase = async ({ supertest, params, @@ -742,13 +806,13 @@ export const getComment = async ({ caseId, commentId, expectedHttpCode = 200, - auth = { user: superUser }, + auth = { user: superUser, space: null }, }: { supertest: st.SuperTest; caseId: string; commentId: string; expectedHttpCode?: number; - auth?: { user: User; space?: string }; + auth?: { user: User; space: string | null }; }): Promise => { const { body: comment } = await supertest .get(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}/${caseId}/comments/${commentId}`) @@ -843,13 +907,18 @@ export const createConnector = async ({ return connector; }; -export const getCaseConnectors = async ( - supertest: st.SuperTest, - expectedHttpCode: number = 200 -): Promise => { +export const getCaseConnectors = async ({ + supertest, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: st.SuperTest; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): Promise => { const { body: connectors } = await supertest - .get(`${CASE_CONFIGURE_CONNECTORS_URL}/_find`) - .set('kbn-xsrf', 'true') + .get(`${getSpaceUrlPrefix(auth.space)}${CASE_CONFIGURE_CONNECTORS_URL}/_find`) + .auth(auth.user.username, auth.user.password) .expect(expectedHttpCode); return connectors; 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 17aac2dd7e2859..03bcf0d538fe3b 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 @@ -22,9 +22,10 @@ import { deleteCases, createComment, getComment, - getAllUserAction, removeServerGeneratedPropertiesFromUserAction, getCase, + superUserSpace1Auth, + getCaseUserActions, } from '../../../../common/lib/utils'; import { getSubCaseDetailsUrl } from '../../../../../../plugins/cases/common/api/helpers'; import { CaseResponse } from '../../../../../../plugins/cases/common/api'; @@ -39,8 +40,6 @@ import { superUser, } from '../../../../common/lib/authentication/users'; -import { superUserSpace1Auth } from '../../../../common/lib/authentication'; - // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertestWithoutAuth = getService('supertestWithoutAuth'); @@ -89,7 +88,7 @@ export default ({ getService }: FtrProviderContext): void => { 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 userActions = await getCaseUserActions({ supertest, caseID: postedCase.id }); const creationUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); expect(creationUserAction).to.eql({ diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts index 674c2c68381b8c..286e08716ebf1e 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts @@ -32,10 +32,11 @@ import { createCase, createComment, updateCase, - getAllUserAction, + getCaseUserActions, removeServerGeneratedPropertiesFromCase, removeServerGeneratedPropertiesFromUserAction, findCases, + superUserSpace1Auth, } from '../../../../common/lib/utils'; import { createSignalsIndex, @@ -58,7 +59,6 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; -import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -110,7 +110,7 @@ export default ({ getService }: FtrProviderContext): void => { }, }); - const userActions = await getAllUserAction(supertest, postedCase.id); + const userActions = await getCaseUserActions({ supertest, caseID: postedCase.id }); const statusUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); @@ -149,7 +149,7 @@ export default ({ getService }: FtrProviderContext): void => { }, }); - const userActions = await getAllUserAction(supertest, postedCase.id); + const userActions = await getCaseUserActions({ supertest, caseID: postedCase.id }); const statusUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); 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 91fb03604b3c4b..50294201f6fbee 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 @@ -22,7 +22,7 @@ import { createCase, removeServerGeneratedPropertiesFromCase, removeServerGeneratedPropertiesFromUserAction, - getAllUserAction, + getCaseUserActions, } from '../../../../common/lib/utils'; import { secOnly, @@ -106,7 +106,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should create a user action when creating a case', async () => { const postedCase = await createCase(supertest, getPostCaseRequest()); - const userActions = await getAllUserAction(supertest, postedCase.id); + const userActions = await getCaseUserActions({ supertest, caseID: postedCase.id }); const creationUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[0]); const { new_value, ...rest } = creationUserAction as CaseUserActionResponse; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts index f58dfa1522d4a8..7a17cf1dd8e081 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts @@ -15,6 +15,7 @@ import { updateCase, getAllCasesStatuses, deleteAllCaseItems, + superUserSpace1Auth, } from '../../../../../common/lib/utils'; import { globalRead, @@ -25,7 +26,6 @@ import { secOnlyRead, superUser, } from '../../../../../common/lib/authentication/users'; -import { superUserSpace1Auth } from '../../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts index 73b85ef97d1194..b7b97557dcd250 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts @@ -22,6 +22,7 @@ import { createComment, deleteComment, deleteAllComments, + superUserSpace1Auth, } from '../../../../common/lib/utils'; import { globalRead, @@ -33,7 +34,6 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; -import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts index 0f73b1ee7a624a..2ec99d039dd000 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts @@ -28,6 +28,7 @@ import { ensureSavedObjectIsAuthorized, getSpaceUrlPrefix, createCase, + superUserSpace1Auth, } from '../../../../common/lib/utils'; import { @@ -40,7 +41,6 @@ import { globalRead, obsSecRead, } from '../../../../common/lib/authentication/users'; -import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts index 361e72bdc79bf6..25df715b43e9a5 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts @@ -18,6 +18,7 @@ import { createCase, createComment, getAllComments, + superUserSpace1Auth, } from '../../../../common/lib/utils'; import { CommentType } from '../../../../../../plugins/cases/common/api'; import { @@ -31,7 +32,6 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; -import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts index 98b6cc5a7a30c5..5b606e06e84dfc 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts @@ -17,6 +17,7 @@ import { createCase, createComment, getComment, + superUserSpace1Auth, } from '../../../../common/lib/utils'; import { CommentType } from '../../../../../../plugins/cases/common/api'; import { @@ -30,7 +31,6 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; -import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts index c1f37d5eb2f057..b00a0382bc7122 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts @@ -34,6 +34,7 @@ import { createCase, createComment, updateComment, + superUserSpace1Auth, } from '../../../../common/lib/utils'; import { globalRead, @@ -45,7 +46,6 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; -import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts index 1fcb49ec10ad4e..a1f24de1b87daf 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts @@ -36,9 +36,10 @@ import { deleteComments, createCase, createComment, - getAllUserAction, + getCaseUserActions, removeServerGeneratedPropertiesFromUserAction, removeServerGeneratedPropertiesFromSavedObject, + superUserSpace1Auth, } from '../../../../common/lib/utils'; import { createSignalsIndex, @@ -61,7 +62,6 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; -import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -140,7 +140,7 @@ export default ({ getService }: FtrProviderContext): void => { caseId: postedCase.id, params: postCommentUserReq, }); - const userActions = await getAllUserAction(supertest, postedCase.id); + const userActions = await getCaseUserActions({ supertest, caseID: postedCase.id }); const commentUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); expect(commentUserAction).to.eql({ diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/get_connectors.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/get_connectors.ts index 5156b9537583f3..46f712ff84aa32 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/get_connectors.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/get_connectors.ts @@ -16,7 +16,7 @@ export default ({ getService }: FtrProviderContext): void => { describe('get_connectors', () => { it('should return an empty find body correctly if no connectors are loaded', async () => { - const connectors = await getCaseConnectors(supertest); + const connectors = await getCaseConnectors({ supertest }); expect(connectors).to.eql([]); }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts index 5cd4082bd3293b..35ebb1a4bf7b19 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts @@ -25,6 +25,7 @@ import { createCase, updateCase, getCaseUserActions, + superUserSpace1Auth, } from '../../../../common/lib/utils'; import { globalRead, @@ -35,7 +36,6 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; -import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts index 3c096cb7557c32..8a58c59718feb6 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts @@ -8,8 +8,6 @@ /* eslint-disable @typescript-eslint/naming-convention */ import expect from '@kbn/expect'; -import * as st from 'supertest'; -import supertestAsPromised from 'supertest-as-promised'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; @@ -21,30 +19,21 @@ import { } from '../../../../common/lib/mock'; import { getConfigurationRequest, - getServiceNowConnector, - createConnector, - createConfiguration, createCase, pushCase, createComment, - CreateConnectorResponse, updateCase, - getAllUserAction, + getCaseUserActions, removeServerGeneratedPropertiesFromUserAction, deleteAllCaseItems, + superUserSpace1Auth, + createCaseWithConnector, } from '../../../../common/lib/utils'; import { ExternalServiceSimulator, getExternalServiceSimulatorPath, } from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; -import { - CaseConnector, - CasePostRequest, - CaseResponse, - CaseStatuses, - CaseUserActionResponse, - ConnectorTypes, -} from '../../../../../../plugins/cases/common/api'; +import { CaseStatuses, CaseUserActionResponse } from '../../../../../../plugins/cases/common/api'; import { globalRead, noKibanaPrivileges, @@ -54,8 +43,6 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; -import { User } from '../../../../common/lib/authentication/types'; -import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -78,70 +65,12 @@ export default ({ getService }: FtrProviderContext): void => { await actionsRemover.removeAll(); }); - const createCaseWithConnector = async ({ - testAgent = supertest, - configureReq = {}, - auth = { user: superUser, space: null }, - createCaseReq = getPostCaseRequest(), - }: { - testAgent?: st.SuperTest; - configureReq?: Record; - auth?: { user: User; space: string | null }; - createCaseReq?: CasePostRequest; - } = {}): Promise<{ - postedCase: CaseResponse; - connector: CreateConnectorResponse; - }> => { - const connector = await createConnector({ - supertest: testAgent, - req: { - ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, - }, - auth, - }); - - actionsRemover.add(auth.space ?? 'default', connector.id, 'action', 'actions'); - await createConfiguration( - testAgent, - { - ...getConfigurationRequest({ - id: connector.id, - name: connector.name, - type: connector.connector_type_id as ConnectorTypes, - }), - ...configureReq, - }, - 200, - auth - ); - - const postedCase = await createCase( - testAgent, - { - ...createCaseReq, - connector: { - id: connector.id, - name: connector.name, - type: connector.connector_type_id, - fields: { - urgency: '2', - impact: '2', - severity: '2', - category: 'software', - subcategory: 'os', - }, - } as CaseConnector, - }, - 200, - auth - ); - - return { postedCase, connector }; - }; - it('should push a case', async () => { - const { postedCase, connector } = await createCaseWithConnector(); + const { postedCase, connector } = await createCaseWithConnector({ + supertest, + servicenowSimulatorURL, + actionsRemover, + }); const theCase = await pushCase({ supertest, caseId: postedCase.id, @@ -167,7 +96,11 @@ export default ({ getService }: FtrProviderContext): void => { }); it('pushes a comment appropriately', async () => { - const { postedCase, connector } = await createCaseWithConnector(); + const { postedCase, connector } = await createCaseWithConnector({ + supertest, + servicenowSimulatorURL, + actionsRemover, + }); await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq }); const theCase = await pushCase({ supertest, @@ -183,6 +116,9 @@ export default ({ getService }: FtrProviderContext): void => { configureReq: { closure_type: 'close-by-pushing', }, + supertest, + servicenowSimulatorURL, + actionsRemover, }); const theCase = await pushCase({ supertest, @@ -194,13 +130,17 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should create the correct user action', async () => { - const { postedCase, connector } = await createCaseWithConnector(); + const { postedCase, connector } = await createCaseWithConnector({ + supertest, + servicenowSimulatorURL, + actionsRemover, + }); const pushedCase = await pushCase({ supertest, caseId: postedCase.id, connectorId: connector.id, }); - const userActions = await getAllUserAction(supertest, pushedCase.id); + const userActions = await getCaseUserActions({ supertest, caseID: pushedCase.id }); const pushUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); const { new_value, ...rest } = pushUserAction as CaseUserActionResponse; @@ -231,6 +171,9 @@ export default ({ getService }: FtrProviderContext): void => { // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests it.skip('should push a collection case but not close it when closure_type: close-by-pushing', async () => { const { postedCase, connector } = await createCaseWithConnector({ + supertest, + servicenowSimulatorURL, + actionsRemover, configureReq: { closure_type: 'close-by-pushing', }, @@ -267,7 +210,11 @@ export default ({ getService }: FtrProviderContext): void => { }); it('unhappy path = 409s when case is closed', async () => { - const { postedCase, connector } = await createCaseWithConnector(); + const { postedCase, connector } = await createCaseWithConnector({ + supertest, + servicenowSimulatorURL, + actionsRemover, + }); await updateCase({ supertest, params: { @@ -294,7 +241,9 @@ export default ({ getService }: FtrProviderContext): void => { it('should push a case that the user has permissions for', async () => { const { postedCase, connector } = await createCaseWithConnector({ - testAgent: supertestWithoutAuth, + supertest, + servicenowSimulatorURL, + actionsRemover, auth: superUserSpace1Auth, }); @@ -308,7 +257,9 @@ export default ({ getService }: FtrProviderContext): void => { it('should not push a case that the user does not have permissions for', async () => { const { postedCase, connector } = await createCaseWithConnector({ - testAgent: supertestWithoutAuth, + supertest, + servicenowSimulatorURL, + actionsRemover, auth: superUserSpace1Auth, createCaseReq: getPostCaseRequest({ owner: 'observabilityFixture' }), }); @@ -327,7 +278,9 @@ export default ({ getService }: FtrProviderContext): void => { user.username } with role(s) ${user.roles.join()} - should NOT push a case`, async () => { const { postedCase, connector } = await createCaseWithConnector({ - testAgent: supertestWithoutAuth, + supertest, + servicenowSimulatorURL, + actionsRemover, auth: superUserSpace1Auth, }); @@ -343,7 +296,9 @@ export default ({ getService }: FtrProviderContext): void => { it('should not push a case in a space that the user does not have permissions for', async () => { const { postedCase, connector } = await createCaseWithConnector({ - testAgent: supertestWithoutAuth, + supertest, + servicenowSimulatorURL, + actionsRemover, auth: { user: superUser, space: 'space2' }, }); diff --git a/x-pack/test/case_api_integration/spaces_only/config.ts b/x-pack/test/case_api_integration/spaces_only/config.ts index 310830a220fb84..53cfdb6f9285db 100644 --- a/x-pack/test/case_api_integration/spaces_only/config.ts +++ b/x-pack/test/case_api_integration/spaces_only/config.ts @@ -10,6 +10,7 @@ import { createTestConfig } from '../common/config'; // eslint-disable-next-line import/no-default-export export default createTestConfig('spaces_only', { disabledPlugins: ['security'], - license: 'basic', - ssl: true, + license: 'trial', + ssl: false, + testFiles: [require.resolve('./tests/trial')], }); diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/alerts/get_cases.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/alerts/get_cases.ts new file mode 100644 index 00000000000000..9587502fb642ce --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/alerts/get_cases.ts @@ -0,0 +1,110 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { getPostCaseRequest, postCommentAlertReq } from '../../../../common/lib/mock'; +import { + createCase, + createComment, + getCaseIDsByAlert, + deleteAllCaseItems, + getAuthWithSuperUser, +} from '../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); + const authSpace2 = getAuthWithSuperUser('space2'); + + describe('get_cases using alertID', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should return all cases with the same alert ID attached to them in space1', async () => { + const [case1, case2, case3] = await Promise.all([ + createCase(supertest, getPostCaseRequest(), 200, authSpace1), + createCase(supertest, getPostCaseRequest(), 200, authSpace1), + createCase(supertest, getPostCaseRequest(), 200, authSpace1), + ]); + + await Promise.all([ + createComment({ + supertest, + caseId: case1.id, + params: postCommentAlertReq, + auth: authSpace1, + }), + createComment({ + supertest, + caseId: case2.id, + params: postCommentAlertReq, + auth: authSpace1, + }), + createComment({ + supertest, + caseId: case3.id, + params: postCommentAlertReq, + auth: authSpace1, + }), + ]); + + const caseIDsWithAlert = await getCaseIDsByAlert({ + supertest, + alertID: 'test-id', + auth: authSpace1, + }); + + expect(caseIDsWithAlert.length).to.eql(3); + expect(caseIDsWithAlert).to.contain(case1.id); + expect(caseIDsWithAlert).to.contain(case2.id); + expect(caseIDsWithAlert).to.contain(case3.id); + }); + + it('should return 1 case in space2 when 2 cases were created in space1 and 1 in space2', async () => { + const [case1, case2, case3] = await Promise.all([ + createCase(supertest, getPostCaseRequest(), 200, authSpace1), + createCase(supertest, getPostCaseRequest(), 200, authSpace1), + createCase(supertest, getPostCaseRequest(), 200, authSpace2), + ]); + + await Promise.all([ + createComment({ + supertest, + caseId: case1.id, + params: postCommentAlertReq, + auth: authSpace1, + }), + createComment({ + supertest, + caseId: case2.id, + params: postCommentAlertReq, + auth: authSpace1, + }), + createComment({ + supertest, + caseId: case3.id, + params: postCommentAlertReq, + auth: authSpace2, + }), + ]); + + const caseIDsWithAlert = await getCaseIDsByAlert({ + supertest, + alertID: 'test-id', + auth: authSpace2, + }); + + expect(caseIDsWithAlert.length).to.eql(1); + expect(caseIDsWithAlert).to.eql([case3.id]); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/delete_cases.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/delete_cases.ts new file mode 100644 index 00000000000000..9de57a1b7abe24 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/delete_cases.ts @@ -0,0 +1,53 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { getPostCaseRequest } from '../../../../common/lib/mock'; +import { + deleteAllCaseItems, + createCase, + deleteCases, + getCase, + getAuthWithSuperUser, +} from '../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); + + describe('delete_cases', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should delete a case in space1', async () => { + const postedCase = await createCase(supertest, getPostCaseRequest(), 200, authSpace1); + const body = await deleteCases({ supertest, caseIDs: [postedCase.id], auth: authSpace1 }); + + await getCase({ supertest, caseId: postedCase.id, expectedHttpCode: 404, auth: authSpace1 }); + expect(body).to.eql({}); + }); + + it('should not delete a case in a different space', async () => { + const postedCase = await createCase(supertest, getPostCaseRequest(), 200, authSpace1); + await deleteCases({ + supertest, + caseIDs: [postedCase.id], + auth: getAuthWithSuperUser('space2'), + expectedHttpCode: 404, + }); + + // the case should still be there + const caseInfo = await getCase({ supertest, caseId: postedCase.id, auth: authSpace1 }); + expect(caseInfo.id).to.eql(postedCase.id); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/find_cases.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/find_cases.ts new file mode 100644 index 00000000000000..6513fe25b28e9f --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/find_cases.ts @@ -0,0 +1,63 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { postCaseReq, findCasesResp } from '../../../../common/lib/mock'; +import { + deleteAllCaseItems, + findCases, + createCase, + getAuthWithSuperUser, +} from '../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); + + describe('find_cases', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should return 3 cases in space1', async () => { + const a = await createCase(supertest, postCaseReq, 200, authSpace1); + const b = await createCase(supertest, postCaseReq, 200, authSpace1); + const c = await createCase(supertest, postCaseReq, 200, authSpace1); + + const cases = await findCases({ supertest, auth: authSpace1 }); + + expect(cases).to.eql({ + ...findCasesResp, + total: 3, + cases: [a, b, c], + count_open_cases: 3, + }); + }); + + it('should return 1 case in space2 when 2 cases were created in space1 and 1 in space2', async () => { + const authSpace2 = getAuthWithSuperUser('space2'); + const [, , space2Case] = await Promise.all([ + createCase(supertest, postCaseReq, 200, authSpace1), + createCase(supertest, postCaseReq, 200, authSpace1), + createCase(supertest, postCaseReq, 200, authSpace2), + ]); + + const cases = await findCases({ supertest, auth: authSpace2 }); + + expect(cases).to.eql({ + ...findCasesResp, + total: 1, + cases: [space2Case], + count_open_cases: 1, + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/get_case.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/get_case.ts new file mode 100644 index 00000000000000..3ea6fac3772edb --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/get_case.ts @@ -0,0 +1,49 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { postCaseResp, getPostCaseRequest, nullUser } from '../../../../common/lib/mock'; +import { + deleteCasesByESQuery, + createCase, + getCase, + removeServerGeneratedPropertiesFromCase, + getAuthWithSuperUser, +} from '../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); + + describe('get_case', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + }); + + it('should return a case in space1', async () => { + const postedCase = await createCase(supertest, getPostCaseRequest(), 200, authSpace1); + const theCase = await getCase({ supertest, caseId: postedCase.id, auth: authSpace1 }); + + const data = removeServerGeneratedPropertiesFromCase(theCase); + expect(data).to.eql({ ...postCaseResp(), created_by: nullUser }); + }); + + it('should not return a case in the wrong space', async () => { + const postedCase = await createCase(supertest, getPostCaseRequest(), 200, authSpace1); + await getCase({ + supertest, + caseId: postedCase.id, + auth: getAuthWithSuperUser('space2'), + expectedHttpCode: 404, + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/patch_cases.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/patch_cases.ts new file mode 100644 index 00000000000000..361358dc40604f --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/patch_cases.ts @@ -0,0 +1,74 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { nullUser, postCaseReq, postCaseResp } from '../../../../common/lib/mock'; +import { + deleteAllCaseItems, + createCase, + updateCase, + removeServerGeneratedPropertiesFromCase, + getAuthWithSuperUser, +} from '../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); + + describe('patch_cases', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should patch a case in space1', async () => { + const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); + const patchedCases = await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', + }, + ], + }, + auth: authSpace1, + }); + + const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); + expect(data).to.eql({ + ...postCaseResp(), + title: 'new title', + updated_by: nullUser, + created_by: nullUser, + }); + }); + + it('should not patch a case in a different space', async () => { + const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', + }, + ], + }, + expectedHttpCode: 404, + auth: getAuthWithSuperUser('space2'), + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/post_case.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/post_case.ts new file mode 100644 index 00000000000000..1fc70b0f97f5d6 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/post_case.ts @@ -0,0 +1,64 @@ +/* + * 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 expect from '@kbn/expect'; + +import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; +import { getPostCaseRequest, nullUser, postCaseResp } from '../../../../common/lib/mock'; +import { + deleteCasesByESQuery, + createCase, + removeServerGeneratedPropertiesFromCase, + getAuthWithSuperUser, +} from '../../../../common/lib/utils'; + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); + + describe('post_case', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + }); + + it('should post a case in space1', async () => { + const postedCase = await createCase( + supertest, + getPostCaseRequest({ + connector: { + id: '123', + name: 'Jira', + type: ConnectorTypes.jira, + fields: { issueType: 'Task', priority: 'High', parent: null }, + }, + }), + 200, + authSpace1 + ); + const data = removeServerGeneratedPropertiesFromCase(postedCase); + + expect(data).to.eql({ + ...postCaseResp( + null, + getPostCaseRequest({ + connector: { + id: '123', + name: 'Jira', + type: ConnectorTypes.jira, + fields: { issueType: 'Task', priority: 'High', parent: null }, + }, + }) + ), + created_by: nullUser, + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/reporters/get_reporters.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/reporters/get_reporters.ts new file mode 100644 index 00000000000000..d3c3176f4649fa --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/reporters/get_reporters.ts @@ -0,0 +1,47 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +import { getPostCaseRequest } from '../../../../../common/lib/mock'; +import { + createCase, + deleteCasesByESQuery, + getAuthWithSuperUser, + getReporters, +} from '../../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); + const authSpace2 = getAuthWithSuperUser('space2'); + + describe('get_reporters', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + }); + + it('should not return reporters when security is disabled', async () => { + await Promise.all([ + createCase(supertest, getPostCaseRequest(), 200, authSpace2), + createCase(supertest, getPostCaseRequest(), 200, authSpace1), + ]); + + const reportersSpace1 = await getReporters({ supertest, auth: authSpace1 }); + const reportersSpace2 = await getReporters({ + supertest, + auth: authSpace2, + }); + + expect(reportersSpace1).to.eql([]); + expect(reportersSpace2).to.eql([]); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/status/get_status.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/status/get_status.ts new file mode 100644 index 00000000000000..7f2a774c28f397 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/status/get_status.ts @@ -0,0 +1,92 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +import { CaseStatuses } from '../../../../../../../plugins/cases/common/api'; +import { postCaseReq } from '../../../../../common/lib/mock'; +import { + createCase, + updateCase, + getAllCasesStatuses, + deleteAllCaseItems, + getAuthWithSuperUser, +} from '../../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); + const authSpace2 = getAuthWithSuperUser('space2'); + + describe('get_status', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should return case statuses in space1', async () => { + /** + * space1: + * open: 1 + * in progress: 1 + * closed: 0 + * space2: + * closed: 1 + */ + const [, inProgressCase, postedCase] = await Promise.all([ + createCase(supertest, postCaseReq, 200, authSpace1), + createCase(supertest, postCaseReq, 200, authSpace1), + createCase(supertest, postCaseReq, 200, authSpace2), + ]); + + await updateCase({ + supertest, + params: { + cases: [ + { + id: inProgressCase.id, + version: inProgressCase.version, + status: CaseStatuses['in-progress'], + }, + ], + }, + auth: authSpace1, + }); + + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses.closed, + }, + ], + }, + auth: authSpace2, + }); + + const statusesSpace1 = await getAllCasesStatuses({ supertest, auth: authSpace1 }); + const statusesSpace2 = await getAllCasesStatuses({ supertest, auth: authSpace2 }); + + expect(statusesSpace1).to.eql({ + count_open_cases: 1, + count_closed_cases: 0, + count_in_progress_cases: 1, + }); + + expect(statusesSpace2).to.eql({ + count_open_cases: 0, + count_closed_cases: 1, + count_in_progress_cases: 0, + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/tags/get_tags.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/tags/get_tags.ts new file mode 100644 index 00000000000000..630628a13b6b9b --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/tags/get_tags.ts @@ -0,0 +1,42 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +import { + deleteCasesByESQuery, + createCase, + getTags, + getAuthWithSuperUser, +} from '../../../../../common/lib/utils'; +import { getPostCaseRequest } from '../../../../../common/lib/mock'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); + const authSpace2 = getAuthWithSuperUser('space2'); + + describe('get_tags', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + }); + + it('should return case tags in space1', async () => { + await createCase(supertest, getPostCaseRequest(), 200, authSpace1); + await createCase(supertest, getPostCaseRequest({ tags: ['unique'] }), 200, authSpace2); + + const tagsSpace1 = await getTags({ supertest, auth: authSpace1 }); + const tagsSpace2 = await getTags({ supertest, auth: authSpace2 }); + + expect(tagsSpace1).to.eql(['defacement']); + expect(tagsSpace2).to.eql(['unique']); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/comments/delete_comment.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/delete_comment.ts new file mode 100644 index 00000000000000..7e5abeb7edc2f2 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/delete_comment.ts @@ -0,0 +1,72 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; +import { + deleteCasesByESQuery, + deleteCasesUserActions, + deleteComments, + createCase, + createComment, + deleteComment, + getAuthWithSuperUser, +} from '../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); + + describe('delete_comment', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + await deleteComments(es); + await deleteCasesUserActions(es); + }); + + it('should delete a comment from space1', async () => { + const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + auth: authSpace1, + }); + + const comment = await deleteComment({ + supertest, + caseId: postedCase.id, + commentId: patchedCase.comments![0].id, + auth: authSpace1, + }); + + expect(comment).to.eql({}); + }); + + it('should not delete a comment from a different space', async () => { + const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + auth: authSpace1, + }); + + await deleteComment({ + supertest, + caseId: postedCase.id, + commentId: patchedCase.comments![0].id, + expectedHttpCode: 404, + auth: getAuthWithSuperUser('space2'), + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/comments/find_comments.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/find_comments.ts new file mode 100644 index 00000000000000..4df4c560413e8c --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/find_comments.ts @@ -0,0 +1,82 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { getPostCaseRequest, postCommentUserReq } from '../../../../common/lib/mock'; +import { + createComment, + deleteCasesByESQuery, + deleteCasesUserActions, + deleteComments, + getSpaceUrlPrefix, + createCase, + getAuthWithSuperUser, +} from '../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); + + describe('find_comments', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + await deleteComments(es); + await deleteCasesUserActions(es); + }); + + it('should find all case comments in space1', async () => { + const caseInfo = await createCase(supertest, getPostCaseRequest(), 200, authSpace1); + await createComment({ + supertest, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: authSpace1, + }); + + const patchedCase = await createComment({ + supertest, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: authSpace1, + }); + + const { body: caseComments } = await supertest + .get(`${getSpaceUrlPrefix(authSpace1.space)}${CASES_URL}/${caseInfo.id}/comments/_find`) + .expect(200); + + expect(caseComments.comments).to.eql(patchedCase.comments); + }); + + it('should not find any case comments in space2', async () => { + const caseInfo = await createCase(supertest, getPostCaseRequest(), 200, authSpace1); + await createComment({ + supertest, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: authSpace1, + }); + + await createComment({ + supertest, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: authSpace1, + }); + + const { body: caseComments } = await supertest + .get(`${getSpaceUrlPrefix('space2')}${CASES_URL}/${caseInfo.id}/comments/_find`) + .expect(200); + + expect(caseComments.comments.length).to.eql(0); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/comments/get_all_comments.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/get_all_comments.ts new file mode 100644 index 00000000000000..ea3766b733cdcf --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/get_all_comments.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; +import { + deleteAllCaseItems, + createCase, + createComment, + getAllComments, + getAuthWithSuperUser, +} from '../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); + + describe('get_all_comments', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should get multiple comments for a single case in space1', async () => { + const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); + await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + auth: authSpace1, + }); + await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + auth: authSpace1, + }); + const comments = await getAllComments({ supertest, caseId: postedCase.id, auth: authSpace1 }); + + expect(comments.length).to.eql(2); + }); + + it('should not find any comments in space2', async () => { + const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); + await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + auth: authSpace1, + }); + await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + auth: authSpace1, + }); + const comments = await getAllComments({ + supertest, + caseId: postedCase.id, + auth: getAuthWithSuperUser('space2'), + }); + + expect(comments.length).to.eql(0); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/comments/get_comment.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/get_comment.ts new file mode 100644 index 00000000000000..b53b2e6e59cfb4 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/get_comment.ts @@ -0,0 +1,66 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; +import { + deleteAllCaseItems, + createCase, + createComment, + getComment, + getAuthWithSuperUser, +} from '../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); + + describe('get_comment', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should get a comment in space1', async () => { + const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + auth: authSpace1, + }); + const comment = await getComment({ + supertest, + caseId: postedCase.id, + commentId: patchedCase.comments![0].id, + auth: authSpace1, + }); + + expect(comment).to.eql(patchedCase.comments![0]); + }); + + it('should not get a comment in space2', async () => { + const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + auth: authSpace1, + }); + await getComment({ + supertest, + caseId: postedCase.id, + commentId: patchedCase.comments![0].id, + auth: getAuthWithSuperUser('space2'), + expectedHttpCode: 404, + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/comments/patch_comment.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/patch_comment.ts new file mode 100644 index 00000000000000..452d05c9c23628 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/patch_comment.ts @@ -0,0 +1,90 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { AttributesTypeUser, CommentType } from '../../../../../../plugins/cases/common/api'; +import { nullUser, postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; +import { + deleteCasesByESQuery, + deleteCasesUserActions, + deleteComments, + createCase, + createComment, + updateComment, + getAuthWithSuperUser, +} from '../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); + + describe('patch_comment', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + await deleteComments(es); + await deleteCasesUserActions(es); + }); + + it('should patch a comment in space1', async () => { + const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + auth: authSpace1, + }); + + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; + const updatedCase = await updateComment({ + supertest, + caseId: postedCase.id, + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + comment: newComment, + type: CommentType.user, + owner: 'securitySolutionFixture', + }, + auth: authSpace1, + }); + + const userComment = updatedCase.comments![0] as AttributesTypeUser; + expect(userComment.comment).to.eql(newComment); + expect(userComment.type).to.eql(CommentType.user); + expect(updatedCase.updated_by).to.eql(nullUser); + }); + + it('should not patch a comment in a different space', async () => { + const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + auth: authSpace1, + }); + + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; + await updateComment({ + supertest, + caseId: postedCase.id, + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + comment: newComment, + type: CommentType.user, + owner: 'securitySolutionFixture', + }, + auth: getAuthWithSuperUser('space2'), + expectedHttpCode: 404, + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/comments/post_comment.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/post_comment.ts new file mode 100644 index 00000000000000..45175e8dafb041 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/post_comment.ts @@ -0,0 +1,75 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { AttributesTypeUser } from '../../../../../../plugins/cases/common/api'; +import { nullUser, postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; +import { + deleteCasesByESQuery, + deleteCasesUserActions, + deleteComments, + createCase, + createComment, + removeServerGeneratedPropertiesFromSavedObject, + getAuthWithSuperUser, +} from '../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); + + describe('post_comment', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + await deleteComments(es); + await deleteCasesUserActions(es); + }); + + it('should post a comment in space1', async () => { + const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + auth: authSpace1, + }); + const comment = removeServerGeneratedPropertiesFromSavedObject( + patchedCase.comments![0] as AttributesTypeUser + ); + + expect(comment).to.eql({ + type: postCommentUserReq.type, + comment: postCommentUserReq.comment, + associationType: 'case', + created_by: nullUser, + pushed_at: null, + pushed_by: null, + updated_by: null, + owner: 'securitySolutionFixture', + }); + + // updates the case correctly after adding a comment + expect(patchedCase.totalComment).to.eql(patchedCase.comments!.length); + expect(patchedCase.updated_by).to.eql(nullUser); + }); + + it('should not post a comment on a case in a different space', async () => { + const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); + await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + auth: getAuthWithSuperUser('space2'), + expectedHttpCode: 404, + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/configure/get_configure.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/configure/get_configure.ts new file mode 100644 index 00000000000000..573b96d71af4ad --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/configure/get_configure.ts @@ -0,0 +1,51 @@ +/* + * 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 expect from '@kbn/expect'; +import { nullUser } from '../../../../common/lib/mock'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { + removeServerGeneratedPropertiesFromSavedObject, + getConfigurationOutput, + deleteConfiguration, + getConfiguration, + createConfiguration, + getConfigurationRequest, + getAuthWithSuperUser, +} from '../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); + + describe('get_configure', () => { + afterEach(async () => { + await deleteConfiguration(es); + }); + + it('should return a configuration in space1', async () => { + await createConfiguration(supertest, getConfigurationRequest(), 200, authSpace1); + const configuration = await getConfiguration({ supertest, auth: authSpace1 }); + + const data = removeServerGeneratedPropertiesFromSavedObject(configuration[0]); + expect(data).to.eql(getConfigurationOutput(false, { created_by: nullUser })); + }); + + it('should not find a configuration when looking in a different space', async () => { + await createConfiguration(supertest, getConfigurationRequest(), 200, authSpace1); + const configuration = await getConfiguration({ + supertest, + auth: getAuthWithSuperUser('space2'), + }); + + expect(configuration).to.eql([]); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/configure/patch_configure.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/configure/patch_configure.ts new file mode 100644 index 00000000000000..f61e8698c11915 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/configure/patch_configure.ts @@ -0,0 +1,77 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { + getConfigurationRequest, + removeServerGeneratedPropertiesFromSavedObject, + getConfigurationOutput, + deleteConfiguration, + createConfiguration, + updateConfiguration, + getAuthWithSuperUser, +} from '../../../../common/lib/utils'; +import { nullUser } from '../../../../common/lib/mock'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); + + describe('patch_configure', () => { + afterEach(async () => { + await deleteConfiguration(es); + }); + + it('should patch a configuration in space1', async () => { + const configuration = await createConfiguration( + supertest, + getConfigurationRequest(), + 200, + authSpace1 + ); + const newConfiguration = await updateConfiguration( + supertest, + configuration.id, + { + closure_type: 'close-by-pushing', + version: configuration.version, + }, + 200, + authSpace1 + ); + + const data = removeServerGeneratedPropertiesFromSavedObject(newConfiguration); + expect(data).to.eql({ + ...getConfigurationOutput(false, { created_by: nullUser, updated_by: nullUser }), + closure_type: 'close-by-pushing', + }); + }); + + it('should not patch a configuration in a different space', async () => { + const configuration = await createConfiguration( + supertest, + getConfigurationRequest(), + 200, + authSpace1 + ); + await updateConfiguration( + supertest, + configuration.id, + { + closure_type: 'close-by-pushing', + version: configuration.version, + }, + 404, + getAuthWithSuperUser('space2') + ); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/configure/post_configure.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/configure/post_configure.ts new file mode 100644 index 00000000000000..161075616925cb --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/configure/post_configure.ts @@ -0,0 +1,44 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { + getConfigurationRequest, + removeServerGeneratedPropertiesFromSavedObject, + getConfigurationOutput, + deleteConfiguration, + createConfiguration, + getAuthWithSuperUser, +} from '../../../../common/lib/utils'; +import { nullUser } from '../../../../common/lib/mock'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); + + describe('post_configure', () => { + afterEach(async () => { + await deleteConfiguration(es); + }); + + it('should create a configuration in space1', async () => { + const configuration = await createConfiguration( + supertest, + getConfigurationRequest(), + 200, + authSpace1 + ); + + const data = removeServerGeneratedPropertiesFromSavedObject(configuration); + expect(data).to.eql(getConfigurationOutput(false, { created_by: nullUser })); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/index.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/index.ts new file mode 100644 index 00000000000000..251a545f106818 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/index.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('Common', function () { + loadTestFile(require.resolve('./alerts/get_cases')); + loadTestFile(require.resolve('./comments/delete_comment')); + loadTestFile(require.resolve('./comments/find_comments')); + loadTestFile(require.resolve('./comments/get_comment')); + loadTestFile(require.resolve('./comments/get_all_comments')); + loadTestFile(require.resolve('./comments/patch_comment')); + loadTestFile(require.resolve('./comments/post_comment')); + loadTestFile(require.resolve('./cases/delete_cases')); + loadTestFile(require.resolve('./cases/find_cases')); + loadTestFile(require.resolve('./cases/get_case')); + loadTestFile(require.resolve('./cases/patch_cases')); + loadTestFile(require.resolve('./cases/post_case')); + loadTestFile(require.resolve('./cases/reporters/get_reporters')); + loadTestFile(require.resolve('./cases/status/get_status')); + loadTestFile(require.resolve('./cases/tags/get_tags')); + loadTestFile(require.resolve('./user_actions/get_all_user_actions')); + loadTestFile(require.resolve('./configure/get_configure')); + loadTestFile(require.resolve('./configure/patch_configure')); + loadTestFile(require.resolve('./configure/post_configure')); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/user_actions/get_all_user_actions.ts new file mode 100644 index 00000000000000..199e53ebd1bb55 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/user_actions/get_all_user_actions.ts @@ -0,0 +1,48 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { getPostCaseRequest } from '../../../../common/lib/mock'; +import { + deleteAllCaseItems, + createCase, + getCaseUserActions, + getAuthWithSuperUser, +} from '../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); + + describe('get_all_user_actions', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it(`should get user actions in space1`, async () => { + const postedCase = await createCase(supertest, getPostCaseRequest(), 200, authSpace1); + const body = await getCaseUserActions({ supertest, caseID: postedCase.id, auth: authSpace1 }); + + expect(body.length).to.eql(1); + }); + + it(`should not get user actions in the wrong space`, async () => { + const postedCase = await createCase(supertest, getPostCaseRequest(), 200, authSpace1); + const body = await getCaseUserActions({ + supertest, + caseID: postedCase.id, + auth: getAuthWithSuperUser('space2'), + }); + + expect(body.length).to.eql(0); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/trial/cases/push_case.ts b/x-pack/test/case_api_integration/spaces_only/tests/trial/cases/push_case.ts new file mode 100644 index 00000000000000..28b7fe60955074 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/trial/cases/push_case.ts @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; + +import { nullUser } from '../../../../common/lib/mock'; +import { + pushCase, + deleteAllCaseItems, + createCaseWithConnector, + getAuthWithSuperUser, +} from '../../../../common/lib/utils'; +import { + ExternalServiceSimulator, + getExternalServiceSimulatorPath, +} from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); + const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); + + describe('push_case', () => { + const actionsRemover = new ActionsRemover(supertest); + + let servicenowSimulatorURL: string = ''; + before(() => { + servicenowSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) + ); + }); + + afterEach(async () => { + await deleteAllCaseItems(es); + await actionsRemover.removeAll(); + }); + + it('should push a case in space1', async () => { + const { postedCase, connector } = await createCaseWithConnector({ + supertest, + servicenowSimulatorURL, + actionsRemover, + auth: authSpace1, + }); + const theCase = await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: connector.id, + auth: authSpace1, + }); + + const { pushed_at, external_url, ...rest } = theCase.external_service!; + + expect(rest).to.eql({ + pushed_by: nullUser, + connector_id: connector.id, + connector_name: connector.name, + external_id: '123', + external_title: 'INC01', + }); + + // external_url is of the form http://elastic:changeme@localhost:5620 which is different between various environments like Jekins + expect( + external_url.includes( + 'api/_actions-FTS-external-service-simulators/servicenow/nav_to.do?uri=incident.do?sys_id=123' + ) + ).to.equal(true); + }); + + it('should not push a case in a different space', async () => { + const { postedCase, connector } = await createCaseWithConnector({ + supertest, + servicenowSimulatorURL, + actionsRemover, + auth: authSpace1, + }); + await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: connector.id, + auth: getAuthWithSuperUser('space2'), + expectedHttpCode: 404, + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_configure.ts b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_configure.ts new file mode 100644 index 00000000000000..a142e6470ae933 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_configure.ts @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +import { + ExternalServiceSimulator, + getExternalServiceSimulatorPath, +} from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; + +import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; +import { + getServiceNowConnector, + createConnector, + createConfiguration, + getConfiguration, + getConfigurationRequest, + removeServerGeneratedPropertiesFromSavedObject, + getConfigurationOutput, + getAuthWithSuperUser, +} from '../../../../common/lib/utils'; +import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; +import { nullUser } from '../../../../common/lib/mock'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const actionsRemover = new ActionsRemover(supertest); + const kibanaServer = getService('kibanaServer'); + const authSpace1 = getAuthWithSuperUser(); + + describe('get_configure', () => { + let servicenowSimulatorURL: string = ''; + + before(() => { + servicenowSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) + ); + }); + + afterEach(async () => { + await actionsRemover.removeAll(); + }); + + it('should return a configuration with a mapping from space1', async () => { + const connector = await createConnector({ + supertest, + req: { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }, + auth: authSpace1, + }); + actionsRemover.add('space1', connector.id, 'action', 'actions'); + + await createConfiguration( + supertest, + getConfigurationRequest({ + id: connector.id, + name: connector.name, + type: connector.connector_type_id as ConnectorTypes, + }), + 200, + authSpace1 + ); + + const configuration = await getConfiguration({ supertest, auth: authSpace1 }); + + const data = removeServerGeneratedPropertiesFromSavedObject(configuration[0]); + 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, + }, + created_by: nullUser, + }) + ); + }); + + it('should not return a configuration with a mapping from a different space', async () => { + const connector = await createConnector({ + supertest, + req: { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }, + auth: authSpace1, + }); + actionsRemover.add('space1', connector.id, 'action', 'actions'); + + await createConfiguration( + supertest, + getConfigurationRequest({ + id: connector.id, + name: connector.name, + type: connector.connector_type_id as ConnectorTypes, + }), + 200, + authSpace1 + ); + + const configuration = await getConfiguration({ + supertest, + auth: getAuthWithSuperUser('space2'), + }); + + expect(configuration).to.eql([]); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_connectors.ts b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_connectors.ts new file mode 100644 index 00000000000000..66759a4dcb39ad --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_connectors.ts @@ -0,0 +1,179 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; +import { + getServiceNowConnector, + getJiraConnector, + getResilientConnector, + createConnector, + getServiceNowSIRConnector, + getAuthWithSuperUser, + getCaseConnectors, +} from '../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const actionsRemover = new ActionsRemover(supertest); + const authSpace1 = getAuthWithSuperUser(); + + describe('get_connectors', () => { + afterEach(async () => { + await actionsRemover.removeAll(); + }); + + it('should return the correct connectors in space1', async () => { + const snConnector = await createConnector({ + supertest, + req: getServiceNowConnector(), + auth: authSpace1, + }); + const emailConnector = await createConnector({ + supertest, + req: { + name: 'An email action', + connector_type_id: '.email', + config: { + service: '__json', + from: 'bob@example.com', + }, + secrets: { + user: 'bob', + password: 'supersecret', + }, + }, + auth: authSpace1, + }); + const jiraConnector = await createConnector({ + supertest, + req: getJiraConnector(), + auth: authSpace1, + }); + const resilientConnector = await createConnector({ + supertest, + req: getResilientConnector(), + auth: authSpace1, + }); + const sir = await createConnector({ + supertest, + req: getServiceNowSIRConnector(), + auth: authSpace1, + }); + + actionsRemover.add(authSpace1.space, sir.id, 'action', 'actions'); + actionsRemover.add(authSpace1.space, snConnector.id, 'action', 'actions'); + actionsRemover.add(authSpace1.space, emailConnector.id, 'action', 'actions'); + actionsRemover.add(authSpace1.space, jiraConnector.id, 'action', 'actions'); + actionsRemover.add(authSpace1.space, resilientConnector.id, 'action', 'actions'); + + const connectors = await getCaseConnectors({ supertest, auth: authSpace1 }); + + expect(connectors).to.eql([ + { + id: jiraConnector.id, + actionTypeId: '.jira', + name: 'Jira Connector', + config: { + apiUrl: 'http://some.non.existent.com', + projectKey: 'pkey', + }, + isPreconfigured: false, + isMissingSecrets: false, + referencedByCount: 0, + }, + { + id: resilientConnector.id, + actionTypeId: '.resilient', + name: 'Resilient Connector', + config: { + apiUrl: 'http://some.non.existent.com', + orgId: 'pkey', + }, + isPreconfigured: false, + isMissingSecrets: false, + referencedByCount: 0, + }, + { + id: snConnector.id, + actionTypeId: '.servicenow', + name: 'ServiceNow Connector', + config: { + apiUrl: 'http://some.non.existent.com', + }, + isPreconfigured: false, + isMissingSecrets: false, + referencedByCount: 0, + }, + { + id: sir.id, + actionTypeId: '.servicenow-sir', + name: 'ServiceNow Connector', + config: { apiUrl: 'http://some.non.existent.com' }, + isPreconfigured: false, + isMissingSecrets: false, + referencedByCount: 0, + }, + ]); + }); + + it('should not return any connectors when looking in the wrong space', async () => { + const snConnector = await createConnector({ + supertest, + req: getServiceNowConnector(), + auth: authSpace1, + }); + const emailConnector = await createConnector({ + supertest, + req: { + name: 'An email action', + connector_type_id: '.email', + config: { + service: '__json', + from: 'bob@example.com', + }, + secrets: { + user: 'bob', + password: 'supersecret', + }, + }, + auth: authSpace1, + }); + const jiraConnector = await createConnector({ + supertest, + req: getJiraConnector(), + auth: authSpace1, + }); + const resilientConnector = await createConnector({ + supertest, + req: getResilientConnector(), + auth: authSpace1, + }); + const sir = await createConnector({ + supertest, + req: getServiceNowSIRConnector(), + auth: authSpace1, + }); + + actionsRemover.add(authSpace1.space, sir.id, 'action', 'actions'); + actionsRemover.add(authSpace1.space, snConnector.id, 'action', 'actions'); + actionsRemover.add(authSpace1.space, emailConnector.id, 'action', 'actions'); + actionsRemover.add(authSpace1.space, jiraConnector.id, 'action', 'actions'); + actionsRemover.add(authSpace1.space, resilientConnector.id, 'action', 'actions'); + + const connectors = await getCaseConnectors({ + supertest, + auth: getAuthWithSuperUser('space2'), + }); + + expect(connectors).to.eql([]); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/index.ts b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/index.ts new file mode 100644 index 00000000000000..0c8c3931d15774 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('configuration tests', function () { + loadTestFile(require.resolve('./get_configure')); + loadTestFile(require.resolve('./get_connectors')); + loadTestFile(require.resolve('./patch_configure')); + loadTestFile(require.resolve('./post_configure')); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/patch_configure.ts b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/patch_configure.ts new file mode 100644 index 00000000000000..5015b9c638617b --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/patch_configure.ts @@ -0,0 +1,162 @@ +/* + * 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 expect from '@kbn/expect'; +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, + removeServerGeneratedPropertiesFromSavedObject, + getConfigurationOutput, + deleteConfiguration, + createConfiguration, + updateConfiguration, + getServiceNowConnector, + createConnector, + getAuthWithSuperUser, +} from '../../../../common/lib/utils'; +import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; +import { nullUser } from '../../../../common/lib/mock'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const kibanaServer = getService('kibanaServer'); + const authSpace1 = getAuthWithSuperUser(); + + describe('patch_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 patch a configuration connector and create mappings in space1', async () => { + const connector = await createConnector({ + supertest, + req: { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }, + auth: authSpace1, + }); + + actionsRemover.add(authSpace1.space, connector.id, 'action', 'actions'); + + // Configuration is created with no connector so the mappings are empty + const configuration = await createConfiguration( + supertest, + getConfigurationRequest(), + 200, + authSpace1 + ); + + // the update request doesn't accept the owner field + const { owner, ...reqWithoutOwner } = getConfigurationRequest({ + id: connector.id, + name: connector.name, + type: connector.connector_type_id as ConnectorTypes, + fields: null, + }); + + const newConfiguration = await updateConfiguration( + supertest, + configuration.id, + { + ...reqWithoutOwner, + version: configuration.version, + }, + 200, + authSpace1 + ); + + const data = removeServerGeneratedPropertiesFromSavedObject(newConfiguration); + expect(data).to.eql({ + ...getConfigurationOutput(true), + created_by: nullUser, + updated_by: nullUser, + connector: { + id: connector.id, + name: connector.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 connector when it is in a different space', async () => { + const connector = await createConnector({ + supertest, + req: { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }, + auth: authSpace1, + }); + + actionsRemover.add(authSpace1.space, connector.id, 'action', 'actions'); + + // Configuration is created with no connector so the mappings are empty + const configuration = await createConfiguration( + supertest, + getConfigurationRequest(), + 200, + authSpace1 + ); + + // the update request doesn't accept the owner field + const { owner, ...reqWithoutOwner } = getConfigurationRequest({ + id: connector.id, + name: connector.name, + type: connector.connector_type_id as ConnectorTypes, + fields: null, + }); + + await updateConfiguration( + supertest, + configuration.id, + { + ...reqWithoutOwner, + version: configuration.version, + }, + 404, + getAuthWithSuperUser('space2') + ); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/post_configure.ts b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/post_configure.ts new file mode 100644 index 00000000000000..d67ca29229dd10 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/post_configure.ts @@ -0,0 +1,105 @@ +/* + * 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 expect from '@kbn/expect'; +import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; +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, + removeServerGeneratedPropertiesFromSavedObject, + getConfigurationOutput, + deleteConfiguration, + createConfiguration, + createConnector, + getServiceNowConnector, + getAuthWithSuperUser, +} from '../../../../common/lib/utils'; +import { nullUser } from '../../../../common/lib/mock'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const kibanaServer = getService('kibanaServer'); + const authSpace1 = getAuthWithSuperUser(); + + 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 with a mapping in space1', async () => { + const connector = await createConnector({ + supertest, + req: { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }, + auth: authSpace1, + }); + + actionsRemover.add(authSpace1.space, connector.id, 'action', 'actions'); + + const postRes = await createConfiguration( + supertest, + getConfigurationRequest({ + id: connector.id, + name: connector.name, + type: connector.connector_type_id as ConnectorTypes, + }), + 200, + authSpace1 + ); + + const data = removeServerGeneratedPropertiesFromSavedObject(postRes); + expect(data).to.eql( + getConfigurationOutput(false, { + created_by: nullUser, + 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, + }, + }) + ); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/index.ts b/x-pack/test/case_api_integration/spaces_only/tests/trial/index.ts similarity index 61% rename from x-pack/test/case_api_integration/spaces_only/tests/index.ts rename to x-pack/test/case_api_integration/spaces_only/tests/trial/index.ts index d35743ea0c7d97..346640aa6b9de1 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/index.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/trial/index.ts @@ -5,12 +5,12 @@ * 2.0. */ -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { createSpaces, deleteSpaces } from '../../common/lib/authentication'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { createSpaces, deleteSpaces } from '../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ loadTestFile, getService }: FtrProviderContext): void => { - describe('cases spaces only enabled', function () { + describe('cases spaces only enabled: trial', function () { // Fastest ciGroup for the moment. this.tags('ciGroup5'); @@ -21,5 +21,10 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => { after(async () => { await deleteSpaces(getService); }); + + loadTestFile(require.resolve('../common')); + + loadTestFile(require.resolve('./cases/push_case')); + loadTestFile(require.resolve('./configure')); }); }; From 3fd893fb3a4a4dad86568f522924026d8f74d58f Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Tue, 11 May 2021 13:23:00 -0400 Subject: [PATCH 054/113] [Cases] Add security only tests (#99679) * Starting spaces tests * Finishing space only tests * Refactoring createCaseWithConnector * Fixing spelling * Addressing PR feedback and creating alert tests * Fixing mocks * Starting security only tests * Adding remainder security only tests * Using helper objects * Fixing type error for null space * Renaming utility variables * Refactoring users and roles for security only tests --- x-pack/scripts/functional_tests.js | 1 + .../common/lib/authentication/index.ts | 25 +- .../common/lib/authentication/roles.ts | 114 +++++++ .../common/lib/authentication/users.ts | 59 ++++ .../case_api_integration/common/lib/utils.ts | 11 +- .../tests/common/cases/delete_cases.ts | 2 +- .../security_only/config.ts | 16 + .../tests/common/alerts/get_cases.ts | 242 +++++++++++++++ .../tests/common/cases/delete_cases.ts | 157 ++++++++++ .../tests/common/cases/find_cases.ts | 245 +++++++++++++++ .../tests/common/cases/get_case.ts | 144 +++++++++ .../tests/common/cases/patch_cases.ts | 243 +++++++++++++++ .../tests/common/cases/post_case.ts | 83 ++++++ .../common/cases/reporters/get_reporters.ts | 155 ++++++++++ .../tests/common/cases/status/get_status.ts | 131 +++++++++ .../tests/common/cases/tags/get_tags.ts | 170 +++++++++++ .../tests/common/comments/delete_comment.ts | 236 +++++++++++++++ .../tests/common/comments/find_comments.ts | 278 ++++++++++++++++++ .../tests/common/comments/get_all_comments.ts | 139 +++++++++ .../tests/common/comments/get_comment.ts | 123 ++++++++ .../tests/common/comments/patch_comment.ts | 189 ++++++++++++ .../tests/common/comments/post_comment.ts | 128 ++++++++ .../tests/common/configure/get_configure.ts | 195 ++++++++++++ .../tests/common/configure/patch_configure.ts | 140 +++++++++ .../tests/common/configure/post_configure.ts | 133 +++++++++ .../security_only/tests/common/index.ts | 33 +++ .../user_actions/get_all_user_actions.ts | 104 +++++++ .../tests/trial/cases/push_case.ts | 128 ++++++++ .../security_only/tests/trial/index.ts | 34 +++ .../security_only/utils.ts | 18 ++ .../tests/trial/configure/get_connectors.ts | 22 +- .../tests/trial/configure/patch_configure.ts | 6 +- .../tests/trial/configure/post_configure.ts | 4 +- 33 files changed, 3687 insertions(+), 21 deletions(-) create mode 100644 x-pack/test/case_api_integration/security_only/config.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/cases/delete_cases.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/cases/find_cases.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/cases/get_case.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/cases/patch_cases.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/cases/post_case.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/cases/status/get_status.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/cases/tags/get_tags.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/comments/delete_comment.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/comments/find_comments.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/comments/get_all_comments.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/comments/get_comment.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/comments/patch_comment.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/comments/post_comment.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/configure/get_configure.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/configure/patch_configure.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/configure/post_configure.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/index.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/user_actions/get_all_user_actions.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/trial/cases/push_case.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/trial/index.ts create mode 100644 x-pack/test/case_api_integration/security_only/utils.ts diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index e2b3c951b07222..1a7f9acc9f1a32 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -37,6 +37,7 @@ const onlyNotInCoverageTests = [ require.resolve('../test/case_api_integration/security_and_spaces/config_basic.ts'), require.resolve('../test/case_api_integration/security_and_spaces/config_trial.ts'), require.resolve('../test/case_api_integration/spaces_only/config.ts'), + require.resolve('../test/case_api_integration/security_only/config.ts'), require.resolve('../test/apm_api_integration/basic/config.ts'), require.resolve('../test/apm_api_integration/trial/config.ts'), require.resolve('../test/apm_api_integration/rules/config.ts'), 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 a72141745e5777..86016b273ea44e 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 @@ -24,7 +24,15 @@ export const createSpaces = async (getService: CommonFtrProviderContext['getServ } }; -const createUsersAndRoles = async (getService: CommonFtrProviderContext['getService']) => { +/** + * Creates the users and roles for use in the tests. Defaults to specific users and roles used by the security_and_spaces + * scenarios but can be passed specific ones as well. + */ +export const createUsersAndRoles = async ( + getService: CommonFtrProviderContext['getService'], + usersToCreate: User[] = users, + rolesToCreate: Role[] = roles +) => { const security = getService('security'); const createRole = async ({ name, privileges }: Role) => { @@ -42,11 +50,11 @@ const createUsersAndRoles = async (getService: CommonFtrProviderContext['getServ }); }; - for (const role of roles) { + for (const role of rolesToCreate) { await createRole(role); } - for (const user of users) { + for (const user of usersToCreate) { await createUser(user); } }; @@ -61,10 +69,15 @@ export const deleteSpaces = async (getService: CommonFtrProviderContext['getServ } } }; -const deleteUsersAndRoles = async (getService: CommonFtrProviderContext['getService']) => { + +export const deleteUsersAndRoles = async ( + getService: CommonFtrProviderContext['getService'], + usersToDelete: User[] = users, + rolesToDelete: Role[] = roles +) => { const security = getService('security'); - for (const user of users) { + for (const user of usersToDelete) { try { await security.user.delete(user.username); } catch (error) { @@ -72,7 +85,7 @@ const deleteUsersAndRoles = async (getService: CommonFtrProviderContext['getServ } } - for (const role of roles) { + for (const role of rolesToDelete) { try { await security.role.delete(role.name); } catch (error) { diff --git a/x-pack/test/case_api_integration/common/lib/authentication/roles.ts b/x-pack/test/case_api_integration/common/lib/authentication/roles.ts index 5ddecd92061065..60e50288f88568 100644 --- a/x-pack/test/case_api_integration/common/lib/authentication/roles.ts +++ b/x-pack/test/case_api_integration/common/lib/authentication/roles.ts @@ -150,3 +150,117 @@ export const roles = [ observabilityOnlyAll, observabilityOnlyRead, ]; + +/** + * These roles have access to all spaces. + */ + +export const securitySolutionOnlyAllSpacesAll: Role = { + name: 'sec_only_all', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + securitySolutionFixture: ['all'], + actions: ['all'], + actionsSimulators: ['all'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const securitySolutionOnlyReadSpacesAll: Role = { + name: 'sec_only_read', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + securitySolutionFixture: ['read'], + actions: ['read'], + actionsSimulators: ['read'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const observabilityOnlyAllSpacesAll: Role = { + name: 'obs_only_all', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + observabilityFixture: ['all'], + actions: ['all'], + actionsSimulators: ['all'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const observabilityOnlyReadSpacesAll: Role = { + name: 'obs_only_read', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + observabilityFixture: ['read'], + actions: ['read'], + actionsSimulators: ['read'], + }, + spaces: ['*'], + }, + ], + }, +}; + +/** + * These roles are specifically for the security_only tests where the spaces plugin is disabled. Most of the roles (except + * for noKibanaPrivileges) have spaces: ['*'] effectively giving it access to the default space since no other spaces + * will exist when the spaces plugin is disabled. + */ +export const rolesDefaultSpace = [ + noKibanaPrivileges, + globalRead, + securitySolutionOnlyAllSpacesAll, + securitySolutionOnlyReadSpacesAll, + observabilityOnlyAllSpacesAll, + observabilityOnlyReadSpacesAll, +]; diff --git a/x-pack/test/case_api_integration/common/lib/authentication/users.ts b/x-pack/test/case_api_integration/common/lib/authentication/users.ts index 06add9ae007933..1fa6e3c9f4990e 100644 --- a/x-pack/test/case_api_integration/common/lib/authentication/users.ts +++ b/x-pack/test/case_api_integration/common/lib/authentication/users.ts @@ -12,6 +12,10 @@ import { observabilityOnlyRead, globalRead as globalReadRole, noKibanaPrivileges as noKibanaPrivilegesRole, + securitySolutionOnlyAllSpacesAll, + securitySolutionOnlyReadSpacesAll, + observabilityOnlyAllSpacesAll, + observabilityOnlyReadSpacesAll, } from './roles'; import { User } from './types'; @@ -80,3 +84,58 @@ export const users = [ globalRead, noKibanaPrivileges, ]; + +/** + * These users will have access to all spaces. + */ + +export const secOnlySpacesAll: User = { + username: 'sec_only', + password: 'sec_only', + roles: [securitySolutionOnlyAllSpacesAll.name], +}; + +export const secOnlyReadSpacesAll: User = { + username: 'sec_only_read', + password: 'sec_only_read', + roles: [securitySolutionOnlyReadSpacesAll.name], +}; + +export const obsOnlySpacesAll: User = { + username: 'obs_only', + password: 'obs_only', + roles: [observabilityOnlyAllSpacesAll.name], +}; + +export const obsOnlyReadSpacesAll: User = { + username: 'obs_only_read', + password: 'obs_only_read', + roles: [observabilityOnlyReadSpacesAll.name], +}; + +export const obsSecSpacesAll: User = { + username: 'obs_sec', + password: 'obs_sec', + roles: [securitySolutionOnlyAllSpacesAll.name, observabilityOnlyAllSpacesAll.name], +}; + +export const obsSecReadSpacesAll: User = { + username: 'obs_sec_read', + password: 'obs_sec_read', + roles: [securitySolutionOnlyReadSpacesAll.name, observabilityOnlyReadSpacesAll.name], +}; + +/** + * These users are for the security_only tests because most of them have access to the default space instead of 'space1' + */ +export const usersDefaultSpace = [ + superUser, + secOnlySpacesAll, + secOnlyReadSpacesAll, + obsOnlySpacesAll, + obsOnlyReadSpacesAll, + obsSecSpacesAll, + obsSecReadSpacesAll, + globalRead, + noKibanaPrivileges, +]; 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 b7a713b6316cb8..855cf513f16d56 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -546,10 +546,19 @@ export const superUserSpace1Auth = getAuthWithSuperUser(); * Returns an auth object with the specified space and user set as super user. The result can be passed to other utility * functions. */ -export function getAuthWithSuperUser(space: string = 'space1'): { user: User; space: string } { +export function getAuthWithSuperUser( + space: string | null = 'space1' +): { user: User; space: string | null } { return { user: superUser, space }; } +/** + * Converts the space into the appropriate string for use by the actions remover utility object. + */ +export function getActionsSpace(space: string | null) { + return space ?? 'default'; +} + export const getSpaceUrlPrefix = (spaceId: string | undefined | null) => { return spaceId && spaceId !== 'default' ? `/s/${spaceId}` : ``; }; 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 03bcf0d538fe3b..bbb9624c4b14be 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 @@ -180,7 +180,7 @@ export default ({ getService }: FtrProviderContext): void => { ); await deleteCases({ - supertest, + supertest: supertestWithoutAuth, caseIDs: [postedCase.id], expectedHttpCode: 204, auth: { user: secOnly, space: 'space1' }, diff --git a/x-pack/test/case_api_integration/security_only/config.ts b/x-pack/test/case_api_integration/security_only/config.ts new file mode 100644 index 00000000000000..5946b8d25b4641 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/config.ts @@ -0,0 +1,16 @@ +/* + * 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 { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('security_only', { + disabledPlugins: ['spaces'], + license: 'trial', + ssl: true, + testFiles: [require.resolve('./tests/trial')], +}); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts new file mode 100644 index 00000000000000..9575bd99112f6b --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts @@ -0,0 +1,242 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { getPostCaseRequest, postCommentAlertReq } from '../../../../common/lib/mock'; +import { + createCase, + createComment, + getCaseIDsByAlert, + deleteAllCaseItems, +} from '../../../../common/lib/utils'; +import { + globalRead, + noKibanaPrivileges, + obsOnlyReadSpacesAll, + obsSecSpacesAll, + obsSecReadSpacesAll, + secOnlyReadSpacesAll, + superUser, +} from '../../../../common/lib/authentication/users'; +import { + obsOnlyDefaultSpaceAuth, + secOnlyDefaultSpaceAuth, + superUserDefaultSpaceAuth, + obsSecDefaultSpaceAuth, +} from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('get_cases using alertID', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + it('should return the correct case IDs', async () => { + const [case1, case2, case3] = await Promise.all([ + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyDefaultSpaceAuth), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyDefaultSpaceAuth), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + obsOnlyDefaultSpaceAuth + ), + ]); + + await Promise.all([ + createComment({ + supertest: supertestWithoutAuth, + caseId: case1.id, + params: postCommentAlertReq, + auth: secOnlyDefaultSpaceAuth, + }), + createComment({ + supertest: supertestWithoutAuth, + caseId: case2.id, + params: postCommentAlertReq, + auth: secOnlyDefaultSpaceAuth, + }), + createComment({ + supertest: supertestWithoutAuth, + caseId: case3.id, + params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, + auth: obsOnlyDefaultSpaceAuth, + }), + ]); + + for (const scenario of [ + { + user: globalRead, + caseIDs: [case1.id, case2.id, case3.id], + }, + { + user: superUser, + caseIDs: [case1.id, case2.id, case3.id], + }, + { user: secOnlyReadSpacesAll, caseIDs: [case1.id, case2.id] }, + { user: obsOnlyReadSpacesAll, caseIDs: [case3.id] }, + { + user: obsSecReadSpacesAll, + caseIDs: [case1.id, case2.id, case3.id], + }, + ]) { + const res = await getCaseIDsByAlert({ + supertest: supertestWithoutAuth, + // cast because the official type is string | string[] but the ids will always be a single value in the tests + alertID: postCommentAlertReq.alertId as string, + auth: { + user: scenario.user, + space: null, + }, + }); + expect(res.length).to.eql(scenario.caseIDs.length); + for (const caseID of scenario.caseIDs) { + expect(res).to.contain(caseID); + } + } + }); + + it(`User ${ + noKibanaPrivileges.username + } with role(s) ${noKibanaPrivileges.roles.join()} - should not get cases`, async () => { + const caseInfo = await createCase(supertest, getPostCaseRequest(), 200, { + user: superUser, + space: null, + }); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentAlertReq, + auth: superUserDefaultSpaceAuth, + }); + + await getCaseIDsByAlert({ + supertest: supertestWithoutAuth, + alertID: postCommentAlertReq.alertId as string, + auth: { user: noKibanaPrivileges, space: null }, + expectedHttpCode: 403, + }); + }); + + it('should return a 404 when attempting to access a space', async () => { + const [case1, case2] = await Promise.all([ + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, obsSecDefaultSpaceAuth), + createCase( + supertestWithoutAuth, + { ...getPostCaseRequest(), owner: 'observabilityFixture' }, + 200, + obsSecDefaultSpaceAuth + ), + ]); + + await Promise.all([ + createComment({ + supertest: supertestWithoutAuth, + caseId: case1.id, + params: postCommentAlertReq, + auth: obsSecDefaultSpaceAuth, + }), + createComment({ + supertest: supertestWithoutAuth, + caseId: case2.id, + params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, + auth: obsSecDefaultSpaceAuth, + }), + ]); + + await getCaseIDsByAlert({ + supertest: supertestWithoutAuth, + alertID: postCommentAlertReq.alertId as string, + auth: { user: obsSecSpacesAll, space: 'space1' }, + query: { owner: 'securitySolutionFixture' }, + expectedHttpCode: 404, + }); + }); + + it('should respect the owner filter when have permissions', async () => { + const [case1, case2] = await Promise.all([ + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, obsSecDefaultSpaceAuth), + createCase( + supertestWithoutAuth, + { ...getPostCaseRequest(), owner: 'observabilityFixture' }, + 200, + obsSecDefaultSpaceAuth + ), + ]); + + await Promise.all([ + createComment({ + supertest: supertestWithoutAuth, + caseId: case1.id, + params: postCommentAlertReq, + auth: obsSecDefaultSpaceAuth, + }), + createComment({ + supertest: supertestWithoutAuth, + caseId: case2.id, + params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, + auth: obsSecDefaultSpaceAuth, + }), + ]); + + const res = await getCaseIDsByAlert({ + supertest: supertestWithoutAuth, + alertID: postCommentAlertReq.alertId as string, + auth: obsSecDefaultSpaceAuth, + query: { owner: 'securitySolutionFixture' }, + }); + + expect(res).to.eql([case1.id]); + }); + + it('should return the correct case IDs when the owner query parameter contains unprivileged values', async () => { + const [case1, case2] = await Promise.all([ + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, obsSecDefaultSpaceAuth), + createCase( + supertestWithoutAuth, + { ...getPostCaseRequest(), owner: 'observabilityFixture' }, + 200, + obsSecDefaultSpaceAuth + ), + ]); + + await Promise.all([ + createComment({ + supertest: supertestWithoutAuth, + caseId: case1.id, + params: postCommentAlertReq, + auth: obsSecDefaultSpaceAuth, + }), + createComment({ + supertest: supertestWithoutAuth, + caseId: case2.id, + params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, + auth: obsSecDefaultSpaceAuth, + }), + ]); + + const res = await getCaseIDsByAlert({ + supertest: supertestWithoutAuth, + alertID: postCommentAlertReq.alertId as string, + auth: secOnlyDefaultSpaceAuth, + // The secOnlyDefaultSpace user does not have permissions for observability cases, so it should only return the security solution one + query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, + }); + + expect(res).to.eql([case1.id]); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/delete_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/delete_cases.ts new file mode 100644 index 00000000000000..9ece177b214914 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/delete_cases.ts @@ -0,0 +1,157 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { getPostCaseRequest } from '../../../../common/lib/mock'; +import { + deleteCasesByESQuery, + deleteCasesUserActions, + deleteComments, + createCase, + deleteCases, + getCase, +} from '../../../../common/lib/utils'; +import { + secOnlySpacesAll, + secOnlyReadSpacesAll, + globalRead, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + noKibanaPrivileges, +} from '../../../../common/lib/authentication/users'; +import { + obsOnlyDefaultSpaceAuth, + secOnlyDefaultSpaceAuth, + superUserDefaultSpaceAuth, +} from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const es = getService('es'); + + describe('delete_cases', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + await deleteComments(es); + await deleteCasesUserActions(es); + }); + + it('User: security solution only - should delete a case', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + secOnlyDefaultSpaceAuth + ); + + await deleteCases({ + supertest: supertestWithoutAuth, + caseIDs: [postedCase.id], + expectedHttpCode: 204, + auth: secOnlyDefaultSpaceAuth, + }); + }); + + it('User: security solution only - should NOT delete a case of different owner', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + secOnlyDefaultSpaceAuth + ); + + await deleteCases({ + supertest: supertestWithoutAuth, + caseIDs: [postedCase.id], + expectedHttpCode: 403, + auth: obsOnlyDefaultSpaceAuth, + }); + }); + + it('should get an error if the user has not permissions to all requested cases', async () => { + const caseSec = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + secOnlyDefaultSpaceAuth + ); + + const caseObs = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + obsOnlyDefaultSpaceAuth + ); + + await deleteCases({ + supertest: supertestWithoutAuth, + caseIDs: [caseSec.id, caseObs.id], + expectedHttpCode: 403, + auth: obsOnlyDefaultSpaceAuth, + }); + + // Cases should have not been deleted. + await getCase({ + supertest: supertestWithoutAuth, + caseId: caseSec.id, + expectedHttpCode: 200, + auth: superUserDefaultSpaceAuth, + }); + + await getCase({ + supertest: supertestWithoutAuth, + caseId: caseObs.id, + expectedHttpCode: 200, + auth: superUserDefaultSpaceAuth, + }); + }); + + for (const user of [ + globalRead, + secOnlyReadSpacesAll, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + noKibanaPrivileges, + ]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT delete a case`, async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserDefaultSpaceAuth + ); + + await deleteCases({ + supertest: supertestWithoutAuth, + caseIDs: [postedCase.id], + expectedHttpCode: 403, + auth: { user, space: null }, + }); + }); + } + + it('should return a 404 when attempting to access a space', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserDefaultSpaceAuth + ); + + await deleteCases({ + supertest: supertestWithoutAuth, + caseIDs: [postedCase.id], + expectedHttpCode: 404, + auth: { user: secOnlySpacesAll, space: 'space1' }, + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/find_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/find_cases.ts new file mode 100644 index 00000000000000..711eccbe162786 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/find_cases.ts @@ -0,0 +1,245 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { getPostCaseRequest } from '../../../../common/lib/mock'; +import { + deleteAllCaseItems, + ensureSavedObjectIsAuthorized, + findCases, + createCase, +} from '../../../../common/lib/utils'; +import { + secOnlySpacesAll, + obsOnlyReadSpacesAll, + secOnlyReadSpacesAll, + noKibanaPrivileges, + superUser, + globalRead, + obsSecReadSpacesAll, +} from '../../../../common/lib/authentication/users'; +import { + obsOnlyDefaultSpaceAuth, + obsSecDefaultSpaceAuth, + secOnlyDefaultSpaceAuth, + superUserDefaultSpaceAuth, +} from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('find_cases', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should return the correct cases', async () => { + await Promise.all([ + // Create case owned by the security solution user + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + secOnlyDefaultSpaceAuth + ), + // Create case owned by the observability user + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + obsOnlyDefaultSpaceAuth + ), + ]); + + for (const scenario of [ + { + user: globalRead, + numberOfExpectedCases: 2, + owners: ['securitySolutionFixture', 'observabilityFixture'], + }, + { + user: superUser, + numberOfExpectedCases: 2, + owners: ['securitySolutionFixture', 'observabilityFixture'], + }, + { + user: secOnlyReadSpacesAll, + numberOfExpectedCases: 1, + owners: ['securitySolutionFixture'], + }, + { + user: obsOnlyReadSpacesAll, + numberOfExpectedCases: 1, + owners: ['observabilityFixture'], + }, + { + user: obsSecReadSpacesAll, + numberOfExpectedCases: 2, + owners: ['securitySolutionFixture', 'observabilityFixture'], + }, + ]) { + const res = await findCases({ + supertest: supertestWithoutAuth, + auth: { + user: scenario.user, + space: null, + }, + }); + + ensureSavedObjectIsAuthorized(res.cases, scenario.numberOfExpectedCases, scenario.owners); + } + }); + + it(`User ${ + noKibanaPrivileges.username + } with role(s) ${noKibanaPrivileges.roles.join()} - should NOT read a case`, async () => { + await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserDefaultSpaceAuth); + + await findCases({ + supertest: supertestWithoutAuth, + auth: { + user: noKibanaPrivileges, + space: null, + }, + expectedHttpCode: 403, + }); + }); + + it('should return a 404 when attempting to access a space', async () => { + await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserDefaultSpaceAuth); + + await findCases({ + supertest: supertestWithoutAuth, + auth: { user: secOnlySpacesAll, space: 'space1' }, + expectedHttpCode: 404, + }); + }); + + 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 + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserDefaultSpaceAuth), + // super user creates a case with owner observabilityFixture + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + superUserDefaultSpaceAuth + ), + ]); + + const res = await findCases({ + supertest: supertestWithoutAuth, + query: { + search: 'securitySolutionFixture observabilityFixture', + searchFields: 'owner', + }, + auth: secOnlyDefaultSpaceAuth, + }); + + ensureSavedObjectIsAuthorized(res.cases, 1, ['securitySolutionFixture']); + }); + + // This test is to prevent a future developer to add the filter attribute without taking into consideration + // the authorizationFilter produced by the cases authorization class + it('should NOT allow to pass a filter query parameter', async () => { + await supertest + .get( + `${CASES_URL}/_find?sortOrder=asc&filter=cases.attributes.owner:"observabilityFixture"` + ) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + }); + + // This test ensures that the user is not allowed to define the namespaces query param + // so she cannot search across spaces + it('should NOT allow to pass a namespaces query parameter', async () => { + await supertest + .get(`${CASES_URL}/_find?sortOrder=asc&namespaces[0]=*`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + + await supertest + .get(`${CASES_URL}/_find?sortOrder=asc&namespaces=*`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + }); + + it('should NOT allow to pass a non supported query parameter', async () => { + await supertest + .get(`${CASES_URL}/_find?notExists=papa`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + }); + + it('should respect the owner filter when having permissions', async () => { + await Promise.all([ + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + obsSecDefaultSpaceAuth + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + obsOnlyDefaultSpaceAuth + ), + ]); + + const res = await findCases({ + supertest: supertestWithoutAuth, + query: { + owner: 'securitySolutionFixture', + searchFields: 'owner', + }, + auth: obsSecDefaultSpaceAuth, + }); + + ensureSavedObjectIsAuthorized(res.cases, 1, ['securitySolutionFixture']); + }); + + 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, + obsSecDefaultSpaceAuth + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + obsSecDefaultSpaceAuth + ), + ]); + + // User with permissions only to security solution request cases from observability + const res = await findCases({ + supertest: supertestWithoutAuth, + query: { + owner: ['securitySolutionFixture', 'observabilityFixture'], + }, + auth: secOnlyDefaultSpaceAuth, + }); + + // Only security solution cases are being returned + ensureSavedObjectIsAuthorized(res.cases, 1, ['securitySolutionFixture']); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/get_case.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/get_case.ts new file mode 100644 index 00000000000000..3bdb4c5ed310e3 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/get_case.ts @@ -0,0 +1,144 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { AttributesTypeUser } from '../../../../../../plugins/cases/common/api'; +import { postCommentUserReq, getPostCaseRequest } from '../../../../common/lib/mock'; +import { + deleteCasesByESQuery, + createCase, + getCase, + createComment, + removeServerGeneratedPropertiesFromSavedObject, +} from '../../../../common/lib/utils'; +import { + secOnlySpacesAll, + obsOnlySpacesAll, + globalRead, + superUser, + secOnlyReadSpacesAll, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + noKibanaPrivileges, + obsSecSpacesAll, +} from '../../../../common/lib/authentication/users'; +import { getUserInfo } from '../../../../common/lib/authentication'; +import { secOnlyDefaultSpaceAuth, superUserDefaultSpaceAuth } from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const es = getService('es'); + + describe('get_case', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + }); + + it('should get a case', async () => { + const newCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserDefaultSpaceAuth + ); + + for (const user of [ + globalRead, + superUser, + secOnlySpacesAll, + secOnlyReadSpacesAll, + obsSecSpacesAll, + obsSecReadSpacesAll, + ]) { + const theCase = await getCase({ + supertest: supertestWithoutAuth, + caseId: newCase.id, + auth: { user, space: null }, + }); + + expect(theCase.owner).to.eql('securitySolutionFixture'); + } + }); + + it('should get a case with comments', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + secOnlyDefaultSpaceAuth + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + expectedHttpCode: 200, + auth: secOnlyDefaultSpaceAuth, + }); + + const theCase = await getCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + includeComments: true, + auth: secOnlyDefaultSpaceAuth, + }); + + 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(secOnlySpacesAll), + pushed_at: null, + pushed_by: null, + updated_by: null, + owner: 'securitySolutionFixture', + }); + }); + + it('should not get a case when the user does not have access to owner', async () => { + const newCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserDefaultSpaceAuth + ); + + for (const user of [noKibanaPrivileges, obsOnlySpacesAll, obsOnlyReadSpacesAll]) { + await getCase({ + supertest: supertestWithoutAuth, + caseId: newCase.id, + expectedHttpCode: 403, + auth: { user, space: null }, + }); + } + }); + + it('should return a 404 when attempting to access a space', async () => { + const newCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserDefaultSpaceAuth + ); + + await getCase({ + supertest: supertestWithoutAuth, + caseId: newCase.id, + expectedHttpCode: 404, + auth: { user: secOnlySpacesAll, space: 'space1' }, + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/patch_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/patch_cases.ts new file mode 100644 index 00000000000000..bfab3fce7adbe8 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/patch_cases.ts @@ -0,0 +1,243 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { getPostCaseRequest, postCaseReq } from '../../../../common/lib/mock'; +import { + deleteAllCaseItems, + createCase, + updateCase, + findCases, + getAuthWithSuperUser, +} from '../../../../common/lib/utils'; + +import { + globalRead, + noKibanaPrivileges, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + secOnlySpacesAll, + secOnlyReadSpacesAll, + superUser, +} from '../../../../common/lib/authentication/users'; +import { + obsOnlyDefaultSpaceAuth, + secOnlyDefaultSpaceAuth, + superUserDefaultSpaceAuth, +} from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('patch_cases', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + it('should update a case when the user has the correct permissions', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + postCaseReq, + 200, + secOnlyDefaultSpaceAuth + ); + + const patchedCases = await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', + }, + ], + }, + auth: secOnlyDefaultSpaceAuth, + }); + + expect(patchedCases[0].owner).to.eql('securitySolutionFixture'); + }); + + it('should update multiple cases when the user has the correct permissions', async () => { + const [case1, case2, case3] = await Promise.all([ + createCase(supertestWithoutAuth, postCaseReq, 200, superUserDefaultSpaceAuth), + createCase(supertestWithoutAuth, postCaseReq, 200, superUserDefaultSpaceAuth), + createCase(supertestWithoutAuth, postCaseReq, 200, superUserDefaultSpaceAuth), + ]); + + const patchedCases = await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: case1.id, + version: case1.version, + title: 'new title', + }, + { + id: case2.id, + version: case2.version, + title: 'new title', + }, + { + id: case3.id, + version: case3.version, + title: 'new title', + }, + ], + }, + auth: secOnlyDefaultSpaceAuth, + }); + + expect(patchedCases[0].owner).to.eql('securitySolutionFixture'); + expect(patchedCases[1].owner).to.eql('securitySolutionFixture'); + expect(patchedCases[2].owner).to.eql('securitySolutionFixture'); + }); + + it('should not update a case when the user does not have the correct ownership', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + obsOnlyDefaultSpaceAuth + ); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', + }, + ], + }, + auth: secOnlyDefaultSpaceAuth, + expectedHttpCode: 403, + }); + }); + + it('should not update any cases when the user does not have the correct ownership', async () => { + const [case1, case2, case3] = await Promise.all([ + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + superUserDefaultSpaceAuth + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + superUserDefaultSpaceAuth + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + superUserDefaultSpaceAuth + ), + ]); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: case1.id, + version: case1.version, + title: 'new title', + }, + { + id: case2.id, + version: case2.version, + title: 'new title', + }, + { + id: case3.id, + version: case3.version, + title: 'new title', + }, + ], + }, + auth: secOnlyDefaultSpaceAuth, + expectedHttpCode: 403, + }); + + const resp = await findCases({ supertest, auth: getAuthWithSuperUser(null) }); + expect(resp.cases.length).to.eql(3); + // the update should have failed and none of the title should have been changed + expect(resp.cases[0].title).to.eql(postCaseReq.title); + expect(resp.cases[1].title).to.eql(postCaseReq.title); + expect(resp.cases[2].title).to.eql(postCaseReq.title); + }); + + for (const user of [ + globalRead, + secOnlyReadSpacesAll, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + noKibanaPrivileges, + ]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT update a case`, async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserDefaultSpaceAuth + ); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', + }, + ], + }, + auth: { user, space: null }, + expectedHttpCode: 403, + }); + }); + } + + it('should return a 404 when attempting to access a space', async () => { + const postedCase = await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { + user: superUser, + space: null, + }); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', + }, + ], + }, + auth: { user: secOnlySpacesAll, space: 'space1' }, + expectedHttpCode: 404, + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/post_case.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/post_case.ts new file mode 100644 index 00000000000000..28043d7155e4a6 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/post_case.ts @@ -0,0 +1,83 @@ +/* + * 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 expect from '@kbn/expect'; + +import { getPostCaseRequest } from '../../../../common/lib/mock'; +import { deleteCasesByESQuery, createCase } from '../../../../common/lib/utils'; +import { + secOnlySpacesAll, + secOnlyReadSpacesAll, + globalRead, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + noKibanaPrivileges, +} from '../../../../common/lib/authentication/users'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { secOnlyDefaultSpaceAuth } from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const es = getService('es'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('post_case', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + }); + + it('User: security solution only - should create a case', async () => { + const theCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + secOnlyDefaultSpaceAuth + ); + expect(theCase.owner).to.eql('securitySolutionFixture'); + }); + + it('User: security solution only - should NOT create a case of different owner', async () => { + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 403, + secOnlyDefaultSpaceAuth + ); + }); + + for (const user of [ + globalRead, + secOnlyReadSpacesAll, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + noKibanaPrivileges, + ]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT create a case`, async () => { + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 403, + { user, space: null } + ); + }); + } + + it('should return a 404 when attempting to access a space', async () => { + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 404, + { + user: secOnlySpacesAll, + space: 'space1', + } + ); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts new file mode 100644 index 00000000000000..4c72dafed053b1 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts @@ -0,0 +1,155 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +import { getPostCaseRequest } from '../../../../../common/lib/mock'; +import { createCase, deleteCasesByESQuery, getReporters } from '../../../../../common/lib/utils'; +import { + secOnlySpacesAll, + obsOnlySpacesAll, + globalRead, + superUser, + secOnlyReadSpacesAll, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + noKibanaPrivileges, + obsSecSpacesAll, +} from '../../../../../common/lib/authentication/users'; +import { getUserInfo } from '../../../../../common/lib/authentication'; +import { + secOnlyDefaultSpaceAuth, + obsOnlyDefaultSpaceAuth, + superUserDefaultSpaceAuth, + obsSecDefaultSpaceAuth, +} from '../../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const es = getService('es'); + + describe('get_reporters', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + }); + + it('User: security solution only - should read the correct reporters', async () => { + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + secOnlyDefaultSpaceAuth + ); + + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + obsOnlyDefaultSpaceAuth + ); + + for (const scenario of [ + { + user: globalRead, + expectedReporters: [getUserInfo(secOnlySpacesAll), getUserInfo(obsOnlySpacesAll)], + }, + { + user: superUser, + expectedReporters: [getUserInfo(secOnlySpacesAll), getUserInfo(obsOnlySpacesAll)], + }, + { user: secOnlyReadSpacesAll, expectedReporters: [getUserInfo(secOnlySpacesAll)] }, + { user: obsOnlyReadSpacesAll, expectedReporters: [getUserInfo(obsOnlySpacesAll)] }, + { + user: obsSecReadSpacesAll, + expectedReporters: [getUserInfo(secOnlySpacesAll), getUserInfo(obsOnlySpacesAll)], + }, + ]) { + const reporters = await getReporters({ + supertest: supertestWithoutAuth, + expectedHttpCode: 200, + auth: { + user: scenario.user, + space: null, + }, + }); + + expect(reporters).to.eql(scenario.expectedReporters); + } + }); + + it(`User ${ + noKibanaPrivileges.username + } with role(s) ${noKibanaPrivileges.roles.join()} - should NOT get all reporters`, async () => { + // super user creates a case at the appropriate space + await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserDefaultSpaceAuth); + + // user should not be able to get all reporters at the appropriate space + await getReporters({ + supertest: supertestWithoutAuth, + expectedHttpCode: 403, + auth: { user: noKibanaPrivileges, space: null }, + }); + }); + + it('should return a 404 when attempting to access a space', async () => { + await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { + user: superUser, + space: null, + }); + + await getReporters({ + supertest: supertestWithoutAuth, + expectedHttpCode: 404, + auth: { user: obsSecSpacesAll, space: 'space1' }, + }); + }); + + it('should respect the owner filter when having permissions', async () => { + await Promise.all([ + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyDefaultSpaceAuth), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + obsOnlyDefaultSpaceAuth + ), + ]); + + const reporters = await getReporters({ + supertest: supertestWithoutAuth, + auth: obsSecDefaultSpaceAuth, + query: { owner: 'securitySolutionFixture' }, + }); + + expect(reporters).to.eql([getUserInfo(secOnlySpacesAll)]); + }); + + it('should return the correct cases when trying to exploit RBAC through the owner query parameter', async () => { + await Promise.all([ + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyDefaultSpaceAuth), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + obsOnlyDefaultSpaceAuth + ), + ]); + + // User with permissions only to security solution request reporters from observability + const reporters = await getReporters({ + supertest: supertestWithoutAuth, + auth: secOnlyDefaultSpaceAuth, + query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, + }); + + // Only security solution reporters are being returned + expect(reporters).to.eql([getUserInfo(secOnlySpacesAll)]); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/status/get_status.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/status/get_status.ts new file mode 100644 index 00000000000000..78ca48b04560c5 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/status/get_status.ts @@ -0,0 +1,131 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +import { CaseStatuses } from '../../../../../../../plugins/cases/common/api'; +import { getPostCaseRequest } from '../../../../../common/lib/mock'; +import { + createCase, + updateCase, + getAllCasesStatuses, + deleteAllCaseItems, +} from '../../../../../common/lib/utils'; +import { + globalRead, + noKibanaPrivileges, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + secOnlySpacesAll, + secOnlyReadSpacesAll, + superUser, +} from '../../../../../common/lib/authentication/users'; +import { superUserDefaultSpaceAuth } from '../../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const es = getService('es'); + + describe('get_status', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + it('should return the correct status stats', async () => { + /** + * Owner: Sec + * open: 0, in-prog: 1, closed: 1 + * Owner: Obs + * open: 1, in-prog: 1 + */ + const [inProgressSec, closedSec, , inProgressObs] = await Promise.all([ + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserDefaultSpaceAuth), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserDefaultSpaceAuth), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + superUserDefaultSpaceAuth + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + superUserDefaultSpaceAuth + ), + ]); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: inProgressSec.id, + version: inProgressSec.version, + status: CaseStatuses['in-progress'], + }, + { + id: closedSec.id, + version: closedSec.version, + status: CaseStatuses.closed, + }, + { + id: inProgressObs.id, + version: inProgressObs.version, + status: CaseStatuses['in-progress'], + }, + ], + }, + auth: superUserDefaultSpaceAuth, + }); + + for (const scenario of [ + { user: globalRead, stats: { open: 1, inProgress: 2, closed: 1 } }, + { user: superUser, stats: { open: 1, inProgress: 2, closed: 1 } }, + { user: secOnlyReadSpacesAll, stats: { open: 0, inProgress: 1, closed: 1 } }, + { user: obsOnlyReadSpacesAll, stats: { open: 1, inProgress: 1, closed: 0 } }, + { user: obsSecReadSpacesAll, stats: { open: 1, inProgress: 2, closed: 1 } }, + ]) { + const statuses = await getAllCasesStatuses({ + supertest: supertestWithoutAuth, + auth: { user: scenario.user, space: null }, + }); + + expect(statuses).to.eql({ + count_open_cases: scenario.stats.open, + count_closed_cases: scenario.stats.closed, + count_in_progress_cases: scenario.stats.inProgress, + }); + } + }); + + it(`should return a 403 when retrieving the statuses when the user ${ + noKibanaPrivileges.username + } with role(s) ${noKibanaPrivileges.roles.join()}`, async () => { + await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserDefaultSpaceAuth); + + await getAllCasesStatuses({ + supertest: supertestWithoutAuth, + auth: { user: noKibanaPrivileges, space: null }, + expectedHttpCode: 403, + }); + }); + + it('should return a 404 when attempting to access a space', async () => { + await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserDefaultSpaceAuth); + + await getAllCasesStatuses({ + supertest: supertestWithoutAuth, + auth: { user: secOnlySpacesAll, space: 'space1' }, + expectedHttpCode: 404, + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/tags/get_tags.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/tags/get_tags.ts new file mode 100644 index 00000000000000..c05d956028752d --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/tags/get_tags.ts @@ -0,0 +1,170 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +import { deleteCasesByESQuery, createCase, getTags } from '../../../../../common/lib/utils'; +import { getPostCaseRequest } from '../../../../../common/lib/mock'; +import { + secOnlySpacesAll, + globalRead, + superUser, + secOnlyReadSpacesAll, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + noKibanaPrivileges, +} from '../../../../../common/lib/authentication/users'; +import { + secOnlyDefaultSpaceAuth, + obsOnlyDefaultSpaceAuth, + obsSecDefaultSpaceAuth, + superUserDefaultSpaceAuth, +} from '../../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const es = getService('es'); + + describe('get_tags', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + }); + + it('should read the correct tags', async () => { + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture', tags: ['sec'] }), + 200, + secOnlyDefaultSpaceAuth + ); + + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture', tags: ['obs'] }), + 200, + obsOnlyDefaultSpaceAuth + ); + + for (const scenario of [ + { + user: globalRead, + expectedTags: ['sec', 'obs'], + }, + { + user: superUser, + expectedTags: ['sec', 'obs'], + }, + { user: secOnlyReadSpacesAll, expectedTags: ['sec'] }, + { user: obsOnlyReadSpacesAll, expectedTags: ['obs'] }, + { + user: obsSecReadSpacesAll, + expectedTags: ['sec', 'obs'], + }, + ]) { + const tags = await getTags({ + supertest: supertestWithoutAuth, + expectedHttpCode: 200, + auth: { + user: scenario.user, + space: null, + }, + }); + + expect(tags).to.eql(scenario.expectedTags); + } + }); + + it(`User ${ + noKibanaPrivileges.username + } with role(s) ${noKibanaPrivileges.roles.join()} - should NOT get all tags`, async () => { + // super user creates a case at the appropriate space + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture', tags: ['sec'] }), + 200, + superUserDefaultSpaceAuth + ); + + // user should not be able to get all tags at the appropriate space + await getTags({ + supertest: supertestWithoutAuth, + expectedHttpCode: 403, + auth: { user: noKibanaPrivileges, space: null }, + }); + }); + + it('should return a 404 when attempting to access a space', async () => { + // super user creates a case at the appropriate space + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture', tags: ['sec'] }), + 200, + superUserDefaultSpaceAuth + ); + + await getTags({ + supertest: supertestWithoutAuth, + expectedHttpCode: 404, + auth: { user: secOnlySpacesAll, space: 'space1' }, + }); + }); + + it('should respect the owner filter when having permissions', async () => { + await Promise.all([ + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture', tags: ['sec'] }), + 200, + obsSecDefaultSpaceAuth + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture', tags: ['obs'] }), + 200, + obsSecDefaultSpaceAuth + ), + ]); + + const tags = await getTags({ + supertest: supertestWithoutAuth, + auth: obsSecDefaultSpaceAuth, + 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, + obsSecDefaultSpaceAuth + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture', tags: ['obs'] }), + 200, + obsSecDefaultSpaceAuth + ), + ]); + + // User with permissions only to security solution request tags from observability + const tags = await getTags({ + supertest: supertestWithoutAuth, + auth: secOnlyDefaultSpaceAuth, + 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_only/tests/common/comments/delete_comment.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/delete_comment.ts new file mode 100644 index 00000000000000..274879c69c4d50 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/delete_comment.ts @@ -0,0 +1,236 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { getPostCaseRequest, postCommentUserReq } from '../../../../common/lib/mock'; +import { + deleteAllCaseItems, + deleteCasesByESQuery, + deleteCasesUserActions, + deleteComments, + createCase, + createComment, + deleteComment, + deleteAllComments, + getAuthWithSuperUser, +} from '../../../../common/lib/utils'; +import { + globalRead, + noKibanaPrivileges, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + secOnlySpacesAll, + secOnlyReadSpacesAll, +} from '../../../../common/lib/authentication/users'; +import { obsOnlyDefaultSpaceAuth, secOnlyDefaultSpaceAuth } from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const es = getService('es'); + const superUserNoSpaceAuth = getAuthWithSuperUser(null); + + describe('delete_comment', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + await deleteComments(es); + await deleteCasesUserActions(es); + }); + + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should delete a comment from the appropriate owner', async () => { + const secCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + secOnlyDefaultSpaceAuth + ); + + const commentResp = await createComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + params: postCommentUserReq, + auth: secOnlyDefaultSpaceAuth, + }); + + await deleteComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + commentId: commentResp.comments![0].id, + auth: secOnlyDefaultSpaceAuth, + }); + }); + + it('should delete multiple comments from the appropriate owner', async () => { + const secCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + secOnlyDefaultSpaceAuth + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + params: postCommentUserReq, + auth: secOnlyDefaultSpaceAuth, + }); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + params: postCommentUserReq, + auth: secOnlyDefaultSpaceAuth, + }); + + await deleteAllComments({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + auth: secOnlyDefaultSpaceAuth, + }); + }); + + it('should not delete a comment from a different owner', async () => { + const secCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + secOnlyDefaultSpaceAuth + ); + + const commentResp = await createComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + params: postCommentUserReq, + auth: secOnlyDefaultSpaceAuth, + }); + + await deleteComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + commentId: commentResp.comments![0].id, + auth: obsOnlyDefaultSpaceAuth, + expectedHttpCode: 403, + }); + + await deleteAllComments({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + auth: obsOnlyDefaultSpaceAuth, + expectedHttpCode: 403, + }); + }); + + for (const user of [ + globalRead, + secOnlyReadSpacesAll, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + ]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT delete a comment`, async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserNoSpaceAuth + ); + + const commentResp = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: superUserNoSpaceAuth, + }); + + await deleteComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + commentId: commentResp.comments![0].id, + auth: { user, space: null }, + expectedHttpCode: 403, + }); + + await deleteAllComments({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + auth: { user, space: null }, + expectedHttpCode: 403, + }); + }); + } + + it('should not delete a comment with no kibana privileges', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserNoSpaceAuth + ); + + const commentResp = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: superUserNoSpaceAuth, + }); + + await deleteComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + commentId: commentResp.comments![0].id, + auth: { user: noKibanaPrivileges, space: null }, + expectedHttpCode: 403, + }); + + await deleteAllComments({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + auth: { user: noKibanaPrivileges, space: null }, + // the find in the delete all will return no results + expectedHttpCode: 404, + }); + }); + + it('should return a 404 when attempting to access a space', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserNoSpaceAuth + ); + + const commentResp = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: superUserNoSpaceAuth, + }); + + await deleteComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + commentId: commentResp.comments![0].id, + auth: { user: secOnlySpacesAll, space: 'space1' }, + expectedHttpCode: 404, + }); + + await deleteAllComments({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + auth: { user: secOnlySpacesAll, space: 'space1' }, + expectedHttpCode: 404, + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/find_comments.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/find_comments.ts new file mode 100644 index 00000000000000..5239c616603a8a --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/find_comments.ts @@ -0,0 +1,278 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { CommentsResponse } from '../../../../../../plugins/cases/common/api'; +import { + getPostCaseRequest, + postCommentAlertReq, + postCommentUserReq, +} from '../../../../common/lib/mock'; +import { + createComment, + deleteAllCaseItems, + deleteCasesByESQuery, + deleteCasesUserActions, + deleteComments, + ensureSavedObjectIsAuthorized, + getSpaceUrlPrefix, + createCase, +} from '../../../../common/lib/utils'; + +import { + secOnlySpacesAll, + obsOnlyReadSpacesAll, + secOnlyReadSpacesAll, + noKibanaPrivileges, + superUser, + globalRead, + obsSecReadSpacesAll, +} from '../../../../common/lib/authentication/users'; +import { + obsOnlyDefaultSpaceAuth, + secOnlyDefaultSpaceAuth, + superUserDefaultSpaceAuth, +} from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('find_comments', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + await deleteComments(es); + await deleteCasesUserActions(es); + }); + + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should return the correct comments', async () => { + const [secCase, obsCase] = await Promise.all([ + // Create case owned by the security solution user + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyDefaultSpaceAuth), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + obsOnlyDefaultSpaceAuth + ), + // Create case owned by the observability user + ]); + + await Promise.all([ + createComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + params: postCommentUserReq, + auth: secOnlyDefaultSpaceAuth, + }), + createComment({ + supertest: supertestWithoutAuth, + caseId: obsCase.id, + params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, + auth: obsOnlyDefaultSpaceAuth, + }), + ]); + + for (const scenario of [ + { + user: globalRead, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: secCase.id, + }, + { + user: globalRead, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: obsCase.id, + }, + { + user: superUser, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: secCase.id, + }, + { + user: superUser, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: obsCase.id, + }, + { + user: secOnlyReadSpacesAll, + numExpectedEntites: 1, + owners: ['securitySolutionFixture'], + caseID: secCase.id, + }, + { + user: obsOnlyReadSpacesAll, + numExpectedEntites: 1, + owners: ['observabilityFixture'], + caseID: obsCase.id, + }, + { + user: obsSecReadSpacesAll, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: secCase.id, + }, + { + user: obsSecReadSpacesAll, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: obsCase.id, + }, + ]) { + const { body: caseComments }: { body: CommentsResponse } = await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(null)}${CASES_URL}/${scenario.caseID}/comments/_find`) + .auth(scenario.user.username, scenario.user.password) + .expect(200); + + ensureSavedObjectIsAuthorized( + caseComments.comments, + scenario.numExpectedEntites, + scenario.owners + ); + } + }); + + it(`User ${ + noKibanaPrivileges.username + } with role(s) ${noKibanaPrivileges.roles.join()} - should NOT read a comment`, async () => { + // super user creates a case and comment in the appropriate space + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserDefaultSpaceAuth + ); + + await createComment({ + supertest: supertestWithoutAuth, + auth: { user: superUser, space: null }, + params: { ...postCommentUserReq, owner: 'securitySolutionFixture' }, + caseId: caseInfo.id, + }); + + // user should not be able to read comments + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(null)}${CASES_URL}/${caseInfo.id}/comments/_find`) + .auth(noKibanaPrivileges.username, noKibanaPrivileges.password) + .expect(403); + }); + + it('should return a 404 when attempting to access a space', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserDefaultSpaceAuth + ); + + await createComment({ + supertest: supertestWithoutAuth, + auth: superUserDefaultSpaceAuth, + params: { ...postCommentUserReq, owner: 'securitySolutionFixture' }, + caseId: caseInfo.id, + }); + + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix('space1')}${CASES_URL}/${caseInfo.id}/comments/_find`) + .auth(secOnlySpacesAll.username, secOnlySpacesAll.password) + .expect(404); + }); + + it('should not return any comments when trying to exploit RBAC through the search query parameter', async () => { + const obsCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + superUserDefaultSpaceAuth + ); + + await createComment({ + supertest: supertestWithoutAuth, + auth: superUserDefaultSpaceAuth, + params: { ...postCommentUserReq, owner: 'observabilityFixture' }, + caseId: obsCase.id, + }); + + const { body: res }: { body: CommentsResponse } = await supertestWithoutAuth + .get( + `${getSpaceUrlPrefix(null)}${CASES_URL}/${ + obsCase.id + }/comments/_find?search=securitySolutionFixture+observabilityFixture` + ) + .auth(secOnlySpacesAll.username, secOnlySpacesAll.password) + .expect(200); + + // shouldn't find any comments since they were created under the observability ownership + ensureSavedObjectIsAuthorized(res.comments, 0, ['securitySolutionFixture']); + }); + + it('should not allow retrieving unauthorized comments using the filter field', async () => { + const obsCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + superUserDefaultSpaceAuth + ); + + await createComment({ + supertest: supertestWithoutAuth, + auth: superUserDefaultSpaceAuth, + params: { ...postCommentUserReq, owner: 'observabilityFixture' }, + caseId: obsCase.id, + }); + + const { body: res } = await supertestWithoutAuth + .get( + `${getSpaceUrlPrefix(null)}${CASES_URL}/${ + obsCase.id + }/comments/_find?filter=cases-comments.attributes.owner:"observabilityFixture"` + ) + .auth(secOnlySpacesAll.username, secOnlySpacesAll.password) + .expect(200); + expect(res.comments.length).to.be(0); + }); + + // This test ensures that the user is not allowed to define the namespaces query param + // so she cannot search across spaces + it('should NOT allow to pass a namespaces query parameter', async () => { + const obsCase = await createCase( + supertest, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200 + ); + + await createComment({ + supertest, + params: { ...postCommentUserReq, owner: 'observabilityFixture' }, + caseId: obsCase.id, + }); + + await supertest.get(`${CASES_URL}/${obsCase.id}/comments/_find?namespaces[0]=*`).expect(400); + + await supertest.get(`${CASES_URL}/${obsCase.id}/comments/_find?namespaces=*`).expect(400); + }); + + it('should NOT allow to pass a non supported query parameter', async () => { + await supertest.get(`${CASES_URL}/id/comments/_find?notExists=papa`).expect(400); + await supertest.get(`${CASES_URL}/id/comments/_find?owner=papa`).expect(400); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/get_all_comments.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/get_all_comments.ts new file mode 100644 index 00000000000000..a0010ef19499fa --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/get_all_comments.ts @@ -0,0 +1,139 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { getPostCaseRequest, postCommentUserReq } from '../../../../common/lib/mock'; +import { + deleteAllCaseItems, + createCase, + createComment, + getAllComments, +} from '../../../../common/lib/utils'; +import { + globalRead, + noKibanaPrivileges, + obsOnlySpacesAll, + obsOnlyReadSpacesAll, + obsSecSpacesAll, + obsSecReadSpacesAll, + secOnlySpacesAll, + secOnlyReadSpacesAll, + superUser, +} from '../../../../common/lib/authentication/users'; +import { superUserDefaultSpaceAuth } from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const es = getService('es'); + + describe('get_all_comments', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + it('should get all comments when the user has the correct permissions', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserDefaultSpaceAuth + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: superUserDefaultSpaceAuth, + }); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: superUserDefaultSpaceAuth, + }); + + for (const user of [ + globalRead, + superUser, + secOnlySpacesAll, + secOnlyReadSpacesAll, + obsSecSpacesAll, + obsSecReadSpacesAll, + ]) { + const comments = await getAllComments({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + auth: { user, space: null }, + }); + + expect(comments.length).to.eql(2); + } + }); + + it('should not get comments when the user does not have correct permission', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserDefaultSpaceAuth + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: superUserDefaultSpaceAuth, + }); + + for (const scenario of [ + { user: noKibanaPrivileges, returnCode: 403 }, + { user: obsOnlySpacesAll, returnCode: 200 }, + { user: obsOnlyReadSpacesAll, returnCode: 200 }, + ]) { + const comments = await getAllComments({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + auth: { user: scenario.user, space: null }, + expectedHttpCode: scenario.returnCode, + }); + + // only check the length if we get a 200 in response + if (scenario.returnCode === 200) { + expect(comments.length).to.be(0); + } + } + }); + + it('should return a 404 when attempting to access a space', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserDefaultSpaceAuth + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: superUserDefaultSpaceAuth, + }); + + await getAllComments({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + auth: { user: secOnlySpacesAll, space: 'space1' }, + expectedHttpCode: 404, + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/get_comment.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/get_comment.ts new file mode 100644 index 00000000000000..79693d3e0a574e --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/get_comment.ts @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { postCommentUserReq, getPostCaseRequest } from '../../../../common/lib/mock'; +import { + deleteAllCaseItems, + createCase, + createComment, + getComment, +} from '../../../../common/lib/utils'; +import { + globalRead, + noKibanaPrivileges, + obsOnlySpacesAll, + obsOnlyReadSpacesAll, + obsSecSpacesAll, + obsSecReadSpacesAll, + secOnlySpacesAll, + secOnlyReadSpacesAll, + superUser, +} from '../../../../common/lib/authentication/users'; +import { superUserDefaultSpaceAuth } from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const es = getService('es'); + + describe('get_comment', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + it('should get a comment when the user has the correct permissions', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserDefaultSpaceAuth + ); + + const caseWithComment = await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: superUserDefaultSpaceAuth, + }); + + for (const user of [ + globalRead, + superUser, + secOnlySpacesAll, + secOnlyReadSpacesAll, + obsSecSpacesAll, + obsSecReadSpacesAll, + ]) { + await getComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + commentId: caseWithComment.comments![0].id, + auth: { user, space: null }, + }); + } + }); + + it('should not get comment when the user does not have correct permissions', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserDefaultSpaceAuth + ); + + const caseWithComment = await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: superUserDefaultSpaceAuth, + }); + + for (const user of [noKibanaPrivileges, obsOnlySpacesAll, obsOnlyReadSpacesAll]) { + await getComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + commentId: caseWithComment.comments![0].id, + auth: { user, space: null }, + expectedHttpCode: 403, + }); + } + }); + + it('should return a 404 when attempting to access a space', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserDefaultSpaceAuth + ); + + const caseWithComment = await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: superUserDefaultSpaceAuth, + }); + + await getComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + commentId: caseWithComment.comments![0].id, + auth: { user: secOnlySpacesAll, space: 'space1' }, + expectedHttpCode: 404, + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/patch_comment.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/patch_comment.ts new file mode 100644 index 00000000000000..7a25ec4ec39812 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/patch_comment.ts @@ -0,0 +1,189 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { AttributesTypeUser, CommentType } from '../../../../../../plugins/cases/common/api'; +import { defaultUser, postCommentUserReq, getPostCaseRequest } from '../../../../common/lib/mock'; +import { + deleteAllCaseItems, + deleteCasesByESQuery, + deleteCasesUserActions, + deleteComments, + createCase, + createComment, + updateComment, +} from '../../../../common/lib/utils'; +import { + globalRead, + noKibanaPrivileges, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + secOnlySpacesAll, + secOnlyReadSpacesAll, +} from '../../../../common/lib/authentication/users'; +import { + obsOnlyDefaultSpaceAuth, + secOnlyDefaultSpaceAuth, + superUserDefaultSpaceAuth, +} from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('patch_comment', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + await deleteComments(es); + await deleteCasesUserActions(es); + }); + + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should update a comment that the user has permissions for', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserDefaultSpaceAuth + ); + + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + auth: superUserDefaultSpaceAuth, + }); + + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; + const updatedCase = await updateComment({ + supertest, + caseId: postedCase.id, + req: { + ...postCommentUserReq, + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + comment: newComment, + }, + auth: secOnlyDefaultSpaceAuth, + }); + + const userComment = updatedCase.comments![0] as AttributesTypeUser; + expect(userComment.comment).to.eql(newComment); + expect(userComment.type).to.eql(CommentType.user); + expect(updatedCase.updated_by).to.eql(defaultUser); + expect(userComment.owner).to.eql('securitySolutionFixture'); + }); + + it('should not update a comment that has a different owner thant he user has access to', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserDefaultSpaceAuth + ); + + const patchedCase = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: superUserDefaultSpaceAuth, + }); + + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; + await updateComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + req: { + ...postCommentUserReq, + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + comment: newComment, + }, + auth: obsOnlyDefaultSpaceAuth, + expectedHttpCode: 403, + }); + }); + + for (const user of [ + globalRead, + secOnlyReadSpacesAll, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + noKibanaPrivileges, + ]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT update a comment`, async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserDefaultSpaceAuth + ); + + const patchedCase = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: superUserDefaultSpaceAuth, + }); + + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; + await updateComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + req: { + ...postCommentUserReq, + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + comment: newComment, + }, + auth: { user, space: null }, + expectedHttpCode: 403, + }); + }); + } + + it('should return a 404 when attempting to access a space', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserDefaultSpaceAuth + ); + + const patchedCase = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: superUserDefaultSpaceAuth, + }); + + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; + await updateComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + req: { + ...postCommentUserReq, + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + comment: newComment, + }, + auth: { user: secOnlySpacesAll, space: 'space1' }, + expectedHttpCode: 404, + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/post_comment.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/post_comment.ts new file mode 100644 index 00000000000000..500308305d131e --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/post_comment.ts @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { postCommentUserReq, getPostCaseRequest } from '../../../../common/lib/mock'; +import { + deleteAllCaseItems, + deleteCasesByESQuery, + deleteCasesUserActions, + deleteComments, + createCase, + createComment, +} from '../../../../common/lib/utils'; + +import { + globalRead, + noKibanaPrivileges, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + secOnlySpacesAll, + secOnlyReadSpacesAll, +} from '../../../../common/lib/authentication/users'; +import { + obsOnlyDefaultSpaceAuth, + secOnlyDefaultSpaceAuth, + superUserDefaultSpaceAuth, +} from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const es = getService('es'); + + describe('post_comment', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + await deleteComments(es); + await deleteCasesUserActions(es); + }); + + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should create a comment when the user has the correct permissions for that owner', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + superUserDefaultSpaceAuth + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: secOnlyDefaultSpaceAuth, + }); + }); + + it('should not create a comment when the user does not have permissions for that owner', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + obsOnlyDefaultSpaceAuth + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: { ...postCommentUserReq, owner: 'observabilityFixture' }, + auth: secOnlyDefaultSpaceAuth, + expectedHttpCode: 403, + }); + }); + + for (const user of [ + globalRead, + secOnlyReadSpacesAll, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + noKibanaPrivileges, + ]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should not create a comment`, async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + superUserDefaultSpaceAuth + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: { user, space: null }, + expectedHttpCode: 403, + }); + }); + } + + it('should return a 404 when attempting to access a space', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + superUserDefaultSpaceAuth + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: { user: secOnlySpacesAll, space: 'space1' }, + expectedHttpCode: 404, + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/configure/get_configure.ts b/x-pack/test/case_api_integration/security_only/tests/common/configure/get_configure.ts new file mode 100644 index 00000000000000..0a8b3ebd8981e3 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/configure/get_configure.ts @@ -0,0 +1,195 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { + deleteConfiguration, + getConfiguration, + createConfiguration, + getConfigurationRequest, + ensureSavedObjectIsAuthorized, +} from '../../../../common/lib/utils'; +import { + secOnlySpacesAll, + obsOnlyReadSpacesAll, + secOnlyReadSpacesAll, + noKibanaPrivileges, + superUser, + globalRead, + obsSecReadSpacesAll, +} from '../../../../common/lib/authentication/users'; +import { + obsOnlyDefaultSpaceAuth, + obsSecDefaultSpaceAuth, + secOnlyDefaultSpaceAuth, + superUserDefaultSpaceAuth, +} from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const es = getService('es'); + + describe('get_configure', () => { + afterEach(async () => { + await deleteConfiguration(es); + }); + + it('should return the correct configuration', async () => { + await createConfiguration( + supertestWithoutAuth, + getConfigurationRequest(), + 200, + secOnlyDefaultSpaceAuth + ); + + await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'observabilityFixture' }, + 200, + obsOnlyDefaultSpaceAuth + ); + + for (const scenario of [ + { + user: globalRead, + numberOfExpectedCases: 2, + owners: ['securitySolutionFixture', 'observabilityFixture'], + }, + { + user: superUser, + numberOfExpectedCases: 2, + owners: ['securitySolutionFixture', 'observabilityFixture'], + }, + { + user: secOnlyReadSpacesAll, + numberOfExpectedCases: 1, + owners: ['securitySolutionFixture'], + }, + { + user: obsOnlyReadSpacesAll, + numberOfExpectedCases: 1, + owners: ['observabilityFixture'], + }, + { + user: obsSecReadSpacesAll, + numberOfExpectedCases: 2, + owners: ['securitySolutionFixture', 'observabilityFixture'], + }, + ]) { + const configuration = await getConfiguration({ + supertest: supertestWithoutAuth, + query: { owner: scenario.owners }, + expectedHttpCode: 200, + auth: { + user: scenario.user, + space: null, + }, + }); + + ensureSavedObjectIsAuthorized( + configuration, + scenario.numberOfExpectedCases, + scenario.owners + ); + } + }); + + it(`User ${ + noKibanaPrivileges.username + } with role(s) ${noKibanaPrivileges.roles.join()} - should NOT read a case configuration`, async () => { + // super user creates a configuration at the appropriate space + await createConfiguration( + supertestWithoutAuth, + getConfigurationRequest(), + 200, + superUserDefaultSpaceAuth + ); + + // user should not be able to read configurations at the appropriate space + await getConfiguration({ + supertest: supertestWithoutAuth, + expectedHttpCode: 403, + auth: { + user: noKibanaPrivileges, + space: null, + }, + }); + }); + + it('should return a 404 when attempting to access a space', async () => { + await createConfiguration( + supertestWithoutAuth, + getConfigurationRequest(), + 200, + superUserDefaultSpaceAuth + ); + + await getConfiguration({ + supertest: supertestWithoutAuth, + expectedHttpCode: 404, + auth: { + user: secOnlySpacesAll, + space: 'space1', + }, + }); + }); + + it('should respect the owner filter when having permissions', async () => { + await Promise.all([ + createConfiguration( + supertestWithoutAuth, + getConfigurationRequest(), + 200, + obsSecDefaultSpaceAuth + ), + createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'observabilityFixture' }, + 200, + obsSecDefaultSpaceAuth + ), + ]); + + const res = await getConfiguration({ + supertest: supertestWithoutAuth, + query: { owner: 'securitySolutionFixture' }, + auth: obsSecDefaultSpaceAuth, + }); + + 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, + obsSecDefaultSpaceAuth + ), + createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'observabilityFixture' }, + 200, + obsSecDefaultSpaceAuth + ), + ]); + + // User with permissions only to security solution request cases from observability + const res = await getConfiguration({ + supertest: supertestWithoutAuth, + query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, + auth: secOnlyDefaultSpaceAuth, + }); + + // Only security solution cases are being returned + ensureSavedObjectIsAuthorized(res, 1, ['securitySolutionFixture']); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/configure/patch_configure.ts b/x-pack/test/case_api_integration/security_only/tests/common/configure/patch_configure.ts new file mode 100644 index 00000000000000..eb1fa01221ae89 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/configure/patch_configure.ts @@ -0,0 +1,140 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; + +import { + getConfigurationRequest, + deleteConfiguration, + createConfiguration, + updateConfiguration, +} from '../../../../common/lib/utils'; +import { + secOnlySpacesAll, + obsOnlyReadSpacesAll, + secOnlyReadSpacesAll, + noKibanaPrivileges, + globalRead, + obsSecReadSpacesAll, +} from '../../../../common/lib/authentication/users'; +import { secOnlyDefaultSpaceAuth, superUserDefaultSpaceAuth } from '../../../utils'; + +// 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('patch_configure', () => { + const actionsRemover = new ActionsRemover(supertest); + + afterEach(async () => { + await deleteConfiguration(es); + await actionsRemover.removeAll(); + }); + + it('User: security solution only - should update a configuration', async () => { + const configuration = await createConfiguration( + supertestWithoutAuth, + getConfigurationRequest(), + 200, + secOnlyDefaultSpaceAuth + ); + + const newConfiguration = await updateConfiguration( + supertestWithoutAuth, + configuration.id, + { + closure_type: 'close-by-pushing', + version: configuration.version, + }, + 200, + secOnlyDefaultSpaceAuth + ); + + 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, + superUserDefaultSpaceAuth + ); + + await updateConfiguration( + supertestWithoutAuth, + configuration.id, + { + closure_type: 'close-by-pushing', + version: configuration.version, + }, + 403, + secOnlyDefaultSpaceAuth + ); + }); + + for (const user of [ + globalRead, + secOnlyReadSpacesAll, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + noKibanaPrivileges, + ]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT update a configuration`, async () => { + const configuration = await createConfiguration( + supertestWithoutAuth, + getConfigurationRequest(), + 200, + superUserDefaultSpaceAuth + ); + + await updateConfiguration( + supertestWithoutAuth, + configuration.id, + { + closure_type: 'close-by-pushing', + version: configuration.version, + }, + 403, + { + user, + space: null, + } + ); + }); + } + + it('should return a 404 when attempting to access a space', async () => { + const configuration = await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, + 200, + superUserDefaultSpaceAuth + ); + + await updateConfiguration( + supertestWithoutAuth, + configuration.id, + { + closure_type: 'close-by-pushing', + version: configuration.version, + }, + 404, + { + user: secOnlySpacesAll, + space: 'space1', + } + ); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/configure/post_configure.ts b/x-pack/test/case_api_integration/security_only/tests/common/configure/post_configure.ts new file mode 100644 index 00000000000000..b3de6ec0487bb0 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/configure/post_configure.ts @@ -0,0 +1,133 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; + +import { + getConfigurationRequest, + deleteConfiguration, + createConfiguration, + getConfiguration, + ensureSavedObjectIsAuthorized, +} from '../../../../common/lib/utils'; + +import { + secOnlySpacesAll, + obsOnlyReadSpacesAll, + secOnlyReadSpacesAll, + noKibanaPrivileges, + globalRead, + obsSecReadSpacesAll, +} from '../../../../common/lib/authentication/users'; +import { secOnlyDefaultSpaceAuth, superUserDefaultSpaceAuth } from '../../../utils'; + +// 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('post_configure', () => { + const actionsRemover = new ActionsRemover(supertest); + + afterEach(async () => { + await deleteConfiguration(es); + await actionsRemover.removeAll(); + }); + + it('User: security solution only - should create a configuration', async () => { + const configuration = await createConfiguration( + supertestWithoutAuth, + getConfigurationRequest(), + 200, + secOnlyDefaultSpaceAuth + ); + + 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, + secOnlyDefaultSpaceAuth + ); + }); + + for (const user of [ + globalRead, + secOnlyReadSpacesAll, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + 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: null, + } + ); + }); + } + + it('should return a 404 when attempting to access a space', async () => { + await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, + 404, + { + user: secOnlySpacesAll, + space: 'space1', + } + ); + }); + + it('it deletes the correct configurations', async () => { + await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, + 200, + superUserDefaultSpaceAuth + ); + + /** + * This API call should not delete the previously created configuration + * as it belongs to a different owner + */ + await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'observabilityFixture' }, + 200, + superUserDefaultSpaceAuth + ); + + const configuration = await getConfiguration({ + supertest: supertestWithoutAuth, + query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, + auth: superUserDefaultSpaceAuth, + }); + + /** + * This ensures that both configuration are returned as expected + * and neither of has been deleted + */ + ensureSavedObjectIsAuthorized(configuration, 2, [ + 'securitySolutionFixture', + 'observabilityFixture', + ]); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/index.ts b/x-pack/test/case_api_integration/security_only/tests/common/index.ts new file mode 100644 index 00000000000000..7dd6dd4e22711b --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/index.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('Common', function () { + loadTestFile(require.resolve('./comments/delete_comment')); + loadTestFile(require.resolve('./comments/find_comments')); + loadTestFile(require.resolve('./comments/get_comment')); + loadTestFile(require.resolve('./comments/get_all_comments')); + loadTestFile(require.resolve('./comments/patch_comment')); + loadTestFile(require.resolve('./comments/post_comment')); + loadTestFile(require.resolve('./alerts/get_cases')); + loadTestFile(require.resolve('./cases/delete_cases')); + loadTestFile(require.resolve('./cases/find_cases')); + loadTestFile(require.resolve('./cases/get_case')); + loadTestFile(require.resolve('./cases/patch_cases')); + loadTestFile(require.resolve('./cases/post_case')); + loadTestFile(require.resolve('./cases/reporters/get_reporters')); + loadTestFile(require.resolve('./cases/status/get_status')); + loadTestFile(require.resolve('./cases/tags/get_tags')); + loadTestFile(require.resolve('./user_actions/get_all_user_actions')); + loadTestFile(require.resolve('./configure/get_configure')); + loadTestFile(require.resolve('./configure/patch_configure')); + loadTestFile(require.resolve('./configure/post_configure')); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/security_only/tests/common/user_actions/get_all_user_actions.ts new file mode 100644 index 00000000000000..bd36ce1b0d9d6a --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/user_actions/get_all_user_actions.ts @@ -0,0 +1,104 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { CaseResponse, CaseStatuses } from '../../../../../../plugins/cases/common/api'; +import { getPostCaseRequest } from '../../../../common/lib/mock'; +import { + deleteAllCaseItems, + createCase, + updateCase, + getCaseUserActions, +} from '../../../../common/lib/utils'; +import { + globalRead, + noKibanaPrivileges, + obsSecSpacesAll, + obsSecReadSpacesAll, + secOnlySpacesAll, + secOnlyReadSpacesAll, + superUser, +} from '../../../../common/lib/authentication/users'; +import { superUserDefaultSpaceAuth } from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const es = getService('es'); + + describe('get_all_user_actions', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + let caseInfo: CaseResponse; + beforeEach(async () => { + caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserDefaultSpaceAuth + ); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: caseInfo.id, + version: caseInfo.version, + status: CaseStatuses.closed, + }, + ], + }, + auth: superUserDefaultSpaceAuth, + }); + }); + + it('should get the user actions for a case when the user has the correct permissions', async () => { + for (const user of [ + globalRead, + superUser, + secOnlySpacesAll, + secOnlyReadSpacesAll, + obsSecSpacesAll, + obsSecReadSpacesAll, + ]) { + const userActions = await getCaseUserActions({ + supertest: supertestWithoutAuth, + caseID: caseInfo.id, + auth: { user, space: null }, + }); + + expect(userActions.length).to.eql(2); + } + }); + + it(`should 403 when requesting the user actions of a case with user ${ + noKibanaPrivileges.username + } with role(s) ${noKibanaPrivileges.roles.join()}`, async () => { + await getCaseUserActions({ + supertest: supertestWithoutAuth, + caseID: caseInfo.id, + auth: { user: noKibanaPrivileges, space: null }, + expectedHttpCode: 403, + }); + }); + + it('should return a 404 when attempting to access a space', async () => { + await getCaseUserActions({ + supertest: supertestWithoutAuth, + caseID: caseInfo.id, + auth: { user: secOnlySpacesAll, space: 'space1' }, + expectedHttpCode: 404, + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/trial/cases/push_case.ts b/x-pack/test/case_api_integration/security_only/tests/trial/cases/push_case.ts new file mode 100644 index 00000000000000..6294400281b92e --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/trial/cases/push_case.ts @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; + +import { getPostCaseRequest } from '../../../../common/lib/mock'; +import { + pushCase, + deleteAllCaseItems, + createCaseWithConnector, +} from '../../../../common/lib/utils'; +import { + ExternalServiceSimulator, + getExternalServiceSimulatorPath, +} from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; +import { + globalRead, + noKibanaPrivileges, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + secOnlySpacesAll, + secOnlyReadSpacesAll, +} from '../../../../common/lib/authentication/users'; +import { secOnlyDefaultSpaceAuth } from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); + const es = getService('es'); + + describe('push_case', () => { + const actionsRemover = new ActionsRemover(supertest); + + let servicenowSimulatorURL: string = ''; + before(() => { + servicenowSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) + ); + }); + + afterEach(async () => { + await deleteAllCaseItems(es); + await actionsRemover.removeAll(); + }); + + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + it('should push a case that the user has permissions for', async () => { + const { postedCase, connector } = await createCaseWithConnector({ + supertest, + servicenowSimulatorURL, + actionsRemover, + }); + + await pushCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + connectorId: connector.id, + auth: secOnlyDefaultSpaceAuth, + }); + }); + + it('should not push a case that the user does not have permissions for', async () => { + const { postedCase, connector } = await createCaseWithConnector({ + supertest, + servicenowSimulatorURL, + actionsRemover, + createCaseReq: getPostCaseRequest({ owner: 'observabilityFixture' }), + }); + + await pushCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + connectorId: connector.id, + auth: secOnlyDefaultSpaceAuth, + expectedHttpCode: 403, + }); + }); + + for (const user of [ + globalRead, + secOnlyReadSpacesAll, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + noKibanaPrivileges, + ]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT push a case`, async () => { + const { postedCase, connector } = await createCaseWithConnector({ + supertest, + servicenowSimulatorURL, + actionsRemover, + }); + + await pushCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + connectorId: connector.id, + auth: { user, space: null }, + expectedHttpCode: 403, + }); + }); + } + + it('should return a 404 when attempting to access a space', async () => { + const { postedCase, connector } = await createCaseWithConnector({ + supertest, + servicenowSimulatorURL, + actionsRemover, + }); + + await pushCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + connectorId: connector.id, + auth: { user: secOnlySpacesAll, space: 'space1' }, + expectedHttpCode: 404, + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/trial/index.ts b/x-pack/test/case_api_integration/security_only/tests/trial/index.ts new file mode 100644 index 00000000000000..550dad5917d452 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/trial/index.ts @@ -0,0 +1,34 @@ +/* + * 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 { rolesDefaultSpace } from '../../../common/lib/authentication/roles'; +import { usersDefaultSpace } from '../../../common/lib/authentication/users'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { createUsersAndRoles, deleteUsersAndRoles } from '../../../common/lib/authentication'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile, getService }: FtrProviderContext): void => { + describe('cases security and spaces enabled: trial', function () { + // Fastest ciGroup for the moment. + this.tags('ciGroup5'); + + before(async () => { + // since spaces are disabled this changes each role to have access to all available spaces (it'll just be the default one) + await createUsersAndRoles(getService, usersDefaultSpace, rolesDefaultSpace); + }); + + after(async () => { + await deleteUsersAndRoles(getService, usersDefaultSpace, rolesDefaultSpace); + }); + + // Trial + loadTestFile(require.resolve('./cases/push_case')); + + // Common + loadTestFile(require.resolve('../common')); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/utils.ts b/x-pack/test/case_api_integration/security_only/utils.ts new file mode 100644 index 00000000000000..7c5764c558bbe4 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/utils.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + obsOnlySpacesAll, + obsSecSpacesAll, + secOnlySpacesAll, +} from '../common/lib/authentication/users'; +import { getAuthWithSuperUser } from '../common/lib/utils'; + +export const secOnlyDefaultSpaceAuth = { user: secOnlySpacesAll, space: null }; +export const obsOnlyDefaultSpaceAuth = { user: obsOnlySpacesAll, space: null }; +export const obsSecDefaultSpaceAuth = { user: obsSecSpacesAll, space: null }; +export const superUserDefaultSpaceAuth = getAuthWithSuperUser(null); diff --git a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_connectors.ts b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_connectors.ts index 66759a4dcb39ad..0301fa3a930cba 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_connectors.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_connectors.ts @@ -17,6 +17,7 @@ import { getServiceNowSIRConnector, getAuthWithSuperUser, getCaseConnectors, + getActionsSpace, } from '../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export @@ -24,6 +25,7 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const actionsRemover = new ActionsRemover(supertest); const authSpace1 = getAuthWithSuperUser(); + const space = getActionsSpace(authSpace1.space); describe('get_connectors', () => { afterEach(async () => { @@ -68,11 +70,11 @@ export default ({ getService }: FtrProviderContext): void => { auth: authSpace1, }); - actionsRemover.add(authSpace1.space, sir.id, 'action', 'actions'); - actionsRemover.add(authSpace1.space, snConnector.id, 'action', 'actions'); - actionsRemover.add(authSpace1.space, emailConnector.id, 'action', 'actions'); - actionsRemover.add(authSpace1.space, jiraConnector.id, 'action', 'actions'); - actionsRemover.add(authSpace1.space, resilientConnector.id, 'action', 'actions'); + actionsRemover.add(space, sir.id, 'action', 'actions'); + actionsRemover.add(space, snConnector.id, 'action', 'actions'); + actionsRemover.add(space, emailConnector.id, 'action', 'actions'); + actionsRemover.add(space, jiraConnector.id, 'action', 'actions'); + actionsRemover.add(space, resilientConnector.id, 'action', 'actions'); const connectors = await getCaseConnectors({ supertest, auth: authSpace1 }); @@ -162,11 +164,11 @@ export default ({ getService }: FtrProviderContext): void => { auth: authSpace1, }); - actionsRemover.add(authSpace1.space, sir.id, 'action', 'actions'); - actionsRemover.add(authSpace1.space, snConnector.id, 'action', 'actions'); - actionsRemover.add(authSpace1.space, emailConnector.id, 'action', 'actions'); - actionsRemover.add(authSpace1.space, jiraConnector.id, 'action', 'actions'); - actionsRemover.add(authSpace1.space, resilientConnector.id, 'action', 'actions'); + actionsRemover.add(space, sir.id, 'action', 'actions'); + actionsRemover.add(space, snConnector.id, 'action', 'actions'); + actionsRemover.add(space, emailConnector.id, 'action', 'actions'); + actionsRemover.add(space, jiraConnector.id, 'action', 'actions'); + actionsRemover.add(space, resilientConnector.id, 'action', 'actions'); const connectors = await getCaseConnectors({ supertest, diff --git a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/patch_configure.ts b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/patch_configure.ts index 5015b9c638617b..14d0debe2ac178 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/patch_configure.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/patch_configure.ts @@ -23,6 +23,7 @@ import { getServiceNowConnector, createConnector, getAuthWithSuperUser, + getActionsSpace, } from '../../../../common/lib/utils'; import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; import { nullUser } from '../../../../common/lib/mock'; @@ -33,6 +34,7 @@ export default ({ getService }: FtrProviderContext): void => { const es = getService('es'); const kibanaServer = getService('kibanaServer'); const authSpace1 = getAuthWithSuperUser(); + const space = getActionsSpace(authSpace1.space); describe('patch_configure', () => { const actionsRemover = new ActionsRemover(supertest); @@ -59,7 +61,7 @@ export default ({ getService }: FtrProviderContext): void => { auth: authSpace1, }); - actionsRemover.add(authSpace1.space, connector.id, 'action', 'actions'); + actionsRemover.add(space, connector.id, 'action', 'actions'); // Configuration is created with no connector so the mappings are empty const configuration = await createConfiguration( @@ -129,7 +131,7 @@ export default ({ getService }: FtrProviderContext): void => { auth: authSpace1, }); - actionsRemover.add(authSpace1.space, connector.id, 'action', 'actions'); + actionsRemover.add(space, connector.id, 'action', 'actions'); // Configuration is created with no connector so the mappings are empty const configuration = await createConfiguration( diff --git a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/post_configure.ts b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/post_configure.ts index d67ca29229dd10..7c5035193d4656 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/post_configure.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/post_configure.ts @@ -23,6 +23,7 @@ import { createConnector, getServiceNowConnector, getAuthWithSuperUser, + getActionsSpace, } from '../../../../common/lib/utils'; import { nullUser } from '../../../../common/lib/mock'; @@ -32,6 +33,7 @@ export default ({ getService }: FtrProviderContext): void => { const es = getService('es'); const kibanaServer = getService('kibanaServer'); const authSpace1 = getAuthWithSuperUser(); + const space = getActionsSpace(authSpace1.space); describe('post_configure', () => { const actionsRemover = new ActionsRemover(supertest); @@ -58,7 +60,7 @@ export default ({ getService }: FtrProviderContext): void => { auth: authSpace1, }); - actionsRemover.add(authSpace1.space, connector.id, 'action', 'actions'); + actionsRemover.add(space, connector.id, 'action', 'actions'); const postRes = await createConfiguration( supertest, From 86568ed0da9e13137123eb1908737b89211c0cdb Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Wed, 12 May 2021 09:19:18 -0400 Subject: [PATCH 055/113] Adding sub feature --- .../security_solution/server/plugin.ts | 57 +++++++++++++------ 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 2d5544286a87d0..9941411c1e7999 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -221,8 +221,45 @@ export class Plugin implements IPlugin Date: Fri, 14 May 2021 08:55:41 -0400 Subject: [PATCH 056/113] [Cases] Cleaning up the services and TODOs (#99723) * Cleaning up the service intialization * Fixing type errors * Adding comments for the api * Working test for cases client * Fix type error * Adding generated docs * Adding more docs and cleaning up types * Cleaning up readme * More clean up and links * Changing some file names * Renaming docs --- x-pack/plugins/cases/README.md | 6 +- x-pack/plugins/cases/common/api/cases/case.ts | 100 ++++++++ .../plugins/cases/common/api/cases/comment.ts | 3 + .../cases/common/api/cases/configure.ts | 13 + .../cases/common/api/cases/sub_case.ts | 30 +++ .../plugins/cases/common/api/saved_object.ts | 33 +++ x-pack/plugins/cases/docs/README.md | 37 +++ .../docs/cases_client/cases_client_api.md | 22 ++ .../classes/client.casesclient.md | 178 ++++++++++++++ .../interfaces/attachments_add.addargs.md | 34 +++ ...attachments_client.attachmentssubclient.md | 147 +++++++++++ .../attachments_delete.deleteallargs.md | 34 +++ .../attachments_delete.deleteargs.md | 45 ++++ .../interfaces/attachments_get.findargs.md | 51 ++++ .../interfaces/attachments_get.getallargs.md | 45 ++++ .../interfaces/attachments_get.getargs.md | 32 +++ .../attachments_update.updateargs.md | 45 ++++ .../interfaces/cases_client.casessubclient.md | 189 +++++++++++++++ .../cases_get.caseidsbyalertidparams.md | 40 +++ .../interfaces/cases_get.getparams.md | 45 ++++ .../interfaces/cases_push.pushparams.md | 34 +++ .../configure_client.configuresubclient.md | 84 +++++++ .../interfaces/stats_client.statssubclient.md | 25 ++ .../sub_cases_client.subcasesclient.md | 89 +++++++ ...typedoc_interfaces.iallcommentsresponse.md | 11 + .../typedoc_interfaces.icasepostrequest.md | 88 +++++++ .../typedoc_interfaces.icaseresponse.md | 228 ++++++++++++++++++ ...typedoc_interfaces.icasesconfigurepatch.md | 43 ++++ ...pedoc_interfaces.icasesconfigurerequest.md | 43 ++++ ...edoc_interfaces.icasesconfigureresponse.md | 123 ++++++++++ .../typedoc_interfaces.icasesfindrequest.md | 133 ++++++++++ .../typedoc_interfaces.icasesfindresponse.md | 79 ++++++ .../typedoc_interfaces.icasespatchrequest.md | 25 ++ .../typedoc_interfaces.icasesresponse.md | 11 + ...doc_interfaces.icaseuseractionsresponse.md | 11 + .../typedoc_interfaces.icommentsresponse.md | 52 ++++ .../typedoc_interfaces.isubcaseresponse.md | 133 ++++++++++ ...ypedoc_interfaces.isubcasesfindresponse.md | 79 ++++++ .../typedoc_interfaces.isubcasesresponse.md | 11 + .../user_actions_client.useractionget.md | 34 +++ ...ser_actions_client.useractionssubclient.md | 31 +++ .../cases_client/modules/attachments_add.md | 9 + .../modules/attachments_client.md | 9 + .../modules/attachments_delete.md | 10 + .../cases_client/modules/attachments_get.md | 11 + .../modules/attachments_update.md | 9 + .../docs/cases_client/modules/cases_client.md | 9 + .../docs/cases_client/modules/cases_get.md | 53 ++++ .../docs/cases_client/modules/cases_push.md | 9 + .../cases/docs/cases_client/modules/client.md | 9 + .../cases_client/modules/configure_client.md | 9 + .../docs/cases_client/modules/stats_client.md | 9 + .../cases_client/modules/sub_cases_client.md | 9 + .../modules/typedoc_interfaces.md | 26 ++ .../modules/user_actions_client.md | 10 + .../cases/docs/cases_client_typedoc.json | 25 ++ .../cases/server/client/attachments/add.ts | 11 + .../cases/server/client/attachments/client.ts | 47 +++- .../cases/server/client/attachments/delete.ts | 24 +- .../cases/server/client/attachments/get.ts | 33 +++ .../cases/server/client/attachments/update.ts | 14 ++ .../cases/server/client/cases/client.ts | 84 +++++-- .../cases/server/client/cases/create.ts | 2 + .../cases/server/client/cases/delete.ts | 5 + .../plugins/cases/server/client/cases/find.ts | 2 + .../plugins/cases/server/client/cases/get.ts | 27 ++- .../plugins/cases/server/client/cases/push.ts | 16 +- .../cases/server/client/cases/update.ts | 5 + x-pack/plugins/cases/server/client/client.ts | 31 +++ .../cases/server/client/configure/client.ts | 37 ++- x-pack/plugins/cases/server/client/factory.ts | 35 +-- .../cases/server/client/stats/client.ts | 5 + .../cases/server/client/sub_cases/client.ts | 36 ++- .../cases/server/client/typedoc_interfaces.ts | 57 +++++ x-pack/plugins/cases/server/client/types.ts | 3 + .../server/client/user_actions/client.ts | 24 +- .../cases/server/client/user_actions/get.ts | 8 +- .../server/connectors/case/index.test.ts | 2 +- .../cases/server/connectors/case/index.ts | 95 ++++---- x-pack/plugins/cases/server/index.ts | 2 + x-pack/plugins/cases/server/plugin.ts | 63 ++--- .../server/routes/api/cases/find_cases.ts | 2 +- .../server/routes/api/cases/push_case.ts | 6 - .../routes/api/sub_case/delete_sub_cases.ts | 2 +- .../routes/api/sub_case/find_sub_cases.ts | 2 +- .../routes/api/sub_case/patch_sub_cases.ts | 2 +- .../plugins/cases/server/routes/api/types.ts | 13 - x-pack/plugins/cases/server/types.ts | 2 - .../plugins/cases_client_user/kibana.json | 10 + .../plugins/cases_client_user/package.json | 14 ++ .../plugins/cases_client_user/server/index.ts | 12 + .../cases_client_user/server/plugin.ts | 68 ++++++ .../common/client/update_alert_status.ts | 167 +++++++++++++ .../security_and_spaces/tests/common/index.ts | 1 + 94 files changed, 3485 insertions(+), 196 deletions(-) create mode 100644 x-pack/plugins/cases/docs/README.md create mode 100644 x-pack/plugins/cases/docs/cases_client/cases_client_api.md create mode 100644 x-pack/plugins/cases/docs/cases_client/classes/client.casesclient.md create mode 100644 x-pack/plugins/cases/docs/cases_client/interfaces/attachments_add.addargs.md create mode 100644 x-pack/plugins/cases/docs/cases_client/interfaces/attachments_client.attachmentssubclient.md create mode 100644 x-pack/plugins/cases/docs/cases_client/interfaces/attachments_delete.deleteallargs.md create mode 100644 x-pack/plugins/cases/docs/cases_client/interfaces/attachments_delete.deleteargs.md create mode 100644 x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.findargs.md create mode 100644 x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getallargs.md create mode 100644 x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getargs.md create mode 100644 x-pack/plugins/cases/docs/cases_client/interfaces/attachments_update.updateargs.md create mode 100644 x-pack/plugins/cases/docs/cases_client/interfaces/cases_client.casessubclient.md create mode 100644 x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.caseidsbyalertidparams.md create mode 100644 x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.getparams.md create mode 100644 x-pack/plugins/cases/docs/cases_client/interfaces/cases_push.pushparams.md create mode 100644 x-pack/plugins/cases/docs/cases_client/interfaces/configure_client.configuresubclient.md create mode 100644 x-pack/plugins/cases/docs/cases_client/interfaces/stats_client.statssubclient.md create mode 100644 x-pack/plugins/cases/docs/cases_client/interfaces/sub_cases_client.subcasesclient.md create mode 100644 x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.iallcommentsresponse.md create mode 100644 x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icasepostrequest.md create mode 100644 x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icaseresponse.md create mode 100644 x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icasesconfigurepatch.md create mode 100644 x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icasesconfigurerequest.md create mode 100644 x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icasesconfigureresponse.md create mode 100644 x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icasesfindrequest.md create mode 100644 x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icasesfindresponse.md create mode 100644 x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icasespatchrequest.md create mode 100644 x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icasesresponse.md create mode 100644 x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icaseuseractionsresponse.md create mode 100644 x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icommentsresponse.md create mode 100644 x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.isubcaseresponse.md create mode 100644 x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.isubcasesfindresponse.md create mode 100644 x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.isubcasesresponse.md create mode 100644 x-pack/plugins/cases/docs/cases_client/interfaces/user_actions_client.useractionget.md create mode 100644 x-pack/plugins/cases/docs/cases_client/interfaces/user_actions_client.useractionssubclient.md create mode 100644 x-pack/plugins/cases/docs/cases_client/modules/attachments_add.md create mode 100644 x-pack/plugins/cases/docs/cases_client/modules/attachments_client.md create mode 100644 x-pack/plugins/cases/docs/cases_client/modules/attachments_delete.md create mode 100644 x-pack/plugins/cases/docs/cases_client/modules/attachments_get.md create mode 100644 x-pack/plugins/cases/docs/cases_client/modules/attachments_update.md create mode 100644 x-pack/plugins/cases/docs/cases_client/modules/cases_client.md create mode 100644 x-pack/plugins/cases/docs/cases_client/modules/cases_get.md create mode 100644 x-pack/plugins/cases/docs/cases_client/modules/cases_push.md create mode 100644 x-pack/plugins/cases/docs/cases_client/modules/client.md create mode 100644 x-pack/plugins/cases/docs/cases_client/modules/configure_client.md create mode 100644 x-pack/plugins/cases/docs/cases_client/modules/stats_client.md create mode 100644 x-pack/plugins/cases/docs/cases_client/modules/sub_cases_client.md create mode 100644 x-pack/plugins/cases/docs/cases_client/modules/typedoc_interfaces.md create mode 100644 x-pack/plugins/cases/docs/cases_client/modules/user_actions_client.md create mode 100644 x-pack/plugins/cases/docs/cases_client_typedoc.json create mode 100644 x-pack/plugins/cases/server/client/typedoc_interfaces.ts create mode 100644 x-pack/test/case_api_integration/common/fixtures/plugins/cases_client_user/kibana.json create mode 100644 x-pack/test/case_api_integration/common/fixtures/plugins/cases_client_user/package.json create mode 100644 x-pack/test/case_api_integration/common/fixtures/plugins/cases_client_user/server/index.ts create mode 100644 x-pack/test/case_api_integration/common/fixtures/plugins/cases_client_user/server/plugin.ts create mode 100644 x-pack/test/case_api_integration/security_and_spaces/tests/common/client/update_alert_status.ts diff --git a/x-pack/plugins/cases/README.md b/x-pack/plugins/cases/README.md index 14afe89829a681..44750d2dd74e52 100644 --- a/x-pack/plugins/cases/README.md +++ b/x-pack/plugins/cases/README.md @@ -14,6 +14,7 @@ Case management in Kibana ## Table of Contents - [Cases API](#cases-api) +- [Cases Client API](#cases-client-api) - [Cases UI](#cases-ui) - [Case Action Type](#case-action-type) _feature in development, disabled by default_ @@ -21,6 +22,9 @@ Case management in Kibana ## Cases API [**Explore the API docs »**](https://www.elastic.co/guide/en/security/current/cases-api-overview.html) +## Cases Client API +[**Cases Client API docs**][cases-client-api-docs] + ## Cases UI #### Embed Cases UI components in any Kibana plugin @@ -263,4 +267,4 @@ For IBM Resilient connectors: [all-cases-modal-img]: images/all_cases_selector_modal.png [recent-cases-img]: images/recent_cases.png [case-view-img]: images/case_view.png - +[cases-client-api-docs]: docs/cases_client/cases_client_api.md diff --git a/x-pack/plugins/cases/common/api/cases/case.ts b/x-pack/plugins/cases/common/api/cases/case.ts index 05ab1a464071ab..b3f7952a61ee7f 100644 --- a/x-pack/plugins/cases/common/api/cases/case.ts +++ b/x-pack/plugins/cases/common/api/cases/case.ts @@ -31,13 +31,37 @@ const SettingsRt = rt.type({ }); const CaseBasicRt = rt.type({ + /** + * The description of the case + */ description: rt.string, + /** + * The current status of the case (open, closed, in-progress) + */ status: CaseStatusRt, + /** + * The identifying strings for filter a case + */ tags: rt.array(rt.string), + /** + * The title of a case + */ title: rt.string, + /** + * The type of a case (individual or collection) + */ [caseTypeField]: CaseTypeRt, + /** + * The external system that the case can be synced with + */ connector: CaseConnectorRt, + /** + * The alert sync settings + */ settings: SettingsRt, + /** + * The plugin owner of the case + */ owner: rt.string, }); @@ -74,11 +98,30 @@ export const CaseAttributesRt = rt.intersection([ ]); const CasePostRequestNoTypeRt = rt.type({ + /** + * Description of the case + */ description: rt.string, + /** + * Identifiers for the case. + */ tags: rt.array(rt.string), + /** + * Title of the case + */ title: rt.string, + /** + * The external configuration for the case + */ connector: CaseConnectorRt, + /** + * Sync settings for alerts + */ settings: SettingsRt, + /** + * The owner here must match the string used when a plugin registers a feature with access to the cases plugin. The user + * creating this case must also be granted access to that plugin's feature. + */ owner: rt.string, }); @@ -97,27 +140,77 @@ export const CasesClientPostRequestRt = rt.type({ * has all the necessary fields. CasesClientPostRequestRt is used for validation. */ export const CasePostRequestRt = rt.intersection([ + /** + * The case type: an individual case (one without children) or a collection case (one with children) + */ rt.partial({ [caseTypeField]: CaseTypeRt }), CasePostRequestNoTypeRt, ]); export const CasesFindRequestRt = rt.partial({ + /** + * Type of a case (individual, or collection) + */ type: CaseTypeRt, + /** + * Tags to filter by + */ tags: rt.union([rt.array(rt.string), rt.string]), + /** + * The status of the case (open, closed, in-progress) + */ status: CaseStatusRt, + /** + * The reporters to filter by + */ reporters: rt.union([rt.array(rt.string), rt.string]), + /** + * Operator to use for the `search` field + */ defaultSearchOperator: rt.union([rt.literal('AND'), rt.literal('OR')]), + /** + * The fields in the entity to return in the response + */ fields: rt.array(rt.string), + /** + * The page of objects to return + */ page: NumberFromString, + /** + * The number of objects to include in each page + */ perPage: NumberFromString, + /** + * An Elasticsearch simple_query_string + */ search: rt.string, + /** + * The fields to perform the simple_query_string parsed query against + */ searchFields: rt.union([rt.array(rt.string), rt.string]), + /** + * The field to use for sorting the found objects. + * + * This only supports, `create_at`, `closed_at`, and `status` + */ sortField: rt.string, + /** + * The order to sort by + */ sortOrder: rt.union([rt.literal('desc'), rt.literal('asc')]), + /** + * The owner(s) to filter by. The user making the request must have privileges to retrieve cases of that + * ownership or they will be ignored. If no owner is included, then all ownership types will be included in the response + * that the user has access to. + */ owner: rt.union([rt.array(rt.string), rt.string]), }); export const CasesByAlertIDRequestRt = rt.partial({ + /** + * The type of cases to retrieve given an alert ID. If no owner is provided, all cases + * that the user has access to will be returned. + */ owner: rt.union([rt.array(rt.string), rt.string]), }); @@ -148,6 +241,9 @@ export const CasesFindResponseRt = rt.intersection([ export const CasePatchRequestRt = rt.intersection([ rt.partial(CaseBasicRt.props), + /** + * The saved object ID and version + */ rt.type({ id: rt.string, version: rt.string }), ]); @@ -180,6 +276,10 @@ export const ExternalServiceResponseRt = rt.intersection([ ]); export const AllTagsFindRequestRt = rt.partial({ + /** + * The owner of the cases to retrieve the tags from. If no owner is provided the tags from all cases + * that the user has access to will be returned. + */ owner: rt.union([rt.array(rt.string), rt.string]), }); diff --git a/x-pack/plugins/cases/common/api/cases/comment.ts b/x-pack/plugins/cases/common/api/cases/comment.ts index 7cc64bbc1e8565..5bc8da95639c85 100644 --- a/x-pack/plugins/cases/common/api/cases/comment.ts +++ b/x-pack/plugins/cases/common/api/cases/comment.ts @@ -133,6 +133,9 @@ export const AllCommentsResponseRt = rt.array(CommentResponseRt); export const FindQueryParamsRt = rt.partial({ ...SavedObjectFindOptionsRt.props, + /** + * If specified the attachments found will be associated to a sub case instead of a case object + */ subCaseId: rt.string, }); diff --git a/x-pack/plugins/cases/common/api/cases/configure.ts b/x-pack/plugins/cases/common/api/cases/configure.ts index eeeb9ed4ebd042..2814dd44f513ff 100644 --- a/x-pack/plugins/cases/common/api/cases/configure.ts +++ b/x-pack/plugins/cases/common/api/cases/configure.ts @@ -16,8 +16,17 @@ import { OWNER_FIELD } from './constants'; const ClosureTypeRT = rt.union([rt.literal('close-by-user'), rt.literal('close-by-pushing')]); const CasesConfigureBasicRt = rt.type({ + /** + * The external connector + */ connector: CaseConnectorRt, + /** + * Whether to close the case after it has been synced with the external system + */ closure_type: ClosureTypeRT, + /** + * The plugin owner that manages this configuration + */ owner: rt.string, }); @@ -53,6 +62,10 @@ export const CaseConfigureResponseRt = rt.intersection([ ]); export const GetConfigureFindRequestRt = rt.partial({ + /** + * The configuration plugin owner to filter the search by. If this is left empty the results will include all configurations + * that the user has permissions to access + */ owner: rt.union([rt.array(rt.string), rt.string]), }); diff --git a/x-pack/plugins/cases/common/api/cases/sub_case.ts b/x-pack/plugins/cases/common/api/cases/sub_case.ts index 826654cab2d7f1..654b74276733bf 100644 --- a/x-pack/plugins/cases/common/api/cases/sub_case.ts +++ b/x-pack/plugins/cases/common/api/cases/sub_case.ts @@ -14,6 +14,9 @@ import { CasesStatusResponseRt } from './status'; import { CaseStatusRt } from './status'; const SubCaseBasicRt = rt.type({ + /** + * The status of the sub case (open, closed, in-progress) + */ status: CaseStatusRt, }); @@ -31,14 +34,41 @@ export const SubCaseAttributesRt = rt.intersection([ ]); export const SubCasesFindRequestRt = rt.partial({ + /** + * The status of the sub case (open, closed, in-progress) + */ status: CaseStatusRt, + /** + * Operator to use for the `search` field + */ defaultSearchOperator: rt.union([rt.literal('AND'), rt.literal('OR')]), + /** + * The fields in the entity to return in the response + */ fields: rt.array(rt.string), + /** + * The page of objects to return + */ page: NumberFromString, + /** + * The number of objects to include in each page + */ perPage: NumberFromString, + /** + * An Elasticsearch simple_query_string + */ search: rt.string, + /** + * The fields to perform the simple_query_string parsed query against + */ searchFields: rt.array(rt.string), + /** + * The field to use for sorting the found objects. + */ sortField: rt.string, + /** + * The order to sort by + */ sortOrder: rt.union([rt.literal('desc'), rt.literal('asc')]), owner: rt.string, }); diff --git a/x-pack/plugins/cases/common/api/saved_object.ts b/x-pack/plugins/cases/common/api/saved_object.ts index e0ae4ee82c490f..2ed6ec2acdfe46 100644 --- a/x-pack/plugins/cases/common/api/saved_object.ts +++ b/x-pack/plugins/cases/common/api/saved_object.ts @@ -23,16 +23,49 @@ export const NumberFromString = new rt.Type( const ReferenceRt = rt.type({ id: rt.string, type: rt.string }); export const SavedObjectFindOptionsRt = rt.partial({ + /** + * The default operator to use for the simple_query_string + */ defaultSearchOperator: rt.union([rt.literal('AND'), rt.literal('OR')]), + /** + * The operator for controlling the logic of the `hasReference` field + */ hasReferenceOperator: rt.union([rt.literal('AND'), rt.literal('OR')]), + /** + * Filter by objects that have an association to another object + */ hasReference: rt.union([rt.array(ReferenceRt), ReferenceRt]), + /** + * The fields to return in the attributes key of the response + */ fields: rt.array(rt.string), + /** + * The filter is a KQL string with the caveat that if you filter with an attribute from your saved object type, it should look like that: savedObjectType.attributes.title: "myTitle". However, If you use a root attribute of a saved object such as updated_at, you will have to define your filter like that: savedObjectType.updated_at > 2018-12-22 + */ filter: rt.string, + /** + * The page of objects to return + */ page: NumberFromString, + /** + * The number of objects to return for a page + */ perPage: NumberFromString, + /** + * An Elasticsearch simple_query_string query that filters the objects in the response + */ search: rt.string, + /** + * The fields to perform the simple_query_string parsed query against + */ searchFields: rt.array(rt.string), + /** + * Sorts the response. Includes "root" and "type" fields. "root" fields exist for all saved objects, such as "updated_at". "type" fields are specific to an object type, such as fields returned in the attributes key of the response. When a single type is defined in the type parameter, the "root" and "type" fields are allowed, and validity checks are made in that order. When multiple types are defined in the type parameter, only "root" fields are allowed + */ sortField: rt.string, + /** + * Order to sort the response + */ sortOrder: rt.union([rt.literal('desc'), rt.literal('asc')]), }); diff --git a/x-pack/plugins/cases/docs/README.md b/x-pack/plugins/cases/docs/README.md new file mode 100644 index 00000000000000..85482d98dc5096 --- /dev/null +++ b/x-pack/plugins/cases/docs/README.md @@ -0,0 +1,37 @@ +# Cases Client API Docs + +This directory contains generated docs using `typedoc` for the cases client API that can be called from other server +plugins. This README will describe how to generate a new version of these markdown docs in the event that new methods +or parameters are added. + +## TypeDoc Info + +See more info at: +and: for the markdown plugin + +## Install dependencies + +```bash +yarn global add typedoc typedoc-plugin-markdown +``` + +## Generate the docs + +```bash +cd x-pack/plugins/cases/docs +npx typedoc --options cases_client_typedoc.json +``` + +After running the above commands the files in the `server` directory will be updated to match the new tsdocs. +If additional markdown directory should be created we can create a new typedoc configuration file and adjust the `out` +directory accordingly. + +## Troubleshooting + +If you run into tsc errors that seem unrelated to the cases plugin try executing these commands before running `typedoc` + +```bash +cd +npx yarn kbn bootstrap +node scripts/build_ts_refs.js --clean --no-cache +``` diff --git a/x-pack/plugins/cases/docs/cases_client/cases_client_api.md b/x-pack/plugins/cases/docs/cases_client/cases_client_api.md new file mode 100644 index 00000000000000..d7e75af3142e64 --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/cases_client_api.md @@ -0,0 +1,22 @@ +Cases Client API Interface + +# Cases Client API Interface + +## Table of contents + +### Modules + +- [attachments/add](modules/attachments_add.md) +- [attachments/client](modules/attachments_client.md) +- [attachments/delete](modules/attachments_delete.md) +- [attachments/get](modules/attachments_get.md) +- [attachments/update](modules/attachments_update.md) +- [cases/client](modules/cases_client.md) +- [cases/get](modules/cases_get.md) +- [cases/push](modules/cases_push.md) +- [client](modules/client.md) +- [configure/client](modules/configure_client.md) +- [stats/client](modules/stats_client.md) +- [sub\_cases/client](modules/sub_cases_client.md) +- [typedoc\_interfaces](modules/typedoc_interfaces.md) +- [user\_actions/client](modules/user_actions_client.md) diff --git a/x-pack/plugins/cases/docs/cases_client/classes/client.casesclient.md b/x-pack/plugins/cases/docs/cases_client/classes/client.casesclient.md new file mode 100644 index 00000000000000..8f6983dc4f769d --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/classes/client.casesclient.md @@ -0,0 +1,178 @@ +[Cases Client API Interface](../cases_client_api.md) / [client](../modules/client.md) / CasesClient + +# Class: CasesClient + +[client](../modules/client.md).CasesClient + +Client wrapper that contains accessor methods for individual entities within the cases system. + +## Table of contents + +### Constructors + +- [constructor](client.casesclient.md#constructor) + +### Properties + +- [\_attachments](client.casesclient.md#_attachments) +- [\_cases](client.casesclient.md#_cases) +- [\_casesClientInternal](client.casesclient.md#_casesclientinternal) +- [\_configure](client.casesclient.md#_configure) +- [\_stats](client.casesclient.md#_stats) +- [\_subCases](client.casesclient.md#_subcases) +- [\_userActions](client.casesclient.md#_useractions) + +### Accessors + +- [attachments](client.casesclient.md#attachments) +- [cases](client.casesclient.md#cases) +- [configure](client.casesclient.md#configure) +- [stats](client.casesclient.md#stats) +- [subCases](client.casesclient.md#subcases) +- [userActions](client.casesclient.md#useractions) + +## Constructors + +### constructor + +\+ **new CasesClient**(`args`: CasesClientArgs): [*CasesClient*](client.casesclient.md) + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `args` | CasesClientArgs | + +**Returns:** [*CasesClient*](client.casesclient.md) + +Defined in: [client.ts:28](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/client.ts#L28) + +## Properties + +### \_attachments + +• `Private` `Readonly` **\_attachments**: [*AttachmentsSubClient*](../interfaces/attachments_client.attachmentssubclient.md) + +Defined in: [client.ts:24](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/client.ts#L24) + +___ + +### \_cases + +• `Private` `Readonly` **\_cases**: [*CasesSubClient*](../interfaces/cases_client.casessubclient.md) + +Defined in: [client.ts:23](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/client.ts#L23) + +___ + +### \_casesClientInternal + +• `Private` `Readonly` **\_casesClientInternal**: *CasesClientInternal* + +Defined in: [client.ts:22](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/client.ts#L22) + +___ + +### \_configure + +• `Private` `Readonly` **\_configure**: [*ConfigureSubClient*](../interfaces/configure_client.configuresubclient.md) + +Defined in: [client.ts:27](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/client.ts#L27) + +___ + +### \_stats + +• `Private` `Readonly` **\_stats**: [*StatsSubClient*](../interfaces/stats_client.statssubclient.md) + +Defined in: [client.ts:28](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/client.ts#L28) + +___ + +### \_subCases + +• `Private` `Readonly` **\_subCases**: [*SubCasesClient*](../interfaces/sub_cases_client.subcasesclient.md) + +Defined in: [client.ts:26](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/client.ts#L26) + +___ + +### \_userActions + +• `Private` `Readonly` **\_userActions**: [*UserActionsSubClient*](../interfaces/user_actions_client.useractionssubclient.md) + +Defined in: [client.ts:25](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/client.ts#L25) + +## Accessors + +### attachments + +• get **attachments**(): [*AttachmentsSubClient*](../interfaces/attachments_client.attachmentssubclient.md) + +Retrieves an interface for interacting with attachments (comments) entities. + +**Returns:** [*AttachmentsSubClient*](../interfaces/attachments_client.attachmentssubclient.md) + +Defined in: [client.ts:50](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/client.ts#L50) + +___ + +### cases + +• get **cases**(): [*CasesSubClient*](../interfaces/cases_client.casessubclient.md) + +Retrieves an interface for interacting with cases entities. + +**Returns:** [*CasesSubClient*](../interfaces/cases_client.casessubclient.md) + +Defined in: [client.ts:43](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/client.ts#L43) + +___ + +### configure + +• get **configure**(): [*ConfigureSubClient*](../interfaces/configure_client.configuresubclient.md) + +Retrieves an interface for interacting with the configuration of external connectors for the plugin entities. + +**Returns:** [*ConfigureSubClient*](../interfaces/configure_client.configuresubclient.md) + +Defined in: [client.ts:76](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/client.ts#L76) + +___ + +### stats + +• get **stats**(): [*StatsSubClient*](../interfaces/stats_client.statssubclient.md) + +Retrieves an interface for retrieving statistics related to the cases entities. + +**Returns:** [*StatsSubClient*](../interfaces/stats_client.statssubclient.md) + +Defined in: [client.ts:83](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/client.ts#L83) + +___ + +### subCases + +• get **subCases**(): [*SubCasesClient*](../interfaces/sub_cases_client.subcasesclient.md) + +Retrieves an interface for interacting with the case as a connector entities. + +Currently this functionality is disabled and will throw an error if this function is called. + +**Returns:** [*SubCasesClient*](../interfaces/sub_cases_client.subcasesclient.md) + +Defined in: [client.ts:66](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/client.ts#L66) + +___ + +### userActions + +• get **userActions**(): [*UserActionsSubClient*](../interfaces/user_actions_client.useractionssubclient.md) + +Retrieves an interface for interacting with the user actions associated with the plugin entities. + +**Returns:** [*UserActionsSubClient*](../interfaces/user_actions_client.useractionssubclient.md) + +Defined in: [client.ts:57](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/client.ts#L57) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_add.addargs.md b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_add.addargs.md new file mode 100644 index 00000000000000..0e67fb488edebb --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_add.addargs.md @@ -0,0 +1,34 @@ +[Cases Client API Interface](../cases_client_api.md) / [attachments/add](../modules/attachments_add.md) / AddArgs + +# Interface: AddArgs + +[attachments/add](../modules/attachments_add.md).AddArgs + +The arguments needed for creating a new attachment to a case. + +## Table of contents + +### Properties + +- [caseId](attachments_add.addargs.md#caseid) +- [comment](attachments_add.addargs.md#comment) + +## Properties + +### caseId + +• **caseId**: *string* + +The case ID that this attachment will be associated with + +Defined in: [attachments/add.ts:308](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/add.ts#L308) + +___ + +### comment + +• **comment**: { `comment`: *string* ; `owner`: *string* ; `type`: user } \| { `alertId`: *string* \| *string*[] ; `index`: *string* \| *string*[] ; `owner`: *string* ; `rule`: { id: string \| null; name: string \| null; } ; `type`: alert \| generatedAlert } + +The attachment values. + +Defined in: [attachments/add.ts:312](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/add.ts#L312) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_client.attachmentssubclient.md b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_client.attachmentssubclient.md new file mode 100644 index 00000000000000..13a7a5a109a511 --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_client.attachmentssubclient.md @@ -0,0 +1,147 @@ +[Cases Client API Interface](../cases_client_api.md) / [attachments/client](../modules/attachments_client.md) / AttachmentsSubClient + +# Interface: AttachmentsSubClient + +[attachments/client](../modules/attachments_client.md).AttachmentsSubClient + +API for interacting with the attachments to a case. + +## Table of contents + +### Methods + +- [add](attachments_client.attachmentssubclient.md#add) +- [delete](attachments_client.attachmentssubclient.md#delete) +- [deleteAll](attachments_client.attachmentssubclient.md#deleteall) +- [find](attachments_client.attachmentssubclient.md#find) +- [get](attachments_client.attachmentssubclient.md#get) +- [getAll](attachments_client.attachmentssubclient.md#getall) +- [update](attachments_client.attachmentssubclient.md#update) + +## Methods + +### add + +▸ **add**(`params`: [*AddArgs*](attachments_add.addargs.md)): *Promise*<[*ICaseResponse*](typedoc_interfaces.icaseresponse.md)\> + +Adds an attachment to a case. + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `params` | [*AddArgs*](attachments_add.addargs.md) | + +**Returns:** *Promise*<[*ICaseResponse*](typedoc_interfaces.icaseresponse.md)\> + +Defined in: [attachments/client.ts:25](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/client.ts#L25) + +___ + +### delete + +▸ **delete**(`deleteArgs`: [*DeleteArgs*](attachments_delete.deleteargs.md)): *Promise* + +Deletes a single attachment for a specific case. + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `deleteArgs` | [*DeleteArgs*](attachments_delete.deleteargs.md) | + +**Returns:** *Promise* + +Defined in: [attachments/client.ts:33](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/client.ts#L33) + +___ + +### deleteAll + +▸ **deleteAll**(`deleteAllArgs`: [*DeleteAllArgs*](attachments_delete.deleteallargs.md)): *Promise* + +Deletes all attachments associated with a single case. + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `deleteAllArgs` | [*DeleteAllArgs*](attachments_delete.deleteallargs.md) | + +**Returns:** *Promise* + +Defined in: [attachments/client.ts:29](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/client.ts#L29) + +___ + +### find + +▸ **find**(`findArgs`: [*FindArgs*](attachments_get.findargs.md)): *Promise*<[*ICommentsResponse*](typedoc_interfaces.icommentsresponse.md)\> + +Retrieves all comments matching the search criteria. + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `findArgs` | [*FindArgs*](attachments_get.findargs.md) | + +**Returns:** *Promise*<[*ICommentsResponse*](typedoc_interfaces.icommentsresponse.md)\> + +Defined in: [attachments/client.ts:37](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/client.ts#L37) + +___ + +### get + +▸ **get**(`getArgs`: [*GetArgs*](attachments_get.getargs.md)): *Promise*<{ `comment`: *string* ; `owner`: *string* ; `type`: user } & { `associationType`: AssociationType ; `created_at`: *string* ; `created_by`: { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `owner`: *string* ; `pushed_at`: ``null`` \| *string* ; `pushed_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `updated_at`: ``null`` \| *string* ; `updated_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } } & { `id`: *string* ; `version`: *string* } & { `alertId`: *string* \| *string*[] ; `index`: *string* \| *string*[] ; `owner`: *string* ; `rule`: { id: string \| null; name: string \| null; } ; `type`: alert \| generatedAlert } & { `associationType`: AssociationType ; `created_at`: *string* ; `created_by`: { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `owner`: *string* ; `pushed_at`: ``null`` \| *string* ; `pushed_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `updated_at`: ``null`` \| *string* ; `updated_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } } & { `id`: *string* ; `version`: *string* }\> + +Retrieves a single attachment for a case. + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `getArgs` | [*GetArgs*](attachments_get.getargs.md) | + +**Returns:** *Promise*<{ `comment`: *string* ; `owner`: *string* ; `type`: user } & { `associationType`: AssociationType ; `created_at`: *string* ; `created_by`: { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `owner`: *string* ; `pushed_at`: ``null`` \| *string* ; `pushed_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `updated_at`: ``null`` \| *string* ; `updated_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } } & { `id`: *string* ; `version`: *string* } & { `alertId`: *string* \| *string*[] ; `index`: *string* \| *string*[] ; `owner`: *string* ; `rule`: { id: string \| null; name: string \| null; } ; `type`: alert \| generatedAlert } & { `associationType`: AssociationType ; `created_at`: *string* ; `created_by`: { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `owner`: *string* ; `pushed_at`: ``null`` \| *string* ; `pushed_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `updated_at`: ``null`` \| *string* ; `updated_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } } & { `id`: *string* ; `version`: *string* }\> + +Defined in: [attachments/client.ts:45](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/client.ts#L45) + +___ + +### getAll + +▸ **getAll**(`getAllArgs`: [*GetAllArgs*](attachments_get.getallargs.md)): *Promise*<[*IAllCommentsResponse*](typedoc_interfaces.iallcommentsresponse.md)\> + +Gets all attachments for a single case. + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `getAllArgs` | [*GetAllArgs*](attachments_get.getallargs.md) | + +**Returns:** *Promise*<[*IAllCommentsResponse*](typedoc_interfaces.iallcommentsresponse.md)\> + +Defined in: [attachments/client.ts:41](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/client.ts#L41) + +___ + +### update + +▸ **update**(`updateArgs`: [*UpdateArgs*](attachments_update.updateargs.md)): *Promise*<[*ICaseResponse*](typedoc_interfaces.icaseresponse.md)\> + +Updates a specific attachment. + +The request must include all fields for the attachment. Even the fields that are not changing. + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `updateArgs` | [*UpdateArgs*](attachments_update.updateargs.md) | + +**Returns:** *Promise*<[*ICaseResponse*](typedoc_interfaces.icaseresponse.md)\> + +Defined in: [attachments/client.ts:51](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/client.ts#L51) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_delete.deleteallargs.md b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_delete.deleteallargs.md new file mode 100644 index 00000000000000..a0f5962fcc453a --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_delete.deleteallargs.md @@ -0,0 +1,34 @@ +[Cases Client API Interface](../cases_client_api.md) / [attachments/delete](../modules/attachments_delete.md) / DeleteAllArgs + +# Interface: DeleteAllArgs + +[attachments/delete](../modules/attachments_delete.md).DeleteAllArgs + +Parameters for deleting all comments of a case or sub case. + +## Table of contents + +### Properties + +- [caseID](attachments_delete.deleteallargs.md#caseid) +- [subCaseID](attachments_delete.deleteallargs.md#subcaseid) + +## Properties + +### caseID + +• **caseID**: *string* + +The case ID to delete all attachments for + +Defined in: [attachments/delete.ts:26](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/delete.ts#L26) + +___ + +### subCaseID + +• `Optional` **subCaseID**: *string* + +If specified the caseID will be ignored and this value will be used to find a sub case for deleting all the attachments + +Defined in: [attachments/delete.ts:30](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/delete.ts#L30) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_delete.deleteargs.md b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_delete.deleteargs.md new file mode 100644 index 00000000000000..ab20f1b64b2a43 --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_delete.deleteargs.md @@ -0,0 +1,45 @@ +[Cases Client API Interface](../cases_client_api.md) / [attachments/delete](../modules/attachments_delete.md) / DeleteArgs + +# Interface: DeleteArgs + +[attachments/delete](../modules/attachments_delete.md).DeleteArgs + +Parameters for deleting a single attachment of a case or sub case. + +## Table of contents + +### Properties + +- [attachmentID](attachments_delete.deleteargs.md#attachmentid) +- [caseID](attachments_delete.deleteargs.md#caseid) +- [subCaseID](attachments_delete.deleteargs.md#subcaseid) + +## Properties + +### attachmentID + +• **attachmentID**: *string* + +The attachment ID to delete + +Defined in: [attachments/delete.ts:44](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/delete.ts#L44) + +___ + +### caseID + +• **caseID**: *string* + +The case ID to delete an attachment from + +Defined in: [attachments/delete.ts:40](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/delete.ts#L40) + +___ + +### subCaseID + +• `Optional` **subCaseID**: *string* + +If specified the caseID will be ignored and this value will be used to find a sub case for deleting the attachment + +Defined in: [attachments/delete.ts:48](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/delete.ts#L48) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.findargs.md b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.findargs.md new file mode 100644 index 00000000000000..2a019220f82199 --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.findargs.md @@ -0,0 +1,51 @@ +[Cases Client API Interface](../cases_client_api.md) / [attachments/get](../modules/attachments_get.md) / FindArgs + +# Interface: FindArgs + +[attachments/get](../modules/attachments_get.md).FindArgs + +Parameters for finding attachments of a case + +## Table of contents + +### Properties + +- [caseID](attachments_get.findargs.md#caseid) +- [queryParams](attachments_get.findargs.md#queryparams) + +## Properties + +### caseID + +• **caseID**: *string* + +The case ID for finding associated attachments + +Defined in: [attachments/get.ts:48](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/get.ts#L48) + +___ + +### queryParams + +• `Optional` **queryParams**: *object* + +Optional parameters for filtering the returned attachments + +#### Type declaration + +| Name | Type | +| :------ | :------ | +| `defaultSearchOperator` | *undefined* \| ``"AND"`` \| ``"OR"`` | +| `fields` | *undefined* \| *string*[] | +| `filter` | *undefined* \| *string* | +| `hasReference` | *undefined* \| { `id`: *string* ; `type`: *string* } \| { `id`: *string* ; `type`: *string* }[] | +| `hasReferenceOperator` | *undefined* \| ``"AND"`` \| ``"OR"`` | +| `page` | *undefined* \| *number* | +| `perPage` | *undefined* \| *number* | +| `search` | *undefined* \| *string* | +| `searchFields` | *undefined* \| *string*[] | +| `sortField` | *undefined* \| *string* | +| `sortOrder` | *undefined* \| ``"desc"`` \| ``"asc"`` | +| `subCaseId` | *undefined* \| *string* | + +Defined in: [attachments/get.ts:52](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/get.ts#L52) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getallargs.md b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getallargs.md new file mode 100644 index 00000000000000..c6f2123ee60562 --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getallargs.md @@ -0,0 +1,45 @@ +[Cases Client API Interface](../cases_client_api.md) / [attachments/get](../modules/attachments_get.md) / GetAllArgs + +# Interface: GetAllArgs + +[attachments/get](../modules/attachments_get.md).GetAllArgs + +Parameters for retrieving all attachments of a case + +## Table of contents + +### Properties + +- [caseID](attachments_get.getallargs.md#caseid) +- [includeSubCaseComments](attachments_get.getallargs.md#includesubcasecomments) +- [subCaseID](attachments_get.getallargs.md#subcaseid) + +## Properties + +### caseID + +• **caseID**: *string* + +The case ID to retrieve all attachments for + +Defined in: [attachments/get.ts:62](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/get.ts#L62) + +___ + +### includeSubCaseComments + +• `Optional` **includeSubCaseComments**: *boolean* + +Optionally include the attachments associated with a sub case + +Defined in: [attachments/get.ts:66](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/get.ts#L66) + +___ + +### subCaseID + +• `Optional` **subCaseID**: *string* + +If included the case ID will be ignored and the attachments will be retrieved from the specified ID of the sub case + +Defined in: [attachments/get.ts:70](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/get.ts#L70) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getargs.md b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getargs.md new file mode 100644 index 00000000000000..ffec56fc54c83f --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getargs.md @@ -0,0 +1,32 @@ +[Cases Client API Interface](../cases_client_api.md) / [attachments/get](../modules/attachments_get.md) / GetArgs + +# Interface: GetArgs + +[attachments/get](../modules/attachments_get.md).GetArgs + +## Table of contents + +### Properties + +- [attachmentID](attachments_get.getargs.md#attachmentid) +- [caseID](attachments_get.getargs.md#caseid) + +## Properties + +### attachmentID + +• **attachmentID**: *string* + +The ID of the attachment to retrieve + +Defined in: [attachments/get.ts:81](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/get.ts#L81) + +___ + +### caseID + +• **caseID**: *string* + +The ID of the case to retrieve an attachment from + +Defined in: [attachments/get.ts:77](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/get.ts#L77) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_update.updateargs.md b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_update.updateargs.md new file mode 100644 index 00000000000000..083723d76b10e8 --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_update.updateargs.md @@ -0,0 +1,45 @@ +[Cases Client API Interface](../cases_client_api.md) / [attachments/update](../modules/attachments_update.md) / UpdateArgs + +# Interface: UpdateArgs + +[attachments/update](../modules/attachments_update.md).UpdateArgs + +Parameters for updating a single attachment + +## Table of contents + +### Properties + +- [caseID](attachments_update.updateargs.md#caseid) +- [subCaseID](attachments_update.updateargs.md#subcaseid) +- [updateRequest](attachments_update.updateargs.md#updaterequest) + +## Properties + +### caseID + +• **caseID**: *string* + +The ID of the case that is associated with this attachment + +Defined in: [attachments/update.ts:29](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/update.ts#L29) + +___ + +### subCaseID + +• `Optional` **subCaseID**: *string* + +The ID of a sub case, if specified a sub case will be searched for to perform the attachment update instead of on a case + +Defined in: [attachments/update.ts:37](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/update.ts#L37) + +___ + +### updateRequest + +• **updateRequest**: { `comment`: *string* ; `owner`: *string* ; `type`: user } & { `id`: *string* ; `version`: *string* } & { `alertId`: *string* \| *string*[] ; `index`: *string* \| *string*[] ; `owner`: *string* ; `rule`: { id: string \| null; name: string \| null; } ; `type`: alert \| generatedAlert } & { `id`: *string* ; `version`: *string* } + +The full attachment request with the fields updated with appropriate values + +Defined in: [attachments/update.ts:33](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/update.ts#L33) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/cases_client.casessubclient.md b/x-pack/plugins/cases/docs/cases_client/interfaces/cases_client.casessubclient.md new file mode 100644 index 00000000000000..14315890b4f963 --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/cases_client.casessubclient.md @@ -0,0 +1,189 @@ +[Cases Client API Interface](../cases_client_api.md) / [cases/client](../modules/cases_client.md) / CasesSubClient + +# Interface: CasesSubClient + +[cases/client](../modules/cases_client.md).CasesSubClient + +API for interacting with the cases entities. + +## Table of contents + +### Methods + +- [create](cases_client.casessubclient.md#create) +- [delete](cases_client.casessubclient.md#delete) +- [find](cases_client.casessubclient.md#find) +- [get](cases_client.casessubclient.md#get) +- [getCaseIDsByAlertID](cases_client.casessubclient.md#getcaseidsbyalertid) +- [getReporters](cases_client.casessubclient.md#getreporters) +- [getTags](cases_client.casessubclient.md#gettags) +- [push](cases_client.casessubclient.md#push) +- [update](cases_client.casessubclient.md#update) + +## Methods + +### create + +▸ **create**(`data`: [*ICasePostRequest*](typedoc_interfaces.icasepostrequest.md)): *Promise*<[*ICaseResponse*](typedoc_interfaces.icaseresponse.md)\> + +Creates a case. + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `data` | [*ICasePostRequest*](typedoc_interfaces.icasepostrequest.md) | + +**Returns:** *Promise*<[*ICaseResponse*](typedoc_interfaces.icaseresponse.md)\> + +Defined in: [cases/client.ts:48](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/cases/client.ts#L48) + +___ + +### delete + +▸ **delete**(`ids`: *string*[]): *Promise* + +Delete a case and all its comments. + +**`params`** ids an array of case IDs to delete + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `ids` | *string*[] | + +**Returns:** *Promise* + +Defined in: [cases/client.ts:72](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/cases/client.ts#L72) + +___ + +### find + +▸ **find**(`params`: [*ICasesFindRequest*](typedoc_interfaces.icasesfindrequest.md)): *Promise*<[*ICasesFindResponse*](typedoc_interfaces.icasesfindresponse.md)\> + +Returns cases that match the search criteria. + +If the `owner` field is left empty then all the cases that the user has access to will be returned. + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `params` | [*ICasesFindRequest*](typedoc_interfaces.icasesfindrequest.md) | + +**Returns:** *Promise*<[*ICasesFindResponse*](typedoc_interfaces.icasesfindresponse.md)\> + +Defined in: [cases/client.ts:54](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/cases/client.ts#L54) + +___ + +### get + +▸ **get**(`params`: [*GetParams*](cases_get.getparams.md)): *Promise*<[*ICaseResponse*](typedoc_interfaces.icaseresponse.md)\> + +Retrieves a single case with the specified ID. + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `params` | [*GetParams*](cases_get.getparams.md) | + +**Returns:** *Promise*<[*ICaseResponse*](typedoc_interfaces.icaseresponse.md)\> + +Defined in: [cases/client.ts:58](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/cases/client.ts#L58) + +___ + +### getCaseIDsByAlertID + +▸ **getCaseIDsByAlertID**(`params`: [*CaseIDsByAlertIDParams*](cases_get.caseidsbyalertidparams.md)): *Promise* + +Retrieves the case IDs given a single alert ID + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `params` | [*CaseIDsByAlertIDParams*](cases_get.caseidsbyalertidparams.md) | + +**Returns:** *Promise* + +Defined in: [cases/client.ts:84](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/cases/client.ts#L84) + +___ + +### getReporters + +▸ **getReporters**(`params`: { `owner`: *undefined* \| *string* \| *string*[] }): *Promise*<{ `email`: *undefined* \| ``null`` \| *string* ; `full_name`: *undefined* \| ``null`` \| *string* ; `username`: *undefined* \| ``null`` \| *string* }[]\> + +Retrieves all the reporters across all accessible cases. + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `params` | *object* | +| `params.owner` | *undefined* \| *string* \| *string*[] | + +**Returns:** *Promise*<{ `email`: *undefined* \| ``null`` \| *string* ; `full_name`: *undefined* \| ``null`` \| *string* ; `username`: *undefined* \| ``null`` \| *string* }[]\> + +Defined in: [cases/client.ts:80](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/cases/client.ts#L80) + +___ + +### getTags + +▸ **getTags**(`params`: { `owner`: *undefined* \| *string* \| *string*[] }): *Promise* + +Retrieves all the tags across all cases the user making the request has access to. + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `params` | *object* | +| `params.owner` | *undefined* \| *string* \| *string*[] | + +**Returns:** *Promise* + +Defined in: [cases/client.ts:76](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/cases/client.ts#L76) + +___ + +### push + +▸ **push**(`args`: [*PushParams*](cases_push.pushparams.md)): *Promise*<[*ICaseResponse*](typedoc_interfaces.icaseresponse.md)\> + +Pushes a specific case to an external system. + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `args` | [*PushParams*](cases_push.pushparams.md) | + +**Returns:** *Promise*<[*ICaseResponse*](typedoc_interfaces.icaseresponse.md)\> + +Defined in: [cases/client.ts:62](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/cases/client.ts#L62) + +___ + +### update + +▸ **update**(`cases`: [*ICasesPatchRequest*](typedoc_interfaces.icasespatchrequest.md)): *Promise*<[*ICasesResponse*](typedoc_interfaces.icasesresponse.md)\> + +Update the specified cases with the passed in values. + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `cases` | [*ICasesPatchRequest*](typedoc_interfaces.icasespatchrequest.md) | + +**Returns:** *Promise*<[*ICasesResponse*](typedoc_interfaces.icasesresponse.md)\> + +Defined in: [cases/client.ts:66](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/cases/client.ts#L66) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.caseidsbyalertidparams.md b/x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.caseidsbyalertidparams.md new file mode 100644 index 00000000000000..d2aea5db75e54e --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.caseidsbyalertidparams.md @@ -0,0 +1,40 @@ +[Cases Client API Interface](../cases_client_api.md) / [cases/get](../modules/cases_get.md) / CaseIDsByAlertIDParams + +# Interface: CaseIDsByAlertIDParams + +[cases/get](../modules/cases_get.md).CaseIDsByAlertIDParams + +Parameters for finding cases IDs using an alert ID + +## Table of contents + +### Properties + +- [alertID](cases_get.caseidsbyalertidparams.md#alertid) +- [options](cases_get.caseidsbyalertidparams.md#options) + +## Properties + +### alertID + +• **alertID**: *string* + +The alert ID to search for + +Defined in: [cases/get.ts:47](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/cases/get.ts#L47) + +___ + +### options + +• **options**: *object* + +The filtering options when searching for associated cases. + +#### Type declaration + +| Name | Type | +| :------ | :------ | +| `owner` | *undefined* \| *string* \| *string*[] | + +Defined in: [cases/get.ts:51](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/cases/get.ts#L51) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.getparams.md b/x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.getparams.md new file mode 100644 index 00000000000000..78704eb8c5d4d8 --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.getparams.md @@ -0,0 +1,45 @@ +[Cases Client API Interface](../cases_client_api.md) / [cases/get](../modules/cases_get.md) / GetParams + +# Interface: GetParams + +[cases/get](../modules/cases_get.md).GetParams + +The parameters for retrieving a case + +## Table of contents + +### Properties + +- [id](cases_get.getparams.md#id) +- [includeComments](cases_get.getparams.md#includecomments) +- [includeSubCaseComments](cases_get.getparams.md#includesubcasecomments) + +## Properties + +### id + +• **id**: *string* + +Case ID + +Defined in: [cases/get.ts:122](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/cases/get.ts#L122) + +___ + +### includeComments + +• `Optional` **includeComments**: *boolean* + +Whether to include the attachments for a case in the response + +Defined in: [cases/get.ts:126](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/cases/get.ts#L126) + +___ + +### includeSubCaseComments + +• `Optional` **includeSubCaseComments**: *boolean* + +Whether to include the attachments for all children of a case in the response + +Defined in: [cases/get.ts:130](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/cases/get.ts#L130) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/cases_push.pushparams.md b/x-pack/plugins/cases/docs/cases_client/interfaces/cases_push.pushparams.md new file mode 100644 index 00000000000000..a6561152910d68 --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/cases_push.pushparams.md @@ -0,0 +1,34 @@ +[Cases Client API Interface](../cases_client_api.md) / [cases/push](../modules/cases_push.md) / PushParams + +# Interface: PushParams + +[cases/push](../modules/cases_push.md).PushParams + +Parameters for pushing a case to an external system + +## Table of contents + +### Properties + +- [caseId](cases_push.pushparams.md#caseid) +- [connectorId](cases_push.pushparams.md#connectorid) + +## Properties + +### caseId + +• **caseId**: *string* + +The ID of a case + +Defined in: [cases/push.ts:53](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/cases/push.ts#L53) + +___ + +### connectorId + +• **connectorId**: *string* + +The ID of an external system to push to + +Defined in: [cases/push.ts:57](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/cases/push.ts#L57) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/configure_client.configuresubclient.md b/x-pack/plugins/cases/docs/cases_client/interfaces/configure_client.configuresubclient.md new file mode 100644 index 00000000000000..082dc808d6e175 --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/configure_client.configuresubclient.md @@ -0,0 +1,84 @@ +[Cases Client API Interface](../cases_client_api.md) / [configure/client](../modules/configure_client.md) / ConfigureSubClient + +# Interface: ConfigureSubClient + +[configure/client](../modules/configure_client.md).ConfigureSubClient + +This is the public API for interacting with the connector configuration for cases. + +## Table of contents + +### Methods + +- [create](configure_client.configuresubclient.md#create) +- [get](configure_client.configuresubclient.md#get) +- [getConnectors](configure_client.configuresubclient.md#getconnectors) +- [update](configure_client.configuresubclient.md#update) + +## Methods + +### create + +▸ **create**(`configuration`: [*ICasesConfigureRequest*](typedoc_interfaces.icasesconfigurerequest.md)): *Promise*<[*ICasesConfigureResponse*](typedoc_interfaces.icasesconfigureresponse.md)\> + +Creates a configuration if one does not already exist. If one exists it is deleted and a new one is created. + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `configuration` | [*ICasesConfigureRequest*](typedoc_interfaces.icasesconfigurerequest.md) | + +**Returns:** *Promise*<[*ICasesConfigureResponse*](typedoc_interfaces.icasesconfigureresponse.md)\> + +Defined in: [configure/client.ts:102](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/configure/client.ts#L102) + +___ + +### get + +▸ **get**(`params`: { `owner`: *undefined* \| *string* \| *string*[] }): *Promise*<{} \| [*ICasesConfigureResponse*](typedoc_interfaces.icasesconfigureresponse.md)\> + +Retrieves the external connector configuration for a particular case owner. + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `params` | *object* | +| `params.owner` | *undefined* \| *string* \| *string*[] | + +**Returns:** *Promise*<{} \| [*ICasesConfigureResponse*](typedoc_interfaces.icasesconfigureresponse.md)\> + +Defined in: [configure/client.ts:84](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/configure/client.ts#L84) + +___ + +### getConnectors + +▸ **getConnectors**(): *Promise* + +Retrieves the valid external connectors supported by the cases plugin. + +**Returns:** *Promise* + +Defined in: [configure/client.ts:88](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/configure/client.ts#L88) + +___ + +### update + +▸ **update**(`configurationId`: *string*, `configurations`: [*ICasesConfigurePatch*](typedoc_interfaces.icasesconfigurepatch.md)): *Promise*<[*ICasesConfigureResponse*](typedoc_interfaces.icasesconfigureresponse.md)\> + +Updates a particular configuration with new values. + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `configurationId` | *string* | the ID of the configuration to update | +| `configurations` | [*ICasesConfigurePatch*](typedoc_interfaces.icasesconfigurepatch.md) | the new configuration parameters | + +**Returns:** *Promise*<[*ICasesConfigureResponse*](typedoc_interfaces.icasesconfigureresponse.md)\> + +Defined in: [configure/client.ts:95](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/configure/client.ts#L95) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/stats_client.statssubclient.md b/x-pack/plugins/cases/docs/cases_client/interfaces/stats_client.statssubclient.md new file mode 100644 index 00000000000000..9093bee1532aaf --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/stats_client.statssubclient.md @@ -0,0 +1,25 @@ +[Cases Client API Interface](../cases_client_api.md) / [stats/client](../modules/stats_client.md) / StatsSubClient + +# Interface: StatsSubClient + +[stats/client](../modules/stats_client.md).StatsSubClient + +Statistics API contract. + +## Table of contents + +### Methods + +- [getStatusTotalsByType](stats_client.statssubclient.md#getstatustotalsbytype) + +## Methods + +### getStatusTotalsByType + +▸ **getStatusTotalsByType**(): *Promise*<{ `count_closed_cases`: *number* ; `count_in_progress_cases`: *number* ; `count_open_cases`: *number* }\> + +Retrieves the total number of open, closed, and in-progress cases. + +**Returns:** *Promise*<{ `count_closed_cases`: *number* ; `count_in_progress_cases`: *number* ; `count_open_cases`: *number* }\> + +Defined in: [stats/client.ts:21](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/stats/client.ts#L21) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/sub_cases_client.subcasesclient.md b/x-pack/plugins/cases/docs/cases_client/interfaces/sub_cases_client.subcasesclient.md new file mode 100644 index 00000000000000..db48224bab6717 --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/sub_cases_client.subcasesclient.md @@ -0,0 +1,89 @@ +[Cases Client API Interface](../cases_client_api.md) / [sub_cases/client](../modules/sub_cases_client.md) / SubCasesClient + +# Interface: SubCasesClient + +[sub_cases/client](../modules/sub_cases_client.md).SubCasesClient + +The API routes for interacting with sub cases. + +## Table of contents + +### Methods + +- [delete](sub_cases_client.subcasesclient.md#delete) +- [find](sub_cases_client.subcasesclient.md#find) +- [get](sub_cases_client.subcasesclient.md#get) +- [update](sub_cases_client.subcasesclient.md#update) + +## Methods + +### delete + +▸ **delete**(`ids`: *string*[]): *Promise* + +Deletes the specified entities and their attachments. + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `ids` | *string*[] | + +**Returns:** *Promise* + +Defined in: [sub_cases/client.ts:60](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/sub_cases/client.ts#L60) + +___ + +### find + +▸ **find**(`findArgs`: FindArgs): *Promise*<[*ISubCasesFindResponse*](typedoc_interfaces.isubcasesfindresponse.md)\> + +Retrieves the sub cases matching the search criteria. + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `findArgs` | FindArgs | + +**Returns:** *Promise*<[*ISubCasesFindResponse*](typedoc_interfaces.isubcasesfindresponse.md)\> + +Defined in: [sub_cases/client.ts:64](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/sub_cases/client.ts#L64) + +___ + +### get + +▸ **get**(`getArgs`: GetArgs): *Promise*<[*ISubCaseResponse*](typedoc_interfaces.isubcaseresponse.md)\> + +Retrieves a single sub case. + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `getArgs` | GetArgs | + +**Returns:** *Promise*<[*ISubCaseResponse*](typedoc_interfaces.isubcaseresponse.md)\> + +Defined in: [sub_cases/client.ts:68](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/sub_cases/client.ts#L68) + +___ + +### update + +▸ **update**(`subCases`: { `subCases`: { `status`: *undefined* \| open \| *any*[*any*] \| closed } & { id: string; version: string; }[] }): *Promise*<[*ISubCasesResponse*](typedoc_interfaces.isubcasesresponse.md)\> + +Updates the specified sub cases to the new values included in the request. + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `subCases` | *object* | +| `subCases.subCases` | { `status`: *undefined* \| open \| *any*[*any*] \| closed } & { id: string; version: string; }[] | + +**Returns:** *Promise*<[*ISubCasesResponse*](typedoc_interfaces.isubcasesresponse.md)\> + +Defined in: [sub_cases/client.ts:72](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/sub_cases/client.ts#L72) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.iallcommentsresponse.md b/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.iallcommentsresponse.md new file mode 100644 index 00000000000000..06322bb51e2ad3 --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.iallcommentsresponse.md @@ -0,0 +1,11 @@ +[Cases Client API Interface](../cases_client_api.md) / [typedoc_interfaces](../modules/typedoc_interfaces.md) / IAllCommentsResponse + +# Interface: IAllCommentsResponse + +[typedoc_interfaces](../modules/typedoc_interfaces.md).IAllCommentsResponse + +## Hierarchy + +- *AllCommentsResponse* + + ↳ **IAllCommentsResponse** diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icasepostrequest.md b/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icasepostrequest.md new file mode 100644 index 00000000000000..70533a15fe6167 --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icasepostrequest.md @@ -0,0 +1,88 @@ +[Cases Client API Interface](../cases_client_api.md) / [typedoc_interfaces](../modules/typedoc_interfaces.md) / ICasePostRequest + +# Interface: ICasePostRequest + +[typedoc_interfaces](../modules/typedoc_interfaces.md).ICasePostRequest + +These are simply to make typedoc not attempt to expand the type aliases. If it attempts to expand them +the docs are huge. + +## Hierarchy + +- *CasePostRequest* + + ↳ **ICasePostRequest** + +## Table of contents + +### Properties + +- [connector](typedoc_interfaces.icasepostrequest.md#connector) +- [description](typedoc_interfaces.icasepostrequest.md#description) +- [owner](typedoc_interfaces.icasepostrequest.md#owner) +- [settings](typedoc_interfaces.icasepostrequest.md#settings) +- [tags](typedoc_interfaces.icasepostrequest.md#tags) +- [title](typedoc_interfaces.icasepostrequest.md#title) +- [type](typedoc_interfaces.icasepostrequest.md#type) + +## Properties + +### connector + +• **connector**: { `id`: *string* ; `name`: *string* } & { `fields`: ``null`` \| { issueType: string \| null; priority: string \| null; parent: string \| null; } ; `type`: jira } & { `id`: *string* ; `name`: *string* } & { `fields`: ``null`` \| { incidentTypes: string[] \| null; severityCode: string \| null; } ; `type`: resilient } & { `id`: *string* ; `name`: *string* } & { `fields`: ``null`` \| { impact: string \| null; severity: string \| null; urgency: string \| null; category: string \| null; subcategory: string \| null; } ; `type`: serviceNowITSM } & { `id`: *string* ; `name`: *string* } & { `fields`: ``null`` \| { category: string \| null; destIp: boolean \| null; malwareHash: boolean \| null; malwareUrl: boolean \| null; priority: string \| null; sourceIp: boolean \| null; subcategory: string \| null; } ; `type`: serviceNowSIR } & { `id`: *string* ; `name`: *string* } & { `fields`: ``null`` ; `type`: none } + +Inherited from: CasePostRequest.connector + +___ + +### description + +• **description**: *string* + +Inherited from: CasePostRequest.description + +___ + +### owner + +• **owner**: *string* + +Inherited from: CasePostRequest.owner + +___ + +### settings + +• **settings**: *object* + +#### Type declaration + +| Name | Type | +| :------ | :------ | +| `syncAlerts` | *boolean* | + +Inherited from: CasePostRequest.settings + +___ + +### tags + +• **tags**: *string*[] + +Inherited from: CasePostRequest.tags + +___ + +### title + +• **title**: *string* + +Inherited from: CasePostRequest.title + +___ + +### type + +• **type**: *undefined* \| collection \| individual + +Inherited from: CasePostRequest.type diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icaseresponse.md b/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icaseresponse.md new file mode 100644 index 00000000000000..5db55e5552473c --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icaseresponse.md @@ -0,0 +1,228 @@ +[Cases Client API Interface](../cases_client_api.md) / [typedoc_interfaces](../modules/typedoc_interfaces.md) / ICaseResponse + +# Interface: ICaseResponse + +[typedoc_interfaces](../modules/typedoc_interfaces.md).ICaseResponse + +## Hierarchy + +- *CaseResponse* + + ↳ **ICaseResponse** + +## Table of contents + +### Properties + +- [closed\_at](typedoc_interfaces.icaseresponse.md#closed_at) +- [closed\_by](typedoc_interfaces.icaseresponse.md#closed_by) +- [comments](typedoc_interfaces.icaseresponse.md#comments) +- [connector](typedoc_interfaces.icaseresponse.md#connector) +- [created\_at](typedoc_interfaces.icaseresponse.md#created_at) +- [created\_by](typedoc_interfaces.icaseresponse.md#created_by) +- [description](typedoc_interfaces.icaseresponse.md#description) +- [external\_service](typedoc_interfaces.icaseresponse.md#external_service) +- [id](typedoc_interfaces.icaseresponse.md#id) +- [owner](typedoc_interfaces.icaseresponse.md#owner) +- [settings](typedoc_interfaces.icaseresponse.md#settings) +- [status](typedoc_interfaces.icaseresponse.md#status) +- [subCaseIds](typedoc_interfaces.icaseresponse.md#subcaseids) +- [subCases](typedoc_interfaces.icaseresponse.md#subcases) +- [tags](typedoc_interfaces.icaseresponse.md#tags) +- [title](typedoc_interfaces.icaseresponse.md#title) +- [totalAlerts](typedoc_interfaces.icaseresponse.md#totalalerts) +- [totalComment](typedoc_interfaces.icaseresponse.md#totalcomment) +- [type](typedoc_interfaces.icaseresponse.md#type) +- [updated\_at](typedoc_interfaces.icaseresponse.md#updated_at) +- [updated\_by](typedoc_interfaces.icaseresponse.md#updated_by) +- [version](typedoc_interfaces.icaseresponse.md#version) + +## Properties + +### closed\_at + +• **closed\_at**: ``null`` \| *string* + +Inherited from: CaseResponse.closed\_at + +___ + +### closed\_by + +• **closed\_by**: ``null`` \| { `email`: *undefined* \| ``null`` \| *string* ; `full_name`: *undefined* \| ``null`` \| *string* ; `username`: *undefined* \| ``null`` \| *string* } + +Inherited from: CaseResponse.closed\_by + +___ + +### comments + +• **comments**: *undefined* \| { `comment`: *string* ; `owner`: *string* ; `type`: user } & { `associationType`: AssociationType ; `created_at`: *string* ; `created_by`: { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `owner`: *string* ; `pushed_at`: ``null`` \| *string* ; `pushed_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `updated_at`: ``null`` \| *string* ; `updated_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } } & { `id`: *string* ; `version`: *string* } & { `alertId`: *string* \| *string*[] ; `index`: *string* \| *string*[] ; `owner`: *string* ; `rule`: { id: string \| null; name: string \| null; } ; `type`: alert \| generatedAlert } & { `associationType`: AssociationType ; `created_at`: *string* ; `created_by`: { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `owner`: *string* ; `pushed_at`: ``null`` \| *string* ; `pushed_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `updated_at`: ``null`` \| *string* ; `updated_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } } & { `id`: *string* ; `version`: *string* }[] + +Inherited from: CaseResponse.comments + +___ + +### connector + +• **connector**: { `id`: *string* ; `name`: *string* } & { `fields`: ``null`` \| { issueType: string \| null; priority: string \| null; parent: string \| null; } ; `type`: jira } & { `id`: *string* ; `name`: *string* } & { `fields`: ``null`` \| { incidentTypes: string[] \| null; severityCode: string \| null; } ; `type`: resilient } & { `id`: *string* ; `name`: *string* } & { `fields`: ``null`` \| { impact: string \| null; severity: string \| null; urgency: string \| null; category: string \| null; subcategory: string \| null; } ; `type`: serviceNowITSM } & { `id`: *string* ; `name`: *string* } & { `fields`: ``null`` \| { category: string \| null; destIp: boolean \| null; malwareHash: boolean \| null; malwareUrl: boolean \| null; priority: string \| null; sourceIp: boolean \| null; subcategory: string \| null; } ; `type`: serviceNowSIR } & { `id`: *string* ; `name`: *string* } & { `fields`: ``null`` ; `type`: none } + +Inherited from: CaseResponse.connector + +___ + +### created\_at + +• **created\_at**: *string* + +Inherited from: CaseResponse.created\_at + +___ + +### created\_by + +• **created\_by**: *object* + +#### Type declaration + +| Name | Type | +| :------ | :------ | +| `email` | *undefined* \| ``null`` \| *string* | +| `full_name` | *undefined* \| ``null`` \| *string* | +| `username` | *undefined* \| ``null`` \| *string* | + +Inherited from: CaseResponse.created\_by + +___ + +### description + +• **description**: *string* + +Inherited from: CaseResponse.description + +___ + +### external\_service + +• **external\_service**: ``null`` \| { `connector_id`: *string* ; `connector_name`: *string* ; `external_id`: *string* ; `external_title`: *string* ; `external_url`: *string* } & { `pushed_at`: *string* ; `pushed_by`: { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } } + +Inherited from: CaseResponse.external\_service + +___ + +### id + +• **id**: *string* + +Inherited from: CaseResponse.id + +___ + +### owner + +• **owner**: *string* + +Inherited from: CaseResponse.owner + +___ + +### settings + +• **settings**: *object* + +#### Type declaration + +| Name | Type | +| :------ | :------ | +| `syncAlerts` | *boolean* | + +Inherited from: CaseResponse.settings + +___ + +### status + +• **status**: CaseStatuses + +Inherited from: CaseResponse.status + +___ + +### subCaseIds + +• **subCaseIds**: *undefined* \| *string*[] + +Inherited from: CaseResponse.subCaseIds + +___ + +### subCases + +• **subCases**: *undefined* \| { `status`: CaseStatuses } & { `closed_at`: ``null`` \| *string* ; `closed_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `created_at`: *string* ; `created_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `owner`: *string* ; `updated_at`: ``null`` \| *string* ; `updated_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } } & { `id`: *string* ; `totalAlerts`: *number* ; `totalComment`: *number* ; `version`: *string* } & { `comments`: *undefined* \| { `comment`: *string* ; `owner`: *string* ; `type`: user } & { `associationType`: AssociationType ; `created_at`: *string* ; `created_by`: { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `owner`: *string* ; `pushed_at`: ``null`` \| *string* ; `pushed_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `updated_at`: ``null`` \| *string* ; `updated_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } } & { `id`: *string* ; `version`: *string* } & { `alertId`: *string* \| *string*[] ; `index`: *string* \| *string*[] ; `owner`: *string* ; `rule`: { id: string \| null; name: string \| null; } ; `type`: alert \| generatedAlert } & { `associationType`: AssociationType ; `created_at`: *string* ; `created_by`: { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `owner`: *string* ; `pushed_at`: ``null`` \| *string* ; `pushed_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `updated_at`: ``null`` \| *string* ; `updated_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } } & { `id`: *string* ; `version`: *string* }[] }[] + +Inherited from: CaseResponse.subCases + +___ + +### tags + +• **tags**: *string*[] + +Inherited from: CaseResponse.tags + +___ + +### title + +• **title**: *string* + +Inherited from: CaseResponse.title + +___ + +### totalAlerts + +• **totalAlerts**: *number* + +Inherited from: CaseResponse.totalAlerts + +___ + +### totalComment + +• **totalComment**: *number* + +Inherited from: CaseResponse.totalComment + +___ + +### type + +• **type**: CaseType + +Inherited from: CaseResponse.type + +___ + +### updated\_at + +• **updated\_at**: ``null`` \| *string* + +Inherited from: CaseResponse.updated\_at + +___ + +### updated\_by + +• **updated\_by**: ``null`` \| { `email`: *undefined* \| ``null`` \| *string* ; `full_name`: *undefined* \| ``null`` \| *string* ; `username`: *undefined* \| ``null`` \| *string* } + +Inherited from: CaseResponse.updated\_by + +___ + +### version + +• **version**: *string* + +Inherited from: CaseResponse.version diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icasesconfigurepatch.md b/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icasesconfigurepatch.md new file mode 100644 index 00000000000000..3854fda03fb6ac --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icasesconfigurepatch.md @@ -0,0 +1,43 @@ +[Cases Client API Interface](../cases_client_api.md) / [typedoc_interfaces](../modules/typedoc_interfaces.md) / ICasesConfigurePatch + +# Interface: ICasesConfigurePatch + +[typedoc_interfaces](../modules/typedoc_interfaces.md).ICasesConfigurePatch + +## Hierarchy + +- *CasesConfigurePatch* + + ↳ **ICasesConfigurePatch** + +## Table of contents + +### Properties + +- [closure\_type](typedoc_interfaces.icasesconfigurepatch.md#closure_type) +- [connector](typedoc_interfaces.icasesconfigurepatch.md#connector) +- [version](typedoc_interfaces.icasesconfigurepatch.md#version) + +## Properties + +### closure\_type + +• **closure\_type**: *undefined* \| ``"close-by-user"`` \| ``"close-by-pushing"`` + +Inherited from: CasesConfigurePatch.closure\_type + +___ + +### connector + +• **connector**: *undefined* \| { `id`: *string* ; `name`: *string* } & { `fields`: ``null`` \| { issueType: string \| null; priority: string \| null; parent: string \| null; } ; `type`: jira } & { `id`: *string* ; `name`: *string* } & { `fields`: ``null`` \| { incidentTypes: string[] \| null; severityCode: string \| null; } ; `type`: resilient } & { `id`: *string* ; `name`: *string* } & { `fields`: ``null`` \| { impact: string \| null; severity: string \| null; urgency: string \| null; category: string \| null; subcategory: string \| null; } ; `type`: serviceNowITSM } & { `id`: *string* ; `name`: *string* } & { `fields`: ``null`` \| { category: string \| null; destIp: boolean \| null; malwareHash: boolean \| null; malwareUrl: boolean \| null; priority: string \| null; sourceIp: boolean \| null; subcategory: string \| null; } ; `type`: serviceNowSIR } & { `id`: *string* ; `name`: *string* } & { `fields`: ``null`` ; `type`: none } + +Inherited from: CasesConfigurePatch.connector + +___ + +### version + +• **version**: *string* + +Inherited from: CasesConfigurePatch.version diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icasesconfigurerequest.md b/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icasesconfigurerequest.md new file mode 100644 index 00000000000000..548e1a5c48f587 --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icasesconfigurerequest.md @@ -0,0 +1,43 @@ +[Cases Client API Interface](../cases_client_api.md) / [typedoc_interfaces](../modules/typedoc_interfaces.md) / ICasesConfigureRequest + +# Interface: ICasesConfigureRequest + +[typedoc_interfaces](../modules/typedoc_interfaces.md).ICasesConfigureRequest + +## Hierarchy + +- *CasesConfigureRequest* + + ↳ **ICasesConfigureRequest** + +## Table of contents + +### Properties + +- [closure\_type](typedoc_interfaces.icasesconfigurerequest.md#closure_type) +- [connector](typedoc_interfaces.icasesconfigurerequest.md#connector) +- [owner](typedoc_interfaces.icasesconfigurerequest.md#owner) + +## Properties + +### closure\_type + +• **closure\_type**: ``"close-by-user"`` \| ``"close-by-pushing"`` + +Inherited from: CasesConfigureRequest.closure\_type + +___ + +### connector + +• **connector**: { `id`: *string* ; `name`: *string* } & { `fields`: ``null`` \| { issueType: string \| null; priority: string \| null; parent: string \| null; } ; `type`: jira } & { `id`: *string* ; `name`: *string* } & { `fields`: ``null`` \| { incidentTypes: string[] \| null; severityCode: string \| null; } ; `type`: resilient } & { `id`: *string* ; `name`: *string* } & { `fields`: ``null`` \| { impact: string \| null; severity: string \| null; urgency: string \| null; category: string \| null; subcategory: string \| null; } ; `type`: serviceNowITSM } & { `id`: *string* ; `name`: *string* } & { `fields`: ``null`` \| { category: string \| null; destIp: boolean \| null; malwareHash: boolean \| null; malwareUrl: boolean \| null; priority: string \| null; sourceIp: boolean \| null; subcategory: string \| null; } ; `type`: serviceNowSIR } & { `id`: *string* ; `name`: *string* } & { `fields`: ``null`` ; `type`: none } + +Inherited from: CasesConfigureRequest.connector + +___ + +### owner + +• **owner**: *string* + +Inherited from: CasesConfigureRequest.owner diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icasesconfigureresponse.md b/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icasesconfigureresponse.md new file mode 100644 index 00000000000000..c493a4c6c0f0c4 --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icasesconfigureresponse.md @@ -0,0 +1,123 @@ +[Cases Client API Interface](../cases_client_api.md) / [typedoc_interfaces](../modules/typedoc_interfaces.md) / ICasesConfigureResponse + +# Interface: ICasesConfigureResponse + +[typedoc_interfaces](../modules/typedoc_interfaces.md).ICasesConfigureResponse + +## Hierarchy + +- *CasesConfigureResponse* + + ↳ **ICasesConfigureResponse** + +## Table of contents + +### Properties + +- [closure\_type](typedoc_interfaces.icasesconfigureresponse.md#closure_type) +- [connector](typedoc_interfaces.icasesconfigureresponse.md#connector) +- [created\_at](typedoc_interfaces.icasesconfigureresponse.md#created_at) +- [created\_by](typedoc_interfaces.icasesconfigureresponse.md#created_by) +- [error](typedoc_interfaces.icasesconfigureresponse.md#error) +- [id](typedoc_interfaces.icasesconfigureresponse.md#id) +- [mappings](typedoc_interfaces.icasesconfigureresponse.md#mappings) +- [owner](typedoc_interfaces.icasesconfigureresponse.md#owner) +- [updated\_at](typedoc_interfaces.icasesconfigureresponse.md#updated_at) +- [updated\_by](typedoc_interfaces.icasesconfigureresponse.md#updated_by) +- [version](typedoc_interfaces.icasesconfigureresponse.md#version) + +## Properties + +### closure\_type + +• **closure\_type**: ``"close-by-user"`` \| ``"close-by-pushing"`` + +Inherited from: CasesConfigureResponse.closure\_type + +___ + +### connector + +• **connector**: { `id`: *string* ; `name`: *string* } & { `fields`: ``null`` \| { issueType: string \| null; priority: string \| null; parent: string \| null; } ; `type`: jira } & { `id`: *string* ; `name`: *string* } & { `fields`: ``null`` \| { incidentTypes: string[] \| null; severityCode: string \| null; } ; `type`: resilient } & { `id`: *string* ; `name`: *string* } & { `fields`: ``null`` \| { impact: string \| null; severity: string \| null; urgency: string \| null; category: string \| null; subcategory: string \| null; } ; `type`: serviceNowITSM } & { `id`: *string* ; `name`: *string* } & { `fields`: ``null`` \| { category: string \| null; destIp: boolean \| null; malwareHash: boolean \| null; malwareUrl: boolean \| null; priority: string \| null; sourceIp: boolean \| null; subcategory: string \| null; } ; `type`: serviceNowSIR } & { `id`: *string* ; `name`: *string* } & { `fields`: ``null`` ; `type`: none } + +Inherited from: CasesConfigureResponse.connector + +___ + +### created\_at + +• **created\_at**: *string* + +Inherited from: CasesConfigureResponse.created\_at + +___ + +### created\_by + +• **created\_by**: *object* + +#### Type declaration + +| Name | Type | +| :------ | :------ | +| `email` | *undefined* \| ``null`` \| *string* | +| `full_name` | *undefined* \| ``null`` \| *string* | +| `username` | *undefined* \| ``null`` \| *string* | + +Inherited from: CasesConfigureResponse.created\_by + +___ + +### error + +• **error**: ``null`` \| *string* + +Inherited from: CasesConfigureResponse.error + +___ + +### id + +• **id**: *string* + +Inherited from: CasesConfigureResponse.id + +___ + +### mappings + +• **mappings**: { `action_type`: ``"append"`` \| ``"nothing"`` \| ``"overwrite"`` ; `source`: ``"description"`` \| ``"title"`` \| ``"comments"`` ; `target`: *string* }[] + +Inherited from: CasesConfigureResponse.mappings + +___ + +### owner + +• **owner**: *string* + +Inherited from: CasesConfigureResponse.owner + +___ + +### updated\_at + +• **updated\_at**: ``null`` \| *string* + +Inherited from: CasesConfigureResponse.updated\_at + +___ + +### updated\_by + +• **updated\_by**: ``null`` \| { `email`: *undefined* \| ``null`` \| *string* ; `full_name`: *undefined* \| ``null`` \| *string* ; `username`: *undefined* \| ``null`` \| *string* } + +Inherited from: CasesConfigureResponse.updated\_by + +___ + +### version + +• **version**: *string* + +Inherited from: CasesConfigureResponse.version diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icasesfindrequest.md b/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icasesfindrequest.md new file mode 100644 index 00000000000000..cb8ec7797677f3 --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icasesfindrequest.md @@ -0,0 +1,133 @@ +[Cases Client API Interface](../cases_client_api.md) / [typedoc_interfaces](../modules/typedoc_interfaces.md) / ICasesFindRequest + +# Interface: ICasesFindRequest + +[typedoc_interfaces](../modules/typedoc_interfaces.md).ICasesFindRequest + +## Hierarchy + +- *CasesFindRequest* + + ↳ **ICasesFindRequest** + +## Table of contents + +### Properties + +- [defaultSearchOperator](typedoc_interfaces.icasesfindrequest.md#defaultsearchoperator) +- [fields](typedoc_interfaces.icasesfindrequest.md#fields) +- [owner](typedoc_interfaces.icasesfindrequest.md#owner) +- [page](typedoc_interfaces.icasesfindrequest.md#page) +- [perPage](typedoc_interfaces.icasesfindrequest.md#perpage) +- [reporters](typedoc_interfaces.icasesfindrequest.md#reporters) +- [search](typedoc_interfaces.icasesfindrequest.md#search) +- [searchFields](typedoc_interfaces.icasesfindrequest.md#searchfields) +- [sortField](typedoc_interfaces.icasesfindrequest.md#sortfield) +- [sortOrder](typedoc_interfaces.icasesfindrequest.md#sortorder) +- [status](typedoc_interfaces.icasesfindrequest.md#status) +- [tags](typedoc_interfaces.icasesfindrequest.md#tags) +- [type](typedoc_interfaces.icasesfindrequest.md#type) + +## Properties + +### defaultSearchOperator + +• **defaultSearchOperator**: *undefined* \| ``"AND"`` \| ``"OR"`` + +Inherited from: CasesFindRequest.defaultSearchOperator + +___ + +### fields + +• **fields**: *undefined* \| *string*[] + +Inherited from: CasesFindRequest.fields + +___ + +### owner + +• **owner**: *undefined* \| *string* \| *string*[] + +Inherited from: CasesFindRequest.owner + +___ + +### page + +• **page**: *undefined* \| *number* + +Inherited from: CasesFindRequest.page + +___ + +### perPage + +• **perPage**: *undefined* \| *number* + +Inherited from: CasesFindRequest.perPage + +___ + +### reporters + +• **reporters**: *undefined* \| *string* \| *string*[] + +Inherited from: CasesFindRequest.reporters + +___ + +### search + +• **search**: *undefined* \| *string* + +Inherited from: CasesFindRequest.search + +___ + +### searchFields + +• **searchFields**: *undefined* \| *string* \| *string*[] + +Inherited from: CasesFindRequest.searchFields + +___ + +### sortField + +• **sortField**: *undefined* \| *string* + +Inherited from: CasesFindRequest.sortField + +___ + +### sortOrder + +• **sortOrder**: *undefined* \| ``"desc"`` \| ``"asc"`` + +Inherited from: CasesFindRequest.sortOrder + +___ + +### status + +• **status**: *undefined* \| open \| *any*[*any*] \| closed + +Inherited from: CasesFindRequest.status + +___ + +### tags + +• **tags**: *undefined* \| *string* \| *string*[] + +Inherited from: CasesFindRequest.tags + +___ + +### type + +• **type**: *undefined* \| collection \| individual + +Inherited from: CasesFindRequest.type diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icasesfindresponse.md b/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icasesfindresponse.md new file mode 100644 index 00000000000000..9be5fd5743a8ee --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icasesfindresponse.md @@ -0,0 +1,79 @@ +[Cases Client API Interface](../cases_client_api.md) / [typedoc_interfaces](../modules/typedoc_interfaces.md) / ICasesFindResponse + +# Interface: ICasesFindResponse + +[typedoc_interfaces](../modules/typedoc_interfaces.md).ICasesFindResponse + +## Hierarchy + +- *CasesFindResponse* + + ↳ **ICasesFindResponse** + +## Table of contents + +### Properties + +- [cases](typedoc_interfaces.icasesfindresponse.md#cases) +- [count\_closed\_cases](typedoc_interfaces.icasesfindresponse.md#count_closed_cases) +- [count\_in\_progress\_cases](typedoc_interfaces.icasesfindresponse.md#count_in_progress_cases) +- [count\_open\_cases](typedoc_interfaces.icasesfindresponse.md#count_open_cases) +- [page](typedoc_interfaces.icasesfindresponse.md#page) +- [per\_page](typedoc_interfaces.icasesfindresponse.md#per_page) +- [total](typedoc_interfaces.icasesfindresponse.md#total) + +## Properties + +### cases + +• **cases**: { `connector`: { id: string; name: string; } & { type: ConnectorTypes.jira; fields: { issueType: string \| null; priority: string \| null; parent: string \| null; } \| null; } & { id: string; name: string; } & { type: ConnectorTypes.resilient; fields: { incidentTypes: string[] \| null; severityCode: string \| null; } \| null; } & { id: string; name: string; } & { type: ConnectorTypes.serviceNowITSM; fields: { impact: string \| null; severity: string \| null; urgency: string \| null; category: string \| null; subcategory: string \| null; } \| null; } & { id: string; name: string; } & { type: ConnectorTypes.serviceNowSIR; fields: { category: string \| null; destIp: boolean \| null; malwareHash: boolean \| null; malwareUrl: boolean \| null; priority: string \| null; sourceIp: boolean \| null; subcategory: string \| null; } \| null; } & { id: string; name: string; } & { type: ConnectorTypes.none; fields: null; } ; `description`: *string* ; `owner`: *string* ; `settings`: { syncAlerts: boolean; } ; `status`: CaseStatuses ; `tags`: *string*[] ; `title`: *string* ; `type`: CaseType } & { `closed_at`: ``null`` \| *string* ; `closed_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `created_at`: *string* ; `created_by`: { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `external_service`: ``null`` \| { connector\_id: string; connector\_name: string; external\_id: string; external\_title: string; external\_url: string; } & { pushed\_at: string; pushed\_by: { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; }; } ; `updated_at`: ``null`` \| *string* ; `updated_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } } & { `id`: *string* ; `totalAlerts`: *number* ; `totalComment`: *number* ; `version`: *string* } & { `comments`: *undefined* \| { `comment`: *string* ; `owner`: *string* ; `type`: user } & { `associationType`: AssociationType ; `created_at`: *string* ; `created_by`: { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `owner`: *string* ; `pushed_at`: ``null`` \| *string* ; `pushed_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `updated_at`: ``null`` \| *string* ; `updated_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } } & { `id`: *string* ; `version`: *string* } & { `alertId`: *string* \| *string*[] ; `index`: *string* \| *string*[] ; `owner`: *string* ; `rule`: { id: string \| null; name: string \| null; } ; `type`: alert \| generatedAlert } & { `associationType`: AssociationType ; `created_at`: *string* ; `created_by`: { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `owner`: *string* ; `pushed_at`: ``null`` \| *string* ; `pushed_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `updated_at`: ``null`` \| *string* ; `updated_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } } & { `id`: *string* ; `version`: *string* }[] ; `subCaseIds`: *undefined* \| *string*[] ; `subCases`: *undefined* \| { `status`: CaseStatuses } & { `closed_at`: ``null`` \| *string* ; `closed_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `created_at`: *string* ; `created_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `owner`: *string* ; `updated_at`: ``null`` \| *string* ; `updated_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } } & { `id`: *string* ; `totalAlerts`: *number* ; `totalComment`: *number* ; `version`: *string* } & { comments?: ((({ comment: string; type: CommentType.user; owner: string; } & { associationType: AssociationType; created\_at: string; created\_by: { email: string \| null \| undefined; full\_name: string \| ... 1 more ... \| undefined; username: string \| ... 1 more ... \| undefined; }; ... 4 more ...; updated\_by: { ...; } ...[] }[] + +Inherited from: CasesFindResponse.cases + +___ + +### count\_closed\_cases + +• **count\_closed\_cases**: *number* + +Inherited from: CasesFindResponse.count\_closed\_cases + +___ + +### count\_in\_progress\_cases + +• **count\_in\_progress\_cases**: *number* + +Inherited from: CasesFindResponse.count\_in\_progress\_cases + +___ + +### count\_open\_cases + +• **count\_open\_cases**: *number* + +Inherited from: CasesFindResponse.count\_open\_cases + +___ + +### page + +• **page**: *number* + +Inherited from: CasesFindResponse.page + +___ + +### per\_page + +• **per\_page**: *number* + +Inherited from: CasesFindResponse.per\_page + +___ + +### total + +• **total**: *number* + +Inherited from: CasesFindResponse.total diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icasespatchrequest.md b/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icasespatchrequest.md new file mode 100644 index 00000000000000..bfdb3b7315e554 --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icasespatchrequest.md @@ -0,0 +1,25 @@ +[Cases Client API Interface](../cases_client_api.md) / [typedoc_interfaces](../modules/typedoc_interfaces.md) / ICasesPatchRequest + +# Interface: ICasesPatchRequest + +[typedoc_interfaces](../modules/typedoc_interfaces.md).ICasesPatchRequest + +## Hierarchy + +- *CasesPatchRequest* + + ↳ **ICasesPatchRequest** + +## Table of contents + +### Properties + +- [cases](typedoc_interfaces.icasespatchrequest.md#cases) + +## Properties + +### cases + +• **cases**: { `connector`: *undefined* \| { `id`: *string* ; `name`: *string* } & { `fields`: ``null`` \| { issueType: string \| null; priority: string \| null; parent: string \| null; } ; `type`: jira } & { `id`: *string* ; `name`: *string* } & { `fields`: ``null`` \| { incidentTypes: string[] \| null; severityCode: string \| null; } ; `type`: resilient } & { `id`: *string* ; `name`: *string* } & { `fields`: ``null`` \| { impact: string \| null; severity: string \| null; urgency: string \| null; category: string \| null; subcategory: string \| null; } ; `type`: serviceNowITSM } & { `id`: *string* ; `name`: *string* } & { `fields`: ``null`` \| { category: string \| null; destIp: boolean \| null; malwareHash: boolean \| null; malwareUrl: boolean \| null; priority: string \| null; sourceIp: boolean \| null; subcategory: string \| null; } ; `type`: serviceNowSIR } & { `id`: *string* ; `name`: *string* } & { `fields`: ``null`` ; `type`: none } ; `description`: *undefined* \| *string* ; `owner`: *undefined* \| *string* ; `settings`: *undefined* \| { `syncAlerts`: *boolean* } ; `status`: *undefined* \| open \| *any*[*any*] \| closed ; `tags`: *undefined* \| *string*[] ; `title`: *undefined* \| *string* ; `type`: *undefined* \| collection \| individual } & { `id`: *string* ; `version`: *string* }[] + +Inherited from: CasesPatchRequest.cases diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icasesresponse.md b/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icasesresponse.md new file mode 100644 index 00000000000000..2c9eed242d1fb1 --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icasesresponse.md @@ -0,0 +1,11 @@ +[Cases Client API Interface](../cases_client_api.md) / [typedoc_interfaces](../modules/typedoc_interfaces.md) / ICasesResponse + +# Interface: ICasesResponse + +[typedoc_interfaces](../modules/typedoc_interfaces.md).ICasesResponse + +## Hierarchy + +- *CasesResponse* + + ↳ **ICasesResponse** diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icaseuseractionsresponse.md b/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icaseuseractionsresponse.md new file mode 100644 index 00000000000000..0347711e331dcf --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icaseuseractionsresponse.md @@ -0,0 +1,11 @@ +[Cases Client API Interface](../cases_client_api.md) / [typedoc_interfaces](../modules/typedoc_interfaces.md) / ICaseUserActionsResponse + +# Interface: ICaseUserActionsResponse + +[typedoc_interfaces](../modules/typedoc_interfaces.md).ICaseUserActionsResponse + +## Hierarchy + +- *CaseUserActionsResponse* + + ↳ **ICaseUserActionsResponse** diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icommentsresponse.md b/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icommentsresponse.md new file mode 100644 index 00000000000000..d34480b2c633cf --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.icommentsresponse.md @@ -0,0 +1,52 @@ +[Cases Client API Interface](../cases_client_api.md) / [typedoc_interfaces](../modules/typedoc_interfaces.md) / ICommentsResponse + +# Interface: ICommentsResponse + +[typedoc_interfaces](../modules/typedoc_interfaces.md).ICommentsResponse + +## Hierarchy + +- *CommentsResponse* + + ↳ **ICommentsResponse** + +## Table of contents + +### Properties + +- [comments](typedoc_interfaces.icommentsresponse.md#comments) +- [page](typedoc_interfaces.icommentsresponse.md#page) +- [per\_page](typedoc_interfaces.icommentsresponse.md#per_page) +- [total](typedoc_interfaces.icommentsresponse.md#total) + +## Properties + +### comments + +• **comments**: { `comment`: *string* ; `owner`: *string* ; `type`: user } & { `associationType`: AssociationType ; `created_at`: *string* ; `created_by`: { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `owner`: *string* ; `pushed_at`: ``null`` \| *string* ; `pushed_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `updated_at`: ``null`` \| *string* ; `updated_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } } & { `id`: *string* ; `version`: *string* } & { `alertId`: *string* \| *string*[] ; `index`: *string* \| *string*[] ; `owner`: *string* ; `rule`: { id: string \| null; name: string \| null; } ; `type`: alert \| generatedAlert } & { `associationType`: AssociationType ; `created_at`: *string* ; `created_by`: { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `owner`: *string* ; `pushed_at`: ``null`` \| *string* ; `pushed_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `updated_at`: ``null`` \| *string* ; `updated_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } } & { `id`: *string* ; `version`: *string* }[] + +Inherited from: CommentsResponse.comments + +___ + +### page + +• **page**: *number* + +Inherited from: CommentsResponse.page + +___ + +### per\_page + +• **per\_page**: *number* + +Inherited from: CommentsResponse.per\_page + +___ + +### total + +• **total**: *number* + +Inherited from: CommentsResponse.total diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.isubcaseresponse.md b/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.isubcaseresponse.md new file mode 100644 index 00000000000000..b33b280d2e7533 --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.isubcaseresponse.md @@ -0,0 +1,133 @@ +[Cases Client API Interface](../cases_client_api.md) / [typedoc_interfaces](../modules/typedoc_interfaces.md) / ISubCaseResponse + +# Interface: ISubCaseResponse + +[typedoc_interfaces](../modules/typedoc_interfaces.md).ISubCaseResponse + +## Hierarchy + +- *SubCaseResponse* + + ↳ **ISubCaseResponse** + +## Table of contents + +### Properties + +- [closed\_at](typedoc_interfaces.isubcaseresponse.md#closed_at) +- [closed\_by](typedoc_interfaces.isubcaseresponse.md#closed_by) +- [comments](typedoc_interfaces.isubcaseresponse.md#comments) +- [created\_at](typedoc_interfaces.isubcaseresponse.md#created_at) +- [created\_by](typedoc_interfaces.isubcaseresponse.md#created_by) +- [id](typedoc_interfaces.isubcaseresponse.md#id) +- [owner](typedoc_interfaces.isubcaseresponse.md#owner) +- [status](typedoc_interfaces.isubcaseresponse.md#status) +- [totalAlerts](typedoc_interfaces.isubcaseresponse.md#totalalerts) +- [totalComment](typedoc_interfaces.isubcaseresponse.md#totalcomment) +- [updated\_at](typedoc_interfaces.isubcaseresponse.md#updated_at) +- [updated\_by](typedoc_interfaces.isubcaseresponse.md#updated_by) +- [version](typedoc_interfaces.isubcaseresponse.md#version) + +## Properties + +### closed\_at + +• **closed\_at**: ``null`` \| *string* + +Inherited from: SubCaseResponse.closed\_at + +___ + +### closed\_by + +• **closed\_by**: ``null`` \| { `email`: *undefined* \| ``null`` \| *string* ; `full_name`: *undefined* \| ``null`` \| *string* ; `username`: *undefined* \| ``null`` \| *string* } + +Inherited from: SubCaseResponse.closed\_by + +___ + +### comments + +• **comments**: *undefined* \| { `comment`: *string* ; `owner`: *string* ; `type`: user } & { `associationType`: AssociationType ; `created_at`: *string* ; `created_by`: { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `owner`: *string* ; `pushed_at`: ``null`` \| *string* ; `pushed_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `updated_at`: ``null`` \| *string* ; `updated_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } } & { `id`: *string* ; `version`: *string* } & { `alertId`: *string* \| *string*[] ; `index`: *string* \| *string*[] ; `owner`: *string* ; `rule`: { id: string \| null; name: string \| null; } ; `type`: alert \| generatedAlert } & { `associationType`: AssociationType ; `created_at`: *string* ; `created_by`: { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `owner`: *string* ; `pushed_at`: ``null`` \| *string* ; `pushed_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `updated_at`: ``null`` \| *string* ; `updated_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } } & { `id`: *string* ; `version`: *string* }[] + +Inherited from: SubCaseResponse.comments + +___ + +### created\_at + +• **created\_at**: *string* + +Inherited from: SubCaseResponse.created\_at + +___ + +### created\_by + +• **created\_by**: ``null`` \| { `email`: *undefined* \| ``null`` \| *string* ; `full_name`: *undefined* \| ``null`` \| *string* ; `username`: *undefined* \| ``null`` \| *string* } + +Inherited from: SubCaseResponse.created\_by + +___ + +### id + +• **id**: *string* + +Inherited from: SubCaseResponse.id + +___ + +### owner + +• **owner**: *string* + +Inherited from: SubCaseResponse.owner + +___ + +### status + +• **status**: CaseStatuses + +Inherited from: SubCaseResponse.status + +___ + +### totalAlerts + +• **totalAlerts**: *number* + +Inherited from: SubCaseResponse.totalAlerts + +___ + +### totalComment + +• **totalComment**: *number* + +Inherited from: SubCaseResponse.totalComment + +___ + +### updated\_at + +• **updated\_at**: ``null`` \| *string* + +Inherited from: SubCaseResponse.updated\_at + +___ + +### updated\_by + +• **updated\_by**: ``null`` \| { `email`: *undefined* \| ``null`` \| *string* ; `full_name`: *undefined* \| ``null`` \| *string* ; `username`: *undefined* \| ``null`` \| *string* } + +Inherited from: SubCaseResponse.updated\_by + +___ + +### version + +• **version**: *string* + +Inherited from: SubCaseResponse.version diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.isubcasesfindresponse.md b/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.isubcasesfindresponse.md new file mode 100644 index 00000000000000..35d63126f608a1 --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.isubcasesfindresponse.md @@ -0,0 +1,79 @@ +[Cases Client API Interface](../cases_client_api.md) / [typedoc_interfaces](../modules/typedoc_interfaces.md) / ISubCasesFindResponse + +# Interface: ISubCasesFindResponse + +[typedoc_interfaces](../modules/typedoc_interfaces.md).ISubCasesFindResponse + +## Hierarchy + +- *SubCasesFindResponse* + + ↳ **ISubCasesFindResponse** + +## Table of contents + +### Properties + +- [count\_closed\_cases](typedoc_interfaces.isubcasesfindresponse.md#count_closed_cases) +- [count\_in\_progress\_cases](typedoc_interfaces.isubcasesfindresponse.md#count_in_progress_cases) +- [count\_open\_cases](typedoc_interfaces.isubcasesfindresponse.md#count_open_cases) +- [page](typedoc_interfaces.isubcasesfindresponse.md#page) +- [per\_page](typedoc_interfaces.isubcasesfindresponse.md#per_page) +- [subCases](typedoc_interfaces.isubcasesfindresponse.md#subcases) +- [total](typedoc_interfaces.isubcasesfindresponse.md#total) + +## Properties + +### count\_closed\_cases + +• **count\_closed\_cases**: *number* + +Inherited from: SubCasesFindResponse.count\_closed\_cases + +___ + +### count\_in\_progress\_cases + +• **count\_in\_progress\_cases**: *number* + +Inherited from: SubCasesFindResponse.count\_in\_progress\_cases + +___ + +### count\_open\_cases + +• **count\_open\_cases**: *number* + +Inherited from: SubCasesFindResponse.count\_open\_cases + +___ + +### page + +• **page**: *number* + +Inherited from: SubCasesFindResponse.page + +___ + +### per\_page + +• **per\_page**: *number* + +Inherited from: SubCasesFindResponse.per\_page + +___ + +### subCases + +• **subCases**: { `status`: CaseStatuses } & { `closed_at`: ``null`` \| *string* ; `closed_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `created_at`: *string* ; `created_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `owner`: *string* ; `updated_at`: ``null`` \| *string* ; `updated_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } } & { `id`: *string* ; `totalAlerts`: *number* ; `totalComment`: *number* ; `version`: *string* } & { `comments`: *undefined* \| { `comment`: *string* ; `owner`: *string* ; `type`: user } & { `associationType`: AssociationType ; `created_at`: *string* ; `created_by`: { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `owner`: *string* ; `pushed_at`: ``null`` \| *string* ; `pushed_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `updated_at`: ``null`` \| *string* ; `updated_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } } & { `id`: *string* ; `version`: *string* } & { `alertId`: *string* \| *string*[] ; `index`: *string* \| *string*[] ; `owner`: *string* ; `rule`: { id: string \| null; name: string \| null; } ; `type`: alert \| generatedAlert } & { `associationType`: AssociationType ; `created_at`: *string* ; `created_by`: { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `owner`: *string* ; `pushed_at`: ``null`` \| *string* ; `pushed_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `updated_at`: ``null`` \| *string* ; `updated_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } } & { `id`: *string* ; `version`: *string* }[] }[] + +Inherited from: SubCasesFindResponse.subCases + +___ + +### total + +• **total**: *number* + +Inherited from: SubCasesFindResponse.total diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.isubcasesresponse.md b/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.isubcasesresponse.md new file mode 100644 index 00000000000000..6ee45e59b53b58 --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/typedoc_interfaces.isubcasesresponse.md @@ -0,0 +1,11 @@ +[Cases Client API Interface](../cases_client_api.md) / [typedoc_interfaces](../modules/typedoc_interfaces.md) / ISubCasesResponse + +# Interface: ISubCasesResponse + +[typedoc_interfaces](../modules/typedoc_interfaces.md).ISubCasesResponse + +## Hierarchy + +- *SubCasesResponse* + + ↳ **ISubCasesResponse** diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/user_actions_client.useractionget.md b/x-pack/plugins/cases/docs/cases_client/interfaces/user_actions_client.useractionget.md new file mode 100644 index 00000000000000..e492747c7baad7 --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/user_actions_client.useractionget.md @@ -0,0 +1,34 @@ +[Cases Client API Interface](../cases_client_api.md) / [user_actions/client](../modules/user_actions_client.md) / UserActionGet + +# Interface: UserActionGet + +[user_actions/client](../modules/user_actions_client.md).UserActionGet + +Parameters for retrieving user actions for a particular case + +## Table of contents + +### Properties + +- [caseId](user_actions_client.useractionget.md#caseid) +- [subCaseId](user_actions_client.useractionget.md#subcaseid) + +## Properties + +### caseId + +• **caseId**: *string* + +The ID of the case + +Defined in: [user_actions/client.ts:19](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/user_actions/client.ts#L19) + +___ + +### subCaseId + +• `Optional` **subCaseId**: *string* + +If specified then a sub case will be used for finding all the user actions + +Defined in: [user_actions/client.ts:23](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/user_actions/client.ts#L23) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/user_actions_client.useractionssubclient.md b/x-pack/plugins/cases/docs/cases_client/interfaces/user_actions_client.useractionssubclient.md new file mode 100644 index 00000000000000..70dc3958b5de66 --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/user_actions_client.useractionssubclient.md @@ -0,0 +1,31 @@ +[Cases Client API Interface](../cases_client_api.md) / [user_actions/client](../modules/user_actions_client.md) / UserActionsSubClient + +# Interface: UserActionsSubClient + +[user_actions/client](../modules/user_actions_client.md).UserActionsSubClient + +API for interacting the actions performed by a user when interacting with the cases entities. + +## Table of contents + +### Methods + +- [getAll](user_actions_client.useractionssubclient.md#getall) + +## Methods + +### getAll + +▸ **getAll**(`clientArgs`: [*UserActionGet*](user_actions_client.useractionget.md)): *Promise*<[*ICaseUserActionsResponse*](typedoc_interfaces.icaseuseractionsresponse.md)\> + +Retrieves all user actions for a particular case. + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `clientArgs` | [*UserActionGet*](user_actions_client.useractionget.md) | + +**Returns:** *Promise*<[*ICaseUserActionsResponse*](typedoc_interfaces.icaseuseractionsresponse.md)\> + +Defined in: [user_actions/client.ts:33](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/user_actions/client.ts#L33) diff --git a/x-pack/plugins/cases/docs/cases_client/modules/attachments_add.md b/x-pack/plugins/cases/docs/cases_client/modules/attachments_add.md new file mode 100644 index 00000000000000..d9ac6e6ce431bb --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/modules/attachments_add.md @@ -0,0 +1,9 @@ +[Cases Client API Interface](../cases_client_api.md) / attachments/add + +# Module: attachments/add + +## Table of contents + +### Interfaces + +- [AddArgs](../interfaces/attachments_add.addargs.md) diff --git a/x-pack/plugins/cases/docs/cases_client/modules/attachments_client.md b/x-pack/plugins/cases/docs/cases_client/modules/attachments_client.md new file mode 100644 index 00000000000000..47d96b98356e71 --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/modules/attachments_client.md @@ -0,0 +1,9 @@ +[Cases Client API Interface](../cases_client_api.md) / attachments/client + +# Module: attachments/client + +## Table of contents + +### Interfaces + +- [AttachmentsSubClient](../interfaces/attachments_client.attachmentssubclient.md) diff --git a/x-pack/plugins/cases/docs/cases_client/modules/attachments_delete.md b/x-pack/plugins/cases/docs/cases_client/modules/attachments_delete.md new file mode 100644 index 00000000000000..0e2cf420b63757 --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/modules/attachments_delete.md @@ -0,0 +1,10 @@ +[Cases Client API Interface](../cases_client_api.md) / attachments/delete + +# Module: attachments/delete + +## Table of contents + +### Interfaces + +- [DeleteAllArgs](../interfaces/attachments_delete.deleteallargs.md) +- [DeleteArgs](../interfaces/attachments_delete.deleteargs.md) diff --git a/x-pack/plugins/cases/docs/cases_client/modules/attachments_get.md b/x-pack/plugins/cases/docs/cases_client/modules/attachments_get.md new file mode 100644 index 00000000000000..99358d66832561 --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/modules/attachments_get.md @@ -0,0 +1,11 @@ +[Cases Client API Interface](../cases_client_api.md) / attachments/get + +# Module: attachments/get + +## Table of contents + +### Interfaces + +- [FindArgs](../interfaces/attachments_get.findargs.md) +- [GetAllArgs](../interfaces/attachments_get.getallargs.md) +- [GetArgs](../interfaces/attachments_get.getargs.md) diff --git a/x-pack/plugins/cases/docs/cases_client/modules/attachments_update.md b/x-pack/plugins/cases/docs/cases_client/modules/attachments_update.md new file mode 100644 index 00000000000000..011fe531ede34b --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/modules/attachments_update.md @@ -0,0 +1,9 @@ +[Cases Client API Interface](../cases_client_api.md) / attachments/update + +# Module: attachments/update + +## Table of contents + +### Interfaces + +- [UpdateArgs](../interfaces/attachments_update.updateargs.md) diff --git a/x-pack/plugins/cases/docs/cases_client/modules/cases_client.md b/x-pack/plugins/cases/docs/cases_client/modules/cases_client.md new file mode 100644 index 00000000000000..c6e9cf17d98402 --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/modules/cases_client.md @@ -0,0 +1,9 @@ +[Cases Client API Interface](../cases_client_api.md) / cases/client + +# Module: cases/client + +## Table of contents + +### Interfaces + +- [CasesSubClient](../interfaces/cases_client.casessubclient.md) diff --git a/x-pack/plugins/cases/docs/cases_client/modules/cases_get.md b/x-pack/plugins/cases/docs/cases_client/modules/cases_get.md new file mode 100644 index 00000000000000..69cd5b856bbd77 --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/modules/cases_get.md @@ -0,0 +1,53 @@ +[Cases Client API Interface](../cases_client_api.md) / cases/get + +# Module: cases/get + +## Table of contents + +### Interfaces + +- [CaseIDsByAlertIDParams](../interfaces/cases_get.caseidsbyalertidparams.md) +- [GetParams](../interfaces/cases_get.getparams.md) + +### Functions + +- [getReporters](cases_get.md#getreporters) +- [getTags](cases_get.md#gettags) + +## Functions + +### getReporters + +▸ **getReporters**(`params`: AllReportersFindRequest, `clientArgs`: CasesClientArgs): *Promise* + +Retrieves the reporters from all the cases. + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `params` | AllReportersFindRequest | +| `clientArgs` | CasesClientArgs | + +**Returns:** *Promise* + +Defined in: [cases/get.ts:279](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/cases/get.ts#L279) + +___ + +### getTags + +▸ **getTags**(`params`: AllTagsFindRequest, `clientArgs`: CasesClientArgs): *Promise* + +Retrieves the tags from all the cases. + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `params` | AllTagsFindRequest | +| `clientArgs` | CasesClientArgs | + +**Returns:** *Promise* + +Defined in: [cases/get.ts:217](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/cases/get.ts#L217) diff --git a/x-pack/plugins/cases/docs/cases_client/modules/cases_push.md b/x-pack/plugins/cases/docs/cases_client/modules/cases_push.md new file mode 100644 index 00000000000000..4be9df64bb4201 --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/modules/cases_push.md @@ -0,0 +1,9 @@ +[Cases Client API Interface](../cases_client_api.md) / cases/push + +# Module: cases/push + +## Table of contents + +### Interfaces + +- [PushParams](../interfaces/cases_push.pushparams.md) diff --git a/x-pack/plugins/cases/docs/cases_client/modules/client.md b/x-pack/plugins/cases/docs/cases_client/modules/client.md new file mode 100644 index 00000000000000..7fb6b64253dd99 --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/modules/client.md @@ -0,0 +1,9 @@ +[Cases Client API Interface](../cases_client_api.md) / client + +# Module: client + +## Table of contents + +### Classes + +- [CasesClient](../classes/client.casesclient.md) diff --git a/x-pack/plugins/cases/docs/cases_client/modules/configure_client.md b/x-pack/plugins/cases/docs/cases_client/modules/configure_client.md new file mode 100644 index 00000000000000..7cfc43e3d0a887 --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/modules/configure_client.md @@ -0,0 +1,9 @@ +[Cases Client API Interface](../cases_client_api.md) / configure/client + +# Module: configure/client + +## Table of contents + +### Interfaces + +- [ConfigureSubClient](../interfaces/configure_client.configuresubclient.md) diff --git a/x-pack/plugins/cases/docs/cases_client/modules/stats_client.md b/x-pack/plugins/cases/docs/cases_client/modules/stats_client.md new file mode 100644 index 00000000000000..992a1a1ab501ab --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/modules/stats_client.md @@ -0,0 +1,9 @@ +[Cases Client API Interface](../cases_client_api.md) / stats/client + +# Module: stats/client + +## Table of contents + +### Interfaces + +- [StatsSubClient](../interfaces/stats_client.statssubclient.md) diff --git a/x-pack/plugins/cases/docs/cases_client/modules/sub_cases_client.md b/x-pack/plugins/cases/docs/cases_client/modules/sub_cases_client.md new file mode 100644 index 00000000000000..6bdf073566b1cd --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/modules/sub_cases_client.md @@ -0,0 +1,9 @@ +[Cases Client API Interface](../cases_client_api.md) / sub_cases/client + +# Module: sub\_cases/client + +## Table of contents + +### Interfaces + +- [SubCasesClient](../interfaces/sub_cases_client.subcasesclient.md) diff --git a/x-pack/plugins/cases/docs/cases_client/modules/typedoc_interfaces.md b/x-pack/plugins/cases/docs/cases_client/modules/typedoc_interfaces.md new file mode 100644 index 00000000000000..4719d2a2719c04 --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/modules/typedoc_interfaces.md @@ -0,0 +1,26 @@ +[Cases Client API Interface](../cases_client_api.md) / typedoc_interfaces + +# Module: typedoc\_interfaces + +This file defines simpler types for typedoc. This helps reduce the type alias expansion for the io-ts types because it +can be very large. These types are equivalent to the io-ts aliases. + +## Table of contents + +### Interfaces + +- [IAllCommentsResponse](../interfaces/typedoc_interfaces.iallcommentsresponse.md) +- [ICasePostRequest](../interfaces/typedoc_interfaces.icasepostrequest.md) +- [ICaseResponse](../interfaces/typedoc_interfaces.icaseresponse.md) +- [ICaseUserActionsResponse](../interfaces/typedoc_interfaces.icaseuseractionsresponse.md) +- [ICasesConfigurePatch](../interfaces/typedoc_interfaces.icasesconfigurepatch.md) +- [ICasesConfigureRequest](../interfaces/typedoc_interfaces.icasesconfigurerequest.md) +- [ICasesConfigureResponse](../interfaces/typedoc_interfaces.icasesconfigureresponse.md) +- [ICasesFindRequest](../interfaces/typedoc_interfaces.icasesfindrequest.md) +- [ICasesFindResponse](../interfaces/typedoc_interfaces.icasesfindresponse.md) +- [ICasesPatchRequest](../interfaces/typedoc_interfaces.icasespatchrequest.md) +- [ICasesResponse](../interfaces/typedoc_interfaces.icasesresponse.md) +- [ICommentsResponse](../interfaces/typedoc_interfaces.icommentsresponse.md) +- [ISubCaseResponse](../interfaces/typedoc_interfaces.isubcaseresponse.md) +- [ISubCasesFindResponse](../interfaces/typedoc_interfaces.isubcasesfindresponse.md) +- [ISubCasesResponse](../interfaces/typedoc_interfaces.isubcasesresponse.md) diff --git a/x-pack/plugins/cases/docs/cases_client/modules/user_actions_client.md b/x-pack/plugins/cases/docs/cases_client/modules/user_actions_client.md new file mode 100644 index 00000000000000..b48e3faac21351 --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client/modules/user_actions_client.md @@ -0,0 +1,10 @@ +[Cases Client API Interface](../cases_client_api.md) / user_actions/client + +# Module: user\_actions/client + +## Table of contents + +### Interfaces + +- [UserActionGet](../interfaces/user_actions_client.useractionget.md) +- [UserActionsSubClient](../interfaces/user_actions_client.useractionssubclient.md) diff --git a/x-pack/plugins/cases/docs/cases_client_typedoc.json b/x-pack/plugins/cases/docs/cases_client_typedoc.json new file mode 100644 index 00000000000000..5f67719b475745 --- /dev/null +++ b/x-pack/plugins/cases/docs/cases_client_typedoc.json @@ -0,0 +1,25 @@ +{ + "entryPoints": [ + "../server/client/client.ts", + "../server/client/typedoc_interfaces.ts", + "../server/client/attachments", + "../server/client/cases/client.ts", + "../server/client/cases/get.ts", + "../server/client/cases/push.ts", + "../server/client/configure/client.ts", + "../server/client/stats/client.ts", + "../server/client/sub_cases/client.ts", + "../server/client/user_actions/client.ts" + ], + "exclude": [ + "**/mock.ts", + "../server/client/cases/+(mock.ts|utils.ts|utils.test.ts|types.ts)" + ], + "excludeExternals": true, + "out": "cases_client", + "theme": "markdown", + "plugin": "typedoc-plugin-markdown", + "entryDocument": "cases_client_api.md", + "readme": "none", + "name": "Cases Client API Interface" +} diff --git a/x-pack/plugins/cases/server/client/attachments/add.ts b/x-pack/plugins/cases/server/client/attachments/add.ts index bce2d4e31908ba..8a16141bd2feb7 100644 --- a/x-pack/plugins/cases/server/client/attachments/add.ts +++ b/x-pack/plugins/cases/server/client/attachments/add.ts @@ -302,10 +302,21 @@ async function getCombinedCase({ * The arguments needed for creating a new attachment to a case. */ export interface AddArgs { + /** + * The case ID that this attachment will be associated with + */ caseId: string; + /** + * The attachment values. + */ comment: CommentRequest; } +/** + * Create an attachment to a case. + * + * @ignore + */ export const addComment = async ( addArgs: AddArgs, clientArgs: CasesClientArgs, diff --git a/x-pack/plugins/cases/server/client/attachments/client.ts b/x-pack/plugins/cases/server/client/attachments/client.ts index 41f1db81719fc2..1f6945a9d0584c 100644 --- a/x-pack/plugins/cases/server/client/attachments/client.ts +++ b/x-pack/plugins/cases/server/client/attachments/client.ts @@ -5,30 +5,57 @@ * 2.0. */ -import { - AllCommentsResponse, - CaseResponse, - CommentResponse, - CommentsResponse, -} from '../../../common/api'; +import { CommentResponse } from '../../../common/api'; import { CasesClientInternal } from '../client_internal'; +import { IAllCommentsResponse, ICaseResponse, ICommentsResponse } from '../typedoc_interfaces'; import { CasesClientArgs } from '../types'; import { AddArgs, addComment } from './add'; import { DeleteAllArgs, deleteAll, DeleteArgs, deleteComment } from './delete'; import { find, FindArgs, get, getAll, GetAllArgs, GetArgs } from './get'; import { update, UpdateArgs } from './update'; +/** + * API for interacting with the attachments to a case. + */ export interface AttachmentsSubClient { - add(params: AddArgs): Promise; + /** + * Adds an attachment to a case. + */ + add(params: AddArgs): Promise; + /** + * Deletes all attachments associated with a single case. + */ deleteAll(deleteAllArgs: DeleteAllArgs): Promise; + /** + * Deletes a single attachment for a specific case. + */ delete(deleteArgs: DeleteArgs): Promise; - find(findArgs: FindArgs): Promise; - getAll(getAllArgs: GetAllArgs): Promise; + /** + * Retrieves all comments matching the search criteria. + */ + find(findArgs: FindArgs): Promise; + /** + * Gets all attachments for a single case. + */ + getAll(getAllArgs: GetAllArgs): Promise; + /** + * Retrieves a single attachment for a case. + */ get(getArgs: GetArgs): Promise; - update(updateArgs: UpdateArgs): Promise; + /** + * Updates a specific attachment. + * + * The request must include all fields for the attachment. Even the fields that are not changing. + */ + update(updateArgs: UpdateArgs): Promise; } +/** + * Creates an API object for interacting with attachments. + * + * @ignore + */ export const createAttachmentsSubClient = ( clientArgs: CasesClientArgs, casesClientInternal: CasesClientInternal diff --git a/x-pack/plugins/cases/server/client/attachments/delete.ts b/x-pack/plugins/cases/server/client/attachments/delete.ts index 83df367d951ee8..28e56c21fd255c 100644 --- a/x-pack/plugins/cases/server/client/attachments/delete.ts +++ b/x-pack/plugins/cases/server/client/attachments/delete.ts @@ -20,21 +20,38 @@ import { Operations } from '../../authorization'; * Parameters for deleting all comments of a case or sub case. */ export interface DeleteAllArgs { + /** + * The case ID to delete all attachments for + */ caseID: string; + /** + * If specified the caseID will be ignored and this value will be used to find a sub case for deleting all the attachments + */ subCaseID?: string; } /** - * Parameters for deleting a single comment of a case or sub case. + * Parameters for deleting a single attachment of a case or sub case. */ export interface DeleteArgs { + /** + * The case ID to delete an attachment from + */ caseID: string; + /** + * The attachment ID to delete + */ attachmentID: string; + /** + * If specified the caseID will be ignored and this value will be used to find a sub case for deleting the attachment + */ subCaseID?: string; } /** * Delete all comments for a case or sub case. + * + * @ignore */ export async function deleteAll( { caseID, subCaseID }: DeleteAllArgs, @@ -108,6 +125,11 @@ export async function deleteAll( } } +/** + * Deletes an attachment + * + * @ignore + */ export async function deleteComment( { caseID, attachmentID, subCaseID }: DeleteArgs, clientArgs: CasesClientArgs diff --git a/x-pack/plugins/cases/server/client/attachments/get.ts b/x-pack/plugins/cases/server/client/attachments/get.ts index f6f5bcfb4f0462..d65d25d0802264 100644 --- a/x-pack/plugins/cases/server/client/attachments/get.ts +++ b/x-pack/plugins/cases/server/client/attachments/get.ts @@ -38,24 +38,53 @@ import { import { Operations } from '../../authorization'; import { includeFieldsRequiredForAuthentication } from '../../authorization/utils'; +/** + * Parameters for finding attachments of a case + */ export interface FindArgs { + /** + * The case ID for finding associated attachments + */ caseID: string; + /** + * Optional parameters for filtering the returned attachments + */ queryParams?: FindQueryParams; } +/** + * Parameters for retrieving all attachments of a case + */ export interface GetAllArgs { + /** + * The case ID to retrieve all attachments for + */ caseID: string; + /** + * Optionally include the attachments associated with a sub case + */ includeSubCaseComments?: boolean; + /** + * If included the case ID will be ignored and the attachments will be retrieved from the specified ID of the sub case + */ subCaseID?: string; } export interface GetArgs { + /** + * The ID of the case to retrieve an attachment from + */ caseID: string; + /** + * The ID of the attachment to retrieve + */ attachmentID: string; } /** * Retrieves the attachments for a case entity. This support pagination. + * + * @ignore */ export async function find( { caseID, queryParams }: FindArgs, @@ -146,6 +175,8 @@ export async function find( /** * Retrieves a single attachment by its ID. + * + * @ignore */ export async function get( { attachmentID, caseID }: GetArgs, @@ -186,6 +217,8 @@ export async function get( /** * Retrieves all the attachments for a case. The `includeSubCaseComments` can be used to include the sub case comments for * collections. If the entity is a sub case, pass in the subCaseID. + * + * @ignore */ export async function getAll( { caseID, includeSubCaseComments, subCaseID }: GetAllArgs, diff --git a/x-pack/plugins/cases/server/client/attachments/update.ts b/x-pack/plugins/cases/server/client/attachments/update.ts index 26c44509abce8c..713fd931dcb909 100644 --- a/x-pack/plugins/cases/server/client/attachments/update.ts +++ b/x-pack/plugins/cases/server/client/attachments/update.ts @@ -19,9 +19,21 @@ import { decodeCommentRequest, ensureAuthorized } from '../utils'; import { createCaseError } from '../../common/error'; import { Operations } from '../../authorization'; +/** + * Parameters for updating a single attachment + */ export interface UpdateArgs { + /** + * The ID of the case that is associated with this attachment + */ caseID: string; + /** + * The full attachment request with the fields updated with appropriate values + */ updateRequest: CommentPatchRequest; + /** + * The ID of a sub case, if specified a sub case will be searched for to perform the attachment update instead of on a case + */ subCaseID?: string; } @@ -78,6 +90,8 @@ async function getCommentableCase({ /** * Update an attachment. + * + * @ignore */ export async function update( { caseID, subCaseID, updateRequest: queryParams }: UpdateArgs, diff --git a/x-pack/plugins/cases/server/client/cases/client.ts b/x-pack/plugins/cases/server/client/cases/client.ts index 06a90a3b2cd95a..20670f331443b4 100644 --- a/x-pack/plugins/cases/server/client/cases/client.ts +++ b/x-pack/plugins/cases/server/client/cases/client.ts @@ -5,57 +5,89 @@ * 2.0. */ -import { ActionsClient } from '../../../../actions/server'; import { CasePostRequest, - CaseResponse, CasesPatchRequest, - CasesResponse, CasesFindRequest, - CasesFindResponse, User, AllTagsFindRequest, AllReportersFindRequest, } from '../../../common/api'; import { CasesClient } from '../client'; import { CasesClientInternal } from '../client_internal'; +import { + ICasePostRequest, + ICaseResponse, + ICasesFindRequest, + ICasesFindResponse, + ICasesPatchRequest, + ICasesResponse, +} from '../typedoc_interfaces'; import { CasesClientArgs } from '../types'; import { create } from './create'; import { deleteCases } from './delete'; import { find } from './find'; -import { CaseIDsByAlertIDParams, get, getCaseIDsByAlertID, getReporters, getTags } from './get'; -import { push } from './push'; +import { + CaseIDsByAlertIDParams, + get, + getCaseIDsByAlertID, + GetParams, + getReporters, + getTags, +} from './get'; +import { push, PushParams } from './push'; import { update } from './update'; -interface CaseGet { - id: string; - includeComments?: boolean; - includeSubCaseComments?: boolean; -} - -interface CasePush { - actionsClient: ActionsClient; - caseId: string; - connectorId: string; -} - /** - * The public API for interacting with cases. + * API for interacting with the cases entities. */ export interface CasesSubClient { - create(data: CasePostRequest): Promise; - find(params: CasesFindRequest): Promise; - get(params: CaseGet): Promise; - push(args: CasePush): Promise; - update(cases: CasesPatchRequest): Promise; + /** + * Creates a case. + */ + create(data: ICasePostRequest): Promise; + /** + * Returns cases that match the search criteria. + * + * If the `owner` field is left empty then all the cases that the user has access to will be returned. + */ + find(params: ICasesFindRequest): Promise; + /** + * Retrieves a single case with the specified ID. + */ + get(params: GetParams): Promise; + /** + * Pushes a specific case to an external system. + */ + push(args: PushParams): Promise; + /** + * Update the specified cases with the passed in values. + */ + update(cases: ICasesPatchRequest): Promise; + /** + * Delete a case and all its comments. + * + * @params ids an array of case IDs to delete + */ delete(ids: string[]): Promise; + /** + * Retrieves all the tags across all cases the user making the request has access to. + */ getTags(params: AllTagsFindRequest): Promise; + /** + * Retrieves all the reporters across all accessible cases. + */ getReporters(params: AllReportersFindRequest): Promise; + /** + * Retrieves the case IDs given a single alert ID + */ getCaseIDsByAlertID(params: CaseIDsByAlertIDParams): Promise; } /** * Creates the interface for CRUD on cases objects. + * + * @ignore */ export const createCasesSubClient = ( clientArgs: CasesClientArgs, @@ -65,8 +97,8 @@ export const createCasesSubClient = ( const casesSubClient: CasesSubClient = { 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), + get: (params: GetParams) => get(params, clientArgs), + push: (params: PushParams) => push(params, clientArgs, casesClient, casesClientInternal), update: (cases: CasesPatchRequest) => update(cases, clientArgs, casesClientInternal), delete: (ids: string[]) => deleteCases(ids, clientArgs), getTags: (params: AllTagsFindRequest) => getTags(params, clientArgs), diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index 334b1a2ee46480..1d3e8d432410d6 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -37,6 +37,8 @@ import { CasesClientArgs } from '..'; /** * Creates a new case. + * + * @ignore */ export const create = async ( data: CasePostRequest, diff --git a/x-pack/plugins/cases/server/client/cases/delete.ts b/x-pack/plugins/cases/server/client/cases/delete.ts index 256a8be2ccbe02..de6d317d7c2d89 100644 --- a/x-pack/plugins/cases/server/client/cases/delete.ts +++ b/x-pack/plugins/cases/server/client/cases/delete.ts @@ -50,6 +50,11 @@ async function deleteSubCases({ ); } +/** + * Deletes the specified cases and their attachments. + * + * @ignore + */ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): Promise { const { savedObjectsClient: soClient, diff --git a/x-pack/plugins/cases/server/client/cases/find.ts b/x-pack/plugins/cases/server/client/cases/find.ts index 0899cd3d0150f2..a7e36461965a9a 100644 --- a/x-pack/plugins/cases/server/client/cases/find.ts +++ b/x-pack/plugins/cases/server/client/cases/find.ts @@ -29,6 +29,8 @@ import { CasesClientArgs } from '..'; /** * Retrieves a case and optionally its comments and sub case comments. + * + * @ignore */ export const find = async ( params: CasesFindRequest, diff --git a/x-pack/plugins/cases/server/client/cases/get.ts b/x-pack/plugins/cases/server/client/cases/get.ts index 92e4ea798eaa21..1434d54f6a2b72 100644 --- a/x-pack/plugins/cases/server/client/cases/get.ts +++ b/x-pack/plugins/cases/server/client/cases/get.ts @@ -37,14 +37,25 @@ import { } from '../utils'; import { CaseService } from '../../services'; +/** + * Parameters for finding cases IDs using an alert ID + */ export interface CaseIDsByAlertIDParams { + /** + * The alert ID to search for + */ alertID: string; + /** + * The filtering options when searching for associated cases. + */ options: CasesByAlertIDRequest; } /** * Case Client wrapper function for retrieving the case IDs that have a particular alert ID * attached to them. This handles RBAC before calling the saved object API. + * + * @ignore */ export const getCaseIDsByAlertID = async ( { alertID, options }: CaseIDsByAlertIDParams, @@ -101,14 +112,28 @@ export const getCaseIDsByAlertID = async ( } }; -interface GetParams { +/** + * The parameters for retrieving a case + */ +export interface GetParams { + /** + * Case ID + */ id: string; + /** + * Whether to include the attachments for a case in the response + */ includeComments?: boolean; + /** + * Whether to include the attachments for all children of a case in the response + */ includeSubCaseComments?: boolean; } /** * Retrieves a case and optionally its comments and sub case comments. + * + * @ignore */ export const get = async ( { id, includeComments, includeSubCaseComments }: GetParams, diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index 846b07885c817f..c85fcd05f7e4dd 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -43,11 +43,25 @@ function shouldCloseByPush( ); } -interface PushParams { +/** + * Parameters for pushing a case to an external system + */ +export interface PushParams { + /** + * The ID of a case + */ caseId: string; + /** + * The ID of an external system to push to + */ connectorId: string; } +/** + * Push a case to an external service. + * + * @ignore + */ export const push = async ( { connectorId, caseId }: PushParams, clientArgs: CasesClientArgs, diff --git a/x-pack/plugins/cases/server/client/cases/update.ts b/x-pack/plugins/cases/server/client/cases/update.ts index de3c499db50984..b11c8574c5d621 100644 --- a/x-pack/plugins/cases/server/client/cases/update.ts +++ b/x-pack/plugins/cases/server/client/cases/update.ts @@ -383,6 +383,11 @@ function partitionPatchRequest( }; } +/** + * Updates the specified cases with new values + * + * @ignore + */ export const update = async ( cases: CasesPatchRequest, clientArgs: CasesClientArgs, diff --git a/x-pack/plugins/cases/server/client/client.ts b/x-pack/plugins/cases/server/client/client.ts index 9d0da7018518f3..4b21b401f5b7bf 100644 --- a/x-pack/plugins/cases/server/client/client.ts +++ b/x-pack/plugins/cases/server/client/client.ts @@ -15,6 +15,9 @@ import { ENABLE_CASE_CONNECTOR } from '../../common/constants'; import { ConfigureSubClient, createConfigurationSubClient } from './configure/client'; import { createStatsSubClient, StatsSubClient } from './stats/client'; +/** + * Client wrapper that contains accessor methods for individual entities within the cases system. + */ export class CasesClient { private readonly _casesClientInternal: CasesClientInternal; private readonly _cases: CasesSubClient; @@ -34,18 +37,32 @@ export class CasesClient { this._stats = createStatsSubClient(args); } + /** + * Retrieves an interface for interacting with cases entities. + */ public get cases() { return this._cases; } + /** + * Retrieves an interface for interacting with attachments (comments) entities. + */ public get attachments() { return this._attachments; } + /** + * Retrieves an interface for interacting with the user actions associated with the plugin entities. + */ public get userActions() { return this._userActions; } + /** + * Retrieves an interface for interacting with the case as a connector entities. + * + * Currently this functionality is disabled and will throw an error if this function is called. + */ public get subCases() { if (!ENABLE_CASE_CONNECTOR) { throw new Error('The case connector feature is disabled'); @@ -53,15 +70,29 @@ export class CasesClient { return this._subCases; } + /** + * Retrieves an interface for interacting with the configuration of external connectors for the plugin entities. + */ public get configure() { return this._configure; } + /** + * Retrieves an interface for retrieving statistics related to the cases entities. + */ public get stats() { return this._stats; } } +/** + * Creates a {@link CasesClient} for interacting with the cases entities + * + * @param args arguments for initializing the cases client + * @returns a {@link CasesClient} + * + * @ignore + */ export const createCasesClient = (args: CasesClientArgs): CasesClient => { return new CasesClient(args); }; diff --git a/x-pack/plugins/cases/server/client/configure/client.ts b/x-pack/plugins/cases/server/client/configure/client.ts index 1e44e615626b77..7145491b8f2bf9 100644 --- a/x-pack/plugins/cases/server/client/configure/client.ts +++ b/x-pack/plugins/cases/server/client/configure/client.ts @@ -54,9 +54,16 @@ import { } from './types'; import { createMappings } from './create_mappings'; import { updateMappings } from './update_mappings'; +import { + ICasesConfigurePatch, + ICasesConfigureRequest, + ICasesConfigureResponse, +} from '../typedoc_interfaces'; /** * Defines the internal helper functions. + * + * @ignore */ export interface InternalConfigureSubClient { getFields(params: ConfigurationGetFields): Promise; @@ -71,18 +78,35 @@ export interface InternalConfigureSubClient { * This is the public API for interacting with the connector configuration for cases. */ export interface ConfigureSubClient { - get(params: GetConfigureFindRequest): Promise; + /** + * Retrieves the external connector configuration for a particular case owner. + */ + get(params: GetConfigureFindRequest): Promise; + /** + * Retrieves the valid external connectors supported by the cases plugin. + */ getConnectors(): Promise; + /** + * Updates a particular configuration with new values. + * + * @param configurationId the ID of the configuration to update + * @param configurations the new configuration parameters + */ update( configurationId: string, - configurations: CasesConfigurePatch - ): Promise; - create(configuration: CasesConfigureRequest): Promise; + configurations: ICasesConfigurePatch + ): Promise; + /** + * Creates a configuration if one does not already exist. If one exists it is deleted and a new one is created. + */ + create(configuration: ICasesConfigureRequest): Promise; } /** * These functions should not be exposed on the plugin contract. They are for internal use to support the CRUD of * configurations. + * + * @ignore */ export const createInternalConfigurationSubClient = ( clientArgs: CasesClientArgs, @@ -100,6 +124,11 @@ export const createInternalConfigurationSubClient = ( return Object.freeze(configureSubClient); }; +/** + * Creates an API object for interacting with the configuration entities + * + * @ignore + */ export const createConfigurationSubClient = ( clientArgs: CasesClientArgs, casesInternalClient: CasesClientInternal diff --git a/x-pack/plugins/cases/server/client/factory.ts b/x-pack/plugins/cases/server/client/factory.ts index 1202fe8c2a421d..86e979fc32647b 100644 --- a/x-pack/plugins/cases/server/client/factory.ts +++ b/x-pack/plugins/cases/server/client/factory.ts @@ -16,12 +16,12 @@ import { SAVED_OBJECT_TYPES } from '../../common/constants'; import { Authorization } from '../authorization/authorization'; import { GetSpaceFn } from '../authorization/types'; import { - AlertServiceContract, CaseConfigureService, CaseService, CaseUserActionService, ConnectorMappingsService, AttachmentService, + AlertService, } from '../services'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; import { PluginStartContract as ActionsPluginStart } from '../../../actions/server'; @@ -29,12 +29,6 @@ import { AuthorizationAuditLogger } from '../authorization'; import { CasesClient, createCasesClient } from '.'; interface CasesClientFactoryArgs { - caseConfigureService: CaseConfigureService; - caseService: CaseService; - connectorMappingsService: ConnectorMappingsService; - userActionService: CaseUserActionService; - alertsService: AlertServiceContract; - attachmentService: AttachmentService; securityPluginSetup?: SecurityPluginSetup; securityPluginStart?: SecurityPluginStart; getSpace: GetSpaceFn; @@ -71,22 +65,14 @@ export class CasesClientFactory { scopedClusterClient, savedObjectsService, }: { - // TODO: make these required when the case connector can get a request and savedObjectsService - request?: KibanaRequest; - savedObjectsService?: SavedObjectsServiceStart; + request: KibanaRequest; + savedObjectsService: SavedObjectsServiceStart; scopedClusterClient: ElasticsearchClient; }): Promise { if (!this.isInitialized || !this.options) { throw new Error('CasesClientFactory must be initialized before calling create'); } - // TODO: remove this - if (!request || !savedObjectsService) { - throw new Error( - 'CasesClientFactory must be initialized with a request and saved object service' - ); - } - const auditLogger = this.options.securityPluginSetup?.audit.asScoped(request); const auth = await Authorization.create({ @@ -97,21 +83,22 @@ export class CasesClientFactory { auditLogger: new AuthorizationAuditLogger(auditLogger), }); - const userInfo = this.options.caseService.getUser({ request }); + const caseService = new CaseService(this.logger, this.options?.securityPluginStart?.authc); + const userInfo = caseService.getUser({ request }); return createCasesClient({ - alertsService: this.options.alertsService, + alertsService: new AlertService(), scopedClusterClient, savedObjectsClient: savedObjectsService.getScopedClient(request, { includedHiddenTypes: SAVED_OBJECT_TYPES, }), // We only want these fields from the userInfo object user: { username: userInfo.username, email: userInfo.email, full_name: userInfo.full_name }, - caseService: this.options.caseService, - caseConfigureService: this.options.caseConfigureService, - connectorMappingsService: this.options.connectorMappingsService, - userActionService: this.options.userActionService, - attachmentService: this.options.attachmentService, + caseService, + caseConfigureService: new CaseConfigureService(this.logger), + connectorMappingsService: new ConnectorMappingsService(this.logger), + userActionService: new CaseUserActionService(this.logger), + attachmentService: new AttachmentService(this.logger), logger: this.logger, authorization: auth, auditLogger, diff --git a/x-pack/plugins/cases/server/client/stats/client.ts b/x-pack/plugins/cases/server/client/stats/client.ts index 8c18c35e8f4fd6..eb9f885a735aad 100644 --- a/x-pack/plugins/cases/server/client/stats/client.ts +++ b/x-pack/plugins/cases/server/client/stats/client.ts @@ -15,11 +15,16 @@ import { constructQueryOptions, getAuthorizationFilter } from '../utils'; * Statistics API contract. */ export interface StatsSubClient { + /** + * Retrieves the total number of open, closed, and in-progress cases. + */ getStatusTotalsByType(): Promise; } /** * Creates the interface for retrieving the number of open, closed, and in progress cases. + * + * @ignore */ export function createStatsSubClient(clientArgs: CasesClientArgs): StatsSubClient { return Object.freeze({ diff --git a/x-pack/plugins/cases/server/client/sub_cases/client.ts b/x-pack/plugins/cases/server/client/sub_cases/client.ts index 102cbee14a2060..3830c84248502a 100644 --- a/x-pack/plugins/cases/server/client/sub_cases/client.ts +++ b/x-pack/plugins/cases/server/client/sub_cases/client.ts @@ -15,7 +15,6 @@ import { SubCasesFindResponse, SubCasesFindResponseRt, SubCasesPatchRequest, - SubCasesResponse, } from '../../../common/api'; import { CasesClientArgs, CasesClientInternal } from '..'; import { countAlertsForID, flattenSubCaseSavedObject, transformSubCases } from '../../common'; @@ -25,29 +24,58 @@ import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; import { constructQueryOptions } from '../utils'; import { defaultPage, defaultPerPage } from '../../routes/api'; import { update } from './update'; +import { ISubCaseResponse, ISubCasesFindResponse, ISubCasesResponse } from '../typedoc_interfaces'; interface FindArgs { + /** + * The case ID for finding associated sub cases + */ caseID: string; + /** + * Options for filtering the returned sub cases + */ queryParams: SubCasesFindRequest; } interface GetArgs { + /** + * A flag to include the attachments with the results + */ includeComments: boolean; + /** + * The ID of the sub case to retrieve + */ id: string; } /** * The API routes for interacting with sub cases. + * + * @public */ export interface SubCasesClient { + /** + * Deletes the specified entities and their attachments. + */ delete(ids: string[]): Promise; - find(findArgs: FindArgs): Promise; - get(getArgs: GetArgs): Promise; - update(subCases: SubCasesPatchRequest): Promise; + /** + * Retrieves the sub cases matching the search criteria. + */ + find(findArgs: FindArgs): Promise; + /** + * Retrieves a single sub case. + */ + get(getArgs: GetArgs): Promise; + /** + * Updates the specified sub cases to the new values included in the request. + */ + update(subCases: SubCasesPatchRequest): Promise; } /** * Creates a client for handling the different exposed API routes for interacting with sub cases. + * + * @ignore */ export function createSubCasesClient( clientArgs: CasesClientArgs, diff --git a/x-pack/plugins/cases/server/client/typedoc_interfaces.ts b/x-pack/plugins/cases/server/client/typedoc_interfaces.ts new file mode 100644 index 00000000000000..bf444ee9420edc --- /dev/null +++ b/x-pack/plugins/cases/server/client/typedoc_interfaces.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * This file defines simpler types for typedoc. This helps reduce the type alias expansion for the io-ts types because it + * can be very large. These types are equivalent to the io-ts aliases. + * @module + */ + +/* eslint-disable @typescript-eslint/no-empty-interface */ + +import { + AllCommentsResponse, + CasePostRequest, + CaseResponse, + CasesConfigurePatch, + CasesConfigureRequest, + CasesConfigureResponse, + CasesFindRequest, + CasesFindResponse, + CasesPatchRequest, + CasesResponse, + CaseUserActionsResponse, + CommentsResponse, + SubCaseResponse, + SubCasesFindResponse, + SubCasesResponse, +} from '../../common'; + +/** + * These are simply to make typedoc not attempt to expand the type aliases. If it attempts to expand them + * the docs are huge. + */ + +export interface ICasePostRequest extends CasePostRequest {} +export interface ICasesFindRequest extends CasesFindRequest {} +export interface ICasesPatchRequest extends CasesPatchRequest {} +export interface ICaseResponse extends CaseResponse {} +export interface ICasesResponse extends CasesResponse {} +export interface ICasesFindResponse extends CasesFindResponse {} + +export interface ICasesConfigureResponse extends CasesConfigureResponse {} +export interface ICasesConfigureRequest extends CasesConfigureRequest {} +export interface ICasesConfigurePatch extends CasesConfigurePatch {} + +export interface ICommentsResponse extends CommentsResponse {} +export interface IAllCommentsResponse extends AllCommentsResponse {} + +export interface ISubCasesFindResponse extends SubCasesFindResponse {} +export interface ISubCaseResponse extends SubCaseResponse {} +export interface ISubCasesResponse extends SubCasesResponse {} + +export interface ICaseUserActionsResponse extends CaseUserActionsResponse {} diff --git a/x-pack/plugins/cases/server/client/types.ts b/x-pack/plugins/cases/server/client/types.ts index 5147cea0b59f0d..340327cecabd9a 100644 --- a/x-pack/plugins/cases/server/client/types.ts +++ b/x-pack/plugins/cases/server/client/types.ts @@ -20,6 +20,9 @@ import { } from '../services'; import { ActionsClient } from '../../../actions/server'; +/** + * Parameters for initializing a cases client + */ export interface CasesClientArgs { readonly scopedClusterClient: ElasticsearchClient; readonly caseConfigureService: CaseConfigureService; 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 909c5337853020..1e2fe8d4f4fca3 100644 --- a/x-pack/plugins/cases/server/client/user_actions/client.ts +++ b/x-pack/plugins/cases/server/client/user_actions/client.ts @@ -5,19 +5,39 @@ * 2.0. */ -import { CaseUserActionsResponse } from '../../../common/api'; +import { ICaseUserActionsResponse } from '../typedoc_interfaces'; import { CasesClientArgs } from '../types'; import { get } from './get'; +/** + * Parameters for retrieving user actions for a particular case + */ export interface UserActionGet { + /** + * The ID of the case + */ caseId: string; + /** + * If specified then a sub case will be used for finding all the user actions + */ subCaseId?: string; } +/** + * API for interacting the actions performed by a user when interacting with the cases entities. + */ export interface UserActionsSubClient { - getAll(clientArgs: UserActionGet): Promise; + /** + * Retrieves all user actions for a particular case. + */ + getAll(clientArgs: UserActionGet): Promise; } +/** + * Creates an API object for interacting with the user action entities + * + * @ignore + */ export const createUserActionsSubClient = (clientArgs: CasesClientArgs): UserActionsSubClient => { const attachmentSubClient: UserActionsSubClient = { getAll: (params: UserActionGet) => get(params, clientArgs), 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 0b03fb75614a80..30e2e3095c8a4b 100644 --- a/x-pack/plugins/cases/server/client/user_actions/get.ts +++ b/x-pack/plugins/cases/server/client/user_actions/get.ts @@ -16,14 +16,10 @@ import { checkEnabledCaseConnectorOrThrow } from '../../common'; import { CasesClientArgs } from '..'; import { ensureAuthorized } from '../utils'; import { Operations } from '../../authorization'; - -interface GetParams { - caseId: string; - subCaseId?: string; -} +import { UserActionGet } from './client'; export const get = async ( - { caseId, subCaseId }: GetParams, + { caseId, subCaseId }: UserActionGet, clientArgs: CasesClientArgs ): Promise => { const { savedObjectsClient, userActionService, logger, authorization, auditLogger } = clientArgs; diff --git a/x-pack/plugins/cases/server/connectors/case/index.test.ts b/x-pack/plugins/cases/server/connectors/case/index.test.ts index a2afc1df4ecf74..0727fbbe767760 100644 --- a/x-pack/plugins/cases/server/connectors/case/index.test.ts +++ b/x-pack/plugins/cases/server/connectors/case/index.test.ts @@ -903,7 +903,7 @@ describe('case connector', () => { } }); - // TODO: enable these when the actions framework provides a request and a saved objects service + // Enable these when the actions framework provides a request and a saved objects service // ENABLE_CASE_CONNECTOR: enable these tests after the case connector feature is completed describe.skip('execute', () => { it('allows only supported sub-actions', async () => { diff --git a/x-pack/plugins/cases/server/connectors/case/index.ts b/x-pack/plugins/cases/server/connectors/case/index.ts index f647c67d286d95..4a706d8fcb52c4 100644 --- a/x-pack/plugins/cases/server/connectors/case/index.ts +++ b/x-pack/plugins/cases/server/connectors/case/index.ts @@ -26,6 +26,7 @@ import * as i18n from './translations'; import { GetActionTypeParams, isCommentGeneratedAlert, separator } from '..'; import { createCaseError } from '../../common/error'; import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; +import { CasesClient } from '../../client'; const supportedSubActions: string[] = ['create', 'update', 'addComment']; @@ -57,16 +58,11 @@ async function executor( throw new Error(msg); } - const { actionId, params, services } = execOptions; + const { actionId, params } = execOptions; const { subAction, subActionParams } = params; let data: CaseExecutorResponse | null = null; - const { scopedClusterClient } = services; - const casesClient = await factory.create({ - request: undefined, - savedObjectsService: undefined, - scopedClusterClient, - }); + let casesClient: CasesClient | undefined; if (!supportedSubActions.includes(subAction)) { const errorMessage = `[Action][Case] subAction ${subAction} not implemented.`; @@ -74,54 +70,57 @@ async function executor( throw new Error(errorMessage); } - if (subAction === 'create') { - try { - data = await casesClient.cases.create({ - ...(subActionParams as CasePostRequest), - }); - } catch (error) { - throw createCaseError({ - message: `Failed to create a case using connector: ${error}`, - error, - logger, - }); + // When the actions framework provides the request and a way to retrieve the saved objects client with access to our + // hidden types then remove this outer if block and initialize the casesClient using the factory. + if (casesClient) { + if (subAction === 'create') { + try { + data = await casesClient.cases.create({ + ...(subActionParams as CasePostRequest), + }); + } catch (error) { + throw createCaseError({ + message: `Failed to create a case using connector: ${error}`, + error, + logger, + }); + } } - } - if (subAction === 'update') { - const updateParamsWithoutNullValues = Object.entries(subActionParams).reduce( - (acc, [key, value]) => ({ - ...acc, - ...(value != null ? { [key]: value } : {}), - }), - {} as CasePatchRequest - ); + if (subAction === 'update') { + const updateParamsWithoutNullValues = Object.entries(subActionParams).reduce( + (acc, [key, value]) => ({ + ...acc, + ...(value != null ? { [key]: value } : {}), + }), + {} as CasePatchRequest + ); - try { - data = await casesClient.cases.update({ cases: [updateParamsWithoutNullValues] }); - } catch (error) { - throw createCaseError({ - message: `Failed to update case using connector id: ${updateParamsWithoutNullValues?.id} version: ${updateParamsWithoutNullValues?.version}: ${error}`, - error, - logger, - }); + try { + data = await casesClient.cases.update({ cases: [updateParamsWithoutNullValues] }); + } catch (error) { + throw createCaseError({ + message: `Failed to update case using connector id: ${updateParamsWithoutNullValues?.id} version: ${updateParamsWithoutNullValues?.version}: ${error}`, + error, + logger, + }); + } } - } - if (subAction === 'addComment') { - const { caseId, comment } = subActionParams as ExecutorSubActionAddCommentParams; - try { - const formattedComment = transformConnectorComment(comment, logger); - data = await casesClient.attachments.add({ caseId, comment: formattedComment }); - } catch (error) { - throw createCaseError({ - message: `Failed to create comment using connector case id: ${caseId}: ${error}`, - error, - logger, - }); + if (subAction === 'addComment') { + const { caseId, comment } = subActionParams as ExecutorSubActionAddCommentParams; + try { + const formattedComment = transformConnectorComment(comment, logger); + data = await casesClient.attachments.add({ caseId, comment: formattedComment }); + } catch (error) { + throw createCaseError({ + message: `Failed to create comment using connector case id: ${caseId}: ${error}`, + error, + logger, + }); + } } } - return { status: 'ok', data: data ?? {}, actionId }; } diff --git a/x-pack/plugins/cases/server/index.ts b/x-pack/plugins/cases/server/index.ts index 628a39ba77489b..fffaf08d2cc430 100644 --- a/x-pack/plugins/cases/server/index.ts +++ b/x-pack/plugins/cases/server/index.ts @@ -18,3 +18,5 @@ export const config: PluginConfigDescriptor = { }; export const plugin = (initializerContext: PluginInitializerContext) => new CasePlugin(initializerContext); + +export { PluginsStartContract } from './plugin'; diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index ad601e132535b7..c9560bb82ded83 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -25,20 +25,13 @@ import { caseUserActionSavedObjectType, subCaseSavedObjectType, } from './saved_object_types'; -import { - CaseConfigureService, - CaseService, - CaseUserActionService, - ConnectorMappingsService, - AlertService, -} from './services'; + import { CasesClient } from './client'; import { registerConnectors } from './connectors'; import type { CasesRequestHandlerContext } from './types'; import { CasesClientFactory } from './client/factory'; import { SpacesPluginStart } from '../../spaces/server'; import { PluginStartContract as FeaturesPluginStart } from '../../features/server'; -import { AttachmentService } from './services/attachments'; function createConfig(context: PluginInitializerContext) { return context.config.get(); @@ -56,14 +49,21 @@ export interface PluginsStart { actions: ActionsPluginStart; } +/** + * Cases server exposed contract for interacting with cases entities. + */ +export interface PluginsStartContract { + /** + * Returns a client which can be used to interact with the cases backend entities. + * + * @param request a KibanaRequest + * @returns a {@link CasesClient} + */ + getCasesClientWithRequest(request: KibanaRequest): Promise; +} + export class CasePlugin { private readonly log: Logger; - private caseConfigureService?: CaseConfigureService; - private caseService?: CaseService; - private connectorMappingsService?: ConnectorMappingsService; - private userActionService?: CaseUserActionService; - private alertsService?: AlertService; - private attachmentService?: AttachmentService; private clientFactory: CasesClientFactory; private securityPluginSetup?: SecurityPluginSetup; @@ -93,16 +93,6 @@ export class CasePlugin { )}] and plugins [${Object.keys(plugins)}]` ); - this.caseService = new CaseService( - this.log, - plugins.security != null ? plugins.security.authc : undefined - ); - this.caseConfigureService = new CaseConfigureService(this.log); - this.connectorMappingsService = new ConnectorMappingsService(this.log); - this.userActionService = new CaseUserActionService(this.log); - this.alertsService = new AlertService(); - this.attachmentService = new AttachmentService(this.log); - core.http.registerRouteHandlerContext( APP_ID, this.createRouteHandlerContext({ @@ -113,11 +103,6 @@ export class CasePlugin { const router = core.http.createRouter(); initCaseApi({ logger: this.log, - caseService: this.caseService, - caseConfigureService: this.caseConfigureService, - connectorMappingsService: this.connectorMappingsService, - userActionService: this.userActionService, - attachmentService: this.attachmentService, router, }); @@ -131,16 +116,10 @@ export class CasePlugin { } } - public start(core: CoreStart, plugins: PluginsStart) { + public start(core: CoreStart, plugins: PluginsStart): PluginsStartContract { this.log.debug(`Starting Case Workflow`); this.clientFactory.initialize({ - alertsService: this.alertsService!, - caseConfigureService: this.caseConfigureService!, - caseService: this.caseService!, - connectorMappingsService: this.connectorMappingsService!, - userActionService: this.userActionService!, - attachmentService: this.attachmentService!, securityPluginSetup: this.securityPluginSetup, securityPluginStart: plugins.security, getSpace: async (request: KibanaRequest) => { @@ -150,19 +129,18 @@ export class CasePlugin { actionsPluginStart: plugins.actions, }); - const getCasesClientWithRequestAndContext = async ( - context: CasesRequestHandlerContext, - request: KibanaRequest - ): Promise => { + const client = core.elasticsearch.client; + + const getCasesClientWithRequest = async (request: KibanaRequest): Promise => { return this.clientFactory.create({ request, - scopedClusterClient: context.core.elasticsearch.client.asCurrentUser, + scopedClusterClient: client.asScoped(request).asCurrentUser, savedObjectsService: core.savedObjects, }); }; return { - getCasesClientWithRequestAndContext, + getCasesClientWithRequest, }; } @@ -177,6 +155,7 @@ export class CasePlugin { }): IContextProvider => { return async (context, request, response) => { const [{ savedObjects }] = await core.getStartServices(); + return { getCasesClient: async () => { return this.clientFactory.create({ diff --git a/x-pack/plugins/cases/server/routes/api/cases/find_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/find_cases.ts index c6ec5245ebd8ae..1ded265a8b1764 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/find_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/find_cases.ts @@ -10,7 +10,7 @@ import { wrapError, escapeHatch } from '../utils'; import { RouteDeps } from '../types'; import { CASES_URL } from '../../../../common/constants'; -export function initFindCasesApi({ caseService, router, logger }: RouteDeps) { +export function initFindCasesApi({ router, logger }: RouteDeps) { router.get( { path: `${CASES_URL}/_find`, diff --git a/x-pack/plugins/cases/server/routes/api/cases/push_case.ts b/x-pack/plugins/cases/server/routes/api/cases/push_case.ts index 4028f192e725cc..a49e1a99c418fe 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/push_case.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/push_case.ts @@ -32,11 +32,6 @@ export function initPushCaseApi({ router, logger }: RouteDeps) { } const casesClient = await context.cases.getCasesClient(); - const actionsClient = context.actions?.getActionsClient(); - - if (actionsClient == null) { - return response.badRequest({ body: 'Action client not found' }); - } const params = pipe( CasePushRequestParamsRt.decode(request.params), @@ -45,7 +40,6 @@ export function initPushCaseApi({ router, logger }: RouteDeps) { return response.ok({ body: await casesClient.cases.push({ - actionsClient, caseId: params.case_id, connectorId: params.connector_id, }), diff --git a/x-pack/plugins/cases/server/routes/api/sub_case/delete_sub_cases.ts b/x-pack/plugins/cases/server/routes/api/sub_case/delete_sub_cases.ts index 45899735ddb04f..11b68b70390fe4 100644 --- a/x-pack/plugins/cases/server/routes/api/sub_case/delete_sub_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/sub_case/delete_sub_cases.ts @@ -10,7 +10,7 @@ import { RouteDeps } from '../types'; import { wrapError } from '../utils'; import { SUB_CASES_PATCH_DEL_URL } from '../../../../common/constants'; -export function initDeleteSubCasesApi({ caseService, router, logger }: RouteDeps) { +export function initDeleteSubCasesApi({ router, logger }: RouteDeps) { router.delete( { path: SUB_CASES_PATCH_DEL_URL, diff --git a/x-pack/plugins/cases/server/routes/api/sub_case/find_sub_cases.ts b/x-pack/plugins/cases/server/routes/api/sub_case/find_sub_cases.ts index 8243e4a9529938..e062f2238439ee 100644 --- a/x-pack/plugins/cases/server/routes/api/sub_case/find_sub_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/sub_case/find_sub_cases.ts @@ -17,7 +17,7 @@ import { RouteDeps } from '../types'; import { escapeHatch, wrapError } from '../utils'; import { SUB_CASES_URL } from '../../../../common/constants'; -export function initFindSubCasesApi({ caseService, router, logger }: RouteDeps) { +export function initFindSubCasesApi({ router, logger }: RouteDeps) { router.get( { path: `${SUB_CASES_URL}/_find`, diff --git a/x-pack/plugins/cases/server/routes/api/sub_case/patch_sub_cases.ts b/x-pack/plugins/cases/server/routes/api/sub_case/patch_sub_cases.ts index ce03c3bf970ab3..1fb260453d188a 100644 --- a/x-pack/plugins/cases/server/routes/api/sub_case/patch_sub_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/sub_case/patch_sub_cases.ts @@ -10,7 +10,7 @@ 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) { +export function initPatchSubCasesApi({ router, logger }: RouteDeps) { router.patch( { path: SUB_CASES_PATCH_DEL_URL, diff --git a/x-pack/plugins/cases/server/routes/api/types.ts b/x-pack/plugins/cases/server/routes/api/types.ts index d41e89dae31f84..9211aee5606a66 100644 --- a/x-pack/plugins/cases/server/routes/api/types.ts +++ b/x-pack/plugins/cases/server/routes/api/types.ts @@ -7,23 +7,10 @@ import type { Logger } from 'kibana/server'; -import type { - CaseConfigureService, - CaseService, - CaseUserActionService, - ConnectorMappingsService, - AttachmentService, -} from '../../services'; - import type { CasesRouter } from '../../types'; export interface RouteDeps { - caseConfigureService: CaseConfigureService; - caseService: CaseService; - connectorMappingsService: ConnectorMappingsService; router: CasesRouter; - userActionService: CaseUserActionService; - attachmentService: AttachmentService; logger: Logger; } diff --git a/x-pack/plugins/cases/server/types.ts b/x-pack/plugins/cases/server/types.ts index b943babc3bbda8..c3b8e0a2732212 100644 --- a/x-pack/plugins/cases/server/types.ts +++ b/x-pack/plugins/cases/server/types.ts @@ -6,7 +6,6 @@ */ import type { IRouter, RequestHandlerContext } from 'src/core/server'; -import type { ActionsApiRequestHandlerContext } from '../../actions/server'; import { ActionTypeConfig, ActionTypeSecrets, @@ -25,7 +24,6 @@ export interface CaseRequestContext { */ export interface CasesRequestHandlerContext extends RequestHandlerContext { cases: CaseRequestContext; - actions: ActionsApiRequestHandlerContext; } /** diff --git a/x-pack/test/case_api_integration/common/fixtures/plugins/cases_client_user/kibana.json b/x-pack/test/case_api_integration/common/fixtures/plugins/cases_client_user/kibana.json new file mode 100644 index 00000000000000..21dd9a58ffaad7 --- /dev/null +++ b/x-pack/test/case_api_integration/common/fixtures/plugins/cases_client_user/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "casesClientUserFixture", + "version": "1.0.0", + "kibanaVersion": "kibana", + "configPath": ["xpack"], + "requiredPlugins": ["features", "cases"], + "optionalPlugins": ["security", "spaces"], + "server": true, + "ui": false +} diff --git a/x-pack/test/case_api_integration/common/fixtures/plugins/cases_client_user/package.json b/x-pack/test/case_api_integration/common/fixtures/plugins/cases_client_user/package.json new file mode 100644 index 00000000000000..d396141fb0059a --- /dev/null +++ b/x-pack/test/case_api_integration/common/fixtures/plugins/cases_client_user/package.json @@ -0,0 +1,14 @@ +{ + "name": "cases-client-user-fixture", + "version": "1.0.0", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "main": "target/test/plugin_api_integration/plugins/cases_client_user_fixture", + "scripts": { + "kbn": "node ../../../../../../../scripts/kbn.js", + "build": "rm -rf './target' && ../../../../../../../node_modules/.bin/tsc" + }, + "license": "Elastic License 2.0" +} diff --git a/x-pack/test/case_api_integration/common/fixtures/plugins/cases_client_user/server/index.ts b/x-pack/test/case_api_integration/common/fixtures/plugins/cases_client_user/server/index.ts new file mode 100644 index 00000000000000..d39c2f2e714df4 --- /dev/null +++ b/x-pack/test/case_api_integration/common/fixtures/plugins/cases_client_user/server/index.ts @@ -0,0 +1,12 @@ +/* + * 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 { PluginInitializerContext } from 'kibana/server'; +import { FixturePlugin } from './plugin'; + +export const plugin = (initializerContext: PluginInitializerContext) => + new FixturePlugin(initializerContext); diff --git a/x-pack/test/case_api_integration/common/fixtures/plugins/cases_client_user/server/plugin.ts b/x-pack/test/case_api_integration/common/fixtures/plugins/cases_client_user/server/plugin.ts new file mode 100644 index 00000000000000..4b307281fa4d95 --- /dev/null +++ b/x-pack/test/case_api_integration/common/fixtures/plugins/cases_client_user/server/plugin.ts @@ -0,0 +1,68 @@ +/* + * 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 { Plugin, CoreSetup, CoreStart, PluginInitializerContext, Logger } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; + +import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../../plugins/features/server'; +import { SpacesPluginStart } from '../../../../../../../plugins/spaces/server'; +import { SecurityPluginStart } from '../../../../../../../plugins/security/server'; +import { PluginsStartContract as CasesPluginStart } from '../../../../../../../plugins/cases/server'; +import { CasesPatchRequest } from '../../../../../../../plugins/cases/common'; + +export interface FixtureSetupDeps { + features: FeaturesPluginSetup; +} + +export interface FixtureStartDeps { + security?: SecurityPluginStart; + spaces?: SpacesPluginStart; + cases?: CasesPluginStart; +} + +export class FixturePlugin implements Plugin { + private readonly log: Logger; + private casesPluginStart?: CasesPluginStart; + constructor(initContext: PluginInitializerContext) { + this.log = initContext.logger.get(); + } + + public setup(core: CoreSetup, deps: FixtureSetupDeps) { + const router = core.http.createRouter(); + /** + * This simply wraps the cases patch case api so that we can test updating the status of an alert using + * the cases client interface instead of going through the case plugin's RESTful interface + */ + router.patch( + { + path: '/api/cases_user/cases', + validate: { + body: schema.object({}, { unknowns: 'allow' }), + }, + }, + async (context, request, response) => { + try { + const client = await this.casesPluginStart?.getCasesClientWithRequest(request); + if (!client) { + throw new Error('Cases client was undefined'); + } + + return response.ok({ + body: await client.cases.update(request.body as CasesPatchRequest), + }); + } catch (error) { + this.log.error(`CasesClientUser failure: ${error}`); + throw error; + } + } + ); + } + public start(core: CoreStart, plugins: FixtureStartDeps) { + this.casesPluginStart = plugins.cases; + } + public stop() {} +} diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/client/update_alert_status.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/client/update_alert_status.ts new file mode 100644 index 00000000000000..44284c0aec6393 --- /dev/null +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/client/update_alert_status.ts @@ -0,0 +1,167 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { postCaseReq } from '../../../../common/lib/mock'; +import { + createCase, + createComment, + deleteAllCaseItems, + getSignalsWithES, +} from '../../../../common/lib/utils'; +import { CasesResponse, CaseStatuses, CommentType } from '../../../../../../plugins/cases/common'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const esArchiver = getService('esArchiver'); + + describe('update_alert_status', () => { + const defaultSignalsIndex = '.siem-signals-default-000001'; + + beforeEach(async () => { + await esArchiver.load('cases/signals/default'); + }); + afterEach(async () => { + await esArchiver.unload('cases/signals/default'); + await deleteAllCaseItems(es); + }); + + it('should update the status of multiple alerts attached to multiple cases using the cases client', async () => { + const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; + const signalID2 = '4d0f4b1533e46b66b43bdd0330d23f39f2cf42a7253153270e38d30cce9ff0c6'; + + // does NOT updates alert status when adding comments and syncAlerts=false + const individualCase1 = await createCase(supertest, { + ...postCaseReq, + settings: { + syncAlerts: false, + }, + }); + + const updatedInd1WithComment = await createComment({ + supertest, + caseId: individualCase1.id, + params: { + alertId: signalID, + index: defaultSignalsIndex, + rule: { id: 'test-rule-id', name: 'test-index-id' }, + type: CommentType.alert, + owner: 'securitySolutionFixture', + }, + }); + + const individualCase2 = await createCase(supertest, { + ...postCaseReq, + settings: { + syncAlerts: false, + }, + }); + + const updatedInd2WithComment = await createComment({ + supertest, + caseId: individualCase2.id, + params: { + alertId: signalID2, + index: defaultSignalsIndex, + rule: { id: 'test-rule-id', name: 'test-index-id' }, + type: CommentType.alert, + owner: 'securitySolutionFixture', + }, + }); + + await es.indices.refresh({ index: defaultSignalsIndex }); + + let signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); + + // There should be no change in their status since syncing is disabled + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.open + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses.open + ); + + // does NOT updates alert status when the status is updated and syncAlerts=false + // this performs the cases update through the test plugin that leverages the cases client instead + // of going through RESTful API of the cases plugin + const { body: updatedIndWithStatus }: { body: CasesResponse } = await supertest + .patch('/api/cases_user/cases') + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: updatedInd1WithComment.id, + version: updatedInd1WithComment.version, + status: CaseStatuses.closed, + }, + { + id: updatedInd2WithComment.id, + version: updatedInd2WithComment.version, + status: CaseStatuses['in-progress'], + }, + ], + }) + .expect(200); + + await es.indices.refresh({ index: defaultSignalsIndex }); + + signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); + + // There should still be no change in their status since syncing is disabled + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.open + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses.open + ); + + // it updates alert status when syncAlerts is turned on + // turn on the sync settings + // this performs the cases update through the test plugin that leverages the cases client instead + // of going through RESTful API of the cases plugin + await supertest + .patch('/api/cases_user/cases') + .set('kbn-xsrf', 'true') + .send({ + cases: updatedIndWithStatus.map((caseInfo) => ({ + id: caseInfo.id, + version: caseInfo.version, + settings: { syncAlerts: true }, + })), + }) + .expect(200); + + await es.indices.refresh({ index: defaultSignalsIndex }); + + signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); + + // alerts should be updated now that the + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.closed + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses['in-progress'] + ); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts index 3a4e6bec704835..9d35d5ec82fc50 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts @@ -10,6 +10,7 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default ({ loadTestFile }: FtrProviderContext): void => { describe('Common', function () { + loadTestFile(require.resolve('./client/update_alert_status')); loadTestFile(require.resolve('./comments/delete_comment')); loadTestFile(require.resolve('./comments/find_comments')); loadTestFile(require.resolve('./comments/get_comment')); From e26de43e47299d63086947ff45ce4356b6d63b31 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Fri, 14 May 2021 09:40:19 -0400 Subject: [PATCH 057/113] Integration tests for cases privs and fixes (#100038) --- .../security_solution/server/plugin.ts | 10 +- .../apis/security/privileges.ts | 2 +- .../security_solution/cases_privileges.ts | 331 ++++++++++++++++++ .../apis/security_solution/index.js | 1 + 4 files changed, 338 insertions(+), 6 deletions(-) create mode 100644 x-pack/test/api_integration/apis/security_solution/cases_privileges.ts diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 9941411c1e7999..153b3e04681a6a 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -221,7 +221,7 @@ export class Plugin implements IPlugin { + describe('security solution cases sub feature privilege', () => { + const es = getService('es'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const supertest = getService('supertest'); + + before(async () => { + await createUsersAndRoles(getService, users, roles); + }); + + after(async () => { + await deleteUsersAndRoles(getService, users, roles); + }); + + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + for (const user of [secAllUser, secReadCasesAllUser]) { + it(`User ${user.username} with role(s) ${user.roles.join()} can create a case`, async () => { + await createCase(supertestWithoutAuth, getPostCaseRequest({ owner: APP_ID }), 200, { + user, + space: null, + }); + }); + } + + for (const user of [ + secAllCasesReadUser, + secReadCasesAllUser, + secReadCasesReadUser, + secReadUser, + ]) { + it(`User ${user.username} with role(s) ${user.roles.join()} can get a case`, async () => { + const caseInfo = await createCase(supertest, getPostCaseRequest({ owner: APP_ID })); + const retrievedCase = await getCase({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + expectedHttpCode: 200, + auth: { user, space: null }, + }); + + expect(caseInfo.owner).to.eql(retrievedCase.owner); + }); + } + + for (const user of [ + secAllCasesReadUser, + secAllCasesNoneUser, + secReadCasesReadUser, + secReadUser, + secReadCasesNoneUser, + ]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} cannot create a case`, async () => { + await createCase(supertestWithoutAuth, getPostCaseRequest({ owner: APP_ID }), 403, { + user, + space: null, + }); + }); + } + + for (const user of [secAllCasesNoneUser, secReadCasesNoneUser]) { + it(`User ${user.username} with role(s) ${user.roles.join()} cannot get a case`, async () => { + const caseInfo = await createCase(supertest, getPostCaseRequest({ owner: APP_ID })); + + await getCase({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + expectedHttpCode: 403, + auth: { user, space: null }, + }); + }); + } + }); +}; diff --git a/x-pack/test/api_integration/apis/security_solution/index.js b/x-pack/test/api_integration/apis/security_solution/index.js index 3f9afba18b9ef9..996f2e74e87f78 100644 --- a/x-pack/test/api_integration/apis/security_solution/index.js +++ b/x-pack/test/api_integration/apis/security_solution/index.js @@ -8,6 +8,7 @@ export default function ({ loadTestFile }) { describe('SecuritySolution Endpoints', () => { loadTestFile(require.resolve('./authentications')); + loadTestFile(require.resolve('./cases_privileges')); loadTestFile(require.resolve('./events')); loadTestFile(require.resolve('./hosts')); loadTestFile(require.resolve('./host_details')); From 2d8601be1dfc7cbccea3c4f76d15836f03e3d92c Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 14 May 2021 16:58:01 +0300 Subject: [PATCH 058/113] [Cases] RBAC on UI (#99478) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/cases/common/constants.ts | 2 + x-pack/plugins/cases/common/ui/types.ts | 2 +- .../public/common/mock/test_providers.tsx | 6 +- .../components/add_comment/index.test.tsx | 4 +- .../public/components/add_comment/index.tsx | 7 +- .../all_cases/all_cases_generic.tsx | 2 + .../components/all_cases/index.test.tsx | 5 +- .../public/components/all_cases/index.tsx | 10 +- .../all_cases/selector_modal/index.test.tsx | 4 +- .../all_cases/selector_modal/index.tsx | 14 ++- .../components/case_view/helpers.test.tsx | 6 +- .../public/components/case_view/index.tsx | 36 +++--- .../components/configure_cases/button.tsx | 1 - .../components/configure_cases/index.test.tsx | 59 ++++++--- .../components/configure_cases/index.tsx | 15 ++- .../connectors/case/alert_fields.tsx | 11 +- .../connectors/case/existing_case.tsx | 9 +- .../public/components/create/flyout.test.tsx | 115 ------------------ .../cases/public/components/create/flyout.tsx | 71 ----------- .../public/components/create/form.test.tsx | 8 +- .../components/create/form_context.test.tsx | 3 +- .../public/components/create/form_context.tsx | 12 +- .../public/components/create/index.test.tsx | 7 +- .../cases/public/components/create/index.tsx | 15 ++- .../cases/public/components/create/mock.ts | 9 +- .../cases/public/components/create/schema.tsx | 1 - .../public/components/create/tags.test.tsx | 8 +- .../public/components/owner_context/index.tsx | 18 +++ .../owner_context/use_owner_context.ts | 21 ++++ .../components/recent_cases/index.test.tsx | 11 +- .../public/components/recent_cases/index.tsx | 16 ++- .../components/recent_cases/recent_cases.tsx | 5 +- .../public/components/tag_list/index.test.tsx | 2 + .../public/components/tag_list/index.tsx | 3 +- .../create_case_modal.test.tsx | 2 + .../create_case_modal.tsx | 3 + .../use_create_case_modal/index.tsx | 5 +- .../cases/public/containers/__mocks__/api.ts | 1 + .../cases/public/containers/api.test.tsx | 29 +++-- x-pack/plugins/cases/public/containers/api.ts | 8 +- .../public/containers/configure/api.test.ts | 17 ++- .../cases/public/containers/configure/api.ts | 7 +- .../cases/public/containers/configure/mock.ts | 7 +- .../configure/use_configure.test.tsx | 86 +++++++++---- .../containers/configure/use_configure.tsx | 28 +++-- .../plugins/cases/public/containers/mock.ts | 17 +-- .../public/containers/use_get_cases.test.tsx | 70 ++++++++--- .../cases/public/containers/use_get_cases.tsx | 18 ++- .../containers/use_get_reporters.test.tsx | 37 ++++-- .../public/containers/use_get_reporters.tsx | 6 +- .../public/containers/use_get_tags.test.tsx | 25 +++- .../cases/public/containers/use_get_tags.tsx | 4 +- .../public/containers/use_post_case.test.tsx | 4 +- .../containers/use_post_comment.test.tsx | 4 +- .../plugins/cases/public/containers/utils.ts | 1 - .../methods/get_all_cases_selector_modal.tsx | 12 +- x-pack/plugins/cases/public/types.ts | 4 + .../plugins/cases/server/client/cases/mock.ts | 19 +-- .../cases/server/client/cases/utils.test.ts | 3 +- .../plugins/cases/server/common/utils.test.ts | 25 ++-- .../server/connectors/case/index.test.ts | 15 +-- .../api/__fixtures__/mock_saved_objects.ts | 31 ++--- .../routes/api/__mocks__/request_responses.ts | 4 +- .../cases/server/scripts/sub_cases/index.ts | 11 +- .../cases/components/all_cases/index.tsx | 1 + .../public/cases/components/create/flyout.tsx | 2 + .../cases/components/create/index.test.tsx | 2 + .../public/cases/components/create/index.tsx | 2 + .../add_to_case_action.test.tsx | 6 +- .../timeline_actions/add_to_case_action.tsx | 6 +- .../public/cases/pages/configure_cases.tsx | 2 + .../components/recent_cases/index.tsx | 1 + .../flyout/add_to_case_button/index.tsx | 1 + 73 files changed, 610 insertions(+), 434 deletions(-) delete mode 100644 x-pack/plugins/cases/public/components/create/flyout.test.tsx delete mode 100644 x-pack/plugins/cases/public/components/create/flyout.tsx create mode 100644 x-pack/plugins/cases/public/components/owner_context/index.tsx create mode 100644 x-pack/plugins/cases/public/components/owner_context/use_owner_context.ts diff --git a/x-pack/plugins/cases/common/constants.ts b/x-pack/plugins/cases/common/constants.ts index 6bd2204e39be75..72c21aa12dcf2b 100644 --- a/x-pack/plugins/cases/common/constants.ts +++ b/x-pack/plugins/cases/common/constants.ts @@ -79,6 +79,8 @@ export const MAX_GENERATED_ALERTS_PER_SUB_CASE = 50; /** * This must be the same value that the security solution plugin uses to define the case kind when it registers the * feature for the 7.13 migration only. + * + * This variable is being also used by test files and mocks. */ export const SECURITY_SOLUTION_OWNER = 'securitySolution'; diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index 7004fd2ab2ea2d..284f5e706292cc 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -130,7 +130,7 @@ export interface ElasticUser { export interface FetchCasesProps extends ApiProps { queryParams?: QueryParams; - filterOptions?: FilterOptions; + filterOptions?: FilterOptions & { owner: string[] }; } export interface ApiProps { diff --git a/x-pack/plugins/cases/public/common/mock/test_providers.tsx b/x-pack/plugins/cases/public/common/mock/test_providers.tsx index 94ee5dd4f2743a..9a08918a483a5a 100644 --- a/x-pack/plugins/cases/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/cases/public/common/mock/test_providers.tsx @@ -10,6 +10,8 @@ import { I18nProvider } from '@kbn/i18n/react'; import React from 'react'; import { BehaviorSubject } from 'rxjs'; import { ThemeProvider } from 'styled-components'; +import { SECURITY_SOLUTION_OWNER } from '../../../common'; +import { OwnerProvider } from '../../components/owner_context'; import { createKibanaContextProviderMock, createStartServicesMock, @@ -29,7 +31,9 @@ const MockKibanaContextProvider = createKibanaContextProviderMock(); const TestProvidersComponent: React.FC = ({ children }) => ( - ({ eui: euiDarkVars, darkMode: true })}>{children} + ({ eui: euiDarkVars, darkMode: true })}> + {children} + ); diff --git a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx index 95f642c7e625a5..23a0fca48592fb 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx @@ -13,7 +13,7 @@ import { noop } from 'lodash/fp'; import { TestProviders } from '../../common/mock'; import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; -import { CommentRequest, CommentType } from '../../../common'; +import { CommentRequest, CommentType, SECURITY_SOLUTION_OWNER } from '../../../common'; import { usePostComment } from '../../containers/use_post_comment'; import { AddComment, AddCommentRefObject } from '.'; import { CasesTimelineIntegrationProvider } from '../timeline_context'; @@ -44,7 +44,7 @@ const defaultPostComment = { const sampleData: CommentRequest = { comment: 'what a cool comment', type: CommentType.user, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }; describe('AddComment ', () => { diff --git a/x-pack/plugins/cases/public/components/add_comment/index.tsx b/x-pack/plugins/cases/public/components/add_comment/index.tsx index 2995fcb4fc35f9..04104f0b9471d0 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.tsx @@ -18,6 +18,7 @@ import { Form, useForm, UseField, useFormData } from '../../common/shared_import import * as i18n from './translations'; import { schema, AddCommentFormSchema } from './schema'; import { InsertTimeline } from '../insert_timeline'; +import { useOwnerContext } from '../owner_context/use_owner_context'; const MySpinner = styled(EuiLoadingSpinner)` position: absolute; top: 50%; @@ -47,6 +48,7 @@ export const AddComment = React.memo( { caseId, disabled, onCommentPosted, onCommentSaving, showLoading = true, subCaseId }, ref ) => { + const owner = useOwnerContext(); const { isLoading, postComment } = usePostComment(); const { form } = useForm({ @@ -78,14 +80,13 @@ export const AddComment = React.memo( } postComment({ caseId, - // TODO: get plugin name - data: { ...data, type: CommentType.user, owner: 'securitySolution' }, + data: { ...data, type: CommentType.user, owner: owner[0] }, updateCase: onCommentPosted, subCaseId, }); reset(); } - }, [caseId, onCommentPosted, onCommentSaving, postComment, reset, submit, subCaseId]); + }, [submit, onCommentSaving, postComment, caseId, owner, onCommentPosted, subCaseId, reset]); return ( diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx index 83f38aab21aa44..429532c86e4da5 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx @@ -38,6 +38,7 @@ import { CasesTableFilters } from './table_filters'; import { EuiBasicTableOnChange } from './types'; import { CasesTable } from './table'; + const ProgressLoader = styled(EuiProgress)` ${({ $isShow }: { $isShow: boolean }) => $isShow @@ -79,6 +80,7 @@ export const AllCasesGeneric = React.memo( userCanCrud, }) => { const { actionLicense } = useGetActionLicense(); + const { data, dispatchUpdateCaseProperty, diff --git a/x-pack/plugins/cases/public/components/all_cases/index.test.tsx b/x-pack/plugins/cases/public/components/all_cases/index.test.tsx index 7233d6bef6e4a4..5ed3215241de59 100644 --- a/x-pack/plugins/cases/public/components/all_cases/index.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/index.test.tsx @@ -13,7 +13,7 @@ import '../../common/mock/match_media'; import { TestProviders } from '../../common/mock'; import { casesStatus, useGetCasesMockState, collectionCase } from '../../containers/mock'; -import { CaseStatuses, CaseType, StatusAll } from '../../../common'; +import { CaseStatuses, CaseType, SECURITY_SOLUTION_OWNER, StatusAll } from '../../../common'; import { getEmptyTagValue } from '../empty_value'; import { useDeleteCases } from '../../containers/use_delete_cases'; import { useGetCases } from '../../containers/use_get_cases'; @@ -53,6 +53,7 @@ describe('AllCasesGeneric', () => { onClick: jest.fn(), }, userCanCrud: true, + owner: [SECURITY_SOLUTION_OWNER], }; const dispatchResetIsDeleted = jest.fn(); @@ -815,7 +816,7 @@ describe('AllCasesGeneric', () => { }, }, id: '1', - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, status: 'open', subCaseIds: [], tags: ['coke', 'pepsi'], diff --git a/x-pack/plugins/cases/public/components/all_cases/index.tsx b/x-pack/plugins/cases/public/components/all_cases/index.tsx index 2c506cd2da4110..3d6c039aa001c5 100644 --- a/x-pack/plugins/cases/public/components/all_cases/index.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/index.tsx @@ -6,9 +6,11 @@ */ import React from 'react'; +import { Owner } from '../../types'; import { CaseDetailsHrefSchema, CasesNavigation } from '../links'; +import { OwnerProvider } from '../owner_context'; import { AllCasesGeneric } from './all_cases_generic'; -export interface AllCasesProps { +export interface AllCasesProps extends Owner { caseDetailsNavigation: CasesNavigation; // if not passed, case name is not displayed as a link (Formerly dependant on isSelector) configureCasesNavigation: CasesNavigation; // if not passed, header with nav is not displayed (Formerly dependant on isSelector) createCaseNavigation: CasesNavigation; @@ -16,7 +18,11 @@ export interface AllCasesProps { } export const AllCases: React.FC = (props) => { - return ; + return ( + + + + ); }; // eslint-disable-next-line import/no-default-export diff --git a/x-pack/plugins/cases/public/components/all_cases/selector_modal/index.test.tsx b/x-pack/plugins/cases/public/components/all_cases/selector_modal/index.test.tsx index b2444c5ccb0ddc..47db45699f8fb3 100644 --- a/x-pack/plugins/cases/public/components/all_cases/selector_modal/index.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/selector_modal/index.test.tsx @@ -11,6 +11,7 @@ import { mount } from 'enzyme'; import { AllCasesSelectorModal } from '.'; import { TestProviders } from '../../../common/mock'; import { AllCasesGeneric } from '../all_cases_generic'; +import { SECURITY_SOLUTION_OWNER } from '../../../../common'; jest.mock('../../../methods'); jest.mock('../all_cases_generic'); @@ -20,6 +21,7 @@ const defaultProps = { createCaseNavigation, onRowClick, userCanCrud: true, + owner: [SECURITY_SOLUTION_OWNER], }; const updateCase = jest.fn(); @@ -59,7 +61,7 @@ describe('AllCasesSelectorModal', () => { }, index: 'index-id', alertId: 'alert-id', - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }, disabledStatuses: [], updateCase, diff --git a/x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx b/x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx index 0a83ef13e8ee67..e7bce984b3cd18 100644 --- a/x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx @@ -12,8 +12,10 @@ import { Case, CaseStatuses, CommentRequestAlertType, SubCase } from '../../../. import { CasesNavigation } from '../../links'; import * as i18n from '../../../common/translations'; import { AllCasesGeneric } from '../all_cases_generic'; +import { Owner } from '../../../types'; +import { OwnerProvider } from '../../owner_context'; -export interface AllCasesSelectorModalProps { +export interface AllCasesSelectorModalProps extends Owner { alertData?: Omit; createCaseNavigation: CasesNavigation; disabledStatuses?: CaseStatuses[]; @@ -29,7 +31,7 @@ const Modal = styled(EuiModal)` `} `; -export const AllCasesSelectorModal: React.FC = ({ +const AllCasesSelectorModalComponent: React.FC = ({ alertData, createCaseNavigation, disabledStatuses, @@ -65,5 +67,13 @@ export const AllCasesSelectorModal: React.FC = ({ ) : null; }; + +export const AllCasesSelectorModal: React.FC = React.memo((props) => { + return ( + + + + ); +}); // eslint-disable-next-line import/no-default-export export { AllCasesSelectorModal as default }; diff --git a/x-pack/plugins/cases/public/components/case_view/helpers.test.tsx b/x-pack/plugins/cases/public/components/case_view/helpers.test.tsx index 47ab272bdc3f86..bf5a9fe5d0a223 100644 --- a/x-pack/plugins/cases/public/components/case_view/helpers.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/helpers.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { AssociationType, CommentType } from '../../../common'; +import { AssociationType, CommentType, SECURITY_SOLUTION_OWNER } from '../../../common'; import { Comment } from '../../containers/types'; import { getManualAlertIdsWithNoRuleId } from './helpers'; @@ -28,7 +28,7 @@ const comments: Comment[] = [ updatedAt: null, updatedBy: null, version: 'WzQ3LDFc', - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }, { associationType: AssociationType.case, @@ -47,7 +47,7 @@ const comments: Comment[] = [ updatedAt: null, updatedBy: null, version: 'WzQ3LDFc', - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }, ]; diff --git a/x-pack/plugins/cases/public/components/case_view/index.tsx b/x-pack/plugins/cases/public/components/case_view/index.tsx index 557f736c513b93..86b13ae5a863c4 100644 --- a/x-pack/plugins/cases/public/components/case_view/index.tsx +++ b/x-pack/plugins/cases/public/components/case_view/index.tsx @@ -43,6 +43,7 @@ import { Ecs } from '../../../common'; import { CasesTimelineIntegration, CasesTimelineIntegrationProvider } from '../timeline_context'; import { useTimelineContext } from '../timeline_context/use_timeline_context'; import { CasesNavigation } from '../links'; +import { OwnerProvider } from '../owner_context'; const gutterTimeline = '70px'; // seems to be a timeline reference from the original file export interface CaseViewComponentProps { @@ -450,6 +451,7 @@ export const CaseComponent = React.memo( tags={caseData.tags} onSubmit={onSubmitTags} isLoading={isLoading && updateKey === 'tags'} + owner={[caseData.owner]} /> - + + + ) ); diff --git a/x-pack/plugins/cases/public/components/configure_cases/button.tsx b/x-pack/plugins/cases/public/components/configure_cases/button.tsx index 1830380be37658..8b3c78ee3aedef 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/button.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/button.tsx @@ -10,7 +10,6 @@ import React, { memo, useMemo } from 'react'; import { CasesNavigation, LinkButton } from '../links'; // TODO: Potentially move into links component? - export interface ConfigureCaseButtonProps { configureCasesNavigation: CasesNavigation; isDisabled: boolean; diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx index 898d6cde19a774..0d9ede9bb7de89 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx @@ -32,7 +32,7 @@ import { useConnectorsResponse, useActionTypesResponse, } from './__mock__'; -import { ConnectorTypes } from '../../../common'; +import { ConnectorTypes, SECURITY_SOLUTION_OWNER } from '../../../common'; jest.mock('../../common/lib/kibana'); jest.mock('../../containers/configure/use_connectors'); @@ -102,7 +102,9 @@ describe('ConfigureCases', () => { useConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, connectors: [] })); useGetUrlSearchMock.mockImplementation(() => searchURL); - wrapper = mount(, { wrappingComponent: TestProviders }); + wrapper = mount(, { + wrappingComponent: TestProviders, + }); }); test('it renders the Connectors', () => { @@ -155,7 +157,9 @@ describe('ConfigureCases', () => { })); useConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, connectors: [] })); useGetUrlSearchMock.mockImplementation(() => searchURL); - wrapper = mount(, { wrappingComponent: TestProviders }); + wrapper = mount(, { + wrappingComponent: TestProviders, + }); }); test('it shows the warning callout when configuration is invalid', () => { @@ -200,7 +204,9 @@ describe('ConfigureCases', () => { useConnectorsMock.mockImplementation(() => useConnectorsResponse); useGetUrlSearchMock.mockImplementation(() => searchURL); - wrapper = mount(, { wrappingComponent: TestProviders }); + wrapper = mount(, { + wrappingComponent: TestProviders, + }); }); test('it renders with correct props', () => { @@ -220,9 +226,12 @@ describe('ConfigureCases', () => { }); test('it disables correctly when the user cannot crud', () => { - const newWrapper = mount(, { - wrappingComponent: TestProviders, - }); + const newWrapper = mount( + , + { + wrappingComponent: TestProviders, + } + ); expect(newWrapper.find('button[data-test-subj="dropdown-connectors"]').prop('disabled')).toBe( true @@ -282,7 +291,9 @@ describe('ConfigureCases', () => { })); useGetUrlSearchMock.mockImplementation(() => searchURL); - wrapper = mount(, { wrappingComponent: TestProviders }); + wrapper = mount(, { + wrappingComponent: TestProviders, + }); }); test('it disables correctly Connector when loading connectors', () => { @@ -315,7 +326,9 @@ describe('ConfigureCases', () => { useActionTypesMock.mockImplementation(() => ({ ...useActionTypesResponse, loading: true })); - wrapper = mount(, { wrappingComponent: TestProviders }); + wrapper = mount(, { + wrappingComponent: TestProviders, + }); expect(wrapper.find(Connectors).prop('isLoading')).toBe(true); }); }); @@ -337,7 +350,9 @@ describe('ConfigureCases', () => { useConnectorsMock.mockImplementation(() => useConnectorsResponse); useGetUrlSearchMock.mockImplementation(() => searchURL); - wrapper = mount(, { wrappingComponent: TestProviders }); + wrapper = mount(, { + wrappingComponent: TestProviders, + }); }); test('it disables correctly Connector when saving configuration', () => { @@ -378,7 +393,9 @@ describe('ConfigureCases', () => { ...useConnectorsResponse, })); useGetUrlSearchMock.mockImplementation(() => searchURL); - wrapper = mount(, { wrappingComponent: TestProviders }); + wrapper = mount(, { + wrappingComponent: TestProviders, + }); }); test('it hides the update connector button when loading the configuration', () => { @@ -420,7 +437,9 @@ describe('ConfigureCases', () => { useConnectorsMock.mockImplementation(() => useConnectorsResponse); useGetUrlSearchMock.mockImplementation(() => searchURL); - wrapper = mount(, { wrappingComponent: TestProviders }); + wrapper = mount(, { + wrappingComponent: TestProviders, + }); }); test('it submits the configuration correctly when changing connector', () => { @@ -462,7 +481,9 @@ describe('ConfigureCases', () => { }, })); - wrapper = mount(, { wrappingComponent: TestProviders }); + wrapper = mount(, { + wrappingComponent: TestProviders, + }); wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); wrapper.update(); @@ -508,7 +529,9 @@ describe('closure options', () => { useConnectorsMock.mockImplementation(() => useConnectorsResponse); useGetUrlSearchMock.mockImplementation(() => searchURL); - wrapper = mount(, { wrappingComponent: TestProviders }); + wrapper = mount(, { + wrappingComponent: TestProviders, + }); }); test('it submits the configuration correctly when changing closure type', () => { @@ -555,7 +578,9 @@ describe('user interactions', () => { }); test('it show the add flyout when pressing the add connector button', () => { - const wrapper = mount(, { wrappingComponent: TestProviders }); + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); wrapper.update(); wrapper.find('button[data-test-subj="dropdown-connector-add-connector"]').simulate('click'); @@ -576,7 +601,9 @@ describe('user interactions', () => { }); test('it show the edit flyout when pressing the update connector button', () => { - const wrapper = mount(, { wrappingComponent: TestProviders }); + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); wrapper .find('button[data-test-subj="case-configure-update-selected-connector-button"]') .simulate('click'); diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.tsx index fdba148e5c61e0..3ee4bc77cd237c 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/index.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/index.tsx @@ -31,6 +31,8 @@ import { normalizeCaseConnector, } from './utils'; import * as i18n from './translations'; +import { Owner } from '../../types'; +import { OwnerProvider } from '../owner_context'; const FormWrapper = styled.div` ${({ theme }) => css` @@ -50,11 +52,11 @@ const FormWrapper = styled.div` `} `; -export interface ConfigureCasesProps { +export interface ConfigureCasesProps extends Owner { userCanCrud: boolean; } -const ConfigureCasesComponent: React.FC = ({ userCanCrud }) => { +const ConfigureCasesComponent: React.FC> = ({ userCanCrud }) => { const { triggersActionsUi } = useKibana().services; const [connectorIsValid, setConnectorIsValid] = useState(true); @@ -223,6 +225,13 @@ const ConfigureCasesComponent: React.FC = ({ userCanCrud }) ); }; -export const ConfigureCases = React.memo(ConfigureCasesComponent); +export const ConfigureCases: React.FC = React.memo((props) => { + return ( + + + + ); +}); + // eslint-disable-next-line import/no-default-export export default ConfigureCases; diff --git a/x-pack/plugins/cases/public/components/connectors/case/alert_fields.tsx b/x-pack/plugins/cases/public/components/connectors/case/alert_fields.tsx index 0c44bcab70679a..8fb34e0cdcbf50 100644 --- a/x-pack/plugins/cases/public/components/connectors/case/alert_fields.tsx +++ b/x-pack/plugins/cases/public/components/connectors/case/alert_fields.tsx @@ -12,12 +12,13 @@ import styled from 'styled-components'; import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import { ActionParamsProps } from '../../../../../triggers_actions_ui/public/types'; -import { CommentType } from '../../../../common'; +import { CommentType, SECURITY_SOLUTION_OWNER } from '../../../../common'; import { CaseActionParams } from './types'; import { ExistingCase } from './existing_case'; import * as i18n from './translations'; +import { OwnerProvider } from '../../owner_context'; const Container = styled.div` ${({ theme }) => ` @@ -89,9 +90,15 @@ const CaseParamsFields: React.FunctionComponent - + + +

{i18n.CASE_CONNECTOR_CALL_OUT_MSG}

diff --git a/x-pack/plugins/cases/public/components/connectors/case/existing_case.tsx b/x-pack/plugins/cases/public/components/connectors/case/existing_case.tsx index 22798843dd8564..aafbfb8b43b789 100644 --- a/x-pack/plugins/cases/public/components/connectors/case/existing_case.tsx +++ b/x-pack/plugins/cases/public/components/connectors/case/existing_case.tsx @@ -21,9 +21,12 @@ interface ExistingCaseProps { } const ExistingCaseComponent: React.FC = ({ onCaseChanged, selectedCase }) => { - const { data: cases, loading: isLoadingCases, refetchCases } = useGetCases(DEFAULT_QUERY_PARAMS, { - ...DEFAULT_FILTER_OPTIONS, - onlyCollectionType: true, + const { data: cases, loading: isLoadingCases, refetchCases } = useGetCases({ + initialQueryParams: DEFAULT_QUERY_PARAMS, + initialFilterOptions: { + ...DEFAULT_FILTER_OPTIONS, + onlyCollectionType: true, + }, }); const onCaseCreated = useCallback( diff --git a/x-pack/plugins/cases/public/components/create/flyout.test.tsx b/x-pack/plugins/cases/public/components/create/flyout.test.tsx deleted file mode 100644 index 5187029ab60c74..00000000000000 --- a/x-pack/plugins/cases/public/components/create/flyout.test.tsx +++ /dev/null @@ -1,115 +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 React, { ReactNode } from 'react'; -import { mount } from 'enzyme'; - -import { CreateCaseFlyout } from './flyout'; -import { TestProviders } from '../../common/mock'; - -jest.mock('../create/form_context', () => { - return { - FormContext: ({ - children, - onSuccess, - }: { - children: ReactNode; - onSuccess: ({ id }: { id: string }) => Promise; - }) => { - return ( - <> - - {children} - - ); - }, - }; -}); - -jest.mock('../create/form', () => { - return { - CreateCaseForm: () => { - return <>{'form'}; - }, - }; -}); - -jest.mock('../create/submit_button', () => { - return { - SubmitCaseButton: () => { - return <>{'Submit'}; - }, - }; -}); - -const onCloseFlyout = jest.fn(); -const onSuccess = jest.fn(); -const defaultProps = { - onCloseFlyout, - onSuccess, -}; - -describe('CreateCaseFlyout', () => { - beforeEach(() => { - jest.resetAllMocks(); - }); - - it('renders', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj='create-case-flyout']`).exists()).toBeTruthy(); - }); - - it('Closing modal calls onCloseCaseModal', () => { - const wrapper = mount( - - - - ); - - wrapper.find('.euiFlyout__closeButton').first().simulate('click'); - expect(onCloseFlyout).toBeCalled(); - }); - - it('pass the correct props to FormContext component', () => { - const wrapper = mount( - - - - ); - - const props = wrapper.find('FormContext').props(); - expect(props).toEqual( - expect.objectContaining({ - onSuccess, - }) - ); - }); - - it('onSuccess called when creating a case', () => { - const wrapper = mount( - - - - ); - - wrapper.find(`[data-test-subj='form-context-on-success']`).first().simulate('click'); - expect(onSuccess).toHaveBeenCalledWith({ id: 'case-id' }); - }); -}); diff --git a/x-pack/plugins/cases/public/components/create/flyout.tsx b/x-pack/plugins/cases/public/components/create/flyout.tsx deleted file mode 100644 index 8ed09865e9eabe..00000000000000 --- a/x-pack/plugins/cases/public/components/create/flyout.tsx +++ /dev/null @@ -1,71 +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 React, { memo } from 'react'; -import styled from 'styled-components'; -import { EuiFlyout, EuiFlyoutHeader, EuiTitle, EuiFlyoutBody } from '@elastic/eui'; - -import { FormContext } from '../create/form_context'; -import { CreateCaseForm } from '../create/form'; -import { SubmitCaseButton } from '../create/submit_button'; -import { Case } from '../../containers/types'; -import * as i18n from '../../common/translations'; - -export interface CreateCaseModalProps { - onCloseFlyout: () => void; - onSuccess: (theCase: Case) => Promise; - afterCaseCreated?: (theCase: Case) => Promise; -} - -const Container = styled.div` - ${({ theme }) => ` - margin-top: ${theme.eui.euiSize}; - text-align: right; - `} -`; - -const StyledFlyout = styled(EuiFlyout)` - ${({ theme }) => ` - z-index: ${theme.eui.euiZModal}; - `} -`; - -// Adding bottom padding because timeline's -// bottom bar gonna hide the submit button. -const FormWrapper = styled.div` - padding-bottom: 50px; -`; - -const CreateCaseFlyoutComponent: React.FC = ({ - onSuccess, - afterCaseCreated, - onCloseFlyout, -}) => { - return ( - - - -

{i18n.CREATE_TITLE}

-
-
- - - - - - - - - - -
- ); -}; - -export const CreateCaseFlyout = memo(CreateCaseFlyoutComponent); - -CreateCaseFlyout.displayName = 'CreateCaseFlyout'; diff --git a/x-pack/plugins/cases/public/components/create/form.test.tsx b/x-pack/plugins/cases/public/components/create/form.test.tsx index 9e59924bdf4837..5f3b778a7cafc9 100644 --- a/x-pack/plugins/cases/public/components/create/form.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form.test.tsx @@ -15,6 +15,8 @@ import { useConnectors } from '../../containers/configure/use_connectors'; import { connectorsMock } from '../../containers/mock'; import { schema, FormProps } from './schema'; import { CreateCaseForm } from './form'; +import { OwnerProvider } from '../owner_context'; +import { SECURITY_SOLUTION_OWNER } from '../../../common'; jest.mock('../../containers/use_get_tags'); jest.mock('../../containers/configure/use_connectors'); @@ -41,7 +43,11 @@ describe('CreateCaseForm', () => { globalForm = form; - return
{children}
; + return ( + +
{children}
+
+ ); }; beforeEach(() => { diff --git a/x-pack/plugins/cases/public/components/create/form_context.test.tsx b/x-pack/plugins/cases/public/components/create/form_context.test.tsx index 9a8671c7fc571d..cb053b2e784cda 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.test.tsx @@ -10,7 +10,7 @@ import { mount, ReactWrapper } from 'enzyme'; import { act, waitFor } from '@testing-library/react'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; -import { ConnectorTypes } from '../../../common'; +import { ConnectorTypes, SECURITY_SOLUTION_OWNER } from '../../../common'; import { TestProviders } from '../../common/mock'; import { usePostCase } from '../../containers/use_post_case'; import { usePostComment } from '../../containers/use_post_comment'; @@ -77,6 +77,7 @@ const defaultPostCase = { const defaultCreateCaseForm = { isLoadingConnectors: false, connectors: [], + owner: SECURITY_SOLUTION_OWNER, }; const defaultPostPushToService = { diff --git a/x-pack/plugins/cases/public/components/create/form_context.tsx b/x-pack/plugins/cases/public/components/create/form_context.tsx index 9ee8aa0fe32884..8584892e1286c0 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.tsx @@ -21,6 +21,7 @@ import { useCaseConfigure } from '../../containers/configure/use_configure'; import { Case } from '../../containers/types'; import { CaseType, ConnectorTypes } from '../../../common'; import { UsePostComment, usePostComment } from '../../containers/use_post_comment'; +import { useOwnerContext } from '../owner_context/use_owner_context'; const initialCaseValue: FormProps = { description: '', @@ -47,6 +48,7 @@ export const FormContext: React.FC = ({ onSuccess, }) => { const { connectors, loading: isLoadingConnectors } = useConnectors(); + const owner = useOwnerContext(); const { connector: configurationConnector } = useCaseConfigure(); const { postCase } = usePostCase(); const { postComment } = usePostComment(); @@ -86,8 +88,7 @@ export const FormContext: React.FC = ({ type: caseType, connector: connectorToUpdate, settings: { syncAlerts }, - // TODO: need to replace this with the value that the plugin registers in the feature registration - owner: 'securitySolution', + owner: owner[0], }); if (afterCaseCreated && updatedCase) { @@ -107,13 +108,14 @@ export const FormContext: React.FC = ({ } }, [ - caseType, connectors, postCase, - postComment, + caseType, + owner, + afterCaseCreated, onSuccess, + postComment, pushCaseToExternalService, - afterCaseCreated, ] ); diff --git a/x-pack/plugins/cases/public/components/create/index.test.tsx b/x-pack/plugins/cases/public/components/create/index.test.tsx index e82af8edc6337a..350b971bb05fc5 100644 --- a/x-pack/plugins/cases/public/components/create/index.test.tsx +++ b/x-pack/plugins/cases/public/components/create/index.test.tsx @@ -29,6 +29,7 @@ import { useGetFieldsByIssueTypeResponse, } from './mock'; import { CreateCase } from '.'; +import { SECURITY_SOLUTION_OWNER } from '../../../common'; jest.mock('../../containers/api'); jest.mock('../../containers/use_get_tags'); @@ -91,7 +92,7 @@ describe('CreateCase case', () => { it('it renders', async () => { const wrapper = mount( - + ); @@ -102,7 +103,7 @@ describe('CreateCase case', () => { it('should call cancel on cancel click', async () => { const wrapper = mount( - + ); @@ -113,7 +114,7 @@ describe('CreateCase case', () => { it('should redirect to new case when posting the case', async () => { const wrapper = mount( - + ); diff --git a/x-pack/plugins/cases/public/components/create/index.tsx b/x-pack/plugins/cases/public/components/create/index.tsx index a1de4d9730b9ff..3362aa6af2078d 100644 --- a/x-pack/plugins/cases/public/components/create/index.tsx +++ b/x-pack/plugins/cases/public/components/create/index.tsx @@ -20,6 +20,8 @@ import { CasesTimelineIntegration, CasesTimelineIntegrationProvider } from '../t import { fieldName as descriptionFieldName } from './description'; import { InsertTimeline } from '../insert_timeline'; import { UsePostComment } from '../../containers/use_post_comment'; +import { Owner } from '../../types'; +import { OwnerProvider } from '../owner_context'; export const CommonUseField = getUseField({ component: Field }); @@ -29,7 +31,7 @@ const Container = styled.div` `} `; -export interface CreateCaseProps { +export interface CreateCaseProps extends Owner { afterCaseCreated?: (theCase: Case, postComment: UsePostComment['postComment']) => Promise; caseType?: CaseType; hideConnectorServiceNowSir?: boolean; @@ -39,7 +41,7 @@ export interface CreateCaseProps { withSteps?: boolean; } -export const CreateCase = ({ +const CreateCaseComponent = ({ afterCaseCreated, caseType, hideConnectorServiceNowSir, @@ -47,7 +49,7 @@ export const CreateCase = ({ onSuccess, timelineIntegration, withSteps, -}: CreateCaseProps) => ( +}: Omit) => ( ); +export const CreateCase: React.FC = React.memo((props) => { + return ( + + + + ); +}); // eslint-disable-next-line import/no-default-export export { CreateCase as default }; diff --git a/x-pack/plugins/cases/public/components/create/mock.ts b/x-pack/plugins/cases/public/components/create/mock.ts index 5a4c00ba8a91c3..fb00f114f480c8 100644 --- a/x-pack/plugins/cases/public/components/create/mock.ts +++ b/x-pack/plugins/cases/public/components/create/mock.ts @@ -5,7 +5,12 @@ * 2.0. */ -import { CasePostRequest, CaseType, ConnectorTypes } from '../../../common'; +import { + CasePostRequest, + CaseType, + ConnectorTypes, + SECURITY_SOLUTION_OWNER, +} from '../../../common'; import { choices } from '../connectors/mock'; export const sampleTags = ['coke', 'pepsi']; @@ -23,7 +28,7 @@ export const sampleData: CasePostRequest = { settings: { syncAlerts: true, }, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }; export const sampleConnectorData = { loading: false, connectors: [] }; diff --git a/x-pack/plugins/cases/public/components/create/schema.tsx b/x-pack/plugins/cases/public/components/create/schema.tsx index ccf9013e6a6fa0..6e6d1a414280eb 100644 --- a/x-pack/plugins/cases/public/components/create/schema.tsx +++ b/x-pack/plugins/cases/public/components/create/schema.tsx @@ -19,7 +19,6 @@ export const schemaTags = { labelAppend: OptionalFieldLabel, }; -// TODO: remove owner from here? export type FormProps = Omit & { connectorId: string; fields: ConnectorTypeFields['fields']; diff --git a/x-pack/plugins/cases/public/components/create/tags.test.tsx b/x-pack/plugins/cases/public/components/create/tags.test.tsx index 2eddb83dcac29a..6efbf1b8c7107d 100644 --- a/x-pack/plugins/cases/public/components/create/tags.test.tsx +++ b/x-pack/plugins/cases/public/components/create/tags.test.tsx @@ -14,6 +14,8 @@ import { useForm, Form, FormHook } from '../../common/shared_imports'; import { useGetTags } from '../../containers/use_get_tags'; import { Tags } from './tags'; import { schema, FormProps } from './schema'; +import { OwnerProvider } from '../owner_context'; +import { SECURITY_SOLUTION_OWNER } from '../../../common'; jest.mock('../../containers/use_get_tags'); const useGetTagsMock = useGetTags as jest.Mock; @@ -31,7 +33,11 @@ describe('Tags', () => { globalForm = form; - return
{children}
; + return ( + +
{children}
+
+ ); }; beforeEach(() => { diff --git a/x-pack/plugins/cases/public/components/owner_context/index.tsx b/x-pack/plugins/cases/public/components/owner_context/index.tsx new file mode 100644 index 00000000000000..5df7eeadd70d56 --- /dev/null +++ b/x-pack/plugins/cases/public/components/owner_context/index.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; + +export const OwnerContext = React.createContext([]); + +export const OwnerProvider: React.FC<{ + owner: string[]; +}> = ({ children, owner }) => { + const [currentOwner] = useState(owner); + + return {children}; +}; diff --git a/x-pack/plugins/cases/public/components/owner_context/use_owner_context.ts b/x-pack/plugins/cases/public/components/owner_context/use_owner_context.ts new file mode 100644 index 00000000000000..a443df1809315a --- /dev/null +++ b/x-pack/plugins/cases/public/components/owner_context/use_owner_context.ts @@ -0,0 +1,21 @@ +/* + * 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 { useContext } from 'react'; +import { OwnerContext } from '.'; + +export const useOwnerContext = () => { + const ownerContext = useContext(OwnerContext); + + if (ownerContext.length === 0) { + throw new Error( + 'useOwnerContext must be used within an OwnerProvider and not be an empty array' + ); + } + + return ownerContext; +}; diff --git a/x-pack/plugins/cases/public/components/recent_cases/index.test.tsx b/x-pack/plugins/cases/public/components/recent_cases/index.test.tsx index 933ea51bffac4c..5893d5f8c5af48 100644 --- a/x-pack/plugins/cases/public/components/recent_cases/index.test.tsx +++ b/x-pack/plugins/cases/public/components/recent_cases/index.test.tsx @@ -12,6 +12,8 @@ import RecentCases from '.'; import { TestProviders } from '../../common/mock'; import { useGetCases } from '../../containers/use_get_cases'; import { useGetCasesMockState } from '../../containers/mock'; +import { SECURITY_SOLUTION_OWNER } from '../../../common'; + jest.mock('../../containers/use_get_cases'); configure({ testIdAttribute: 'data-test-subj' }); const defaultProps = { @@ -28,6 +30,7 @@ const defaultProps = { onClick: jest.fn(), }, maxCasesToShow: 10, + owner: [SECURITY_SOLUTION_OWNER], }; const setFilters = jest.fn(); const mockData = { @@ -40,6 +43,7 @@ describe('RecentCases', () => { jest.clearAllMocks(); useGetCasesMock.mockImplementation(() => mockData); }); + it('is good at loading', () => { useGetCasesMock.mockImplementation(() => ({ ...mockData, @@ -52,6 +56,7 @@ describe('RecentCases', () => { ); expect(getAllByTestId('loadingPlaceholders')).toHaveLength(3); }); + it('is good at rendering cases', () => { const { getAllByTestId } = render( @@ -60,14 +65,18 @@ describe('RecentCases', () => { ); expect(getAllByTestId('case-details-link')).toHaveLength(5); }); + it('is good at rendering max cases', () => { render( ); - expect(useGetCasesMock).toBeCalledWith({ perPage: 2 }); + expect(useGetCasesMock).toBeCalledWith({ + initialQueryParams: { perPage: 2 }, + }); }); + it('updates filters', () => { const { getByTestId } = render( diff --git a/x-pack/plugins/cases/public/components/recent_cases/index.tsx b/x-pack/plugins/cases/public/components/recent_cases/index.tsx index 05aff25d0dbd85..bb34f651d52dfd 100644 --- a/x-pack/plugins/cases/public/components/recent_cases/index.tsx +++ b/x-pack/plugins/cases/public/components/recent_cases/index.tsx @@ -14,20 +14,22 @@ import { RecentCasesFilters } from './filters'; import { RecentCasesComp } from './recent_cases'; import { FilterMode as RecentCasesFilterMode } from './types'; import { useCurrentUser } from '../../common/lib/kibana'; +import { Owner } from '../../types'; +import { OwnerProvider } from '../owner_context'; -export interface RecentCasesProps { +export interface RecentCasesProps extends Owner { allCasesNavigation: CasesNavigation; caseDetailsNavigation: CasesNavigation; createCaseNavigation: CasesNavigation; maxCasesToShow: number; } -const RecentCases = ({ +const RecentCasesComponent = ({ allCasesNavigation, caseDetailsNavigation, createCaseNavigation, maxCasesToShow, -}: RecentCasesProps) => { +}: Omit) => { const currentUser = useCurrentUser(); const [recentCasesFilterBy, setRecentCasesFilterBy] = useState( 'recentlyCreated' @@ -87,5 +89,13 @@ const RecentCases = ({ ); }; +export const RecentCases: React.FC = React.memo((props) => { + return ( + + + + ); +}); + // eslint-disable-next-line import/no-default-export export { RecentCases as default }; diff --git a/x-pack/plugins/cases/public/components/recent_cases/recent_cases.tsx b/x-pack/plugins/cases/public/components/recent_cases/recent_cases.tsx index 12935e75c064f9..5b4313530e4904 100644 --- a/x-pack/plugins/cases/public/components/recent_cases/recent_cases.tsx +++ b/x-pack/plugins/cases/public/components/recent_cases/recent_cases.tsx @@ -32,6 +32,7 @@ export interface RecentCasesProps { createCaseNavigation: CasesNavigation; maxCasesToShow: number; } + const usePrevious = (value: Partial) => { const ref = useRef(); useEffect(() => { @@ -46,7 +47,9 @@ export const RecentCasesComp = ({ maxCasesToShow, }: RecentCasesProps) => { const previousFilterOptions = usePrevious(filterOptions); - const { data, loading, setFilters } = useGetCases({ perPage: maxCasesToShow }); + const { data, loading, setFilters } = useGetCases({ + initialQueryParams: { perPage: maxCasesToShow }, + }); useEffect(() => { if (previousFilterOptions !== undefined && !isEqual(previousFilterOptions, filterOptions)) { diff --git a/x-pack/plugins/cases/public/components/tag_list/index.test.tsx b/x-pack/plugins/cases/public/components/tag_list/index.test.tsx index 296c4ba0e893b9..b3fbcd30d4e978 100644 --- a/x-pack/plugins/cases/public/components/tag_list/index.test.tsx +++ b/x-pack/plugins/cases/public/components/tag_list/index.test.tsx @@ -14,6 +14,7 @@ import { TestProviders } from '../../common/mock'; import { waitFor } from '@testing-library/react'; import { useForm } from '../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'; import { useGetTags } from '../../containers/use_get_tags'; +import { SECURITY_SOLUTION_OWNER } from '../../../common'; jest.mock('../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'); jest.mock('../../containers/use_get_tags'); @@ -37,6 +38,7 @@ const defaultProps = { isLoading: false, onSubmit, tags: [], + owner: [SECURITY_SOLUTION_OWNER], }; describe('TagList ', () => { diff --git a/x-pack/plugins/cases/public/components/tag_list/index.tsx b/x-pack/plugins/cases/public/components/tag_list/index.tsx index 137d58932b6efa..f2605933696796 100644 --- a/x-pack/plugins/cases/public/components/tag_list/index.tsx +++ b/x-pack/plugins/cases/public/components/tag_list/index.tsx @@ -32,6 +32,7 @@ interface TagListProps { isLoading: boolean; onSubmit: (a: string[]) => void; tags: string[]; + owner: string[]; } const MyFlexGroup = styled(EuiFlexGroup)` @@ -44,7 +45,7 @@ const MyFlexGroup = styled(EuiFlexGroup)` `; export const TagList = React.memo( - ({ disabled = false, isLoading, onSubmit, tags }: TagListProps) => { + ({ disabled = false, isLoading, onSubmit, tags, owner }: TagListProps) => { const initialState = { tags }; const { form } = useForm({ defaultValue: initialState, diff --git a/x-pack/plugins/cases/public/components/use_create_case_modal/create_case_modal.test.tsx b/x-pack/plugins/cases/public/components/use_create_case_modal/create_case_modal.test.tsx index 661a0eedfeae46..4c39b721cac47e 100644 --- a/x-pack/plugins/cases/public/components/use_create_case_modal/create_case_modal.test.tsx +++ b/x-pack/plugins/cases/public/components/use_create_case_modal/create_case_modal.test.tsx @@ -11,6 +11,7 @@ import { mount } from 'enzyme'; import { CreateCaseModal } from './create_case_modal'; import { TestProviders } from '../../common/mock'; import { getCreateCaseLazy as getCreateCase } from '../../methods'; +import { SECURITY_SOLUTION_OWNER } from '../../../common'; jest.mock('../../methods'); const getCreateCaseMock = getCreateCase as jest.Mock; @@ -20,6 +21,7 @@ const defaultProps = { isModalOpen: true, onCloseCaseModal, onSuccess, + owner: SECURITY_SOLUTION_OWNER, }; describe('CreateCaseModal', () => { diff --git a/x-pack/plugins/cases/public/components/use_create_case_modal/create_case_modal.tsx b/x-pack/plugins/cases/public/components/use_create_case_modal/create_case_modal.tsx index e78b432b3a27c3..a4278e53ea3415 100644 --- a/x-pack/plugins/cases/public/components/use_create_case_modal/create_case_modal.tsx +++ b/x-pack/plugins/cases/public/components/use_create_case_modal/create_case_modal.tsx @@ -19,6 +19,7 @@ export interface CreateCaseModalProps { isModalOpen: boolean; onCloseCaseModal: () => void; onSuccess: (theCase: Case) => Promise; + owner: string; } const CreateModalComponent: React.FC = ({ @@ -27,6 +28,7 @@ const CreateModalComponent: React.FC = ({ isModalOpen, onCloseCaseModal, onSuccess, + owner, }) => { return isModalOpen ? ( @@ -40,6 +42,7 @@ const CreateModalComponent: React.FC = ({ onCancel: onCloseCaseModal, onSuccess, withSteps: false, + owner: [owner], })} diff --git a/x-pack/plugins/cases/public/components/use_create_case_modal/index.tsx b/x-pack/plugins/cases/public/components/use_create_case_modal/index.tsx index 7ad85773a79176..09f8eb65b12b75 100644 --- a/x-pack/plugins/cases/public/components/use_create_case_modal/index.tsx +++ b/x-pack/plugins/cases/public/components/use_create_case_modal/index.tsx @@ -7,6 +7,7 @@ import React, { useState, useCallback, useMemo } from 'react'; import { Case, CaseType } from '../../../common'; +import { useOwnerContext } from '../owner_context/use_owner_context'; import { CreateCaseModal } from './create_case_modal'; export interface UseCreateCaseModalProps { @@ -26,6 +27,7 @@ export const useCreateCaseModal = ({ onCaseCreated, hideConnectorServiceNowSir = false, }: UseCreateCaseModalProps) => { + const owner = useOwnerContext(); const [isModalOpen, setIsModalOpen] = useState(false); const closeModal = useCallback(() => setIsModalOpen(false), []); const openModal = useCallback(() => setIsModalOpen(true), []); @@ -46,12 +48,13 @@ export const useCreateCaseModal = ({ isModalOpen={isModalOpen} onCloseCaseModal={closeModal} onSuccess={onSuccess} + owner={owner[0]} /> ), isModalOpen, closeModal, openModal, }), - [caseType, closeModal, hideConnectorServiceNowSir, isModalOpen, onSuccess, openModal] + [caseType, closeModal, hideConnectorServiceNowSir, isModalOpen, onSuccess, openModal, owner] ); }; diff --git a/x-pack/plugins/cases/public/containers/__mocks__/api.ts b/x-pack/plugins/cases/public/containers/__mocks__/api.ts index 4dbb10da95b2da..006ad3f7afe604 100644 --- a/x-pack/plugins/cases/public/containers/__mocks__/api.ts +++ b/x-pack/plugins/cases/public/containers/__mocks__/api.ts @@ -62,6 +62,7 @@ export const getCases = async ({ reporters: [], status: CaseStatuses.open, tags: [], + owner: [], }, queryParams = { page: 1, diff --git a/x-pack/plugins/cases/public/containers/api.test.tsx b/x-pack/plugins/cases/public/containers/api.test.tsx index ff1f2084e18de1..bee6110c39a306 100644 --- a/x-pack/plugins/cases/public/containers/api.test.tsx +++ b/x-pack/plugins/cases/public/containers/api.test.tsx @@ -7,7 +7,7 @@ import { KibanaServices } from '../common/lib/kibana'; -import { ConnectorTypes, CommentType, CaseStatuses } from '../../common'; +import { ConnectorTypes, CommentType, CaseStatuses, SECURITY_SOLUTION_OWNER } from '../../common'; import { CASES_URL } from '../../common'; import { @@ -127,7 +127,7 @@ describe('Case Configuration API', () => { }); test('check url, method, signal', async () => { await getCases({ - filterOptions: DEFAULT_FILTER_OPTIONS, + filterOptions: { ...DEFAULT_FILTER_OPTIONS, owner: [SECURITY_SOLUTION_OWNER] }, queryParams: DEFAULT_QUERY_PARAMS, signal: abortCtrl.signal, }); @@ -137,6 +137,7 @@ describe('Case Configuration API', () => { ...DEFAULT_QUERY_PARAMS, reporters: [], tags: [], + owner: [SECURITY_SOLUTION_OWNER], }, signal: abortCtrl.signal, }); @@ -150,6 +151,7 @@ describe('Case Configuration API', () => { tags, status: CaseStatuses.open, search: 'hello', + owner: [SECURITY_SOLUTION_OWNER], }, queryParams: DEFAULT_QUERY_PARAMS, signal: abortCtrl.signal, @@ -162,6 +164,7 @@ describe('Case Configuration API', () => { tags: ['"coke"', '"pepsi"'], search: 'hello', status: CaseStatuses.open, + owner: [SECURITY_SOLUTION_OWNER], }, signal: abortCtrl.signal, }); @@ -177,6 +180,7 @@ describe('Case Configuration API', () => { tags: weirdTags, status: CaseStatuses.open, search: 'hello', + owner: [SECURITY_SOLUTION_OWNER], }, queryParams: DEFAULT_QUERY_PARAMS, signal: abortCtrl.signal, @@ -189,6 +193,7 @@ describe('Case Configuration API', () => { tags: ['"("', '"\\"double\\""'], search: 'hello', status: CaseStatuses.open, + owner: [SECURITY_SOLUTION_OWNER], }, signal: abortCtrl.signal, }); @@ -196,7 +201,7 @@ describe('Case Configuration API', () => { test('happy path', async () => { const resp = await getCases({ - filterOptions: DEFAULT_FILTER_OPTIONS, + filterOptions: { ...DEFAULT_FILTER_OPTIONS, owner: [SECURITY_SOLUTION_OWNER] }, queryParams: DEFAULT_QUERY_PARAMS, signal: abortCtrl.signal, }); @@ -250,15 +255,18 @@ describe('Case Configuration API', () => { }); test('check url, method, signal', async () => { - await getReporters(abortCtrl.signal); + await getReporters(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/reporters`, { method: 'GET', signal: abortCtrl.signal, + query: { + owner: [SECURITY_SOLUTION_OWNER], + }, }); }); test('happy path', async () => { - const resp = await getReporters(abortCtrl.signal); + const resp = await getReporters(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]); expect(resp).toEqual(respReporters); }); }); @@ -270,15 +278,18 @@ describe('Case Configuration API', () => { }); test('check url, method, signal', async () => { - await getTags(abortCtrl.signal); + await getTags(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/tags`, { method: 'GET', signal: abortCtrl.signal, + query: { + owner: [SECURITY_SOLUTION_OWNER], + }, }); }); test('happy path', async () => { - const resp = await getTags(abortCtrl.signal); + const resp = await getTags(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]); expect(resp).toEqual(tags); }); }); @@ -395,7 +406,7 @@ describe('Case Configuration API', () => { settings: { syncAlerts: true, }, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }; test('check url, method, signal', async () => { @@ -420,7 +431,7 @@ describe('Case Configuration API', () => { }); const data = { comment: 'comment', - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, type: CommentType.user as const, }; diff --git a/x-pack/plugins/cases/public/containers/api.ts b/x-pack/plugins/cases/public/containers/api.ts index 75263d4d389785..0b9b236cef6e17 100644 --- a/x-pack/plugins/cases/public/containers/api.ts +++ b/x-pack/plugins/cases/public/containers/api.ts @@ -119,18 +119,20 @@ export const getCasesStatus = async (signal: AbortSignal): Promise return convertToCamelCase(decodeCasesStatusResponse(response)); }; -export const getTags = async (signal: AbortSignal): Promise => { +export const getTags = async (signal: AbortSignal, owner: string[]): Promise => { const response = await KibanaServices.get().http.fetch(CASE_TAGS_URL, { method: 'GET', signal, + query: { ...(owner.length > 0 ? { owner } : {}) }, }); return response ?? []; }; -export const getReporters = async (signal: AbortSignal): Promise => { +export const getReporters = async (signal: AbortSignal, owner: string[]): Promise => { const response = await KibanaServices.get().http.fetch(CASE_REPORTERS_URL, { method: 'GET', signal, + query: { ...(owner.length > 0 ? { owner } : {}) }, }); return response ?? []; }; @@ -171,6 +173,7 @@ export const getCases = async ({ reporters: [], status: StatusAll, tags: [], + owner: [], }, queryParams = { page: 1, @@ -186,6 +189,7 @@ export const getCases = async ({ status: filterOptions.status, ...(filterOptions.search.length > 0 ? { search: filterOptions.search } : {}), ...(filterOptions.onlyCollectionType ? { type: CaseType.collection } : {}), + ...(filterOptions.owner.length > 0 ? { owner: filterOptions.owner } : {}), ...queryParams, }; const response = await KibanaServices.get().http.fetch(`${CASES_URL}/_find`, { diff --git a/x-pack/plugins/cases/public/containers/configure/api.test.ts b/x-pack/plugins/cases/public/containers/configure/api.test.ts index 4732c030ea5056..ad13526b41d38f 100644 --- a/x-pack/plugins/cases/public/containers/configure/api.test.ts +++ b/x-pack/plugins/cases/public/containers/configure/api.test.ts @@ -19,7 +19,7 @@ import { caseConfigurationResposeMock, caseConfigurationCamelCaseResponseMock, } from './mock'; -import { ConnectorTypes } from '../../../common'; +import { ConnectorTypes, SECURITY_SOLUTION_OWNER } from '../../../common'; import { KibanaServices } from '../../common/lib/kibana'; const abortCtrl = new AbortController(); @@ -57,21 +57,30 @@ describe('Case Configuration API', () => { }); test('check url, method, signal', async () => { - await getCaseConfigure({ signal: abortCtrl.signal }); + await getCaseConfigure({ signal: abortCtrl.signal, owner: [SECURITY_SOLUTION_OWNER] }); expect(fetchMock).toHaveBeenCalledWith('/api/cases/configure', { method: 'GET', signal: abortCtrl.signal, + query: { + owner: [SECURITY_SOLUTION_OWNER], + }, }); }); test('happy path', async () => { - const resp = await getCaseConfigure({ signal: abortCtrl.signal }); + const resp = await getCaseConfigure({ + signal: abortCtrl.signal, + owner: [SECURITY_SOLUTION_OWNER], + }); expect(resp).toEqual(caseConfigurationCamelCaseResponseMock); }); test('return null on empty response', async () => { fetchMock.mockResolvedValue({}); - const resp = await getCaseConfigure({ signal: abortCtrl.signal }); + const resp = await getCaseConfigure({ + signal: abortCtrl.signal, + owner: [SECURITY_SOLUTION_OWNER], + }); expect(resp).toBe(null); }); }); diff --git a/x-pack/plugins/cases/public/containers/configure/api.ts b/x-pack/plugins/cases/public/containers/configure/api.ts index 2d26e390050574..a6d530caa588eb 100644 --- a/x-pack/plugins/cases/public/containers/configure/api.ts +++ b/x-pack/plugins/cases/public/containers/configure/api.ts @@ -37,13 +37,16 @@ export const fetchConnectors = async ({ signal }: ApiProps): Promise => { +export const getCaseConfigure = async ({ + signal, + owner, +}: ApiProps & { owner: string[] }): Promise => { const response = await KibanaServices.get().http.fetch( CASE_CONFIGURE_URL, { method: 'GET', signal, + query: { ...(owner.length > 0 ? { owner } : {}) }, } ); diff --git a/x-pack/plugins/cases/public/containers/configure/mock.ts b/x-pack/plugins/cases/public/containers/configure/mock.ts index 3329fa02a54b9f..ef287ea866dcbb 100644 --- a/x-pack/plugins/cases/public/containers/configure/mock.ts +++ b/x-pack/plugins/cases/public/containers/configure/mock.ts @@ -11,6 +11,7 @@ import { CasesConfigureResponse, CasesConfigureRequest, ConnectorTypes, + SECURITY_SOLUTION_OWNER, } from '../../../common'; import { CaseConfigure, CaseConnectorMapping } from './types'; @@ -130,7 +131,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', + owner: SECURITY_SOLUTION_OWNER, version: 'WzHJ12', }; @@ -141,7 +142,7 @@ export const caseConfigurationMock: CasesConfigureRequest = { type: ConnectorTypes.jira, fields: null, }, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, closure_type: 'close-by-user', }; @@ -161,5 +162,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', + owner: SECURITY_SOLUTION_OWNER, }; diff --git a/x-pack/plugins/cases/public/containers/configure/use_configure.test.tsx b/x-pack/plugins/cases/public/containers/configure/use_configure.test.tsx index 4d2abbcaec4d4c..d8d552ceb8b7a2 100644 --- a/x-pack/plugins/cases/public/containers/configure/use_configure.test.tsx +++ b/x-pack/plugins/cases/public/containers/configure/use_configure.test.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import React from 'react'; import { renderHook, act } from '@testing-library/react-hooks'; import { initialState, @@ -15,6 +16,7 @@ import { import { mappings, caseConfigurationCamelCaseResponseMock } from './mock'; import * as api from './api'; import { ConnectorTypes } from '../../../common'; +import { TestProviders } from '../../common/mock'; const mockErrorToast = jest.fn(); const mockSuccessToast = jest.fn(); @@ -49,8 +51,11 @@ describe('useConfigure', () => { test('init', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useCaseConfigure() + const { result, waitForNextUpdate } = renderHook( + () => useCaseConfigure(), + { + wrapper: ({ children }) => {children}, + } ); await waitForNextUpdate(); expect(result.current).toEqual({ @@ -67,8 +72,11 @@ describe('useConfigure', () => { test('fetch case configuration', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useCaseConfigure() + const { result, waitForNextUpdate } = renderHook( + () => useCaseConfigure(), + { + wrapper: ({ children }) => {children}, + } ); await waitForNextUpdate(); await waitForNextUpdate(); @@ -99,8 +107,11 @@ describe('useConfigure', () => { const spyOnGetCaseConfigure = jest.spyOn(api, 'getCaseConfigure'); await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useCaseConfigure() + const { result, waitForNextUpdate } = renderHook( + () => useCaseConfigure(), + { + wrapper: ({ children }) => {children}, + } ); await waitForNextUpdate(); await waitForNextUpdate(); @@ -111,8 +122,11 @@ describe('useConfigure', () => { test('correctly sets mappings', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useCaseConfigure() + const { result, waitForNextUpdate } = renderHook( + () => useCaseConfigure(), + { + wrapper: ({ children }) => {children}, + } ); await waitForNextUpdate(); await waitForNextUpdate(); @@ -124,8 +138,11 @@ describe('useConfigure', () => { test('set isLoading to true when fetching case configuration', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useCaseConfigure() + const { result, waitForNextUpdate } = renderHook( + () => useCaseConfigure(), + { + wrapper: ({ children }) => {children}, + } ); await waitForNextUpdate(); await waitForNextUpdate(); @@ -137,8 +154,11 @@ describe('useConfigure', () => { test('persist case configuration', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useCaseConfigure() + const { result, waitForNextUpdate } = renderHook( + () => useCaseConfigure(), + { + wrapper: ({ children }) => {children}, + } ); await waitForNextUpdate(); await waitForNextUpdate(); @@ -166,8 +186,11 @@ describe('useConfigure', () => { ); await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useCaseConfigure() + const { result, waitForNextUpdate } = renderHook( + () => useCaseConfigure(), + { + wrapper: ({ children }) => {children}, + } ); await waitForNextUpdate(); await waitForNextUpdate(); @@ -192,8 +215,11 @@ describe('useConfigure', () => { ); await act(async () => { - const { waitForNextUpdate } = renderHook(() => - useCaseConfigure() + const { waitForNextUpdate } = renderHook( + () => useCaseConfigure(), + { + wrapper: ({ children }) => {children}, + } ); await waitForNextUpdate(); await waitForNextUpdate(); @@ -221,8 +247,11 @@ describe('useConfigure', () => { ); await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useCaseConfigure() + const { result, waitForNextUpdate } = renderHook( + () => useCaseConfigure(), + { + wrapper: ({ children }) => {children}, + } ); await waitForNextUpdate(); await waitForNextUpdate(); @@ -245,8 +274,11 @@ describe('useConfigure', () => { ); await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useCaseConfigure() + const { result, waitForNextUpdate } = renderHook( + () => useCaseConfigure(), + { + wrapper: ({ children }) => {children}, + } ); await waitForNextUpdate(); await waitForNextUpdate(); @@ -266,8 +298,11 @@ describe('useConfigure', () => { }); await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useCaseConfigure() + const { result, waitForNextUpdate } = renderHook( + () => useCaseConfigure(), + { + wrapper: ({ children }) => {children}, + } ); await waitForNextUpdate(); @@ -302,8 +337,11 @@ describe('useConfigure', () => { }); await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useCaseConfigure() + const { result, waitForNextUpdate } = renderHook( + () => useCaseConfigure(), + { + wrapper: ({ children }) => {children}, + } ); await waitForNextUpdate(); diff --git a/x-pack/plugins/cases/public/containers/configure/use_configure.tsx b/x-pack/plugins/cases/public/containers/configure/use_configure.tsx index b4a4ab35b96d7e..d02a22bde408cf 100644 --- a/x-pack/plugins/cases/public/containers/configure/use_configure.tsx +++ b/x-pack/plugins/cases/public/containers/configure/use_configure.tsx @@ -12,6 +12,7 @@ import * as i18n from './translations'; import { ClosureType, CaseConfigure, CaseConnector, CaseConnectorMapping } from './types'; import { ConnectorTypes } from '../../../common'; import { useToasts } from '../../common/lib/kibana'; +import { useOwnerContext } from '../../components/owner_context/use_owner_context'; export type ConnectorConfiguration = { connector: CaseConnector } & { closureType: CaseConfigure['closureType']; @@ -155,6 +156,7 @@ export const initialState: State = { }; export const useCaseConfigure = (): ReturnUseCaseConfigure => { + const owner = useOwnerContext(); const [state, dispatch] = useReducer(configureCasesReducer, initialState); const toasts = useToasts(); const setCurrentConfiguration = useCallback((configuration: ConnectorConfiguration) => { @@ -213,7 +215,6 @@ export const useCaseConfigure = (): ReturnUseCaseConfigure => { }); }, []); - // TODO: refactor const setID = useCallback((id: string) => { dispatch({ payload: id, @@ -234,7 +235,10 @@ export const useCaseConfigure = (): ReturnUseCaseConfigure => { abortCtrlRefetchRef.current = new AbortController(); setLoading(true); - const res = await getCaseConfigure({ signal: abortCtrlRefetchRef.current.signal }); + const res = await getCaseConfigure({ + signal: abortCtrlRefetchRef.current.signal, + owner, + }); if (!isCancelledRefetchRef.current) { if (res != null) { @@ -295,8 +299,8 @@ export const useCaseConfigure = (): ReturnUseCaseConfigure => { const res = state.version.length === 0 ? await postCaseConfigure( - // TODO: use constant after https://github.com/elastic/kibana/pull/97646 is being merged - { ...connectorObj, owner: 'securitySolution' }, + // The first owner will be used for case creation + { ...connectorObj, owner: owner[0] }, abortCtrlPersistRef.current.signal ) : await patchCaseConfigure( @@ -347,17 +351,17 @@ export const useCaseConfigure = (): ReturnUseCaseConfigure => { } }, [ - setClosureType, - setConnector, - setCurrentConfiguration, - setMappings, setPersistLoading, - setVersion, - setID, - state.currentConfiguration.connector, state.version, - // TODO: do we need this? state.id, + state.currentConfiguration.connector, + owner, + setConnector, + setClosureType, + setVersion, + setID, + setMappings, + setCurrentConfiguration, toasts, ] ); diff --git a/x-pack/plugins/cases/public/containers/mock.ts b/x-pack/plugins/cases/public/containers/mock.ts index 4871fa1555a129..72fee3c602c4e9 100644 --- a/x-pack/plugins/cases/public/containers/mock.ts +++ b/x-pack/plugins/cases/public/containers/mock.ts @@ -19,6 +19,7 @@ import { CommentResponse, CommentType, ConnectorTypes, + SECURITY_SOLUTION_OWNER, UserAction, UserActionField, } from '../../common'; @@ -47,7 +48,7 @@ export const basicComment: Comment = { id: basicCommentId, createdAt: basicCreatedAt, createdBy: elasticUser, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, pushedAt: null, pushedBy: null, updatedAt: null, @@ -63,7 +64,7 @@ export const alertComment: Comment = { id: 'alert-comment-id', createdAt: basicCreatedAt, createdBy: elasticUser, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, pushedAt: null, pushedBy: null, rule: { @@ -77,7 +78,7 @@ export const alertComment: Comment = { export const basicCase: Case = { type: CaseType.individual, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, closedAt: null, closedBy: null, id: basicCaseId, @@ -108,7 +109,7 @@ export const basicCase: Case = { export const collectionCase: Case = { type: CaseType.collection, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, closedAt: null, closedBy: null, id: 'collection-id', @@ -185,7 +186,7 @@ const basicAction = { newValue: 'what a cool value', caseId: basicCaseId, commentId: null, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }; export const cases: Case[] = [ @@ -235,7 +236,7 @@ export const basicCommentSnake: CommentResponse = { id: basicCommentId, created_at: basicCreatedAt, created_by: elasticUserSnake, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, pushed_at: null, pushed_by: null, updated_at: null, @@ -260,7 +261,7 @@ export const basicCaseSnake: CaseResponse = { external_service: null, updated_at: basicUpdatedAt, updated_by: elasticUserSnake, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, } as CaseResponse; export const casesStatusSnake: CasesStatusResponse = { @@ -318,7 +319,7 @@ const basicActionSnake = { new_value: 'what a cool value', case_id: basicCaseId, comment_id: null, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }; export const getUserActionSnake = (af: UserActionField, a: UserAction) => ({ ...basicActionSnake, diff --git a/x-pack/plugins/cases/public/containers/use_get_cases.test.tsx b/x-pack/plugins/cases/public/containers/use_get_cases.test.tsx index b07fec4984eb12..b3a6932c6971c3 100644 --- a/x-pack/plugins/cases/public/containers/use_get_cases.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_cases.test.tsx @@ -5,8 +5,9 @@ * 2.0. */ +import React from 'react'; import { renderHook, act } from '@testing-library/react-hooks'; -import { CaseStatuses } from '../../common'; +import { CaseStatuses, SECURITY_SOLUTION_OWNER } from '../../common'; import { DEFAULT_FILTER_OPTIONS, DEFAULT_QUERY_PARAMS, @@ -17,6 +18,7 @@ import { import { UpdateKey } from './types'; import { allCases, basicCase } from './mock'; import * as api from './api'; +import { TestProviders } from '../common/mock'; jest.mock('./api'); jest.mock('../common/lib/kibana'); @@ -30,7 +32,10 @@ describe('useGetCases', () => { it('init', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useGetCases()); + const { result, waitForNextUpdate } = renderHook(() => useGetCases(), { + wrapper: ({ children }) => {children}, + }); + await waitForNextUpdate(); expect(result.current).toEqual({ data: initialData, @@ -51,11 +56,13 @@ describe('useGetCases', () => { it('calls getCases with correct arguments', async () => { const spyOnGetCases = jest.spyOn(api, 'getCases'); await act(async () => { - const { waitForNextUpdate } = renderHook(() => useGetCases()); + const { waitForNextUpdate } = renderHook(() => useGetCases(), { + wrapper: ({ children }) => {children}, + }); await waitForNextUpdate(); await waitForNextUpdate(); expect(spyOnGetCases).toBeCalledWith({ - filterOptions: DEFAULT_FILTER_OPTIONS, + filterOptions: { ...DEFAULT_FILTER_OPTIONS, owner: [SECURITY_SOLUTION_OWNER] }, queryParams: DEFAULT_QUERY_PARAMS, signal: abortCtrl.signal, }); @@ -64,7 +71,9 @@ describe('useGetCases', () => { it('fetch cases', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useGetCases()); + const { result, waitForNextUpdate } = renderHook(() => useGetCases(), { + wrapper: ({ children }) => {children}, + }); await waitForNextUpdate(); await waitForNextUpdate(); expect(result.current).toEqual({ @@ -82,6 +91,7 @@ describe('useGetCases', () => { }); }); }); + it('dispatch update case property', async () => { const spyOnPatchCase = jest.spyOn(api, 'patchCase'); await act(async () => { @@ -92,7 +102,9 @@ describe('useGetCases', () => { refetchCasesStatus: jest.fn(), version: '99999', }; - const { result, waitForNextUpdate } = renderHook(() => useGetCases()); + const { result, waitForNextUpdate } = renderHook(() => useGetCases(), { + wrapper: ({ children }) => {children}, + }); await waitForNextUpdate(); await waitForNextUpdate(); result.current.dispatchUpdateCaseProperty(updateCase); @@ -109,7 +121,9 @@ describe('useGetCases', () => { it('refetch cases', async () => { const spyOnGetCases = jest.spyOn(api, 'getCases'); await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useGetCases()); + const { result, waitForNextUpdate } = renderHook(() => useGetCases(), { + wrapper: ({ children }) => {children}, + }); await waitForNextUpdate(); await waitForNextUpdate(); result.current.refetchCases(); @@ -119,7 +133,9 @@ describe('useGetCases', () => { it('set isLoading to true when refetching case', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useGetCases()); + const { result, waitForNextUpdate } = renderHook(() => useGetCases(), { + wrapper: ({ children }) => {children}, + }); await waitForNextUpdate(); await waitForNextUpdate(); result.current.refetchCases(); @@ -135,7 +151,9 @@ describe('useGetCases', () => { }); await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useGetCases()); + const { result, waitForNextUpdate } = renderHook(() => useGetCases(), { + wrapper: ({ children }) => {children}, + }); await waitForNextUpdate(); await waitForNextUpdate(); @@ -154,6 +172,7 @@ describe('useGetCases', () => { }); }); }); + it('set filters', async () => { await act(async () => { const spyOnGetCases = jest.spyOn(api, 'getCases'); @@ -162,40 +181,61 @@ describe('useGetCases', () => { tags: ['new'], status: CaseStatuses.closed, }; - const { result, waitForNextUpdate } = renderHook(() => useGetCases()); + + const { result, waitForNextUpdate } = renderHook(() => useGetCases(), { + wrapper: ({ children }) => {children}, + }); + await waitForNextUpdate(); await waitForNextUpdate(); result.current.setFilters(newFilters); await waitForNextUpdate(); + expect(spyOnGetCases.mock.calls[1][0]).toEqual({ - filterOptions: { ...DEFAULT_FILTER_OPTIONS, ...newFilters }, + filterOptions: { + ...DEFAULT_FILTER_OPTIONS, + ...newFilters, + owner: [SECURITY_SOLUTION_OWNER], + }, queryParams: DEFAULT_QUERY_PARAMS, signal: abortCtrl.signal, }); }); }); + it('set query params', async () => { await act(async () => { const spyOnGetCases = jest.spyOn(api, 'getCases'); const newQueryParams = { page: 2, }; - const { result, waitForNextUpdate } = renderHook(() => useGetCases()); + + const { result, waitForNextUpdate } = renderHook(() => useGetCases(), { + wrapper: ({ children }) => {children}, + }); + await waitForNextUpdate(); await waitForNextUpdate(); result.current.setQueryParams(newQueryParams); await waitForNextUpdate(); + expect(spyOnGetCases.mock.calls[1][0]).toEqual({ - filterOptions: DEFAULT_FILTER_OPTIONS, - queryParams: { ...DEFAULT_QUERY_PARAMS, ...newQueryParams }, + filterOptions: { ...DEFAULT_FILTER_OPTIONS, owner: [SECURITY_SOLUTION_OWNER] }, + queryParams: { + ...DEFAULT_QUERY_PARAMS, + ...newQueryParams, + }, signal: abortCtrl.signal, }); }); }); + it('set selected cases', async () => { await act(async () => { const selectedCases = [basicCase]; - const { result, waitForNextUpdate } = renderHook(() => useGetCases()); + const { result, waitForNextUpdate } = renderHook(() => useGetCases(), { + wrapper: ({ children }) => {children}, + }); await waitForNextUpdate(); await waitForNextUpdate(); result.current.setSelectedCases(selectedCases); diff --git a/x-pack/plugins/cases/public/containers/use_get_cases.tsx b/x-pack/plugins/cases/public/containers/use_get_cases.tsx index ec1abd62149269..b3aa374f5418e1 100644 --- a/x-pack/plugins/cases/public/containers/use_get_cases.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_cases.tsx @@ -19,6 +19,7 @@ import { import { useToasts } from '../common/lib/kibana'; import * as i18n from './translations'; import { getCases, patchCase } from './api'; +import { useOwnerContext } from '../components/owner_context/use_owner_context'; export interface UseGetCasesState { data: AllCases; @@ -139,12 +140,19 @@ export interface UseGetCases extends UseGetCasesState { const empty = {}; export const useGetCases = ( - initialQueryParams: Partial = empty, - initialFilterOptions: Partial = empty + params: { + initialQueryParams?: Partial; + initialFilterOptions?: Partial; + } = {} ): UseGetCases => { + const owner = useOwnerContext(); + const { initialQueryParams = empty, initialFilterOptions = empty } = params; const [state, dispatch] = useReducer(dataFetchReducer, { data: initialData, - filterOptions: { ...DEFAULT_FILTER_OPTIONS, ...initialFilterOptions }, + filterOptions: { + ...DEFAULT_FILTER_OPTIONS, + ...initialFilterOptions, + }, isError: false, loading: [], queryParams: { ...DEFAULT_QUERY_PARAMS, ...initialQueryParams }, @@ -177,7 +185,7 @@ export const useGetCases = ( dispatch({ type: 'FETCH_INIT', payload: 'cases' }); const response = await getCases({ - filterOptions, + filterOptions: { ...filterOptions, owner }, queryParams, signal: abortCtrlFetchCases.current.signal, }); @@ -200,7 +208,7 @@ export const useGetCases = ( } } }, - [toasts] + [owner, toasts] ); const dispatchUpdateCaseProperty = useCallback( diff --git a/x-pack/plugins/cases/public/containers/use_get_reporters.test.tsx b/x-pack/plugins/cases/public/containers/use_get_reporters.test.tsx index 8345ddf107872f..692c5237f58bf8 100644 --- a/x-pack/plugins/cases/public/containers/use_get_reporters.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_reporters.test.tsx @@ -5,10 +5,13 @@ * 2.0. */ +import React from 'react'; import { renderHook, act } from '@testing-library/react-hooks'; import { useGetReporters, UseGetReporters } from './use_get_reporters'; import { reporters, respReporters } from './mock'; import * as api from './api'; +import { TestProviders } from '../common/mock'; +import { SECURITY_SOLUTION_OWNER } from '../../common'; jest.mock('./api'); jest.mock('../common/lib/kibana'); @@ -22,8 +25,11 @@ describe('useGetReporters', () => { it('init', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useGetReporters() + const { result, waitForNextUpdate } = renderHook( + () => useGetReporters(), + { + wrapper: ({ children }) => {children}, + } ); await waitForNextUpdate(); expect(result.current).toEqual({ @@ -39,17 +45,22 @@ describe('useGetReporters', () => { it('calls getReporters api', async () => { const spyOnGetReporters = jest.spyOn(api, 'getReporters'); await act(async () => { - const { waitForNextUpdate } = renderHook(() => useGetReporters()); + const { waitForNextUpdate } = renderHook(() => useGetReporters(), { + wrapper: ({ children }) => {children}, + }); await waitForNextUpdate(); await waitForNextUpdate(); - expect(spyOnGetReporters).toBeCalledWith(abortCtrl.signal); + expect(spyOnGetReporters).toBeCalledWith(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]); }); }); it('fetch reporters', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useGetReporters() + const { result, waitForNextUpdate } = renderHook( + () => useGetReporters(), + { + wrapper: ({ children }) => {children}, + } ); await waitForNextUpdate(); await waitForNextUpdate(); @@ -66,8 +77,11 @@ describe('useGetReporters', () => { it('refetch reporters', async () => { const spyOnGetReporters = jest.spyOn(api, 'getReporters'); await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useGetReporters() + const { result, waitForNextUpdate } = renderHook( + () => useGetReporters(), + { + wrapper: ({ children }) => {children}, + } ); await waitForNextUpdate(); await waitForNextUpdate(); @@ -83,8 +97,11 @@ describe('useGetReporters', () => { }); await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useGetReporters() + const { result, waitForNextUpdate } = renderHook( + () => useGetReporters(), + { + wrapper: ({ children }) => {children}, + } ); await waitForNextUpdate(); await waitForNextUpdate(); diff --git a/x-pack/plugins/cases/public/containers/use_get_reporters.tsx b/x-pack/plugins/cases/public/containers/use_get_reporters.tsx index a9d28de33cb41d..b3c2eff2c8e014 100644 --- a/x-pack/plugins/cases/public/containers/use_get_reporters.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_reporters.tsx @@ -12,6 +12,7 @@ import { User } from '../../common'; import { getReporters } from './api'; import * as i18n from './translations'; import { useToasts } from '../common/lib/kibana'; +import { useOwnerContext } from '../components/owner_context/use_owner_context'; interface ReportersState { reporters: string[]; @@ -32,6 +33,7 @@ export interface UseGetReporters extends ReportersState { } export const useGetReporters = (): UseGetReporters => { + const owner = useOwnerContext(); const [reportersState, setReporterState] = useState(initialData); const toasts = useToasts(); @@ -48,7 +50,7 @@ export const useGetReporters = (): UseGetReporters => { isLoading: true, }); - const response = await getReporters(abortCtrlRef.current.signal); + const response = await getReporters(abortCtrlRef.current.signal, owner); const myReporters = response .map((r) => (r.full_name == null || isEmpty(r.full_name) ? r.username ?? '' : r.full_name)) .filter((u) => !isEmpty(u)); @@ -78,7 +80,7 @@ export const useGetReporters = (): UseGetReporters => { }); } } - }, [reportersState, toasts]); + }, [owner, reportersState, toasts]); useEffect(() => { fetchReporters(); diff --git a/x-pack/plugins/cases/public/containers/use_get_tags.test.tsx b/x-pack/plugins/cases/public/containers/use_get_tags.test.tsx index 3fecfb51b958c2..60d368aca0a04d 100644 --- a/x-pack/plugins/cases/public/containers/use_get_tags.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_tags.test.tsx @@ -5,10 +5,13 @@ * 2.0. */ +import React from 'react'; import { renderHook, act } from '@testing-library/react-hooks'; import { useGetTags, UseGetTags } from './use_get_tags'; import { tags } from './mock'; import * as api from './api'; +import { TestProviders } from '../common/mock'; +import { SECURITY_SOLUTION_OWNER } from '../../common'; jest.mock('./api'); jest.mock('../common/lib/kibana'); @@ -22,7 +25,9 @@ describe('useGetTags', () => { it('init', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useGetTags()); + const { result, waitForNextUpdate } = renderHook(() => useGetTags(), { + wrapper: ({ children }) => {children}, + }); await waitForNextUpdate(); expect(result.current).toEqual({ tags: [], @@ -36,16 +41,20 @@ describe('useGetTags', () => { it('calls getTags api', async () => { const spyOnGetTags = jest.spyOn(api, 'getTags'); await act(async () => { - const { waitForNextUpdate } = renderHook(() => useGetTags()); + const { waitForNextUpdate } = renderHook(() => useGetTags(), { + wrapper: ({ children }) => {children}, + }); await waitForNextUpdate(); await waitForNextUpdate(); - expect(spyOnGetTags).toBeCalledWith(abortCtrl.signal); + expect(spyOnGetTags).toBeCalledWith(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]); }); }); it('fetch tags', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useGetTags()); + const { result, waitForNextUpdate } = renderHook(() => useGetTags(), { + wrapper: ({ children }) => {children}, + }); await waitForNextUpdate(); await waitForNextUpdate(); expect(result.current).toEqual({ @@ -60,7 +69,9 @@ describe('useGetTags', () => { it('refetch tags', async () => { const spyOnGetTags = jest.spyOn(api, 'getTags'); await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useGetTags()); + const { result, waitForNextUpdate } = renderHook(() => useGetTags(), { + wrapper: ({ children }) => {children}, + }); await waitForNextUpdate(); await waitForNextUpdate(); result.current.fetchTags(); @@ -75,7 +86,9 @@ describe('useGetTags', () => { }); await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => useGetTags()); + const { result, waitForNextUpdate } = renderHook(() => useGetTags(), { + wrapper: ({ children }) => {children}, + }); await waitForNextUpdate(); await waitForNextUpdate(); diff --git a/x-pack/plugins/cases/public/containers/use_get_tags.tsx b/x-pack/plugins/cases/public/containers/use_get_tags.tsx index 4368b025baa382..362e7ebf8fbf34 100644 --- a/x-pack/plugins/cases/public/containers/use_get_tags.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_tags.tsx @@ -7,6 +7,7 @@ import { useEffect, useReducer, useRef, useCallback } from 'react'; import { useToasts } from '../common/lib/kibana'; +import { useOwnerContext } from '../components/owner_context/use_owner_context'; import { getTags } from './api'; import * as i18n from './translations'; @@ -52,6 +53,7 @@ const dataFetchReducer = (state: TagsState, action: Action): TagsState => { const initialData: string[] = []; export const useGetTags = (): UseGetTags => { + const owner = useOwnerContext(); const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: true, isError: false, @@ -68,7 +70,7 @@ export const useGetTags = (): UseGetTags => { abortCtrlRef.current = new AbortController(); dispatch({ type: 'FETCH_INIT' }); - const response = await getTags(abortCtrlRef.current.signal); + const response = await getTags(abortCtrlRef.current.signal, owner); if (!isCancelledRef.current) { dispatch({ type: 'FETCH_SUCCESS', payload: response }); diff --git a/x-pack/plugins/cases/public/containers/use_post_case.test.tsx b/x-pack/plugins/cases/public/containers/use_post_case.test.tsx index c1d030e7618c35..d2b638b4c846f1 100644 --- a/x-pack/plugins/cases/public/containers/use_post_case.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_post_case.test.tsx @@ -8,7 +8,7 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { usePostCase, UsePostCase } from './use_post_case'; import * as api from './api'; -import { ConnectorTypes } from '../../common'; +import { ConnectorTypes, SECURITY_SOLUTION_OWNER } from '../../common'; import { basicCasePost } from './mock'; jest.mock('./api'); @@ -29,7 +29,7 @@ describe('usePostCase', () => { settings: { syncAlerts: true, }, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }; beforeEach(() => { jest.clearAllMocks(); diff --git a/x-pack/plugins/cases/public/containers/use_post_comment.test.tsx b/x-pack/plugins/cases/public/containers/use_post_comment.test.tsx index a9750f213f3d65..8a86d9becdfde2 100644 --- a/x-pack/plugins/cases/public/containers/use_post_comment.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_post_comment.test.tsx @@ -7,7 +7,7 @@ import { renderHook, act } from '@testing-library/react-hooks'; -import { CommentType } from '../../common'; +import { CommentType, SECURITY_SOLUTION_OWNER } from '../../common'; import { usePostComment, UsePostComment } from './use_post_comment'; import { basicCaseId, basicSubCaseId } from './mock'; import * as api from './api'; @@ -20,7 +20,7 @@ describe('usePostComment', () => { const samplePost = { comment: 'a comment', type: CommentType.user as const, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }; const updateCaseCallback = jest.fn(); beforeEach(() => { diff --git a/x-pack/plugins/cases/public/containers/utils.ts b/x-pack/plugins/cases/public/containers/utils.ts index fc8064adf5d94e..de67b1cfbd6fa4 100644 --- a/x-pack/plugins/cases/public/containers/utils.ts +++ b/x-pack/plugins/cases/public/containers/utils.ts @@ -94,7 +94,6 @@ export const decodeCasesResponse = (respCase?: CasesResponse) => export const decodeCasesFindResponse = (respCases?: CasesFindResponse) => pipe(CasesFindResponseRt.decode(respCases), fold(throwErrors(createToasterPlainError), identity)); -// TODO: might need to refactor this export const decodeCaseConfigurationsResponse = (respCase?: CasesConfigurationsResponse) => { return pipe( CaseConfigurationsResponseRt.decode(respCase), diff --git a/x-pack/plugins/cases/public/methods/get_all_cases_selector_modal.tsx b/x-pack/plugins/cases/public/methods/get_all_cases_selector_modal.tsx index b6caae39c284ab..dbb466129c60b7 100644 --- a/x-pack/plugins/cases/public/methods/get_all_cases_selector_modal.tsx +++ b/x-pack/plugins/cases/public/methods/get_all_cases_selector_modal.tsx @@ -8,10 +8,14 @@ import React, { lazy, Suspense } from 'react'; import { EuiLoadingSpinner } from '@elastic/eui'; import { AllCasesSelectorModalProps } from '../components/all_cases/selector_modal'; +import { OwnerProvider } from '../components/owner_context'; +import { Owner } from '../types'; const AllCasesSelectorModalLazy = lazy(() => import('../components/all_cases/selector_modal')); -export const getAllCasesSelectorModalLazy = (props: AllCasesSelectorModalProps) => ( - }> - - +export const getAllCasesSelectorModalLazy = (props: AllCasesSelectorModalProps & Owner) => ( + + }> + + + ); diff --git a/x-pack/plugins/cases/public/types.ts b/x-pack/plugins/cases/public/types.ts index 269d1773b34046..2193832492aa22 100644 --- a/x-pack/plugins/cases/public/types.ts +++ b/x-pack/plugins/cases/public/types.ts @@ -39,6 +39,10 @@ export type StartServices = CoreStart & security: SecurityPluginSetup; }; +export interface Owner { + owner: string[]; +} + export interface CasesUiStart { getAllCases: (props: AllCasesProps) => ReactElement; getAllCasesSelectorModal: ( diff --git a/x-pack/plugins/cases/server/client/cases/mock.ts b/x-pack/plugins/cases/server/client/cases/mock.ts index a8079d6095ba32..23db57c6d3097e 100644 --- a/x-pack/plugins/cases/server/client/cases/mock.ts +++ b/x-pack/plugins/cases/server/client/cases/mock.ts @@ -12,6 +12,7 @@ import { CaseUserActionsResponse, AssociationType, CommentResponseAlertsType, + SECURITY_SOLUTION_OWNER, } from '../../../common'; import { BasicParams } from './types'; @@ -39,7 +40,7 @@ export const comment: CommentResponse = { email: 'testemail@elastic.co', username: 'elastic', }, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, pushed_at: null, pushed_by: null, updated_at: '2019-11-25T21:55:00.177Z', @@ -67,7 +68,7 @@ export const commentAlert: CommentResponse = { email: 'testemail@elastic.co', username: 'elastic', }, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, pushed_at: null, pushed_by: null, updated_at: '2019-11-25T21:55:00.177Z', @@ -85,7 +86,7 @@ export const commentAlertMultipleIds: CommentResponseAlertsType = { alertId: ['alert-id-1', 'alert-id-2'], index: 'alert-index-1', type: CommentType.alert as const, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }; export const commentGeneratedAlert: CommentResponseAlertsType = { @@ -135,7 +136,7 @@ export const userActions: CaseUserActionsResponse = [ action_id: 'fd830c60-6646-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', comment_id: null, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }, { action_field: ['pushed'], @@ -152,7 +153,7 @@ export const userActions: CaseUserActionsResponse = [ action_id: '0a801750-6647-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', comment_id: null, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }, { action_field: ['comment'], @@ -168,7 +169,7 @@ export const userActions: CaseUserActionsResponse = [ action_id: '7373eb60-6647-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', comment_id: 'comment-alert-1', - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }, { action_field: ['comment'], @@ -184,7 +185,7 @@ export const userActions: CaseUserActionsResponse = [ action_id: '7abc6410-6647-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', comment_id: 'comment-alert-2', - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }, { action_field: ['pushed'], @@ -201,7 +202,7 @@ export const userActions: CaseUserActionsResponse = [ action_id: '9b91d8f0-6647-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', comment_id: null, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }, { action_field: ['comment'], @@ -217,6 +218,6 @@ export const userActions: CaseUserActionsResponse = [ action_id: '0818e5e0-6648-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', comment_id: 'comment-user-1', - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }, ]; diff --git a/x-pack/plugins/cases/server/client/cases/utils.test.ts b/x-pack/plugins/cases/server/client/cases/utils.test.ts index 9dd36d2f8e534d..9f18fa4931e62e 100644 --- a/x-pack/plugins/cases/server/client/cases/utils.test.ts +++ b/x-pack/plugins/cases/server/client/cases/utils.test.ts @@ -29,6 +29,7 @@ import { transformFields, } from './utils'; import { flattenCaseSavedObject } from '../../common'; +import { SECURITY_SOLUTION_OWNER } from '../../../common'; const formatComment = { commentId: commentObj.id, @@ -701,7 +702,7 @@ describe('utils', () => { action_id: '9b91d8f0-6647-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', comment_id: null, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }, ]); diff --git a/x-pack/plugins/cases/server/common/utils.test.ts b/x-pack/plugins/cases/server/common/utils.test.ts index 4057cf4f3f52d1..322e45094eda4e 100644 --- a/x-pack/plugins/cases/server/common/utils.test.ts +++ b/x-pack/plugins/cases/server/common/utils.test.ts @@ -6,6 +6,7 @@ */ import { SavedObjectsFindResponse } from 'kibana/server'; +import { SECURITY_SOLUTION_OWNER } from '../../common'; import { AssociationType, CaseResponse, @@ -587,7 +588,7 @@ describe('common utils', () => { full_name: 'Elastic', username: 'elastic', associationType: AssociationType.case, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }; const res = transformNewComment(comment); @@ -616,7 +617,7 @@ describe('common utils', () => { comment: 'A comment', type: CommentType.user as const, createdDate: '2020-04-09T09:43:51.778Z', - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, associationType: AssociationType.case, }; @@ -650,7 +651,7 @@ describe('common utils', () => { email: null, full_name: null, username: null, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, associationType: AssociationType.case, }; @@ -684,7 +685,7 @@ describe('common utils', () => { createCommentFindResponse([ { ids: ['1'], - comments: [{ comment: '', type: CommentType.user, owner: 'securitySolution' }], + comments: [{ comment: '', type: CommentType.user, owner: SECURITY_SOLUTION_OWNER }], }, ]).saved_objects[0] ) @@ -706,7 +707,7 @@ describe('common utils', () => { id: 'rule-id-1', name: 'rule-name-1', }, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }, ], }, @@ -730,7 +731,7 @@ describe('common utils', () => { id: 'rule-id-1', name: 'rule-name-1', }, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }, ], }, @@ -751,7 +752,7 @@ describe('common utils', () => { { alertId: ['a', 'b'], index: '', - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, type: CommentType.alert, rule: { id: 'rule-id-1', @@ -760,7 +761,7 @@ describe('common utils', () => { }, { comment: '', - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, type: CommentType.user, }, ], @@ -780,7 +781,7 @@ describe('common utils', () => { ids: ['1'], comments: [ { - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, alertId: ['a', 'b'], index: '', type: CommentType.alert, @@ -795,7 +796,7 @@ describe('common utils', () => { ids: ['2'], comments: [ { - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, comment: '', type: CommentType.user, }, @@ -819,7 +820,7 @@ describe('common utils', () => { ids: ['1', '2'], comments: [ { - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, alertId: ['a', 'b'], index: '', type: CommentType.alert, @@ -851,7 +852,7 @@ describe('common utils', () => { ids: ['1', '2'], comments: [ { - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, alertId: ['a', 'b'], index: '', type: CommentType.alert, diff --git a/x-pack/plugins/cases/server/connectors/case/index.test.ts b/x-pack/plugins/cases/server/connectors/case/index.test.ts index 0727fbbe767760..7b8f57bf0d3bfb 100644 --- a/x-pack/plugins/cases/server/connectors/case/index.test.ts +++ b/x-pack/plugins/cases/server/connectors/case/index.test.ts @@ -27,6 +27,7 @@ import { createCasesClientFactory, createCasesClientMock, } from '../../client/mocks'; +import { SECURITY_SOLUTION_OWNER } from '../../../common'; const services = actionsMock.createServices(); let caseActionType: CaseActionType; @@ -753,7 +754,7 @@ describe('case connector', () => { comment: { comment: 'a comment', type: CommentType.user, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }, }, }; @@ -774,7 +775,7 @@ describe('case connector', () => { id: null, name: null, }, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }, }, }; @@ -958,7 +959,7 @@ describe('case connector', () => { settings: { syncAlerts: true, }, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }; mockCasesClient.cases.create.mockReturnValue(Promise.resolve(createReturn)); @@ -1055,7 +1056,7 @@ describe('case connector', () => { settings: { syncAlerts: true, }, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }, ]; @@ -1136,7 +1137,7 @@ describe('case connector', () => { username: 'awesome', }, id: 'mock-comment', - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, pushed_at: null, pushed_by: null, updated_at: null, @@ -1147,7 +1148,7 @@ describe('case connector', () => { settings: { syncAlerts: true, }, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }; mockCasesClient.attachments.add.mockReturnValue(Promise.resolve(commentReturn)); @@ -1160,7 +1161,7 @@ describe('case connector', () => { comment: { comment: 'a comment', type: CommentType.user, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }, }, }; 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 ff188426dd96db..bddceef8d782e6 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 @@ -21,6 +21,7 @@ import { import { CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, CASE_USER_ACTION_SAVED_OBJECT, + SECURITY_SOLUTION_OWNER, } from '../../../../common/constants'; import { mappings } from '../../../client/configure/mock'; @@ -58,7 +59,7 @@ export const mockCases: Array> = [ settings: { syncAlerts: true, }, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }, references: [], updated_at: '2019-11-25T21:54:48.952Z', @@ -97,7 +98,7 @@ export const mockCases: Array> = [ settings: { syncAlerts: true, }, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }, references: [], updated_at: '2019-11-25T22:32:00.900Z', @@ -140,7 +141,7 @@ export const mockCases: Array> = [ settings: { syncAlerts: true, }, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }, references: [], updated_at: '2019-11-25T22:32:17.947Z', @@ -187,7 +188,7 @@ export const mockCases: Array> = [ settings: { syncAlerts: true, }, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }, references: [], updated_at: '2019-11-25T22:32:17.947Z', @@ -250,7 +251,7 @@ export const mockCaseComments: Array> = [ email: 'testemail@elastic.co', username: 'elastic', }, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, pushed_at: null, pushed_by: null, updated_at: '2019-11-25T21:55:00.177Z', @@ -283,7 +284,7 @@ export const mockCaseComments: Array> = [ email: 'testemail@elastic.co', username: 'elastic', }, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, pushed_at: null, pushed_by: null, updated_at: '2019-11-25T21:55:14.633Z', @@ -317,7 +318,7 @@ export const mockCaseComments: Array> = [ email: 'testemail@elastic.co', username: 'elastic', }, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, pushed_at: null, pushed_by: null, updated_at: '2019-11-25T22:32:30.608Z', @@ -351,7 +352,7 @@ export const mockCaseComments: Array> = [ email: 'testemail@elastic.co', username: 'elastic', }, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, pushed_at: null, pushed_by: null, rule: { @@ -389,7 +390,7 @@ export const mockCaseComments: Array> = [ email: 'testemail@elastic.co', username: 'elastic', }, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, pushed_at: null, pushed_by: null, rule: { @@ -427,7 +428,7 @@ export const mockCaseComments: Array> = [ email: 'testemail@elastic.co', username: 'elastic', }, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, pushed_at: null, pushed_by: null, rule: { @@ -477,7 +478,7 @@ export const mockCaseConfigure: Array> = email: 'testemail@elastic.co', username: 'elastic', }, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }, references: [], updated_at: '2020-04-09T09:43:51.778Z', @@ -491,7 +492,7 @@ export const mockCaseMappings: Array> = [ id: 'mock-mappings-1', attributes: { mappings: mappings[ConnectorTypes.jira], - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }, references: [], }, @@ -503,7 +504,7 @@ export const mockCaseMappingsResilient: Array> = id: 'mock-mappings-1', attributes: { mappings: mappings[ConnectorTypes.resilient], - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }, references: [], }, @@ -534,7 +535,7 @@ export const mockUserActions: Array> = [ new_value: '{"title":"A case","tags":["case"],"description":"Yeah!","connector":{"id":"connector-od","name":"My Connector","type":".servicenow-sir","fields":{"category":"Denial of Service","destIp":true,"malwareHash":true,"malwareUrl":true,"priority":"2","sourceIp":true,"subcategory":"45"}},"settings":{"syncAlerts":true}}', old_value: null, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }, version: 'WzYsMV0=', references: [], @@ -554,7 +555,7 @@ export const mockUserActions: Array> = [ new_value: '{"type":"alert","alertId":"cec3da90fb37a44407145adf1593f3b0d5ad94c4654201f773d63b5d4706128e","index":".siem-signals-default-000008"}', old_value: null, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }, version: 'WzYsMV0=', references: [], diff --git a/x-pack/plugins/cases/server/routes/api/__mocks__/request_responses.ts b/x-pack/plugins/cases/server/routes/api/__mocks__/request_responses.ts index 32e42fea5c2072..f3e6bcd7fc9ffc 100644 --- a/x-pack/plugins/cases/server/routes/api/__mocks__/request_responses.ts +++ b/x-pack/plugins/cases/server/routes/api/__mocks__/request_responses.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CasePostRequest, ConnectorTypes } from '../../../../common/api'; +import { SECURITY_SOLUTION_OWNER, CasePostRequest, ConnectorTypes } from '../../../../common'; export const newCase: CasePostRequest = { title: 'My new case', @@ -20,5 +20,5 @@ export const newCase: CasePostRequest = { settings: { syncAlerts: true, }, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }; diff --git a/x-pack/plugins/cases/server/scripts/sub_cases/index.ts b/x-pack/plugins/cases/server/scripts/sub_cases/index.ts index 2d37916919084a..edabe9c4d4a1fb 100644 --- a/x-pack/plugins/cases/server/scripts/sub_cases/index.ts +++ b/x-pack/plugins/cases/server/scripts/sub_cases/index.ts @@ -8,7 +8,14 @@ import yargs from 'yargs'; import { ToolingLog } from '@kbn/dev-utils'; import { KbnClient } from '@kbn/test'; -import { CaseResponse, CaseType, CommentType, ConnectorTypes, CASES_URL } from '../../../common'; +import { + CaseResponse, + CaseType, + CommentType, + ConnectorTypes, + CASES_URL, + SECURITY_SOLUTION_OWNER, +} from '../../../common'; import { ActionResult, ActionTypeExecutorResult } from '../../../../actions/common'; import { ContextTypeGeneratedAlertType, createAlertsString } from '../../connectors'; @@ -101,7 +108,7 @@ async function handleGenGroupAlerts(argv: any) { console.log('Case id: ', caseID); const comment: ContextTypeGeneratedAlertType = { - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, type: CommentType.generatedAlert, alerts: createAlertsString( argv.ids.map((id: string) => ({ diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx index 60fa0e4aafd8ea..337c07fa93eabe 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx @@ -73,6 +73,7 @@ export const AllCases = React.memo(({ userCanCrud }) => { onClick: goToCreateCase, }, userCanCrud, + owner: [APP_ID], }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/flyout.tsx b/x-pack/plugins/security_solution/public/cases/components/create/flyout.tsx index 0f9f64b32bdd03..1023bfc8b0206a 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/flyout.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/flyout.tsx @@ -12,6 +12,7 @@ import { EuiFlyout, EuiFlyoutHeader, EuiTitle, EuiFlyoutBody } from '@elastic/eu import * as i18n from '../../translations'; import { useKibana } from '../../../common/lib/kibana'; import { Case } from '../../../../../cases/common'; +import { APP_ID } from '../../../../common/constants'; export interface CreateCaseModalProps { afterCaseCreated?: (theCase: Case) => Promise; @@ -65,6 +66,7 @@ const CreateCaseFlyoutComponent: React.FC = ({ onCancel: onCloseFlyout, onSuccess, withSteps: false, + owner: [APP_ID], })} diff --git a/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx index 2d5faef8aa009a..1a6015d1bbd457 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx @@ -17,6 +17,7 @@ import { Create } from '.'; import { useKibana } from '../../../common/lib/kibana'; import { Case } from '../../../../../cases/public/containers/types'; import { basicCase } from '../../../../../cases/public/containers/mock'; +import { APP_ID } from '../../../../common/constants'; jest.mock('../use_insert_timeline'); jest.mock('../../../common/lib/kibana'); @@ -47,6 +48,7 @@ describe('Create case', () => { ); expect(mockCreateCase).toHaveBeenCalled(); + expect(mockCreateCase.mock.calls[0][0].owner).toEqual([APP_ID]); }); it('should redirect to all cases on cancel click', async () => { diff --git a/x-pack/plugins/security_solution/public/cases/components/create/index.tsx b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx index 4a1a64f5fcb417..f946cefd3494c9 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx @@ -13,6 +13,7 @@ import { getCaseDetailsUrl } from '../../../common/components/link_to'; import { useKibana } from '../../../common/lib/kibana'; import * as timelineMarkdownPlugin from '../../../common/components/markdown_editor/plugins/timeline'; import { useInsertTimeline } from '../use_insert_timeline'; +import { APP_ID } from '../../../../common/constants'; export const Create = React.memo(() => { const { cases } = useKibana().services; @@ -43,6 +44,7 @@ export const Create = React.memo(() => { useInsertTimeline, }, }, + owner: [APP_ID], })} ); diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx index fa37fb53a54b06..162758a90b7baa 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx @@ -14,7 +14,7 @@ import { useStateToaster } from '../../../common/components/toasters'; import { TestProviders } from '../../../common/mock'; import { AddToCaseAction } from './add_to_case_action'; import { basicCase } from '../../../../../cases/public/containers/mock'; -import { Case } from '../../../../../cases/common'; +import { Case, SECURITY_SOLUTION_OWNER } from '../../../../../cases/common'; jest.mock('../../../common/lib/kibana'); jest.mock('../../../common/components/link_to', () => { @@ -116,7 +116,7 @@ describe('AddToCaseAction', () => { alertId: 'test-id', index: 'test-index', rule: { id: 'rule-id', name: 'rule-name' }, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }); }); @@ -143,7 +143,7 @@ describe('AddToCaseAction', () => { id: 'rule-id', name: null, }, - owner: 'securitySolution', + owner: SECURITY_SOLUTION_OWNER, }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx index f7594dbb4c180a..19c59f2f57d879 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx @@ -111,8 +111,7 @@ const AddToCaseActionComponent: React.FC = ({ id: rule?.id != null ? rule.id[0] : null, name: rule?.name != null ? rule.name[0] : null, }, - // TODO: refactor - owner: 'securitySolution', + owner: APP_ID, }, updateCase, }); @@ -238,7 +237,7 @@ const AddToCaseActionComponent: React.FC = ({ id: rule?.id != null ? rule.id[0] : null, name: rule?.name != null ? rule.name[0] : null, }, - owner: 'securitySolution', + owner: APP_ID, }, createCaseNavigation: { href: formatUrl(getCreateCaseUrl()), @@ -248,6 +247,7 @@ const AddToCaseActionComponent: React.FC = ({ onRowClick: onCaseClicked, updateCase: onCaseSuccess, userCanCrud: userPermissions?.crud ?? false, + owner: [APP_ID], })} ); diff --git a/x-pack/plugins/security_solution/public/cases/pages/configure_cases.tsx b/x-pack/plugins/security_solution/public/cases/pages/configure_cases.tsx index 3e838f47e6dc2b..c735fd5bc85677 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/configure_cases.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/configure_cases.tsx @@ -19,6 +19,7 @@ import { navTabs } from '../../app/home/home_navigations'; import { CaseHeaderPage } from '../components/case_header_page'; import { WhitePageWrapper, SectionWrapper } from '../components/wrappers'; import * as i18n from './translations'; +import { APP_ID } from '../../../common/constants'; const ConfigureCasesPageComponent: React.FC = () => { const { cases } = useKibana().services; @@ -55,6 +56,7 @@ const ConfigureCasesPageComponent: React.FC = () => { {cases.getConfigureCases({ userCanCrud: userPermissions?.crud ?? false, + owner: [APP_ID], })} diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_cases/index.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_cases/index.tsx index bcf9953d70d830..fc2e2e87ffc5f1 100644 --- a/x-pack/plugins/security_solution/public/overview/components/recent_cases/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/recent_cases/index.tsx @@ -58,6 +58,7 @@ const RecentCasesComponent = () => { }, }, maxCasesToShow: MAX_CASES_TO_SHOW, + owner: [APP_ID], }); }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx index a4c6fe1e344b3b..0f583b838d86c7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx @@ -177,6 +177,7 @@ const AddToCaseButtonComponent: React.FC = ({ timelineId }) => { }, onRowClick, userCanCrud: userPermissions?.crud ?? false, + owner: [APP_ID], })} ); From 3c7670bf69b91770172516913c14a2ed8dd0561c Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Fri, 14 May 2021 11:19:37 -0400 Subject: [PATCH 059/113] Fixing case ids by alert id route call --- .../detections/containers/detection_engine/alerts/api.ts | 3 +++ .../detection_engine/alerts/use_cases_from_alerts.tsx | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts index 300005b23caaa6..be1e6fdfbe0873 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts @@ -140,9 +140,12 @@ export const createHostIsolation = async ({ */ export const getCaseIdsFromAlertId = async ({ alertId, + owner, }: { alertId: string; + owner: string[]; }): Promise => KibanaServices.get().http.fetch(getCasesFromAlertsUrl(alertId), { method: 'get', + query: { ...(owner.length > 0 ? { owner } : {}) }, }); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.tsx index fb130eb7447005..85b80a588e88d2 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_cases_from_alerts.tsx @@ -7,6 +7,7 @@ import { isEmpty } from 'lodash'; import { useEffect, useState } from 'react'; +import { APP_ID } from '../../../../../common/constants'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { getCaseIdsFromAlertId } from './api'; import { CASES_FROM_ALERTS_FAILURE } from './translations'; @@ -28,7 +29,7 @@ export const useCasesFromAlerts = ({ alertId }: { alertId: string }): CasesFromA setLoading(true); const fetchData = async () => { try { - const casesResponse = await getCaseIdsFromAlertId({ alertId }); + const casesResponse = await getCaseIdsFromAlertId({ alertId, owner: [APP_ID] }); if (isMounted) { setCases(casesResponse); } From dafb4fe48f406880ffd80261fe8de77f0e999d5e Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Fri, 14 May 2021 12:27:28 -0400 Subject: [PATCH 060/113] [Cases] Fixing UI feature permissions and adding UI tests (#100074) * Integration tests for cases privs and fixes * Fixing ui cases permissions and adding tests * Adding test for collection failure and fixing jest * Renaming variables --- .../cases/public/common/lib/kibana/hooks.ts | 26 -- .../integration/cases/privileges.spec.ts | 234 ++++++++++++++++++ .../security_solution/cypress/objects/case.ts | 7 +- .../security_solution/cypress/tasks/common.ts | 31 ++- .../cypress/tasks/create_new_case.ts | 3 +- .../security_solution/cypress/tasks/login.ts | 71 ++++++ .../add_to_case_action.test.tsx | 6 +- .../timeline_actions/add_to_case_action.tsx | 4 +- .../public/cases/pages/case.tsx | 4 +- .../public/cases/pages/case_details.tsx | 4 +- .../public/cases/pages/configure_cases.tsx | 4 +- .../public/cases/pages/create_case.tsx | 4 +- .../common/lib/kibana/__mocks__/index.ts | 2 +- .../public/common/lib/kibana/hooks.ts | 17 +- .../flyout/add_to_case_button/index.tsx | 4 +- .../security_solution/server/plugin.ts | 8 +- .../tests/common/cases/post_case.ts | 5 + 17 files changed, 364 insertions(+), 70 deletions(-) create mode 100644 x-pack/plugins/security_solution/cypress/integration/cases/privileges.spec.ts diff --git a/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts b/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts index cb90568982282d..5246e09f6b0f30 100644 --- a/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts +++ b/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts @@ -104,29 +104,3 @@ export const useCurrentUser = (): AuthenticatedElasticUser | null => { }, [fetchUser]); return user; }; - -export interface UseGetUserSavedObjectPermissions { - crud: boolean; - read: boolean; -} - -export const useGetUserSavedObjectPermissions = () => { - const [ - savedObjectsPermissions, - setSavedObjectsPermissions, - ] = useState(null); - const uiCapabilities = useKibana().services.application.capabilities; - - useEffect(() => { - const capabilitiesCanUserCRUD: boolean = - typeof uiCapabilities.siem.crud === 'boolean' ? uiCapabilities.siem.crud : false; - const capabilitiesCanUserRead: boolean = - typeof uiCapabilities.siem.show === 'boolean' ? uiCapabilities.siem.show : false; - setSavedObjectsPermissions({ - crud: capabilitiesCanUserCRUD, - read: capabilitiesCanUserRead, - }); - }, [uiCapabilities]); - - return savedObjectsPermissions; -}; diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/privileges.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases/privileges.spec.ts new file mode 100644 index 00000000000000..4d6c60e93ee208 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/cases/privileges.spec.ts @@ -0,0 +1,234 @@ +/* + * 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 { TestCaseWithoutTimeline } from '../../objects/case'; +import { ALL_CASES_NAME } from '../../screens/all_cases'; + +import { goToCreateNewCase } from '../../tasks/all_cases'; +import { cleanKibana, deleteCases } from '../../tasks/common'; + +import { + backToCases, + createCase, + fillCasesMandatoryfields, + filterStatusOpen, +} from '../../tasks/create_new_case'; +import { + constructUrlWithUser, + getEnvAuth, + loginWithUserAndWaitForPageWithoutDateRange, +} from '../../tasks/login'; + +import { CASES_URL } from '../../urls/navigation'; + +interface User { + username: string; + password: string; + description?: string; + roles: string[]; +} + +interface UserInfo { + username: string; + full_name: string; + email: string; +} + +interface FeaturesPrivileges { + [featureId: string]: string[]; +} + +interface ElasticsearchIndices { + names: string[]; + privileges: string[]; +} + +interface ElasticSearchPrivilege { + cluster?: string[]; + indices?: ElasticsearchIndices[]; +} + +interface KibanaPrivilege { + spaces: string[]; + base?: string[]; + feature?: FeaturesPrivileges; +} + +interface Role { + name: string; + privileges: { + elasticsearch?: ElasticSearchPrivilege; + kibana?: KibanaPrivilege[]; + }; +} + +const secAll: Role = { + name: 'sec_all_role', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + siem: ['all'], + actions: ['all'], + actionsSimulators: ['all'], + }, + spaces: ['*'], + }, + ], + }, +}; + +const secAllUser: User = { + username: 'sec_all_user', + password: 'password', + roles: [secAll.name], +}; + +const secReadCasesAll: Role = { + name: 'sec_read_cases_all_role', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + siem: ['minimal_read', 'cases_all'], + actions: ['all'], + actionsSimulators: ['all'], + }, + spaces: ['*'], + }, + ], + }, +}; + +const secReadCasesAllUser: User = { + username: 'sec_read_cases_all_user', + password: 'password', + roles: [secReadCasesAll.name], +}; + +const usersToCreate = [secAllUser, secReadCasesAllUser]; +const rolesToCreate = [secAll, secReadCasesAll]; + +const getUserInfo = (user: User): UserInfo => ({ + username: user.username, + full_name: user.username.replace('_', ' '), + email: `${user.username}@elastic.co`, +}); + +const createUsersAndRoles = (users: User[], roles: Role[]) => { + const envUser = getEnvAuth(); + for (const role of roles) { + cy.log(`Creating role: ${JSON.stringify(role)}`); + cy.request({ + body: role.privileges, + headers: { 'kbn-xsrf': 'cypress-creds-via-config' }, + method: 'PUT', + url: constructUrlWithUser(envUser, `/api/security/role/${role.name}`), + }) + .its('status') + .should('eql', 204); + } + + for (const user of users) { + const userInfo = getUserInfo(user); + cy.log(`Creating user: ${JSON.stringify(user)}`); + cy.request({ + body: { + username: user.username, + password: user.password, + roles: user.roles, + full_name: userInfo.full_name, + email: userInfo.email, + }, + headers: { 'kbn-xsrf': 'cypress-creds-via-config' }, + method: 'POST', + url: constructUrlWithUser(envUser, `/internal/security/users/${user.username}`), + }) + .its('status') + .should('eql', 200); + } +}; + +const deleteUsersAndRoles = (users: User[], roles: Role[]) => { + const envUser = getEnvAuth(); + for (const user of users) { + cy.log(`Deleting user: ${JSON.stringify(user)}`); + cy.request({ + headers: { 'kbn-xsrf': 'cypress-creds-via-config' }, + method: 'DELETE', + url: constructUrlWithUser(envUser, `/internal/security/users/${user.username}`), + failOnStatusCode: false, + }) + .its('status') + .should('oneOf', [204, 404]); + } + + for (const role of roles) { + cy.log(`Deleting role: ${JSON.stringify(role)}`); + cy.request({ + headers: { 'kbn-xsrf': 'cypress-creds-via-config' }, + method: 'DELETE', + url: constructUrlWithUser(envUser, `/api/security/role/${role.name}`), + failOnStatusCode: false, + }) + .its('status') + .should('oneOf', [204, 404]); + } +}; + +const testCase: TestCaseWithoutTimeline = { + name: 'This is the title of the case', + tags: ['Tag1', 'Tag2'], + description: 'This is the case description', + reporter: 'elastic', + owner: 'securitySolution', +}; + +describe('Cases privileges', () => { + before(() => { + cleanKibana(); + createUsersAndRoles(usersToCreate, rolesToCreate); + }); + + after(() => { + deleteUsersAndRoles(usersToCreate, rolesToCreate); + cleanKibana(); + }); + + beforeEach(() => { + deleteCases(); + }); + + for (const user of [secAllUser, secReadCasesAllUser]) { + it(`User ${user.username} with role(s) ${user.roles.join()} can create a case`, () => { + loginWithUserAndWaitForPageWithoutDateRange(CASES_URL, user); + goToCreateNewCase(); + fillCasesMandatoryfields(testCase); + createCase(); + backToCases(); + filterStatusOpen(); + + cy.get(ALL_CASES_NAME).should('have.text', testCase.name); + }); + } +}); diff --git a/x-pack/plugins/security_solution/cypress/objects/case.ts b/x-pack/plugins/security_solution/cypress/objects/case.ts index 278eab29f0a62e..847236688dee74 100644 --- a/x-pack/plugins/security_solution/cypress/objects/case.ts +++ b/x-pack/plugins/security_solution/cypress/objects/case.ts @@ -7,11 +7,14 @@ import { CompleteTimeline, timeline } from './timeline'; -export interface TestCase { +export interface TestCase extends TestCaseWithoutTimeline { + timeline: CompleteTimeline; +} + +export interface TestCaseWithoutTimeline { name: string; tags: string[]; description: string; - timeline: CompleteTimeline; reporter: string; owner: string; } diff --git a/x-pack/plugins/security_solution/cypress/tasks/common.ts b/x-pack/plugins/security_solution/cypress/tasks/common.ts index 468b0e22838ddd..d726d5daa5cbc4 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/common.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/common.ts @@ -106,19 +106,7 @@ export const cleanKibana = () => { }, }); - cy.request('POST', `${kibanaIndexUrl}/_delete_by_query?conflicts=proceed`, { - query: { - bool: { - filter: [ - { - match: { - type: 'cases', - }, - }, - ], - }, - }, - }); + deleteCases(); cy.request('POST', `${kibanaIndexUrl}/_delete_by_query?conflicts=proceed`, { query: { @@ -149,4 +137,21 @@ export const cleanKibana = () => { esArchiverResetKibana(); }; +export const deleteCases = () => { + const kibanaIndexUrl = `${Cypress.env('ELASTICSEARCH_URL')}/.kibana_\*`; + cy.request('POST', `${kibanaIndexUrl}/_delete_by_query?conflicts=proceed`, { + query: { + bool: { + filter: [ + { + match: { + type: 'cases', + }, + }, + ], + }, + }, + }); +}; + export const scrollToBottom = () => cy.scrollTo('bottom'); diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts index ed9174e2a74bb9..6f1868d047c067 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts @@ -10,6 +10,7 @@ import { JiraConnectorOptions, ServiceNowconnectorOptions, TestCase, + TestCaseWithoutTimeline, } from '../objects/case'; import { ALL_CASES_OPEN_CASES_COUNT, ALL_CASES_OPEN_FILTER } from '../screens/all_cases'; @@ -46,7 +47,7 @@ export const filterStatusOpen = () => { cy.get(ALL_CASES_OPEN_FILTER).click(); }; -export const fillCasesMandatoryfields = (newCase: TestCase) => { +export const fillCasesMandatoryfields = (newCase: TestCaseWithoutTimeline) => { cy.get(TITLE_INPUT).type(newCase.name, { force: true }); newCase.tags.forEach((tag) => { cy.get(TAGS_INPUT).type(`${tag}{enter}`, { force: true }); diff --git a/x-pack/plugins/security_solution/cypress/tasks/login.ts b/x-pack/plugins/security_solution/cypress/tasks/login.ts index 0a0e578ffd3824..be447993273fbe 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/login.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/login.ts @@ -67,6 +67,32 @@ export const getUrlWithRoute = (role: ROLES, route: string) => { return theUrl; }; +interface User { + username: string; + password: string; +} + +/** + * Builds a URL with basic auth using the passed in user. + * + * @param user the user information to build the basic auth with + * @param route string route to visit + */ +export const constructUrlWithUser = (user: User, route: string) => { + const hostname = Cypress.env('hostname'); + const username = user.username; + const password = user.password; + const protocol = Cypress.env('protocol'); + const port = Cypress.env('configport'); + + const path = `${route.startsWith('/') ? '' : '/'}${route}`; + const strUrl = `${protocol}://${username}:${password}@${hostname}:${port}${path}`; + const builtUrl = new URL(strUrl); + + cy.log(`origin: ${builtUrl.href}`); + return builtUrl.href; +}; + export const getCurlScriptEnvVars = () => ({ ELASTICSEARCH_URL: Cypress.env('ELASTICSEARCH_URL'), ELASTICSEARCH_USERNAME: Cypress.env('ELASTICSEARCH_USERNAME'), @@ -102,6 +128,23 @@ export const deleteRoleAndUser = (role: ROLES) => { }); }; +export const loginWithUser = (user: User) => { + cy.request({ + body: { + providerType: 'basic', + providerName: 'basic', + currentURL: '/', + params: { + username: user.username, + password: user.password, + }, + }, + headers: { 'kbn-xsrf': 'cypress-creds-via-config' }, + method: 'POST', + url: constructUrlWithUser(user, LOGIN_API_ENDPOINT), + }); +}; + export const loginWithRole = async (role: ROLES) => { postRoleAndUser(role); const theUrl = Url.format({ @@ -214,6 +257,28 @@ const loginViaConfig = () => { }); }; +/** + * Get the configured auth details that were used to spawn cypress + * + * @returns the default Elasticsearch username and password for this environment + */ +export const getEnvAuth = (): User => { + if (credentialsProvidedByEnvironment()) { + return { + username: Cypress.env(ELASTICSEARCH_USERNAME), + password: Cypress.env(ELASTICSEARCH_PASSWORD), + }; + } else { + let user: User = { username: '', password: '' }; + cy.readFile(KIBANA_DEV_YML_PATH).then((devYml) => { + const config = yaml.safeLoad(devYml); + user = { username: config.elasticsearch.username, password: config.elasticsearch.password }; + }); + + return user; + } +}; + /** * Authenticates with Kibana, visits the specified `url`, and waits for the * Kibana global nav to be displayed before continuing @@ -232,6 +297,12 @@ export const loginAndWaitForPageWithoutDateRange = (url: string, role?: ROLES) = cy.get('[data-test-subj="headerGlobalNav"]', { timeout: 120000 }); }; +export const loginWithUserAndWaitForPageWithoutDateRange = (url: string, user: User) => { + loginWithUser(user); + cy.visit(constructUrlWithUser(user, url)); + cy.get('[data-test-subj="headerGlobalNav"]', { timeout: 120000 }); +}; + export const loginAndWaitForTimeline = (timelineId: string, role?: ROLES) => { const route = `/app/security/timelines?timeline=(id:'${timelineId}',isOpen:!t)`; diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx index 162758a90b7baa..77fa9e8b3cc8c9 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { mount } from 'enzyme'; import { EuiGlobalToastList } from '@elastic/eui'; -import { useKibana, useGetUserSavedObjectPermissions } from '../../../common/lib/kibana'; +import { useKibana, useGetUserCasesPermissions } from '../../../common/lib/kibana'; import { useStateToaster } from '../../../common/components/toasters'; import { TestProviders } from '../../../common/mock'; import { AddToCaseAction } from './add_to_case_action'; @@ -62,7 +62,7 @@ describe('AddToCaseAction', () => { getAllCasesSelectorModal: mockAllCasesModal.mockImplementation(() => <>{'test'}), }; (useStateToaster as jest.Mock).mockReturnValue([jest.fn(), mockDispatchToaster]); - (useGetUserSavedObjectPermissions as jest.Mock).mockReturnValue({ + (useGetUserCasesPermissions as jest.Mock).mockReturnValue({ crud: true, read: true, }); @@ -201,7 +201,7 @@ describe('AddToCaseAction', () => { }); it('disabled when user does not have crud permissions', () => { - (useGetUserSavedObjectPermissions as jest.Mock).mockReturnValue({ + (useGetUserCasesPermissions as jest.Mock).mockReturnValue({ crud: false, read: true, }); diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx index 6ed67dd2107a80..4435b32e07cc2e 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx @@ -27,7 +27,7 @@ import { } from '../../../common/components/link_to'; import { useStateToaster } from '../../../common/components/toasters'; import { useControl } from '../../../common/hooks/use_control'; -import { useGetUserSavedObjectPermissions, useKibana } from '../../../common/lib/kibana'; +import { useGetUserCasesPermissions, useKibana } from '../../../common/lib/kibana'; import { ActionIconItem } from '../../../timelines/components/timeline/body/actions/action_icon_item'; import { CreateCaseFlyout } from '../create/flyout'; import { createUpdateSuccessToaster } from './helpers'; @@ -67,7 +67,7 @@ const AddToCaseActionComponent: React.FC = ({ const [isPopoverOpen, setIsPopoverOpen] = useState(false); const openPopover = useCallback(() => setIsPopoverOpen(true), []); const closePopover = useCallback(() => setIsPopoverOpen(false), []); - const userPermissions = useGetUserSavedObjectPermissions(); + const userPermissions = useGetUserCasesPermissions(); const isEventSupported = !isEmpty(ecsRowData.signal?.rule?.id); const userCanCrud = userPermissions?.crud ?? false; diff --git a/x-pack/plugins/security_solution/public/cases/pages/case.tsx b/x-pack/plugins/security_solution/public/cases/pages/case.tsx index ff7589e9deb2a9..4f0163eb8190a6 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/case.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/case.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { WrapperPage } from '../../common/components/wrapper_page'; -import { useGetUserSavedObjectPermissions } from '../../common/lib/kibana'; +import { useGetUserCasesPermissions } from '../../common/lib/kibana'; import { SpyRoute } from '../../common/utils/route/spy_routes'; import { AllCases } from '../components/all_cases'; @@ -17,7 +17,7 @@ import { CaseSavedObjectNoPermissions } from './saved_object_no_permissions'; import { SecurityPageName } from '../../app/types'; export const CasesPage = React.memo(() => { - const userPermissions = useGetUserSavedObjectPermissions(); + const userPermissions = useGetUserCasesPermissions(); return userPermissions == null || userPermissions?.read ? ( <> diff --git a/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx b/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx index 1841ca39ae853b..03407c7a5adaab 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx @@ -12,7 +12,7 @@ import { SecurityPageName } from '../../app/types'; import { SpyRoute } from '../../common/utils/route/spy_routes'; import { WrapperPage } from '../../common/components/wrapper_page'; import { useGetUrlSearch } from '../../common/components/navigation/use_get_url_search'; -import { useGetUserSavedObjectPermissions } from '../../common/lib/kibana'; +import { useGetUserCasesPermissions } from '../../common/lib/kibana'; import { getCaseUrl } from '../../common/components/link_to'; import { navTabs } from '../../app/home/home_navigations'; import { CaseView } from '../components/case_view'; @@ -20,7 +20,7 @@ import { savedObjectReadOnlyErrorMessage, CaseCallOut } from '../components/call export const CaseDetailsPage = React.memo(() => { const history = useHistory(); - const userPermissions = useGetUserSavedObjectPermissions(); + const userPermissions = useGetUserCasesPermissions(); const { detailName: caseId, subCaseId } = useParams<{ detailName?: string; subCaseId?: string; diff --git a/x-pack/plugins/security_solution/public/cases/pages/configure_cases.tsx b/x-pack/plugins/security_solution/public/cases/pages/configure_cases.tsx index c735fd5bc85677..905167c232c7d8 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/configure_cases.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/configure_cases.tsx @@ -13,7 +13,7 @@ import { SecurityPageName } from '../../app/types'; import { getCaseUrl } from '../../common/components/link_to'; import { useGetUrlSearch } from '../../common/components/navigation/use_get_url_search'; import { WrapperPage } from '../../common/components/wrapper_page'; -import { useGetUserSavedObjectPermissions, useKibana } from '../../common/lib/kibana'; +import { useGetUserCasesPermissions, useKibana } from '../../common/lib/kibana'; import { SpyRoute } from '../../common/utils/route/spy_routes'; import { navTabs } from '../../app/home/home_navigations'; import { CaseHeaderPage } from '../components/case_header_page'; @@ -24,7 +24,7 @@ import { APP_ID } from '../../../common/constants'; const ConfigureCasesPageComponent: React.FC = () => { const { cases } = useKibana().services; const history = useHistory(); - const userPermissions = useGetUserSavedObjectPermissions(); + const userPermissions = useGetUserCasesPermissions(); const search = useGetUrlSearch(navTabs.case); const backOptions = useMemo( diff --git a/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx b/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx index 24b179f4a41bf9..41344a8deb3b12 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx @@ -12,7 +12,7 @@ import { SecurityPageName } from '../../app/types'; import { getCaseUrl } from '../../common/components/link_to'; import { useGetUrlSearch } from '../../common/components/navigation/use_get_url_search'; import { WrapperPage } from '../../common/components/wrapper_page'; -import { useGetUserSavedObjectPermissions } from '../../common/lib/kibana'; +import { useGetUserCasesPermissions } from '../../common/lib/kibana'; import { SpyRoute } from '../../common/utils/route/spy_routes'; import { navTabs } from '../../app/home/home_navigations'; import { CaseHeaderPage } from '../components/case_header_page'; @@ -21,7 +21,7 @@ import * as i18n from './translations'; export const CreateCasePage = React.memo(() => { const history = useHistory(); - const userPermissions = useGetUserSavedObjectPermissions(); + const userPermissions = useGetUserCasesPermissions(); const search = useGetUrlSearch(navTabs.case); const backOptions = useMemo( diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts index 79c7b21158005b..eb0ae1ae1dee9e 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts @@ -45,4 +45,4 @@ export const useToasts = jest export const useCurrentUser = jest.fn(); export const withKibana = jest.fn(createWithKibanaMock()); export const KibanaContextProvider = jest.fn(createKibanaContextProviderMock()); -export const useGetUserSavedObjectPermissions = jest.fn(); +export const useGetUserCasesPermissions = jest.fn(); diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts index 6b5599292f6d44..4a2caefba1b973 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts @@ -138,28 +138,25 @@ export const useCurrentUser = (): AuthenticatedElasticUser | null => { return user; }; -export interface UseGetUserSavedObjectPermissions { +export interface UseGetUserCasesPermissions { crud: boolean; read: boolean; } -export const useGetUserSavedObjectPermissions = () => { - const [ - savedObjectsPermissions, - setSavedObjectsPermissions, - ] = useState(null); +export const useGetUserCasesPermissions = () => { + const [casesPermissions, setCasesPermissions] = useState(null); const uiCapabilities = useKibana().services.application.capabilities; useEffect(() => { const capabilitiesCanUserCRUD: boolean = - typeof uiCapabilities.siem.crud === 'boolean' ? uiCapabilities.siem.crud : false; + typeof uiCapabilities.siem.crud_cases === 'boolean' ? uiCapabilities.siem.crud_cases : false; const capabilitiesCanUserRead: boolean = - typeof uiCapabilities.siem.show === 'boolean' ? uiCapabilities.siem.show : false; - setSavedObjectsPermissions({ + typeof uiCapabilities.siem.read_cases === 'boolean' ? uiCapabilities.siem.read_cases : false; + setCasesPermissions({ crud: capabilitiesCanUserCRUD, read: capabilitiesCanUserRead, }); }, [uiCapabilities]); - return savedObjectsPermissions; + return casesPermissions; }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx index 0f583b838d86c7..dd21b33afa5b4c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx @@ -15,7 +15,7 @@ import { APP_ID } from '../../../../../common/constants'; import { timelineSelectors } from '../../../../timelines/store/timeline'; import { setInsertTimeline, showTimeline } from '../../../store/timeline/actions'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; -import { useGetUserSavedObjectPermissions, useKibana } from '../../../../common/lib/kibana'; +import { useGetUserCasesPermissions, useKibana } from '../../../../common/lib/kibana'; import { TimelineStatus, TimelineId, TimelineType } from '../../../../../common/types/timeline'; import { getCreateCaseUrl, @@ -71,7 +71,7 @@ const AddToCaseButtonComponent: React.FC = ({ timelineId }) => { ); const { formatUrl } = useFormatUrl(SecurityPageName.case); - const userPermissions = useGetUserSavedObjectPermissions(); + const userPermissions = useGetUserCasesPermissions(); const goToCreateCase = useCallback( (ev) => { ev.preventDefault(); diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index b88e805624ec61..31ef59ba29bc3c 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -237,7 +237,9 @@ export class Plugin implements IPlugin + ui: ['crud_cases', 'read_cases'], // uiCapabilities.siem.crud_cases cases: { all: [APP_ID], }, @@ -250,7 +252,9 @@ export class Plugin implements IPlugin + ui: ['read_cases'], // uiCapabilities.siem.read_cases cases: { read: [APP_ID], }, 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 50294201f6fbee..787ce533dbaf4d 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 @@ -15,6 +15,7 @@ import { ConnectorJiraTypeFields, CaseStatuses, CaseUserActionResponse, + CaseType, } from '../../../../../../plugins/cases/common/api'; import { getPostCaseRequest, postCaseResp, defaultUser } from '../../../../common/lib/mock'; import { @@ -151,6 +152,10 @@ export default ({ getService }: FtrProviderContext): void => { }); describe('unhappy path', () => { + it('should not allow creating a collection style case', async () => { + await createCase(supertest, getPostCaseRequest({ type: CaseType.collection }), 400); + }); + it('400s when bad query supplied', async () => { await supertest .post(CASES_URL) From 698e8e2a1c0d6e7f5ccea4df2ce828f489b15473 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Fri, 14 May 2021 16:32:40 -0400 Subject: [PATCH 061/113] Fixing type error --- x-pack/plugins/security_solution/server/endpoint/mocks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts index d8be1cc8de2002..ab679420f3db15 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts @@ -88,7 +88,7 @@ export const createMockEndpointAppContextServiceStartContract = (): jest.Mocked< exceptionListsClient: listMock.getExceptionListClient(), packagePolicyService: createPackagePolicyServiceMock(), cases: { - getCasesClientWithRequestAndContext: jest.fn(), + getCasesClientWithRequest: jest.fn(), }, }; }; From 71225eb8f99d9d14f65a79dbbc98d21f954d8f5f Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Mon, 17 May 2021 10:47:50 -0400 Subject: [PATCH 062/113] Adding some comments --- .../plugins/cases/server/authorization/audit_logger.ts | 9 +++++++++ .../cases/server/authorization/authorization.ts | 10 ++++++++++ x-pack/plugins/cases/server/authorization/types.ts | 9 +++++++++ x-pack/plugins/cases/server/client/factory.ts | 4 ++++ 4 files changed, 32 insertions(+) diff --git a/x-pack/plugins/cases/server/authorization/audit_logger.ts b/x-pack/plugins/cases/server/authorization/audit_logger.ts index 216cf7d9c20e00..82f9f6efdc11e3 100644 --- a/x-pack/plugins/cases/server/authorization/audit_logger.ts +++ b/x-pack/plugins/cases/server/authorization/audit_logger.ts @@ -13,6 +13,9 @@ enum AuthorizationResult { Authorized = 'Authorized', } +/** + * Audit logger for authorization operations + */ export class AuthorizationAuditLogger { private readonly auditLogger?: AuditLogger; @@ -63,6 +66,9 @@ export class AuthorizationAuditLogger { }); } + /** + * Creates a audit message describing a failure to authorize + */ public failure({ username, owners, @@ -95,6 +101,9 @@ export class AuthorizationAuditLogger { return message; } + /** + * Creates a audit message describing a successful authorization + */ public success({ username, operation, diff --git a/x-pack/plugins/cases/server/authorization/authorization.ts b/x-pack/plugins/cases/server/authorization/authorization.ts index adb684c60a1bd2..31e99392ae31b7 100644 --- a/x-pack/plugins/cases/server/authorization/authorization.ts +++ b/x-pack/plugins/cases/server/authorization/authorization.ts @@ -79,6 +79,13 @@ export class Authorization { return this.securityAuth?.mode?.useRbacForRequest(this.request) ?? false; } + /** + * Checks that the user making the request for the passed in owners and operation has the correct authorization. This + * function will throw if the user is not authorized for the requested operation and owners. + * + * @param owners an array of strings describing the case owners attempting to be authorized + * @param operation information describing the operation attempting to be authorized + */ public async ensureAuthorized(owners: string[], operation: OperationDetails) { const { securityAuth } = this; const areAllOwnersAvailable = owners.every((owner) => this.featureCaseOwners.has(owner)); @@ -116,6 +123,9 @@ export class Authorization { // else security is disabled so let the operation proceed } + /** + * Returns an object to filter the saved object find request to the authorized owners of an entity. + */ public async getFindAuthorizationFilter( operation: OperationDetails ): Promise { diff --git a/x-pack/plugins/cases/server/authorization/types.ts b/x-pack/plugins/cases/server/authorization/types.ts index c50eba7549c42a..8d0ec93b33b038 100644 --- a/x-pack/plugins/cases/server/authorization/types.ts +++ b/x-pack/plugins/cases/server/authorization/types.ts @@ -94,7 +94,16 @@ export interface OperationDetails { * Defines the helper methods and necessary information for authorizing the find API's request. */ export interface AuthorizationFilter { + /** + * The owner filter to pass to the saved object client's find operation that is scoped to the authorized owners + */ filter?: KueryNode; + /** + * Utility function for checking that the returned entities are in fact authorized for the user making the request + */ ensureSavedObjectIsAuthorized: (owner: string) => void; + /** + * Logs a successful audit message for the request + */ logSuccessfulAuthorization: () => void; } diff --git a/x-pack/plugins/cases/server/client/factory.ts b/x-pack/plugins/cases/server/client/factory.ts index 86e979fc32647b..bd049bcd703952 100644 --- a/x-pack/plugins/cases/server/client/factory.ts +++ b/x-pack/plugins/cases/server/client/factory.ts @@ -60,6 +60,10 @@ export class CasesClientFactory { this.options = options; } + /** + * Creates a cases client for the current request. This request will be used to authorize the operations done through + * the client. + */ public async create({ request, scopedClusterClient, From f7a816bee8479199202f61c60a86598639e2ab5f Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 18 May 2021 13:25:30 +0300 Subject: [PATCH 063/113] Validate cases features --- .../features/server/feature_registry.test.ts | 174 ++++++++++++++++++ .../plugins/features/server/feature_schema.ts | 33 +++- 2 files changed, 206 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/features/server/feature_registry.test.ts b/x-pack/plugins/features/server/feature_registry.test.ts index eb9b35cc644a7d..201c454d697dfd 100644 --- a/x-pack/plugins/features/server/feature_registry.test.ts +++ b/x-pack/plugins/features/server/feature_registry.test.ts @@ -1001,6 +1001,180 @@ describe('FeatureRegistry', () => { ); }); + it(`prevents privileges from specifying cases entries that don't exist at the root level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + category: { id: 'foo', label: 'foo' }, + cases: ['bar'], + privileges: { + all: { + cases: { + all: ['foo', 'bar'], + read: ['baz'], + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + read: { + cases: { read: ['foo', 'bar', 'baz'] }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.all has unknown cases entries: foo, baz"` + ); + }); + + it(`prevents features from specifying cases entries that don't exist at the privilege level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + category: { id: 'foo', label: 'foo' }, + cases: ['foo', 'bar', 'baz'], + privileges: { + all: { + cases: { all: ['foo'] }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + read: { + cases: { all: ['foo'] }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + subFeatures: [ + { + name: 'my sub feature', + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'cool-sub-feature-privilege', + name: 'cool privilege', + includeIn: 'none', + savedObject: { + all: [], + read: [], + }, + ui: [], + cases: { all: ['bar'] }, + }, + ], + }, + ], + }, + ], + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature test-feature specifies cases entries which are not granted to any privileges: baz"` + ); + }); + + it(`prevents reserved privileges from specifying cases entries that don't exist at the root level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + category: { id: 'foo', label: 'foo' }, + cases: ['bar'], + privileges: null, + reserved: { + description: 'something', + privileges: [ + { + id: 'reserved', + privilege: { + cases: { all: ['foo', 'bar', 'baz'] }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + ], + }, + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.reserved has unknown cases entries: foo, baz"` + ); + }); + + it(`prevents features from specifying cases entries that don't exist at the reserved privilege level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + category: { id: 'foo', label: 'foo' }, + cases: ['foo', 'bar', 'baz'], + privileges: null, + reserved: { + description: 'something', + privileges: [ + { + id: 'reserved', + privilege: { + cases: { all: ['foo', 'bar'] }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + ], + }, + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature test-feature specifies cases entries which are not granted to any privileges: baz"` + ); + }); + it(`prevents privileges from specifying management sections that don't exist at the root level`, () => { const feature: KibanaFeatureConfig = { id: 'test-feature', diff --git a/x-pack/plugins/features/server/feature_schema.ts b/x-pack/plugins/features/server/feature_schema.ts index e3525f82607e7a..11ea0bcfed0198 100644 --- a/x-pack/plugins/features/server/feature_schema.ts +++ b/x-pack/plugins/features/server/feature_schema.ts @@ -171,7 +171,7 @@ export function validateKibanaFeature(feature: KibanaFeatureConfig) { throw validateResult.error; } // the following validation can't be enforced by the Joi schema, since it'd require us looking "up" the object graph for the list of valid value, which they explicitly forbid. - const { app = [], management = {}, catalogue = [], alerting = [] } = feature; + const { app = [], management = {}, catalogue = [], alerting = [], cases = [] } = feature; const unseenApps = new Set(app); @@ -186,6 +186,8 @@ export function validateKibanaFeature(feature: KibanaFeatureConfig) { const unseenAlertTypes = new Set(alerting); + const unseenCasesTypes = new Set(cases); + function validateAppEntry(privilegeId: string, entry: readonly string[] = []) { entry.forEach((privilegeApp) => unseenApps.delete(privilegeApp)); @@ -229,6 +231,23 @@ export function validateKibanaFeature(feature: KibanaFeatureConfig) { } } + function validateCasesEntry(privilegeId: string, entry: FeatureKibanaPrivileges['cases']) { + const all = entry?.all ?? []; + const read = entry?.read ?? []; + + all.forEach((privilegeCasesTypes) => unseenCasesTypes.delete(privilegeCasesTypes)); + read.forEach((privilegeCasesTypes) => unseenCasesTypes.delete(privilegeCasesTypes)); + + const unknownCasesEntries = difference([...all, ...read], cases); + if (unknownCasesEntries.length > 0) { + throw new Error( + `Feature privilege ${ + feature.id + }.${privilegeId} has unknown cases entries: ${unknownCasesEntries.join(', ')}` + ); + } + } + function validateManagementEntry( privilegeId: string, managementEntry: Record = {} @@ -290,6 +309,7 @@ export function validateKibanaFeature(feature: KibanaFeatureConfig) { validateManagementEntry(privilegeId, privilegeDefinition.management); validateAlertingEntry(privilegeId, privilegeDefinition.alerting); + validateCasesEntry(privilegeId, privilegeDefinition.cases); }); const subFeatureEntries = feature.subFeatures ?? []; @@ -300,6 +320,7 @@ export function validateKibanaFeature(feature: KibanaFeatureConfig) { validateCatalogueEntry(subFeaturePrivilege.id, subFeaturePrivilege.catalogue); validateManagementEntry(subFeaturePrivilege.id, subFeaturePrivilege.management); validateAlertingEntry(subFeaturePrivilege.id, subFeaturePrivilege.alerting); + validateCasesEntry(subFeaturePrivilege.id, subFeaturePrivilege.cases); }); }); }); @@ -350,6 +371,16 @@ export function validateKibanaFeature(feature: KibanaFeatureConfig) { ).join(',')}` ); } + + if (unseenCasesTypes.size > 0) { + throw new Error( + `Feature ${ + feature.id + } specifies cases entries which are not granted to any privileges: ${Array.from( + unseenCasesTypes.values() + ).join(',')}` + ); + } } export function validateElasticsearchFeature(feature: ElasticsearchFeatureConfig) { From fd39b255fe5e3cdc8639b4b7e7a0216c4bcd54a0 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 18 May 2021 16:18:20 +0300 Subject: [PATCH 064/113] Fix new schema --- x-pack/plugins/features/server/feature_schema.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/features/server/feature_schema.ts b/x-pack/plugins/features/server/feature_schema.ts index e72ebc7bf42975..d27151f06bb533 100644 --- a/x-pack/plugins/features/server/feature_schema.ts +++ b/x-pack/plugins/features/server/feature_schema.ts @@ -85,6 +85,12 @@ const kibanaPrivilegeSchema = schema.object({ read: schema.maybe(alertingSchema), }) ), + cases: schema.maybe( + schema.object({ + all: schema.maybe(casesSchema), + read: schema.maybe(casesSchema), + }) + ), savedObject: schema.object({ all: schema.arrayOf(schema.string()), read: schema.arrayOf(schema.string()), @@ -113,8 +119,8 @@ const kibanaIndependentSubFeaturePrivilegeSchema = schema.object({ ), cases: schema.maybe( schema.object({ - all: schema.maybe(alertingSchema), - read: schema.maybe(alertingSchema), + all: schema.maybe(casesSchema), + read: schema.maybe(casesSchema), }) ), api: schema.maybe(schema.arrayOf(schema.string())), From e3d3e8c2540cd8c41477c45f36b50efddbb12077 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 18 May 2021 12:43:23 -0600 Subject: [PATCH 065/113] Cases routing --- x-pack/plugins/observability/kibana.json | 3 +- .../components/app/cases/__mock__/form.ts | 52 ++++ .../components/app/cases/__mock__/router.ts | 40 +++ .../components/app/cases/all_cases/index.tsx | 79 ++++++ .../app/cases/callout/callout.test.tsx | 90 ++++++ .../components/app/cases/callout/callout.tsx | 54 ++++ .../app/cases/callout/helpers.test.tsx | 29 ++ .../components/app/cases/callout/helpers.tsx | 22 ++ .../app/cases/callout/index.test.tsx | 217 +++++++++++++++ .../components/app/cases/callout/index.tsx | 103 +++++++ .../app/cases/callout/translations.ts | 30 ++ .../components/app/cases/callout/types.ts | 13 + .../app/cases/case_view/helpers.test.tsx | 27 ++ .../components/app/cases/case_view/helpers.ts | 131 +++++++++ .../components/app/cases/case_view/index.tsx | 256 ++++++++++++++++++ .../app/cases/case_view/translations.ts | 14 + .../public/components/app/cases/constants.ts | 9 + .../app/cases/create/flyout.test.tsx | 56 ++++ .../components/app/cases/create/flyout.tsx | 78 ++++++ .../app/cases/create/index.test.tsx | 120 ++++++++ .../components/app/cases/create/index.tsx | 53 ++++ .../components/app/cases/wrappers/index.tsx | 21 ++ .../public/pages/cases/all_cases.tsx | 37 +++ .../public/pages/cases/case_details.tsx | 14 + .../public/pages/cases/configure_cases.tsx | 14 + .../public/pages/cases/create_case.tsx | 14 + .../public/pages/cases/empty_page.tsx | 118 ++++++++ .../public/pages/cases/index.tsx | 49 ---- .../observability/public/pages/cases/links.ts | 56 ++++ .../cases/saved_object_no_permissions.tsx | 38 +++ .../public/pages/cases/translations.ts | 216 +++++++++++++++ x-pack/plugins/observability/public/plugin.ts | 6 +- .../observability/public/routes/index.tsx | 47 +++- .../public/utils/kibana_react.ts | 13 + x-pack/plugins/observability/tsconfig.json | 1 + 35 files changed, 2061 insertions(+), 59 deletions(-) create mode 100644 x-pack/plugins/observability/public/components/app/cases/__mock__/form.ts create mode 100644 x-pack/plugins/observability/public/components/app/cases/__mock__/router.ts create mode 100644 x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx create mode 100644 x-pack/plugins/observability/public/components/app/cases/callout/callout.test.tsx create mode 100644 x-pack/plugins/observability/public/components/app/cases/callout/callout.tsx create mode 100644 x-pack/plugins/observability/public/components/app/cases/callout/helpers.test.tsx create mode 100644 x-pack/plugins/observability/public/components/app/cases/callout/helpers.tsx create mode 100644 x-pack/plugins/observability/public/components/app/cases/callout/index.test.tsx create mode 100644 x-pack/plugins/observability/public/components/app/cases/callout/index.tsx create mode 100644 x-pack/plugins/observability/public/components/app/cases/callout/translations.ts create mode 100644 x-pack/plugins/observability/public/components/app/cases/callout/types.ts create mode 100644 x-pack/plugins/observability/public/components/app/cases/case_view/helpers.test.tsx create mode 100644 x-pack/plugins/observability/public/components/app/cases/case_view/helpers.ts create mode 100644 x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx create mode 100644 x-pack/plugins/observability/public/components/app/cases/case_view/translations.ts create mode 100644 x-pack/plugins/observability/public/components/app/cases/constants.ts create mode 100644 x-pack/plugins/observability/public/components/app/cases/create/flyout.test.tsx create mode 100644 x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx create mode 100644 x-pack/plugins/observability/public/components/app/cases/create/index.test.tsx create mode 100644 x-pack/plugins/observability/public/components/app/cases/create/index.tsx create mode 100644 x-pack/plugins/observability/public/components/app/cases/wrappers/index.tsx create mode 100644 x-pack/plugins/observability/public/pages/cases/all_cases.tsx create mode 100644 x-pack/plugins/observability/public/pages/cases/case_details.tsx create mode 100644 x-pack/plugins/observability/public/pages/cases/configure_cases.tsx create mode 100644 x-pack/plugins/observability/public/pages/cases/create_case.tsx create mode 100644 x-pack/plugins/observability/public/pages/cases/empty_page.tsx delete mode 100644 x-pack/plugins/observability/public/pages/cases/index.tsx create mode 100644 x-pack/plugins/observability/public/pages/cases/links.ts create mode 100644 x-pack/plugins/observability/public/pages/cases/saved_object_no_permissions.tsx create mode 100644 x-pack/plugins/observability/public/pages/cases/translations.ts create mode 100644 x-pack/plugins/observability/public/utils/kibana_react.ts diff --git a/x-pack/plugins/observability/kibana.json b/x-pack/plugins/observability/kibana.json index 52d5493ae69a49..7f48d13b4945b8 100644 --- a/x-pack/plugins/observability/kibana.json +++ b/x-pack/plugins/observability/kibana.json @@ -16,7 +16,8 @@ "data", "alerting", "ruleRegistry", - "triggersActionsUi" + "triggersActionsUi", + "cases" ], "ui": true, "server": true, diff --git a/x-pack/plugins/observability/public/components/app/cases/__mock__/form.ts b/x-pack/plugins/observability/public/components/app/cases/__mock__/form.ts new file mode 100644 index 00000000000000..17bbcd8cdea64d --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/__mock__/form.ts @@ -0,0 +1,52 @@ +/* + * 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 { useForm } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'; +import { useFormData } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data'; + +jest.mock( + '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' +); +jest.mock( + '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data' +); + +export const mockFormHook = { + isSubmitted: false, + isSubmitting: false, + isValid: true, + submit: jest.fn(), + subscribe: jest.fn(), + setFieldValue: jest.fn(), + setFieldErrors: jest.fn(), + getFields: jest.fn(), + getFormData: jest.fn(), + /* Returns a list of all errors in the form */ + getErrors: jest.fn(), + reset: jest.fn(), + __options: {}, + __formData$: {}, + __addField: jest.fn(), + __removeField: jest.fn(), + __validateFields: jest.fn(), + __updateFormDataAt: jest.fn(), + __readFieldConfigFromSchema: jest.fn(), + __getFieldDefaultValue: jest.fn(), +}; + +export const getFormMock = (sampleData: any) => ({ + ...mockFormHook, + submit: () => + Promise.resolve({ + data: sampleData, + isValid: true, + }), + getFormData: () => sampleData, +}); + +export const useFormMock = useForm as jest.Mock; +export const useFormDataMock = useFormData as jest.Mock; diff --git a/x-pack/plugins/observability/public/components/app/cases/__mock__/router.ts b/x-pack/plugins/observability/public/components/app/cases/__mock__/router.ts new file mode 100644 index 00000000000000..58b7bb0ac26883 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/__mock__/router.ts @@ -0,0 +1,40 @@ +/* + * 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 { Router } from 'react-router-dom'; +// eslint-disable-next-line @kbn/eslint/module_migration +import routeData from 'react-router'; +type Action = 'PUSH' | 'POP' | 'REPLACE'; +const pop: Action = 'POP'; +const location = { + pathname: '/network', + search: '', + state: '', + hash: '', +}; +export const mockHistory = { + length: 2, + location, + action: pop, + push: jest.fn(), + replace: jest.fn(), + go: jest.fn(), + goBack: jest.fn(), + goForward: jest.fn(), + block: jest.fn(), + createHref: jest.fn(), + listen: jest.fn(), +}; + +export const mockLocation = { + pathname: '/welcome', + hash: '', + search: '', + state: '', +}; + +export { Router, routeData }; diff --git a/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx b/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx new file mode 100644 index 00000000000000..c6692cace8a6b1 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx @@ -0,0 +1,79 @@ +/* + * 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 React, { useCallback } from 'react'; +import { useHistory } from 'react-router-dom'; + +import { + getCaseDetailsUrl, + getConfigureCasesUrl, + getCreateCaseUrl, + useFormatUrl, +} from '../../../../pages/cases/links'; +import { useKibana } from '../../../../utils/kibana_react'; +import { CASES_APP_ID } from '../constants'; + +export interface AllCasesNavProps { + detailName: string; + search?: string; + subCaseId?: string; +} + +interface AllCasesProps { + userCanCrud: boolean; +} +export const AllCases = React.memo(({ userCanCrud }) => { + const { + cases: casesUi, + application: { navigateToApp }, + } = useKibana().services; + const history = useHistory(); + const { formatUrl } = useFormatUrl(); + + const goToCreateCase = useCallback( + (ev) => { + ev.preventDefault(); + navigateToApp(`${CASES_APP_ID}`, { + path: getCreateCaseUrl(), + }); + }, + [navigateToApp] + ); + + const goToCaseConfigure = useCallback( + (ev) => { + ev.preventDefault(); + history.push(getConfigureCasesUrl()); + }, + [history] + ); + + return casesUi.getAllCases({ + caseDetailsNavigation: { + href: ({ detailName, subCaseId }: AllCasesNavProps) => { + return formatUrl(getCaseDetailsUrl({ id: detailName, subCaseId })); + }, + onClick: ({ detailName, subCaseId, search }: AllCasesNavProps) => { + navigateToApp(`${CASES_APP_ID}`, { + path: getCaseDetailsUrl({ id: detailName, subCaseId }), + }); + }, + }, + configureCasesNavigation: { + href: formatUrl(getConfigureCasesUrl()), + onClick: goToCaseConfigure, + }, + createCaseNavigation: { + href: formatUrl(getCreateCaseUrl()), + onClick: goToCreateCase, + }, + userCanCrud, + owner: [CASES_APP_ID], + }); +}); + +AllCases.displayName = 'AllCases'; diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/callout.test.tsx b/x-pack/plugins/observability/public/components/app/cases/callout/callout.test.tsx new file mode 100644 index 00000000000000..926fe7b63fb5af --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/callout/callout.test.tsx @@ -0,0 +1,90 @@ +/* + * 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 React from 'react'; +import { mount } from 'enzyme'; + +import { CallOut, CallOutProps } from './callout'; + +describe('Callout', () => { + const defaultProps: CallOutProps = { + id: 'md5-hex', + type: 'primary', + title: 'a tittle', + messages: [ + { + id: 'generic-error', + title: 'message-one', + description:

{'error'}

, + }, + ], + showCallOut: true, + handleDismissCallout: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('It renders the callout', () => { + const wrapper = mount(); + expect(wrapper.find(`[data-test-subj="case-callout-md5-hex"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="callout-messages-md5-hex"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="callout-dismiss-md5-hex"]`).exists()).toBeTruthy(); + }); + + it('hides the callout', () => { + const wrapper = mount(); + expect(wrapper.find(`[data-test-subj="case-callout-md5-hex"]`).exists()).toBeFalsy(); + }); + + it('does not shows any messages when the list is empty', () => { + const wrapper = mount(); + expect(wrapper.find(`[data-test-subj="callout-messages-md5-hex"]`).exists()).toBeFalsy(); + }); + + it('transform the button color correctly - primary', () => { + const wrapper = mount(); + const className = + wrapper.find(`button[data-test-subj="callout-dismiss-md5-hex"]`).first().prop('className') ?? + ''; + expect(className.includes('euiButton--primary')).toBeTruthy(); + }); + + it('transform the button color correctly - success', () => { + const wrapper = mount(); + const className = + wrapper.find(`button[data-test-subj="callout-dismiss-md5-hex"]`).first().prop('className') ?? + ''; + expect(className.includes('euiButton--secondary')).toBeTruthy(); + }); + + it('transform the button color correctly - warning', () => { + const wrapper = mount(); + const className = + wrapper.find(`button[data-test-subj="callout-dismiss-md5-hex"]`).first().prop('className') ?? + ''; + expect(className.includes('euiButton--warning')).toBeTruthy(); + }); + + it('transform the button color correctly - danger', () => { + const wrapper = mount(); + const className = + wrapper.find(`button[data-test-subj="callout-dismiss-md5-hex"]`).first().prop('className') ?? + ''; + expect(className.includes('euiButton--danger')).toBeTruthy(); + }); + + it('dismiss the callout correctly', () => { + const wrapper = mount(); + expect(wrapper.find(`[data-test-subj="callout-dismiss-md5-hex"]`).exists()).toBeTruthy(); + wrapper.find(`button[data-test-subj="callout-dismiss-md5-hex"]`).simulate('click'); + wrapper.update(); + + expect(defaultProps.handleDismissCallout).toHaveBeenCalledWith('md5-hex', 'primary'); + }); +}); diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/callout.tsx b/x-pack/plugins/observability/public/components/app/cases/callout/callout.tsx new file mode 100644 index 00000000000000..6e108c81d79622 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/callout/callout.tsx @@ -0,0 +1,54 @@ +/* + * 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 { EuiCallOut, EuiButton, EuiDescriptionList } from '@elastic/eui'; +import { isEmpty } from 'lodash/fp'; +import React, { memo, useCallback } from 'react'; + +import { ErrorMessage } from './types'; +import * as i18n from './translations'; + +export interface CallOutProps { + id: string; + type: NonNullable; + title: string; + messages: ErrorMessage[]; + showCallOut: boolean; + handleDismissCallout: (id: string, type: NonNullable) => void; +} + +function CallOutComponent({ + id, + type, + title, + messages, + showCallOut, + handleDismissCallout, +}: CallOutProps) { + const handleCallOut = useCallback(() => handleDismissCallout(id, type), [ + handleDismissCallout, + id, + type, + ]); + + return showCallOut ? ( + + {!isEmpty(messages) && ( + + )} + + {i18n.DISMISS_CALLOUT} + + + ) : null; +} + +export const CallOut = memo(CallOutComponent); diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/helpers.test.tsx b/x-pack/plugins/observability/public/components/app/cases/callout/helpers.test.tsx new file mode 100644 index 00000000000000..b5b92a33748742 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/callout/helpers.test.tsx @@ -0,0 +1,29 @@ +/* + * 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 md5 from 'md5'; +import { createCalloutId } from './helpers'; + +describe('createCalloutId', () => { + it('creates id correctly with one id', () => { + const digest = md5('one'); + const id = createCalloutId(['one']); + expect(id).toBe(digest); + }); + + it('creates id correctly with multiples ids', () => { + const digest = md5('one|two|three'); + const id = createCalloutId(['one', 'two', 'three']); + expect(id).toBe(digest); + }); + + it('creates id correctly with multiples ids and delimiter', () => { + const digest = md5('one,two,three'); + const id = createCalloutId(['one', 'two', 'three'], ','); + expect(id).toBe(digest); + }); +}); diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/helpers.tsx b/x-pack/plugins/observability/public/components/app/cases/callout/helpers.tsx new file mode 100644 index 00000000000000..2a7804579a57e8 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/callout/helpers.tsx @@ -0,0 +1,22 @@ +/* + * 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 React from 'react'; +import md5 from 'md5'; + +import * as i18n from './translations'; +import { ErrorMessage } from './types'; + +export const savedObjectReadOnlyErrorMessage: ErrorMessage = { + id: 'read-only-privileges-error', + title: i18n.READ_ONLY_SAVED_OBJECT_TITLE, + description: <>{i18n.READ_ONLY_SAVED_OBJECT_MSG}, + errorType: 'warning', +}; + +export const createCalloutId = (ids: string[], delimiter: string = '|'): string => + md5(ids.join(delimiter)); diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/index.test.tsx b/x-pack/plugins/observability/public/components/app/cases/callout/index.test.tsx new file mode 100644 index 00000000000000..5d1daf0c2f7bb1 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/callout/index.test.tsx @@ -0,0 +1,217 @@ +/* + * 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 React from 'react'; +import { mount } from 'enzyme'; + +import { useMessagesStorage } from '../../../common/containers/local_storage/use_messages_storage'; +import { TestProviders } from '../../../common/mock'; +import { createCalloutId } from './helpers'; +import { CaseCallOut, CaseCallOutProps } from '.'; + +jest.mock('../../../common/containers/local_storage/use_messages_storage'); + +const useSecurityLocalStorageMock = useMessagesStorage as jest.Mock; +const securityLocalStorageMock = { + getMessages: jest.fn(() => []), + addMessage: jest.fn(), +}; + +describe('CaseCallOut ', () => { + beforeEach(() => { + jest.clearAllMocks(); + useSecurityLocalStorageMock.mockImplementation(() => securityLocalStorageMock); + }); + + it('renders a callout correctly', () => { + const props: CaseCallOutProps = { + title: 'hey title', + messages: [ + { id: 'message-one', title: 'title', description:

{'we have two messages'}

}, + { id: 'message-two', title: 'title', description:

{'for real'}

}, + ], + }; + const wrapper = mount( + + + + ); + + const id = createCalloutId(['message-one', 'message-two']); + expect(wrapper.find(`[data-test-subj="callout-messages-${id}"]`).last().exists()).toBeTruthy(); + }); + + it('groups the messages correctly', () => { + const props: CaseCallOutProps = { + title: 'hey title', + messages: [ + { + id: 'message-one', + title: 'title one', + description:

{'we have two messages'}

, + errorType: 'danger', + }, + { id: 'message-two', title: 'title two', description:

{'for real'}

}, + ], + }; + + const wrapper = mount( + + + + ); + + const idDanger = createCalloutId(['message-one']); + const idPrimary = createCalloutId(['message-two']); + + expect( + wrapper.find(`[data-test-subj="case-callout-${idPrimary}"]`).last().exists() + ).toBeTruthy(); + expect( + wrapper.find(`[data-test-subj="case-callout-${idDanger}"]`).last().exists() + ).toBeTruthy(); + }); + + it('dismisses the callout correctly', () => { + const props: CaseCallOutProps = { + title: 'hey title', + messages: [ + { id: 'message-one', title: 'title', description:

{'we have two messages'}

}, + ], + }; + const wrapper = mount( + + + + ); + + const id = createCalloutId(['message-one']); + + expect(wrapper.find(`[data-test-subj="case-callout-${id}"]`).last().exists()).toBeTruthy(); + wrapper.find(`[data-test-subj="callout-dismiss-${id}"]`).last().simulate('click'); + expect(wrapper.find(`[data-test-subj="case-callout-${id}"]`).exists()).toBeFalsy(); + }); + + it('persist the callout of type primary when dismissed', () => { + const props: CaseCallOutProps = { + title: 'hey title', + messages: [ + { id: 'message-one', title: 'title', description:

{'we have two messages'}

}, + ], + }; + + const wrapper = mount( + + + + ); + + const id = createCalloutId(['message-one']); + expect(securityLocalStorageMock.getMessages).toHaveBeenCalledWith('case'); + wrapper.find(`[data-test-subj="callout-dismiss-${id}"]`).last().simulate('click'); + expect(securityLocalStorageMock.addMessage).toHaveBeenCalledWith('case', id); + }); + + it('do not show the callout if is in the localStorage', () => { + const props: CaseCallOutProps = { + title: 'hey title', + messages: [ + { id: 'message-one', title: 'title', description:

{'we have two messages'}

}, + ], + }; + + const id = createCalloutId(['message-one']); + + useSecurityLocalStorageMock.mockImplementation(() => ({ + ...securityLocalStorageMock, + getMessages: jest.fn(() => [id]), + })); + + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="case-callout-${id}"]`).last().exists()).toBeFalsy(); + }); + + it('do not persist a callout of type danger', () => { + const props: CaseCallOutProps = { + title: 'hey title', + messages: [ + { + id: 'message-one', + title: 'title one', + description:

{'we have two messages'}

, + errorType: 'danger', + }, + ], + }; + + const wrapper = mount( + + + + ); + + const id = createCalloutId(['message-one']); + wrapper.find(`button[data-test-subj="callout-dismiss-${id}"]`).simulate('click'); + wrapper.update(); + expect(securityLocalStorageMock.addMessage).not.toHaveBeenCalled(); + }); + + it('do not persist a callout of type warning', () => { + const props: CaseCallOutProps = { + title: 'hey title', + messages: [ + { + id: 'message-one', + title: 'title one', + description:

{'we have two messages'}

, + errorType: 'warning', + }, + ], + }; + + const wrapper = mount( + + + + ); + + const id = createCalloutId(['message-one']); + wrapper.find(`button[data-test-subj="callout-dismiss-${id}"]`).simulate('click'); + wrapper.update(); + expect(securityLocalStorageMock.addMessage).not.toHaveBeenCalled(); + }); + + it('do not persist a callout of type success', () => { + const props: CaseCallOutProps = { + title: 'hey title', + messages: [ + { + id: 'message-one', + title: 'title one', + description:

{'we have two messages'}

, + errorType: 'success', + }, + ], + }; + + const wrapper = mount( + + + + ); + + const id = createCalloutId(['message-one']); + wrapper.find(`button[data-test-subj="callout-dismiss-${id}"]`).simulate('click'); + wrapper.update(); + expect(securityLocalStorageMock.addMessage).not.toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/index.tsx b/x-pack/plugins/observability/public/components/app/cases/callout/index.tsx new file mode 100644 index 00000000000000..a385e3a56197b2 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/callout/index.tsx @@ -0,0 +1,103 @@ +/* + * 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 { EuiSpacer } from '@elastic/eui'; +import React, { memo, useCallback, useState, useMemo } from 'react'; + +import { useMessagesStorage } from '../../../common/containers/local_storage/use_messages_storage'; +import { CallOut } from './callout'; +import { ErrorMessage } from './types'; +import { createCalloutId } from './helpers'; + +export * from './helpers'; + +export interface CaseCallOutProps { + title: string; + messages?: ErrorMessage[]; +} + +type GroupByTypeMessages = { + [key in NonNullable]: { + messagesId: string[]; + messages: ErrorMessage[]; + }; +}; + +interface CalloutVisibility { + [index: string]: boolean; +} + +function CaseCallOutComponent({ title, messages = [] }: CaseCallOutProps) { + const { getMessages, addMessage } = useMessagesStorage(); + + const caseMessages = useMemo(() => getMessages('case'), [getMessages]); + const dismissedCallouts = useMemo( + () => + caseMessages.reduce( + (acc, id) => ({ + ...acc, + [id]: false, + }), + {} + ), + [caseMessages] + ); + + const [calloutVisibility, setCalloutVisibility] = useState(dismissedCallouts); + const handleCallOut = useCallback( + (id, type) => { + setCalloutVisibility((prevState) => ({ ...prevState, [id]: false })); + if (type === 'primary') { + addMessage('case', id); + } + }, + [setCalloutVisibility, addMessage] + ); + + const groupedByTypeErrorMessages = useMemo( + () => + messages.reduce( + (acc: GroupByTypeMessages, currentMessage: ErrorMessage) => { + const type = currentMessage.errorType == null ? 'primary' : currentMessage.errorType; + return { + ...acc, + [type]: { + messagesId: [...(acc[type]?.messagesId ?? []), currentMessage.id], + messages: [...(acc[type]?.messages ?? []), currentMessage], + }, + }; + }, + {} as GroupByTypeMessages + ), + [messages] + ); + + return ( + <> + {(Object.keys(groupedByTypeErrorMessages) as Array).map( + (type: NonNullable) => { + const id = createCalloutId(groupedByTypeErrorMessages[type].messagesId); + return ( + + + + + ); + } + )} + + ); +} + +export const CaseCallOut = memo(CaseCallOutComponent); diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/translations.ts b/x-pack/plugins/observability/public/components/app/cases/callout/translations.ts new file mode 100644 index 00000000000000..4a5f32684ccdec --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/callout/translations.ts @@ -0,0 +1,30 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const READ_ONLY_SAVED_OBJECT_TITLE = i18n.translate( + 'xpack.securitySolution.cases.readOnlySavedObjectTitle', + { + defaultMessage: 'You cannot open new or update existing cases', + } +); + +export const READ_ONLY_SAVED_OBJECT_MSG = i18n.translate( + 'xpack.securitySolution.cases.readOnlySavedObjectDescription', + { + defaultMessage: + 'You only have privileges to view cases. If you need to open and update cases, contact your Kibana administrator.', + } +); + +export const DISMISS_CALLOUT = i18n.translate( + 'xpack.securitySolution.cases.dismissErrorsPushServiceCallOutTitle', + { + defaultMessage: 'Dismiss', + } +); diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/types.ts b/x-pack/plugins/observability/public/components/app/cases/callout/types.ts new file mode 100644 index 00000000000000..84d79ee391b8ff --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/callout/types.ts @@ -0,0 +1,13 @@ +/* + * 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 ErrorMessage { + id: string; + title: string; + description: JSX.Element; + errorType?: 'primary' | 'success' | 'warning' | 'danger'; +} diff --git a/x-pack/plugins/observability/public/components/app/cases/case_view/helpers.test.tsx b/x-pack/plugins/observability/public/components/app/cases/case_view/helpers.test.tsx new file mode 100644 index 00000000000000..5eb0a03fc5db7f --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/case_view/helpers.test.tsx @@ -0,0 +1,27 @@ +/* + * 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 { buildAlertsQuery } from './helpers'; + +describe('Case view helpers', () => { + describe('buildAlertsQuery', () => { + it('it builds the alerts query', () => { + expect(buildAlertsQuery(['alert-id-1', 'alert-id-2'])).toEqual({ + query: { + bool: { + filter: { + ids: { + values: ['alert-id-1', 'alert-id-2'], + }, + }, + }, + }, + size: 10000, + }); + }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/app/cases/case_view/helpers.ts b/x-pack/plugins/observability/public/components/app/cases/case_view/helpers.ts new file mode 100644 index 00000000000000..94813862a983a5 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/case_view/helpers.ts @@ -0,0 +1,131 @@ +/* + * 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 { isObject, get, isString, isNumber } from 'lodash'; +import { useMemo } from 'react'; +import { useSourcererScope } from '../../../common/containers/sourcerer'; +import { SourcererScopeName } from '../../../common/store/sourcerer/model'; +import { useQueryAlerts } from '../../../detections/containers/detection_engine/alerts/use_query'; +import { Ecs } from '../../../../../cases/common'; + +// TODO we need to allow -> docValueFields: [{ field: "@timestamp" }], +export const buildAlertsQuery = (alertIds: string[]) => { + if (alertIds.length === 0) { + return {}; + } + return { + query: { + bool: { + filter: { + ids: { + values: alertIds, + }, + }, + }, + }, + size: 10000, + }; +}; + +export const toStringArray = (value: unknown): string[] => { + if (Array.isArray(value)) { + return value.reduce((acc, v) => { + if (v != null) { + switch (typeof v) { + case 'number': + case 'boolean': + return [...acc, v.toString()]; + case 'object': + try { + return [...acc, JSON.stringify(v)]; + } catch { + return [...acc, 'Invalid Object']; + } + case 'string': + return [...acc, v]; + default: + return [...acc, `${v}`]; + } + } + return acc; + }, []); + } else if (value == null) { + return []; + } else if (!Array.isArray(value) && typeof value === 'object') { + try { + return [JSON.stringify(value)]; + } catch { + return ['Invalid Object']; + } + } else { + return [`${value}`]; + } +}; + +export const formatAlertToEcsSignal = (alert: {}): Ecs => + Object.keys(alert).reduce((accumulator, key) => { + const item = get(alert, key); + if (item != null && isObject(item)) { + return { ...accumulator, [key]: formatAlertToEcsSignal(item) }; + } else if (Array.isArray(item) || isString(item) || isNumber(item)) { + return { ...accumulator, [key]: toStringArray(item) }; + } + return accumulator; + }, {} as Ecs); +interface Signal { + rule: { + id: string; + name: string; + to: string; + from: string; + }; +} + +interface SignalHit { + _id: string; + _index: string; + _source: { + '@timestamp': string; + signal: Signal; + }; +} + +export interface Alert { + _id: string; + _index: string; + '@timestamp': string; + signal: Signal; + [key: string]: unknown; +} +export const useFetchAlertData = (alertIds: string[]): [boolean, Record] => { + const { selectedPatterns } = useSourcererScope(SourcererScopeName.detections); + const alertsQuery = useMemo(() => buildAlertsQuery(alertIds), [alertIds]); + + const { loading: isLoadingAlerts, data: alertsData } = useQueryAlerts({ + query: alertsQuery, + indexName: selectedPatterns[0], + }); + + const alerts = useMemo( + () => + alertsData?.hits.hits.reduce>( + (acc, { _id, _index, _source }) => ({ + ...acc, + [_id]: { + ...formatAlertToEcsSignal(_source), + _id, + _index, + timestamp: _source['@timestamp'], + }, + }), + {} + ) ?? {}, + [alertsData?.hits.hits] + ); + + return [isLoadingAlerts, alerts]; +}; diff --git a/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx b/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx new file mode 100644 index 00000000000000..f61bf93c0e6408 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx @@ -0,0 +1,256 @@ +/* + * 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 React, { useCallback, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { useHistory } from 'react-router-dom'; +import { SearchResponse } from 'elasticsearch'; +import { isEmpty } from 'lodash'; + +import { + getCaseDetailsUrl, + getCaseDetailsUrlWithCommentId, + getCaseUrl, + getConfigureCasesUrl, + getRuleDetailsUrl, + useFormatUrl, +} from '../../../common/components/link_to'; +import { Ecs } from '../../../../common/ecs'; +import { Case } from '../../../../../cases/common'; +import { TimelineNonEcsData } from '../../../../common/search_strategy'; +import { TimelineId } from '../../../../common/types/timeline'; +import { SecurityPageName } from '../../../app/types'; +import { KibanaServices, useKibana } from '../../../common/lib/kibana'; +import { APP_ID, DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../common/constants'; +import { timelineActions } from '../../../timelines/store/timeline'; +import { useSourcererScope } from '../../../common/containers/sourcerer'; +import { SourcererScopeName } from '../../../common/store/sourcerer/model'; +import { DetailsPanel } from '../../../timelines/components/side_panel'; +import { InvestigateInTimelineAction } from '../../../detections/components/alerts_table/timeline_actions/investigate_in_timeline_action'; +import { buildAlertsQuery, formatAlertToEcsSignal, useFetchAlertData } from './helpers'; +import { SEND_ALERT_TO_TIMELINE } from './translations'; +import { useInsertTimeline } from '../use_insert_timeline'; +import { SpyRoute } from '../../../common/utils/route/spy_routes'; +import * as timelineMarkdownPlugin from '../../../common/components/markdown_editor/plugins/timeline'; + +interface Props { + caseId: string; + subCaseId?: string; + userCanCrud: boolean; +} + +export interface OnUpdateFields { + key: keyof Case; + value: Case[keyof Case]; + onSuccess?: () => void; + onError?: () => void; +} + +export interface CaseProps extends Props { + fetchCase: () => void; + caseData: Case; + updateCase: (newCase: Case) => void; +} + +function TimelineDetailsPanel() { + const { browserFields, docValueFields } = useSourcererScope(SourcererScopeName.detections); + + return ( + + ); +} + +function InvestigateInTimelineActionComponent(alertIds: string[]) { + const EMPTY_ARRAY: TimelineNonEcsData[] = []; + const fetchEcsAlertsData = async (fetchAlertIds?: string[]): Promise => { + if (isEmpty(fetchAlertIds)) { + return []; + } + const alertResponse = await KibanaServices.get().http.fetch< + SearchResponse<{ '@timestamp': string; [key: string]: unknown }> + >(DETECTION_ENGINE_QUERY_SIGNALS_URL, { + method: 'POST', + body: JSON.stringify(buildAlertsQuery(fetchAlertIds ?? [])), + }); + return ( + alertResponse?.hits.hits.reduce( + (acc, { _id, _index, _source }) => [ + ...acc, + { + ...formatAlertToEcsSignal(_source as {}), + _id, + _index, + timestamp: _source['@timestamp'], + }, + ], + [] + ) ?? [] + ); + }; + + return ( + + ); +} + +export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) => { + const [spyState, setSpyState] = useState<{ caseTitle: string | undefined }>({ + caseTitle: undefined, + }); + + const onCaseDataSuccess = useCallback( + (data: Case) => { + if (spyState.caseTitle === undefined) { + setSpyState({ caseTitle: data.title }); + } + }, + [spyState.caseTitle] + ); + + const { + cases: casesUi, + application: { navigateToApp }, + } = useKibana().services; + const history = useHistory(); + const dispatch = useDispatch(); + const { formatUrl, search } = useFormatUrl(SecurityPageName.case); + const { formatUrl: detectionsFormatUrl, search: detectionsUrlSearch } = useFormatUrl( + SecurityPageName.detections + ); + + const allCasesLink = getCaseUrl(search); + const formattedAllCasesLink = formatUrl(allCasesLink); + const backToAllCasesOnClick = useCallback( + (ev) => { + ev.preventDefault(); + history.push(allCasesLink); + }, + [allCasesLink, history] + ); + const caseDetailsLink = formatUrl(getCaseDetailsUrl({ id: caseId }), { absolute: true }); + const getCaseDetailHrefWithCommentId = (commentId: string) => { + return formatUrl(getCaseDetailsUrlWithCommentId({ id: caseId, commentId, subCaseId }), { + absolute: true, + }); + }; + + const configureCasesHref = formatUrl(getConfigureCasesUrl()); + const onConfigureCasesNavClick = useCallback( + (ev) => { + ev.preventDefault(); + history.push(getConfigureCasesUrl(search)); + }, + [history, search] + ); + + const onDetectionsRuleDetailsClick = useCallback( + (ruleId: string | null | undefined) => { + navigateToApp(`${APP_ID}:${SecurityPageName.detections}`, { + path: getRuleDetailsUrl(ruleId ?? ''), + }); + }, + [navigateToApp] + ); + + const getDetectionsRuleDetailsHref = useCallback( + (ruleId) => { + return detectionsFormatUrl(getRuleDetailsUrl(ruleId ?? '', detectionsUrlSearch)); + }, + [detectionsFormatUrl, detectionsUrlSearch] + ); + + const showAlertDetails = useCallback( + (alertId: string, index: string) => { + dispatch( + timelineActions.toggleDetailPanel({ + panelView: 'eventDetail', + timelineId: TimelineId.casePage, + params: { + eventId: alertId, + indexName: index, + }, + }) + ); + }, + [dispatch] + ); + + const onComponentInitialized = useCallback(() => { + dispatch( + timelineActions.createTimeline({ + id: TimelineId.casePage, + columns: [], + indexNames: [], + expandedDetail: {}, + show: false, + }) + ); + }, [dispatch]); + return ( + <> + {casesUi.getCaseView({ + allCasesNavigation: { + href: formattedAllCasesLink, + onClick: backToAllCasesOnClick, + }, + caseDetailsNavigation: { + href: caseDetailsLink, + onClick: () => { + navigateToApp(`${APP_ID}:${SecurityPageName.case}`, { + path: getCaseDetailsUrl({ id: caseId }), + }); + }, + }, + caseId, + configureCasesNavigation: { + href: configureCasesHref, + onClick: onConfigureCasesNavClick, + }, + getCaseDetailHrefWithCommentId, + onCaseDataSuccess, + onComponentInitialized, + ruleDetailsNavigation: { + href: getDetectionsRuleDetailsHref, + onClick: onDetectionsRuleDetailsClick, + }, + showAlertDetails, + subCaseId, + timelineIntegration: { + editor_plugins: { + parsingPlugin: timelineMarkdownPlugin.parser, + processingPluginRenderer: timelineMarkdownPlugin.renderer, + uiPlugin: timelineMarkdownPlugin.plugin, + }, + hooks: { + useInsertTimeline, + }, + ui: { + renderInvestigateInTimelineActionComponent: InvestigateInTimelineActionComponent, + renderTimelineDetailsPanel: TimelineDetailsPanel, + }, + }, + useFetchAlertData, + userCanCrud, + })} + + + ); +}); + +CaseView.displayName = 'CaseView'; diff --git a/x-pack/plugins/observability/public/components/app/cases/case_view/translations.ts b/x-pack/plugins/observability/public/components/app/cases/case_view/translations.ts new file mode 100644 index 00000000000000..d7b66bbac38dfc --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/case_view/translations.ts @@ -0,0 +1,14 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +export const SEND_ALERT_TO_TIMELINE = i18n.translate( + 'xpack.securitySolution.cases.caseView.sendAlertToTimelineTooltip', + { + defaultMessage: 'Investigate in timeline', + } +); diff --git a/x-pack/plugins/observability/public/components/app/cases/constants.ts b/x-pack/plugins/observability/public/components/app/cases/constants.ts new file mode 100644 index 00000000000000..82b76b082101c8 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/constants.ts @@ -0,0 +1,9 @@ +/* + * 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 const CASES_APP_ID = 'observability-cases'; +export const CASES_OWNER = 'observability'; diff --git a/x-pack/plugins/observability/public/components/app/cases/create/flyout.test.tsx b/x-pack/plugins/observability/public/components/app/cases/create/flyout.test.tsx new file mode 100644 index 00000000000000..d413a2d5e0018a --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/create/flyout.test.tsx @@ -0,0 +1,56 @@ +/* + * 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 React from 'react'; +import { mount } from 'enzyme'; + +import '../../../common/mock/match_media'; +import { CreateCaseFlyout } from './flyout'; +import { TestProviders } from '../../../common/mock'; + +jest.mock('../../../common/lib/kibana', () => ({ + useKibana: () => ({ + services: { + cases: { + getCreateCase: jest.fn(), + }, + }, + }), +})); +const onCloseFlyout = jest.fn(); +const onSuccess = jest.fn(); +const defaultProps = { + onCloseFlyout, + onSuccess, +}; + +describe('CreateCaseFlyout', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('renders', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj='create-case-flyout']`).exists()).toBeTruthy(); + }); + + it('Closing modal calls onCloseCaseModal', () => { + const wrapper = mount( + + + + ); + + wrapper.find('.euiFlyout__closeButton').first().simulate('click'); + expect(onCloseFlyout).toBeCalled(); + }); +}); diff --git a/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx b/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx new file mode 100644 index 00000000000000..2c5b20bca42127 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import styled from 'styled-components'; +import { EuiFlyout, EuiFlyoutHeader, EuiTitle, EuiFlyoutBody } from '@elastic/eui'; + +import * as i18n from '../../translations'; +import { useKibana } from '../../../common/lib/kibana'; +import { Case } from '../../../../../cases/common'; +import { APP_ID } from '../../../../common/constants'; + +export interface CreateCaseModalProps { + afterCaseCreated?: (theCase: Case) => Promise; + onCloseFlyout: () => void; + onSuccess: (theCase: Case) => Promise; +} + +const StyledFlyout = styled(EuiFlyout)` + ${({ theme }) => ` + z-index: ${theme.eui.euiZModal}; + `} +`; +// Adding bottom padding because timeline's +// bottom bar gonna hide the submit button. +const StyledEuiFlyoutBody = styled(EuiFlyoutBody)` + ${({ theme }) => ` + && .euiFlyoutBody__overflow { + overflow-y: auto; + overflow-x: hidden; + } + + && .euiFlyoutBody__overflowContent { + display: block; + padding: ${theme.eui.paddingSizes.l} ${theme.eui.paddingSizes.l} 70px; + height: auto; + } + `} +`; + +const FormWrapper = styled.div` + width: 100%; +`; +function CreateCaseFlyoutComponent({ + afterCaseCreated, + onCloseFlyout, + onSuccess, +}: CreateCaseModalProps) { + const { cases } = useKibana().services; + return ( + + + +

{i18n.CREATE_TITLE}

+
+
+ + + {cases.getCreateCase({ + afterCaseCreated, + onCancel: onCloseFlyout, + onSuccess, + withSteps: false, + owner: [APP_ID], + })} + + +
+ ); +} + +export const CreateCaseFlyout = memo(CreateCaseFlyoutComponent); + +CreateCaseFlyout.displayName = 'CreateCaseFlyout'; diff --git a/x-pack/plugins/observability/public/components/app/cases/create/index.test.tsx b/x-pack/plugins/observability/public/components/app/cases/create/index.test.tsx new file mode 100644 index 00000000000000..1a6015d1bbd457 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/create/index.test.tsx @@ -0,0 +1,120 @@ +/* + * 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 React from 'react'; +import { mount } from 'enzyme'; +import { act, waitFor } from '@testing-library/react'; +import { noop } from 'lodash/fp'; + +import { TestProviders } from '../../../common/mock'; +import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; +import { useInsertTimeline } from '../use_insert_timeline'; +import { Create } from '.'; +import { useKibana } from '../../../common/lib/kibana'; +import { Case } from '../../../../../cases/public/containers/types'; +import { basicCase } from '../../../../../cases/public/containers/mock'; +import { APP_ID } from '../../../../common/constants'; + +jest.mock('../use_insert_timeline'); +jest.mock('../../../common/lib/kibana'); + +const useInsertTimelineMock = useInsertTimeline as jest.Mock; + +describe('Create case', () => { + const mockCreateCase = jest.fn(); + beforeEach(() => { + jest.resetAllMocks(); + jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); + (useKibana as jest.Mock).mockReturnValue({ + services: { + cases: { + getCreateCase: mockCreateCase, + }, + }, + }); + }); + + it('it renders', () => { + mount( + + + + + + ); + + expect(mockCreateCase).toHaveBeenCalled(); + expect(mockCreateCase.mock.calls[0][0].owner).toEqual([APP_ID]); + }); + + it('should redirect to all cases on cancel click', async () => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + cases: { + getCreateCase: ({ onCancel }: { onCancel: () => Promise }) => { + onCancel(); + }, + }, + }, + }); + mount( + + + + + + ); + + await waitFor(() => expect(mockHistory.push).toHaveBeenCalledWith('/')); + }); + + it('should redirect to new case when posting the case', async () => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + cases: { + getCreateCase: ({ onSuccess }: { onSuccess: (theCase: Case) => Promise }) => { + onSuccess(basicCase); + }, + }, + }, + }); + mount( + + + + + + ); + + await waitFor(() => expect(mockHistory.push).toHaveBeenNthCalledWith(1, '/basic-case-id')); + }); + + it.skip('it should insert a timeline', async () => { + let attachTimeline = noop; + useInsertTimelineMock.mockImplementation((value, onTimelineAttached) => { + attachTimeline = onTimelineAttached; + }); + + const wrapper = mount( + + + + + + ); + + act(() => { + attachTimeline('[title](url)'); + }); + + await waitFor(() => { + expect(wrapper.find(`[data-test-subj="caseDescription"] textarea`).text()).toBe( + '[title](url)' + ); + }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/app/cases/create/index.tsx b/x-pack/plugins/observability/public/components/app/cases/create/index.tsx new file mode 100644 index 00000000000000..f946cefd3494c9 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/create/index.tsx @@ -0,0 +1,53 @@ +/* + * 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 React, { useCallback } from 'react'; +import { EuiPanel } from '@elastic/eui'; +import { useHistory } from 'react-router-dom'; + +import { getCaseDetailsUrl } from '../../../common/components/link_to'; +import { useKibana } from '../../../common/lib/kibana'; +import * as timelineMarkdownPlugin from '../../../common/components/markdown_editor/plugins/timeline'; +import { useInsertTimeline } from '../use_insert_timeline'; +import { APP_ID } from '../../../../common/constants'; + +export const Create = React.memo(() => { + const { cases } = useKibana().services; + const history = useHistory(); + const onSuccess = useCallback( + async ({ id }) => { + history.push(getCaseDetailsUrl({ id })); + }, + [history] + ); + + const handleSetIsCancel = useCallback(() => { + history.push('/'); + }, [history]); + + return ( + + {cases.getCreateCase({ + onCancel: handleSetIsCancel, + onSuccess, + timelineIntegration: { + editor_plugins: { + parsingPlugin: timelineMarkdownPlugin.parser, + processingPluginRenderer: timelineMarkdownPlugin.renderer, + uiPlugin: timelineMarkdownPlugin.plugin, + }, + hooks: { + useInsertTimeline, + }, + }, + owner: [APP_ID], + })} + + ); +}); + +Create.displayName = 'Create'; diff --git a/x-pack/plugins/observability/public/components/app/cases/wrappers/index.tsx b/x-pack/plugins/observability/public/components/app/cases/wrappers/index.tsx new file mode 100644 index 00000000000000..477fb77d98ee80 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/cases/wrappers/index.tsx @@ -0,0 +1,21 @@ +/* + * 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 styled from 'styled-components'; + +export const WhitePageWrapper = styled.div` + background-color: ${({ theme }) => theme.eui.euiColorEmptyShade}; + border-top: ${({ theme }) => theme.eui.euiBorderThin}; + flex: 1 1 auto; +`; + +export const SectionWrapper = styled.div` + box-sizing: content-box; + margin: 0 auto; + max-width: 1175px; + width: 100%; +`; diff --git a/x-pack/plugins/observability/public/pages/cases/all_cases.tsx b/x-pack/plugins/observability/public/pages/cases/all_cases.tsx new file mode 100644 index 00000000000000..bd73a85bbeefa9 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/cases/all_cases.tsx @@ -0,0 +1,37 @@ +/* + * 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 React from 'react'; + +// import { useGetUserCasesPermissions } from '../../common/lib/kibana'; +import { AllCases } from '../../components/app/cases/all_cases'; + +// import { savedObjectReadOnlyErrorMessage, CaseCallOut } from '../../components/app/cases/callout'; +// import { CaseSavedObjectNoPermissions } from './saved_object_no_permissions'; + +export const AllCasesPage = React.memo(() => { + // const userPermissions = useGetUserCasesPermissions(); + + // userPermissions == null || userPermissions?.read ? ( + return ( + <> + {/* {userPermissions != null && !userPermissions?.crud && userPermissions?.read && (*/} + {/* */} + {/* )}*/} + + + ); + // ) + // : ( + // + // ); +}); + +AllCasesPage.displayName = 'AllCasesPage'; diff --git a/x-pack/plugins/observability/public/pages/cases/case_details.tsx b/x-pack/plugins/observability/public/pages/cases/case_details.tsx new file mode 100644 index 00000000000000..6b357360405cdb --- /dev/null +++ b/x-pack/plugins/observability/public/pages/cases/case_details.tsx @@ -0,0 +1,14 @@ +/* + * 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 React from 'react'; + +export const CaseDetailsPage = React.memo(() => { + return

{`Case Details Page`}

; +}); + +CaseDetailsPage.displayName = 'CaseDetailsPage'; diff --git a/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx b/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx new file mode 100644 index 00000000000000..139c2f657ec456 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx @@ -0,0 +1,14 @@ +/* + * 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 React from 'react'; + +export const ConfigureCasesPage = React.memo(() => { + return

{`Configure Cases Page`}

; +}); + +ConfigureCasesPage.displayName = 'ConfigureCasesPage'; diff --git a/x-pack/plugins/observability/public/pages/cases/create_case.tsx b/x-pack/plugins/observability/public/pages/cases/create_case.tsx new file mode 100644 index 00000000000000..e24321b4148cc4 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/cases/create_case.tsx @@ -0,0 +1,14 @@ +/* + * 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 React from 'react'; + +export const CreateCasePage = React.memo(() => { + return

{`Create Case`}

; +}); + +CreateCasePage.displayName = 'CreateCasePage'; diff --git a/x-pack/plugins/observability/public/pages/cases/empty_page.tsx b/x-pack/plugins/observability/public/pages/cases/empty_page.tsx new file mode 100644 index 00000000000000..ac691d8be2bd5f --- /dev/null +++ b/x-pack/plugins/observability/public/pages/cases/empty_page.tsx @@ -0,0 +1,118 @@ +/* + * 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 { + EuiButton, + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + IconType, + EuiCard, +} from '@elastic/eui'; +import React, { MouseEventHandler, ReactNode, useMemo } from 'react'; +import styled from 'styled-components'; + +const EmptyPrompt = styled(EuiEmptyPrompt)` + align-self: center; /* Corrects horizontal centering in IE11 */ + max-width: 60em; +`; + +EmptyPrompt.displayName = 'EmptyPrompt'; + +interface EmptyPageActions { + icon?: IconType; + label: string; + target?: string; + url: string; + descriptionTitle?: string; + description?: string; + fill?: boolean; + onClick?: MouseEventHandler; +} + +export type EmptyPageActionsProps = Record; + +interface EmptyPageProps { + actions: EmptyPageActionsProps; + 'data-test-subj'?: string; + message?: ReactNode; + title: string; +} + +const EmptyPageComponent = React.memo(({ actions, message, title, ...rest }) => { + const titles = Object.keys(actions); + const maxItemWidth = 283; + const renderActions = useMemo( + () => + Object.values(actions) + .filter((a) => a.label && a.url) + .map( + ( + { icon, label, target, url, descriptionTitle, description, onClick, fill = true }, + idx + ) => + descriptionTitle != null || description != null ? ( + + + {label} + + } + /> + + ) : ( + + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} + + {label} + + + ) + ), + [actions, titles] + ); + + return ( + {title}} + body={message &&

{message}

} + actions={{renderActions}} + {...rest} + /> + ); +}); + +EmptyPageComponent.displayName = 'EmptyPageComponent'; + +export const EmptyPage = React.memo(EmptyPageComponent); +EmptyPage.displayName = 'EmptyPage'; diff --git a/x-pack/plugins/observability/public/pages/cases/index.tsx b/x-pack/plugins/observability/public/pages/cases/index.tsx deleted file mode 100644 index dd7f7875b568e4..00000000000000 --- a/x-pack/plugins/observability/public/pages/cases/index.tsx +++ /dev/null @@ -1,49 +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 { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiPageTemplate } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { ExperimentalBadge } from '../../components/shared/experimental_badge'; -import { RouteParams } from '../../routes'; - -interface CasesProps { - routeParams: RouteParams<'/cases'>; -} - -export function CasesPage(props: CasesProps) { - return ( - - {i18n.translate('xpack.observability.casesTitle', { defaultMessage: 'Cases' })}{' '} - - - ), - }} - > - - - -

- {i18n.translate('xpack.observability.casesDisclaimerText', { - defaultMessage: 'This is the future home of cases.', - })} -

-
-
-
-
- ); -} diff --git a/x-pack/plugins/observability/public/pages/cases/links.ts b/x-pack/plugins/observability/public/pages/cases/links.ts new file mode 100644 index 00000000000000..fd89864aa6d233 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/cases/links.ts @@ -0,0 +1,56 @@ +/* + * 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 { useCallback } from 'react'; +import { isEmpty } from 'lodash/fp'; +import { useKibana } from '../../utils/kibana_react'; +import { CASES_APP_ID } from '../../components/app/cases/constants'; + +export const getCaseDetailsUrl = ({ id, subCaseId }: { id: string; subCaseId?: string }) => { + if (subCaseId) { + return `/${encodeURIComponent(id)}/sub-cases/${encodeURIComponent(subCaseId)}`; + } + return `/${encodeURIComponent(id)}`; +}; + +export type FormatUrl = (path: string) => string; +export const useFormatUrl = () => { + const { getUrlForApp } = useKibana().services.application; + const formatUrl = useCallback( + (path: string) => { + const pathArr = path.split('?'); + const formattedPath = `${pathArr[0]}${isEmpty(pathArr[1]) ? '' : `?${pathArr[1]}`}`; + return getUrlForApp(`${CASES_APP_ID}`, { + path: formattedPath, + absolute: false, + }); + }, + [getUrlForApp] + ); + return { formatUrl }; +}; + +export const getCaseDetailsUrlWithCommentId = ({ + id, + commentId, + subCaseId, +}: { + id: string; + commentId: string; + subCaseId?: string; +}) => { + if (subCaseId) { + return `/${encodeURIComponent(id)}/sub-cases/${encodeURIComponent( + subCaseId + )}/${encodeURIComponent(commentId)}`; + } + return `/${encodeURIComponent(id)}/${encodeURIComponent(commentId)}`; +}; + +export const getCreateCaseUrl = () => `/create`; + +export const getConfigureCasesUrl = () => `/configure`; diff --git a/x-pack/plugins/observability/public/pages/cases/saved_object_no_permissions.tsx b/x-pack/plugins/observability/public/pages/cases/saved_object_no_permissions.tsx new file mode 100644 index 00000000000000..2953f1518a1f87 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/cases/saved_object_no_permissions.tsx @@ -0,0 +1,38 @@ +/* + * 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 React, { useMemo } from 'react'; + +import { EmptyPage } from './empty_page'; +import * as i18n from './translations'; +import { useKibana } from '../../utils/kibana_react'; + +export const CaseSavedObjectNoPermissions = React.memo(() => { + const docLinks = useKibana().services.docLinks; + const actions = useMemo( + () => ({ + savedObject: { + icon: 'documents', + label: i18n.GO_TO_DOCUMENTATION, + url: `${docLinks.ELASTIC_WEBSITE_URL}guide/en/security/${docLinks.DOC_LINK_VERSION}s`, + target: '_blank', + }, + }), + [docLinks] + ); + + return ( + + ); +}); + +CaseSavedObjectNoPermissions.displayName = 'CaseSavedObjectNoPermissions'; diff --git a/x-pack/plugins/observability/public/pages/cases/translations.ts b/x-pack/plugins/observability/public/pages/cases/translations.ts new file mode 100644 index 00000000000000..d3daa5d9def992 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/cases/translations.ts @@ -0,0 +1,216 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const SAVED_OBJECT_NO_PERMISSIONS_TITLE = i18n.translate( + 'xpack.observability.cases.caseSavedObjectNoPermissionsTitle', + { + defaultMessage: 'Kibana feature privileges required', + } +); + +export const SAVED_OBJECT_NO_PERMISSIONS_MSG = i18n.translate( + 'xpack.observability.cases.caseSavedObjectNoPermissionsMessage', + { + defaultMessage: + 'To view cases, you must have privileges for the Saved Object Management feature in the Kibana space. For more information, contact your Kibana administrator.', + } +); + +export const BACK_TO_ALL = i18n.translate('xpack.observability.cases.caseView.backLabel', { + defaultMessage: 'Back to cases', +}); + +export const CANCEL = i18n.translate('xpack.observability.cases.caseView.cancel', { + defaultMessage: 'Cancel', +}); + +export const DELETE_CASE = i18n.translate( + 'xpack.observability.cases.confirmDeleteCase.deleteCase', + { + defaultMessage: 'Delete case', + } +); + +export const DELETE_CASES = i18n.translate( + 'xpack.observability.cases.confirmDeleteCase.deleteCases', + { + defaultMessage: 'Delete cases', + } +); + +export const NAME = i18n.translate('xpack.observability.cases.caseView.name', { + defaultMessage: 'Name', +}); + +export const OPENED_ON = i18n.translate('xpack.observability.cases.caseView.openedOn', { + defaultMessage: 'Opened on', +}); + +export const CLOSED_ON = i18n.translate('xpack.observability.cases.caseView.closedOn', { + defaultMessage: 'Closed on', +}); + +export const REPORTER = i18n.translate('xpack.observability.cases.caseView.reporterLabel', { + defaultMessage: 'Reporter', +}); + +export const PARTICIPANTS = i18n.translate('xpack.observability.cases.caseView.particpantsLabel', { + defaultMessage: 'Participants', +}); + +export const CREATE_BC_TITLE = i18n.translate('xpack.observability.cases.caseView.breadcrumb', { + defaultMessage: 'Create', +}); + +export const CREATE_TITLE = i18n.translate('xpack.observability.cases.caseView.create', { + defaultMessage: 'Create new case', +}); + +export const DESCRIPTION = i18n.translate('xpack.observability.cases.caseView.description', { + defaultMessage: 'Description', +}); + +export const DESCRIPTION_REQUIRED = i18n.translate( + 'xpack.observability.cases.createCase.descriptionFieldRequiredError', + { + defaultMessage: 'A description is required.', + } +); + +export const COMMENT_REQUIRED = i18n.translate( + 'xpack.observability.cases.caseView.commentFieldRequiredError', + { + defaultMessage: 'A comment is required.', + } +); + +export const REQUIRED_FIELD = i18n.translate( + 'xpack.observability.cases.caseView.fieldRequiredError', + { + defaultMessage: 'Required field', + } +); + +export const EDIT = i18n.translate('xpack.observability.cases.caseView.edit', { + defaultMessage: 'Edit', +}); + +export const OPTIONAL = i18n.translate('xpack.observability.cases.caseView.optional', { + defaultMessage: 'Optional', +}); + +export const PAGE_TITLE = i18n.translate('xpack.observability.cases.pageTitle', { + defaultMessage: 'Cases', +}); + +export const CREATE_CASE = i18n.translate('xpack.observability.cases.caseView.createCase', { + defaultMessage: 'Create case', +}); + +export const CLOSE_CASE = i18n.translate('xpack.observability.cases.caseView.closeCase', { + defaultMessage: 'Close case', +}); + +export const REOPEN_CASE = i18n.translate('xpack.observability.cases.caseView.reopenCase', { + defaultMessage: 'Reopen case', +}); + +export const CASE_NAME = i18n.translate('xpack.observability.cases.caseView.caseName', { + defaultMessage: 'Case name', +}); + +export const TO = i18n.translate('xpack.observability.cases.caseView.to', { + defaultMessage: 'to', +}); + +export const TAGS = i18n.translate('xpack.observability.cases.caseView.tags', { + defaultMessage: 'Tags', +}); + +export const ACTIONS = i18n.translate('xpack.observability.cases.allCases.actions', { + defaultMessage: 'Actions', +}); + +export const NO_TAGS_AVAILABLE = i18n.translate( + 'xpack.observability.cases.allCases.noTagsAvailable', + { + defaultMessage: 'No tags available', + } +); + +export const NO_REPORTERS_AVAILABLE = i18n.translate( + 'xpack.observability.cases.caseView.noReportersAvailable', + { + defaultMessage: 'No reporters available.', + } +); + +export const COMMENTS = i18n.translate('xpack.observability.cases.allCases.comments', { + defaultMessage: 'Comments', +}); + +export const TAGS_HELP = i18n.translate('xpack.observability.cases.createCase.fieldTagsHelpText', { + defaultMessage: + 'Type one or more custom identifying tags for this case. Press enter after each tag to begin a new one.', +}); + +export const NO_TAGS = i18n.translate('xpack.observability.cases.caseView.noTags', { + defaultMessage: 'No tags are currently assigned to this case.', +}); + +export const TITLE_REQUIRED = i18n.translate( + 'xpack.observability.cases.createCase.titleFieldRequiredError', + { + defaultMessage: 'A title is required.', + } +); + +export const CONFIGURE_CASES_PAGE_TITLE = i18n.translate( + 'xpack.observability.cases.configureCases.headerTitle', + { + defaultMessage: 'Configure cases', + } +); + +export const CONFIGURE_CASES_BUTTON = i18n.translate( + 'xpack.observability.cases.configureCasesButton', + { + defaultMessage: 'Edit external connection', + } +); + +export const ADD_COMMENT = i18n.translate('xpack.observability.cases.caseView.comment.addComment', { + defaultMessage: 'Add comment', +}); + +export const ADD_COMMENT_HELP_TEXT = i18n.translate( + 'xpack.observability.cases.caseView.comment.addCommentHelpText', + { + defaultMessage: 'Add a new comment...', + } +); + +export const SAVE = i18n.translate('xpack.observability.cases.caseView.description.save', { + defaultMessage: 'Save', +}); + +export const GO_TO_DOCUMENTATION = i18n.translate( + 'xpack.observability.cases.caseView.goToDocumentationButton', + { + defaultMessage: 'View documentation', + } +); + +export const CONNECTORS = i18n.translate('xpack.observability.cases.caseView.connectors', { + defaultMessage: 'External Incident Management System', +}); + +export const EDIT_CONNECTOR = i18n.translate('xpack.observability.cases.caseView.editConnector', { + defaultMessage: 'Change external incident management system', +}); diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index 6856bc97b4a360..115282aaaf1102 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -34,6 +34,8 @@ import { createCallObservabilityApi } from './services/call_observability_api'; import { toggleOverviewLinkInNav } from './toggle_overview_link_in_nav'; import { ConfigSchema } from '.'; import { createObservabilityRuleTypeRegistry } from './rules/create_observability_rule_type_registry'; +import { CASES_APP_ID } from './components/app/cases/constants'; +import { CasesUiStart } from '../../cases/public'; export type ObservabilityPublicSetup = ReturnType; @@ -44,6 +46,7 @@ export interface ObservabilityPublicPluginsSetup { } export interface ObservabilityPublicPluginsStart { + cases: CasesUiStart; home?: HomePublicPluginStart; triggersActionsUi: TriggersAndActionsUIPublicPluginStart; data: DataPublicPluginStart; @@ -107,7 +110,6 @@ export class Plugin mount, updater$, }); - if (config.unsafe.alertingExperience.enabled) { coreSetup.application.register({ id: 'observability-alerts', @@ -121,7 +123,7 @@ export class Plugin }); coreSetup.application.register({ - id: 'observability-cases', + id: CASES_APP_ID, title: 'Cases', appRoute: '/app/observability/cases', order: 8050, diff --git a/x-pack/plugins/observability/public/routes/index.tsx b/x-pack/plugins/observability/public/routes/index.tsx index 0bdb03995ad467..5fccece577cd7b 100644 --- a/x-pack/plugins/observability/public/routes/index.tsx +++ b/x-pack/plugins/observability/public/routes/index.tsx @@ -13,8 +13,10 @@ import { LandingPage } from '../pages/landing'; import { OverviewPage } from '../pages/overview'; import { jsonRt } from './json_rt'; import { AlertsPage } from '../pages/alerts'; -import { CasesPage } from '../pages/cases'; +import { CreateCasePage } from '../pages/cases/create_case'; import { ExploratoryViewPage } from '../components/shared/exploratory_view'; +import { CaseDetailsPage } from '../pages/cases/case_details'; +import { AllCasesPage } from '../pages/cases/all_cases'; export type RouteParams = DecodeParams; @@ -78,14 +80,45 @@ export const routes = { }, '/cases': { handler: (routeParams: any) => { - return ; + return ; }, params: { - query: t.partial({ - rangeFrom: t.string, - rangeTo: t.string, - refreshPaused: jsonRt.pipe(t.boolean), - refreshInterval: jsonRt.pipe(t.number), + path: t.partial({ + detailName: t.string, + }), + }, + breadcrumb: [ + { + text: i18n.translate('xpack.observability.cases.breadcrumb', { + defaultMessage: 'Cases', + }), + }, + ], + }, + '/cases/create': { + handler: (routeParams: any) => { + return ; + }, + params: { + path: t.partial({ + detailName: t.string, + }), + }, + breadcrumb: [ + { + text: i18n.translate('xpack.observability.cases.breadcrumb', { + defaultMessage: 'Cases', + }), + }, + ], + }, + '/cases/:detailName': { + handler: (routeParams: any) => { + return ; + }, + params: { + path: t.partial({ + detailName: t.string, }), }, breadcrumb: [ diff --git a/x-pack/plugins/observability/public/utils/kibana_react.ts b/x-pack/plugins/observability/public/utils/kibana_react.ts new file mode 100644 index 00000000000000..2efac4616fad47 --- /dev/null +++ b/x-pack/plugins/observability/public/utils/kibana_react.ts @@ -0,0 +1,13 @@ +/* + * 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 { CoreStart } from 'kibana/public'; +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { ObservabilityPublicPluginsStart } from '../plugin'; +const useTypedKibana = () => useKibana(); + +export { useTypedKibana as useKibana }; diff --git a/x-pack/plugins/observability/tsconfig.json b/x-pack/plugins/observability/tsconfig.json index 814d55bfd61fb2..b6ed0a0a3d17f6 100644 --- a/x-pack/plugins/observability/tsconfig.json +++ b/x-pack/plugins/observability/tsconfig.json @@ -24,6 +24,7 @@ { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, { "path": "../alerting/tsconfig.json" }, { "path": "../licensing/tsconfig.json" }, + { "path": "../cases/tsconfig.json" }, { "path": "../lens/tsconfig.json" }, { "path": "../rule_registry/tsconfig.json" }, { "path": "../translations/tsconfig.json" } From eb7fdd7a42c2b1ccc69418f23fea9af89ab07eeb Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Tue, 18 May 2021 16:59:12 -0400 Subject: [PATCH 066/113] Adding cases feature --- x-pack/plugins/observability/kibana.json | 3 +- x-pack/plugins/observability/server/plugin.ts | 86 +++++++++++++++++-- .../apis/features/features/features.ts | 1 + .../apis/security/privileges.ts | 1 + 4 files changed, 83 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/observability/kibana.json b/x-pack/plugins/observability/kibana.json index 7f48d13b4945b8..773cd5cd14f1a9 100644 --- a/x-pack/plugins/observability/kibana.json +++ b/x-pack/plugins/observability/kibana.json @@ -17,7 +17,8 @@ "alerting", "ruleRegistry", "triggersActionsUi", - "cases" + "cases", + "features" ], "ui": true, "server": true, diff --git a/x-pack/plugins/observability/server/plugin.ts b/x-pack/plugins/observability/server/plugin.ts index 046a9a62d5fa79..954046fc87dfbb 100644 --- a/x-pack/plugins/observability/server/plugin.ts +++ b/x-pack/plugins/observability/server/plugin.ts @@ -5,7 +5,13 @@ * 2.0. */ -import { PluginInitializerContext, Plugin, CoreSetup } from 'src/core/server'; +import { i18n } from '@kbn/i18n'; +import { + PluginInitializerContext, + Plugin, + CoreSetup, + DEFAULT_APP_CATEGORIES, +} from '../../../../src/core/server'; import { RuleDataClient } from '../../rule_registry/server'; import { ObservabilityConfig } from '.'; import { @@ -14,23 +20,89 @@ import { AnnotationsAPI, } from './lib/annotations/bootstrap_annotations'; import type { RuleRegistryPluginSetupContract } from '../../rule_registry/server'; +import { PluginSetupContract as FeaturesSetup } from '../../features/server'; import { uiSettings } from './ui_settings'; import { registerRoutes } from './routes/register_routes'; import { getGlobalObservabilityServerRouteRepository } from './routes/get_global_observability_server_route_repository'; +import { + CASE_COMMENT_SAVED_OBJECT, + CASE_CONFIGURE_SAVED_OBJECT, + CASE_SAVED_OBJECT, + CASE_USER_ACTION_SAVED_OBJECT, +} from '../../cases/common'; export type ObservabilityPluginSetup = ReturnType; +interface PluginSetup { + features: FeaturesSetup; + ruleRegistry: RuleRegistryPluginSetupContract; +} + +const caseSavedObjects = [ + CASE_SAVED_OBJECT, + CASE_USER_ACTION_SAVED_OBJECT, + CASE_COMMENT_SAVED_OBJECT, + CASE_CONFIGURE_SAVED_OBJECT, +]; + +const OBS_CASES_ID = 'observabilityCases'; +const OBSERVABILITY = 'observability'; + export class ObservabilityPlugin implements Plugin { constructor(private readonly initContext: PluginInitializerContext) { this.initContext = initContext; } - public setup( - core: CoreSetup, - plugins: { - ruleRegistry: RuleRegistryPluginSetupContract; - } - ) { + public setup(core: CoreSetup, plugins: PluginSetup) { + plugins.features.registerKibanaFeature({ + id: OBS_CASES_ID, + name: i18n.translate('xpack.observability.featureRegistry.linkSecuritySolutionTitle', { + defaultMessage: 'Cases', + }), + order: 1100, + category: DEFAULT_APP_CATEGORIES.observability, + app: [OBS_CASES_ID, 'kibana'], + catalogue: [OBSERVABILITY], + cases: [OBSERVABILITY], + management: { + insightsAndAlerting: ['triggersActions'], + }, + privileges: { + all: { + app: [OBS_CASES_ID, 'kibana'], + catalogue: [OBSERVABILITY], + cases: { + all: [OBSERVABILITY], + }, + api: [], + savedObject: { + all: [...caseSavedObjects], + read: ['config'], + }, + management: { + insightsAndAlerting: ['triggersActions'], + }, + ui: ['crud_cases', 'read_cases'], // uiCapabilities.observabilityCases.crud_cases or read_cases + }, + read: { + app: [OBS_CASES_ID, 'kibana'], + catalogue: [OBSERVABILITY], + cases: { + read: [OBSERVABILITY], + }, + api: [], + savedObject: { + all: [], + read: ['config', ...caseSavedObjects], + }, + management: { + insightsAndAlerting: ['triggersActions'], + }, + ui: ['read_cases'], // uiCapabilities.observabilityCases.read_cases + }, + }, + }); + const config = this.initContext.config.get(); let annotationsApiPromise: Promise | undefined; diff --git a/x-pack/test/api_integration/apis/features/features/features.ts b/x-pack/test/api_integration/apis/features/features/features.ts index 25f38222bfb6cb..275626664bef07 100644 --- a/x-pack/test/api_integration/apis/features/features/features.ts +++ b/x-pack/test/api_integration/apis/features/features/features.ts @@ -114,6 +114,7 @@ export default function ({ getService }: FtrProviderContext) { 'infrastructure', 'logs', 'maps', + 'observabilityCases', 'uptime', 'siem', 'fleet', diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index e79e4caf9cbfba..787c499a260826 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -60,6 +60,7 @@ export default function ({ getService }: FtrProviderContext) { canvas: ['all', 'read', 'minimal_all', 'minimal_read', 'generate_report'], infrastructure: ['all', 'read'], logs: ['all', 'read'], + observabilityCases: ['all', 'read'], uptime: ['all', 'read'], apm: ['all', 'read'], ml: ['all', 'read'], From ce68009ff10e2fda9a8ddea95fe4b9ed7b93bd8c Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 18 May 2021 15:11:35 -0600 Subject: [PATCH 067/113] wip --- .../all_cases/all_cases_generic.tsx | 3 + .../public/components/all_cases/header.tsx | 4 +- .../public/components/all_cases/index.tsx | 1 + .../components/app/cases/__mock__/form.ts | 4 +- .../components/app/cases/all_cases/index.tsx | 3 +- .../app/cases/callout/index.test.tsx | 5 +- .../components/app/cases/callout/index.tsx | 4 +- .../app/cases/callout/translations.ts | 6 +- .../components/app/cases/case_view/helpers.ts | 127 ++++++----- .../components/app/cases/case_view/index.tsx | 206 ++++-------------- .../app/cases/case_view/translations.ts | 2 +- .../public/components/app/cases/constants.ts | 1 + .../components/app/cases/create/flyout.tsx | 10 +- .../components/app/cases/create/index.tsx | 20 +- .../app}/cases/translations.ts | 0 .../public/hooks/use_messages_storage.tsx | 65 ++++++ .../public/pages/cases/all_cases.tsx | 15 +- .../public/pages/cases/case_details.tsx | 28 ++- .../public/pages/cases/create_case.tsx | 55 ++++- .../observability/public/pages/cases/links.ts | 18 +- .../cases/saved_object_no_permissions.tsx | 2 +- x-pack/plugins/observability/public/plugin.ts | 2 +- 22 files changed, 309 insertions(+), 272 deletions(-) rename x-pack/plugins/observability/public/{pages => components/app}/cases/translations.ts (100%) create mode 100644 x-pack/plugins/observability/public/hooks/use_messages_storage.tsx diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx index 41509d9c0d1358..4b235b9fec8688 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx @@ -65,6 +65,7 @@ interface AllCasesGenericProps { hiddenStatuses?: CaseStatusWithAllStatus[]; isSelectorView?: boolean; onRowClick?: (theCase?: Case | SubCase) => void; + showTitle?: boolean; updateCase?: (newCase: Case) => void; userCanCrud: boolean; } @@ -78,6 +79,7 @@ export const AllCasesGeneric = React.memo( hiddenStatuses = [], isSelectorView, onRowClick, + showTitle, updateCase, userCanCrud, }) => { @@ -271,6 +273,7 @@ export const AllCasesGeneric = React.memo( createCaseNavigation={createCaseNavigation} configureCasesNavigation={configureCasesNavigation} refresh={refresh} + showTitle={showTitle} userCanCrud={userCanCrud} /> )} diff --git a/x-pack/plugins/cases/public/components/all_cases/header.tsx b/x-pack/plugins/cases/public/components/all_cases/header.tsx index a6737b987e2c42..7452fe7e44b3c4 100644 --- a/x-pack/plugins/cases/public/components/all_cases/header.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/header.tsx @@ -20,6 +20,7 @@ interface OwnProps { configureCasesNavigation: CasesNavigation; createCaseNavigation: CasesNavigation; refresh: number; + showTitle?: boolean; userCanCrud: boolean; } @@ -40,9 +41,10 @@ export const CasesTableHeader: FunctionComponent = ({ configureCasesNavigation, createCaseNavigation, refresh, + showTitle = true, userCanCrud, }) => ( - + ; // if not passed, case name is not displayed as a link (Formerly dependant on isSelector) configureCasesNavigation: CasesNavigation; // if not passed, header with nav is not displayed (Formerly dependant on isSelector) createCaseNavigation: CasesNavigation; + showTitle?: boolean; userCanCrud: boolean; } diff --git a/x-pack/plugins/observability/public/components/app/cases/__mock__/form.ts b/x-pack/plugins/observability/public/components/app/cases/__mock__/form.ts index 17bbcd8cdea64d..5fd62410d9954a 100644 --- a/x-pack/plugins/observability/public/components/app/cases/__mock__/form.ts +++ b/x-pack/plugins/observability/public/components/app/cases/__mock__/form.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { useForm } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'; -import { useFormData } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data'; +import { useForm } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'; +import { useFormData } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data'; jest.mock( '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' diff --git a/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx b/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx index c6692cace8a6b1..f22186406176f1 100644 --- a/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx @@ -32,7 +32,7 @@ export const AllCases = React.memo(({ userCanCrud }) => { application: { navigateToApp }, } = useKibana().services; const history = useHistory(); - const { formatUrl } = useFormatUrl(); + const { formatUrl } = useFormatUrl(CASES_APP_ID); const goToCreateCase = useCallback( (ev) => { @@ -71,6 +71,7 @@ export const AllCases = React.memo(({ userCanCrud }) => { href: formatUrl(getCreateCaseUrl()), onClick: goToCreateCase, }, + showTitle: false, userCanCrud, owner: [CASES_APP_ID], }); diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/index.test.tsx b/x-pack/plugins/observability/public/components/app/cases/callout/index.test.tsx index 5d1daf0c2f7bb1..762b02bf94b169 100644 --- a/x-pack/plugins/observability/public/components/app/cases/callout/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/callout/index.test.tsx @@ -8,13 +8,12 @@ import React from 'react'; import { mount } from 'enzyme'; -import { useMessagesStorage } from '../../../common/containers/local_storage/use_messages_storage'; -import { TestProviders } from '../../../common/mock'; +import { useMessagesStorage } from '../../../../hooks/use_messages_storage'; import { createCalloutId } from './helpers'; import { CaseCallOut, CaseCallOutProps } from '.'; jest.mock('../../../common/containers/local_storage/use_messages_storage'); - +const TestProviders = (children: any) => children; const useSecurityLocalStorageMock = useMessagesStorage as jest.Mock; const securityLocalStorageMock = { getMessages: jest.fn(() => []), diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/index.tsx b/x-pack/plugins/observability/public/components/app/cases/callout/index.tsx index a385e3a56197b2..cb1958b9926faa 100644 --- a/x-pack/plugins/observability/public/components/app/cases/callout/index.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/callout/index.tsx @@ -8,10 +8,10 @@ import { EuiSpacer } from '@elastic/eui'; import React, { memo, useCallback, useState, useMemo } from 'react'; -import { useMessagesStorage } from '../../../common/containers/local_storage/use_messages_storage'; import { CallOut } from './callout'; import { ErrorMessage } from './types'; import { createCalloutId } from './helpers'; +import { useMessagesStorage } from '../../../../hooks/use_messages_storage'; export * from './helpers'; @@ -38,7 +38,7 @@ function CaseCallOutComponent({ title, messages = [] }: CaseCallOutProps) { const dismissedCallouts = useMemo( () => caseMessages.reduce( - (acc, id) => ({ + (acc: CalloutVisibility, id) => ({ ...acc, [id]: false, }), diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/translations.ts b/x-pack/plugins/observability/public/components/app/cases/callout/translations.ts index 4a5f32684ccdec..5ac17662d789da 100644 --- a/x-pack/plugins/observability/public/components/app/cases/callout/translations.ts +++ b/x-pack/plugins/observability/public/components/app/cases/callout/translations.ts @@ -8,14 +8,14 @@ import { i18n } from '@kbn/i18n'; export const READ_ONLY_SAVED_OBJECT_TITLE = i18n.translate( - 'xpack.securitySolution.cases.readOnlySavedObjectTitle', + 'xpack.observability.cases.readOnlySavedObjectTitle', { defaultMessage: 'You cannot open new or update existing cases', } ); export const READ_ONLY_SAVED_OBJECT_MSG = i18n.translate( - 'xpack.securitySolution.cases.readOnlySavedObjectDescription', + 'xpack.observability.cases.readOnlySavedObjectDescription', { defaultMessage: 'You only have privileges to view cases. If you need to open and update cases, contact your Kibana administrator.', @@ -23,7 +23,7 @@ export const READ_ONLY_SAVED_OBJECT_MSG = i18n.translate( ); export const DISMISS_CALLOUT = i18n.translate( - 'xpack.securitySolution.cases.dismissErrorsPushServiceCallOutTitle', + 'xpack.observability.cases.dismissErrorsPushServiceCallOutTitle', { defaultMessage: 'Dismiss', } diff --git a/x-pack/plugins/observability/public/components/app/cases/case_view/helpers.ts b/x-pack/plugins/observability/public/components/app/cases/case_view/helpers.ts index 94813862a983a5..04da744fdd62e0 100644 --- a/x-pack/plugins/observability/public/components/app/cases/case_view/helpers.ts +++ b/x-pack/plugins/observability/public/components/app/cases/case_view/helpers.ts @@ -7,10 +7,10 @@ import { isObject, get, isString, isNumber } from 'lodash'; import { useMemo } from 'react'; -import { useSourcererScope } from '../../../common/containers/sourcerer'; -import { SourcererScopeName } from '../../../common/store/sourcerer/model'; -import { useQueryAlerts } from '../../../detections/containers/detection_engine/alerts/use_query'; -import { Ecs } from '../../../../../cases/common'; +// import { useSourcererScope } from '../../../common/containers/sourcerer'; +// import { SourcererScopeName } from '../../../common/store/sourcerer/model'; +// import { useQueryAlerts } from '../../../detections/containers/detection_engine/alerts/use_query'; +// import { Ecs } from '../../../../../cases/common'; // TODO we need to allow -> docValueFields: [{ field: "@timestamp" }], export const buildAlertsQuery = (alertIds: string[]) => { @@ -66,66 +66,65 @@ export const toStringArray = (value: unknown): string[] => { } }; -export const formatAlertToEcsSignal = (alert: {}): Ecs => - Object.keys(alert).reduce((accumulator, key) => { - const item = get(alert, key); - if (item != null && isObject(item)) { - return { ...accumulator, [key]: formatAlertToEcsSignal(item) }; - } else if (Array.isArray(item) || isString(item) || isNumber(item)) { - return { ...accumulator, [key]: toStringArray(item) }; - } - return accumulator; - }, {} as Ecs); -interface Signal { - rule: { - id: string; - name: string; - to: string; - from: string; - }; -} - -interface SignalHit { - _id: string; - _index: string; - _source: { - '@timestamp': string; - signal: Signal; - }; -} +// export const formatAlertToEcsSignal = (alert: {}): Ecs => +// Object.keys(alert).reduce((accumulator, key) => { +// const item = get(alert, key); +// if (item != null && isObject(item)) { +// return { ...accumulator, [key]: formatAlertToEcsSignal(item) }; +// } else if (Array.isArray(item) || isString(item) || isNumber(item)) { +// return { ...accumulator, [key]: toStringArray(item) }; +// } +// return accumulator; +// }, {} as Ecs); +// interface Signal { +// rule: { +// id: string; +// name: string; +// to: string; +// from: string; +// }; +// } -export interface Alert { - _id: string; - _index: string; - '@timestamp': string; - signal: Signal; - [key: string]: unknown; -} -export const useFetchAlertData = (alertIds: string[]): [boolean, Record] => { - const { selectedPatterns } = useSourcererScope(SourcererScopeName.detections); - const alertsQuery = useMemo(() => buildAlertsQuery(alertIds), [alertIds]); - - const { loading: isLoadingAlerts, data: alertsData } = useQueryAlerts({ - query: alertsQuery, - indexName: selectedPatterns[0], - }); - - const alerts = useMemo( - () => - alertsData?.hits.hits.reduce>( - (acc, { _id, _index, _source }) => ({ - ...acc, - [_id]: { - ...formatAlertToEcsSignal(_source), - _id, - _index, - timestamp: _source['@timestamp'], - }, - }), - {} - ) ?? {}, - [alertsData?.hits.hits] - ); +// interface SignalHit { +// _id: string; +// _index: string; +// _source: { +// '@timestamp': string; +// signal: Signal; +// }; +// } +// +// export interface Alert { +// _id: string; +// _index: string; +// '@timestamp': string; +// signal: Signal; +// [key: string]: unknown; +// } +export const useFetchAlertData = (alertIds: string[]) => { + // const alertsQuery = useMemo(() => buildAlertsQuery(alertIds), [alertIds]); + // + // const { loading: isLoadingAlerts, data: alertsData } = // useQueryAlerts({ + // query: alertsQuery, + // indexName: selectedPatterns[0], + // }); + // + // const alerts = useMemo( + // () => + // alertsData?.hits.hits.reduce>( + // (acc, { _id, _index, _source }) => ({ + // ...acc, + // [_id]: { + // ...formatAlertToEcsSignal(_source), + // _id, + // _index, + // timestamp: _source['@timestamp'], + // }, + // }), + // {} + // ) ?? {}, + // [alertsData?.hits.hits] + // ); - return [isLoadingAlerts, alerts]; + return [false, {}]; }; diff --git a/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx b/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx index f61bf93c0e6408..b634eccd1e909b 100644 --- a/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx @@ -6,11 +6,7 @@ */ import React, { useCallback, useState } from 'react'; -import { useDispatch } from 'react-redux'; import { useHistory } from 'react-router-dom'; -import { SearchResponse } from 'elasticsearch'; -import { isEmpty } from 'lodash'; - import { getCaseDetailsUrl, getCaseDetailsUrlWithCommentId, @@ -18,24 +14,11 @@ import { getConfigureCasesUrl, getRuleDetailsUrl, useFormatUrl, -} from '../../../common/components/link_to'; -import { Ecs } from '../../../../common/ecs'; -import { Case } from '../../../../../cases/common'; -import { TimelineNonEcsData } from '../../../../common/search_strategy'; -import { TimelineId } from '../../../../common/types/timeline'; -import { SecurityPageName } from '../../../app/types'; -import { KibanaServices, useKibana } from '../../../common/lib/kibana'; -import { APP_ID, DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../common/constants'; -import { timelineActions } from '../../../timelines/store/timeline'; -import { useSourcererScope } from '../../../common/containers/sourcerer'; -import { SourcererScopeName } from '../../../common/store/sourcerer/model'; -import { DetailsPanel } from '../../../timelines/components/side_panel'; -import { InvestigateInTimelineAction } from '../../../detections/components/alerts_table/timeline_actions/investigate_in_timeline_action'; -import { buildAlertsQuery, formatAlertToEcsSignal, useFetchAlertData } from './helpers'; -import { SEND_ALERT_TO_TIMELINE } from './translations'; -import { useInsertTimeline } from '../use_insert_timeline'; -import { SpyRoute } from '../../../common/utils/route/spy_routes'; -import * as timelineMarkdownPlugin from '../../../common/components/markdown_editor/plugins/timeline'; +} from '../../../../pages/cases/links'; +import { Case } from '../../../../../../cases/common'; +import { useFetchAlertData } from './helpers'; +import { useKibana } from '../../../../utils/kibana_react'; +import { ALERTS_APP_ID, CASES_APP_ID } from '../constants'; interface Props { caseId: string; @@ -56,59 +39,6 @@ export interface CaseProps extends Props { updateCase: (newCase: Case) => void; } -function TimelineDetailsPanel() { - const { browserFields, docValueFields } = useSourcererScope(SourcererScopeName.detections); - - return ( - - ); -} - -function InvestigateInTimelineActionComponent(alertIds: string[]) { - const EMPTY_ARRAY: TimelineNonEcsData[] = []; - const fetchEcsAlertsData = async (fetchAlertIds?: string[]): Promise => { - if (isEmpty(fetchAlertIds)) { - return []; - } - const alertResponse = await KibanaServices.get().http.fetch< - SearchResponse<{ '@timestamp': string; [key: string]: unknown }> - >(DETECTION_ENGINE_QUERY_SIGNALS_URL, { - method: 'POST', - body: JSON.stringify(buildAlertsQuery(fetchAlertIds ?? [])), - }); - return ( - alertResponse?.hits.hits.reduce( - (acc, { _id, _index, _source }) => [ - ...acc, - { - ...formatAlertToEcsSignal(_source as {}), - _id, - _index, - timestamp: _source['@timestamp'], - }, - ], - [] - ) ?? [] - ); - }; - - return ( - - ); -} - export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) => { const [spyState, setSpyState] = useState<{ caseTitle: string | undefined }>({ caseTitle: undefined, @@ -128,13 +58,10 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = application: { navigateToApp }, } = useKibana().services; const history = useHistory(); - const dispatch = useDispatch(); - const { formatUrl, search } = useFormatUrl(SecurityPageName.case); - const { formatUrl: detectionsFormatUrl, search: detectionsUrlSearch } = useFormatUrl( - SecurityPageName.detections - ); + const { formatUrl } = useFormatUrl(CASES_APP_ID); + const { formatUrl: detectionsFormatUrl } = useFormatUrl(ALERTS_APP_ID); - const allCasesLink = getCaseUrl(search); + const allCasesLink = getCaseUrl(); const formattedAllCasesLink = formatUrl(allCasesLink); const backToAllCasesOnClick = useCallback( (ev) => { @@ -154,14 +81,14 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = const onConfigureCasesNavClick = useCallback( (ev) => { ev.preventDefault(); - history.push(getConfigureCasesUrl(search)); + history.push(getConfigureCasesUrl()); }, - [history, search] + [history] ); const onDetectionsRuleDetailsClick = useCallback( (ruleId: string | null | undefined) => { - navigateToApp(`${APP_ID}:${SecurityPageName.detections}`, { + navigateToApp(`observability-alerts`, { path: getRuleDetailsUrl(ruleId ?? ''), }); }, @@ -170,87 +97,44 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = const getDetectionsRuleDetailsHref = useCallback( (ruleId) => { - return detectionsFormatUrl(getRuleDetailsUrl(ruleId ?? '', detectionsUrlSearch)); + return detectionsFormatUrl(getRuleDetailsUrl(ruleId ?? '')); }, - [detectionsFormatUrl, detectionsUrlSearch] + [detectionsFormatUrl] ); - const showAlertDetails = useCallback( - (alertId: string, index: string) => { - dispatch( - timelineActions.toggleDetailPanel({ - panelView: 'eventDetail', - timelineId: TimelineId.casePage, - params: { - eventId: alertId, - indexName: index, - }, - }) - ); - }, - [dispatch] - ); + const showAlertDetails = useCallback((alertId: string, index: string) => { + // TO DO show alert details (in timeline on security solution) + }, []); - const onComponentInitialized = useCallback(() => { - dispatch( - timelineActions.createTimeline({ - id: TimelineId.casePage, - columns: [], - indexNames: [], - expandedDetail: {}, - show: false, - }) - ); - }, [dispatch]); - return ( - <> - {casesUi.getCaseView({ - allCasesNavigation: { - href: formattedAllCasesLink, - onClick: backToAllCasesOnClick, - }, - caseDetailsNavigation: { - href: caseDetailsLink, - onClick: () => { - navigateToApp(`${APP_ID}:${SecurityPageName.case}`, { - path: getCaseDetailsUrl({ id: caseId }), - }); - }, - }, - caseId, - configureCasesNavigation: { - href: configureCasesHref, - onClick: onConfigureCasesNavClick, - }, - getCaseDetailHrefWithCommentId, - onCaseDataSuccess, - onComponentInitialized, - ruleDetailsNavigation: { - href: getDetectionsRuleDetailsHref, - onClick: onDetectionsRuleDetailsClick, - }, - showAlertDetails, - subCaseId, - timelineIntegration: { - editor_plugins: { - parsingPlugin: timelineMarkdownPlugin.parser, - processingPluginRenderer: timelineMarkdownPlugin.renderer, - uiPlugin: timelineMarkdownPlugin.plugin, - }, - hooks: { - useInsertTimeline, - }, - ui: { - renderInvestigateInTimelineActionComponent: InvestigateInTimelineActionComponent, - renderTimelineDetailsPanel: TimelineDetailsPanel, - }, - }, - useFetchAlertData, - userCanCrud, - })} - - - ); + return casesUi.getCaseView({ + allCasesNavigation: { + href: formattedAllCasesLink, + onClick: backToAllCasesOnClick, + }, + caseDetailsNavigation: { + href: caseDetailsLink, + onClick: () => { + navigateToApp(`${CASES_APP_ID}`, { + path: getCaseDetailsUrl({ id: caseId }), + }); + }, + }, + caseId, + configureCasesNavigation: { + href: configureCasesHref, + onClick: onConfigureCasesNavClick, + }, + getCaseDetailHrefWithCommentId, + onCaseDataSuccess, + ruleDetailsNavigation: { + href: getDetectionsRuleDetailsHref, + onClick: onDetectionsRuleDetailsClick, + }, + showAlertDetails, + subCaseId, + useFetchAlertData, + userCanCrud, + }); }); CaseView.displayName = 'CaseView'; diff --git a/x-pack/plugins/observability/public/components/app/cases/case_view/translations.ts b/x-pack/plugins/observability/public/components/app/cases/case_view/translations.ts index d7b66bbac38dfc..f734ff8e3be7ec 100644 --- a/x-pack/plugins/observability/public/components/app/cases/case_view/translations.ts +++ b/x-pack/plugins/observability/public/components/app/cases/case_view/translations.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; export const SEND_ALERT_TO_TIMELINE = i18n.translate( - 'xpack.securitySolution.cases.caseView.sendAlertToTimelineTooltip', + 'xpack.observability.cases.caseView.sendAlertToTimelineTooltip', { defaultMessage: 'Investigate in timeline', } diff --git a/x-pack/plugins/observability/public/components/app/cases/constants.ts b/x-pack/plugins/observability/public/components/app/cases/constants.ts index 82b76b082101c8..fe268ed81b96cd 100644 --- a/x-pack/plugins/observability/public/components/app/cases/constants.ts +++ b/x-pack/plugins/observability/public/components/app/cases/constants.ts @@ -6,4 +6,5 @@ */ export const CASES_APP_ID = 'observability-cases'; +export const ALERTS_APP_ID = 'observability-alerts'; export const CASES_OWNER = 'observability'; diff --git a/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx b/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx index 2c5b20bca42127..078174f9bc1e22 100644 --- a/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx @@ -9,10 +9,10 @@ import React, { memo } from 'react'; import styled from 'styled-components'; import { EuiFlyout, EuiFlyoutHeader, EuiTitle, EuiFlyoutBody } from '@elastic/eui'; -import * as i18n from '../../translations'; -import { useKibana } from '../../../common/lib/kibana'; -import { Case } from '../../../../../cases/common'; -import { APP_ID } from '../../../../common/constants'; +import * as i18n from '../translations'; +import { Case } from '../../../../../../cases/common'; +import { CASES_OWNER } from '../constants'; +import { useKibana } from '../../../../utils/kibana_react'; export interface CreateCaseModalProps { afterCaseCreated?: (theCase: Case) => Promise; @@ -65,7 +65,7 @@ function CreateCaseFlyoutComponent({ onCancel: onCloseFlyout, onSuccess, withSteps: false, - owner: [APP_ID], + owner: [CASES_OWNER], })} diff --git a/x-pack/plugins/observability/public/components/app/cases/create/index.tsx b/x-pack/plugins/observability/public/components/app/cases/create/index.tsx index f946cefd3494c9..b1306e896c87ec 100644 --- a/x-pack/plugins/observability/public/components/app/cases/create/index.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/create/index.tsx @@ -9,11 +9,9 @@ import React, { useCallback } from 'react'; import { EuiPanel } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; -import { getCaseDetailsUrl } from '../../../common/components/link_to'; -import { useKibana } from '../../../common/lib/kibana'; -import * as timelineMarkdownPlugin from '../../../common/components/markdown_editor/plugins/timeline'; -import { useInsertTimeline } from '../use_insert_timeline'; -import { APP_ID } from '../../../../common/constants'; +import { useKibana } from '../../../../utils/kibana_react'; +import { getCaseDetailsUrl } from '../../../../pages/cases/links'; +import { CASES_OWNER } from '../constants'; export const Create = React.memo(() => { const { cases } = useKibana().services; @@ -34,17 +32,7 @@ export const Create = React.memo(() => { {cases.getCreateCase({ onCancel: handleSetIsCancel, onSuccess, - timelineIntegration: { - editor_plugins: { - parsingPlugin: timelineMarkdownPlugin.parser, - processingPluginRenderer: timelineMarkdownPlugin.renderer, - uiPlugin: timelineMarkdownPlugin.plugin, - }, - hooks: { - useInsertTimeline, - }, - }, - owner: [APP_ID], + owner: [CASES_OWNER], })} ); diff --git a/x-pack/plugins/observability/public/pages/cases/translations.ts b/x-pack/plugins/observability/public/components/app/cases/translations.ts similarity index 100% rename from x-pack/plugins/observability/public/pages/cases/translations.ts rename to x-pack/plugins/observability/public/components/app/cases/translations.ts diff --git a/x-pack/plugins/observability/public/hooks/use_messages_storage.tsx b/x-pack/plugins/observability/public/hooks/use_messages_storage.tsx new file mode 100644 index 00000000000000..7063fdeeeb8df0 --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_messages_storage.tsx @@ -0,0 +1,65 @@ +/* + * 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 { useCallback } from 'react'; +import { useKibana } from '../utils/kibana_react'; + +export interface UseMessagesStorage { + getMessages: (plugin: string) => string[]; + addMessage: (plugin: string, id: string) => void; + removeMessage: (plugin: string, id: string) => void; + clearAllMessages: (plugin: string) => void; + hasMessage: (plugin: string, id: string) => boolean; +} + +export const useMessagesStorage = (): UseMessagesStorage => { + // @ts-ignore + const { storage } = useKibana().services; + console.log('STORAGE', storage); + + const getMessages = useCallback( + (plugin: string): string[] => storage.get(`${plugin}-messages`) ?? [], + [storage] + ); + + const addMessage = useCallback( + (plugin: string, id: string) => { + const pluginStorage = storage.get(`${plugin}-messages`) ?? []; + storage.set(`${plugin}-messages`, [...pluginStorage, id]); + }, + [storage] + ); + + const hasMessage = useCallback( + (plugin: string, id: string): boolean => { + const pluginStorage = storage.get(`${plugin}-messages`) ?? []; + return pluginStorage.filter((val: string) => val === id).length > 0; + }, + [storage] + ); + + const removeMessage = useCallback( + (plugin: string, id: string) => { + const pluginStorage = storage.get(`${plugin}-messages`) ?? []; + storage.set(`${plugin}-messages`, [...pluginStorage.filter((val: string) => val !== id)]); + }, + [storage] + ); + + const clearAllMessages = useCallback( + (plugin: string): string[] => storage.remove(`${plugin}-messages`), + [storage] + ); + + return { + getMessages, + addMessage, + clearAllMessages, + removeMessage, + hasMessage, + }; +}; diff --git a/x-pack/plugins/observability/public/pages/cases/all_cases.tsx b/x-pack/plugins/observability/public/pages/cases/all_cases.tsx index bd73a85bbeefa9..4cd422b3e3a7bd 100644 --- a/x-pack/plugins/observability/public/pages/cases/all_cases.tsx +++ b/x-pack/plugins/observability/public/pages/cases/all_cases.tsx @@ -6,9 +6,12 @@ */ import React from 'react'; +import { EuiPageTemplate } from '@elastic/eui'; // import { useGetUserCasesPermissions } from '../../common/lib/kibana'; import { AllCases } from '../../components/app/cases/all_cases'; +import * as i18n from '../../components/app/cases/translations'; +import { ExperimentalBadge } from '../../components/shared/experimental_badge'; // import { savedObjectReadOnlyErrorMessage, CaseCallOut } from '../../components/app/cases/callout'; // import { CaseSavedObjectNoPermissions } from './saved_object_no_permissions'; @@ -25,7 +28,17 @@ export const AllCasesPage = React.memo(() => { {/* messages={[{ ...savedObjectReadOnlyErrorMessage, title: '' }]}*/} {/* />*/} {/* )}*/} - + + {i18n.PAGE_TITLE} + + ), + }} + > + + ); // ) diff --git a/x-pack/plugins/observability/public/pages/cases/case_details.tsx b/x-pack/plugins/observability/public/pages/cases/case_details.tsx index 6b357360405cdb..cf8996bb13c8ad 100644 --- a/x-pack/plugins/observability/public/pages/cases/case_details.tsx +++ b/x-pack/plugins/observability/public/pages/cases/case_details.tsx @@ -6,9 +6,35 @@ */ import React from 'react'; +import { useHistory, useParams } from 'react-router-dom'; +// import { useGetUserCasesPermissions } from '../../../../security_solution/public/common/lib/kibana'; + +import { CaseView } from '../../components/app/cases/case_view'; export const CaseDetailsPage = React.memo(() => { - return

{`Case Details Page`}

; + const history = useHistory(); + // const userPermissions = useGetUserCasesPermissions(); + const { detailName: caseId, subCaseId } = useParams<{ + detailName?: string; + subCaseId?: string; + }>(); + // + // if (userPermissions != null && !userPermissions.read) { + // history.replace(getCaseUrl(search)); + // return null; + // } + + return caseId != null ? ( + <> + {/* {userPermissions != null && !userPermissions?.crud && userPermissions?.read && (*/} + {/* */} + {/* )}*/} + + + ) : null; }); CaseDetailsPage.displayName = 'CaseDetailsPage'; diff --git a/x-pack/plugins/observability/public/pages/cases/create_case.tsx b/x-pack/plugins/observability/public/pages/cases/create_case.tsx index e24321b4148cc4..2f2213d496fae5 100644 --- a/x-pack/plugins/observability/public/pages/cases/create_case.tsx +++ b/x-pack/plugins/observability/public/pages/cases/create_case.tsx @@ -5,10 +5,61 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback, useMemo } from 'react'; +import { useHistory } from 'react-router-dom'; +import { EuiButtonEmpty, EuiPageTemplate } from '@elastic/eui'; +import styled from 'styled-components'; +import * as i18n from '../../components/app/cases/translations'; +import { Create } from '../../components/app/cases/create'; +import { ExperimentalBadge } from '../../components/shared/experimental_badge'; +import { getCaseUrl } from './links'; +const ButtonEmpty = styled(EuiButtonEmpty)` + display: block; +`; +ButtonEmpty.displayName = 'ButtonEmpty'; export const CreateCasePage = React.memo(() => { - return

{`Create Case`}

; + const history = useHistory(); + // const userPermissions = useGetUserCasesPermissions(); + + const backOptions = useMemo( + () => ({ + href: getCaseUrl(), + text: i18n.BACK_TO_ALL, + }), + [] + ); + const goTo = useCallback( + (ev) => { + ev.preventDefault(); + if (backOptions) { + history.push(backOptions.href ?? ''); + } + }, + [backOptions, history] + ); + + // if (userPermissions != null && !userPermissions.crud) { + // history.replace(getCaseUrl(search)); + // return null; + // } + + return ( + + + {backOptions.text} + + {i18n.CREATE_TITLE} + + ), + }} + > + + + ); }); CreateCasePage.displayName = 'CreateCasePage'; diff --git a/x-pack/plugins/observability/public/pages/cases/links.ts b/x-pack/plugins/observability/public/pages/cases/links.ts index fd89864aa6d233..ddc70ef4303efb 100644 --- a/x-pack/plugins/observability/public/pages/cases/links.ts +++ b/x-pack/plugins/observability/public/pages/cases/links.ts @@ -8,7 +8,6 @@ import { useCallback } from 'react'; import { isEmpty } from 'lodash/fp'; import { useKibana } from '../../utils/kibana_react'; -import { CASES_APP_ID } from '../../components/app/cases/constants'; export const getCaseDetailsUrl = ({ id, subCaseId }: { id: string; subCaseId?: string }) => { if (subCaseId) { @@ -16,20 +15,23 @@ export const getCaseDetailsUrl = ({ id, subCaseId }: { id: string; subCaseId?: s } return `/${encodeURIComponent(id)}`; }; +interface FormatUrlOptions { + absolute: boolean; +} -export type FormatUrl = (path: string) => string; -export const useFormatUrl = () => { +export type FormatUrl = (path: string, options?: Partial) => string; +export const useFormatUrl = (appId: string) => { const { getUrlForApp } = useKibana().services.application; const formatUrl = useCallback( - (path: string) => { + (path: string, { absolute = false } = {}) => { const pathArr = path.split('?'); const formattedPath = `${pathArr[0]}${isEmpty(pathArr[1]) ? '' : `?${pathArr[1]}`}`; - return getUrlForApp(`${CASES_APP_ID}`, { + return getUrlForApp(`${appId}`, { path: formattedPath, - absolute: false, + absolute, }); }, - [getUrlForApp] + [appId, getUrlForApp] ); return { formatUrl }; }; @@ -54,3 +56,5 @@ export const getCaseDetailsUrlWithCommentId = ({ export const getCreateCaseUrl = () => `/create`; export const getConfigureCasesUrl = () => `/configure`; +export const getCaseUrl = () => `/cases`; +export const getRuleDetailsUrl = (detailName: string) => `/rules/id/${detailName}`; diff --git a/x-pack/plugins/observability/public/pages/cases/saved_object_no_permissions.tsx b/x-pack/plugins/observability/public/pages/cases/saved_object_no_permissions.tsx index 2953f1518a1f87..5e087909e43aee 100644 --- a/x-pack/plugins/observability/public/pages/cases/saved_object_no_permissions.tsx +++ b/x-pack/plugins/observability/public/pages/cases/saved_object_no_permissions.tsx @@ -8,7 +8,7 @@ import React, { useMemo } from 'react'; import { EmptyPage } from './empty_page'; -import * as i18n from './translations'; +import * as i18n from '../../components/app/cases/translations'; import { useKibana } from '../../utils/kibana_react'; export const CaseSavedObjectNoPermissions = React.memo(() => { diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index 115282aaaf1102..3c3bdcd31413f5 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -88,7 +88,7 @@ export class Plugin const { renderApp } = await import('./application'); // Get start services const [coreStart, pluginsStart] = await coreSetup.getStartServices(); - + console.log('pluginsStart', pluginsStart); return renderApp({ config, core: coreStart, From 722649eb26544019711bdb9236125a5c5d51048a Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 18 May 2021 15:18:32 -0600 Subject: [PATCH 068/113] i made a case --- .../public/components/app/cases/all_cases/index.tsx | 4 ++-- x-pack/plugins/observability/public/plugin.ts | 1 - x-pack/plugins/observability/server/plugin.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx b/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx index f22186406176f1..cbc1e7abd3c9da 100644 --- a/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx @@ -15,7 +15,7 @@ import { useFormatUrl, } from '../../../../pages/cases/links'; import { useKibana } from '../../../../utils/kibana_react'; -import { CASES_APP_ID } from '../constants'; +import { CASES_APP_ID, CASES_OWNER } from '../constants'; export interface AllCasesNavProps { detailName: string; @@ -73,7 +73,7 @@ export const AllCases = React.memo(({ userCanCrud }) => { }, showTitle: false, userCanCrud, - owner: [CASES_APP_ID], + owner: [CASES_OWNER], }); }); diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index 3c3bdcd31413f5..2fc73839d85ab2 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -88,7 +88,6 @@ export class Plugin const { renderApp } = await import('./application'); // Get start services const [coreStart, pluginsStart] = await coreSetup.getStartServices(); - console.log('pluginsStart', pluginsStart); return renderApp({ config, core: coreStart, diff --git a/x-pack/plugins/observability/server/plugin.ts b/x-pack/plugins/observability/server/plugin.ts index 954046fc87dfbb..15b2d89da01ade 100644 --- a/x-pack/plugins/observability/server/plugin.ts +++ b/x-pack/plugins/observability/server/plugin.ts @@ -56,7 +56,7 @@ export class ObservabilityPlugin implements Plugin { public setup(core: CoreSetup, plugins: PluginSetup) { plugins.features.registerKibanaFeature({ id: OBS_CASES_ID, - name: i18n.translate('xpack.observability.featureRegistry.linkSecuritySolutionTitle', { + name: i18n.translate('xpack.observability.featureRegistry.linkObservabilityTitle', { defaultMessage: 'Cases', }), order: 1100, From 85e3de7fb9c3fa77023308e7b7ab0f518162cb19 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 19 May 2021 13:04:32 -0600 Subject: [PATCH 069/113] cases in obs --- .../components/app/cases/all_cases/index.tsx | 7 ++- .../components/app/cases/create/index.tsx | 15 +++-- .../public/pages/cases/case_details.tsx | 1 - .../public/pages/cases/configure_cases.tsx | 57 +++++++++++++++++-- .../observability/public/routes/index.tsx | 22 +++++-- 5 files changed, 82 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx b/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx index cbc1e7abd3c9da..2b30ee3c08bc17 100644 --- a/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx @@ -31,7 +31,6 @@ export const AllCases = React.memo(({ userCanCrud }) => { cases: casesUi, application: { navigateToApp }, } = useKibana().services; - const history = useHistory(); const { formatUrl } = useFormatUrl(CASES_APP_ID); const goToCreateCase = useCallback( @@ -47,9 +46,11 @@ export const AllCases = React.memo(({ userCanCrud }) => { const goToCaseConfigure = useCallback( (ev) => { ev.preventDefault(); - history.push(getConfigureCasesUrl()); + navigateToApp(`${CASES_APP_ID}`, { + path: getConfigureCasesUrl(), + }); }, - [history] + [navigateToApp] ); return casesUi.getAllCases({ diff --git a/x-pack/plugins/observability/public/components/app/cases/create/index.tsx b/x-pack/plugins/observability/public/components/app/cases/create/index.tsx index b1306e896c87ec..0200a4791f43d0 100644 --- a/x-pack/plugins/observability/public/components/app/cases/create/index.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/create/index.tsx @@ -10,17 +10,22 @@ import { EuiPanel } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; import { useKibana } from '../../../../utils/kibana_react'; -import { getCaseDetailsUrl } from '../../../../pages/cases/links'; -import { CASES_OWNER } from '../constants'; +import { getCaseDetailsUrl, getCreateCaseUrl } from '../../../../pages/cases/links'; +import { CASES_APP_ID, CASES_OWNER } from '../constants'; export const Create = React.memo(() => { - const { cases } = useKibana().services; + const { + cases, + application: { navigateToApp }, + } = useKibana().services; const history = useHistory(); const onSuccess = useCallback( async ({ id }) => { - history.push(getCaseDetailsUrl({ id })); + navigateToApp(`${CASES_APP_ID}`, { + path: getCaseDetailsUrl({ id }), + }); }, - [history] + [navigateToApp] ); const handleSetIsCancel = useCallback(() => { diff --git a/x-pack/plugins/observability/public/pages/cases/case_details.tsx b/x-pack/plugins/observability/public/pages/cases/case_details.tsx index cf8996bb13c8ad..65b28e17d5287a 100644 --- a/x-pack/plugins/observability/public/pages/cases/case_details.tsx +++ b/x-pack/plugins/observability/public/pages/cases/case_details.tsx @@ -12,7 +12,6 @@ import { useHistory, useParams } from 'react-router-dom'; import { CaseView } from '../../components/app/cases/case_view'; export const CaseDetailsPage = React.memo(() => { - const history = useHistory(); // const userPermissions = useGetUserCasesPermissions(); const { detailName: caseId, subCaseId } = useParams<{ detailName?: string; diff --git a/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx b/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx index 139c2f657ec456..2c53afe28c1e00 100644 --- a/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx +++ b/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx @@ -5,10 +5,57 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback } from 'react'; +import styled from 'styled-components'; -export const ConfigureCasesPage = React.memo(() => { - return

{`Configure Cases Page`}

; -}); +import { EuiButtonEmpty, EuiPageTemplate } from '@elastic/eui'; +import * as i18n from '../../components/app/cases/translations'; +import { ExperimentalBadge } from '../../components/shared/experimental_badge'; +import { CASES_APP_ID, CASES_OWNER } from '../../components/app/cases/constants'; +import { useKibana } from '../../utils/kibana_react'; -ConfigureCasesPage.displayName = 'ConfigureCasesPage'; +const ButtonEmpty = styled(EuiButtonEmpty)` + display: block; +`; +function ConfigureCasesPageComponent() { + const { + cases, + application: { navigateToApp }, + } = useKibana().services; + // const userPermissions = useGetUserCasesPermissions(); + + const goTo = useCallback( + (ev) => { + ev.preventDefault(); + navigateToApp(`${CASES_APP_ID}`); + }, + [navigateToApp] + ); + + // if (userPermissions != null && !userPermissions.read) { + // navigateToApp(`${CASES_APP_ID}`); + // return null; + // } + + return ( + + + {i18n.BACK_TO_ALL} + + {i18n.CONFIGURE_CASES_PAGE_TITLE} + + ), + }} + > + {cases.getConfigureCases({ + userCanCrud: true, // userPermissions?.crud ?? false, + owner: [CASES_OWNER], + })} + + ); +} + +export const ConfigureCasesPage = React.memo(ConfigureCasesPageComponent); diff --git a/x-pack/plugins/observability/public/routes/index.tsx b/x-pack/plugins/observability/public/routes/index.tsx index 5fccece577cd7b..2b15149f72ef20 100644 --- a/x-pack/plugins/observability/public/routes/index.tsx +++ b/x-pack/plugins/observability/public/routes/index.tsx @@ -16,6 +16,7 @@ import { AlertsPage } from '../pages/alerts'; import { CreateCasePage } from '../pages/cases/create_case'; import { ExploratoryViewPage } from '../components/shared/exploratory_view'; import { CaseDetailsPage } from '../pages/cases/case_details'; +import { ConfigureCasesPage } from '../pages/cases/configure_cases'; import { AllCasesPage } from '../pages/cases/all_cases'; export type RouteParams = DecodeParams; @@ -96,14 +97,23 @@ export const routes = { ], }, '/cases/create': { - handler: (routeParams: any) => { - return ; + handler: () => { + return ; }, - params: { - path: t.partial({ - detailName: t.string, - }), + params: {}, + breadcrumb: [ + { + text: i18n.translate('xpack.observability.cases.breadcrumb', { + defaultMessage: 'Cases', + }), + }, + ], + }, + '/cases/configure': { + handler: () => { + return ; }, + params: {}, breadcrumb: [ { text: i18n.translate('xpack.observability.cases.breadcrumb', { From 1c7b61327a87255425b1ac94c622d87111e61549 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 20 May 2021 11:58:48 -0600 Subject: [PATCH 070/113] add back in user permissions --- .../hooks/use_get_cases_user_permissions.tsx | 36 +++++++++++++++++ .../public/pages/cases/all_cases.tsx | 32 +++++++-------- .../public/pages/cases/case_details.tsx | 40 ++++++++++++------- .../public/pages/cases/cases.stories.tsx | 7 ++-- .../public/pages/cases/configure_cases.tsx | 13 +++--- .../public/pages/cases/create_case.tsx | 36 +++++++---------- .../observability/public/routes/index.tsx | 10 ++--- 7 files changed, 103 insertions(+), 71 deletions(-) create mode 100644 x-pack/plugins/observability/public/hooks/use_get_cases_user_permissions.tsx diff --git a/x-pack/plugins/observability/public/hooks/use_get_cases_user_permissions.tsx b/x-pack/plugins/observability/public/hooks/use_get_cases_user_permissions.tsx new file mode 100644 index 00000000000000..1d97f739415657 --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_get_cases_user_permissions.tsx @@ -0,0 +1,36 @@ +/* + * 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 { useEffect, useState } from 'react'; +import { useKibana } from '../utils/kibana_react'; + +export interface UseGetUserCasesPermissions { + crud: boolean; + read: boolean; +} + +export const useGetUserCasesPermissions = () => { + const [casesPermissions, setCasesPermissions] = useState(null); + const uiCapabilities = useKibana().services.application.capabilities; + + useEffect(() => { + const capabilitiesCanUserCRUD: boolean = + typeof uiCapabilities.observabilityCases.crud_cases === 'boolean' + ? uiCapabilities.observabilityCases.crud_cases + : false; + const capabilitiesCanUserRead: boolean = + typeof uiCapabilities.observabilityCases.read_cases === 'boolean' + ? uiCapabilities.observabilityCases.read_cases + : false; + setCasesPermissions({ + crud: capabilitiesCanUserCRUD, + read: capabilitiesCanUserRead, + }); + }, [uiCapabilities]); + + return casesPermissions; +}; diff --git a/x-pack/plugins/observability/public/pages/cases/all_cases.tsx b/x-pack/plugins/observability/public/pages/cases/all_cases.tsx index 4cd422b3e3a7bd..a0beb4e8c7898b 100644 --- a/x-pack/plugins/observability/public/pages/cases/all_cases.tsx +++ b/x-pack/plugins/observability/public/pages/cases/all_cases.tsx @@ -8,26 +8,24 @@ import React from 'react'; import { EuiPageTemplate } from '@elastic/eui'; -// import { useGetUserCasesPermissions } from '../../common/lib/kibana'; import { AllCases } from '../../components/app/cases/all_cases'; import * as i18n from '../../components/app/cases/translations'; import { ExperimentalBadge } from '../../components/shared/experimental_badge'; -// import { savedObjectReadOnlyErrorMessage, CaseCallOut } from '../../components/app/cases/callout'; -// import { CaseSavedObjectNoPermissions } from './saved_object_no_permissions'; +import { savedObjectReadOnlyErrorMessage, CaseCallOut } from '../../components/app/cases/callout'; +import { CaseSavedObjectNoPermissions } from './saved_object_no_permissions'; +import { useGetUserCasesPermissions } from '../../hooks/use_get_cases_user_permissions'; export const AllCasesPage = React.memo(() => { - // const userPermissions = useGetUserCasesPermissions(); - - // userPermissions == null || userPermissions?.read ? ( - return ( + const userPermissions = useGetUserCasesPermissions(); + return userPermissions == null || userPermissions?.read ? ( <> - {/* {userPermissions != null && !userPermissions?.crud && userPermissions?.read && (*/} - {/* */} - {/* )}*/} + {userPermissions != null && !userPermissions?.crud && userPermissions?.read && ( + + )} { ), }} > - + + ) : ( + ); - // ) - // : ( - // - // ); }); AllCasesPage.displayName = 'AllCasesPage'; diff --git a/x-pack/plugins/observability/public/pages/cases/case_details.tsx b/x-pack/plugins/observability/public/pages/cases/case_details.tsx index 65b28e17d5287a..ed1887e14753e3 100644 --- a/x-pack/plugins/observability/public/pages/cases/case_details.tsx +++ b/x-pack/plugins/observability/public/pages/cases/case_details.tsx @@ -6,32 +6,42 @@ */ import React from 'react'; -import { useHistory, useParams } from 'react-router-dom'; -// import { useGetUserCasesPermissions } from '../../../../security_solution/public/common/lib/kibana'; +import { useParams } from 'react-router-dom'; import { CaseView } from '../../components/app/cases/case_view'; +import { useGetUserCasesPermissions } from '../../hooks/use_get_cases_user_permissions'; +import { useKibana } from '../../utils/kibana_react'; +import { CASES_APP_ID } from '../../components/app/cases/constants'; +import { CaseCallOut, savedObjectReadOnlyErrorMessage } from '../../components/app/cases/callout'; export const CaseDetailsPage = React.memo(() => { - // const userPermissions = useGetUserCasesPermissions(); + const { + application: { navigateToApp }, + } = useKibana().services; + const userPermissions = useGetUserCasesPermissions(); const { detailName: caseId, subCaseId } = useParams<{ detailName?: string; subCaseId?: string; }>(); - // - // if (userPermissions != null && !userPermissions.read) { - // history.replace(getCaseUrl(search)); - // return null; - // } + + if (userPermissions != null && !userPermissions.read) { + navigateToApp(`${CASES_APP_ID}`); + return null; + } return caseId != null ? ( <> - {/* {userPermissions != null && !userPermissions?.crud && userPermissions?.read && (*/} - {/* */} - {/* )}*/} - + {userPermissions != null && !userPermissions?.crud && userPermissions?.read && ( + + )} + ) : null; }); diff --git a/x-pack/plugins/observability/public/pages/cases/cases.stories.tsx b/x-pack/plugins/observability/public/pages/cases/cases.stories.tsx index 49df932766b335..13d8795193238d 100644 --- a/x-pack/plugins/observability/public/pages/cases/cases.stories.tsx +++ b/x-pack/plugins/observability/public/pages/cases/cases.stories.tsx @@ -6,12 +6,11 @@ */ import React, { ComponentType } from 'react'; -import { CasesPage } from '.'; -import { RouteParams } from '../../routes'; +import { AllCasesPage } from './all_cases'; export default { title: 'app/Cases', - component: CasesPage, + component: AllCasesPage, decorators: [ (Story: ComponentType) => { return ; @@ -20,5 +19,5 @@ export default { }; export function EmptyState() { - return } />; + return ; } diff --git a/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx b/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx index 2c53afe28c1e00..ccfd455c9bc8f3 100644 --- a/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx +++ b/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx @@ -13,6 +13,7 @@ import * as i18n from '../../components/app/cases/translations'; import { ExperimentalBadge } from '../../components/shared/experimental_badge'; import { CASES_APP_ID, CASES_OWNER } from '../../components/app/cases/constants'; import { useKibana } from '../../utils/kibana_react'; +import { useGetUserCasesPermissions } from '../../hooks/use_get_cases_user_permissions'; const ButtonEmpty = styled(EuiButtonEmpty)` display: block; @@ -22,7 +23,7 @@ function ConfigureCasesPageComponent() { cases, application: { navigateToApp }, } = useKibana().services; - // const userPermissions = useGetUserCasesPermissions(); + const userPermissions = useGetUserCasesPermissions(); const goTo = useCallback( (ev) => { @@ -32,10 +33,10 @@ function ConfigureCasesPageComponent() { [navigateToApp] ); - // if (userPermissions != null && !userPermissions.read) { - // navigateToApp(`${CASES_APP_ID}`); - // return null; - // } + if (userPermissions != null && !userPermissions.read) { + navigateToApp(`${CASES_APP_ID}`); + return null; + } return ( {cases.getConfigureCases({ - userCanCrud: true, // userPermissions?.crud ?? false, + userCanCrud: userPermissions?.crud ?? false, owner: [CASES_OWNER], })} diff --git a/x-pack/plugins/observability/public/pages/cases/create_case.tsx b/x-pack/plugins/observability/public/pages/cases/create_case.tsx index 2f2213d496fae5..42c96047fbc278 100644 --- a/x-pack/plugins/observability/public/pages/cases/create_case.tsx +++ b/x-pack/plugins/observability/public/pages/cases/create_case.tsx @@ -5,44 +5,38 @@ * 2.0. */ -import React, { useCallback, useMemo } from 'react'; -import { useHistory } from 'react-router-dom'; +import React, { useCallback } from 'react'; import { EuiButtonEmpty, EuiPageTemplate } from '@elastic/eui'; import styled from 'styled-components'; import * as i18n from '../../components/app/cases/translations'; import { Create } from '../../components/app/cases/create'; import { ExperimentalBadge } from '../../components/shared/experimental_badge'; -import { getCaseUrl } from './links'; +import { CASES_APP_ID } from '../../components/app/cases/constants'; +import { useKibana } from '../../utils/kibana_react'; +import { useGetUserCasesPermissions } from '../../hooks/use_get_cases_user_permissions'; const ButtonEmpty = styled(EuiButtonEmpty)` display: block; `; ButtonEmpty.displayName = 'ButtonEmpty'; export const CreateCasePage = React.memo(() => { - const history = useHistory(); - // const userPermissions = useGetUserCasesPermissions(); + const userPermissions = useGetUserCasesPermissions(); + const { + application: { navigateToApp }, + } = useKibana().services; - const backOptions = useMemo( - () => ({ - href: getCaseUrl(), - text: i18n.BACK_TO_ALL, - }), - [] - ); const goTo = useCallback( (ev) => { ev.preventDefault(); - if (backOptions) { - history.push(backOptions.href ?? ''); - } + navigateToApp(`${CASES_APP_ID}`); }, - [backOptions, history] + [navigateToApp] ); - // if (userPermissions != null && !userPermissions.crud) { - // history.replace(getCaseUrl(search)); - // return null; - // } + if (userPermissions != null && !userPermissions.crud) { + navigateToApp(`${CASES_APP_ID}`); + return null; + } return ( { pageTitle: ( <> - {backOptions.text} + {i18n.BACK_TO_ALL} {i18n.CREATE_TITLE} diff --git a/x-pack/plugins/observability/public/routes/index.tsx b/x-pack/plugins/observability/public/routes/index.tsx index 2b15149f72ef20..76da497d823585 100644 --- a/x-pack/plugins/observability/public/routes/index.tsx +++ b/x-pack/plugins/observability/public/routes/index.tsx @@ -80,14 +80,10 @@ export const routes = { ], }, '/cases': { - handler: (routeParams: any) => { - return ; - }, - params: { - path: t.partial({ - detailName: t.string, - }), + handler: () => { + return ; }, + params: {}, breadcrumb: [ { text: i18n.translate('xpack.observability.cases.breadcrumb', { From 66c1d4330a1a230a74cedd46f17901b9daf5a8f1 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Mon, 24 May 2021 18:29:01 -0400 Subject: [PATCH 071/113] Adding owner param for the status stats --- .../plugins/cases/common/api/cases/status.ts | 9 ++++ .../classes/client.casesclient.md | 28 +++++------ .../interfaces/attachments_add.addargs.md | 4 +- ...attachments_client.attachmentssubclient.md | 14 +++--- .../attachments_delete.deleteallargs.md | 4 +- .../attachments_delete.deleteargs.md | 6 +-- .../interfaces/attachments_get.findargs.md | 4 +- .../interfaces/attachments_get.getallargs.md | 6 +-- .../interfaces/attachments_get.getargs.md | 4 +- .../attachments_update.updateargs.md | 6 +-- .../interfaces/cases_client.casessubclient.md | 18 +++---- .../cases_get.caseidsbyalertidparams.md | 4 +- .../interfaces/cases_get.getparams.md | 6 +-- .../interfaces/cases_push.pushparams.md | 4 +- .../configure_client.configuresubclient.md | 8 +-- .../interfaces/stats_client.statssubclient.md | 11 +++- .../sub_cases_client.subcasesclient.md | 8 +-- .../user_actions_client.useractionget.md | 4 +- ...ser_actions_client.useractionssubclient.md | 2 +- .../docs/cases_client/modules/cases_get.md | 4 +- .../cases/public/containers/api.test.tsx | 5 +- x-pack/plugins/cases/public/containers/api.ts | 6 ++- .../containers/use_get_cases_status.tsx | 4 +- .../cases/server/client/stats/client.ts | 50 +++++++++++++++---- .../server/routes/api/stats/get_status.ts | 8 +-- .../case_api_integration/common/lib/utils.ts | 4 +- .../tests/common/cases/status/get_status.ts | 13 +++++ .../tests/common/cases/status/get_status.ts | 13 +++++ 28 files changed, 168 insertions(+), 89 deletions(-) diff --git a/x-pack/plugins/cases/common/api/cases/status.ts b/x-pack/plugins/cases/common/api/cases/status.ts index 7286e19da91592..d37e68007a21dd 100644 --- a/x-pack/plugins/cases/common/api/cases/status.ts +++ b/x-pack/plugins/cases/common/api/cases/status.ts @@ -27,4 +27,13 @@ export const CasesStatusResponseRt = rt.type({ count_closed_cases: rt.number, }); +export const CasesStatusRequestRt = rt.partial({ + /** + * The owner of the cases to retrieve the status stats from. If no owner is provided the stats for all cases + * that the user has access to will be returned. + */ + owner: rt.union([rt.array(rt.string), rt.string]), +}); + export type CasesStatusResponse = rt.TypeOf; +export type CasesStatusRequest = rt.TypeOf; diff --git a/x-pack/plugins/cases/docs/cases_client/classes/client.casesclient.md b/x-pack/plugins/cases/docs/cases_client/classes/client.casesclient.md index 8f6983dc4f769d..98e2f284da4a60 100644 --- a/x-pack/plugins/cases/docs/cases_client/classes/client.casesclient.md +++ b/x-pack/plugins/cases/docs/cases_client/classes/client.casesclient.md @@ -45,7 +45,7 @@ Client wrapper that contains accessor methods for individual entities within the **Returns:** [*CasesClient*](client.casesclient.md) -Defined in: [client.ts:28](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/client.ts#L28) +Defined in: [client.ts:28](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/client.ts#L28) ## Properties @@ -53,7 +53,7 @@ Defined in: [client.ts:28](https://github.com/jonathan-buttner/kibana/blob/085f8 • `Private` `Readonly` **\_attachments**: [*AttachmentsSubClient*](../interfaces/attachments_client.attachmentssubclient.md) -Defined in: [client.ts:24](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/client.ts#L24) +Defined in: [client.ts:24](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/client.ts#L24) ___ @@ -61,7 +61,7 @@ ___ • `Private` `Readonly` **\_cases**: [*CasesSubClient*](../interfaces/cases_client.casessubclient.md) -Defined in: [client.ts:23](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/client.ts#L23) +Defined in: [client.ts:23](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/client.ts#L23) ___ @@ -69,7 +69,7 @@ ___ • `Private` `Readonly` **\_casesClientInternal**: *CasesClientInternal* -Defined in: [client.ts:22](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/client.ts#L22) +Defined in: [client.ts:22](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/client.ts#L22) ___ @@ -77,7 +77,7 @@ ___ • `Private` `Readonly` **\_configure**: [*ConfigureSubClient*](../interfaces/configure_client.configuresubclient.md) -Defined in: [client.ts:27](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/client.ts#L27) +Defined in: [client.ts:27](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/client.ts#L27) ___ @@ -85,7 +85,7 @@ ___ • `Private` `Readonly` **\_stats**: [*StatsSubClient*](../interfaces/stats_client.statssubclient.md) -Defined in: [client.ts:28](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/client.ts#L28) +Defined in: [client.ts:28](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/client.ts#L28) ___ @@ -93,7 +93,7 @@ ___ • `Private` `Readonly` **\_subCases**: [*SubCasesClient*](../interfaces/sub_cases_client.subcasesclient.md) -Defined in: [client.ts:26](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/client.ts#L26) +Defined in: [client.ts:26](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/client.ts#L26) ___ @@ -101,7 +101,7 @@ ___ • `Private` `Readonly` **\_userActions**: [*UserActionsSubClient*](../interfaces/user_actions_client.useractionssubclient.md) -Defined in: [client.ts:25](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/client.ts#L25) +Defined in: [client.ts:25](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/client.ts#L25) ## Accessors @@ -113,7 +113,7 @@ Retrieves an interface for interacting with attachments (comments) entities. **Returns:** [*AttachmentsSubClient*](../interfaces/attachments_client.attachmentssubclient.md) -Defined in: [client.ts:50](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/client.ts#L50) +Defined in: [client.ts:50](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/client.ts#L50) ___ @@ -125,7 +125,7 @@ Retrieves an interface for interacting with cases entities. **Returns:** [*CasesSubClient*](../interfaces/cases_client.casessubclient.md) -Defined in: [client.ts:43](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/client.ts#L43) +Defined in: [client.ts:43](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/client.ts#L43) ___ @@ -137,7 +137,7 @@ Retrieves an interface for interacting with the configuration of external connec **Returns:** [*ConfigureSubClient*](../interfaces/configure_client.configuresubclient.md) -Defined in: [client.ts:76](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/client.ts#L76) +Defined in: [client.ts:76](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/client.ts#L76) ___ @@ -149,7 +149,7 @@ Retrieves an interface for retrieving statistics related to the cases entities. **Returns:** [*StatsSubClient*](../interfaces/stats_client.statssubclient.md) -Defined in: [client.ts:83](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/client.ts#L83) +Defined in: [client.ts:83](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/client.ts#L83) ___ @@ -163,7 +163,7 @@ Currently this functionality is disabled and will throw an error if this functio **Returns:** [*SubCasesClient*](../interfaces/sub_cases_client.subcasesclient.md) -Defined in: [client.ts:66](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/client.ts#L66) +Defined in: [client.ts:66](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/client.ts#L66) ___ @@ -175,4 +175,4 @@ Retrieves an interface for interacting with the user actions associated with the **Returns:** [*UserActionsSubClient*](../interfaces/user_actions_client.useractionssubclient.md) -Defined in: [client.ts:57](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/client.ts#L57) +Defined in: [client.ts:57](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/client.ts#L57) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_add.addargs.md b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_add.addargs.md index 0e67fb488edebb..1bbca9167a5c2d 100644 --- a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_add.addargs.md +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_add.addargs.md @@ -21,7 +21,7 @@ The arguments needed for creating a new attachment to a case. The case ID that this attachment will be associated with -Defined in: [attachments/add.ts:308](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/add.ts#L308) +Defined in: [attachments/add.ts:308](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/attachments/add.ts#L308) ___ @@ -31,4 +31,4 @@ ___ The attachment values. -Defined in: [attachments/add.ts:312](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/add.ts#L312) +Defined in: [attachments/add.ts:312](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/attachments/add.ts#L312) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_client.attachmentssubclient.md b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_client.attachmentssubclient.md index 13a7a5a109a511..e9f65bcf9915ab 100644 --- a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_client.attachmentssubclient.md +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_client.attachmentssubclient.md @@ -34,7 +34,7 @@ Adds an attachment to a case. **Returns:** *Promise*<[*ICaseResponse*](typedoc_interfaces.icaseresponse.md)\> -Defined in: [attachments/client.ts:25](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/client.ts#L25) +Defined in: [attachments/client.ts:25](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/attachments/client.ts#L25) ___ @@ -52,7 +52,7 @@ Deletes a single attachment for a specific case. **Returns:** *Promise* -Defined in: [attachments/client.ts:33](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/client.ts#L33) +Defined in: [attachments/client.ts:33](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/attachments/client.ts#L33) ___ @@ -70,7 +70,7 @@ Deletes all attachments associated with a single case. **Returns:** *Promise* -Defined in: [attachments/client.ts:29](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/client.ts#L29) +Defined in: [attachments/client.ts:29](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/attachments/client.ts#L29) ___ @@ -88,7 +88,7 @@ Retrieves all comments matching the search criteria. **Returns:** *Promise*<[*ICommentsResponse*](typedoc_interfaces.icommentsresponse.md)\> -Defined in: [attachments/client.ts:37](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/client.ts#L37) +Defined in: [attachments/client.ts:37](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/attachments/client.ts#L37) ___ @@ -106,7 +106,7 @@ Retrieves a single attachment for a case. **Returns:** *Promise*<{ `comment`: *string* ; `owner`: *string* ; `type`: user } & { `associationType`: AssociationType ; `created_at`: *string* ; `created_by`: { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `owner`: *string* ; `pushed_at`: ``null`` \| *string* ; `pushed_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `updated_at`: ``null`` \| *string* ; `updated_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } } & { `id`: *string* ; `version`: *string* } & { `alertId`: *string* \| *string*[] ; `index`: *string* \| *string*[] ; `owner`: *string* ; `rule`: { id: string \| null; name: string \| null; } ; `type`: alert \| generatedAlert } & { `associationType`: AssociationType ; `created_at`: *string* ; `created_by`: { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `owner`: *string* ; `pushed_at`: ``null`` \| *string* ; `pushed_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } ; `updated_at`: ``null`` \| *string* ; `updated_by`: ``null`` \| { email: string \| null \| undefined; full\_name: string \| null \| undefined; username: string \| null \| undefined; } } & { `id`: *string* ; `version`: *string* }\> -Defined in: [attachments/client.ts:45](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/client.ts#L45) +Defined in: [attachments/client.ts:45](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/attachments/client.ts#L45) ___ @@ -124,7 +124,7 @@ Gets all attachments for a single case. **Returns:** *Promise*<[*IAllCommentsResponse*](typedoc_interfaces.iallcommentsresponse.md)\> -Defined in: [attachments/client.ts:41](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/client.ts#L41) +Defined in: [attachments/client.ts:41](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/attachments/client.ts#L41) ___ @@ -144,4 +144,4 @@ The request must include all fields for the attachment. Even the fields that are **Returns:** *Promise*<[*ICaseResponse*](typedoc_interfaces.icaseresponse.md)\> -Defined in: [attachments/client.ts:51](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/client.ts#L51) +Defined in: [attachments/client.ts:51](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/attachments/client.ts#L51) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_delete.deleteallargs.md b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_delete.deleteallargs.md index a0f5962fcc453a..26b00ac6e037eb 100644 --- a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_delete.deleteallargs.md +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_delete.deleteallargs.md @@ -21,7 +21,7 @@ Parameters for deleting all comments of a case or sub case. The case ID to delete all attachments for -Defined in: [attachments/delete.ts:26](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/delete.ts#L26) +Defined in: [attachments/delete.ts:26](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/attachments/delete.ts#L26) ___ @@ -31,4 +31,4 @@ ___ If specified the caseID will be ignored and this value will be used to find a sub case for deleting all the attachments -Defined in: [attachments/delete.ts:30](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/delete.ts#L30) +Defined in: [attachments/delete.ts:30](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/attachments/delete.ts#L30) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_delete.deleteargs.md b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_delete.deleteargs.md index ab20f1b64b2a43..f9d4038eb417af 100644 --- a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_delete.deleteargs.md +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_delete.deleteargs.md @@ -22,7 +22,7 @@ Parameters for deleting a single attachment of a case or sub case. The attachment ID to delete -Defined in: [attachments/delete.ts:44](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/delete.ts#L44) +Defined in: [attachments/delete.ts:44](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/attachments/delete.ts#L44) ___ @@ -32,7 +32,7 @@ ___ The case ID to delete an attachment from -Defined in: [attachments/delete.ts:40](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/delete.ts#L40) +Defined in: [attachments/delete.ts:40](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/attachments/delete.ts#L40) ___ @@ -42,4 +42,4 @@ ___ If specified the caseID will be ignored and this value will be used to find a sub case for deleting the attachment -Defined in: [attachments/delete.ts:48](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/delete.ts#L48) +Defined in: [attachments/delete.ts:48](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/attachments/delete.ts#L48) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.findargs.md b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.findargs.md index 2a019220f82199..dbbac0065be854 100644 --- a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.findargs.md +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.findargs.md @@ -21,7 +21,7 @@ Parameters for finding attachments of a case The case ID for finding associated attachments -Defined in: [attachments/get.ts:48](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/get.ts#L48) +Defined in: [attachments/get.ts:48](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/attachments/get.ts#L48) ___ @@ -48,4 +48,4 @@ Optional parameters for filtering the returned attachments | `sortOrder` | *undefined* \| ``"desc"`` \| ``"asc"`` | | `subCaseId` | *undefined* \| *string* | -Defined in: [attachments/get.ts:52](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/get.ts#L52) +Defined in: [attachments/get.ts:52](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/attachments/get.ts#L52) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getallargs.md b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getallargs.md index c6f2123ee60562..dbd66291e22de8 100644 --- a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getallargs.md +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getallargs.md @@ -22,7 +22,7 @@ Parameters for retrieving all attachments of a case The case ID to retrieve all attachments for -Defined in: [attachments/get.ts:62](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/get.ts#L62) +Defined in: [attachments/get.ts:62](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/attachments/get.ts#L62) ___ @@ -32,7 +32,7 @@ ___ Optionally include the attachments associated with a sub case -Defined in: [attachments/get.ts:66](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/get.ts#L66) +Defined in: [attachments/get.ts:66](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/attachments/get.ts#L66) ___ @@ -42,4 +42,4 @@ ___ If included the case ID will be ignored and the attachments will be retrieved from the specified ID of the sub case -Defined in: [attachments/get.ts:70](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/get.ts#L70) +Defined in: [attachments/get.ts:70](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/attachments/get.ts#L70) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getargs.md b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getargs.md index ffec56fc54c83f..abfd4bb5958d33 100644 --- a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getargs.md +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_get.getargs.md @@ -19,7 +19,7 @@ The ID of the attachment to retrieve -Defined in: [attachments/get.ts:81](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/get.ts#L81) +Defined in: [attachments/get.ts:81](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/attachments/get.ts#L81) ___ @@ -29,4 +29,4 @@ ___ The ID of the case to retrieve an attachment from -Defined in: [attachments/get.ts:77](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/get.ts#L77) +Defined in: [attachments/get.ts:77](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/attachments/get.ts#L77) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_update.updateargs.md b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_update.updateargs.md index 083723d76b10e8..b571067175f62f 100644 --- a/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_update.updateargs.md +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/attachments_update.updateargs.md @@ -22,7 +22,7 @@ Parameters for updating a single attachment The ID of the case that is associated with this attachment -Defined in: [attachments/update.ts:29](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/update.ts#L29) +Defined in: [attachments/update.ts:29](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/attachments/update.ts#L29) ___ @@ -32,7 +32,7 @@ ___ The ID of a sub case, if specified a sub case will be searched for to perform the attachment update instead of on a case -Defined in: [attachments/update.ts:37](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/update.ts#L37) +Defined in: [attachments/update.ts:37](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/attachments/update.ts#L37) ___ @@ -42,4 +42,4 @@ ___ The full attachment request with the fields updated with appropriate values -Defined in: [attachments/update.ts:33](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/attachments/update.ts#L33) +Defined in: [attachments/update.ts:33](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/attachments/update.ts#L33) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/cases_client.casessubclient.md b/x-pack/plugins/cases/docs/cases_client/interfaces/cases_client.casessubclient.md index 14315890b4f963..e7d7dea34d0adf 100644 --- a/x-pack/plugins/cases/docs/cases_client/interfaces/cases_client.casessubclient.md +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/cases_client.casessubclient.md @@ -36,7 +36,7 @@ Creates a case. **Returns:** *Promise*<[*ICaseResponse*](typedoc_interfaces.icaseresponse.md)\> -Defined in: [cases/client.ts:48](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/cases/client.ts#L48) +Defined in: [cases/client.ts:48](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/cases/client.ts#L48) ___ @@ -56,7 +56,7 @@ Delete a case and all its comments. **Returns:** *Promise* -Defined in: [cases/client.ts:72](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/cases/client.ts#L72) +Defined in: [cases/client.ts:72](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/cases/client.ts#L72) ___ @@ -76,7 +76,7 @@ If the `owner` field is left empty then all the cases that the user has access t **Returns:** *Promise*<[*ICasesFindResponse*](typedoc_interfaces.icasesfindresponse.md)\> -Defined in: [cases/client.ts:54](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/cases/client.ts#L54) +Defined in: [cases/client.ts:54](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/cases/client.ts#L54) ___ @@ -94,7 +94,7 @@ Retrieves a single case with the specified ID. **Returns:** *Promise*<[*ICaseResponse*](typedoc_interfaces.icaseresponse.md)\> -Defined in: [cases/client.ts:58](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/cases/client.ts#L58) +Defined in: [cases/client.ts:58](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/cases/client.ts#L58) ___ @@ -112,7 +112,7 @@ Retrieves the case IDs given a single alert ID **Returns:** *Promise* -Defined in: [cases/client.ts:84](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/cases/client.ts#L84) +Defined in: [cases/client.ts:84](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/cases/client.ts#L84) ___ @@ -131,7 +131,7 @@ Retrieves all the reporters across all accessible cases. **Returns:** *Promise*<{ `email`: *undefined* \| ``null`` \| *string* ; `full_name`: *undefined* \| ``null`` \| *string* ; `username`: *undefined* \| ``null`` \| *string* }[]\> -Defined in: [cases/client.ts:80](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/cases/client.ts#L80) +Defined in: [cases/client.ts:80](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/cases/client.ts#L80) ___ @@ -150,7 +150,7 @@ Retrieves all the tags across all cases the user making the request has access t **Returns:** *Promise* -Defined in: [cases/client.ts:76](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/cases/client.ts#L76) +Defined in: [cases/client.ts:76](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/cases/client.ts#L76) ___ @@ -168,7 +168,7 @@ Pushes a specific case to an external system. **Returns:** *Promise*<[*ICaseResponse*](typedoc_interfaces.icaseresponse.md)\> -Defined in: [cases/client.ts:62](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/cases/client.ts#L62) +Defined in: [cases/client.ts:62](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/cases/client.ts#L62) ___ @@ -186,4 +186,4 @@ Update the specified cases with the passed in values. **Returns:** *Promise*<[*ICasesResponse*](typedoc_interfaces.icasesresponse.md)\> -Defined in: [cases/client.ts:66](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/cases/client.ts#L66) +Defined in: [cases/client.ts:66](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/cases/client.ts#L66) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.caseidsbyalertidparams.md b/x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.caseidsbyalertidparams.md index d2aea5db75e54e..1b8abba1a4071a 100644 --- a/x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.caseidsbyalertidparams.md +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.caseidsbyalertidparams.md @@ -21,7 +21,7 @@ Parameters for finding cases IDs using an alert ID The alert ID to search for -Defined in: [cases/get.ts:47](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/cases/get.ts#L47) +Defined in: [cases/get.ts:47](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/cases/get.ts#L47) ___ @@ -37,4 +37,4 @@ The filtering options when searching for associated cases. | :------ | :------ | | `owner` | *undefined* \| *string* \| *string*[] | -Defined in: [cases/get.ts:51](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/cases/get.ts#L51) +Defined in: [cases/get.ts:51](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/cases/get.ts#L51) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.getparams.md b/x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.getparams.md index 78704eb8c5d4d8..8c12b5533ac189 100644 --- a/x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.getparams.md +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/cases_get.getparams.md @@ -22,7 +22,7 @@ The parameters for retrieving a case Case ID -Defined in: [cases/get.ts:122](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/cases/get.ts#L122) +Defined in: [cases/get.ts:122](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/cases/get.ts#L122) ___ @@ -32,7 +32,7 @@ ___ Whether to include the attachments for a case in the response -Defined in: [cases/get.ts:126](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/cases/get.ts#L126) +Defined in: [cases/get.ts:126](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/cases/get.ts#L126) ___ @@ -42,4 +42,4 @@ ___ Whether to include the attachments for all children of a case in the response -Defined in: [cases/get.ts:130](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/cases/get.ts#L130) +Defined in: [cases/get.ts:130](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/cases/get.ts#L130) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/cases_push.pushparams.md b/x-pack/plugins/cases/docs/cases_client/interfaces/cases_push.pushparams.md index a6561152910d68..9f1810e4f0cc2b 100644 --- a/x-pack/plugins/cases/docs/cases_client/interfaces/cases_push.pushparams.md +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/cases_push.pushparams.md @@ -21,7 +21,7 @@ Parameters for pushing a case to an external system The ID of a case -Defined in: [cases/push.ts:53](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/cases/push.ts#L53) +Defined in: [cases/push.ts:53](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/cases/push.ts#L53) ___ @@ -31,4 +31,4 @@ ___ The ID of an external system to push to -Defined in: [cases/push.ts:57](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/cases/push.ts#L57) +Defined in: [cases/push.ts:57](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/cases/push.ts#L57) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/configure_client.configuresubclient.md b/x-pack/plugins/cases/docs/cases_client/interfaces/configure_client.configuresubclient.md index 082dc808d6e175..9b3827a57a9d3e 100644 --- a/x-pack/plugins/cases/docs/cases_client/interfaces/configure_client.configuresubclient.md +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/configure_client.configuresubclient.md @@ -31,7 +31,7 @@ Creates a configuration if one does not already exist. If one exists it is delet **Returns:** *Promise*<[*ICasesConfigureResponse*](typedoc_interfaces.icasesconfigureresponse.md)\> -Defined in: [configure/client.ts:102](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/configure/client.ts#L102) +Defined in: [configure/client.ts:102](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/configure/client.ts#L102) ___ @@ -50,7 +50,7 @@ Retrieves the external connector configuration for a particular case owner. **Returns:** *Promise*<{} \| [*ICasesConfigureResponse*](typedoc_interfaces.icasesconfigureresponse.md)\> -Defined in: [configure/client.ts:84](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/configure/client.ts#L84) +Defined in: [configure/client.ts:84](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/configure/client.ts#L84) ___ @@ -62,7 +62,7 @@ Retrieves the valid external connectors supported by the cases plugin. **Returns:** *Promise* -Defined in: [configure/client.ts:88](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/configure/client.ts#L88) +Defined in: [configure/client.ts:88](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/configure/client.ts#L88) ___ @@ -81,4 +81,4 @@ Updates a particular configuration with new values. **Returns:** *Promise*<[*ICasesConfigureResponse*](typedoc_interfaces.icasesconfigureresponse.md)\> -Defined in: [configure/client.ts:95](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/configure/client.ts#L95) +Defined in: [configure/client.ts:95](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/configure/client.ts#L95) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/stats_client.statssubclient.md b/x-pack/plugins/cases/docs/cases_client/interfaces/stats_client.statssubclient.md index 9093bee1532aaf..7e012053952778 100644 --- a/x-pack/plugins/cases/docs/cases_client/interfaces/stats_client.statssubclient.md +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/stats_client.statssubclient.md @@ -16,10 +16,17 @@ Statistics API contract. ### getStatusTotalsByType -▸ **getStatusTotalsByType**(): *Promise*<{ `count_closed_cases`: *number* ; `count_in_progress_cases`: *number* ; `count_open_cases`: *number* }\> +▸ **getStatusTotalsByType**(`params`: { `owner`: *undefined* \| *string* \| *string*[] }): *Promise*<{ `count_closed_cases`: *number* ; `count_in_progress_cases`: *number* ; `count_open_cases`: *number* }\> Retrieves the total number of open, closed, and in-progress cases. +#### Parameters + +| Name | Type | +| :------ | :------ | +| `params` | *object* | +| `params.owner` | *undefined* \| *string* \| *string*[] | + **Returns:** *Promise*<{ `count_closed_cases`: *number* ; `count_in_progress_cases`: *number* ; `count_open_cases`: *number* }\> -Defined in: [stats/client.ts:21](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/stats/client.ts#L21) +Defined in: [stats/client.ts:34](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/stats/client.ts#L34) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/sub_cases_client.subcasesclient.md b/x-pack/plugins/cases/docs/cases_client/interfaces/sub_cases_client.subcasesclient.md index db48224bab6717..76df26524b7b04 100644 --- a/x-pack/plugins/cases/docs/cases_client/interfaces/sub_cases_client.subcasesclient.md +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/sub_cases_client.subcasesclient.md @@ -31,7 +31,7 @@ Deletes the specified entities and their attachments. **Returns:** *Promise* -Defined in: [sub_cases/client.ts:60](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/sub_cases/client.ts#L60) +Defined in: [sub_cases/client.ts:60](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/sub_cases/client.ts#L60) ___ @@ -49,7 +49,7 @@ Retrieves the sub cases matching the search criteria. **Returns:** *Promise*<[*ISubCasesFindResponse*](typedoc_interfaces.isubcasesfindresponse.md)\> -Defined in: [sub_cases/client.ts:64](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/sub_cases/client.ts#L64) +Defined in: [sub_cases/client.ts:64](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/sub_cases/client.ts#L64) ___ @@ -67,7 +67,7 @@ Retrieves a single sub case. **Returns:** *Promise*<[*ISubCaseResponse*](typedoc_interfaces.isubcaseresponse.md)\> -Defined in: [sub_cases/client.ts:68](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/sub_cases/client.ts#L68) +Defined in: [sub_cases/client.ts:68](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/sub_cases/client.ts#L68) ___ @@ -86,4 +86,4 @@ Updates the specified sub cases to the new values included in the request. **Returns:** *Promise*<[*ISubCasesResponse*](typedoc_interfaces.isubcasesresponse.md)\> -Defined in: [sub_cases/client.ts:72](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/sub_cases/client.ts#L72) +Defined in: [sub_cases/client.ts:72](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/sub_cases/client.ts#L72) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/user_actions_client.useractionget.md b/x-pack/plugins/cases/docs/cases_client/interfaces/user_actions_client.useractionget.md index e492747c7baad7..2c0c084ab9b304 100644 --- a/x-pack/plugins/cases/docs/cases_client/interfaces/user_actions_client.useractionget.md +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/user_actions_client.useractionget.md @@ -21,7 +21,7 @@ Parameters for retrieving user actions for a particular case The ID of the case -Defined in: [user_actions/client.ts:19](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/user_actions/client.ts#L19) +Defined in: [user_actions/client.ts:19](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/user_actions/client.ts#L19) ___ @@ -31,4 +31,4 @@ ___ If specified then a sub case will be used for finding all the user actions -Defined in: [user_actions/client.ts:23](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/user_actions/client.ts#L23) +Defined in: [user_actions/client.ts:23](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/user_actions/client.ts#L23) diff --git a/x-pack/plugins/cases/docs/cases_client/interfaces/user_actions_client.useractionssubclient.md b/x-pack/plugins/cases/docs/cases_client/interfaces/user_actions_client.useractionssubclient.md index 70dc3958b5de66..f03667eccb8586 100644 --- a/x-pack/plugins/cases/docs/cases_client/interfaces/user_actions_client.useractionssubclient.md +++ b/x-pack/plugins/cases/docs/cases_client/interfaces/user_actions_client.useractionssubclient.md @@ -28,4 +28,4 @@ Retrieves all user actions for a particular case. **Returns:** *Promise*<[*ICaseUserActionsResponse*](typedoc_interfaces.icaseuseractionsresponse.md)\> -Defined in: [user_actions/client.ts:33](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/user_actions/client.ts#L33) +Defined in: [user_actions/client.ts:33](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/user_actions/client.ts#L33) diff --git a/x-pack/plugins/cases/docs/cases_client/modules/cases_get.md b/x-pack/plugins/cases/docs/cases_client/modules/cases_get.md index 69cd5b856bbd77..9e896881df17bc 100644 --- a/x-pack/plugins/cases/docs/cases_client/modules/cases_get.md +++ b/x-pack/plugins/cases/docs/cases_client/modules/cases_get.md @@ -31,7 +31,7 @@ Retrieves the reporters from all the cases. **Returns:** *Promise* -Defined in: [cases/get.ts:279](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/cases/get.ts#L279) +Defined in: [cases/get.ts:279](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/cases/get.ts#L279) ___ @@ -50,4 +50,4 @@ Retrieves the tags from all the cases. **Returns:** *Promise* -Defined in: [cases/get.ts:217](https://github.com/jonathan-buttner/kibana/blob/085f89ff3ca/x-pack/plugins/cases/server/client/cases/get.ts#L217) +Defined in: [cases/get.ts:217](https://github.com/jonathan-buttner/kibana/blob/2085a3b4480/x-pack/plugins/cases/server/client/cases/get.ts#L217) diff --git a/x-pack/plugins/cases/public/containers/api.test.tsx b/x-pack/plugins/cases/public/containers/api.test.tsx index bee6110c39a306..5ed5e4a61a121f 100644 --- a/x-pack/plugins/cases/public/containers/api.test.tsx +++ b/x-pack/plugins/cases/public/containers/api.test.tsx @@ -215,15 +215,16 @@ describe('Case Configuration API', () => { fetchMock.mockResolvedValue(casesStatusSnake); }); test('check url, method, signal', async () => { - await getCasesStatus(abortCtrl.signal); + await getCasesStatus(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/status`, { method: 'GET', signal: abortCtrl.signal, + owner: [SECURITY_SOLUTION_OWNER], }); }); test('happy path', async () => { - const resp = await getCasesStatus(abortCtrl.signal); + const resp = await getCasesStatus(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]); expect(resp).toEqual(casesStatus); }); }); diff --git a/x-pack/plugins/cases/public/containers/api.ts b/x-pack/plugins/cases/public/containers/api.ts index 0b9b236cef6e17..66a4d174b0ffb8 100644 --- a/x-pack/plugins/cases/public/containers/api.ts +++ b/x-pack/plugins/cases/public/containers/api.ts @@ -111,10 +111,14 @@ export const getSubCase = async ( return convertToCamelCase(decodeCaseResponse(response)); }; -export const getCasesStatus = async (signal: AbortSignal): Promise => { +export const getCasesStatus = async ( + signal: AbortSignal, + owner: string[] +): Promise => { const response = await KibanaServices.get().http.fetch(CASE_STATUS_URL, { method: 'GET', signal, + query: { ...(owner.length > 0 ? { owner } : {}) }, }); return convertToCamelCase(decodeCasesStatusResponse(response)); }; diff --git a/x-pack/plugins/cases/public/containers/use_get_cases_status.tsx b/x-pack/plugins/cases/public/containers/use_get_cases_status.tsx index c3244bb38f1513..909bc42345759f 100644 --- a/x-pack/plugins/cases/public/containers/use_get_cases_status.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_cases_status.tsx @@ -7,6 +7,7 @@ import { useCallback, useEffect, useState, useRef } from 'react'; +import { useOwnerContext } from '../components/owner_context/use_owner_context'; import { getCasesStatus } from './api'; import * as i18n from './translations'; import { CasesStatus } from './types'; @@ -30,6 +31,7 @@ export interface UseGetCasesStatus extends CasesStatusState { } export const useGetCasesStatus = (): UseGetCasesStatus => { + const owner = useOwnerContext(); const [casesStatusState, setCasesStatusState] = useState(initialData); const toasts = useToasts(); const isCancelledRef = useRef(false); @@ -45,7 +47,7 @@ export const useGetCasesStatus = (): UseGetCasesStatus => { isLoading: true, }); - const response = await getCasesStatus(abortCtrlRef.current.signal); + const response = await getCasesStatus(abortCtrlRef.current.signal, owner); if (!isCancelledRef.current) { setCasesStatusState({ diff --git a/x-pack/plugins/cases/server/client/stats/client.ts b/x-pack/plugins/cases/server/client/stats/client.ts index eb9f885a735aad..7259829f5603e8 100644 --- a/x-pack/plugins/cases/server/client/stats/client.ts +++ b/x-pack/plugins/cases/server/client/stats/client.ts @@ -5,8 +5,21 @@ * 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 { CasesClientArgs } from '..'; -import { CasesStatusResponse, CasesStatusResponseRt, caseStatuses } from '../../../common/api'; +import { + CasesStatusRequest, + CasesStatusResponse, + CasesStatusResponseRt, + caseStatuses, + throwErrors, + excess, + CasesStatusRequestRt, +} from '../../../common/api'; import { Operations } from '../../authorization'; import { createCaseError } from '../../common/error'; import { constructQueryOptions, getAuthorizationFilter } from '../utils'; @@ -18,7 +31,7 @@ export interface StatsSubClient { /** * Retrieves the total number of open, closed, and in-progress cases. */ - getStatusTotalsByType(): Promise; + getStatusTotalsByType(params: CasesStatusRequest): Promise; } /** @@ -28,18 +41,29 @@ export interface StatsSubClient { */ export function createStatsSubClient(clientArgs: CasesClientArgs): StatsSubClient { return Object.freeze({ - getStatusTotalsByType: () => getStatusTotalsByType(clientArgs), + getStatusTotalsByType: (params: CasesStatusRequest) => + getStatusTotalsByType(params, clientArgs), }); } -async function getStatusTotalsByType({ - savedObjectsClient: soClient, - caseService, - logger, - authorization, - auditLogger, -}: CasesClientArgs): Promise { +async function getStatusTotalsByType( + params: CasesStatusRequest, + clientArgs: CasesClientArgs +): Promise { + const { + savedObjectsClient: soClient, + caseService, + logger, + authorization, + auditLogger, + } = clientArgs; + try { + const queryParams = pipe( + excess(CasesStatusRequestRt).decode(params), + fold(throwErrors(Boom.badRequest), identity) + ); + const { filter: authorizationFilter, ensureSavedObjectsAreAuthorized, @@ -52,7 +76,11 @@ async function getStatusTotalsByType({ const [openCases, inProgressCases, closedCases] = await Promise.all([ ...caseStatuses.map((status) => { - const statusQuery = constructQueryOptions({ status, authorizationFilter }); + const statusQuery = constructQueryOptions({ + owner: queryParams.owner, + status, + authorizationFilter, + }); return caseService.findCaseStatusStats({ soClient, caseOptions: statusQuery.case, diff --git a/x-pack/plugins/cases/server/routes/api/stats/get_status.ts b/x-pack/plugins/cases/server/routes/api/stats/get_status.ts index 3d9dc73860ef94..7fef5f59e24594 100644 --- a/x-pack/plugins/cases/server/routes/api/stats/get_status.ts +++ b/x-pack/plugins/cases/server/routes/api/stats/get_status.ts @@ -6,22 +6,22 @@ */ import { RouteDeps } from '../types'; -import { wrapError } from '../utils'; +import { escapeHatch, wrapError } from '../utils'; import { CASE_STATUS_URL } from '../../../../common/constants'; +import { CasesStatusRequest } from '../../../../common'; export function initGetCasesStatusApi({ router, logger }: RouteDeps) { router.get( { path: CASE_STATUS_URL, - validate: {}, + validate: { query: escapeHatch }, }, async (context, request, response) => { try { const client = await context.cases.getCasesClient(); - return response.ok({ - body: await client.stats.getStatusTotalsByType(), + body: await client.stats.getStatusTotalsByType(request.query as CasesStatusRequest), }); } catch (error) { logger.error(`Failed to get status stats in route: ${error}`); 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 855cf513f16d56..581e4f2a326d60 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -954,15 +954,17 @@ export const getAllCasesStatuses = async ({ supertest, expectedHttpCode = 200, auth = { user: superUser, space: null }, + query = {}, }: { supertest: st.SuperTest; expectedHttpCode?: number; auth?: { user: User; space: string | null }; + query?: Record; }): Promise => { const { body: statuses } = await supertest .get(`${getSpaceUrlPrefix(auth.space)}${CASE_STATUS_URL}`) .auth(auth.user.username, auth.user.password) - .set('kbn-xsrf', 'true') + .query({ ...query }) .expect(expectedHttpCode); return statuses; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts index 7a17cf1dd8e081..02ace7077a20a5 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts @@ -134,10 +134,23 @@ export default ({ getService }: FtrProviderContext): void => { { user: secOnlyRead, stats: { open: 0, inProgress: 1, closed: 1 } }, { user: obsOnlyRead, stats: { open: 1, inProgress: 1, closed: 0 } }, { user: obsSecRead, stats: { open: 1, inProgress: 2, closed: 1 } }, + { + user: obsSecRead, + stats: { open: 1, inProgress: 1, closed: 0 }, + owner: 'observabilityFixture', + }, + { + user: obsSecRead, + stats: { open: 1, inProgress: 2, closed: 1 }, + owner: ['observabilityFixture', 'securitySolutionFixture'], + }, ]) { const statuses = await getAllCasesStatuses({ supertest: supertestWithoutAuth, auth: { user: scenario.user, space: 'space1' }, + query: { + owner: scenario.owner, + }, }); expect(statuses).to.eql({ diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/status/get_status.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/status/get_status.ts index 78ca48b04560c5..245c7d1fdbfc54 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/status/get_status.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/status/get_status.ts @@ -92,10 +92,23 @@ export default ({ getService }: FtrProviderContext): void => { { user: secOnlyReadSpacesAll, stats: { open: 0, inProgress: 1, closed: 1 } }, { user: obsOnlyReadSpacesAll, stats: { open: 1, inProgress: 1, closed: 0 } }, { user: obsSecReadSpacesAll, stats: { open: 1, inProgress: 2, closed: 1 } }, + { + user: obsSecReadSpacesAll, + stats: { open: 1, inProgress: 1, closed: 0 }, + owner: 'observabilityFixture', + }, + { + user: obsSecReadSpacesAll, + stats: { open: 1, inProgress: 2, closed: 1 }, + owner: ['observabilityFixture', 'securitySolutionFixture'], + }, ]) { const statuses = await getAllCasesStatuses({ supertest: supertestWithoutAuth, auth: { user: scenario.user, space: null }, + query: { + owner: scenario.owner, + }, }); expect(statuses).to.eql({ From 34b16c5e1d54f49647a5f793700c77c4e0b1cb96 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 25 May 2021 11:35:43 +0300 Subject: [PATCH 072/113] Fix get case status tests --- .../cases/public/containers/api.test.tsx | 2 +- .../containers/use_get_cases_status.test.tsx | 33 ++++++++++++++----- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/cases/public/containers/api.test.tsx b/x-pack/plugins/cases/public/containers/api.test.tsx index 5ed5e4a61a121f..afd6b51b5f35d0 100644 --- a/x-pack/plugins/cases/public/containers/api.test.tsx +++ b/x-pack/plugins/cases/public/containers/api.test.tsx @@ -219,7 +219,7 @@ describe('Case Configuration API', () => { expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/status`, { method: 'GET', signal: abortCtrl.signal, - owner: [SECURITY_SOLUTION_OWNER], + query: { owner: [SECURITY_SOLUTION_OWNER] }, }); }); diff --git a/x-pack/plugins/cases/public/containers/use_get_cases_status.test.tsx b/x-pack/plugins/cases/public/containers/use_get_cases_status.test.tsx index f795d5cc60e712..b9047fdafee610 100644 --- a/x-pack/plugins/cases/public/containers/use_get_cases_status.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_cases_status.test.tsx @@ -5,10 +5,13 @@ * 2.0. */ +import React from 'react'; import { renderHook, act } from '@testing-library/react-hooks'; import { useGetCasesStatus, UseGetCasesStatus } from './use_get_cases_status'; import { casesStatus } from './mock'; import * as api from './api'; +import { TestProviders } from '../common/mock'; +import { SECURITY_SOLUTION_OWNER } from '../../common'; jest.mock('./api'); jest.mock('../common/lib/kibana'); @@ -22,8 +25,11 @@ describe('useGetCasesStatus', () => { it('init', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useGetCasesStatus() + const { result, waitForNextUpdate } = renderHook( + () => useGetCasesStatus(), + { + wrapper: ({ children }) => {children}, + } ); await waitForNextUpdate(); expect(result.current).toEqual({ @@ -40,19 +46,25 @@ describe('useGetCasesStatus', () => { it('calls getCasesStatus api', async () => { const spyOnGetCasesStatus = jest.spyOn(api, 'getCasesStatus'); await act(async () => { - const { waitForNextUpdate } = renderHook(() => - useGetCasesStatus() + const { waitForNextUpdate } = renderHook( + () => useGetCasesStatus(), + { + wrapper: ({ children }) => {children}, + } ); await waitForNextUpdate(); await waitForNextUpdate(); - expect(spyOnGetCasesStatus).toBeCalledWith(abortCtrl.signal); + expect(spyOnGetCasesStatus).toBeCalledWith(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]); }); }); it('fetch reporters', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useGetCasesStatus() + const { result, waitForNextUpdate } = renderHook( + () => useGetCasesStatus(), + { + wrapper: ({ children }) => {children}, + } ); await waitForNextUpdate(); await waitForNextUpdate(); @@ -74,8 +86,11 @@ describe('useGetCasesStatus', () => { }); await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useGetCasesStatus() + const { result, waitForNextUpdate } = renderHook( + () => useGetCasesStatus(), + { + wrapper: ({ children }) => {children}, + } ); await waitForNextUpdate(); await waitForNextUpdate(); From fb7e7a9e8bbf2580ae09c25d9148113b5af7ce90 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 25 May 2021 07:10:15 -0600 Subject: [PATCH 073/113] in middle of removing alerts stuff --- .../all_cases/all_cases_generic.tsx | 3 + .../public/components/all_cases/columns.tsx | 24 ++++--- .../public/components/all_cases/index.tsx | 1 + .../public/components/case_view/index.tsx | 8 ++- .../components/user_action_tree/index.tsx | 28 ++++---- .../components/app/cases/all_cases/index.tsx | 2 +- .../components/app/cases/case_view/helpers.ts | 72 ++----------------- .../components/app/cases/case_view/index.tsx | 30 +------- .../public/components/app/cases/constants.ts | 1 - .../observability/public/pages/cases/links.ts | 1 - 10 files changed, 46 insertions(+), 124 deletions(-) diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx index 4b235b9fec8688..a364f8bf2b068a 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx @@ -62,6 +62,7 @@ interface AllCasesGenericProps { caseDetailsNavigation?: CasesNavigation; // if not passed, case name is not displayed as a link (Formerly dependant on isSelectorView) configureCasesNavigation?: CasesNavigation; // if not passed, header with nav is not displayed (Formerly dependant on isSelectorView) createCaseNavigation: CasesNavigation; + disableAlerts?: boolean; hiddenStatuses?: CaseStatusWithAllStatus[]; isSelectorView?: boolean; onRowClick?: (theCase?: Case | SubCase) => void; @@ -76,6 +77,7 @@ export const AllCasesGeneric = React.memo( caseDetailsNavigation, configureCasesNavigation, createCaseNavigation, + disableAlerts, hiddenStatuses = [], isSelectorView, onRowClick, @@ -192,6 +194,7 @@ export const AllCasesGeneric = React.memo( const columns = useCasesColumns({ caseDetailsNavigation, + disableAlerts, dispatchUpdateCaseProperty, filterStatus: filterOptions.status, handleIsLoading, diff --git a/x-pack/plugins/cases/public/components/all_cases/columns.tsx b/x-pack/plugins/cases/public/components/all_cases/columns.tsx index cf5da3928446e1..947d405d188cf0 100644 --- a/x-pack/plugins/cases/public/components/all_cases/columns.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/columns.tsx @@ -55,6 +55,7 @@ const renderStringField = (field: string, dataTestSubj: string) => export interface GetCasesColumn { caseDetailsNavigation?: CasesNavigation; + disableAlerts?: boolean; dispatchUpdateCaseProperty: (u: UpdateCase) => void; filterStatus: string; handleIsLoading: (a: boolean) => void; @@ -64,6 +65,7 @@ export interface GetCasesColumn { } export const useCasesColumns = ({ caseDetailsNavigation, + disableAlerts = false, dispatchUpdateCaseProperty, filterStatus, handleIsLoading, @@ -203,15 +205,19 @@ export const useCasesColumns = ({ }, truncateText: true, }, - { - align: RIGHT_ALIGNMENT, - field: 'totalAlerts', - name: ALERTS, - render: (totalAlerts: Case['totalAlerts']) => - totalAlerts != null - ? renderStringField(`${totalAlerts}`, `case-table-column-alertsCount`) - : getEmptyTagValue(), - }, + ...(!disableAlerts + ? [ + { + align: RIGHT_ALIGNMENT, + field: 'totalAlerts', + name: ALERTS, + render: (totalAlerts: Case['totalAlerts']) => + totalAlerts != null + ? renderStringField(`${totalAlerts}`, `case-table-column-alertsCount`) + : getEmptyTagValue(), + }, + ] + : []), { align: RIGHT_ALIGNMENT, field: 'totalComment', diff --git a/x-pack/plugins/cases/public/components/all_cases/index.tsx b/x-pack/plugins/cases/public/components/all_cases/index.tsx index 09c2742f5b50aa..1f836b318dbad9 100644 --- a/x-pack/plugins/cases/public/components/all_cases/index.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/index.tsx @@ -14,6 +14,7 @@ export interface AllCasesProps extends Owner { caseDetailsNavigation: CasesNavigation; // if not passed, case name is not displayed as a link (Formerly dependant on isSelector) configureCasesNavigation: CasesNavigation; // if not passed, header with nav is not displayed (Formerly dependant on isSelector) createCaseNavigation: CasesNavigation; + disableAlerts?: boolean; showTitle?: boolean; userCanCrud: boolean; } diff --git a/x-pack/plugins/cases/public/components/case_view/index.tsx b/x-pack/plugins/cases/public/components/case_view/index.tsx index 86b13ae5a863c4..4b1629c4c588a9 100644 --- a/x-pack/plugins/cases/public/components/case_view/index.tsx +++ b/x-pack/plugins/cases/public/components/case_view/index.tsx @@ -53,8 +53,8 @@ export interface CaseViewComponentProps { configureCasesNavigation: CasesNavigation; getCaseDetailHrefWithCommentId: (commentId: string) => string; onComponentInitialized?: () => void; - ruleDetailsNavigation: CasesNavigation; - showAlertDetails: (alertId: string, index: string) => void; + ruleDetailsNavigation?: CasesNavigation; + showAlertDetails?: (alertId: string, index: string) => void; subCaseId?: string; useFetchAlertData: (alertIds: string[]) => [boolean, Record]; userCanCrud: boolean; @@ -327,7 +327,9 @@ export const CaseComponent = React.memo( const onShowAlertDetails = useCallback( (alertId: string, index: string) => { - showAlertDetails(alertId, index); + if (showAlertDetails) { + showAlertDetails(alertId, index); + } }, [showAlertDetails] ); diff --git a/x-pack/plugins/cases/public/components/user_action_tree/index.tsx b/x-pack/plugins/cases/public/components/user_action_tree/index.tsx index 09b024fb2ca3d1..1d14d3995b902a 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/index.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/index.tsx @@ -397,18 +397,22 @@ export const UserActionTree = React.memo( return [ ...comments, - getAlertAttachment({ - action, - alertId, - getCaseDetailHrefWithCommentId, - getRuleDetailsHref, - index: alertIndex, - loadingAlertData, - onRuleDetailsClick, - ruleId, - ruleName, - onShowAlertDetails, - }), + getRuleDetailsHref != null && getRuleDetailsHref != null + ? [ + getAlertAttachment({ + action, + alertId, + getCaseDetailHrefWithCommentId, + getRuleDetailsHref, + index: alertIndex, + loadingAlertData, + onRuleDetailsClick, + ruleId, + ruleName, + onShowAlertDetails, + }), + ] + : [], ]; } else if (comment != null && comment.type === CommentType.generatedAlert) { // TODO: clean this up diff --git a/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx b/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx index 2b30ee3c08bc17..52bc68e8fc49ef 100644 --- a/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx @@ -6,7 +6,6 @@ */ import React, { useCallback } from 'react'; -import { useHistory } from 'react-router-dom'; import { getCaseDetailsUrl, @@ -72,6 +71,7 @@ export const AllCases = React.memo(({ userCanCrud }) => { href: formatUrl(getCreateCaseUrl()), onClick: goToCreateCase, }, + disableAlerts: true, showTitle: false, userCanCrud, owner: [CASES_OWNER], diff --git a/x-pack/plugins/observability/public/components/app/cases/case_view/helpers.ts b/x-pack/plugins/observability/public/components/app/cases/case_view/helpers.ts index 04da744fdd62e0..9b9e1a570617a5 100644 --- a/x-pack/plugins/observability/public/components/app/cases/case_view/helpers.ts +++ b/x-pack/plugins/observability/public/components/app/cases/case_view/helpers.ts @@ -5,12 +5,7 @@ * 2.0. */ -import { isObject, get, isString, isNumber } from 'lodash'; -import { useMemo } from 'react'; -// import { useSourcererScope } from '../../../common/containers/sourcerer'; -// import { SourcererScopeName } from '../../../common/store/sourcerer/model'; -// import { useQueryAlerts } from '../../../detections/containers/detection_engine/alerts/use_query'; -// import { Ecs } from '../../../../../cases/common'; +import { Ecs } from '../../../../../../cases/common'; // TODO we need to allow -> docValueFields: [{ field: "@timestamp" }], export const buildAlertsQuery = (alertIds: string[]) => { @@ -66,65 +61,6 @@ export const toStringArray = (value: unknown): string[] => { } }; -// export const formatAlertToEcsSignal = (alert: {}): Ecs => -// Object.keys(alert).reduce((accumulator, key) => { -// const item = get(alert, key); -// if (item != null && isObject(item)) { -// return { ...accumulator, [key]: formatAlertToEcsSignal(item) }; -// } else if (Array.isArray(item) || isString(item) || isNumber(item)) { -// return { ...accumulator, [key]: toStringArray(item) }; -// } -// return accumulator; -// }, {} as Ecs); -// interface Signal { -// rule: { -// id: string; -// name: string; -// to: string; -// from: string; -// }; -// } - -// interface SignalHit { -// _id: string; -// _index: string; -// _source: { -// '@timestamp': string; -// signal: Signal; -// }; -// } -// -// export interface Alert { -// _id: string; -// _index: string; -// '@timestamp': string; -// signal: Signal; -// [key: string]: unknown; -// } -export const useFetchAlertData = (alertIds: string[]) => { - // const alertsQuery = useMemo(() => buildAlertsQuery(alertIds), [alertIds]); - // - // const { loading: isLoadingAlerts, data: alertsData } = // useQueryAlerts({ - // query: alertsQuery, - // indexName: selectedPatterns[0], - // }); - // - // const alerts = useMemo( - // () => - // alertsData?.hits.hits.reduce>( - // (acc, { _id, _index, _source }) => ({ - // ...acc, - // [_id]: { - // ...formatAlertToEcsSignal(_source), - // _id, - // _index, - // timestamp: _source['@timestamp'], - // }, - // }), - // {} - // ) ?? {}, - // [alertsData?.hits.hits] - // ); - - return [false, {}]; -}; +// no alerts in observability so far +// dummy hook for now +export const useFetchAlertData = (): [boolean, Record] => [false, {}]; diff --git a/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx b/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx index b634eccd1e909b..e1df364a99fffe 100644 --- a/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx @@ -12,13 +12,12 @@ import { getCaseDetailsUrlWithCommentId, getCaseUrl, getConfigureCasesUrl, - getRuleDetailsUrl, useFormatUrl, } from '../../../../pages/cases/links'; import { Case } from '../../../../../../cases/common'; import { useFetchAlertData } from './helpers'; import { useKibana } from '../../../../utils/kibana_react'; -import { ALERTS_APP_ID, CASES_APP_ID } from '../constants'; +import { CASES_APP_ID } from '../constants'; interface Props { caseId: string; @@ -59,7 +58,6 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = } = useKibana().services; const history = useHistory(); const { formatUrl } = useFormatUrl(CASES_APP_ID); - const { formatUrl: detectionsFormatUrl } = useFormatUrl(ALERTS_APP_ID); const allCasesLink = getCaseUrl(); const formattedAllCasesLink = formatUrl(allCasesLink); @@ -85,27 +83,6 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = }, [history] ); - - const onDetectionsRuleDetailsClick = useCallback( - (ruleId: string | null | undefined) => { - navigateToApp(`observability-alerts`, { - path: getRuleDetailsUrl(ruleId ?? ''), - }); - }, - [navigateToApp] - ); - - const getDetectionsRuleDetailsHref = useCallback( - (ruleId) => { - return detectionsFormatUrl(getRuleDetailsUrl(ruleId ?? '')); - }, - [detectionsFormatUrl] - ); - - const showAlertDetails = useCallback((alertId: string, index: string) => { - // TO DO show alert details (in timeline on security solution) - }, []); - return casesUi.getCaseView({ allCasesNavigation: { href: formattedAllCasesLink, @@ -126,11 +103,6 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = }, getCaseDetailHrefWithCommentId, onCaseDataSuccess, - ruleDetailsNavigation: { - href: getDetectionsRuleDetailsHref, - onClick: onDetectionsRuleDetailsClick, - }, - showAlertDetails, subCaseId, useFetchAlertData, userCanCrud, diff --git a/x-pack/plugins/observability/public/components/app/cases/constants.ts b/x-pack/plugins/observability/public/components/app/cases/constants.ts index fe268ed81b96cd..82b76b082101c8 100644 --- a/x-pack/plugins/observability/public/components/app/cases/constants.ts +++ b/x-pack/plugins/observability/public/components/app/cases/constants.ts @@ -6,5 +6,4 @@ */ export const CASES_APP_ID = 'observability-cases'; -export const ALERTS_APP_ID = 'observability-alerts'; export const CASES_OWNER = 'observability'; diff --git a/x-pack/plugins/observability/public/pages/cases/links.ts b/x-pack/plugins/observability/public/pages/cases/links.ts index ddc70ef4303efb..b922ace15e1ea9 100644 --- a/x-pack/plugins/observability/public/pages/cases/links.ts +++ b/x-pack/plugins/observability/public/pages/cases/links.ts @@ -57,4 +57,3 @@ export const getCreateCaseUrl = () => `/create`; export const getConfigureCasesUrl = () => `/configure`; export const getCaseUrl = () => `/cases`; -export const getRuleDetailsUrl = (detailName: string) => `/rules/id/${detailName}`; From 882d97cc4ec17bde4ee98751d6d269c7a941ae29 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 25 May 2021 10:09:23 -0600 Subject: [PATCH 074/113] removed alerting and fixing types --- .../components/case_action_bar/index.test.tsx | 1 + .../components/case_action_bar/index.tsx | 42 ++++++++++--------- .../public/components/case_view/index.tsx | 11 +++-- .../cases/public/components/create/form.tsx | 13 +++--- .../cases/public/components/create/index.tsx | 3 ++ .../components/user_action_tree/index.tsx | 32 +++++++------- .../components/app/cases/case_view/helpers.ts | 2 +- .../app/cases/create/flyout.test.tsx | 13 +++--- .../components/app/cases/create/index.tsx | 3 +- 9 files changed, 69 insertions(+), 51 deletions(-) diff --git a/x-pack/plugins/cases/public/components/case_action_bar/index.test.tsx b/x-pack/plugins/cases/public/components/case_action_bar/index.test.tsx index 0d29335ea730e7..4af8b9f0effb38 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/index.test.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/index.test.tsx @@ -17,6 +17,7 @@ describe('CaseActionBar', () => { const onUpdateField = jest.fn(); const defaultProps = { caseData: basicCase, + isAlerting: true, isLoading: false, onRefresh, onUpdateField, diff --git a/x-pack/plugins/cases/public/components/case_action_bar/index.tsx b/x-pack/plugins/cases/public/components/case_action_bar/index.tsx index 0f06dde6a86d13..aeb84762867bf2 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/index.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/index.tsx @@ -40,6 +40,7 @@ interface CaseActionBarProps { caseData: Case; currentExternalIncident: CaseService | null; disabled?: boolean; + isAlerting: boolean; isLoading: boolean; onRefresh: () => void; onUpdateField: (args: OnUpdateFields) => void; @@ -48,6 +49,7 @@ const CaseActionBarComponent: React.FC = ({ caseData, currentExternalIncident, disabled = false, + isAlerting, isLoading, onRefresh, onUpdateField, @@ -103,25 +105,27 @@ const CaseActionBarComponent: React.FC = ({ - - - - - {i18n.SYNC_ALERTS} - - - - - - - - - - + {isAlerting && ( + + + + + {i18n.SYNC_ALERTS} + + + + + + + + + + + )} {i18n.CASE_REFRESH} diff --git a/x-pack/plugins/cases/public/components/case_view/index.tsx b/x-pack/plugins/cases/public/components/case_view/index.tsx index 4b1629c4c588a9..b3d5424d525770 100644 --- a/x-pack/plugins/cases/public/components/case_view/index.tsx +++ b/x-pack/plugins/cases/public/components/case_view/index.tsx @@ -361,9 +361,10 @@ export const CaseComponent = React.memo( title={caseData.title} > ( <> = React.memo( ({ connectors = empty, + disableAlerts = false, isLoadingConnectors = false, hideConnectorServiceNowSir = false, withSteps = true, @@ -99,11 +101,10 @@ export const CreateCaseForm: React.FC = React.memo( [connectors, hideConnectorServiceNowSir, isLoadingConnectors, isSubmitting] ); - const allSteps = useMemo(() => [firstStep, secondStep, thirdStep], [ - firstStep, - secondStep, - thirdStep, - ]); + const allSteps = useMemo( + () => [firstStep, ...(!disableAlerts ? [secondStep] : []), thirdStep], + [disableAlerts, firstStep, secondStep, thirdStep] + ); return ( <> @@ -117,7 +118,7 @@ export const CreateCaseForm: React.FC = React.memo( ) : ( <> {firstStep.children} - {secondStep.children} + {!disableAlerts && secondStep.children} {thirdStep.children} )} diff --git a/x-pack/plugins/cases/public/components/create/index.tsx b/x-pack/plugins/cases/public/components/create/index.tsx index 3362aa6af2078d..139a2103f6042b 100644 --- a/x-pack/plugins/cases/public/components/create/index.tsx +++ b/x-pack/plugins/cases/public/components/create/index.tsx @@ -34,6 +34,7 @@ const Container = styled.div` export interface CreateCaseProps extends Owner { afterCaseCreated?: (theCase: Case, postComment: UsePostComment['postComment']) => Promise; caseType?: CaseType; + disableAlerts?: boolean; hideConnectorServiceNowSir?: boolean; onCancel: () => void; onSuccess: (theCase: Case) => Promise; @@ -45,6 +46,7 @@ const CreateCaseComponent = ({ afterCaseCreated, caseType, hideConnectorServiceNowSir, + disableAlerts, onCancel, onSuccess, timelineIntegration, @@ -59,6 +61,7 @@ const CreateCaseComponent = ({ > diff --git a/x-pack/plugins/cases/public/components/user_action_tree/index.tsx b/x-pack/plugins/cases/public/components/user_action_tree/index.tsx index 1d14d3995b902a..156e011a18d8d7 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/index.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/index.tsx @@ -21,7 +21,7 @@ import { isRight } from 'fp-ts/Either'; import * as i18n from './translations'; -import { Case, CaseUserActions } from '../../containers/types'; +import { Case, CaseUserActions } from '../../../common'; import { useUpdateComment } from '../../containers/use_update_comment'; import { useCurrentUser } from '../../common/lib/kibana'; import { AddComment, AddCommentRefObject } from '../add_comment'; @@ -56,7 +56,7 @@ export interface UserActionTreeProps { caseUserActions: CaseUserActions[]; connectors: ActionConnector[]; data: Case; - getRuleDetailsHref: (ruleId: string | null | undefined) => string; + getRuleDetailsHref?: (ruleId: string | null | undefined) => string; fetchUserActions: () => void; isLoadingDescription: boolean; isLoadingUserActions: boolean; @@ -397,7 +397,7 @@ export const UserActionTree = React.memo( return [ ...comments, - getRuleDetailsHref != null && getRuleDetailsHref != null + ...(getRuleDetailsHref != null ? [ getAlertAttachment({ action, @@ -412,7 +412,7 @@ export const UserActionTree = React.memo( onShowAlertDetails, }), ] - : [], + : []), ]; } else if (comment != null && comment.type === CommentType.generatedAlert) { // TODO: clean this up @@ -426,16 +426,20 @@ export const UserActionTree = React.memo( return [ ...comments, - getGeneratedAlertsAttachment({ - action, - alertIds, - getCaseDetailHrefWithCommentId, - getRuleDetailsHref, - onRuleDetailsClick, - renderInvestigateInTimelineActionComponent, - ruleId: comment.rule?.id ?? '', - ruleName: comment.rule?.name ?? i18n.UNKNOWN_RULE, - }), + ...(getRuleDetailsHref != null + ? [ + getGeneratedAlertsAttachment({ + action, + alertIds, + getCaseDetailHrefWithCommentId, + getRuleDetailsHref, + onRuleDetailsClick, + renderInvestigateInTimelineActionComponent, + ruleId: comment.rule?.id ?? '', + ruleName: comment.rule?.name ?? i18n.UNKNOWN_RULE, + }), + ] + : []), ]; } } diff --git a/x-pack/plugins/observability/public/components/app/cases/case_view/helpers.ts b/x-pack/plugins/observability/public/components/app/cases/case_view/helpers.ts index 9b9e1a570617a5..2bb72ed38290c5 100644 --- a/x-pack/plugins/observability/public/components/app/cases/case_view/helpers.ts +++ b/x-pack/plugins/observability/public/components/app/cases/case_view/helpers.ts @@ -62,5 +62,5 @@ export const toStringArray = (value: unknown): string[] => { }; // no alerts in observability so far -// dummy hook for now +// dummy hook for now as hooks cannot be called conditionally export const useFetchAlertData = (): [boolean, Record] => [false, {}]; diff --git a/x-pack/plugins/observability/public/components/app/cases/create/flyout.test.tsx b/x-pack/plugins/observability/public/components/app/cases/create/flyout.test.tsx index d413a2d5e0018a..c3580671237471 100644 --- a/x-pack/plugins/observability/public/components/app/cases/create/flyout.test.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/create/flyout.test.tsx @@ -7,12 +7,11 @@ import React from 'react'; import { mount } from 'enzyme'; +import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; -import '../../../common/mock/match_media'; import { CreateCaseFlyout } from './flyout'; -import { TestProviders } from '../../../common/mock'; -jest.mock('../../../common/lib/kibana', () => ({ +jest.mock('../../../../utils/kibana_react', () => ({ useKibana: () => ({ services: { cases: { @@ -35,9 +34,9 @@ describe('CreateCaseFlyout', () => { it('renders', () => { const wrapper = mount( - + - + ); expect(wrapper.find(`[data-test-subj='create-case-flyout']`).exists()).toBeTruthy(); @@ -45,9 +44,9 @@ describe('CreateCaseFlyout', () => { it('Closing modal calls onCloseCaseModal', () => { const wrapper = mount( - + - + ); wrapper.find('.euiFlyout__closeButton').first().simulate('click'); diff --git a/x-pack/plugins/observability/public/components/app/cases/create/index.tsx b/x-pack/plugins/observability/public/components/app/cases/create/index.tsx index 0200a4791f43d0..03d9af4696ab00 100644 --- a/x-pack/plugins/observability/public/components/app/cases/create/index.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/create/index.tsx @@ -10,7 +10,7 @@ import { EuiPanel } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; import { useKibana } from '../../../../utils/kibana_react'; -import { getCaseDetailsUrl, getCreateCaseUrl } from '../../../../pages/cases/links'; +import { getCaseDetailsUrl } from '../../../../pages/cases/links'; import { CASES_APP_ID, CASES_OWNER } from '../constants'; export const Create = React.memo(() => { @@ -35,6 +35,7 @@ export const Create = React.memo(() => { return ( {cases.getCreateCase({ + disableAlerts: true, onCancel: handleSetIsCancel, onSuccess, owner: [CASES_OWNER], From fe20f5c416592050d5dc61c6c1d376f1a9b5f7c9 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 25 May 2021 10:23:50 -0600 Subject: [PATCH 075/113] fix submitCase when no alerting --- x-pack/plugins/cases/public/components/create/form_context.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/cases/public/components/create/form_context.tsx b/x-pack/plugins/cases/public/components/create/form_context.tsx index 8584892e1286c0..30a60fb5c1e47f 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.tsx @@ -73,7 +73,7 @@ export const FormContext: React.FC = ({ const submitCase = useCallback( async ( - { connectorId: dataConnectorId, fields, syncAlerts, ...dataWithoutConnectorId }, + { connectorId: dataConnectorId, fields, syncAlerts = true, ...dataWithoutConnectorId }, isValid ) => { if (isValid) { From 5345d40d4fe86e45bfe7ada00762fde346994db1 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 25 May 2021 10:56:45 -0600 Subject: [PATCH 076/113] fix up breadcrumbs --- .../components/app/cases/case_view/index.tsx | 22 +++++++++---- .../public/hooks/use_breadcrumbs.ts | 6 ++++ .../observability/public/routes/index.tsx | 31 +++++++------------ 3 files changed, 33 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx b/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx index e1df364a99fffe..7c844ce9bf7bd3 100644 --- a/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx @@ -18,6 +18,7 @@ import { Case } from '../../../../../../cases/common'; import { useFetchAlertData } from './helpers'; import { useKibana } from '../../../../utils/kibana_react'; import { CASES_APP_ID } from '../constants'; +import { casesBreadcrumb, useBreadcrumbs } from '../../../../hooks/use_breadcrumbs'; interface Props { caseId: string; @@ -39,17 +40,26 @@ export interface CaseProps extends Props { } export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) => { - const [spyState, setSpyState] = useState<{ caseTitle: string | undefined }>({ - caseTitle: undefined, - }); + const [caseTitle, setCaseTitle] = useState(null); + + useBreadcrumbs([ + casesBreadcrumb, + ...(caseTitle !== null + ? [ + { + text: caseTitle, + }, + ] + : []), + ]); const onCaseDataSuccess = useCallback( (data: Case) => { - if (spyState.caseTitle === undefined) { - setSpyState({ caseTitle: data.title }); + if (caseTitle === null) { + setCaseTitle(data.title); } }, - [spyState.caseTitle] + [caseTitle] ); const { diff --git a/x-pack/plugins/observability/public/hooks/use_breadcrumbs.ts b/x-pack/plugins/observability/public/hooks/use_breadcrumbs.ts index d31b6b52744c0c..c2d7dadf0ed7ea 100644 --- a/x-pack/plugins/observability/public/hooks/use_breadcrumbs.ts +++ b/x-pack/plugins/observability/public/hooks/use_breadcrumbs.ts @@ -40,6 +40,12 @@ export const makeBaseBreadcrumb = (href: string): EuiBreadcrumb => { }; }; +export const casesBreadcrumb: EuiBreadcrumb = { + text: i18n.translate('xpack.observability.breadcrumbs.observability.cases', { + defaultMessage: 'Cases', + }), +}; + export const useBreadcrumbs = (extraCrumbs: ChromeBreadcrumb[]) => { const params = useQueryParams(); diff --git a/x-pack/plugins/observability/public/routes/index.tsx b/x-pack/plugins/observability/public/routes/index.tsx index 76da497d823585..a5ed961ec29b3d 100644 --- a/x-pack/plugins/observability/public/routes/index.tsx +++ b/x-pack/plugins/observability/public/routes/index.tsx @@ -18,6 +18,7 @@ import { ExploratoryViewPage } from '../components/shared/exploratory_view'; import { CaseDetailsPage } from '../pages/cases/case_details'; import { ConfigureCasesPage } from '../pages/cases/configure_cases'; import { AllCasesPage } from '../pages/cases/all_cases'; +import { casesBreadcrumb } from '../hooks/use_breadcrumbs'; export type RouteParams = DecodeParams; @@ -84,13 +85,7 @@ export const routes = { return ; }, params: {}, - breadcrumb: [ - { - text: i18n.translate('xpack.observability.cases.breadcrumb', { - defaultMessage: 'Cases', - }), - }, - ], + breadcrumb: [casesBreadcrumb], }, '/cases/create': { handler: () => { @@ -98,9 +93,10 @@ export const routes = { }, params: {}, breadcrumb: [ + casesBreadcrumb, { - text: i18n.translate('xpack.observability.cases.breadcrumb', { - defaultMessage: 'Cases', + text: i18n.translate('xpack.observability.breadcrumbs.observability.cases.create', { + defaultMessage: 'Create', }), }, ], @@ -111,29 +107,24 @@ export const routes = { }, params: {}, breadcrumb: [ + casesBreadcrumb, { - text: i18n.translate('xpack.observability.cases.breadcrumb', { - defaultMessage: 'Cases', + text: i18n.translate('xpack.observability.breadcrumbs.observability.cases.configure', { + defaultMessage: 'Configure', }), }, ], }, '/cases/:detailName': { - handler: (routeParams: any) => { - return ; + handler: () => { + return ; }, params: { path: t.partial({ detailName: t.string, }), }, - breadcrumb: [ - { - text: i18n.translate('xpack.observability.cases.breadcrumb', { - defaultMessage: 'Cases', - }), - }, - ], + breadcrumb: [casesBreadcrumb], }, '/alerts': { handler: (routeParams: any) => { From 8c2cc612c5e6994808851c6a57b769e0886189b7 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 25 May 2021 11:24:19 -0600 Subject: [PATCH 077/113] Fix jest --- .../app/cases/callout/index.test.tsx | 36 ++++---- .../components/app/cases/case_view/index.tsx | 2 - .../app/cases/create/index.test.tsx | 84 ++++++------------- .../components/app/cases/create/index.tsx | 11 +-- .../public/hooks/use_messages_storage.tsx | 1 - 5 files changed, 48 insertions(+), 86 deletions(-) diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/index.test.tsx b/x-pack/plugins/observability/public/components/app/cases/callout/index.test.tsx index 762b02bf94b169..29b37b94305bf6 100644 --- a/x-pack/plugins/observability/public/components/app/cases/callout/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/callout/index.test.tsx @@ -8,12 +8,12 @@ import React from 'react'; import { mount } from 'enzyme'; +import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; import { useMessagesStorage } from '../../../../hooks/use_messages_storage'; import { createCalloutId } from './helpers'; import { CaseCallOut, CaseCallOutProps } from '.'; -jest.mock('../../../common/containers/local_storage/use_messages_storage'); -const TestProviders = (children: any) => children; +jest.mock('../../../../hooks/use_messages_storage'); const useSecurityLocalStorageMock = useMessagesStorage as jest.Mock; const securityLocalStorageMock = { getMessages: jest.fn(() => []), @@ -35,9 +35,9 @@ describe('CaseCallOut ', () => { ], }; const wrapper = mount( - + - + ); const id = createCalloutId(['message-one', 'message-two']); @@ -59,9 +59,9 @@ describe('CaseCallOut ', () => { }; const wrapper = mount( - + - + ); const idDanger = createCalloutId(['message-one']); @@ -83,9 +83,9 @@ describe('CaseCallOut ', () => { ], }; const wrapper = mount( - + - + ); const id = createCalloutId(['message-one']); @@ -104,9 +104,9 @@ describe('CaseCallOut ', () => { }; const wrapper = mount( - + - + ); const id = createCalloutId(['message-one']); @@ -131,9 +131,9 @@ describe('CaseCallOut ', () => { })); const wrapper = mount( - + - + ); expect(wrapper.find(`[data-test-subj="case-callout-${id}"]`).last().exists()).toBeFalsy(); @@ -153,9 +153,9 @@ describe('CaseCallOut ', () => { }; const wrapper = mount( - + - + ); const id = createCalloutId(['message-one']); @@ -178,9 +178,9 @@ describe('CaseCallOut ', () => { }; const wrapper = mount( - + - + ); const id = createCalloutId(['message-one']); @@ -203,9 +203,9 @@ describe('CaseCallOut ', () => { }; const wrapper = mount( - + - + ); const id = createCalloutId(['message-one']); diff --git a/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx b/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx index 7c844ce9bf7bd3..2f00d40d953acb 100644 --- a/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx @@ -118,5 +118,3 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = userCanCrud, }); }); - -CaseView.displayName = 'CaseView'; diff --git a/x-pack/plugins/observability/public/components/app/cases/create/index.test.tsx b/x-pack/plugins/observability/public/components/app/cases/create/index.test.tsx index 1a6015d1bbd457..ec7511836328b0 100644 --- a/x-pack/plugins/observability/public/components/app/cases/create/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/create/index.test.tsx @@ -7,48 +7,41 @@ import React from 'react'; import { mount } from 'enzyme'; -import { act, waitFor } from '@testing-library/react'; -import { noop } from 'lodash/fp'; - -import { TestProviders } from '../../../common/mock'; -import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; -import { useInsertTimeline } from '../use_insert_timeline'; +import { waitFor } from '@testing-library/react'; +import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; import { Create } from '.'; -import { useKibana } from '../../../common/lib/kibana'; -import { Case } from '../../../../../cases/public/containers/types'; -import { basicCase } from '../../../../../cases/public/containers/mock'; -import { APP_ID } from '../../../../common/constants'; - -jest.mock('../use_insert_timeline'); -jest.mock('../../../common/lib/kibana'); +import { useKibana } from '../../../../utils/kibana_react'; +import { basicCase } from '../../../../../../cases/public/containers/mock'; +import { CASES_APP_ID, CASES_OWNER } from '../constants'; +import { Case } from '../../../../../../cases/common'; +import { getCaseDetailsUrl } from '../../../../pages/cases/links'; -const useInsertTimelineMock = useInsertTimeline as jest.Mock; +jest.mock('../../../../utils/kibana_react'); describe('Create case', () => { const mockCreateCase = jest.fn(); + const mockNavigateToApp = jest.fn(); beforeEach(() => { jest.resetAllMocks(); - jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); (useKibana as jest.Mock).mockReturnValue({ services: { cases: { getCreateCase: mockCreateCase, }, + application: { navigateToApp: mockNavigateToApp }, }, }); }); it('it renders', () => { mount( - - - - - + + + ); expect(mockCreateCase).toHaveBeenCalled(); - expect(mockCreateCase.mock.calls[0][0].owner).toEqual([APP_ID]); + expect(mockCreateCase.mock.calls[0][0].owner).toEqual([CASES_OWNER]); }); it('should redirect to all cases on cancel click', async () => { @@ -59,17 +52,16 @@ describe('Create case', () => { onCancel(); }, }, + application: { navigateToApp: mockNavigateToApp }, }, }); mount( - - - - - + + + ); - await waitFor(() => expect(mockHistory.push).toHaveBeenCalledWith('/')); + await waitFor(() => expect(mockNavigateToApp).toHaveBeenCalledWith(`${CASES_APP_ID}`)); }); it('should redirect to new case when posting the case', async () => { @@ -80,41 +72,19 @@ describe('Create case', () => { onSuccess(basicCase); }, }, + application: { navigateToApp: mockNavigateToApp }, }, }); mount( - - - - - + + + ); - await waitFor(() => expect(mockHistory.push).toHaveBeenNthCalledWith(1, '/basic-case-id')); - }); - - it.skip('it should insert a timeline', async () => { - let attachTimeline = noop; - useInsertTimelineMock.mockImplementation((value, onTimelineAttached) => { - attachTimeline = onTimelineAttached; - }); - - const wrapper = mount( - - - - - + await waitFor(() => + expect(mockNavigateToApp).toHaveBeenNthCalledWith(1, `${CASES_APP_ID}`, { + path: getCaseDetailsUrl({ id: basicCase.id }), + }) ); - - act(() => { - attachTimeline('[title](url)'); - }); - - await waitFor(() => { - expect(wrapper.find(`[data-test-subj="caseDescription"] textarea`).text()).toBe( - '[title](url)' - ); - }); }); }); diff --git a/x-pack/plugins/observability/public/components/app/cases/create/index.tsx b/x-pack/plugins/observability/public/components/app/cases/create/index.tsx index 03d9af4696ab00..d7e2daea2490b4 100644 --- a/x-pack/plugins/observability/public/components/app/cases/create/index.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/create/index.tsx @@ -7,7 +7,6 @@ import React, { useCallback } from 'react'; import { EuiPanel } from '@elastic/eui'; -import { useHistory } from 'react-router-dom'; import { useKibana } from '../../../../utils/kibana_react'; import { getCaseDetailsUrl } from '../../../../pages/cases/links'; @@ -18,19 +17,15 @@ export const Create = React.memo(() => { cases, application: { navigateToApp }, } = useKibana().services; - const history = useHistory(); const onSuccess = useCallback( - async ({ id }) => { + async ({ id }) => navigateToApp(`${CASES_APP_ID}`, { path: getCaseDetailsUrl({ id }), - }); - }, + }), [navigateToApp] ); - const handleSetIsCancel = useCallback(() => { - history.push('/'); - }, [history]); + const handleSetIsCancel = useCallback(() => navigateToApp(`${CASES_APP_ID}`), [navigateToApp]); return ( diff --git a/x-pack/plugins/observability/public/hooks/use_messages_storage.tsx b/x-pack/plugins/observability/public/hooks/use_messages_storage.tsx index 7063fdeeeb8df0..cb457ab61fed3e 100644 --- a/x-pack/plugins/observability/public/hooks/use_messages_storage.tsx +++ b/x-pack/plugins/observability/public/hooks/use_messages_storage.tsx @@ -19,7 +19,6 @@ export interface UseMessagesStorage { export const useMessagesStorage = (): UseMessagesStorage => { // @ts-ignore const { storage } = useKibana().services; - console.log('STORAGE', storage); const getMessages = useCallback( (plugin: string): string[] => storage.get(`${plugin}-messages`) ?? [], From 528c9be256852f97e5e62d1bc8ed0073a36be74f Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 25 May 2021 11:31:44 -0600 Subject: [PATCH 078/113] Fix types --- x-pack/plugins/observability/public/hooks/use_breadcrumbs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/observability/public/hooks/use_breadcrumbs.ts b/x-pack/plugins/observability/public/hooks/use_breadcrumbs.ts index c2d7dadf0ed7ea..595cc86f8d9fcf 100644 --- a/x-pack/plugins/observability/public/hooks/use_breadcrumbs.ts +++ b/x-pack/plugins/observability/public/hooks/use_breadcrumbs.ts @@ -40,7 +40,7 @@ export const makeBaseBreadcrumb = (href: string): EuiBreadcrumb => { }; }; -export const casesBreadcrumb: EuiBreadcrumb = { +export const casesBreadcrumb = { text: i18n.translate('xpack.observability.breadcrumbs.observability.cases', { defaultMessage: 'Cases', }), From bd694caa16cf5d0ab97b8f138910d946a0d812d4 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 25 May 2021 11:45:35 -0600 Subject: [PATCH 079/113] change key for message storage --- .../public/components/app/cases/callout/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/index.tsx b/x-pack/plugins/observability/public/components/app/cases/callout/index.tsx index cb1958b9926faa..67085bdd48832a 100644 --- a/x-pack/plugins/observability/public/components/app/cases/callout/index.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/callout/index.tsx @@ -34,7 +34,7 @@ interface CalloutVisibility { function CaseCallOutComponent({ title, messages = [] }: CaseCallOutProps) { const { getMessages, addMessage } = useMessagesStorage(); - const caseMessages = useMemo(() => getMessages('case'), [getMessages]); + const caseMessages = useMemo(() => getMessages('observability'), [getMessages]); const dismissedCallouts = useMemo( () => caseMessages.reduce( @@ -52,7 +52,7 @@ function CaseCallOutComponent({ title, messages = [] }: CaseCallOutProps) { (id, type) => { setCalloutVisibility((prevState) => ({ ...prevState, [id]: false })); if (type === 'primary') { - addMessage('case', id); + addMessage('observability', id); } }, [setCalloutVisibility, addMessage] From 9ba1243eefa649c21454fb2a3e34092743fa1029 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Tue, 25 May 2021 15:29:47 -0400 Subject: [PATCH 080/113] Adjusting permissions text and fixing status --- .../cases/public/common/translations.ts | 10 ++++---- .../public/components/callout/helpers.tsx | 4 ++-- .../public/components/callout/translations.ts | 13 ++++------ .../components/case_action_bar/index.tsx | 1 + .../status_context_menu.test.tsx | 12 ++++++++++ .../case_action_bar/status_context_menu.tsx | 11 ++++++--- .../public/components/status/status.test.tsx | 24 +++++++++++++++++++ .../cases/public/components/status/status.tsx | 9 ++++++- .../cases/components/callout/helpers.tsx | 4 ++-- .../cases/components/callout/translations.ts | 8 +++---- .../public/cases/pages/case.tsx | 4 ++-- ...issions.tsx => feature_no_permissions.tsx} | 14 +++++------ .../public/cases/pages/translations.ts | 10 ++++---- .../public/cases/translations.ts | 10 ++++---- .../translations/translations/ja-JP.json | 4 ---- .../translations/translations/zh-CN.json | 4 ---- 16 files changed, 90 insertions(+), 52 deletions(-) rename x-pack/plugins/security_solution/public/cases/pages/{saved_object_no_permissions.tsx => feature_no_permissions.tsx} (64%) diff --git a/x-pack/plugins/cases/public/common/translations.ts b/x-pack/plugins/cases/public/common/translations.ts index 834bd1292ccdd6..85cfb60b1d6b81 100644 --- a/x-pack/plugins/cases/public/common/translations.ts +++ b/x-pack/plugins/cases/public/common/translations.ts @@ -7,18 +7,18 @@ import { i18n } from '@kbn/i18n'; -export const SAVED_OBJECT_NO_PERMISSIONS_TITLE = i18n.translate( - 'xpack.cases.caseSavedObjectNoPermissionsTitle', +export const CASES_FEATURE_NO_PERMISSIONS_TITLE = i18n.translate( + 'xpack.cases.caseFeatureNoPermissionsTitle', { defaultMessage: 'Kibana feature privileges required', } ); -export const SAVED_OBJECT_NO_PERMISSIONS_MSG = i18n.translate( - 'xpack.cases.caseSavedObjectNoPermissionsMessage', +export const CASES_FEATURE_NO_PERMISSIONS_MSG = i18n.translate( + 'xpack.cases.caseFeatureNoPermissionsMessage', { defaultMessage: - 'To view cases, you must have privileges for the Saved Object Management feature in the Kibana space. For more information, contact your Kibana administrator.', + 'To view cases, you must have privileges for the Cases feature in the Kibana space. For more information, contact your Kibana administrator.', } ); diff --git a/x-pack/plugins/cases/public/components/callout/helpers.tsx b/x-pack/plugins/cases/public/components/callout/helpers.tsx index 2a7804579a57e8..3409c5eb94245e 100644 --- a/x-pack/plugins/cases/public/components/callout/helpers.tsx +++ b/x-pack/plugins/cases/public/components/callout/helpers.tsx @@ -13,8 +13,8 @@ import { ErrorMessage } from './types'; export const savedObjectReadOnlyErrorMessage: ErrorMessage = { id: 'read-only-privileges-error', - title: i18n.READ_ONLY_SAVED_OBJECT_TITLE, - description: <>{i18n.READ_ONLY_SAVED_OBJECT_MSG}, + title: i18n.READ_ONLY_FEATURE_TITLE, + description: <>{i18n.READ_ONLY_FEATURE_MSG}, errorType: 'warning', }; diff --git a/x-pack/plugins/cases/public/components/callout/translations.ts b/x-pack/plugins/cases/public/components/callout/translations.ts index 6d4b55603a06fd..dca622e60c863b 100644 --- a/x-pack/plugins/cases/public/components/callout/translations.ts +++ b/x-pack/plugins/cases/public/components/callout/translations.ts @@ -7,17 +7,14 @@ import { i18n } from '@kbn/i18n'; -export const READ_ONLY_SAVED_OBJECT_TITLE = i18n.translate('xpack.cases.readOnlySavedObjectTitle', { +export const READ_ONLY_FEATURE_TITLE = i18n.translate('xpack.cases.readOnlyFeatureTitle', { defaultMessage: 'You cannot open new or update existing cases', }); -export const READ_ONLY_SAVED_OBJECT_MSG = i18n.translate( - 'xpack.cases.readOnlySavedObjectDescription', - { - defaultMessage: - 'You only have privileges to view cases. If you need to open and update cases, contact your Kibana administrator.', - } -); +export const READ_ONLY_FEATURE_MSG = i18n.translate('xpack.cases.readOnlyFeatureDescription', { + defaultMessage: + 'You only have privileges to view cases. If you need to open and update cases, contact your Kibana administrator.', +}); export const DISMISS_CALLOUT = i18n.translate('xpack.cases.dismissErrorsPushServiceCallOutTitle', { defaultMessage: 'Dismiss', diff --git a/x-pack/plugins/cases/public/components/case_action_bar/index.tsx b/x-pack/plugins/cases/public/components/case_action_bar/index.tsx index 0f06dde6a86d13..a68ae4b3ca6a74 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/index.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/index.tsx @@ -83,6 +83,7 @@ const CaseActionBarComponent: React.FC = ({ diff --git a/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.test.tsx b/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.test.tsx index 29cca46d372f02..54cbbc5b6841f5 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.test.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.test.tsx @@ -26,6 +26,18 @@ describe('SyncAlertsSwitch', () => { expect(wrapper.find(`[data-test-subj="case-view-status-dropdown"]`).exists()).toBeTruthy(); }); + it('it renders when disabled', async () => { + const wrapper = mount( + + ); + + expect(wrapper.find(`[data-test-subj="case-view-status-dropdown"]`).exists()).toBeTruthy(); + }); + it('it renders the current status correctly', async () => { const wrapper = mount( diff --git a/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.tsx b/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.tsx index 2922b797f9d404..65a220b65e4031 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.tsx @@ -13,16 +13,21 @@ import { Status } from '../status'; interface Props { currentStatus: CaseStatuses; + disabled?: boolean; onStatusChanged: (status: CaseStatuses) => void; } -const StatusContextMenuComponent: React.FC = ({ currentStatus, onStatusChanged }) => { +const StatusContextMenuComponent: React.FC = ({ + currentStatus, + onStatusChanged, + disabled = false, +}) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const closePopover = useCallback(() => setIsPopoverOpen(false), []); const openPopover = useCallback(() => setIsPopoverOpen(true), []); const popOverButton = useMemo( - () => , - [currentStatus, openPopover] + () => , + [disabled, currentStatus, openPopover] ); const onContextMenuItemClick = useMemo( diff --git a/x-pack/plugins/cases/public/components/status/status.test.tsx b/x-pack/plugins/cases/public/components/status/status.test.tsx index 7cddbf5ca4a1dc..4d13e57fbdee71 100644 --- a/x-pack/plugins/cases/public/components/status/status.test.tsx +++ b/x-pack/plugins/cases/public/components/status/status.test.tsx @@ -31,6 +31,30 @@ describe('Stats', () => { ).toBeTruthy(); }); + it('it renders with the pop over enabled by default', async () => { + const wrapper = mount(); + + expect( + wrapper + .find(`[data-test-subj="status-badge-open"] .euiBadge__iconButton`) + .first() + .prop('disabled') + ).toBe(false); + }); + + it('it renders with the pop over disabled when initialized disabled', async () => { + const wrapper = mount( + + ); + + expect( + wrapper + .find(`[data-test-subj="status-badge-open"] .euiBadge__iconButton`) + .first() + .prop('disabled') + ).toBe(true); + }); + it('it calls onClick when pressing the badge', async () => { const wrapper = mount(); diff --git a/x-pack/plugins/cases/public/components/status/status.tsx b/x-pack/plugins/cases/public/components/status/status.tsx index 03dca8642aed7d..3b832ce155400c 100644 --- a/x-pack/plugins/cases/public/components/status/status.tsx +++ b/x-pack/plugins/cases/public/components/status/status.tsx @@ -14,12 +14,18 @@ import * as i18n from './translations'; import { CaseStatusWithAllStatus, StatusAll } from '../../../common'; interface Props { + disabled?: boolean; type: CaseStatusWithAllStatus; withArrow?: boolean; onClick?: () => void; } -const StatusComponent: React.FC = ({ type, withArrow = false, onClick = noop }) => { +const StatusComponent: React.FC = ({ + type, + disabled = false, + withArrow = false, + onClick = noop, +}) => { const props = useMemo( () => ({ color: type === StatusAll ? allCaseStatus[StatusAll].color : statuses[type].color, @@ -34,6 +40,7 @@ const StatusComponent: React.FC = ({ type, withArrow = false, onClick = n iconOnClick={onClick} iconOnClickAriaLabel={i18n.STATUS_ICON_ARIA} data-test-subj={`status-badge-${type}`} + isDisabled={disabled} > {type === StatusAll ? allCaseStatus[StatusAll].label : statuses[type].label} diff --git a/x-pack/plugins/security_solution/public/cases/components/callout/helpers.tsx b/x-pack/plugins/security_solution/public/cases/components/callout/helpers.tsx index 2a7804579a57e8..3409c5eb94245e 100644 --- a/x-pack/plugins/security_solution/public/cases/components/callout/helpers.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/callout/helpers.tsx @@ -13,8 +13,8 @@ import { ErrorMessage } from './types'; export const savedObjectReadOnlyErrorMessage: ErrorMessage = { id: 'read-only-privileges-error', - title: i18n.READ_ONLY_SAVED_OBJECT_TITLE, - description: <>{i18n.READ_ONLY_SAVED_OBJECT_MSG}, + title: i18n.READ_ONLY_FEATURE_TITLE, + description: <>{i18n.READ_ONLY_FEATURE_MSG}, errorType: 'warning', }; diff --git a/x-pack/plugins/security_solution/public/cases/components/callout/translations.ts b/x-pack/plugins/security_solution/public/cases/components/callout/translations.ts index 4a5f32684ccdec..db4809126452f9 100644 --- a/x-pack/plugins/security_solution/public/cases/components/callout/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/components/callout/translations.ts @@ -7,15 +7,15 @@ import { i18n } from '@kbn/i18n'; -export const READ_ONLY_SAVED_OBJECT_TITLE = i18n.translate( - 'xpack.securitySolution.cases.readOnlySavedObjectTitle', +export const READ_ONLY_FEATURE_TITLE = i18n.translate( + 'xpack.securitySolution.cases.readOnlyFeatureTitle', { defaultMessage: 'You cannot open new or update existing cases', } ); -export const READ_ONLY_SAVED_OBJECT_MSG = i18n.translate( - 'xpack.securitySolution.cases.readOnlySavedObjectDescription', +export const READ_ONLY_FEATURE_MSG = i18n.translate( + 'xpack.securitySolution.cases.readOnlyFeatureDescription', { defaultMessage: 'You only have privileges to view cases. If you need to open and update cases, contact your Kibana administrator.', diff --git a/x-pack/plugins/security_solution/public/cases/pages/case.tsx b/x-pack/plugins/security_solution/public/cases/pages/case.tsx index 4f0163eb8190a6..4ec29b676afe6e 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/case.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/case.tsx @@ -13,7 +13,7 @@ import { SpyRoute } from '../../common/utils/route/spy_routes'; import { AllCases } from '../components/all_cases'; import { savedObjectReadOnlyErrorMessage, CaseCallOut } from '../components/callout'; -import { CaseSavedObjectNoPermissions } from './saved_object_no_permissions'; +import { CaseFeatureNoPermissions } from './feature_no_permissions'; import { SecurityPageName } from '../../app/types'; export const CasesPage = React.memo(() => { @@ -33,7 +33,7 @@ export const CasesPage = React.memo(() => { ) : ( - + ); }); diff --git a/x-pack/plugins/security_solution/public/cases/pages/saved_object_no_permissions.tsx b/x-pack/plugins/security_solution/public/cases/pages/feature_no_permissions.tsx similarity index 64% rename from x-pack/plugins/security_solution/public/cases/pages/saved_object_no_permissions.tsx rename to x-pack/plugins/security_solution/public/cases/pages/feature_no_permissions.tsx index dd173e18ca63e9..9975db3d8b6fb7 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/saved_object_no_permissions.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/feature_no_permissions.tsx @@ -11,14 +11,14 @@ import { EmptyPage } from '../../common/components/empty_page'; import * as i18n from './translations'; import { useKibana } from '../../common/lib/kibana'; -export const CaseSavedObjectNoPermissions = React.memo(() => { +export const CaseFeatureNoPermissions = React.memo(() => { const docLinks = useKibana().services.docLinks; const actions = useMemo( () => ({ - savedObject: { + feature: { icon: 'documents', label: i18n.GO_TO_DOCUMENTATION, - url: `${docLinks.ELASTIC_WEBSITE_URL}guide/en/security/${docLinks.DOC_LINK_VERSION}s`, + url: `${docLinks.links.siem.gettingStarted}`, target: '_blank', }, }), @@ -28,11 +28,11 @@ export const CaseSavedObjectNoPermissions = React.memo(() => { return ( ); }); -CaseSavedObjectNoPermissions.displayName = 'CaseSavedObjectNoPermissions'; +CaseFeatureNoPermissions.displayName = 'CaseFeatureNoPermissions'; diff --git a/x-pack/plugins/security_solution/public/cases/pages/translations.ts b/x-pack/plugins/security_solution/public/cases/pages/translations.ts index 0abf7461681cf6..e45aca87ff1f95 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/pages/translations.ts @@ -7,18 +7,18 @@ import { i18n } from '@kbn/i18n'; -export const SAVED_OBJECT_NO_PERMISSIONS_TITLE = i18n.translate( - 'xpack.securitySolution.cases.caseSavedObjectNoPermissionsTitle', +export const CASES_FEATURE_NO_PERMISSIONS_TITLE = i18n.translate( + 'xpack.securitySolution.cases.caseFeatureNoPermissionsTitle', { defaultMessage: 'Kibana feature privileges required', } ); -export const SAVED_OBJECT_NO_PERMISSIONS_MSG = i18n.translate( - 'xpack.securitySolution.cases.caseSavedObjectNoPermissionsMessage', +export const CASES_FEATURE_NO_PERMISSIONS_MSG = i18n.translate( + 'xpack.securitySolution.cases.caseFeatureNoPermissionsMessage', { defaultMessage: - 'To view cases, you must have privileges for the Saved Object Management feature in the Kibana space. For more information, contact your Kibana administrator.', + 'To view cases, you must have privileges for the Cases feature in the Kibana space. For more information, contact your Kibana administrator.', } ); diff --git a/x-pack/plugins/security_solution/public/cases/translations.ts b/x-pack/plugins/security_solution/public/cases/translations.ts index e8e4f207f2d235..63fc5695ebab1f 100644 --- a/x-pack/plugins/security_solution/public/cases/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/translations.ts @@ -7,18 +7,18 @@ import { i18n } from '@kbn/i18n'; -export const SAVED_OBJECT_NO_PERMISSIONS_TITLE = i18n.translate( - 'xpack.securitySolution.cases.caseSavedObjectNoPermissionsTitle', +export const CASES_FEATURE_NO_PERMISSIONS_TITLE = i18n.translate( + 'xpack.securitySolution.cases.caseFeatureNoPermissionsTitle', { defaultMessage: 'Kibana feature privileges required', } ); -export const SAVED_OBJECT_NO_PERMISSIONS_MSG = i18n.translate( - 'xpack.securitySolution.cases.caseSavedObjectNoPermissionsMessage', +export const CASES_FEATURE_NO_PERMISSIONS_MSG = i18n.translate( + 'xpack.securitySolution.cases.caseFeatureNoPermissionsMessage', { defaultMessage: - 'To view cases, you must have privileges for the Saved Object Management feature in the Kibana space. For more information, contact your Kibana administrator.', + 'To view cases, you must have privileges for the Cases feature in the Kibana space. For more information, contact your Kibana administrator.', } ); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 134f58236cfee3..d78fd58b55a7ae 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -19442,8 +19442,6 @@ "xpack.securitySolution.cases.allCases.actions": "アクション", "xpack.securitySolution.cases.allCases.comments": "コメント", "xpack.securitySolution.cases.allCases.noTagsAvailable": "利用可能なタグがありません", - "xpack.securitySolution.cases.caseSavedObjectNoPermissionsMessage": "ケースを表示するには、Kibana スペースで保存されたオブジェクト管理機能の権限が必要です。詳細については、Kibana管理者に連絡してください。", - "xpack.securitySolution.cases.caseSavedObjectNoPermissionsTitle": "Kibana機能権限が必要です", "xpack.securitySolution.cases.caseTable.caseDetailsLinkAria": "クリックすると、タイトル{detailName}のケースを表示します", "xpack.securitySolution.cases.caseTable.closedCases": "終了したケース", "xpack.securitySolution.cases.caseTable.inProgressCases": "進行中のケース", @@ -19491,8 +19489,6 @@ "xpack.securitySolution.cases.createCase.titleFieldRequiredError": "タイトルが必要です。", "xpack.securitySolution.cases.dismissErrorsPushServiceCallOutTitle": "閉じる", "xpack.securitySolution.cases.pageTitle": "ケース", - "xpack.securitySolution.cases.readOnlySavedObjectDescription": "ケースを表示する権限のみが付与されています。ケースを開いて更新する必要がある場合は、Kibana管理者に連絡してください。", - "xpack.securitySolution.cases.readOnlySavedObjectTitle": "新しいケースを開いたり、既存のケースを更新したりすることはできません", "xpack.securitySolution.cases.settings.syncAlertsSwitchLabelOff": "オフ", "xpack.securitySolution.cases.settings.syncAlertsSwitchLabelOn": "オン", "xpack.securitySolution.cases.timeline.actions.addCase": "ケースに追加", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 67677f86ddbf78..b12380d278d160 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -19715,8 +19715,6 @@ "xpack.securitySolution.cases.allCases.actions": "操作", "xpack.securitySolution.cases.allCases.comments": "注释", "xpack.securitySolution.cases.allCases.noTagsAvailable": "没有可用标签", - "xpack.securitySolution.cases.caseSavedObjectNoPermissionsMessage": "要查看案例,必须对 Kibana 工作区中的已保存对象管理功能有权限。有关详细信息,请联系您的 Kibana 管理员。", - "xpack.securitySolution.cases.caseSavedObjectNoPermissionsTitle": "需要 Kibana 功能权限", "xpack.securitySolution.cases.caseTable.caseDetailsLinkAria": "单击以访问标题为 {detailName} 的案例", "xpack.securitySolution.cases.caseTable.closedCases": "已关闭案例", "xpack.securitySolution.cases.caseTable.inProgressCases": "进行中的案例", @@ -19764,8 +19762,6 @@ "xpack.securitySolution.cases.createCase.titleFieldRequiredError": "标题必填。", "xpack.securitySolution.cases.dismissErrorsPushServiceCallOutTitle": "关闭", "xpack.securitySolution.cases.pageTitle": "案例", - "xpack.securitySolution.cases.readOnlySavedObjectDescription": "您仅有权查看案例。如果需要创建和更新案例,请联系您的 Kibana 管理员。", - "xpack.securitySolution.cases.readOnlySavedObjectTitle": "您无法创建新案例或更新现有案例", "xpack.securitySolution.cases.settings.syncAlertsSwitchLabelOff": "关闭", "xpack.securitySolution.cases.settings.syncAlertsSwitchLabelOn": "开启", "xpack.securitySolution.cases.timeline.actions.addCase": "添加到案例", From 9ef3ab41264276ac3c1a4953924a307341d3cc4e Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 25 May 2021 14:21:30 -0600 Subject: [PATCH 081/113] may it please CI --- .../public/components/app/cases/callout/helpers.tsx | 4 ++-- .../components/app/cases/callout/index.test.tsx | 4 ++-- .../components/app/cases/callout/translations.ts | 8 ++++---- .../public/components/app/cases/translations.ts | 11 +++++------ .../observability/public/pages/cases/all_cases.tsx | 4 ++-- ..._no_permissions.tsx => feature_no_permissions.tsx} | 10 +++++----- x-pack/plugins/translations/translations/ja-JP.json | 4 ---- x-pack/plugins/translations/translations/zh-CN.json | 4 ---- .../api_integration/apis/security/privileges_basic.ts | 1 + 9 files changed, 21 insertions(+), 29 deletions(-) rename x-pack/plugins/observability/public/pages/cases/{saved_object_no_permissions.tsx => feature_no_permissions.tsx} (74%) diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/helpers.tsx b/x-pack/plugins/observability/public/components/app/cases/callout/helpers.tsx index 2a7804579a57e8..3409c5eb94245e 100644 --- a/x-pack/plugins/observability/public/components/app/cases/callout/helpers.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/callout/helpers.tsx @@ -13,8 +13,8 @@ import { ErrorMessage } from './types'; export const savedObjectReadOnlyErrorMessage: ErrorMessage = { id: 'read-only-privileges-error', - title: i18n.READ_ONLY_SAVED_OBJECT_TITLE, - description: <>{i18n.READ_ONLY_SAVED_OBJECT_MSG}, + title: i18n.READ_ONLY_FEATURE_TITLE, + description: <>{i18n.READ_ONLY_FEATURE_MSG}, errorType: 'warning', }; diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/index.test.tsx b/x-pack/plugins/observability/public/components/app/cases/callout/index.test.tsx index 29b37b94305bf6..e7ed339d99e907 100644 --- a/x-pack/plugins/observability/public/components/app/cases/callout/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/callout/index.test.tsx @@ -110,9 +110,9 @@ describe('CaseCallOut ', () => { ); const id = createCalloutId(['message-one']); - expect(securityLocalStorageMock.getMessages).toHaveBeenCalledWith('case'); + expect(securityLocalStorageMock.getMessages).toHaveBeenCalledWith('observability'); wrapper.find(`[data-test-subj="callout-dismiss-${id}"]`).last().simulate('click'); - expect(securityLocalStorageMock.addMessage).toHaveBeenCalledWith('case', id); + expect(securityLocalStorageMock.addMessage).toHaveBeenCalledWith('observability', id); }); it('do not show the callout if is in the localStorage', () => { diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/translations.ts b/x-pack/plugins/observability/public/components/app/cases/callout/translations.ts index 5ac17662d789da..cb7236b445be12 100644 --- a/x-pack/plugins/observability/public/components/app/cases/callout/translations.ts +++ b/x-pack/plugins/observability/public/components/app/cases/callout/translations.ts @@ -7,15 +7,15 @@ import { i18n } from '@kbn/i18n'; -export const READ_ONLY_SAVED_OBJECT_TITLE = i18n.translate( - 'xpack.observability.cases.readOnlySavedObjectTitle', +export const READ_ONLY_FEATURE_TITLE = i18n.translate( + 'xpack.observability.cases.readOnlyFeatureTitle', { defaultMessage: 'You cannot open new or update existing cases', } ); -export const READ_ONLY_SAVED_OBJECT_MSG = i18n.translate( - 'xpack.observability.cases.readOnlySavedObjectDescription', +export const READ_ONLY_FEATURE_MSG = i18n.translate( + 'xpack.observability.cases.readOnlyFeatureDescription', { defaultMessage: 'You only have privileges to view cases. If you need to open and update cases, contact your Kibana administrator.', diff --git a/x-pack/plugins/observability/public/components/app/cases/translations.ts b/x-pack/plugins/observability/public/components/app/cases/translations.ts index d3daa5d9def992..243e444c01328a 100644 --- a/x-pack/plugins/observability/public/components/app/cases/translations.ts +++ b/x-pack/plugins/observability/public/components/app/cases/translations.ts @@ -7,21 +7,20 @@ import { i18n } from '@kbn/i18n'; -export const SAVED_OBJECT_NO_PERMISSIONS_TITLE = i18n.translate( - 'xpack.observability.cases.caseSavedObjectNoPermissionsTitle', +export const CASES_FEATURE_NO_PERMISSIONS_TITLE = i18n.translate( + 'xpack.observability.cases.caseFeatureNoPermissionsTitle', { defaultMessage: 'Kibana feature privileges required', } ); -export const SAVED_OBJECT_NO_PERMISSIONS_MSG = i18n.translate( - 'xpack.observability.cases.caseSavedObjectNoPermissionsMessage', +export const CASES_FEATURE_NO_PERMISSIONS_MSG = i18n.translate( + 'xpack.observability.cases.caseFeatureNoPermissionsMessage', { defaultMessage: - 'To view cases, you must have privileges for the Saved Object Management feature in the Kibana space. For more information, contact your Kibana administrator.', + 'To view cases, you must have privileges for the Cases feature in the Kibana space. For more information, contact your Kibana administrator.', } ); - export const BACK_TO_ALL = i18n.translate('xpack.observability.cases.caseView.backLabel', { defaultMessage: 'Back to cases', }); diff --git a/x-pack/plugins/observability/public/pages/cases/all_cases.tsx b/x-pack/plugins/observability/public/pages/cases/all_cases.tsx index a0beb4e8c7898b..0c69055cc1ab32 100644 --- a/x-pack/plugins/observability/public/pages/cases/all_cases.tsx +++ b/x-pack/plugins/observability/public/pages/cases/all_cases.tsx @@ -13,7 +13,7 @@ import * as i18n from '../../components/app/cases/translations'; import { ExperimentalBadge } from '../../components/shared/experimental_badge'; import { savedObjectReadOnlyErrorMessage, CaseCallOut } from '../../components/app/cases/callout'; -import { CaseSavedObjectNoPermissions } from './saved_object_no_permissions'; +import { CaseFeatureNoPermissions } from './feature_no_permissions'; import { useGetUserCasesPermissions } from '../../hooks/use_get_cases_user_permissions'; export const AllCasesPage = React.memo(() => { @@ -39,7 +39,7 @@ export const AllCasesPage = React.memo(() => { ) : ( - + ); }); diff --git a/x-pack/plugins/observability/public/pages/cases/saved_object_no_permissions.tsx b/x-pack/plugins/observability/public/pages/cases/feature_no_permissions.tsx similarity index 74% rename from x-pack/plugins/observability/public/pages/cases/saved_object_no_permissions.tsx rename to x-pack/plugins/observability/public/pages/cases/feature_no_permissions.tsx index 5e087909e43aee..5075570c15b3eb 100644 --- a/x-pack/plugins/observability/public/pages/cases/saved_object_no_permissions.tsx +++ b/x-pack/plugins/observability/public/pages/cases/feature_no_permissions.tsx @@ -11,7 +11,7 @@ import { EmptyPage } from './empty_page'; import * as i18n from '../../components/app/cases/translations'; import { useKibana } from '../../utils/kibana_react'; -export const CaseSavedObjectNoPermissions = React.memo(() => { +export const CaseFeatureNoPermissions = React.memo(() => { const docLinks = useKibana().services.docLinks; const actions = useMemo( () => ({ @@ -28,11 +28,11 @@ export const CaseSavedObjectNoPermissions = React.memo(() => { return ( ); }); -CaseSavedObjectNoPermissions.displayName = 'CaseSavedObjectNoPermissions'; +CaseFeatureNoPermissions.displayName = 'CaseSavedObjectNoPermissions'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 134f58236cfee3..fcef4aac506ce3 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -17755,10 +17755,6 @@ "xpack.observability.alertsTable.viewInAppButtonLabel": "アプリで表示", "xpack.observability.alertsTitle": "アラート", "xpack.observability.breadcrumbs.observability": "オブザーバビリティ", - "xpack.observability.cases.breadcrumb": "ケース", - "xpack.observability.casesDisclaimerText": "これは将来のケースのホームです。", - "xpack.observability.casesDisclaimerTitle": "まもなくリリース", - "xpack.observability.casesTitle": "ケース", "xpack.observability.emptySection.apps.alert.description": "503 エラーが累積していますか?サービスは応答していますか?CPUとRAMの使用量が跳ね上がっていますか?このような警告を、事後にではなく、発生と同時に把握しましょう。", "xpack.observability.emptySection.apps.alert.link": "アラートの作成", "xpack.observability.emptySection.apps.alert.title": "アラートが見つかりません。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 67677f86ddbf78..359b1721c66e01 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -17996,10 +17996,6 @@ "xpack.observability.alertsTable.viewInAppButtonLabel": "在应用中查看", "xpack.observability.alertsTitle": "告警", "xpack.observability.breadcrumbs.observability": "可观测性", - "xpack.observability.cases.breadcrumb": "案例", - "xpack.observability.casesDisclaimerText": "这是案例的未来之家。", - "xpack.observability.casesDisclaimerTitle": "即将推出", - "xpack.observability.casesTitle": "案例", "xpack.observability.emptySection.apps.alert.description": "503 错误是否越来越多?服务是否响应?CPU 和 RAM 利用率是否激增?实时查看警告,而不是事后再进行剖析。", "xpack.observability.emptySection.apps.alert.link": "创建告警", "xpack.observability.emptySection.apps.alert.title": "未找到告警。", diff --git a/x-pack/test/api_integration/apis/security/privileges_basic.ts b/x-pack/test/api_integration/apis/security/privileges_basic.ts index 0893bb49cd2159..25266da2cdfb34 100644 --- a/x-pack/test/api_integration/apis/security/privileges_basic.ts +++ b/x-pack/test/api_integration/apis/security/privileges_basic.ts @@ -33,6 +33,7 @@ export default function ({ getService }: FtrProviderContext) { maps: ['all', 'read'], canvas: ['all', 'read'], infrastructure: ['all', 'read'], + observabilityCases: ['all', 'read'], logs: ['all', 'read'], uptime: ['all', 'read'], apm: ['all', 'read'], From 5df7d33ff4a7cacae6ab458965596f4147a6ba5e Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 26 May 2021 08:46:50 -0600 Subject: [PATCH 082/113] arrys rbac changes --- x-pack/plugins/observability/server/plugin.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/observability/server/plugin.ts b/x-pack/plugins/observability/server/plugin.ts index 15b2d89da01ade..b9af50f08357ce 100644 --- a/x-pack/plugins/observability/server/plugin.ts +++ b/x-pack/plugins/observability/server/plugin.ts @@ -76,8 +76,8 @@ export class ObservabilityPlugin implements Plugin { }, api: [], savedObject: { - all: [...caseSavedObjects], - read: ['config'], + all: [], + read: [], }, management: { insightsAndAlerting: ['triggersActions'], @@ -93,7 +93,7 @@ export class ObservabilityPlugin implements Plugin { api: [], savedObject: { all: [], - read: ['config', ...caseSavedObjects], + read: [], }, management: { insightsAndAlerting: ['triggersActions'], From 315a6e489939d2021e93bf06e011a8c4870ca952 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Wed, 26 May 2021 17:06:55 -0400 Subject: [PATCH 083/113] Address PR feedback --- .../cases/common/api/cases/constants.ts | 27 +++++++++++- .../server/authorization/authorization.ts | 11 ++++- .../cases/server/authorization/index.ts | 42 +++++++++---------- .../cases/server/client/attachments/add.ts | 16 +++---- .../cases/server/client/attachments/delete.ts | 4 +- .../cases/server/client/attachments/get.ts | 6 +-- .../cases/server/client/attachments/update.ts | 2 +- .../cases/server/client/cases/create.ts | 10 +++-- .../cases/server/client/cases/delete.ts | 2 +- .../plugins/cases/server/client/cases/find.ts | 12 ++++-- .../plugins/cases/server/client/cases/get.ts | 28 +++++++++---- .../plugins/cases/server/client/cases/push.ts | 14 +++---- .../cases/server/client/cases/update.ts | 29 +++++++------ .../cases/server/client/configure/client.ts | 6 +-- .../client/configure/create_mappings.ts | 4 +- .../server/client/configure/get_mappings.ts | 4 +- .../client/configure/update_mappings.ts | 4 +- x-pack/plugins/cases/server/client/factory.ts | 6 ++- .../cases/server/client/stats/client.ts | 2 +- .../cases/server/client/sub_cases/client.ts | 6 +-- .../cases/server/client/sub_cases/update.ts | 12 +++--- x-pack/plugins/cases/server/client/types.ts | 11 ++++- .../cases/server/client/user_actions/get.ts | 10 ++++- x-pack/plugins/cases/server/client/utils.ts | 10 +---- x-pack/plugins/cases/server/plugin.ts | 4 +- .../server/saved_object_types/migrations.ts | 12 +++--- .../cases/server/services/cases/index.ts | 4 +- .../common/feature_kibana_privileges.ts | 8 ++-- .../security_solution/server/plugin.ts | 22 ++-------- 29 files changed, 190 insertions(+), 138 deletions(-) diff --git a/x-pack/plugins/cases/common/api/cases/constants.ts b/x-pack/plugins/cases/common/api/cases/constants.ts index b8dd13c5d490ea..92755ec633ecc6 100644 --- a/x-pack/plugins/cases/common/api/cases/constants.ts +++ b/x-pack/plugins/cases/common/api/cases/constants.ts @@ -6,6 +6,31 @@ */ /** - * The field used for authorization in various entities within cases. + * This field is used for authorization of the entities within the cases plugin. Each entity within Cases will have the owner field + * set to a string that represents the plugin that "owns" (i.e. the plugin that originally issued the POST request to + * create the entity) the entity. + * + * The Authorization class constructs a string composed of the operation being performed (createCase, getComment, etc), + * and the owner of the entity being acted upon or created. This string is then given to the Security plugin which + * checks to see if the user making the request has that particular string stored within it's privileges. If it does, + * then the operation succeeds, otherwise the operation fails. + * + * APIs that create/update an entity require that the owner field be passed in the body of the request. + * APIs that search for entities typically require that the owner be passed as a query parameter. + * APIs that specify an ID of an entity directly generally don't need to specify the owner field. + * + * For APIs that create/update an entity, the RBAC implementation checks to see if the user making the request has the + * correct privileges for performing that action (a create/update) for the specified owner. + * This check is done through the Security plugin's API. + * + * For APIs that search for entities, the RBAC implementation creates a filter for the saved objects query that limits + * the search to only owners that the user has access to. We also check that the objects returned by the saved objects + * API have the limited owner scope. If we find one that the user does not have permissions for, we throw a 403 error. + * The owner field that is passed in as a query parameter can be used to further limit the results. If a user attempts + * to pass an owner that they do not have access to, the owner is ignored. + * + * For APIs that retrieve/delete entities directly using their ID, the RBAC implementation requests the object first, + * and then checks to see if the user making the request has access to that operation and owner. If the user does, the + * operation continues, otherwise we throw a 403. */ export const OWNER_FIELD = 'owner'; diff --git a/x-pack/plugins/cases/server/authorization/authorization.ts b/x-pack/plugins/cases/server/authorization/authorization.ts index 31e99392ae31b7..296a125418023e 100644 --- a/x-pack/plugins/cases/server/authorization/authorization.ts +++ b/x-pack/plugins/cases/server/authorization/authorization.ts @@ -5,13 +5,14 @@ * 2.0. */ -import { KibanaRequest } from 'kibana/server'; +import { KibanaRequest, Logger } from 'kibana/server'; import Boom from '@hapi/boom'; import { SecurityPluginStart } from '../../../security/server'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; import { AuthorizationFilter, GetSpaceFn } from './types'; import { getOwnersFilter } from './utils'; import { AuthorizationAuditLogger, OperationDetails } from '.'; +import { createCaseError } from '../common'; /** * This class handles ensuring that the user making a request has the correct permissions @@ -49,12 +50,14 @@ export class Authorization { getSpace, features, auditLogger, + logger, }: { request: KibanaRequest; securityAuth?: SecurityPluginStart['authz']; getSpace: GetSpaceFn; features: FeaturesPluginStart; auditLogger: AuthorizationAuditLogger; + logger: Logger; }): Promise { // Since we need to do async operations, this static method handles that before creating the Auth class let caseOwners: Set; @@ -69,7 +72,11 @@ export class Authorization { .flatMap((feature) => feature.cases ?? []) ); } catch (error) { - caseOwners = new Set(); + throw createCaseError({ + message: `Failed to create Authorization class: ${error}`, + error, + logger, + }); } return new Authorization({ request, securityAuth, caseOwners, auditLogger }); diff --git a/x-pack/plugins/cases/server/authorization/index.ts b/x-pack/plugins/cases/server/authorization/index.ts index 7dea7b7b47f925..1356111ff1664a 100644 --- a/x-pack/plugins/cases/server/authorization/index.ts +++ b/x-pack/plugins/cases/server/authorization/index.ts @@ -81,7 +81,7 @@ export const Operations: Record => { const { - savedObjectsClient, + unsecuredSavedObjectsClient, attachmentService, caseService, userActionService, @@ -152,7 +152,7 @@ const addGeneratedAlerts = async ( }); const caseInfo = await caseService.getCase({ - soClient: savedObjectsClient, + soClient: unsecuredSavedObjectsClient, id: caseId, }); @@ -171,7 +171,7 @@ const addGeneratedAlerts = async ( const subCase = await getSubCase({ caseService, - savedObjectsClient, + savedObjectsClient: unsecuredSavedObjectsClient, caseId, createdAt: createdDate, userActionService, @@ -182,7 +182,7 @@ const addGeneratedAlerts = async ( logger, collection: caseInfo, subCase, - soClient: savedObjectsClient, + soClient: unsecuredSavedObjectsClient, caseService, attachmentService, }); @@ -212,7 +212,7 @@ const addGeneratedAlerts = async ( } await userActionService.bulkCreate({ - soClient: savedObjectsClient, + soClient: unsecuredSavedObjectsClient, actions: [ buildCommentUserActionItem({ action: 'create', @@ -329,7 +329,7 @@ export const addComment = async ( ); const { - savedObjectsClient, + unsecuredSavedObjectsClient, caseService, userActionService, attachmentService, @@ -366,7 +366,7 @@ export const addComment = async ( const combinedCase = await getCombinedCase({ caseService, attachmentService, - soClient: savedObjectsClient, + soClient: unsecuredSavedObjectsClient, id: caseId, logger, }); @@ -398,7 +398,7 @@ export const addComment = async ( } await userActionService.bulkCreate({ - soClient: savedObjectsClient, + soClient: unsecuredSavedObjectsClient, actions: [ buildCommentUserActionItem({ action: 'create', diff --git a/x-pack/plugins/cases/server/client/attachments/delete.ts b/x-pack/plugins/cases/server/client/attachments/delete.ts index 28e56c21fd255c..359c7a0672275f 100644 --- a/x-pack/plugins/cases/server/client/attachments/delete.ts +++ b/x-pack/plugins/cases/server/client/attachments/delete.ts @@ -59,7 +59,7 @@ export async function deleteAll( ): Promise { const { user, - savedObjectsClient: soClient, + unsecuredSavedObjectsClient: soClient, caseService, attachmentService, userActionService, @@ -136,7 +136,7 @@ export async function deleteComment( ) { const { user, - savedObjectsClient: soClient, + unsecuredSavedObjectsClient: soClient, attachmentService, userActionService, logger, diff --git a/x-pack/plugins/cases/server/client/attachments/get.ts b/x-pack/plugins/cases/server/client/attachments/get.ts index d65d25d0802264..6bd0383c508da0 100644 --- a/x-pack/plugins/cases/server/client/attachments/get.ts +++ b/x-pack/plugins/cases/server/client/attachments/get.ts @@ -91,7 +91,7 @@ export async function find( clientArgs: CasesClientArgs ): Promise { const { - savedObjectsClient: soClient, + unsecuredSavedObjectsClient: soClient, caseService, logger, authorization, @@ -184,7 +184,7 @@ export async function get( ): Promise { const { attachmentService, - savedObjectsClient: soClient, + unsecuredSavedObjectsClient: soClient, logger, authorization, auditLogger, @@ -225,7 +225,7 @@ export async function getAll( clientArgs: CasesClientArgs ): Promise { const { - savedObjectsClient: soClient, + unsecuredSavedObjectsClient: soClient, caseService, logger, authorization, diff --git a/x-pack/plugins/cases/server/client/attachments/update.ts b/x-pack/plugins/cases/server/client/attachments/update.ts index 713fd931dcb909..8a8503cc942572 100644 --- a/x-pack/plugins/cases/server/client/attachments/update.ts +++ b/x-pack/plugins/cases/server/client/attachments/update.ts @@ -100,7 +100,7 @@ export async function update( const { attachmentService, caseService, - savedObjectsClient: soClient, + unsecuredSavedObjectsClient: soClient, logger, user, userActionService, diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index 1d3e8d432410d6..b0b1a38c0cd636 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -45,7 +45,7 @@ export const create = async ( clientArgs: CasesClientArgs ): Promise => { const { - savedObjectsClient, + unsecuredSavedObjectsClient, caseService, caseConfigureService, userActionService, @@ -87,11 +87,13 @@ export const create = async ( // eslint-disable-next-line @typescript-eslint/naming-convention const { username, full_name, email } = user; const createdDate = new Date().toISOString(); - const myCaseConfigure = await caseConfigureService.find({ soClient: savedObjectsClient }); + const myCaseConfigure = await caseConfigureService.find({ + soClient: unsecuredSavedObjectsClient, + }); const caseConfigureConnector = getConnectorFromConfiguration(myCaseConfigure); const newCase = await caseService.postNewCase({ - soClient: savedObjectsClient, + soClient: unsecuredSavedObjectsClient, attributes: transformNewCase({ createdDate, newCase: query, @@ -104,7 +106,7 @@ export const create = async ( }); await userActionService.bulkCreate({ - soClient: savedObjectsClient, + soClient: unsecuredSavedObjectsClient, actions: [ buildCaseUserActionItem({ action: 'create', diff --git a/x-pack/plugins/cases/server/client/cases/delete.ts b/x-pack/plugins/cases/server/client/cases/delete.ts index de6d317d7c2d89..81a8e618d6d157 100644 --- a/x-pack/plugins/cases/server/client/cases/delete.ts +++ b/x-pack/plugins/cases/server/client/cases/delete.ts @@ -57,7 +57,7 @@ async function deleteSubCases({ */ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): Promise { const { - savedObjectsClient: soClient, + unsecuredSavedObjectsClient: soClient, caseService, attachmentService, user, diff --git a/x-pack/plugins/cases/server/client/cases/find.ts b/x-pack/plugins/cases/server/client/cases/find.ts index a7e36461965a9a..8c007d1a1a9111 100644 --- a/x-pack/plugins/cases/server/client/cases/find.ts +++ b/x-pack/plugins/cases/server/client/cases/find.ts @@ -36,7 +36,13 @@ export const find = async ( params: CasesFindRequest, clientArgs: CasesClientArgs ): Promise => { - const { savedObjectsClient, caseService, authorization: auth, auditLogger, logger } = clientArgs; + const { + unsecuredSavedObjectsClient, + caseService, + authorization: auth, + auditLogger, + logger, + } = clientArgs; try { const queryParams = pipe( @@ -65,7 +71,7 @@ export const find = async ( const caseQueries = constructQueryOptions({ ...queryArgs, authorizationFilter }); const cases = await caseService.findCasesGroupedByID({ - soClient: savedObjectsClient, + soClient: unsecuredSavedObjectsClient, caseOptions: { ...queryParams, ...caseQueries.case, @@ -86,7 +92,7 @@ export const find = async ( ...caseStatuses.map((status) => { const statusQuery = constructQueryOptions({ ...queryArgs, status, authorizationFilter }); return caseService.findCaseStatusStats({ - soClient: savedObjectsClient, + soClient: unsecuredSavedObjectsClient, caseOptions: statusQuery.case, subCaseOptions: statusQuery.subCase, ensureSavedObjectsAreAuthorized, diff --git a/x-pack/plugins/cases/server/client/cases/get.ts b/x-pack/plugins/cases/server/client/cases/get.ts index 1434d54f6a2b72..30ffedaba8923e 100644 --- a/x-pack/plugins/cases/server/client/cases/get.ts +++ b/x-pack/plugins/cases/server/client/cases/get.ts @@ -61,7 +61,13 @@ export const getCaseIDsByAlertID = async ( { alertID, options }: CaseIDsByAlertIDParams, clientArgs: CasesClientArgs ): Promise => { - const { savedObjectsClient, caseService, logger, authorization, auditLogger } = clientArgs; + const { + unsecuredSavedObjectsClient: savedObjectsClient, + caseService, + logger, + authorization, + auditLogger, + } = clientArgs; try { const queryParams = pipe( @@ -139,7 +145,13 @@ export const get = async ( { id, includeComments, includeSubCaseComments }: GetParams, clientArgs: CasesClientArgs ): Promise => { - const { savedObjectsClient, caseService, logger, authorization: auth, auditLogger } = clientArgs; + const { + unsecuredSavedObjectsClient, + caseService, + logger, + authorization: auth, + auditLogger, + } = clientArgs; try { if (!ENABLE_CASE_CONNECTOR && includeSubCaseComments) { @@ -154,17 +166,17 @@ export const get = async ( if (ENABLE_CASE_CONNECTOR) { const [caseInfo, subCasesForCaseId] = await Promise.all([ caseService.getCase({ - soClient: savedObjectsClient, + soClient: unsecuredSavedObjectsClient, id, }), - caseService.findSubCasesByCaseId({ soClient: savedObjectsClient, ids: [id] }), + caseService.findSubCasesByCaseId({ soClient: unsecuredSavedObjectsClient, ids: [id] }), ]); theCase = caseInfo; subCaseIds = subCasesForCaseId.saved_objects.map((so) => so.id); } else { theCase = await caseService.getCase({ - soClient: savedObjectsClient, + soClient: unsecuredSavedObjectsClient, id, }); } @@ -187,7 +199,7 @@ export const get = async ( } const theComments = await caseService.getAllCaseComments({ - soClient: savedObjectsClient, + soClient: unsecuredSavedObjectsClient, id, options: { sortField: 'created_at', @@ -219,7 +231,7 @@ export async function getTags( clientArgs: CasesClientArgs ): Promise { const { - savedObjectsClient: soClient, + unsecuredSavedObjectsClient: soClient, caseService, logger, authorization: auth, @@ -281,7 +293,7 @@ export async function getReporters( clientArgs: CasesClientArgs ): Promise { const { - savedObjectsClient: soClient, + unsecuredSavedObjectsClient: soClient, caseService, logger, authorization: auth, diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index c85fcd05f7e4dd..af395e9d8768a9 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -69,7 +69,7 @@ export const push = async ( casesClientInternal: CasesClientInternal ): Promise => { const { - savedObjectsClient, + unsecuredSavedObjectsClient, attachmentService, caseService, caseConfigureService, @@ -151,12 +151,12 @@ export const push = async ( /* Start of update case with push information */ const [myCase, myCaseConfigure, comments] = await Promise.all([ caseService.getCase({ - soClient: savedObjectsClient, + soClient: unsecuredSavedObjectsClient, id: caseId, }), - caseConfigureService.find({ soClient: savedObjectsClient }), + caseConfigureService.find({ soClient: unsecuredSavedObjectsClient }), caseService.getAllCaseComments({ - soClient: savedObjectsClient, + soClient: unsecuredSavedObjectsClient, id: caseId, options: { fields: [], @@ -186,7 +186,7 @@ export const push = async ( const [updatedCase, updatedComments] = await Promise.all([ caseService.patchCase({ - soClient: savedObjectsClient, + soClient: unsecuredSavedObjectsClient, caseId, updatedAttributes: { ...(shouldMarkAsClosed @@ -204,7 +204,7 @@ export const push = async ( }), attachmentService.bulkUpdate({ - soClient: savedObjectsClient, + soClient: unsecuredSavedObjectsClient, comments: comments.saved_objects .filter((comment) => comment.attributes.pushed_at == null) .map((comment) => ({ @@ -218,7 +218,7 @@ export const push = async ( }), userActionService.bulkCreate({ - soClient: savedObjectsClient, + soClient: unsecuredSavedObjectsClient, actions: [ ...(shouldMarkAsClosed ? [ diff --git a/x-pack/plugins/cases/server/client/cases/update.ts b/x-pack/plugins/cases/server/client/cases/update.ts index b11c8574c5d621..a0da2e489ac7ac 100644 --- a/x-pack/plugins/cases/server/client/cases/update.ts +++ b/x-pack/plugins/cases/server/client/cases/update.ts @@ -56,6 +56,7 @@ import { UpdateAlertRequest } from '../alerts/client'; import { CasesClientInternal } from '../client_internal'; import { CasesClientArgs } from '..'; import { Operations } from '../../authorization'; +import { OwnerEntity } from '../types'; /** * Throws an error if any of the requests attempt to update a collection style cases' status field. @@ -356,11 +357,12 @@ function partitionPatchRequest( ): { nonExistingCases: CasePatchRequest[]; conflictedCases: CasePatchRequest[]; - casesToAuthorize: Array>; + // This will be a deduped array of case IDs with their corresponding owner + casesToAuthorize: OwnerEntity[]; } { const nonExistingCases: CasePatchRequest[] = []; const conflictedCases: CasePatchRequest[] = []; - const casesToAuthorize: Array> = []; + const casesToAuthorize: Map = new Map(); for (const reqCase of patchReqCases) { const foundCase = casesMap.get(reqCase.id); @@ -370,16 +372,16 @@ function partitionPatchRequest( } else if (foundCase.version !== reqCase.version) { conflictedCases.push(reqCase); // let's try to authorize the conflicted case even though we'll fail after afterwards just in case - casesToAuthorize.push(foundCase); + casesToAuthorize.set(foundCase.id, { id: foundCase.id, owner: foundCase.attributes.owner }); } else { - casesToAuthorize.push(foundCase); + casesToAuthorize.set(foundCase.id, { id: foundCase.id, owner: foundCase.attributes.owner }); } } return { nonExistingCases, conflictedCases, - casesToAuthorize, + casesToAuthorize: Array.from(casesToAuthorize.values()), }; } @@ -394,7 +396,7 @@ export const update = async ( casesClientInternal: CasesClientInternal ): Promise => { const { - savedObjectsClient, + unsecuredSavedObjectsClient, caseService, userActionService, user, @@ -409,7 +411,7 @@ export const update = async ( try { const myCases = await caseService.getCases({ - soClient: savedObjectsClient, + soClient: unsecuredSavedObjectsClient, caseIds: query.cases.map((q) => q.id), }); @@ -426,7 +428,7 @@ export const update = async ( await ensureAuthorized({ authorization, auditLogger, - owners: casesToAuthorize.map((caseInfo) => caseInfo.attributes.owner), + owners: casesToAuthorize.map((caseInfo) => caseInfo.owner), operation: Operations.updateCase, savedObjectIDs: casesToAuthorize.map((caseInfo) => caseInfo.id), }); @@ -479,16 +481,17 @@ export const update = async ( await throwIfInvalidUpdateOfTypeWithAlerts({ requests: updateFilterCases, caseService, - soClient: savedObjectsClient, + soClient: unsecuredSavedObjectsClient, }); // eslint-disable-next-line @typescript-eslint/naming-convention const { username, full_name, email } = user; const updatedDt = new Date().toISOString(); const updatedCases = await caseService.patchCases({ - soClient: savedObjectsClient, + soClient: unsecuredSavedObjectsClient, cases: updateFilterCases.map((thisCase) => { - const { id: caseId, version, ...updateCaseAttributes } = thisCase; + // intentionally removing owner from the case so that we don't accidentally allow it to be updated + const { id: caseId, version, owner, ...updateCaseAttributes } = thisCase; let closedInfo = {}; if (updateCaseAttributes.status && updateCaseAttributes.status === CaseStatuses.closed) { closedInfo = { @@ -547,7 +550,7 @@ export const update = async ( casesWithStatusChangedAndSynced, casesWithSyncSettingChangedToOn, caseService, - soClient: savedObjectsClient, + soClient: unsecuredSavedObjectsClient, casesClientInternal, casesMap, }); @@ -570,7 +573,7 @@ export const update = async ( }); await userActionService.bulkCreate({ - soClient: savedObjectsClient, + soClient: unsecuredSavedObjectsClient, actions: buildCaseUserActions({ originalCases: myCases.saved_objects, updatedCases: updatedCases.saved_objects, diff --git a/x-pack/plugins/cases/server/client/configure/client.ts b/x-pack/plugins/cases/server/client/configure/client.ts index 7145491b8f2bf9..65e89f9d819b21 100644 --- a/x-pack/plugins/cases/server/client/configure/client.ts +++ b/x-pack/plugins/cases/server/client/configure/client.ts @@ -149,7 +149,7 @@ async function get( casesClientInternal: CasesClientInternal ): Promise { const { - savedObjectsClient: soClient, + unsecuredSavedObjectsClient: soClient, caseConfigureService, logger, authorization, @@ -264,7 +264,7 @@ async function update( const { caseConfigureService, logger, - savedObjectsClient: soClient, + unsecuredSavedObjectsClient: soClient, user, authorization, auditLogger, @@ -381,7 +381,7 @@ async function create( casesClientInternal: CasesClientInternal ): Promise { const { - savedObjectsClient: soClient, + unsecuredSavedObjectsClient: soClient, caseConfigureService, logger, user, diff --git a/x-pack/plugins/cases/server/client/configure/create_mappings.ts b/x-pack/plugins/cases/server/client/configure/create_mappings.ts index 1b31033ef83a5e..bdd4b31377ee06 100644 --- a/x-pack/plugins/cases/server/client/configure/create_mappings.ts +++ b/x-pack/plugins/cases/server/client/configure/create_mappings.ts @@ -16,7 +16,7 @@ export const createMappings = async ( clientArgs: CasesClientArgs, casesClientInternal: CasesClientInternal ): Promise => { - const { savedObjectsClient, connectorMappingsService, logger } = clientArgs; + const { unsecuredSavedObjectsClient, connectorMappingsService, logger } = clientArgs; try { if (connectorType === ConnectorTypes.none) { @@ -29,7 +29,7 @@ export const createMappings = async ( }); const theMapping = await connectorMappingsService.post({ - soClient: savedObjectsClient, + soClient: unsecuredSavedObjectsClient, attributes: { mappings: res.defaultMappings, owner, 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 efc7ac30a98c03..f00a62c8cd039a 100644 --- a/x-pack/plugins/cases/server/client/configure/get_mappings.ts +++ b/x-pack/plugins/cases/server/client/configure/get_mappings.ts @@ -16,7 +16,7 @@ export const getMappings = async ( { connectorType, connectorId }: MappingsArgs, clientArgs: CasesClientArgs ): Promise['saved_objects']> => { - const { savedObjectsClient, connectorMappingsService, logger } = clientArgs; + const { unsecuredSavedObjectsClient, connectorMappingsService, logger } = clientArgs; try { if (connectorType === ConnectorTypes.none) { @@ -24,7 +24,7 @@ export const getMappings = async ( } const myConnectorMappings = await connectorMappingsService.find({ - soClient: savedObjectsClient, + soClient: unsecuredSavedObjectsClient, options: { hasReference: { type: ACTION_SAVED_OBJECT_TYPE, diff --git a/x-pack/plugins/cases/server/client/configure/update_mappings.ts b/x-pack/plugins/cases/server/client/configure/update_mappings.ts index 78c9a286df16bf..ddac074c432719 100644 --- a/x-pack/plugins/cases/server/client/configure/update_mappings.ts +++ b/x-pack/plugins/cases/server/client/configure/update_mappings.ts @@ -16,7 +16,7 @@ export const updateMappings = async ( clientArgs: CasesClientArgs, casesClientInternal: CasesClientInternal ): Promise => { - const { savedObjectsClient, connectorMappingsService, logger } = clientArgs; + const { unsecuredSavedObjectsClient, connectorMappingsService, logger } = clientArgs; try { if (connectorType === ConnectorTypes.none) { @@ -29,7 +29,7 @@ export const updateMappings = async ( }); const theMapping = await connectorMappingsService.update({ - soClient: savedObjectsClient, + soClient: unsecuredSavedObjectsClient, mappingId, attributes: { mappings: res.defaultMappings, diff --git a/x-pack/plugins/cases/server/client/factory.ts b/x-pack/plugins/cases/server/client/factory.ts index bd049bcd703952..18b9be3aa5b197 100644 --- a/x-pack/plugins/cases/server/client/factory.ts +++ b/x-pack/plugins/cases/server/client/factory.ts @@ -85,6 +85,7 @@ export class CasesClientFactory { getSpace: this.options.getSpace, features: this.options.featuresPluginStart, auditLogger: new AuthorizationAuditLogger(auditLogger), + logger: this.logger, }); const caseService = new CaseService(this.logger, this.options?.securityPluginStart?.authc); @@ -93,8 +94,11 @@ export class CasesClientFactory { return createCasesClient({ alertsService: new AlertService(), scopedClusterClient, - savedObjectsClient: savedObjectsService.getScopedClient(request, { + unsecuredSavedObjectsClient: savedObjectsService.getScopedClient(request, { includedHiddenTypes: SAVED_OBJECT_TYPES, + // this tells the security plugin to not perform SO authorization and audit logging since we are handling + // that manually using our Authorization class and audit logger. + excludedWrappers: ['security'], }), // We only want these fields from the userInfo object user: { username: userInfo.username, email: userInfo.email, full_name: userInfo.full_name }, diff --git a/x-pack/plugins/cases/server/client/stats/client.ts b/x-pack/plugins/cases/server/client/stats/client.ts index 7259829f5603e8..4cd8823883c4bf 100644 --- a/x-pack/plugins/cases/server/client/stats/client.ts +++ b/x-pack/plugins/cases/server/client/stats/client.ts @@ -51,7 +51,7 @@ async function getStatusTotalsByType( clientArgs: CasesClientArgs ): Promise { const { - savedObjectsClient: soClient, + unsecuredSavedObjectsClient: soClient, caseService, logger, authorization, diff --git a/x-pack/plugins/cases/server/client/sub_cases/client.ts b/x-pack/plugins/cases/server/client/sub_cases/client.ts index 3830c84248502a..4552d4042012e4 100644 --- a/x-pack/plugins/cases/server/client/sub_cases/client.ts +++ b/x-pack/plugins/cases/server/client/sub_cases/client.ts @@ -93,7 +93,7 @@ export function createSubCasesClient( async function deleteSubCase(ids: string[], clientArgs: CasesClientArgs): Promise { try { const { - savedObjectsClient: soClient, + unsecuredSavedObjectsClient: soClient, user, userActionService, caseService, @@ -161,7 +161,7 @@ async function find( clientArgs: CasesClientArgs ): Promise { try { - const { savedObjectsClient: soClient, caseService } = clientArgs; + const { unsecuredSavedObjectsClient: soClient, caseService } = clientArgs; const ids = [caseID]; const { subCase: subCaseQueryOptions } = constructQueryOptions({ @@ -220,7 +220,7 @@ async function get( clientArgs: CasesClientArgs ): Promise { try { - const { savedObjectsClient: soClient, caseService } = clientArgs; + const { unsecuredSavedObjectsClient: soClient, caseService } = clientArgs; const subCase = await caseService.getSubCase({ soClient, diff --git a/x-pack/plugins/cases/server/client/sub_cases/update.ts b/x-pack/plugins/cases/server/client/sub_cases/update.ts index de7a75634d7fbd..1ad579b3b210e0 100644 --- a/x-pack/plugins/cases/server/client/sub_cases/update.ts +++ b/x-pack/plugins/cases/server/client/sub_cases/update.ts @@ -271,10 +271,10 @@ export async function update({ ); try { - const { savedObjectsClient: soClient, user, caseService, userActionService } = clientArgs; + const { unsecuredSavedObjectsClient, user, caseService, userActionService } = clientArgs; const bulkSubCases = await caseService.getSubCases({ - soClient, + soClient: unsecuredSavedObjectsClient, ids: query.subCases.map((q) => q.id), }); @@ -292,7 +292,7 @@ export async function update({ } const subIDToParentCase = await getParentCases({ - soClient, + soClient: unsecuredSavedObjectsClient, caseService, subCaseIDs: nonEmptySubCaseRequests.map((subCase) => subCase.id), subCasesMap, @@ -300,7 +300,7 @@ export async function update({ const updatedAt = new Date().toISOString(); const updatedCases = await caseService.patchSubCases({ - soClient, + soClient: unsecuredSavedObjectsClient, subCases: nonEmptySubCaseRequests.map((thisCase) => { const { id: subCaseId, version, ...updateSubCaseAttributes } = thisCase; let closedInfo: { closed_at: string | null; closed_by: User | null } = { @@ -352,7 +352,7 @@ export async function update({ await updateAlerts({ caseService, - soClient, + soClient: unsecuredSavedObjectsClient, casesClientInternal, subCasesToSync: subCasesToSyncAlertsFor, logger: clientArgs.logger, @@ -380,7 +380,7 @@ export async function update({ ); await userActionService.bulkCreate({ - soClient, + soClient: unsecuredSavedObjectsClient, actions: buildSubCaseUserActions({ originalSubCases: bulkSubCases.saved_objects, updatedSubCases: updatedCases.saved_objects, diff --git a/x-pack/plugins/cases/server/client/types.ts b/x-pack/plugins/cases/server/client/types.ts index 340327cecabd9a..9e54b013dc602b 100644 --- a/x-pack/plugins/cases/server/client/types.ts +++ b/x-pack/plugins/cases/server/client/types.ts @@ -29,7 +29,7 @@ export interface CasesClientArgs { readonly caseService: CaseService; readonly connectorMappingsService: ConnectorMappingsService; readonly user: User; - readonly savedObjectsClient: SavedObjectsClientContract; + readonly unsecuredSavedObjectsClient: SavedObjectsClientContract; readonly userActionService: CaseUserActionService; readonly alertsService: AlertServiceContract; readonly attachmentService: AttachmentService; @@ -38,3 +38,12 @@ export interface CasesClientArgs { readonly auditLogger?: AuditLogger; readonly actionsClient: PublicMethodsOf; } + +/** + * Describes an entity with the necessary fields to identify if the user is authorized to interact with the saved object + * returned from some find query. + */ +export interface OwnerEntity { + owner: string; + id: string; +} 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 30e2e3095c8a4b..4fbc4d333133f7 100644 --- a/x-pack/plugins/cases/server/client/user_actions/get.ts +++ b/x-pack/plugins/cases/server/client/user_actions/get.ts @@ -22,13 +22,19 @@ export const get = async ( { caseId, subCaseId }: UserActionGet, clientArgs: CasesClientArgs ): Promise => { - const { savedObjectsClient, userActionService, logger, authorization, auditLogger } = clientArgs; + const { + unsecuredSavedObjectsClient, + userActionService, + logger, + authorization, + auditLogger, + } = clientArgs; try { checkEnabledCaseConnectorOrThrow(subCaseId); const userActions = await userActionService.getAll({ - soClient: savedObjectsClient, + soClient: unsecuredSavedObjectsClient, caseId, subCaseId, }); diff --git a/x-pack/plugins/cases/server/client/utils.ts b/x-pack/plugins/cases/server/client/utils.ts index 931372cc1d6c95..d42947ad17edda 100644 --- a/x-pack/plugins/cases/server/client/utils.ts +++ b/x-pack/plugins/cases/server/client/utils.ts @@ -40,6 +40,7 @@ import { } from '../common'; import { Authorization, DATABASE_CATEGORY, ECS_OUTCOMES, OperationDetails } from '../authorization'; import { AuditLogger } from '../../../security/server'; +import { OwnerEntity } from './types'; export const decodeCommentRequest = (comment: CommentRequest) => { if (isCommentRequestTypeUser(comment)) { @@ -562,15 +563,6 @@ export async function ensureAuthorized({ } } -/** - * Describes an entity with the necessary fields to identify if the user is authorized to interact with the saved object - * returned from some find query. - */ -interface OwnerEntity { - owner: string; - id: string; -} - /** * Function callback for making sure the found saved objects are of the authorized owner */ diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index 1b7cae54383419..34cf71aff58ba8 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -154,10 +154,10 @@ export class CasePlugin { core: CoreSetup; }): IContextProvider => { return async (context, request, response) => { - const [{ savedObjects }] = await core.getStartServices(); - return { getCasesClient: async () => { + const [{ savedObjects }] = await core.getStartServices(); + return this.clientFactory.create({ request, scopedClusterClient: context.core.elasticsearch.client.asCurrentUser, diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations.ts b/x-pack/plugins/cases/server/saved_object_types/migrations.ts index 20a9ed79e1c0e1..3d0bab68cf458d 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations.ts @@ -129,7 +129,7 @@ export const caseMigrations = { references: doc.references || [], }; }, - '7.13.0': ( + '7.14.0': ( doc: SavedObjectUnsanitizedDoc> ): SavedObjectSanitizedDoc => { return addOwnerToSO(doc); @@ -156,7 +156,7 @@ export const configureMigrations = { references: doc.references || [], }; }, - '7.13.0': ( + '7.14.0': ( doc: SavedObjectUnsanitizedDoc> ): SavedObjectSanitizedDoc => { return addOwnerToSO(doc); @@ -202,7 +202,7 @@ export const userActionsMigrations = { references: doc.references || [], }; }, - '7.13.0': ( + '7.14.0': ( doc: SavedObjectUnsanitizedDoc> ): SavedObjectSanitizedDoc => { return addOwnerToSO(doc); @@ -257,7 +257,7 @@ export const commentsMigrations = { references: doc.references || [], }; }, - '7.13.0': ( + '7.14.0': ( doc: SavedObjectUnsanitizedDoc> ): SavedObjectSanitizedDoc => { return addOwnerToSO(doc); @@ -265,7 +265,7 @@ export const commentsMigrations = { }; export const connectorMappingsMigrations = { - '7.13.0': ( + '7.14.0': ( doc: SavedObjectUnsanitizedDoc> ): SavedObjectSanitizedDoc => { return addOwnerToSO(doc); @@ -273,7 +273,7 @@ export const connectorMappingsMigrations = { }; export const subCasesMigrations = { - '7.13.0': ( + '7.14.0': ( doc: SavedObjectUnsanitizedDoc> ): SavedObjectSanitizedDoc => { return addOwnerToSO(doc); diff --git a/x-pack/plugins/cases/server/services/cases/index.ts b/x-pack/plugins/cases/server/services/cases/index.ts index c2e0135b003fa1..3703be19ec0f54 100644 --- a/x-pack/plugins/cases/server/services/cases/index.ts +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -1050,7 +1050,7 @@ export class CaseService { filter: cloneDeep(filter), }); } catch (error) { - this.log.error(`Error on GET cases: ${error}`); + this.log.error(`Error on GET tags: ${error}`); throw error; } } @@ -1075,7 +1075,7 @@ export class CaseService { email: null, }; } catch (error) { - this.log.error(`Error on GET cases: ${error}`); + this.log.error(`Error on GET user: ${error}`); throw error; } } diff --git a/x-pack/plugins/features/common/feature_kibana_privileges.ts b/x-pack/plugins/features/common/feature_kibana_privileges.ts index d95c12df5deb9c..0febca20f94740 100644 --- a/x-pack/plugins/features/common/feature_kibana_privileges.ts +++ b/x-pack/plugins/features/common/feature_kibana_privileges.ts @@ -106,12 +106,12 @@ export interface FeatureKibanaPrivileges { }; /** - * If your feature requires access to specific types of cases, then specify your access needs here. The values here should - * be a unique identifier for the type of case you want access to. + * If your feature requires access to specific owners of cases (aka plugins that have created cases), then specify your access needs here. The values here should + * be unique identifiers for the owners of cases you want access to. */ cases?: { /** - * List of case types which users should have full read/write access to when granted this privilege. + * List of case owners which users should have full read/write access to when granted this privilege. * @example * ```ts * { @@ -121,7 +121,7 @@ export interface FeatureKibanaPrivileges { */ all?: readonly string[]; /** - * List of case types which users should have read-only access to when granted this privilege. + * List of case owners which users should have read-only access to when granted this privilege. * @example * ```ts * { diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 4893e87635259c..a5d239824e508d 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -122,14 +122,6 @@ const securitySubPlugins = [ `${APP_ID}:${SecurityPageName.administration}`, ]; -const caseSavedObjects = [ - 'cases', - 'cases-comments', - 'cases-sub-case', - 'cases-configure', - 'cases-user-actions', -]; - export class Plugin implements IPlugin { private readonly logger: Logger; private readonly config: ConfigType; @@ -236,7 +228,7 @@ export class Plugin implements IPlugin @@ -268,15 +260,12 @@ export class Plugin implements IPlugin Date: Wed, 26 May 2021 19:30:39 -0400 Subject: [PATCH 084/113] Adding top level feature back --- x-pack/plugins/security_solution/server/plugin.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index a5d239824e508d..ec3da00797c400 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -270,6 +270,9 @@ export class Plugin implements IPlugin Date: Thu, 27 May 2021 13:29:20 -0400 Subject: [PATCH 085/113] Fixing feature privileges --- .../feature_privilege_iterator.ts | 5 +++ .../security_solution/server/plugin.ts | 6 ---- .../tests/common/comments/delete_comment.ts | 34 +------------------ 3 files changed, 6 insertions(+), 39 deletions(-) diff --git a/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.ts b/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.ts index e194a051c8a6e5..ada7a209dcf77e 100644 --- a/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.ts +++ b/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.ts @@ -116,6 +116,11 @@ function mergeWithSubFeatures( subFeaturePrivilege.alerting?.read ?? [] ), }; + + mergedConfig.cases = { + all: mergeArrays(mergedConfig.cases?.all ?? [], subFeaturePrivilege.cases?.all ?? []), + read: mergeArrays(mergedConfig.cases?.read ?? [], subFeaturePrivilege.cases?.read ?? []), + }; } return mergedConfig; } diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index ec3da00797c400..a5d239824e508d 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -270,9 +270,6 @@ export class Plugin implements IPlugin { }); }); - for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead]) { + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { it(`User ${ user.username } with role(s) ${user.roles.join()} - should NOT delete a comment`, async () => { @@ -305,38 +305,6 @@ export default ({ getService }: FtrProviderContext): void => { }); } - it('should not delete a comment with no kibana privileges', async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - superUserSpace1Auth - ); - - const commentResp = await createComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - params: postCommentUserReq, - auth: superUserSpace1Auth, - }); - - await deleteComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - commentId: commentResp.comments![0].id, - auth: { user: noKibanaPrivileges, space: 'space1' }, - expectedHttpCode: 403, - }); - - await deleteAllComments({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - auth: { user: noKibanaPrivileges, space: 'space1' }, - // the find in the delete all will return no results - expectedHttpCode: 404, - }); - }); - it('should NOT delete a comment in a space with where the user does not have permissions', async () => { const postedCase = await createCase( supertestWithoutAuth, From acf550be73cc9b4704b82d22f149ebe1c8978c01 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Thu, 27 May 2021 14:37:14 -0400 Subject: [PATCH 086/113] Renaming --- x-pack/plugins/cases/server/client/attachments/add.ts | 6 +++--- .../plugins/cases/server/client/attachments/update.ts | 4 ++-- x-pack/plugins/cases/server/client/cases/delete.ts | 4 ++-- x-pack/plugins/cases/server/client/cases/get.ts | 4 ++-- x-pack/plugins/cases/server/client/cases/update.ts | 10 +++++----- x-pack/plugins/cases/server/client/factory.ts | 4 ++-- x-pack/plugins/cases/server/client/sub_cases/update.ts | 8 ++++---- x-pack/plugins/cases/server/client/types.ts | 4 ++-- .../cases/server/common/models/commentable_case.ts | 6 +++--- .../cases/server/routes/api/__fixtures__/authc_mock.ts | 2 +- x-pack/plugins/cases/server/services/cases/index.ts | 2 +- x-pack/plugins/cases/server/services/index.ts | 2 +- x-pack/plugins/cases/server/services/mocks.ts | 6 +++--- 13 files changed, 31 insertions(+), 31 deletions(-) diff --git a/x-pack/plugins/cases/server/client/attachments/add.ts b/x-pack/plugins/cases/server/client/attachments/add.ts index 1e21ba0bd06b28..a334e4bb6e5e70 100644 --- a/x-pack/plugins/cases/server/client/attachments/add.ts +++ b/x-pack/plugins/cases/server/client/attachments/add.ts @@ -35,7 +35,7 @@ import { buildCommentUserActionItem, } from '../../services/user_actions/helpers'; -import { AttachmentService, CaseService, CaseUserActionService } from '../../services'; +import { AttachmentService, CasesService, CaseUserActionService } from '../../services'; import { CommentableCase, createAlertUpdateRequest, @@ -60,7 +60,7 @@ async function getSubCase({ userActionService, user, }: { - caseService: CaseService; + caseService: CasesService; savedObjectsClient: SavedObjectsClientContract; caseId: string; createdAt: string; @@ -245,7 +245,7 @@ async function getCombinedCase({ id, logger, }: { - caseService: CaseService; + caseService: CasesService; attachmentService: AttachmentService; soClient: SavedObjectsClientContract; id: string; diff --git a/x-pack/plugins/cases/server/client/attachments/update.ts b/x-pack/plugins/cases/server/client/attachments/update.ts index 8a8503cc942572..5f07aa25fb3849 100644 --- a/x-pack/plugins/cases/server/client/attachments/update.ts +++ b/x-pack/plugins/cases/server/client/attachments/update.ts @@ -12,7 +12,7 @@ import { SavedObjectsClientContract, Logger } from 'kibana/server'; import { checkEnabledCaseConnectorOrThrow, CommentableCase } from '../../common'; import { buildCommentUserActionItem } from '../../services/user_actions/helpers'; import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../common/constants'; -import { AttachmentService, CaseService } from '../../services'; +import { AttachmentService, CasesService } from '../../services'; import { CaseResponse, CommentPatchRequest } from '../../../common/api'; import { CasesClientArgs } from '..'; import { decodeCommentRequest, ensureAuthorized } from '../utils'; @@ -39,7 +39,7 @@ export interface UpdateArgs { interface CombinedCaseParams { attachmentService: AttachmentService; - caseService: CaseService; + caseService: CasesService; soClient: SavedObjectsClientContract; caseID: string; logger: Logger; diff --git a/x-pack/plugins/cases/server/client/cases/delete.ts b/x-pack/plugins/cases/server/client/cases/delete.ts index 81a8e618d6d157..597b8ad0fca00a 100644 --- a/x-pack/plugins/cases/server/client/cases/delete.ts +++ b/x-pack/plugins/cases/server/client/cases/delete.ts @@ -10,7 +10,7 @@ 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 { AttachmentService, CasesService } from '../../services'; import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; import { Operations } from '../../authorization'; import { ensureAuthorized } from '../utils'; @@ -23,7 +23,7 @@ async function deleteSubCases({ caseIds, }: { attachmentService: AttachmentService; - caseService: CaseService; + caseService: CasesService; soClient: SavedObjectsClientContract; caseIds: string[]; }) { diff --git a/x-pack/plugins/cases/server/client/cases/get.ts b/x-pack/plugins/cases/server/client/cases/get.ts index 30ffedaba8923e..0dadc128b3ceb4 100644 --- a/x-pack/plugins/cases/server/client/cases/get.ts +++ b/x-pack/plugins/cases/server/client/cases/get.ts @@ -35,7 +35,7 @@ import { ensureAuthorized, getAuthorizationFilter, } from '../utils'; -import { CaseService } from '../../services'; +import { CasesService } from '../../services'; /** * Parameters for finding cases IDs using an alert ID @@ -106,7 +106,7 @@ export const getCaseIDsByAlertID = async ( logSuccessfulAuthorization(); - return CaseService.getCaseIDsFromAlertAggs(commentsWithAlert); + return CasesService.getCaseIDsFromAlertAggs(commentsWithAlert); } catch (error) { throw createCaseError({ message: `Failed to get case IDs using alert ID: ${alertID} options: ${JSON.stringify( diff --git a/x-pack/plugins/cases/server/client/cases/update.ts b/x-pack/plugins/cases/server/client/cases/update.ts index a0da2e489ac7ac..1cda4863ffe410 100644 --- a/x-pack/plugins/cases/server/client/cases/update.ts +++ b/x-pack/plugins/cases/server/client/cases/update.ts @@ -38,7 +38,7 @@ import { import { buildCaseUserActions } from '../../services/user_actions/helpers'; import { ensureAuthorized, getCaseToUpdate } from '../utils'; -import { CaseService } from '../../services'; +import { CasesService } from '../../services'; import { CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT, @@ -137,7 +137,7 @@ async function throwIfInvalidUpdateOfTypeWithAlerts({ soClient, }: { requests: ESCasePatchRequest[]; - caseService: CaseService; + caseService: CasesService; soClient: SavedObjectsClientContract; }) { const getAlertsForID = async (caseToUpdate: ESCasePatchRequest) => { @@ -199,7 +199,7 @@ async function getAlertComments({ soClient, }: { casesToSync: ESCasePatchRequest[]; - caseService: CaseService; + caseService: CasesService; soClient: SavedObjectsClientContract; }): Promise> { const idsOfCasesToSync = casesToSync.map((casePatchReq) => casePatchReq.id); @@ -228,7 +228,7 @@ async function getSubCasesToStatus({ soClient, }: { totalAlerts: SavedObjectsFindResponse; - caseService: CaseService; + caseService: CasesService; soClient: SavedObjectsClientContract; }): Promise> { const subCasesToRetrieve = totalAlerts.saved_objects.reduce((acc, alertComment) => { @@ -298,7 +298,7 @@ async function updateAlerts({ casesWithSyncSettingChangedToOn: ESCasePatchRequest[]; casesWithStatusChangedAndSynced: ESCasePatchRequest[]; casesMap: Map>; - caseService: CaseService; + caseService: CasesService; soClient: SavedObjectsClientContract; casesClientInternal: CasesClientInternal; }) { diff --git a/x-pack/plugins/cases/server/client/factory.ts b/x-pack/plugins/cases/server/client/factory.ts index 18b9be3aa5b197..7110e7e9e1d922 100644 --- a/x-pack/plugins/cases/server/client/factory.ts +++ b/x-pack/plugins/cases/server/client/factory.ts @@ -17,7 +17,7 @@ import { Authorization } from '../authorization/authorization'; import { GetSpaceFn } from '../authorization/types'; import { CaseConfigureService, - CaseService, + CasesService, CaseUserActionService, ConnectorMappingsService, AttachmentService, @@ -88,7 +88,7 @@ export class CasesClientFactory { logger: this.logger, }); - const caseService = new CaseService(this.logger, this.options?.securityPluginStart?.authc); + const caseService = new CasesService(this.logger, this.options?.securityPluginStart?.authc); const userInfo = caseService.getUser({ request }); return createCasesClient({ diff --git a/x-pack/plugins/cases/server/client/sub_cases/update.ts b/x-pack/plugins/cases/server/client/sub_cases/update.ts index 1ad579b3b210e0..9e64a7b8731b16 100644 --- a/x-pack/plugins/cases/server/client/sub_cases/update.ts +++ b/x-pack/plugins/cases/server/client/sub_cases/update.ts @@ -17,7 +17,7 @@ import { } from 'kibana/server'; import { nodeBuilder } from '../../../../../../src/plugins/data/common'; -import { CaseService } from '../../services'; +import { CasesService } from '../../services'; import { CaseStatuses, SubCasesPatchRequest, @@ -119,7 +119,7 @@ async function getParentCases({ subCaseIDs, subCasesMap, }: { - caseService: CaseService; + caseService: CasesService; soClient: SavedObjectsClientContract; subCaseIDs: string[]; subCasesMap: Map>; @@ -185,7 +185,7 @@ async function getAlertComments({ soClient, }: { subCasesToSync: SubCasePatchRequest[]; - caseService: CaseService; + caseService: CasesService; soClient: SavedObjectsClientContract; }): Promise> { const ids = subCasesToSync.map((subCase) => subCase.id); @@ -211,7 +211,7 @@ async function updateAlerts({ logger, subCasesToSync, }: { - caseService: CaseService; + caseService: CasesService; soClient: SavedObjectsClientContract; casesClientInternal: CasesClientInternal; logger: Logger; diff --git a/x-pack/plugins/cases/server/client/types.ts b/x-pack/plugins/cases/server/client/types.ts index 9e54b013dc602b..7d1c0855061c26 100644 --- a/x-pack/plugins/cases/server/client/types.ts +++ b/x-pack/plugins/cases/server/client/types.ts @@ -13,7 +13,7 @@ import { Authorization } from '../authorization/authorization'; import { AlertServiceContract, CaseConfigureService, - CaseService, + CasesService, CaseUserActionService, ConnectorMappingsService, AttachmentService, @@ -26,7 +26,7 @@ import { ActionsClient } from '../../../actions/server'; export interface CasesClientArgs { readonly scopedClusterClient: ElasticsearchClient; readonly caseConfigureService: CaseConfigureService; - readonly caseService: CaseService; + readonly caseService: CasesService; readonly connectorMappingsService: ConnectorMappingsService; readonly user: User; readonly unsecuredSavedObjectsClient: SavedObjectsClientContract; diff --git a/x-pack/plugins/cases/server/common/models/commentable_case.ts b/x-pack/plugins/cases/server/common/models/commentable_case.ts index 81b5aca58f7979..894e1f9a7f518f 100644 --- a/x-pack/plugins/cases/server/common/models/commentable_case.ts +++ b/x-pack/plugins/cases/server/common/models/commentable_case.ts @@ -35,7 +35,7 @@ import { transformNewComment, } from '..'; import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../common/constants'; -import { AttachmentService, CaseService } from '../../services'; +import { AttachmentService, CasesService } from '../../services'; import { createCaseError } from '../error'; import { countAlertsForID } from '../index'; @@ -53,7 +53,7 @@ interface CommentableCaseParams { collection: SavedObject; subCase?: SavedObject; soClient: SavedObjectsClientContract; - caseService: CaseService; + caseService: CasesService; attachmentService: AttachmentService; logger: Logger; } @@ -66,7 +66,7 @@ export class CommentableCase { private readonly collection: SavedObject; private readonly subCase?: SavedObject; private readonly soClient: SavedObjectsClientContract; - private readonly caseService: CaseService; + private readonly caseService: CasesService; private readonly attachmentService: AttachmentService; private readonly logger: Logger; diff --git a/x-pack/plugins/cases/server/routes/api/__fixtures__/authc_mock.ts b/x-pack/plugins/cases/server/routes/api/__fixtures__/authc_mock.ts index 66d3ffe5f23d16..a9292229d5eea4 100644 --- a/x-pack/plugins/cases/server/routes/api/__fixtures__/authc_mock.ts +++ b/x-pack/plugins/cases/server/routes/api/__fixtures__/authc_mock.ts @@ -16,7 +16,7 @@ function createAuthenticationMock({ authc.getCurrentUser.mockReturnValue( currentUser !== undefined ? // if we pass in null then use the null user (has null for each field) this is the default behavior - // for the CaseService getUser method + // for the CasesService getUser method currentUser !== null ? currentUser : nullUser diff --git a/x-pack/plugins/cases/server/services/cases/index.ts b/x-pack/plugins/cases/server/services/cases/index.ts index 3703be19ec0f54..38e9881cbdccce 100644 --- a/x-pack/plugins/cases/server/services/cases/index.ts +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -211,7 +211,7 @@ const transformNewSubCase = ({ }; }; -export class CaseService { +export class CasesService { constructor( private readonly log: Logger, private readonly authentication?: SecurityPluginSetup['authc'] diff --git a/x-pack/plugins/cases/server/services/index.ts b/x-pack/plugins/cases/server/services/index.ts index cffe7df91743fc..6a56001f29cac9 100644 --- a/x-pack/plugins/cases/server/services/index.ts +++ b/x-pack/plugins/cases/server/services/index.ts @@ -7,7 +7,7 @@ import { SavedObjectsClientContract } from 'kibana/server'; -export { CaseService } from './cases'; +export { CasesService } from './cases'; export { CaseConfigureService } from './configure'; export { CaseUserActionService } from './user_actions'; export { ConnectorMappingsService } from './connector_mappings'; diff --git a/x-pack/plugins/cases/server/services/mocks.ts b/x-pack/plugins/cases/server/services/mocks.ts index fb80ec4d2bda0f..ce9aec942220a5 100644 --- a/x-pack/plugins/cases/server/services/mocks.ts +++ b/x-pack/plugins/cases/server/services/mocks.ts @@ -9,13 +9,13 @@ import { PublicMethodsOf } from '@kbn/utility-types'; import { AlertServiceContract, CaseConfigureService, - CaseService, + CasesService, CaseUserActionService, ConnectorMappingsService, AttachmentService, } from '.'; -export type CaseServiceMock = jest.Mocked; +export type CaseServiceMock = jest.Mocked; export type CaseConfigureServiceMock = jest.Mocked; export type ConnectorMappingsServiceMock = jest.Mocked; export type CaseUserActionServiceMock = jest.Mocked; @@ -23,7 +23,7 @@ export type AlertServiceMock = jest.Mocked; export type AttachmentServiceMock = jest.Mocked; export const createCaseServiceMock = (): CaseServiceMock => { - const service: PublicMethodsOf = { + const service: PublicMethodsOf = { createSubCase: jest.fn(), deleteCase: jest.fn(), deleteSubCase: jest.fn(), From 148623e6448f6f93d0a2da7bfdd32f10782f4dab Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Thu, 27 May 2021 14:37:29 -0400 Subject: [PATCH 087/113] Removing uneeded else --- x-pack/plugins/cases/public/containers/configure/api.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/cases/public/containers/configure/api.ts b/x-pack/plugins/cases/public/containers/configure/api.ts index a6d530caa588eb..c972e2fc5c5fb1 100644 --- a/x-pack/plugins/cases/public/containers/configure/api.ts +++ b/x-pack/plugins/cases/public/containers/configure/api.ts @@ -54,12 +54,10 @@ export const getCaseConfigure = async ({ const decodedConfigs = decodeCaseConfigurationsResponse(response); if (Array.isArray(decodedConfigs) && decodedConfigs.length > 0) { return convertToCamelCase(decodedConfigs[0]); - } else { - return null; } - } else { - return null; } + + return null; }; export const getConnectorMappings = async ({ signal }: ApiProps): Promise => { From 7acf83aa7c26488dcc43ccdc11f708921992568a Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Thu, 27 May 2021 14:56:51 -0400 Subject: [PATCH 088/113] Fixing tests and adding cases merge tests --- .../__snapshots__/oss_features.test.ts.snap | 24 +++ .../feature_privilege_iterator.test.ts | 161 ++++++++++++++++++ 2 files changed, 185 insertions(+) diff --git a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap index 88712f2ac14c03..afd5b6803f4dd5 100644 --- a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap +++ b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap @@ -522,6 +522,10 @@ Array [ "dashboards", "kibana", ], + "cases": Object { + "all": Array [], + "read": Array [], + }, "catalogue": Array [ "dashboard", ], @@ -661,6 +665,10 @@ Array [ "discover", "kibana", ], + "cases": Object { + "all": Array [], + "read": Array [], + }, "catalogue": Array [ "discover", ], @@ -897,6 +905,10 @@ Array [ "lens", "kibana", ], + "cases": Object { + "all": Array [], + "read": Array [], + }, "catalogue": Array [ "visualize", ], @@ -1020,6 +1032,10 @@ Array [ "dashboards", "kibana", ], + "cases": Object { + "all": Array [], + "read": Array [], + }, "catalogue": Array [ "dashboard", ], @@ -1159,6 +1175,10 @@ Array [ "discover", "kibana", ], + "cases": Object { + "all": Array [], + "read": Array [], + }, "catalogue": Array [ "discover", ], @@ -1395,6 +1415,10 @@ Array [ "lens", "kibana", ], + "cases": Object { + "all": Array [], + "read": Array [], + }, "catalogue": Array [ "visualize", ], diff --git a/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.test.ts b/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.test.ts index 6acc29793797fe..120470bafd4cfb 100644 --- a/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.test.ts +++ b/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.test.ts @@ -49,6 +49,10 @@ describe('featurePrivilegeIterator', () => { all: ['alerting-all-type'], read: ['alerting-read-type'], }, + cases: { + all: ['cases-all-type'], + read: ['cases-read-type'], + }, ui: ['ui-action'], }, read: { @@ -65,6 +69,9 @@ describe('featurePrivilegeIterator', () => { alerting: { read: ['alerting-read-type'], }, + cases: { + read: ['cases-read-type'], + }, ui: ['ui-action'], }, }, @@ -96,6 +103,10 @@ describe('featurePrivilegeIterator', () => { all: ['alerting-all-type'], read: ['alerting-read-type'], }, + cases: { + all: ['cases-all-type'], + read: ['cases-read-type'], + }, ui: ['ui-action'], }, }, @@ -115,6 +126,9 @@ describe('featurePrivilegeIterator', () => { alerting: { read: ['alerting-read-type'], }, + cases: { + read: ['cases-read-type'], + }, ui: ['ui-action'], }, }, @@ -142,6 +156,10 @@ describe('featurePrivilegeIterator', () => { all: ['alerting-all-type'], read: ['alerting-read-type'], }, + cases: { + all: ['cases-all-type'], + read: ['cases-read-type'], + }, ui: ['ui-action'], }, read: { @@ -158,6 +176,9 @@ describe('featurePrivilegeIterator', () => { alerting: { read: ['alerting-read-type'], }, + cases: { + read: ['cases-read-type'], + }, ui: ['ui-action'], }, }, @@ -190,6 +211,10 @@ describe('featurePrivilegeIterator', () => { all: ['alerting-all-type'], read: ['alerting-read-type'], }, + cases: { + all: ['cases-all-type'], + read: ['cases-read-type'], + }, ui: ['ui-action'], }, }, @@ -218,6 +243,10 @@ describe('featurePrivilegeIterator', () => { all: ['alerting-all-type'], read: ['alerting-read-type'], }, + cases: { + all: ['cases-all-type'], + read: ['cases-read-type'], + }, ui: ['ui-action'], }, read: { @@ -234,6 +263,9 @@ describe('featurePrivilegeIterator', () => { alerting: { read: ['alerting-read-type'], }, + cases: { + read: ['cases-read-type'], + }, ui: ['ui-action'], }, }, @@ -262,6 +294,10 @@ describe('featurePrivilegeIterator', () => { all: ['alerting-all-sub-type'], read: ['alerting-read-sub-type'], }, + cases: { + all: ['cases-all-sub-type'], + read: ['cases-read-sub-type'], + }, ui: ['ui-sub-type'], }, ], @@ -296,6 +332,10 @@ describe('featurePrivilegeIterator', () => { all: ['alerting-all-type'], read: ['alerting-read-type'], }, + cases: { + all: ['cases-all-type'], + read: ['cases-read-type'], + }, ui: ['ui-action'], }, }, @@ -315,6 +355,9 @@ describe('featurePrivilegeIterator', () => { alerting: { read: ['alerting-read-type'], }, + cases: { + read: ['cases-read-type'], + }, ui: ['ui-action'], }, }, @@ -343,6 +386,10 @@ describe('featurePrivilegeIterator', () => { all: ['alerting-all-type'], read: ['alerting-read-type'], }, + cases: { + all: ['cases-all-type'], + read: ['cases-read-type'], + }, ui: ['ui-action'], }, read: { @@ -359,6 +406,9 @@ describe('featurePrivilegeIterator', () => { alerting: { read: ['alerting-read-type'], }, + cases: { + read: ['cases-read-type'], + }, ui: ['ui-action'], }, }, @@ -387,6 +437,10 @@ describe('featurePrivilegeIterator', () => { all: ['alerting-all-sub-type'], read: ['alerting-read-sub-type'], }, + cases: { + all: ['cases-all-sub-type'], + read: ['cases-read-sub-type'], + }, ui: ['ui-sub-type'], }, ], @@ -421,6 +475,10 @@ describe('featurePrivilegeIterator', () => { all: ['alerting-all-type'], read: ['alerting-read-type'], }, + cases: { + all: ['cases-all-type'], + read: ['cases-read-type'], + }, ui: ['ui-action'], }, }, @@ -440,6 +498,9 @@ describe('featurePrivilegeIterator', () => { alerting: { read: ['alerting-read-type'], }, + cases: { + read: ['cases-read-type'], + }, ui: ['ui-action'], }, }, @@ -468,6 +529,10 @@ describe('featurePrivilegeIterator', () => { all: ['alerting-all-type'], read: ['alerting-read-type'], }, + cases: { + all: ['cases-all-type'], + read: ['cases-read-type'], + }, ui: ['ui-action'], }, read: { @@ -484,6 +549,9 @@ describe('featurePrivilegeIterator', () => { alerting: { read: ['alerting-read-type'], }, + cases: { + read: ['cases-read-type'], + }, ui: ['ui-action'], }, }, @@ -513,6 +581,10 @@ describe('featurePrivilegeIterator', () => { all: ['alerting-all-sub-type'], read: ['alerting-read-sub-type'], }, + cases: { + all: ['cases-all-sub-type'], + read: ['cases-read-sub-type'], + }, ui: ['ui-sub-type'], }, ], @@ -548,6 +620,10 @@ describe('featurePrivilegeIterator', () => { all: ['alerting-all-type', 'alerting-all-sub-type'], read: ['alerting-read-type', 'alerting-read-sub-type'], }, + cases: { + all: ['cases-all-type', 'cases-all-sub-type'], + read: ['cases-read-type', 'cases-read-sub-type'], + }, ui: ['ui-action', 'ui-sub-type'], }, }, @@ -569,6 +645,10 @@ describe('featurePrivilegeIterator', () => { all: ['alerting-all-sub-type'], read: ['alerting-read-type', 'alerting-read-sub-type'], }, + cases: { + all: ['cases-all-sub-type'], + read: ['cases-read-type', 'cases-read-sub-type'], + }, ui: ['ui-action', 'ui-sub-type'], }, }, @@ -597,6 +677,10 @@ describe('featurePrivilegeIterator', () => { all: ['alerting-all-type'], read: ['alerting-read-type'], }, + cases: { + all: ['cases-all-type'], + read: ['cases-read-type'], + }, ui: ['ui-action'], }, read: { @@ -613,6 +697,9 @@ describe('featurePrivilegeIterator', () => { alerting: { read: ['alerting-read-type'], }, + cases: { + read: ['cases-read-type'], + }, ui: ['ui-action'], }, }, @@ -640,6 +727,9 @@ describe('featurePrivilegeIterator', () => { alerting: { read: ['alerting-read-type'], }, + cases: { + read: ['cases-read-type'], + }, ui: ['ui-action'], }, ], @@ -674,6 +764,10 @@ describe('featurePrivilegeIterator', () => { all: ['alerting-all-type'], read: ['alerting-read-type'], }, + cases: { + all: ['cases-all-type'], + read: ['cases-read-type'], + }, ui: ['ui-action'], }, }, @@ -694,6 +788,10 @@ describe('featurePrivilegeIterator', () => { all: [], read: ['alerting-read-type'], }, + cases: { + all: [], + read: ['cases-read-type'], + }, ui: ['ui-action'], }, }, @@ -722,6 +820,10 @@ describe('featurePrivilegeIterator', () => { all: ['alerting-all-type'], read: ['alerting-read-type'], }, + cases: { + all: ['cases-all-type'], + read: ['cases-read-type'], + }, ui: ['ui-action'], }, read: { @@ -738,6 +840,9 @@ describe('featurePrivilegeIterator', () => { alerting: { read: ['alerting-read-type'], }, + cases: { + read: ['cases-read-type'], + }, ui: ['ui-action'], }, }, @@ -767,6 +872,10 @@ describe('featurePrivilegeIterator', () => { all: ['alerting-all-sub-type'], read: ['alerting-read-sub-type'], }, + cases: { + all: ['cases-all-sub-type'], + read: ['cases-read-sub-type'], + }, ui: ['ui-sub-type'], }, ], @@ -802,6 +911,10 @@ describe('featurePrivilegeIterator', () => { all: ['alerting-all-type', 'alerting-all-sub-type'], read: ['alerting-read-type', 'alerting-read-sub-type'], }, + cases: { + all: ['cases-all-type', 'cases-all-sub-type'], + read: ['cases-read-type', 'cases-read-sub-type'], + }, ui: ['ui-action', 'ui-sub-type'], }, }, @@ -821,6 +934,9 @@ describe('featurePrivilegeIterator', () => { alerting: { read: ['alerting-read-type'], }, + cases: { + read: ['cases-read-type'], + }, ui: ['ui-action'], }, }, @@ -849,6 +965,10 @@ describe('featurePrivilegeIterator', () => { all: ['alerting-all-type'], read: ['alerting-read-type'], }, + cases: { + all: ['cases-all-type'], + read: ['cases-read-type'], + }, ui: ['ui-action'], }, read: { @@ -865,6 +985,9 @@ describe('featurePrivilegeIterator', () => { alerting: { read: ['alerting-read-type'], }, + cases: { + read: ['cases-read-type'], + }, ui: ['ui-action'], }, }, @@ -895,6 +1018,10 @@ describe('featurePrivilegeIterator', () => { all: ['alerting-all-sub-type'], read: ['alerting-read-sub-type'], }, + cases: { + all: ['cases-all-sub-type'], + read: ['cases-read-sub-type'], + }, ui: ['ui-sub-type'], }, ], @@ -929,6 +1056,10 @@ describe('featurePrivilegeIterator', () => { all: ['alerting-all-type'], read: ['alerting-read-type'], }, + cases: { + all: ['cases-all-type'], + read: ['cases-read-type'], + }, ui: ['ui-action'], }, }, @@ -948,6 +1079,9 @@ describe('featurePrivilegeIterator', () => { alerting: { read: ['alerting-read-type'], }, + cases: { + read: ['cases-read-type'], + }, ui: ['ui-action'], }, }, @@ -1002,6 +1136,10 @@ describe('featurePrivilegeIterator', () => { all: ['alerting-all-sub-type'], read: ['alerting-read-sub-type'], }, + cases: { + all: ['cases-all-sub-type'], + read: ['cases-read-sub-type'], + }, ui: ['ui-sub-type'], }, ], @@ -1037,6 +1175,10 @@ describe('featurePrivilegeIterator', () => { all: ['alerting-all-sub-type'], read: ['alerting-read-sub-type'], }, + cases: { + all: ['cases-all-sub-type'], + read: ['cases-read-sub-type'], + }, ui: ['ui-sub-type'], }, }, @@ -1058,6 +1200,10 @@ describe('featurePrivilegeIterator', () => { all: ['alerting-all-sub-type'], read: ['alerting-read-sub-type'], }, + cases: { + all: ['cases-all-sub-type'], + read: ['cases-read-sub-type'], + }, ui: ['ui-sub-type'], }, }, @@ -1086,6 +1232,10 @@ describe('featurePrivilegeIterator', () => { all: ['alerting-all-type'], read: ['alerting-read-type'], }, + cases: { + all: ['cases-all-type'], + read: ['cases-read-type'], + }, ui: ['ui-action'], }, read: { @@ -1102,6 +1252,9 @@ describe('featurePrivilegeIterator', () => { alerting: { read: ['alerting-read-type'], }, + cases: { + read: ['cases-read-type'], + }, ui: ['ui-action'], }, }, @@ -1154,6 +1307,10 @@ describe('featurePrivilegeIterator', () => { all: ['alerting-all-type'], read: ['alerting-read-type'], }, + cases: { + all: ['cases-all-type'], + read: ['cases-read-type'], + }, ui: ['ui-action'], }, }, @@ -1174,6 +1331,10 @@ describe('featurePrivilegeIterator', () => { all: [], read: ['alerting-read-type'], }, + cases: { + all: [], + read: ['cases-read-type'], + }, ui: ['ui-action'], }, }, From 445c846fe0e2bc19fff2debc2b95d6578995c876 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Tue, 1 Jun 2021 09:11:56 -0400 Subject: [PATCH 089/113] [Cases][Security Solution] Basic license security solution API tests (#100925) * Cleaning up the fixture plugins * Adding basic feature test --- .../test/api_integration_basic/apis/index.ts | 1 + .../security_solution/cases_privileges.ts | 183 ++++++++++++++++++ .../apis/security_solution/index.ts | 14 ++ .../plugins/observability/server/plugin.ts | 17 +- .../security_solution/server/plugin.ts | 16 +- .../tests/common/comments/delete_comment.ts | 33 +--- .../security_only/tests/trial/index.ts | 2 +- 7 files changed, 204 insertions(+), 62 deletions(-) create mode 100644 x-pack/test/api_integration_basic/apis/security_solution/cases_privileges.ts create mode 100644 x-pack/test/api_integration_basic/apis/security_solution/index.ts diff --git a/x-pack/test/api_integration_basic/apis/index.ts b/x-pack/test/api_integration_basic/apis/index.ts index 323a8e95c4b2b4..27869095bd792a 100644 --- a/x-pack/test/api_integration_basic/apis/index.ts +++ b/x-pack/test/api_integration_basic/apis/index.ts @@ -13,5 +13,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./ml')); loadTestFile(require.resolve('./transform')); + loadTestFile(require.resolve('./security_solution')); }); } diff --git a/x-pack/test/api_integration_basic/apis/security_solution/cases_privileges.ts b/x-pack/test/api_integration_basic/apis/security_solution/cases_privileges.ts new file mode 100644 index 00000000000000..532249a049b470 --- /dev/null +++ b/x-pack/test/api_integration_basic/apis/security_solution/cases_privileges.ts @@ -0,0 +1,183 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; +import { + createUsersAndRoles, + deleteUsersAndRoles, +} from '../../../case_api_integration/common/lib/authentication'; + +import { Role, User } from '../../../case_api_integration/common/lib/authentication/types'; +import { + createCase, + deleteAllCaseItems, + getCase, +} from '../../../case_api_integration/common/lib/utils'; +import { getPostCaseRequest } from '../../../case_api_integration/common/lib/mock'; +import { APP_ID } from '../../../../plugins/security_solution/common/constants'; + +const secAll: Role = { + name: 'sec_all_role', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + siem: ['all'], + actions: ['all'], + actionsSimulators: ['all'], + }, + spaces: ['*'], + }, + ], + }, +}; + +const secAllUser: User = { + username: 'sec_all_user', + password: 'password', + roles: [secAll.name], +}; + +const secRead: Role = { + name: 'sec_read_role', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + siem: ['read'], + actions: ['all'], + actionsSimulators: ['all'], + }, + spaces: ['*'], + }, + ], + }, +}; + +const secReadUser: User = { + username: 'sec_read_user', + password: 'password', + roles: [secRead.name], +}; + +const secNone: Role = { + name: 'sec_none_role', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + actions: ['all'], + actionsSimulators: ['all'], + }, + spaces: ['*'], + }, + ], + }, +}; + +const secNoneUser: User = { + username: 'sec_none_user', + password: 'password', + roles: [secNone.name], +}; + +const roles = [secAll, secRead, secNone]; + +const users = [secAllUser, secReadUser, secNoneUser]; + +export default ({ getService }: FtrProviderContext): void => { + describe('cases feature privilege', () => { + const es = getService('es'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const supertest = getService('supertest'); + + before(async () => { + await createUsersAndRoles(getService, users, roles); + }); + + after(async () => { + await deleteUsersAndRoles(getService, users, roles); + }); + + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it(`User ${ + secAllUser.username + } with role(s) ${secAllUser.roles.join()} can create a case`, async () => { + await createCase(supertestWithoutAuth, getPostCaseRequest({ owner: APP_ID }), 200, { + user: secAllUser, + space: null, + }); + }); + + it(`User ${ + secReadUser.username + } with role(s) ${secReadUser.roles.join()} can get a case`, async () => { + const caseInfo = await createCase(supertest, getPostCaseRequest({ owner: APP_ID })); + const retrievedCase = await getCase({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + expectedHttpCode: 200, + auth: { user: secReadUser, space: null }, + }); + + expect(caseInfo.owner).to.eql(retrievedCase.owner); + }); + + for (const user of [secReadUser, secNoneUser]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} cannot create a case`, async () => { + await createCase(supertestWithoutAuth, getPostCaseRequest({ owner: APP_ID }), 403, { + user, + space: null, + }); + }); + } + + it(`User ${ + secNoneUser.username + } with role(s) ${secNoneUser.roles.join()} cannot get a case`, async () => { + const caseInfo = await createCase(supertest, getPostCaseRequest({ owner: APP_ID })); + + await getCase({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + expectedHttpCode: 403, + auth: { user: secNoneUser, space: null }, + }); + }); + }); +}; diff --git a/x-pack/test/api_integration_basic/apis/security_solution/index.ts b/x-pack/test/api_integration_basic/apis/security_solution/index.ts new file mode 100644 index 00000000000000..90560c6c677d49 --- /dev/null +++ b/x-pack/test/api_integration_basic/apis/security_solution/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('SecuritySolution Endpoints basic licsense', () => { + loadTestFile(require.resolve('./cases_privileges')); + }); +} diff --git a/x-pack/test/case_api_integration/common/fixtures/plugins/observability/server/plugin.ts b/x-pack/test/case_api_integration/common/fixtures/plugins/observability/server/plugin.ts index 9ce9d0e1ae1d1e..f94358be2bc19b 100644 --- a/x-pack/test/case_api_integration/common/fixtures/plugins/observability/server/plugin.ts +++ b/x-pack/test/case_api_integration/common/fixtures/plugins/observability/server/plugin.ts @@ -20,19 +20,6 @@ export interface FixtureStartDeps { spaces?: SpacesPluginStart; } -/** - * These are a copy of the values here: x-pack/plugins/cases/common/constants.ts because when the plugin attempts to - * import them from the constants.ts file it gets an error. - */ -const casesSavedObjectTypes = [ - 'cases', - 'cases-connector-mappings', - 'cases-sub-case', - 'cases-user-actions', - 'cases-comments', - 'cases-configure', -]; - export class FixturePlugin implements Plugin { public setup(core: CoreSetup, deps: FixtureSetupDeps) { const { features } = deps; @@ -49,7 +36,7 @@ export class FixturePlugin implements Plugin { public setup(core: CoreSetup, deps: FixtureSetupDeps) { const { features } = deps; @@ -48,7 +36,7 @@ export class FixturePlugin implements Plugin { secOnlyReadSpacesAll, obsOnlyReadSpacesAll, obsSecReadSpacesAll, + noKibanaPrivileges, ]) { it(`User ${ user.username @@ -170,38 +171,6 @@ export default ({ getService }: FtrProviderContext): void => { }); } - it('should not delete a comment with no kibana privileges', async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest(), - 200, - superUserNoSpaceAuth - ); - - const commentResp = await createComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - params: postCommentUserReq, - auth: superUserNoSpaceAuth, - }); - - await deleteComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - commentId: commentResp.comments![0].id, - auth: { user: noKibanaPrivileges, space: null }, - expectedHttpCode: 403, - }); - - await deleteAllComments({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - auth: { user: noKibanaPrivileges, space: null }, - // the find in the delete all will return no results - expectedHttpCode: 404, - }); - }); - it('should return a 404 when attempting to access a space', async () => { const postedCase = await createCase( supertestWithoutAuth, diff --git a/x-pack/test/case_api_integration/security_only/tests/trial/index.ts b/x-pack/test/case_api_integration/security_only/tests/trial/index.ts index 550dad5917d452..86a44459a58370 100644 --- a/x-pack/test/case_api_integration/security_only/tests/trial/index.ts +++ b/x-pack/test/case_api_integration/security_only/tests/trial/index.ts @@ -12,7 +12,7 @@ import { createUsersAndRoles, deleteUsersAndRoles } from '../../../common/lib/au // eslint-disable-next-line import/no-default-export export default ({ loadTestFile, getService }: FtrProviderContext): void => { - describe('cases security and spaces enabled: trial', function () { + describe('cases security only enabled: trial', function () { // Fastest ciGroup for the moment. this.tags('ciGroup5'); From bee51fd2f791c53ffda43cc91fdb3734c0eecb4b Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 1 Jun 2021 08:34:51 -0600 Subject: [PATCH 090/113] midst of nathans pr changes --- .../public/components/app/cases/case_view/index.tsx | 9 ++++----- ...ermissions.tsx => use_get_user_cases_permissions.tsx} | 4 ++-- .../observability/public/hooks/use_messages_storage.tsx | 1 - .../observability/public/pages/cases/all_cases.tsx | 2 +- .../observability/public/pages/cases/case_details.tsx | 2 +- .../observability/public/pages/cases/configure_cases.tsx | 2 +- .../observability/public/pages/cases/create_case.tsx | 2 +- .../plugins/observability/public/utils/kibana_react.ts | 8 +++++++- 8 files changed, 17 insertions(+), 13 deletions(-) rename x-pack/plugins/observability/public/hooks/{use_get_cases_user_permissions.tsx => use_get_user_cases_permissions.tsx} (95%) diff --git a/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx b/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx index 2f00d40d953acb..31a08a71cd107a 100644 --- a/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx @@ -42,8 +42,11 @@ export interface CaseProps extends Props { export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) => { const [caseTitle, setCaseTitle] = useState(null); + const { cases: casesUi, application } = useKibana().services; + const { navigateToApp } = application; + const href = application?.getUrlForApp('observability-cases') ?? ''; useBreadcrumbs([ - casesBreadcrumb, + { ...casesBreadcrumb, href }, ...(caseTitle !== null ? [ { @@ -62,10 +65,6 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = [caseTitle] ); - const { - cases: casesUi, - application: { navigateToApp }, - } = useKibana().services; const history = useHistory(); const { formatUrl } = useFormatUrl(CASES_APP_ID); diff --git a/x-pack/plugins/observability/public/hooks/use_get_cases_user_permissions.tsx b/x-pack/plugins/observability/public/hooks/use_get_user_cases_permissions.tsx similarity index 95% rename from x-pack/plugins/observability/public/hooks/use_get_cases_user_permissions.tsx rename to x-pack/plugins/observability/public/hooks/use_get_user_cases_permissions.tsx index 1d97f739415657..69858ccde3df97 100644 --- a/x-pack/plugins/observability/public/hooks/use_get_cases_user_permissions.tsx +++ b/x-pack/plugins/observability/public/hooks/use_get_user_cases_permissions.tsx @@ -13,7 +13,7 @@ export interface UseGetUserCasesPermissions { read: boolean; } -export const useGetUserCasesPermissions = () => { +export function useGetUserCasesPermissions() { const [casesPermissions, setCasesPermissions] = useState(null); const uiCapabilities = useKibana().services.application.capabilities; @@ -33,4 +33,4 @@ export const useGetUserCasesPermissions = () => { }, [uiCapabilities]); return casesPermissions; -}; +} diff --git a/x-pack/plugins/observability/public/hooks/use_messages_storage.tsx b/x-pack/plugins/observability/public/hooks/use_messages_storage.tsx index cb457ab61fed3e..266d5ba7bf0e17 100644 --- a/x-pack/plugins/observability/public/hooks/use_messages_storage.tsx +++ b/x-pack/plugins/observability/public/hooks/use_messages_storage.tsx @@ -17,7 +17,6 @@ export interface UseMessagesStorage { } export const useMessagesStorage = (): UseMessagesStorage => { - // @ts-ignore const { storage } = useKibana().services; const getMessages = useCallback( diff --git a/x-pack/plugins/observability/public/pages/cases/all_cases.tsx b/x-pack/plugins/observability/public/pages/cases/all_cases.tsx index 0c69055cc1ab32..124ed9251a0916 100644 --- a/x-pack/plugins/observability/public/pages/cases/all_cases.tsx +++ b/x-pack/plugins/observability/public/pages/cases/all_cases.tsx @@ -14,7 +14,7 @@ import { ExperimentalBadge } from '../../components/shared/experimental_badge'; import { savedObjectReadOnlyErrorMessage, CaseCallOut } from '../../components/app/cases/callout'; import { CaseFeatureNoPermissions } from './feature_no_permissions'; -import { useGetUserCasesPermissions } from '../../hooks/use_get_cases_user_permissions'; +import { useGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions'; export const AllCasesPage = React.memo(() => { const userPermissions = useGetUserCasesPermissions(); diff --git a/x-pack/plugins/observability/public/pages/cases/case_details.tsx b/x-pack/plugins/observability/public/pages/cases/case_details.tsx index ed1887e14753e3..5137941ef780c5 100644 --- a/x-pack/plugins/observability/public/pages/cases/case_details.tsx +++ b/x-pack/plugins/observability/public/pages/cases/case_details.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { useParams } from 'react-router-dom'; import { CaseView } from '../../components/app/cases/case_view'; -import { useGetUserCasesPermissions } from '../../hooks/use_get_cases_user_permissions'; +import { useGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions'; import { useKibana } from '../../utils/kibana_react'; import { CASES_APP_ID } from '../../components/app/cases/constants'; import { CaseCallOut, savedObjectReadOnlyErrorMessage } from '../../components/app/cases/callout'; diff --git a/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx b/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx index ccfd455c9bc8f3..ec3712aed9a107 100644 --- a/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx +++ b/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx @@ -13,7 +13,7 @@ import * as i18n from '../../components/app/cases/translations'; import { ExperimentalBadge } from '../../components/shared/experimental_badge'; import { CASES_APP_ID, CASES_OWNER } from '../../components/app/cases/constants'; import { useKibana } from '../../utils/kibana_react'; -import { useGetUserCasesPermissions } from '../../hooks/use_get_cases_user_permissions'; +import { useGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions'; const ButtonEmpty = styled(EuiButtonEmpty)` display: block; diff --git a/x-pack/plugins/observability/public/pages/cases/create_case.tsx b/x-pack/plugins/observability/public/pages/cases/create_case.tsx index 42c96047fbc278..04990dbca6206d 100644 --- a/x-pack/plugins/observability/public/pages/cases/create_case.tsx +++ b/x-pack/plugins/observability/public/pages/cases/create_case.tsx @@ -13,7 +13,7 @@ import { Create } from '../../components/app/cases/create'; import { ExperimentalBadge } from '../../components/shared/experimental_badge'; import { CASES_APP_ID } from '../../components/app/cases/constants'; import { useKibana } from '../../utils/kibana_react'; -import { useGetUserCasesPermissions } from '../../hooks/use_get_cases_user_permissions'; +import { useGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions'; const ButtonEmpty = styled(EuiButtonEmpty)` display: block; diff --git a/x-pack/plugins/observability/public/utils/kibana_react.ts b/x-pack/plugins/observability/public/utils/kibana_react.ts index 2efac4616fad47..532003e30a1601 100644 --- a/x-pack/plugins/observability/public/utils/kibana_react.ts +++ b/x-pack/plugins/observability/public/utils/kibana_react.ts @@ -7,7 +7,13 @@ import { CoreStart } from 'kibana/public'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { ObservabilityPublicPluginsStart } from '../plugin'; -const useTypedKibana = () => useKibana(); + +export type StartServices = CoreStart & + ObservabilityPublicPluginsStart & { + storage: Storage; + }; +const useTypedKibana = () => useKibana(); export { useTypedKibana as useKibana }; From bf02730d54a917aedf6c5987ee96d5dfd24ca456 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 1 Jun 2021 15:13:40 -0600 Subject: [PATCH 091/113] redirect after delete --- .../components/case_action_bar/actions.tsx | 9 +++--- .../components/case_action_bar/index.tsx | 7 +++-- .../public/components/case_view/index.tsx | 5 ++-- .../cases/public/containers/api.test.tsx | 29 ------------------- x-pack/plugins/cases/public/containers/api.ts | 2 +- 5 files changed, 13 insertions(+), 39 deletions(-) diff --git a/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx b/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx index b8d9d7f85a9ef1..1674198556f124 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx @@ -7,26 +7,27 @@ import { isEmpty } from 'lodash/fp'; import React, { useMemo } from 'react'; -import { useHistory } from 'react-router-dom'; import * as i18n from '../case_view/translations'; import { useDeleteCases } from '../../containers/use_delete_cases'; import { ConfirmDeleteCaseModal } from '../confirm_delete_case'; import { PropertyActions } from '../property_actions'; -import { Case } from '../../containers/types'; +import { Case } from '../../../common'; import { CaseService } from '../../containers/use_get_case_user_actions'; +import { CasesNavigation } from '../links'; interface CaseViewActions { + allCasesNavigation: CasesNavigation; caseData: Case; currentExternalIncident: CaseService | null; disabled?: boolean; } const ActionsComponent: React.FC = ({ + allCasesNavigation, caseData, currentExternalIncident, disabled = false, }) => { - const history = useHistory(); // Delete case const { handleToggleModal, @@ -57,7 +58,7 @@ const ActionsComponent: React.FC = ({ ); if (isDeleted) { - history.push('/'); + allCasesNavigation.onClick(({ preventDefault: () => null } as unknown) as MouseEvent); return null; } return ( diff --git a/x-pack/plugins/cases/public/components/case_action_bar/index.tsx b/x-pack/plugins/cases/public/components/case_action_bar/index.tsx index 98bf67617c7afb..3b7c043f48be23 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/index.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/index.tsx @@ -16,16 +16,16 @@ import { EuiFlexItem, EuiIconTip, } from '@elastic/eui'; -import { CaseStatuses, CaseType } from '../../../common'; +import { Case, CaseStatuses, CaseType } from '../../../common'; import * as i18n from '../case_view/translations'; import { FormattedRelativePreferenceDate } from '../formatted_date'; import { Actions } from './actions'; -import { Case } from '../../containers/types'; import { CaseService } from '../../containers/use_get_case_user_actions'; import { StatusContextMenu } from './status_context_menu'; import { getStatusDate, getStatusTitle } from './helpers'; import { SyncAlertsSwitch } from '../case_settings/sync_alerts_switch'; import { OnUpdateFields } from '../case_view'; +import { CasesNavigation } from '../links'; const MyDescriptionList = styled(EuiDescriptionList)` ${({ theme }) => css` @@ -37,6 +37,7 @@ const MyDescriptionList = styled(EuiDescriptionList)` `; interface CaseActionBarProps { + allCasesNavigation: CasesNavigation; caseData: Case; currentExternalIncident: CaseService | null; disabled?: boolean; @@ -46,6 +47,7 @@ interface CaseActionBarProps { onUpdateField: (args: OnUpdateFields) => void; } const CaseActionBarComponent: React.FC = ({ + allCasesNavigation, caseData, currentExternalIncident, disabled = false, @@ -134,6 +136,7 @@ const CaseActionBarComponent: React.FC = ({ ( title={caseData.title} > { }); }); - test('tags with weird chars get handled gracefully', async () => { - const weirdTags: string[] = ['(', '"double"']; - - await getCases({ - filterOptions: { - ...DEFAULT_FILTER_OPTIONS, - reporters: [...respReporters, { username: null, full_name: null, email: null }], - tags: weirdTags, - status: CaseStatuses.open, - search: 'hello', - owner: [SECURITY_SOLUTION_OWNER], - }, - queryParams: DEFAULT_QUERY_PARAMS, - signal: abortCtrl.signal, - }); - expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/_find`, { - method: 'GET', - query: { - ...DEFAULT_QUERY_PARAMS, - reporters, - tags: ['"("', '"\\"double\\""'], - search: 'hello', - status: CaseStatuses.open, - owner: [SECURITY_SOLUTION_OWNER], - }, - signal: abortCtrl.signal, - }); - }); - test('happy path', async () => { const resp = await getCases({ filterOptions: { ...DEFAULT_FILTER_OPTIONS, owner: [SECURITY_SOLUTION_OWNER] }, diff --git a/x-pack/plugins/cases/public/containers/api.ts b/x-pack/plugins/cases/public/containers/api.ts index 66a4d174b0ffb8..fc1dc34b4e1aca 100644 --- a/x-pack/plugins/cases/public/containers/api.ts +++ b/x-pack/plugins/cases/public/containers/api.ts @@ -189,7 +189,7 @@ export const getCases = async ({ }: FetchCasesProps): Promise => { const query = { reporters: filterOptions.reporters.map((r) => r.username ?? '').filter((r) => r !== ''), - tags: filterOptions.tags.map((t) => `"${t.replace(/"/g, '\\"')}"`), + tags: filterOptions.tags, status: filterOptions.status, ...(filterOptions.search.length > 0 ? { search: filterOptions.search } : {}), ...(filterOptions.onlyCollectionType ? { type: CaseType.collection } : {}), From c0f2105db7a1f11a8949690f950e27b73be00203 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 2 Jun 2021 13:30:29 -0600 Subject: [PATCH 092/113] does not exist page --- .../components/case_view/does_not_exist.tsx | 31 +++++++++++++++++++ .../public/components/case_view/index.tsx | 3 +- .../components/case_view/translations.ts | 17 ++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/cases/public/components/case_view/does_not_exist.tsx diff --git a/x-pack/plugins/cases/public/components/case_view/does_not_exist.tsx b/x-pack/plugins/cases/public/components/case_view/does_not_exist.tsx new file mode 100644 index 00000000000000..094acb4bc0d5e6 --- /dev/null +++ b/x-pack/plugins/cases/public/components/case_view/does_not_exist.tsx @@ -0,0 +1,31 @@ +/* + * 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 React from 'react'; + +import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; +import * as i18n from './translations'; +import { CasesNavigation } from '../links'; +interface Props { + allCasesNavigation: CasesNavigation; + caseId: string; +} +export const DoesNotExist = ({ allCasesNavigation, caseId }: Props) => ( + {i18n.DOES_NOT_EXIST_TITLE}} + titleSize="xs" + body={

{i18n.DOES_NOT_EXIST_DESCRIPTION(caseId)}

} + actions={ + + {i18n.DOES_NOT_EXIST_BUTTON} + + } + /> +); diff --git a/x-pack/plugins/cases/public/components/case_view/index.tsx b/x-pack/plugins/cases/public/components/case_view/index.tsx index 8c2b79a9efec77..a71c2c84691bce 100644 --- a/x-pack/plugins/cases/public/components/case_view/index.tsx +++ b/x-pack/plugins/cases/public/components/case_view/index.tsx @@ -42,6 +42,7 @@ import { CasesTimelineIntegration, CasesTimelineIntegrationProvider } from '../t import { useTimelineContext } from '../timeline_context/use_timeline_context'; import { CasesNavigation } from '../links'; import { OwnerProvider } from '../owner_context'; +import { DoesNotExist } from './does_not_exist'; const gutterTimeline = '70px'; // seems to be a timeline reference from the original file export interface CaseViewComponentProps { @@ -499,7 +500,7 @@ export const CaseView = React.memo( }: CaseViewProps) => { const { data, isLoading, isError, fetchCase, updateCase } = useGetCase(caseId, subCaseId); if (isError) { - return null; + return ; } if (isLoading) { return ( diff --git a/x-pack/plugins/cases/public/components/case_view/translations.ts b/x-pack/plugins/cases/public/components/case_view/translations.ts index 41ffbbd9342dad..3d4558ac3d4a00 100644 --- a/x-pack/plugins/cases/public/components/case_view/translations.ts +++ b/x-pack/plugins/cases/public/components/case_view/translations.ts @@ -128,3 +128,20 @@ export const CHANGED_CONNECTOR_FIELD = i18n.translate('xpack.cases.caseView.fiel export const SYNC_ALERTS = i18n.translate('xpack.cases.caseView.syncAlertsLabel', { defaultMessage: `Sync alerts`, }); + +export const DOES_NOT_EXIST_TITLE = i18n.translate('xpack.cases.caseView.doesNotExist.title', { + defaultMessage: 'This case does not exist', +}); + +export const DOES_NOT_EXIST_DESCRIPTION = (caseId: string) => + i18n.translate('xpack.cases.caseView.doesNotExist.description', { + values: { + caseId, + }, + defaultMessage: + 'A case with id {caseId} could not be found. This likely means the case has been deleted, or the id is incorrect.', + }); + +export const DOES_NOT_EXIST_BUTTON = i18n.translate('xpack.cases.caseView.doesNotExist.button', { + defaultMessage: 'Back to Cases', +}); From 9266b1734cd45b64e614f6fef538300be9113814 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 2 Jun 2021 13:32:38 -0600 Subject: [PATCH 093/113] fix tests --- .../case_action_bar/actions.test.tsx | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/cases/public/components/case_action_bar/actions.test.tsx b/x-pack/plugins/cases/public/components/case_action_bar/actions.test.tsx index 886e740d564470..ac64f9878b5532 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/actions.test.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/actions.test.tsx @@ -49,7 +49,14 @@ describe('CaseView actions', () => { it('clicking trash toggles modal', () => { const wrapper = mount( - + ); @@ -67,7 +74,14 @@ describe('CaseView actions', () => { })); const wrapper = mount( - + ); @@ -82,6 +96,10 @@ describe('CaseView actions', () => { const wrapper = mount( Date: Wed, 2 Jun 2021 13:33:46 -0600 Subject: [PATCH 094/113] fix types --- .../cases/public/components/case_action_bar/index.test.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/x-pack/plugins/cases/public/components/case_action_bar/index.test.tsx b/x-pack/plugins/cases/public/components/case_action_bar/index.test.tsx index 4af8b9f0effb38..33d9a7433f0fee 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/index.test.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/index.test.tsx @@ -16,6 +16,10 @@ describe('CaseActionBar', () => { const onRefresh = jest.fn(); const onUpdateField = jest.fn(); const defaultProps = { + allCasesNavigation: { + href: 'all-cases-href', + onClick: jest.fn(), + }, caseData: basicCase, isAlerting: true, isLoading: false, From 8f2cb30574f4ddd2e02e6ad4064f39f3d702d350 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 3 Jun 2021 07:18:31 -0600 Subject: [PATCH 095/113] fix jest --- x-pack/plugins/cases/public/containers/api.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/cases/public/containers/api.test.tsx b/x-pack/plugins/cases/public/containers/api.test.tsx index 1ac3acf00c4e14..fa6c6cffb45f37 100644 --- a/x-pack/plugins/cases/public/containers/api.test.tsx +++ b/x-pack/plugins/cases/public/containers/api.test.tsx @@ -161,7 +161,7 @@ describe('Case Configuration API', () => { query: { ...DEFAULT_QUERY_PARAMS, reporters, - tags: ['"coke"', '"pepsi"'], + tags: ['coke', 'pepsi'], search: 'hello', status: CaseStatuses.open, owner: [SECURITY_SOLUTION_OWNER], From d59dbad1c7b6c4cfe87ba60449e7a29e6f45f478 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Thu, 3 Jun 2021 09:40:55 -0400 Subject: [PATCH 096/113] renaming to unsecuredSavedObjectsClient (#101215) --- .../cases/server/client/attachments/add.ts | 41 ++-- .../cases/server/client/attachments/delete.ts | 16 +- .../cases/server/client/attachments/get.ts | 16 +- .../cases/server/client/attachments/update.ts | 22 +- .../cases/server/client/cases/create.ts | 6 +- .../cases/server/client/cases/delete.ts | 29 +-- .../plugins/cases/server/client/cases/find.ts | 4 +- .../plugins/cases/server/client/cases/get.ts | 23 ++- .../plugins/cases/server/client/cases/push.ts | 12 +- .../cases/server/client/cases/update.ts | 40 ++-- .../cases/server/client/configure/client.ts | 18 +- .../client/configure/create_mappings.ts | 2 +- .../server/client/configure/get_mappings.ts | 2 +- .../client/configure/update_mappings.ts | 2 +- .../cases/server/client/stats/client.ts | 4 +- .../cases/server/client/sub_cases/client.ts | 24 +-- .../cases/server/client/sub_cases/update.ts | 32 +-- .../cases/server/client/user_actions/get.ts | 2 +- .../server/common/models/commentable_case.ts | 24 +-- .../server/services/attachments/index.ts | 38 ++-- .../cases/server/services/cases/index.ts | 194 ++++++++++-------- .../cases/server/services/configure/index.ts | 26 ++- .../services/connector_mappings/index.ts | 18 +- x-pack/plugins/cases/server/services/index.ts | 2 +- .../server/services/user_actions/index.ts | 10 +- 25 files changed, 335 insertions(+), 272 deletions(-) diff --git a/x-pack/plugins/cases/server/client/attachments/add.ts b/x-pack/plugins/cases/server/client/attachments/add.ts index a334e4bb6e5e70..b453e1feb5d632 100644 --- a/x-pack/plugins/cases/server/client/attachments/add.ts +++ b/x-pack/plugins/cases/server/client/attachments/add.ts @@ -54,23 +54,26 @@ import { Operations } from '../../authorization'; async function getSubCase({ caseService, - savedObjectsClient, + unsecuredSavedObjectsClient, caseId, createdAt, userActionService, user, }: { caseService: CasesService; - savedObjectsClient: SavedObjectsClientContract; + unsecuredSavedObjectsClient: SavedObjectsClientContract; caseId: string; createdAt: string; userActionService: CaseUserActionService; user: User; }): Promise> { - const mostRecentSubCase = await caseService.getMostRecentSubCase(savedObjectsClient, caseId); + const mostRecentSubCase = await caseService.getMostRecentSubCase( + unsecuredSavedObjectsClient, + caseId + ); if (mostRecentSubCase && mostRecentSubCase.attributes.status !== CaseStatuses.closed) { const subCaseAlertsAttachement = await caseService.getAllSubCaseComments({ - soClient: savedObjectsClient, + unsecuredSavedObjectsClient, id: mostRecentSubCase.id, options: { fields: [], @@ -89,13 +92,13 @@ async function getSubCase({ } const newSubCase = await caseService.createSubCase({ - soClient: savedObjectsClient, + unsecuredSavedObjectsClient, createdAt, caseId, createdBy: user, }); await userActionService.bulkCreate({ - soClient: savedObjectsClient, + unsecuredSavedObjectsClient, actions: [ buildCaseUserActionItem({ action: 'create', @@ -152,7 +155,7 @@ const addGeneratedAlerts = async ( }); const caseInfo = await caseService.getCase({ - soClient: unsecuredSavedObjectsClient, + unsecuredSavedObjectsClient, id: caseId, }); @@ -171,7 +174,7 @@ const addGeneratedAlerts = async ( const subCase = await getSubCase({ caseService, - savedObjectsClient: unsecuredSavedObjectsClient, + unsecuredSavedObjectsClient, caseId, createdAt: createdDate, userActionService, @@ -182,7 +185,7 @@ const addGeneratedAlerts = async ( logger, collection: caseInfo, subCase, - soClient: unsecuredSavedObjectsClient, + unsecuredSavedObjectsClient, caseService, attachmentService, }); @@ -212,7 +215,7 @@ const addGeneratedAlerts = async ( } await userActionService.bulkCreate({ - soClient: unsecuredSavedObjectsClient, + unsecuredSavedObjectsClient, actions: [ buildCommentUserActionItem({ action: 'create', @@ -241,25 +244,25 @@ const addGeneratedAlerts = async ( async function getCombinedCase({ caseService, attachmentService, - soClient, + unsecuredSavedObjectsClient, id, logger, }: { caseService: CasesService; attachmentService: AttachmentService; - soClient: SavedObjectsClientContract; + unsecuredSavedObjectsClient: SavedObjectsClientContract; id: string; logger: Logger; }): Promise { const [casePromise, subCasePromise] = await Promise.allSettled([ caseService.getCase({ - soClient, + unsecuredSavedObjectsClient, id, }), ...(ENABLE_CASE_CONNECTOR ? [ caseService.getSubCase({ - soClient, + unsecuredSavedObjectsClient, id, }), ] @@ -269,7 +272,7 @@ async function getCombinedCase({ if (subCasePromise.status === 'fulfilled') { if (subCasePromise.value.references.length > 0) { const caseValue = await caseService.getCase({ - soClient, + unsecuredSavedObjectsClient, id: subCasePromise.value.references[0].id, }); return new CommentableCase({ @@ -278,7 +281,7 @@ async function getCombinedCase({ subCase: subCasePromise.value, caseService, attachmentService, - soClient, + unsecuredSavedObjectsClient, }); } else { throw Boom.badRequest('Sub case found without reference to collection'); @@ -293,7 +296,7 @@ async function getCombinedCase({ collection: casePromise.value, caseService, attachmentService, - soClient, + unsecuredSavedObjectsClient, }); } } @@ -366,7 +369,7 @@ export const addComment = async ( const combinedCase = await getCombinedCase({ caseService, attachmentService, - soClient: unsecuredSavedObjectsClient, + unsecuredSavedObjectsClient, id: caseId, logger, }); @@ -398,7 +401,7 @@ export const addComment = async ( } await userActionService.bulkCreate({ - soClient: unsecuredSavedObjectsClient, + unsecuredSavedObjectsClient, actions: [ buildCommentUserActionItem({ action: 'create', diff --git a/x-pack/plugins/cases/server/client/attachments/delete.ts b/x-pack/plugins/cases/server/client/attachments/delete.ts index 359c7a0672275f..d9a2b00ec50ae7 100644 --- a/x-pack/plugins/cases/server/client/attachments/delete.ts +++ b/x-pack/plugins/cases/server/client/attachments/delete.ts @@ -59,7 +59,7 @@ export async function deleteAll( ): Promise { const { user, - unsecuredSavedObjectsClient: soClient, + unsecuredSavedObjectsClient, caseService, attachmentService, userActionService, @@ -73,7 +73,7 @@ export async function deleteAll( const id = subCaseID ?? caseID; const comments = await caseService.getCommentsByAssociation({ - soClient, + unsecuredSavedObjectsClient, id, associationType: subCaseID ? AssociationType.subCase : AssociationType.case, }); @@ -93,7 +93,7 @@ export async function deleteAll( await Promise.all( comments.saved_objects.map((comment) => attachmentService.delete({ - soClient, + unsecuredSavedObjectsClient, attachmentId: comment.id, }) ) @@ -102,7 +102,7 @@ export async function deleteAll( const deleteDate = new Date().toISOString(); await userActionService.bulkCreate({ - soClient, + unsecuredSavedObjectsClient, actions: comments.saved_objects.map((comment) => buildCommentUserActionItem({ action: 'delete', @@ -136,7 +136,7 @@ export async function deleteComment( ) { const { user, - unsecuredSavedObjectsClient: soClient, + unsecuredSavedObjectsClient, attachmentService, userActionService, logger, @@ -150,7 +150,7 @@ export async function deleteComment( const deleteDate = new Date().toISOString(); const myComment = await attachmentService.get({ - soClient, + unsecuredSavedObjectsClient, attachmentId: attachmentID, }); @@ -175,12 +175,12 @@ export async function deleteComment( } await attachmentService.delete({ - soClient, + unsecuredSavedObjectsClient, attachmentId: attachmentID, }); await userActionService.bulkCreate({ - soClient, + unsecuredSavedObjectsClient, actions: [ buildCommentUserActionItem({ action: 'delete', diff --git a/x-pack/plugins/cases/server/client/attachments/get.ts b/x-pack/plugins/cases/server/client/attachments/get.ts index 6bd0383c508da0..9d85a90324a6c4 100644 --- a/x-pack/plugins/cases/server/client/attachments/get.ts +++ b/x-pack/plugins/cases/server/client/attachments/get.ts @@ -91,7 +91,7 @@ export async function find( clientArgs: CasesClientArgs ): Promise { const { - unsecuredSavedObjectsClient: soClient, + unsecuredSavedObjectsClient, caseService, logger, authorization, @@ -124,7 +124,7 @@ export async function find( const args = queryParams ? { caseService, - soClient, + unsecuredSavedObjectsClient, id, options: { // We need this because the default behavior of getAllCaseComments is to return all the comments @@ -141,7 +141,7 @@ export async function find( } : { caseService, - soClient, + unsecuredSavedObjectsClient, id, options: { page: defaultPage, @@ -184,7 +184,7 @@ export async function get( ): Promise { const { attachmentService, - unsecuredSavedObjectsClient: soClient, + unsecuredSavedObjectsClient, logger, authorization, auditLogger, @@ -192,7 +192,7 @@ export async function get( try { const comment = await attachmentService.get({ - soClient, + unsecuredSavedObjectsClient, attachmentId: attachmentID, }); @@ -225,7 +225,7 @@ export async function getAll( clientArgs: CasesClientArgs ): Promise { const { - unsecuredSavedObjectsClient: soClient, + unsecuredSavedObjectsClient, caseService, logger, authorization, @@ -256,7 +256,7 @@ export async function getAll( if (subCaseID) { comments = await caseService.getAllSubCaseComments({ - soClient, + unsecuredSavedObjectsClient, id: subCaseID, options: { filter, @@ -265,7 +265,7 @@ export async function getAll( }); } else { comments = await caseService.getAllCaseComments({ - soClient, + unsecuredSavedObjectsClient, id: caseID, includeSubCaseComments, options: { diff --git a/x-pack/plugins/cases/server/client/attachments/update.ts b/x-pack/plugins/cases/server/client/attachments/update.ts index 5f07aa25fb3849..3310f9e8f6aa6d 100644 --- a/x-pack/plugins/cases/server/client/attachments/update.ts +++ b/x-pack/plugins/cases/server/client/attachments/update.ts @@ -40,7 +40,7 @@ export interface UpdateArgs { interface CombinedCaseParams { attachmentService: AttachmentService; caseService: CasesService; - soClient: SavedObjectsClientContract; + unsecuredSavedObjectsClient: SavedObjectsClientContract; caseID: string; logger: Logger; subCaseId?: string; @@ -49,7 +49,7 @@ interface CombinedCaseParams { async function getCommentableCase({ attachmentService, caseService, - soClient, + unsecuredSavedObjectsClient, caseID, subCaseId, logger, @@ -57,11 +57,11 @@ async function getCommentableCase({ if (subCaseId) { const [caseInfo, subCase] = await Promise.all([ caseService.getCase({ - soClient, + unsecuredSavedObjectsClient, id: caseID, }), caseService.getSubCase({ - soClient, + unsecuredSavedObjectsClient, id: subCaseId, }), ]); @@ -70,19 +70,19 @@ async function getCommentableCase({ caseService, collection: caseInfo, subCase, - soClient, + unsecuredSavedObjectsClient, logger, }); } else { const caseInfo = await caseService.getCase({ - soClient, + unsecuredSavedObjectsClient, id: caseID, }); return new CommentableCase({ attachmentService, caseService, collection: caseInfo, - soClient, + unsecuredSavedObjectsClient, logger, }); } @@ -100,7 +100,7 @@ export async function update( const { attachmentService, caseService, - unsecuredSavedObjectsClient: soClient, + unsecuredSavedObjectsClient, logger, user, userActionService, @@ -122,14 +122,14 @@ export async function update( const commentableCase = await getCommentableCase({ attachmentService, caseService, - soClient, + unsecuredSavedObjectsClient, caseID, subCaseId: subCaseID, logger, }); const myComment = await attachmentService.get({ - soClient, + unsecuredSavedObjectsClient, attachmentId: queryCommentId, }); @@ -179,7 +179,7 @@ export async function update( }); await userActionService.bulkCreate({ - soClient, + unsecuredSavedObjectsClient, actions: [ buildCommentUserActionItem({ action: 'update', diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index b0b1a38c0cd636..e1edcfdda0423e 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -88,12 +88,12 @@ export const create = async ( const { username, full_name, email } = user; const createdDate = new Date().toISOString(); const myCaseConfigure = await caseConfigureService.find({ - soClient: unsecuredSavedObjectsClient, + unsecuredSavedObjectsClient, }); const caseConfigureConnector = getConnectorFromConfiguration(myCaseConfigure); const newCase = await caseService.postNewCase({ - soClient: unsecuredSavedObjectsClient, + unsecuredSavedObjectsClient, attributes: transformNewCase({ createdDate, newCase: query, @@ -106,7 +106,7 @@ export const create = async ( }); await userActionService.bulkCreate({ - soClient: unsecuredSavedObjectsClient, + unsecuredSavedObjectsClient, actions: [ buildCaseUserActionItem({ action: 'create', diff --git a/x-pack/plugins/cases/server/client/cases/delete.ts b/x-pack/plugins/cases/server/client/cases/delete.ts index 597b8ad0fca00a..8ad48bde7f9711 100644 --- a/x-pack/plugins/cases/server/client/cases/delete.ts +++ b/x-pack/plugins/cases/server/client/cases/delete.ts @@ -19,19 +19,22 @@ import { OWNER_FIELD } from '../../../common/api'; async function deleteSubCases({ attachmentService, caseService, - soClient, + unsecuredSavedObjectsClient, caseIds, }: { attachmentService: AttachmentService; caseService: CasesService; - soClient: SavedObjectsClientContract; + unsecuredSavedObjectsClient: SavedObjectsClientContract; caseIds: string[]; }) { - const subCasesForCaseIds = await caseService.findSubCasesByCaseId({ soClient, ids: caseIds }); + const subCasesForCaseIds = await caseService.findSubCasesByCaseId({ + unsecuredSavedObjectsClient, + ids: caseIds, + }); const subCaseIDs = subCasesForCaseIds.saved_objects.map((subCase) => subCase.id); const commentsForSubCases = await caseService.getAllSubCaseComments({ - soClient, + unsecuredSavedObjectsClient, id: subCaseIDs, }); @@ -39,13 +42,13 @@ async function deleteSubCases({ // per case ID await Promise.all( commentsForSubCases.saved_objects.map((commentSO) => - attachmentService.delete({ soClient, attachmentId: commentSO.id }) + attachmentService.delete({ unsecuredSavedObjectsClient, attachmentId: commentSO.id }) ) ); await Promise.all( subCasesForCaseIds.saved_objects.map((subCaseSO) => - caseService.deleteSubCase(soClient, subCaseSO.id) + caseService.deleteSubCase(unsecuredSavedObjectsClient, subCaseSO.id) ) ); } @@ -57,7 +60,7 @@ async function deleteSubCases({ */ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): Promise { const { - unsecuredSavedObjectsClient: soClient, + unsecuredSavedObjectsClient, caseService, attachmentService, user, @@ -67,7 +70,7 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P auditLogger, } = clientArgs; try { - const cases = await caseService.getCases({ soClient, caseIds: ids }); + const cases = await caseService.getCases({ unsecuredSavedObjectsClient, caseIds: ids }); const soIds = new Set(); const owners = new Set(); @@ -96,7 +99,7 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P await Promise.all( ids.map((id) => caseService.deleteCase({ - soClient, + unsecuredSavedObjectsClient, id, }) ) @@ -105,7 +108,7 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P const comments = await Promise.all( ids.map((id) => caseService.getAllCaseComments({ - soClient, + unsecuredSavedObjectsClient, id, }) ) @@ -117,7 +120,7 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P Promise.all( c.saved_objects.map(({ id }) => attachmentService.delete({ - soClient, + unsecuredSavedObjectsClient, attachmentId: id, }) ) @@ -130,7 +133,7 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P await deleteSubCases({ attachmentService, caseService, - soClient, + unsecuredSavedObjectsClient, caseIds: ids, }); } @@ -138,7 +141,7 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P const deleteDate = new Date().toISOString(); await userActionService.bulkCreate({ - soClient, + unsecuredSavedObjectsClient, actions: cases.saved_objects.map((caseInfo) => buildCaseUserActionItem({ action: 'delete', diff --git a/x-pack/plugins/cases/server/client/cases/find.ts b/x-pack/plugins/cases/server/client/cases/find.ts index 8c007d1a1a9111..633261100ddeaa 100644 --- a/x-pack/plugins/cases/server/client/cases/find.ts +++ b/x-pack/plugins/cases/server/client/cases/find.ts @@ -71,7 +71,7 @@ export const find = async ( const caseQueries = constructQueryOptions({ ...queryArgs, authorizationFilter }); const cases = await caseService.findCasesGroupedByID({ - soClient: unsecuredSavedObjectsClient, + unsecuredSavedObjectsClient, caseOptions: { ...queryParams, ...caseQueries.case, @@ -92,7 +92,7 @@ export const find = async ( ...caseStatuses.map((status) => { const statusQuery = constructQueryOptions({ ...queryArgs, status, authorizationFilter }); return caseService.findCaseStatusStats({ - soClient: unsecuredSavedObjectsClient, + unsecuredSavedObjectsClient, caseOptions: statusQuery.case, subCaseOptions: statusQuery.subCase, ensureSavedObjectsAreAuthorized, diff --git a/x-pack/plugins/cases/server/client/cases/get.ts b/x-pack/plugins/cases/server/client/cases/get.ts index 0dadc128b3ceb4..cf6d12ceae0a0c 100644 --- a/x-pack/plugins/cases/server/client/cases/get.ts +++ b/x-pack/plugins/cases/server/client/cases/get.ts @@ -62,7 +62,7 @@ export const getCaseIDsByAlertID = async ( clientArgs: CasesClientArgs ): Promise => { const { - unsecuredSavedObjectsClient: savedObjectsClient, + unsecuredSavedObjectsClient, caseService, logger, authorization, @@ -92,7 +92,7 @@ export const getCaseIDsByAlertID = async ( ); const commentsWithAlert = await caseService.getCaseIdsByAlertId({ - soClient: savedObjectsClient, + unsecuredSavedObjectsClient, alertId: alertID, filter, }); @@ -166,17 +166,20 @@ export const get = async ( if (ENABLE_CASE_CONNECTOR) { const [caseInfo, subCasesForCaseId] = await Promise.all([ caseService.getCase({ - soClient: unsecuredSavedObjectsClient, + unsecuredSavedObjectsClient, id, }), - caseService.findSubCasesByCaseId({ soClient: unsecuredSavedObjectsClient, ids: [id] }), + caseService.findSubCasesByCaseId({ + unsecuredSavedObjectsClient, + ids: [id], + }), ]); theCase = caseInfo; subCaseIds = subCasesForCaseId.saved_objects.map((so) => so.id); } else { theCase = await caseService.getCase({ - soClient: unsecuredSavedObjectsClient, + unsecuredSavedObjectsClient, id, }); } @@ -199,7 +202,7 @@ export const get = async ( } const theComments = await caseService.getAllCaseComments({ - soClient: unsecuredSavedObjectsClient, + unsecuredSavedObjectsClient, id, options: { sortField: 'created_at', @@ -231,7 +234,7 @@ export async function getTags( clientArgs: CasesClientArgs ): Promise { const { - unsecuredSavedObjectsClient: soClient, + unsecuredSavedObjectsClient, caseService, logger, authorization: auth, @@ -257,7 +260,7 @@ export async function getTags( const filter = combineAuthorizedAndOwnerFilter(queryParams.owner, authorizationFilter); const cases = await caseService.getTags({ - soClient, + unsecuredSavedObjectsClient, filter, }); @@ -293,7 +296,7 @@ export async function getReporters( clientArgs: CasesClientArgs ): Promise { const { - unsecuredSavedObjectsClient: soClient, + unsecuredSavedObjectsClient, caseService, logger, authorization: auth, @@ -319,7 +322,7 @@ export async function getReporters( const filter = combineAuthorizedAndOwnerFilter(queryParams.owner, authorizationFilter); const cases = await caseService.getReporters({ - soClient, + unsecuredSavedObjectsClient, filter, }); diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index af395e9d8768a9..74d3fb1373fd75 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -151,12 +151,12 @@ export const push = async ( /* Start of update case with push information */ const [myCase, myCaseConfigure, comments] = await Promise.all([ caseService.getCase({ - soClient: unsecuredSavedObjectsClient, + unsecuredSavedObjectsClient, id: caseId, }), - caseConfigureService.find({ soClient: unsecuredSavedObjectsClient }), + caseConfigureService.find({ unsecuredSavedObjectsClient }), caseService.getAllCaseComments({ - soClient: unsecuredSavedObjectsClient, + unsecuredSavedObjectsClient, id: caseId, options: { fields: [], @@ -186,7 +186,7 @@ export const push = async ( const [updatedCase, updatedComments] = await Promise.all([ caseService.patchCase({ - soClient: unsecuredSavedObjectsClient, + unsecuredSavedObjectsClient, caseId, updatedAttributes: { ...(shouldMarkAsClosed @@ -204,7 +204,7 @@ export const push = async ( }), attachmentService.bulkUpdate({ - soClient: unsecuredSavedObjectsClient, + unsecuredSavedObjectsClient, comments: comments.saved_objects .filter((comment) => comment.attributes.pushed_at == null) .map((comment) => ({ @@ -218,7 +218,7 @@ export const push = async ( }), userActionService.bulkCreate({ - soClient: unsecuredSavedObjectsClient, + unsecuredSavedObjectsClient, actions: [ ...(shouldMarkAsClosed ? [ diff --git a/x-pack/plugins/cases/server/client/cases/update.ts b/x-pack/plugins/cases/server/client/cases/update.ts index 1cda4863ffe410..1dabca40146f8d 100644 --- a/x-pack/plugins/cases/server/client/cases/update.ts +++ b/x-pack/plugins/cases/server/client/cases/update.ts @@ -134,15 +134,15 @@ function throwIfUpdateOwner(requests: ESCasePatchRequest[]) { async function throwIfInvalidUpdateOfTypeWithAlerts({ requests, caseService, - soClient, + unsecuredSavedObjectsClient, }: { requests: ESCasePatchRequest[]; caseService: CasesService; - soClient: SavedObjectsClientContract; + unsecuredSavedObjectsClient: SavedObjectsClientContract; }) { const getAlertsForID = async (caseToUpdate: ESCasePatchRequest) => { const alerts = await caseService.getAllCaseComments({ - soClient, + unsecuredSavedObjectsClient, id: caseToUpdate.id, options: { fields: [], @@ -196,17 +196,17 @@ function getID( async function getAlertComments({ casesToSync, caseService, - soClient, + unsecuredSavedObjectsClient, }: { casesToSync: ESCasePatchRequest[]; caseService: CasesService; - soClient: SavedObjectsClientContract; + unsecuredSavedObjectsClient: SavedObjectsClientContract; }): Promise> { const idsOfCasesToSync = casesToSync.map((casePatchReq) => casePatchReq.id); // getAllCaseComments will by default get all the comments, unless page or perPage fields are set return caseService.getAllCaseComments({ - soClient, + unsecuredSavedObjectsClient, id: idsOfCasesToSync, includeSubCaseComments: true, options: { @@ -225,11 +225,11 @@ async function getAlertComments({ async function getSubCasesToStatus({ totalAlerts, caseService, - soClient, + unsecuredSavedObjectsClient, }: { totalAlerts: SavedObjectsFindResponse; caseService: CasesService; - soClient: SavedObjectsClientContract; + unsecuredSavedObjectsClient: SavedObjectsClientContract; }): Promise> { const subCasesToRetrieve = totalAlerts.saved_objects.reduce((acc, alertComment) => { if ( @@ -246,7 +246,7 @@ async function getSubCasesToStatus({ const subCases = await caseService.getSubCases({ ids: Array.from(subCasesToRetrieve.values()), - soClient, + unsecuredSavedObjectsClient, }); return subCases.saved_objects.reduce((acc, subCase) => { @@ -292,14 +292,14 @@ async function updateAlerts({ casesWithStatusChangedAndSynced, casesMap, caseService, - soClient, + unsecuredSavedObjectsClient, casesClientInternal, }: { casesWithSyncSettingChangedToOn: ESCasePatchRequest[]; casesWithStatusChangedAndSynced: ESCasePatchRequest[]; casesMap: Map>; caseService: CasesService; - soClient: SavedObjectsClientContract; + unsecuredSavedObjectsClient: SavedObjectsClientContract; casesClientInternal: CasesClientInternal; }) { /** @@ -324,11 +324,15 @@ async function updateAlerts({ const totalAlerts = await getAlertComments({ casesToSync, caseService, - soClient, + unsecuredSavedObjectsClient, }); // get a map of sub case id to the sub case status - const subCasesToStatus = await getSubCasesToStatus({ totalAlerts, soClient, caseService }); + const subCasesToStatus = await getSubCasesToStatus({ + totalAlerts, + unsecuredSavedObjectsClient, + caseService, + }); // create an array of requests that indicate the id, index, and status to update an alert const alertsToUpdate = totalAlerts.saved_objects.reduce( @@ -411,7 +415,7 @@ export const update = async ( try { const myCases = await caseService.getCases({ - soClient: unsecuredSavedObjectsClient, + unsecuredSavedObjectsClient, caseIds: query.cases.map((q) => q.id), }); @@ -481,14 +485,14 @@ export const update = async ( await throwIfInvalidUpdateOfTypeWithAlerts({ requests: updateFilterCases, caseService, - soClient: unsecuredSavedObjectsClient, + unsecuredSavedObjectsClient, }); // eslint-disable-next-line @typescript-eslint/naming-convention const { username, full_name, email } = user; const updatedDt = new Date().toISOString(); const updatedCases = await caseService.patchCases({ - soClient: unsecuredSavedObjectsClient, + unsecuredSavedObjectsClient, cases: updateFilterCases.map((thisCase) => { // intentionally removing owner from the case so that we don't accidentally allow it to be updated const { id: caseId, version, owner, ...updateCaseAttributes } = thisCase; @@ -550,7 +554,7 @@ export const update = async ( casesWithStatusChangedAndSynced, casesWithSyncSettingChangedToOn, caseService, - soClient: unsecuredSavedObjectsClient, + unsecuredSavedObjectsClient, casesClientInternal, casesMap, }); @@ -573,7 +577,7 @@ export const update = async ( }); await userActionService.bulkCreate({ - soClient: unsecuredSavedObjectsClient, + unsecuredSavedObjectsClient, actions: buildCaseUserActions({ originalCases: myCases.saved_objects, updatedCases: updatedCases.saved_objects, diff --git a/x-pack/plugins/cases/server/client/configure/client.ts b/x-pack/plugins/cases/server/client/configure/client.ts index 65e89f9d819b21..e0bf8c7d82308c 100644 --- a/x-pack/plugins/cases/server/client/configure/client.ts +++ b/x-pack/plugins/cases/server/client/configure/client.ts @@ -149,7 +149,7 @@ async function get( casesClientInternal: CasesClientInternal ): Promise { const { - unsecuredSavedObjectsClient: soClient, + unsecuredSavedObjectsClient, caseConfigureService, logger, authorization, @@ -179,7 +179,7 @@ async function get( let error: string | null = null; const myCaseConfigure = await caseConfigureService.find({ - soClient, + unsecuredSavedObjectsClient, options: { filter }, }); @@ -264,7 +264,7 @@ async function update( const { caseConfigureService, logger, - unsecuredSavedObjectsClient: soClient, + unsecuredSavedObjectsClient, user, authorization, auditLogger, @@ -291,7 +291,7 @@ async function update( ); const configuration = await caseConfigureService.get({ - soClient, + unsecuredSavedObjectsClient, configurationId, }); @@ -345,7 +345,7 @@ async function update( } const patch = await caseConfigureService.patch({ - soClient, + unsecuredSavedObjectsClient, configurationId: configuration.id, updatedAttributes: { ...queryWithoutVersionAndConnector, @@ -381,7 +381,7 @@ async function create( casesClientInternal: CasesClientInternal ): Promise { const { - unsecuredSavedObjectsClient: soClient, + unsecuredSavedObjectsClient, caseConfigureService, logger, user, @@ -413,7 +413,7 @@ async function create( ); const myCaseConfigure = await caseConfigureService.find({ - soClient, + unsecuredSavedObjectsClient, options: { filter }, }); @@ -429,7 +429,7 @@ async function create( if (myCaseConfigure.saved_objects.length > 0) { await Promise.all( myCaseConfigure.saved_objects.map((cc) => - caseConfigureService.delete({ soClient, configurationId: cc.id }) + caseConfigureService.delete({ unsecuredSavedObjectsClient, configurationId: cc.id }) ) ); } @@ -460,7 +460,7 @@ async function create( } const post = await caseConfigureService.post({ - soClient, + unsecuredSavedObjectsClient, attributes: { ...configuration, connector: transformCaseConnectorToEsConnector(configuration.connector), diff --git a/x-pack/plugins/cases/server/client/configure/create_mappings.ts b/x-pack/plugins/cases/server/client/configure/create_mappings.ts index bdd4b31377ee06..b01f10d7a9e43b 100644 --- a/x-pack/plugins/cases/server/client/configure/create_mappings.ts +++ b/x-pack/plugins/cases/server/client/configure/create_mappings.ts @@ -29,7 +29,7 @@ export const createMappings = async ( }); const theMapping = await connectorMappingsService.post({ - soClient: unsecuredSavedObjectsClient, + unsecuredSavedObjectsClient, attributes: { mappings: res.defaultMappings, owner, 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 f00a62c8cd039a..3489c06b1da5ad 100644 --- a/x-pack/plugins/cases/server/client/configure/get_mappings.ts +++ b/x-pack/plugins/cases/server/client/configure/get_mappings.ts @@ -24,7 +24,7 @@ export const getMappings = async ( } const myConnectorMappings = await connectorMappingsService.find({ - soClient: unsecuredSavedObjectsClient, + unsecuredSavedObjectsClient, options: { hasReference: { type: ACTION_SAVED_OBJECT_TYPE, diff --git a/x-pack/plugins/cases/server/client/configure/update_mappings.ts b/x-pack/plugins/cases/server/client/configure/update_mappings.ts index ddac074c432719..7eccf4cbbe5829 100644 --- a/x-pack/plugins/cases/server/client/configure/update_mappings.ts +++ b/x-pack/plugins/cases/server/client/configure/update_mappings.ts @@ -29,7 +29,7 @@ export const updateMappings = async ( }); const theMapping = await connectorMappingsService.update({ - soClient: unsecuredSavedObjectsClient, + unsecuredSavedObjectsClient, mappingId, attributes: { mappings: res.defaultMappings, diff --git a/x-pack/plugins/cases/server/client/stats/client.ts b/x-pack/plugins/cases/server/client/stats/client.ts index 4cd8823883c4bf..9816bfe1fd7cff 100644 --- a/x-pack/plugins/cases/server/client/stats/client.ts +++ b/x-pack/plugins/cases/server/client/stats/client.ts @@ -51,7 +51,7 @@ async function getStatusTotalsByType( clientArgs: CasesClientArgs ): Promise { const { - unsecuredSavedObjectsClient: soClient, + unsecuredSavedObjectsClient, caseService, logger, authorization, @@ -82,7 +82,7 @@ async function getStatusTotalsByType( authorizationFilter, }); return caseService.findCaseStatusStats({ - soClient, + unsecuredSavedObjectsClient, caseOptions: statusQuery.case, subCaseOptions: statusQuery.subCase, ensureSavedObjectsAreAuthorized, diff --git a/x-pack/plugins/cases/server/client/sub_cases/client.ts b/x-pack/plugins/cases/server/client/sub_cases/client.ts index 4552d4042012e4..b35d58ce060108 100644 --- a/x-pack/plugins/cases/server/client/sub_cases/client.ts +++ b/x-pack/plugins/cases/server/client/sub_cases/client.ts @@ -93,7 +93,7 @@ export function createSubCasesClient( async function deleteSubCase(ids: string[], clientArgs: CasesClientArgs): Promise { try { const { - unsecuredSavedObjectsClient: soClient, + unsecuredSavedObjectsClient, user, userActionService, caseService, @@ -101,8 +101,8 @@ async function deleteSubCase(ids: string[], clientArgs: CasesClientArgs): Promis } = clientArgs; const [comments, subCases] = await Promise.all([ - caseService.getAllSubCaseComments({ soClient, id: ids }), - caseService.getSubCases({ soClient, ids }), + caseService.getAllSubCaseComments({ unsecuredSavedObjectsClient, id: ids }), + caseService.getSubCases({ unsecuredSavedObjectsClient, ids }), ]); const subCaseErrors = subCases.saved_objects.filter((subCase) => subCase.error !== undefined); @@ -123,16 +123,16 @@ async function deleteSubCase(ids: string[], clientArgs: CasesClientArgs): Promis await Promise.all( comments.saved_objects.map((comment) => - attachmentService.delete({ soClient, attachmentId: comment.id }) + attachmentService.delete({ unsecuredSavedObjectsClient, attachmentId: comment.id }) ) ); - await Promise.all(ids.map((id) => caseService.deleteSubCase(soClient, id))); + await Promise.all(ids.map((id) => caseService.deleteSubCase(unsecuredSavedObjectsClient, id))); const deleteDate = new Date().toISOString(); await userActionService.bulkCreate({ - soClient, + unsecuredSavedObjectsClient, actions: subCases.saved_objects.map((subCase) => buildCaseUserActionItem({ action: 'delete', @@ -161,7 +161,7 @@ async function find( clientArgs: CasesClientArgs ): Promise { try { - const { unsecuredSavedObjectsClient: soClient, caseService } = clientArgs; + const { unsecuredSavedObjectsClient, caseService } = clientArgs; const ids = [caseID]; const { subCase: subCaseQueryOptions } = constructQueryOptions({ @@ -170,7 +170,7 @@ async function find( }); const subCases = await caseService.findSubCasesGroupByCase({ - soClient, + unsecuredSavedObjectsClient, ids, options: { sortField: 'created_at', @@ -188,7 +188,7 @@ async function find( sortByField: queryParams.sortField, }); return caseService.findSubCaseStatusStats({ - soClient, + unsecuredSavedObjectsClient, options: statusQueryOptions ?? {}, ids, }); @@ -220,10 +220,10 @@ async function get( clientArgs: CasesClientArgs ): Promise { try { - const { unsecuredSavedObjectsClient: soClient, caseService } = clientArgs; + const { unsecuredSavedObjectsClient, caseService } = clientArgs; const subCase = await caseService.getSubCase({ - soClient, + unsecuredSavedObjectsClient, id, }); @@ -236,7 +236,7 @@ async function get( } const theComments = await caseService.getAllSubCaseComments({ - soClient, + unsecuredSavedObjectsClient, id, options: { sortField: 'created_at', diff --git a/x-pack/plugins/cases/server/client/sub_cases/update.ts b/x-pack/plugins/cases/server/client/sub_cases/update.ts index 9e64a7b8731b16..b49d36d7a27d4b 100644 --- a/x-pack/plugins/cases/server/client/sub_cases/update.ts +++ b/x-pack/plugins/cases/server/client/sub_cases/update.ts @@ -115,19 +115,19 @@ function getParentIDs({ async function getParentCases({ caseService, - soClient, + unsecuredSavedObjectsClient, subCaseIDs, subCasesMap, }: { caseService: CasesService; - soClient: SavedObjectsClientContract; + unsecuredSavedObjectsClient: SavedObjectsClientContract; subCaseIDs: string[]; subCasesMap: Map>; }): Promise>> { const parentIDInfo = getParentIDs({ subCaseIDs, subCasesMap }); const parentCases = await caseService.getCases({ - soClient, + unsecuredSavedObjectsClient, caseIds: parentIDInfo.ids, }); @@ -182,15 +182,15 @@ function getID(comment: SavedObject): string | undefined { async function getAlertComments({ subCasesToSync, caseService, - soClient, + unsecuredSavedObjectsClient, }: { subCasesToSync: SubCasePatchRequest[]; caseService: CasesService; - soClient: SavedObjectsClientContract; + unsecuredSavedObjectsClient: SavedObjectsClientContract; }): Promise> { const ids = subCasesToSync.map((subCase) => subCase.id); return caseService.getAllSubCaseComments({ - soClient, + unsecuredSavedObjectsClient, id: ids, options: { filter: nodeBuilder.or([ @@ -206,13 +206,13 @@ async function getAlertComments({ */ async function updateAlerts({ caseService, - soClient, + unsecuredSavedObjectsClient, casesClientInternal, logger, subCasesToSync, }: { caseService: CasesService; - soClient: SavedObjectsClientContract; + unsecuredSavedObjectsClient: SavedObjectsClientContract; casesClientInternal: CasesClientInternal; logger: Logger; subCasesToSync: SubCasePatchRequest[]; @@ -223,7 +223,11 @@ async function updateAlerts({ return acc; }, new Map()); // get all the alerts for all sub cases that need to be synced - const totalAlerts = await getAlertComments({ caseService, soClient, subCasesToSync }); + const totalAlerts = await getAlertComments({ + caseService, + unsecuredSavedObjectsClient, + subCasesToSync, + }); // create a map of the status (open, closed, etc) to alert info that needs to be updated const alertsToUpdate = totalAlerts.saved_objects.reduce( (acc: UpdateAlertRequest[], alertComment) => { @@ -274,7 +278,7 @@ export async function update({ const { unsecuredSavedObjectsClient, user, caseService, userActionService } = clientArgs; const bulkSubCases = await caseService.getSubCases({ - soClient: unsecuredSavedObjectsClient, + unsecuredSavedObjectsClient, ids: query.subCases.map((q) => q.id), }); @@ -292,7 +296,7 @@ export async function update({ } const subIDToParentCase = await getParentCases({ - soClient: unsecuredSavedObjectsClient, + unsecuredSavedObjectsClient, caseService, subCaseIDs: nonEmptySubCaseRequests.map((subCase) => subCase.id), subCasesMap, @@ -300,7 +304,7 @@ export async function update({ const updatedAt = new Date().toISOString(); const updatedCases = await caseService.patchSubCases({ - soClient: unsecuredSavedObjectsClient, + unsecuredSavedObjectsClient, subCases: nonEmptySubCaseRequests.map((thisCase) => { const { id: subCaseId, version, ...updateSubCaseAttributes } = thisCase; let closedInfo: { closed_at: string | null; closed_by: User | null } = { @@ -352,7 +356,7 @@ export async function update({ await updateAlerts({ caseService, - soClient: unsecuredSavedObjectsClient, + unsecuredSavedObjectsClient, casesClientInternal, subCasesToSync: subCasesToSyncAlertsFor, logger: clientArgs.logger, @@ -380,7 +384,7 @@ export async function update({ ); await userActionService.bulkCreate({ - soClient: unsecuredSavedObjectsClient, + unsecuredSavedObjectsClient, actions: buildSubCaseUserActions({ originalSubCases: bulkSubCases.saved_objects, updatedSubCases: updatedCases.saved_objects, 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 4fbc4d333133f7..7cc1dc7d27dfe9 100644 --- a/x-pack/plugins/cases/server/client/user_actions/get.ts +++ b/x-pack/plugins/cases/server/client/user_actions/get.ts @@ -34,7 +34,7 @@ export const get = async ( checkEnabledCaseConnectorOrThrow(subCaseId); const userActions = await userActionService.getAll({ - soClient: unsecuredSavedObjectsClient, + unsecuredSavedObjectsClient, caseId, subCaseId, }); diff --git a/x-pack/plugins/cases/server/common/models/commentable_case.ts b/x-pack/plugins/cases/server/common/models/commentable_case.ts index 894e1f9a7f518f..2d1e1e18b50987 100644 --- a/x-pack/plugins/cases/server/common/models/commentable_case.ts +++ b/x-pack/plugins/cases/server/common/models/commentable_case.ts @@ -52,7 +52,7 @@ interface NewCommentResp { interface CommentableCaseParams { collection: SavedObject; subCase?: SavedObject; - soClient: SavedObjectsClientContract; + unsecuredSavedObjectsClient: SavedObjectsClientContract; caseService: CasesService; attachmentService: AttachmentService; logger: Logger; @@ -65,7 +65,7 @@ interface CommentableCaseParams { export class CommentableCase { private readonly collection: SavedObject; private readonly subCase?: SavedObject; - private readonly soClient: SavedObjectsClientContract; + private readonly unsecuredSavedObjectsClient: SavedObjectsClientContract; private readonly caseService: CasesService; private readonly attachmentService: AttachmentService; private readonly logger: Logger; @@ -73,14 +73,14 @@ export class CommentableCase { constructor({ collection, subCase, - soClient, + unsecuredSavedObjectsClient, caseService, attachmentService, logger, }: CommentableCaseParams) { this.collection = collection; this.subCase = subCase; - this.soClient = soClient; + this.unsecuredSavedObjectsClient = unsecuredSavedObjectsClient; this.caseService = caseService; this.attachmentService = attachmentService; this.logger = logger; @@ -144,7 +144,7 @@ export class CommentableCase { if (this.subCase) { const updatedSubCase = await this.caseService.patchSubCase({ - soClient: this.soClient, + unsecuredSavedObjectsClient: this.unsecuredSavedObjectsClient, subCaseId: this.subCase.id, updatedAttributes: { updated_at: date, @@ -166,7 +166,7 @@ export class CommentableCase { } const updatedCase = await this.caseService.patchCase({ - soClient: this.soClient, + unsecuredSavedObjectsClient: this.unsecuredSavedObjectsClient, caseId: this.collection.id, updatedAttributes: { updated_at: date, @@ -186,7 +186,7 @@ export class CommentableCase { version: updatedCase.version ?? this.collection.version, }, subCase: updatedSubCaseAttributes, - soClient: this.soClient, + unsecuredSavedObjectsClient: this.unsecuredSavedObjectsClient, caseService: this.caseService, attachmentService: this.attachmentService, logger: this.logger, @@ -217,7 +217,7 @@ export class CommentableCase { const [comment, commentableCase] = await Promise.all([ this.attachmentService.update({ - soClient: this.soClient, + unsecuredSavedObjectsClient: this.unsecuredSavedObjectsClient, attachmentId: id, updatedAttributes: { ...queryRestAttributes, @@ -272,7 +272,7 @@ export class CommentableCase { const [comment, commentableCase] = await Promise.all([ this.attachmentService.create({ - soClient: this.soClient, + unsecuredSavedObjectsClient: this.unsecuredSavedObjectsClient, attributes: transformNewComment({ associationType: this.subCase ? AssociationType.subCase : AssociationType.case, createdDate, @@ -310,7 +310,7 @@ export class CommentableCase { public async encode(): Promise { try { const collectionCommentStats = await this.caseService.getAllCaseComments({ - soClient: this.soClient, + unsecuredSavedObjectsClient: this.unsecuredSavedObjectsClient, id: this.collection.id, options: { fields: [], @@ -320,7 +320,7 @@ export class CommentableCase { }); const collectionComments = await this.caseService.getAllCaseComments({ - soClient: this.soClient, + unsecuredSavedObjectsClient: this.unsecuredSavedObjectsClient, id: this.collection.id, options: { fields: [], @@ -340,7 +340,7 @@ export class CommentableCase { if (this.subCase) { const subCaseComments = await this.caseService.getAllSubCaseComments({ - soClient: this.soClient, + unsecuredSavedObjectsClient: this.unsecuredSavedObjectsClient, id: this.subCase.id, }); const totalAlerts = diff --git a/x-pack/plugins/cases/server/services/attachments/index.ts b/x-pack/plugins/cases/server/services/attachments/index.ts index 2308e90320c62c..c9b9d11a896899 100644 --- a/x-pack/plugins/cases/server/services/attachments/index.ts +++ b/x-pack/plugins/cases/server/services/attachments/index.ts @@ -40,35 +40,47 @@ export class AttachmentService { constructor(private readonly log: Logger) {} public async get({ - soClient, + unsecuredSavedObjectsClient, attachmentId, }: GetAttachmentArgs): Promise> { try { this.log.debug(`Attempting to GET attachment ${attachmentId}`); - return await soClient.get(CASE_COMMENT_SAVED_OBJECT, attachmentId); + return await unsecuredSavedObjectsClient.get( + CASE_COMMENT_SAVED_OBJECT, + attachmentId + ); } catch (error) { this.log.error(`Error on GET attachment ${attachmentId}: ${error}`); throw error; } } - public async delete({ soClient, attachmentId }: GetAttachmentArgs) { + public async delete({ unsecuredSavedObjectsClient, attachmentId }: GetAttachmentArgs) { try { this.log.debug(`Attempting to GET attachment ${attachmentId}`); - return await soClient.delete(CASE_COMMENT_SAVED_OBJECT, attachmentId); + return await unsecuredSavedObjectsClient.delete(CASE_COMMENT_SAVED_OBJECT, attachmentId); } catch (error) { this.log.error(`Error on GET attachment ${attachmentId}: ${error}`); throw error; } } - public async create({ soClient, attributes, references, id }: CreateAttachmentArgs) { + public async create({ + unsecuredSavedObjectsClient, + attributes, + references, + id, + }: CreateAttachmentArgs) { try { this.log.debug(`Attempting to POST a new comment`); - return await soClient.create(CASE_COMMENT_SAVED_OBJECT, attributes, { - references, - id, - }); + return await unsecuredSavedObjectsClient.create( + CASE_COMMENT_SAVED_OBJECT, + attributes, + { + references, + id, + } + ); } catch (error) { this.log.error(`Error on POST a new comment: ${error}`); throw error; @@ -76,14 +88,14 @@ export class AttachmentService { } public async update({ - soClient, + unsecuredSavedObjectsClient, attachmentId, updatedAttributes, version, }: UpdateAttachmentArgs) { try { this.log.debug(`Attempting to UPDATE comment ${attachmentId}`); - return await soClient.update( + return await unsecuredSavedObjectsClient.update( CASE_COMMENT_SAVED_OBJECT, attachmentId, updatedAttributes, @@ -95,12 +107,12 @@ export class AttachmentService { } } - public async bulkUpdate({ soClient, comments }: BulkUpdateAttachmentArgs) { + public async bulkUpdate({ unsecuredSavedObjectsClient, comments }: BulkUpdateAttachmentArgs) { try { this.log.debug( `Attempting to UPDATE comments ${comments.map((c) => c.attachmentId).join(', ')}` ); - return await soClient.bulkUpdate( + return await unsecuredSavedObjectsClient.bulkUpdate( comments.map((c) => ({ type: CASE_COMMENT_SAVED_OBJECT, id: c.attachmentId, diff --git a/x-pack/plugins/cases/server/services/cases/index.ts b/x-pack/plugins/cases/server/services/cases/index.ts index 38e9881cbdccce..1cd5ded87d76ba 100644 --- a/x-pack/plugins/cases/server/services/cases/index.ts +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -77,20 +77,20 @@ interface GetSubCasesArgs extends ClientArgs { } interface FindCommentsArgs { - soClient: SavedObjectsClientContract; + unsecuredSavedObjectsClient: SavedObjectsClientContract; id: string | string[]; options?: SavedObjectFindOptionsKueryNode; } interface FindCaseCommentsArgs { - soClient: SavedObjectsClientContract; + unsecuredSavedObjectsClient: SavedObjectsClientContract; id: string | string[]; options?: SavedObjectFindOptionsKueryNode; includeSubCaseComments?: boolean; } interface FindSubCaseCommentsArgs { - soClient: SavedObjectsClientContract; + unsecuredSavedObjectsClient: SavedObjectsClientContract; id: string | string[]; options?: SavedObjectFindOptionsKueryNode; } @@ -104,7 +104,7 @@ interface FindSubCasesByIDArgs extends FindCasesArgs { } interface FindSubCasesStatusStats { - soClient: SavedObjectsClientContract; + unsecuredSavedObjectsClient: SavedObjectsClientContract; options: SavedObjectFindOptionsKueryNode; ids: string[]; } @@ -132,15 +132,15 @@ interface PatchCasesArgs extends ClientArgs { } interface PatchSubCase { - soClient: SavedObjectsClientContract; + unsecuredSavedObjectsClient: SavedObjectsClientContract; subCaseId: string; updatedAttributes: Partial; version?: string; } interface PatchSubCases { - soClient: SavedObjectsClientContract; - subCases: Array>; + unsecuredSavedObjectsClient: SavedObjectsClientContract; + subCases: Array>; } interface GetUserArgs { @@ -160,7 +160,7 @@ interface CaseCommentStats { } interface FindCommentsByAssociationArgs { - soClient: SavedObjectsClientContract; + unsecuredSavedObjectsClient: SavedObjectsClientContract; id: string | string[]; associationType: AssociationType; options?: SavedObjectFindOptionsKueryNode; @@ -181,12 +181,12 @@ interface CasesMapWithPageInfo { type FindCaseOptions = CasesFindRequest & SavedObjectFindOptionsKueryNode; interface GetTagsArgs { - soClient: SavedObjectsClientContract; + unsecuredSavedObjectsClient: SavedObjectsClientContract; filter?: KueryNode; } interface GetReportersArgs { - soClient: SavedObjectsClientContract; + unsecuredSavedObjectsClient: SavedObjectsClientContract; filter?: KueryNode; } @@ -234,7 +234,7 @@ export class CasesService { }); public async getCaseIdsByAlertId({ - soClient, + unsecuredSavedObjectsClient, alertId, filter, }: GetCaseIdsByAlertIdArgs): Promise< @@ -247,7 +247,10 @@ export class CasesService { filter, ]); - let response = await soClient.find({ + let response = await unsecuredSavedObjectsClient.find< + CommentAttributes, + GetCaseIdsByAlertIdAggs + >({ type: CASE_COMMENT_SAVED_OBJECT, fields: includeFieldsRequiredForAuthentication(), page: 1, @@ -257,7 +260,10 @@ export class CasesService { filter: combinedFilter, }); if (response.total > 100) { - response = await soClient.find({ + response = await unsecuredSavedObjectsClient.find< + CommentAttributes, + GetCaseIdsByAlertIdAggs + >({ type: CASE_COMMENT_SAVED_OBJECT, fields: includeFieldsRequiredForAuthentication(), page: 1, @@ -287,22 +293,22 @@ export class CasesService { * Returns a map of all cases combined with their sub cases if they are collections. */ public async findCasesGroupedByID({ - soClient, + unsecuredSavedObjectsClient, caseOptions, subCaseOptions, }: { - soClient: SavedObjectsClientContract; + unsecuredSavedObjectsClient: SavedObjectsClientContract; caseOptions: FindCaseOptions; subCaseOptions?: SavedObjectFindOptionsKueryNode; }): Promise { const cases = await this.findCases({ - soClient, + unsecuredSavedObjectsClient, options: caseOptions, }); const subCasesResp = ENABLE_CASE_CONNECTOR ? await this.findSubCasesGroupByCase({ - soClient, + unsecuredSavedObjectsClient, options: subCaseOptions, ids: cases.saved_objects .filter((caseInfo) => caseInfo.attributes.type === CaseType.collection) @@ -343,7 +349,7 @@ export class CasesService { * in another request (the one below this comment). */ const totalCommentsForCases = await this.getCaseCommentStats({ - soClient, + unsecuredSavedObjectsClient, ids: Array.from(casesMap.keys()), associationType: AssociationType.case, }); @@ -374,18 +380,18 @@ export class CasesService { * This also counts sub cases. Parent cases are excluded from the statistics. */ public async findCaseStatusStats({ - soClient, + unsecuredSavedObjectsClient, caseOptions, subCaseOptions, ensureSavedObjectsAreAuthorized, }: { - soClient: SavedObjectsClientContract; + unsecuredSavedObjectsClient: SavedObjectsClientContract; caseOptions: SavedObjectFindOptionsKueryNode; ensureSavedObjectsAreAuthorized: EnsureSOAuthCallback; subCaseOptions?: SavedObjectFindOptionsKueryNode; }): Promise { const casesStats = await this.findCases({ - soClient, + unsecuredSavedObjectsClient, options: { ...caseOptions, fields: [], @@ -415,7 +421,7 @@ export class CasesService { * don't have the same title and tags, we'd need to account for that as well. */ const cases = await this.findCases({ - soClient, + unsecuredSavedObjectsClient, options: { ...caseOptions, fields: includeFieldsRequiredForAuthentication([caseTypeField]), @@ -437,7 +443,7 @@ export class CasesService { if (ENABLE_CASE_CONNECTOR && subCaseOptions) { subCasesTotal = await this.findSubCaseStatusStats({ - soClient, + unsecuredSavedObjectsClient, options: cloneDeep(subCaseOptions), ids: caseIds, }); @@ -454,20 +460,20 @@ export class CasesService { * Retrieves the comments attached to a case or sub case. */ public async getCommentsByAssociation({ - soClient, + unsecuredSavedObjectsClient, id, associationType, options, }: FindCommentsByAssociationArgs): Promise> { if (associationType === AssociationType.subCase) { return this.getAllSubCaseComments({ - soClient, + unsecuredSavedObjectsClient, id, options, }); } else { return this.getAllCaseComments({ - soClient, + unsecuredSavedObjectsClient, id, options, }); @@ -478,11 +484,11 @@ export class CasesService { * Returns the number of total comments and alerts for a case (or sub case) */ public async getCaseCommentStats({ - soClient, + unsecuredSavedObjectsClient, ids, associationType, }: { - soClient: SavedObjectsClientContract; + unsecuredSavedObjectsClient: SavedObjectsClientContract; ids: string[]; associationType: AssociationType; }): Promise { @@ -499,7 +505,7 @@ export class CasesService { const allComments = await Promise.all( ids.map((id) => this.getCommentsByAssociation({ - soClient, + unsecuredSavedObjectsClient, associationType, id, options: { page: 1, perPage: 1 }, @@ -508,7 +514,7 @@ export class CasesService { ); const alerts = await this.getCommentsByAssociation({ - soClient, + unsecuredSavedObjectsClient, associationType, id: ids, options: { @@ -544,11 +550,11 @@ export class CasesService { * Returns all the sub cases for a set of case IDs. Comment statistics are also returned. */ public async findSubCasesGroupByCase({ - soClient, + unsecuredSavedObjectsClient, options, ids, }: { - soClient: SavedObjectsClientContract; + unsecuredSavedObjectsClient: SavedObjectsClientContract; options?: SavedObjectFindOptionsKueryNode; ids: string[]; }): Promise { @@ -572,7 +578,7 @@ export class CasesService { } const subCases = await this.findSubCases({ - soClient, + unsecuredSavedObjectsClient, options: { ...options, hasReference: ids.map((id) => { @@ -585,7 +591,7 @@ export class CasesService { }); const subCaseComments = await this.getCaseCommentStats({ - soClient, + unsecuredSavedObjectsClient, ids: subCases.saved_objects.map((subCase) => subCase.id), associationType: AssociationType.subCase, }); @@ -624,7 +630,7 @@ export class CasesService { * Calculates the number of sub cases for a given set of options for a set of case IDs. */ public async findSubCaseStatusStats({ - soClient, + unsecuredSavedObjectsClient, options, ids, }: FindSubCasesStatusStats): Promise { @@ -633,7 +639,7 @@ export class CasesService { } const subCases = await this.findSubCases({ - soClient, + unsecuredSavedObjectsClient, options: { ...options, page: 1, @@ -652,14 +658,14 @@ export class CasesService { } public async createSubCase({ - soClient, + unsecuredSavedObjectsClient, createdAt, caseId, createdBy, }: CreateSubCaseArgs): Promise> { try { this.log.debug(`Attempting to POST a new sub case`); - return soClient.create( + return unsecuredSavedObjectsClient.create( SUB_CASE_SAVED_OBJECT, // ENABLE_CASE_CONNECTOR: populate the owner field correctly transformNewSubCase({ createdAt, createdBy, owner: '' }), @@ -679,10 +685,13 @@ export class CasesService { } } - public async getMostRecentSubCase(soClient: SavedObjectsClientContract, caseId: string) { + public async getMostRecentSubCase( + unsecuredSavedObjectsClient: SavedObjectsClientContract, + caseId: string + ) { try { this.log.debug(`Attempting to find most recent sub case for caseID: ${caseId}`); - const subCases = await soClient.find({ + const subCases = await unsecuredSavedObjectsClient.find({ perPage: 1, sortField: 'created_at', sortOrder: 'desc', @@ -700,20 +709,20 @@ export class CasesService { } } - public async deleteSubCase(soClient: SavedObjectsClientContract, id: string) { + public async deleteSubCase(unsecuredSavedObjectsClient: SavedObjectsClientContract, id: string) { try { this.log.debug(`Attempting to DELETE sub case ${id}`); - return await soClient.delete(SUB_CASE_SAVED_OBJECT, id); + return await unsecuredSavedObjectsClient.delete(SUB_CASE_SAVED_OBJECT, id); } catch (error) { this.log.error(`Error on DELETE sub case ${id}: ${error}`); throw error; } } - public async deleteCase({ soClient, id: caseId }: GetCaseArgs) { + public async deleteCase({ unsecuredSavedObjectsClient, id: caseId }: GetCaseArgs) { try { this.log.debug(`Attempting to DELETE case ${caseId}`); - return await soClient.delete(CASE_SAVED_OBJECT, caseId); + return await unsecuredSavedObjectsClient.delete(CASE_SAVED_OBJECT, caseId); } catch (error) { this.log.error(`Error on DELETE case ${caseId}: ${error}`); throw error; @@ -721,21 +730,24 @@ export class CasesService { } public async getCase({ - soClient, + unsecuredSavedObjectsClient, id: caseId, }: GetCaseArgs): Promise> { try { this.log.debug(`Attempting to GET case ${caseId}`); - return await soClient.get(CASE_SAVED_OBJECT, caseId); + return await unsecuredSavedObjectsClient.get(CASE_SAVED_OBJECT, caseId); } catch (error) { this.log.error(`Error on GET case ${caseId}: ${error}`); throw error; } } - public async getSubCase({ soClient, id }: GetCaseArgs): Promise> { + public async getSubCase({ + unsecuredSavedObjectsClient, + id, + }: GetCaseArgs): Promise> { try { this.log.debug(`Attempting to GET sub case ${id}`); - return await soClient.get(SUB_CASE_SAVED_OBJECT, id); + return await unsecuredSavedObjectsClient.get(SUB_CASE_SAVED_OBJECT, id); } catch (error) { this.log.error(`Error on GET sub case ${id}: ${error}`); throw error; @@ -743,12 +755,12 @@ export class CasesService { } public async getSubCases({ - soClient, + unsecuredSavedObjectsClient, ids, }: GetSubCasesArgs): Promise> { try { this.log.debug(`Attempting to GET sub cases ${ids.join(', ')}`); - return await soClient.bulkGet( + return await unsecuredSavedObjectsClient.bulkGet( ids.map((id) => ({ type: SUB_CASE_SAVED_OBJECT, id })) ); } catch (error) { @@ -758,12 +770,12 @@ export class CasesService { } public async getCases({ - soClient, + unsecuredSavedObjectsClient, caseIds, }: GetCasesArgs): Promise> { try { this.log.debug(`Attempting to GET cases ${caseIds.join(', ')}`); - return await soClient.bulkGet( + return await unsecuredSavedObjectsClient.bulkGet( caseIds.map((caseId) => ({ type: CASE_SAVED_OBJECT, id: caseId })) ); } catch (error) { @@ -773,12 +785,12 @@ export class CasesService { } public async findCases({ - soClient, + unsecuredSavedObjectsClient, options, }: FindCasesArgs): Promise> { try { this.log.debug(`Attempting to find cases`); - return await soClient.find({ + return await unsecuredSavedObjectsClient.find({ sortField: defaultSortField, ...cloneDeep(options), type: CASE_SAVED_OBJECT, @@ -790,7 +802,7 @@ export class CasesService { } public async findSubCases({ - soClient, + unsecuredSavedObjectsClient, options, }: FindCasesArgs): Promise> { try { @@ -798,14 +810,14 @@ export class CasesService { // if the page or perPage options are set then respect those instead of trying to // grab all sub cases if (options?.page !== undefined || options?.perPage !== undefined) { - return soClient.find({ + return unsecuredSavedObjectsClient.find({ sortField: defaultSortField, ...cloneDeep(options), type: SUB_CASE_SAVED_OBJECT, }); } - const stats = await soClient.find({ + const stats = await unsecuredSavedObjectsClient.find({ fields: [], page: 1, perPage: 1, @@ -813,7 +825,7 @@ export class CasesService { ...cloneDeep(options), type: SUB_CASE_SAVED_OBJECT, }); - return soClient.find({ + return unsecuredSavedObjectsClient.find({ page: 1, perPage: stats.total, sortField: defaultSortField, @@ -833,7 +845,7 @@ export class CasesService { * @param id the saved object ID of the parent collection to find sub cases for. */ public async findSubCasesByCaseId({ - soClient, + unsecuredSavedObjectsClient, ids, options, }: FindSubCasesByIDArgs): Promise> { @@ -849,7 +861,7 @@ export class CasesService { try { this.log.debug(`Attempting to GET sub cases for case collection id ${ids.join(', ')}`); return this.findSubCases({ - soClient, + unsecuredSavedObjectsClient, options: { ...options, hasReference: ids.map((id) => ({ @@ -877,21 +889,21 @@ export class CasesService { } private async getAllComments({ - soClient, + unsecuredSavedObjectsClient, id, options, }: FindCommentsArgs): Promise> { try { this.log.debug(`Attempting to GET all comments internal for id ${JSON.stringify(id)}`); if (options?.page !== undefined || options?.perPage !== undefined) { - return soClient.find({ + return unsecuredSavedObjectsClient.find({ type: CASE_COMMENT_SAVED_OBJECT, sortField: defaultSortField, ...cloneDeep(options), }); } // get the total number of comments that are in ES then we'll grab them all in one go - const stats = await soClient.find({ + const stats = await unsecuredSavedObjectsClient.find({ type: CASE_COMMENT_SAVED_OBJECT, fields: [], page: 1, @@ -901,7 +913,7 @@ export class CasesService { ...cloneDeep(options), }); - return soClient.find({ + return unsecuredSavedObjectsClient.find({ type: CASE_COMMENT_SAVED_OBJECT, page: 1, perPage: stats.total, @@ -922,7 +934,7 @@ export class CasesService { * sub case comments are excluded. If the `filter` field is included in the options, it will override this behavior */ public async getAllCaseComments({ - soClient, + unsecuredSavedObjectsClient, id, options, includeSubCaseComments = false, @@ -954,7 +966,7 @@ export class CasesService { this.log.debug(`Attempting to GET all comments for case caseID ${JSON.stringify(id)}`); return await this.getAllComments({ - soClient, + unsecuredSavedObjectsClient, id, options: { hasReferenceOperator: 'OR', @@ -970,7 +982,7 @@ export class CasesService { } public async getAllSubCaseComments({ - soClient, + unsecuredSavedObjectsClient, id, options, }: FindSubCaseCommentsArgs): Promise> { @@ -987,7 +999,7 @@ export class CasesService { this.log.debug(`Attempting to GET all comments for sub case caseID ${JSON.stringify(id)}`); return await this.getAllComments({ - soClient, + unsecuredSavedObjectsClient, id, options: { hasReferenceOperator: 'OR', @@ -1002,12 +1014,12 @@ export class CasesService { } public async getReporters({ - soClient, + unsecuredSavedObjectsClient, filter, }: GetReportersArgs): Promise> { try { this.log.debug(`Attempting to GET all reporters`); - const firstReporters = await soClient.find({ + const firstReporters = await unsecuredSavedObjectsClient.find({ type: CASE_SAVED_OBJECT, fields: ['created_by', OWNER_FIELD], page: 1, @@ -1015,7 +1027,7 @@ export class CasesService { filter: cloneDeep(filter), }); - return await soClient.find({ + return await unsecuredSavedObjectsClient.find({ type: CASE_SAVED_OBJECT, fields: ['created_by', OWNER_FIELD], page: 1, @@ -1029,12 +1041,12 @@ export class CasesService { } public async getTags({ - soClient, + unsecuredSavedObjectsClient, filter, }: GetTagsArgs): Promise> { try { this.log.debug(`Attempting to GET all cases`); - const firstTags = await soClient.find({ + const firstTags = await unsecuredSavedObjectsClient.find({ type: CASE_SAVED_OBJECT, fields: ['tags', OWNER_FIELD], page: 1, @@ -1042,7 +1054,7 @@ export class CasesService { filter: cloneDeep(filter), }); - return await soClient.find({ + return await unsecuredSavedObjectsClient.find({ type: CASE_SAVED_OBJECT, fields: ['tags', OWNER_FIELD], page: 1, @@ -1080,20 +1092,29 @@ export class CasesService { } } - public async postNewCase({ soClient, attributes, id }: PostCaseArgs) { + public async postNewCase({ unsecuredSavedObjectsClient, attributes, id }: PostCaseArgs) { try { this.log.debug(`Attempting to POST a new case`); - return await soClient.create(CASE_SAVED_OBJECT, attributes, { id }); + return await unsecuredSavedObjectsClient.create( + CASE_SAVED_OBJECT, + attributes, + { id } + ); } catch (error) { this.log.error(`Error on POST a new case: ${error}`); throw error; } } - public async patchCase({ soClient, caseId, updatedAttributes, version }: PatchCaseArgs) { + public async patchCase({ + unsecuredSavedObjectsClient, + caseId, + updatedAttributes, + version, + }: PatchCaseArgs) { try { this.log.debug(`Attempting to UPDATE case ${caseId}`); - return await soClient.update( + return await unsecuredSavedObjectsClient.update( CASE_SAVED_OBJECT, caseId, { ...updatedAttributes }, @@ -1105,10 +1126,10 @@ export class CasesService { } } - public async patchCases({ soClient, cases }: PatchCasesArgs) { + public async patchCases({ unsecuredSavedObjectsClient, cases }: PatchCasesArgs) { try { this.log.debug(`Attempting to UPDATE case ${cases.map((c) => c.caseId).join(', ')}`); - return await soClient.bulkUpdate( + return await unsecuredSavedObjectsClient.bulkUpdate( cases.map((c) => ({ type: CASE_SAVED_OBJECT, id: c.caseId, @@ -1122,10 +1143,15 @@ export class CasesService { } } - public async patchSubCase({ soClient, subCaseId, updatedAttributes, version }: PatchSubCase) { + public async patchSubCase({ + unsecuredSavedObjectsClient, + subCaseId, + updatedAttributes, + version, + }: PatchSubCase) { try { this.log.debug(`Attempting to UPDATE sub case ${subCaseId}`); - return await soClient.update( + return await unsecuredSavedObjectsClient.update( SUB_CASE_SAVED_OBJECT, subCaseId, { ...updatedAttributes }, @@ -1137,12 +1163,12 @@ export class CasesService { } } - public async patchSubCases({ soClient, subCases }: PatchSubCases) { + public async patchSubCases({ unsecuredSavedObjectsClient, subCases }: PatchSubCases) { try { this.log.debug( `Attempting to UPDATE sub case ${subCases.map((c) => c.subCaseId).join(', ')}` ); - return await soClient.bulkUpdate( + return await unsecuredSavedObjectsClient.bulkUpdate( subCases.map((c) => ({ type: SUB_CASE_SAVED_OBJECT, id: c.subCaseId, diff --git a/x-pack/plugins/cases/server/services/configure/index.ts b/x-pack/plugins/cases/server/services/configure/index.ts index 28e9af01f9d735..8ea1c903622b72 100644 --- a/x-pack/plugins/cases/server/services/configure/index.ts +++ b/x-pack/plugins/cases/server/services/configure/index.ts @@ -13,7 +13,7 @@ import { ESCasesConfigureAttributes } from '../../../common/api'; import { CASE_CONFIGURE_SAVED_OBJECT } from '../../../common/constants'; interface ClientArgs { - soClient: SavedObjectsClientContract; + unsecuredSavedObjectsClient: SavedObjectsClientContract; } interface GetCaseConfigureArgs extends ClientArgs { @@ -36,20 +36,20 @@ interface PatchCaseConfigureArgs extends ClientArgs { export class CaseConfigureService { constructor(private readonly log: Logger) {} - public async delete({ soClient, configurationId }: GetCaseConfigureArgs) { + public async delete({ unsecuredSavedObjectsClient, configurationId }: GetCaseConfigureArgs) { try { this.log.debug(`Attempting to DELETE case configure ${configurationId}`); - return await soClient.delete(CASE_CONFIGURE_SAVED_OBJECT, configurationId); + return await unsecuredSavedObjectsClient.delete(CASE_CONFIGURE_SAVED_OBJECT, configurationId); } catch (error) { this.log.debug(`Error on DELETE case configure ${configurationId}: ${error}`); throw error; } } - public async get({ soClient, configurationId }: GetCaseConfigureArgs) { + public async get({ unsecuredSavedObjectsClient, configurationId }: GetCaseConfigureArgs) { try { this.log.debug(`Attempting to GET case configuration ${configurationId}`); - return await soClient.get( + return await unsecuredSavedObjectsClient.get( CASE_CONFIGURE_SAVED_OBJECT, configurationId ); @@ -59,10 +59,10 @@ export class CaseConfigureService { } } - public async find({ soClient, options }: FindCaseConfigureArgs) { + public async find({ unsecuredSavedObjectsClient, options }: FindCaseConfigureArgs) { try { this.log.debug(`Attempting to find all case configuration`); - return await soClient.find({ + return await unsecuredSavedObjectsClient.find({ ...cloneDeep(options), // Get the latest configuration sortField: 'created_at', @@ -75,10 +75,10 @@ export class CaseConfigureService { } } - public async post({ soClient, attributes, id }: PostCaseConfigureArgs) { + public async post({ unsecuredSavedObjectsClient, attributes, id }: PostCaseConfigureArgs) { try { this.log.debug(`Attempting to POST a new case configuration`); - return await soClient.create( + return await unsecuredSavedObjectsClient.create( CASE_CONFIGURE_SAVED_OBJECT, { ...attributes, @@ -91,10 +91,14 @@ export class CaseConfigureService { } } - public async patch({ soClient, configurationId, updatedAttributes }: PatchCaseConfigureArgs) { + public async patch({ + unsecuredSavedObjectsClient, + configurationId, + updatedAttributes, + }: PatchCaseConfigureArgs) { try { this.log.debug(`Attempting to UPDATE case configuration ${configurationId}`); - return await soClient.update( + return await unsecuredSavedObjectsClient.update( CASE_CONFIGURE_SAVED_OBJECT, configurationId, { 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 44892336458213..e3ac5b4c55cf3b 100644 --- a/x-pack/plugins/cases/server/services/connector_mappings/index.ts +++ b/x-pack/plugins/cases/server/services/connector_mappings/index.ts @@ -12,7 +12,7 @@ import { CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT } from '../../../common/constants' import { SavedObjectFindOptionsKueryNode } from '../../common'; interface ClientArgs { - soClient: SavedObjectsClientContract; + unsecuredSavedObjectsClient: SavedObjectsClientContract; } interface FindConnectorMappingsArgs extends ClientArgs { options?: SavedObjectFindOptionsKueryNode; @@ -32,10 +32,10 @@ interface UpdateConnectorMappingsArgs extends ClientArgs { export class ConnectorMappingsService { constructor(private readonly log: Logger) {} - public async find({ soClient, options }: FindConnectorMappingsArgs) { + public async find({ unsecuredSavedObjectsClient, options }: FindConnectorMappingsArgs) { try { this.log.debug(`Attempting to find all connector mappings`); - return await soClient.find({ + return await unsecuredSavedObjectsClient.find({ ...options, type: CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, }); @@ -45,10 +45,14 @@ export class ConnectorMappingsService { } } - public async post({ soClient, attributes, references }: PostConnectorMappingsArgs) { + public async post({ + unsecuredSavedObjectsClient, + attributes, + references, + }: PostConnectorMappingsArgs) { try { this.log.debug(`Attempting to POST a new connector mappings`); - return await soClient.create( + return await unsecuredSavedObjectsClient.create( CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, attributes, { @@ -62,14 +66,14 @@ export class ConnectorMappingsService { } public async update({ - soClient, + unsecuredSavedObjectsClient, mappingId, attributes, references, }: UpdateConnectorMappingsArgs) { try { this.log.debug(`Attempting to UPDATE connector mappings ${mappingId}`); - return await soClient.update( + return await unsecuredSavedObjectsClient.update( CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, mappingId, attributes, diff --git a/x-pack/plugins/cases/server/services/index.ts b/x-pack/plugins/cases/server/services/index.ts index 6a56001f29cac9..09895d9392441e 100644 --- a/x-pack/plugins/cases/server/services/index.ts +++ b/x-pack/plugins/cases/server/services/index.ts @@ -15,5 +15,5 @@ export { AlertService, AlertServiceContract } from './alerts'; export { AttachmentService } from './attachments'; export interface ClientArgs { - soClient: SavedObjectsClientContract; + unsecuredSavedObjectsClient: SavedObjectsClientContract; } diff --git a/x-pack/plugins/cases/server/services/user_actions/index.ts b/x-pack/plugins/cases/server/services/user_actions/index.ts index 0da640de2a6ca2..e691b9305fb37f 100644 --- a/x-pack/plugins/cases/server/services/user_actions/index.ts +++ b/x-pack/plugins/cases/server/services/user_actions/index.ts @@ -32,11 +32,11 @@ interface PostCaseUserActionArgs extends ClientArgs { export class CaseUserActionService { constructor(private readonly log: Logger) {} - public async getAll({ soClient, caseId, subCaseId }: GetCaseUserActionArgs) { + public async getAll({ unsecuredSavedObjectsClient, caseId, subCaseId }: GetCaseUserActionArgs) { try { const id = subCaseId ?? caseId; const type = subCaseId ? SUB_CASE_SAVED_OBJECT : CASE_SAVED_OBJECT; - const caseUserActionInfo = await soClient.find({ + const caseUserActionInfo = await unsecuredSavedObjectsClient.find({ type: CASE_USER_ACTION_SAVED_OBJECT, fields: [], hasReference: { type, id }, @@ -44,7 +44,7 @@ export class CaseUserActionService { perPage: 1, }); - return await soClient.find({ + return await unsecuredSavedObjectsClient.find({ type: CASE_USER_ACTION_SAVED_OBJECT, hasReference: { type, id }, page: 1, @@ -58,10 +58,10 @@ export class CaseUserActionService { } } - public async bulkCreate({ soClient, actions }: PostCaseUserActionArgs) { + public async bulkCreate({ unsecuredSavedObjectsClient, actions }: PostCaseUserActionArgs) { try { this.log.debug(`Attempting to POST a new case user action`); - return await soClient.bulkCreate( + return await unsecuredSavedObjectsClient.bulkCreate( actions.map((action) => ({ type: CASE_USER_ACTION_SAVED_OBJECT, ...action })) ); } catch (error) { From 739fd6fc221b23d154752538ea098aae22470a8b Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Thu, 3 Jun 2021 13:15:44 -0400 Subject: [PATCH 097/113] [Cases] RBAC Refactoring audit logging (#100952) * Refactoring audit logging * Adding unit tests for authorization classes * Addressing feedback and adding util tests * return undefined on empty array * fixing eslint --- .../__snapshots__/audit_logger.test.ts.snap | 1765 +++++++++++++++++ .../server/authorization/audit_logger.test.ts | 208 ++ .../server/authorization/audit_logger.ts | 145 +- .../authorization/authorization.test.ts | 977 +++++++++ .../server/authorization/authorization.ts | 98 +- .../cases/server/authorization/index.test.ts | 23 + .../cases/server/authorization/index.ts | 74 +- .../cases/server/authorization/mock.ts | 2 +- .../cases/server/authorization/types.ts | 26 +- .../cases/server/authorization/utils.test.ts | 297 +++ .../cases/server/authorization/utils.ts | 9 +- .../cases/server/client/attachments/add.ts | 18 +- .../cases/server/client/attachments/delete.ts | 20 +- .../cases/server/client/attachments/get.ts | 61 +- .../cases/server/client/attachments/update.ts | 10 +- .../cases/server/client/cases/create.ts | 10 +- .../cases/server/client/cases/delete.ts | 20 +- .../plugins/cases/server/client/cases/find.ts | 19 +- .../plugins/cases/server/client/cases/get.ts | 70 +- .../plugins/cases/server/client/cases/push.ts | 9 +- .../cases/server/client/cases/update.ts | 13 +- .../cases/server/client/configure/client.ts | 50 +- x-pack/plugins/cases/server/client/factory.ts | 1 - .../cases/server/client/stats/client.ts | 19 +- x-pack/plugins/cases/server/client/types.ts | 11 - .../cases/server/client/user_actions/get.ts | 19 +- x-pack/plugins/cases/server/client/utils.ts | 137 +- .../cases/server/services/cases/index.ts | 3 +- .../authorization/actions/actions.mock.ts | 3 + 29 files changed, 3546 insertions(+), 571 deletions(-) create mode 100644 x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap create mode 100644 x-pack/plugins/cases/server/authorization/audit_logger.test.ts create mode 100644 x-pack/plugins/cases/server/authorization/authorization.test.ts create mode 100644 x-pack/plugins/cases/server/authorization/index.test.ts create mode 100644 x-pack/plugins/cases/server/authorization/utils.test.ts diff --git a/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap b/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap new file mode 100644 index 00000000000000..7f5b8406b89f32 --- /dev/null +++ b/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap @@ -0,0 +1,1765 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`audit_logger log function event structure creates the correct audit event for operation: "createCase" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_create", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "creation", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "Failed attempt to create cases [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "createCase" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_create", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "creation", + ], + }, + "message": "Failed attempt to create a case as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "createCase" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_create", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "creation", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases", + }, + }, + "message": "User is creating cases [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "createCase" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_create", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "creation", + ], + }, + "message": "User is creating a case as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "createComment" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_comment_create", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "creation", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases-comments", + }, + }, + "message": "Failed attempt to create cases-comments [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "createComment" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_comment_create", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "creation", + ], + }, + "message": "Failed attempt to create a comments as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "createComment" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_comment_create", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "creation", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases-comments", + }, + }, + "message": "User is creating cases-comments [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "createComment" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_comment_create", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "creation", + ], + }, + "message": "User is creating a comments as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "createConfiguration" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_configuration_create", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "creation", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases-configure", + }, + }, + "message": "Failed attempt to create cases-configure [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "createConfiguration" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_configuration_create", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "creation", + ], + }, + "message": "Failed attempt to create a case configuration as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "createConfiguration" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_configuration_create", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "creation", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases-configure", + }, + }, + "message": "User is creating cases-configure [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "createConfiguration" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_configuration_create", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "creation", + ], + }, + "message": "User is creating a case configuration as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "deleteAllComments" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_comment_delete_all", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "deletion", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases-comments", + }, + }, + "message": "Failed attempt to delete cases-comments [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "deleteAllComments" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_comment_delete_all", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "deletion", + ], + }, + "message": "Failed attempt to delete a comments as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "deleteAllComments" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_comment_delete_all", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "deletion", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases-comments", + }, + }, + "message": "User is deleting cases-comments [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "deleteAllComments" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_comment_delete_all", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "deletion", + ], + }, + "message": "User is deleting a comments as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "deleteCase" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_delete", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "deletion", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "Failed attempt to delete cases [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "deleteCase" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_delete", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "deletion", + ], + }, + "message": "Failed attempt to delete a case as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "deleteCase" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_delete", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "deletion", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases", + }, + }, + "message": "User is deleting cases [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "deleteCase" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_delete", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "deletion", + ], + }, + "message": "User is deleting a case as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "deleteComment" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_comment_delete", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "deletion", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases-comments", + }, + }, + "message": "Failed attempt to delete cases-comments [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "deleteComment" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_comment_delete", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "deletion", + ], + }, + "message": "Failed attempt to delete a comments as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "deleteComment" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_comment_delete", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "deletion", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases-comments", + }, + }, + "message": "User is deleting cases-comments [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "deleteComment" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_comment_delete", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "deletion", + ], + }, + "message": "User is deleting a comments as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "findCases" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_find", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "Failed attempt to access cases [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "findCases" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_find", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "message": "Failed attempt to access a cases as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "findCases" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_find", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases", + }, + }, + "message": "User has accessed cases [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "findCases" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_find", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "message": "User has accessed a cases as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "findComments" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_comment_find", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases-comments", + }, + }, + "message": "Failed attempt to access cases-comments [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "findComments" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_comment_find", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "message": "Failed attempt to access a comments as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "findComments" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_comment_find", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases-comments", + }, + }, + "message": "User has accessed cases-comments [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "findComments" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_comment_find", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "message": "User has accessed a comments as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "findConfigurations" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_configuration_find", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases-configure", + }, + }, + "message": "Failed attempt to access cases-configure [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "findConfigurations" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_configuration_find", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "message": "Failed attempt to access a case configurations as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "findConfigurations" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_configuration_find", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases-configure", + }, + }, + "message": "User has accessed cases-configure [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "findConfigurations" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_configuration_find", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "message": "User has accessed a case configurations as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getAllComments" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_comment_get_all", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases-comments", + }, + }, + "message": "Failed attempt to access cases-comments [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getAllComments" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_comment_get_all", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "message": "Failed attempt to access a comments as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getAllComments" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_comment_get_all", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases-comments", + }, + }, + "message": "User has accessed cases-comments [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getAllComments" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_comment_get_all", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "message": "User has accessed a comments as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getCase" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_get", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "Failed attempt to access cases [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getCase" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_get", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "message": "Failed attempt to access a case as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getCase" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_get", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases", + }, + }, + "message": "User has accessed cases [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getCase" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_get", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "message": "User has accessed a case as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getCaseIDsByAlertID" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_ids_by_alert_id_get", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases-comments", + }, + }, + "message": "Failed attempt to access cases-comments [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getCaseIDsByAlertID" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_ids_by_alert_id_get", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "message": "Failed attempt to access a cases as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getCaseIDsByAlertID" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_ids_by_alert_id_get", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases-comments", + }, + }, + "message": "User has accessed cases-comments [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getCaseIDsByAlertID" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_ids_by_alert_id_get", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "message": "User has accessed a cases as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getCaseStatuses" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_find_statuses", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "Failed attempt to access cases [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getCaseStatuses" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_find_statuses", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "message": "Failed attempt to access a cases as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getCaseStatuses" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_find_statuses", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases", + }, + }, + "message": "User has accessed cases [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getCaseStatuses" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_find_statuses", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "message": "User has accessed a cases as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getComment" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_comment_get", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases-comments", + }, + }, + "message": "Failed attempt to access cases-comments [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getComment" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_comment_get", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "message": "Failed attempt to access a comments as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getComment" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_comment_get", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases-comments", + }, + }, + "message": "User has accessed cases-comments [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getComment" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_comment_get", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "message": "User has accessed a comments as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getReporters" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_reporters_get", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "Failed attempt to access cases [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getReporters" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_reporters_get", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "message": "Failed attempt to access a case as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getReporters" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_reporters_get", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases", + }, + }, + "message": "User has accessed cases [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getReporters" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_reporters_get", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "message": "User has accessed a case as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getTags" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_tags_get", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "Failed attempt to access cases [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getTags" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_tags_get", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "message": "Failed attempt to access a case as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getTags" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_tags_get", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases", + }, + }, + "message": "User has accessed cases [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getTags" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_tags_get", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "message": "User has accessed a case as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getUserActions" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_user_actions_get", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases-user-actions", + }, + }, + "message": "Failed attempt to access cases-user-actions [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getUserActions" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_user_actions_get", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "message": "Failed attempt to access a user actions as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getUserActions" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_user_actions_get", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases-user-actions", + }, + }, + "message": "User has accessed cases-user-actions [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getUserActions" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_user_actions_get", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "message": "User has accessed a user actions as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "pushCase" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_push", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "change", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "Failed attempt to update cases [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "pushCase" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_push", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "change", + ], + }, + "message": "Failed attempt to update a case as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "pushCase" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_push", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "change", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases", + }, + }, + "message": "User is updating cases [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "pushCase" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_push", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "change", + ], + }, + "message": "User is updating a case as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "updateCase" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_update", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "change", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "Failed attempt to update cases [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "updateCase" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_update", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "change", + ], + }, + "message": "Failed attempt to update a case as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "updateCase" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_update", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "change", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases", + }, + }, + "message": "User is updating cases [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "updateCase" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_update", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "change", + ], + }, + "message": "User is updating a case as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "updateComment" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_comment_update", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "change", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases-comments", + }, + }, + "message": "Failed attempt to update cases-comments [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "updateComment" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_comment_update", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "change", + ], + }, + "message": "Failed attempt to update a comments as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "updateComment" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_comment_update", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "change", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases-comments", + }, + }, + "message": "User is updating cases-comments [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "updateComment" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_comment_update", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "change", + ], + }, + "message": "User is updating a comments as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "updateConfiguration" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_configuration_update", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "change", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases-configure", + }, + }, + "message": "Failed attempt to update cases-configure [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "updateConfiguration" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_configuration_update", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "change", + ], + }, + "message": "Failed attempt to update a case configuration as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "updateConfiguration" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_configuration_update", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "change", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases-configure", + }, + }, + "message": "User is updating cases-configure [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "updateConfiguration" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_configuration_update", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "change", + ], + }, + "message": "User is updating a case configuration as any owners", +} +`; diff --git a/x-pack/plugins/cases/server/authorization/audit_logger.test.ts b/x-pack/plugins/cases/server/authorization/audit_logger.test.ts new file mode 100644 index 00000000000000..d54b5164b10b94 --- /dev/null +++ b/x-pack/plugins/cases/server/authorization/audit_logger.test.ts @@ -0,0 +1,208 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AuditLogger } from '../../../../plugins/security/server'; +import { Operations } from '.'; +import { AuthorizationAuditLogger } from './audit_logger'; +import { ReadOperations } from './types'; + +describe('audit_logger', () => { + it('creates a failure message without any owners', () => { + expect( + AuthorizationAuditLogger.createFailureMessage({ + owners: [], + operation: Operations.createCase, + }) + ).toBe('Unauthorized to create case of any owner'); + }); + + it('creates a failure message with owners', () => { + expect( + AuthorizationAuditLogger.createFailureMessage({ + owners: ['a', 'b'], + operation: Operations.createCase, + }) + ).toBe('Unauthorized to create case with owners: "a, b"'); + }); + + describe('log function', () => { + const mockLogger: jest.Mocked = { + log: jest.fn(), + }; + + let logger: AuthorizationAuditLogger; + + beforeEach(() => { + mockLogger.log.mockReset(); + logger = new AuthorizationAuditLogger(mockLogger); + }); + + it('does not throw an error when the underlying audit logger is undefined', () => { + const authLogger = new AuthorizationAuditLogger(); + jest.spyOn(authLogger, 'log'); + + expect(() => { + authLogger.log({ + operation: Operations.createCase, + entity: { + owner: 'a', + id: '1', + }, + }); + }).not.toThrow(); + + expect(authLogger.log).toHaveBeenCalledTimes(1); + }); + + it('logs a message with a saved object ID in the message field', () => { + logger.log({ + operation: Operations.createCase, + entity: { + owner: 'a', + id: '1', + }, + }); + expect(mockLogger.log.mock.calls[0][0]?.message).toContain('[id=1]'); + }); + + it('creates the owner part of the message when no owners are specified', () => { + logger.log({ + operation: Operations.createCase, + }); + + expect(mockLogger.log.mock.calls[0][0]?.message).toContain('as any owners'); + }); + + it('creates the owner part of the message when an owner is specified', () => { + logger.log({ + operation: Operations.createCase, + entity: { + owner: 'a', + id: '1', + }, + }); + + expect(mockLogger.log.mock.calls[0][0]?.message).toContain('as owner "a"'); + }); + + it('creates a failure message when passed an error', () => { + logger.log({ + operation: Operations.createCase, + entity: { + owner: 'a', + id: '1', + }, + error: new Error('error occurred'), + }); + + expect(mockLogger.log.mock.calls[0][0]?.message).toBe( + 'Failed attempt to create cases [id=1] as owner "a"' + ); + + expect(mockLogger.log.mock.calls[0][0]?.event?.outcome).toBe('failure'); + }); + + it('creates a write operation message', () => { + logger.log({ + operation: Operations.createCase, + entity: { + owner: 'a', + id: '1', + }, + }); + + expect(mockLogger.log.mock.calls[0][0]?.message).toBe( + 'User is creating cases [id=1] as owner "a"' + ); + + expect(mockLogger.log.mock.calls[0][0]?.event?.outcome).toBe('unknown'); + }); + + it('creates a read operation message', () => { + logger.log({ + operation: Operations.getCase, + entity: { + owner: 'a', + id: '1', + }, + }); + + expect(mockLogger.log.mock.calls[0][0]?.message).toBe( + 'User has accessed cases [id=1] as owner "a"' + ); + + expect(mockLogger.log.mock.calls[0][0]?.event?.outcome).toBe('success'); + }); + + describe('event structure', () => { + // I would have preferred to do these as match inline but that isn't supported because this is essentially a for loop + // for reference: https://github.com/facebook/jest/issues/9409#issuecomment-629272237 + + // This loops through all operation keys + it.each(Array.from(Object.keys(Operations)))( + `creates the correct audit event for operation: "%s" without an error or entity`, + (operationKey) => { + // forcing the cast here because using a string throws a type error + const key = operationKey as ReadOperations; + logger.log({ + operation: Operations[key], + }); + expect(mockLogger.log.mock.calls[0][0]).toMatchSnapshot(); + } + ); + + // This loops through all operation keys + it.each(Array.from(Object.keys(Operations)))( + `creates the correct audit event for operation: "%s" with an error but no entity`, + (operationKey) => { + // forcing the cast here because using a string throws a type error + const key = operationKey as ReadOperations; + logger.log({ + operation: Operations[key], + error: new Error('an error'), + }); + expect(mockLogger.log.mock.calls[0][0]).toMatchSnapshot(); + } + ); + + // This loops through all operation keys + it.each(Array.from(Object.keys(Operations)))( + `creates the correct audit event for operation: "%s" with an error and entity`, + (operationKey) => { + // forcing the cast here because using a string throws a type error + const key = operationKey as ReadOperations; + logger.log({ + operation: Operations[key], + entity: { + owner: 'awesome', + id: '1', + }, + error: new Error('an error'), + }); + expect(mockLogger.log.mock.calls[0][0]).toMatchSnapshot(); + } + ); + + // This loops through all operation keys + it.each(Array.from(Object.keys(Operations)))( + `creates the correct audit event for operation: "%s" without an error but with an entity`, + (operationKey) => { + // forcing the cast here because using a string throws a type error + const key = operationKey as ReadOperations; + logger.log({ + operation: Operations[key], + entity: { + owner: 'super', + id: '5', + }, + }); + expect(mockLogger.log.mock.calls[0][0]).toMatchSnapshot(); + } + ); + }); + }); +}); diff --git a/x-pack/plugins/cases/server/authorization/audit_logger.ts b/x-pack/plugins/cases/server/authorization/audit_logger.ts index 82f9f6efdc11e3..a59dfaaa4dabee 100644 --- a/x-pack/plugins/cases/server/authorization/audit_logger.ts +++ b/x-pack/plugins/cases/server/authorization/audit_logger.ts @@ -5,12 +5,15 @@ * 2.0. */ -import { DATABASE_CATEGORY, ECS_OUTCOMES, OperationDetails } from '.'; -import { AuditLogger } from '../../../security/server'; +import { EcsEventOutcome } from 'kibana/server'; +import { DATABASE_CATEGORY, ECS_OUTCOMES, isWriteOperation, OperationDetails } from '.'; +import { AuditEvent, AuditLogger } from '../../../security/server'; +import { OwnerEntity } from './types'; -enum AuthorizationResult { - Unauthorized = 'Unauthorized', - Authorized = 'Authorized', +interface CreateAuditMsgParams { + operation: OperationDetails; + entity?: OwnerEntity; + error?: Error; } /** @@ -19,106 +22,80 @@ enum AuthorizationResult { export class AuthorizationAuditLogger { private readonly auditLogger?: AuditLogger; - constructor(logger: AuditLogger | undefined) { + constructor(logger?: AuditLogger) { this.auditLogger = logger; } - private static createMessage({ - result, - owners, - operation, - }: { - result: AuthorizationResult; - owners?: string[]; - operation: OperationDetails; - }): string { - const ownerMsg = owners == null ? 'of any owner' : `with owners: "${owners.join(', ')}"`; - /** - * This will take the form: - * `Unauthorized to create case with owners: "securitySolution, observability"` - * `Unauthorized to find cases of any owner`. - */ - return `${result} to ${operation.verbs.present} ${operation.docType} ${ownerMsg}`; - } + /** + * Creates an AuditEvent describing the state of a request. + */ + private static createAuditMsg({ operation, error, entity }: CreateAuditMsgParams): AuditEvent { + const doc = + entity !== undefined + ? `${operation.savedObjectType} [id=${entity.id}]` + : `a ${operation.docType}`; - private logSuccessEvent({ - message, - operation, - username, - }: { - message: string; - operation: OperationDetails; - username?: string; - }) { - this.auditLogger?.log({ - message: `${username ?? 'unknown user'} ${message}`, + const ownerText = entity === undefined ? 'as any owners' : `as owner "${entity.owner}"`; + + let message: string; + let outcome: EcsEventOutcome; + + if (error) { + message = `Failed attempt to ${operation.verbs.present} ${doc} ${ownerText}`; + outcome = ECS_OUTCOMES.failure; + } else if (isWriteOperation(operation)) { + message = `User is ${operation.verbs.progressive} ${doc} ${ownerText}`; + outcome = ECS_OUTCOMES.unknown; + } else { + message = `User has ${operation.verbs.past} ${doc} ${ownerText}`; + outcome = ECS_OUTCOMES.success; + } + + return { + message, event: { action: operation.action, category: DATABASE_CATEGORY, - type: [operation.type], - outcome: ECS_OUTCOMES.success, + type: [operation.ecsType], + outcome, }, - ...(username != null && { - user: { - name: username, + ...(entity !== undefined && { + kibana: { + saved_object: { type: operation.savedObjectType, id: entity.id }, }, }), - }); + ...(error !== undefined && { + error: { + code: error.name, + message: error.message, + }, + }), + }; } /** - * Creates a audit message describing a failure to authorize + * Creates a message to be passed to an Error or Boom. */ - public failure({ - username, + public static createFailureMessage({ owners, operation, }: { - username?: string; - owners?: string[]; + owners: string[]; operation: OperationDetails; - }): string { - const message = AuthorizationAuditLogger.createMessage({ - result: AuthorizationResult.Unauthorized, - owners, - operation, - }); - this.auditLogger?.log({ - message: `${username ?? 'unknown user'} ${message}`, - event: { - action: operation.action, - category: DATABASE_CATEGORY, - type: [operation.type], - outcome: ECS_OUTCOMES.failure, - }, - // add the user information if we have it - ...(username != null && { - user: { - name: username, - }, - }), - }); - return message; + }) { + const ownerMsg = owners.length <= 0 ? 'of any owner' : `with owners: "${owners.join(', ')}"`; + /** + * This will take the form: + * `Unauthorized to create case with owners: "securitySolution, observability"` + * `Unauthorized to access cases of any owner` + */ + return `Unauthorized to ${operation.verbs.present} ${operation.docType} ${ownerMsg}`; } /** - * Creates a audit message describing a successful authorization + * Logs an audit event based on the status of an operation. */ - public success({ - username, - operation, - owners, - }: { - username?: string; - owners: string[]; - operation: OperationDetails; - }): string { - const message = AuthorizationAuditLogger.createMessage({ - result: AuthorizationResult.Authorized, - owners, - operation, - }); - this.logSuccessEvent({ message, operation, username }); - return message; + public log(auditMsgParams: CreateAuditMsgParams) { + this.auditLogger?.log(AuthorizationAuditLogger.createAuditMsg(auditMsgParams)); } } diff --git a/x-pack/plugins/cases/server/authorization/authorization.test.ts b/x-pack/plugins/cases/server/authorization/authorization.test.ts new file mode 100644 index 00000000000000..e602de565f2948 --- /dev/null +++ b/x-pack/plugins/cases/server/authorization/authorization.test.ts @@ -0,0 +1,977 @@ +/* + * 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 { securityMock } from '../../../../plugins/security/server/mocks'; +import { httpServerMock, loggingSystemMock } from '../../../../../src/core/server/mocks'; +import { featuresPluginMock } from '../../../../plugins/features/server/mocks'; +import { Authorization, Operations } from '.'; +import { Space } from '../../../spaces/server'; +import { AuthorizationAuditLogger } from './audit_logger'; +import { KibanaRequest } from 'kibana/server'; +import { KibanaFeature } from '../../../../plugins/features/common'; +import { AuditLogger, SecurityPluginStart } from '../../../security/server'; +import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; + +describe('authorization', () => { + let request: KibanaRequest; + let mockLogger: jest.Mocked; + + beforeEach(() => { + request = httpServerMock.createKibanaRequest(); + mockLogger = { + log: jest.fn(), + }; + }); + + describe('create', () => { + let securityStart: jest.Mocked; + let featuresStart: jest.Mocked; + + beforeEach(() => { + securityStart = securityMock.createStart(); + featuresStart = featuresPluginMock.createStart(); + featuresStart.getKibanaFeatures.mockReturnValue(([ + { id: '1', cases: ['a'] }, + ] as unknown) as KibanaFeature[]); + }); + + it('creates an Authorization object', async () => { + expect.assertions(2); + + const getSpace = jest.fn(); + const authPromise = Authorization.create({ + request, + securityAuth: securityStart.authz, + getSpace, + features: featuresStart, + auditLogger: new AuthorizationAuditLogger(), + logger: loggingSystemMock.createLogger(), + }); + + await expect(authPromise).resolves.toBeDefined(); + await expect(authPromise).resolves.not.toThrow(); + }); + + it('throws and error when a failure occurs', async () => { + expect.assertions(1); + + const getSpace = jest.fn(async () => { + throw new Error('space error'); + }); + + const authPromise = Authorization.create({ + request, + securityAuth: securityStart.authz, + getSpace, + features: featuresStart, + auditLogger: new AuthorizationAuditLogger(), + logger: loggingSystemMock.createLogger(), + }); + + await expect(authPromise).rejects.toThrow(); + }); + }); + + describe('ensureAuthorized', () => { + const feature = { id: '1', cases: ['a'] }; + + let securityStart: ReturnType; + let featuresStart: jest.Mocked; + let auth: Authorization; + + beforeEach(async () => { + securityStart = securityMock.createStart(); + securityStart.authz.mode.useRbacForRequest.mockReturnValue(true); + securityStart.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue( + jest.fn(async () => ({ hasAllRequested: true })) + ); + + featuresStart = featuresPluginMock.createStart(); + featuresStart.getKibanaFeatures.mockReturnValue(([feature] as unknown) as KibanaFeature[]); + + auth = await Authorization.create({ + request, + securityAuth: securityStart.authz, + getSpace: jest.fn(), + features: featuresStart, + auditLogger: new AuthorizationAuditLogger(mockLogger), + logger: loggingSystemMock.createLogger(), + }); + }); + + it('throws an error when the owner passed in is not included in the features when security is disabled', async () => { + expect.assertions(1); + securityStart.authz.mode.useRbacForRequest.mockReturnValue(false); + + try { + await auth.ensureAuthorized({ + entities: [{ id: '1', owner: 'b' }], + operation: Operations.createCase, + }); + } catch (error) { + expect(error.message).toBe('Unauthorized to create case with owners: "b"'); + } + }); + + it('throws an error when the owner passed in is not included in the features when security undefined', async () => { + expect.assertions(1); + + auth = await Authorization.create({ + request, + getSpace: jest.fn(), + features: featuresStart, + auditLogger: new AuthorizationAuditLogger(), + logger: loggingSystemMock.createLogger(), + }); + + try { + await auth.ensureAuthorized({ + entities: [{ id: '1', owner: 'b' }], + operation: Operations.createCase, + }); + } catch (error) { + expect(error.message).toBe('Unauthorized to create case with owners: "b"'); + } + }); + + it('throws an error when the owner passed in is not included in the features when security is enabled', async () => { + expect.assertions(1); + + try { + await auth.ensureAuthorized({ + entities: [{ id: '1', owner: 'b' }], + operation: Operations.createCase, + }); + } catch (error) { + expect(error.message).toBe('Unauthorized to create case with owners: "b"'); + } + }); + + it('logs the error thrown when the passed in owner is not one of the features', async () => { + expect.assertions(2); + + try { + await auth.ensureAuthorized({ + entities: [ + { id: '1', owner: 'b' }, + { id: '5', owner: 'z' }, + ], + operation: Operations.createCase, + }); + } catch (error) { + expect(mockLogger.log.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "error": Object { + "code": "Error", + "message": "Unauthorized to create case with owners: \\"b, z\\"", + }, + "event": Object { + "action": "case_create", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "creation", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "Failed attempt to create cases [id=1] as owner \\"b\\"", + }, + ], + Array [ + Object { + "error": Object { + "code": "Error", + "message": "Unauthorized to create case with owners: \\"b, z\\"", + }, + "event": Object { + "action": "case_create", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "creation", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases", + }, + }, + "message": "Failed attempt to create cases [id=5] as owner \\"z\\"", + }, + ], + ] + `); + expect(error.message).toBe('Unauthorized to create case with owners: "b, z"'); + } + }); + + it('throws an error when the user does not have all the requested privileges', async () => { + expect.assertions(1); + + securityStart.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue( + jest.fn(async () => ({ hasAllRequested: false })) + ); + + try { + await auth.ensureAuthorized({ + entities: [{ id: '1', owner: 'a' }], + operation: Operations.createCase, + }); + } catch (error) { + expect(error.message).toBe('Unauthorized to create case with owners: "a"'); + } + }); + + it('throws an error when owner does not exist because it was from a disabled plugin', async () => { + expect.assertions(1); + + auth = await Authorization.create({ + request, + securityAuth: securityStart.authz, + getSpace: jest.fn(async () => ({ disabledFeatures: [feature.id] } as Space)), + features: featuresStart, + auditLogger: new AuthorizationAuditLogger(), + logger: loggingSystemMock.createLogger(), + }); + + try { + await auth.ensureAuthorized({ + entities: [{ id: '100', owner: feature.cases[0] }], + operation: Operations.createCase, + }); + } catch (error) { + expect(error.message).toBe( + `Unauthorized to create case with owners: "${feature.cases[0]}"` + ); + } + }); + + it('does not throw an error when the user has the privileges needed', async () => { + expect.assertions(1); + + featuresStart.getKibanaFeatures.mockReturnValue(([ + feature, + { id: '2', cases: ['other-owner'] }, + ] as unknown) as KibanaFeature[]); + + auth = await Authorization.create({ + request, + securityAuth: securityStart.authz, + getSpace: jest.fn(), + features: featuresStart, + auditLogger: new AuthorizationAuditLogger(), + logger: loggingSystemMock.createLogger(), + }); + + await expect( + auth.ensureAuthorized({ + entities: [ + { id: '100', owner: feature.cases[0] }, + { id: '3', owner: 'other-owner' }, + ], + operation: Operations.createCase, + }) + ).resolves.not.toThrow(); + }); + + it('does not throw an error when the user has the privileges needed with a feature specifying multiple owners', async () => { + expect.assertions(1); + + featuresStart.getKibanaFeatures.mockReturnValue(([ + { id: '2', cases: ['a', 'other-owner'] }, + ] as unknown) as KibanaFeature[]); + + auth = await Authorization.create({ + request, + securityAuth: securityStart.authz, + getSpace: jest.fn(), + features: featuresStart, + auditLogger: new AuthorizationAuditLogger(), + logger: loggingSystemMock.createLogger(), + }); + + await expect( + auth.ensureAuthorized({ + entities: [ + { id: '100', owner: 'a' }, + { id: '3', owner: 'other-owner' }, + ], + operation: Operations.createCase, + }) + ).resolves.not.toThrow(); + }); + + it('logs a successful authorization when the user has the privileges needed with a feature specifying multiple owners', async () => { + expect.assertions(2); + + featuresStart.getKibanaFeatures.mockReturnValue(([ + { id: '2', cases: ['a', 'other-owner'] }, + ] as unknown) as KibanaFeature[]); + + auth = await Authorization.create({ + request, + securityAuth: securityStart.authz, + getSpace: jest.fn(), + features: featuresStart, + auditLogger: new AuthorizationAuditLogger(mockLogger), + logger: loggingSystemMock.createLogger(), + }); + + await expect( + auth.ensureAuthorized({ + entities: [ + { id: '100', owner: 'a' }, + { id: '3', owner: 'other-owner' }, + ], + operation: Operations.createCase, + }) + ).resolves.not.toThrow(); + + expect(mockLogger.log.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "event": Object { + "action": "case_create", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "creation", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "100", + "type": "cases", + }, + }, + "message": "User is creating cases [id=100] as owner \\"a\\"", + }, + ], + Array [ + Object { + "event": Object { + "action": "case_create", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "creation", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "3", + "type": "cases", + }, + }, + "message": "User is creating cases [id=3] as owner \\"other-owner\\"", + }, + ], + ] + `); + }); + }); + + describe('getAuthorizationFilter', () => { + const feature = { id: '1', cases: ['a', 'b'] }; + + let securityStart: ReturnType; + let featuresStart: jest.Mocked; + let auth: Authorization; + + beforeEach(async () => { + securityStart = securityMock.createStart(); + securityStart.authz.mode.useRbacForRequest.mockReturnValue(true); + securityStart.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue( + jest.fn(async () => ({ + hasAllRequested: true, + username: 'super', + privileges: { kibana: [] }, + })) + ); + + featuresStart = featuresPluginMock.createStart(); + featuresStart.getKibanaFeatures.mockReturnValue(([feature] as unknown) as KibanaFeature[]); + + auth = await Authorization.create({ + request, + securityAuth: securityStart.authz, + getSpace: jest.fn(), + features: featuresStart, + auditLogger: new AuthorizationAuditLogger(mockLogger), + logger: loggingSystemMock.createLogger(), + }); + }); + + it('throws and logs an error when there are no registered owners from plugins and security is enabled', async () => { + expect.assertions(2); + + featuresStart.getKibanaFeatures.mockReturnValue([]); + + auth = await Authorization.create({ + request, + securityAuth: securityStart.authz, + getSpace: jest.fn(), + features: featuresStart, + auditLogger: new AuthorizationAuditLogger(mockLogger), + logger: loggingSystemMock.createLogger(), + }); + + try { + await auth.getAuthorizationFilter(Operations.findCases); + } catch (error) { + expect(error.message).toBe('Unauthorized to access cases of any owner'); + } + + expect(mockLogger.log.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "error": Object { + "code": "Error", + "message": "Unauthorized to access cases of any owner", + }, + "event": Object { + "action": "case_find", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "message": "Failed attempt to access a cases as any owners", + }, + ], + ] + `); + }); + + it('does not throw an error or log when a feature owner exists and security is disabled', async () => { + expect.assertions(3); + + auth = await Authorization.create({ + request, + getSpace: jest.fn(), + features: featuresStart, + auditLogger: new AuthorizationAuditLogger(mockLogger), + logger: loggingSystemMock.createLogger(), + }); + + const helpersPromise = auth.getAuthorizationFilter(Operations.findCases); + await expect(helpersPromise).resolves.not.toThrow(); + const helpers = await Promise.resolve(helpersPromise); + helpers.ensureSavedObjectsAreAuthorized([ + { id: '1', owner: 'blah' }, + { id: '2', owner: 'something-else' }, + ]); + + expect(helpers.filter).toBeUndefined(); + + expect(mockLogger.log.mock.calls).toMatchInlineSnapshot(`Array []`); + }); + + describe('hasAllRequested: true', () => { + it('logs and does not throw an error when passed the matching owners', async () => { + expect.assertions(3); + + const helpersPromise = auth.getAuthorizationFilter(Operations.findCases); + await expect(helpersPromise).resolves.not.toThrow(); + const helpers = await Promise.resolve(helpersPromise); + helpers.ensureSavedObjectsAreAuthorized([ + { id: '1', owner: 'a' }, + { id: '2', owner: 'b' }, + ]); + + expect(helpers.filter).toMatchInlineSnapshot(` + Object { + "arguments": Array [ + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "cases.attributes.owner", + }, + Object { + "type": "literal", + "value": "a", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "cases.attributes.owner", + }, + Object { + "type": "literal", + "value": "b", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + ], + "function": "or", + "type": "function", + } + `); + + expect(mockLogger.log.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "event": Object { + "action": "case_find", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "User has accessed cases [id=1] as owner \\"a\\"", + }, + ], + Array [ + Object { + "event": Object { + "action": "case_find", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "2", + "type": "cases", + }, + }, + "message": "User has accessed cases [id=2] as owner \\"b\\"", + }, + ], + ] + `); + }); + + it('logs and throws an error when passed an invalid owner', async () => { + expect.assertions(4); + + const helpersPromise = auth.getAuthorizationFilter(Operations.findCases); + await expect(helpersPromise).resolves.not.toThrow(); + const helpers = await Promise.resolve(helpersPromise); + try { + helpers.ensureSavedObjectsAreAuthorized([ + { id: '1', owner: 'a' }, + // c is an invalid owner, because it was not registered by a feature + { id: '2', owner: 'c' }, + ]); + } catch (error) { + expect(error.message).toBe('Unauthorized to access cases with owners: "c"'); + } + + expect(helpers.filter).toMatchInlineSnapshot(` + Object { + "arguments": Array [ + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "cases.attributes.owner", + }, + Object { + "type": "literal", + "value": "a", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "cases.attributes.owner", + }, + Object { + "type": "literal", + "value": "b", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + ], + "function": "or", + "type": "function", + } + `); + + expect(mockLogger.log.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "event": Object { + "action": "case_find", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "User has accessed cases [id=1] as owner \\"a\\"", + }, + ], + Array [ + Object { + "error": Object { + "code": "Error", + "message": "Unauthorized to access cases with owners: \\"c\\"", + }, + "event": Object { + "action": "case_find", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "2", + "type": "cases", + }, + }, + "message": "Failed attempt to access cases [id=2] as owner \\"c\\"", + }, + ], + ] + `); + }); + }); + + describe('hasAllRequested: false', () => { + beforeEach(async () => { + securityStart.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue( + jest.fn(async () => ({ + hasAllRequested: false, + username: 'super', + privileges: { + kibana: [ + { + authorized: true, + privilege: 'a:getCase', + }, + { + authorized: true, + privilege: 'b:getCase', + }, + { + authorized: false, + privilege: 'c:getCase', + }, + ], + }, + })) + ); + + (securityStart.authz.actions.cases.get as jest.MockedFunction< + typeof securityStart.authz.actions.cases.get + >).mockImplementation((owner, opName) => { + return `${owner}:${opName}`; + }); + + featuresStart.getKibanaFeatures.mockReturnValue(([ + { id: 'a', cases: ['a', 'b', 'c'] }, + ] as unknown) as KibanaFeature[]); + + auth = await Authorization.create({ + request, + securityAuth: securityStart.authz, + getSpace: jest.fn(), + features: featuresStart, + auditLogger: new AuthorizationAuditLogger(mockLogger), + logger: loggingSystemMock.createLogger(), + }); + }); + + it('logs and does not throw an error when passed the matching owners', async () => { + expect.assertions(3); + + const helpersPromise = auth.getAuthorizationFilter(Operations.findCases); + await expect(helpersPromise).resolves.not.toThrow(); + const helpers = await Promise.resolve(helpersPromise); + helpers.ensureSavedObjectsAreAuthorized([ + { id: '1', owner: 'a' }, + { id: '2', owner: 'b' }, + ]); + + expect(helpers.filter).toMatchInlineSnapshot(` + Object { + "arguments": Array [ + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "cases.attributes.owner", + }, + Object { + "type": "literal", + "value": "a", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "cases.attributes.owner", + }, + Object { + "type": "literal", + "value": "b", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + ], + "function": "or", + "type": "function", + } + `); + + expect(mockLogger.log.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "event": Object { + "action": "case_find", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "User has accessed cases [id=1] as owner \\"a\\"", + }, + ], + Array [ + Object { + "event": Object { + "action": "case_find", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "2", + "type": "cases", + }, + }, + "message": "User has accessed cases [id=2] as owner \\"b\\"", + }, + ], + ] + `); + }); + + it('logs and throws an error when passed an invalid owner', async () => { + expect.assertions(4); + + const helpersPromise = auth.getAuthorizationFilter(Operations.findCases); + await expect(helpersPromise).resolves.not.toThrow(); + const helpers = await Promise.resolve(helpersPromise); + try { + helpers.ensureSavedObjectsAreAuthorized([ + { id: '1', owner: 'a' }, + // c is an invalid owner, because it was not registered by a feature + { id: '2', owner: 'c' }, + ]); + } catch (error) { + expect(error.message).toBe('Unauthorized to access cases with owners: "c"'); + } + + expect(helpers.filter).toMatchInlineSnapshot(` + Object { + "arguments": Array [ + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "cases.attributes.owner", + }, + Object { + "type": "literal", + "value": "a", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "cases.attributes.owner", + }, + Object { + "type": "literal", + "value": "b", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + ], + "function": "or", + "type": "function", + } + `); + + expect(mockLogger.log.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "event": Object { + "action": "case_find", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "User has accessed cases [id=1] as owner \\"a\\"", + }, + ], + Array [ + Object { + "error": Object { + "code": "Error", + "message": "Unauthorized to access cases with owners: \\"c\\"", + }, + "event": Object { + "action": "case_find", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "2", + "type": "cases", + }, + }, + "message": "Failed attempt to access cases [id=2] as owner \\"c\\"", + }, + ], + ] + `); + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/server/authorization/authorization.ts b/x-pack/plugins/cases/server/authorization/authorization.ts index 296a125418023e..a363874857d564 100644 --- a/x-pack/plugins/cases/server/authorization/authorization.ts +++ b/x-pack/plugins/cases/server/authorization/authorization.ts @@ -9,10 +9,11 @@ import { KibanaRequest, Logger } from 'kibana/server'; import Boom from '@hapi/boom'; import { SecurityPluginStart } from '../../../security/server'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; -import { AuthorizationFilter, GetSpaceFn } from './types'; +import { AuthFilterHelpers, GetSpaceFn } from './types'; import { getOwnersFilter } from './utils'; import { AuthorizationAuditLogger, OperationDetails } from '.'; import { createCaseError } from '../common'; +import { OwnerEntity } from './types'; /** * This class handles ensuring that the user making a request has the correct permissions @@ -90,10 +91,49 @@ export class Authorization { * Checks that the user making the request for the passed in owners and operation has the correct authorization. This * function will throw if the user is not authorized for the requested operation and owners. * - * @param owners an array of strings describing the case owners attempting to be authorized + * @param entities an array of entities describing the case owners in conjunction with the saved object ID attempting + * to be authorized * @param operation information describing the operation attempting to be authorized */ - public async ensureAuthorized(owners: string[], operation: OperationDetails) { + public async ensureAuthorized({ + entities, + operation, + }: { + entities: OwnerEntity[]; + operation: OperationDetails; + }) { + const logSavedObjects = (error?: Error) => { + for (const entity of entities) { + this.auditLogger.log({ operation, error, entity }); + } + }; + + try { + await this._ensureAuthorized( + entities.map((entity) => entity.owner), + operation + ); + } catch (error) { + logSavedObjects(error); + throw error; + } + + logSavedObjects(); + } + + /** + * Returns an object to filter the saved object find request to the authorized owners of an entity. + */ + public async getAuthorizationFilter(operation: OperationDetails): Promise { + try { + return await this._getAuthorizationFilter(operation); + } catch (error) { + this.auditLogger.log({ error, operation }); + throw error; + } + } + + private async _ensureAuthorized(owners: string[], operation: OperationDetails) { const { securityAuth } = this; const areAllOwnersAvailable = owners.every((owner) => this.featureCaseOwners.has(owner)); @@ -103,7 +143,7 @@ export class Authorization { ); const checkPrivileges = securityAuth.checkPrivilegesDynamicallyWithRequest(this.request); - const { hasAllRequested, username } = await checkPrivileges({ + const { hasAllRequested } = await checkPrivileges({ kibana: requiredPrivileges, }); @@ -115,55 +155,53 @@ export class Authorization { * as Privileged. * This check will ensure we don't accidentally let these through */ - throw Boom.forbidden(this.auditLogger.failure({ username, owners, operation })); + throw Boom.forbidden(AuthorizationAuditLogger.createFailureMessage({ owners, operation })); } - if (hasAllRequested) { - this.auditLogger.success({ username, operation, owners }); - } else { - throw Boom.forbidden(this.auditLogger.failure({ owners, operation, username })); + if (!hasAllRequested) { + throw Boom.forbidden(AuthorizationAuditLogger.createFailureMessage({ owners, operation })); } } else if (!areAllOwnersAvailable) { - throw Boom.forbidden(this.auditLogger.failure({ owners, operation })); + throw Boom.forbidden(AuthorizationAuditLogger.createFailureMessage({ owners, operation })); } // else security is disabled so let the operation proceed } - /** - * Returns an object to filter the saved object find request to the authorized owners of an entity. - */ - public async getFindAuthorizationFilter( - operation: OperationDetails - ): Promise { + private async _getAuthorizationFilter(operation: OperationDetails): Promise { const { securityAuth } = this; if (securityAuth && this.shouldCheckAuthorization()) { - const { username, authorizedOwners } = await this.getAuthorizedOwners([operation]); + const { authorizedOwners } = await this.getAuthorizedOwners([operation]); if (!authorizedOwners.length) { - throw Boom.forbidden(this.auditLogger.failure({ username, operation })); + throw Boom.forbidden( + AuthorizationAuditLogger.createFailureMessage({ owners: authorizedOwners, operation }) + ); } return { filter: getOwnersFilter(operation.savedObjectType, authorizedOwners), - ensureSavedObjectIsAuthorized: (owner: string) => { - if (!authorizedOwners.includes(owner)) { - throw Boom.forbidden( - this.auditLogger.failure({ username, operation, owners: [owner] }) - ); - } - }, - logSuccessfulAuthorization: () => { - if (authorizedOwners.length) { - this.auditLogger.success({ username, owners: authorizedOwners, operation }); + ensureSavedObjectsAreAuthorized: (entities: OwnerEntity[]) => { + for (const entity of entities) { + if (!authorizedOwners.includes(entity.owner)) { + const error = Boom.forbidden( + AuthorizationAuditLogger.createFailureMessage({ + operation, + owners: [entity.owner], + }) + ); + this.auditLogger.log({ error, operation, entity }); + throw error; + } + + this.auditLogger.log({ operation, entity }); } }, }; } return { - ensureSavedObjectIsAuthorized: (owner: string) => {}, - logSuccessfulAuthorization: () => {}, + ensureSavedObjectsAreAuthorized: (entities: OwnerEntity[]) => {}, }; } diff --git a/x-pack/plugins/cases/server/authorization/index.test.ts b/x-pack/plugins/cases/server/authorization/index.test.ts new file mode 100644 index 00000000000000..ef2a5eed09eaa4 --- /dev/null +++ b/x-pack/plugins/cases/server/authorization/index.test.ts @@ -0,0 +1,23 @@ +/* + * 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 { isWriteOperation, Operations } from '.'; +import { OperationDetails } from './types'; + +describe('index tests', () => { + it('should identify a write operation', () => { + expect(isWriteOperation(Operations.createCase)).toBeTruthy(); + }); + + it('should return false when the operation is not a write operation', () => { + expect(isWriteOperation(Operations.getCase)).toBeFalsy(); + }); + + it('should not identify an invalid operation as a write operation', () => { + expect(isWriteOperation({ name: 'blah' } as OperationDetails)).toBeFalsy(); + }); +}); diff --git a/x-pack/plugins/cases/server/authorization/index.ts b/x-pack/plugins/cases/server/authorization/index.ts index 1356111ff1664a..9a8b44a4a4f5d6 100644 --- a/x-pack/plugins/cases/server/authorization/index.ts +++ b/x-pack/plugins/cases/server/authorization/index.ts @@ -73,13 +73,23 @@ export const ECS_OUTCOMES: Record = { unknown: 'unknown', }; +/** + * Determines if the passed in operation was a write operation. + * + * @param operation an OperationDetails object describing the operation that occurred + * @returns true if the passed in operation was a write operation + */ +export function isWriteOperation(operation: OperationDetails): boolean { + return Object.values(WriteOperations).includes(operation.name as WriteOperations); +} + /** * Definition of all APIs within the cases backend. */ export const Operations: Record = { // case operations [WriteOperations.CreateCase]: { - type: EVENT_TYPES.creation, + ecsType: EVENT_TYPES.creation, name: WriteOperations.CreateCase, action: 'case_create', verbs: createVerbs, @@ -87,7 +97,7 @@ export const Operations: Record; export const createAuthorizationMock = () => { const mocked: AuthorizationMock = { ensureAuthorized: jest.fn(), - getFindAuthorizationFilter: jest.fn(), + getAuthorizationFilter: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/cases/server/authorization/types.ts b/x-pack/plugins/cases/server/authorization/types.ts index 8d0ec93b33b038..4651d45ab3b5f7 100644 --- a/x-pack/plugins/cases/server/authorization/types.ts +++ b/x-pack/plugins/cases/server/authorization/types.ts @@ -66,14 +66,14 @@ export interface OperationDetails { /** * The ECS event type that this operation should be audit logged as (creation, deletion, access, etc) */ - type: EcsEventType; + ecsType: EcsEventType; /** * The name of the operation to authorize against for the privilege check. * These values need to match one of the operation strings defined here: x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts */ name: string; /** - * The ECS `event.action` field, should be in the form of - e.g get-comment, find-cases + * The ECS `event.action` field, should be in the form of _ e.g comment_get, case_fined */ action: string; /** @@ -90,10 +90,24 @@ export interface OperationDetails { savedObjectType: string; } +/** + * Describes an entity with the necessary fields to identify if the user is authorized to interact with the saved object + * returned from some find query. + */ +export interface OwnerEntity { + owner: string; + id: string; +} + +/** + * Function callback for making sure the found saved objects are of the authorized owner + */ +export type EnsureSOAuthCallback = (entities: OwnerEntity[]) => void; + /** * Defines the helper methods and necessary information for authorizing the find API's request. */ -export interface AuthorizationFilter { +export interface AuthFilterHelpers { /** * The owner filter to pass to the saved object client's find operation that is scoped to the authorized owners */ @@ -101,9 +115,5 @@ export interface AuthorizationFilter { /** * Utility function for checking that the returned entities are in fact authorized for the user making the request */ - ensureSavedObjectIsAuthorized: (owner: string) => void; - /** - * Logs a successful audit message for the request - */ - logSuccessfulAuthorization: () => void; + ensureSavedObjectsAreAuthorized: EnsureSOAuthCallback; } diff --git a/x-pack/plugins/cases/server/authorization/utils.test.ts b/x-pack/plugins/cases/server/authorization/utils.test.ts new file mode 100644 index 00000000000000..3ebf6ee398e38e --- /dev/null +++ b/x-pack/plugins/cases/server/authorization/utils.test.ts @@ -0,0 +1,297 @@ +/* + * 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 { nodeBuilder } from '../../../../../src/plugins/data/common'; +import { OWNER_FIELD } from '../../common'; +import { + combineFilterWithAuthorizationFilter, + ensureFieldIsSafeForQuery, + getOwnersFilter, + includeFieldsRequiredForAuthentication, +} from './utils'; + +describe('utils', () => { + describe('combineFilterWithAuthorizationFilter', () => { + it('returns undefined if neither a filter or authorizationFilter are passed', () => { + expect(combineFilterWithAuthorizationFilter()).toBeUndefined(); + }); + + it('returns a single KueryNode when only a filter is passed in', () => { + const node = nodeBuilder.is('a', 'hello'); + expect(combineFilterWithAuthorizationFilter(node)).toMatchInlineSnapshot(` + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "a", + }, + Object { + "type": "literal", + "value": "hello", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + } + `); + }); + + it('returns a single KueryNode when only an authorizationFilter is passed in', () => { + const node = nodeBuilder.is('a', 'hello'); + expect(combineFilterWithAuthorizationFilter(undefined, node)).toMatchInlineSnapshot(` + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "a", + }, + Object { + "type": "literal", + "value": "hello", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + } + `); + }); + + it("returns a single KueryNode and'ing together the passed in parameters", () => { + const node = nodeBuilder.is('a', 'hello'); + const node2 = nodeBuilder.is('b', 'hi'); + + expect(combineFilterWithAuthorizationFilter(node, node2)).toMatchInlineSnapshot(` + Object { + "arguments": Array [ + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "a", + }, + Object { + "type": "literal", + "value": "hello", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "b", + }, + Object { + "type": "literal", + "value": "hi", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + ], + "function": "and", + "type": "function", + } + `); + }); + + it("returns a single KueryNode and'ing together the passed in parameters in opposite order", () => { + const node = nodeBuilder.is('a', 'hello'); + const node2 = nodeBuilder.is('b', 'hi'); + + expect(combineFilterWithAuthorizationFilter(node2, node)).toMatchInlineSnapshot(` + Object { + "arguments": Array [ + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "b", + }, + Object { + "type": "literal", + "value": "hi", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "a", + }, + Object { + "type": "literal", + "value": "hello", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + ], + "function": "and", + "type": "function", + } + `); + }); + }); + + describe('includeFieldsRequiredForAuthentication', () => { + it('returns undefined when the fields parameter is not specified', () => { + expect(includeFieldsRequiredForAuthentication()).toBeUndefined(); + }); + + it('returns an array with a single entry containing the owner field', () => { + expect(includeFieldsRequiredForAuthentication([])).toStrictEqual([OWNER_FIELD]); + }); + + it('returns an array without duplicates and including the owner field', () => { + expect(includeFieldsRequiredForAuthentication(['a', 'b', 'a'])).toStrictEqual([ + 'a', + 'b', + OWNER_FIELD, + ]); + }); + }); + + describe('ensureFieldIsSafeForQuery', () => { + it("throws an error if field contains character that aren't safe in a KQL query", () => { + expect(() => ensureFieldIsSafeForQuery('id', 'cases-*')).toThrowError( + `expected id not to include invalid character: *` + ); + + expect(() => ensureFieldIsSafeForQuery('id', '<=""')).toThrowError( + `expected id not to include invalid character: <=` + ); + + expect(() => ensureFieldIsSafeForQuery('id', '>=""')).toThrowError( + `expected id not to include invalid character: >=` + ); + + expect(() => ensureFieldIsSafeForQuery('id', '1 or caseid:123')).toThrowError( + `expected id not to include whitespace and invalid character: :` + ); + + expect(() => ensureFieldIsSafeForQuery('id', ') or caseid:123')).toThrowError( + `expected id not to include whitespace and invalid characters: ), :` + ); + + expect(() => ensureFieldIsSafeForQuery('id', 'some space')).toThrowError( + `expected id not to include whitespace` + ); + }); + + it("doesn't throw an error if field is safe as part of a KQL query", () => { + expect(() => ensureFieldIsSafeForQuery('id', '123-0456-678')).not.toThrow(); + }); + }); + + describe('getOwnersFilter', () => { + it('returns undefined when the owners parameter is an empty array', () => { + expect(getOwnersFilter('a', [])).toBeUndefined(); + }); + + it('constructs a KueryNode with only a single node', () => { + expect(getOwnersFilter('a', ['hello'])).toMatchInlineSnapshot(` + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "a.attributes.owner", + }, + Object { + "type": "literal", + "value": "hello", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + } + `); + }); + + it("constructs a KueryNode or'ing together two filters", () => { + expect(getOwnersFilter('a', ['hello', 'hi'])).toMatchInlineSnapshot(` + Object { + "arguments": Array [ + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "a.attributes.owner", + }, + Object { + "type": "literal", + "value": "hello", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "a.attributes.owner", + }, + Object { + "type": "literal", + "value": "hi", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + ], + "function": "or", + "type": "function", + } + `); + }); + }); +}); diff --git a/x-pack/plugins/cases/server/authorization/utils.ts b/x-pack/plugins/cases/server/authorization/utils.ts index eb2dcc1a0f2e40..19dc37d0c3fdf3 100644 --- a/x-pack/plugins/cases/server/authorization/utils.ts +++ b/x-pack/plugins/cases/server/authorization/utils.ts @@ -9,7 +9,14 @@ import { remove, uniq } from 'lodash'; import { nodeBuilder, KueryNode } from '../../../../../src/plugins/data/common'; import { OWNER_FIELD } from '../../common/api'; -export const getOwnersFilter = (savedObjectType: string, owners: string[]): KueryNode => { +export const getOwnersFilter = ( + savedObjectType: string, + owners: string[] +): KueryNode | undefined => { + if (owners.length <= 0) { + return; + } + return nodeBuilder.or( owners.reduce((query, owner) => { ensureFieldIsSafeForQuery(OWNER_FIELD, owner); diff --git a/x-pack/plugins/cases/server/client/attachments/add.ts b/x-pack/plugins/cases/server/client/attachments/add.ts index b453e1feb5d632..9008e0fc28dee8 100644 --- a/x-pack/plugins/cases/server/client/attachments/add.ts +++ b/x-pack/plugins/cases/server/client/attachments/add.ts @@ -49,7 +49,7 @@ import { CASE_COMMENT_SAVED_OBJECT, } from '../../../common'; -import { decodeCommentRequest, ensureAuthorized } from '../utils'; +import { decodeCommentRequest } from '../utils'; import { Operations } from '../../authorization'; async function getSubCase({ @@ -126,7 +126,6 @@ const addGeneratedAlerts = async ( caseService, userActionService, logger, - auditLogger, authorization, } = clientArgs; @@ -146,11 +145,8 @@ const addGeneratedAlerts = async ( const createdDate = new Date().toISOString(); const savedObjectID = SavedObjectsUtils.generateId(); - await ensureAuthorized({ - authorization, - auditLogger, - owners: [comment.owner], - savedObjectIDs: [savedObjectID], + await authorization.ensureAuthorized({ + entities: [{ owner: comment.owner, id: savedObjectID }], operation: Operations.createComment, }); @@ -339,7 +335,6 @@ export const addComment = async ( user, logger, authorization, - auditLogger, } = clientArgs; if (isCommentRequestTypeGenAlert(comment)) { @@ -356,12 +351,9 @@ export const addComment = async ( try { const savedObjectID = SavedObjectsUtils.generateId(); - await ensureAuthorized({ - authorization, - auditLogger, + await authorization.ensureAuthorized({ operation: Operations.createComment, - owners: [comment.owner], - savedObjectIDs: [savedObjectID], + entities: [{ owner: comment.owner, id: savedObjectID }], }); const createdDate = new Date().toISOString(); diff --git a/x-pack/plugins/cases/server/client/attachments/delete.ts b/x-pack/plugins/cases/server/client/attachments/delete.ts index d9a2b00ec50ae7..d935a0c8f09db0 100644 --- a/x-pack/plugins/cases/server/client/attachments/delete.ts +++ b/x-pack/plugins/cases/server/client/attachments/delete.ts @@ -13,7 +13,6 @@ import { CasesClientArgs } from '../types'; import { buildCommentUserActionItem } from '../../services/user_actions/helpers'; import { createCaseError } from '../../common/error'; import { checkEnabledCaseConnectorOrThrow } from '../../common'; -import { ensureAuthorized } from '../utils'; import { Operations } from '../../authorization'; /** @@ -65,7 +64,6 @@ export async function deleteAll( userActionService, logger, authorization, - auditLogger, } = clientArgs; try { @@ -82,12 +80,12 @@ export async function deleteAll( throw Boom.notFound(`No comments found for ${id}.`); } - await ensureAuthorized({ - authorization, - auditLogger, + await authorization.ensureAuthorized({ operation: Operations.deleteAllComments, - savedObjectIDs: comments.saved_objects.map((comment) => comment.id), - owners: comments.saved_objects.map((comment) => comment.attributes.owner), + entities: comments.saved_objects.map((comment) => ({ + owner: comment.attributes.owner, + id: comment.id, + })), }); await Promise.all( @@ -141,7 +139,6 @@ export async function deleteComment( userActionService, logger, authorization, - auditLogger, } = clientArgs; try { @@ -158,11 +155,8 @@ export async function deleteComment( throw Boom.notFound(`This comment ${attachmentID} does not exist anymore.`); } - await ensureAuthorized({ - authorization, - auditLogger, - owners: [myComment.attributes.owner], - savedObjectIDs: [myComment.id], + await authorization.ensureAuthorized({ + entities: [{ owner: myComment.attributes.owner, id: myComment.id }], operation: Operations.deleteComment, }); diff --git a/x-pack/plugins/cases/server/client/attachments/get.ts b/x-pack/plugins/cases/server/client/attachments/get.ts index 9d85a90324a6c4..e15bdcc7c8c2b5 100644 --- a/x-pack/plugins/cases/server/client/attachments/get.ts +++ b/x-pack/plugins/cases/server/client/attachments/get.ts @@ -29,12 +29,7 @@ import { import { createCaseError } from '../../common/error'; import { defaultPage, defaultPerPage } from '../../routes/api'; import { CasesClientArgs } from '../types'; -import { - combineFilters, - ensureAuthorized, - getAuthorizationFilter, - stringToKueryNode, -} from '../utils'; +import { combineFilters, stringToKueryNode } from '../utils'; import { Operations } from '../../authorization'; import { includeFieldsRequiredForAuthentication } from '../../authorization/utils'; @@ -90,13 +85,7 @@ export async function find( { caseID, queryParams }: FindArgs, clientArgs: CasesClientArgs ): Promise { - const { - unsecuredSavedObjectsClient, - caseService, - logger, - authorization, - auditLogger, - } = clientArgs; + const { unsecuredSavedObjectsClient, caseService, logger, authorization } = clientArgs; try { checkEnabledCaseConnectorOrThrow(queryParams?.subCaseId); @@ -104,12 +93,7 @@ export async function find( const { filter: authorizationFilter, ensureSavedObjectsAreAuthorized, - logSuccessfulAuthorization, - } = await getAuthorizationFilter({ - authorization, - auditLogger, - operation: Operations.findComments, - }); + } = await authorization.getAuthorizationFilter(Operations.findComments); const id = queryParams?.subCaseId ?? caseID; const associationType = queryParams?.subCaseId ? AssociationType.subCase : AssociationType.case; @@ -161,8 +145,6 @@ export async function find( })) ); - logSuccessfulAuthorization(); - return CommentsResponseRt.encode(transformComments(theComments)); } catch (error) { throw createCaseError({ @@ -182,13 +164,7 @@ export async function get( { attachmentID, caseID }: GetArgs, clientArgs: CasesClientArgs ): Promise { - const { - attachmentService, - unsecuredSavedObjectsClient, - logger, - authorization, - auditLogger, - } = clientArgs; + const { attachmentService, unsecuredSavedObjectsClient, logger, authorization } = clientArgs; try { const comment = await attachmentService.get({ @@ -196,11 +172,8 @@ export async function get( attachmentId: attachmentID, }); - await ensureAuthorized({ - authorization, - auditLogger, - owners: [comment.attributes.owner], - savedObjectIDs: [comment.id], + await authorization.ensureAuthorized({ + entities: [{ owner: comment.attributes.owner, id: comment.id }], operation: Operations.getComment, }); @@ -224,13 +197,7 @@ export async function getAll( { caseID, includeSubCaseComments, subCaseID }: GetAllArgs, clientArgs: CasesClientArgs ): Promise { - const { - unsecuredSavedObjectsClient, - caseService, - logger, - authorization, - auditLogger, - } = clientArgs; + const { unsecuredSavedObjectsClient, caseService, logger, authorization } = clientArgs; try { let comments: SavedObjectsFindResponse; @@ -244,15 +211,9 @@ export async function getAll( ); } - const { - filter, - ensureSavedObjectsAreAuthorized, - logSuccessfulAuthorization, - } = await getAuthorizationFilter({ - authorization, - auditLogger, - operation: Operations.getAllComments, - }); + const { filter, ensureSavedObjectsAreAuthorized } = await authorization.getAuthorizationFilter( + Operations.getAllComments + ); if (subCaseID) { comments = await caseService.getAllSubCaseComments({ @@ -279,8 +240,6 @@ export async function getAll( comments.saved_objects.map((comment) => ({ id: comment.id, owner: comment.attributes.owner })) ); - logSuccessfulAuthorization(); - return AllCommentsResponseRt.encode(flattenCommentSavedObjects(comments.saved_objects)); } catch (error) { throw createCaseError({ diff --git a/x-pack/plugins/cases/server/client/attachments/update.ts b/x-pack/plugins/cases/server/client/attachments/update.ts index 3310f9e8f6aa6d..c0566ff6468144 100644 --- a/x-pack/plugins/cases/server/client/attachments/update.ts +++ b/x-pack/plugins/cases/server/client/attachments/update.ts @@ -15,7 +15,7 @@ import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../common/consta import { AttachmentService, CasesService } from '../../services'; import { CaseResponse, CommentPatchRequest } from '../../../common/api'; import { CasesClientArgs } from '..'; -import { decodeCommentRequest, ensureAuthorized } from '../utils'; +import { decodeCommentRequest } from '../utils'; import { createCaseError } from '../../common/error'; import { Operations } from '../../authorization'; @@ -105,7 +105,6 @@ export async function update( user, userActionService, authorization, - auditLogger, } = clientArgs; try { @@ -137,12 +136,9 @@ export async function update( throw Boom.notFound(`This comment ${queryCommentId} does not exist anymore.`); } - await ensureAuthorized({ - authorization, - auditLogger, + await authorization.ensureAuthorized({ + entities: [{ owner: myComment.attributes.owner, id: myComment.id }], operation: Operations.updateComment, - savedObjectIDs: [myComment.id], - owners: [myComment.attributes.owner], }); if (myComment.attributes.type !== queryRestAttributes.type) { diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index e1edcfdda0423e..879edd5eb1b5c8 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -23,7 +23,7 @@ import { OWNER_FIELD, } from '../../../common'; import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; -import { ensureAuthorized, getConnectorFromConfiguration } from '../utils'; +import { getConnectorFromConfiguration } from '../utils'; import { createCaseError } from '../../common/error'; import { Operations } from '../../authorization'; @@ -52,7 +52,6 @@ export const create = async ( user, logger, authorization: auth, - auditLogger, } = clientArgs; // default to an individual case if the type is not defined. @@ -76,12 +75,9 @@ export const create = async ( try { const savedObjectID = SavedObjectsUtils.generateId(); - await ensureAuthorized({ + await auth.ensureAuthorized({ operation: Operations.createCase, - owners: [query.owner], - authorization: auth, - auditLogger, - savedObjectIDs: [savedObjectID], + entities: [{ owner: query.owner, id: savedObjectID }], }); // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/x-pack/plugins/cases/server/client/cases/delete.ts b/x-pack/plugins/cases/server/client/cases/delete.ts index 8ad48bde7f9711..b66abc6cc7be4b 100644 --- a/x-pack/plugins/cases/server/client/cases/delete.ts +++ b/x-pack/plugins/cases/server/client/cases/delete.ts @@ -12,8 +12,7 @@ import { CasesClientArgs } from '..'; import { createCaseError } from '../../common/error'; import { AttachmentService, CasesService } from '../../services'; import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; -import { Operations } from '../../authorization'; -import { ensureAuthorized } from '../utils'; +import { Operations, OwnerEntity } from '../../authorization'; import { OWNER_FIELD } from '../../../common/api'; async function deleteSubCases({ @@ -66,13 +65,11 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P user, userActionService, logger, - authorization: auth, - auditLogger, + authorization, } = clientArgs; try { const cases = await caseService.getCases({ unsecuredSavedObjectsClient, caseIds: ids }); - const soIds = new Set(); - const owners = new Set(); + const entities = new Map(); for (const theCase of cases.saved_objects) { // bulkGet can return an error. @@ -83,17 +80,12 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P logger, }); } - - soIds.add(theCase.id); - owners.add(theCase.attributes.owner); + entities.set(theCase.id, { id: theCase.id, owner: theCase.attributes.owner }); } - await ensureAuthorized({ + await authorization.ensureAuthorized({ operation: Operations.deleteCase, - owners: [...owners.values()], - authorization: auth, - auditLogger, - savedObjectIDs: [...soIds.values()], + entities: Array.from(entities.values()), }); await Promise.all( diff --git a/x-pack/plugins/cases/server/client/cases/find.ts b/x-pack/plugins/cases/server/client/cases/find.ts index 633261100ddeaa..3b4efe78f642bb 100644 --- a/x-pack/plugins/cases/server/client/cases/find.ts +++ b/x-pack/plugins/cases/server/client/cases/find.ts @@ -21,7 +21,7 @@ import { } from '../../../common/api'; import { createCaseError } from '../../common/error'; -import { constructQueryOptions, getAuthorizationFilter } from '../utils'; +import { constructQueryOptions } from '../utils'; import { includeFieldsRequiredForAuthentication } from '../../authorization/utils'; import { Operations } from '../../authorization'; import { transformCases } from '../../common'; @@ -36,13 +36,7 @@ export const find = async ( params: CasesFindRequest, clientArgs: CasesClientArgs ): Promise => { - const { - unsecuredSavedObjectsClient, - caseService, - authorization: auth, - auditLogger, - logger, - } = clientArgs; + const { unsecuredSavedObjectsClient, caseService, authorization, logger } = clientArgs; try { const queryParams = pipe( @@ -53,12 +47,7 @@ export const find = async ( const { filter: authorizationFilter, ensureSavedObjectsAreAuthorized, - logSuccessfulAuthorization, - } = await getAuthorizationFilter({ - authorization: auth, - operation: Operations.findCases, - auditLogger, - }); + } = await authorization.getAuthorizationFilter(Operations.findCases); const queryArgs = { tags: queryParams.tags, @@ -100,8 +89,6 @@ export const find = async ( }), ]); - logSuccessfulAuthorization(); - return CasesFindResponseRt.encode( transformCases({ casesMap: cases.casesMap, diff --git a/x-pack/plugins/cases/server/client/cases/get.ts b/x-pack/plugins/cases/server/client/cases/get.ts index cf6d12ceae0a0c..7a8100ad60ff32 100644 --- a/x-pack/plugins/cases/server/client/cases/get.ts +++ b/x-pack/plugins/cases/server/client/cases/get.ts @@ -30,11 +30,7 @@ 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'; +import { combineAuthorizedAndOwnerFilter } from '../utils'; import { CasesService } from '../../services'; /** @@ -61,13 +57,7 @@ export const getCaseIDsByAlertID = async ( { alertID, options }: CaseIDsByAlertIDParams, clientArgs: CasesClientArgs ): Promise => { - const { - unsecuredSavedObjectsClient, - caseService, - logger, - authorization, - auditLogger, - } = clientArgs; + const { unsecuredSavedObjectsClient, caseService, logger, authorization } = clientArgs; try { const queryParams = pipe( @@ -78,12 +68,7 @@ export const getCaseIDsByAlertID = async ( const { filter: authorizationFilter, ensureSavedObjectsAreAuthorized, - logSuccessfulAuthorization, - } = await getAuthorizationFilter({ - authorization, - operation: Operations.getCaseIDsByAlertID, - auditLogger, - }); + } = await authorization.getAuthorizationFilter(Operations.getCaseIDsByAlertID); const filter = combineAuthorizedAndOwnerFilter( queryParams.owner, @@ -104,8 +89,6 @@ export const getCaseIDsByAlertID = async ( })) ); - logSuccessfulAuthorization(); - return CasesService.getCaseIDsFromAlertAggs(commentsWithAlert); } catch (error) { throw createCaseError({ @@ -145,13 +128,7 @@ export const get = async ( { id, includeComments, includeSubCaseComments }: GetParams, clientArgs: CasesClientArgs ): Promise => { - const { - unsecuredSavedObjectsClient, - caseService, - logger, - authorization: auth, - auditLogger, - } = clientArgs; + const { unsecuredSavedObjectsClient, caseService, logger, authorization } = clientArgs; try { if (!ENABLE_CASE_CONNECTOR && includeSubCaseComments) { @@ -184,12 +161,9 @@ export const get = async ( }); } - await ensureAuthorized({ + await authorization.ensureAuthorized({ operation: Operations.getCase, - owners: [theCase.attributes.owner], - authorization: auth, - auditLogger, - savedObjectIDs: [theCase.id], + entities: [{ owner: theCase.attributes.owner, id: theCase.id }], }); if (!includeComments) { @@ -233,13 +207,7 @@ export async function getTags( params: AllTagsFindRequest, clientArgs: CasesClientArgs ): Promise { - const { - unsecuredSavedObjectsClient, - caseService, - logger, - authorization: auth, - auditLogger, - } = clientArgs; + const { unsecuredSavedObjectsClient, caseService, logger, authorization } = clientArgs; try { const queryParams = pipe( @@ -250,12 +218,7 @@ export async function getTags( const { filter: authorizationFilter, ensureSavedObjectsAreAuthorized, - logSuccessfulAuthorization, - } = await getAuthorizationFilter({ - authorization: auth, - operation: Operations.findCases, - auditLogger, - }); + } = await authorization.getAuthorizationFilter(Operations.findCases); const filter = combineAuthorizedAndOwnerFilter(queryParams.owner, authorizationFilter); @@ -280,7 +243,6 @@ export async function getTags( }); ensureSavedObjectsAreAuthorized(mappedCases); - logSuccessfulAuthorization(); return [...tags.values()]; } catch (error) { @@ -295,13 +257,7 @@ export async function getReporters( params: AllReportersFindRequest, clientArgs: CasesClientArgs ): Promise { - const { - unsecuredSavedObjectsClient, - caseService, - logger, - authorization: auth, - auditLogger, - } = clientArgs; + const { unsecuredSavedObjectsClient, caseService, logger, authorization } = clientArgs; try { const queryParams = pipe( @@ -312,12 +268,7 @@ export async function getReporters( const { filter: authorizationFilter, ensureSavedObjectsAreAuthorized, - logSuccessfulAuthorization, - } = await getAuthorizationFilter({ - authorization: auth, - operation: Operations.getReporters, - auditLogger, - }); + } = await authorization.getAuthorizationFilter(Operations.getReporters); const filter = combineAuthorizedAndOwnerFilter(queryParams.owner, authorizationFilter); @@ -346,7 +297,6 @@ export async function getReporters( }); ensureSavedObjectsAreAuthorized(mappedCases); - logSuccessfulAuthorization(); return UsersRt.encode([...reporters.values()]); } catch (error) { diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index 74d3fb1373fd75..dd527122d06168 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -24,7 +24,6 @@ import { createIncident, getCommentContextFromAttributes } from './utils'; import { createCaseError, flattenCaseSavedObject, getAlertInfoFromComments } from '../../common'; import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; import { CasesClient, CasesClientArgs, CasesClientInternal } from '..'; -import { ensureAuthorized } from '../utils'; import { Operations } from '../../authorization'; /** @@ -77,7 +76,6 @@ export const push = async ( actionsClient, user, logger, - auditLogger, authorization, } = clientArgs; @@ -93,12 +91,9 @@ export const push = async ( casesClient.userActions.getAll({ caseId }), ]); - await ensureAuthorized({ - authorization, - auditLogger, + await authorization.ensureAuthorized({ + entities: [{ owner: theCase.owner, id: caseId }], operation: Operations.pushCase, - savedObjectIDs: [caseId], - owners: [theCase.owner], }); // We need to change the logic when we support subcases diff --git a/x-pack/plugins/cases/server/client/cases/update.ts b/x-pack/plugins/cases/server/client/cases/update.ts index 1dabca40146f8d..db20ba83184470 100644 --- a/x-pack/plugins/cases/server/client/cases/update.ts +++ b/x-pack/plugins/cases/server/client/cases/update.ts @@ -36,7 +36,7 @@ import { CommentAttributes, } from '../../../common/api'; import { buildCaseUserActions } from '../../services/user_actions/helpers'; -import { ensureAuthorized, getCaseToUpdate } from '../utils'; +import { getCaseToUpdate } from '../utils'; import { CasesService } from '../../services'; import { @@ -55,8 +55,7 @@ import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; import { UpdateAlertRequest } from '../alerts/client'; import { CasesClientInternal } from '../client_internal'; import { CasesClientArgs } from '..'; -import { Operations } from '../../authorization'; -import { OwnerEntity } from '../types'; +import { Operations, OwnerEntity } from '../../authorization'; /** * Throws an error if any of the requests attempt to update a collection style cases' status field. @@ -406,7 +405,6 @@ export const update = async ( user, logger, authorization, - auditLogger, } = clientArgs; const query = pipe( excess(CasesPatchRequestRt).decode(cases), @@ -429,12 +427,9 @@ export const update = async ( query.cases ); - await ensureAuthorized({ - authorization, - auditLogger, - owners: casesToAuthorize.map((caseInfo) => caseInfo.owner), + await authorization.ensureAuthorized({ + entities: casesToAuthorize, operation: Operations.updateCase, - savedObjectIDs: casesToAuthorize.map((caseInfo) => caseInfo.id), }); if (nonExistingCases.length > 0) { diff --git a/x-pack/plugins/cases/server/client/configure/client.ts b/x-pack/plugins/cases/server/client/configure/client.ts index e0bf8c7d82308c..14348e03f99cc1 100644 --- a/x-pack/plugins/cases/server/client/configure/client.ts +++ b/x-pack/plugins/cases/server/client/configure/client.ts @@ -41,11 +41,7 @@ import { getMappings } from './get_mappings'; import { FindActionResult } from '../../../../actions/server/types'; import { ActionType } from '../../../../actions/common'; import { Operations } from '../../authorization'; -import { - combineAuthorizedAndOwnerFilter, - ensureAuthorized, - getAuthorizationFilter, -} from '../utils'; +import { combineAuthorizedAndOwnerFilter } from '../utils'; import { ConfigurationGetFields, MappingsArgs, @@ -148,13 +144,7 @@ async function get( clientArgs: CasesClientArgs, casesClientInternal: CasesClientInternal ): Promise { - const { - unsecuredSavedObjectsClient, - caseConfigureService, - logger, - authorization, - auditLogger, - } = clientArgs; + const { unsecuredSavedObjectsClient, caseConfigureService, logger, authorization } = clientArgs; try { const queryParams = pipe( excess(GetConfigureFindRequestRt).decode(params), @@ -164,12 +154,7 @@ async function get( const { filter: authorizationFilter, ensureSavedObjectsAreAuthorized, - logSuccessfulAuthorization, - } = await getAuthorizationFilter({ - authorization, - operation: Operations.findConfigurations, - auditLogger, - }); + } = await authorization.getAuthorizationFilter(Operations.findConfigurations); const filter = combineAuthorizedAndOwnerFilter( queryParams.owner, @@ -190,8 +175,6 @@ async function get( })) ); - logSuccessfulAuthorization(); - const configurations = await Promise.all( myCaseConfigure.saved_objects.map(async (configuration) => { const { connector, ...caseConfigureWithoutConnector } = configuration?.attributes ?? { @@ -267,7 +250,6 @@ async function update( unsecuredSavedObjectsClient, user, authorization, - auditLogger, } = clientArgs; try { @@ -295,12 +277,9 @@ async function update( configurationId, }); - await ensureAuthorized({ + await authorization.ensureAuthorized({ operation: Operations.updateConfiguration, - owners: [configuration.attributes.owner], - authorization, - auditLogger, - savedObjectIDs: [configuration.id], + entities: [{ owner: configuration.attributes.owner, id: configuration.id }], }); if (version !== configuration.version) { @@ -386,7 +365,6 @@ async function create( logger, user, authorization, - auditLogger, } = clientArgs; try { let error = null; @@ -394,17 +372,14 @@ async function create( const { filter: authorizationFilter, ensureSavedObjectsAreAuthorized, - logSuccessfulAuthorization, - } = await getAuthorizationFilter({ - authorization, + } = await authorization.getAuthorizationFilter( /** * 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, - }); + Operations.createConfiguration + ); const filter = combineAuthorizedAndOwnerFilter( configuration.owner, @@ -424,8 +399,6 @@ async function create( })) ); - logSuccessfulAuthorization(); - if (myCaseConfigure.saved_objects.length > 0) { await Promise.all( myCaseConfigure.saved_objects.map((cc) => @@ -436,12 +409,9 @@ async function create( const savedObjectID = SavedObjectsUtils.generateId(); - await ensureAuthorized({ + await authorization.ensureAuthorized({ operation: Operations.createConfiguration, - owners: [configuration.owner], - authorization, - auditLogger, - savedObjectIDs: [savedObjectID], + entities: [{ owner: configuration.owner, id: savedObjectID }], }); const creationDate = new Date().toISOString(); diff --git a/x-pack/plugins/cases/server/client/factory.ts b/x-pack/plugins/cases/server/client/factory.ts index 7110e7e9e1d922..4644efb61916f7 100644 --- a/x-pack/plugins/cases/server/client/factory.ts +++ b/x-pack/plugins/cases/server/client/factory.ts @@ -109,7 +109,6 @@ export class CasesClientFactory { attachmentService: new AttachmentService(this.logger), logger: this.logger, authorization: auth, - auditLogger, actionsClient: await this.options.actionsPluginStart.getActionsClientWithRequest(request), }); } diff --git a/x-pack/plugins/cases/server/client/stats/client.ts b/x-pack/plugins/cases/server/client/stats/client.ts index 9816bfe1fd7cff..0e222d54ab218b 100644 --- a/x-pack/plugins/cases/server/client/stats/client.ts +++ b/x-pack/plugins/cases/server/client/stats/client.ts @@ -22,7 +22,7 @@ import { } from '../../../common/api'; import { Operations } from '../../authorization'; import { createCaseError } from '../../common/error'; -import { constructQueryOptions, getAuthorizationFilter } from '../utils'; +import { constructQueryOptions } from '../utils'; /** * Statistics API contract. @@ -50,13 +50,7 @@ async function getStatusTotalsByType( params: CasesStatusRequest, clientArgs: CasesClientArgs ): Promise { - const { - unsecuredSavedObjectsClient, - caseService, - logger, - authorization, - auditLogger, - } = clientArgs; + const { unsecuredSavedObjectsClient, caseService, logger, authorization } = clientArgs; try { const queryParams = pipe( @@ -67,12 +61,7 @@ async function getStatusTotalsByType( const { filter: authorizationFilter, ensureSavedObjectsAreAuthorized, - logSuccessfulAuthorization, - } = await getAuthorizationFilter({ - authorization, - operation: Operations.getCaseStatuses, - auditLogger, - }); + } = await authorization.getAuthorizationFilter(Operations.getCaseStatuses); const [openCases, inProgressCases, closedCases] = await Promise.all([ ...caseStatuses.map((status) => { @@ -90,8 +79,6 @@ async function getStatusTotalsByType( }), ]); - logSuccessfulAuthorization(); - return CasesStatusResponseRt.encode({ count_open_cases: openCases, count_in_progress_cases: inProgressCases, diff --git a/x-pack/plugins/cases/server/client/types.ts b/x-pack/plugins/cases/server/client/types.ts index 7d1c0855061c26..f6b229b94800d8 100644 --- a/x-pack/plugins/cases/server/client/types.ts +++ b/x-pack/plugins/cases/server/client/types.ts @@ -8,7 +8,6 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import { ElasticsearchClient, SavedObjectsClientContract, Logger } from 'kibana/server'; import { User } from '../../common/api'; -import { AuditLogger } from '../../../security/server'; import { Authorization } from '../authorization/authorization'; import { AlertServiceContract, @@ -35,15 +34,5 @@ export interface CasesClientArgs { readonly attachmentService: AttachmentService; readonly logger: Logger; readonly authorization: PublicMethodsOf; - readonly auditLogger?: AuditLogger; readonly actionsClient: PublicMethodsOf; } - -/** - * Describes an entity with the necessary fields to identify if the user is authorized to interact with the saved object - * returned from some find query. - */ -export interface OwnerEntity { - owner: string; - id: string; -} 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 7cc1dc7d27dfe9..a0dddc79ef4b44 100644 --- a/x-pack/plugins/cases/server/client/user_actions/get.ts +++ b/x-pack/plugins/cases/server/client/user_actions/get.ts @@ -14,7 +14,6 @@ import { CaseUserActionsResponseRt, CaseUserActionsResponse } from '../../../com import { createCaseError } from '../../common/error'; import { checkEnabledCaseConnectorOrThrow } from '../../common'; import { CasesClientArgs } from '..'; -import { ensureAuthorized } from '../utils'; import { Operations } from '../../authorization'; import { UserActionGet } from './client'; @@ -22,13 +21,7 @@ export const get = async ( { caseId, subCaseId }: UserActionGet, clientArgs: CasesClientArgs ): Promise => { - const { - unsecuredSavedObjectsClient, - userActionService, - logger, - authorization, - auditLogger, - } = clientArgs; + const { unsecuredSavedObjectsClient, userActionService, logger, authorization } = clientArgs; try { checkEnabledCaseConnectorOrThrow(subCaseId); @@ -39,11 +32,11 @@ export const get = async ( subCaseId, }); - await ensureAuthorized({ - authorization, - auditLogger, - owners: userActions.saved_objects.map((userAction) => userAction.attributes.owner), - savedObjectIDs: userActions.saved_objects.map((userAction) => userAction.id), + await authorization.ensureAuthorized({ + entities: userActions.saved_objects.map((userAction) => ({ + owner: userAction.attributes.owner, + id: userAction.id, + })), operation: Operations.getUserActions, }); diff --git a/x-pack/plugins/cases/server/client/utils.ts b/x-pack/plugins/cases/server/client/utils.ts index d42947ad17edda..7ceb9cec60c398 100644 --- a/x-pack/plugins/cases/server/client/utils.ts +++ b/x-pack/plugins/cases/server/client/utils.ts @@ -12,8 +12,7 @@ import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; -import { EcsEventOutcome, SavedObjectsFindResponse } from 'kibana/server'; -import { PublicMethodsOf } from '@kbn/utility-types'; +import { SavedObjectsFindResponse } from 'kibana/server'; import { nodeBuilder, KueryNode } from '../../../../../src/plugins/data/common'; import { esKuery } from '../../../../../src/plugins/data/server'; import { @@ -30,7 +29,6 @@ import { OWNER_FIELD, } from '../../common/api'; import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../common/constants'; -import { AuditEvent } from '../../../security/server'; import { combineFilterWithAuthorizationFilter } from '../authorization/utils'; import { getIDsAndIndicesAsArrays, @@ -38,9 +36,6 @@ import { isCommentRequestTypeUser, SavedObjectFindOptionsKueryNode, } from '../common'; -import { Authorization, DATABASE_CATEGORY, ECS_OUTCOMES, OperationDetails } from '../authorization'; -import { AuditLogger } from '../../../security/server'; -import { OwnerEntity } from './types'; export const decodeCommentRequest = (comment: CommentRequest) => { if (isCommentRequestTypeUser(comment)) { @@ -482,133 +477,3 @@ export const sortToSnake = (sortField: string | undefined): SortFieldCase => { return SortFieldCase.createdAt; } }; - -/** - * Creates an AuditEvent describing the state of a request. - */ -function createAuditMsg({ - operation, - outcome, - error, - savedObjectID, -}: { - operation: OperationDetails; - savedObjectID?: string; - outcome?: EcsEventOutcome; - error?: Error; -}): AuditEvent { - const doc = - savedObjectID != null - ? `${operation.savedObjectType} [id=${savedObjectID}]` - : `a ${operation.docType}`; - const message = error - ? `Failed attempt to ${operation.verbs.present} ${doc}` - : outcome === ECS_OUTCOMES.unknown - ? `User is ${operation.verbs.progressive} ${doc}` - : `User has ${operation.verbs.past} ${doc}`; - - return { - message, - event: { - action: operation.action, - category: DATABASE_CATEGORY, - type: [operation.type], - outcome: outcome ?? (error ? ECS_OUTCOMES.failure : ECS_OUTCOMES.success), - }, - ...(savedObjectID != null && { - kibana: { - saved_object: { type: operation.savedObjectType, id: savedObjectID }, - }, - }), - ...(error != null && { - error: { - code: error.name, - message: error.message, - }, - }), - }; -} - -/** - * Wraps the Authorization class' ensureAuthorized call in a try/catch to handle the audit logging - * on a failure. - */ -export async function ensureAuthorized({ - owners, - operation, - savedObjectIDs, - authorization, - auditLogger, -}: { - owners: string[]; - operation: OperationDetails; - savedObjectIDs: string[]; - authorization: PublicMethodsOf; - auditLogger?: AuditLogger; -}) { - const logSavedObjects = ({ outcome, error }: { outcome?: EcsEventOutcome; error?: Error }) => { - for (const savedObjectID of savedObjectIDs) { - auditLogger?.log(createAuditMsg({ operation, outcome, error, savedObjectID })); - } - }; - - try { - await authorization.ensureAuthorized(owners, operation); - - // log that we're attempting an operation - logSavedObjects({ outcome: ECS_OUTCOMES.unknown }); - } catch (error) { - logSavedObjects({ error }); - throw error; - } -} - -/** - * Function callback for making sure the found saved objects are of the authorized owner - */ -export type EnsureSOAuthCallback = (entities: OwnerEntity[]) => void; - -interface AuthFilterHelpers { - filter?: KueryNode; - ensureSavedObjectsAreAuthorized: EnsureSOAuthCallback; - logSuccessfulAuthorization: () => void; -} - -/** - * Wraps the Authorization class' method for determining which found saved objects the user making the request - * is authorized to interact with. - */ -export async function getAuthorizationFilter({ - operation, - authorization, - auditLogger, -}: { - operation: OperationDetails; - authorization: PublicMethodsOf; - auditLogger?: AuditLogger; -}): Promise { - try { - const { - filter, - ensureSavedObjectIsAuthorized, - logSuccessfulAuthorization, - } = await authorization.getFindAuthorizationFilter(operation); - return { - filter, - ensureSavedObjectsAreAuthorized: (entities: OwnerEntity[]) => { - for (const entity of entities) { - try { - ensureSavedObjectIsAuthorized(entity.owner); - auditLogger?.log(createAuditMsg({ operation, savedObjectID: entity.id })); - } catch (error) { - auditLogger?.log(createAuditMsg({ error, operation, savedObjectID: entity.id })); - } - } - }, - logSuccessfulAuthorization, - }; - } catch (error) { - auditLogger?.log(createAuditMsg({ error, operation })); - throw error; - } -} diff --git a/x-pack/plugins/cases/server/services/cases/index.ts b/x-pack/plugins/cases/server/services/cases/index.ts index 1cd5ded87d76ba..196314a0ecbfb5 100644 --- a/x-pack/plugins/cases/server/services/cases/index.ts +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -51,8 +51,9 @@ import { SUB_CASE_SAVED_OBJECT, } from '../../../common/constants'; import { ClientArgs } from '..'; -import { combineFilters, EnsureSOAuthCallback } from '../../client/utils'; +import { combineFilters } from '../../client/utils'; import { includeFieldsRequiredForAuthentication } from '../../authorization/utils'; +import { EnsureSOAuthCallback } from '../../authorization'; interface GetCaseIdsByAlertIdArgs extends ClientArgs { alertId: string; diff --git a/x-pack/plugins/security/server/authorization/actions/actions.mock.ts b/x-pack/plugins/security/server/authorization/actions/actions.mock.ts index 97890e21c0eb7e..ba627f08c00ca1 100644 --- a/x-pack/plugins/security/server/authorization/actions/actions.mock.ts +++ b/x-pack/plugins/security/server/authorization/actions/actions.mock.ts @@ -9,6 +9,7 @@ import type { Actions } from './actions'; import { AlertingActions } from './alerting'; import { ApiActions } from './api'; import { AppActions } from './app'; +import { CasesActions } from './cases'; import { SavedObjectActions } from './saved_object'; import { SpaceActions } from './space'; import { UIActions } from './ui'; @@ -19,6 +20,7 @@ jest.mock('./saved_object'); jest.mock('./space'); jest.mock('./ui'); jest.mock('./alerting'); +jest.mock('./cases'); const create = (versionNumber: string) => { const t = ({ @@ -27,6 +29,7 @@ const create = (versionNumber: string) => { login: 'login:', savedObject: new SavedObjectActions(versionNumber), alerting: new AlertingActions(versionNumber), + cases: new CasesActions(versionNumber), space: new SpaceActions(versionNumber), ui: new UIActions(versionNumber), version: `version:${versionNumber}`, From 9d8707881985379705876fe59e5963f346d8ddca Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 3 Jun 2021 12:37:42 -0600 Subject: [PATCH 098/113] fixing links --- .../public/components/case_view/index.tsx | 6 ++ .../components/use_push_to_service/index.tsx | 4 +- .../components/app/cases/all_cases/index.tsx | 41 +++++------- .../components/app/cases/case_view/index.tsx | 67 +++++++++---------- .../public/hooks/use_breadcrumbs.ts | 34 +++++++--- .../public/pages/cases/all_cases.tsx | 7 +- .../public/pages/cases/configure_cases.tsx | 22 ++++-- .../public/pages/cases/create_case.tsx | 8 ++- .../observability/public/pages/cases/links.ts | 2 +- .../observability/public/routes/index.tsx | 24 ++----- 10 files changed, 106 insertions(+), 109 deletions(-) diff --git a/x-pack/plugins/cases/public/components/case_view/index.tsx b/x-pack/plugins/cases/public/components/case_view/index.tsx index a71c2c84691bce..44d5bf11ce5897 100644 --- a/x-pack/plugins/cases/public/components/case_view/index.tsx +++ b/x-pack/plugins/cases/public/components/case_view/index.tsx @@ -14,6 +14,8 @@ import { EuiLoadingContent, EuiLoadingSpinner, EuiHorizontalRule, + EuiLink, + EuiButton, } from '@elastic/eui'; import { CaseStatuses, CaseAttributes, CaseType, Case, CaseConnector, Ecs } from '../../../common'; @@ -346,6 +348,10 @@ export const CaseComponent = React.memo( return ( <> + {`Click click motherfucker`} + {`Click click motherfucker`} + {i18n.LINK_CONNECTOR_CONFIGURE} ), diff --git a/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx b/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx index 52bc68e8fc49ef..8c013acb84bb99 100644 --- a/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback } from 'react'; +import React from 'react'; import { getCaseDetailsUrl, @@ -32,44 +32,33 @@ export const AllCases = React.memo(({ userCanCrud }) => { } = useKibana().services; const { formatUrl } = useFormatUrl(CASES_APP_ID); - const goToCreateCase = useCallback( - (ev) => { - ev.preventDefault(); - navigateToApp(`${CASES_APP_ID}`, { - path: getCreateCaseUrl(), - }); - }, - [navigateToApp] - ); - - const goToCaseConfigure = useCallback( - (ev) => { - ev.preventDefault(); - navigateToApp(`${CASES_APP_ID}`, { - path: getConfigureCasesUrl(), - }); - }, - [navigateToApp] - ); - return casesUi.getAllCases({ caseDetailsNavigation: { href: ({ detailName, subCaseId }: AllCasesNavProps) => { return formatUrl(getCaseDetailsUrl({ id: detailName, subCaseId })); }, - onClick: ({ detailName, subCaseId, search }: AllCasesNavProps) => { + onClick: async ({ detailName, subCaseId, search }: AllCasesNavProps) => navigateToApp(`${CASES_APP_ID}`, { path: getCaseDetailsUrl({ id: detailName, subCaseId }), - }); - }, + }), }, configureCasesNavigation: { href: formatUrl(getConfigureCasesUrl()), - onClick: goToCaseConfigure, + onClick: async (ev) => { + ev.preventDefault(); + return navigateToApp(`${CASES_APP_ID}`, { + path: getConfigureCasesUrl(), + }); + }, }, createCaseNavigation: { href: formatUrl(getCreateCaseUrl()), - onClick: goToCreateCase, + onClick: async (ev) => { + ev.preventDefault(); + return navigateToApp(`${CASES_APP_ID}`, { + path: getCreateCaseUrl(), + }); + }, }, disableAlerts: true, showTitle: false, diff --git a/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx b/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx index 31a08a71cd107a..5fbac989716c94 100644 --- a/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/case_view/index.tsx @@ -6,7 +6,6 @@ */ import React, { useCallback, useState } from 'react'; -import { useHistory } from 'react-router-dom'; import { getCaseDetailsUrl, getCaseDetailsUrlWithCommentId, @@ -18,7 +17,7 @@ import { Case } from '../../../../../../cases/common'; import { useFetchAlertData } from './helpers'; import { useKibana } from '../../../../utils/kibana_react'; import { CASES_APP_ID } from '../constants'; -import { casesBreadcrumb, useBreadcrumbs } from '../../../../hooks/use_breadcrumbs'; +import { casesBreadcrumbs, useBreadcrumbs } from '../../../../hooks/use_breadcrumbs'; interface Props { caseId: string; @@ -44,9 +43,11 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = const { cases: casesUi, application } = useKibana().services; const { navigateToApp } = application; - const href = application?.getUrlForApp('observability-cases') ?? ''; + const allCasesLink = getCaseUrl(); + const { formatUrl } = useFormatUrl(CASES_APP_ID); + const href = formatUrl(allCasesLink); useBreadcrumbs([ - { ...casesBreadcrumb, href }, + { ...casesBreadcrumbs.cases, href }, ...(caseTitle !== null ? [ { @@ -65,42 +66,33 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = [caseTitle] ); - const history = useHistory(); - const { formatUrl } = useFormatUrl(CASES_APP_ID); - - const allCasesLink = getCaseUrl(); - const formattedAllCasesLink = formatUrl(allCasesLink); - const backToAllCasesOnClick = useCallback( - (ev) => { - ev.preventDefault(); - history.push(allCasesLink); - }, - [allCasesLink, history] + const configureCasesLink = getConfigureCasesUrl(); + const allCasesHref = href; + const configureCasesHref = formatUrl(configureCasesLink); + const caseDetailsHref = formatUrl(getCaseDetailsUrl({ id: caseId }), { absolute: true }); + const getCaseDetailHrefWithCommentId = useCallback( + (commentId: string) => + formatUrl(getCaseDetailsUrlWithCommentId({ id: caseId, commentId, subCaseId }), { + absolute: true, + }), + [caseId, formatUrl, subCaseId] ); - const caseDetailsLink = formatUrl(getCaseDetailsUrl({ id: caseId }), { absolute: true }); - const getCaseDetailHrefWithCommentId = (commentId: string) => { - return formatUrl(getCaseDetailsUrlWithCommentId({ id: caseId, commentId, subCaseId }), { - absolute: true, - }); - }; - const configureCasesHref = formatUrl(getConfigureCasesUrl()); - const onConfigureCasesNavClick = useCallback( - (ev) => { - ev.preventDefault(); - history.push(getConfigureCasesUrl()); - }, - [history] - ); return casesUi.getCaseView({ allCasesNavigation: { - href: formattedAllCasesLink, - onClick: backToAllCasesOnClick, + href: allCasesHref, + onClick: async (e) => { + e.preventDefault(); + return navigateToApp(`${CASES_APP_ID}`, { + path: allCasesLink, + }); + }, }, caseDetailsNavigation: { - href: caseDetailsLink, - onClick: () => { - navigateToApp(`${CASES_APP_ID}`, { + href: caseDetailsHref, + onClick: async (e) => { + e.preventDefault(); + return navigateToApp(`${CASES_APP_ID}`, { path: getCaseDetailsUrl({ id: caseId }), }); }, @@ -108,7 +100,12 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = caseId, configureCasesNavigation: { href: configureCasesHref, - onClick: onConfigureCasesNavClick, + onClick: async (e) => { + e.preventDefault(); + return navigateToApp(`${CASES_APP_ID}`, { + path: configureCasesLink, + }); + }, }, getCaseDetailHrefWithCommentId, onCaseDataSuccess, diff --git a/x-pack/plugins/observability/public/hooks/use_breadcrumbs.ts b/x-pack/plugins/observability/public/hooks/use_breadcrumbs.ts index 595cc86f8d9fcf..090031e314fd1a 100644 --- a/x-pack/plugins/observability/public/hooks/use_breadcrumbs.ts +++ b/x-pack/plugins/observability/public/hooks/use_breadcrumbs.ts @@ -9,8 +9,8 @@ import { ChromeBreadcrumb } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { MouseEvent, useEffect } from 'react'; import { EuiBreadcrumb } from '@elastic/eui'; -import { useKibana } from '../../../../../src/plugins/kibana_react/public'; import { useQueryParams } from './use_query_params'; +import { useKibana } from '../utils/kibana_react'; function handleBreadcrumbClick( breadcrumbs: ChromeBreadcrumb[], @@ -39,23 +39,35 @@ export const makeBaseBreadcrumb = (href: string): EuiBreadcrumb => { href, }; }; - -export const casesBreadcrumb = { - text: i18n.translate('xpack.observability.breadcrumbs.observability.cases', { - defaultMessage: 'Cases', - }), +export const casesBreadcrumbs = { + cases: { + text: i18n.translate('xpack.observability.breadcrumbs.observability.cases', { + defaultMessage: 'Cases', + }), + }, + create: { + text: i18n.translate('xpack.observability.breadcrumbs.observability.cases.create', { + defaultMessage: 'Create', + }), + }, + configure: { + text: i18n.translate('xpack.observability.breadcrumbs.observability.cases.configure', { + defaultMessage: 'Configure', + }), + }, }; - export const useBreadcrumbs = (extraCrumbs: ChromeBreadcrumb[]) => { const params = useQueryParams(); const { - services: { chrome, application }, + services: { + chrome: { setBreadcrumbs }, + application: { getUrlForApp, navigateToUrl }, + }, } = useKibana(); - const setBreadcrumbs = chrome?.setBreadcrumbs; - const appPath = application?.getUrlForApp('observability-overview') ?? ''; - const navigate = application?.navigateToUrl; + const appPath = getUrlForApp('observability-overview') ?? ''; + const navigate = navigateToUrl; useEffect(() => { if (setBreadcrumbs) { diff --git a/x-pack/plugins/observability/public/pages/cases/all_cases.tsx b/x-pack/plugins/observability/public/pages/cases/all_cases.tsx index 4a1823d76fa711..0895acad282ab0 100644 --- a/x-pack/plugins/observability/public/pages/cases/all_cases.tsx +++ b/x-pack/plugins/observability/public/pages/cases/all_cases.tsx @@ -9,7 +9,6 @@ import React from 'react'; import { AllCases } from '../../components/app/cases/all_cases'; import * as i18n from '../../components/app/cases/translations'; -import { ExperimentalBadge } from '../../components/shared/experimental_badge'; import { savedObjectReadOnlyErrorMessage, CaseCallOut } from '../../components/app/cases/callout'; import { CaseFeatureNoPermissions } from './feature_no_permissions'; @@ -29,11 +28,7 @@ export const AllCasesPage = React.memo(() => { )} - {i18n.PAGE_TITLE} - - ), + pageTitle: <>{i18n.PAGE_TITLE}, }} > diff --git a/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx b/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx index 39e44cbdce6ef9..acc6bdf68fba75 100644 --- a/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx +++ b/x-pack/plugins/observability/public/pages/cases/configure_cases.tsx @@ -10,11 +10,12 @@ import styled from 'styled-components'; import { EuiButtonEmpty } from '@elastic/eui'; import * as i18n from '../../components/app/cases/translations'; -import { ExperimentalBadge } from '../../components/shared/experimental_badge'; import { CASES_APP_ID, CASES_OWNER } from '../../components/app/cases/constants'; import { useKibana } from '../../utils/kibana_react'; import { useGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions'; import { usePluginContext } from '../../hooks/use_plugin_context'; +import { casesBreadcrumbs, useBreadcrumbs } from '../../hooks/use_breadcrumbs'; +import { getCaseUrl, useFormatUrl } from './links'; const ButtonEmpty = styled(EuiButtonEmpty)` display: block; @@ -26,14 +27,16 @@ function ConfigureCasesPageComponent() { } = useKibana().services; const userPermissions = useGetUserCasesPermissions(); const { ObservabilityPageTemplate } = usePluginContext(); - const goTo = useCallback( - (ev) => { + const onClickGoToCases = useCallback( + async (ev) => { ev.preventDefault(); - navigateToApp(`${CASES_APP_ID}`); + return navigateToApp(`${CASES_APP_ID}`); }, [navigateToApp] ); - + const { formatUrl } = useFormatUrl(CASES_APP_ID); + const href = formatUrl(getCaseUrl()); + useBreadcrumbs([{ ...casesBreadcrumbs.cases, href }, casesBreadcrumbs.configure]); if (userPermissions != null && !userPermissions.read) { navigateToApp(`${CASES_APP_ID}`); return null; @@ -44,10 +47,15 @@ function ConfigureCasesPageComponent() { pageHeader={{ pageTitle: ( <> - + {i18n.BACK_TO_ALL} - {i18n.CONFIGURE_CASES_PAGE_TITLE} + {i18n.CONFIGURE_CASES_PAGE_TITLE} ), }} diff --git a/x-pack/plugins/observability/public/pages/cases/create_case.tsx b/x-pack/plugins/observability/public/pages/cases/create_case.tsx index 8e3997da5184ab..c325372abefeb4 100644 --- a/x-pack/plugins/observability/public/pages/cases/create_case.tsx +++ b/x-pack/plugins/observability/public/pages/cases/create_case.tsx @@ -10,11 +10,12 @@ import { EuiButtonEmpty } from '@elastic/eui'; import styled from 'styled-components'; import * as i18n from '../../components/app/cases/translations'; import { Create } from '../../components/app/cases/create'; -import { ExperimentalBadge } from '../../components/shared/experimental_badge'; import { CASES_APP_ID } from '../../components/app/cases/constants'; import { useKibana } from '../../utils/kibana_react'; import { useGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions'; import { usePluginContext } from '../../hooks/use_plugin_context'; +import { getCaseUrl, useFormatUrl } from './links'; +import { casesBreadcrumbs, useBreadcrumbs } from '../../hooks/use_breadcrumbs'; const ButtonEmpty = styled(EuiButtonEmpty)` display: block; @@ -35,6 +36,9 @@ export const CreateCasePage = React.memo(() => { [navigateToApp] ); + const { formatUrl } = useFormatUrl(CASES_APP_ID); + const href = formatUrl(getCaseUrl()); + useBreadcrumbs([{ ...casesBreadcrumbs.cases, href }, casesBreadcrumbs.create]); if (userPermissions != null && !userPermissions.crud) { navigateToApp(`${CASES_APP_ID}`); return null; @@ -48,7 +52,7 @@ export const CreateCasePage = React.memo(() => { {i18n.BACK_TO_ALL} - {i18n.CREATE_TITLE} + {i18n.CREATE_TITLE} ), }} diff --git a/x-pack/plugins/observability/public/pages/cases/links.ts b/x-pack/plugins/observability/public/pages/cases/links.ts index b922ace15e1ea9..768d74ec4e7ee3 100644 --- a/x-pack/plugins/observability/public/pages/cases/links.ts +++ b/x-pack/plugins/observability/public/pages/cases/links.ts @@ -56,4 +56,4 @@ export const getCaseDetailsUrlWithCommentId = ({ export const getCreateCaseUrl = () => `/create`; export const getConfigureCasesUrl = () => `/configure`; -export const getCaseUrl = () => `/cases`; +export const getCaseUrl = () => `/`; diff --git a/x-pack/plugins/observability/public/routes/index.tsx b/x-pack/plugins/observability/public/routes/index.tsx index a56cdfa9747005..a2a67a42bd166a 100644 --- a/x-pack/plugins/observability/public/routes/index.tsx +++ b/x-pack/plugins/observability/public/routes/index.tsx @@ -18,7 +18,7 @@ import { ExploratoryViewPage } from '../components/shared/exploratory_view'; import { CaseDetailsPage } from '../pages/cases/case_details'; import { ConfigureCasesPage } from '../pages/cases/configure_cases'; import { AllCasesPage } from '../pages/cases/all_cases'; -import { casesBreadcrumb } from '../hooks/use_breadcrumbs'; +import { casesBreadcrumbs } from '../hooks/use_breadcrumbs'; import { alertStatusRt } from '../../common/typings'; export type RouteParams = DecodeParams; @@ -86,35 +86,21 @@ export const routes = { return ; }, params: {}, - breadcrumb: [casesBreadcrumb], + breadcrumb: [casesBreadcrumbs.cases], }, '/cases/create': { handler: () => { return ; }, params: {}, - breadcrumb: [ - casesBreadcrumb, - { - text: i18n.translate('xpack.observability.breadcrumbs.observability.cases.create', { - defaultMessage: 'Create', - }), - }, - ], + breadcrumb: [casesBreadcrumbs.cases, casesBreadcrumbs.create], }, '/cases/configure': { handler: () => { return ; }, params: {}, - breadcrumb: [ - casesBreadcrumb, - { - text: i18n.translate('xpack.observability.breadcrumbs.observability.cases.configure', { - defaultMessage: 'Configure', - }), - }, - ], + breadcrumb: [casesBreadcrumbs.cases, casesBreadcrumbs.configure], }, '/cases/:detailName': { handler: () => { @@ -125,7 +111,7 @@ export const routes = { detailName: t.string, }), }, - breadcrumb: [casesBreadcrumb], + breadcrumb: [casesBreadcrumbs.cases], }, '/alerts': { handler: (routeParams: any) => { From e66eb40655084675d2d984c780b5798f3c795ba3 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Fri, 4 Jun 2021 14:05:15 -0400 Subject: [PATCH 099/113] [Cases] Cleaning up RBAC integration tests (#101324) * Adding tests for space permissions * Adding tests for testing a disable feature --- .../security_solution/server/plugin.ts | 33 +++++++++++++++++++ .../common/lib/authentication/roles.ts | 26 +++++++++++++++ .../common/lib/authentication/spaces.ts | 2 +- .../common/lib/authentication/users.ts | 8 +++++ .../case_api_integration/common/lib/utils.ts | 8 ++++- .../tests/common/cases/delete_cases.ts | 25 +++++++++++--- .../tests/common/cases/post_case.ts | 17 ++++++++++ .../tests/common/comments/delete_comment.ts | 31 +++++++++++++++++ .../tests/common/configure/patch_configure.ts | 26 +++++++++++++++ .../tests/common/configure/post_configure.ts | 17 ++++++++++ .../tests/common/comments/get_comment.ts | 2 +- 11 files changed, 188 insertions(+), 7 deletions(-) diff --git a/x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/server/plugin.ts b/x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/server/plugin.ts index 9083d65e9a3495..bd3569ef528164 100644 --- a/x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/server/plugin.ts +++ b/x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/server/plugin.ts @@ -54,6 +54,39 @@ export class FixturePlugin implements Plugin & { + overrides?: Record; +}; + export const getConfigurationRequest = ({ id = 'none', name = 'none', type = ConnectorTypes.none, fields = null, -}: Partial = {}): CasesConfigureRequest => { + overrides, +}: ConfigRequestParams = {}): CasesConfigureRequest => { return { connector: { id, @@ -288,6 +293,7 @@ export const getConfigurationRequest = ({ } as CaseConnector, closure_type: 'close-by-user', owner: 'securitySolutionFixture', + ...overrides, }; }; 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 bbb9624c4b14be..964e9135aba7b7 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 @@ -285,10 +285,27 @@ export default ({ getService }: FtrProviderContext): void => { ); /** - * We expect a 404 because the bulkGet inside the delete - * route should return a 404 when requesting a case from - * a different space. - * */ + * secOnly does not have access to space2 so it should 403 + */ + await deleteCases({ + supertest: supertestWithoutAuth, + caseIDs: [postedCase.id], + expectedHttpCode: 403, + auth: { user: secOnly, space: 'space2' }, + }); + }); + + it('should NOT delete a case created in space2 by making a request to space1', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space2', + } + ); + await deleteCases({ supertest: supertestWithoutAuth, caseIDs: [postedCase.id], 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 787ce533dbaf4d..e8337fa9db5023 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 @@ -32,6 +32,7 @@ import { obsOnlyRead, obsSecRead, noKibanaPrivileges, + testDisabled, } from '../../../../common/lib/authentication/users'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; @@ -240,6 +241,22 @@ export default ({ getService }: FtrProviderContext): void => { }); describe('rbac', () => { + it('returns a 403 when attempting to create a case with an owner that was from a disabled feature in the space', async () => { + const theCase = ((await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'testDisabledFixture' }), + 403, + { + user: testDisabled, + space: 'space1', + } + )) as unknown) as { message: string }; + + expect(theCase.message).to.eql( + 'Unauthorized to create case with owners: "testDisabledFixture"' + ); + }); + it('User: security solution only - should create a case', async () => { const theCase = await createCase( supertestWithoutAuth, diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts index 3336abfa47e7c4..fc0b62ff924b51 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts @@ -320,6 +320,37 @@ export default ({ getService }: FtrProviderContext): void => { auth: { user: superUser, space: 'space2' }, }); + await deleteComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + commentId: commentResp.comments![0].id, + auth: { user: secOnly, space: 'space2' }, + expectedHttpCode: 403, + }); + + await deleteAllComments({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + auth: { user: secOnly, space: 'space2' }, + expectedHttpCode: 403, + }); + }); + + it('should NOT delete a comment created in space2 by making a request to space1', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space2' } + ); + + const commentResp = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: { user: superUser, space: 'space2' }, + }); + await deleteComment({ supertest: supertestWithoutAuth, caseId: postedCase.id, 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 ced727f8e4e75e..323b1b377e5555 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 @@ -224,6 +224,32 @@ export default ({ getService }: FtrProviderContext): void => { } ); + await updateConfiguration( + supertestWithoutAuth, + configuration.id, + { + closure_type: 'close-by-pushing', + version: configuration.version, + }, + 403, + { + user: secOnly, + space: 'space2', + } + ); + }); + + it('should NOT update a configuration created in space2 by making a request to space1', async () => { + const configuration = await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, + 200, + { + user: superUser, + space: 'space2', + } + ); + await updateConfiguration( supertestWithoutAuth, configuration.id, 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 f1dae9f319109b..44ec24f688f201 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 @@ -28,6 +28,7 @@ import { globalRead, obsSecRead, superUser, + testDisabled, } from '../../../../common/lib/authentication/users'; // eslint-disable-next-line import/no-default-export @@ -196,6 +197,22 @@ export default ({ getService }: FtrProviderContext): void => { }); describe('rbac', () => { + it('returns a 403 when attempting to create a configuration with an owner that was from a disabled feature in the space', async () => { + const configuration = ((await createConfiguration( + supertestWithoutAuth, + getConfigurationRequest({ overrides: { owner: 'testDisabledFixture' } }), + 403, + { + user: testDisabled, + space: 'space1', + } + )) as unknown) as { message: string }; + + expect(configuration.message).to.eql( + 'Unauthorized to create case configuration with owners: "testDisabledFixture"' + ); + }); + it('User: security solution only - should create a configuration', async () => { const configuration = await createConfiguration( supertestWithoutAuth, diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/comments/get_comment.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/get_comment.ts index b53b2e6e59cfb4..048700993087db 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/comments/get_comment.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/get_comment.ts @@ -46,7 +46,7 @@ export default ({ getService }: FtrProviderContext): void => { expect(comment).to.eql(patchedCase.comments![0]); }); - it('should not get a comment in space2', async () => { + it('should not get a comment in space2 when it was created in space1', async () => { const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); const patchedCase = await createComment({ supertest, From 6611765080759bf86843bf344497bf4429582d11 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 7 Jun 2021 08:11:36 -0600 Subject: [PATCH 100/113] remove testing links --- x-pack/plugins/cases/public/components/case_view/index.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/x-pack/plugins/cases/public/components/case_view/index.tsx b/x-pack/plugins/cases/public/components/case_view/index.tsx index 44d5bf11ce5897..54b3312491ede0 100644 --- a/x-pack/plugins/cases/public/components/case_view/index.tsx +++ b/x-pack/plugins/cases/public/components/case_view/index.tsx @@ -348,10 +348,6 @@ export const CaseComponent = React.memo( return ( <> - {`Click click motherfucker`} - {`Click click motherfucker`} Date: Mon, 7 Jun 2021 08:57:12 -0600 Subject: [PATCH 101/113] add comments --- x-pack/plugins/cases/public/components/case_view/index.tsx | 2 -- .../public/components/app/cases/create/flyout.tsx | 5 ++++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/cases/public/components/case_view/index.tsx b/x-pack/plugins/cases/public/components/case_view/index.tsx index 54b3312491ede0..a71c2c84691bce 100644 --- a/x-pack/plugins/cases/public/components/case_view/index.tsx +++ b/x-pack/plugins/cases/public/components/case_view/index.tsx @@ -14,8 +14,6 @@ import { EuiLoadingContent, EuiLoadingSpinner, EuiHorizontalRule, - EuiLink, - EuiButton, } from '@elastic/eui'; import { CaseStatuses, CaseAttributes, CaseType, Case, CaseConnector, Ecs } from '../../../common'; diff --git a/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx b/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx index 078174f9bc1e22..df29d02e8d830e 100644 --- a/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx @@ -27,6 +27,7 @@ const StyledFlyout = styled(EuiFlyout)` `; // Adding bottom padding because timeline's // bottom bar gonna hide the submit button. +// might not need for obs, test this when implementing this component const StyledEuiFlyoutBody = styled(EuiFlyoutBody)` ${({ theme }) => ` && .euiFlyoutBody__overflow { @@ -45,6 +46,7 @@ const StyledEuiFlyoutBody = styled(EuiFlyoutBody)` const FormWrapper = styled.div` width: 100%; `; + function CreateCaseFlyoutComponent({ afterCaseCreated, onCloseFlyout, @@ -72,7 +74,8 @@ function CreateCaseFlyoutComponent({ ); } - +// not yet used +// committing for use with alerting #RAC export const CreateCaseFlyout = memo(CreateCaseFlyoutComponent); CreateCaseFlyout.displayName = 'CreateCaseFlyout'; From 831dedbe6be54e69b5b77e4bb806ffad40ab7926 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 7 Jun 2021 12:19:51 -0600 Subject: [PATCH 102/113] pr changes and remove unnecessary vars --- .../case_action_bar/actions.test.tsx | 33 ++++-------- .../components/case_action_bar/index.test.tsx | 2 +- .../cases/public/containers/api.test.tsx | 4 +- x-pack/plugins/observability/kibana.json | 14 ++--- .../components/app/cases/__mock__/form.ts | 52 ------------------- .../components/app/cases/__mock__/router.ts | 40 -------------- .../app/cases/callout/callout.test.tsx | 4 +- .../app/cases/case_view/translations.ts | 14 ----- .../components/app/cases/translations.ts | 12 ----- .../public/pages/cases/create_case.tsx | 4 +- 10 files changed, 25 insertions(+), 154 deletions(-) delete mode 100644 x-pack/plugins/observability/public/components/app/cases/__mock__/form.ts delete mode 100644 x-pack/plugins/observability/public/components/app/cases/__mock__/router.ts delete mode 100644 x-pack/plugins/observability/public/components/app/cases/case_view/translations.ts diff --git a/x-pack/plugins/cases/public/components/case_action_bar/actions.test.tsx b/x-pack/plugins/cases/public/components/case_action_bar/actions.test.tsx index ac64f9878b5532..ed8e238db75e75 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/actions.test.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/actions.test.tsx @@ -26,7 +26,14 @@ jest.mock('react-router-dom', () => { }), }; }); - +const defaultProps = { + allCasesNavigation: { + href: 'all-cases-href', + onClick: () => {}, + }, + caseData: basicCase, + currentExternalIncident: null, +}; describe('CaseView actions', () => { const handleOnDeleteConfirm = jest.fn(); const handleToggleModal = jest.fn(); @@ -49,14 +56,7 @@ describe('CaseView actions', () => { it('clicking trash toggles modal', () => { const wrapper = mount( - + ); @@ -74,14 +74,7 @@ describe('CaseView actions', () => { })); const wrapper = mount( - + ); @@ -96,11 +89,7 @@ describe('CaseView actions', () => { const wrapper = mount( { const defaultProps = { allCasesNavigation: { href: 'all-cases-href', - onClick: jest.fn(), + onClick: () => {}, }, caseData: basicCase, isAlerting: true, diff --git a/x-pack/plugins/cases/public/containers/api.test.tsx b/x-pack/plugins/cases/public/containers/api.test.tsx index c706a3ae2966c9..f9e128e7f713d7 100644 --- a/x-pack/plugins/cases/public/containers/api.test.tsx +++ b/x-pack/plugins/cases/public/containers/api.test.tsx @@ -161,7 +161,7 @@ describe('Case Configuration API', () => { query: { ...DEFAULT_QUERY_PARAMS, reporters, - tags: ['"coke"', '"pepsi"'], + tags: ['coke', 'pepsi'], search: 'hello', status: CaseStatuses.open, owner: [SECURITY_SOLUTION_OWNER], @@ -190,7 +190,7 @@ describe('Case Configuration API', () => { query: { ...DEFAULT_QUERY_PARAMS, reporters, - tags: ['coke', 'pepsi'], + tags: ['(', '"double"'], search: 'hello', status: CaseStatuses.open, owner: [SECURITY_SOLUTION_OWNER], diff --git a/x-pack/plugins/observability/kibana.json b/x-pack/plugins/observability/kibana.json index 773cd5cd14f1a9..d13140f0be16ce 100644 --- a/x-pack/plugins/observability/kibana.json +++ b/x-pack/plugins/observability/kibana.json @@ -7,18 +7,18 @@ "observability" ], "optionalPlugins": [ - "licensing", "home", - "usageCollection", - "lens" + "lens", + "licensing", + "usageCollection" ], "requiredPlugins": [ - "data", "alerting", - "ruleRegistry", - "triggersActionsUi", "cases", - "features" + "data", + "features", + "ruleRegistry", + "triggersActionsUi" ], "ui": true, "server": true, diff --git a/x-pack/plugins/observability/public/components/app/cases/__mock__/form.ts b/x-pack/plugins/observability/public/components/app/cases/__mock__/form.ts deleted file mode 100644 index 5fd62410d9954a..00000000000000 --- a/x-pack/plugins/observability/public/components/app/cases/__mock__/form.ts +++ /dev/null @@ -1,52 +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 { useForm } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'; -import { useFormData } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data'; - -jest.mock( - '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' -); -jest.mock( - '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data' -); - -export const mockFormHook = { - isSubmitted: false, - isSubmitting: false, - isValid: true, - submit: jest.fn(), - subscribe: jest.fn(), - setFieldValue: jest.fn(), - setFieldErrors: jest.fn(), - getFields: jest.fn(), - getFormData: jest.fn(), - /* Returns a list of all errors in the form */ - getErrors: jest.fn(), - reset: jest.fn(), - __options: {}, - __formData$: {}, - __addField: jest.fn(), - __removeField: jest.fn(), - __validateFields: jest.fn(), - __updateFormDataAt: jest.fn(), - __readFieldConfigFromSchema: jest.fn(), - __getFieldDefaultValue: jest.fn(), -}; - -export const getFormMock = (sampleData: any) => ({ - ...mockFormHook, - submit: () => - Promise.resolve({ - data: sampleData, - isValid: true, - }), - getFormData: () => sampleData, -}); - -export const useFormMock = useForm as jest.Mock; -export const useFormDataMock = useFormData as jest.Mock; diff --git a/x-pack/plugins/observability/public/components/app/cases/__mock__/router.ts b/x-pack/plugins/observability/public/components/app/cases/__mock__/router.ts deleted file mode 100644 index 58b7bb0ac26883..00000000000000 --- a/x-pack/plugins/observability/public/components/app/cases/__mock__/router.ts +++ /dev/null @@ -1,40 +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 { Router } from 'react-router-dom'; -// eslint-disable-next-line @kbn/eslint/module_migration -import routeData from 'react-router'; -type Action = 'PUSH' | 'POP' | 'REPLACE'; -const pop: Action = 'POP'; -const location = { - pathname: '/network', - search: '', - state: '', - hash: '', -}; -export const mockHistory = { - length: 2, - location, - action: pop, - push: jest.fn(), - replace: jest.fn(), - go: jest.fn(), - goBack: jest.fn(), - goForward: jest.fn(), - block: jest.fn(), - createHref: jest.fn(), - listen: jest.fn(), -}; - -export const mockLocation = { - pathname: '/welcome', - hash: '', - search: '', - state: '', -}; - -export { Router, routeData }; diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/callout.test.tsx b/x-pack/plugins/observability/public/components/app/cases/callout/callout.test.tsx index 926fe7b63fb5af..cf50a579fca3ac 100644 --- a/x-pack/plugins/observability/public/components/app/cases/callout/callout.test.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/callout/callout.test.tsx @@ -30,7 +30,7 @@ describe('Callout', () => { jest.clearAllMocks(); }); - it('It renders the callout', () => { + it('renders the callout', () => { const wrapper = mount(); expect(wrapper.find(`[data-test-subj="case-callout-md5-hex"]`).exists()).toBeTruthy(); expect(wrapper.find(`[data-test-subj="callout-messages-md5-hex"]`).exists()).toBeTruthy(); @@ -42,7 +42,7 @@ describe('Callout', () => { expect(wrapper.find(`[data-test-subj="case-callout-md5-hex"]`).exists()).toBeFalsy(); }); - it('does not shows any messages when the list is empty', () => { + it('does not show any messages when the list is empty', () => { const wrapper = mount(); expect(wrapper.find(`[data-test-subj="callout-messages-md5-hex"]`).exists()).toBeFalsy(); }); diff --git a/x-pack/plugins/observability/public/components/app/cases/case_view/translations.ts b/x-pack/plugins/observability/public/components/app/cases/case_view/translations.ts deleted file mode 100644 index f734ff8e3be7ec..00000000000000 --- a/x-pack/plugins/observability/public/components/app/cases/case_view/translations.ts +++ /dev/null @@ -1,14 +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 { i18n } from '@kbn/i18n'; -export const SEND_ALERT_TO_TIMELINE = i18n.translate( - 'xpack.observability.cases.caseView.sendAlertToTimelineTooltip', - { - defaultMessage: 'Investigate in timeline', - } -); diff --git a/x-pack/plugins/observability/public/components/app/cases/translations.ts b/x-pack/plugins/observability/public/components/app/cases/translations.ts index 243e444c01328a..1a5abe218edf52 100644 --- a/x-pack/plugins/observability/public/components/app/cases/translations.ts +++ b/x-pack/plugins/observability/public/components/app/cases/translations.ts @@ -47,14 +47,6 @@ export const NAME = i18n.translate('xpack.observability.cases.caseView.name', { defaultMessage: 'Name', }); -export const OPENED_ON = i18n.translate('xpack.observability.cases.caseView.openedOn', { - defaultMessage: 'Opened on', -}); - -export const CLOSED_ON = i18n.translate('xpack.observability.cases.caseView.closedOn', { - defaultMessage: 'Closed on', -}); - export const REPORTER = i18n.translate('xpack.observability.cases.caseView.reporterLabel', { defaultMessage: 'Reporter', }); @@ -63,10 +55,6 @@ export const PARTICIPANTS = i18n.translate('xpack.observability.cases.caseView.p defaultMessage: 'Participants', }); -export const CREATE_BC_TITLE = i18n.translate('xpack.observability.cases.caseView.breadcrumb', { - defaultMessage: 'Create', -}); - export const CREATE_TITLE = i18n.translate('xpack.observability.cases.caseView.create', { defaultMessage: 'Create new case', }); diff --git a/x-pack/plugins/observability/public/pages/cases/create_case.tsx b/x-pack/plugins/observability/public/pages/cases/create_case.tsx index c325372abefeb4..d0e25e6263075b 100644 --- a/x-pack/plugins/observability/public/pages/cases/create_case.tsx +++ b/x-pack/plugins/observability/public/pages/cases/create_case.tsx @@ -29,9 +29,9 @@ export const CreateCasePage = React.memo(() => { } = useKibana().services; const goTo = useCallback( - (ev) => { + async (ev) => { ev.preventDefault(); - navigateToApp(`${CASES_APP_ID}`); + return navigateToApp(CASES_APP_ID); }, [navigateToApp] ); From 511f3f7c0be2e69b7ae88404ceaf0f13bed908d5 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 9 Jun 2021 07:56:18 -0600 Subject: [PATCH 103/113] pr changes and WIP func tests --- test/functional/config.js | 3 + .../components/case_action_bar/index.tsx | 6 +- .../components/case_view/does_not_exist.tsx | 5 +- .../public/components/case_view/index.tsx | 2 +- x-pack/plugins/observability/common/const.ts | 9 + .../public/components/app/cases/constants.ts | 4 +- .../hooks/use_get_user_cases_permissions.tsx | 9 +- .../public/pages/cases/empty_page.tsx | 2 +- x-pack/plugins/observability/server/plugin.ts | 25 +- .../observability/feature_controls/index.ts | 15 ++ .../observability_security.ts | 235 ++++++++++++++++++ .../functional/apps/observability/index.ts | 15 ++ x-pack/test/functional/config.js | 2 + x-pack/test/functional/page_objects/index.ts | 2 + .../page_objects/observability_page.ts | 121 +++++++++ 15 files changed, 425 insertions(+), 30 deletions(-) create mode 100644 x-pack/plugins/observability/common/const.ts create mode 100644 x-pack/test/functional/apps/observability/feature_controls/index.ts create mode 100644 x-pack/test/functional/apps/observability/feature_controls/observability_security.ts create mode 100644 x-pack/test/functional/apps/observability/index.ts create mode 100644 x-pack/test/functional/page_objects/observability_page.ts diff --git a/test/functional/config.js b/test/functional/config.js index 4a6791a3bc62fb..eac21e5a456184 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -97,6 +97,9 @@ export default async function ({ readConfigFile }) { pathname: '/app/home', hash: '/', }, + observabilityCases: { + pathname: '/app/observability/cases', + }, }, junit: { reportName: 'Chrome UI Functional Tests', diff --git a/x-pack/plugins/cases/public/components/case_action_bar/index.tsx b/x-pack/plugins/cases/public/components/case_action_bar/index.tsx index 3b7c043f48be23..d8e012b0721065 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/index.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/index.tsx @@ -41,7 +41,7 @@ interface CaseActionBarProps { caseData: Case; currentExternalIncident: CaseService | null; disabled?: boolean; - isAlerting: boolean; + disableAlerting: boolean; isLoading: boolean; onRefresh: () => void; onUpdateField: (args: OnUpdateFields) => void; @@ -51,7 +51,7 @@ const CaseActionBarComponent: React.FC = ({ caseData, currentExternalIncident, disabled = false, - isAlerting, + disableAlerting, isLoading, onRefresh, onUpdateField, @@ -108,7 +108,7 @@ const CaseActionBarComponent: React.FC = ({ - {isAlerting && ( + {!disableAlerting && ( diff --git a/x-pack/plugins/cases/public/components/case_view/does_not_exist.tsx b/x-pack/plugins/cases/public/components/case_view/does_not_exist.tsx index 094acb4bc0d5e6..627c90a45aa66f 100644 --- a/x-pack/plugins/cases/public/components/case_view/does_not_exist.tsx +++ b/x-pack/plugins/cases/public/components/case_view/does_not_exist.tsx @@ -10,14 +10,15 @@ import React from 'react'; import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; import * as i18n from './translations'; import { CasesNavigation } from '../links'; + interface Props { allCasesNavigation: CasesNavigation; caseId: string; } + export const DoesNotExist = ({ allCasesNavigation, caseId }: Props) => ( {i18n.DOES_NOT_EXIST_TITLE}} titleSize="xs" diff --git a/x-pack/plugins/cases/public/components/case_view/index.tsx b/x-pack/plugins/cases/public/components/case_view/index.tsx index a71c2c84691bce..d205a8249c4dd5 100644 --- a/x-pack/plugins/cases/public/components/case_view/index.tsx +++ b/x-pack/plugins/cases/public/components/case_view/index.tsx @@ -364,7 +364,7 @@ export const CaseComponent = React.memo( caseData={caseData} currentExternalIncident={currentExternalIncident} disabled={!userCanCrud} - isAlerting={ruleDetailsNavigation != null} + disableAlerting={ruleDetailsNavigation == null} isLoading={isLoading && (updateKey === 'status' || updateKey === 'settings')} onRefresh={handleRefresh} onUpdateField={onUpdateField} diff --git a/x-pack/plugins/observability/common/const.ts b/x-pack/plugins/observability/common/const.ts new file mode 100644 index 00000000000000..43fd5d213b4a66 --- /dev/null +++ b/x-pack/plugins/observability/common/const.ts @@ -0,0 +1,9 @@ +/* + * 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 const CASES_APP_ID = 'observability-cases'; +export const OBSERVABILITY = 'observability'; diff --git a/x-pack/plugins/observability/public/components/app/cases/constants.ts b/x-pack/plugins/observability/public/components/app/cases/constants.ts index 82b76b082101c8..3c1f868fec084f 100644 --- a/x-pack/plugins/observability/public/components/app/cases/constants.ts +++ b/x-pack/plugins/observability/public/components/app/cases/constants.ts @@ -5,5 +5,7 @@ * 2.0. */ -export const CASES_APP_ID = 'observability-cases'; +import { CASES_APP_ID } from '../../../../common/const'; + +export { CASES_APP_ID }; export const CASES_OWNER = 'observability'; diff --git a/x-pack/plugins/observability/public/hooks/use_get_user_cases_permissions.tsx b/x-pack/plugins/observability/public/hooks/use_get_user_cases_permissions.tsx index 69858ccde3df97..9f4ed59a45f2b6 100644 --- a/x-pack/plugins/observability/public/hooks/use_get_user_cases_permissions.tsx +++ b/x-pack/plugins/observability/public/hooks/use_get_user_cases_permissions.tsx @@ -7,6 +7,7 @@ import { useEffect, useState } from 'react'; import { useKibana } from '../utils/kibana_react'; +import { CASES_APP_ID } from '../../common/const'; export interface UseGetUserCasesPermissions { crud: boolean; @@ -19,12 +20,12 @@ export function useGetUserCasesPermissions() { useEffect(() => { const capabilitiesCanUserCRUD: boolean = - typeof uiCapabilities.observabilityCases.crud_cases === 'boolean' - ? uiCapabilities.observabilityCases.crud_cases + typeof uiCapabilities[CASES_APP_ID].crud_cases === 'boolean' + ? (uiCapabilities[CASES_APP_ID].crud_cases as boolean) : false; const capabilitiesCanUserRead: boolean = - typeof uiCapabilities.observabilityCases.read_cases === 'boolean' - ? uiCapabilities.observabilityCases.read_cases + typeof uiCapabilities[CASES_APP_ID].read_cases === 'boolean' + ? (uiCapabilities[CASES_APP_ID].read_cases as boolean) : false; setCasesPermissions({ crud: capabilitiesCanUserCRUD, diff --git a/x-pack/plugins/observability/public/pages/cases/empty_page.tsx b/x-pack/plugins/observability/public/pages/cases/empty_page.tsx index ac691d8be2bd5f..c6fc4b59ef77c3 100644 --- a/x-pack/plugins/observability/public/pages/cases/empty_page.tsx +++ b/x-pack/plugins/observability/public/pages/cases/empty_page.tsx @@ -103,7 +103,7 @@ const EmptyPageComponent = React.memo(({ actions, message, title return ( {title}} body={message &&

{message}

} actions={{renderActions}} diff --git a/x-pack/plugins/observability/server/plugin.ts b/x-pack/plugins/observability/server/plugin.ts index e6801a7e285944..cfcaeb25d29e40 100644 --- a/x-pack/plugins/observability/server/plugin.ts +++ b/x-pack/plugins/observability/server/plugin.ts @@ -24,6 +24,7 @@ import { PluginSetupContract as FeaturesSetup } from '../../features/server'; import { uiSettings } from './ui_settings'; import { registerRoutes } from './routes/register_routes'; import { getGlobalObservabilityServerRouteRepository } from './routes/get_global_observability_server_route_repository'; +import { CASES_APP_ID, OBSERVABILITY } from '../common/const'; export type ObservabilityPluginSetup = ReturnType; @@ -32,9 +33,6 @@ interface PluginSetup { ruleRegistry: RuleRegistryPluginSetupContract; } -const OBS_CASES_ID = 'observabilityCases'; -const OBSERVABILITY = 'observability'; - export class ObservabilityPlugin implements Plugin { constructor(private readonly initContext: PluginInitializerContext) { this.initContext = initContext; @@ -42,21 +40,18 @@ export class ObservabilityPlugin implements Plugin { public setup(core: CoreSetup, plugins: PluginSetup) { plugins.features.registerKibanaFeature({ - id: OBS_CASES_ID, + id: CASES_APP_ID, name: i18n.translate('xpack.observability.featureRegistry.linkObservabilityTitle', { defaultMessage: 'Cases', }), order: 1100, category: DEFAULT_APP_CATEGORIES.observability, - app: [OBS_CASES_ID, 'kibana'], + app: [CASES_APP_ID, 'kibana'], catalogue: [OBSERVABILITY], cases: [OBSERVABILITY], - management: { - insightsAndAlerting: ['triggersActions'], - }, privileges: { all: { - app: [OBS_CASES_ID, 'kibana'], + app: [CASES_APP_ID, 'kibana'], catalogue: [OBSERVABILITY], cases: { all: [OBSERVABILITY], @@ -66,13 +61,10 @@ export class ObservabilityPlugin implements Plugin { all: [], read: [], }, - management: { - insightsAndAlerting: ['triggersActions'], - }, - ui: ['crud_cases', 'read_cases'], // uiCapabilities.observabilityCases.crud_cases or read_cases + ui: ['crud_cases', 'read_cases'], // uiCapabilities[CASES_APP_ID].crud_cases or read_cases }, read: { - app: [OBS_CASES_ID, 'kibana'], + app: [CASES_APP_ID, 'kibana'], catalogue: [OBSERVABILITY], cases: { read: [OBSERVABILITY], @@ -82,10 +74,7 @@ export class ObservabilityPlugin implements Plugin { all: [], read: [], }, - management: { - insightsAndAlerting: ['triggersActions'], - }, - ui: ['read_cases'], // uiCapabilities.observabilityCases.read_cases + ui: ['read_cases'], // uiCapabilities[uiCapabilities[CASES_APP_ID]].read_cases }, }, }); diff --git a/x-pack/test/functional/apps/observability/feature_controls/index.ts b/x-pack/test/functional/apps/observability/feature_controls/index.ts new file mode 100644 index 00000000000000..53fba574134438 --- /dev/null +++ b/x-pack/test/functional/apps/observability/feature_controls/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('feature controls', function () { + this.tags('skipFirefox'); + loadTestFile(require.resolve('./observability_security')); + }); +} diff --git a/x-pack/test/functional/apps/observability/feature_controls/observability_security.ts b/x-pack/test/functional/apps/observability/feature_controls/observability_security.ts new file mode 100644 index 00000000000000..e09eb4aa34ae61 --- /dev/null +++ b/x-pack/test/functional/apps/observability/feature_controls/observability_security.ts @@ -0,0 +1,235 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects([ + 'common', + 'observability', + 'error', + 'security', + 'spaceSelector', + ]); + const appsMenu = getService('appsMenu'); + const globalNav = getService('globalNav'); + const testSubjects = getService('testSubjects'); + describe('wowzeroni', function () { + this.tags(['skipFirefox']); + // before(async () => { + // await esArchiver.load('uptimw/default'); + // }); + // + // after(async () => { + // await esArchiver.unload('uptimw/default'); + // }); + + describe('global observability all privileges', () => { + before(async () => { + await security.role.create('cases_observability_all_role', { + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [ + { spaces: ['*'], base: [], feature: { 'observability-cases': ['all'], logs: ['all'] } }, + ], + }); + + await security.user.create('cases_observability_all_user', { + password: 'cases_observability_all_user-password', + roles: ['cases_observability_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.forceLogout(); + + await PageObjects.security.login( + 'cases_observability_all_user', + 'cases_observability_all_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await PageObjects.security.forceLogout(); + await Promise.all([ + security.role.delete('cases_observability_all_role'), + security.user.delete('cases_observability_all_user'), + ]); + }); + // const delay = (ms) => new Promise((res) => setTimeout(res, ms)); + + it('shows observability/cases navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map((link) => link.text).slice(0, 2); + expect(navLinks).to.eql(['Overview', 'Cases']); + }); + + it(`landing page shows "Create new case" button`, async () => { + await PageObjects.common.navigateToActualUrl('observabilityCases'); + await PageObjects.observability.expectCreateCaseButtonEnabled(); + }); + + // it(`doesn't show read-only badge`, async () => { + // await globalNav.badgeMissingOrFail(); + // }); + // + // it(`allows a workpad to be created`, async () => { + // await PageObjects.common.navigateToActualUrl('observability'); + // + // await testSubjects.click('createNewCaseBtn'); + // + // await PageObjects.observability.expectCreateCase(); + // }); + // + // it(`allows a workpad to be edited`, async () => { + // await PageObjects.common.navigateToActualUrl( + // 'observability', + // 'workpad/workpad-1705f884-6224-47de-ba49-ca224fe6ec31', + // { + // ensureCurrentUrl: true, + // shouldLoginIfPrompted: false, + // } + // ); + // + // await PageObjects.observability.expectAddElementButton(); + // }); + }); + + describe.skip('global observability read-only privileges', () => { + before(async () => { + await security.role.create('cases_observability_read_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + observability: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('cases_observability_read_user', { + password: 'cases_observability_read_user-password', + roles: ['cases_observability_read_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'cases_observability_read_user', + 'cases_observability_read_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('cases_observability_read_role'); + await security.user.delete('cases_observability_read_user'); + }); + + it('shows observability navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map((link) => link.text); + expect(navLinks).to.eql(['Overview', 'Observability']); + }); + + it(`landing page shows disabled "Create new workpad" button`, async () => { + await PageObjects.common.navigateToActualUrl('observability', '', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await PageObjects.observability.expectCreateWorkpadButtonDisabled(); + }); + + it(`shows read-only badge`, async () => { + await globalNav.badgeExistsOrFail('Read only'); + }); + + it(`does not allow a workpad to be created`, async () => { + await PageObjects.common.navigateToActualUrl('observability', 'workpad/create', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + + // expect redirection to observability landing + await PageObjects.observability.expectCreateWorkpadButtonDisabled(); + }); + + it(`does not allow a workpad to be edited`, async () => { + await PageObjects.common.navigateToActualUrl( + 'observability', + 'workpad/workpad-1705f884-6224-47de-ba49-ca224fe6ec31', + { + ensureCurrentUrl: true, + shouldLoginIfPrompted: false, + } + ); + + await PageObjects.observability.expectNoAddElementButton(); + }); + }); + + describe.skip('no observability privileges', () => { + before(async () => { + await security.role.create('no_observability_privileges_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + discover: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('no_observability_privileges_user', { + password: 'no_observability_privileges_user-password', + roles: ['no_observability_privileges_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'no_observability_privileges_user', + 'no_observability_privileges_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('no_observability_privileges_role'); + await security.user.delete('no_observability_privileges_user'); + }); + + it(`returns a 403`, async () => { + await PageObjects.common.navigateToActualUrl('observability', '', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + PageObjects.error.expectForbidden(); + }); + + it(`create new workpad returns a 403`, async () => { + await PageObjects.common.navigateToActualUrl('observability', 'workpad/create', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + PageObjects.error.expectForbidden(); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/observability/index.ts b/x-pack/test/functional/apps/observability/index.ts new file mode 100644 index 00000000000000..b7f03b5f27bae4 --- /dev/null +++ b/x-pack/test/functional/apps/observability/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Observability specs', function () { + this.tags('ciGroup6'); + loadTestFile(require.resolve('./feature_controls')); + }); +} diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index ee5be48a07663c..1c7e2279c00d86 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -60,6 +60,7 @@ export default async function ({ readConfigFile }) { resolve(__dirname, './apps/reporting_management'), resolve(__dirname, './apps/management'), resolve(__dirname, './apps/reporting'), + resolve(__dirname, './apps/observability'), // This license_management file must be last because it is destructive. resolve(__dirname, './apps/license_management'), @@ -94,6 +95,7 @@ export default async function ({ readConfigFile }) { '--xpack.discoverEnhanced.actions.exploreDataInContextMenu.enabled=true', '--timelion.ui.enabled=true', '--savedObjects.maxImportPayloadBytes=10485760', // for OSS test management/_import_objects + '--xpack.observability.unsafe.cases.enabled=true', ], }, uiSettings: { diff --git a/x-pack/test/functional/page_objects/index.ts b/x-pack/test/functional/page_objects/index.ts index e83420a9cea1d9..170b8c2121caca 100644 --- a/x-pack/test/functional/page_objects/index.ts +++ b/x-pack/test/functional/page_objects/index.ts @@ -8,6 +8,7 @@ import { pageObjects as kibanaFunctionalPageObjects } from '../../../../test/functional/page_objects'; import { CanvasPageProvider } from './canvas_page'; +import { ObservabilityPageProvider } from './observability_page'; import { SecurityPageProvider } from './security_page'; import { MonitoringPageProvider } from './monitoring_page'; // @ts-ignore not ts yet @@ -83,4 +84,5 @@ export const pageObjects = { navigationalSearch: NavigationalSearchProvider, banners: BannersPageProvider, detections: DetectionsPageProvider, + observability: ObservabilityPageProvider, }; diff --git a/x-pack/test/functional/page_objects/observability_page.ts b/x-pack/test/functional/page_objects/observability_page.ts new file mode 100644 index 00000000000000..7342c9d83ab24c --- /dev/null +++ b/x-pack/test/functional/page_objects/observability_page.ts @@ -0,0 +1,121 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../ftr_provider_context'; + +export function ObservabilityPageProvider({ getService, getPageObjects }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const find = getService('find'); + const browser = getService('browser'); + const PageObjects = getPageObjects(['common']); + + return { + async enterFullscreen() { + const elem = await find.byCssSelector('[aria-label="View fullscreen"]', 20000); + await elem.click(); + }, + + async exitFullscreen() { + await browser.pressKeys(browser.keys.ESCAPE); + }, + + async openExpressionEditor() { + await testSubjects.click('observabilityExpressionEditorButton'); + }, + + async waitForWorkpadElements() { + await testSubjects.findAll( + 'observabilityWorkpadPage > observabilityWorkpadPageElementContent' + ); + }, + + /* + * Finds the first workpad in the loader (uses find, not findAll) and + * ensures the expected name is the actual name. Then it clicks the element + * to load the workpad. Resolves once the workpad is in the DOM + */ + async loadFirstWorkpad(workpadName: string) { + const elem = await testSubjects.find('observabilityWorkpadLoaderWorkpad'); + const text = await elem.getVisibleText(); + expect(text).to.be(workpadName); + await elem.click(); + await testSubjects.existOrFail('observabilityWorkpadPage'); + }, + + async fillOutCustomElementForm(name: string, description: string) { + // Fill out the custom element form and submit it + await testSubjects.setValue('observabilityCustomElementForm-name', name, { + clearWithKeyboard: true, + }); + await testSubjects.setValue('observabilityCustomElementForm-description', description, { + clearWithKeyboard: true, + }); + + await testSubjects.click('observabilityCustomElementForm-submit'); + }, + + async expectCreateCaseButtonEnabled() { + const button = await testSubjects.find('createNewCaseBtn', 20000); + const disabledAttr = await button.getAttribute('disabled'); + expect(disabledAttr).to.be(null); + }, + + async expectCreateCaseButtonDisabled() { + const button = await testSubjects.find('createNewCaseBtn', 20000); + const disabledAttr = await button.getAttribute('disabled'); + expect(disabledAttr).to.be('true'); + }, + + async openSavedElementsModal() { + await testSubjects.click('add-element-button'); + await testSubjects.click('saved-elements-menu-option'); + + await PageObjects.common.sleep(1000); // give time for modal animation to complete + }, + async closeSavedElementsModal() { + await testSubjects.click('saved-elements-modal-close-button'); + }, + + async expectCreateCase() { + await testSubjects.existOrFail('case-creation-form-steps'); + }, + + async expectNoAddElementButton() { + // Ensure page is fully loaded first by waiting for the refresh button + const refreshPopoverExists = await testSubjects.exists('observability-refresh-control', { + timeout: 20000, + }); + expect(refreshPopoverExists).to.be(true); + + await testSubjects.missingOrFail('add-element-button'); + }, + + async getTimeFiltersFromDebug() { + await testSubjects.existOrFail('observabilityDebug__content'); + + const contentElem = await testSubjects.find('observabilityDebug__content'); + const content = await contentElem.getVisibleText(); + + const filters = JSON.parse(content); + + return filters.and.filter((f: any) => f.filterType === 'time'); + }, + + async getMatchFiltersFromDebug() { + await testSubjects.existOrFail('observabilityDebug__content'); + + const contentElem = await testSubjects.find('observabilityDebug__content'); + const content = await contentElem.getVisibleText(); + + const filters = JSON.parse(content); + + return filters.and.filter((f: any) => f.filterType === 'exactly'); + }, + }; +} From 58a4ecf7d294fdba3a75053a8025c462e21dae8a Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 9 Jun 2021 09:04:14 -0600 Subject: [PATCH 104/113] add es archiver --- .../observability_security.ts | 69 ++-- .../es_archives/cases/default/data.json.gz | Bin 0 -> 1355 bytes .../es_archives/cases/default/mappings.json | 322 ++++++++++++++++++ 3 files changed, 353 insertions(+), 38 deletions(-) create mode 100644 x-pack/test/functional/es_archives/cases/default/data.json.gz create mode 100644 x-pack/test/functional/es_archives/cases/default/mappings.json diff --git a/x-pack/test/functional/apps/observability/feature_controls/observability_security.ts b/x-pack/test/functional/apps/observability/feature_controls/observability_security.ts index e09eb4aa34ae61..4397fdb2c76944 100644 --- a/x-pack/test/functional/apps/observability/feature_controls/observability_security.ts +++ b/x-pack/test/functional/apps/observability/feature_controls/observability_security.ts @@ -23,13 +23,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); describe('wowzeroni', function () { this.tags(['skipFirefox']); - // before(async () => { - // await esArchiver.load('uptimw/default'); - // }); - // - // after(async () => { - // await esArchiver.unload('uptimw/default'); - // }); + before(async () => { + await esArchiver.load('cases/default'); + }); + + after(async () => { + await esArchiver.unload('cases/default'); + }); describe('global observability all privileges', () => { before(async () => { @@ -64,7 +64,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { security.user.delete('cases_observability_all_user'), ]); }); - // const delay = (ms) => new Promise((res) => setTimeout(res, ms)); + const delay = (ms) => new Promise((res) => setTimeout(res, ms)); it('shows observability/cases navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text).slice(0, 2); @@ -76,33 +76,26 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.observability.expectCreateCaseButtonEnabled(); }); - // it(`doesn't show read-only badge`, async () => { - // await globalNav.badgeMissingOrFail(); - // }); - // - // it(`allows a workpad to be created`, async () => { - // await PageObjects.common.navigateToActualUrl('observability'); - // - // await testSubjects.click('createNewCaseBtn'); - // - // await PageObjects.observability.expectCreateCase(); - // }); - // - // it(`allows a workpad to be edited`, async () => { - // await PageObjects.common.navigateToActualUrl( - // 'observability', - // 'workpad/workpad-1705f884-6224-47de-ba49-ca224fe6ec31', - // { - // ensureCurrentUrl: true, - // shouldLoginIfPrompted: false, - // } - // ); - // - // await PageObjects.observability.expectAddElementButton(); - // }); + it.skip(`doesn't show read-only badge`, async () => { + await globalNav.badgeMissingOrFail(); + }); + + it(`allows a case to be created`, async () => { + await PageObjects.common.navigateToActualUrl('observabilityCases'); + + await testSubjects.click('createNewCaseBtn'); + + await PageObjects.observability.expectCreateCase(); + }); + + it.only(`allows a workpad to be edited`, async () => { + await PageObjects.common.navigateToActualUrl('observabilityCases'); + await delay(3000000); + await PageObjects.observability.expectAddElementButton(); + }); }); - describe.skip('global observability read-only privileges', () => { + describe('global observability read-only privileges', () => { before(async () => { await security.role.create('cases_observability_read_role', { elasticsearch: { @@ -144,7 +137,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it(`landing page shows disabled "Create new workpad" button`, async () => { - await PageObjects.common.navigateToActualUrl('observability', '', { + await PageObjects.common.navigateToActualUrl('observabilityCases', '', { ensureCurrentUrl: false, shouldLoginIfPrompted: false, }); @@ -156,7 +149,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it(`does not allow a workpad to be created`, async () => { - await PageObjects.common.navigateToActualUrl('observability', 'workpad/create', { + await PageObjects.common.navigateToActualUrl('observabilityCases', 'workpad/create', { ensureCurrentUrl: false, shouldLoginIfPrompted: false, }); @@ -179,7 +172,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }); - describe.skip('no observability privileges', () => { + describe('no observability privileges', () => { before(async () => { await security.role.create('no_observability_privileges_role', { elasticsearch: { @@ -216,7 +209,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it(`returns a 403`, async () => { - await PageObjects.common.navigateToActualUrl('observability', '', { + await PageObjects.common.navigateToActualUrl('observabilityCases', '', { ensureCurrentUrl: false, shouldLoginIfPrompted: false, }); @@ -224,7 +217,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it(`create new workpad returns a 403`, async () => { - await PageObjects.common.navigateToActualUrl('observability', 'workpad/create', { + await PageObjects.common.navigateToActualUrl('observabilityCases', 'workpad/create', { ensureCurrentUrl: false, shouldLoginIfPrompted: false, }); diff --git a/x-pack/test/functional/es_archives/cases/default/data.json.gz b/x-pack/test/functional/es_archives/cases/default/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..9ec4efad4bdfc74c556b4cee7a5a66595c56934e GIT binary patch literal 1355 zcmV-R1+@AfiwFP!000026YZKybK5o$$M60W3@`1$r19W8m!>^6Gi_%YPY-#F1_YKC z8j4g&FpehU?_PjTlI~YUiq%qTfF~f{7&NCXfLCkf9fRE-<_q|M6!6tZZ zsmrF~_jj;X(K`*MDKBdf6Q*=?(d7B%mMLBAFqK6CT$NQXm^Ne*a$#=qrqlZFvg@)O zXi*km_u30KgWZ;O>fF|}vD|e#Q~^`#ao7YA?4ZCw_!`Fvq6v*%FYsU6JFaMl*Bb-O zn9NO>nas-%lK%ubtCi#~FZ=z{R8y!dJ#4GnclfAG#C)Oy>sYWoLFH`$ReL|+O(oUhRhc(!*p5>TO3C7;9wwk( z6#PXFl`2_UCtH!CT}WcGV~Wz^(z07i(It1jjo3+ zm+q6adbsS+#h$)w>n;^!P(i_=o19H5HUnJ7*);b?w!5-J{Cat}y9@KXySVstEO8hEMI2et@IgvQjPl)e^7#~TT{+I`Z*)n#qvO?6OsIp*-Glq#QoOxB=R8ZH4m~vZ| z({jM%xAs3xso<^MZ+=$KflHP@>o-3y^njoD+kWi-eRBX?roumyQ}&9Jw^CZdr80{KMvA@L9%v3UrazC%JYFFs1V zP|F$mKQ7eD)cCMe<{Gfy7_47&v3CD#5>o?aRpYx6AxA>+DW&s!0NpT|CxQ-T8TDEA zwLv!+55WmWZWKp-Ky>i;HJ4S#vDNDNw0`el(LN+~-N3b(jLV_NdPZ!F5TSO@e0TY% z=yRrPO6?Eb*$^EUw~}SmN7cc|2c!mJ5r)(^n-CtXii}3FLo+4-SjGaB?Ute!EB?Ne z726|8e;dnu<1te+ttvq3?DjY%5Tq?A{D(m?F1sH( zt!U>3#R$;eYAOeB7WM+@N5A zGfK>H9~WdL9uTIE-n38}VSxnUyG2{$`Kk6L{xuE~pC*X7zUQB%X8-!bOfSYw9X~ro ztB+6jwpMQ?ep{+3d6ZO>_$j4&7;T4Ccjwx7!~Xm0^OWjydD}dNJTDJy8ege$ZmFrP zExt(gMp+l|Ocs5`K3>lsoV0Q0c${VrlX?2iF#{kT1noH{9bed-{=x>E7ir`yRcLyy z$6k*i*v$cYO~-Fz_A;Yao2ReyQy#`5^^n7(kS&?P^s>PTFho4yaYQ^ZGUE}aO;OA8 zC?V91#;@_lukpU0ukm`79i2=ZPQc?n9f|W)#G#%Wka#4{ITPn+c{9T17}%Jirh`v^ N{{?*4zRYVd003P)t>6Fv literal 0 HcmV?d00001 diff --git a/x-pack/test/functional/es_archives/cases/default/mappings.json b/x-pack/test/functional/es_archives/cases/default/mappings.json new file mode 100644 index 00000000000000..28d9daff50d94f --- /dev/null +++ b/x-pack/test/functional/es_archives/cases/default/mappings.json @@ -0,0 +1,322 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "properties": { + "cases": { + "properties": { + "closed_at": { + "type": "date" + }, + "closed_by": { + "properties": { + "username": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "connector": { + "properties": { + "fields": { + "properties": { + "key": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "value": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "id": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "type": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "full_name": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "username": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "description": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "external_service": { + "properties": { + "connector_id": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "connector_name": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "external_id": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "external_title": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "external_url": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "username": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + } + } + }, + "owner": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "settings": { + "properties": { + "syncAlerts": { + "type": "boolean" + } + } + }, + "status": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "tags": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "title": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "type": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "full_name": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "username": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + } + } + }, + "coreMigrationVersion": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "migrationVersion": { + "properties": { + "cases": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "type": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "updated_at": { + "type": "date" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} From 109a5da8a276c24092cacedbb7f7671e8237afae Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Wed, 9 Jun 2021 11:33:04 -0400 Subject: [PATCH 105/113] Getting cases to show up in the nav --- x-pack/plugins/observability/public/plugin.ts | 5 +++-- .../public/toggle_overview_link_in_nav.tsx | 11 ++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index d97d7ef84812a1..03c3fb3c27e58e 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -66,6 +66,7 @@ export class Plugin ObservabilityPublicPluginsStart > { private readonly appUpdater$ = new BehaviorSubject(() => ({})); + private readonly casesAppUpdater$ = new BehaviorSubject(() => ({})); private readonly navigationRegistry = createNavigationRegistry(); constructor(private readonly initializerContext: PluginInitializerContext) { @@ -136,7 +137,7 @@ export class Plugin category, euiIconType, mount, - updater$, + updater$: this.casesAppUpdater$, }); } @@ -190,7 +191,7 @@ export class Plugin }; } public start({ application }: CoreStart) { - toggleOverviewLinkInNav(this.appUpdater$, application); + toggleOverviewLinkInNav(this.appUpdater$, this.casesAppUpdater$, application); const PageTemplate = createLazyObservabilityPageTemplate({ currentAppId$: application.currentAppId$, diff --git a/x-pack/plugins/observability/public/toggle_overview_link_in_nav.tsx b/x-pack/plugins/observability/public/toggle_overview_link_in_nav.tsx index 5110db15def88f..fd30bf3b73f151 100644 --- a/x-pack/plugins/observability/public/toggle_overview_link_in_nav.tsx +++ b/x-pack/plugins/observability/public/toggle_overview_link_in_nav.tsx @@ -7,13 +7,22 @@ import { Subject } from 'rxjs'; import { AppNavLinkStatus, AppUpdater, ApplicationStart } from '../../../../src/core/public'; +import { CASES_APP_ID } from '../common/const'; export function toggleOverviewLinkInNav( updater$: Subject, + casesUpdater$: Subject, { capabilities }: ApplicationStart ) { - const { apm, logs, metrics, uptime } = capabilities.navLinks; + const { apm, logs, metrics, uptime, [CASES_APP_ID]: cases } = capabilities.navLinks; const someVisible = Object.values({ apm, logs, metrics, uptime }).some((visible) => visible); + + // if cases is enabled then we want to show it in the sidebar but not the navigation unless one of the other features + // is enabled + if (cases) { + casesUpdater$.next(() => ({ navLinkStatus: AppNavLinkStatus.visible })); + } + if (!someVisible) { updater$.next(() => ({ navLinkStatus: AppNavLinkStatus.hidden, From 66f26455fb5fc12b022c7558e585e43594768477 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 9 Jun 2021 10:03:45 -0600 Subject: [PATCH 106/113] finish func tests --- .../public/components/callout/callout.tsx | 7 +- .../observability_security.ts | 98 ++++++++----------- .../page_objects/observability_page.ts | 96 ++++-------------- 3 files changed, 66 insertions(+), 135 deletions(-) diff --git a/x-pack/plugins/cases/public/components/callout/callout.tsx b/x-pack/plugins/cases/public/components/callout/callout.tsx index 8e2f439f02c4bd..21c8876f2d3de8 100644 --- a/x-pack/plugins/cases/public/components/callout/callout.tsx +++ b/x-pack/plugins/cases/public/components/callout/callout.tsx @@ -36,7 +36,12 @@ const CallOutComponent = ({ ]); return showCallOut ? ( - + {!isEmpty(messages) && ( )} diff --git a/x-pack/test/functional/apps/observability/feature_controls/observability_security.ts b/x-pack/test/functional/apps/observability/feature_controls/observability_security.ts index 4397fdb2c76944..72a3958b206a1e 100644 --- a/x-pack/test/functional/apps/observability/feature_controls/observability_security.ts +++ b/x-pack/test/functional/apps/observability/feature_controls/observability_security.ts @@ -19,9 +19,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { 'spaceSelector', ]); const appsMenu = getService('appsMenu'); - const globalNav = getService('globalNav'); const testSubjects = getService('testSubjects'); - describe('wowzeroni', function () { + describe('observability security feature controls', function () { this.tags(['skipFirefox']); before(async () => { await esArchiver.load('cases/default'); @@ -31,7 +30,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await esArchiver.unload('cases/default'); }); - describe('global observability all privileges', () => { + describe('observability cases all privileges', () => { before(async () => { await security.role.create('cases_observability_all_role', { elasticsearch: { cluster: [], indices: [], run_as: [] }, @@ -64,7 +63,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { security.user.delete('cases_observability_all_user'), ]); }); - const delay = (ms) => new Promise((res) => setTimeout(res, ms)); it('shows observability/cases navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text).slice(0, 2); @@ -76,8 +74,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.observability.expectCreateCaseButtonEnabled(); }); - it.skip(`doesn't show read-only badge`, async () => { - await globalNav.badgeMissingOrFail(); + it(`doesn't show read-only badge`, async () => { + await PageObjects.observability.expectNoReadOnlyCallout(); }); it(`allows a case to be created`, async () => { @@ -88,25 +86,27 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.observability.expectCreateCase(); }); - it.only(`allows a workpad to be edited`, async () => { - await PageObjects.common.navigateToActualUrl('observabilityCases'); - await delay(3000000); - await PageObjects.observability.expectAddElementButton(); + it(`allows a case to be edited`, async () => { + await PageObjects.common.navigateToUrl( + 'observabilityCases', + '4c32e6b0-c3c5-11eb-b389-3fadeeafa60f', + { + shouldUseHashForSubUrl: false, + } + ); + await PageObjects.observability.expectAddCommentButton(); }); }); - describe('global observability read-only privileges', () => { + describe('observability cases read-only privileges', () => { before(async () => { await security.role.create('cases_observability_read_role', { - elasticsearch: { - indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], - }, + elasticsearch: { cluster: [], indices: [], run_as: [] }, kibana: [ { - feature: { - observability: ['read'], - }, spaces: ['*'], + base: [], + feature: { 'observability-cases': ['read'], logs: ['all'] }, }, ], }); @@ -131,53 +131,45 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await security.user.delete('cases_observability_read_user'); }); - it('shows observability navlink', async () => { - const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Overview', 'Observability']); + it('shows observability/cases navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map((link) => link.text).slice(0, 2); + expect(navLinks).to.eql(['Overview', 'Cases']); }); - it(`landing page shows disabled "Create new workpad" button`, async () => { - await PageObjects.common.navigateToActualUrl('observabilityCases', '', { - ensureCurrentUrl: false, - shouldLoginIfPrompted: false, - }); - await PageObjects.observability.expectCreateWorkpadButtonDisabled(); + it(`landing page shows disabled "Create new case" button`, async () => { + await PageObjects.common.navigateToActualUrl('observabilityCases'); + await PageObjects.observability.expectCreateCaseButtonDisabled(); }); - it(`shows read-only badge`, async () => { - await globalNav.badgeExistsOrFail('Read only'); + it(`shows read-only callout`, async () => { + await PageObjects.observability.expectReadOnlyCallout(); }); - it(`does not allow a workpad to be created`, async () => { - await PageObjects.common.navigateToActualUrl('observabilityCases', 'workpad/create', { - ensureCurrentUrl: false, - shouldLoginIfPrompted: false, + it(`does not allow a case to be created`, async () => { + await PageObjects.common.navigateToUrl('observabilityCases', 'create', { + shouldUseHashForSubUrl: false, }); - // expect redirection to observability landing - await PageObjects.observability.expectCreateWorkpadButtonDisabled(); + // expect redirection to observability cases landing + await PageObjects.observability.expectCreateCaseButtonDisabled(); }); - it(`does not allow a workpad to be edited`, async () => { - await PageObjects.common.navigateToActualUrl( - 'observability', - 'workpad/workpad-1705f884-6224-47de-ba49-ca224fe6ec31', + it(`does not allow a case to be edited`, async () => { + await PageObjects.common.navigateToUrl( + 'observabilityCases', + '4c32e6b0-c3c5-11eb-b389-3fadeeafa60f', { - ensureCurrentUrl: true, - shouldLoginIfPrompted: false, + shouldUseHashForSubUrl: false, } ); - - await PageObjects.observability.expectNoAddElementButton(); + await PageObjects.observability.expectAddCommentButtonDisabled(); }); }); describe('no observability privileges', () => { before(async () => { await security.role.create('no_observability_privileges_role', { - elasticsearch: { - indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], - }, + elasticsearch: { cluster: [], indices: [], run_as: [] }, kibana: [ { feature: { @@ -209,19 +201,15 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it(`returns a 403`, async () => { - await PageObjects.common.navigateToActualUrl('observabilityCases', '', { - ensureCurrentUrl: false, - shouldLoginIfPrompted: false, - }); - PageObjects.error.expectForbidden(); + await PageObjects.common.navigateToActualUrl('observabilityCases'); + await PageObjects.observability.expectForbidden(); }); - it(`create new workpad returns a 403`, async () => { - await PageObjects.common.navigateToActualUrl('observabilityCases', 'workpad/create', { - ensureCurrentUrl: false, - shouldLoginIfPrompted: false, + it.skip(`create new case returns a 403`, async () => { + await PageObjects.common.navigateToUrl('observabilityCases', 'create', { + shouldUseHashForSubUrl: false, }); - PageObjects.error.expectForbidden(); + await PageObjects.observability.expectForbidden(); }); }); }); diff --git a/x-pack/test/functional/page_objects/observability_page.ts b/x-pack/test/functional/page_objects/observability_page.ts index 7342c9d83ab24c..95016c31d10541 100644 --- a/x-pack/test/functional/page_objects/observability_page.ts +++ b/x-pack/test/functional/page_objects/observability_page.ts @@ -12,54 +12,8 @@ import { FtrProviderContext } from '../ftr_provider_context'; export function ObservabilityPageProvider({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const find = getService('find'); - const browser = getService('browser'); - const PageObjects = getPageObjects(['common']); return { - async enterFullscreen() { - const elem = await find.byCssSelector('[aria-label="View fullscreen"]', 20000); - await elem.click(); - }, - - async exitFullscreen() { - await browser.pressKeys(browser.keys.ESCAPE); - }, - - async openExpressionEditor() { - await testSubjects.click('observabilityExpressionEditorButton'); - }, - - async waitForWorkpadElements() { - await testSubjects.findAll( - 'observabilityWorkpadPage > observabilityWorkpadPageElementContent' - ); - }, - - /* - * Finds the first workpad in the loader (uses find, not findAll) and - * ensures the expected name is the actual name. Then it clicks the element - * to load the workpad. Resolves once the workpad is in the DOM - */ - async loadFirstWorkpad(workpadName: string) { - const elem = await testSubjects.find('observabilityWorkpadLoaderWorkpad'); - const text = await elem.getVisibleText(); - expect(text).to.be(workpadName); - await elem.click(); - await testSubjects.existOrFail('observabilityWorkpadPage'); - }, - - async fillOutCustomElementForm(name: string, description: string) { - // Fill out the custom element form and submit it - await testSubjects.setValue('observabilityCustomElementForm-name', name, { - clearWithKeyboard: true, - }); - await testSubjects.setValue('observabilityCustomElementForm-description', description, { - clearWithKeyboard: true, - }); - - await testSubjects.click('observabilityCustomElementForm-submit'); - }, - async expectCreateCaseButtonEnabled() { const button = await testSubjects.find('createNewCaseBtn', 20000); const disabledAttr = await button.getAttribute('disabled'); @@ -72,50 +26,34 @@ export function ObservabilityPageProvider({ getService, getPageObjects }: FtrPro expect(disabledAttr).to.be('true'); }, - async openSavedElementsModal() { - await testSubjects.click('add-element-button'); - await testSubjects.click('saved-elements-menu-option'); - - await PageObjects.common.sleep(1000); // give time for modal animation to complete + async expectReadOnlyCallout() { + await testSubjects.existOrFail('case-callout-e41900b01c9ef0fa81dd6ff326083fb3'); }, - async closeSavedElementsModal() { - await testSubjects.click('saved-elements-modal-close-button'); + + async expectNoReadOnlyCallout() { + await testSubjects.missingOrFail('case-callout-e41900b01c9ef0fa81dd6ff326083fb3'); }, async expectCreateCase() { await testSubjects.existOrFail('case-creation-form-steps'); }, - async expectNoAddElementButton() { - // Ensure page is fully loaded first by waiting for the refresh button - const refreshPopoverExists = await testSubjects.exists('observability-refresh-control', { - timeout: 20000, - }); - expect(refreshPopoverExists).to.be(true); - - await testSubjects.missingOrFail('add-element-button'); + async expectAddCommentButton() { + const button = await testSubjects.find('submit-comment', 20000); + const disabledAttr = await button.getAttribute('disabled'); + expect(disabledAttr).to.be(null); }, - async getTimeFiltersFromDebug() { - await testSubjects.existOrFail('observabilityDebug__content'); - - const contentElem = await testSubjects.find('observabilityDebug__content'); - const content = await contentElem.getVisibleText(); - - const filters = JSON.parse(content); - - return filters.and.filter((f: any) => f.filterType === 'time'); + async expectAddCommentButtonDisabled() { + const button = await testSubjects.find('submit-comment', 20000); + const disabledAttr = await button.getAttribute('disabled'); + expect(disabledAttr).to.be('true'); }, - async getMatchFiltersFromDebug() { - await testSubjects.existOrFail('observabilityDebug__content'); - - const contentElem = await testSubjects.find('observabilityDebug__content'); - const content = await contentElem.getVisibleText(); - - const filters = JSON.parse(content); - - return filters.and.filter((f: any) => f.filterType === 'exactly'); + async expectForbidden() { + const h2 = await find.byCssSelector('body', 20000); + const text = await h2.getVisibleText(); + expect(text).to.contain('Kibana feature privileges required'); }, }; } From 0f34792249e78d11ef46cdaf2e3de3a46d18568a Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 9 Jun 2021 10:32:57 -0600 Subject: [PATCH 107/113] more pr updates --- .../public/components/callout/helpers.tsx | 2 +- .../components/app/cases/callout/helpers.tsx | 2 +- .../components/app/cases/callout/index.tsx | 5 +- .../app/cases/case_view/helpers.test.tsx | 27 ---------- .../components/app/cases/case_view/helpers.ts | 54 ------------------- .../public/hooks/use_messages_storage.tsx | 7 ++- .../public/pages/cases/all_cases.tsx | 6 +-- .../public/pages/cases/case_details.tsx | 6 +-- .../cases/components/callout/helpers.tsx | 2 +- .../public/cases/pages/case.tsx | 6 +-- .../public/cases/pages/case_details.tsx | 6 +-- 11 files changed, 23 insertions(+), 100 deletions(-) delete mode 100644 x-pack/plugins/observability/public/components/app/cases/case_view/helpers.test.tsx diff --git a/x-pack/plugins/cases/public/components/callout/helpers.tsx b/x-pack/plugins/cases/public/components/callout/helpers.tsx index 3409c5eb94245e..29b17cd426c58b 100644 --- a/x-pack/plugins/cases/public/components/callout/helpers.tsx +++ b/x-pack/plugins/cases/public/components/callout/helpers.tsx @@ -11,7 +11,7 @@ import md5 from 'md5'; import * as i18n from './translations'; import { ErrorMessage } from './types'; -export const savedObjectReadOnlyErrorMessage: ErrorMessage = { +export const permissionsReadOnlyErrorMessage: ErrorMessage = { id: 'read-only-privileges-error', title: i18n.READ_ONLY_FEATURE_TITLE, description: <>{i18n.READ_ONLY_FEATURE_MSG}, diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/helpers.tsx b/x-pack/plugins/observability/public/components/app/cases/callout/helpers.tsx index 3409c5eb94245e..29b17cd426c58b 100644 --- a/x-pack/plugins/observability/public/components/app/cases/callout/helpers.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/callout/helpers.tsx @@ -11,7 +11,7 @@ import md5 from 'md5'; import * as i18n from './translations'; import { ErrorMessage } from './types'; -export const savedObjectReadOnlyErrorMessage: ErrorMessage = { +export const permissionsReadOnlyErrorMessage: ErrorMessage = { id: 'read-only-privileges-error', title: i18n.READ_ONLY_FEATURE_TITLE, description: <>{i18n.READ_ONLY_FEATURE_MSG}, diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/index.tsx b/x-pack/plugins/observability/public/components/app/cases/callout/index.tsx index 67085bdd48832a..43cb6fd352a53e 100644 --- a/x-pack/plugins/observability/public/components/app/cases/callout/index.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/callout/index.tsx @@ -12,6 +12,7 @@ import { CallOut } from './callout'; import { ErrorMessage } from './types'; import { createCalloutId } from './helpers'; import { useMessagesStorage } from '../../../../hooks/use_messages_storage'; +import { OBSERVABILITY } from '../../../../../common/const'; export * from './helpers'; @@ -34,7 +35,7 @@ interface CalloutVisibility { function CaseCallOutComponent({ title, messages = [] }: CaseCallOutProps) { const { getMessages, addMessage } = useMessagesStorage(); - const caseMessages = useMemo(() => getMessages('observability'), [getMessages]); + const caseMessages = useMemo(() => getMessages(OBSERVABILITY), [getMessages]); const dismissedCallouts = useMemo( () => caseMessages.reduce( @@ -52,7 +53,7 @@ function CaseCallOutComponent({ title, messages = [] }: CaseCallOutProps) { (id, type) => { setCalloutVisibility((prevState) => ({ ...prevState, [id]: false })); if (type === 'primary') { - addMessage('observability', id); + addMessage(OBSERVABILITY, id); } }, [setCalloutVisibility, addMessage] diff --git a/x-pack/plugins/observability/public/components/app/cases/case_view/helpers.test.tsx b/x-pack/plugins/observability/public/components/app/cases/case_view/helpers.test.tsx deleted file mode 100644 index 5eb0a03fc5db7f..00000000000000 --- a/x-pack/plugins/observability/public/components/app/cases/case_view/helpers.test.tsx +++ /dev/null @@ -1,27 +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 { buildAlertsQuery } from './helpers'; - -describe('Case view helpers', () => { - describe('buildAlertsQuery', () => { - it('it builds the alerts query', () => { - expect(buildAlertsQuery(['alert-id-1', 'alert-id-2'])).toEqual({ - query: { - bool: { - filter: { - ids: { - values: ['alert-id-1', 'alert-id-2'], - }, - }, - }, - }, - size: 10000, - }); - }); - }); -}); diff --git a/x-pack/plugins/observability/public/components/app/cases/case_view/helpers.ts b/x-pack/plugins/observability/public/components/app/cases/case_view/helpers.ts index 2bb72ed38290c5..b180c15b4487ab 100644 --- a/x-pack/plugins/observability/public/components/app/cases/case_view/helpers.ts +++ b/x-pack/plugins/observability/public/components/app/cases/case_view/helpers.ts @@ -7,60 +7,6 @@ import { Ecs } from '../../../../../../cases/common'; -// TODO we need to allow -> docValueFields: [{ field: "@timestamp" }], -export const buildAlertsQuery = (alertIds: string[]) => { - if (alertIds.length === 0) { - return {}; - } - return { - query: { - bool: { - filter: { - ids: { - values: alertIds, - }, - }, - }, - }, - size: 10000, - }; -}; - -export const toStringArray = (value: unknown): string[] => { - if (Array.isArray(value)) { - return value.reduce((acc, v) => { - if (v != null) { - switch (typeof v) { - case 'number': - case 'boolean': - return [...acc, v.toString()]; - case 'object': - try { - return [...acc, JSON.stringify(v)]; - } catch { - return [...acc, 'Invalid Object']; - } - case 'string': - return [...acc, v]; - default: - return [...acc, `${v}`]; - } - } - return acc; - }, []); - } else if (value == null) { - return []; - } else if (!Array.isArray(value) && typeof value === 'object') { - try { - return [JSON.stringify(value)]; - } catch { - return ['Invalid Object']; - } - } else { - return [`${value}`]; - } -}; - // no alerts in observability so far // dummy hook for now as hooks cannot be called conditionally export const useFetchAlertData = (): [boolean, Record] => [false, {}]; diff --git a/x-pack/plugins/observability/public/hooks/use_messages_storage.tsx b/x-pack/plugins/observability/public/hooks/use_messages_storage.tsx index 266d5ba7bf0e17..d67910f00dc769 100644 --- a/x-pack/plugins/observability/public/hooks/use_messages_storage.tsx +++ b/x-pack/plugins/observability/public/hooks/use_messages_storage.tsx @@ -35,7 +35,7 @@ export const useMessagesStorage = (): UseMessagesStorage => { const hasMessage = useCallback( (plugin: string, id: string): boolean => { const pluginStorage = storage.get(`${plugin}-messages`) ?? []; - return pluginStorage.filter((val: string) => val === id).length > 0; + return pluginStorage.includes((val: string) => val === id); }, [storage] ); @@ -43,7 +43,10 @@ export const useMessagesStorage = (): UseMessagesStorage => { const removeMessage = useCallback( (plugin: string, id: string) => { const pluginStorage = storage.get(`${plugin}-messages`) ?? []; - storage.set(`${plugin}-messages`, [...pluginStorage.filter((val: string) => val !== id)]); + storage.set( + `${plugin}-messages`, + pluginStorage.filter((val: string) => val !== id) + ); }, [storage] ); diff --git a/x-pack/plugins/observability/public/pages/cases/all_cases.tsx b/x-pack/plugins/observability/public/pages/cases/all_cases.tsx index 0895acad282ab0..4131cdc40738f2 100644 --- a/x-pack/plugins/observability/public/pages/cases/all_cases.tsx +++ b/x-pack/plugins/observability/public/pages/cases/all_cases.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { AllCases } from '../../components/app/cases/all_cases'; import * as i18n from '../../components/app/cases/translations'; -import { savedObjectReadOnlyErrorMessage, CaseCallOut } from '../../components/app/cases/callout'; +import { permissionsReadOnlyErrorMessage, CaseCallOut } from '../../components/app/cases/callout'; import { CaseFeatureNoPermissions } from './feature_no_permissions'; import { useGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions'; import { usePluginContext } from '../../hooks/use_plugin_context'; @@ -22,8 +22,8 @@ export const AllCasesPage = React.memo(() => { <> {userPermissions != null && !userPermissions?.crud && userPermissions?.read && ( )} { const { @@ -33,8 +33,8 @@ export const CaseDetailsPage = React.memo(() => { <> {userPermissions != null && !userPermissions?.crud && userPermissions?.read && ( )} {i18n.READ_ONLY_FEATURE_MSG}, diff --git a/x-pack/plugins/security_solution/public/cases/pages/case.tsx b/x-pack/plugins/security_solution/public/cases/pages/case.tsx index 4ec29b676afe6e..9613e327ebe9f3 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/case.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/case.tsx @@ -12,7 +12,7 @@ import { useGetUserCasesPermissions } from '../../common/lib/kibana'; import { SpyRoute } from '../../common/utils/route/spy_routes'; import { AllCases } from '../components/all_cases'; -import { savedObjectReadOnlyErrorMessage, CaseCallOut } from '../components/callout'; +import { permissionsReadOnlyErrorMessage, CaseCallOut } from '../components/callout'; import { CaseFeatureNoPermissions } from './feature_no_permissions'; import { SecurityPageName } from '../../app/types'; @@ -24,8 +24,8 @@ export const CasesPage = React.memo(() => { {userPermissions != null && !userPermissions?.crud && userPermissions?.read && ( )} diff --git a/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx b/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx index 03407c7a5adaab..bbc29828731cb4 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx @@ -16,7 +16,7 @@ import { useGetUserCasesPermissions } from '../../common/lib/kibana'; import { getCaseUrl } from '../../common/components/link_to'; import { navTabs } from '../../app/home/home_navigations'; import { CaseView } from '../components/case_view'; -import { savedObjectReadOnlyErrorMessage, CaseCallOut } from '../components/callout'; +import { permissionsReadOnlyErrorMessage, CaseCallOut } from '../components/callout'; export const CaseDetailsPage = React.memo(() => { const history = useHistory(); @@ -37,8 +37,8 @@ export const CaseDetailsPage = React.memo(() => { {userPermissions != null && !userPermissions?.crud && userPermissions?.read && ( )} Date: Wed, 9 Jun 2021 10:35:31 -0600 Subject: [PATCH 108/113] fix telemetry --- src/plugins/telemetry/schema/oss_plugins.json | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 7b6c4ba9788f14..70f91b0762de9d 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -3970,6 +3970,137 @@ } } }, + "observability-cases": { + "properties": { + "appId": { + "type": "keyword", + "_meta": { + "description": "The application being tracked" + } + }, + "viewId": { + "type": "keyword", + "_meta": { + "description": "Always `main`" + } + }, + "clicks_total": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application since we started counting them" + } + }, + "clicks_7_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 7 days" + } + }, + "clicks_30_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 30 days" + } + }, + "clicks_90_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 90 days" + } + }, + "minutes_on_screen_total": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen since we started counting them." + } + }, + "minutes_on_screen_7_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 7 days" + } + }, + "minutes_on_screen_30_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 30 days" + } + }, + "minutes_on_screen_90_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 90 days" + } + }, + "views": { + "type": "array", + "items": { + "properties": { + "appId": { + "type": "keyword", + "_meta": { + "description": "The application being tracked" + } + }, + "viewId": { + "type": "keyword", + "_meta": { + "description": "The application view being tracked" + } + }, + "clicks_total": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application sub view since we started counting them" + } + }, + "clicks_7_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 7 days" + } + }, + "clicks_30_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 30 days" + } + }, + "clicks_90_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 90 days" + } + }, + "minutes_on_screen_total": { + "type": "float", + "_meta": { + "description": "Minutes the application sub view is active and on-screen since we started counting them." + } + }, + "minutes_on_screen_7_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 7 days" + } + }, + "minutes_on_screen_30_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 30 days" + } + }, + "minutes_on_screen_90_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 90 days" + } + } + } + } + } + } + }, "observability-overview": { "properties": { "appId": { From 101470059ca6fdc8707c8bd7b0b95930390a23f9 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 9 Jun 2021 15:00:57 -0600 Subject: [PATCH 109/113] fix a few types and test --- .../server/collectors/application_usage/schema.ts | 2 +- src/plugins/telemetry/schema/oss_plugins.json | 2 +- x-pack/plugins/cases/public/components/callout/callout.tsx | 7 +------ .../cases/public/components/case_action_bar/index.test.tsx | 2 +- x-pack/plugins/observability/common/const.ts | 2 +- .../public/toggle_overview_link_in_nav.test.tsx | 7 ++++--- .../api_integration/apis/features/features/features.ts | 2 +- x-pack/test/api_integration/apis/security/privileges.ts | 2 +- .../test/api_integration/apis/security/privileges_basic.ts | 2 +- .../feature_controls/observability_security.ts | 4 ++-- 10 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts index 5ea5b6d66763ac..e7c6b53ff97b3c 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts @@ -148,7 +148,7 @@ export const applicationUsageSchema = { maps: commonSchema, ml: commonSchema, monitoring: commonSchema, - 'observability-cases': commonSchema, + observabilityCases: commonSchema, 'observability-overview': commonSchema, osquery: commonSchema, security_account: commonSchema, diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 70f91b0762de9d..51df1d3162b7c3 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -3970,7 +3970,7 @@ } } }, - "observability-cases": { + "observabilityCases": { "properties": { "appId": { "type": "keyword", diff --git a/x-pack/plugins/cases/public/components/callout/callout.tsx b/x-pack/plugins/cases/public/components/callout/callout.tsx index 21c8876f2d3de8..8e2f439f02c4bd 100644 --- a/x-pack/plugins/cases/public/components/callout/callout.tsx +++ b/x-pack/plugins/cases/public/components/callout/callout.tsx @@ -36,12 +36,7 @@ const CallOutComponent = ({ ]); return showCallOut ? ( - + {!isEmpty(messages) && ( )} diff --git a/x-pack/plugins/cases/public/components/case_action_bar/index.test.tsx b/x-pack/plugins/cases/public/components/case_action_bar/index.test.tsx index a857bdb72dde76..724d35b20df535 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/index.test.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/index.test.tsx @@ -21,7 +21,7 @@ describe('CaseActionBar', () => { onClick: () => {}, }, caseData: basicCase, - isAlerting: true, + disableAlerting: false, isLoading: false, onRefresh, onUpdateField, diff --git a/x-pack/plugins/observability/common/const.ts b/x-pack/plugins/observability/common/const.ts index 43fd5d213b4a66..7065d8ccc6b344 100644 --- a/x-pack/plugins/observability/common/const.ts +++ b/x-pack/plugins/observability/common/const.ts @@ -5,5 +5,5 @@ * 2.0. */ -export const CASES_APP_ID = 'observability-cases'; +export const CASES_APP_ID = 'observabilityCases'; export const OBSERVABILITY = 'observability'; diff --git a/x-pack/plugins/observability/public/toggle_overview_link_in_nav.test.tsx b/x-pack/plugins/observability/public/toggle_overview_link_in_nav.test.tsx index bbcc12b4831830..0e07260cef9238 100644 --- a/x-pack/plugins/observability/public/toggle_overview_link_in_nav.test.tsx +++ b/x-pack/plugins/observability/public/toggle_overview_link_in_nav.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { Subject } from 'rxjs'; +import { BehaviorSubject, Subject } from 'rxjs'; import { AppUpdater, AppNavLinkStatus } from '../../../../src/core/public'; import { applicationServiceMock } from '../../../../src/core/public/mocks'; @@ -14,6 +14,7 @@ import { toggleOverviewLinkInNav } from './toggle_overview_link_in_nav'; describe('toggleOverviewLinkInNav', () => { let applicationStart: ReturnType; let subjectMock: jest.Mocked>; + let casesMock: jest.Mocked>; beforeEach(() => { applicationStart = applicationServiceMock.createStartContract(); @@ -34,7 +35,7 @@ describe('toggleOverviewLinkInNav', () => { }, }; - toggleOverviewLinkInNav(subjectMock, applicationStart); + toggleOverviewLinkInNav(subjectMock, casesMock, applicationStart); expect(subjectMock.next).toHaveBeenCalledTimes(1); const updater = subjectMock.next.mock.calls[0][0]!; @@ -54,7 +55,7 @@ describe('toggleOverviewLinkInNav', () => { }, }; - toggleOverviewLinkInNav(subjectMock, applicationStart); + toggleOverviewLinkInNav(subjectMock, casesMock, applicationStart); expect(subjectMock.next).not.toHaveBeenCalled(); }); diff --git a/x-pack/test/api_integration/apis/features/features/features.ts b/x-pack/test/api_integration/apis/features/features/features.ts index 48342dc3b80f54..275626664bef07 100644 --- a/x-pack/test/api_integration/apis/features/features/features.ts +++ b/x-pack/test/api_integration/apis/features/features/features.ts @@ -114,7 +114,7 @@ export default function ({ getService }: FtrProviderContext) { 'infrastructure', 'logs', 'maps', - 'observability-cases', + 'observabilityCases', 'uptime', 'siem', 'fleet', diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index 73f923d0cbbca7..2468bfec633210 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -60,7 +60,7 @@ export default function ({ getService }: FtrProviderContext) { canvas: ['all', 'read', 'minimal_all', 'minimal_read', 'generate_report'], infrastructure: ['all', 'read'], logs: ['all', 'read'], - 'observability-cases': ['all', 'read'], + observabilityCases: ['all', 'read'], uptime: ['all', 'read'], apm: ['all', 'read'], ml: ['all', 'read'], diff --git a/x-pack/test/api_integration/apis/security/privileges_basic.ts b/x-pack/test/api_integration/apis/security/privileges_basic.ts index 6be26cc3d75c74..25266da2cdfb34 100644 --- a/x-pack/test/api_integration/apis/security/privileges_basic.ts +++ b/x-pack/test/api_integration/apis/security/privileges_basic.ts @@ -33,7 +33,7 @@ export default function ({ getService }: FtrProviderContext) { maps: ['all', 'read'], canvas: ['all', 'read'], infrastructure: ['all', 'read'], - 'observability-cases': ['all', 'read'], + observabilityCases: ['all', 'read'], logs: ['all', 'read'], uptime: ['all', 'read'], apm: ['all', 'read'], diff --git a/x-pack/test/functional/apps/observability/feature_controls/observability_security.ts b/x-pack/test/functional/apps/observability/feature_controls/observability_security.ts index 72a3958b206a1e..c8c25e5649de70 100644 --- a/x-pack/test/functional/apps/observability/feature_controls/observability_security.ts +++ b/x-pack/test/functional/apps/observability/feature_controls/observability_security.ts @@ -35,7 +35,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await security.role.create('cases_observability_all_role', { elasticsearch: { cluster: [], indices: [], run_as: [] }, kibana: [ - { spaces: ['*'], base: [], feature: { 'observability-cases': ['all'], logs: ['all'] } }, + { spaces: ['*'], base: [], feature: { observabilityCases: ['all'], logs: ['all'] } }, ], }); @@ -106,7 +106,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { { spaces: ['*'], base: [], - feature: { 'observability-cases': ['read'], logs: ['all'] }, + feature: { observabilityCases: ['read'], logs: ['all'] }, }, ], }); From 7d7a10a0ca8ebf7c8f9ad8f4b4325e1de0e99e44 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 10 Jun 2021 08:41:41 -0600 Subject: [PATCH 110/113] fix es archiver path --- .../observability/feature_controls/observability_security.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/test/functional/apps/observability/feature_controls/observability_security.ts b/x-pack/test/functional/apps/observability/feature_controls/observability_security.ts index c8c25e5649de70..d27f1acdd3e317 100644 --- a/x-pack/test/functional/apps/observability/feature_controls/observability_security.ts +++ b/x-pack/test/functional/apps/observability/feature_controls/observability_security.ts @@ -23,11 +23,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('observability security feature controls', function () { this.tags(['skipFirefox']); before(async () => { - await esArchiver.load('cases/default'); + await esArchiver.load('x-pack/test/functional/es_archives/cases/default'); }); after(async () => { - await esArchiver.unload('cases/default'); + await esArchiver.unload('x-pack/test/functional/es_archives/cases/default'); }); describe('observability cases all privileges', () => { From e31813199451ae20dc54a4873c64ddf06190d0f1 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 10 Jun 2021 09:16:45 -0600 Subject: [PATCH 111/113] fix type --- .../observability/public/toggle_overview_link_in_nav.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/observability/public/toggle_overview_link_in_nav.test.tsx b/x-pack/plugins/observability/public/toggle_overview_link_in_nav.test.tsx index 0e07260cef9238..caee692ced2c57 100644 --- a/x-pack/plugins/observability/public/toggle_overview_link_in_nav.test.tsx +++ b/x-pack/plugins/observability/public/toggle_overview_link_in_nav.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { BehaviorSubject, Subject } from 'rxjs'; +import { Subject } from 'rxjs'; import { AppUpdater, AppNavLinkStatus } from '../../../../src/core/public'; import { applicationServiceMock } from '../../../../src/core/public/mocks'; From 8522fc65fde1e624d040b795d40f6c864c55ff00 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 10 Jun 2021 11:05:49 -0600 Subject: [PATCH 112/113] few more pr changes --- .../public/components/callout/callout.tsx | 6 ++---- .../components/case_action_bar/actions.tsx | 2 +- .../public/components/case_view/index.tsx | 8 ++------ .../cases/public/components/links/index.tsx | 2 +- .../components/app/cases/all_cases/index.tsx | 8 ++++++-- .../components/app/cases/callout/callout.tsx | 6 ++---- .../components/app/cases/case_view/index.tsx | 18 ++++++++++++------ .../cases/components/callout/callout.tsx | 6 ++---- 8 files changed, 28 insertions(+), 28 deletions(-) diff --git a/x-pack/plugins/cases/public/components/callout/callout.tsx b/x-pack/plugins/cases/public/components/callout/callout.tsx index 8e2f439f02c4bd..4cd7fad10fe70c 100644 --- a/x-pack/plugins/cases/public/components/callout/callout.tsx +++ b/x-pack/plugins/cases/public/components/callout/callout.tsx @@ -35,11 +35,9 @@ const CallOutComponent = ({ type, ]); - return showCallOut ? ( + return showCallOut && !isEmpty(messages) ? ( - {!isEmpty(messages) && ( - - )} + = ({ ); if (isDeleted) { - allCasesNavigation.onClick(({ preventDefault: () => null } as unknown) as MouseEvent); + allCasesNavigation.onClick(null); return null; } return ( diff --git a/x-pack/plugins/cases/public/components/case_view/index.tsx b/x-pack/plugins/cases/public/components/case_view/index.tsx index d205a8249c4dd5..df57e49073a604 100644 --- a/x-pack/plugins/cases/public/components/case_view/index.tsx +++ b/x-pack/plugins/cases/public/components/case_view/index.tsx @@ -383,12 +383,8 @@ export const CaseComponent = React.memo( <> { +export interface CasesNavigation { href: K extends 'configurable' ? (arg: T) => string : string; onClick: (arg: T) => void; } diff --git a/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx b/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx index 8c013acb84bb99..1636d08aa56e4d 100644 --- a/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/all_cases/index.tsx @@ -45,7 +45,9 @@ export const AllCases = React.memo(({ userCanCrud }) => { configureCasesNavigation: { href: formatUrl(getConfigureCasesUrl()), onClick: async (ev) => { - ev.preventDefault(); + if (ev != null) { + ev.preventDefault(); + } return navigateToApp(`${CASES_APP_ID}`, { path: getConfigureCasesUrl(), }); @@ -54,7 +56,9 @@ export const AllCases = React.memo(({ userCanCrud }) => { createCaseNavigation: { href: formatUrl(getCreateCaseUrl()), onClick: async (ev) => { - ev.preventDefault(); + if (ev != null) { + ev.preventDefault(); + } return navigateToApp(`${CASES_APP_ID}`, { path: getCreateCaseUrl(), }); diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/callout.tsx b/x-pack/plugins/observability/public/components/app/cases/callout/callout.tsx index 6e108c81d79622..4cb3875f75acbb 100644 --- a/x-pack/plugins/observability/public/components/app/cases/callout/callout.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/callout/callout.tsx @@ -35,11 +35,9 @@ function CallOutComponent({ type, ]); - return showCallOut ? ( + return showCallOut && !isEmpty(messages) ? ( - {!isEmpty(messages) && ( - - )} + { - e.preventDefault(); + onClick: async (ev) => { + if (ev != null) { + ev.preventDefault(); + } return navigateToApp(`${CASES_APP_ID}`, { path: allCasesLink, }); @@ -90,8 +92,10 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = }, caseDetailsNavigation: { href: caseDetailsHref, - onClick: async (e) => { - e.preventDefault(); + onClick: async (ev) => { + if (ev != null) { + ev.preventDefault(); + } return navigateToApp(`${CASES_APP_ID}`, { path: getCaseDetailsUrl({ id: caseId }), }); @@ -100,8 +104,10 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = caseId, configureCasesNavigation: { href: configureCasesHref, - onClick: async (e) => { - e.preventDefault(); + onClick: async (ev) => { + if (ev != null) { + ev.preventDefault(); + } return navigateToApp(`${CASES_APP_ID}`, { path: configureCasesLink, }); diff --git a/x-pack/plugins/security_solution/public/cases/components/callout/callout.tsx b/x-pack/plugins/security_solution/public/cases/components/callout/callout.tsx index 8e2f439f02c4bd..4cd7fad10fe70c 100644 --- a/x-pack/plugins/security_solution/public/cases/components/callout/callout.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/callout/callout.tsx @@ -35,11 +35,9 @@ const CallOutComponent = ({ type, ]); - return showCallOut ? ( + return showCallOut && !isEmpty(messages) ? ( - {!isEmpty(messages) && ( - - )} + Date: Thu, 10 Jun 2021 14:24:13 -0600 Subject: [PATCH 113/113] fix --- x-pack/plugins/cases/public/components/callout/callout.test.tsx | 2 +- .../public/components/app/cases/callout/callout.test.tsx | 2 +- .../public/cases/components/callout/callout.test.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/cases/public/components/callout/callout.test.tsx b/x-pack/plugins/cases/public/components/callout/callout.test.tsx index 926fe7b63fb5af..0a0caa40a8783e 100644 --- a/x-pack/plugins/cases/public/components/callout/callout.test.tsx +++ b/x-pack/plugins/cases/public/components/callout/callout.test.tsx @@ -80,7 +80,7 @@ describe('Callout', () => { }); it('dismiss the callout correctly', () => { - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.find(`[data-test-subj="callout-dismiss-md5-hex"]`).exists()).toBeTruthy(); wrapper.find(`button[data-test-subj="callout-dismiss-md5-hex"]`).simulate('click'); wrapper.update(); diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/callout.test.tsx b/x-pack/plugins/observability/public/components/app/cases/callout/callout.test.tsx index cf50a579fca3ac..b0b6fc0e3b7933 100644 --- a/x-pack/plugins/observability/public/components/app/cases/callout/callout.test.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/callout/callout.test.tsx @@ -80,7 +80,7 @@ describe('Callout', () => { }); it('dismiss the callout correctly', () => { - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.find(`[data-test-subj="callout-dismiss-md5-hex"]`).exists()).toBeTruthy(); wrapper.find(`button[data-test-subj="callout-dismiss-md5-hex"]`).simulate('click'); wrapper.update(); diff --git a/x-pack/plugins/security_solution/public/cases/components/callout/callout.test.tsx b/x-pack/plugins/security_solution/public/cases/components/callout/callout.test.tsx index 926fe7b63fb5af..0a0caa40a8783e 100644 --- a/x-pack/plugins/security_solution/public/cases/components/callout/callout.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/callout/callout.test.tsx @@ -80,7 +80,7 @@ describe('Callout', () => { }); it('dismiss the callout correctly', () => { - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.find(`[data-test-subj="callout-dismiss-md5-hex"]`).exists()).toBeTruthy(); wrapper.find(`button[data-test-subj="callout-dismiss-md5-hex"]`).simulate('click'); wrapper.update();