From 8b4717505e77b3b5bf06a12300976fc8de575e15 Mon Sep 17 00:00:00 2001 From: Pablo Neves Machado Date: Wed, 19 Jul 2023 14:22:36 +0200 Subject: [PATCH] Add PLI authorisation for Cases Connectors --- .../plugins/cases/common/constants/index.ts | 1 + x-pack/plugins/cases/common/ui/types.ts | 4 +- .../cases/common/utils/capabilities.ts | 4 +- .../public/client/helpers/can_use_cases.ts | 5 +- .../client/helpers/capabilities.test.ts | 8 ++ .../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 | 24 +++++ .../components/configure_cases/connectors.tsx | 5 +- .../components/create/connector.test.tsx | 26 +++++ .../public/components/create/connector.tsx | 5 +- .../components/edit_connector/index.test.tsx | 53 ++++++++++ .../components/edit_connector/index.tsx | 16 +-- .../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 | 98 +++++++++++-------- .../plugins/cases/public/application.tsx | 1 + 23 files changed, 253 insertions(+), 56 deletions(-) diff --git a/x-pack/plugins/cases/common/constants/index.ts b/x-pack/plugins/cases/common/constants/index.ts index dcaf2bb0e646087..132046a82c26640 100644 --- a/x-pack/plugins/cases/common/constants/index.ts +++ b/x-pack/plugins/cases/common/constants/index.ts @@ -157,6 +157,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_CONNECTOR_CAPABILITY = 'cases_connector' as const; /** * Cases API Tags diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index e0cadcc12b6d001..4062fffd9712636 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -28,7 +28,7 @@ import type { CommentResponseTypePersistableState, GetCaseUsersResponse, } from '../api'; -import type { PUSH_CASES_CAPABILITY } from '../constants'; +import type { CASES_CONNECTOR_CAPABILITY, PUSH_CASES_CAPABILITY } from '../constants'; import type { SnakeToCamelCase } from '../types'; import type { ActionConnector, UserAction } from '../types/domain'; import type { @@ -287,6 +287,7 @@ export interface CasesPermissions { update: boolean; delete: boolean; push: boolean; + connectors: boolean; } export interface CasesCapabilities { @@ -295,4 +296,5 @@ export interface CasesCapabilities { [UPDATE_CASES_CAPABILITY]: boolean; [DELETE_CASES_CAPABILITY]: boolean; [PUSH_CASES_CAPABILITY]: boolean; + [CASES_CONNECTOR_CAPABILITY]: boolean; } diff --git a/x-pack/plugins/cases/common/utils/capabilities.ts b/x-pack/plugins/cases/common/utils/capabilities.ts index a508d112019660b..a041490f6613617 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_CONNECTOR_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_CONNECTOR_CAPABILITY, ] as const, - read: [READ_CASES_CAPABILITY] as const, + read: [READ_CASES_CAPABILITY, CASES_CONNECTOR_CAPABILITY] as const, delete: [DELETE_CASES_CAPABILITY] as const, }); 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 34af1c3865da556..1cc22c07997026a 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 58d6d61e8032411..48e6cddd44274da 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_connector: 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,12 @@ describe('getUICapabilities', () => { update_cases: true, delete_cases: true, push_cases: true, + cases_connector: true, }) ).toMatchInlineSnapshot(` Object { "all": false, + "connectors": true, "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 f09ac844489520d..f996bed062df359 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_CONNECTOR_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_CONNECTOR_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 812840b1553e3e5..c540824b1ebb5b5 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 eea1561076ae460..1c4f7911340cdb2 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 @@ -76,6 +76,7 @@ export const createStartServicesMock = ({ license }: StartServiceArgs = {}): Sta update_cases: true, delete_cases: true, push_cases: true, + cases_connector: 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 01d1dc64952ed32..8ba70e3e03a6e55 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 }); @@ -26,6 +40,7 @@ export const buildCasesPermissions = (overrides: Partial update_cases: false, delete_cases: false, push_cases: false, + cases_connector: false, }); export const readCasesCapabilities = () => buildCasesCapabilities({ @@ -53,6 +70,7 @@ export const readCasesCapabilities = () => update_cases: false, delete_cases: false, push_cases: false, + cases_connector: true, }); export const writeCasesCapabilities = () => { return 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_connector: overrides?.cases_connector ?? 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 c77340b4f37acce..0fcd72e2735a2c0 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 @@ -18,6 +18,16 @@ import { ConnectorsDropdown } from './connectors_dropdown'; import { connectors, actionTypes } from './__mock__'; import { ConnectorTypes } from '../../../common/types/domain'; +const mockUseCasesContext = jest.fn().mockReturnValue({ + permissions: { + connectors: true, + }, +}); + +jest.mock('../cases_context/use_cases_context', () => ({ + useCasesContext: () => mockUseCasesContext(), +})); + describe('Connectors', () => { let wrapper: ReactWrapper; let appMockRender: AppMockRenderer; @@ -161,4 +171,18 @@ 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 () => { + mockUseCasesContext.mockReturnValue({ + permissions: { + connectors: false, + }, + }); + + 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 0b51323f3ffd83b..3a1a51ccc2b4067 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 hasReadPermissions = permissions.connectors && actions.read; const connectorsName = connector?.name ?? 'none'; @@ -105,7 +108,7 @@ const ConnectorsComponent: React.FC = ({ > - {actions.read ? ( + {hasReadPermissions ? ( ({ + useCasesContext: () => mockUseCasesContext(), +})); + const useGetIncidentTypesMock = useGetIncidentTypes as jest.Mock; const useGetSeverityMock = useGetSeverity as jest.Mock; const useGetChoicesMock = useGetChoices as jest.Mock; @@ -190,4 +200,20 @@ describe('Connector', () => { 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 () => { + mockUseCasesContext.mockReturnValue({ + permissions: { + connectors: false, + }, + }); + + 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 58bf659b68cf59b..7422e671fa4bbd0 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 21ff3af65a3362c..aff69577e0d4cd3 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 @@ -17,6 +17,7 @@ import { readCasesPermissions, noPushCasesPermissions, TestProviders, + allCasesPermissions, } from '../../common/mock'; import { basicCase, connectorsMock } from '../../containers/mock'; import { getCaseConnectorsMockResponse } from '../../common/mock/connectors'; @@ -32,6 +33,8 @@ const defaultProps: EditConnectorProps = { onSubmit, }; +const noConnectorsPermission = { ...allCasesPermissions(), connectors: false }; + describe('EditConnector ', () => { let appMockRender: AppMockRenderer; @@ -274,6 +277,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: noConnectorsPermission }); + + 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 +299,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: noConnectorsPermission }); + + 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 +318,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: noConnectorsPermission }); + + 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 +347,15 @@ describe('EditConnector ', () => { }); }); + it('does not show the push button if the user does not have access to cases actions', async () => { + appMockRender = createAppMockRenderer({ permissions: noConnectorsPermission }); + + 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 +371,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: noConnectorsPermission, + }); + + 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 ee819af75b4001d..cf2d9b6dcc5381c 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 hasReadPermissions = 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 && hasReadPermissions ? ( - {!isLoading && !isEdit && hasErrorMessages && hasActionsReadPermissions && ( + {!isLoading && !isEdit && hasErrorMessages && hasReadPermissions && ( )} - {!hasActionsReadPermissions && ( + {!hasReadPermissions && ( {i18n.READ_ACTIONS_PERMISSIONS_ERROR_MSG} )} - {hasActionsReadPermissions && !isEdit && ( + {hasReadPermissions && !isEdit && ( )} - {hasActionsReadPermissions && isEdit && ( + {hasReadPermissions && isEdit && ( = (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 4151655c5a2bd48..21c6a08815b7627 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 2b3ff9cfbaf54a1..a0b6a8aed95b040 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 8dd64424e41e5ad..d177934cb02ee43 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 4a736fc622055d7..73b6c54b223f8c1 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 @@ -155,6 +155,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( @@ -169,6 +170,7 @@ export const useGetUserCasesPermissions = () => { update: casesCapabilities.update, delete: casesCapabilities.delete, push: casesCapabilities.push, + connectors: casesCapabilities.connectors, }); }, [ casesCapabilities.all, @@ -177,6 +179,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 5384e68c5945f28..0775ac12f6ab2db 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,7 @@ import { createUICapabilities as createCasesUICapabilities, getApiTags as getCasesApiTags, } from '@kbn/cases-plugin/common'; +import { CASES_CONNECTOR_CAPABILITY } 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 +22,58 @@ 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_CONNECTOR_CAPABILITY + ); + const casesReadUICapabilities = casesCapabilities.read.filter( + (capability) => capability !== CASES_CONNECTOR_CAPABILITY + ); + + 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: 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: casesAllUICapabilities, }, - savedObject: { - all: [], - read: [...filesSavedObjectTypes], + read: { + api: casesApiTags.read, + 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 +90,13 @@ export const getCasesBaseKibanaSubFeatureIds = (): CasesSubFeatureId[] => [ */ export const getCasesAppFeaturesConfig = (): AppFeaturesCasesConfig => ({ [AppFeatureCasesKey.casesConnectors]: { - // TODO: Add cases connector configuration privileges + privileges: { + all: { + ui: [CASES_CONNECTOR_CAPABILITY], + }, + read: { + ui: [CASES_CONNECTOR_CAPABILITY], + }, + }, }, }); 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 422a74a29bab704..3843dc467c805d7 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: CommentType.user as const, comment: 'test' }];