mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
# 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:
parent
43f84bb5c9
commit
886f509607
8 changed files with 146 additions and 64 deletions
|
@ -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(
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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,
|
||||
];
|
||||
|
|
|
@ -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'));
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue