[Cases] Add new sub feature privilege to prevent access to the cases settings page (#170635)

This commit is contained in:
Christos Nasikas 2023-11-28 12:24:45 +02:00 committed by GitHub
parent fcdd44ffeb
commit 56887ac1f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
89 changed files with 758 additions and 502 deletions

View file

@ -99,6 +99,7 @@ export enum SecuritySubFeatureId {
/** Sub-features IDs for Cases */
export enum CasesSubFeatureId {
deleteCases = 'deleteCasesSubFeature',
casesSettings = 'casesSettingsSubFeature',
}
/** Sub-features IDs for Security Assistant */

View file

@ -17,6 +17,7 @@ import type { CasesFeatureParams } from './types';
*/
export const getCasesBaseKibanaSubFeatureIds = (): CasesSubFeatureId[] => [
CasesSubFeatureId.deleteCases,
CasesSubFeatureId.casesSettings,
];
/**
@ -60,7 +61,42 @@ export const getCasesSubFeaturesMap = ({
],
};
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,
},
],
},
],
};
return new Map<CasesSubFeatureId, SubFeatureConfig>([
[CasesSubFeatureId.deleteCases, deleteCasesSubFeature],
[CasesSubFeatureId.casesSettings, casesSettingsCasesSubFeature],
]);
};

View file

@ -162,6 +162,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_SETTINGS_CAPABILITY = 'cases_settings' as const;
export const CASES_CONNECTORS_CAPABILITY = 'cases_connectors' as const;
/**

View file

@ -29,6 +29,7 @@ export type {
Ecs,
CaseViewRefreshPropInterface,
CasesPermissions,
CasesCapabilities,
CasesStatus,
} from './ui/types';
@ -52,6 +53,7 @@ export {
CASE_COMMENT_SAVED_OBJECT,
CASES_CONNECTORS_CAPABILITY,
GET_CONNECTORS_CONFIGURE_API_TAG,
CASES_SETTINGS_CAPABILITY,
} from './constants';
export type { AttachmentAttributes } from './types/domain';

View file

@ -12,7 +12,11 @@ import type {
READ_CASES_CAPABILITY,
UPDATE_CASES_CAPABILITY,
} from '..';
import type { CASES_CONNECTORS_CAPABILITY, PUSH_CASES_CAPABILITY } from '../constants';
import type {
CASES_CONNECTORS_CAPABILITY,
CASES_SETTINGS_CAPABILITY,
PUSH_CASES_CAPABILITY,
} from '../constants';
import type { SnakeToCamelCase } from '../types';
import type {
CaseSeverity,
@ -299,6 +303,7 @@ export interface CasesPermissions {
delete: boolean;
push: boolean;
connectors: boolean;
settings: boolean;
}
export interface CasesCapabilities {
@ -308,4 +313,5 @@ export interface CasesCapabilities {
[DELETE_CASES_CAPABILITY]: boolean;
[PUSH_CASES_CAPABILITY]: boolean;
[CASES_CONNECTORS_CAPABILITY]: boolean;
[CASES_SETTINGS_CAPABILITY]: boolean;
}

View file

@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { createUICapabilities } from './capabilities';
describe('createUICapabilities', () => {
it('returns the UI capabilities correctly', () => {
expect(createUICapabilities()).toMatchInlineSnapshot(`
Object {
"all": Array [
"create_cases",
"read_cases",
"update_cases",
"push_cases",
"cases_connectors",
],
"delete": Array [
"delete_cases",
],
"read": Array [
"read_cases",
"cases_connectors",
],
"settings": Array [
"cases_settings",
],
}
`);
});
});

View file

@ -12,12 +12,14 @@ import {
PUSH_CASES_CAPABILITY,
READ_CASES_CAPABILITY,
UPDATE_CASES_CAPABILITY,
CASES_SETTINGS_CAPABILITY,
} from '../constants';
export interface CasesUiCapabilities {
all: readonly string[];
read: readonly string[];
delete: readonly string[];
settings: readonly string[];
}
/**
* Return the UI capabilities for each type of operation. These strings must match the values defined in the UI
@ -33,4 +35,5 @@ export const createUICapabilities = (): CasesUiCapabilities => ({
] as const,
read: [READ_CASES_CAPABILITY, CASES_CONNECTORS_CAPABILITY] as const,
delete: [DELETE_CASES_CAPABILITY] as const,
settings: [CASES_SETTINGS_CAPABILITY] as const,
});

View file

@ -40,10 +40,19 @@ 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 && acc.connectors;
acc.all = acc.all || userCapabilitiesForOwner.all || allFromAcc;
acc.connectors = acc.connectors || userCapabilitiesForOwner.connectors;
acc.settings = acc.settings || userCapabilitiesForOwner.settings;
const allFromAcc =
acc.create &&
acc.read &&
acc.update &&
acc.delete &&
acc.push &&
acc.connectors &&
acc.settings;
acc.all = acc.all || userCapabilitiesForOwner.all || allFromAcc;
return acc;
},
@ -55,6 +64,7 @@ export const canUseCases =
delete: false,
push: false,
connectors: false,
settings: false,
}
);

View file

@ -17,6 +17,7 @@ describe('getUICapabilities', () => {
"delete": false,
"push": false,
"read": false,
"settings": false,
"update": false,
}
`);
@ -31,6 +32,7 @@ describe('getUICapabilities', () => {
"delete": false,
"push": false,
"read": false,
"settings": false,
"update": false,
}
`);
@ -45,6 +47,7 @@ describe('getUICapabilities', () => {
"delete": false,
"push": false,
"read": false,
"settings": false,
"update": false,
}
`);
@ -68,6 +71,7 @@ describe('getUICapabilities', () => {
"delete": false,
"push": false,
"read": false,
"settings": false,
"update": false,
}
`);
@ -82,6 +86,7 @@ describe('getUICapabilities', () => {
"delete": false,
"push": false,
"read": false,
"settings": false,
"update": false,
}
`);
@ -105,6 +110,7 @@ describe('getUICapabilities', () => {
"delete": true,
"push": true,
"read": true,
"settings": false,
"update": true,
}
`);
@ -113,23 +119,65 @@ describe('getUICapabilities', () => {
it('returns false for the all field when cases_connectors is false', () => {
expect(
getUICapabilities({
create_cases: false,
create_cases: true,
read_cases: true,
update_cases: true,
delete_cases: true,
push_cases: true,
cases_connectors: false,
cases_settings: true,
})
).toMatchInlineSnapshot(`
Object {
"all": false,
"connectors": false,
"create": false,
"create": true,
"delete": true,
"push": true,
"read": true,
"settings": true,
"update": true,
}
`);
});
it('returns false for the all field when cases_settings is false', () => {
expect(
getUICapabilities({
create_cases: true,
read_cases: true,
update_cases: true,
delete_cases: true,
push_cases: true,
cases_connectors: true,
cases_settings: false,
})
).toMatchInlineSnapshot(`
Object {
"all": false,
"connectors": true,
"create": true,
"delete": true,
"push": true,
"read": true,
"settings": false,
"update": true,
}
`);
});
it('returns true for cases_settings when it is set to true in the ui capabilities', () => {
expect(getUICapabilities({ cases_settings: true })).toMatchInlineSnapshot(`
Object {
"all": false,
"connectors": false,
"create": false,
"delete": false,
"push": false,
"read": false,
"settings": true,
"update": false,
}
`);
});
});

View file

@ -8,6 +8,7 @@
import type { CasesPermissions } from '../../../common';
import {
CASES_CONNECTORS_CAPABILITY,
CASES_SETTINGS_CAPABILITY,
CREATE_CASES_CAPABILITY,
DELETE_CASES_CAPABILITY,
PUSH_CASES_CAPABILITY,
@ -24,7 +25,9 @@ export const getUICapabilities = (
const deletePriv = !!featureCapabilities?.[DELETE_CASES_CAPABILITY];
const push = !!featureCapabilities?.[PUSH_CASES_CAPABILITY];
const connectors = !!featureCapabilities?.[CASES_CONNECTORS_CAPABILITY];
const all = create && read && update && deletePriv && push && connectors;
const settings = !!featureCapabilities?.[CASES_SETTINGS_CAPABILITY];
const all = create && read && update && deletePriv && push && connectors && settings;
return {
all,
@ -34,5 +37,6 @@ export const getUICapabilities = (
delete: deletePriv,
push,
connectors,
settings,
};
};

View file

@ -194,6 +194,7 @@ export const useApplicationCapabilities = (): UseApplicationCapabilities => {
delete: permissions.delete,
push: permissions.push,
connectors: permissions.connectors,
settings: permissions.settings,
},
visualize: { crud: !!capabilities.visualize?.save, read: !!capabilities.visualize?.show },
dashboard: {
@ -215,6 +216,7 @@ export const useApplicationCapabilities = (): UseApplicationCapabilities => {
permissions.delete,
permissions.push,
permissions.connectors,
permissions.settings,
]
);
};

View file

@ -75,6 +75,7 @@ export const createStartServicesMock = ({ license }: StartServiceArgs = {}): Sta
delete_cases: true,
push_cases: true,
cases_connectors: true,
cases_settings: true,
},
visualize: { save: true, show: true },
dashboard: { show: true, createNew: true },

View file

@ -16,7 +16,9 @@ export const noCasesPermissions = () =>
delete: false,
push: false,
connectors: false,
settings: false,
});
export const readCasesPermissions = () =>
buildCasesPermissions({
read: true,
@ -25,6 +27,7 @@ export const readCasesPermissions = () =>
delete: false,
push: false,
connectors: true,
settings: false,
});
export const noCreateCasesPermissions = () => buildCasesPermissions({ create: false });
export const noUpdateCasesPermissions = () => buildCasesPermissions({ update: false });
@ -34,6 +37,7 @@ 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 noCasesSettingsPermission = () => buildCasesPermissions({ settings: false });
export const buildCasesPermissions = (overrides: Partial<Omit<CasesPermissions, 'all'>> = {}) => {
const create = overrides.create ?? true;
@ -42,7 +46,8 @@ export const buildCasesPermissions = (overrides: Partial<Omit<CasesPermissions,
const deletePermissions = overrides.delete ?? true;
const push = overrides.push ?? true;
const connectors = overrides.connectors ?? true;
const all = create && read && update && deletePermissions && push;
const settings = overrides.settings ?? true;
const all = create && read && update && deletePermissions && push && settings && connectors;
return {
all,
@ -52,6 +57,7 @@ export const buildCasesPermissions = (overrides: Partial<Omit<CasesPermissions,
delete: deletePermissions,
push,
connectors,
settings,
};
};
@ -64,6 +70,7 @@ export const noCasesCapabilities = () =>
delete_cases: false,
push_cases: false,
cases_connectors: false,
cases_settings: false,
});
export const readCasesCapabilities = () =>
buildCasesCapabilities({
@ -71,6 +78,7 @@ export const readCasesCapabilities = () =>
update_cases: false,
delete_cases: false,
push_cases: false,
cases_settings: false,
});
export const writeCasesCapabilities = () => {
return buildCasesCapabilities({
@ -86,5 +94,6 @@ export const buildCasesCapabilities = (overrides?: Partial<CasesCapabilities>) =
delete_cases: overrides?.delete_cases ?? true,
push_cases: overrides?.push_cases ?? true,
cases_connectors: overrides?.cases_connectors ?? true,
cases_settings: overrides?.cases_settings ?? true,
};
};

View file

@ -46,9 +46,9 @@ describe('CasesTableHeader', () => {
expect(result.getByTestId('configure-case-button')).toBeInTheDocument();
});
it('does not display the configure button when the user does not have update privileges', () => {
it('does not display the configure button when the user does not have settings privileges', () => {
appMockRender = createAppMockRenderer({
permissions: buildCasesPermissions({ update: false }),
permissions: buildCasesPermissions({ settings: false }),
});
const result = appMockRender.render(<CasesTableHeader actionsErrors={[]} />);

View file

@ -0,0 +1,55 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { screen } from '@testing-library/react';
import type { AppMockRenderer } from '../../common/mock';
import {
createAppMockRenderer,
noCasesSettingsPermission,
noCreateCasesPermissions,
buildCasesPermissions,
} from '../../common/mock';
import { NavButtons } from './nav_buttons';
describe('NavButtons', () => {
let appMockRenderer: AppMockRenderer;
beforeEach(() => {
appMockRenderer = createAppMockRenderer();
});
it('shows the configure case button', () => {
appMockRenderer.render(<NavButtons actionsErrors={[]} />);
expect(screen.getByTestId('configure-case-button')).toBeInTheDocument();
});
it('does not render the case create button with no create permissions', () => {
appMockRenderer = createAppMockRenderer({ permissions: noCreateCasesPermissions() });
appMockRenderer.render(<NavButtons actionsErrors={[]} />);
expect(screen.queryByTestId('createNewCaseBtn')).not.toBeInTheDocument();
});
it('does not render the case configure button with no settings permissions', () => {
appMockRenderer = createAppMockRenderer({ permissions: noCasesSettingsPermission() });
appMockRenderer.render(<NavButtons actionsErrors={[]} />);
expect(screen.queryByTestId('configure-case-button')).not.toBeInTheDocument();
});
it('does not render any button with no create and no settings permissions', () => {
appMockRenderer = createAppMockRenderer({
permissions: buildCasesPermissions({ create: false, settings: false }),
});
appMockRenderer.render(<NavButtons actionsErrors={[]} />);
expect(screen.queryByTestId('createNewCaseBtn')).not.toBeInTheDocument();
expect(screen.queryByTestId('configure-case-button')).not.toBeInTheDocument();
});
});

View file

@ -43,14 +43,14 @@ export const NavButtons: FunctionComponent<Props> = ({ actionsErrors }) => {
[navigateToCreateCase]
);
if (!permissions.create && !permissions.update) {
if (!permissions.create && !permissions.settings) {
return null;
}
return (
<EuiFlexItem>
<ButtonFlexGroup responsive={false}>
{permissions.update && (
{permissions.settings && (
<EuiFlexItem grow={false}>
<ConfigureCaseButton
label={i18n.CONFIGURE_CASES_BUTTON}

View file

@ -11,8 +11,8 @@ import type { MemoryRouterProps } from 'react-router';
import { render, screen, waitFor } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import {
noCasesSettingsPermission,
noCreateCasesPermissions,
noUpdateCasesPermissions,
readCasesPermissions,
TestProviders,
} from '../../common/mock';
@ -96,14 +96,14 @@ describe('Cases routes', () => {
});
});
describe('Configure cases', () => {
it('navigates to the configure cases page', () => {
describe('Cases settings', () => {
it('navigates to the cases settings page', () => {
renderWithRouter(['/cases/configure']);
expect(screen.getByText('Settings')).toBeInTheDocument();
});
it('shows the no privileges page if the user does not have update privileges', () => {
renderWithRouter(['/cases/configure'], noUpdateCasesPermissions());
it('shows the no privileges page if the user does not have settings privileges', () => {
renderWithRouter(['/cases/configure'], noCasesSettingsPermission());
expect(screen.getByText('Privileges required')).toBeInTheDocument();
});
});

View file

@ -71,7 +71,7 @@ const CasesRoutesComponent: React.FC<CasesRoutesProps> = ({
</Route>
<Route path={getCasesConfigurePath(basePath)}>
{permissions.update ? (
{permissions.settings ? (
<ConfigureCases />
) : (
<NoPrivilegesPage pageName={i18n.CONFIGURE_CASES_PAGE_NAME} />

View file

@ -6,14 +6,18 @@
*/
import React from 'react';
import { mount } from 'enzyme';
import { screen } from '@testing-library/react';
import type { CallOutProps } from './callout';
import { CallOut } from './callout';
import { CLOSED_CASE_PUSH_ERROR_ID } from './types';
import { TestProviders } from '../../../common/mock';
import type { AppMockRenderer } from '../../../common/mock';
import { noCasesSettingsPermission, createAppMockRenderer } from '../../../common/mock';
import userEvent from '@testing-library/user-event';
describe('Callout', () => {
let appMockRenderer: AppMockRenderer;
const handleButtonClick = jest.fn();
const defaultProps: CallOutProps = {
id: 'md5-hex',
@ -31,50 +35,19 @@ describe('Callout', () => {
beforeEach(() => {
jest.clearAllMocks();
appMockRenderer = createAppMockRenderer();
});
it('It renders the callout', () => {
const wrapper = mount(<CallOut {...defaultProps} />);
expect(wrapper.find(`[data-test-subj="case-callout-md5-hex"]`).exists()).toBeTruthy();
expect(wrapper.find(`[data-test-subj="callout-messages-md5-hex"]`).exists()).toBeTruthy();
expect(wrapper.find(`[data-test-subj="callout-onclick-md5-hex"]`).exists()).toBeTruthy();
appMockRenderer.render(<CallOut {...defaultProps} />);
expect(screen.getByTestId('case-callout-md5-hex')).toBeInTheDocument();
expect(screen.getByTestId('callout-messages-md5-hex')).toBeInTheDocument();
expect(screen.getByTestId('callout-onclick-md5-hex')).toBeInTheDocument();
});
it('does not shows any messages when the list is empty', () => {
const wrapper = mount(<CallOut {...defaultProps} messages={[]} />);
expect(wrapper.find(`[data-test-subj="callout-messages-md5-hex"]`).exists()).toBeFalsy();
});
it('transform the button color correctly - primary', () => {
const wrapper = mount(<CallOut {...defaultProps} />);
const className =
wrapper.find(`button[data-test-subj="callout-onclick-md5-hex"]`).first().prop('className') ??
'';
expect(className.includes('primary')).toBeTruthy();
});
it('transform the button color correctly - success', () => {
const wrapper = mount(<CallOut {...defaultProps} type={'success'} />);
const className =
wrapper.find(`button[data-test-subj="callout-onclick-md5-hex"]`).first().prop('className') ??
'';
expect(className.includes('success')).toBeTruthy();
});
it('transform the button color correctly - warning', () => {
const wrapper = mount(<CallOut {...defaultProps} type={'warning'} />);
const className =
wrapper.find(`button[data-test-subj="callout-onclick-md5-hex"]`).first().prop('className') ??
'';
expect(className.includes('warning')).toBeTruthy();
});
it('transform the button color correctly - danger', () => {
const wrapper = mount(<CallOut {...defaultProps} type={'danger'} />);
const className =
wrapper.find(`button[data-test-subj="callout-onclick-md5-hex"]`).first().prop('className') ??
'';
expect(className.includes('danger')).toBeTruthy();
appMockRenderer.render(<CallOut {...defaultProps} messages={[]} />);
expect(screen.queryByTestId('callout-messages-md5-hex')).not.toBeInTheDocument();
});
it('does not show the button when case is closed error is present', () => {
@ -89,15 +62,9 @@ describe('Callout', () => {
],
};
const wrapper = mount(
<TestProviders>
<CallOut {...props} />
</TestProviders>
);
appMockRenderer.render(<CallOut {...props} />);
expect(wrapper.find(`button[data-test-subj="callout-onclick-md5-hex"]`).exists()).toEqual(
false
);
expect(screen.queryByTestId('callout-onclick-md5-hex')).not.toBeInTheDocument();
});
it('does not show the button when license error is present', () => {
@ -106,22 +73,27 @@ describe('Callout', () => {
hasLicenseError: true,
};
const wrapper = mount(
<TestProviders>
<CallOut {...props} />
</TestProviders>
);
appMockRenderer.render(<CallOut {...props} />);
expect(wrapper.find(`button[data-test-subj="callout-onclick-md5-hex"]`).exists()).toEqual(
false
);
expect(screen.queryByTestId('callout-onclick-md5-hex')).not.toBeInTheDocument();
});
it('does not show the button with no settings permissions', () => {
appMockRenderer = createAppMockRenderer({ permissions: noCasesSettingsPermission() });
appMockRenderer.render(<CallOut {...defaultProps} />);
expect(screen.queryByTestId('callout-onclick-md5-hex')).not.toBeInTheDocument();
});
// use this for storage if we ever want to bring that back
it('onClick passes id and type', () => {
const wrapper = mount(<CallOut {...defaultProps} />);
expect(wrapper.find(`[data-test-subj="callout-onclick-md5-hex"]`).exists()).toBeTruthy();
wrapper.find(`button[data-test-subj="callout-onclick-md5-hex"]`).simulate('click');
appMockRenderer.render(<CallOut {...defaultProps} />);
expect(screen.getByTestId('callout-onclick-md5-hex')).toBeInTheDocument();
userEvent.click(screen.getByTestId('callout-onclick-md5-hex'));
expect(handleButtonClick.mock.calls[0][1]).toEqual('md5-hex');
expect(handleButtonClick.mock.calls[0][2]).toEqual('primary');
});

View file

@ -12,6 +12,7 @@ import React, { memo, useCallback, useMemo } from 'react';
import type { ErrorMessage } from './types';
import { CLOSED_CASE_PUSH_ERROR_ID } from './types';
import * as i18n from './translations';
import { useCasesContext } from '../../cases_context/use_cases_context';
export interface CallOutProps {
handleButtonClick: (
@ -32,6 +33,8 @@ const CallOutComponent = ({
type,
hasLicenseError,
}: CallOutProps) => {
const { permissions } = useCasesContext();
const handleCallOut = useCallback(
(e) => handleButtonClick(e, id, type),
[handleButtonClick, id, type]
@ -57,7 +60,7 @@ const CallOutComponent = ({
size="s"
>
<EuiDescriptionList data-test-subj={`callout-messages-${id}`} listItems={messages} />
{!isCaseClosed && !hasLicenseError && (
{!isCaseClosed && !hasLicenseError && permissions.settings && (
<EuiButton
data-test-subj={`callout-onclick-${id}`}
color={type === 'success' ? 'success' : type}

View file

@ -46,6 +46,8 @@ const helpersMock: jest.Mocked<CasesUiStart['helpers']> = {
update: false,
delete: false,
push: false,
connectors: false,
settings: false,
}),
getRuleIdFromEvent: jest.fn(),
groupAlertsByRule: jest.fn(),

View file

@ -100,6 +100,33 @@ export const getCasesKibanaFeature = (): KibanaFeatureConfig => {
},
],
},
{
name: i18n.translate('xpack.cases.features.casesSettingsSubFeatureName', {
defaultMessage: 'Case Settings',
}),
privilegeGroups: [
{
groupType: 'independent',
privileges: [
{
id: 'cases_settings',
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,
},
],
},
],
},
],
};
};

View file

@ -12,8 +12,6 @@ import { sampleAttribute } from '../../configurations/test_data/sample_attribute
import * as pluginHook from '../../../../../hooks/use_plugin_context';
import { TypedLensByValueInput } from '@kbn/lens-plugin/public';
import { ExpViewActionMenuContent } from './action_menu';
import { noCasesPermissions as mockUseGetCasesPermissions } from '@kbn/observability-shared-plugin/public';
import * as obsHooks from '@kbn/observability-shared-plugin/public/hooks/use_get_user_cases_permissions';
jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({
appMountParameters: {
@ -21,13 +19,6 @@ jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({
},
} as any);
jest.spyOn(obsHooks, 'useGetUserCasesPermissions').mockImplementation(
() =>
({
useGetUserCasesPermissions: jest.fn(() => mockUseGetCasesPermissions()),
} as any)
);
describe('Action Menu', function () {
afterAll(() => {
jest.clearAllMocks();

View file

@ -12,8 +12,6 @@ import { ExploratoryView } from './exploratory_view';
import * as obsvDataViews from '../../../utils/observability_data_views/observability_data_views';
import * as pluginHook from '../../../hooks/use_plugin_context';
import { createStubIndexPattern } from '@kbn/data-plugin/common/stubs';
import { noCasesPermissions as mockUseGetCasesPermissions } from '@kbn/observability-shared-plugin/public/utils/cases_permissions';
import * as obsHooks from '@kbn/observability-shared-plugin/public/hooks/use_get_user_cases_permissions';
jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({
appMountParameters: {
@ -21,13 +19,6 @@ jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({
},
} as any);
jest.spyOn(obsHooks, 'useGetUserCasesPermissions').mockImplementation(
() =>
({
useGetUserCasesPermissions: jest.fn(() => mockUseGetCasesPermissions()),
} as any)
);
describe('ExploratoryView', () => {
mockAppDataView();

View file

@ -13,10 +13,15 @@ import * as useCaseHook from '../hooks/use_add_to_case';
import * as datePicker from '../components/date_range_picker';
import moment from 'moment';
import { noCasesPermissions as mockUseGetCasesPermissions } from '@kbn/observability-shared-plugin/public';
import * as obsHooks from '@kbn/observability-shared-plugin/public/hooks/use_get_user_cases_permissions';
jest.spyOn(obsHooks, 'useGetUserCasesPermissions').mockReturnValue(mockUseGetCasesPermissions());
describe('AddToCaseAction', function () {
const coreRenderProps = {
cases: {
ui: { getAllCasesSelectorModal: jest.fn() },
helpers: { canUseCases: () => mockUseGetCasesPermissions() },
},
};
beforeEach(() => {
jest.spyOn(datePicker, 'parseRelativeDate').mockRestore();
});
@ -26,7 +31,8 @@ describe('AddToCaseAction', function () {
<AddToCaseAction
lensAttributes={{ title: 'Performance distribution' } as any}
timeRange={{ to: 'now', from: 'now-5m' }}
/>
/>,
{ core: coreRenderProps }
);
expect(await findByText('Add to case')).toBeInTheDocument();
});
@ -39,7 +45,8 @@ describe('AddToCaseAction', function () {
<AddToCaseAction
lensAttributes={{ title: 'Performance distribution' } as any}
timeRange={{ to: 'now', from: 'now-5m' }}
/>
/>,
{ core: coreRenderProps }
);
expect(await findByText('Add to case')).toBeInTheDocument();
@ -60,7 +67,8 @@ describe('AddToCaseAction', function () {
const useAddToCaseHook = jest.spyOn(useCaseHook, 'useAddToCase');
const { getByText } = render(
<AddToCaseAction lensAttributes={null} timeRange={{ to: '', from: '' }} owner="security" />
<AddToCaseAction lensAttributes={null} timeRange={{ to: '', from: '' }} owner="security" />,
{ core: coreRenderProps }
);
expect(await forNearestButton(getByText)('Add to case')).toBeDisabled();
@ -95,7 +103,7 @@ describe('AddToCaseAction', function () {
lensAttributes={{ title: 'Performance distribution' } as any}
timeRange={{ to: 'now', from: 'now-5m' }}
/>,
{ initSeries }
{ initSeries, core: coreRenderProps }
);
fireEvent.click(await findByText('Add to case'));
@ -111,6 +119,7 @@ describe('AddToCaseAction', function () {
delete: false,
push: false,
connectors: false,
settings: false,
},
})
);

View file

@ -16,7 +16,6 @@ import {
} from '@kbn/cases-plugin/public';
import { TypedLensByValueInput } from '@kbn/lens-plugin/public';
import { observabilityFeatureId } from '@kbn/observability-shared-plugin/public';
import { useGetUserCasesPermissions } from '@kbn/observability-shared-plugin/public';
import { ObservabilityAppServices } from '../../../../application/types';
import { useAddToCase } from '../hooks/use_add_to_case';
import { parseRelativeDate } from '../components/date_range_picker';
@ -37,7 +36,7 @@ export function AddToCaseAction({
timeRange,
}: AddToCaseProps) {
const kServices = useKibana<ObservabilityAppServices>().services;
const userCasesPermissions = useGetUserCasesPermissions();
const userCasesPermissions = kServices.cases.helpers.canUseCases([observabilityFeatureId]);
const {
cases,

View file

@ -204,6 +204,16 @@ export interface FeatureKibanaPrivileges {
* ```
*/
delete?: readonly string[];
/**
* List of case owners which users should have settings access to when granted this privilege.
* @example
* ```ts
* {
* settings: ['securitySolution']
* }
* ```
*/
settings?: readonly string[];
};
/**

View file

@ -558,6 +558,7 @@ Array [
"delete": Array [],
"push": Array [],
"read": Array [],
"settings": Array [],
"update": Array [],
},
"catalogue": Array [
@ -710,6 +711,7 @@ Array [
"delete": Array [],
"push": Array [],
"read": Array [],
"settings": Array [],
"update": Array [],
},
"catalogue": Array [
@ -1032,6 +1034,7 @@ Array [
"delete": Array [],
"push": Array [],
"read": Array [],
"settings": Array [],
"update": Array [],
},
"catalogue": Array [
@ -1169,6 +1172,7 @@ Array [
"delete": Array [],
"push": Array [],
"read": Array [],
"settings": Array [],
"update": Array [],
},
"catalogue": Array [
@ -1321,6 +1325,7 @@ Array [
"delete": Array [],
"push": Array [],
"read": Array [],
"settings": Array [],
"update": Array [],
},
"catalogue": Array [
@ -1643,6 +1648,7 @@ Array [
"delete": Array [],
"push": Array [],
"read": Array [],
"settings": Array [],
"update": Array [],
},
"catalogue": Array [

View file

@ -77,6 +77,7 @@ describe('featurePrivilegeIterator', () => {
update: ['cases-update-type'],
delete: ['cases-delete-type'],
push: ['cases-push-type'],
settings: ['cases-settings-type'],
},
ui: ['ui-action'],
},
@ -146,6 +147,7 @@ describe('featurePrivilegeIterator', () => {
update: ['cases-update-type'],
delete: ['cases-delete-type'],
push: ['cases-push-type'],
settings: ['cases-settings-type'],
},
ui: ['ui-action'],
},
@ -214,6 +216,7 @@ describe('featurePrivilegeIterator', () => {
update: ['cases-update-type'],
delete: ['cases-delete-type'],
push: ['cases-push-type'],
settings: ['cases-settings-type'],
},
ui: ['ui-action'],
},
@ -284,6 +287,7 @@ describe('featurePrivilegeIterator', () => {
update: ['cases-update-type'],
delete: ['cases-delete-type'],
push: ['cases-push-type'],
settings: ['cases-settings-type'],
},
ui: ['ui-action'],
},
@ -324,6 +328,7 @@ describe('featurePrivilegeIterator', () => {
update: ['cases-update-type'],
delete: ['cases-delete-type'],
push: ['cases-push-type'],
settings: ['cases-settings-type'],
},
ui: ['ui-action'],
},
@ -385,6 +390,7 @@ describe('featurePrivilegeIterator', () => {
update: ['cases-update-sub-type'],
delete: ['cases-delete-sub-type'],
push: ['cases-push-sub-type'],
settings: ['cases-settings-sub-type'],
},
ui: ['ui-sub-type'],
},
@ -431,6 +437,7 @@ describe('featurePrivilegeIterator', () => {
update: ['cases-update-type'],
delete: ['cases-delete-type'],
push: ['cases-push-type'],
settings: ['cases-settings-type'],
},
ui: ['ui-action'],
},
@ -498,6 +505,7 @@ describe('featurePrivilegeIterator', () => {
update: ['cases-update-type'],
delete: ['cases-delete-type'],
push: ['cases-push-type'],
settings: ['cases-settings-type'],
},
ui: ['ui-action'],
},
@ -559,6 +567,7 @@ describe('featurePrivilegeIterator', () => {
update: ['cases-update-sub-type'],
delete: ['cases-delete-sub-type'],
push: ['cases-push-sub-type'],
settings: ['cases-settings-sub-type'],
},
ui: ['ui-sub-type'],
},
@ -605,6 +614,7 @@ describe('featurePrivilegeIterator', () => {
update: ['cases-update-type'],
delete: ['cases-delete-type'],
push: ['cases-push-type'],
settings: ['cases-settings-type'],
},
ui: ['ui-action'],
},
@ -672,6 +682,7 @@ describe('featurePrivilegeIterator', () => {
update: ['cases-update-type'],
delete: ['cases-delete-type'],
push: ['cases-push-type'],
settings: ['cases-settings-type'],
},
ui: ['ui-action'],
},
@ -734,6 +745,7 @@ describe('featurePrivilegeIterator', () => {
update: ['cases-update-sub-type'],
delete: ['cases-delete-sub-type'],
push: ['cases-push-sub-type'],
settings: ['cases-settings-sub-type'],
},
ui: ['ui-sub-type'],
},
@ -783,6 +795,7 @@ describe('featurePrivilegeIterator', () => {
update: ['cases-update-type', 'cases-update-sub-type'],
delete: ['cases-delete-type', 'cases-delete-sub-type'],
push: ['cases-push-type', 'cases-push-sub-type'],
settings: ['cases-settings-type', 'cases-settings-sub-type'],
},
ui: ['ui-action', 'ui-sub-type'],
},
@ -818,6 +831,7 @@ describe('featurePrivilegeIterator', () => {
update: ['cases-update-sub-type'],
delete: ['cases-delete-sub-type'],
push: ['cases-push-sub-type'],
settings: ['cases-settings-sub-type'],
},
ui: ['ui-action', 'ui-sub-type'],
},
@ -860,6 +874,7 @@ describe('featurePrivilegeIterator', () => {
update: ['cases-update-type'],
delete: ['cases-delete-type'],
push: ['cases-push-type'],
settings: ['cases-settings-type'],
},
ui: ['ui-action'],
},
@ -964,6 +979,7 @@ describe('featurePrivilegeIterator', () => {
update: ['cases-update-type'],
delete: ['cases-delete-type'],
push: ['cases-push-type'],
settings: ['cases-settings-type'],
},
ui: ['ui-action'],
},
@ -998,6 +1014,7 @@ describe('featurePrivilegeIterator', () => {
update: [],
delete: [],
push: [],
settings: [],
},
ui: ['ui-action'],
},
@ -1038,6 +1055,7 @@ describe('featurePrivilegeIterator', () => {
update: ['cases-update-type'],
delete: ['cases-delete-type'],
push: ['cases-push-type'],
settings: ['cases-settings-type'],
},
ui: ['ui-action'],
},
@ -1100,6 +1118,7 @@ describe('featurePrivilegeIterator', () => {
update: ['cases-update-sub-type'],
delete: ['cases-delete-sub-type'],
push: ['cases-push-sub-type'],
settings: ['cases-settings-sub-type'],
},
ui: ['ui-sub-type'],
},
@ -1149,6 +1168,7 @@ describe('featurePrivilegeIterator', () => {
update: ['cases-update-type', 'cases-update-sub-type'],
delete: ['cases-delete-type', 'cases-delete-sub-type'],
push: ['cases-push-type', 'cases-push-sub-type'],
settings: ['cases-settings-type', 'cases-settings-sub-type'],
},
ui: ['ui-action', 'ui-sub-type'],
},
@ -1341,6 +1361,7 @@ describe('featurePrivilegeIterator', () => {
update: ['cases-update-sub-type'],
delete: ['cases-delete-sub-type'],
push: ['cases-push-sub-type'],
settings: ['cases-settings-sub-type'],
},
ui: ['ui-sub-type'],
},
@ -1390,6 +1411,7 @@ describe('featurePrivilegeIterator', () => {
update: ['cases-update-sub-type'],
delete: ['cases-delete-sub-type'],
push: ['cases-push-sub-type'],
settings: ['cases-settings-sub-type'],
},
ui: ['ui-sub-type'],
},
@ -1425,6 +1447,7 @@ describe('featurePrivilegeIterator', () => {
update: ['cases-update-sub-type'],
delete: ['cases-delete-sub-type'],
push: ['cases-push-sub-type'],
settings: ['cases-settings-sub-type'],
},
ui: ['ui-sub-type'],
},
@ -1465,6 +1488,7 @@ describe('featurePrivilegeIterator', () => {
update: ['cases-update-type'],
delete: ['cases-delete-type'],
push: ['cases-push-type'],
settings: ['cases-settings-type'],
},
ui: ['ui-action'],
},
@ -1555,6 +1579,7 @@ describe('featurePrivilegeIterator', () => {
update: ['cases-update-type'],
delete: ['cases-delete-type'],
push: ['cases-push-type'],
settings: ['cases-settings-type'],
},
ui: ['ui-action'],
},
@ -1589,6 +1614,7 @@ describe('featurePrivilegeIterator', () => {
update: [],
delete: [],
push: [],
settings: [],
},
ui: ['ui-action'],
},

View file

@ -147,6 +147,10 @@ function mergeWithSubFeatures(
subFeaturePrivilege.cases?.delete ?? []
),
push: mergeArrays(mergedConfig.cases?.push ?? [], subFeaturePrivilege.cases?.push ?? []),
settings: mergeArrays(
mergedConfig.cases?.settings ?? [],
subFeaturePrivilege.cases?.settings ?? []
),
};
}
return mergedConfig;

View file

@ -82,6 +82,7 @@ const casesSchemaObject = schema.maybe(
update: schema.maybe(casesSchema),
delete: schema.maybe(casesSchema),
push: schema.maybe(casesSchema),
settings: schema.maybe(casesSchema),
})
);

View file

@ -1,50 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useEffect, useState } from 'react';
import { CasesPermissions } from '@kbn/cases-plugin/common';
import { useKibana } from '../utils/kibana_react';
import { casesFeatureId } from '../../common';
export function useGetUserCasesPermissions() {
const [casesPermissions, setCasesPermissions] = useState<CasesPermissions>({
all: false,
read: false,
create: false,
update: false,
delete: false,
push: false,
connectors: false,
});
const uiCapabilities = useKibana().services.application.capabilities;
const casesCapabilities = useKibana().services.cases.helpers.getUICapabilities(
uiCapabilities[casesFeatureId]
);
useEffect(() => {
setCasesPermissions({
all: casesCapabilities.all,
create: casesCapabilities.create,
read: casesCapabilities.read,
update: casesCapabilities.update,
delete: casesCapabilities.delete,
push: casesCapabilities.push,
connectors: casesCapabilities.connectors,
});
}, [
casesCapabilities.all,
casesCapabilities.create,
casesCapabilities.read,
casesCapabilities.update,
casesCapabilities.delete,
casesCapabilities.push,
casesCapabilities.connectors,
]);
return casesPermissions;
}

View file

@ -76,16 +76,6 @@ jest.mock('../../hooks/use_fetch_rule', () => {
};
});
jest.mock('@kbn/observability-shared-plugin/public');
jest.mock('../../hooks/use_get_user_cases_permissions', () => ({
useGetUserCasesPermissions: () => ({
all: true,
create: true,
delete: true,
push: true,
read: true,
update: true,
}),
}));
const useFetchAlertDetailMock = useFetchAlertDetail as jest.Mock;
const useParamsMock = useParams as jest.Mock;

View file

@ -58,7 +58,7 @@ export function AlertDetails() {
const [isLoading, alert] = useFetchAlertDetail(alertId);
const [ruleTypeModel, setRuleTypeModel] = useState<RuleTypeModel | null>(null);
const CasesContext = getCasesContext();
const userCasesPermissions = canUseCases();
const userCasesPermissions = canUseCases([observabilityFeatureId]);
const { rule } = useFetchRule({
ruleId: alert?.fields[ALERT_RULE_UUID],
});

View file

@ -18,6 +18,7 @@ import * as pluginContext from '../../../hooks/use_plugin_context';
import { ConfigSchema, ObservabilityPublicPluginsStart } from '../../../plugin';
import { AppMountParameters, CoreStart } from '@kbn/core/public';
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
import { allCasesPermissions, noCasesPermissions } from '@kbn/observability-shared-plugin/public';
const refresh = jest.fn();
const caseHooksReturnedValue = {
@ -36,15 +37,13 @@ mockUseKibanaReturnValue.services.cases.hooks.useCasesAddToExistingCaseModal.moc
caseHooksReturnedValue
);
mockUseKibanaReturnValue.services.cases.helpers.canUseCases.mockReturnValue(allCasesPermissions());
jest.mock('../../../utils/kibana_react', () => ({
__esModule: true,
useKibana: jest.fn(() => mockUseKibanaReturnValue),
}));
jest.mock('../../../hooks/use_get_user_cases_permissions', () => ({
useGetUserCasesPermissions: jest.fn(() => ({ create: true, read: true })),
}));
jest.mock('@kbn/triggers-actions-ui-plugin/public/common/lib/kibana/kibana_react', () => ({
useKibana: jest.fn(() => ({
services: { notifications: { toasts: { addDanger: jest.fn(), addSuccess: jest.fn() } } },
@ -175,4 +174,18 @@ describe('ObservabilityActions component', () => {
expect(refresh).toHaveBeenCalled();
});
it('should hide the case actions without permissions', async () => {
mockUseKibanaReturnValue.services.cases.helpers.canUseCases.mockReturnValue(
noCasesPermissions()
);
const wrapper = await setup('nothing');
wrapper.find('[data-test-subj="alertsTableRowActionMore"]').hostNodes().simulate('click');
expect(wrapper.find('[data-test-subj="add-to-new-case-action"]').hostNodes().length).toBe(0);
expect(wrapper.find('[data-test-subj="add-to-existing-case-action"]').hostNodes().length).toBe(
0
);
});
});

View file

@ -30,12 +30,11 @@ import {
} from '@kbn/rule-data-utils';
import { useBulkUntrackAlerts } from '@kbn/triggers-actions-ui-plugin/public';
import { useKibana } from '../../../utils/kibana_react';
import { useGetUserCasesPermissions } from '../../../hooks/use_get_user_cases_permissions';
import { isAlertDetailsEnabledPerApp } from '../../../utils/is_alert_details_enabled';
import { parseAlert } from '../helpers/parse_alert';
import { paths } from '../../../../common/locators/paths';
import { RULE_DETAILS_PAGE_ID } from '../../rule_details/constants';
import type { ObservabilityRuleTypeRegistry } from '../../..';
import { observabilityFeatureId, ObservabilityRuleTypeRegistry } from '../../..';
import type { ConfigSchema } from '../../../plugin';
import type { TopAlert } from '../../../typings/alerts';
@ -62,15 +61,15 @@ export function AlertActions({
}: Props) {
const {
cases: {
helpers: { getRuleIdFromEvent },
helpers: { getRuleIdFromEvent, canUseCases },
hooks: { useCasesAddToNewCaseFlyout, useCasesAddToExistingCaseModal },
},
http: {
basePath: { prepend },
},
} = useKibana().services;
const userCasesPermissions = useGetUserCasesPermissions();
const { mutateAsync: untrackAlerts } = useBulkUntrackAlerts();
const userCasesPermissions = canUseCases([observabilityFeatureId]);
const parseObservabilityAlert = useMemo(
() => parseAlert(observabilityRuleTypeRegistry),

View file

@ -7,15 +7,17 @@
import React from 'react';
import { useGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions';
import { observabilityFeatureId } from '../../../common';
import { usePluginContext } from '../../hooks/use_plugin_context';
import { Cases } from './components/cases';
import { CaseFeatureNoPermissions } from './components/feature_no_permissions';
import { HeaderMenu } from '../overview/components/header_menu/header_menu';
import { useKibana } from '../../utils/kibana_react';
export function CasesPage() {
const userCasesPermissions = useGetUserCasesPermissions();
const { ObservabilityPageTemplate } = usePluginContext();
const { canUseCases } = useKibana().services.cases.helpers;
const userCasesPermissions = canUseCases([observabilityFeatureId]);
return userCasesPermissions.read ? (
<ObservabilityPageTemplate isPageDataLoaded data-test-subj="o11yCasesPage">

View file

@ -27,6 +27,7 @@ const defaultProps: CasesProps = {
push: true,
update: true,
connectors: true,
settings: true,
},
};
@ -43,5 +44,6 @@ CasesPageWithNoPermissions.args = {
push: false,
update: false,
connectors: false,
settings: false,
},
};

View file

@ -1,15 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export const noCasesPermissions = () => ({
all: false,
create: false,
read: false,
update: false,
delete: false,
push: false,
});

View file

@ -175,6 +175,36 @@ export class ObservabilityPlugin implements Plugin<ObservabilityPluginSetup> {
},
],
},
{
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,
},
],
},
],
},
],
});

View file

@ -1,52 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useEffect, useState } from 'react';
import { CasesPermissions } from '@kbn/cases-plugin/common';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { casesFeatureId } from '../../common';
import { ObservabilitySharedStart } from '../plugin';
export function useGetUserCasesPermissions() {
const [casesPermissions, setCasesPermissions] = useState<CasesPermissions>({
all: false,
read: false,
create: false,
update: false,
delete: false,
push: false,
connectors: false,
});
const uiCapabilities = useKibana().services.application!.capabilities;
const casesCapabilities =
useKibana<ObservabilitySharedStart>().services.cases.helpers.getUICapabilities(
uiCapabilities[casesFeatureId]
);
useEffect(() => {
setCasesPermissions({
all: casesCapabilities.all,
create: casesCapabilities.create,
read: casesCapabilities.read,
update: casesCapabilities.update,
delete: casesCapabilities.delete,
push: casesCapabilities.push,
connectors: casesCapabilities.connectors,
});
}, [
casesCapabilities.all,
casesCapabilities.create,
casesCapabilities.read,
casesCapabilities.update,
casesCapabilities.delete,
casesCapabilities.push,
casesCapabilities.connectors,
]);
return casesPermissions;
}

View file

@ -57,7 +57,6 @@ export {
} from './hooks/use_track_metric';
export type { TrackEvent } from './hooks/use_track_metric';
export { useQuickTimeRanges } from './hooks/use_quick_time_ranges';
export { useGetUserCasesPermissions } from './hooks/use_get_user_cases_permissions';
export { useTimeZone } from './hooks/use_time_zone';
export { useChartTheme } from './hooks/use_chart_theme';
export { useLinkProps, shouldHandleLinkEvent } from './hooks/use_link_props';
@ -66,7 +65,7 @@ export { NavigationWarningPromptProvider, Prompt } from './components/navigation
export type { ApmIndicesConfig, UXMetrics } from './types';
export { noCasesPermissions } from './utils/cases_permissions';
export { noCasesPermissions, allCasesPermissions } from './utils/cases_permissions';
export {
type ObservabilityActionContextMenuItemProps,

View file

@ -13,4 +13,16 @@ export const noCasesPermissions = () => ({
delete: false,
push: false,
connectors: false,
settings: false,
});
export const allCasesPermissions = () => ({
all: true,
create: true,
read: true,
update: true,
delete: true,
push: true,
connectors: true,
settings: true,
});

View file

@ -5,7 +5,6 @@ Array [
"cases:observability/pushCase",
"cases:observability/createCase",
"cases:observability/createComment",
"cases:observability/createConfiguration",
"cases:observability/getCase",
"cases:observability/getComment",
"cases:observability/getTags",
@ -14,9 +13,10 @@ Array [
"cases:observability/findConfigurations",
"cases:observability/updateCase",
"cases:observability/updateComment",
"cases:observability/updateConfiguration",
"cases:observability/deleteCase",
"cases:observability/deleteComment",
"cases:observability/createConfiguration",
"cases:observability/updateConfiguration",
]
`;
@ -24,7 +24,6 @@ exports[`cases feature_privilege_builder within feature grants create privileges
Array [
"cases:securitySolution/createCase",
"cases:securitySolution/createComment",
"cases:securitySolution/createConfiguration",
]
`;
@ -52,10 +51,16 @@ Array [
]
`;
exports[`cases feature_privilege_builder within feature grants settings privileges under feature with id observability 1`] = `
Array [
"cases:observability/createConfiguration",
"cases:observability/updateConfiguration",
]
`;
exports[`cases feature_privilege_builder within feature grants update privileges under feature with id observability 1`] = `
Array [
"cases:observability/updateCase",
"cases:observability/updateComment",
"cases:observability/updateConfiguration",
]
`;

View file

@ -47,6 +47,7 @@ describe(`cases`, () => {
['read', 'observability'],
['update', 'observability'],
['delete', 'securitySolution'],
['settings', 'observability'],
])('grants %s privileges under feature with id %s', (operation, featureID) => {
const actions = new Actions();
const casesFeaturePrivilege = new FeaturePrivilegeCasesBuilder(actions);
@ -55,7 +56,6 @@ describe(`cases`, () => {
cases: {
[operation]: [featureID],
},
savedObject: {
all: [],
read: [],
@ -88,8 +88,8 @@ describe(`cases`, () => {
update: ['obs'],
delete: ['security'],
read: ['obs'],
settings: ['security'],
},
savedObject: {
all: [],
read: [],
@ -113,7 +113,6 @@ describe(`cases`, () => {
"cases:security/pushCase",
"cases:security/createCase",
"cases:security/createComment",
"cases:security/createConfiguration",
"cases:security/getCase",
"cases:security/getComment",
"cases:security/getTags",
@ -122,9 +121,10 @@ describe(`cases`, () => {
"cases:security/findConfigurations",
"cases:security/updateCase",
"cases:security/updateComment",
"cases:security/updateConfiguration",
"cases:security/deleteCase",
"cases:security/deleteComment",
"cases:security/createConfiguration",
"cases:security/updateConfiguration",
"cases:obs/getCase",
"cases:obs/getComment",
"cases:obs/getTags",
@ -133,7 +133,6 @@ describe(`cases`, () => {
"cases:obs/findConfigurations",
"cases:obs/updateCase",
"cases:obs/updateComment",
"cases:obs/updateConfiguration",
]
`);
});
@ -147,7 +146,6 @@ describe(`cases`, () => {
all: ['security', 'other-security'],
read: ['obs', 'other-obs'],
},
savedObject: {
all: [],
read: [],
@ -171,7 +169,6 @@ describe(`cases`, () => {
"cases:security/pushCase",
"cases:security/createCase",
"cases:security/createComment",
"cases:security/createConfiguration",
"cases:security/getCase",
"cases:security/getComment",
"cases:security/getTags",
@ -180,13 +177,13 @@ describe(`cases`, () => {
"cases:security/findConfigurations",
"cases:security/updateCase",
"cases:security/updateComment",
"cases:security/updateConfiguration",
"cases:security/deleteCase",
"cases:security/deleteComment",
"cases:security/createConfiguration",
"cases:security/updateConfiguration",
"cases:other-security/pushCase",
"cases:other-security/createCase",
"cases:other-security/createComment",
"cases:other-security/createConfiguration",
"cases:other-security/getCase",
"cases:other-security/getComment",
"cases:other-security/getTags",
@ -195,9 +192,10 @@ describe(`cases`, () => {
"cases:other-security/findConfigurations",
"cases:other-security/updateCase",
"cases:other-security/updateComment",
"cases:other-security/updateConfiguration",
"cases:other-security/deleteCase",
"cases:other-security/deleteComment",
"cases:other-security/createConfiguration",
"cases:other-security/updateConfiguration",
"cases:obs/getCase",
"cases:obs/getComment",
"cases:obs/getTags",

View file

@ -13,11 +13,16 @@ import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder';
export type CasesSupportedOperations = typeof allOperations[number];
// if you add a value here you'll likely also need to make changes here:
// x-pack/plugins/cases/server/authorization/index.ts
/**
* If you add a new operation type (all, push, update, etc) you should also
* extend the mapping here x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.ts
*
* Also if you add a new operation (createCase, updateCase, etc) here you'll likely also need to make changes here:
* x-pack/plugins/cases/server/authorization/index.ts
*/
const pushOperations = ['pushCase'] as const;
const createOperations = ['createCase', 'createComment', 'createConfiguration'] as const;
const createOperations = ['createCase', 'createComment'] as const;
const readOperations = [
'getCase',
'getComment',
@ -26,14 +31,16 @@ const readOperations = [
'getUserActions',
'findConfigurations',
] as const;
const updateOperations = ['updateCase', 'updateComment', 'updateConfiguration'] as const;
const updateOperations = ['updateCase', 'updateComment'] as const;
const deleteOperations = ['deleteCase', 'deleteComment'] as const;
const settingsOperations = ['createConfiguration', 'updateConfiguration'] as const;
const allOperations = [
...pushOperations,
...createOperations,
...readOperations,
...updateOperations,
...deleteOperations,
...settingsOperations,
] as const;
export class FeaturePrivilegeCasesBuilder extends BaseFeaturePrivilegeBuilder {
@ -57,6 +64,7 @@ export class FeaturePrivilegeCasesBuilder extends BaseFeaturePrivilegeBuilder {
...getCasesPrivilege(readOperations, privilegeDefinition.cases?.read),
...getCasesPrivilege(updateOperations, privilegeDefinition.cases?.update),
...getCasesPrivilege(deleteOperations, privilegeDefinition.cases?.delete),
...getCasesPrivilege(settingsOperations, privilegeDefinition.cases?.settings),
]);
}
}

View file

@ -14,7 +14,7 @@ import type { AppLeaveHandler } from '@kbn/core/public';
import { APP_ID } from '../../common/constants';
import { RouteCapture } from '../common/components/endpoint/route_capture';
import { useGetUserCasesPermissions, useKibana } from '../common/lib/kibana';
import { useKibana } from '../common/lib/kibana';
import type { AppAction } from '../common/store/actions';
import { ManageRoutesSpy } from '../common/utils/route/manage_spy_routes';
import { NotFoundPage } from './404';
@ -29,7 +29,7 @@ interface RouterProps {
const PageRouterComponent: FC<RouterProps> = ({ children, history, onAppLeave }) => {
const { cases } = useKibana().services;
const CasesContext = cases.ui.getCasesContext();
const userCasesPermissions = useGetUserCasesPermissions();
const userCasesPermissions = cases.helpers.canUseCases([APP_ID]);
const dispatch = useDispatch<(action: AppAction) => void>();
useEffect(() => {
return () => {

View file

@ -8,7 +8,7 @@
import {
CREATE_CASES_CAPABILITY,
READ_CASES_CAPABILITY,
UPDATE_CASES_CAPABILITY,
CASES_SETTINGS_CAPABILITY,
} from '@kbn/cases-plugin/common';
import { getCasesDeepLinks } from '@kbn/cases-plugin/public';
import { CASES_FEATURE_ID, CASES_PATH, SecurityPageName } from '../../common/constants';
@ -22,7 +22,7 @@ const casesLinks = getCasesDeepLinks<LinkItem>({
capabilities: [`${CASES_FEATURE_ID}.${READ_CASES_CAPABILITY}`],
},
[SecurityPageName.caseConfigure]: {
capabilities: [`${CASES_FEATURE_ID}.${UPDATE_CASES_CAPABILITY}`],
capabilities: [`${CASES_FEATURE_ID}.${CASES_SETTINGS_CAPABILITY}`],
sideNavDisabled: true,
},
[SecurityPageName.caseCreate]: {

View file

@ -21,7 +21,7 @@ import { TimelineId } from '../../../common/types/timeline';
import { getRuleDetailsUrl, useFormatUrl } from '../../common/components/link_to';
import { useGetUserCasesPermissions, useKibana, useNavigation } from '../../common/lib/kibana';
import { useKibana, useNavigation } from '../../common/lib/kibana';
import {
APP_ID,
CASES_PATH,
@ -56,7 +56,7 @@ const TimelineDetailsPanel = () => {
const CaseContainerComponent: React.FC = () => {
const { cases } = useKibana().services;
const { getAppUrl, navigateTo } = useNavigation();
const userCasesPermissions = useGetUserCasesPermissions();
const userCasesPermissions = cases.helpers.canUseCases([APP_ID]);
const dispatch = useDispatch();
const { formatUrl: detectionsFormatUrl, search: detectionsUrlSearch } = useFormatUrl(
SecurityPageName.rules

View file

@ -5,34 +5,39 @@
* 2.0.
*/
export const noCasesCapabilities = () => ({
import type { CasesPermissions, CasesCapabilities } from '@kbn/cases-plugin/common';
export const noCasesCapabilities = (): CasesCapabilities => ({
create_cases: false,
read_cases: false,
update_cases: false,
delete_cases: false,
push_cases: false,
cases_connector: false,
cases_connectors: false,
cases_settings: false,
});
export const readCasesCapabilities = () => ({
export const readCasesCapabilities = (): CasesCapabilities => ({
create_cases: false,
read_cases: true,
update_cases: false,
delete_cases: false,
push_cases: false,
cases_connector: true,
cases_connectors: true,
cases_settings: false,
});
export const allCasesCapabilities = () => ({
export const allCasesCapabilities = (): CasesCapabilities => ({
create_cases: true,
read_cases: true,
update_cases: true,
delete_cases: true,
push_cases: true,
cases_connector: true,
cases_connectors: true,
cases_settings: true,
});
export const noCasesPermissions = () => ({
export const noCasesPermissions = (): CasesPermissions => ({
all: false,
create: false,
read: false,
@ -40,9 +45,10 @@ export const noCasesPermissions = () => ({
delete: false,
push: false,
connectors: false,
settings: false,
});
export const readCasesPermissions = () => ({
export const readCasesPermissions = (): CasesPermissions => ({
all: false,
create: false,
read: true,
@ -50,9 +56,10 @@ export const readCasesPermissions = () => ({
delete: false,
push: false,
connectors: true,
settings: false,
});
export const writeCasesPermissions = () => ({
export const writeCasesPermissions = (): CasesPermissions => ({
all: false,
create: true,
read: false,
@ -60,9 +67,10 @@ export const writeCasesPermissions = () => ({
delete: true,
push: true,
connectors: true,
settings: true,
});
export const allCasesPermissions = () => ({
export const allCasesPermissions = (): CasesPermissions => ({
all: true,
create: true,
read: true,
@ -70,4 +78,5 @@ export const allCasesPermissions = () => ({
delete: true,
push: true,
connectors: true,
settings: true,
});

View file

@ -26,7 +26,7 @@ import { mockAlertDetailsData } from './__mocks__';
import type { TimelineEventsDetailsItem } from '../../../../common/search_strategy';
import { TimelineTabs } from '../../../../common/types/timeline';
import { useInvestigationTimeEnrichment } from '../../containers/cti/event_enrichment';
import { useGetUserCasesPermissions, useKibana } from '../../lib/kibana';
import { useKibana } from '../../lib/kibana';
import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers';
import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features';
@ -44,14 +44,8 @@ jest.mock('../../../timelines/components/timeline/body/renderers', () => {
});
jest.mock('../../lib/kibana');
const originalKibanaLib = jest.requireActual('../../lib/kibana');
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
// Restore the useGetUserCasesPermissions so the calling functions can receive a valid permissions object
// The returned permissions object will indicate that the user does not have permissions by default
const mockUseGetUserCasesPermissions = useGetUserCasesPermissions as jest.Mock;
mockUseGetUserCasesPermissions.mockImplementation(originalKibanaLib.useGetUserCasesPermissions);
jest.mock('../../containers/cti/event_enrichment');
jest.mock('../../../detection_engine/rule_management/logic/use_rule_with_fallback', () => {

View file

@ -12,7 +12,6 @@ import { TestProviders } from '../../../mock';
import type { TimelineEventsDetailsItem } from '../../../../../common/search_strategy/timeline';
import { useKibana as mockUseKibana } from '../../../lib/kibana/__mocks__';
import { useGetUserCasesPermissions } from '../../../lib/kibana';
import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features';
import { licenseService } from '../../../hooks/use_license';
import { noCasesPermissions, readCasesPermissions } from '../../../../cases_test_utils';
@ -20,12 +19,13 @@ import { Insights } from './insights';
import * as i18n from './translations';
const mockedUseKibana = mockUseKibana();
const mockCanUseCases = jest.fn();
jest.mock('../../../lib/kibana', () => {
const original = jest.requireActual('../../../lib/kibana');
return {
...original,
useGetUserCasesPermissions: jest.fn(),
useToasts: jest.fn().mockReturnValue({ addWarning: jest.fn() }),
useKibana: () => ({
...mockedUseKibana,
@ -35,12 +35,12 @@ jest.mock('../../../lib/kibana', () => {
api: {
getRelatedCases: jest.fn(),
},
helpers: { canUseCases: mockCanUseCases },
},
},
}),
};
});
const mockUseGetUserCasesPermissions = useGetUserCasesPermissions as jest.Mock;
jest.mock('../../../hooks/use_license', () => {
const licenseServiceInstance = {
@ -94,7 +94,7 @@ const data: TimelineEventsDetailsItem[] = [
describe('Insights', () => {
beforeEach(() => {
mockUseGetUserCasesPermissions.mockReturnValue(noCasesPermissions());
mockCanUseCases.mockReturnValue(noCasesPermissions());
});
it('does not render when there is no content to show', () => {
@ -116,7 +116,7 @@ describe('Insights', () => {
// It will show for all users that are able to read case data.
// Enabling that permission, will show the case insight module which
// is necessary to pass this test.
mockUseGetUserCasesPermissions.mockReturnValue(readCasesPermissions());
mockCanUseCases.mockReturnValue(readCasesPermissions());
render(
<TestProviders>

View file

@ -11,12 +11,12 @@ import { euiStyled } from '@kbn/kibana-react-plugin/common';
import { ALERT_SUPPRESSION_DOCS_COUNT } from '@kbn/rule-data-utils';
import { find } from 'lodash/fp';
import { APP_ID } from '../../../../../common';
import * as i18n from './translations';
import type { BrowserFields } from '../../../containers/source';
import type { TimelineEventsDetailsItem } from '../../../../../common/search_strategy/timeline';
import { hasData } from './helpers';
import { useGetUserCasesPermissions } from '../../../lib/kibana';
import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features';
import { useLicense } from '../../../hooks/use_license';
import { RelatedAlertsByProcessAncestry } from './related_alerts_by_process_ancestry';
@ -24,6 +24,7 @@ import { RelatedCases } from './related_cases';
import { RelatedAlertsBySourceEvent } from './related_alerts_by_source_event';
import { RelatedAlertsBySession } from './related_alerts_by_session';
import { RelatedAlertsUpsell } from './related_alerts_upsell';
import { useKibana } from '../../../lib/kibana';
const StyledInsightItem = euiStyled(EuiFlexItem)`
border: 1px solid ${({ theme }) => theme.eui.euiColorLightShade};
@ -45,6 +46,7 @@ interface Props {
*/
export const Insights = React.memo<Props>(
({ browserFields, eventId, data, isReadOnly, scopeId }) => {
const { cases } = useKibana().services;
const isRelatedAlertsByProcessAncestryEnabled = useIsExperimentalFeatureEnabled(
'insightsRelatedAlertsByProcessAncestry'
);
@ -83,7 +85,7 @@ export const Insights = React.memo<Props>(
);
const hasAlertSuppressionField = hasData(alertSuppressionField);
const userCasesPermissions = useGetUserCasesPermissions();
const userCasesPermissions = cases.helpers.canUseCases([APP_ID]);
const hasCasesReadPermissions = userCasesPermissions.read;
// Make sure that the alert has at least one of the associated fields

View file

@ -10,7 +10,6 @@ import React from 'react';
import { TestProviders } from '../../../mock';
import { useKibana as mockUseKibana } from '../../../lib/kibana/__mocks__';
import { useGetUserCasesPermissions } from '../../../lib/kibana';
import { RelatedCases } from './related_cases';
import { noCasesPermissions, readCasesPermissions } from '../../../../cases_test_utils';
import { CASES_LOADING, CASES_COUNT } from './translations';
@ -19,13 +18,14 @@ import { AlertsCasesTourSteps } from '../../guided_onboarding_tour/tour_config';
const mockedUseKibana = mockUseKibana();
const mockGetRelatedCases = jest.fn();
const mockCanUseCases = jest.fn();
jest.mock('../../guided_onboarding_tour');
jest.mock('../../../lib/kibana', () => {
const original = jest.requireActual('../../../lib/kibana');
return {
...original,
useGetUserCasesPermissions: jest.fn(),
useToasts: jest.fn().mockReturnValue({ addWarning: jest.fn() }),
useKibana: () => ({
...mockedUseKibana,
@ -35,6 +35,7 @@ jest.mock('../../../lib/kibana', () => {
api: {
getRelatedCases: mockGetRelatedCases,
},
helpers: { canUseCases: mockCanUseCases },
},
},
}),
@ -47,7 +48,7 @@ window.HTMLElement.prototype.scrollIntoView = scrollToMock;
describe('Related Cases', () => {
beforeEach(() => {
(useGetUserCasesPermissions as jest.Mock).mockReturnValue(readCasesPermissions());
mockCanUseCases.mockReturnValue(readCasesPermissions());
(useTourContext as jest.Mock).mockReturnValue({
activeStep: AlertsCasesTourSteps.viewCase,
incrementStep: () => null,
@ -58,7 +59,7 @@ describe('Related Cases', () => {
});
describe('When user does not have cases read permissions', () => {
beforeEach(() => {
(useGetUserCasesPermissions as jest.Mock).mockReturnValue(noCasesPermissions());
mockCanUseCases.mockReturnValue(noCasesPermissions());
});
test('should not show related cases when user does not have permissions', async () => {
await act(async () => {

View file

@ -22,7 +22,6 @@ import { useTimelineEvents } from './use_timelines_events';
import { getDefaultControlColumn } from '../../../timelines/components/timeline/body/control_columns';
import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers';
import type { UseFieldBrowserOptionsProps } from '../../../timelines/components/fields_browser';
import { useGetUserCasesPermissions } from '../../lib/kibana';
import { TableId } from '@kbn/securitysolution-data-table';
import { mount } from 'enzyme';
@ -38,13 +37,6 @@ jest.mock('react-redux', () => {
};
});
const originalKibanaLib = jest.requireActual('../../lib/kibana');
// Restore the useGetUserCasesPermissions so the calling functions can receive a valid permissions object
// The returned permissions object will indicate that the user does not have permissions by default
const mockUseGetUserCasesPermissions = useGetUserCasesPermissions as jest.Mock;
mockUseGetUserCasesPermissions.mockImplementation(originalKibanaLib.useGetUserCasesPermissions);
jest.mock('./use_timelines_events');
jest.mock('../../utils/normalize_time_range');

View file

@ -11,21 +11,12 @@ import { TestProviders } from '../../mock';
import { TEST_ID, SessionsView, defaultSessionsFilter } from '.';
import type { EntityType } from '@kbn/timelines-plugin/common';
import type { SessionsComponentsProps } from './types';
import { useGetUserCasesPermissions } from '../../lib/kibana';
import { TableId } from '@kbn/securitysolution-data-table';
import { licenseService } from '../../hooks/use_license';
import { mount } from 'enzyme';
import type { EventsViewerProps } from '../events_viewer';
jest.mock('../../lib/kibana');
const originalKibanaLib = jest.requireActual('../../lib/kibana');
// Restore the useGetUserCasesPermissions so the calling functions can receive a valid permissions object
// The returned permissions object will indicate that the user does not have permissions by default
const mockUseGetUserCasesPermissions = useGetUserCasesPermissions as jest.Mock;
mockUseGetUserCasesPermissions.mockImplementation(originalKibanaLib.useGetUserCasesPermissions);
jest.mock('../../utils/normalize_time_range');
const startDate = '2022-03-22T22:10:56.794Z';

View file

@ -101,6 +101,7 @@ describe('VisualizationActions', () => {
.fn()
.mockReturnValue({ open: mockGetCreateCaseFlyoutOpen }),
},
helpers: { canUseCases: jest.fn().mockReturnValue(allCasesPermissions()) },
},
application: {
capabilities: { [CASES_FEATURE_ID]: allCasesCapabilities() },

View file

@ -8,7 +8,6 @@ import { renderHook } from '@testing-library/react-hooks';
import { useKibana as mockUseKibana } from '../../lib/kibana/__mocks__';
import { kpiHostMetricLensAttributes } from './lens_attributes/hosts/kpi_host_metric';
import { useAddToExistingCase } from './use_add_to_existing_case';
import { useGetUserCasesPermissions } from '../../lib/kibana';
import {
allCasesPermissions,
readCasesPermissions,
@ -18,13 +17,13 @@ import { AttachmentType } from '@kbn/cases-plugin/common';
const mockedUseKibana = mockUseKibana();
const mockGetUseCasesAddToExistingCaseModal = jest.fn();
const mockCanUseCases = jest.fn();
jest.mock('../../lib/kibana', () => {
const original = jest.requireActual('../../lib/kibana');
return {
...original,
useGetUserCasesPermissions: jest.fn(),
useKibana: () => ({
...mockedUseKibana,
services: {
@ -33,6 +32,7 @@ jest.mock('../../lib/kibana', () => {
hooks: {
useCasesAddToExistingCaseModal: mockGetUseCasesAddToExistingCaseModal,
},
helpers: { canUseCases: mockCanUseCases },
},
},
}),
@ -47,7 +47,7 @@ describe('useAddToExistingCase', () => {
};
beforeEach(() => {
(useGetUserCasesPermissions as jest.Mock).mockReturnValue(allCasesPermissions());
mockCanUseCases.mockReturnValue(allCasesPermissions());
});
it('useCasesAddToExistingCaseModal with attachments', () => {
@ -68,7 +68,7 @@ describe('useAddToExistingCase', () => {
});
it("disables the button if the user can't create but can read", () => {
(useGetUserCasesPermissions as jest.Mock).mockReturnValue(readCasesPermissions());
mockCanUseCases.mockReturnValue(readCasesPermissions());
const { result } = renderHook(() =>
useAddToExistingCase({
@ -81,7 +81,7 @@ describe('useAddToExistingCase', () => {
});
it("disables the button if the user can't read but can create", () => {
(useGetUserCasesPermissions as jest.Mock).mockReturnValue(writeCasesPermissions());
mockCanUseCases.mockReturnValue(writeCasesPermissions());
const { result } = renderHook(() =>
useAddToExistingCase({

View file

@ -8,7 +8,8 @@ import { useCallback, useMemo } from 'react';
import { AttachmentType, LENS_ATTACHMENT_TYPE } from '@kbn/cases-plugin/common';
import type { CaseAttachmentsWithoutOwner } from '@kbn/cases-plugin/public';
import { useKibana, useGetUserCasesPermissions } from '../../lib/kibana';
import { APP_ID } from '../../../../common';
import { useKibana } from '../../lib/kibana';
import { ADD_TO_CASE_SUCCESS } from './translations';
import type { LensAttributes } from './types';
@ -21,8 +22,8 @@ export const useAddToExistingCase = ({
lensAttributes: LensAttributes | null;
timeRange: { from: string; to: string } | null;
}) => {
const userCasesPermissions = useGetUserCasesPermissions();
const { cases } = useKibana().services;
const userCasesPermissions = cases.helpers.canUseCases([APP_ID]);
const attachments = useMemo(() => {
return [
{

View file

@ -8,7 +8,6 @@ import { renderHook } from '@testing-library/react-hooks';
import { useKibana as mockUseKibana } from '../../lib/kibana/__mocks__';
import { kpiHostMetricLensAttributes } from './lens_attributes/hosts/kpi_host_metric';
import { useAddToNewCase } from './use_add_to_new_case';
import { useGetUserCasesPermissions } from '../../lib/kibana';
import {
allCasesPermissions,
readCasesPermissions,
@ -20,13 +19,13 @@ jest.mock('../../lib/kibana/kibana_react');
const mockedUseKibana = mockUseKibana();
const mockGetUseCasesAddToNewCaseFlyout = jest.fn();
const mockCanUseCases = jest.fn();
jest.mock('../../lib/kibana', () => {
const original = jest.requireActual('../../lib/kibana');
return {
...original,
useGetUserCasesPermissions: jest.fn(),
useKibana: () => ({
...mockedUseKibana,
services: {
@ -35,6 +34,7 @@ jest.mock('../../lib/kibana', () => {
hooks: {
useCasesAddToNewCaseFlyout: mockGetUseCasesAddToNewCaseFlyout,
},
helpers: { canUseCases: mockCanUseCases },
},
},
}),
@ -47,7 +47,7 @@ describe('useAddToNewCase', () => {
to: '2022-03-07T15:59:59.999Z',
};
beforeEach(() => {
(useGetUserCasesPermissions as jest.Mock).mockReturnValue(allCasesPermissions());
mockCanUseCases.mockReturnValue(allCasesPermissions());
});
it('useCasesAddToNewCaseFlyout with attachments', () => {
@ -64,7 +64,7 @@ describe('useAddToNewCase', () => {
});
it("disables the button if the user can't create but can read", () => {
(useGetUserCasesPermissions as jest.Mock).mockReturnValue(readCasesPermissions());
mockCanUseCases.mockReturnValue(readCasesPermissions());
const { result } = renderHook(() =>
useAddToNewCase({
@ -76,7 +76,7 @@ describe('useAddToNewCase', () => {
});
it("disables the button if the user can't read but can create", () => {
(useGetUserCasesPermissions as jest.Mock).mockReturnValue(writeCasesPermissions());
mockCanUseCases.mockReturnValue(writeCasesPermissions());
const { result } = renderHook(() =>
useAddToNewCase({

View file

@ -8,7 +8,8 @@ import { useCallback, useMemo } from 'react';
import { AttachmentType, LENS_ATTACHMENT_TYPE } from '@kbn/cases-plugin/common';
import type { CaseAttachmentsWithoutOwner } from '@kbn/cases-plugin/public';
import { useKibana, useGetUserCasesPermissions } from '../../lib/kibana';
import { APP_ID } from '../../../../common';
import { useKibana } from '../../lib/kibana';
import { ADD_TO_CASE_SUCCESS } from './translations';
import type { LensAttributes } from './types';
@ -20,8 +21,9 @@ export interface UseAddToNewCaseProps {
}
export const useAddToNewCase = ({ onClick, timeRange, lensAttributes }: UseAddToNewCaseProps) => {
const userCasesPermissions = useGetUserCasesPermissions();
const { cases } = useKibana().services;
const userCasesPermissions = cases.helpers.canUseCases([APP_ID]);
const attachments = useMemo(() => {
return [
{

View file

@ -95,7 +95,6 @@ export const useToasts = jest
export const useCurrentUser = jest.fn();
export const withKibana = jest.fn(createWithKibanaMock());
export const KibanaContextProvider = jest.fn(createKibanaContextProviderMock());
export const useGetUserCasesPermissions = jest.fn();
export const useAppUrl = jest.fn().mockReturnValue({
getAppUrl: jest
.fn()

View file

@ -14,7 +14,6 @@ import { camelCase, isArray, isObject } from 'lodash';
import { set } from '@kbn/safer-lodash-set';
import type { AuthenticatedUser } from '@kbn/security-plugin/common';
import type { Capabilities } from '@kbn/core/public';
import type { CasesPermissions } from '@kbn/cases-plugin/common';
import {
useGetAppUrl,
useNavigateTo,
@ -22,11 +21,7 @@ import {
type GetAppUrl,
type NavigateTo,
} from '@kbn/security-solution-navigation';
import {
CASES_FEATURE_ID,
DEFAULT_DATE_FORMAT,
DEFAULT_DATE_FORMAT_TZ,
} from '../../../../common/constants';
import { DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ } from '../../../../common/constants';
import { errorToToaster, useStateToaster } from '../../components/toasters';
import type { StartServices } from '../../../types';
import { useUiSetting, useKibana } from './kibana_react';
@ -153,44 +148,6 @@ export const useCurrentUser = (): AuthenticatedElasticUser | null => {
return user;
};
export const useGetUserCasesPermissions = () => {
const [casesPermissions, setCasesPermissions] = useState<CasesPermissions>({
all: false,
create: false,
read: false,
update: false,
delete: false,
push: false,
connectors: false,
});
const uiCapabilities = useKibana().services.application.capabilities;
const casesCapabilities = useKibana().services.cases.helpers.getUICapabilities(
uiCapabilities[CASES_FEATURE_ID]
);
useEffect(() => {
setCasesPermissions({
all: casesCapabilities.all,
create: casesCapabilities.create,
read: casesCapabilities.read,
update: casesCapabilities.update,
delete: casesCapabilities.delete,
push: casesCapabilities.push,
connectors: casesCapabilities.connectors,
});
}, [
casesCapabilities.all,
casesCapabilities.create,
casesCapabilities.read,
casesCapabilities.update,
casesCapabilities.delete,
casesCapabilities.push,
casesCapabilities.connectors,
]);
return casesPermissions;
};
export const useAppUrl = useGetAppUrl;
export { useNavigateTo, useNavigation };
export type { GetAppUrl, NavigateTo };

View file

@ -117,7 +117,7 @@ export const createStartServicesMock = (
const discover = discoverPluginMock.createStartContract();
const cases = mockCasesContract();
const dataViewServiceMock = dataViewPluginMocks.createStartContract();
cases.helpers.getUICapabilities.mockReturnValue(noCasesPermissions());
cases.helpers.canUseCases.mockReturnValue(noCasesPermissions());
const triggersActionsUi = triggersActionsUiMock.createStart();
const cloudExperiments = cloudExperimentsMock.createStartMock();
const guidedOnboarding = guidedOnboardingMock.createStart();

View file

@ -74,17 +74,22 @@ jest.mock('../../../../common/lib/kibana', () => {
application: {
capabilities: { siem: { crud_alerts: true, read_alerts: true } },
},
cases: mockCasesContract(),
cases: {
...mockCasesContract(),
helpers: {
canUseCases: jest.fn().mockReturnValue({
all: true,
create: true,
read: true,
update: true,
delete: true,
push: true,
}),
getRuleIdFromEvent: jest.fn(),
},
},
},
}),
useGetUserCasesPermissions: jest.fn().mockReturnValue({
all: true,
create: true,
read: true,
update: true,
delete: true,
push: true,
}),
};
});

View file

@ -12,7 +12,7 @@ import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useAddToCaseActions } from './use_add_to_case_actions';
import { TestProviders } from '../../../../common/mock';
import { useGetUserCasesPermissions, useKibana } from '../../../../common/lib/kibana';
import { useKibana } from '../../../../common/lib/kibana';
import { useTourContext } from '../../../../common/components/guided_onboarding_tour';
import {
AlertsCasesTourSteps,
@ -20,6 +20,7 @@ import {
} from '../../../../common/components/guided_onboarding_tour/tour_config';
import { CasesTourSteps } from '../../../../common/components/guided_onboarding_tour/cases_tour_steps';
import type { AlertTableContextMenuItem } from '../types';
import { allCasesPermissions } from '../../../../cases_test_utils';
jest.mock('../../../../common/components/guided_onboarding_tour');
jest.mock('../../../../common/lib/kibana');
@ -76,15 +77,6 @@ describe('useAddToCaseActions', () => {
isTourShown: () => false,
});
(useGetUserCasesPermissions as jest.Mock).mockReturnValue({
all: true,
create: true,
read: true,
update: true,
delete: true,
push: true,
});
useKibanaMock.mockReturnValue({
services: {
cases: {
@ -94,6 +86,7 @@ describe('useAddToCaseActions', () => {
},
helpers: {
getRuleIdFromEvent: () => null,
canUseCases: jest.fn().mockReturnValue(allCasesPermissions()),
},
},
},

View file

@ -9,6 +9,7 @@ import React, { useCallback, useMemo } from 'react';
import { AttachmentType } from '@kbn/cases-plugin/common';
import type { CaseAttachmentsWithoutOwner } from '@kbn/cases-plugin/public';
import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
import { APP_ID } from '../../../../../common';
import { CasesTourSteps } from '../../../../common/components/guided_onboarding_tour/cases_tour_steps';
import {
AlertsCasesTourSteps,
@ -16,7 +17,7 @@ import {
SecurityStepId,
} from '../../../../common/components/guided_onboarding_tour/tour_config';
import { useTourContext } from '../../../../common/components/guided_onboarding_tour';
import { useGetUserCasesPermissions, useKibana } from '../../../../common/lib/kibana';
import { useKibana } from '../../../../common/lib/kibana';
import type { TimelineNonEcsData } from '../../../../../common/search_strategy';
import { ADD_TO_EXISTING_CASE, ADD_TO_NEW_CASE } from '../translations';
import type { AlertTableContextMenuItem } from '../types';
@ -43,7 +44,7 @@ export const useAddToCaseActions = ({
refetch,
}: UseAddToCaseActions) => {
const { cases: casesUi } = useKibana().services;
const userCasesPermissions = useGetUserCasesPermissions();
const userCasesPermissions = casesUi.helpers.canUseCases([APP_ID]);
const isAlert = useMemo(() => {
return ecsData?.event?.kind?.includes('signal');

View file

@ -23,7 +23,6 @@ import { usePreviewHistogram } from './use_preview_histogram';
import { PreviewHistogram } from './preview_histogram';
import { ALL_VALUES_ZEROS_TITLE } from '../../../../common/components/charts/translation';
import { useGetUserCasesPermissions } from '../../../../common/lib/kibana';
import { useTimelineEvents } from '../../../../common/components/events_viewer/use_timelines_events';
import { TableId } from '@kbn/securitysolution-data-table';
import { createStore } from '../../../../common/store';
@ -58,12 +57,7 @@ const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as j
const getMockUseIsExperimentalFeatureEnabled =
(mockMapping?: Partial<ExperimentalFeatures>) => (flag: keyof typeof allowedExperimentalValues) =>
mockMapping ? mockMapping?.[flag] : allowedExperimentalValues?.[flag];
const originalKibanaLib = jest.requireActual('../../../../common/lib/kibana');
// Restore the useGetUserCasesPermissions so the calling functions can receive a valid permissions object
// The returned permissions object will indicate that the user does not have permissions by default
const mockUseGetUserCasesPermissions = useGetUserCasesPermissions as jest.Mock;
mockUseGetUserCasesPermissions.mockImplementation(originalKibanaLib.useGetUserCasesPermissions);
const mockUseFieldBrowserOptions = jest.fn();
jest.mock('../../../../timelines/components/fields_browser', () => ({
useFieldBrowserOptions: (props: UseFieldBrowserOptionsProps) => mockUseFieldBrowserOptions(props),

View file

@ -18,7 +18,7 @@ import { TimelineId } from '../../../../common/types/timeline';
import { TestProviders } from '../../../common/mock';
import { mockTimelines } from '../../../common/mock/mock_timelines_plugin';
import { createStartServicesMock } from '../../../common/lib/kibana/kibana_react.mock';
import { useKibana, useGetUserCasesPermissions, useHttp } from '../../../common/lib/kibana';
import { useKibana, useHttp } from '../../../common/lib/kibana';
import { mockCasesContract } from '@kbn/cases-plugin/public/mocks';
import { initialUserPrivilegesState as mockInitialUserPrivilegesState } from '../../../common/components/user_privileges/user_privileges_context';
import { useUserPrivileges } from '../../../common/components/user_privileges';
@ -46,7 +46,6 @@ jest.mock('../user_info', () => ({
}));
jest.mock('../../../common/lib/kibana');
(useGetUserCasesPermissions as jest.Mock).mockReturnValue(allCasesPermissions());
jest.mock('../../containers/detection_engine/alerts/use_alerts_privileges', () => ({
useAlertsPrivileges: jest.fn().mockReturnValue({ hasIndexWrite: true, hasKibanaCRUD: true }),
@ -119,7 +118,13 @@ describe('take action dropdown', () => {
services: {
...mockStartServicesMock,
timelines: { ...mockTimelines },
cases: mockCasesContract(),
cases: {
...mockCasesContract(),
helpers: {
canUseCases: jest.fn().mockReturnValue(allCasesPermissions()),
getRuleIdFromEvent: () => null,
},
},
osquery: {
isOsqueryAvailable: jest.fn().mockReturnValue(true),
},

View file

@ -7,13 +7,32 @@
import { renderHook } from '@testing-library/react-hooks';
import { useGetUserCasesPermissions } from '../../../../common/lib/kibana';
import { useKibana as mockUseKibana } from '../../../../common/lib/kibana/__mocks__';
import { useShowRelatedCases } from './use_show_related_cases';
jest.mock('../../../../common/lib/kibana');
const mockedUseKibana = mockUseKibana();
const mockCanUseCases = jest.fn();
jest.mock('../../../../common/lib/kibana/kibana_react', () => {
const original = jest.requireActual('../../../../common/lib/kibana/kibana_react');
return {
...original,
useKibana: () => ({
...mockedUseKibana,
services: {
...mockedUseKibana.services,
cases: {
helpers: { canUseCases: mockCanUseCases },
},
},
}),
};
});
describe('useShowRelatedCases', () => {
it(`should return false if user doesn't have cases read privilege`, () => {
(useGetUserCasesPermissions as jest.Mock).mockReturnValue({
mockCanUseCases.mockReturnValue({
all: false,
create: false,
read: false,
@ -28,7 +47,7 @@ describe('useShowRelatedCases', () => {
});
it('should return true if user has cases read privilege', () => {
(useGetUserCasesPermissions as jest.Mock).mockReturnValue({
mockCanUseCases.mockReturnValue({
all: false,
create: false,
read: true,

View file

@ -5,12 +5,15 @@
* 2.0.
*/
import { useGetUserCasesPermissions } from '../../../../common/lib/kibana';
import { APP_ID } from '../../../../../common';
import { useKibana } from '../../../../common/lib/kibana/kibana_react';
/**
* Returns true if the user has read privileges for cases, false otherwise
*/
export const useShowRelatedCases = (): boolean => {
const userCasesPermissions = useGetUserCasesPermissions();
const { cases } = useKibana().services;
const userCasesPermissions = cases.helpers.canUseCases([APP_ID]);
return userCasesPermissions.read;
};

View file

@ -7,14 +7,14 @@
import React from 'react';
import { useGetUserCasesPermissions, useKibana } from '../../../common/lib/kibana';
import { useKibana } from '../../../common/lib/kibana';
import { APP_ID } from '../../../../common/constants';
const MAX_CASES_TO_SHOW = 3;
const RecentCasesComponent = () => {
const { cases } = useKibana().services;
const userCasesPermissions = useGetUserCasesPermissions();
const userCasesPermissions = cases.helpers.canUseCases([APP_ID]);
return cases.ui.getRecentCases({
permissions: userCasesPermissions,

View file

@ -10,7 +10,7 @@ import { mount } from 'enzyme';
import { waitFor } from '@testing-library/react';
import { TestProviders } from '../../../common/mock';
import { Sidebar } from './sidebar';
import { useGetUserCasesPermissions, useKibana } from '../../../common/lib/kibana';
import { useKibana } from '../../../common/lib/kibana';
import type { CaseUiClientMock } from '@kbn/cases-plugin/public/mocks';
import { casesPluginMock } from '@kbn/cases-plugin/public/mocks';
import { noCasesPermissions, readCasesPermissions } from '../../../cases_test_utils';
@ -38,7 +38,7 @@ describe('Sidebar', () => {
});
it('does not render the recently created cases section when the user does not have read permissions', async () => {
(useGetUserCasesPermissions as jest.Mock).mockReturnValue(noCasesPermissions());
casesMock.helpers.canUseCases.mockReturnValue(noCasesPermissions());
await waitFor(() =>
mount(
@ -52,7 +52,7 @@ describe('Sidebar', () => {
});
it('does render the recently created cases section when the user has read permissions', async () => {
(useGetUserCasesPermissions as jest.Mock).mockReturnValue(readCasesPermissions());
casesMock.helpers.canUseCases.mockReturnValue(readCasesPermissions());
await waitFor(() =>
mount(

View file

@ -8,7 +8,12 @@
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import React, { useMemo } from 'react';
import { ENABLE_NEWS_FEED_SETTING, NEWS_FEED_URL_SETTING } from '../../../../common/constants';
import { useKibana } from '../../../common/lib/kibana/kibana_react';
import {
APP_ID,
ENABLE_NEWS_FEED_SETTING,
NEWS_FEED_URL_SETTING,
} from '../../../../common/constants';
import { Filters as RecentTimelinesFilters } from '../recent_timelines/filters';
import { StatefulRecentTimelines } from '../recent_timelines';
import { StatefulNewsFeed } from '../../../common/components/news_feed';
@ -17,7 +22,6 @@ import { SidebarHeader } from '../../../common/components/sidebar_header';
import * as i18n from '../../pages/translations';
import { RecentCases } from '../recent_cases';
import { useGetUserCasesPermissions } from '../../../common/lib/kibana';
const SidebarSpacerComponent = () => (
<EuiFlexItem grow={false}>
@ -30,6 +34,7 @@ export const Sidebar = React.memo<{
recentTimelinesFilterBy: RecentTimelinesFilterMode;
setRecentTimelinesFilterBy: (filterBy: RecentTimelinesFilterMode) => void;
}>(({ recentTimelinesFilterBy, setRecentTimelinesFilterBy }) => {
const { cases } = useKibana().services;
const recentTimelinesFilters = useMemo(
() => (
<RecentTimelinesFilters
@ -41,7 +46,8 @@ export const Sidebar = React.memo<{
);
// only render the recently created cases view if the user has at least read permissions
const hasCasesReadPermissions = useGetUserCasesPermissions().read;
const userCasesPermissions = cases.helpers.canUseCases([APP_ID]);
const hasCasesReadPermissions = userCasesPermissions.read;
return (
<EuiFlexGroup direction="column" responsive={false} gutterSize="l">

View file

@ -30,14 +30,6 @@ jest.mock('../../common/lib/kibana', () => {
return {
...original,
KibanaServices: mockKibanaServices,
useGetUserCasesPermissions: () => ({
all: false,
create: false,
read: true,
update: false,
delete: false,
push: false,
}),
useKibana: jest.fn(),
useUiSetting$: () => ['0,0.[000]'],
};
@ -80,6 +72,16 @@ describe('DataQuality', () => {
hooks: {
useCasesAddToNewCaseFlyout: jest.fn(),
},
helpers: {
canUseCases: jest.fn().mockReturnValue({
all: false,
create: false,
read: true,
update: false,
delete: false,
push: false,
}),
},
},
configSettings: { ILMEnabled: true },
},
@ -307,6 +309,16 @@ describe('DataQuality', () => {
hooks: {
useCasesAddToNewCaseFlyout: jest.fn(),
},
helpers: {
canUseCases: jest.fn().mockReturnValue({
all: false,
create: false,
read: true,
update: false,
delete: false,
push: false,
}),
},
},
configSettings: { ILMEnabled: false },
},

View file

@ -38,15 +38,9 @@ import { HeaderPage } from '../../common/components/header_page';
import { LandingPageComponent } from '../../common/components/landing_page';
import { useLocalStorage } from '../../common/components/local_storage';
import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper';
import { DEFAULT_BYTES_FORMAT, DEFAULT_NUMBER_FORMAT } from '../../../common/constants';
import { APP_ID, DEFAULT_BYTES_FORMAT, DEFAULT_NUMBER_FORMAT } from '../../../common/constants';
import { useSourcererDataView } from '../../common/containers/sourcerer';
import {
KibanaServices,
useGetUserCasesPermissions,
useKibana,
useToasts,
useUiSetting$,
} from '../../common/lib/kibana';
import { KibanaServices, useKibana, useToasts, useUiSetting$ } from '../../common/lib/kibana';
import { SpyRoute } from '../../common/utils/route/spy_routes';
import { useSignalIndex } from '../../detections/containers/detection_engine/alerts/use_signal_index';
import * as i18n from './translations';
@ -141,9 +135,7 @@ const DataQualityComponent: React.FC = () => {
const httpFetch = KibanaServices.get().http.fetch;
const { baseTheme, theme } = useThemes();
const toasts = useToasts();
const {
services: { telemetry },
} = useKibana();
const addSuccessToast = useCallback(
(toast: { title: string }) => {
toasts.addSuccess(toast);
@ -156,7 +148,7 @@ const DataQualityComponent: React.FC = () => {
const [selectedOptions, setSelectedOptions] = useState<EuiComboBoxOptionOption[]>(defaultOptions);
const { indicesExist, loading: isSourcererLoading, selectedPatterns } = useSourcererDataView();
const { signalIndexName, loading: isSignalIndexNameLoading } = useSignalIndex();
const { configSettings, cases } = useKibana().services;
const { configSettings, cases, telemetry } = useKibana().services;
const isILMAvailable = configSettings.ILMEnabled;
const [startDate, setStartTime] = useState<string>();
@ -210,7 +202,7 @@ const DataQualityComponent: React.FC = () => {
key: LOCAL_STORAGE_KEY,
});
const userCasesPermissions = useGetUserCasesPermissions();
const userCasesPermissions = cases.helpers.canUseCases([APP_ID]);
const canUserCreateAndReadCases = useCallback(
() => userCasesPermissions.create && userCasesPermissions.read,
[userCasesPermissions.create, userCasesPermissions.read]

View file

@ -11,6 +11,7 @@ import { render } from '@testing-library/react';
import { DetectionResponse } from './detection_response';
import { TestProviders } from '../../common/mock';
import { noCasesPermissions, readCasesPermissions } from '../../cases_test_utils';
import { useKibana as mockUseKibana } from '../../common/lib/kibana/__mocks__';
jest.mock('../components/detection_response/alerts_by_status', () => ({
AlertsByStatus: () => <div data-test-subj="mock_AlertsByStatus" />,
@ -75,12 +76,24 @@ jest.mock('../../detections/containers/detection_engine/alerts/use_alerts_privil
}));
const defaultUseCasesPermissionsReturn = readCasesPermissions();
const mockUseCasesPermissions = jest.fn(() => defaultUseCasesPermissionsReturn);
jest.mock('../../common/lib/kibana/hooks', () => {
const original = jest.requireActual('../../common/lib/kibana/hooks');
const mockedUseKibana = mockUseKibana();
const mockCanUseCases = jest.fn();
jest.mock('../../common/lib/kibana', () => {
const original = jest.requireActual('../../common/lib/kibana');
return {
...original,
useGetUserCasesPermissions: () => mockUseCasesPermissions(),
useKibana: () => ({
...mockedUseKibana,
services: {
...mockedUseKibana.services,
cases: {
helpers: { canUseCases: mockCanUseCases },
},
},
}),
};
});
@ -90,7 +103,7 @@ describe('DetectionResponse', () => {
mockUseSourcererDataView.mockReturnValue(defaultUseSourcererReturn);
mockUseAlertsPrivileges.mockReturnValue(defaultUseAlertsPrivilegesReturn);
mockUseSignalIndex.mockReturnValue(defaultUseSignalIndexReturn);
mockUseCasesPermissions.mockReturnValue(defaultUseCasesPermissionsReturn);
mockCanUseCases.mockReturnValue(defaultUseCasesPermissionsReturn);
});
it('should render default page', () => {
@ -197,7 +210,7 @@ describe('DetectionResponse', () => {
});
it('should not render cases data sections if the user does not have cases read permission', () => {
mockUseCasesPermissions.mockReturnValue(noCasesPermissions());
mockCanUseCases.mockReturnValue(noCasesPermissions());
const result = render(
<TestProviders>
@ -218,7 +231,7 @@ describe('DetectionResponse', () => {
});
it('should render page permissions message if the user does not have read permission', () => {
mockUseCasesPermissions.mockReturnValue(noCasesPermissions());
mockCanUseCases.mockReturnValue(noCasesPermissions());
mockUseAlertsPrivileges.mockReturnValue({
hasKibanaREAD: true,
hasIndexRead: false,

View file

@ -7,6 +7,7 @@
import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
import type { DocLinks } from '@kbn/doc-links';
import { APP_ID } from '../../../common';
import { InputsModelId } from '../../common/store/inputs/constants';
import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features';
import { SocTrends } from '../components/detection_response/soc_trends';
@ -18,7 +19,6 @@ import { useSourcererDataView } from '../../common/containers/sourcerer';
import { useSignalIndex } from '../../detections/containers/detection_engine/alerts/use_signal_index';
import { useAlertsPrivileges } from '../../detections/containers/detection_engine/alerts/use_alerts_privileges';
import { HeaderPage } from '../../common/components/header_page';
import { useGetUserCasesPermissions } from '../../common/lib/kibana';
import { LandingPageComponent } from '../../common/components/landing_page';
import { AlertsByStatus } from '../components/detection_response/alerts_by_status';
@ -31,13 +31,16 @@ import { CasesByStatus } from '../components/detection_response/cases_by_status'
import { NoPrivileges } from '../../common/components/no_privileges';
import { FiltersGlobal } from '../../common/components/filters_global';
import { useGlobalFilterQuery } from '../../common/hooks/use_global_filter_query';
import { useKibana } from '../../common/lib/kibana';
const DetectionResponseComponent = () => {
const { cases } = useKibana().services;
const { filterQuery } = useGlobalFilterQuery();
const { indicesExist, loading: isSourcererLoading, sourcererDataView } = useSourcererDataView();
const { signalIndexName } = useSignalIndex();
const { hasKibanaREAD, hasIndexRead } = useAlertsPrivileges();
const canReadCases = useGetUserCasesPermissions().read;
const userCasesPermissions = cases.helpers.canUseCases([APP_ID]);
const canReadCases = userCasesPermissions.read;
const canReadAlerts = hasKibanaREAD && hasIndexRead;
const isSocTrendsEnabled = useIsExperimentalFeatureEnabled('socTrendsEnabled');
if (!canReadAlerts && !canReadCases) {

View file

@ -7,19 +7,46 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { useKibana, useGetUserCasesPermissions } from '../../../../common/lib/kibana';
import { TestProviders, mockIndexNames, mockIndexPattern } from '../../../../common/mock';
import { useSourcererDataView } from '../../../../common/containers/sourcerer';
import { allCasesPermissions, readCasesPermissions } from '../../../../cases_test_utils';
import { mockBrowserFields } from '../../../../common/containers/source/mock';
import { TimelineActionMenu } from '.';
import { TimelineId, TimelineTabs } from '../../../../../common/types';
import { useKibana as mockUseKibana } from '../../../../common/lib/kibana/__mocks__';
const mockUseSourcererDataView: jest.Mock = useSourcererDataView as jest.Mock;
const mockedUseKibana = mockUseKibana();
const mockCanUseCases = jest.fn();
jest.mock('../../../../common/containers/sourcerer');
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
jest.mock('../../../../common/lib/kibana');
jest.mock('../../../../common/lib/kibana/kibana_react', () => {
const original = jest.requireActual('../../../../common/lib/kibana/kibana_react');
return {
...original,
useKibana: () => ({
...mockedUseKibana,
services: {
...mockedUseKibana.services,
cases: {
...mockedUseKibana.services.cases,
helpers: { canUseCases: mockCanUseCases },
},
},
application: {
capabilities: {
navLinks: {},
management: {},
catalogue: {},
actions: { show: true, crud: true },
},
},
}),
};
});
jest.mock('@kbn/i18n-react', () => {
const originalModule = jest.requireActual('@kbn/i18n-react');
const FormattedRelative = jest.fn().mockImplementation(() => '20 hours ago');
@ -41,20 +68,15 @@ describe('Action menu', () => {
beforeEach(() => {
// Mocking these services is required for the header component to render.
mockUseSourcererDataView.mockImplementation(() => sourcererDefaultValue);
useKibanaMock().services.application.capabilities = {
navLinks: {},
management: {},
catalogue: {},
actions: { show: true, crud: true },
};
});
afterEach(() => {
jest.clearAllMocks();
});
describe('AddToCaseButton', () => {
it('renders the button when the user has create and read permissions', () => {
(useGetUserCasesPermissions as jest.Mock).mockReturnValue(allCasesPermissions());
mockCanUseCases.mockReturnValue(allCasesPermissions());
render(
<TestProviders>
@ -70,7 +92,7 @@ describe('Action menu', () => {
});
it('does not render the button when the user does not have create permissions', () => {
(useGetUserCasesPermissions as jest.Mock).mockReturnValue(readCasesPermissions());
mockCanUseCases.mockReturnValue(readCasesPermissions());
render(
<TestProviders>

View file

@ -7,7 +7,8 @@
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React from 'react';
import { useGetUserCasesPermissions } from '../../../../common/lib/kibana';
import { useKibana } from '../../../../common/lib/kibana/kibana_react';
import { APP_ID } from '../../../../../common';
import type { TimelineTabs } from '../../../../../common/types';
import { InspectButton } from '../../../../common/components/inspect';
import { InputsModelId } from '../../../../common/store/inputs/constants';
@ -29,7 +30,9 @@ const TimelineActionMenuComponent = ({
activeTab,
isInspectButtonDisabled,
}: TimelineActionMenuProps) => {
const userCasesPermissions = useGetUserCasesPermissions();
const { cases } = useKibana().services;
const userCasesPermissions = cases.helpers.canUseCases([APP_ID]);
return (
<EuiFlexGroup
gutterSize="xs"

View file

@ -10,7 +10,7 @@ import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl';
import { useKibana, useGetUserCasesPermissions } from '../../../../common/lib/kibana';
import { useKibana } from '../../../../common/lib/kibana';
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
import { mockTimelineModel, TestProviders } from '../../../../common/mock';
import { AddToCaseButton } from '.';
@ -36,13 +36,6 @@ jest.mock('react-redux', () => {
});
jest.mock('../../../../common/lib/kibana');
const originalKibanaLib = jest.requireActual('../../../../common/lib/kibana');
// Restore the useGetUserCasesPermissions so the calling functions can receive a valid permissions object
// The returned permissions object will indicate that the user does not have permissions by default
const mockUseGetUserCasesPermissions = useGetUserCasesPermissions as jest.Mock;
mockUseGetUserCasesPermissions.mockImplementation(originalKibanaLib.useGetUserCasesPermissions);
jest.mock('../../../../common/hooks/use_selector');
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;

View file

@ -15,7 +15,7 @@ import { APP_ID, APP_UI_ID } from '../../../../../common/constants';
import { timelineSelectors } from '../../../store/timeline';
import { setInsertTimeline, showTimeline } from '../../../store/timeline/actions';
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
import { useGetUserCasesPermissions, useKibana } from '../../../../common/lib/kibana';
import { useKibana } from '../../../../common/lib/kibana';
import { TimelineId } from '../../../../../common/types/timeline';
import { TimelineStatus, TimelineType } from '../../../../../common/api/timeline';
import { getCreateCaseUrl, getCaseDetailsUrl } from '../../../../common/components/link_to';
@ -68,7 +68,7 @@ const AddToCaseButtonComponent: React.FC<Props> = ({ timelineId }) => {
[dispatch, graphEventId, navigateToApp, savedObjectId, timelineId, timelineTitle]
);
const userCasesPermissions = useGetUserCasesPermissions();
const userCasesPermissions = cases.helpers.canUseCases([APP_ID]);
const handleButtonClick = useCallback(() => {
setPopover((currentIsOpen) => !currentIsOpen);

View file

@ -13,11 +13,7 @@ import { TimelineId } from '../../../../../../common/types/timeline';
import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
import { mockAlertDetailsData } from '../../../../../common/components/event_details/__mocks__';
import type { TimelineEventsDetailsItem } from '../../../../../../common/search_strategy';
import {
KibanaServices,
useGetUserCasesPermissions,
useKibana,
} from '../../../../../common/lib/kibana';
import { KibanaServices, useKibana } from '../../../../../common/lib/kibana';
import { coreMock } from '@kbn/core/public/mocks';
import { mockCasesContract } from '@kbn/cases-plugin/public/mocks';
@ -70,12 +66,6 @@ jest.mock('../../../../../detections/components/user_info', () => ({
}));
jest.mock('../../../../../common/lib/kibana');
const originalKibanaLib = jest.requireActual('../../../../../common/lib/kibana');
// Restore the useGetUserCasesPermissions so the calling functions can receive a valid permissions object
// The returned permissions object will indicate that the user does not have permissions by default
const mockUseGetUserCasesPermissions = useGetUserCasesPermissions as jest.Mock;
mockUseGetUserCasesPermissions.mockImplementation(originalKibanaLib.useGetUserCasesPermissions);
jest.mock(
'../../../../../detections/containers/detection_engine/alerts/use_alerts_privileges',

View file

@ -11,11 +11,7 @@ import '../../../../common/mock/match_media';
import { TestProviders } from '../../../../common/mock';
import { TimelineId, TimelineTabs } from '../../../../../common/types/timeline';
import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
import {
KibanaServices,
useKibana,
useGetUserCasesPermissions,
} from '../../../../common/lib/kibana';
import { KibanaServices, useKibana } from '../../../../common/lib/kibana';
import { mockBrowserFields, mockRuntimeMappings } from '../../../../common/containers/source/mock';
import { coreMock } from '@kbn/core/public/mocks';
import { mockCasesContext } from '@kbn/cases-plugin/public/mocks/mock_cases_context';
@ -156,6 +152,9 @@ describe('event details panel component', () => {
ui: {
getCasesContext: () => mockCasesContext,
},
cases: {
helpers: { canUseCases: jest.fn().mockReturnValue(allCasesPermissions()) },
},
},
timelines: {
getHoverActions: jest.fn().mockReturnValue({
@ -168,11 +167,12 @@ describe('event details panel component', () => {
},
},
});
(useGetUserCasesPermissions as jest.Mock).mockReturnValue(allCasesPermissions());
});
afterEach(() => {
jest.clearAllMocks();
});
test('it renders the take action dropdown in the timeline version', () => {
const wrapper = render(
<TestProviders>

View file

@ -32,7 +32,6 @@ import { defaultRowRenderers } from './body/renderers';
import { useSourcererDataView } from '../../../common/containers/sourcerer';
import { createStore } from '../../../common/store';
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
import { useGetUserCasesPermissions } from '../../../common/lib/kibana';
jest.mock('../../containers', () => ({
useTimelineEvents: jest.fn(),
@ -43,12 +42,6 @@ jest.mock('./tabs_content', () => ({
}));
jest.mock('../../../common/lib/kibana');
const originalKibanaLib = jest.requireActual('../../../common/lib/kibana');
// Restore the useGetUserCasesPermissions so the calling functions can receive a valid permissions object
// The returned permissions object will indicate that the user does not have permissions by default
const mockUseGetUserCasesPermissions = useGetUserCasesPermissions as jest.Mock;
mockUseGetUserCasesPermissions.mockImplementation(originalKibanaLib.useGetUserCasesPermissions);
jest.mock('../../../common/utils/normalize_time_range');
jest.mock('@kbn/i18n-react', () => {

View file

@ -22,8 +22,22 @@ export default function ({ getService }: FtrProviderContext) {
savedObjectsTagging: ['all', 'read', 'minimal_all', 'minimal_read'],
canvas: ['all', 'read', 'minimal_all', 'minimal_read'],
maps: ['all', 'read', 'minimal_all', 'minimal_read'],
generalCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'],
observabilityCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'],
generalCases: [
'all',
'read',
'minimal_all',
'minimal_read',
'cases_delete',
'cases_settings',
],
observabilityCases: [
'all',
'read',
'minimal_all',
'minimal_read',
'cases_delete',
'cases_settings',
],
observabilityAIAssistant: ['all', 'read', 'minimal_all', 'minimal_read'],
slo: ['all', 'read', 'minimal_all', 'minimal_read'],
fleetv2: ['all', 'read', 'minimal_all', 'minimal_read'],
@ -57,7 +71,14 @@ export default function ({ getService }: FtrProviderContext) {
],
uptime: ['all', 'read', 'minimal_all', 'minimal_read', 'elastic_managed_locations_enabled'],
securitySolutionAssistant: ['all', 'read', 'minimal_all', 'minimal_read'],
securitySolutionCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'],
securitySolutionCases: [
'all',
'read',
'minimal_all',
'minimal_read',
'cases_delete',
'cases_settings',
],
infrastructure: ['all', 'read', 'minimal_all', 'minimal_read'],
logs: ['all', 'read', 'minimal_all', 'minimal_read'],
apm: ['all', 'read', 'minimal_all', 'minimal_read'],

View file

@ -98,8 +98,22 @@ export default function ({ getService }: FtrProviderContext) {
savedObjectsTagging: ['all', 'read', 'minimal_all', 'minimal_read'],
canvas: ['all', 'read', 'minimal_all', 'minimal_read'],
maps: ['all', 'read', 'minimal_all', 'minimal_read'],
generalCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'],
observabilityCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'],
generalCases: [
'all',
'read',
'minimal_all',
'minimal_read',
'cases_delete',
'cases_settings',
],
observabilityCases: [
'all',
'read',
'minimal_all',
'minimal_read',
'cases_delete',
'cases_settings',
],
observabilityAIAssistant: ['all', 'read', 'minimal_all', 'minimal_read'],
slo: ['all', 'read', 'minimal_all', 'minimal_read'],
fleetv2: ['all', 'read', 'minimal_all', 'minimal_read'],
@ -139,7 +153,14 @@ export default function ({ getService }: FtrProviderContext) {
'minimal_read',
],
securitySolutionAssistant: ['all', 'read', 'minimal_all', 'minimal_read'],
securitySolutionCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'],
securitySolutionCases: [
'all',
'read',
'minimal_all',
'minimal_read',
'cases_delete',
'cases_settings',
],
infrastructure: ['all', 'read', 'minimal_all', 'minimal_read'],
logs: ['all', 'read', 'minimal_all', 'minimal_read'],
apm: ['all', 'read', 'minimal_all', 'minimal_read'],

View file

@ -68,13 +68,13 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
},
subFeatures: [
{
name: 'Custom privileges',
name: 'Delete',
privilegeGroups: [
{
groupType: 'independent',
privileges: [
{
name: 'Delete',
name: 'Delete cases and comments',
id: 'cases_delete',
includeIn: 'all',
cases: {
@ -90,6 +90,29 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
},
],
},
{
name: 'Case Settings',
privilegeGroups: [
{
groupType: 'independent',
privileges: [
{
name: 'Edit Case Settings',
id: 'cases_settings',
includeIn: 'all',
cases: {
settings: ['securitySolutionFixture'],
},
savedObject: {
all: [...filesSavedObjectTypes],
read: [...filesSavedObjectTypes],
},
ui: [],
},
],
},
],
},
],
});

View file

@ -43,6 +43,7 @@ const permissions = {
delete: true,
push: true,
connectors: true,
settings: true,
};
const attachments = [{ type: AttachmentType.user as const, comment: 'test' }];