diff --git a/x-pack/platform/packages/private/security/authorization_core/src/privileges/feature_privilege_builder/__snapshots__/cases.test.ts.snap b/x-pack/platform/packages/private/security/authorization_core/src/privileges/feature_privilege_builder/__snapshots__/cases.test.ts.snap index 2997187697c40..39160d9eb5e58 100644 --- a/x-pack/platform/packages/private/security/authorization_core/src/privileges/feature_privilege_builder/__snapshots__/cases.test.ts.snap +++ b/x-pack/platform/packages/private/security/authorization_core/src/privileges/feature_privilege_builder/__snapshots__/cases.test.ts.snap @@ -18,6 +18,7 @@ Array [ "cases:observability/updateConfiguration", "cases:observability/createComment", "cases:observability/reopenCase", + "cases:observability/assignCase", ] `; diff --git a/x-pack/platform/packages/private/security/authorization_core/src/privileges/feature_privilege_builder/cases.test.ts b/x-pack/platform/packages/private/security/authorization_core/src/privileges/feature_privilege_builder/cases.test.ts index eae3bbc942e34..8aa6ec7cf25e7 100644 --- a/x-pack/platform/packages/private/security/authorization_core/src/privileges/feature_privilege_builder/cases.test.ts +++ b/x-pack/platform/packages/private/security/authorization_core/src/privileges/feature_privilege_builder/cases.test.ts @@ -130,6 +130,7 @@ describe(`cases`, () => { "cases:security/updateConfiguration", "cases:security/createComment", "cases:security/reopenCase", + "cases:security/assignCase", "cases:obs/getCase", "cases:obs/getComment", "cases:obs/getTags", @@ -187,6 +188,7 @@ describe(`cases`, () => { "cases:security/updateConfiguration", "cases:security/createComment", "cases:security/reopenCase", + "cases:security/assignCase", "cases:other-security/pushCase", "cases:other-security/createCase", "cases:other-security/getCase", @@ -203,6 +205,7 @@ describe(`cases`, () => { "cases:other-security/updateConfiguration", "cases:other-security/createComment", "cases:other-security/reopenCase", + "cases:other-security/assignCase", "cases:obs/getCase", "cases:obs/getComment", "cases:obs/getTags", diff --git a/x-pack/platform/packages/private/security/authorization_core/src/privileges/feature_privilege_builder/cases.ts b/x-pack/platform/packages/private/security/authorization_core/src/privileges/feature_privilege_builder/cases.ts index 5876ef50ea191..da612a522775a 100644 --- a/x-pack/platform/packages/private/security/authorization_core/src/privileges/feature_privilege_builder/cases.ts +++ b/x-pack/platform/packages/private/security/authorization_core/src/privileges/feature_privilege_builder/cases.ts @@ -37,6 +37,7 @@ const deleteOperations = ['deleteCase', 'deleteComment'] as const; const settingsOperations = ['createConfiguration', 'updateConfiguration'] as const; const createCommentOperations = ['createComment'] as const; const reopenOperations = ['reopenCase'] as const; +const assignOperations = ['assignCase'] as const; const allOperations = [ ...pushOperations, ...createOperations, @@ -46,6 +47,7 @@ const allOperations = [ ...settingsOperations, ...createCommentOperations, ...reopenOperations, + ...assignOperations, ] as const; export class FeaturePrivilegeCasesBuilder extends BaseFeaturePrivilegeBuilder { @@ -71,6 +73,7 @@ export class FeaturePrivilegeCasesBuilder extends BaseFeaturePrivilegeBuilder { ...getCasesPrivilege(settingsOperations, privilegeDefinition.cases?.settings), ...getCasesPrivilege(createCommentOperations, privilegeDefinition.cases?.createComment), ...getCasesPrivilege(reopenOperations, privilegeDefinition.cases?.reopenCase), + ...getCasesPrivilege(assignOperations, privilegeDefinition.cases?.assign), ]); } } diff --git a/x-pack/platform/plugins/shared/cases/common/constants/application.ts b/x-pack/platform/plugins/shared/cases/common/constants/application.ts index 01bbea157e7d2..18afd107e378e 100644 --- a/x-pack/platform/plugins/shared/cases/common/constants/application.ts +++ b/x-pack/platform/plugins/shared/cases/common/constants/application.ts @@ -12,9 +12,11 @@ import { CASE_VIEW_PAGE_TABS } from '../types'; */ export const APP_ID = 'cases' as const; -/** @deprecated Please use FEATURE_ID_V2 instead */ +/** @deprecated Please use FEATURE_ID_V3 instead */ export const FEATURE_ID = 'generalCases' as const; +/** @deprecated Please use FEATURE_ID_V3 instead */ export const FEATURE_ID_V2 = 'generalCasesV2' as const; +export const FEATURE_ID_V3 = 'generalCasesV3' as const; export const APP_OWNER = 'cases' as const; export const APP_PATH = '/app/management/insightsAndAlerting/cases' as const; export const CASES_CREATE_PATH = '/create' as const; diff --git a/x-pack/platform/plugins/shared/cases/common/constants/index.ts b/x-pack/platform/plugins/shared/cases/common/constants/index.ts index 76787c791b808..fd04a774e6452 100644 --- a/x-pack/platform/plugins/shared/cases/common/constants/index.ts +++ b/x-pack/platform/plugins/shared/cases/common/constants/index.ts @@ -186,6 +186,7 @@ export const CASES_SETTINGS_CAPABILITY = 'cases_settings' as const; export const CASES_CONNECTORS_CAPABILITY = 'cases_connectors' as const; export const CASES_REOPEN_CAPABILITY = 'case_reopen' as const; export const CREATE_COMMENT_CAPABILITY = 'create_comment' as const; +export const ASSIGN_CASE_CAPABILITY = 'cases_assign' as const; /** * Cases API Tags diff --git a/x-pack/platform/plugins/shared/cases/common/index.ts b/x-pack/platform/plugins/shared/cases/common/index.ts index 916b49163b696..37f0115f027fe 100644 --- a/x-pack/platform/plugins/shared/cases/common/index.ts +++ b/x-pack/platform/plugins/shared/cases/common/index.ts @@ -58,6 +58,7 @@ export { CASES_SETTINGS_CAPABILITY, CREATE_COMMENT_CAPABILITY, CASES_REOPEN_CAPABILITY, + ASSIGN_CASE_CAPABILITY, } from './constants'; export type { AttachmentAttributes } from './types/domain'; diff --git a/x-pack/platform/plugins/shared/cases/common/ui/types.ts b/x-pack/platform/plugins/shared/cases/common/ui/types.ts index 827caecadce21..fc74ebd5077af 100644 --- a/x-pack/platform/plugins/shared/cases/common/ui/types.ts +++ b/x-pack/platform/plugins/shared/cases/common/ui/types.ts @@ -13,6 +13,7 @@ import type { UPDATE_CASES_CAPABILITY, CREATE_COMMENT_CAPABILITY, CASES_REOPEN_CAPABILITY, + ASSIGN_CASE_CAPABILITY, } from '..'; import type { CASES_CONNECTORS_CAPABILITY, @@ -325,6 +326,7 @@ export interface CasesPermissions { settings: boolean; reopenCase: boolean; createComment: boolean; + assign: boolean; } export interface CasesCapabilities { @@ -337,4 +339,5 @@ export interface CasesCapabilities { [CASES_SETTINGS_CAPABILITY]: boolean; [CREATE_COMMENT_CAPABILITY]: boolean; [CASES_REOPEN_CAPABILITY]: boolean; + [ASSIGN_CASE_CAPABILITY]: boolean; } diff --git a/x-pack/platform/plugins/shared/cases/common/utils/capabilities.test.tsx b/x-pack/platform/plugins/shared/cases/common/utils/capabilities.test.tsx index 6194cfd9aef02..edfb5a14da532 100644 --- a/x-pack/platform/plugins/shared/cases/common/utils/capabilities.test.tsx +++ b/x-pack/platform/plugins/shared/cases/common/utils/capabilities.test.tsx @@ -18,6 +18,9 @@ describe('createUICapabilities', () => { "push_cases", "cases_connectors", ], + "assignCase": Array [ + "cases_assign", + ], "createComment": Array [ "create_comment", ], diff --git a/x-pack/platform/plugins/shared/cases/common/utils/capabilities.ts b/x-pack/platform/plugins/shared/cases/common/utils/capabilities.ts index 7328ec87d2069..7ef3300d5fd7b 100644 --- a/x-pack/platform/plugins/shared/cases/common/utils/capabilities.ts +++ b/x-pack/platform/plugins/shared/cases/common/utils/capabilities.ts @@ -15,6 +15,7 @@ import { CASES_SETTINGS_CAPABILITY, CASES_REOPEN_CAPABILITY, CREATE_COMMENT_CAPABILITY, + ASSIGN_CASE_CAPABILITY, } from '../constants'; export interface CasesUiCapabilities { @@ -24,6 +25,7 @@ export interface CasesUiCapabilities { settings: readonly string[]; reopenCase: readonly string[]; createComment: readonly string[]; + assignCase: readonly string[]; } /** * Return the UI capabilities for each type of operation. These strings must match the values defined in the UI @@ -42,4 +44,5 @@ export const createUICapabilities = (): CasesUiCapabilities => ({ settings: [CASES_SETTINGS_CAPABILITY] as const, reopenCase: [CASES_REOPEN_CAPABILITY] as const, createComment: [CREATE_COMMENT_CAPABILITY] as const, + assignCase: [ASSIGN_CASE_CAPABILITY] as const, }); diff --git a/x-pack/platform/plugins/shared/cases/public/client/helpers/can_use_cases.test.ts b/x-pack/platform/plugins/shared/cases/public/client/helpers/can_use_cases.test.ts index 69eca9d064602..4fe7a25fead3b 100644 --- a/x-pack/platform/plugins/shared/cases/public/client/helpers/can_use_cases.test.ts +++ b/x-pack/platform/plugins/shared/cases/public/client/helpers/can_use_cases.test.ts @@ -20,67 +20,67 @@ import { canUseCases } from './can_use_cases'; type CasesCapabilities = Pick< ApplicationStart['capabilities'], - 'securitySolutionCasesV2' | 'observabilityCasesV2' | 'generalCasesV2' + 'securitySolutionCasesV3' | 'observabilityCasesV3' | 'generalCasesV3' >; const hasAll: CasesCapabilities = { - securitySolutionCasesV2: allCasesCapabilities(), - observabilityCasesV2: allCasesCapabilities(), - generalCasesV2: allCasesCapabilities(), + securitySolutionCasesV3: allCasesCapabilities(), + observabilityCasesV3: allCasesCapabilities(), + generalCasesV3: allCasesCapabilities(), }; const hasNone: CasesCapabilities = { - securitySolutionCasesV2: noCasesCapabilities(), - observabilityCasesV2: noCasesCapabilities(), - generalCasesV2: noCasesCapabilities(), + securitySolutionCasesV3: noCasesCapabilities(), + observabilityCasesV3: noCasesCapabilities(), + generalCasesV3: noCasesCapabilities(), }; const hasSecurity: CasesCapabilities = { - securitySolutionCasesV2: allCasesCapabilities(), - observabilityCasesV2: noCasesCapabilities(), - generalCasesV2: noCasesCapabilities(), + securitySolutionCasesV3: allCasesCapabilities(), + observabilityCasesV3: noCasesCapabilities(), + generalCasesV3: noCasesCapabilities(), }; const hasObservability: CasesCapabilities = { - securitySolutionCasesV2: noCasesCapabilities(), - observabilityCasesV2: allCasesCapabilities(), - generalCasesV2: noCasesCapabilities(), + securitySolutionCasesV3: noCasesCapabilities(), + observabilityCasesV3: allCasesCapabilities(), + generalCasesV3: noCasesCapabilities(), }; const hasObservabilityWriteTrue: CasesCapabilities = { - securitySolutionCasesV2: noCasesCapabilities(), - observabilityCasesV2: writeCasesCapabilities(), - generalCasesV2: noCasesCapabilities(), + securitySolutionCasesV3: noCasesCapabilities(), + observabilityCasesV3: writeCasesCapabilities(), + generalCasesV3: noCasesCapabilities(), }; const hasSecurityWriteTrue: CasesCapabilities = { - securitySolutionCasesV2: writeCasesCapabilities(), - observabilityCasesV2: noCasesCapabilities(), - generalCasesV2: noCasesCapabilities(), + securitySolutionCasesV3: writeCasesCapabilities(), + observabilityCasesV3: noCasesCapabilities(), + generalCasesV3: noCasesCapabilities(), }; const hasObservabilityReadTrue: CasesCapabilities = { - securitySolutionCasesV2: noCasesCapabilities(), - observabilityCasesV2: readCasesCapabilities(), - generalCasesV2: noCasesCapabilities(), + securitySolutionCasesV3: noCasesCapabilities(), + observabilityCasesV3: readCasesCapabilities(), + generalCasesV3: noCasesCapabilities(), }; const hasSecurityReadTrue: CasesCapabilities = { - securitySolutionCasesV2: readCasesCapabilities(), - observabilityCasesV2: noCasesCapabilities(), - generalCasesV2: noCasesCapabilities(), + securitySolutionCasesV3: readCasesCapabilities(), + observabilityCasesV3: noCasesCapabilities(), + generalCasesV3: noCasesCapabilities(), }; const hasSecurityWriteAndObservabilityRead: CasesCapabilities = { - securitySolutionCasesV2: writeCasesCapabilities(), - observabilityCasesV2: readCasesCapabilities(), - generalCasesV2: noCasesCapabilities(), + securitySolutionCasesV3: writeCasesCapabilities(), + observabilityCasesV3: readCasesCapabilities(), + generalCasesV3: noCasesCapabilities(), }; const hasSecurityConnectors: CasesCapabilities = { - securitySolutionCasesV2: readCasesCapabilities(), - observabilityCasesV2: noCasesCapabilities(), - generalCasesV2: noCasesCapabilities(), + securitySolutionCasesV3: readCasesCapabilities(), + observabilityCasesV3: noCasesCapabilities(), + generalCasesV3: noCasesCapabilities(), }; describe('canUseCases', () => { diff --git a/x-pack/platform/plugins/shared/cases/public/client/helpers/can_use_cases.ts b/x-pack/platform/plugins/shared/cases/public/client/helpers/can_use_cases.ts index 3e318132f8adf..4f68e0c61f119 100644 --- a/x-pack/platform/plugins/shared/cases/public/client/helpers/can_use_cases.ts +++ b/x-pack/platform/plugins/shared/cases/public/client/helpers/can_use_cases.ts @@ -7,7 +7,7 @@ import type { ApplicationStart } from '@kbn/core/public'; import { - FEATURE_ID_V2, + FEATURE_ID_V3, GENERAL_CASES_OWNER, OBSERVABILITY_OWNER, SECURITY_SOLUTION_OWNER, @@ -32,9 +32,9 @@ export const canUseCases = owners: CasesOwners[] = [OBSERVABILITY_OWNER, SECURITY_SOLUTION_OWNER, GENERAL_CASES_OWNER] ): CasesPermissions => { const aggregatedPermissions = owners.reduce( + // eslint-disable-next-line complexity (acc, owner) => { const userCapabilitiesForOwner = getUICapabilities(capabilities[getFeatureID(owner)]); - acc.create = acc.create || userCapabilitiesForOwner.create; acc.read = acc.read || userCapabilitiesForOwner.read; acc.update = acc.update || userCapabilitiesForOwner.update; @@ -44,6 +44,7 @@ export const canUseCases = acc.settings = acc.settings || userCapabilitiesForOwner.settings; acc.reopenCase = acc.reopenCase || userCapabilitiesForOwner.reopenCase; acc.createComment = acc.createComment || userCapabilitiesForOwner.createComment; + acc.assign = acc.assign || userCapabilitiesForOwner.assign; const allFromAcc = acc.create && @@ -54,7 +55,8 @@ export const canUseCases = acc.connectors && acc.settings && acc.reopenCase && - acc.createComment; + acc.createComment && + acc.assign; acc.all = acc.all || userCapabilitiesForOwner.all || allFromAcc; @@ -71,6 +73,7 @@ export const canUseCases = settings: false, reopenCase: false, createComment: false, + assign: false, } ); @@ -81,8 +84,8 @@ export const canUseCases = const getFeatureID = (owner: CasesOwners) => { if (owner === GENERAL_CASES_OWNER) { - return FEATURE_ID_V2; + return FEATURE_ID_V3; } - return `${owner}CasesV2`; + return `${owner}CasesV3`; }; diff --git a/x-pack/platform/plugins/shared/cases/public/client/helpers/capabilities.test.ts b/x-pack/platform/plugins/shared/cases/public/client/helpers/capabilities.test.ts index ec1b90eee0eb1..c04bfed11da27 100644 --- a/x-pack/platform/plugins/shared/cases/public/client/helpers/capabilities.test.ts +++ b/x-pack/platform/plugins/shared/cases/public/client/helpers/capabilities.test.ts @@ -12,6 +12,7 @@ describe('getUICapabilities', () => { expect(getUICapabilities(undefined)).toMatchInlineSnapshot(` Object { "all": false, + "assign": false, "connectors": false, "create": false, "createComment": false, @@ -29,6 +30,7 @@ describe('getUICapabilities', () => { expect(getUICapabilities()).toMatchInlineSnapshot(` Object { "all": false, + "assign": false, "connectors": false, "create": false, "createComment": false, @@ -46,6 +48,7 @@ describe('getUICapabilities', () => { expect(getUICapabilities({ create_cases: true })).toMatchInlineSnapshot(` Object { "all": false, + "assign": false, "connectors": false, "create": true, "createComment": false, @@ -72,6 +75,7 @@ describe('getUICapabilities', () => { ).toMatchInlineSnapshot(` Object { "all": false, + "assign": false, "connectors": false, "create": false, "createComment": false, @@ -89,6 +93,7 @@ describe('getUICapabilities', () => { expect(getUICapabilities({})).toMatchInlineSnapshot(` Object { "all": false, + "assign": false, "connectors": false, "create": false, "createComment": false, @@ -115,6 +120,7 @@ describe('getUICapabilities', () => { ).toMatchInlineSnapshot(` Object { "all": false, + "assign": false, "connectors": true, "create": false, "createComment": false, @@ -142,6 +148,7 @@ describe('getUICapabilities', () => { ).toMatchInlineSnapshot(` Object { "all": false, + "assign": false, "connectors": false, "create": true, "createComment": false, @@ -169,6 +176,7 @@ describe('getUICapabilities', () => { ).toMatchInlineSnapshot(` Object { "all": false, + "assign": false, "connectors": true, "create": true, "createComment": false, @@ -186,6 +194,7 @@ describe('getUICapabilities', () => { expect(getUICapabilities({ cases_settings: true })).toMatchInlineSnapshot(` Object { "all": false, + "assign": false, "connectors": false, "create": false, "createComment": false, diff --git a/x-pack/platform/plugins/shared/cases/public/client/helpers/capabilities.ts b/x-pack/platform/plugins/shared/cases/public/client/helpers/capabilities.ts index 634cb3188602d..cbf467986b6ac 100644 --- a/x-pack/platform/plugins/shared/cases/public/client/helpers/capabilities.ts +++ b/x-pack/platform/plugins/shared/cases/public/client/helpers/capabilities.ts @@ -16,6 +16,7 @@ import { UPDATE_CASES_CAPABILITY, CASES_REOPEN_CAPABILITY, CREATE_COMMENT_CAPABILITY, + ASSIGN_CASE_CAPABILITY, } from '../../../common/constants'; export const getUICapabilities = ( @@ -30,6 +31,7 @@ export const getUICapabilities = ( const settings = !!featureCapabilities?.[CASES_SETTINGS_CAPABILITY]; const reopenCase = !!featureCapabilities?.[CASES_REOPEN_CAPABILITY]; const createComment = !!featureCapabilities?.[CREATE_COMMENT_CAPABILITY]; + const assignCases = !!featureCapabilities?.[ASSIGN_CASE_CAPABILITY]; const all = create && @@ -40,7 +42,8 @@ export const getUICapabilities = ( connectors && settings && reopenCase && - createComment; + createComment && + assignCases; return { all, @@ -53,5 +56,6 @@ export const getUICapabilities = ( settings, reopenCase, createComment, + assign: assignCases, }; }; diff --git a/x-pack/platform/plugins/shared/cases/public/common/lib/kibana/hooks.test.tsx b/x-pack/platform/plugins/shared/cases/public/common/lib/kibana/hooks.test.tsx index 73d1822c62499..81b1d8a938a2a 100644 --- a/x-pack/platform/plugins/shared/cases/public/common/lib/kibana/hooks.test.tsx +++ b/x-pack/platform/plugins/shared/cases/public/common/lib/kibana/hooks.test.tsx @@ -20,7 +20,7 @@ describe('hooks', () => { expect(result.current).toEqual({ actions: { crud: true, read: true }, - generalCasesV2: allCasesPermissions(), + generalCasesV3: allCasesPermissions(), visualize: { crud: true, read: true }, dashboard: { crud: true, read: true }, }); diff --git a/x-pack/platform/plugins/shared/cases/public/common/lib/kibana/hooks.ts b/x-pack/platform/plugins/shared/cases/public/common/lib/kibana/hooks.ts index 6a309111ceddb..356108ac528d1 100644 --- a/x-pack/platform/plugins/shared/cases/public/common/lib/kibana/hooks.ts +++ b/x-pack/platform/plugins/shared/cases/public/common/lib/kibana/hooks.ts @@ -15,7 +15,7 @@ import type { NavigateToAppOptions } from '@kbn/core/public'; import { getUICapabilities } from '../../../client/helpers/capabilities'; import { convertToCamelCase } from '../../../api/utils'; import { - FEATURE_ID_V2, + FEATURE_ID_V3, DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ, } from '../../../../common/constants'; @@ -166,7 +166,7 @@ interface Capabilities { } interface UseApplicationCapabilities { actions: Capabilities; - generalCasesV2: CasesPermissions; + generalCasesV3: CasesPermissions; visualize: Capabilities; dashboard: Capabilities; } @@ -178,13 +178,13 @@ interface UseApplicationCapabilities { export const useApplicationCapabilities = (): UseApplicationCapabilities => { const capabilities = useKibana().services?.application?.capabilities; - const casesCapabilities = capabilities[FEATURE_ID_V2]; + const casesCapabilities = capabilities[FEATURE_ID_V3]; const permissions = getUICapabilities(casesCapabilities); return useMemo( () => ({ actions: { crud: !!capabilities.actions?.save, read: !!capabilities.actions?.show }, - generalCasesV2: { + generalCasesV3: { all: permissions.all, create: permissions.create, read: permissions.read, @@ -195,6 +195,7 @@ export const useApplicationCapabilities = (): UseApplicationCapabilities => { settings: permissions.settings, reopenCase: permissions.reopenCase, createComment: permissions.createComment, + assign: permissions.assign, }, visualize: { crud: !!capabilities.visualize?.save, read: !!capabilities.visualize?.show }, dashboard: { @@ -219,6 +220,7 @@ export const useApplicationCapabilities = (): UseApplicationCapabilities => { permissions.settings, permissions.reopenCase, permissions.createComment, + permissions.assign, ] ); }; diff --git a/x-pack/platform/plugins/shared/cases/public/common/lib/kibana/kibana_react.mock.tsx b/x-pack/platform/plugins/shared/cases/public/common/lib/kibana/kibana_react.mock.tsx index e644c9604d495..8332ebd24b1c6 100644 --- a/x-pack/platform/plugins/shared/cases/public/common/lib/kibana/kibana_react.mock.tsx +++ b/x-pack/platform/plugins/shared/cases/public/common/lib/kibana/kibana_react.mock.tsx @@ -83,7 +83,7 @@ export const createStartServicesMock = ({ license }: StartServiceArgs = {}): Sta services.application.capabilities = { ...services.application.capabilities, actions: { save: true, show: true }, - generalCasesV2: { + generalCasesV3: { create_cases: true, read_cases: true, update_cases: true, @@ -93,6 +93,7 @@ export const createStartServicesMock = ({ license }: StartServiceArgs = {}): Sta cases_settings: true, case_reopen: true, create_comment: true, + cases_assign: true, }, visualize: { save: true, show: true }, dashboard: { show: true, createNew: true }, diff --git a/x-pack/platform/plugins/shared/cases/public/common/mock/permissions.ts b/x-pack/platform/plugins/shared/cases/public/common/mock/permissions.ts index 9e08120a8c275..19982e453b13d 100644 --- a/x-pack/platform/plugins/shared/cases/public/common/mock/permissions.ts +++ b/x-pack/platform/plugins/shared/cases/public/common/mock/permissions.ts @@ -19,6 +19,7 @@ export const noCasesPermissions = () => settings: false, createComment: false, reopenCase: false, + assign: false, }); export const readCasesPermissions = () => @@ -32,12 +33,14 @@ export const readCasesPermissions = () => settings: false, createComment: false, reopenCase: false, + assign: false, }); export const noCreateCasesPermissions = () => buildCasesPermissions({ create: false }); export const noCreateCommentCasesPermissions = () => buildCasesPermissions({ createComment: false }); export const noUpdateCasesPermissions = () => buildCasesPermissions({ update: false, reopenCase: false }); +export const noAssignCasesPermissions = () => buildCasesPermissions({ assign: false }); export const noPushCasesPermissions = () => buildCasesPermissions({ push: false }); export const noDeleteCasesPermissions = () => buildCasesPermissions({ delete: false }); export const noReopenCasesPermissions = () => buildCasesPermissions({ reopenCase: false }); @@ -51,6 +54,7 @@ export const onlyCreateCommentPermissions = () => push: false, createComment: true, reopenCase: false, + assign: false, }); export const onlyDeleteCasesPermission = () => buildCasesPermissions({ @@ -61,6 +65,7 @@ export const onlyDeleteCasesPermission = () => push: false, createComment: false, reopenCase: false, + assign: false, }); // In practice, a real life user should never have this configuration, but testing for thoroughness export const onlyReopenCasesPermission = () => @@ -72,6 +77,7 @@ export const onlyReopenCasesPermission = () => push: false, createComment: false, reopenCase: true, + assign: false, }); export const noConnectorsCasePermission = () => buildCasesPermissions({ connectors: false }); export const noCasesSettingsPermission = () => buildCasesPermissions({ settings: false }); @@ -87,6 +93,7 @@ export const buildCasesPermissions = (overrides: Partial cases_settings: false, create_comment: false, case_reopen: false, + cases_assign: false, }); export const readCasesCapabilities = () => buildCasesCapabilities({ @@ -134,6 +144,7 @@ export const readCasesCapabilities = () => cases_settings: false, create_comment: false, case_reopen: false, + cases_assign: false, }); export const writeCasesCapabilities = () => { return buildCasesCapabilities({ @@ -152,5 +163,6 @@ export const buildCasesCapabilities = (overrides?: Partial) = cases_settings: overrides?.cases_settings ?? true, create_comment: overrides?.create_comment ?? true, case_reopen: overrides?.case_reopen ?? true, + cases_assign: overrides?.cases_assign ?? true, }; }; diff --git a/x-pack/platform/plugins/shared/cases/public/common/use_cases_features.tsx b/x-pack/platform/plugins/shared/cases/public/common/use_cases_features.tsx index b9910c366fb11..c063d7dfe3572 100644 --- a/x-pack/platform/plugins/shared/cases/public/common/use_cases_features.tsx +++ b/x-pack/platform/plugins/shared/cases/public/common/use_cases_features.tsx @@ -20,7 +20,10 @@ export interface UseCasesFeatures { } export const useCasesFeatures = (): UseCasesFeatures => { - const { features } = useCasesContext(); + const { + features, + permissions: { assign }, + } = useCasesContext(); const { isAtLeastPlatinum } = useLicense(); const hasLicenseGreaterThanPlatinum = isAtLeastPlatinum(); @@ -37,11 +40,17 @@ export const useCasesFeatures = (): UseCasesFeatures => { */ isSyncAlertsEnabled: !features.alerts.enabled ? false : features.alerts.sync, metricsFeatures: features.metrics, - caseAssignmentAuthorized: hasLicenseGreaterThanPlatinum, + caseAssignmentAuthorized: hasLicenseGreaterThanPlatinum && assign, pushToServiceAuthorized: hasLicenseGreaterThanPlatinum, observablesAuthorized: hasLicenseGreaterThanPlatinum, }), - [features.alerts.enabled, features.alerts.sync, features.metrics, hasLicenseGreaterThanPlatinum] + [ + features.alerts.enabled, + features.alerts.sync, + features.metrics, + hasLicenseGreaterThanPlatinum, + assign, + ] ); return casesFeatures; diff --git a/x-pack/platform/plugins/shared/cases/public/components/actions/use_items_action.tsx b/x-pack/platform/plugins/shared/cases/public/components/actions/use_items_action.tsx index 238aec45781c0..c5264b699fbb2 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/actions/use_items_action.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/actions/use_items_action.tsx @@ -34,7 +34,8 @@ export const useItemsAction = ({ const [isFlyoutOpen, setIsFlyoutOpen] = useState(false); const [selectedCasesToEdit, setSelectedCasesToEdit] = useState([]); const canUpdateStatus = permissions.update; - const isActionDisabled = isDisabled || !canUpdateStatus; + const canUpdateAssignee = permissions.assign; + const isActionDisabled = isDisabled || (!canUpdateStatus && !canUpdateAssignee); const onFlyoutClosed = useCallback(() => setIsFlyoutOpen(false), []); const openFlyout = useCallback( diff --git a/x-pack/platform/plugins/shared/cases/public/components/all_cases/all_cases_list.tsx b/x-pack/platform/plugins/shared/cases/public/components/all_cases/all_cases_list.tsx index 82e011366e884..343a006ae86b3 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/all_cases/all_cases_list.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/all_cases/all_cases_list.tsx @@ -47,6 +47,7 @@ export interface AllCasesListProps { export const AllCasesList = React.memo( ({ hiddenStatuses = [], isSelectorView = false, onRowClick }) => { const { owner, permissions } = useCasesContext(); + const availableSolutions = useAvailableCasesOwners(getAllPermissionsExceptFrom('delete')); const isLoading = useIsLoadingCases(); const { euiTheme } = useEuiTheme(); diff --git a/x-pack/platform/plugins/shared/cases/public/components/all_cases/use_actions.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/all_cases/use_actions.test.tsx index e98926bcd0b40..05a380b2744aa 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/all_cases/use_actions.test.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/all_cases/use_actions.test.tsx @@ -396,6 +396,7 @@ describe('useActions', () => { connectors: true, settings: false, createComment: false, + assign: false, }, }); @@ -431,6 +432,7 @@ describe('useActions', () => { connectors: true, settings: false, createComment: false, + assign: false, }, }); @@ -466,6 +468,7 @@ describe('useActions', () => { connectors: true, settings: false, createComment: false, + assign: false, }, }); diff --git a/x-pack/platform/plugins/shared/cases/public/components/all_cases/use_actions.tsx b/x-pack/platform/plugins/shared/cases/public/components/all_cases/use_actions.tsx index e34f64a2a6283..d35e3ab3a6fdf 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/all_cases/use_actions.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/all_cases/use_actions.tsx @@ -38,6 +38,7 @@ const ActionColumnComponent: React.FC<{ theCase: CaseUI; disableActions: boolean const tooglePopover = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [isPopoverOpen]); const closePopover = useCallback(() => setIsPopoverOpen(false), []); const refreshCases = useRefreshCases(); + const { permissions } = useCasesContext(); const shouldDisable = useShouldDisableStatus(); @@ -83,6 +84,7 @@ const ActionColumnComponent: React.FC<{ theCase: CaseUI; disableActions: boolean const canDelete = deleteAction.canDelete; const canUpdate = statusAction.canUpdateStatus; + const canAssign = permissions.assign; const panels = useMemo((): EuiContextMenuPanelDescriptor[] => { const mainPanelItems: EuiContextMenuPanelItemDescriptor[] = []; @@ -136,6 +138,9 @@ const ActionColumnComponent: React.FC<{ theCase: CaseUI; disableActions: boolean if (canUpdate) { mainPanelItems.push(tagsAction.getAction([theCase])); + } + + if (canAssign) { mainPanelItems.push(assigneesAction.getAction([theCase])); } @@ -164,6 +169,7 @@ const ActionColumnComponent: React.FC<{ theCase: CaseUI; disableActions: boolean }, [ assigneesAction, canDelete, + canAssign, canUpdate, copyIDAction, deleteAction, @@ -242,7 +248,8 @@ interface UseBulkActionsProps { export const useActions = ({ disableActions }: UseBulkActionsProps): UseBulkActionsReturnValue => { const { permissions } = useCasesContext(); - const shouldShowActions = permissions.update || permissions.delete || permissions.reopenCase; + const shouldShowActions = + permissions.update || permissions.delete || permissions.reopenCase || permissions.assign; return { actions: shouldShowActions diff --git a/x-pack/platform/plugins/shared/cases/public/components/all_cases/use_bulk_actions.tsx b/x-pack/platform/plugins/shared/cases/public/components/all_cases/use_bulk_actions.tsx index 98828b00369f5..f20ce20a8b0e4 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/all_cases/use_bulk_actions.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/all_cases/use_bulk_actions.tsx @@ -18,6 +18,7 @@ import { useStatusAction } from '../actions/status/use_status_action'; import { EditTagsFlyout } from '../actions/tags/edit_tags_flyout'; import { useTagsAction } from '../actions/tags/use_tags_action'; import { ConfirmDeleteCaseModal } from '../confirm_delete_case'; +import { useCasesContext } from '../cases_context/use_cases_context'; import { useAssigneesAction } from '../actions/assignees/use_assignees_action'; import { EditAssigneesFlyout } from '../actions/assignees/edit_assignees_flyout'; import * as i18n from './translations'; @@ -40,6 +41,7 @@ export const useBulkActions = ({ onActionSuccess, }: UseBulkActionsProps): UseBulkActionsReturnValue => { const isDisabled = selectedCases.length === 0; + const { permissions } = useCasesContext(); const deleteAction = useDeleteAction({ isDisabled, @@ -73,6 +75,7 @@ export const useBulkActions = ({ const canDelete = deleteAction.canDelete; const canUpdate = statusAction.canUpdateStatus; + const canAssign = permissions.assign; const panels = useMemo((): EuiContextMenuPanelDescriptor[] => { const mainPanelItems: EuiContextMenuPanelItemDescriptor[] = []; @@ -110,6 +113,9 @@ export const useBulkActions = ({ if (canUpdate) { mainPanelItems.push(tagsAction.getAction(selectedCases)); + } + + if (canAssign) { mainPanelItems.push(assigneesAction.getAction(selectedCases)); } @@ -141,6 +147,7 @@ export const useBulkActions = ({ }, [ canDelete, canUpdate, + canAssign, deleteAction, isDisabled, selectedCases, diff --git a/x-pack/platform/plugins/shared/cases/public/components/all_cases/utility_bar.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/all_cases/utility_bar.test.tsx index 63d8cd2e5faab..0f91de7bc2b65 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/all_cases/utility_bar.test.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/all_cases/utility_bar.test.tsx @@ -209,6 +209,44 @@ describe('Severity form field', () => { }); }); + it('does show the bulk actions with only assign permissions', async () => { + appMockRender = createAppMockRenderer({ + permissions: { + ...noCasesPermissions(), + assign: true, + }, + }); + appMockRender.render(); + + expect(await screen.findByTestId('case-table-bulk-actions-link-icon')).toBeInTheDocument(); + }); + + it('shows bulk actions when only assignCase and update permissions are present', async () => { + appMockRender = createAppMockRenderer({ + permissions: { + ...noCasesPermissions(), + assign: true, + update: true, + }, + }); + appMockRender.render(); + + expect(await screen.findByTestId('case-table-bulk-actions-link-icon')).toBeInTheDocument(); + }); + + it('shows bulk actions when only assignCase and delete permissions are present', async () => { + appMockRender = createAppMockRenderer({ + permissions: { + ...noCasesPermissions(), + assign: true, + delete: true, + }, + }); + appMockRender.render(); + + expect(await screen.findByTestId('case-table-bulk-actions-link-icon')).toBeInTheDocument(); + }); + describe('Maximum number of cases', () => { const newProps = { ...props, diff --git a/x-pack/platform/plugins/shared/cases/public/components/all_cases/utility_bar.tsx b/x-pack/platform/plugins/shared/cases/public/components/all_cases/utility_bar.tsx index 389de5068ed51..3f7088acd03a0 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/all_cases/utility_bar.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/all_cases/utility_bar.tsx @@ -95,7 +95,7 @@ export const CasesTableUtilityBar: FunctionComponent = React.memo( * in the useBulkActions hook. */ const showBulkActions = - (permissions.update || permissions.delete || permissions.reopenCase) && + (permissions.update || permissions.delete || permissions.reopenCase || permissions.assign) && selectedCases.length > 0; const visibleCases = diff --git a/x-pack/platform/plugins/shared/cases/public/components/app/index.tsx b/x-pack/platform/plugins/shared/cases/public/components/app/index.tsx index eaa334470ab0f..86cf83064e4b5 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/app/index.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/app/index.tsx @@ -39,7 +39,7 @@ const CasesAppComponent: React.FC = ({ getFilesClient, owner: [APP_OWNER], useFetchAlertData: () => [false, {}], - permissions: userCapabilities.generalCasesV2, + permissions: userCapabilities.generalCasesV3, basePath: '/', features: { alerts: { enabled: true, sync: false } }, })} diff --git a/x-pack/platform/plugins/shared/cases/public/components/app/use_available_owners.test.ts b/x-pack/platform/plugins/shared/cases/public/components/app/use_available_owners.test.ts index 5a19e9a0f995b..a63268a1d6bdd 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/app/use_available_owners.test.ts +++ b/x-pack/platform/plugins/shared/cases/public/components/app/use_available_owners.test.ts @@ -21,15 +21,15 @@ jest.mock('../../common/lib/kibana'); const useKibanaMock = useKibana as jest.MockedFunction; const hasAll = { - securitySolutionCasesV2: allCasesCapabilities(), - observabilityCasesV2: allCasesCapabilities(), - generalCasesV2: allCasesCapabilities(), + securitySolutionCasesV3: allCasesCapabilities(), + observabilityCasesV3: allCasesCapabilities(), + generalCasesV3: allCasesCapabilities(), }; const secAllObsReadGenNone = { - securitySolutionCasesV2: allCasesCapabilities(), - observabilityCasesV2: readCasesCapabilities(), - generalCasesV2: noCasesCapabilities(), + securitySolutionCasesV3: allCasesCapabilities(), + observabilityCasesV3: readCasesCapabilities(), + generalCasesV3: noCasesCapabilities(), }; const unrelatedFeatures = { diff --git a/x-pack/platform/plugins/shared/cases/public/components/app/use_available_owners.ts b/x-pack/platform/plugins/shared/cases/public/components/app/use_available_owners.ts index 4220ff8cdecd4..ecaaafe965dd7 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/app/use_available_owners.ts +++ b/x-pack/platform/plugins/shared/cases/public/components/app/use_available_owners.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { APP_ID, FEATURE_ID_V2 } from '../../../common/constants'; +import { APP_ID, FEATURE_ID_V3 } from '../../../common/constants'; import { useKibana } from '../../common/lib/kibana'; import type { CasesPermissions } from '../../containers/types'; import { allCasePermissions } from '../../utils/permissions'; @@ -22,14 +22,14 @@ export const useAvailableCasesOwners = ( capabilities: Capability[] = allCasePermissions ): string[] => { const { capabilities: kibanaCapabilities } = useKibana().services.application; - return Object.entries(kibanaCapabilities).reduce( (availableOwners: string[], [featureId, kibanaCapability]) => { - if (!featureId.endsWith('CasesV2')) { + if (!featureId.endsWith('CasesV3')) { return availableOwners; } for (const cap of capabilities) { - const hasCapability = !!kibanaCapability[`${cap}_cases`]; + const hasCapability = + !!kibanaCapability[`${cap}_cases`] || !!kibanaCapability[`cases_${cap}`]; if (!hasCapability) { return availableOwners; } @@ -42,9 +42,9 @@ export const useAvailableCasesOwners = ( }; const getOwnerFromFeatureID = (featureID: string) => { - if (featureID === FEATURE_ID_V2) { + if (featureID === FEATURE_ID_V3) { return APP_ID; } - return featureID.replace('CasesV2', ''); + return featureID.replace('CasesV3', ''); }; diff --git a/x-pack/platform/plugins/shared/cases/public/components/case_view/components/assign_users.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/case_view/components/assign_users.test.tsx index 36384b9b4cd6f..22910c5451490 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/case_view/components/assign_users.test.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/case_view/components/assign_users.test.tsx @@ -11,7 +11,7 @@ import { userProfiles, userProfilesMap } from '../../../containers/user_profiles import { fireEvent, screen, waitFor } from '@testing-library/react'; import React from 'react'; import type { AppMockRenderer } from '../../../common/mock'; -import { createAppMockRenderer, noUpdateCasesPermissions } from '../../../common/mock'; +import { createAppMockRenderer, noAssignCasesPermissions } from '../../../common/mock'; import type { AssignUsersProps } from './assign_users'; import { AssignUsers } from './assign_users'; import { waitForEuiPopoverClose, waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl'; @@ -49,15 +49,15 @@ describe('AssignUsers', () => { expect(screen.getByText('No users are assigned')).toBeInTheDocument(); }); - it('does not show the suggest users edit button when the user does not have update permissions', () => { - appMockRender = createAppMockRenderer({ permissions: noUpdateCasesPermissions() }); + it('does not show the suggest users edit button when the user does not have assign permissions', () => { + appMockRender = createAppMockRenderer({ permissions: noAssignCasesPermissions() }); appMockRender.render(); expect(screen.queryByText('case-view-assignees-edit')).not.toBeInTheDocument(); }); - it('does not show the assign users link when the user does not have update permissions', () => { - appMockRender = createAppMockRenderer({ permissions: noUpdateCasesPermissions() }); + it('does not show the assign users link when the user does not have assign permissions', () => { + appMockRender = createAppMockRenderer({ permissions: noAssignCasesPermissions() }); appMockRender.render(); expect(screen.queryByTestId('assign yourself')).not.toBeInTheDocument(); diff --git a/x-pack/platform/plugins/shared/cases/public/components/case_view/components/assign_users.tsx b/x-pack/platform/plugins/shared/cases/public/components/case_view/components/assign_users.tsx index 6b29a9df14d91..fd4777f63a609 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/case_view/components/assign_users.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/case_view/components/assign_users.tsx @@ -180,7 +180,7 @@ const AssignUsersComponent: React.FC = ({ {isLoading && } - {!isLoading && permissions.update && ( + {!isLoading && permissions.assign && ( { appMockRender.coreStart.application.capabilities = { ...appMockRender.coreStart.application.capabilities, - observabilityCasesV2: { - create_cases: true, - read_cases: true, - update_cases: true, - push_cases: true, - cases_connectors: true, - delete_cases: true, - cases_settings: true, - }, - securitySolutionCasesV2: { - create_cases: true, - read_cases: true, - update_cases: true, - push_cases: true, - cases_connectors: true, - delete_cases: true, - cases_settings: true, - }, + generalCasesV3: allCasesCapabilities(), + observabilityCasesV3: allCasesCapabilities(), + securitySolutionCasesV3: allCasesCapabilities(), }; const spyOnGetCases = jest.spyOn(api, 'getCases'); @@ -107,6 +92,12 @@ describe('useGetCases', () => { it('should set only the available owners when no owner is provided', async () => { appMockRender = createAppMockRenderer({ owner: [] }); + + appMockRender.coreStart.application.capabilities = { + ...appMockRender.coreStart.application.capabilities, + generalCasesV3: allCasesCapabilities(), + }; + const spyOnGetCases = jest.spyOn(api, 'getCases'); renderHook(() => useGetCases(), { diff --git a/x-pack/platform/plugins/shared/cases/public/utils/permissions.test.ts b/x-pack/platform/plugins/shared/cases/public/utils/permissions.test.ts index 66e63e6950dd4..1235959243ccb 100644 --- a/x-pack/platform/plugins/shared/cases/public/utils/permissions.test.ts +++ b/x-pack/platform/plugins/shared/cases/public/utils/permissions.test.ts @@ -10,7 +10,15 @@ import { getAllPermissionsExceptFrom, isReadOnlyPermissions } from './permission describe('permissions', () => { describe('isReadOnlyPermissions', () => { - const tests = [['update'], ['create'], ['delete'], ['push'], ['all']]; + const tests = [ + ['update'], + ['create'], + ['delete'], + ['push'], + ['all'], + ['assign'], + ['createComment'], + ]; it('returns true if the user has only read permissions', async () => { expect(isReadOnlyPermissions(readCasesPermissions())).toBe(true); @@ -31,7 +39,13 @@ describe('permissions', () => { describe('getAllPermissionsExceptFrom', () => { it('returns the correct permissions', async () => { - expect(getAllPermissionsExceptFrom('create')).toEqual(['read', 'update', 'delete', 'push']); + expect(getAllPermissionsExceptFrom('create')).toEqual([ + 'read', + 'update', + 'delete', + 'push', + 'assign', + ]); }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/public/utils/permissions.ts b/x-pack/platform/plugins/shared/cases/public/utils/permissions.ts index 29aba5648abd9..3f2d1c7b4cccf 100644 --- a/x-pack/platform/plugins/shared/cases/public/utils/permissions.ts +++ b/x-pack/platform/plugins/shared/cases/public/utils/permissions.ts @@ -14,13 +14,22 @@ export const isReadOnlyPermissions = (permissions: CasesPermissions) => { !permissions.update && !permissions.delete && !permissions.push && + !permissions.assign && + !permissions.createComment && permissions.read ); }; type CasePermission = Exclude; -export const allCasePermissions: CasePermission[] = ['create', 'read', 'update', 'delete', 'push']; +export const allCasePermissions: CasePermission[] = [ + 'create', + 'read', + 'update', + 'delete', + 'push', + 'assign', +]; export const getAllPermissionsExceptFrom = (capToExclude: CasePermission): CasePermission[] => allCasePermissions.filter((permission) => permission !== capToExclude) as CasePermission[]; diff --git a/x-pack/platform/plugins/shared/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap b/x-pack/platform/plugins/shared/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap index b8129f9111b9c..79e052ca9c87c 100644 --- a/x-pack/platform/plugins/shared/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap +++ b/x-pack/platform/plugins/shared/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap @@ -1,5 +1,89 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`audit_logger log function event structure creates the correct audit event for operation: "assignCase" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "cases_assign", + "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: "assignCase" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "cases_assign", + "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: "assignCase" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "cases_assign", + "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: "assignCase" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "cases_assign", + "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: "bulkCreateAttachments" with an error and entity 1`] = ` Object { "error": Object { diff --git a/x-pack/platform/plugins/shared/cases/server/authorization/index.ts b/x-pack/platform/plugins/shared/cases/server/authorization/index.ts index 1b4d7cc4bd730..caa697594cc1e 100644 --- a/x-pack/platform/plugins/shared/cases/server/authorization/index.ts +++ b/x-pack/platform/plugins/shared/cases/server/authorization/index.ts @@ -190,6 +190,14 @@ const CaseOperations = { docType: 'case', savedObjectType: CASE_SAVED_OBJECT, }, + [WriteOperations.AssignCase]: { + ecsType: EVENT_TYPES.change, + name: WriteOperations.AssignCase as const, + action: 'cases_assign', + verbs: updateVerbs, + docType: 'case', + savedObjectType: CASE_SAVED_OBJECT, + }, }; const ConfigurationOperations = { diff --git a/x-pack/platform/plugins/shared/cases/server/authorization/types.ts b/x-pack/platform/plugins/shared/cases/server/authorization/types.ts index 537071b857e6d..1720062b649d5 100644 --- a/x-pack/platform/plugins/shared/cases/server/authorization/types.ts +++ b/x-pack/platform/plugins/shared/cases/server/authorization/types.ts @@ -64,6 +64,7 @@ export enum WriteOperations { CreateConfiguration = 'createConfiguration', UpdateConfiguration = 'updateConfiguration', ReopenCase = 'reopenCase', + AssignCase = 'assignCase', } /** diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_create.test.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_create.test.ts index ace3c39d31e0a..fb5e2c7ca54e5 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_create.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_create.test.ts @@ -416,6 +416,65 @@ describe('bulkCreate', () => { { id: 'mock-saved-object-id', owner: 'securitySolution' }, { id: 'mock-saved-object-id', owner: 'cases' }, ], + operation: [ + { + action: 'cases_assign', + docType: 'case', + ecsType: 'change', + name: 'assignCase', + savedObjectType: 'cases', + verbs: { past: 'updated', present: 'update', progressive: 'updating' }, + }, + { + action: 'case_create', + docType: 'case', + ecsType: 'creation', + name: 'createCase', + savedObjectType: 'cases', + verbs: { past: 'created', present: 'create', progressive: 'creating' }, + }, + ], + }); + }); + + it('validates with assign+create operations when cases have assignees', async () => { + await bulkCreate( + { cases: [getCases()[0], getCases({ owner: 'cases' })[0]] }, + clientArgs, + casesClientMock + ); + + expect(clientArgs.authorization.ensureAuthorized).toHaveBeenCalledWith({ + entities: [ + { id: 'mock-saved-object-id', owner: 'securitySolution' }, + { id: 'mock-saved-object-id', owner: 'cases' }, + ], + operation: [ + { + action: 'cases_assign', + docType: 'case', + ecsType: 'change', + name: 'assignCase', + savedObjectType: 'cases', + verbs: { past: 'updated', present: 'update', progressive: 'updating' }, + }, + { + action: 'case_create', + docType: 'case', + ecsType: 'creation', + name: 'createCase', + savedObjectType: 'cases', + verbs: { past: 'created', present: 'create', progressive: 'creating' }, + }, + ], + }); + }); + + it('validates with only create operation when cases have no assignees', async () => { + await bulkCreate({ cases: [getCases({ assignees: [] })[0]] }, clientArgs, casesClientMock); + + expect(clientArgs.authorization.ensureAuthorized).toHaveBeenCalledWith({ + entities: [{ id: 'mock-saved-object-id', owner: 'securitySolution' }], operation: { action: 'case_create', docType: 'case', diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_create.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_create.ts index b94e16481a35a..12f351347d4d5 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_create.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_create.ts @@ -54,10 +54,20 @@ export const bulkCreate = async ( const casesWithIds = getCaseWithIds(decodedData); - await auth.ensureAuthorized({ - operation: Operations.createCase, - entities: casesWithIds.map((theCase) => ({ owner: theCase.owner, id: theCase.id })), - }); + if ( + casesWithIds.filter((theCase) => theCase.assignees && theCase.assignees.length !== 0).length > + 0 + ) { + await auth.ensureAuthorized({ + operation: [Operations.assignCase, Operations.createCase], + entities: casesWithIds.map((theCase) => ({ owner: theCase.owner, id: theCase.id })), + }); + } else { + await auth.ensureAuthorized({ + operation: Operations.createCase, + entities: casesWithIds.map((theCase) => ({ owner: theCase.owner, id: theCase.id })), + }); + } const hasPlatinumLicenseOrGreater = await licensingService.isAtLeastPlatinum(); diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_update.test.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_update.test.ts index da0fc03712067..9e80cf5752006 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_update.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_update.test.ts @@ -20,7 +20,7 @@ import { import { mockCases } from '../../mocks'; import { createCasesClientMock, createCasesClientMockArgs } from '../mocks'; import { Operations } from '../../authorization'; -import { bulkUpdate } from './bulk_update'; +import { bulkUpdate, getOperationsToAuthorize } from './bulk_update'; describe('update', () => { const cases = { @@ -315,6 +315,90 @@ describe('update', () => { 'Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: The length of the field assignees is too long. Array must be of length <= 10.' ); }); + + it('returns only updateCase operation when no reopened cases or changed assignees', () => { + const operations = getOperationsToAuthorize({ + reopenedCases: [], + changedAssignees: [], + allCases: cases.cases, + }); + expect(operations).toEqual([Operations.updateCase]); + }); + + it('returns only assignCase operation when all cases are assignee changes', () => { + const operations = getOperationsToAuthorize({ + reopenedCases: [], + changedAssignees: cases.cases, + allCases: cases.cases, + }); + expect(operations).toEqual([Operations.assignCase]); + }); + + it('returns only reopenCase operation when all cases are being reopened', () => { + const operations = getOperationsToAuthorize({ + reopenedCases: cases.cases, + changedAssignees: [], + allCases: cases.cases, + }); + expect(operations).toEqual([Operations.reopenCase]); + }); + + it('returns assignCase and updateCase when some cases have non-assignee changes', () => { + const case2 = { id: 'case-2', version: '1' }; + const operations = getOperationsToAuthorize({ + reopenedCases: [], + changedAssignees: cases.cases, + allCases: [...cases.cases, case2], + }); + expect(operations).toEqual([Operations.assignCase, Operations.updateCase]); + }); + + it('returns reopenCase and updateCase when some cases have non-reopen changes', () => { + const case2 = { id: 'case-2', version: '1' }; + const operations = getOperationsToAuthorize({ + reopenedCases: cases.cases, + changedAssignees: [], + allCases: [...cases.cases, case2], + }); + expect(operations).toEqual([Operations.reopenCase, Operations.updateCase]); + }); + + it('returns all operations when cases have mixed changes', () => { + const case2 = { id: 'case-2', version: '1' }; + const case3 = { id: 'case-3', version: '1' }; + const operations = getOperationsToAuthorize({ + reopenedCases: cases.cases, + changedAssignees: [case2], + allCases: [...cases.cases, case2, case3], + }); + expect(operations).toEqual([ + Operations.reopenCase, + Operations.assignCase, + Operations.updateCase, + ]); + }); + + it('handles empty casesToAuthorize array', () => { + const operations = getOperationsToAuthorize({ + reopenedCases: [], + changedAssignees: [], + allCases: [], + }); + expect(operations).toEqual([]); + }); + + it('returns only combined operations when all cases have both reopen and assignee changes', () => { + const operations = getOperationsToAuthorize({ + reopenedCases: cases.cases, + changedAssignees: cases.cases, + allCases: cases.cases, + }); + expect(operations).toEqual([ + Operations.reopenCase, + Operations.assignCase, + Operations.updateCase, + ]); + }); }); describe('Category', () => { @@ -1514,6 +1598,59 @@ describe('update', () => { ); }); + it('throws an error if the case is not found', async () => { + clientArgsMock.services.caseService.getCases.mockResolvedValue({ saved_objects: [] }); + + await expect( + bulkUpdate( + { + cases: [ + { + id: mockCases[0].id, + version: mockCases[0].version ?? '', + status: CaseStatuses.open, + }, + ], + }, + clientArgsMock, + casesClientMock + ) + ).rejects.toThrow( + 'Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: These cases mock-id-1 do not exist. Please check you have the correct ids.' + ); + }); + + it('throws an error if the case is not found and the SO clients returns an SO object', async () => { + clientArgsMock.services.caseService.getCases.mockResolvedValue({ + saved_objects: [ + { + type: 'cases', + id: 'mock-id-1', + references: [], + error: { error: 'Non found', message: 'Non found', statusCode: 404 }, + }, + ], + }); + + await expect( + bulkUpdate( + { + cases: [ + { + id: mockCases[0].id, + version: mockCases[0].version ?? '', + status: CaseStatuses.open, + }, + ], + }, + clientArgsMock, + casesClientMock + ) + ).rejects.toThrow( + 'Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: These cases mock-id-1 do not exist. Please check you have the correct ids.' + ); + }); + describe('Validate max user actions per page', () => { beforeEach(() => { jest.clearAllMocks(); @@ -1674,7 +1811,7 @@ describe('update', () => { }); }); - it('checks authorization for both reopenCase and updateCase operations when reopening a case', async () => { + it('checks authorization for only reopenCase', async () => { // Mock a closed case const closedCase = { ...mockCases[0], @@ -1683,6 +1820,7 @@ describe('update', () => { status: CaseStatuses.closed, }, }; + clientArgs.services.caseService.getCases.mockResolvedValue({ saved_objects: [closedCase] }); clientArgs.services.caseService.patchCases.mockResolvedValue({ @@ -1703,7 +1841,10 @@ describe('update', () => { casesClientMock ); - expect(clientArgs.authorization.ensureAuthorized).not.toThrow(); + expect(clientArgs.authorization.ensureAuthorized).toHaveBeenCalledWith({ + entities: [{ id: closedCase.id, owner: closedCase.attributes.owner }], + operation: [Operations.reopenCase], + }); }); it('throws when user is not authorized to update case', async () => { @@ -1728,38 +1869,6 @@ describe('update', () => { `"Failed to update case, ids: [{\\"id\\":\\"mock-id-1\\",\\"version\\":\\"WzAsMV0=\\"}]: Error: Unauthorized"` ); }); - - it('throws when user is not authorized to reopen case', async () => { - const closedCase = { - ...mockCases[0], - attributes: { - ...mockCases[0].attributes, - status: CaseStatuses.closed, - }, - }; - clientArgs.services.caseService.getCases.mockResolvedValue({ saved_objects: [closedCase] }); - - const error = new Error('Unauthorized to reopen case'); - clientArgs.authorization.ensureAuthorized.mockRejectedValueOnce(error); // Reject reopenCase - - await expect( - bulkUpdate( - { - cases: [ - { - id: closedCase.id, - version: closedCase.version ?? '', - status: CaseStatuses.open, - }, - ], - }, - clientArgs, - casesClientMock - ) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Failed to update case, ids: [{\\"id\\":\\"mock-id-1\\",\\"version\\":\\"WzAsMV0=\\"}]: Error: Unauthorized to reopen case"` - ); - }); }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_update.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_update.ts index 9a90168b858de..d714d7f22e83c 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_update.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_update.ts @@ -14,13 +14,14 @@ import type { SavedObjectsFindResult, SavedObjectsUpdateResponse, } from '@kbn/core/server'; +import { isEqual } from 'lodash'; import { nodeBuilder } from '@kbn/es-query'; import type { AlertService, CasesService, CaseUserActionService } from '../../services'; import type { UpdateAlertStatusRequest } from '../alerts/types'; import type { CasesClient, CasesClientArgs } from '..'; -import type { OwnerEntity } from '../../authorization'; +import type { OwnerEntity, OperationDetails } from '../../authorization'; import type { PatchCasesArgs } from '../../services/cases/types'; import type { UserActionEvent, UserActionsDict } from '../../services/user_actions/types'; @@ -273,10 +274,12 @@ function partitionPatchRequest( // This will be a deduped array of case IDs with their corresponding owner casesToAuthorize: OwnerEntity[]; reopenedCases: CasePatchRequest[]; + changedAssignees: CasePatchRequest[]; } { const nonExistingCases: CasePatchRequest[] = []; const conflictedCases: CasePatchRequest[] = []; const reopenedCases: CasePatchRequest[] = []; + const changedAssignees: CasePatchRequest[] = []; const casesToAuthorize: Map = new Map(); for (const reqCase of patchReqCases) { @@ -295,19 +298,62 @@ function partitionPatchRequest( ) { // Track cases that are closed and a user is attempting to reopen reopenedCases.push(reqCase); + casesToAuthorize.set(foundCase.id, { id: foundCase.id, owner: foundCase.attributes.owner }); } else { casesToAuthorize.set(foundCase.id, { id: foundCase.id, owner: foundCase.attributes.owner }); } + if (reqCase.assignees) { + if ( + !isEqual( + reqCase.assignees.map(({ uid }) => uid), + foundCase?.attributes.assignees.map(({ uid }) => uid) + ) && + foundCase + ) { + changedAssignees.push(reqCase); + } + } } return { nonExistingCases, conflictedCases, reopenedCases, + changedAssignees, casesToAuthorize: Array.from(casesToAuthorize.values()), }; } +export function getOperationsToAuthorize({ + reopenedCases, + changedAssignees, + allCases, +}: { + reopenedCases: CasePatchRequest[]; + changedAssignees: CasePatchRequest[]; + allCases: CasePatchRequest[]; +}): OperationDetails[] { + const operations: OperationDetails[] = []; + const onlyAssigneeOperations = + reopenedCases.length === 0 && changedAssignees.length === allCases.length; + const onlyReopenOperations = + changedAssignees.length === 0 && reopenedCases.length === allCases.length; + + if (reopenedCases.length > 0) { + operations.push(Operations.reopenCase); + } + + if (changedAssignees.length > 0) { + operations.push(Operations.assignCase); + } + + if (!onlyAssigneeOperations && !onlyReopenOperations) { + operations.push(Operations.updateCase); + } + + return operations; +} + export interface UpdateRequestWithOriginalCase { updateReq: CasePatchRequest; originalCase: CaseSavedObjectTransformed; @@ -354,13 +400,14 @@ export const bulkUpdate = async ( return acc; }, new Map()); - const { nonExistingCases, conflictedCases, casesToAuthorize, reopenedCases } = + const { nonExistingCases, conflictedCases, casesToAuthorize, reopenedCases, changedAssignees } = partitionPatchRequest(casesMap, query.cases); - const operationsToAuthorize = - reopenedCases.length > 0 - ? [Operations.reopenCase, Operations.updateCase] - : [Operations.updateCase]; + const operationsToAuthorize = getOperationsToAuthorize({ + reopenedCases, + changedAssignees, + allCases: query.cases, + }); await authorization.ensureAuthorized({ entities: casesToAuthorize, diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/create.test.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/create.test.ts index 5a3eb7bd4f54f..9bc5455a0633d 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/create.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/create.test.ts @@ -116,6 +116,51 @@ describe('create', () => { `Failed to create case: Error: In order to assign users to cases, you must be subscribed to an Elastic Platinum license` ); }); + + it('validates with assign+create operations when cases have assignees', async () => { + clientArgs.services.licensingService.isAtLeastPlatinum.mockResolvedValue(true); + await create(theCase, clientArgs, casesClientMock); + + expect(clientArgs.authorization.ensureAuthorized).toHaveBeenCalledWith( + expect.objectContaining({ + operation: [ + { + action: 'cases_assign', + docType: 'case', + ecsType: 'change', + name: 'assignCase', + savedObjectType: 'cases', + verbs: { past: 'updated', present: 'update', progressive: 'updating' }, + }, + { + action: 'case_create', + docType: 'case', + ecsType: 'creation', + name: 'createCase', + savedObjectType: 'cases', + verbs: { past: 'created', present: 'create', progressive: 'creating' }, + }, + ], + }) + ); + }); + + it('validates with only create operation when cases have no assignees', async () => { + await create({ ...theCase, assignees: [] }, clientArgs, casesClientMock); + + expect(clientArgs.authorization.ensureAuthorized).toHaveBeenCalledWith( + expect.objectContaining({ + operation: { + action: 'case_create', + docType: 'case', + ecsType: 'creation', + name: 'createCase', + savedObjectType: 'cases', + verbs: { past: 'created', present: 'create', progressive: 'creating' }, + }, + }) + ); + }); }); describe('Attributes', () => { diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/create.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/create.ts index b1d444ec2a3c5..88dafdb4e9dc3 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/create.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/create.ts @@ -52,11 +52,17 @@ export const create = async ( validateCustomFields(customFieldsValidationParams); const savedObjectID = SavedObjectsUtils.generateId(); - - await auth.ensureAuthorized({ - operation: Operations.createCase, - entities: [{ owner: query.owner, id: savedObjectID }], - }); + if (query.assignees && query.assignees.length > 0) { + await auth.ensureAuthorized({ + operation: [Operations.assignCase, Operations.createCase], + entities: [{ owner: query.owner, id: savedObjectID }], + }); + } else { + await auth.ensureAuthorized({ + operation: Operations.createCase, + entities: [{ owner: query.owner, id: savedObjectID }], + }); + } /** * Assign users to a case is only available to Platinum+ diff --git a/x-pack/platform/plugins/shared/cases/server/connectors/cases/index.test.ts b/x-pack/platform/plugins/shared/cases/server/connectors/cases/index.test.ts index c0480d694184f..4c9320d969adb 100644 --- a/x-pack/platform/plugins/shared/cases/server/connectors/cases/index.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/connectors/cases/index.test.ts @@ -37,6 +37,7 @@ describe('getCasesConnectorType', () => { 'cases:my-owner/deleteComment', 'cases:my-owner/findConfigurations', 'cases:my-owner/reopenCase', + 'cases:my-owner/assignCase', ]); }); @@ -358,6 +359,7 @@ describe('getCasesConnectorType', () => { 'cases:securitySolution/deleteComment', 'cases:securitySolution/findConfigurations', 'cases:securitySolution/reopenCase', + 'cases:securitySolution/assignCase', ]); }); @@ -379,6 +381,7 @@ describe('getCasesConnectorType', () => { 'cases:observability/deleteComment', 'cases:observability/findConfigurations', 'cases:observability/reopenCase', + 'cases:observability/assignCase', ]); }); @@ -400,6 +403,7 @@ describe('getCasesConnectorType', () => { 'cases:securitySolution/deleteComment', 'cases:securitySolution/findConfigurations', 'cases:securitySolution/reopenCase', + 'cases:securitySolution/assignCase', ]); }); }); diff --git a/x-pack/platform/plugins/shared/cases/server/connectors/cases/utils.test.ts b/x-pack/platform/plugins/shared/cases/server/connectors/cases/utils.test.ts index 55ffb5c7170bd..d528aad486bc4 100644 --- a/x-pack/platform/plugins/shared/cases/server/connectors/cases/utils.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/connectors/cases/utils.test.ts @@ -508,6 +508,7 @@ describe('utils', () => { 'cases:my-owner/deleteComment', 'cases:my-owner/findConfigurations', 'cases:my-owner/reopenCase', + 'cases:my-owner/assignCase', ]); }); }); diff --git a/x-pack/platform/plugins/shared/cases/server/connectors/cases/utils.ts b/x-pack/platform/plugins/shared/cases/server/connectors/cases/utils.ts index e8dc85fa389d2..cc1b7dd9b8922 100644 --- a/x-pack/platform/plugins/shared/cases/server/connectors/cases/utils.ts +++ b/x-pack/platform/plugins/shared/cases/server/connectors/cases/utils.ts @@ -121,5 +121,6 @@ export const constructRequiredKibanaPrivileges = (owner: string): string[] => { `cases:${owner}/deleteComment`, `cases:${owner}/findConfigurations`, `cases:${owner}/reopenCase`, + `cases:${owner}/assignCase`, ]; }; diff --git a/x-pack/platform/plugins/shared/cases/server/features/constants.ts b/x-pack/platform/plugins/shared/cases/server/features/constants.ts index fb0a0f4554dee..13c50e36b91be 100644 --- a/x-pack/platform/plugins/shared/cases/server/features/constants.ts +++ b/x-pack/platform/plugins/shared/cases/server/features/constants.ts @@ -16,3 +16,4 @@ export const CASES_DELETE_SUB_PRIVILEGE_ID = 'cases_delete'; export const CASES_SETTINGS_SUB_PRIVILEGE_ID = 'cases_settings'; export const CASES_CREATE_COMMENT_SUB_PRIVILEGE_ID = 'create_comment'; export const CASES_REOPEN_SUB_PRIVILEGE_ID = 'case_reopen'; +export const CASES_ASSIGN_SUB_PRIVILEGE_ID = 'cases_assign'; diff --git a/x-pack/platform/plugins/shared/cases/server/features/index.ts b/x-pack/platform/plugins/shared/cases/server/features/index.ts index afa3dfab9b311..f2f19b7d270f2 100644 --- a/x-pack/platform/plugins/shared/cases/server/features/index.ts +++ b/x-pack/platform/plugins/shared/cases/server/features/index.ts @@ -8,8 +8,10 @@ import type { KibanaFeatureConfig } from '@kbn/features-plugin/common'; import { getV1 } from './v1'; import { getV2 } from './v2'; +import { getV3 } from './v3'; export const getCasesKibanaFeatures = (): { v1: KibanaFeatureConfig; v2: KibanaFeatureConfig; -} => ({ v1: getV1(), v2: getV2() }); + v3: KibanaFeatureConfig; +} => ({ v1: getV1(), v2: getV2(), v3: getV3() }); diff --git a/x-pack/platform/plugins/shared/cases/server/features/v1.ts b/x-pack/platform/plugins/shared/cases/server/features/v1.ts index 25a43434f3723..30ffe7d1f015e 100644 --- a/x-pack/platform/plugins/shared/cases/server/features/v1.ts +++ b/x-pack/platform/plugins/shared/cases/server/features/v1.ts @@ -12,7 +12,7 @@ import { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/s import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import { KibanaFeatureScope } from '@kbn/features-plugin/common'; -import { APP_ID, FEATURE_ID, FEATURE_ID_V2 } from '../../common/constants'; +import { APP_ID, FEATURE_ID, FEATURE_ID_V3 } from '../../common/constants'; import { createUICapabilities, getApiTags } from '../../common'; import { CASES_DELETE_SUB_PRIVILEGE_ID, CASES_SETTINGS_SUB_PRIVILEGE_ID } from './constants'; @@ -35,7 +35,7 @@ export const getV1 = (): KibanaFeatureConfig => { 'The {currentId} permissions are deprecated, please see {casesFeatureIdV2}.', values: { currentId: FEATURE_ID, - casesFeatureIdV2: FEATURE_ID_V2, + casesFeatureIdV2: FEATURE_ID_V3, }, }), }, @@ -61,6 +61,7 @@ export const getV1 = (): KibanaFeatureConfig => { push: [APP_ID], createComment: [APP_ID], reopenCase: [APP_ID], + assign: [APP_ID], }, management: { insightsAndAlerting: [APP_ID], @@ -69,13 +70,18 @@ export const getV1 = (): KibanaFeatureConfig => { all: [...filesSavedObjectTypes], read: [...filesSavedObjectTypes], }, - ui: capabilities.all, + ui: [ + ...capabilities.all, + ...capabilities.createComment, + ...capabilities.reopenCase, + ...capabilities.assignCase, + ], replacedBy: { - default: [{ feature: FEATURE_ID_V2, privileges: ['all'] }], + default: [{ feature: FEATURE_ID_V3, privileges: ['all'] }], minimal: [ { - feature: FEATURE_ID_V2, - privileges: ['minimal_all', 'create_comment', 'case_reopen'], + feature: FEATURE_ID_V3, + privileges: ['minimal_all', 'create_comment', 'case_reopen', 'cases_assign'], }, ], }, @@ -94,8 +100,8 @@ export const getV1 = (): KibanaFeatureConfig => { }, ui: capabilities.read, replacedBy: { - default: [{ feature: FEATURE_ID_V2, privileges: ['read'] }], - minimal: [{ feature: FEATURE_ID_V2, privileges: ['minimal_read'] }], + default: [{ feature: FEATURE_ID_V3, privileges: ['read'] }], + minimal: [{ feature: FEATURE_ID_V3, privileges: ['minimal_read'] }], }, }, }, @@ -124,7 +130,7 @@ export const getV1 = (): KibanaFeatureConfig => { }, ui: capabilities.delete, replacedBy: [ - { feature: FEATURE_ID_V2, privileges: [CASES_DELETE_SUB_PRIVILEGE_ID] }, + { feature: FEATURE_ID_V3, privileges: [CASES_DELETE_SUB_PRIVILEGE_ID] }, ], }, ], @@ -154,7 +160,7 @@ export const getV1 = (): KibanaFeatureConfig => { }, ui: capabilities.settings, replacedBy: [ - { feature: FEATURE_ID_V2, privileges: [CASES_SETTINGS_SUB_PRIVILEGE_ID] }, + { feature: FEATURE_ID_V3, privileges: [CASES_SETTINGS_SUB_PRIVILEGE_ID] }, ], }, ], diff --git a/x-pack/platform/plugins/shared/cases/server/features/v2.ts b/x-pack/platform/plugins/shared/cases/server/features/v2.ts index fca97303f02ab..7eb48a9c55508 100644 --- a/x-pack/platform/plugins/shared/cases/server/features/v2.ts +++ b/x-pack/platform/plugins/shared/cases/server/features/v2.ts @@ -12,7 +12,7 @@ import { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/s import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import { KibanaFeatureScope } from '@kbn/features-plugin/common'; -import { APP_ID, FEATURE_ID_V2 } from '../../common/constants'; +import { APP_ID, FEATURE_ID_V2, FEATURE_ID_V3 } from '../../common/constants'; import { createUICapabilities, getApiTags } from '../../common'; import { CASES_DELETE_SUB_PRIVILEGE_ID, @@ -34,6 +34,16 @@ export const getV2 = (): KibanaFeatureConfig => { const apiTags = getApiTags(APP_ID); return { + deprecated: { + notice: i18n.translate('xpack.cases.features.casesFeatureV2.deprecationMessage', { + defaultMessage: + 'The {currentId} permissions are deprecated, please see {casesFeatureIdV3}.', + values: { + currentId: FEATURE_ID_V2, + casesFeatureIdV3: FEATURE_ID_V3, + }, + }), + }, id: FEATURE_ID_V2, name: i18n.translate('xpack.cases.features.casesFeatureName', { defaultMessage: 'Cases', @@ -54,6 +64,7 @@ export const getV2 = (): KibanaFeatureConfig => { read: [APP_ID], update: [APP_ID], push: [APP_ID], + assign: [APP_ID], }, management: { insightsAndAlerting: [APP_ID], @@ -62,7 +73,16 @@ export const getV2 = (): KibanaFeatureConfig => { all: [...filesSavedObjectTypes], read: [...filesSavedObjectTypes], }, - ui: capabilities.all, + ui: [...capabilities.all, ...capabilities.assignCase], + replacedBy: { + default: [{ feature: FEATURE_ID_V3, privileges: ['all'] }], + minimal: [ + { + feature: FEATURE_ID_V3, + privileges: ['minimal_all', 'cases_assign'], + }, + ], + }, }, read: { api: apiTags.read, @@ -77,6 +97,10 @@ export const getV2 = (): KibanaFeatureConfig => { read: [...filesSavedObjectTypes], }, ui: capabilities.read, + replacedBy: { + default: [{ feature: FEATURE_ID_V3, privileges: ['read'] }], + minimal: [{ feature: FEATURE_ID_V3, privileges: ['minimal_read'] }], + }, }, }, subFeatures: [ @@ -103,6 +127,9 @@ export const getV2 = (): KibanaFeatureConfig => { delete: [APP_ID], }, ui: capabilities.delete, + replacedBy: [ + { feature: FEATURE_ID_V3, privileges: [CASES_DELETE_SUB_PRIVILEGE_ID] }, + ], }, ], }, @@ -130,6 +157,9 @@ export const getV2 = (): KibanaFeatureConfig => { settings: [APP_ID], }, ui: capabilities.settings, + replacedBy: [ + { feature: FEATURE_ID_V3, privileges: [CASES_SETTINGS_SUB_PRIVILEGE_ID] }, + ], }, ], }, @@ -158,6 +188,9 @@ export const getV2 = (): KibanaFeatureConfig => { createComment: [APP_ID], }, ui: capabilities.createComment, + replacedBy: [ + { feature: FEATURE_ID_V3, privileges: [CASES_CREATE_COMMENT_SUB_PRIVILEGE_ID] }, + ], }, ], }, @@ -185,6 +218,9 @@ export const getV2 = (): KibanaFeatureConfig => { reopenCase: [APP_ID], }, ui: capabilities.reopenCase, + replacedBy: [ + { feature: FEATURE_ID_V3, privileges: [CASES_REOPEN_SUB_PRIVILEGE_ID] }, + ], }, ], }, diff --git a/x-pack/platform/plugins/shared/cases/server/features/v3.ts b/x-pack/platform/plugins/shared/cases/server/features/v3.ts new file mode 100644 index 0000000000000..732fd837468c0 --- /dev/null +++ b/x-pack/platform/plugins/shared/cases/server/features/v3.ts @@ -0,0 +1,223 @@ +/* + * 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'; + +import type { KibanaFeatureConfig } from '@kbn/features-plugin/common'; +import { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/saved_objects'; +import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; + +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; +import { APP_ID, FEATURE_ID_V3 } from '../../common/constants'; +import { createUICapabilities, getApiTags } from '../../common'; +import { + CASES_DELETE_SUB_PRIVILEGE_ID, + CASES_SETTINGS_SUB_PRIVILEGE_ID, + CASES_CREATE_COMMENT_SUB_PRIVILEGE_ID, + CASES_REOPEN_SUB_PRIVILEGE_ID, + CASES_ASSIGN_SUB_PRIVILEGE_ID, +} from './constants'; + +/** + * The order of appearance in the feature privilege page + * under the management section. Cases should be under + * the Actions and Connectors feature + */ + +const FEATURE_ORDER = 3100; + +export const getV3 = (): KibanaFeatureConfig => { + const capabilities = createUICapabilities(); + const apiTags = getApiTags(APP_ID); + + return { + id: FEATURE_ID_V3, + name: i18n.translate('xpack.cases.features.casesFeatureName', { + defaultMessage: 'Cases', + }), + category: DEFAULT_APP_CATEGORIES.management, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], + app: [], + order: FEATURE_ORDER, + management: { + insightsAndAlerting: [APP_ID], + }, + cases: [APP_ID], + privileges: { + all: { + api: apiTags.all, + cases: { + create: [APP_ID], + read: [APP_ID], + update: [APP_ID], + push: [APP_ID], + }, + management: { + insightsAndAlerting: [APP_ID], + }, + savedObject: { + all: [...filesSavedObjectTypes], + read: [...filesSavedObjectTypes], + }, + ui: capabilities.all, + }, + read: { + api: apiTags.read, + cases: { + read: [APP_ID], + }, + management: { + insightsAndAlerting: [APP_ID], + }, + savedObject: { + all: [], + read: [...filesSavedObjectTypes], + }, + ui: capabilities.read, + }, + }, + subFeatures: [ + { + name: i18n.translate('xpack.cases.features.deleteSubFeatureName', { + defaultMessage: 'Delete', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + api: apiTags.delete, + id: CASES_DELETE_SUB_PRIVILEGE_ID, + name: i18n.translate('xpack.cases.features.deleteSubFeatureDetails', { + defaultMessage: 'Delete cases and comments', + }), + includeIn: 'all', + savedObject: { + all: [...filesSavedObjectTypes], + read: [...filesSavedObjectTypes], + }, + cases: { + delete: [APP_ID], + }, + ui: capabilities.delete, + }, + ], + }, + ], + }, + { + name: i18n.translate('xpack.cases.features.casesSettingsSubFeatureName', { + defaultMessage: 'Case settings', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: CASES_SETTINGS_SUB_PRIVILEGE_ID, + name: i18n.translate('xpack.cases.features.casesSettingsSubFeatureDetails', { + defaultMessage: 'Edit case settings', + }), + includeIn: 'all', + savedObject: { + all: [...filesSavedObjectTypes], + read: [...filesSavedObjectTypes], + }, + cases: { + settings: [APP_ID], + }, + ui: capabilities.settings, + }, + ], + }, + ], + }, + { + name: i18n.translate('xpack.cases.features.addCommentsSubFeatureName', { + defaultMessage: 'Create comments & attachments', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + api: apiTags.createComment, + id: CASES_CREATE_COMMENT_SUB_PRIVILEGE_ID, + name: i18n.translate('xpack.cases.features.addCommentsSubFeatureDetails', { + defaultMessage: 'Add comments to cases', + }), + includeIn: 'all', + savedObject: { + all: [...filesSavedObjectTypes], + read: [...filesSavedObjectTypes], + }, + cases: { + createComment: [APP_ID], + }, + ui: capabilities.createComment, + }, + ], + }, + ], + }, + { + name: i18n.translate('xpack.cases.features.reopenCaseSubFeatureName', { + defaultMessage: 'Re-open', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: CASES_REOPEN_SUB_PRIVILEGE_ID, + name: i18n.translate('xpack.cases.features.reopenCaseSubFeatureDetails', { + defaultMessage: 'Re-open closed cases', + }), + includeIn: 'all', + savedObject: { + all: [], + read: [], + }, + cases: { + reopenCase: [APP_ID], + }, + ui: capabilities.reopenCase, + }, + ], + }, + ], + }, + { + name: i18n.translate('xpack.cases.features.assignUsersSubFeatureName', { + defaultMessage: 'Assign users', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: CASES_ASSIGN_SUB_PRIVILEGE_ID, + name: i18n.translate('xpack.cases.features.assignUsersSubFeatureName', { + defaultMessage: 'Assign users to cases', + }), + includeIn: 'all', + savedObject: { + all: [], + read: [], + }, + cases: { + assign: [APP_ID], + }, + ui: capabilities.assignCase, + }, + ], + }, + ], + }, + ], + }; +}; diff --git a/x-pack/platform/plugins/shared/cases/server/plugin.ts b/x-pack/platform/plugins/shared/cases/server/plugin.ts index d62aa5582ec32..f7f00b3c49b6c 100644 --- a/x-pack/platform/plugins/shared/cases/server/plugin.ts +++ b/x-pack/platform/plugins/shared/cases/server/plugin.ts @@ -100,6 +100,7 @@ export class CasePlugin const casesFeatures = getCasesKibanaFeatures(); plugins.features.registerKibanaFeature(casesFeatures.v1); plugins.features.registerKibanaFeature(casesFeatures.v2); + plugins.features.registerKibanaFeature(casesFeatures.v3); } registerSavedObjects({ diff --git a/x-pack/platform/plugins/shared/features/common/feature_kibana_privileges.ts b/x-pack/platform/plugins/shared/features/common/feature_kibana_privileges.ts index 725ababcaaf90..5a7f074b12ee7 100644 --- a/x-pack/platform/plugins/shared/features/common/feature_kibana_privileges.ts +++ b/x-pack/platform/plugins/shared/features/common/feature_kibana_privileges.ts @@ -238,6 +238,16 @@ export interface FeatureKibanaPrivileges { * ``` */ reopenCase?: readonly string[]; + /** + * List of case owners whose users should have assignCase access when granted this privilege. + * @example + * ```ts + * { + * assign: ['securitySolution'] + * } + * ``` + */ + assign?: readonly string[]; }; /** diff --git a/x-pack/platform/plugins/shared/features/server/__snapshots__/oss_features.test.ts.snap b/x-pack/platform/plugins/shared/features/server/__snapshots__/oss_features.test.ts.snap index cc32fa26b475d..fabfb34c67c4b 100644 --- a/x-pack/platform/plugins/shared/features/server/__snapshots__/oss_features.test.ts.snap +++ b/x-pack/platform/plugins/shared/features/server/__snapshots__/oss_features.test.ts.snap @@ -558,6 +558,7 @@ Array [ ], "cases": Object { "all": Array [], + "assign": Array [], "create": Array [], "createComment": Array [], "delete": Array [], @@ -722,6 +723,7 @@ Array [ ], "cases": Object { "all": Array [], + "assign": Array [], "create": Array [], "createComment": Array [], "delete": Array [], @@ -1075,6 +1077,7 @@ Array [ ], "cases": Object { "all": Array [], + "assign": Array [], "create": Array [], "createComment": Array [], "delete": Array [], @@ -1222,6 +1225,7 @@ Array [ ], "cases": Object { "all": Array [], + "assign": Array [], "create": Array [], "createComment": Array [], "delete": Array [], @@ -1386,6 +1390,7 @@ Array [ ], "cases": Object { "all": Array [], + "assign": Array [], "create": Array [], "createComment": Array [], "delete": Array [], @@ -1739,6 +1744,7 @@ Array [ ], "cases": Object { "all": Array [], + "assign": Array [], "create": Array [], "createComment": Array [], "delete": Array [], diff --git a/x-pack/platform/plugins/shared/features/server/feature_privilege_iterator/feature_privilege_iterator.test.ts b/x-pack/platform/plugins/shared/features/server/feature_privilege_iterator/feature_privilege_iterator.test.ts index c4e542a7ebcde..fba4b8cf8b54a 100644 --- a/x-pack/platform/plugins/shared/features/server/feature_privilege_iterator/feature_privilege_iterator.test.ts +++ b/x-pack/platform/plugins/shared/features/server/feature_privilege_iterator/feature_privilege_iterator.test.ts @@ -80,6 +80,7 @@ describe('featurePrivilegeIterator', () => { settings: ['cases-settings-type'], createComment: ['cases-create-comment-type'], reopenCase: ['cases-reopen-type'], + assign: ['cases-assign-type'], }, ui: ['ui-action'], }, @@ -152,6 +153,7 @@ describe('featurePrivilegeIterator', () => { settings: ['cases-settings-type'], createComment: ['cases-create-comment-type'], reopenCase: ['cases-reopen-type'], + assign: ['cases-assign-type'], }, ui: ['ui-action'], }, @@ -223,6 +225,7 @@ describe('featurePrivilegeIterator', () => { settings: ['cases-settings-type'], createComment: ['cases-create-comment-type'], reopenCase: ['cases-reopen-type'], + assign: ['cases-assign-type'], }, ui: ['ui-action'], }, @@ -296,6 +299,7 @@ describe('featurePrivilegeIterator', () => { settings: ['cases-settings-type'], createComment: ['cases-create-comment-type'], reopenCase: ['cases-reopen-type'], + assign: ['cases-assign-type'], }, ui: ['ui-action'], }, @@ -339,6 +343,7 @@ describe('featurePrivilegeIterator', () => { settings: ['cases-settings-type'], createComment: ['cases-create-comment-type'], reopenCase: ['cases-reopen-type'], + assign: ['cases-assign-type'], }, ui: ['ui-action'], }, @@ -403,6 +408,7 @@ describe('featurePrivilegeIterator', () => { settings: ['cases-settings-sub-type'], createComment: ['cases-create-comment-type'], reopenCase: ['cases-reopen-type'], + assign: ['cases-assign-type'], }, ui: ['ui-sub-type'], }, @@ -452,6 +458,7 @@ describe('featurePrivilegeIterator', () => { settings: ['cases-settings-type'], createComment: ['cases-create-comment-type'], reopenCase: ['cases-reopen-type'], + assign: ['cases-assign-type'], }, ui: ['ui-action'], }, @@ -522,6 +529,7 @@ describe('featurePrivilegeIterator', () => { settings: ['cases-settings-type'], createComment: ['cases-create-comment-type'], reopenCase: ['cases-reopen-type'], + assign: ['cases-assign-type'], }, ui: ['ui-action'], }, @@ -586,6 +594,7 @@ describe('featurePrivilegeIterator', () => { settings: ['cases-settings-sub-type'], createComment: ['cases-create-comment-type'], reopenCase: ['cases-reopen-type'], + assign: ['cases-assign-type'], }, ui: ['ui-sub-type'], }, @@ -635,6 +644,7 @@ describe('featurePrivilegeIterator', () => { settings: ['cases-settings-type'], createComment: ['cases-create-comment-type'], reopenCase: ['cases-reopen-type'], + assign: ['cases-assign-type'], }, ui: ['ui-action'], }, @@ -705,6 +715,7 @@ describe('featurePrivilegeIterator', () => { settings: ['cases-settings-type'], createComment: ['cases-create-comment-type'], reopenCase: ['cases-reopen-type'], + assign: ['cases-assign-type'], }, ui: ['ui-action'], }, @@ -770,6 +781,7 @@ describe('featurePrivilegeIterator', () => { settings: ['cases-settings-sub-type'], createComment: ['cases-create-comment-sub-type'], reopenCase: ['cases-reopen-sub-type'], + assign: ['cases-assign-sub-type'], }, ui: ['ui-sub-type'], }, @@ -822,6 +834,7 @@ describe('featurePrivilegeIterator', () => { settings: ['cases-settings-type', 'cases-settings-sub-type'], createComment: ['cases-create-comment-type', 'cases-create-comment-sub-type'], reopenCase: ['cases-reopen-type', 'cases-reopen-sub-type'], + assign: ['cases-assign-type', 'cases-assign-sub-type'], }, ui: ['ui-action', 'ui-sub-type'], }, @@ -860,6 +873,7 @@ describe('featurePrivilegeIterator', () => { settings: ['cases-settings-sub-type'], createComment: ['cases-create-comment-sub-type'], reopenCase: ['cases-reopen-sub-type'], + assign: ['cases-assign-sub-type'], }, ui: ['ui-action', 'ui-sub-type'], }, @@ -905,6 +919,7 @@ describe('featurePrivilegeIterator', () => { settings: ['cases-settings-type'], createComment: ['cases-create-comment-type'], reopenCase: ['cases-reopen-type'], + assign: ['cases-assign-type'], }, ui: ['ui-action'], }, @@ -1012,6 +1027,7 @@ describe('featurePrivilegeIterator', () => { settings: ['cases-settings-type'], createComment: ['cases-create-comment-type'], reopenCase: ['cases-reopen-type'], + assign: ['cases-assign-type'], }, ui: ['ui-action'], }, @@ -1049,6 +1065,7 @@ describe('featurePrivilegeIterator', () => { settings: [], createComment: [], reopenCase: [], + assign: [], }, ui: ['ui-action'], }, @@ -1092,6 +1109,7 @@ describe('featurePrivilegeIterator', () => { settings: ['cases-settings-type'], createComment: ['cases-create-comment-type'], reopenCase: ['cases-reopen-type'], + assign: ['cases-assign-type'], }, ui: ['ui-action'], }, @@ -1157,6 +1175,7 @@ describe('featurePrivilegeIterator', () => { settings: ['cases-settings-sub-type'], createComment: ['cases-create-comment-sub-type'], reopenCase: ['cases-reopen-sub-type'], + assign: ['cases-assign-sub-type'], }, ui: ['ui-sub-type'], }, @@ -1209,6 +1228,7 @@ describe('featurePrivilegeIterator', () => { settings: ['cases-settings-type', 'cases-settings-sub-type'], createComment: ['cases-create-comment-type', 'cases-create-comment-sub-type'], reopenCase: ['cases-reopen-type', 'cases-reopen-sub-type'], + assign: ['cases-assign-type', 'cases-assign-sub-type'], }, ui: ['ui-action', 'ui-sub-type'], }, @@ -1456,6 +1476,7 @@ describe('featurePrivilegeIterator', () => { settings: ['cases-settings-sub-type'], createComment: ['cases-create-comment-sub-type'], reopenCase: ['cases-reopen-sub-type'], + assign: [], }, ui: ['ui-sub-type'], }, @@ -1494,6 +1515,7 @@ describe('featurePrivilegeIterator', () => { settings: ['cases-settings-sub-type'], createComment: ['cases-create-comment-sub-type'], reopenCase: ['cases-reopen-sub-type'], + assign: [], }, ui: ['ui-sub-type'], }, @@ -1630,6 +1652,7 @@ describe('featurePrivilegeIterator', () => { settings: ['cases-settings-type'], createComment: ['cases-create-comment-type'], reopenCase: ['cases-reopen-type'], + assign: [], }, ui: ['ui-action'], }, @@ -1667,6 +1690,7 @@ describe('featurePrivilegeIterator', () => { settings: [], createComment: [], reopenCase: [], + assign: [], }, ui: ['ui-action'], }, diff --git a/x-pack/platform/plugins/shared/features/server/feature_privilege_iterator/feature_privilege_iterator.ts b/x-pack/platform/plugins/shared/features/server/feature_privilege_iterator/feature_privilege_iterator.ts index 45518004430a4..792b4a3d0ded9 100644 --- a/x-pack/platform/plugins/shared/features/server/feature_privilege_iterator/feature_privilege_iterator.ts +++ b/x-pack/platform/plugins/shared/features/server/feature_privilege_iterator/feature_privilege_iterator.ts @@ -180,6 +180,10 @@ function mergeWithSubFeatures( mergedConfig.cases?.reopenCase ?? [], subFeaturePrivilege.cases?.reopenCase ?? [] ), + assign: mergeArrays( + mergedConfig.cases?.assign ?? [], + subFeaturePrivilege.cases?.assign ?? [] + ), }; } return mergedConfig; diff --git a/x-pack/platform/plugins/shared/features/server/feature_schema.ts b/x-pack/platform/plugins/shared/features/server/feature_schema.ts index 4766e597f2211..6b7c6e1fa2e0a 100644 --- a/x-pack/platform/plugins/shared/features/server/feature_schema.ts +++ b/x-pack/platform/plugins/shared/features/server/feature_schema.ts @@ -92,6 +92,7 @@ const casesSchemaObject = schema.maybe( settings: schema.maybe(casesSchema), createComment: schema.maybe(casesSchema), reopenCase: schema.maybe(casesSchema), + assign: schema.maybe(casesSchema), }) ); diff --git a/x-pack/solutions/observability/plugins/exploratory_view/public/components/shared/exploratory_view/header/add_to_case_action.test.tsx b/x-pack/solutions/observability/plugins/exploratory_view/public/components/shared/exploratory_view/header/add_to_case_action.test.tsx index 011fb93553ac4..ad7abb87d8d08 100644 --- a/x-pack/solutions/observability/plugins/exploratory_view/public/components/shared/exploratory_view/header/add_to_case_action.test.tsx +++ b/x-pack/solutions/observability/plugins/exploratory_view/public/components/shared/exploratory_view/header/add_to_case_action.test.tsx @@ -122,6 +122,7 @@ describe('AddToCaseAction', function () { settings: false, createComment: false, reopenCase: false, + assign: false, }, }) ); diff --git a/x-pack/solutions/observability/plugins/observability/common/index.ts b/x-pack/solutions/observability/plugins/observability/common/index.ts index 6ffad0b2ed436..044d10572d796 100644 --- a/x-pack/solutions/observability/plugins/observability/common/index.ts +++ b/x-pack/solutions/observability/plugins/observability/common/index.ts @@ -66,6 +66,7 @@ export { /** @deprecated deprecated in 8.17. Please use casesFeatureIdV2 instead */ export const casesFeatureId = 'observabilityCases'; export const casesFeatureIdV2 = 'observabilityCasesV2'; +export const casesFeatureIdV3 = 'observabilityCasesV3'; export const sloFeatureId = 'slo'; // The ID of the observability app. Should more appropriately be called // 'observability' but it's used in telemetry by applicationUsage so we don't diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/cases/components/cases.stories.tsx b/x-pack/solutions/observability/plugins/observability/public/pages/cases/components/cases.stories.tsx index a0fa1368d28f6..862d26354f777 100644 --- a/x-pack/solutions/observability/plugins/observability/public/pages/cases/components/cases.stories.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/pages/cases/components/cases.stories.tsx @@ -30,6 +30,7 @@ const defaultProps: CasesProps = { settings: true, reopenCase: true, createComment: true, + assign: true, }, }; @@ -49,5 +50,6 @@ CasesPageWithNoPermissions.args = { settings: false, reopenCase: false, createComment: false, + assign: false, }, }; diff --git a/x-pack/solutions/observability/plugins/observability/server/features/cases_v1.ts b/x-pack/solutions/observability/plugins/observability/server/features/cases_v1.ts index 836d1f44731e9..8a111035173af 100644 --- a/x-pack/solutions/observability/plugins/observability/server/features/cases_v1.ts +++ b/x-pack/solutions/observability/plugins/observability/server/features/cases_v1.ts @@ -10,7 +10,7 @@ import { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/s import { i18n } from '@kbn/i18n'; import { KibanaFeatureConfig, KibanaFeatureScope } from '@kbn/features-plugin/common'; import { CasesUiCapabilities, CasesApiTags } from '@kbn/cases-plugin/common'; -import { casesFeatureId, casesFeatureIdV2, observabilityFeatureId } from '../../common'; +import { casesFeatureId, casesFeatureIdV3, observabilityFeatureId } from '../../common'; export const getCasesFeature = ( casesCapabilities: CasesUiCapabilities, @@ -22,10 +22,10 @@ export const getCasesFeature = ( 'xpack.observability.featureRegistry.linkObservabilityTitle.deprecationMessage', { defaultMessage: - 'The {currentId} permissions are deprecated, please see {casesFeatureIdV2}.', + 'The {currentId} permissions are deprecated, please see {casesFeatureIdV3}.', values: { currentId: casesFeatureId, - casesFeatureIdV2, + casesFeatureIdV3, }, } ), @@ -52,18 +52,24 @@ export const getCasesFeature = ( push: [observabilityFeatureId], createComment: [observabilityFeatureId], reopenCase: [observabilityFeatureId], + assign: [observabilityFeatureId], }, savedObject: { all: [...filesSavedObjectTypes], read: [...filesSavedObjectTypes], }, - ui: casesCapabilities.all, + ui: [ + ...casesCapabilities.all, + ...casesCapabilities.createComment, + ...casesCapabilities.reopenCase, + ...casesCapabilities.assignCase, + ], replacedBy: { - default: [{ feature: casesFeatureIdV2, privileges: ['all'] }], + default: [{ feature: casesFeatureIdV3, privileges: ['all'] }], minimal: [ { - feature: casesFeatureIdV2, - privileges: ['minimal_all', 'create_comment', 'case_reopen'], + feature: casesFeatureIdV3, + privileges: ['minimal_all', 'create_comment', 'case_reopen', 'cases_assign'], }, ], }, @@ -81,8 +87,8 @@ export const getCasesFeature = ( }, ui: casesCapabilities.read, replacedBy: { - default: [{ feature: casesFeatureIdV2, privileges: ['read'] }], - minimal: [{ feature: casesFeatureIdV2, privileges: ['minimal_read'] }], + default: [{ feature: casesFeatureIdV3, privileges: ['read'] }], + minimal: [{ feature: casesFeatureIdV3, privileges: ['minimal_read'] }], }, }, }, @@ -110,7 +116,7 @@ export const getCasesFeature = ( delete: [observabilityFeatureId], }, ui: casesCapabilities.delete, - replacedBy: [{ feature: casesFeatureIdV2, privileges: ['cases_delete'] }], + replacedBy: [{ feature: casesFeatureIdV3, privileges: ['cases_delete'] }], }, ], }, @@ -141,7 +147,7 @@ export const getCasesFeature = ( settings: [observabilityFeatureId], }, ui: casesCapabilities.settings, - replacedBy: [{ feature: casesFeatureIdV2, privileges: ['cases_settings'] }], + replacedBy: [{ feature: casesFeatureIdV3, privileges: ['cases_settings'] }], }, ], }, diff --git a/x-pack/solutions/observability/plugins/observability/server/features/cases_v2.ts b/x-pack/solutions/observability/plugins/observability/server/features/cases_v2.ts index 52b501a62bb2e..9f3e899f5ff6e 100644 --- a/x-pack/solutions/observability/plugins/observability/server/features/cases_v2.ts +++ b/x-pack/solutions/observability/plugins/observability/server/features/cases_v2.ts @@ -10,12 +10,26 @@ import { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/s import { i18n } from '@kbn/i18n'; import { KibanaFeatureConfig, KibanaFeatureScope } from '@kbn/features-plugin/common'; import { CasesUiCapabilities, CasesApiTags } from '@kbn/cases-plugin/common'; -import { casesFeatureIdV2, casesFeatureId, observabilityFeatureId } from '../../common'; +import { + casesFeatureIdV2, + casesFeatureId, + observabilityFeatureId, + casesFeatureIdV3, +} from '../../common'; export const getCasesFeatureV2 = ( casesCapabilities: CasesUiCapabilities, casesApiTags: CasesApiTags ): KibanaFeatureConfig => ({ + deprecated: { + notice: i18n.translate('xpack.observability.featureRegistry.casesFeature.deprecationMessage', { + defaultMessage: 'The {currentId} permissions are deprecated, please see {casesFeatureIdV3}.', + values: { + currentId: casesFeatureIdV2, + casesFeatureIdV3, + }, + }), + }, id: casesFeatureIdV2, name: i18n.translate('xpack.observability.featureRegistry.linkObservabilityTitle', { defaultMessage: 'Cases', @@ -36,12 +50,22 @@ export const getCasesFeatureV2 = ( read: [observabilityFeatureId], update: [observabilityFeatureId], push: [observabilityFeatureId], + assign: [observabilityFeatureId], }, savedObject: { all: [...filesSavedObjectTypes], read: [...filesSavedObjectTypes], }, - ui: casesCapabilities.all, + ui: [...casesCapabilities.all, ...casesCapabilities.assignCase], + replacedBy: { + default: [{ feature: casesFeatureIdV3, privileges: ['all'] }], + minimal: [ + { + feature: casesFeatureIdV3, + privileges: ['minimal_all', 'cases_assign'], + }, + ], + }, }, read: { api: casesApiTags.read, @@ -55,6 +79,10 @@ export const getCasesFeatureV2 = ( read: [...filesSavedObjectTypes], }, ui: casesCapabilities.read, + replacedBy: { + default: [{ feature: casesFeatureIdV3, privileges: ['read'] }], + minimal: [{ feature: casesFeatureIdV3, privileges: ['minimal_read'] }], + }, }, }, subFeatures: [ @@ -81,6 +109,7 @@ export const getCasesFeatureV2 = ( delete: [observabilityFeatureId], }, ui: casesCapabilities.delete, + replacedBy: [{ feature: casesFeatureIdV3, privileges: ['cases_delete'] }], }, ], }, @@ -111,6 +140,7 @@ export const getCasesFeatureV2 = ( settings: [observabilityFeatureId], }, ui: casesCapabilities.settings, + replacedBy: [{ feature: casesFeatureIdV3, privileges: ['cases_settings'] }], }, ], }, @@ -142,6 +172,7 @@ export const getCasesFeatureV2 = ( createComment: [observabilityFeatureId], }, ui: casesCapabilities.createComment, + replacedBy: [{ feature: casesFeatureIdV3, privileges: ['create_comment'] }], }, ], }, @@ -172,6 +203,7 @@ export const getCasesFeatureV2 = ( reopenCase: [observabilityFeatureId], }, ui: casesCapabilities.reopenCase, + replacedBy: [{ feature: casesFeatureIdV3, privileges: ['case_reopen'] }], }, ], }, diff --git a/x-pack/solutions/observability/plugins/observability/server/features/cases_v3.ts b/x-pack/solutions/observability/plugins/observability/server/features/cases_v3.ts new file mode 100644 index 0000000000000..fad40422f0baa --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability/server/features/cases_v3.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 { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; +import { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/saved_objects'; +import { i18n } from '@kbn/i18n'; +import { KibanaFeatureConfig, KibanaFeatureScope } from '@kbn/features-plugin/common'; +import { CasesUiCapabilities, CasesApiTags } from '@kbn/cases-plugin/common'; +import { casesFeatureIdV3, casesFeatureId, observabilityFeatureId } from '../../common'; + +export const getCasesFeatureV3 = ( + casesCapabilities: CasesUiCapabilities, + casesApiTags: CasesApiTags +): KibanaFeatureConfig => ({ + id: casesFeatureIdV3, + name: i18n.translate('xpack.observability.featureRegistry.linkObservabilityTitle', { + defaultMessage: 'Cases', + }), + order: 1100, + category: DEFAULT_APP_CATEGORIES.observability, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], + app: [casesFeatureId, 'kibana'], + catalogue: [observabilityFeatureId], + cases: [observabilityFeatureId], + privileges: { + all: { + api: casesApiTags.all, + app: [casesFeatureId, 'kibana'], + catalogue: [observabilityFeatureId], + cases: { + create: [observabilityFeatureId], + read: [observabilityFeatureId], + update: [observabilityFeatureId], + push: [observabilityFeatureId], + }, + savedObject: { + all: [...filesSavedObjectTypes], + read: [...filesSavedObjectTypes], + }, + ui: casesCapabilities.all, + }, + read: { + api: casesApiTags.read, + app: [casesFeatureId, 'kibana'], + catalogue: [observabilityFeatureId], + cases: { + read: [observabilityFeatureId], + }, + savedObject: { + all: [], + read: [...filesSavedObjectTypes], + }, + ui: casesCapabilities.read, + }, + }, + subFeatures: [ + { + name: i18n.translate('xpack.observability.featureRegistry.deleteSubFeatureName', { + defaultMessage: 'Delete', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + api: casesApiTags.delete, + id: 'cases_delete', + name: i18n.translate('xpack.observability.featureRegistry.deleteSubFeatureDetails', { + defaultMessage: 'Delete cases and comments', + }), + includeIn: 'all', + savedObject: { + all: [...filesSavedObjectTypes], + read: [...filesSavedObjectTypes], + }, + cases: { + delete: [observabilityFeatureId], + }, + ui: casesCapabilities.delete, + }, + ], + }, + ], + }, + { + name: i18n.translate('xpack.observability.featureRegistry.casesSettingsSubFeatureName', { + defaultMessage: 'Case settings', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'cases_settings', + name: i18n.translate( + 'xpack.observability.featureRegistry.casesSettingsSubFeatureDetails', + { + defaultMessage: 'Edit case settings', + } + ), + includeIn: 'all', + savedObject: { + all: [...filesSavedObjectTypes], + read: [...filesSavedObjectTypes], + }, + cases: { + settings: [observabilityFeatureId], + }, + ui: casesCapabilities.settings, + }, + ], + }, + ], + }, + { + name: i18n.translate('xpack.observability.featureRegistry.addCommentsSubFeatureName', { + defaultMessage: 'Create comments & attachments', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + api: casesApiTags.createComment, + id: 'create_comment', + name: i18n.translate( + 'xpack.observability.featureRegistry.addCommentsSubFeatureDetails', + { + defaultMessage: 'Add comments to cases', + } + ), + includeIn: 'all', + savedObject: { + all: [...filesSavedObjectTypes], + read: [...filesSavedObjectTypes], + }, + cases: { + createComment: [observabilityFeatureId], + }, + ui: casesCapabilities.createComment, + }, + ], + }, + ], + }, + { + name: i18n.translate('xpack.observability.featureRegistry.reopenCaseSubFeatureName', { + defaultMessage: 'Re-open', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'case_reopen', + name: i18n.translate( + 'xpack.observability.featureRegistry.reopenCaseSubFeatureDetails', + { + defaultMessage: 'Re-open closed cases', + } + ), + includeIn: 'all', + savedObject: { + all: [], + read: [], + }, + cases: { + reopenCase: [observabilityFeatureId], + }, + ui: casesCapabilities.reopenCase, + }, + ], + }, + ], + }, + { + name: i18n.translate('xpack.observability.features.assignUsersSubFeatureName', { + defaultMessage: 'Assign users', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'cases_assign', + name: i18n.translate('xpack.observability.features.assignUsersSubFeatureName', { + defaultMessage: 'Assign users to cases', + }), + includeIn: 'all', + savedObject: { + all: [], + read: [], + }, + cases: { + assign: [observabilityFeatureId], + }, + ui: casesCapabilities.assignCase, + }, + ], + }, + ], + }, + ], +}); diff --git a/x-pack/solutions/observability/plugins/observability/server/plugin.ts b/x-pack/solutions/observability/plugins/observability/server/plugin.ts index 88e5ac667ee97..241b9da17e402 100644 --- a/x-pack/solutions/observability/plugins/observability/server/plugin.ts +++ b/x-pack/solutions/observability/plugins/observability/server/plugin.ts @@ -51,6 +51,7 @@ import { uiSettings } from './ui_settings'; import { OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES } from '../common/constants'; import { getCasesFeature } from './features/cases_v1'; import { getCasesFeatureV2 } from './features/cases_v2'; +import { getCasesFeatureV3 } from './features/cases_v3'; export type ObservabilityPluginSetup = ReturnType; @@ -103,6 +104,7 @@ export class ObservabilityPlugin plugins.features.registerKibanaFeature(getCasesFeature(casesCapabilities, casesApiTags)); plugins.features.registerKibanaFeature(getCasesFeatureV2(casesCapabilities, casesApiTags)); + plugins.features.registerKibanaFeature(getCasesFeatureV3(casesCapabilities, casesApiTags)); let annotationsApiPromise: Promise | undefined; diff --git a/x-pack/solutions/observability/plugins/observability_shared/common/index.ts b/x-pack/solutions/observability/plugins/observability_shared/common/index.ts index 24d12362d7cfa..e78807165e14f 100644 --- a/x-pack/solutions/observability/plugins/observability_shared/common/index.ts +++ b/x-pack/solutions/observability/plugins/observability_shared/common/index.ts @@ -8,7 +8,7 @@ import { AlertConsumers } from '@kbn/rule-data-utils'; export const observabilityFeatureId = 'observability'; export const observabilityAppId = 'observability-overview'; -export const casesFeatureId = 'observabilityCasesV2'; +export const casesFeatureId = 'observabilityCasesV3'; export const sloFeatureId = 'slo'; // SLO alerts table in slo detail page diff --git a/x-pack/solutions/observability/plugins/observability_shared/public/utils/cases_permissions.ts b/x-pack/solutions/observability/plugins/observability_shared/public/utils/cases_permissions.ts index 0b3699e49b40c..a99d59a4e48b5 100644 --- a/x-pack/solutions/observability/plugins/observability_shared/public/utils/cases_permissions.ts +++ b/x-pack/solutions/observability/plugins/observability_shared/public/utils/cases_permissions.ts @@ -16,6 +16,7 @@ export const noCasesPermissions = () => ({ settings: false, createComment: false, reopenCase: false, + assign: false, }); export const allCasesPermissions = () => ({ @@ -29,4 +30,5 @@ export const allCasesPermissions = () => ({ settings: true, createComment: true, reopenCase: true, + assign: true, }); diff --git a/x-pack/solutions/security/packages/features/product_features.ts b/x-pack/solutions/security/packages/features/product_features.ts index 1f55fd51bae1d..a6e2d252cee7b 100644 --- a/x-pack/solutions/security/packages/features/product_features.ts +++ b/x-pack/solutions/security/packages/features/product_features.ts @@ -5,8 +5,8 @@ * 2.0. */ +export { getCasesFeature, getCasesV2Feature, getCasesV3Feature } from './src/cases'; export { getSecurityFeature, getSecurityV2Feature } from './src/security'; -export { getCasesFeature, getCasesV2Feature } from './src/cases'; export { getAssistantFeature } from './src/assistant'; export { getAttackDiscoveryFeature } from './src/attack_discovery'; export { getTimelineFeature } from './src/timeline'; diff --git a/x-pack/solutions/security/packages/features/src/cases/index.ts b/x-pack/solutions/security/packages/features/src/cases/index.ts index 17e5110538b37..8cab878d0dff8 100644 --- a/x-pack/solutions/security/packages/features/src/cases/index.ts +++ b/x-pack/solutions/security/packages/features/src/cases/index.ts @@ -17,6 +17,11 @@ import { getCasesBaseKibanaSubFeatureIdsV2, getCasesSubFeaturesMapV2, } from './v2_features/kibana_sub_features'; +import { getCasesBaseKibanaFeatureV3 } from './v3_features/kibana_features'; +import { + getCasesBaseKibanaSubFeatureIdsV3, + getCasesSubFeaturesMapV3, +} from './v3_features/kibana_sub_features'; /** * @deprecated Use getCasesV2Feature instead @@ -36,3 +41,11 @@ export const getCasesV2Feature = ( baseKibanaSubFeatureIds: getCasesBaseKibanaSubFeatureIdsV2(), subFeaturesMap: getCasesSubFeaturesMapV2(params), }); + +export const getCasesV3Feature = ( + params: CasesFeatureParams +): ProductFeatureParams => ({ + baseKibanaFeature: getCasesBaseKibanaFeatureV3(params), + baseKibanaSubFeatureIds: getCasesBaseKibanaSubFeatureIdsV3(), + subFeaturesMap: getCasesSubFeaturesMapV3(params), +}); diff --git a/x-pack/solutions/security/packages/features/src/cases/v1_features/kibana_features.ts b/x-pack/solutions/security/packages/features/src/cases/v1_features/kibana_features.ts index db442d894363a..8721aebdd858a 100644 --- a/x-pack/solutions/security/packages/features/src/cases/v1_features/kibana_features.ts +++ b/x-pack/solutions/security/packages/features/src/cases/v1_features/kibana_features.ts @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import type { BaseKibanaFeatureConfig } from '../../types'; -import { APP_ID, CASES_FEATURE_ID, CASES_FEATURE_ID_V2 } from '../../constants'; +import { APP_ID, CASES_FEATURE_ID, CASES_FEATURE_ID_V3 } from '../../constants'; import type { CasesFeatureParams } from '../types'; /** @@ -30,7 +30,7 @@ export const getCasesBaseKibanaFeature = ({ 'The {currentId} permissions are deprecated, please see {casesFeatureIdV2}.', values: { currentId: CASES_FEATURE_ID, - casesFeatureIdV2: CASES_FEATURE_ID_V2, + casesFeatureIdV2: CASES_FEATURE_ID_V3, }, } ), @@ -60,18 +60,24 @@ export const getCasesBaseKibanaFeature = ({ push: [APP_ID], createComment: [APP_ID], reopenCase: [APP_ID], + assign: [APP_ID], }, savedObject: { all: [...savedObjects.files], read: [...savedObjects.files], }, - ui: uiCapabilities.all, + ui: [ + ...uiCapabilities.all, + ...uiCapabilities.createComment, + ...uiCapabilities.reopenCase, + ...uiCapabilities.assignCase, + ], replacedBy: { - default: [{ feature: CASES_FEATURE_ID_V2, privileges: ['all'] }], + default: [{ feature: CASES_FEATURE_ID_V3, privileges: ['all'] }], minimal: [ { - feature: CASES_FEATURE_ID_V2, - privileges: ['minimal_all', 'create_comment', 'case_reopen'], + feature: CASES_FEATURE_ID_V3, + privileges: ['minimal_all', 'create_comment', 'case_reopen', 'cases_assign'], }, ], }, @@ -89,8 +95,8 @@ export const getCasesBaseKibanaFeature = ({ }, ui: uiCapabilities.read, replacedBy: { - default: [{ feature: CASES_FEATURE_ID_V2, privileges: ['read'] }], - minimal: [{ feature: CASES_FEATURE_ID_V2, privileges: ['minimal_read'] }], + default: [{ feature: CASES_FEATURE_ID_V3, privileges: ['read'] }], + minimal: [{ feature: CASES_FEATURE_ID_V3, privileges: ['minimal_read'] }], }, }, }, diff --git a/x-pack/solutions/security/packages/features/src/cases/v1_features/kibana_sub_features.ts b/x-pack/solutions/security/packages/features/src/cases/v1_features/kibana_sub_features.ts index ade0dbab2bfea..07c33a31800e1 100644 --- a/x-pack/solutions/security/packages/features/src/cases/v1_features/kibana_sub_features.ts +++ b/x-pack/solutions/security/packages/features/src/cases/v1_features/kibana_sub_features.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import type { SubFeatureConfig } from '@kbn/features-plugin/common'; import { CasesSubFeatureId } from '../../product_features_keys'; -import { APP_ID, CASES_FEATURE_ID_V2 } from '../../constants'; +import { APP_ID, CASES_FEATURE_ID_V3 } from '../../constants'; import type { CasesFeatureParams } from '../types'; /** @@ -56,7 +56,7 @@ export const getCasesSubFeaturesMap = ({ delete: [APP_ID], }, ui: uiCapabilities.delete, - replacedBy: [{ feature: CASES_FEATURE_ID_V2, privileges: ['cases_delete'] }], + replacedBy: [{ feature: CASES_FEATURE_ID_V3, privileges: ['cases_delete'] }], }, ], }, @@ -91,7 +91,7 @@ export const getCasesSubFeaturesMap = ({ settings: [APP_ID], }, ui: uiCapabilities.settings, - replacedBy: [{ feature: CASES_FEATURE_ID_V2, privileges: ['cases_settings'] }], + replacedBy: [{ feature: CASES_FEATURE_ID_V3, privileges: ['cases_settings'] }], }, ], }, diff --git a/x-pack/solutions/security/packages/features/src/cases/v2_features/kibana_features.ts b/x-pack/solutions/security/packages/features/src/cases/v2_features/kibana_features.ts index c0c025335d054..8588712e1de38 100644 --- a/x-pack/solutions/security/packages/features/src/cases/v2_features/kibana_features.ts +++ b/x-pack/solutions/security/packages/features/src/cases/v2_features/kibana_features.ts @@ -10,7 +10,12 @@ import { i18n } from '@kbn/i18n'; import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import type { BaseKibanaFeatureConfig } from '../../types'; -import { APP_ID, CASES_FEATURE_ID_V2, CASES_FEATURE_ID } from '../../constants'; +import { + APP_ID, + CASES_FEATURE_ID_V2, + CASES_FEATURE_ID, + CASES_FEATURE_ID_V3, +} from '../../constants'; import type { CasesFeatureParams } from '../types'; export const getCasesBaseKibanaFeatureV2 = ({ @@ -19,6 +24,19 @@ export const getCasesBaseKibanaFeatureV2 = ({ savedObjects, }: CasesFeatureParams): BaseKibanaFeatureConfig => { return { + deprecated: { + notice: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.casesFeature.deprecationMessage', + { + defaultMessage: + 'The {currentId} permissions are deprecated, please see {casesFeatureIdV3}.', + values: { + currentId: CASES_FEATURE_ID_V2, + casesFeatureIdV3: CASES_FEATURE_ID_V3, + }, + } + ), + }, id: CASES_FEATURE_ID_V2, name: i18n.translate( 'securitySolutionPackages.features.featureRegistry.linkSecuritySolutionCaseTitle', @@ -42,12 +60,22 @@ export const getCasesBaseKibanaFeatureV2 = ({ read: [APP_ID], update: [APP_ID], push: [APP_ID], + assign: [APP_ID], }, savedObject: { all: [...savedObjects.files], read: [...savedObjects.files], }, - ui: uiCapabilities.all, + ui: [...uiCapabilities.all, ...uiCapabilities.assignCase], + replacedBy: { + default: [{ feature: CASES_FEATURE_ID_V3, privileges: ['all'] }], + minimal: [ + { + feature: CASES_FEATURE_ID_V3, + privileges: ['minimal_all', 'cases_assign'], + }, + ], + }, }, read: { api: apiTags.read, @@ -61,6 +89,10 @@ export const getCasesBaseKibanaFeatureV2 = ({ read: [...savedObjects.files], }, ui: uiCapabilities.read, + replacedBy: { + default: [{ feature: CASES_FEATURE_ID_V3, privileges: ['read'] }], + minimal: [{ feature: CASES_FEATURE_ID_V3, privileges: ['minimal_read'] }], + }, }, }, }; diff --git a/x-pack/solutions/security/packages/features/src/cases/v2_features/kibana_sub_features.ts b/x-pack/solutions/security/packages/features/src/cases/v2_features/kibana_sub_features.ts index 59aeb866039d4..ddc369ab45927 100644 --- a/x-pack/solutions/security/packages/features/src/cases/v2_features/kibana_sub_features.ts +++ b/x-pack/solutions/security/packages/features/src/cases/v2_features/kibana_sub_features.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import type { SubFeatureConfig } from '@kbn/features-plugin/common'; import { CasesSubFeatureId } from '../../product_features_keys'; -import { APP_ID } from '../../constants'; +import { APP_ID, CASES_FEATURE_ID_V3 } from '../../constants'; import type { CasesFeatureParams } from '../types'; /** @@ -57,6 +57,7 @@ export const getCasesSubFeaturesMapV2 = ({ delete: [APP_ID], }, ui: uiCapabilities.delete, + replacedBy: [{ feature: CASES_FEATURE_ID_V3, privileges: ['cases_delete'] }], }, ], }, @@ -91,6 +92,7 @@ export const getCasesSubFeaturesMapV2 = ({ settings: [APP_ID], }, ui: uiCapabilities.settings, + replacedBy: [{ feature: CASES_FEATURE_ID_V3, privileges: ['cases_settings'] }], }, ], }, @@ -128,6 +130,7 @@ export const getCasesSubFeaturesMapV2 = ({ createComment: [APP_ID], }, ui: uiCapabilities.createComment, + replacedBy: [{ feature: CASES_FEATURE_ID_V3, privileges: ['create_comment'] }], }, ], }, @@ -161,6 +164,7 @@ export const getCasesSubFeaturesMapV2 = ({ reopenCase: [APP_ID], }, ui: uiCapabilities.reopenCase, + replacedBy: [{ feature: CASES_FEATURE_ID_V3, privileges: ['case_reopen'] }], }, ], }, diff --git a/x-pack/solutions/security/packages/features/src/cases/v3_features/kibana_features.ts b/x-pack/solutions/security/packages/features/src/cases/v3_features/kibana_features.ts new file mode 100644 index 0000000000000..c9a08ebb8614d --- /dev/null +++ b/x-pack/solutions/security/packages/features/src/cases/v3_features/kibana_features.ts @@ -0,0 +1,67 @@ +/* + * 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'; + +import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; +import type { BaseKibanaFeatureConfig } from '../../types'; +import { APP_ID, CASES_FEATURE_ID_V3, CASES_FEATURE_ID } from '../../constants'; +import type { CasesFeatureParams } from '../types'; + +export const getCasesBaseKibanaFeatureV3 = ({ + uiCapabilities, + apiTags, + savedObjects, +}: CasesFeatureParams): BaseKibanaFeatureConfig => { + return { + id: CASES_FEATURE_ID_V3, + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.linkSecuritySolutionCaseTitle', + { + defaultMessage: 'Cases', + } + ), + order: 1100, + category: DEFAULT_APP_CATEGORIES.security, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], + app: [CASES_FEATURE_ID, 'kibana'], + catalogue: [APP_ID], + cases: [APP_ID], + privileges: { + all: { + api: apiTags.all, + app: [CASES_FEATURE_ID, 'kibana'], + catalogue: [APP_ID], + cases: { + create: [APP_ID], + read: [APP_ID], + update: [APP_ID], + push: [APP_ID], + }, + savedObject: { + all: [...savedObjects.files], + read: [...savedObjects.files], + }, + ui: uiCapabilities.all, + }, + read: { + api: apiTags.read, + app: [CASES_FEATURE_ID, 'kibana'], + catalogue: [APP_ID], + cases: { + read: [APP_ID], + }, + savedObject: { + all: [], + read: [...savedObjects.files], + }, + ui: uiCapabilities.read, + }, + }, + }; +}; diff --git a/x-pack/solutions/security/packages/features/src/cases/v3_features/kibana_sub_features.ts b/x-pack/solutions/security/packages/features/src/cases/v3_features/kibana_sub_features.ts new file mode 100644 index 0000000000000..b1672d25d0c3b --- /dev/null +++ b/x-pack/solutions/security/packages/features/src/cases/v3_features/kibana_sub_features.ts @@ -0,0 +1,204 @@ +/* + * 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'; +import type { SubFeatureConfig } from '@kbn/features-plugin/common'; +import { CasesSubFeatureId } from '../../product_features_keys'; +import { APP_ID } from '../../constants'; +import type { CasesFeatureParams } from '../types'; + +/** + * Sub-features that will always be available for Security Cases + * regardless of the product type. + */ +export const getCasesBaseKibanaSubFeatureIdsV3 = (): CasesSubFeatureId[] => [ + CasesSubFeatureId.deleteCases, + CasesSubFeatureId.casesSettings, + CasesSubFeatureId.createComment, + CasesSubFeatureId.reopenCase, + CasesSubFeatureId.assignUsers, +]; + +/** + * Defines all the Security Solution Cases subFeatures available. + * The order of the subFeatures is the order they will be displayed + */ +export const getCasesSubFeaturesMapV3 = ({ + uiCapabilities, + apiTags, + savedObjects, +}: CasesFeatureParams) => { + const deleteCasesSubFeature: SubFeatureConfig = { + name: i18n.translate('securitySolutionPackages.features.featureRegistry.deleteSubFeatureName', { + defaultMessage: 'Delete', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + api: apiTags.delete, + id: 'cases_delete', + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.deleteSubFeatureDetails', + { + defaultMessage: 'Delete cases and comments', + } + ), + includeIn: 'all', + savedObject: { + all: [...savedObjects.files], + read: [...savedObjects.files], + }, + cases: { + delete: [APP_ID], + }, + ui: uiCapabilities.delete, + }, + ], + }, + ], + }; + + const casesSettingsCasesSubFeature: SubFeatureConfig = { + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.casesSettingsSubFeatureName', + { + defaultMessage: 'Case settings', + } + ), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'cases_settings', + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.casesSettingsSubFeatureDetails', + { + defaultMessage: 'Edit case settings', + } + ), + includeIn: 'all', + savedObject: { + all: [...savedObjects.files], + read: [...savedObjects.files], + }, + cases: { + settings: [APP_ID], + }, + ui: uiCapabilities.settings, + }, + ], + }, + ], + }; + + const casesAddCommentsCasesSubFeature: SubFeatureConfig = { + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.addCommentsSubFeatureName', + { + defaultMessage: 'Create comments & attachments', + } + ), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + api: apiTags.createComment, + id: 'create_comment', + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.addCommentsSubFeatureDetails', + { + defaultMessage: 'Add comments to cases', + } + ), + includeIn: 'all', + savedObject: { + all: [...savedObjects.files], + read: [...savedObjects.files], + }, + cases: { + createComment: [APP_ID], + }, + ui: uiCapabilities.createComment, + }, + ], + }, + ], + }; + const casesreopenCaseSubFeature: SubFeatureConfig = { + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.reopenCaseSubFeatureName', + { + defaultMessage: 'Re-open', + } + ), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'case_reopen', + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.reopenCaseSubFeatureDetails', + { + defaultMessage: 'Re-open closed cases', + } + ), + includeIn: 'all', + savedObject: { + all: [], + read: [], + }, + cases: { + reopenCase: [APP_ID], + }, + ui: uiCapabilities.reopenCase, + }, + ], + }, + ], + }; + + const casesAssignUsersCasesSubFeature: SubFeatureConfig = { + name: i18n.translate('securitySolutionPackages.features.assignUsersSubFeatureName', { + defaultMessage: 'Assign users', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'cases_assign', + name: i18n.translate('securitySolutionPackages.features.assignUsersSubFeatureName', { + defaultMessage: 'Assign users to cases', + }), + includeIn: 'all', + savedObject: { + all: [], + read: [], + }, + cases: { + assign: [APP_ID], + }, + ui: uiCapabilities.assignCase, + }, + ], + }, + ], + }; + + return new Map([ + [CasesSubFeatureId.deleteCases, deleteCasesSubFeature], + [CasesSubFeatureId.casesSettings, casesSettingsCasesSubFeature], + [CasesSubFeatureId.createComment, casesAddCommentsCasesSubFeature], + [CasesSubFeatureId.reopenCase, casesreopenCaseSubFeature], + [CasesSubFeatureId.assignUsers, casesAssignUsersCasesSubFeature], + ]); +}; diff --git a/x-pack/solutions/security/packages/features/src/constants.ts b/x-pack/solutions/security/packages/features/src/constants.ts index 9615dc0d278b1..43d8bda53dc79 100644 --- a/x-pack/solutions/security/packages/features/src/constants.ts +++ b/x-pack/solutions/security/packages/features/src/constants.ts @@ -20,6 +20,9 @@ export const CASES_FEATURE_ID = 'securitySolutionCases' as const; // New version created in 8.17 to adopt the roles migration changes export const CASES_FEATURE_ID_V2 = 'securitySolutionCasesV2' as const; +// New version created in 8.18 for case assignees +export const CASES_FEATURE_ID_V3 = 'securitySolutionCasesV3' as const; + export const SECURITY_SOLUTION_CASES_APP_ID = 'securitySolutionCases' as const; export const ASSISTANT_FEATURE_ID = 'securitySolutionAssistant' as const; diff --git a/x-pack/solutions/security/packages/features/src/product_features_keys.ts b/x-pack/solutions/security/packages/features/src/product_features_keys.ts index 55d3fc70e0eab..63e049b1f2462 100644 --- a/x-pack/solutions/security/packages/features/src/product_features_keys.ts +++ b/x-pack/solutions/security/packages/features/src/product_features_keys.ts @@ -177,6 +177,7 @@ export enum CasesSubFeatureId { casesSettings = 'casesSettingsSubFeature', createComment = 'createCommentSubFeature', reopenCase = 'reopenCaseSubFeature', + assignUsers = 'assignUsersSubFeature', } /** Sub-features IDs for Security Assistant */ diff --git a/x-pack/solutions/security/plugins/security_solution/common/constants.ts b/x-pack/solutions/security/plugins/security_solution/common/constants.ts index 9ba8c2f4a6ab8..43a09d8221d62 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/constants.ts @@ -21,7 +21,7 @@ export const APP_ID = 'securitySolution' as const; export const APP_UI_ID = 'securitySolutionUI' as const; export const ASSISTANT_FEATURE_ID = 'securitySolutionAssistant' as const; export const ATTACK_DISCOVERY_FEATURE_ID = 'securitySolutionAttackDiscovery' as const; -export const CASES_FEATURE_ID = 'securitySolutionCasesV2' as const; +export const CASES_FEATURE_ID = 'securitySolutionCasesV3' as const; export const TIMELINE_FEATURE_ID = 'securitySolutionTimeline' as const; export const NOTES_FEATURE_ID = 'securitySolutionNotes' as const; export const SERVER_APP_ID = 'siem' as const; diff --git a/x-pack/solutions/security/plugins/security_solution/public/cases_test_utils.ts b/x-pack/solutions/security/plugins/security_solution/public/cases_test_utils.ts index f3c356507bcfe..8352959b888fb 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/cases_test_utils.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/cases_test_utils.ts @@ -17,6 +17,7 @@ export const noCasesCapabilities = (): CasesCapabilities => ({ cases_settings: false, case_reopen: false, create_comment: false, + cases_assign: false, }); export const readCasesCapabilities = (): CasesCapabilities => ({ @@ -29,6 +30,7 @@ export const readCasesCapabilities = (): CasesCapabilities => ({ cases_settings: false, case_reopen: false, create_comment: false, + cases_assign: false, }); export const allCasesCapabilities = (): CasesCapabilities => ({ @@ -41,6 +43,7 @@ export const allCasesCapabilities = (): CasesCapabilities => ({ cases_settings: true, case_reopen: true, create_comment: true, + cases_assign: true, }); export const noCasesPermissions = (): CasesPermissions => ({ @@ -54,6 +57,7 @@ export const noCasesPermissions = (): CasesPermissions => ({ settings: false, reopenCase: false, createComment: false, + assign: false, }); export const readCasesPermissions = (): CasesPermissions => ({ @@ -67,6 +71,7 @@ export const readCasesPermissions = (): CasesPermissions => ({ settings: false, reopenCase: false, createComment: false, + assign: false, }); export const writeCasesPermissions = (): CasesPermissions => ({ @@ -80,6 +85,7 @@ export const writeCasesPermissions = (): CasesPermissions => ({ settings: true, reopenCase: true, createComment: true, + assign: true, }); export const allCasesPermissions = (): CasesPermissions => ({ @@ -93,4 +99,5 @@ export const allCasesPermissions = (): CasesPermissions => ({ settings: true, reopenCase: true, createComment: true, + assign: true, }); diff --git a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/endpoint_operations_analyst.ts b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/endpoint_operations_analyst.ts index 21bf6c7346c16..8a72ec983bcdb 100644 --- a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/endpoint_operations_analyst.ts +++ b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/endpoint_operations_analyst.ts @@ -55,7 +55,7 @@ export const getEndpointOperationsAnalyst: () => Omit = () => { fleet: ['all'], fleetv2: ['all'], osquery: ['all'], - securitySolutionCasesV2: ['all'], + securitySolutionCasesV3: ['all'], builtinAlerts: ['all'], siemV2: [ 'all', diff --git a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/without_response_actions_role.ts b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/without_response_actions_role.ts index 4e5b7aa28fff0..53d8003618266 100644 --- a/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/without_response_actions_role.ts +++ b/x-pack/solutions/security/plugins/security_solution/scripts/endpoint/common/roles_users/without_response_actions_role.ts @@ -37,7 +37,7 @@ export const getNoResponseActionsRole: () => Omit = () => ({ advancedSettings: ['all'], dev_tools: ['all'], fleet: ['all'], - generalCasesV2: ['all'], + generalCasesV3: ['all'], indexPatterns: ['all'], osquery: ['all'], savedObjectsManagement: ['all'], diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/mocks.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/mocks.ts index 3b385c70e0b5f..7d0001c4d9ac7 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/mocks.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/mocks.ts @@ -36,6 +36,11 @@ jest.mock('@kbn/security-solution-features/product_features', () => ({ baseKibanaSubFeatureIds: [], subFeaturesMap: new Map(), })), + getCasesV3Feature: jest.fn(() => ({ + baseKibanaFeature: {}, + baseKibanaSubFeatureIds: [], + subFeaturesMap: new Map(), + })), getAssistantFeature: jest.fn(() => ({ baseKibanaFeature: {}, baseKibanaSubFeatureIds: [], diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_service.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_service.test.ts index b0c6254a139a6..5d9a028e10eab 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_service.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_service.test.ts @@ -45,6 +45,7 @@ jest.mock('@kbn/security-solution-features/product_features', () => ({ getAssistantFeature: () => mockGetFeature(), getCasesFeature: () => mockGetFeature(), getCasesV2Feature: () => mockGetFeature(), + getCasesV3Feature: () => mockGetFeature(), getSecurityFeature: () => mockGetFeature(), getSecurityV2Feature: () => mockGetFeature(), getTimelineFeature: () => mockGetFeature(), @@ -60,8 +61,8 @@ describe('ProductFeaturesService', () => { const experimentalFeatures = {} as ExperimentalFeatures; new ProductFeaturesService(loggerMock.create(), experimentalFeatures); - expect(mockGetFeature).toHaveBeenCalledTimes(8); - expect(MockedProductFeatures).toHaveBeenCalledTimes(8); + expect(mockGetFeature).toHaveBeenCalledTimes(9); + expect(MockedProductFeatures).toHaveBeenCalledTimes(9); }); it('should init all ProductFeatures when initialized', () => { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_service.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_service.ts index edcf0faa19659..2c2f96face5d3 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_service.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/product_features_service/product_features_service.ts @@ -21,6 +21,7 @@ import { getCasesFeature, getSecurityFeature, getCasesV2Feature, + getCasesV3Feature, getSecurityV2Feature, getTimelineFeature, getNotesFeature, @@ -46,6 +47,7 @@ export class ProductFeaturesService { private securityV2ProductFeatures: ProductFeatures; private casesProductFeatures: ProductFeatures; private casesProductV2Features: ProductFeatures; + private casesProductFeaturesV3: ProductFeatures; private securityAssistantProductFeatures: ProductFeatures; private attackDiscoveryProductFeatures: ProductFeatures; private timelineProductFeatures: ProductFeatures; @@ -103,6 +105,18 @@ export class ProductFeaturesService { casesV2Feature.baseKibanaSubFeatureIds ); + const casesV3Feature = getCasesV3Feature({ + uiCapabilities: casesUiCapabilities, + apiTags: casesApiTags, + savedObjects: { files: filesSavedObjectTypes }, + }); + this.casesProductFeaturesV3 = new ProductFeatures( + this.logger, + casesV3Feature.subFeaturesMap, + casesV3Feature.baseKibanaFeature, + casesV3Feature.baseKibanaSubFeatureIds + ); + const assistantFeature = getAssistantFeature(this.experimentalFeatures); this.securityAssistantProductFeatures = new ProductFeatures( this.logger, @@ -148,6 +162,7 @@ export class ProductFeaturesService { this.securityV2ProductFeatures.init(featuresSetup); this.casesProductFeatures.init(featuresSetup); this.casesProductV2Features.init(featuresSetup); + this.casesProductFeaturesV3.init(featuresSetup); this.securityAssistantProductFeatures.init(featuresSetup); this.attackDiscoveryProductFeatures.init(featuresSetup); this.timelineProductFeatures.init(featuresSetup); @@ -162,6 +177,7 @@ export class ProductFeaturesService { const casesProductFeaturesConfig = configurator.cases(); this.casesProductFeatures.setConfig(casesProductFeaturesConfig); this.casesProductV2Features.setConfig(casesProductFeaturesConfig); + this.casesProductFeaturesV3.setConfig(casesProductFeaturesConfig); const securityAssistantProductFeaturesConfig = configurator.securityAssistant(); this.securityAssistantProductFeatures.setConfig(securityAssistantProductFeaturesConfig); diff --git a/x-pack/test/api_integration/apis/cases/common/roles.ts b/x-pack/test/api_integration/apis/cases/common/roles.ts index 21ad6943ba0df..f27ce68f1ddf2 100644 --- a/x-pack/test/api_integration/apis/cases/common/roles.ts +++ b/x-pack/test/api_integration/apis/cases/common/roles.ts @@ -136,6 +136,81 @@ export const secCasesV2All: Role = { }, }; +export const secCasesV3All: Role = { + name: 'sec_cases_v3_all_role_api_int', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + siem: ['all'], + securitySolutionCasesV3: ['all'], + actions: ['all'], + actionsSimulators: ['all'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const secCasesV2NoReopenWithCreateComment: Role = { + name: 'sec_cases_v2_no_reopen_role_api_int', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + siem: ['all'], + securitySolutionCasesV2: ['read', 'update', 'create', 'cases_delete', 'create_comment'], + actions: ['all'], + actionsSimulators: ['all'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const secCasesV2NoCreateCommentWithReopen: Role = { + name: 'sec_cases_v2_create_comment_no_reopen_role_api_int', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + siem: ['all'], + securitySolutionCasesV2: ['read', 'update', 'create', 'delete', 'case_reopen'], + actions: ['all'], + actionsSimulators: ['all'], + }, + spaces: ['*'], + }, + ], + }, +}; + export const secAllSpace1: Role = { name: 'sec_all_role_space1_api_int', privileges: { @@ -434,6 +509,131 @@ export const casesV2All: Role = { }, }; +export const casesV3All: Role = { + name: 'cases_v3_all_role_api_int', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + spaces: ['*'], + base: [], + feature: { + generalCasesV3: ['all'], + actions: ['all'], + actionsSimulators: ['all'], + }, + }, + ], + }, +}; + +export const casesV3NoAssignee: Role = { + name: 'cases_v3_no_assignee_role_api_int', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + spaces: ['*'], + base: [], + feature: { + generalCasesV3: ['minimal_read', 'cases_delete', 'case_reopen', 'create_comment'], + actions: ['all'], + actionsSimulators: ['all'], + }, + }, + ], + }, +}; + +export const casesV3ReadAndAssignee: Role = { + name: 'cases_v3_read_assignee_role_api_int', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + spaces: ['*'], + base: [], + feature: { + generalCasesV3: ['minimal_read', 'cases_assign'], + actions: ['all'], + actionsSimulators: ['all'], + }, + }, + ], + }, +}; + +export const casesV2NoReopenWithCreateComment: Role = { + name: 'cases_v2_no_reopen_role_api_int', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + spaces: ['*'], + base: [], + feature: { + generalCasesV2: ['read', 'update', 'create', 'cases_delete', 'create_comment'], + actions: ['all'], + actionsSimulators: ['all'], + }, + }, + ], + }, +}; + +export const casesV2NoCreateCommentWithReopen: Role = { + name: 'cases_v2_no_create_comment_role_api_int', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + spaces: ['*'], + base: [], + feature: { + generalCasesV2: ['read', 'update', 'create', 'cases_delete', 'case_reopen'], + actions: ['all'], + actionsSimulators: ['all'], + }, + }, + ], + }, +}; + export const casesRead: Role = { name: 'cases_read_role_api_int', privileges: { @@ -583,6 +783,87 @@ export const obsCasesV2All: Role = { }, }; +export const obsCasesV3All: Role = { + name: 'obs_cases_v3_all_role_api_int', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + spaces: ['*'], + base: [], + feature: { + observabilityCasesV3: ['all'], + actions: ['all'], + actionsSimulators: ['all'], + }, + }, + ], + }, +}; + +export const obsCasesV2NoReopenWithCreateComment: Role = { + name: 'obs_cases_v2_no_reopen_role_api_int', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + spaces: ['*'], + base: [], + feature: { + observabilityCasesV2: [ + 'read', + 'cases_update', + 'create', + 'cases_delete', + 'create_comment', + ], + actions: ['all'], + actionsSimulators: ['all'], + }, + }, + ], + }, +}; + +export const obsCasesV2NoCreateCommentWithReopen: Role = { + name: 'obs_cases_v2_no_create_comment_role_api_int', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + spaces: ['*'], + base: [], + feature: { + observabilityCasesV2: ['read', 'update', 'create', 'cases_delete', 'case_reopen'], + actions: ['all'], + actionsSimulators: ['all'], + }, + }, + ], + }, +}; + export const obsCasesRead: Role = { name: 'obs_cases_read_role_api_int', privileges: { @@ -613,6 +894,9 @@ export const roles = [ secAllCasesNoDelete, secAll, secCasesV2All, + secCasesV3All, + secCasesV2NoReopenWithCreateComment, + secCasesV2NoCreateCommentWithReopen, secAllSpace1, secAllCasesRead, secAllCasesNone, @@ -625,11 +909,19 @@ export const roles = [ casesNoDelete, casesAll, casesV2All, + casesV3All, + casesV3NoAssignee, + casesV3ReadAndAssignee, + casesV2NoReopenWithCreateComment, + casesV2NoCreateCommentWithReopen, casesRead, obsCasesOnlyDelete, obsCasesOnlyReadDelete, obsCasesNoDelete, obsCasesAll, obsCasesV2All, + obsCasesV3All, + obsCasesV2NoReopenWithCreateComment, + obsCasesV2NoCreateCommentWithReopen, obsCasesRead, ]; diff --git a/x-pack/test/api_integration/apis/cases/common/users.ts b/x-pack/test/api_integration/apis/cases/common/users.ts index a64b9767498fb..b4f8d3d6c4f5e 100644 --- a/x-pack/test/api_integration/apis/cases/common/users.ts +++ b/x-pack/test/api_integration/apis/cases/common/users.ts @@ -9,18 +9,23 @@ import { User } from '../../../../cases_api_integration/common/lib/authenticatio import { casesAll, casesV2All, + casesV3All, + casesV3NoAssignee, + casesV3ReadAndAssignee, casesNoDelete, casesOnlyDelete, casesOnlyReadDelete, casesRead, obsCasesAll, obsCasesV2All, + obsCasesV3All, obsCasesNoDelete, obsCasesOnlyDelete, obsCasesOnlyReadDelete, obsCasesRead, secAll, secCasesV2All, + secCasesV3All, secAllCasesNoDelete, secAllCasesNone, secAllCasesOnlyDelete, @@ -31,6 +36,12 @@ import { secReadCasesAll, secReadCasesNone, secReadCasesRead, + casesV2NoReopenWithCreateComment, + obsCasesV2NoReopenWithCreateComment, + secCasesV2NoReopenWithCreateComment, + secCasesV2NoCreateCommentWithReopen, + casesV2NoCreateCommentWithReopen, + obsCasesV2NoCreateCommentWithReopen, } from './roles'; /** @@ -67,6 +78,24 @@ export const secCasesV2AllUser: User = { roles: [secCasesV2All.name], }; +export const secCasesV3AllUser: User = { + username: 'sec_cases_v3_all_user_api_int', + password: 'password', + roles: [secCasesV3All.name], +}; + +export const secCasesV2NoReopenWithCreateCommentUser: User = { + username: 'sec_cases_v2_no_reopen_with_create_comment_user_api_int', + password: 'password', + roles: [secCasesV2NoReopenWithCreateComment.name], +}; + +export const secCasesV2NoCreateCommentWithReopenUser: User = { + username: 'sec_cases_v2_no_create_comment_with_reopen_user_api_int', + password: 'password', + roles: [secCasesV2NoCreateCommentWithReopen.name], +}; + export const secAllSpace1User: User = { username: 'sec_all_space1_user_api_int', password: 'password', @@ -143,6 +172,36 @@ export const casesV2AllUser: User = { roles: [casesV2All.name], }; +export const casesV3AllUser: User = { + username: 'cases_v3_all_user_api_int', + password: 'password', + roles: [casesV3All.name], +}; + +export const casesV3NoAssigneeUser: User = { + username: 'cases_v3_no_assignee_user_api_int', + password: 'password', + roles: [casesV3NoAssignee.name], +}; + +export const casesV3ReadAndAssignUser: User = { + username: 'cases_v3_read_and_assignee_user_api_int', + password: 'password', + roles: [casesV3ReadAndAssignee.name], +}; + +export const casesV2NoReopenWithCreateCommentUser: User = { + username: 'cases_v2_no_reopen_with_create_comment_user_api_int', + password: 'password', + roles: [casesV2NoReopenWithCreateComment.name], +}; + +export const casesV2NoCreateCommentWithReopenUser: User = { + username: 'cases_v2_no_create_comment_with_reopen_user_api_int', + password: 'password', + roles: [casesV2NoCreateCommentWithReopen.name], +}; + export const casesReadUser: User = { username: 'cases_read_user_api_int', password: 'password', @@ -183,6 +242,24 @@ export const obsCasesV2AllUser: User = { roles: [obsCasesV2All.name], }; +export const obsCasesV3AllUser: User = { + username: 'obs_cases_v3_all_user_api_int', + password: 'password', + roles: [obsCasesV3All.name], +}; + +export const obsCasesV2NoReopenWithCreateCommentUser: User = { + username: 'obs_cases_v2_no_reopen_with_create_comment_user_api_int', + password: 'password', + roles: [obsCasesV2NoReopenWithCreateComment.name], +}; + +export const obsCasesV2NoCreateCommentWithReopenUser: User = { + username: 'obs_cases_v2_no_create_comment_with_reopen_user_api_int', + password: 'password', + roles: [obsCasesV2NoCreateCommentWithReopen.name], +}; + export const obsCasesReadUser: User = { username: 'obs_cases_read_user_api_int', password: 'password', @@ -211,6 +288,9 @@ export const users = [ secAllCasesNoDeleteUser, secAllUser, secCasesV2AllUser, + secCasesV3AllUser, + secCasesV2NoReopenWithCreateCommentUser, + secCasesV2NoCreateCommentWithReopenUser, secAllSpace1User, secAllCasesReadUser, secAllCasesNoneUser, @@ -223,12 +303,20 @@ export const users = [ casesNoDeleteUser, casesAllUser, casesV2AllUser, + casesV3AllUser, + casesV3NoAssigneeUser, + casesV3ReadAndAssignUser, + casesV2NoReopenWithCreateCommentUser, + casesV2NoCreateCommentWithReopenUser, casesReadUser, obsCasesOnlyDeleteUser, obsCasesOnlyReadDeleteUser, obsCasesNoDeleteUser, obsCasesAllUser, obsCasesV2AllUser, + obsCasesV3AllUser, + obsCasesV2NoReopenWithCreateCommentUser, + obsCasesV2NoCreateCommentWithReopenUser, obsCasesReadUser, obsSecCasesAllUser, obsSecCasesReadUser, diff --git a/x-pack/test/api_integration/apis/cases/privileges.ts b/x-pack/test/api_integration/apis/cases/privileges.ts index 53a1767f5c1a7..66939f7603bec 100644 --- a/x-pack/test/api_integration/apis/cases/privileges.ts +++ b/x-pack/test/api_integration/apis/cases/privileges.ts @@ -20,14 +20,19 @@ import { getCase, createComment, updateCaseStatus, + updateCaseAssignee, } from '../../../cases_api_integration/common/lib/api'; import { casesAllUser, casesV2AllUser, + casesV3AllUser, + casesV3NoAssigneeUser, + casesV3ReadAndAssignUser, casesNoDeleteUser, casesOnlyDeleteUser, obsCasesAllUser, obsCasesV2AllUser, + obsCasesV3AllUser, obsCasesNoDeleteUser, obsCasesOnlyDeleteUser, secAllCasesNoDeleteUser, @@ -36,12 +41,20 @@ import { secAllCasesReadUser, secAllUser, secCasesV2AllUser, + secCasesV3AllUser, secReadCasesAllUser, secReadCasesNoneUser, secReadCasesReadUser, secReadUser, + casesV2NoReopenWithCreateCommentUser, + casesV2NoCreateCommentWithReopenUser, + obsCasesV2NoReopenWithCreateCommentUser, + obsCasesV2NoCreateCommentWithReopenUser, + secCasesV2NoReopenWithCreateCommentUser, + secCasesV2NoCreateCommentWithReopenUser, } from './common/users'; import { getPostCaseRequest } from '../../../cases_api_integration/common/lib/mock'; +import { suggestUserProfiles } from '../../../cases_api_integration/common/lib/api/user_profiles'; export default ({ getService }: FtrProviderContext): void => { describe('feature privilege', () => { @@ -63,9 +76,13 @@ export default ({ getService }: FtrProviderContext): void => { { user: obsCasesAllUser, owner: OBSERVABILITY_APP_ID }, { user: obsCasesV2AllUser, owner: OBSERVABILITY_APP_ID }, { user: obsCasesNoDeleteUser, owner: OBSERVABILITY_APP_ID }, + { user: secCasesV3AllUser, owner: SECURITY_SOLUTION_APP_ID }, + { user: casesV3AllUser, owner: CASES_APP_ID }, + { user: obsCasesV3AllUser, owner: OBSERVABILITY_APP_ID }, + { user: casesV3NoAssigneeUser, owner: CASES_APP_ID }, ]) { it(`User ${user.username} with role(s) ${user.roles.join()} can create a case`, async () => { - await createCase(supertestWithoutAuth, getPostCaseRequest({ owner }), 200, { + await createCase(supertest, getPostCaseRequest({ owner }), 200, { user, space: null, }); @@ -83,6 +100,10 @@ export default ({ getService }: FtrProviderContext): void => { { user: obsCasesAllUser, owner: OBSERVABILITY_APP_ID }, { user: obsCasesV2AllUser, owner: OBSERVABILITY_APP_ID }, { user: obsCasesNoDeleteUser, owner: OBSERVABILITY_APP_ID }, + { user: secCasesV3AllUser, owner: SECURITY_SOLUTION_APP_ID }, + { user: casesV3AllUser, owner: CASES_APP_ID }, + { user: obsCasesV3AllUser, owner: OBSERVABILITY_APP_ID }, + { user: casesV3NoAssigneeUser, owner: CASES_APP_ID }, ]) { it(`User ${user.username} with role(s) ${user.roles.join()} can get a case`, async () => { const caseInfo = await createCase(supertest, getPostCaseRequest({ owner })); @@ -105,6 +126,7 @@ export default ({ getService }: FtrProviderContext): void => { { user: secReadCasesNoneUser, owner: SECURITY_SOLUTION_APP_ID }, { user: casesOnlyDeleteUser, owner: CASES_APP_ID }, { user: obsCasesOnlyDeleteUser, owner: OBSERVABILITY_APP_ID }, + { user: casesV3NoAssigneeUser, owner: CASES_APP_ID }, ]) { it(`User ${ user.username @@ -145,6 +167,10 @@ export default ({ getService }: FtrProviderContext): void => { { user: obsCasesAllUser, owner: OBSERVABILITY_APP_ID }, { user: obsCasesV2AllUser, owner: OBSERVABILITY_APP_ID }, { user: obsCasesOnlyDeleteUser, owner: OBSERVABILITY_APP_ID }, + { user: secCasesV3AllUser, owner: SECURITY_SOLUTION_APP_ID }, + { user: casesV3AllUser, owner: CASES_APP_ID }, + { user: obsCasesV3AllUser, owner: OBSERVABILITY_APP_ID }, + { user: casesV3NoAssigneeUser, owner: CASES_APP_ID }, ]) { it(`User ${user.username} with role(s) ${user.roles.join()} can delete a case`, async () => { const caseInfo = await createCase(supertest, getPostCaseRequest({ owner })); @@ -183,6 +209,9 @@ export default ({ getService }: FtrProviderContext): void => { { user: obsCasesV2AllUser, owner: OBSERVABILITY_APP_ID }, { user: casesAllUser, owner: CASES_APP_ID }, { user: casesV2AllUser, owner: CASES_APP_ID }, + { user: secCasesV3AllUser, owner: SECURITY_SOLUTION_APP_ID }, + { user: casesV3AllUser, owner: CASES_APP_ID }, + { user: obsCasesV3AllUser, owner: OBSERVABILITY_APP_ID }, ]) { it(`User ${user.username} with role(s) ${user.roles.join()} can reopen a case`, async () => { const caseInfo = await createCase(supertest, getPostCaseRequest({ owner })); @@ -190,7 +219,63 @@ export default ({ getService }: FtrProviderContext): void => { supertest: supertestWithoutAuth, caseId: caseInfo.id, status: 'closed' as CaseStatuses, - version: '2', + version: caseInfo.version, + expectedHttpCode: 200, + auth: { user, space: null }, + }); + + const updatedCase = await getCase({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + expectedHttpCode: 200, + auth: { user, space: null }, + }); + + await updateCaseStatus({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + status: 'open' as CaseStatuses, + version: updatedCase.version, + expectedHttpCode: 200, + auth: { user, space: null }, + }); + }); + } + + for (const { user, owner, userWithFullPerms } of [ + { + user: casesV2NoCreateCommentWithReopenUser, + owner: CASES_APP_ID, + userWithFullPerms: casesV3AllUser, + }, + { + user: obsCasesV2NoCreateCommentWithReopenUser, + owner: OBSERVABILITY_APP_ID, + userWithFullPerms: obsCasesV3AllUser, + }, + { + user: secCasesV2NoCreateCommentWithReopenUser, + owner: SECURITY_SOLUTION_APP_ID, + userWithFullPerms: secCasesV3AllUser, + }, + { user: casesV3NoAssigneeUser, owner: CASES_APP_ID, userWithFullPerms: casesV3AllUser }, + ]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} can reopen a case, if it's closed`, async () => { + const caseInfo = await createCase(supertest, getPostCaseRequest({ owner })); + await updateCaseStatus({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + status: 'closed' as CaseStatuses, + version: caseInfo.version, + expectedHttpCode: 200, + auth: { user: userWithFullPerms, space: null }, + }); + + const updatedCase = await getCase({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, expectedHttpCode: 200, auth: { user, space: null }, }); @@ -199,10 +284,58 @@ export default ({ getService }: FtrProviderContext): void => { supertest: supertestWithoutAuth, caseId: caseInfo.id, status: 'open' as CaseStatuses, - version: '3', + version: updatedCase.version, + expectedHttpCode: 200, + auth: { user, space: null }, + }); + }); + } + + for (const { user, owner, userWithFullPerms } of [ + { + user: casesV2NoReopenWithCreateCommentUser, + owner: CASES_APP_ID, + userWithFullPerms: casesV3AllUser, + }, + { + user: obsCasesV2NoReopenWithCreateCommentUser, + owner: OBSERVABILITY_APP_ID, + userWithFullPerms: obsCasesV3AllUser, + }, + { + user: secCasesV2NoReopenWithCreateCommentUser, + owner: SECURITY_SOLUTION_APP_ID, + userWithFullPerms: secCasesV3AllUser, + }, + ]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} CANNOT reopen a case`, async () => { + const caseInfo = await createCase(supertest, getPostCaseRequest({ owner })); + await updateCaseStatus({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + status: 'closed' as CaseStatuses, + version: caseInfo.version, + expectedHttpCode: 200, + auth: { user: userWithFullPerms, space: null }, + }); + + const updatedCase = await getCase({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, expectedHttpCode: 200, auth: { user, space: null }, }); + + await updateCaseStatus({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + status: 'open' as CaseStatuses, + version: updatedCase.version, + expectedHttpCode: 403, + auth: { user, space: null }, + }); }); } @@ -213,6 +346,13 @@ export default ({ getService }: FtrProviderContext): void => { { user: obsCasesV2AllUser, owner: OBSERVABILITY_APP_ID }, { user: casesAllUser, owner: CASES_APP_ID }, { user: casesV2AllUser, owner: CASES_APP_ID }, + { user: casesV2NoReopenWithCreateCommentUser, owner: CASES_APP_ID }, + { user: obsCasesV2NoReopenWithCreateCommentUser, owner: OBSERVABILITY_APP_ID }, + { user: secCasesV2NoReopenWithCreateCommentUser, owner: SECURITY_SOLUTION_APP_ID }, + { user: secCasesV3AllUser, owner: SECURITY_SOLUTION_APP_ID }, + { user: casesV3AllUser, owner: CASES_APP_ID }, + { user: obsCasesV3AllUser, owner: OBSERVABILITY_APP_ID }, + { user: casesV3NoAssigneeUser, owner: CASES_APP_ID }, ]) { it(`User ${user.username} with role(s) ${user.roles.join()} can add comments`, async () => { const caseInfo = await createCase(supertest, getPostCaseRequest({ owner })); @@ -230,5 +370,109 @@ export default ({ getService }: FtrProviderContext): void => { }); }); } + + for (const { user, owner, userWithFullPerms } of [ + { user: casesV3NoAssigneeUser, owner: CASES_APP_ID, userWithFullPerms: casesV3AllUser }, + { + user: casesV2NoCreateCommentWithReopenUser, + owner: CASES_APP_ID, + userWithFullPerms: casesV3AllUser, + }, + { + user: obsCasesV2NoCreateCommentWithReopenUser, + owner: OBSERVABILITY_APP_ID, + userWithFullPerms: obsCasesV3AllUser, + }, + { + user: secCasesV2NoCreateCommentWithReopenUser, + owner: SECURITY_SOLUTION_APP_ID, + userWithFullPerms: secCasesV3AllUser, + }, + { user: secReadUser, owner: SECURITY_SOLUTION_APP_ID, userWithFullPerms: secAllUser }, + { user: casesOnlyDeleteUser, owner: CASES_APP_ID, userWithFullPerms: casesAllUser }, + { + user: obsCasesOnlyDeleteUser, + owner: OBSERVABILITY_APP_ID, + userWithFullPerms: obsCasesAllUser, + }, + ]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} CANNOT change assignee`, async () => { + const caseInfo = await createCase(supertest, getPostCaseRequest({ owner })); + const [{ uid: assigneeId }] = await suggestUserProfiles({ + supertest: supertestWithoutAuth, + req: { name: userWithFullPerms.username, owners: [owner], size: 1 }, + auth: { user: userWithFullPerms, space: null }, + }); + await updateCaseAssignee({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + assigneeId, + expectedHttpCode: 403, + auth: { user, space: null }, + version: caseInfo.version, + }); + }); + } + + for (const { user, owner } of [ + { user: casesV3ReadAndAssignUser, owner: CASES_APP_ID }, + { user: secAllUser, owner: SECURITY_SOLUTION_APP_ID }, + { user: casesAllUser, owner: CASES_APP_ID }, + { user: casesV2AllUser, owner: CASES_APP_ID }, + { user: secCasesV2AllUser, owner: SECURITY_SOLUTION_APP_ID }, + { user: obsCasesV2AllUser, owner: OBSERVABILITY_APP_ID }, + { user: obsCasesAllUser, owner: OBSERVABILITY_APP_ID }, + { user: secCasesV2AllUser, owner: SECURITY_SOLUTION_APP_ID }, + { user: obsCasesAllUser, owner: OBSERVABILITY_APP_ID }, + { user: obsCasesV2AllUser, owner: OBSERVABILITY_APP_ID }, + { user: secCasesV3AllUser, owner: SECURITY_SOLUTION_APP_ID }, + { user: casesV3AllUser, owner: CASES_APP_ID }, + { user: obsCasesV3AllUser, owner: OBSERVABILITY_APP_ID }, + ]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} CAN change assignee`, async () => { + const caseInfo = await createCase(supertest, getPostCaseRequest({ owner })); + const [{ uid: assigneeId }] = await suggestUserProfiles({ + supertest: supertestWithoutAuth, + req: { name: user.username, owners: [owner], size: 1 }, + auth: { user, space: null }, + }); + await updateCaseAssignee({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + assigneeId, + expectedHttpCode: 200, + version: caseInfo.version, + auth: { user, space: null }, + }); + }); + } + + for (const { user, owner } of [ + { user: casesV2NoCreateCommentWithReopenUser, owner: CASES_APP_ID }, + { user: obsCasesV2NoCreateCommentWithReopenUser, owner: OBSERVABILITY_APP_ID }, + { user: secCasesV2NoCreateCommentWithReopenUser, owner: SECURITY_SOLUTION_APP_ID }, + ]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} CANNOT add comments`, async () => { + const caseInfo = await createCase(supertest, getPostCaseRequest({ owner })); + const comment: UserCommentAttachmentPayload = { + comment: 'test', + owner, + type: AttachmentType.user, + }; + await createComment({ + params: comment, + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + expectedHttpCode: 403, + auth: { user, space: null }, + }); + }); + } }); }; 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 bb721d29aaeb8..059e368cf689e 100644 --- a/x-pack/test/api_integration/apis/features/features/features.ts +++ b/x-pack/test/api_integration/apis/features/features/features.ts @@ -113,7 +113,7 @@ export default function ({ getService }: FtrProviderContext) { 'guidedOnboardingFeature', 'monitoring', 'observabilityAIAssistant', - 'observabilityCasesV2', + 'observabilityCasesV3', 'savedObjectsManagement', 'savedQueryManagement', 'savedObjectsTagging', @@ -121,7 +121,7 @@ export default function ({ getService }: FtrProviderContext) { 'apm', 'stackAlerts', 'canvas', - 'generalCasesV2', + 'generalCasesV3', 'infrastructure', 'inventory', 'logs', @@ -137,7 +137,7 @@ export default function ({ getService }: FtrProviderContext) { 'slo', 'securitySolutionAssistant', 'securitySolutionAttackDiscovery', - 'securitySolutionCasesV2', + 'securitySolutionCasesV3', 'securitySolutionTimeline', 'securitySolutionNotes', 'fleet', @@ -170,7 +170,7 @@ export default function ({ getService }: FtrProviderContext) { 'guidedOnboardingFeature', 'monitoring', 'observabilityAIAssistant', - 'observabilityCasesV2', + 'observabilityCasesV3', 'savedObjectsManagement', 'savedQueryManagement', 'savedObjectsTagging', @@ -178,7 +178,7 @@ export default function ({ getService }: FtrProviderContext) { 'apm', 'stackAlerts', 'canvas', - 'generalCasesV2', + 'generalCasesV3', 'infrastructure', 'inventory', 'logs', @@ -195,7 +195,7 @@ export default function ({ getService }: FtrProviderContext) { 'slo', 'securitySolutionAssistant', 'securitySolutionAttackDiscovery', - 'securitySolutionCasesV2', + 'securitySolutionCasesV3', 'securitySolutionTimeline', 'securitySolutionNotes', 'fleet', diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index 33742e70ff8b7..d2a9fa2557d7c 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -40,6 +40,17 @@ export default function ({ getService }: FtrProviderContext) { 'create_comment', 'case_reopen', ], + generalCasesV3: [ + 'all', + 'read', + 'minimal_all', + 'minimal_read', + 'cases_delete', + 'cases_settings', + 'create_comment', + 'case_reopen', + 'cases_assign', + ], observabilityCases: [ 'all', 'read', @@ -58,6 +69,17 @@ export default function ({ getService }: FtrProviderContext) { 'create_comment', 'case_reopen', ], + observabilityCasesV3: [ + 'all', + 'read', + 'minimal_all', + 'minimal_read', + 'cases_delete', + 'cases_settings', + 'create_comment', + 'case_reopen', + 'cases_assign', + ], observabilityAIAssistant: ['all', 'read', 'minimal_all', 'minimal_read'], slo: ['all', 'read', 'minimal_all', 'minimal_read'], searchPlayground: ['all', 'read', 'minimal_all', 'minimal_read'], @@ -164,6 +186,17 @@ export default function ({ getService }: FtrProviderContext) { 'create_comment', 'case_reopen', ], + securitySolutionCasesV3: [ + 'all', + 'read', + 'minimal_all', + 'minimal_read', + 'cases_delete', + 'cases_settings', + 'create_comment', + 'case_reopen', + 'cases_assign', + ], securitySolutionTimeline: ['all', 'read', 'minimal_all', 'minimal_read'], securitySolutionNotes: ['all', 'read', 'minimal_all', 'minimal_read'], infrastructure: ['all', 'read', 'minimal_all', 'minimal_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 a80aef834fb41..df477d93f23ad 100644 --- a/x-pack/test/api_integration/apis/security/privileges_basic.ts +++ b/x-pack/test/api_integration/apis/security/privileges_basic.ts @@ -33,8 +33,10 @@ export default function ({ getService }: FtrProviderContext) { maps: ['all', 'read', 'minimal_all', 'minimal_read'], generalCases: ['all', 'read', 'minimal_all', 'minimal_read'], generalCasesV2: ['all', 'read', 'minimal_all', 'minimal_read'], + generalCasesV3: ['all', 'read', 'minimal_all', 'minimal_read'], observabilityCases: ['all', 'read', 'minimal_all', 'minimal_read'], observabilityCasesV2: ['all', 'read', 'minimal_all', 'minimal_read'], + observabilityCasesV3: ['all', 'read', 'minimal_all', 'minimal_read'], observabilityAIAssistant: ['all', 'read', 'minimal_all', 'minimal_read'], slo: ['all', 'read', 'minimal_all', 'minimal_read'], canvas: ['all', 'read', 'minimal_all', 'minimal_read'], @@ -53,6 +55,7 @@ export default function ({ getService }: FtrProviderContext) { securitySolutionAttackDiscovery: ['all', 'read', 'minimal_all', 'minimal_read'], securitySolutionCases: ['all', 'read', 'minimal_all', 'minimal_read'], securitySolutionCasesV2: ['all', 'read', 'minimal_all', 'minimal_read'], + securitySolutionCasesV3: ['all', 'read', 'minimal_all', 'minimal_read'], securitySolutionNotes: ['all', 'read', 'minimal_all', 'minimal_read'], securitySolutionTimeline: ['all', 'read', 'minimal_all', 'minimal_read'], searchPlayground: ['all', 'read', 'minimal_all', 'minimal_read'], @@ -133,6 +136,17 @@ export default function ({ getService }: FtrProviderContext) { 'create_comment', 'case_reopen', ], + generalCasesV3: [ + 'all', + 'read', + 'minimal_all', + 'minimal_read', + 'cases_delete', + 'cases_settings', + 'create_comment', + 'case_reopen', + 'cases_assign', + ], observabilityCases: [ 'all', 'read', @@ -151,6 +165,17 @@ export default function ({ getService }: FtrProviderContext) { 'create_comment', 'case_reopen', ], + observabilityCasesV3: [ + 'all', + 'read', + 'minimal_all', + 'minimal_read', + 'cases_delete', + 'cases_settings', + 'create_comment', + 'case_reopen', + 'cases_assign', + ], observabilityAIAssistant: ['all', 'read', 'minimal_all', 'minimal_read'], slo: ['all', 'read', 'minimal_all', 'minimal_read'], searchPlayground: ['all', 'read', 'minimal_all', 'minimal_read'], @@ -257,6 +282,17 @@ export default function ({ getService }: FtrProviderContext) { 'create_comment', 'case_reopen', ], + securitySolutionCasesV3: [ + 'all', + 'read', + 'minimal_all', + 'minimal_read', + 'cases_delete', + 'cases_settings', + 'create_comment', + 'case_reopen', + 'cases_assign', + ], securitySolutionTimeline: ['all', 'read', 'minimal_all', 'minimal_read'], securitySolutionNotes: ['all', 'read', 'minimal_all', 'minimal_read'], infrastructure: ['all', 'read', 'minimal_all', 'minimal_read'], 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 index 2a85320d14edf..a1c2eb5f7c03f 100644 --- 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 @@ -37,7 +37,7 @@ const secAll: Role = { { feature: { siem: ['all'], - securitySolutionCasesV2: ['all'], + securitySolutionCasesV3: ['all'], actions: ['all'], actionsSimulators: ['all'], }, @@ -68,7 +68,7 @@ const secRead: Role = { { feature: { siem: ['read'], - securitySolutionCasesV2: ['read'], + securitySolutionCasesV3: ['read'], actions: ['all'], actionsSimulators: ['all'], }, diff --git a/x-pack/test/cases_api_integration/common/lib/api/case.ts b/x-pack/test/cases_api_integration/common/lib/api/case.ts index 9f03a62032c89..be9576fe55f87 100644 --- a/x-pack/test/cases_api_integration/common/lib/api/case.ts +++ b/x-pack/test/cases_api_integration/common/lib/api/case.ts @@ -10,7 +10,7 @@ import { Case, CaseStatuses } from '@kbn/cases-plugin/common/types/domain'; import { CasePostRequest, CasesFindResponse, - CasePatchRequest, + CasesPatchRequest, } from '@kbn/cases-plugin/common/types/api'; import type SuperTest from 'supertest'; import { ToolingLog } from '@kbn/tooling-log'; @@ -106,21 +106,60 @@ export const updateCaseStatus = async ({ }: { supertest: SuperTest.Agent; caseId: string; - version?: string; + version: string; status?: CaseStatuses; expectedHttpCode?: number; auth?: { user: User; space: string | null }; }) => { - const updateRequest: CasePatchRequest = { - status, - version, - id: caseId, + const updateRequest: CasesPatchRequest = { + cases: [ + { + status, + version, + id: caseId, + }, + ], }; const { body: updatedCase } = await supertest - .patch(`/api/cases/${caseId}`) + .patch(`${getSpaceUrlPrefix(auth?.space)}${CASES_URL}`) .auth(auth.user.username, auth.user.password) .set('kbn-xsrf', 'xxx') - .send(updateRequest); + .send(updateRequest) + .expect(expectedHttpCode); + return updatedCase; +}; + +export const updateCaseAssignee = async ({ + supertest, + caseId, + version = '2', + assigneeId, + expectedHttpCode = 204, + auth = { user: superUser, space: null }, +}: { + supertest: SuperTest.Agent; + caseId: string; + version?: string; + assigneeId: string; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}) => { + const updateRequest: CasesPatchRequest = { + cases: [ + { + version, + assignees: [{ uid: assigneeId }], + id: caseId, + }, + ], + }; + + const { body: updatedCase } = await supertest + .patch(`${getSpaceUrlPrefix(auth?.space)}${CASES_URL}`) + .auth(auth.user.username, auth.user.password) + .set('kbn-xsrf', 'xxx') + .send(updateRequest) + .expect(expectedHttpCode); return updatedCase; }; diff --git a/x-pack/test/cases_api_integration/common/plugins/security_solution/server/plugin.ts b/x-pack/test/cases_api_integration/common/plugins/security_solution/server/plugin.ts index 34f4c6d7423c0..63fb13be2c611 100644 --- a/x-pack/test/cases_api_integration/common/plugins/security_solution/server/plugin.ts +++ b/x-pack/test/cases_api_integration/common/plugins/security_solution/server/plugin.ts @@ -161,6 +161,29 @@ export class FixturePlugin implements Plugin { before(async () => { await observability.users.setTestUserRole( observability.users.defineBasicObservabilityRole({ - observabilityCasesV2: ['all'], + observabilityCasesV3: ['all'], logs: ['all'], }) ); @@ -75,7 +75,7 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { before(async () => { await observability.users.setTestUserRole( observability.users.defineBasicObservabilityRole({ - observabilityCasesV2: ['read'], + observabilityCasesV3: ['read'], logs: ['all'], }) ); diff --git a/x-pack/test/observability_functional/apps/observability/pages/cases/case_details.ts b/x-pack/test/observability_functional/apps/observability/pages/cases/case_details.ts index 90fc09af9c6ad..392789c3604d2 100644 --- a/x-pack/test/observability_functional/apps/observability/pages/cases/case_details.ts +++ b/x-pack/test/observability_functional/apps/observability/pages/cases/case_details.ts @@ -33,7 +33,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { before(async () => { await observability.users.setTestUserRole( observability.users.defineBasicObservabilityRole({ - observabilityCasesV2: ['all'], + observabilityCasesV3: ['all'], logs: ['all'], }) ); diff --git a/x-pack/test/security_api_integration/tests/features/deprecated_features.ts b/x-pack/test/security_api_integration/tests/features/deprecated_features.ts index acb50a8514a1a..4875500362fdb 100644 --- a/x-pack/test/security_api_integration/tests/features/deprecated_features.ts +++ b/x-pack/test/security_api_integration/tests/features/deprecated_features.ts @@ -181,8 +181,11 @@ export default function ({ getService }: FtrProviderContext) { "case_4_feature_a", "case_4_feature_b", "generalCases", + "generalCasesV2", "observabilityCases", + "observabilityCasesV2", "securitySolutionCases", + "securitySolutionCasesV2", "siem", ] `); diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/privileges.ts b/x-pack/test/security_solution_cypress/cypress/tasks/privileges.ts index 0fd8e6f11b8be..31eca3c55fc22 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/privileges.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/privileges.ts @@ -69,6 +69,7 @@ export const secAll: Role = { securitySolutionAttackDiscovery: ['all'], securitySolutionCases: ['all'], securitySolutionCasesV2: ['all'], + securitySolutionCasesV3: ['all'], actions: ['all'], actionsSimulators: ['all'], }, @@ -105,6 +106,7 @@ export const secReadCasesAll: Role = { securitySolutionAttackDiscovery: ['all'], securitySolutionCases: ['all'], securitySolutionCasesV2: ['all'], + securitySolutionCasesV3: ['all'], actions: ['all'], actionsSimulators: ['all'], }, @@ -141,6 +143,7 @@ export const secAllCasesOnlyReadDelete: Role = { securitySolutionAttackDiscovery: ['all'], securitySolutionCases: ['cases_read', 'cases_delete'], securitySolutionCasesV2: ['cases_read', 'cases_delete'], + securitySolutionCasesV3: ['cases_read', 'cases_delete'], actions: ['all'], actionsSimulators: ['all'], }, @@ -177,6 +180,7 @@ export const secAllCasesNoDelete: Role = { securitySolutionAttackDiscovery: ['all'], securitySolutionCases: ['minimal_all'], securitySolutionCasesV2: ['minimal_all'], + securitySolutionCasesV3: ['minimal_all'], actions: ['all'], actionsSimulators: ['all'], }, diff --git a/x-pack/test/spaces_api_integration/common/suites/create.ts b/x-pack/test/spaces_api_integration/common/suites/create.ts index 5599d065eb93c..87c6b670666e2 100644 --- a/x-pack/test/spaces_api_integration/common/suites/create.ts +++ b/x-pack/test/spaces_api_integration/common/suites/create.ts @@ -82,10 +82,12 @@ export function createTestSuiteFactory(esArchiver: any, supertest: SuperTest) 'logs', 'observabilityCases', 'observabilityCasesV2', + 'observabilityCasesV3', 'securitySolutionAssistant', 'securitySolutionAttackDiscovery', 'securitySolutionCases', 'securitySolutionCasesV2', + 'securitySolutionCasesV3', 'securitySolutionNotes', 'securitySolutionTimeline', 'siem', diff --git a/x-pack/test/spaces_api_integration/common/suites/get_all.ts b/x-pack/test/spaces_api_integration/common/suites/get_all.ts index 9f4abd8001f6d..df1d64ecef650 100644 --- a/x-pack/test/spaces_api_integration/common/suites/get_all.ts +++ b/x-pack/test/spaces_api_integration/common/suites/get_all.ts @@ -81,10 +81,12 @@ const ALL_SPACE_RESULTS: Space[] = [ 'logs', 'observabilityCases', 'observabilityCasesV2', + 'observabilityCasesV3', 'securitySolutionAssistant', 'securitySolutionAttackDiscovery', 'securitySolutionCases', 'securitySolutionCasesV2', + 'securitySolutionCasesV3', 'securitySolutionNotes', 'securitySolutionTimeline', 'siem', diff --git a/x-pack/test/spaces_api_integration/spaces_only/telemetry/telemetry.ts b/x-pack/test/spaces_api_integration/spaces_only/telemetry/telemetry.ts index 9c9e0849d5783..18e1f50fe6357 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/telemetry/telemetry.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/telemetry/telemetry.ts @@ -67,6 +67,7 @@ export default function ({ getService }: FtrProviderContext) { stackAlerts: 0, generalCases: 0, generalCasesV2: 0, + generalCasesV3: 0, maps: 2, canvas: 2, ml: 0, @@ -75,6 +76,7 @@ export default function ({ getService }: FtrProviderContext) { osquery: 0, observabilityCases: 0, observabilityCasesV2: 0, + observabilityCasesV3: 0, uptime: 0, slo: 0, infrastructure: 0, @@ -92,6 +94,7 @@ export default function ({ getService }: FtrProviderContext) { siemV2: 0, securitySolutionCases: 0, securitySolutionCasesV2: 0, + securitySolutionCasesV3: 0, securitySolutionAssistant: 0, securitySolutionAttackDiscovery: 0, securitySolutionTimeline: 0,