[8.16] [ResponseOps][Cases] Fix edit cases settings privilege (#202053) (#202987)

# Backport

This will backport the following commits from `main` to `8.16`:
- [[ResponseOps][Cases] Fix edit cases settings privilege
(#202053)](https://github.com/elastic/kibana/pull/202053)

<!--- Backport version: 8.9.8 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Janki
Salvi","email":"117571355+js-jankisalvi@users.noreply.github.com"},"sourceCommit":{"committedDate":"2024-12-04T15:55:08Z","message":"[ResponseOps][Cases]
Fix edit cases settings privilege (#202053)\n\n## Summary\r\n\r\nFixes
https://github.com/elastic/kibana/issues/197650\r\n\r\nAlso fixes an
issue where user has `cases: all ` and `edit case\r\nsettings: false`,
user was able to edit settings.\r\n\r\nUsed `permissions.settings`
instead of `permissions.update` and\r\n`permissions.create` for custom
fields and templates.\r\n\r\n### How to test\r\n- Verify by creating a
user with different combinations of cases and\r\nedit case settings
privileges\r\n\r\n### Checklist\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios","sha":"8e8ba53116c16cc9b9122de27415cf8519cc1863","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:skip","Team:ResponseOps","v9.0.0","Feature:Cases","backport:prev-minor","v8.17.0","v8.18.0","v8.16.2"],"number":202053,"url":"https://github.com/elastic/kibana/pull/202053","mergeCommit":{"message":"[ResponseOps][Cases]
Fix edit cases settings privilege (#202053)\n\n## Summary\r\n\r\nFixes
https://github.com/elastic/kibana/issues/197650\r\n\r\nAlso fixes an
issue where user has `cases: all ` and `edit case\r\nsettings: false`,
user was able to edit settings.\r\n\r\nUsed `permissions.settings`
instead of `permissions.update` and\r\n`permissions.create` for custom
fields and templates.\r\n\r\n### How to test\r\n- Verify by creating a
user with different combinations of cases and\r\nedit case settings
privileges\r\n\r\n### Checklist\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios","sha":"8e8ba53116c16cc9b9122de27415cf8519cc1863"}},"sourceBranch":"main","suggestedTargetBranches":["8.16"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","labelRegex":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/202053","number":202053,"mergeCommit":{"message":"[ResponseOps][Cases]
Fix edit cases settings privilege (#202053)\n\n## Summary\r\n\r\nFixes
https://github.com/elastic/kibana/issues/197650\r\n\r\nAlso fixes an
issue where user has `cases: all ` and `edit case\r\nsettings: false`,
user was able to edit settings.\r\n\r\nUsed `permissions.settings`
instead of `permissions.update` and\r\n`permissions.create` for custom
fields and templates.\r\n\r\n### How to test\r\n- Verify by creating a
user with different combinations of cases and\r\nedit case settings
privileges\r\n\r\n### Checklist\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios","sha":"8e8ba53116c16cc9b9122de27415cf8519cc1863"}},{"branch":"8.17","label":"v8.17.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"url":"https://github.com/elastic/kibana/pull/202970","number":202970,"state":"OPEN"},{"branch":"8.x","label":"v8.18.0","labelRegex":"^v8.18.0$","isSourceBranch":false,"url":"https://github.com/elastic/kibana/pull/202971","number":202971,"state":"OPEN"},{"branch":"8.16","label":"v8.16.2","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->
This commit is contained in:
Janki Salvi 2024-12-05 13:07:19 +00:00 committed by GitHub
parent 43f84bb5c9
commit 886f509607
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 146 additions and 64 deletions

View file

@ -12,7 +12,7 @@ import { waitFor, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ConfigureCases } from '.';
import { noUpdateCasesPermissions, TestProviders, createAppMockRenderer } from '../../common/mock';
import { noCasesSettingsPermission, TestProviders, createAppMockRenderer } from '../../common/mock';
import { customFieldsConfigurationMock, templatesConfigurationMock } from '../../containers/mock';
import type { AppMockRenderer } from '../../common/mock';
import { Connectors } from './connectors';
@ -200,10 +200,10 @@ describe('ConfigureCases', () => {
expect(wrapper.find('[data-test-subj="edit-connector-flyout"]').exists()).toBe(false);
});
test('it disables correctly when the user cannot update', () => {
test('it disables correctly when the user does not have settings privilege', () => {
const newWrapper = mount(<ConfigureCases />, {
wrappingComponent: TestProviders as ComponentType<React.PropsWithChildren<{}>>,
wrappingComponentProps: { permissions: noUpdateCasesPermissions() },
wrappingComponentProps: { permissions: noCasesSettingsPermission() },
});
expect(newWrapper.find('button[data-test-subj="dropdown-connectors"]').prop('disabled')).toBe(

View file

@ -476,12 +476,7 @@ export const ConfigureCases: React.FC = React.memo(() => {
flyOutVisibility?.type === 'customField' && flyOutVisibility?.visible ? (
<CommonFlyout<CustomFieldConfiguration>
isLoading={loadingCaseConfigure || isPersistingConfiguration}
disabled={
!permissions.create ||
!permissions.update ||
loadingCaseConfigure ||
isPersistingConfiguration
}
disabled={!permissions.settings || loadingCaseConfigure || isPersistingConfiguration}
onCloseFlyout={onCloseCustomFieldFlyout}
onSaveField={onCustomFieldSave}
renderHeader={() => (
@ -498,12 +493,7 @@ export const ConfigureCases: React.FC = React.memo(() => {
flyOutVisibility?.type === 'template' && flyOutVisibility?.visible ? (
<CommonFlyout<TemplateFormProps, TemplateConfiguration>
isLoading={loadingCaseConfigure || isPersistingConfiguration}
disabled={
!permissions.create ||
!permissions.update ||
loadingCaseConfigure ||
isPersistingConfiguration
}
disabled={!permissions.settings || loadingCaseConfigure || isPersistingConfiguration}
onCloseFlyout={onCloseTemplateFlyout}
onSaveField={onTemplateSave}
renderHeader={() => (
@ -561,7 +551,9 @@ export const ConfigureCases: React.FC = React.memo(() => {
<div css={sectionWrapperCss}>
<ClosureOptions
closureTypeSelected={closureType}
disabled={isPersistingConfiguration || isLoadingConnectors || !permissions.update}
disabled={
isPersistingConfiguration || isLoadingConnectors || !permissions.settings
}
onChangeClosureType={onChangeClosureType}
/>
</div>
@ -570,13 +562,15 @@ export const ConfigureCases: React.FC = React.memo(() => {
<Connectors
actionTypes={actionTypes}
connectors={connectors ?? []}
disabled={isPersistingConfiguration || isLoadingConnectors || !permissions.update}
disabled={
isPersistingConfiguration || isLoadingConnectors || !permissions.settings
}
handleShowEditFlyout={onClickUpdateConnector}
isLoading={isLoadingAny}
mappings={mappings}
onChangeConnector={onChangeConnector}
selectedConnector={connector}
updateConnectorDisabled={updateConnectorDisabled || !permissions.update}
updateConnectorDisabled={updateConnectorDisabled || !permissions.settings}
/>
</div>
<EuiSpacer size="xl" />

View file

@ -17,7 +17,6 @@ import {
} from '@elastic/eui';
import * as i18n from './translations';
import { useCasesContext } from '../cases_context/use_cases_context';
import type { CustomFieldsConfiguration } from '../../../common/types/domain';
import { MAX_CUSTOM_FIELDS_PER_CASE } from '../../../common/constants';
import { CustomFieldsList } from './custom_fields_list';
@ -38,8 +37,6 @@ const CustomFieldsComponent: React.FC<Props> = ({
handleEditCustomField,
customFields,
}) => {
const { permissions } = useCasesContext();
const canAddCustomFields = permissions.create && permissions.update;
const [error, setError] = useState<boolean>(false);
const onAddCustomField = useCallback(() => {
@ -64,7 +61,7 @@ const CustomFieldsComponent: React.FC<Props> = ({
setError(false);
}
return canAddCustomFields ? (
return (
<EuiDescribedFormGroup
fullWidth
title={<h3>{i18n.TITLE}</h3>}
@ -113,10 +110,11 @@ const CustomFieldsComponent: React.FC<Props> = ({
)}
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
</EuiPanel>
</EuiDescribedFormGroup>
) : null;
);
};
CustomFieldsComponent.displayName = 'CustomFields';

View file

@ -17,7 +17,6 @@ import {
} from '@elastic/eui';
import { MAX_TEMPLATES_LENGTH } from '../../../common/constants';
import type { CasesConfigurationUITemplate } from '../../../common/ui';
import { useCasesContext } from '../cases_context/use_cases_context';
import { ExperimentalBadge } from '../experimental_badge/experimental_badge';
import * as i18n from './translations';
import { TemplatesList } from './templates_list';
@ -39,8 +38,6 @@ const TemplatesComponent: React.FC<Props> = ({
onEditTemplate,
onDeleteTemplate,
}) => {
const { permissions } = useCasesContext();
const canAddTemplates = permissions.create && permissions.update;
const [error, setError] = useState<boolean>(false);
const handleAddTemplate = useCallback(() => {
@ -103,31 +100,29 @@ const TemplatesComponent: React.FC<Props> = ({
</EuiFlexItem>
</EuiFlexGroup>
) : null}
{canAddTemplates ? (
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false}>
{templates.length < MAX_TEMPLATES_LENGTH ? (
<EuiButtonEmpty
isLoading={isLoading}
isDisabled={disabled || error}
size="s"
onClick={handleAddTemplate}
iconType="plusInCircle"
data-test-subj="add-template"
>
{i18n.ADD_TEMPLATE}
</EuiButtonEmpty>
) : (
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false}>
<EuiText>{i18n.MAX_TEMPLATE_LIMIT(MAX_TEMPLATES_LENGTH)}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
)}
<EuiSpacer size="s" />
</EuiFlexItem>
</EuiFlexGroup>
) : null}
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false}>
{templates.length < MAX_TEMPLATES_LENGTH ? (
<EuiButtonEmpty
isLoading={isLoading}
isDisabled={disabled || error}
size="s"
onClick={handleAddTemplate}
iconType="plusInCircle"
data-test-subj="add-template"
>
{i18n.ADD_TEMPLATE}
</EuiButtonEmpty>
) : (
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false}>
<EuiText>{i18n.MAX_TEMPLATE_LIMIT(MAX_TEMPLATES_LENGTH)}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
)}
<EuiSpacer size="s" />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiDescribedFormGroup>
);

View file

@ -59,6 +59,30 @@ export const casesNoDelete: Role = {
},
};
export const casesReadAndEditSettings: Role = {
name: 'cases_read_and_edit_settings',
privileges: {
elasticsearch: {
indices: [
{
names: ['*'],
privileges: ['all'],
},
],
},
kibana: [
{
feature: {
generalCases: ['minimal_read', 'cases_settings'],
actions: ['all'],
actionsSimulators: ['all'],
},
spaces: ['*'],
},
],
},
};
export const casesAll: Role = {
name: 'cases_all_role',
privileges: {
@ -83,4 +107,4 @@ export const casesAll: Role = {
},
};
export const roles = [casesReadDelete, casesNoDelete, casesAll];
export const roles = [casesReadDelete, casesNoDelete, casesAll, casesReadAndEditSettings];

View file

@ -6,7 +6,7 @@
*/
import { User } from '../../../../cases_api_integration/common/lib/authentication/types';
import { casesAll, casesNoDelete, casesReadDelete } from './roles';
import { casesAll, casesNoDelete, casesReadDelete, casesReadAndEditSettings } from './roles';
/**
* Users for Cases in the Stack
@ -18,12 +18,6 @@ export const casesReadDeleteUser: User = {
roles: [casesReadDelete.name],
};
export const casesNoDeleteUser: User = {
username: 'cases_no_delete_user',
password: 'password',
roles: [casesNoDelete.name],
};
export const casesAllUser: User = {
username: 'cases_all_user',
password: 'password',
@ -36,4 +30,22 @@ export const casesAllUser2: User = {
roles: [casesAll.name],
};
export const users = [casesReadDeleteUser, casesNoDeleteUser, casesAllUser, casesAllUser2];
export const casesReadAndEditSettingsUser: User = {
username: 'cases_read_and_edit_settings_user',
password: 'password',
roles: [casesReadAndEditSettings.name],
};
export const casesNoDeleteUser: User = {
username: 'cases_no_delete_user',
password: 'password',
roles: [casesNoDelete.name],
};
export const users = [
casesReadDeleteUser,
casesNoDeleteUser,
casesAllUser,
casesAllUser2,
casesReadAndEditSettingsUser,
];

View file

@ -11,6 +11,6 @@ export default ({ loadTestFile }: FtrProviderContext) => {
describe('Cases - group 1', function () {
loadTestFile(require.resolve('./create_case_form'));
loadTestFile(require.resolve('./view_case'));
loadTestFile(require.resolve('./deletion'));
loadTestFile(require.resolve('./sub_privileges'));
});
};

View file

@ -5,8 +5,16 @@
* 2.0.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
import { users, roles, casesReadDeleteUser, casesAllUser, casesNoDeleteUser } from '../common';
import {
users,
roles,
casesReadDeleteUser,
casesAllUser,
casesNoDeleteUser,
casesReadAndEditSettingsUser,
} from '../common';
import {
createUsersAndRoles,
deleteUsersAndRoles,
@ -16,8 +24,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
const PageObjects = getPageObjects(['security', 'header']);
const testSubjects = getService('testSubjects');
const cases = getService('cases');
const toasts = getService('toasts');
describe('cases deletion sub privilege', () => {
describe('cases sub privilege', () => {
before(async () => {
await createUsersAndRoles(getService, users, roles);
await PageObjects.security.forceLogout();
@ -29,7 +38,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await PageObjects.security.forceLogout();
});
describe('create two cases', () => {
describe('cases_delete', () => {
beforeEach(async () => {
await cases.api.createNthRandomCases(2);
});
@ -119,6 +128,56 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
});
}
});
describe('cases_settings', () => {
afterEach(async () => {
await cases.api.deleteAllCases();
});
for (const user of [casesReadAndEditSettingsUser, casesAllUser]) {
describe(`logging in with user ${user.username}`, () => {
before(async () => {
await PageObjects.security.login(user.username, user.password);
});
after(async () => {
await PageObjects.security.forceLogout();
});
it(`User ${user.username} can navigate to settings`, async () => {
await cases.navigation.navigateToConfigurationPage();
});
it(`User ${user.username} can update settings`, async () => {
await cases.common.selectRadioGroupValue(
'closure-options-radio-group',
'close-by-pushing'
);
const toast = await toasts.getElementByIndex(1);
expect(await toast.getVisibleText()).to.be('Settings successfully updated');
await toasts.dismissAll();
});
});
}
// below users do not have access to settings
for (const user of [casesNoDeleteUser, casesReadDeleteUser]) {
describe(`cannot access settings page with user ${user.username}`, () => {
before(async () => {
await PageObjects.security.login(user.username, user.password);
});
after(async () => {
await PageObjects.security.forceLogout();
});
it(`User ${user.username} cannot navigate to settings`, async () => {
await cases.navigation.navigateToApp();
await testSubjects.missingOrFail('configure-case-button');
});
});
}
});
});
};