From aa42bccd406e42b0f8c1b3453054d377f08f181a Mon Sep 17 00:00:00 2001 From: Pablo Machado Date: Mon, 7 Aug 2023 10:22:10 +0200 Subject: [PATCH] [Security Solutions] Add PLI authorisation for Cases Connector (#161343) ## Summary * Create a new capability called `cases_connectors` which will control the access to the cases connector feature. Note that for users to have access to this feature they also need to be authorized for cases feature and actions feature. * Create a new API tag `casesGetConnectorsConfigure` to restrict access to the Get Connectors APIs. ## Authorization For the authorization of users we use a) a new UI capability b) a new API access tag and c) the existing Cases RBAC. The Cases feature privilege in Security solution is constructed based on the configuration provided by the security serverless plugin. The UI capability, the API tag, and the cases operations will be added/removed depending on the configuration. ### UI capability We include the `CASES_CONNECTORS_CAPABILITY` which will be used by the UI to show/hide various UI components responsible for the case connectors feature. ### APIs There are two APIs that use connectors in Cases. The [Get Connectors API](https://www.elastic.co/guide/en/kibana/current/case-apis.html#findCaseConnectors) which returns all supported connectors by Cases and the [Push Case API](https://www.elastic.co/guide/en/kibana/current/case-apis.html#pushCaseDefaultSpace) that push a case to an external service. #### Get Connectors API The Get Connectors API does not interact with any of the cases' saved objects. It uses the `actionsClient`, provided by the actions plugin, to get all connectors and filter out the ones supported by cases. For that reason, an API tag called `GET_CONNECTORS_CONFIGURE_API_TAG` is added to the API to control access. If the user has access to any of the Cases kibana privilege features (Security, Observability, or Stack) it will have access to the API. This is an expected behavior and in the Security serverless project, only one Case feature will be available. #### Push Case API The Push Case API already authorizes users by using the Cases RBAC. The user should have the `push` operation set in the Cases Kibana feature privilege to be able to use the API. ## Permissions
Cases | Actions | Case Connectors | Outcome -- | -- | -- | -- read | all | all | See the connector but cannot edit (current behavior) read | all | none | Hide the connectors in Cases read | read | all | See the connector but cannot edit (current behavior) read | read | none | Hide the connectors in Cases all | all | all | Full access all | all | none | Hide the connectors in Cases all | read | all | See the connector but cannot edit (current behavior) all | read | none | Hide the connectors in Cases

When the Actions is set to `none` all connector features are hidden ### How to test it? #### ESS * Run ESS and check if it still works as expected for all combinations of cases and actions permissions. #### Serverless * Run Serverless with security essentials (serverless.security.yml) and check if it works as expected for all combinations of cases and actions permissions. ``` xpack.serverless.security.productTypes: [ { product_line: 'security', product_tier: 'essentials' } ] ``` * Run Serverless with security complete (config/serverless.security.yml) and check if it works as expected for all combinations of cases and actions permissions. ``` xpack.serverless.security.productTypes: [ { product_line: 'security', product_tier: 'complete' }, ] ``` ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Christos Nasikas --- .../plugins/cases/common/constants/index.ts | 6 + x-pack/plugins/cases/common/ui/types.ts | 5 +- .../utils/__snapshots__/api_tags.test.ts.snap | 6 + x-pack/plugins/cases/common/utils/api_tags.ts | 21 +++- .../cases/common/utils/capabilities.ts | 4 +- .../client/helpers/can_use_cases.test.ts | 16 ++- .../public/client/helpers/can_use_cases.ts | 5 +- .../client/helpers/capabilities.test.ts | 31 +++++ .../public/client/helpers/capabilities.ts | 5 +- .../cases/public/common/lib/kibana/hooks.ts | 2 + .../common/lib/kibana/kibana_react.mock.tsx | 1 + .../cases/public/common/mock/permissions.ts | 23 +++- .../configure_cases/connectors.test.tsx | 18 ++- .../components/configure_cases/connectors.tsx | 5 +- .../components/create/connector.test.tsx | 18 ++- .../public/components/create/connector.tsx | 5 +- .../components/edit_connector/index.test.tsx | 54 ++++++++- .../components/edit_connector/index.tsx | 48 ++++---- .../use_get_supported_action_connectors.tsx | 5 +- ...t_supported_action_connectors.tsx.test.tsx | 18 ++- .../__snapshots__/audit_logger.test.ts.snap | 8 +- .../cases/server/authorization/index.ts | 8 +- .../routes/api/configure/get_connectors.ts | 3 + .../plugins/cases/server/routes/api/types.ts | 2 +- .../header/add_to_case_action.test.tsx | 1 + .../hooks/use_get_user_cases_permissions.tsx | 3 + .../pages/cases/components/cases.stories.tsx | 11 +- .../hooks/use_get_user_cases_permissions.tsx | 3 + .../public/utils/cases_permissions.ts | 1 + .../public/cases_test_utils.ts | 7 ++ .../public/common/lib/kibana/hooks.ts | 3 + .../security_cases_kibana_features.ts | 114 ++++++++++++------ .../common/lib/authentication/roles.ts | 25 ++++ .../common/lib/authentication/users.ts | 8 ++ .../common/plugins/cases/server/plugin.ts | 39 ++++++ .../plugins/observability/server/plugin.ts | 4 +- .../security_solution/server/plugin.ts | 4 +- .../tests/basic/cases/push_case.ts | 2 +- .../tests/trial/cases/push_case.ts | 19 +++ .../tests/trial/configure/get_connectors.ts | 12 +- .../plugins/cases/public/application.tsx | 1 + 41 files changed, 478 insertions(+), 96 deletions(-) diff --git a/x-pack/plugins/cases/common/constants/index.ts b/x-pack/plugins/cases/common/constants/index.ts index ac689ad5b29d4..01c8543493f7a 100644 --- a/x-pack/plugins/cases/common/constants/index.ts +++ b/x-pack/plugins/cases/common/constants/index.ts @@ -158,6 +158,7 @@ export const READ_CASES_CAPABILITY = 'read_cases' as const; export const UPDATE_CASES_CAPABILITY = 'update_cases' as const; export const DELETE_CASES_CAPABILITY = 'delete_cases' as const; export const PUSH_CASES_CAPABILITY = 'push_cases' as const; +export const CASES_CONNECTORS_CAPABILITY = 'cases_connectors' as const; /** * Cases API Tags @@ -173,6 +174,11 @@ export const SUGGEST_USER_PROFILES_API_TAG = 'casesSuggestUserProfiles'; */ export const BULK_GET_USER_PROFILES_API_TAG = 'bulkGetUserProfiles'; +/** + * This tag is registered for the connectors (configure) get API + */ +export const GET_CONNECTORS_CONFIGURE_API_TAG = 'casesGetConnectorsConfigure'; + /** * User profiles */ diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index d8790a82e5ded..ebcaceaceff4f 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -12,7 +12,8 @@ import type { READ_CASES_CAPABILITY, UPDATE_CASES_CAPABILITY, } from '..'; -import type { PUSH_CASES_CAPABILITY } from '../constants'; +import type { CaseMetricsFeature, CasesMetricsResponse, SingleCaseMetricsResponse } from '../api'; +import type { CASES_CONNECTORS_CAPABILITY, PUSH_CASES_CAPABILITY } from '../constants'; import type { SnakeToCamelCase } from '../types'; import type { CaseSeverity, @@ -285,6 +286,7 @@ export interface CasesPermissions { update: boolean; delete: boolean; push: boolean; + connectors: boolean; } export interface CasesCapabilities { @@ -293,4 +295,5 @@ export interface CasesCapabilities { [UPDATE_CASES_CAPABILITY]: boolean; [DELETE_CASES_CAPABILITY]: boolean; [PUSH_CASES_CAPABILITY]: boolean; + [CASES_CONNECTORS_CAPABILITY]: boolean; } diff --git a/x-pack/plugins/cases/common/utils/__snapshots__/api_tags.test.ts.snap b/x-pack/plugins/cases/common/utils/__snapshots__/api_tags.test.ts.snap index ea1ef29e71c59..9cca596cc84d8 100644 --- a/x-pack/plugins/cases/common/utils/__snapshots__/api_tags.test.ts.snap +++ b/x-pack/plugins/cases/common/utils/__snapshots__/api_tags.test.ts.snap @@ -5,6 +5,7 @@ Object { "all": Array [ "casesSuggestUserProfiles", "bulkGetUserProfiles", + "casesGetConnectorsConfigure", "casesFilesCasesCreate", "casesFilesCasesRead", ], @@ -14,6 +15,7 @@ Object { "read": Array [ "casesSuggestUserProfiles", "bulkGetUserProfiles", + "casesGetConnectorsConfigure", "casesFilesCasesRead", ], } @@ -24,6 +26,7 @@ Object { "all": Array [ "casesSuggestUserProfiles", "bulkGetUserProfiles", + "casesGetConnectorsConfigure", "observabilityFilesCasesCreate", "observabilityFilesCasesRead", ], @@ -33,6 +36,7 @@ Object { "read": Array [ "casesSuggestUserProfiles", "bulkGetUserProfiles", + "casesGetConnectorsConfigure", "observabilityFilesCasesRead", ], } @@ -43,6 +47,7 @@ Object { "all": Array [ "casesSuggestUserProfiles", "bulkGetUserProfiles", + "casesGetConnectorsConfigure", "securitySolutionFilesCasesCreate", "securitySolutionFilesCasesRead", ], @@ -52,6 +57,7 @@ Object { "read": Array [ "casesSuggestUserProfiles", "bulkGetUserProfiles", + "casesGetConnectorsConfigure", "securitySolutionFilesCasesRead", ], } diff --git a/x-pack/plugins/cases/common/utils/api_tags.ts b/x-pack/plugins/cases/common/utils/api_tags.ts index d9e3ad25a04c0..2568c0e79b9a0 100644 --- a/x-pack/plugins/cases/common/utils/api_tags.ts +++ b/x-pack/plugins/cases/common/utils/api_tags.ts @@ -5,7 +5,11 @@ * 2.0. */ -import { BULK_GET_USER_PROFILES_API_TAG, SUGGEST_USER_PROFILES_API_TAG } from '../constants'; +import { + BULK_GET_USER_PROFILES_API_TAG, + GET_CONNECTORS_CONFIGURE_API_TAG, + SUGGEST_USER_PROFILES_API_TAG, +} from '../constants'; import { HttpApiTagOperation } from '../constants/types'; import type { Owner } from '../constants/types'; import { constructFilesHttpOperationTag } from '../files'; @@ -16,8 +20,19 @@ export const getApiTags = (owner: Owner) => { const read = constructFilesHttpOperationTag(owner, HttpApiTagOperation.Read); return { - all: [SUGGEST_USER_PROFILES_API_TAG, BULK_GET_USER_PROFILES_API_TAG, create, read] as const, - read: [SUGGEST_USER_PROFILES_API_TAG, BULK_GET_USER_PROFILES_API_TAG, read] as const, + all: [ + SUGGEST_USER_PROFILES_API_TAG, + BULK_GET_USER_PROFILES_API_TAG, + GET_CONNECTORS_CONFIGURE_API_TAG, + create, + read, + ] as const, + read: [ + SUGGEST_USER_PROFILES_API_TAG, + BULK_GET_USER_PROFILES_API_TAG, + GET_CONNECTORS_CONFIGURE_API_TAG, + read, + ] as const, delete: [deleteTag] as const, }; }; diff --git a/x-pack/plugins/cases/common/utils/capabilities.ts b/x-pack/plugins/cases/common/utils/capabilities.ts index a508d11201966..e9c05eda47171 100644 --- a/x-pack/plugins/cases/common/utils/capabilities.ts +++ b/x-pack/plugins/cases/common/utils/capabilities.ts @@ -6,6 +6,7 @@ */ import { + CASES_CONNECTORS_CAPABILITY, CREATE_CASES_CAPABILITY, DELETE_CASES_CAPABILITY, PUSH_CASES_CAPABILITY, @@ -23,7 +24,8 @@ export const createUICapabilities = () => ({ READ_CASES_CAPABILITY, UPDATE_CASES_CAPABILITY, PUSH_CASES_CAPABILITY, + CASES_CONNECTORS_CAPABILITY, ] as const, - read: [READ_CASES_CAPABILITY] as const, + read: [READ_CASES_CAPABILITY, CASES_CONNECTORS_CAPABILITY] as const, delete: [DELETE_CASES_CAPABILITY] as const, }); diff --git a/x-pack/plugins/cases/public/client/helpers/can_use_cases.test.ts b/x-pack/plugins/cases/public/client/helpers/can_use_cases.test.ts index 9898f9c374c78..5b82919523f36 100644 --- a/x-pack/plugins/cases/public/client/helpers/can_use_cases.test.ts +++ b/x-pack/plugins/cases/public/client/helpers/can_use_cases.test.ts @@ -11,8 +11,8 @@ import { allCasesPermissions, noCasesCapabilities, noCasesPermissions, - readCasesCapabilities, readCasesPermissions, + readCasesCapabilities, writeCasesCapabilities, writeCasesPermissions, } from '../../common/mock'; @@ -77,6 +77,12 @@ const hasSecurityWriteAndObservabilityRead: CasesCapabilities = { generalCases: noCasesCapabilities(), }; +const hasSecurityConnectors: CasesCapabilities = { + securitySolutionCases: readCasesCapabilities(), + observabilityCases: noCasesCapabilities(), + generalCases: noCasesCapabilities(), +}; + describe('canUseCases', () => { it.each([hasAll, hasSecurity, hasObservability, hasSecurityWriteAndObservabilityRead])( 'returns true for all permissions, if a user has access to both on any solution', @@ -109,4 +115,12 @@ describe('canUseCases', () => { expect(permissions).toStrictEqual(noCasesPermissions()); } ); + + it.each([hasSecurityConnectors])( + 'returns true for only connectors, if a user has access to only connectors on any solution', + (capability) => { + const permissions = canUseCases(capability)(); + expect(permissions).toStrictEqual(readCasesPermissions()); + } + ); }); diff --git a/x-pack/plugins/cases/public/client/helpers/can_use_cases.ts b/x-pack/plugins/cases/public/client/helpers/can_use_cases.ts index 34af1c3865da5..1cc22c0799702 100644 --- a/x-pack/plugins/cases/public/client/helpers/can_use_cases.ts +++ b/x-pack/plugins/cases/public/client/helpers/can_use_cases.ts @@ -40,8 +40,10 @@ export const canUseCases = acc.update = acc.update || userCapabilitiesForOwner.update; acc.delete = acc.delete || userCapabilitiesForOwner.delete; acc.push = acc.push || userCapabilitiesForOwner.push; - const allFromAcc = acc.create && acc.read && acc.update && acc.delete && acc.push; + const allFromAcc = + acc.create && acc.read && acc.update && acc.delete && acc.push && acc.connectors; acc.all = acc.all || userCapabilitiesForOwner.all || allFromAcc; + acc.connectors = acc.connectors || userCapabilitiesForOwner.connectors; return acc; }, @@ -52,6 +54,7 @@ export const canUseCases = update: false, delete: false, push: false, + connectors: false, } ); diff --git a/x-pack/plugins/cases/public/client/helpers/capabilities.test.ts b/x-pack/plugins/cases/public/client/helpers/capabilities.test.ts index 58d6d61e80324..a3f741f373032 100644 --- a/x-pack/plugins/cases/public/client/helpers/capabilities.test.ts +++ b/x-pack/plugins/cases/public/client/helpers/capabilities.test.ts @@ -12,6 +12,7 @@ describe('getUICapabilities', () => { expect(getUICapabilities(undefined)).toMatchInlineSnapshot(` Object { "all": false, + "connectors": false, "create": false, "delete": false, "push": false, @@ -25,6 +26,7 @@ describe('getUICapabilities', () => { expect(getUICapabilities()).toMatchInlineSnapshot(` Object { "all": false, + "connectors": false, "create": false, "delete": false, "push": false, @@ -38,6 +40,7 @@ describe('getUICapabilities', () => { expect(getUICapabilities({ create_cases: true })).toMatchInlineSnapshot(` Object { "all": false, + "connectors": false, "create": true, "delete": false, "push": false, @@ -55,10 +58,12 @@ describe('getUICapabilities', () => { update_cases: false, delete_cases: false, push_cases: false, + cases_connectors: false, }) ).toMatchInlineSnapshot(` Object { "all": false, + "connectors": false, "create": false, "delete": false, "push": false, @@ -72,6 +77,7 @@ describe('getUICapabilities', () => { expect(getUICapabilities({})).toMatchInlineSnapshot(` Object { "all": false, + "connectors": false, "create": false, "delete": false, "push": false, @@ -89,10 +95,35 @@ describe('getUICapabilities', () => { update_cases: true, delete_cases: true, push_cases: true, + cases_connectors: true, }) ).toMatchInlineSnapshot(` Object { "all": false, + "connectors": true, + "create": false, + "delete": true, + "push": true, + "read": true, + "update": true, + } + `); + }); + + it('returns false for the all field when cases_connectors is false', () => { + expect( + getUICapabilities({ + create_cases: false, + read_cases: true, + update_cases: true, + delete_cases: true, + push_cases: true, + cases_connectors: false, + }) + ).toMatchInlineSnapshot(` + Object { + "all": false, + "connectors": false, "create": false, "delete": true, "push": true, diff --git a/x-pack/plugins/cases/public/client/helpers/capabilities.ts b/x-pack/plugins/cases/public/client/helpers/capabilities.ts index f09ac84448952..278512fef623c 100644 --- a/x-pack/plugins/cases/public/client/helpers/capabilities.ts +++ b/x-pack/plugins/cases/public/client/helpers/capabilities.ts @@ -7,6 +7,7 @@ import type { CasesPermissions } from '../../../common'; import { + CASES_CONNECTORS_CAPABILITY, CREATE_CASES_CAPABILITY, DELETE_CASES_CAPABILITY, PUSH_CASES_CAPABILITY, @@ -22,7 +23,8 @@ export const getUICapabilities = ( const update = !!featureCapabilities?.[UPDATE_CASES_CAPABILITY]; const deletePriv = !!featureCapabilities?.[DELETE_CASES_CAPABILITY]; const push = !!featureCapabilities?.[PUSH_CASES_CAPABILITY]; - const all = create && read && update && deletePriv && push; + const connectors = !!featureCapabilities?.[CASES_CONNECTORS_CAPABILITY]; + const all = create && read && update && deletePriv && push && connectors; return { all, @@ -31,5 +33,6 @@ export const getUICapabilities = ( update, delete: deletePriv, push, + connectors, }; }; diff --git a/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts b/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts index 812840b1553e3..c540824b1ebb5 100644 --- a/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts +++ b/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts @@ -193,6 +193,7 @@ export const useApplicationCapabilities = (): UseApplicationCapabilities => { update: permissions.update, delete: permissions.delete, push: permissions.push, + connectors: permissions.connectors, }, visualize: { crud: !!capabilities.visualize?.save, read: !!capabilities.visualize?.show }, dashboard: { @@ -213,6 +214,7 @@ export const useApplicationCapabilities = (): UseApplicationCapabilities => { permissions.update, permissions.delete, permissions.push, + permissions.connectors, ] ); }; diff --git a/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.tsx b/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.tsx index 819c7099b5cb8..31ea452874c28 100644 --- a/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.tsx +++ b/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.tsx @@ -74,6 +74,7 @@ export const createStartServicesMock = ({ license }: StartServiceArgs = {}): Sta update_cases: true, delete_cases: true, push_cases: true, + cases_connectors: true, }, visualize: { save: true, show: true }, dashboard: { show: true, createNew: true }, diff --git a/x-pack/plugins/cases/public/common/mock/permissions.ts b/x-pack/plugins/cases/public/common/mock/permissions.ts index 01d1dc64952ed..4d68e9d36c776 100644 --- a/x-pack/plugins/cases/public/common/mock/permissions.ts +++ b/x-pack/plugins/cases/public/common/mock/permissions.ts @@ -9,9 +9,23 @@ import type { CasesCapabilities, CasesPermissions } from '../../containers/types export const allCasesPermissions = () => buildCasesPermissions(); export const noCasesPermissions = () => - buildCasesPermissions({ read: false, create: false, update: false, delete: false, push: false }); + buildCasesPermissions({ + read: false, + create: false, + update: false, + delete: false, + push: false, + connectors: false, + }); export const readCasesPermissions = () => - buildCasesPermissions({ read: true, create: false, update: false, delete: false, push: false }); + buildCasesPermissions({ + read: true, + create: false, + update: false, + delete: false, + push: false, + connectors: true, + }); export const noCreateCasesPermissions = () => buildCasesPermissions({ create: false }); export const noUpdateCasesPermissions = () => buildCasesPermissions({ update: false }); export const noPushCasesPermissions = () => buildCasesPermissions({ push: false }); @@ -19,6 +33,7 @@ export const noDeleteCasesPermissions = () => buildCasesPermissions({ delete: fa export const writeCasesPermissions = () => buildCasesPermissions({ read: false }); export const onlyDeleteCasesPermission = () => buildCasesPermissions({ read: false, create: false, update: false, delete: true, push: false }); +export const noConnectorsCasePermission = () => buildCasesPermissions({ connectors: false }); export const buildCasesPermissions = (overrides: Partial> = {}) => { const create = overrides.create ?? true; @@ -26,6 +41,7 @@ export const buildCasesPermissions = (overrides: Partial update_cases: false, delete_cases: false, push_cases: false, + cases_connectors: false, }); export const readCasesCapabilities = () => buildCasesCapabilities({ @@ -67,5 +85,6 @@ export const buildCasesCapabilities = (overrides?: Partial) = update_cases: overrides?.update_cases ?? true, delete_cases: overrides?.delete_cases ?? true, push_cases: overrides?.push_cases ?? true, + cases_connectors: overrides?.cases_connectors ?? true, }; }; diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx index c77340b4f37ac..9db8d82c6b315 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx @@ -12,8 +12,12 @@ import { render, screen } from '@testing-library/react'; import type { Props } from './connectors'; import { Connectors } from './connectors'; -import type { AppMockRenderer } from '../../common/mock'; -import { createAppMockRenderer, TestProviders } from '../../common/mock'; +import { + type AppMockRenderer, + noConnectorsCasePermission, + createAppMockRenderer, + TestProviders, +} from '../../common/mock'; import { ConnectorsDropdown } from './connectors_dropdown'; import { connectors, actionTypes } from './__mock__'; import { ConnectorTypes } from '../../../common/types/domain'; @@ -161,4 +165,14 @@ describe('Connectors', () => { ).toBeInTheDocument(); expect(result.queryByTestId('case-connectors-dropdown')).toBe(null); }); + + it('shows the actions permission message if the user does not have access to case connector', async () => { + appMockRender = createAppMockRenderer({ permissions: noConnectorsCasePermission() }); + + const result = appMockRender.render(); + expect( + result.getByTestId('configure-case-connector-permissions-error-msg') + ).toBeInTheDocument(); + expect(result.queryByTestId('case-connectors-dropdown')).toBe(null); + }); }); diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx index 0b51323f3ffd8..87e3100f087a7 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx @@ -27,6 +27,7 @@ import { ConnectorTypes } from '../../../common/types/domain'; import { DeprecatedCallout } from '../connectors/deprecated_callout'; import { isDeprecatedConnector } from '../utils'; import { useApplicationCapabilities } from '../../common/lib/kibana'; +import { useCasesContext } from '../cases_context/use_cases_context'; const EuiFormRowExtended = styled(EuiFormRow)` .euiFormRow__labelWrapper { @@ -63,6 +64,8 @@ const ConnectorsComponent: React.FC = ({ () => connectors.find((c) => c.id === selectedConnector.id), [connectors, selectedConnector.id] ); + const { permissions } = useCasesContext(); + const canUseConnectors = permissions.connectors && actions.read; const connectorsName = connector?.name ?? 'none'; @@ -105,7 +108,7 @@ const ConnectorsComponent: React.FC = ({ > - {actions.read ? ( + {canUseConnectors ? ( { expect(result.getByTestId('create-case-connector-permissions-error-msg')).toBeInTheDocument(); expect(result.queryByTestId('caseConnectors')).toBe(null); }); + + it('shows the actions permission message if the user does not have access to case connector', async () => { + appMockRender = createAppMockRenderer({ permissions: noConnectorsCasePermission() }); + + const result = appMockRender.render( + + + + ); + expect(result.getByTestId('create-case-connector-permissions-error-msg')).toBeInTheDocument(); + expect(result.queryByTestId('caseConnectors')).toBe(null); + }); }); diff --git a/x-pack/plugins/cases/public/components/create/connector.tsx b/x-pack/plugins/cases/public/components/create/connector.tsx index 58bf659b68cf5..7422e671fa4bb 100644 --- a/x-pack/plugins/cases/public/components/create/connector.tsx +++ b/x-pack/plugins/cases/public/components/create/connector.tsx @@ -18,6 +18,7 @@ import { useCaseConfigure } from '../../containers/configure/use_configure'; import { getConnectorById, getConnectorsFormValidators } from '../utils'; import { useApplicationCapabilities } from '../../common/lib/kibana'; import * as i18n from '../../common/translations'; +import { useCasesContext } from '../cases_context/use_cases_context'; interface Props { connectors: ActionConnector[]; @@ -30,6 +31,8 @@ const ConnectorComponent: React.FC = ({ connectors, isLoading, isLoadingC const connector = getConnectorById(connectorId, connectors) ?? null; const { connector: configurationConnector } = useCaseConfigure(); const { actions } = useApplicationCapabilities(); + const { permissions } = useCasesContext(); + const hasReadPermissions = permissions.connectors && actions.read; const defaultConnectorId = useMemo(() => { return connectors.some((c) => c.id === configurationConnector.id) @@ -42,7 +45,7 @@ const ConnectorComponent: React.FC = ({ connectors, isLoading, isLoadingC connectors, }); - if (!actions.read) { + if (!hasReadPermissions) { return ( {i18n.READ_ACTIONS_PERMISSIONS_ERROR_MSG} diff --git a/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx b/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx index 21ff3af65a336..b68641526c46a 100644 --- a/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx +++ b/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx @@ -11,12 +11,14 @@ import userEvent from '@testing-library/user-event'; import type { EditConnectorProps } from '.'; import { EditConnector } from '.'; -import type { AppMockRenderer } from '../../common/mock'; + import { + type AppMockRenderer, createAppMockRenderer, readCasesPermissions, noPushCasesPermissions, TestProviders, + noConnectorsCasePermission, } from '../../common/mock'; import { basicCase, connectorsMock } from '../../containers/mock'; import { getCaseConnectorsMockResponse } from '../../common/mock/connectors'; @@ -274,6 +276,17 @@ describe('EditConnector ', () => { }); }); + it('does not show the callout if the user does not have access to cases connectors', async () => { + const props = { ...defaultProps, connectors: [] }; + appMockRender = createAppMockRenderer({ permissions: noConnectorsCasePermission() }); + + const result = appMockRender.render(); + await waitFor(() => { + expect(result.getByTestId('edit-connector-permissions-error-msg')).toBeInTheDocument(); + expect(result.queryByTestId('push-callouts')).toBe(null); + }); + }); + it('does not show the connectors previewer if the user does not have read access to actions', async () => { const props = { ...defaultProps, connectors: [] }; appMockRender.coreStart.application.capabilities = { @@ -285,6 +298,14 @@ describe('EditConnector ', () => { expect(result.queryByTestId('connector-fields-preview')).not.toBeInTheDocument(); }); + it('does not show the connectors previewer if the user does not have access to cases connectors', async () => { + const props = { ...defaultProps, connectors: [] }; + appMockRender = createAppMockRenderer({ permissions: noConnectorsCasePermission() }); + + const result = appMockRender.render(); + expect(result.queryByTestId('connector-fields-preview')).not.toBeInTheDocument(); + }); + it('does not show the connectors form if the user does not have read access to actions', async () => { const props = { ...defaultProps, connectors: [] }; appMockRender.coreStart.application.capabilities = { @@ -296,6 +317,14 @@ describe('EditConnector ', () => { expect(result.queryByTestId('edit-connector-fields-form-flex-item')).not.toBeInTheDocument(); }); + it('does not show the connectors form if the user does not have access to cases connectors', async () => { + const props = { ...defaultProps, connectors: [] }; + appMockRender = createAppMockRenderer({ permissions: noConnectorsCasePermission() }); + + const result = appMockRender.render(); + expect(result.queryByTestId('edit-connector-fields-form-flex-item')).not.toBeInTheDocument(); + }); + it('does not show the push button if the user does not have read access to actions', async () => { appMockRender.coreStart.application.capabilities = { ...appMockRender.coreStart.application.capabilities, @@ -317,6 +346,15 @@ describe('EditConnector ', () => { }); }); + it('does not show the push button if the user does not have access to cases actions', async () => { + appMockRender = createAppMockRenderer({ permissions: noConnectorsCasePermission() }); + + const result = appMockRender.render(); + await waitFor(() => { + expect(result.queryByTestId('push-to-external-service')).toBe(null); + }); + }); + it('does not show the edit connectors pencil if the user does not have read access to actions', async () => { const props = { ...defaultProps, connectors: [] }; appMockRender.coreStart.application.capabilities = { @@ -332,6 +370,20 @@ describe('EditConnector ', () => { }); }); + it('does not show the edit connectors pencil if the user does not have access to case connectors', async () => { + const props = { ...defaultProps, connectors: [] }; + appMockRender = createAppMockRenderer({ + permissions: noConnectorsCasePermission(), + }); + + appMockRender.render(); + + await waitFor(() => { + expect(screen.getByTestId('connector-edit-header')).toBeInTheDocument(); + expect(screen.queryByTestId('connector-edit-button')).not.toBeInTheDocument(); + }); + }); + it('does not show the edit connectors pencil if the user does not have push permissions', async () => { const props = { ...defaultProps, connectors: [] }; appMockRender = createAppMockRenderer({ permissions: noPushCasesPermissions() }); diff --git a/x-pack/plugins/cases/public/components/edit_connector/index.tsx b/x-pack/plugins/cases/public/components/edit_connector/index.tsx index ee819af75b400..27d5e75b550fb 100644 --- a/x-pack/plugins/cases/public/components/edit_connector/index.tsx +++ b/x-pack/plugins/cases/public/components/edit_connector/index.tsx @@ -21,6 +21,7 @@ import { PushButton } from './push_button'; import { PushCallouts } from './push_callouts'; import { ConnectorsForm } from './connectors_form'; import { ConnectorFieldsPreviewForm } from '../connectors/fields_preview_form'; +import { useCasesContext } from '../cases_context/use_cases_context'; export interface EditConnectorProps { caseData: CaseUI; @@ -45,7 +46,8 @@ export const EditConnector = React.memo( const [isEdit, setIsEdit] = useState(false); const { actions } = useApplicationCapabilities(); - const hasActionsReadPermissions = actions.read; + const { permissions } = useCasesContext(); + const canUseConnectors = permissions.connectors && actions.read; const onEditClick = useCallback(() => setIsEdit(true), []); const onCancelConnector = useCallback(() => setIsEdit(false), []); @@ -102,7 +104,7 @@ export const EditConnector = React.memo(

{i18n.CONNECTORS}

- {!isLoading && !isEdit && hasPushPermissions && hasActionsReadPermissions ? ( + {!isLoading && !isEdit && hasPushPermissions && canUseConnectors ? ( - {!isLoading && !isEdit && hasErrorMessages && hasActionsReadPermissions && ( + {!isLoading && !isEdit && hasErrorMessages && canUseConnectors && ( )} - {!hasActionsReadPermissions && ( + {!canUseConnectors && ( {i18n.READ_ACTIONS_PERMISSIONS_ERROR_MSG} )} - {hasActionsReadPermissions && !isEdit && ( + {canUseConnectors && !isEdit && ( )} - {hasActionsReadPermissions && isEdit && ( + {canUseConnectors && isEdit && ( )} - {!hasErrorMessages && - !isLoading && - !isEdit && - hasPushPermissions && - hasActionsReadPermissions && ( - - - 0 || !needsToBePushed || !hasPushPermissions} - connectorName={connectorWithName.name} - /> - - - )} + {!hasErrorMessages && !isLoading && !isEdit && hasPushPermissions && canUseConnectors && ( + + + 0 || !needsToBePushed || !hasPushPermissions} + connectorName={connectorWithName.name} + /> + + + )}
diff --git a/x-pack/plugins/cases/public/containers/configure/use_get_supported_action_connectors.tsx b/x-pack/plugins/cases/public/containers/configure/use_get_supported_action_connectors.tsx index 0bff8bce61c3e..e98d63debce4b 100644 --- a/x-pack/plugins/cases/public/containers/configure/use_get_supported_action_connectors.tsx +++ b/x-pack/plugins/cases/public/containers/configure/use_get_supported_action_connectors.tsx @@ -11,14 +11,17 @@ import { useApplicationCapabilities, useToasts } from '../../common/lib/kibana'; import * as i18n from './translations'; import { casesQueriesKeys } from '../constants'; import type { ServerError } from '../../types'; +import { useCasesContext } from '../../components/cases_context/use_cases_context'; export function useGetSupportedActionConnectors() { const toasts = useToasts(); const { actions } = useApplicationCapabilities(); + const { permissions } = useCasesContext(); + return useQuery( casesQueriesKeys.connectorsList(), async ({ signal }) => { - if (!actions.read) { + if (!actions.read || !permissions.connectors) { return []; } return getSupportedActionConnectors({ signal }); diff --git a/x-pack/plugins/cases/public/containers/configure/use_get_supported_action_connectors.tsx.test.tsx b/x-pack/plugins/cases/public/containers/configure/use_get_supported_action_connectors.tsx.test.tsx index 36cbd9417e375..c6a05daff7f4e 100644 --- a/x-pack/plugins/cases/public/containers/configure/use_get_supported_action_connectors.tsx.test.tsx +++ b/x-pack/plugins/cases/public/containers/configure/use_get_supported_action_connectors.tsx.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { renderHook } from '@testing-library/react-hooks'; import * as api from './api'; -import { TestProviders } from '../../common/mock'; +import { noConnectorsCasePermission, TestProviders } from '../../common/mock'; import { useApplicationCapabilities, useToasts } from '../../common/lib/kibana'; import { useGetSupportedActionConnectors } from './use_get_supported_action_connectors'; @@ -65,4 +65,20 @@ describe('useConnectors', () => { expect(spyOnFetchConnectors).not.toHaveBeenCalled(); expect(result.current.data).toEqual([]); }); + + it('does not fetch connectors when the user does not has access to connectors', async () => { + const spyOnFetchConnectors = jest.spyOn(api, 'getSupportedActionConnectors'); + useApplicationCapabilitiesMock().actions = { crud: true, read: true }; + + const { result, waitForNextUpdate } = renderHook(() => useGetSupportedActionConnectors(), { + wrapper: ({ children }) => ( + {children} + ), + }); + + await waitForNextUpdate(); + + expect(spyOnFetchConnectors).not.toHaveBeenCalled(); + expect(result.current.data).toEqual([]); + }); }); diff --git a/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap b/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap index 18eef843e88c2..ebb9501ff8960 100644 --- a/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap +++ b/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap @@ -2458,7 +2458,7 @@ Object { "type": "cases", }, }, - "message": "Failed attempt to update cases [id=1] as owner \\"awesome\\"", + "message": "Failed attempt to push cases [id=1] as owner \\"awesome\\"", } `; @@ -2478,7 +2478,7 @@ Object { "change", ], }, - "message": "Failed attempt to update a case as any owners", + "message": "Failed attempt to push a case as any owners", } `; @@ -2500,7 +2500,7 @@ Object { "type": "cases", }, }, - "message": "User is updating cases [id=5] as owner \\"super\\"", + "message": "User is pushing cases [id=5] as owner \\"super\\"", } `; @@ -2516,7 +2516,7 @@ Object { "change", ], }, - "message": "User is updating a case as any owners", + "message": "User is pushing a case as any owners", } `; diff --git a/x-pack/plugins/cases/server/authorization/index.ts b/x-pack/plugins/cases/server/authorization/index.ts index 4bd881364cfc2..12653aa6079e6 100644 --- a/x-pack/plugins/cases/server/authorization/index.ts +++ b/x-pack/plugins/cases/server/authorization/index.ts @@ -39,6 +39,12 @@ const updateVerbs: Verbs = { past: 'updated', }; +const pushVerbs: Verbs = { + present: 'push', + progressive: 'pushing', + past: 'pushed', +}; + const deleteVerbs: Verbs = { present: 'delete', progressive: 'deleting', @@ -164,7 +170,7 @@ const CaseOperations = { ecsType: EVENT_TYPES.change, name: WriteOperations.PushCase as const, action: 'case_push', - verbs: updateVerbs, + verbs: pushVerbs, docType: 'case', savedObjectType: CASE_SAVED_OBJECT, }, diff --git a/x-pack/plugins/cases/server/routes/api/configure/get_connectors.ts b/x-pack/plugins/cases/server/routes/api/configure/get_connectors.ts index 4c28b896bd855..5929e39a2dd32 100644 --- a/x-pack/plugins/cases/server/routes/api/configure/get_connectors.ts +++ b/x-pack/plugins/cases/server/routes/api/configure/get_connectors.ts @@ -15,6 +15,9 @@ import { createCasesRoute } from '../create_cases_route'; export const getConnectorsRoute = createCasesRoute({ method: 'get', path: `${CASE_CONFIGURE_CONNECTORS_URL}/_find`, + routerOptions: { + tags: ['access:casesGetConnectorsConfigure'], + }, handler: async ({ context, response }) => { try { const caseContext = await context.cases; diff --git a/x-pack/plugins/cases/server/routes/api/types.ts b/x-pack/plugins/cases/server/routes/api/types.ts index 3dafad71b3cd8..a24e170ccaa16 100644 --- a/x-pack/plugins/cases/server/routes/api/types.ts +++ b/x-pack/plugins/cases/server/routes/api/types.ts @@ -40,7 +40,7 @@ interface CaseRouteHandlerArguments { kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; } -type CaseRouteTags = 'access:casesSuggestUserProfiles'; +type CaseRouteTags = 'access:casesSuggestUserProfiles' | 'access:casesGetConnectorsConfigure'; export interface CaseRoute

{ method: 'get' | 'post' | 'put' | 'delete' | 'patch'; diff --git a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/header/add_to_case_action.test.tsx b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/header/add_to_case_action.test.tsx index 71f7cfec8e5d2..a4d8a88507e82 100644 --- a/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/header/add_to_case_action.test.tsx +++ b/x-pack/plugins/exploratory_view/public/components/shared/exploratory_view/header/add_to_case_action.test.tsx @@ -110,6 +110,7 @@ describe('AddToCaseAction', function () { update: false, delete: false, push: false, + connectors: false, }, }) ); diff --git a/x-pack/plugins/observability/public/hooks/use_get_user_cases_permissions.tsx b/x-pack/plugins/observability/public/hooks/use_get_user_cases_permissions.tsx index d2c47da425a01..ea80fc8f8cc1c 100644 --- a/x-pack/plugins/observability/public/hooks/use_get_user_cases_permissions.tsx +++ b/x-pack/plugins/observability/public/hooks/use_get_user_cases_permissions.tsx @@ -18,6 +18,7 @@ export function useGetUserCasesPermissions() { update: false, delete: false, push: false, + connectors: false, }); const uiCapabilities = useKibana().services.application.capabilities; @@ -33,6 +34,7 @@ export function useGetUserCasesPermissions() { update: casesCapabilities.update, delete: casesCapabilities.delete, push: casesCapabilities.push, + connectors: casesCapabilities.connectors, }); }, [ casesCapabilities.all, @@ -41,6 +43,7 @@ export function useGetUserCasesPermissions() { casesCapabilities.update, casesCapabilities.delete, casesCapabilities.push, + casesCapabilities.connectors, ]); return casesPermissions; diff --git a/x-pack/plugins/observability/public/pages/cases/components/cases.stories.tsx b/x-pack/plugins/observability/public/pages/cases/components/cases.stories.tsx index ab490670f63ee..d0fc1d01734f2 100644 --- a/x-pack/plugins/observability/public/pages/cases/components/cases.stories.tsx +++ b/x-pack/plugins/observability/public/pages/cases/components/cases.stories.tsx @@ -19,7 +19,15 @@ export default { const Template: ComponentStory = (props: CasesProps) => ; const defaultProps: CasesProps = { - permissions: { read: true, all: true, create: true, delete: true, push: true, update: true }, + permissions: { + read: true, + all: true, + create: true, + delete: true, + push: true, + update: true, + connectors: true, + }, }; export const CasesPageWithAllPermissions = Template.bind({}); @@ -34,5 +42,6 @@ CasesPageWithNoPermissions.args = { delete: false, push: false, update: false, + connectors: false, }, }; diff --git a/x-pack/plugins/observability_shared/public/hooks/use_get_user_cases_permissions.tsx b/x-pack/plugins/observability_shared/public/hooks/use_get_user_cases_permissions.tsx index 4151655c5a2bd..21c6a08815b76 100644 --- a/x-pack/plugins/observability_shared/public/hooks/use_get_user_cases_permissions.tsx +++ b/x-pack/plugins/observability_shared/public/hooks/use_get_user_cases_permissions.tsx @@ -19,6 +19,7 @@ export function useGetUserCasesPermissions() { update: false, delete: false, push: false, + connectors: false, }); const uiCapabilities = useKibana().services.application!.capabilities; @@ -35,6 +36,7 @@ export function useGetUserCasesPermissions() { update: casesCapabilities.update, delete: casesCapabilities.delete, push: casesCapabilities.push, + connectors: casesCapabilities.connectors, }); }, [ casesCapabilities.all, @@ -43,6 +45,7 @@ export function useGetUserCasesPermissions() { casesCapabilities.update, casesCapabilities.delete, casesCapabilities.push, + casesCapabilities.connectors, ]); return casesPermissions; diff --git a/x-pack/plugins/observability_shared/public/utils/cases_permissions.ts b/x-pack/plugins/observability_shared/public/utils/cases_permissions.ts index 2b3ff9cfbaf54..a0b6a8aed95b0 100644 --- a/x-pack/plugins/observability_shared/public/utils/cases_permissions.ts +++ b/x-pack/plugins/observability_shared/public/utils/cases_permissions.ts @@ -12,4 +12,5 @@ export const noCasesPermissions = () => ({ update: false, delete: false, push: false, + connectors: false, }); diff --git a/x-pack/plugins/security_solution/public/cases_test_utils.ts b/x-pack/plugins/security_solution/public/cases_test_utils.ts index 8dd64424e41e5..d177934cb02ee 100644 --- a/x-pack/plugins/security_solution/public/cases_test_utils.ts +++ b/x-pack/plugins/security_solution/public/cases_test_utils.ts @@ -11,6 +11,7 @@ export const noCasesCapabilities = () => ({ update_cases: false, delete_cases: false, push_cases: false, + cases_connector: false, }); export const readCasesCapabilities = () => ({ @@ -19,6 +20,7 @@ export const readCasesCapabilities = () => ({ update_cases: false, delete_cases: false, push_cases: false, + cases_connector: true, }); export const allCasesCapabilities = () => ({ @@ -27,6 +29,7 @@ export const allCasesCapabilities = () => ({ update_cases: true, delete_cases: true, push_cases: true, + cases_connector: true, }); export const noCasesPermissions = () => ({ @@ -36,6 +39,7 @@ export const noCasesPermissions = () => ({ update: false, delete: false, push: false, + connectors: false, }); export const readCasesPermissions = () => ({ @@ -45,6 +49,7 @@ export const readCasesPermissions = () => ({ update: false, delete: false, push: false, + connectors: true, }); export const writeCasesPermissions = () => ({ @@ -54,6 +59,7 @@ export const writeCasesPermissions = () => ({ update: true, delete: true, push: true, + connectors: true, }); export const allCasesPermissions = () => ({ @@ -63,4 +69,5 @@ export const allCasesPermissions = () => ({ update: true, delete: true, push: true, + connectors: true, }); diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts index 1713f35a9a2d2..043d1a0ab36f0 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts @@ -161,6 +161,7 @@ export const useGetUserCasesPermissions = () => { update: false, delete: false, push: false, + connectors: false, }); const uiCapabilities = useKibana().services.application.capabilities; const casesCapabilities = useKibana().services.cases.helpers.getUICapabilities( @@ -175,6 +176,7 @@ export const useGetUserCasesPermissions = () => { update: casesCapabilities.update, delete: casesCapabilities.delete, push: casesCapabilities.push, + connectors: casesCapabilities.connectors, }); }, [ casesCapabilities.all, @@ -183,6 +185,7 @@ export const useGetUserCasesPermissions = () => { casesCapabilities.update, casesCapabilities.delete, casesCapabilities.push, + casesCapabilities.connectors, ]); return casesPermissions; diff --git a/x-pack/plugins/security_solution/server/lib/app_features/security_cases_kibana_features.ts b/x-pack/plugins/security_solution/server/lib/app_features/security_cases_kibana_features.ts index 5384e68c5945f..a2bf02c59b306 100644 --- a/x-pack/plugins/security_solution/server/lib/app_features/security_cases_kibana_features.ts +++ b/x-pack/plugins/security_solution/server/lib/app_features/security_cases_kibana_features.ts @@ -13,6 +13,10 @@ import { createUICapabilities as createCasesUICapabilities, getApiTags as getCasesApiTags, } from '@kbn/cases-plugin/common'; +import { + CASES_CONNECTORS_CAPABILITY, + GET_CONNECTORS_CONFIGURE_API_TAG, +} from '@kbn/cases-plugin/common/constants'; import type { AppFeaturesCasesConfig, BaseKibanaFeatureConfig } from './types'; import { APP_ID, CASES_FEATURE_ID } from '../../../common/constants'; import { CasesSubFeatureId } from './security_cases_kibana_sub_features'; @@ -21,48 +25,66 @@ import { AppFeatureCasesKey } from '../../../common/types/app_features'; const casesCapabilities = createCasesUICapabilities(); const casesApiTags = getCasesApiTags(APP_ID); -export const getCasesBaseKibanaFeature = (): BaseKibanaFeatureConfig => ({ - id: CASES_FEATURE_ID, - name: i18n.translate('xpack.securitySolution.featureRegistry.linkSecuritySolutionCaseTitle', { - defaultMessage: 'Cases', - }), - order: 1100, - category: DEFAULT_APP_CATEGORIES.security, - app: [CASES_FEATURE_ID, 'kibana'], - catalogue: [APP_ID], - cases: [APP_ID], - privileges: { - all: { - api: casesApiTags.all, - app: [CASES_FEATURE_ID, 'kibana'], - catalogue: [APP_ID], - cases: { - create: [APP_ID], - read: [APP_ID], - update: [APP_ID], - push: [APP_ID], - }, - savedObject: { - all: [...filesSavedObjectTypes], - read: [...filesSavedObjectTypes], - }, - ui: casesCapabilities.all, - }, - read: { - api: casesApiTags.read, - app: [CASES_FEATURE_ID, 'kibana'], - catalogue: [APP_ID], - cases: { - read: [APP_ID], +export const getCasesBaseKibanaFeature = (): BaseKibanaFeatureConfig => { + // On SecuritySolution essentials cases does not have the connector feature + const casesAllUICapabilities = casesCapabilities.all.filter( + (capability) => capability !== CASES_CONNECTORS_CAPABILITY + ); + + const casesReadUICapabilities = casesCapabilities.read.filter( + (capability) => capability !== CASES_CONNECTORS_CAPABILITY + ); + + const casesAllAPICapabilities = casesApiTags.all.filter( + (capability) => capability !== GET_CONNECTORS_CONFIGURE_API_TAG + ); + + const casesReadAPICapabilities = casesApiTags.read.filter( + (capability) => capability !== GET_CONNECTORS_CONFIGURE_API_TAG + ); + + return { + id: CASES_FEATURE_ID, + name: i18n.translate('xpack.securitySolution.featureRegistry.linkSecuritySolutionCaseTitle', { + defaultMessage: 'Cases', + }), + order: 1100, + category: DEFAULT_APP_CATEGORIES.security, + app: [CASES_FEATURE_ID, 'kibana'], + catalogue: [APP_ID], + cases: [APP_ID], + privileges: { + all: { + api: casesAllAPICapabilities, + app: [CASES_FEATURE_ID, 'kibana'], + catalogue: [APP_ID], + cases: { + create: [APP_ID], + read: [APP_ID], + update: [APP_ID], + }, + savedObject: { + all: [...filesSavedObjectTypes], + read: [...filesSavedObjectTypes], + }, + ui: casesAllUICapabilities, }, - savedObject: { - all: [], - read: [...filesSavedObjectTypes], + read: { + api: casesReadAPICapabilities, + app: [CASES_FEATURE_ID, 'kibana'], + catalogue: [APP_ID], + cases: { + read: [APP_ID], + }, + savedObject: { + all: [], + read: [...filesSavedObjectTypes], + }, + ui: casesReadUICapabilities, }, - ui: casesCapabilities.read, }, - }, -}); + }; +}; export const getCasesBaseKibanaSubFeatureIds = (): CasesSubFeatureId[] => [ CasesSubFeatureId.deleteCases, @@ -79,6 +101,18 @@ export const getCasesBaseKibanaSubFeatureIds = (): CasesSubFeatureId[] => [ */ export const getCasesAppFeaturesConfig = (): AppFeaturesCasesConfig => ({ [AppFeatureCasesKey.casesConnectors]: { - // TODO: Add cases connector configuration privileges + privileges: { + all: { + api: [GET_CONNECTORS_CONFIGURE_API_TAG], // Add cases connector get connectors API privileges + ui: [CASES_CONNECTORS_CAPABILITY], // Add cases connector UI privileges + cases: { + push: [APP_ID], // Add cases connector push privileges + }, + }, + read: { + api: [GET_CONNECTORS_CONFIGURE_API_TAG], // Add cases connector get connectors API privileges + ui: [CASES_CONNECTORS_CAPABILITY], // Add cases connector UI privileges + }, + }, }, }); diff --git a/x-pack/test/cases_api_integration/common/lib/authentication/roles.ts b/x-pack/test/cases_api_integration/common/lib/authentication/roles.ts index 49ce36a740ac8..85c33bc9517f3 100644 --- a/x-pack/test/cases_api_integration/common/lib/authentication/roles.ts +++ b/x-pack/test/cases_api_integration/common/lib/authentication/roles.ts @@ -44,6 +44,30 @@ export const noCasesPrivilegesSpace1: Role = { }, }; +export const noCasesConnectors: Role = { + name: 'no_cases_connectors', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + testNoCasesConnectorFixture: ['all'], + actions: ['all'], + actionsSimulators: ['all'], + }, + spaces: ['*'], + }, + ], + }, +}; + export const globalRead: Role = { name: 'global_read', privileges: { @@ -353,6 +377,7 @@ export const securitySolutionOnlyAllSpacesRole: Role = { export const roles = [ noKibanaPrivileges, noCasesPrivilegesSpace1, + noCasesConnectors, globalRead, securitySolutionOnlyAll, securitySolutionOnlyRead, diff --git a/x-pack/test/cases_api_integration/common/lib/authentication/users.ts b/x-pack/test/cases_api_integration/common/lib/authentication/users.ts index 8a3d5ddb8d30b..a4b7828d74b9e 100644 --- a/x-pack/test/cases_api_integration/common/lib/authentication/users.ts +++ b/x-pack/test/cases_api_integration/common/lib/authentication/users.ts @@ -21,6 +21,7 @@ import { securitySolutionOnlyReadAlerts, securitySolutionOnlyReadNoIndexAlerts, securitySolutionOnlyReadDelete, + noCasesConnectors as noCasesConnectorRole, } from './roles'; import { User } from './types'; @@ -126,6 +127,12 @@ export const noCasesPrivilegesSpace1: User = { roles: [noCasesPrivilegesSpace1Role.name], }; +export const noCasesConnectors: User = { + username: 'no_cases_connectors', + password: 'no_cases_connectors', + roles: [noCasesConnectorRole.name], +}; + /** * These users will have access to all spaces. */ @@ -154,4 +161,5 @@ export const users = [ noKibanaPrivileges, noCasesPrivilegesSpace1, testDisabled, + noCasesConnectors, ]; diff --git a/x-pack/test/cases_api_integration/common/plugins/cases/server/plugin.ts b/x-pack/test/cases_api_integration/common/plugins/cases/server/plugin.ts index 90ef74625119e..b2ac0602774d5 100644 --- a/x-pack/test/cases_api_integration/common/plugins/cases/server/plugin.ts +++ b/x-pack/test/cases_api_integration/common/plugins/cases/server/plugin.ts @@ -40,6 +40,45 @@ export class FixturePlugin implements Plugin { expect(theCase.status).to.eql('open'); }); + + it('should return 403 when the user does not have access to push', async () => { + const { postedCase } = await createCaseWithConnector({ + supertest, + serviceNowSimulatorURL, + actionsRemover, + configureReq: { owner: 'testNoCasesConnectorFixture' }, + createCaseReq: { ...getPostCaseRequest(), owner: 'testNoCasesConnectorFixture' }, + }); + + await pushCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + connectorId: postedCase.connector.id, + expectedHttpCode: 403, + auth: { user: noCasesConnectors, space: null }, + }); + }); }); }); }); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts index e75fed4f399b7..d124047831e28 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; import { @@ -20,10 +20,12 @@ import { getCaseConnectors, getCasesWebhookConnector, } from '../../../../common/lib/api'; +import { noCasesConnectors } from '../../../../common/lib/authentication/users'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const actionsRemover = new ActionsRemover(supertest); describe('get_connectors', () => { @@ -184,5 +186,13 @@ export default ({ getService }: FtrProviderContext): void => { }, ]); }); + + it('should return 403 when the user does not have access to the case connectors', async () => { + await getCaseConnectors({ + supertest: supertestWithoutAuth, + auth: { user: noCasesConnectors, space: null }, + expectedHttpCode: 403, + }); + }); }); }; diff --git a/x-pack/test/functional_with_es_ssl/plugins/cases/public/application.tsx b/x-pack/test/functional_with_es_ssl/plugins/cases/public/application.tsx index ad28e229e23a6..afc7860303db5 100644 --- a/x-pack/test/functional_with_es_ssl/plugins/cases/public/application.tsx +++ b/x-pack/test/functional_with_es_ssl/plugins/cases/public/application.tsx @@ -42,6 +42,7 @@ const permissions = { update: true, delete: true, push: true, + connectors: true, }; const attachments = [{ type: AttachmentType.user as const, comment: 'test' }];