mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[8.x] [Cases] [Security Solution] New cases subfeatures, add comments and reopen cases (#194898) (#200807)
# Backport This will backport the following commits from `main` to `8.x`: - [[Cases] [Security Solution] New cases subfeatures, add comments and reopen cases (#194898)](https://github.com/elastic/kibana/pull/194898) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Kevin Qualters","email":"56408403+kqualters-elastic@users.noreply.github.com"},"sourceCommit":{"committedDate":"2024-11-19T19:15:38Z","message":"[Cases] [Security Solution] New cases subfeatures, add comments and reopen cases (#194898)\n\n## Summary\r\n\r\nThis pr adds 2 new sub feature permissions to the cases plugin in\r\nstack/security/observability, that behave as follows. The first is for\r\ncontrolling the ability to reopen cases. When Cases has the read\r\npermission, and the reopen permission is not enabled, users have\r\npermissions as before. When enabled, users can move cases from closed to\r\nopen/in progress, but nothing else. If a user has all and this\r\npermission, they can do anything as before, if the option is unselected,\r\nthey can change case properties, and change a case from open to\r\nanything, in progress to anything, but if the case is closed, are unable\r\nto reopen it.\r\n\r\nThe 2nd permission is 'Add comment'. When enabled and the user has case\r\nread permissions, users can add comments, but not make any other changes\r\nto the case. When the user has read and this deselected, read functions\r\nas before. When a user has this permission and cases is all, this\r\nfunctions as all. When they have all but this permission is deselected,\r\nthe user can do everything normally, except add cases comments.\r\n\r\n### Checklist\r\n\r\n- [x] Any text added follows [EUI's writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing), uses\r\nsentence case text and includes [i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n- [ ]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas added for features that require explanation or tutorials\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\r\n\r\n---------\r\n\r\nCo-authored-by: Michael Olorunnisola <michael.olorunnisola@elastic.co>\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"0afae423443ba13c47a263c4cbc270ea09942148","branchLabelMapping":{"^v9.0.0$":"main","^v8.17.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["v9.0.0","release_note:feature","ci:project-deploy-observability","Team:obs-ux-management","apm:review","backport:version","v8.17.0"],"title":"[Cases] [Security Solution] New cases subfeatures, add comments and reopen cases","number":194898,"url":"https://github.com/elastic/kibana/pull/194898","mergeCommit":{"message":"[Cases] [Security Solution] New cases subfeatures, add comments and reopen cases (#194898)\n\n## Summary\r\n\r\nThis pr adds 2 new sub feature permissions to the cases plugin in\r\nstack/security/observability, that behave as follows. The first is for\r\ncontrolling the ability to reopen cases. When Cases has the read\r\npermission, and the reopen permission is not enabled, users have\r\npermissions as before. When enabled, users can move cases from closed to\r\nopen/in progress, but nothing else. If a user has all and this\r\npermission, they can do anything as before, if the option is unselected,\r\nthey can change case properties, and change a case from open to\r\nanything, in progress to anything, but if the case is closed, are unable\r\nto reopen it.\r\n\r\nThe 2nd permission is 'Add comment'. When enabled and the user has case\r\nread permissions, users can add comments, but not make any other changes\r\nto the case. When the user has read and this deselected, read functions\r\nas before. When a user has this permission and cases is all, this\r\nfunctions as all. When they have all but this permission is deselected,\r\nthe user can do everything normally, except add cases comments.\r\n\r\n### Checklist\r\n\r\n- [x] Any text added follows [EUI's writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing), uses\r\nsentence case text and includes [i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n- [ ]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas added for features that require explanation or tutorials\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\r\n\r\n---------\r\n\r\nCo-authored-by: Michael Olorunnisola <michael.olorunnisola@elastic.co>\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"0afae423443ba13c47a263c4cbc270ea09942148"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/194898","number":194898,"mergeCommit":{"message":"[Cases] [Security Solution] New cases subfeatures, add comments and reopen cases (#194898)\n\n## Summary\r\n\r\nThis pr adds 2 new sub feature permissions to the cases plugin in\r\nstack/security/observability, that behave as follows. The first is for\r\ncontrolling the ability to reopen cases. When Cases has the read\r\npermission, and the reopen permission is not enabled, users have\r\npermissions as before. When enabled, users can move cases from closed to\r\nopen/in progress, but nothing else. If a user has all and this\r\npermission, they can do anything as before, if the option is unselected,\r\nthey can change case properties, and change a case from open to\r\nanything, in progress to anything, but if the case is closed, are unable\r\nto reopen it.\r\n\r\nThe 2nd permission is 'Add comment'. When enabled and the user has case\r\nread permissions, users can add comments, but not make any other changes\r\nto the case. When the user has read and this deselected, read functions\r\nas before. When a user has this permission and cases is all, this\r\nfunctions as all. When they have all but this permission is deselected,\r\nthe user can do everything normally, except add cases comments.\r\n\r\n### Checklist\r\n\r\n- [x] Any text added follows [EUI's writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing), uses\r\nsentence case text and includes [i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n- [ ]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas added for features that require explanation or tutorials\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\r\n\r\n---------\r\n\r\nCo-authored-by: Michael Olorunnisola <michael.olorunnisola@elastic.co>\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"0afae423443ba13c47a263c4cbc270ea09942148"}},{"branch":"8.x","label":"v8.17.0","branchLabelMappingKey":"^v8.17.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> Co-authored-by: Kevin Qualters <56408403+kqualters-elastic@users.noreply.github.com>
This commit is contained in:
parent
4a9f70d814
commit
c501d2f589
145 changed files with 3541 additions and 516 deletions
|
@ -46,7 +46,7 @@ viewer:
|
|||
- feature_siem.read
|
||||
- feature_siem.read_alerts
|
||||
- feature_siem.endpoint_list_read
|
||||
- feature_securitySolutionCases.read
|
||||
- feature_securitySolutionCasesV2.read
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.read
|
||||
|
@ -126,7 +126,7 @@ editor:
|
|||
- feature_siem.process_operations_all
|
||||
- feature_siem.actions_log_management_all # Response actions history
|
||||
- feature_siem.file_operations_all
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionCasesV2.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.read
|
||||
|
@ -175,7 +175,7 @@ t1_analyst:
|
|||
- feature_siem.read
|
||||
- feature_siem.read_alerts
|
||||
- feature_siem.endpoint_list_read
|
||||
- feature_securitySolutionCases.read
|
||||
- feature_securitySolutionCasesV2.read
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.read
|
||||
|
@ -230,7 +230,7 @@ t2_analyst:
|
|||
- feature_siem.read
|
||||
- feature_siem.read_alerts
|
||||
- feature_siem.endpoint_list_read
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionCasesV2.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.read
|
||||
|
@ -300,7 +300,7 @@ t3_analyst:
|
|||
- feature_siem.actions_log_management_all # Response actions history
|
||||
- feature_siem.file_operations_all
|
||||
- feature_siem.scan_operations_all
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionCasesV2.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.read
|
||||
|
@ -362,7 +362,7 @@ threat_intelligence_analyst:
|
|||
- feature_siem.all
|
||||
- feature_siem.endpoint_list_read
|
||||
- feature_siem.blocklist_all
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionCasesV2.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.read
|
||||
|
@ -430,7 +430,7 @@ rule_author:
|
|||
- feature_siem.host_isolation_exceptions_read
|
||||
- feature_siem.blocklist_all # Elastic Defend Policy Management
|
||||
- feature_siem.actions_log_management_read
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionCasesV2.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.read
|
||||
|
@ -502,7 +502,7 @@ soc_manager:
|
|||
- feature_siem.file_operations_all
|
||||
- feature_siem.execute_operations_all
|
||||
- feature_siem.scan_operations_all
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionCasesV2.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.all
|
||||
|
@ -562,7 +562,7 @@ detections_admin:
|
|||
- feature_siem.all
|
||||
- feature_siem.read_alerts
|
||||
- feature_siem.crud_alerts
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionCasesV2.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.all
|
||||
|
@ -621,7 +621,7 @@ platform_engineer:
|
|||
- feature_siem.host_isolation_exceptions_all
|
||||
- feature_siem.blocklist_all # Elastic Defend Policy Management
|
||||
- feature_siem.actions_log_management_read
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionCasesV2.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.all
|
||||
|
@ -694,7 +694,7 @@ endpoint_operations_analyst:
|
|||
- feature_siem.file_operations_all
|
||||
- feature_siem.execute_operations_all
|
||||
- feature_siem.scan_operations_all
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionCasesV2.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.all
|
||||
|
@ -769,7 +769,7 @@ endpoint_policy_manager:
|
|||
- feature_siem.event_filters_all
|
||||
- feature_siem.host_isolation_exceptions_all
|
||||
- feature_siem.blocklist_all # Elastic Defend Policy Management
|
||||
- feature_securitySolutionCases.all
|
||||
- feature_securitySolutionCasesV2.all
|
||||
- feature_securitySolutionAssistant.all
|
||||
- feature_securitySolutionAttackDiscovery.all
|
||||
- feature_actions.all
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
"siem": ["read", "read_alerts"],
|
||||
"securitySolutionAssistant": ["all"],
|
||||
"securitySolutionAttackDiscovery": ["all"],
|
||||
"securitySolutionCases": ["read"],
|
||||
"securitySolutionCasesV2": ["read"],
|
||||
"actions": ["read"],
|
||||
"builtInAlerts": ["read"]
|
||||
},
|
||||
|
@ -82,7 +82,7 @@
|
|||
"siem": ["read", "read_alerts"],
|
||||
"securitySolutionAssistant": ["all"],
|
||||
"securitySolutionAttackDiscovery": ["all"],
|
||||
"securitySolutionCases": ["read"],
|
||||
"securitySolutionCasesV2": ["read"],
|
||||
"actions": ["read"],
|
||||
"builtInAlerts": ["read"]
|
||||
},
|
||||
|
@ -150,7 +150,7 @@
|
|||
"actions_log_management_all",
|
||||
"file_operations_all"
|
||||
],
|
||||
"securitySolutionCases": ["all"],
|
||||
"securitySolutionCasesV2": ["all"],
|
||||
"securitySolutionAssistant": ["all"],
|
||||
"securitySolutionAttackDiscovery": ["all"],
|
||||
"actions": ["read"],
|
||||
|
@ -210,7 +210,7 @@
|
|||
"siem": ["all", "read_alerts", "crud_alerts"],
|
||||
"securitySolutionAssistant": ["all"],
|
||||
"securitySolutionAttackDiscovery": ["all"],
|
||||
"securitySolutionCases": ["all"],
|
||||
"securitySolutionCasesV2": ["all"],
|
||||
"actions": ["read"],
|
||||
"builtInAlerts": ["all"]
|
||||
},
|
||||
|
@ -263,7 +263,7 @@
|
|||
"siem": ["all", "read_alerts", "crud_alerts"],
|
||||
"securitySolutionAssistant": ["all"],
|
||||
"securitySolutionAttackDiscovery": ["all"],
|
||||
"securitySolutionCases": ["all"],
|
||||
"securitySolutionCasesV2": ["all"],
|
||||
"actions": ["all"],
|
||||
"builtInAlerts": ["all"]
|
||||
},
|
||||
|
@ -311,7 +311,7 @@
|
|||
"siem": ["all", "read_alerts", "crud_alerts"],
|
||||
"securitySolutionAssistant": ["all"],
|
||||
"securitySolutionAttackDiscovery": ["all"],
|
||||
"securitySolutionCases": ["all"],
|
||||
"securitySolutionCasesV2": ["all"],
|
||||
"actions": ["read"],
|
||||
"builtInAlerts": ["all"],
|
||||
"dev_tools": ["all"]
|
||||
|
@ -366,7 +366,7 @@
|
|||
"siem": ["all", "read_alerts", "crud_alerts"],
|
||||
"securitySolutionAssistant": ["all"],
|
||||
"securitySolutionAttackDiscovery": ["all"],
|
||||
"securitySolutionCases": ["all"],
|
||||
"securitySolutionCasesV2": ["all"],
|
||||
"actions": ["all"],
|
||||
"builtInAlerts": ["all"]
|
||||
},
|
||||
|
|
|
@ -6,6 +6,6 @@
|
|||
*/
|
||||
|
||||
export { getSecurityFeature } from './src/security';
|
||||
export { getCasesFeature } from './src/cases';
|
||||
export { getCasesFeature, getCasesV2Feature } from './src/cases';
|
||||
export { getAssistantFeature } from './src/assistant';
|
||||
export { getAttackDiscoveryFeature } from './src/attack_discovery';
|
||||
|
|
|
@ -6,10 +6,21 @@
|
|||
*/
|
||||
import type { CasesSubFeatureId } from '../product_features_keys';
|
||||
import type { ProductFeatureParams } from '../types';
|
||||
import { getCasesBaseKibanaFeature } from './kibana_features';
|
||||
import { getCasesBaseKibanaSubFeatureIds, getCasesSubFeaturesMap } from './kibana_sub_features';
|
||||
import { getCasesBaseKibanaFeature } from './v1_features/kibana_features';
|
||||
import {
|
||||
getCasesBaseKibanaSubFeatureIds,
|
||||
getCasesSubFeaturesMap,
|
||||
} from './v1_features/kibana_sub_features';
|
||||
import type { CasesFeatureParams } from './types';
|
||||
import { getCasesBaseKibanaFeatureV2 } from './v2_features/kibana_features';
|
||||
import {
|
||||
getCasesBaseKibanaSubFeatureIdsV2,
|
||||
getCasesSubFeaturesMapV2,
|
||||
} from './v2_features/kibana_sub_features';
|
||||
|
||||
/**
|
||||
* @deprecated Use getCasesV2Feature instead
|
||||
*/
|
||||
export const getCasesFeature = (
|
||||
params: CasesFeatureParams
|
||||
): ProductFeatureParams<CasesSubFeatureId> => ({
|
||||
|
@ -17,3 +28,11 @@ export const getCasesFeature = (
|
|||
baseKibanaSubFeatureIds: getCasesBaseKibanaSubFeatureIds(),
|
||||
subFeaturesMap: getCasesSubFeaturesMap(params),
|
||||
});
|
||||
|
||||
export const getCasesV2Feature = (
|
||||
params: CasesFeatureParams
|
||||
): ProductFeatureParams<CasesSubFeatureId> => ({
|
||||
baseKibanaFeature: getCasesBaseKibanaFeatureV2(params),
|
||||
baseKibanaSubFeatureIds: getCasesBaseKibanaSubFeatureIdsV2(),
|
||||
subFeaturesMap: getCasesSubFeaturesMapV2(params),
|
||||
});
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { CasesUiCapabilities, CasesApiTags } from '@kbn/cases-plugin/common';
|
||||
import type { ProductFeatureCasesKey, CasesSubFeatureId } from '../product_features_keys';
|
||||
import type { ProductFeatureKibanaConfig } from '../types';
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common';
|
||||
import { KibanaFeatureScope } from '@kbn/features-plugin/common';
|
||||
import type { BaseKibanaFeatureConfig } from '../../types';
|
||||
import { APP_ID, CASES_FEATURE_ID, CASES_FEATURE_ID_V2 } from '../../constants';
|
||||
import type { CasesFeatureParams } from '../types';
|
||||
|
||||
/**
|
||||
* @deprecated Use getCasesBaseKibanaFeatureV2 instead
|
||||
*/
|
||||
export const getCasesBaseKibanaFeature = ({
|
||||
uiCapabilities,
|
||||
apiTags,
|
||||
savedObjects,
|
||||
}: CasesFeatureParams): BaseKibanaFeatureConfig => {
|
||||
return {
|
||||
deprecated: {
|
||||
notice: i18n.translate(
|
||||
'securitySolutionPackages.features.featureRegistry.linkSecuritySolutionCase.deprecationMessage',
|
||||
{
|
||||
defaultMessage:
|
||||
'The {currentId} permissions are deprecated, please see {casesFeatureIdV2}.',
|
||||
values: {
|
||||
currentId: CASES_FEATURE_ID,
|
||||
casesFeatureIdV2: CASES_FEATURE_ID_V2,
|
||||
},
|
||||
}
|
||||
),
|
||||
},
|
||||
id: CASES_FEATURE_ID,
|
||||
name: i18n.translate(
|
||||
'securitySolutionPackages.features.featureRegistry.linkSecuritySolutionCaseTitleDeprecated',
|
||||
{
|
||||
defaultMessage: 'Cases (Deprecated)',
|
||||
}
|
||||
),
|
||||
order: 1100,
|
||||
category: DEFAULT_APP_CATEGORIES.security,
|
||||
scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security],
|
||||
app: [CASES_FEATURE_ID, 'kibana'],
|
||||
catalogue: [APP_ID],
|
||||
cases: [APP_ID],
|
||||
privileges: {
|
||||
all: {
|
||||
api: [...apiTags.all, ...apiTags.createComment],
|
||||
app: [CASES_FEATURE_ID, 'kibana'],
|
||||
catalogue: [APP_ID],
|
||||
cases: {
|
||||
create: [APP_ID],
|
||||
read: [APP_ID],
|
||||
update: [APP_ID],
|
||||
push: [APP_ID],
|
||||
createComment: [APP_ID],
|
||||
reopenCase: [APP_ID],
|
||||
},
|
||||
savedObject: {
|
||||
all: [...savedObjects.files],
|
||||
read: [...savedObjects.files],
|
||||
},
|
||||
ui: uiCapabilities.all,
|
||||
replacedBy: {
|
||||
default: [{ feature: CASES_FEATURE_ID_V2, privileges: ['all'] }],
|
||||
minimal: [
|
||||
{
|
||||
feature: CASES_FEATURE_ID_V2,
|
||||
privileges: ['minimal_all', 'create_comment', 'case_reopen'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
read: {
|
||||
api: apiTags.read,
|
||||
app: [CASES_FEATURE_ID, 'kibana'],
|
||||
catalogue: [APP_ID],
|
||||
cases: {
|
||||
read: [APP_ID],
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [...savedObjects.files],
|
||||
},
|
||||
ui: uiCapabilities.read,
|
||||
replacedBy: {
|
||||
default: [{ feature: CASES_FEATURE_ID_V2, privileges: ['read'] }],
|
||||
minimal: [{ feature: CASES_FEATURE_ID_V2, privileges: ['minimal_read'] }],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
|
@ -7,9 +7,9 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { SubFeatureConfig } from '@kbn/features-plugin/common';
|
||||
import { CasesSubFeatureId } from '../product_features_keys';
|
||||
import { APP_ID } from '../constants';
|
||||
import type { CasesFeatureParams } from './types';
|
||||
import { CasesSubFeatureId } from '../../product_features_keys';
|
||||
import { APP_ID, CASES_FEATURE_ID_V2 } from '../../constants';
|
||||
import type { CasesFeatureParams } from '../types';
|
||||
|
||||
/**
|
||||
* Sub-features that will always be available for Security Cases
|
||||
|
@ -21,7 +21,8 @@ export const getCasesBaseKibanaSubFeatureIds = (): CasesSubFeatureId[] => [
|
|||
];
|
||||
|
||||
/**
|
||||
* Defines all the Security Assistant subFeatures available.
|
||||
* @deprecated Use getCasesSubFeaturesMapV2 instead
|
||||
* @description - Defines all the Security Solution Cases available.
|
||||
* The order of the subFeatures is the order they will be displayed
|
||||
*/
|
||||
export const getCasesSubFeaturesMap = ({
|
||||
|
@ -55,6 +56,7 @@ export const getCasesSubFeaturesMap = ({
|
|||
delete: [APP_ID],
|
||||
},
|
||||
ui: uiCapabilities.delete,
|
||||
replacedBy: [{ feature: CASES_FEATURE_ID_V2, privileges: ['cases_delete'] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -89,6 +91,7 @@ export const getCasesSubFeaturesMap = ({
|
|||
settings: [APP_ID],
|
||||
},
|
||||
ui: uiCapabilities.settings,
|
||||
replacedBy: [{ feature: CASES_FEATURE_ID_V2, privileges: ['cases_settings'] }],
|
||||
},
|
||||
],
|
||||
},
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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 type { ProductFeatureCasesKey, CasesSubFeatureId } from '../../product_features_keys';
|
||||
import type { ProductFeatureKibanaConfig } from '../../types';
|
||||
|
||||
export type DefaultCasesProductFeaturesConfig = Record<
|
||||
ProductFeatureCasesKey,
|
||||
ProductFeatureKibanaConfig<CasesSubFeatureId>
|
||||
>;
|
|
@ -9,17 +9,17 @@ import { i18n } from '@kbn/i18n';
|
|||
|
||||
import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common';
|
||||
import { KibanaFeatureScope } from '@kbn/features-plugin/common';
|
||||
import type { BaseKibanaFeatureConfig } from '../types';
|
||||
import { APP_ID, CASES_FEATURE_ID } from '../constants';
|
||||
import type { CasesFeatureParams } from './types';
|
||||
import type { BaseKibanaFeatureConfig } from '../../types';
|
||||
import { APP_ID, CASES_FEATURE_ID_V2, CASES_FEATURE_ID } from '../../constants';
|
||||
import type { CasesFeatureParams } from '../types';
|
||||
|
||||
export const getCasesBaseKibanaFeature = ({
|
||||
export const getCasesBaseKibanaFeatureV2 = ({
|
||||
uiCapabilities,
|
||||
apiTags,
|
||||
savedObjects,
|
||||
}: CasesFeatureParams): BaseKibanaFeatureConfig => {
|
||||
return {
|
||||
id: CASES_FEATURE_ID,
|
||||
id: CASES_FEATURE_ID_V2,
|
||||
name: i18n.translate(
|
||||
'securitySolutionPackages.features.featureRegistry.linkSecuritySolutionCaseTitle',
|
||||
{
|
||||
|
@ -41,6 +41,7 @@ export const getCasesBaseKibanaFeature = ({
|
|||
create: [APP_ID],
|
||||
read: [APP_ID],
|
||||
update: [APP_ID],
|
||||
push: [APP_ID],
|
||||
},
|
||||
savedObject: {
|
||||
all: [...savedObjects.files],
|
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import type { SubFeatureConfig } from '@kbn/features-plugin/common';
|
||||
import { CasesSubFeatureId } from '../../product_features_keys';
|
||||
import { APP_ID } from '../../constants';
|
||||
import type { CasesFeatureParams } from '../types';
|
||||
|
||||
/**
|
||||
* Sub-features that will always be available for Security Cases
|
||||
* regardless of the product type.
|
||||
*/
|
||||
export const getCasesBaseKibanaSubFeatureIdsV2 = (): CasesSubFeatureId[] => [
|
||||
CasesSubFeatureId.deleteCases,
|
||||
CasesSubFeatureId.casesSettings,
|
||||
CasesSubFeatureId.createComment,
|
||||
CasesSubFeatureId.reopenCase,
|
||||
];
|
||||
|
||||
/**
|
||||
* Defines all the Security Solution Cases subFeatures available.
|
||||
* The order of the subFeatures is the order they will be displayed
|
||||
*/
|
||||
export const getCasesSubFeaturesMapV2 = ({
|
||||
uiCapabilities,
|
||||
apiTags,
|
||||
savedObjects,
|
||||
}: CasesFeatureParams) => {
|
||||
const deleteCasesSubFeature: SubFeatureConfig = {
|
||||
name: i18n.translate('securitySolutionPackages.features.featureRegistry.deleteSubFeatureName', {
|
||||
defaultMessage: 'Delete',
|
||||
}),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
api: apiTags.delete,
|
||||
id: 'cases_delete',
|
||||
name: i18n.translate(
|
||||
'securitySolutionPackages.features.featureRegistry.deleteSubFeatureDetails',
|
||||
{
|
||||
defaultMessage: 'Delete cases and comments',
|
||||
}
|
||||
),
|
||||
includeIn: 'all',
|
||||
savedObject: {
|
||||
all: [...savedObjects.files],
|
||||
read: [...savedObjects.files],
|
||||
},
|
||||
cases: {
|
||||
delete: [APP_ID],
|
||||
},
|
||||
ui: uiCapabilities.delete,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/* The below sub features were newly added in v2 (8.17) */
|
||||
|
||||
const casesAddCommentsCasesSubFeature: SubFeatureConfig = {
|
||||
name: i18n.translate(
|
||||
'securitySolutionPackages.features.featureRegistry.addCommentsSubFeatureName',
|
||||
{
|
||||
defaultMessage: 'Create comments & attachments',
|
||||
}
|
||||
),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
api: apiTags.createComment,
|
||||
id: 'create_comment',
|
||||
name: i18n.translate(
|
||||
'securitySolutionPackages.features.featureRegistry.addCommentsSubFeatureDetails',
|
||||
{
|
||||
defaultMessage: 'Add comments to cases',
|
||||
}
|
||||
),
|
||||
includeIn: 'all',
|
||||
savedObject: {
|
||||
all: [...savedObjects.files],
|
||||
read: [...savedObjects.files],
|
||||
},
|
||||
cases: {
|
||||
createComment: [APP_ID],
|
||||
},
|
||||
ui: uiCapabilities.createComment,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const casesreopenCaseSubFeature: SubFeatureConfig = {
|
||||
name: i18n.translate(
|
||||
'securitySolutionPackages.features.featureRegistry.reopenCaseSubFeatureName',
|
||||
{
|
||||
defaultMessage: 'Re-open',
|
||||
}
|
||||
),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
id: 'case_reopen',
|
||||
name: i18n.translate(
|
||||
'securitySolutionPackages.features.featureRegistry.reopenCaseSubFeatureDetails',
|
||||
{
|
||||
defaultMessage: 'Re-open closed cases',
|
||||
}
|
||||
),
|
||||
includeIn: 'all',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
cases: {
|
||||
reopenCase: [APP_ID],
|
||||
},
|
||||
ui: uiCapabilities.reopenCase,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return new Map<CasesSubFeatureId, SubFeatureConfig>([
|
||||
[CasesSubFeatureId.deleteCases, deleteCasesSubFeature],
|
||||
[CasesSubFeatureId.casesSettings, casesSettingsCasesSubFeature],
|
||||
/* The below sub features were newly added in v2 (8.17) */
|
||||
[CasesSubFeatureId.createComment, casesAddCommentsCasesSubFeature],
|
||||
[CasesSubFeatureId.reopenCase, casesreopenCaseSubFeature],
|
||||
]);
|
||||
};
|
|
@ -9,7 +9,16 @@
|
|||
export const APP_ID = 'securitySolution' as const;
|
||||
export const SERVER_APP_ID = 'siem' as const;
|
||||
|
||||
/**
|
||||
* @deprecated deprecated in 8.17. Use CASE_FEATURE_ID_V2 instead
|
||||
*/
|
||||
export const CASES_FEATURE_ID = 'securitySolutionCases' as const;
|
||||
|
||||
// New version created in 8.17 to adopt the roles migration changes
|
||||
export const CASES_FEATURE_ID_V2 = 'securitySolutionCasesV2' as const;
|
||||
|
||||
export const SECURITY_SOLUTION_CASES_APP_ID = 'securitySolutionCases' as const;
|
||||
|
||||
export const ASSISTANT_FEATURE_ID = 'securitySolutionAssistant' as const;
|
||||
export const ATTACK_DISCOVERY_FEATURE_ID = 'securitySolutionAttackDiscovery' as const;
|
||||
|
||||
|
|
|
@ -148,6 +148,8 @@ export enum SecuritySubFeatureId {
|
|||
export enum CasesSubFeatureId {
|
||||
deleteCases = 'deleteCasesSubFeature',
|
||||
casesSettings = 'casesSettingsSubFeature',
|
||||
createComment = 'createCommentSubFeature',
|
||||
reopenCase = 'reopenCaseSubFeature',
|
||||
}
|
||||
|
||||
/** Sub-features IDs for Security Assistant */
|
||||
|
|
|
@ -4,7 +4,6 @@ exports[`cases feature_privilege_builder within feature grants all privileges un
|
|||
Array [
|
||||
"cases:observability/pushCase",
|
||||
"cases:observability/createCase",
|
||||
"cases:observability/createComment",
|
||||
"cases:observability/getCase",
|
||||
"cases:observability/getComment",
|
||||
"cases:observability/getTags",
|
||||
|
@ -17,12 +16,19 @@ Array [
|
|||
"cases:observability/deleteComment",
|
||||
"cases:observability/createConfiguration",
|
||||
"cases:observability/updateConfiguration",
|
||||
"cases:observability/createComment",
|
||||
"cases:observability/reopenCase",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`cases feature_privilege_builder within feature grants create privileges under feature with id securitySolution 1`] = `
|
||||
Array [
|
||||
"cases:securitySolution/createCase",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`cases feature_privilege_builder within feature grants createComment privileges under feature with id securitySolution 1`] = `
|
||||
Array [
|
||||
"cases:securitySolution/createComment",
|
||||
]
|
||||
`;
|
||||
|
@ -51,6 +57,12 @@ Array [
|
|||
]
|
||||
`;
|
||||
|
||||
exports[`cases feature_privilege_builder within feature grants reopenCase privileges under feature with id observability 1`] = `
|
||||
Array [
|
||||
"cases:observability/reopenCase",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`cases feature_privilege_builder within feature grants settings privileges under feature with id observability 1`] = `
|
||||
Array [
|
||||
"cases:observability/createConfiguration",
|
||||
|
|
|
@ -48,6 +48,8 @@ describe(`cases`, () => {
|
|||
['update', 'observability'],
|
||||
['delete', 'securitySolution'],
|
||||
['settings', 'observability'],
|
||||
['createComment', 'securitySolution'],
|
||||
['reopenCase', 'observability'],
|
||||
])('grants %s privileges under feature with id %s', (operation, featureID) => {
|
||||
const actions = new Actions();
|
||||
const casesFeaturePrivilege = new FeaturePrivilegeCasesBuilder(actions);
|
||||
|
@ -89,6 +91,8 @@ describe(`cases`, () => {
|
|||
delete: ['security'],
|
||||
read: ['obs'],
|
||||
settings: ['security'],
|
||||
createComment: ['security'],
|
||||
reopenCase: ['security'],
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
|
@ -112,7 +116,6 @@ describe(`cases`, () => {
|
|||
Array [
|
||||
"cases:security/pushCase",
|
||||
"cases:security/createCase",
|
||||
"cases:security/createComment",
|
||||
"cases:security/getCase",
|
||||
"cases:security/getComment",
|
||||
"cases:security/getTags",
|
||||
|
@ -125,6 +128,8 @@ describe(`cases`, () => {
|
|||
"cases:security/deleteComment",
|
||||
"cases:security/createConfiguration",
|
||||
"cases:security/updateConfiguration",
|
||||
"cases:security/createComment",
|
||||
"cases:security/reopenCase",
|
||||
"cases:obs/getCase",
|
||||
"cases:obs/getComment",
|
||||
"cases:obs/getTags",
|
||||
|
@ -168,7 +173,6 @@ describe(`cases`, () => {
|
|||
Array [
|
||||
"cases:security/pushCase",
|
||||
"cases:security/createCase",
|
||||
"cases:security/createComment",
|
||||
"cases:security/getCase",
|
||||
"cases:security/getComment",
|
||||
"cases:security/getTags",
|
||||
|
@ -181,9 +185,10 @@ describe(`cases`, () => {
|
|||
"cases:security/deleteComment",
|
||||
"cases:security/createConfiguration",
|
||||
"cases:security/updateConfiguration",
|
||||
"cases:security/createComment",
|
||||
"cases:security/reopenCase",
|
||||
"cases:other-security/pushCase",
|
||||
"cases:other-security/createCase",
|
||||
"cases:other-security/createComment",
|
||||
"cases:other-security/getCase",
|
||||
"cases:other-security/getComment",
|
||||
"cases:other-security/getTags",
|
||||
|
@ -196,6 +201,8 @@ describe(`cases`, () => {
|
|||
"cases:other-security/deleteComment",
|
||||
"cases:other-security/createConfiguration",
|
||||
"cases:other-security/updateConfiguration",
|
||||
"cases:other-security/createComment",
|
||||
"cases:other-security/reopenCase",
|
||||
"cases:obs/getCase",
|
||||
"cases:obs/getComment",
|
||||
"cases:obs/getTags",
|
||||
|
|
|
@ -22,7 +22,7 @@ export type CasesSupportedOperations = (typeof allOperations)[number];
|
|||
*/
|
||||
|
||||
const pushOperations = ['pushCase'] as const;
|
||||
const createOperations = ['createCase', 'createComment'] as const;
|
||||
const createOperations = ['createCase'] as const;
|
||||
const readOperations = [
|
||||
'getCase',
|
||||
'getComment',
|
||||
|
@ -31,9 +31,12 @@ const readOperations = [
|
|||
'getUserActions',
|
||||
'findConfigurations',
|
||||
] as const;
|
||||
// Update operations do not currently include the ability to re-open a case
|
||||
const updateOperations = ['updateCase', 'updateComment'] as const;
|
||||
const deleteOperations = ['deleteCase', 'deleteComment'] as const;
|
||||
const settingsOperations = ['createConfiguration', 'updateConfiguration'] as const;
|
||||
const createCommentOperations = ['createComment'] as const;
|
||||
const reopenOperations = ['reopenCase'] as const;
|
||||
const allOperations = [
|
||||
...pushOperations,
|
||||
...createOperations,
|
||||
|
@ -41,6 +44,8 @@ const allOperations = [
|
|||
...updateOperations,
|
||||
...deleteOperations,
|
||||
...settingsOperations,
|
||||
...createCommentOperations,
|
||||
...reopenOperations,
|
||||
] as const;
|
||||
|
||||
export class FeaturePrivilegeCasesBuilder extends BaseFeaturePrivilegeBuilder {
|
||||
|
@ -56,7 +61,6 @@ export class FeaturePrivilegeCasesBuilder extends BaseFeaturePrivilegeBuilder {
|
|||
operations.map((operation) => this.actions.cases.get(owner, operation))
|
||||
);
|
||||
};
|
||||
|
||||
return uniq([
|
||||
...getCasesPrivilege(allOperations, privilegeDefinition.cases?.all),
|
||||
...getCasesPrivilege(pushOperations, privilegeDefinition.cases?.push),
|
||||
|
@ -65,6 +69,8 @@ export class FeaturePrivilegeCasesBuilder extends BaseFeaturePrivilegeBuilder {
|
|||
...getCasesPrivilege(updateOperations, privilegeDefinition.cases?.update),
|
||||
...getCasesPrivilege(deleteOperations, privilegeDefinition.cases?.delete),
|
||||
...getCasesPrivilege(settingsOperations, privilegeDefinition.cases?.settings),
|
||||
...getCasesPrivilege(createCommentOperations, privilegeDefinition.cases?.createComment),
|
||||
...getCasesPrivilege(reopenOperations, privilegeDefinition.cases?.reopenCase),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,9 @@ import { CASE_VIEW_PAGE_TABS } from '../types';
|
|||
*/
|
||||
|
||||
export const APP_ID = 'cases' as const;
|
||||
/** @deprecated Please use FEATURE_ID_V2 instead */
|
||||
export const FEATURE_ID = 'generalCases' as const;
|
||||
export const FEATURE_ID_V2 = 'generalCasesV2' as const;
|
||||
export const APP_OWNER = 'cases' as const;
|
||||
export const APP_PATH = '/app/management/insightsAndAlerting/cases' as const;
|
||||
export const CASES_CREATE_PATH = '/create' as const;
|
||||
|
|
|
@ -174,6 +174,8 @@ 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;
|
||||
export const CASES_REOPEN_CAPABILITY = 'case_reopen' as const;
|
||||
export const CREATE_COMMENT_CAPABILITY = 'create_comment' as const;
|
||||
|
||||
/**
|
||||
* Cases API Tags
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
export type {
|
||||
CasesBulkGetResponse,
|
||||
CasePostRequest,
|
||||
CasePatchRequest,
|
||||
GetRelatedCasesByAlertResponse,
|
||||
UserActionFindResponse,
|
||||
} from './types/api';
|
||||
|
@ -38,6 +39,7 @@ export { CaseSeverity } from './types/domain';
|
|||
export {
|
||||
APP_ID,
|
||||
FEATURE_ID,
|
||||
FEATURE_ID_V2,
|
||||
CASES_URL,
|
||||
SECURITY_SOLUTION_OWNER,
|
||||
OBSERVABILITY_OWNER,
|
||||
|
@ -55,6 +57,8 @@ export {
|
|||
CASES_CONNECTORS_CAPABILITY,
|
||||
GET_CONNECTORS_CONFIGURE_API_TAG,
|
||||
CASES_SETTINGS_CAPABILITY,
|
||||
CREATE_COMMENT_CAPABILITY,
|
||||
CASES_REOPEN_CAPABILITY,
|
||||
} from './constants';
|
||||
|
||||
export type { AttachmentAttributes } from './types/domain';
|
||||
|
|
|
@ -11,6 +11,8 @@ import type {
|
|||
DELETE_CASES_CAPABILITY,
|
||||
READ_CASES_CAPABILITY,
|
||||
UPDATE_CASES_CAPABILITY,
|
||||
CREATE_COMMENT_CAPABILITY,
|
||||
CASES_REOPEN_CAPABILITY,
|
||||
} from '..';
|
||||
import type {
|
||||
CASES_CONNECTORS_CAPABILITY,
|
||||
|
@ -305,6 +307,8 @@ export interface CasesPermissions {
|
|||
push: boolean;
|
||||
connectors: boolean;
|
||||
settings: boolean;
|
||||
reopenCase: boolean;
|
||||
createComment: boolean;
|
||||
}
|
||||
|
||||
export interface CasesCapabilities {
|
||||
|
@ -315,4 +319,6 @@ export interface CasesCapabilities {
|
|||
[PUSH_CASES_CAPABILITY]: boolean;
|
||||
[CASES_CONNECTORS_CAPABILITY]: boolean;
|
||||
[CASES_SETTINGS_CAPABILITY]: boolean;
|
||||
[CREATE_COMMENT_CAPABILITY]: boolean;
|
||||
[CASES_REOPEN_CAPABILITY]: boolean;
|
||||
}
|
||||
|
|
|
@ -6,9 +6,11 @@ Object {
|
|||
"casesSuggestUserProfiles",
|
||||
"bulkGetUserProfiles",
|
||||
"casesGetConnectorsConfigure",
|
||||
"casesFilesCasesCreate",
|
||||
"casesFilesCasesRead",
|
||||
],
|
||||
"createComment": Array [
|
||||
"casesFilesCasesCreate",
|
||||
],
|
||||
"delete": Array [
|
||||
"casesFilesCasesDelete",
|
||||
],
|
||||
|
@ -27,9 +29,11 @@ Object {
|
|||
"casesSuggestUserProfiles",
|
||||
"bulkGetUserProfiles",
|
||||
"casesGetConnectorsConfigure",
|
||||
"observabilityFilesCasesCreate",
|
||||
"observabilityFilesCasesRead",
|
||||
],
|
||||
"createComment": Array [
|
||||
"observabilityFilesCasesCreate",
|
||||
],
|
||||
"delete": Array [
|
||||
"observabilityFilesCasesDelete",
|
||||
],
|
||||
|
@ -48,9 +52,11 @@ Object {
|
|||
"casesSuggestUserProfiles",
|
||||
"bulkGetUserProfiles",
|
||||
"casesGetConnectorsConfigure",
|
||||
"securitySolutionFilesCasesCreate",
|
||||
"securitySolutionFilesCasesRead",
|
||||
],
|
||||
"createComment": Array [
|
||||
"securitySolutionFilesCasesCreate",
|
||||
],
|
||||
"delete": Array [
|
||||
"securitySolutionFilesCasesDelete",
|
||||
],
|
||||
|
|
|
@ -18,6 +18,7 @@ export interface CasesApiTags {
|
|||
all: readonly string[];
|
||||
read: readonly string[];
|
||||
delete: readonly string[];
|
||||
createComment: readonly string[];
|
||||
}
|
||||
|
||||
export const getApiTags = (owner: Owner): CasesApiTags => {
|
||||
|
@ -30,7 +31,6 @@ export const getApiTags = (owner: Owner): CasesApiTags => {
|
|||
SUGGEST_USER_PROFILES_API_TAG,
|
||||
BULK_GET_USER_PROFILES_API_TAG,
|
||||
GET_CONNECTORS_CONFIGURE_API_TAG,
|
||||
create,
|
||||
read,
|
||||
] as const,
|
||||
read: [
|
||||
|
@ -40,5 +40,6 @@ export const getApiTags = (owner: Owner): CasesApiTags => {
|
|||
read,
|
||||
] as const,
|
||||
delete: [deleteTag] as const,
|
||||
createComment: [create] as const,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -17,6 +17,10 @@ describe('createUICapabilities', () => {
|
|||
"update_cases",
|
||||
"push_cases",
|
||||
"cases_connectors",
|
||||
"cases_settings",
|
||||
],
|
||||
"createComment": Array [
|
||||
"create_comment",
|
||||
],
|
||||
"delete": Array [
|
||||
"delete_cases",
|
||||
|
@ -25,6 +29,9 @@ describe('createUICapabilities', () => {
|
|||
"read_cases",
|
||||
"cases_connectors",
|
||||
],
|
||||
"reopenCase": Array [
|
||||
"case_reopen",
|
||||
],
|
||||
"settings": Array [
|
||||
"cases_settings",
|
||||
],
|
||||
|
|
|
@ -13,6 +13,8 @@ import {
|
|||
READ_CASES_CAPABILITY,
|
||||
UPDATE_CASES_CAPABILITY,
|
||||
CASES_SETTINGS_CAPABILITY,
|
||||
CASES_REOPEN_CAPABILITY,
|
||||
CREATE_COMMENT_CAPABILITY,
|
||||
} from '../constants';
|
||||
|
||||
export interface CasesUiCapabilities {
|
||||
|
@ -20,6 +22,8 @@ export interface CasesUiCapabilities {
|
|||
read: readonly string[];
|
||||
delete: readonly string[];
|
||||
settings: readonly string[];
|
||||
reopenCase: readonly string[];
|
||||
createComment: readonly string[];
|
||||
}
|
||||
/**
|
||||
* Return the UI capabilities for each type of operation. These strings must match the values defined in the UI
|
||||
|
@ -32,8 +36,11 @@ export const createUICapabilities = (): CasesUiCapabilities => ({
|
|||
UPDATE_CASES_CAPABILITY,
|
||||
PUSH_CASES_CAPABILITY,
|
||||
CASES_CONNECTORS_CAPABILITY,
|
||||
CASES_SETTINGS_CAPABILITY,
|
||||
] as const,
|
||||
read: [READ_CASES_CAPABILITY, CASES_CONNECTORS_CAPABILITY] as const,
|
||||
delete: [DELETE_CASES_CAPABILITY] as const,
|
||||
settings: [CASES_SETTINGS_CAPABILITY] as const,
|
||||
reopenCase: [CASES_REOPEN_CAPABILITY] as const,
|
||||
createComment: [CREATE_COMMENT_CAPABILITY] as const,
|
||||
});
|
||||
|
|
|
@ -20,67 +20,67 @@ import { canUseCases } from './can_use_cases';
|
|||
|
||||
type CasesCapabilities = Pick<
|
||||
ApplicationStart['capabilities'],
|
||||
'securitySolutionCases' | 'observabilityCases' | 'generalCases'
|
||||
'securitySolutionCasesV2' | 'observabilityCasesV2' | 'generalCasesV2'
|
||||
>;
|
||||
|
||||
const hasAll: CasesCapabilities = {
|
||||
securitySolutionCases: allCasesCapabilities(),
|
||||
observabilityCases: allCasesCapabilities(),
|
||||
generalCases: allCasesCapabilities(),
|
||||
securitySolutionCasesV2: allCasesCapabilities(),
|
||||
observabilityCasesV2: allCasesCapabilities(),
|
||||
generalCasesV2: allCasesCapabilities(),
|
||||
};
|
||||
|
||||
const hasNone: CasesCapabilities = {
|
||||
securitySolutionCases: noCasesCapabilities(),
|
||||
observabilityCases: noCasesCapabilities(),
|
||||
generalCases: noCasesCapabilities(),
|
||||
securitySolutionCasesV2: noCasesCapabilities(),
|
||||
observabilityCasesV2: noCasesCapabilities(),
|
||||
generalCasesV2: noCasesCapabilities(),
|
||||
};
|
||||
|
||||
const hasSecurity: CasesCapabilities = {
|
||||
securitySolutionCases: allCasesCapabilities(),
|
||||
observabilityCases: noCasesCapabilities(),
|
||||
generalCases: noCasesCapabilities(),
|
||||
securitySolutionCasesV2: allCasesCapabilities(),
|
||||
observabilityCasesV2: noCasesCapabilities(),
|
||||
generalCasesV2: noCasesCapabilities(),
|
||||
};
|
||||
|
||||
const hasObservability: CasesCapabilities = {
|
||||
securitySolutionCases: noCasesCapabilities(),
|
||||
observabilityCases: allCasesCapabilities(),
|
||||
generalCases: noCasesCapabilities(),
|
||||
securitySolutionCasesV2: noCasesCapabilities(),
|
||||
observabilityCasesV2: allCasesCapabilities(),
|
||||
generalCasesV2: noCasesCapabilities(),
|
||||
};
|
||||
|
||||
const hasObservabilityWriteTrue: CasesCapabilities = {
|
||||
securitySolutionCases: noCasesCapabilities(),
|
||||
observabilityCases: writeCasesCapabilities(),
|
||||
generalCases: noCasesCapabilities(),
|
||||
securitySolutionCasesV2: noCasesCapabilities(),
|
||||
observabilityCasesV2: writeCasesCapabilities(),
|
||||
generalCasesV2: noCasesCapabilities(),
|
||||
};
|
||||
|
||||
const hasSecurityWriteTrue: CasesCapabilities = {
|
||||
securitySolutionCases: writeCasesCapabilities(),
|
||||
observabilityCases: noCasesCapabilities(),
|
||||
generalCases: noCasesCapabilities(),
|
||||
securitySolutionCasesV2: writeCasesCapabilities(),
|
||||
observabilityCasesV2: noCasesCapabilities(),
|
||||
generalCasesV2: noCasesCapabilities(),
|
||||
};
|
||||
|
||||
const hasObservabilityReadTrue: CasesCapabilities = {
|
||||
securitySolutionCases: noCasesCapabilities(),
|
||||
observabilityCases: readCasesCapabilities(),
|
||||
generalCases: noCasesCapabilities(),
|
||||
securitySolutionCasesV2: noCasesCapabilities(),
|
||||
observabilityCasesV2: readCasesCapabilities(),
|
||||
generalCasesV2: noCasesCapabilities(),
|
||||
};
|
||||
|
||||
const hasSecurityReadTrue: CasesCapabilities = {
|
||||
securitySolutionCases: readCasesCapabilities(),
|
||||
observabilityCases: noCasesCapabilities(),
|
||||
generalCases: noCasesCapabilities(),
|
||||
securitySolutionCasesV2: readCasesCapabilities(),
|
||||
observabilityCasesV2: noCasesCapabilities(),
|
||||
generalCasesV2: noCasesCapabilities(),
|
||||
};
|
||||
|
||||
const hasSecurityWriteAndObservabilityRead: CasesCapabilities = {
|
||||
securitySolutionCases: writeCasesCapabilities(),
|
||||
observabilityCases: readCasesCapabilities(),
|
||||
generalCases: noCasesCapabilities(),
|
||||
securitySolutionCasesV2: writeCasesCapabilities(),
|
||||
observabilityCasesV2: readCasesCapabilities(),
|
||||
generalCasesV2: noCasesCapabilities(),
|
||||
};
|
||||
|
||||
const hasSecurityConnectors: CasesCapabilities = {
|
||||
securitySolutionCases: readCasesCapabilities(),
|
||||
observabilityCases: noCasesCapabilities(),
|
||||
generalCases: noCasesCapabilities(),
|
||||
securitySolutionCasesV2: readCasesCapabilities(),
|
||||
observabilityCasesV2: noCasesCapabilities(),
|
||||
generalCasesV2: noCasesCapabilities(),
|
||||
};
|
||||
|
||||
describe('canUseCases', () => {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import type { ApplicationStart } from '@kbn/core/public';
|
||||
import {
|
||||
FEATURE_ID,
|
||||
FEATURE_ID_V2,
|
||||
GENERAL_CASES_OWNER,
|
||||
OBSERVABILITY_OWNER,
|
||||
SECURITY_SOLUTION_OWNER,
|
||||
|
@ -42,6 +42,8 @@ export const canUseCases =
|
|||
acc.push = acc.push || userCapabilitiesForOwner.push;
|
||||
acc.connectors = acc.connectors || userCapabilitiesForOwner.connectors;
|
||||
acc.settings = acc.settings || userCapabilitiesForOwner.settings;
|
||||
acc.reopenCase = acc.reopenCase || userCapabilitiesForOwner.reopenCase;
|
||||
acc.createComment = acc.createComment || userCapabilitiesForOwner.createComment;
|
||||
|
||||
const allFromAcc =
|
||||
acc.create &&
|
||||
|
@ -50,7 +52,9 @@ export const canUseCases =
|
|||
acc.delete &&
|
||||
acc.push &&
|
||||
acc.connectors &&
|
||||
acc.settings;
|
||||
acc.settings &&
|
||||
acc.reopenCase &&
|
||||
acc.createComment;
|
||||
|
||||
acc.all = acc.all || userCapabilitiesForOwner.all || allFromAcc;
|
||||
|
||||
|
@ -65,6 +69,8 @@ export const canUseCases =
|
|||
push: false,
|
||||
connectors: false,
|
||||
settings: false,
|
||||
reopenCase: false,
|
||||
createComment: false,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -75,8 +81,8 @@ export const canUseCases =
|
|||
|
||||
const getFeatureID = (owner: CasesOwners) => {
|
||||
if (owner === GENERAL_CASES_OWNER) {
|
||||
return FEATURE_ID;
|
||||
return FEATURE_ID_V2;
|
||||
}
|
||||
|
||||
return `${owner}Cases`;
|
||||
return `${owner}CasesV2`;
|
||||
};
|
||||
|
|
|
@ -14,9 +14,11 @@ describe('getUICapabilities', () => {
|
|||
"all": false,
|
||||
"connectors": false,
|
||||
"create": false,
|
||||
"createComment": false,
|
||||
"delete": false,
|
||||
"push": false,
|
||||
"read": false,
|
||||
"reopenCase": false,
|
||||
"settings": false,
|
||||
"update": false,
|
||||
}
|
||||
|
@ -29,9 +31,11 @@ describe('getUICapabilities', () => {
|
|||
"all": false,
|
||||
"connectors": false,
|
||||
"create": false,
|
||||
"createComment": false,
|
||||
"delete": false,
|
||||
"push": false,
|
||||
"read": false,
|
||||
"reopenCase": false,
|
||||
"settings": false,
|
||||
"update": false,
|
||||
}
|
||||
|
@ -44,9 +48,11 @@ describe('getUICapabilities', () => {
|
|||
"all": false,
|
||||
"connectors": false,
|
||||
"create": true,
|
||||
"createComment": false,
|
||||
"delete": false,
|
||||
"push": false,
|
||||
"read": false,
|
||||
"reopenCase": false,
|
||||
"settings": false,
|
||||
"update": false,
|
||||
}
|
||||
|
@ -68,9 +74,11 @@ describe('getUICapabilities', () => {
|
|||
"all": false,
|
||||
"connectors": false,
|
||||
"create": false,
|
||||
"createComment": false,
|
||||
"delete": false,
|
||||
"push": false,
|
||||
"read": false,
|
||||
"reopenCase": false,
|
||||
"settings": false,
|
||||
"update": false,
|
||||
}
|
||||
|
@ -83,9 +91,11 @@ describe('getUICapabilities', () => {
|
|||
"all": false,
|
||||
"connectors": false,
|
||||
"create": false,
|
||||
"createComment": false,
|
||||
"delete": false,
|
||||
"push": false,
|
||||
"read": false,
|
||||
"reopenCase": false,
|
||||
"settings": false,
|
||||
"update": false,
|
||||
}
|
||||
|
@ -107,9 +117,11 @@ describe('getUICapabilities', () => {
|
|||
"all": false,
|
||||
"connectors": true,
|
||||
"create": false,
|
||||
"createComment": false,
|
||||
"delete": true,
|
||||
"push": true,
|
||||
"read": true,
|
||||
"reopenCase": false,
|
||||
"settings": false,
|
||||
"update": true,
|
||||
}
|
||||
|
@ -132,9 +144,11 @@ describe('getUICapabilities', () => {
|
|||
"all": false,
|
||||
"connectors": false,
|
||||
"create": true,
|
||||
"createComment": false,
|
||||
"delete": true,
|
||||
"push": true,
|
||||
"read": true,
|
||||
"reopenCase": false,
|
||||
"settings": true,
|
||||
"update": true,
|
||||
}
|
||||
|
@ -157,9 +171,11 @@ describe('getUICapabilities', () => {
|
|||
"all": false,
|
||||
"connectors": true,
|
||||
"create": true,
|
||||
"createComment": false,
|
||||
"delete": true,
|
||||
"push": true,
|
||||
"read": true,
|
||||
"reopenCase": false,
|
||||
"settings": false,
|
||||
"update": true,
|
||||
}
|
||||
|
@ -172,9 +188,11 @@ describe('getUICapabilities', () => {
|
|||
"all": false,
|
||||
"connectors": false,
|
||||
"create": false,
|
||||
"createComment": false,
|
||||
"delete": false,
|
||||
"push": false,
|
||||
"read": false,
|
||||
"reopenCase": false,
|
||||
"settings": true,
|
||||
"update": false,
|
||||
}
|
||||
|
|
|
@ -14,6 +14,8 @@ import {
|
|||
PUSH_CASES_CAPABILITY,
|
||||
READ_CASES_CAPABILITY,
|
||||
UPDATE_CASES_CAPABILITY,
|
||||
CASES_REOPEN_CAPABILITY,
|
||||
CREATE_COMMENT_CAPABILITY,
|
||||
} from '../../../common/constants';
|
||||
|
||||
export const getUICapabilities = (
|
||||
|
@ -26,8 +28,19 @@ export const getUICapabilities = (
|
|||
const push = !!featureCapabilities?.[PUSH_CASES_CAPABILITY];
|
||||
const connectors = !!featureCapabilities?.[CASES_CONNECTORS_CAPABILITY];
|
||||
const settings = !!featureCapabilities?.[CASES_SETTINGS_CAPABILITY];
|
||||
const reopenCase = !!featureCapabilities?.[CASES_REOPEN_CAPABILITY];
|
||||
const createComment = !!featureCapabilities?.[CREATE_COMMENT_CAPABILITY];
|
||||
|
||||
const all = create && read && update && deletePriv && push && connectors && settings;
|
||||
const all =
|
||||
create &&
|
||||
read &&
|
||||
update &&
|
||||
deletePriv &&
|
||||
push &&
|
||||
connectors &&
|
||||
settings &&
|
||||
reopenCase &&
|
||||
createComment;
|
||||
|
||||
return {
|
||||
all,
|
||||
|
@ -38,5 +51,7 @@ export const getUICapabilities = (
|
|||
push,
|
||||
connectors,
|
||||
settings,
|
||||
reopenCase,
|
||||
createComment,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -48,7 +48,7 @@ export const useNavigation = jest.fn().mockReturnValue({
|
|||
|
||||
export const useApplicationCapabilities = jest.fn().mockReturnValue({
|
||||
actions: { crud: true, read: true },
|
||||
generalCases: { crud: true, read: true },
|
||||
generalCasesV2: { crud: true, read: true },
|
||||
visualize: { crud: true, read: true },
|
||||
dashboard: { crud: true, read: true },
|
||||
});
|
||||
|
|
|
@ -23,7 +23,7 @@ describe('hooks', () => {
|
|||
|
||||
expect(result.current).toEqual({
|
||||
actions: { crud: true, read: true },
|
||||
generalCases: allCasesPermissions(),
|
||||
generalCasesV2: allCasesPermissions(),
|
||||
visualize: { crud: true, read: true },
|
||||
dashboard: { crud: true, read: true },
|
||||
});
|
||||
|
|
|
@ -15,7 +15,7 @@ import type { NavigateToAppOptions } from '@kbn/core/public';
|
|||
import { getUICapabilities } from '../../../client/helpers/capabilities';
|
||||
import { convertToCamelCase } from '../../../api/utils';
|
||||
import {
|
||||
FEATURE_ID,
|
||||
FEATURE_ID_V2,
|
||||
DEFAULT_DATE_FORMAT,
|
||||
DEFAULT_DATE_FORMAT_TZ,
|
||||
} from '../../../../common/constants';
|
||||
|
@ -166,7 +166,7 @@ interface Capabilities {
|
|||
}
|
||||
interface UseApplicationCapabilities {
|
||||
actions: Capabilities;
|
||||
generalCases: CasesPermissions;
|
||||
generalCasesV2: CasesPermissions;
|
||||
visualize: Capabilities;
|
||||
dashboard: Capabilities;
|
||||
}
|
||||
|
@ -178,13 +178,13 @@ interface UseApplicationCapabilities {
|
|||
|
||||
export const useApplicationCapabilities = (): UseApplicationCapabilities => {
|
||||
const capabilities = useKibana().services?.application?.capabilities;
|
||||
const casesCapabilities = capabilities[FEATURE_ID];
|
||||
const casesCapabilities = capabilities[FEATURE_ID_V2];
|
||||
const permissions = getUICapabilities(casesCapabilities);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
actions: { crud: !!capabilities.actions?.save, read: !!capabilities.actions?.show },
|
||||
generalCases: {
|
||||
generalCasesV2: {
|
||||
all: permissions.all,
|
||||
create: permissions.create,
|
||||
read: permissions.read,
|
||||
|
@ -193,6 +193,8 @@ export const useApplicationCapabilities = (): UseApplicationCapabilities => {
|
|||
push: permissions.push,
|
||||
connectors: permissions.connectors,
|
||||
settings: permissions.settings,
|
||||
reopenCase: permissions.reopenCase,
|
||||
createComment: permissions.createComment,
|
||||
},
|
||||
visualize: { crud: !!capabilities.visualize?.save, read: !!capabilities.visualize?.show },
|
||||
dashboard: {
|
||||
|
@ -215,6 +217,8 @@ export const useApplicationCapabilities = (): UseApplicationCapabilities => {
|
|||
permissions.push,
|
||||
permissions.connectors,
|
||||
permissions.settings,
|
||||
permissions.reopenCase,
|
||||
permissions.createComment,
|
||||
]
|
||||
);
|
||||
};
|
||||
|
|
|
@ -83,7 +83,7 @@ export const createStartServicesMock = ({ license }: StartServiceArgs = {}): Sta
|
|||
services.application.capabilities = {
|
||||
...services.application.capabilities,
|
||||
actions: { save: true, show: true },
|
||||
generalCases: {
|
||||
generalCasesV2: {
|
||||
create_cases: true,
|
||||
read_cases: true,
|
||||
update_cases: true,
|
||||
|
@ -91,6 +91,8 @@ export const createStartServicesMock = ({ license }: StartServiceArgs = {}): Sta
|
|||
push_cases: true,
|
||||
cases_connectors: true,
|
||||
cases_settings: true,
|
||||
case_reopen: true,
|
||||
create_comment: true,
|
||||
},
|
||||
visualize: { save: true, show: true },
|
||||
dashboard: { show: true, createNew: true },
|
||||
|
|
|
@ -17,6 +17,8 @@ export const noCasesPermissions = () =>
|
|||
push: false,
|
||||
connectors: false,
|
||||
settings: false,
|
||||
createComment: false,
|
||||
reopenCase: false,
|
||||
});
|
||||
|
||||
export const readCasesPermissions = () =>
|
||||
|
@ -28,16 +30,52 @@ export const readCasesPermissions = () =>
|
|||
push: false,
|
||||
connectors: true,
|
||||
settings: false,
|
||||
createComment: false,
|
||||
reopenCase: false,
|
||||
});
|
||||
export const noCreateCasesPermissions = () => buildCasesPermissions({ create: false });
|
||||
export const noUpdateCasesPermissions = () => buildCasesPermissions({ update: false });
|
||||
export const noCreateCommentCasesPermissions = () =>
|
||||
buildCasesPermissions({ createComment: false });
|
||||
export const noUpdateCasesPermissions = () =>
|
||||
buildCasesPermissions({ update: false, reopenCase: false });
|
||||
export const noPushCasesPermissions = () => buildCasesPermissions({ push: false });
|
||||
export const noDeleteCasesPermissions = () => buildCasesPermissions({ delete: false });
|
||||
export const noReopenCasesPermissions = () => buildCasesPermissions({ reopenCase: false });
|
||||
export const writeCasesPermissions = () => buildCasesPermissions({ read: false });
|
||||
export const onlyCreateCommentPermissions = () =>
|
||||
buildCasesPermissions({
|
||||
read: false,
|
||||
create: false,
|
||||
update: false,
|
||||
delete: true,
|
||||
push: false,
|
||||
createComment: true,
|
||||
reopenCase: false,
|
||||
});
|
||||
export const onlyDeleteCasesPermission = () =>
|
||||
buildCasesPermissions({ read: false, create: false, update: false, delete: true, push: false });
|
||||
buildCasesPermissions({
|
||||
read: false,
|
||||
create: false,
|
||||
update: false,
|
||||
delete: true,
|
||||
push: false,
|
||||
createComment: false,
|
||||
reopenCase: false,
|
||||
});
|
||||
// In practice, a real life user should never have this configuration, but testing for thoroughness
|
||||
export const onlyReopenCasesPermission = () =>
|
||||
buildCasesPermissions({
|
||||
read: false,
|
||||
create: false,
|
||||
update: false,
|
||||
delete: false,
|
||||
push: false,
|
||||
createComment: false,
|
||||
reopenCase: true,
|
||||
});
|
||||
export const noConnectorsCasePermission = () => buildCasesPermissions({ connectors: false });
|
||||
export const noCasesSettingsPermission = () => buildCasesPermissions({ settings: false });
|
||||
export const disabledReopenCasePermission = () => buildCasesPermissions({ reopenCase: false });
|
||||
|
||||
export const buildCasesPermissions = (overrides: Partial<Omit<CasesPermissions, 'all'>> = {}) => {
|
||||
const create = overrides.create ?? true;
|
||||
|
@ -47,7 +85,18 @@ export const buildCasesPermissions = (overrides: Partial<Omit<CasesPermissions,
|
|||
const push = overrides.push ?? true;
|
||||
const connectors = overrides.connectors ?? true;
|
||||
const settings = overrides.settings ?? true;
|
||||
const all = create && read && update && deletePermissions && push && settings && connectors;
|
||||
const reopenCase = overrides.reopenCase ?? true;
|
||||
const createComment = overrides.createComment ?? true;
|
||||
const all =
|
||||
create &&
|
||||
read &&
|
||||
update &&
|
||||
deletePermissions &&
|
||||
push &&
|
||||
settings &&
|
||||
connectors &&
|
||||
reopenCase &&
|
||||
createComment;
|
||||
|
||||
return {
|
||||
all,
|
||||
|
@ -58,6 +107,8 @@ export const buildCasesPermissions = (overrides: Partial<Omit<CasesPermissions,
|
|||
push,
|
||||
connectors,
|
||||
settings,
|
||||
reopenCase,
|
||||
createComment,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -71,6 +122,8 @@ export const noCasesCapabilities = () =>
|
|||
push_cases: false,
|
||||
cases_connectors: false,
|
||||
cases_settings: false,
|
||||
create_comment: false,
|
||||
case_reopen: false,
|
||||
});
|
||||
export const readCasesCapabilities = () =>
|
||||
buildCasesCapabilities({
|
||||
|
@ -79,6 +132,8 @@ export const readCasesCapabilities = () =>
|
|||
delete_cases: false,
|
||||
push_cases: false,
|
||||
cases_settings: false,
|
||||
create_comment: false,
|
||||
case_reopen: false,
|
||||
});
|
||||
export const writeCasesCapabilities = () => {
|
||||
return buildCasesCapabilities({
|
||||
|
@ -95,5 +150,7 @@ export const buildCasesCapabilities = (overrides?: Partial<CasesCapabilities>) =
|
|||
push_cases: overrides?.push_cases ?? true,
|
||||
cases_connectors: overrides?.cases_connectors ?? true,
|
||||
cases_settings: overrides?.cases_settings ?? true,
|
||||
create_comment: overrides?.create_comment ?? true,
|
||||
case_reopen: overrides?.case_reopen ?? true,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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 { renderHook } from '@testing-library/react-hooks';
|
||||
import { CaseStatuses } from '../../../../common/types/domain';
|
||||
import { useUserPermissions } from '../../user_actions/use_user_permissions';
|
||||
import { useShouldDisableStatus } from './use_should_disable_status';
|
||||
|
||||
jest.mock('../../user_actions/use_user_permissions');
|
||||
const mockUseUserPermissions = useUserPermissions as jest.Mock;
|
||||
|
||||
describe('useShouldDisableStatus', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should disable status when user has no permissions', () => {
|
||||
mockUseUserPermissions.mockReturnValue({
|
||||
canUpdate: false,
|
||||
canReopenCase: false,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useShouldDisableStatus());
|
||||
|
||||
const cases = [{ status: CaseStatuses.open }];
|
||||
expect(result.current(cases)).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow status change when user has all permissions', () => {
|
||||
mockUseUserPermissions.mockReturnValue({
|
||||
canUpdate: true,
|
||||
canReopenCase: true,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useShouldDisableStatus());
|
||||
|
||||
const cases = [{ status: CaseStatuses.open }];
|
||||
expect(result.current(cases)).toBe(false);
|
||||
});
|
||||
|
||||
it('should only allow reopening when user can only reopen cases', () => {
|
||||
mockUseUserPermissions.mockReturnValue({
|
||||
canUpdate: false,
|
||||
canReopenCase: true,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useShouldDisableStatus());
|
||||
|
||||
const cases = [{ status: CaseStatuses.closed }, { status: CaseStatuses.open }];
|
||||
|
||||
expect(result.current(cases)).toBe(false);
|
||||
|
||||
const closedCases = [{ status: CaseStatuses.closed }];
|
||||
expect(result.current(closedCases)).toBe(false);
|
||||
});
|
||||
|
||||
it('should prevent reopening closed cases when user cannot reopen', () => {
|
||||
mockUseUserPermissions.mockReturnValue({
|
||||
canUpdate: true,
|
||||
canReopenCase: false,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useShouldDisableStatus());
|
||||
|
||||
const closedCases = [{ status: CaseStatuses.closed }];
|
||||
expect(result.current(closedCases)).toBe(true);
|
||||
|
||||
const openCases = [{ status: CaseStatuses.open }];
|
||||
expect(result.current(openCases)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle multiple selected cases correctly', () => {
|
||||
mockUseUserPermissions.mockReturnValue({
|
||||
canUpdate: true,
|
||||
canReopenCase: false,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useShouldDisableStatus());
|
||||
|
||||
const mixedCases = [{ status: CaseStatuses.open }, { status: CaseStatuses.closed }];
|
||||
|
||||
expect(result.current(mixedCases)).toBe(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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 { useCallback } from 'react';
|
||||
import type { CasesUI } from '../../../../common';
|
||||
import { CaseStatuses } from '../../../../common/types/domain';
|
||||
|
||||
import { useUserPermissions } from '../../user_actions/use_user_permissions';
|
||||
|
||||
export const useShouldDisableStatus = () => {
|
||||
const { canUpdate, canReopenCase } = useUserPermissions();
|
||||
|
||||
const shouldDisableStatusFn = useCallback(
|
||||
(selectedCases: Array<Pick<CasesUI[number], 'status'>>) => {
|
||||
// Read Only + Disabled => Cannot do anything
|
||||
const missingAllUpdatePermissions = !canUpdate && !canReopenCase;
|
||||
if (missingAllUpdatePermissions) return true;
|
||||
|
||||
// All + Enabled reopen => can change status at any point in any way
|
||||
if (canUpdate && canReopenCase) return false;
|
||||
|
||||
const selectedCasesContainsClosed = selectedCases.some(
|
||||
(theCase) => theCase.status === CaseStatuses.closed
|
||||
);
|
||||
|
||||
if (selectedCasesContainsClosed) {
|
||||
return !canReopenCase;
|
||||
} else {
|
||||
return !canUpdate;
|
||||
}
|
||||
},
|
||||
[canReopenCase, canUpdate]
|
||||
);
|
||||
|
||||
return shouldDisableStatusFn;
|
||||
};
|
|
@ -13,7 +13,11 @@ import { useStatusAction } from './use_status_action';
|
|||
import * as api from '../../../containers/api';
|
||||
import { basicCase } from '../../../containers/mock';
|
||||
import { CaseStatuses } from '../../../../common/types/domain';
|
||||
import { useUserPermissions } from '../../user_actions/use_user_permissions';
|
||||
import { useShouldDisableStatus } from './use_should_disable_status';
|
||||
|
||||
jest.mock('../../user_actions/use_user_permissions');
|
||||
jest.mock('./use_should_disable_status');
|
||||
jest.mock('../../../containers/api');
|
||||
|
||||
describe('useStatusAction', () => {
|
||||
|
@ -24,6 +28,12 @@ describe('useStatusAction', () => {
|
|||
beforeEach(() => {
|
||||
appMockRender = createAppMockRenderer();
|
||||
jest.clearAllMocks();
|
||||
(useShouldDisableStatus as jest.Mock).mockReturnValue(() => false);
|
||||
|
||||
(useUserPermissions as jest.Mock).mockReturnValue({
|
||||
canUpdate: true,
|
||||
canReopenCase: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders an action', async () => {
|
||||
|
@ -43,7 +53,7 @@ describe('useStatusAction', () => {
|
|||
Array [
|
||||
Object {
|
||||
"data-test-subj": "cases-bulk-action-status-open",
|
||||
"disabled": true,
|
||||
"disabled": false,
|
||||
"icon": "empty",
|
||||
"key": "cases-bulk-action-status-open",
|
||||
"name": "Open",
|
||||
|
@ -172,6 +182,8 @@ describe('useStatusAction', () => {
|
|||
];
|
||||
|
||||
it.each(disabledTests)('disables the status button correctly: %s', async (status, index) => {
|
||||
(useShouldDisableStatus as jest.Mock).mockReturnValue(() => true);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useStatusAction({ onAction, onActionSuccess, isDisabled: false }),
|
||||
{
|
||||
|
@ -197,4 +209,36 @@ describe('useStatusAction', () => {
|
|||
expect(actions[index].disabled).toBe(true);
|
||||
}
|
||||
);
|
||||
|
||||
it('respects user permissions when everything is false', () => {
|
||||
(useUserPermissions as jest.Mock).mockReturnValue({
|
||||
canUpdate: false,
|
||||
canReopenCase: false,
|
||||
});
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useStatusAction({ onAction, onActionSuccess, isDisabled: false }),
|
||||
{
|
||||
wrapper: appMockRender.AppWrapper,
|
||||
}
|
||||
);
|
||||
|
||||
expect(result.current.canUpdateStatus).toBe(false);
|
||||
});
|
||||
|
||||
it('respects user permissions when only reopen is true', () => {
|
||||
(useUserPermissions as jest.Mock).mockReturnValue({
|
||||
canUpdate: false,
|
||||
canReopenCase: true,
|
||||
});
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useStatusAction({ onAction, onActionSuccess, isDisabled: false }),
|
||||
{
|
||||
wrapper: appMockRender.AppWrapper,
|
||||
}
|
||||
);
|
||||
|
||||
expect(result.current.canUpdateStatus).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,7 +14,8 @@ import { CaseStatuses } from '../../../../common/types/domain';
|
|||
import * as i18n from './translations';
|
||||
import type { UseActionProps } from '../types';
|
||||
import { statuses } from '../../status';
|
||||
import { useCasesContext } from '../../cases_context/use_cases_context';
|
||||
import { useUserPermissions } from '../../user_actions/use_user_permissions';
|
||||
import { useShouldDisableStatus } from './use_should_disable_status';
|
||||
|
||||
const getStatusToasterMessage = (status: CaseStatuses, cases: CasesUI): string => {
|
||||
const totalCases = cases.length;
|
||||
|
@ -35,9 +36,6 @@ interface UseStatusActionProps extends UseActionProps {
|
|||
selectedStatus?: CaseStatuses;
|
||||
}
|
||||
|
||||
const shouldDisableStatus = (cases: CasesUI, status: CaseStatuses) =>
|
||||
cases.every((theCase) => theCase.status === status);
|
||||
|
||||
export const useStatusAction = ({
|
||||
onAction,
|
||||
onActionSuccess,
|
||||
|
@ -45,10 +43,7 @@ export const useStatusAction = ({
|
|||
selectedStatus,
|
||||
}: UseStatusActionProps) => {
|
||||
const { mutate: updateCases } = useUpdateCases();
|
||||
const { permissions } = useCasesContext();
|
||||
const canUpdateStatus = permissions.update;
|
||||
const isActionDisabled = isDisabled || !canUpdateStatus;
|
||||
|
||||
const { canUpdate, canReopenCase } = useUserPermissions();
|
||||
const handleUpdateCaseStatus = useCallback(
|
||||
(selectedCases: CasesUI, status: CaseStatuses) => {
|
||||
onAction();
|
||||
|
@ -69,6 +64,8 @@ export const useStatusAction = ({
|
|||
[onAction, updateCases, onActionSuccess]
|
||||
);
|
||||
|
||||
const shouldDisableStatus = useShouldDisableStatus();
|
||||
|
||||
const getStatusIcon = (status: CaseStatuses): string =>
|
||||
selectedStatus && selectedStatus === status ? 'check' : 'empty';
|
||||
|
||||
|
@ -78,7 +75,7 @@ export const useStatusAction = ({
|
|||
name: statuses[CaseStatuses.open].label,
|
||||
icon: getStatusIcon(CaseStatuses.open),
|
||||
onClick: () => handleUpdateCaseStatus(selectedCases, CaseStatuses.open),
|
||||
disabled: isActionDisabled || shouldDisableStatus(selectedCases, CaseStatuses.open),
|
||||
disabled: isDisabled || shouldDisableStatus(selectedCases),
|
||||
'data-test-subj': 'cases-bulk-action-status-open',
|
||||
key: 'cases-bulk-action-status-open',
|
||||
},
|
||||
|
@ -86,8 +83,7 @@ export const useStatusAction = ({
|
|||
name: statuses[CaseStatuses['in-progress']].label,
|
||||
icon: getStatusIcon(CaseStatuses['in-progress']),
|
||||
onClick: () => handleUpdateCaseStatus(selectedCases, CaseStatuses['in-progress']),
|
||||
disabled:
|
||||
isActionDisabled || shouldDisableStatus(selectedCases, CaseStatuses['in-progress']),
|
||||
disabled: isDisabled || shouldDisableStatus(selectedCases),
|
||||
'data-test-subj': 'cases-bulk-action-status-in-progress',
|
||||
key: 'cases-bulk-action-status-in-progress',
|
||||
},
|
||||
|
@ -95,14 +91,14 @@ export const useStatusAction = ({
|
|||
name: statuses[CaseStatuses.closed].label,
|
||||
icon: getStatusIcon(CaseStatuses.closed),
|
||||
onClick: () => handleUpdateCaseStatus(selectedCases, CaseStatuses.closed),
|
||||
disabled: isActionDisabled || shouldDisableStatus(selectedCases, CaseStatuses.closed),
|
||||
disabled: isDisabled || shouldDisableStatus(selectedCases),
|
||||
'data-test-subj': 'cases-bulk-action-status-closed',
|
||||
key: 'cases-bulk-status-action',
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
return { getActions, canUpdateStatus };
|
||||
return { getActions, canUpdateStatus: canUpdate || canReopenCase };
|
||||
};
|
||||
|
||||
export type UseStatusAction = ReturnType<typeof useStatusAction>;
|
||||
|
|
|
@ -10,7 +10,12 @@ import { waitFor, act, fireEvent, screen } from '@testing-library/react';
|
|||
import userEvent from '@testing-library/user-event';
|
||||
import { noop } from 'lodash/fp';
|
||||
|
||||
import { noCreateCasesPermissions, TestProviders, createAppMockRenderer } from '../../common/mock';
|
||||
import {
|
||||
onlyCreateCommentPermissions,
|
||||
noCreateCommentCasesPermissions,
|
||||
TestProviders,
|
||||
createAppMockRenderer,
|
||||
} from '../../common/mock';
|
||||
|
||||
import { AttachmentType } from '../../../common/types/domain';
|
||||
import { SECURITY_SOLUTION_OWNER, MAX_COMMENT_LENGTH } from '../../../common/constants';
|
||||
|
@ -93,19 +98,36 @@ describe('AddComment ', () => {
|
|||
expect(screen.getByTestId('submit-comment')).toHaveAttribute('disabled');
|
||||
});
|
||||
|
||||
it('should hide the component when the user does not have create permissions', () => {
|
||||
it('should hide the component when the user does not have createComment permissions', () => {
|
||||
createAttachmentsMock.mockImplementation(() => ({
|
||||
...defaultResponse,
|
||||
isLoading: true,
|
||||
}));
|
||||
|
||||
appMockRender.render(
|
||||
<TestProviders permissions={noCreateCasesPermissions()}>
|
||||
<TestProviders permissions={noCreateCommentCasesPermissions()}>
|
||||
<AddComment {...{ ...addCommentProps }} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('loading-spinner')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('add-comment-form-wrapper')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show the component when the user does not have create permissions, but has createComment permissions', () => {
|
||||
createAttachmentsMock.mockImplementation(() => ({
|
||||
...defaultResponse,
|
||||
isLoading: true,
|
||||
}));
|
||||
|
||||
appMockRender.render(
|
||||
<TestProviders permissions={onlyCreateCommentPermissions()}>
|
||||
<AddComment {...{ ...addCommentProps }} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('loading-spinner')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('add-comment-form-wrapper')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should post comment on submit click', async () => {
|
||||
|
|
|
@ -191,8 +191,8 @@ export const AddComment = React.memo(
|
|||
size="xl"
|
||||
/>
|
||||
)}
|
||||
{permissions.create && (
|
||||
<Form form={form}>
|
||||
{permissions.createComment && (
|
||||
<Form form={form} data-test-subj="add-comment-form-wrapper">
|
||||
<UseField
|
||||
path={fieldName}
|
||||
component={MarkdownEditorForm}
|
||||
|
|
|
@ -17,6 +17,7 @@ import { useActions } from './use_actions';
|
|||
import { basicCase } from '../../containers/mock';
|
||||
import * as api from '../../containers/api';
|
||||
import type { AppMockRenderer } from '../../common/mock';
|
||||
import { CaseStatuses } from '../../../common/types/domain';
|
||||
import {
|
||||
createAppMockRenderer,
|
||||
noDeleteCasesPermissions,
|
||||
|
@ -382,5 +383,98 @@ describe('useActions', () => {
|
|||
expect(res.getByTestId(`case-action-popover-button-${basicCase.id}`)).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows actions when user only has reopenCase permission and only when case is closed', async () => {
|
||||
appMockRender = createAppMockRenderer({
|
||||
permissions: {
|
||||
all: false,
|
||||
read: true,
|
||||
create: false,
|
||||
update: false,
|
||||
delete: false,
|
||||
reopenCase: true,
|
||||
push: false,
|
||||
connectors: true,
|
||||
settings: false,
|
||||
createComment: false,
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useActions({ disableActions: false }), {
|
||||
wrapper: appMockRender.AppWrapper,
|
||||
});
|
||||
|
||||
expect(result.current.actions).not.toBe(null);
|
||||
const caseWithClosedStatus = { ...basicCase, status: CaseStatuses.closed };
|
||||
const comp = result.current.actions!.render(caseWithClosedStatus) as React.ReactElement;
|
||||
const res = appMockRender.render(comp);
|
||||
|
||||
await user.click(res.getByTestId(`case-action-popover-button-${basicCase.id}`));
|
||||
await waitForEuiPopoverOpen();
|
||||
|
||||
expect(res.queryByTestId(`case-action-status-panel-${basicCase.id}`)).toBeInTheDocument();
|
||||
expect(res.queryByTestId(`case-action-severity-panel-${basicCase.id}`)).toBeFalsy();
|
||||
expect(res.queryByTestId('cases-bulk-action-delete')).toBeFalsy();
|
||||
expect(res.getByTestId('cases-action-copy-id')).toBeInTheDocument();
|
||||
expect(res.queryByTestId(`actions-separator-${basicCase.id}`)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('shows actions with combination of reopenCase and other permissions', async () => {
|
||||
appMockRender = createAppMockRenderer({
|
||||
permissions: {
|
||||
all: false,
|
||||
read: true,
|
||||
create: false,
|
||||
update: false,
|
||||
delete: true,
|
||||
reopenCase: true,
|
||||
push: false,
|
||||
connectors: true,
|
||||
settings: false,
|
||||
createComment: false,
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useActions({ disableActions: false }), {
|
||||
wrapper: appMockRender.AppWrapper,
|
||||
});
|
||||
|
||||
expect(result.current.actions).not.toBe(null);
|
||||
const caseWithClosedStatus = { ...basicCase, status: CaseStatuses.closed };
|
||||
|
||||
const comp = result.current.actions!.render(caseWithClosedStatus) as React.ReactElement;
|
||||
const res = appMockRender.render(comp);
|
||||
|
||||
await user.click(res.getByTestId(`case-action-popover-button-${basicCase.id}`));
|
||||
await waitForEuiPopoverOpen();
|
||||
|
||||
expect(res.queryByTestId(`case-action-status-panel-${basicCase.id}`)).toBeInTheDocument();
|
||||
expect(res.queryByTestId(`case-action-severity-panel-${basicCase.id}`)).toBeFalsy();
|
||||
expect(res.getByTestId('cases-bulk-action-delete')).toBeInTheDocument();
|
||||
expect(res.getByTestId('cases-action-copy-id')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows no actions with everything false but read', async () => {
|
||||
appMockRender = createAppMockRenderer({
|
||||
permissions: {
|
||||
all: false,
|
||||
read: true,
|
||||
create: false,
|
||||
update: false,
|
||||
delete: false,
|
||||
reopenCase: false,
|
||||
push: false,
|
||||
connectors: true,
|
||||
settings: false,
|
||||
createComment: false,
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useActions({ disableActions: false }), {
|
||||
wrapper: appMockRender.AppWrapper,
|
||||
});
|
||||
|
||||
expect(result.current.actions).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -28,6 +28,7 @@ import { EditTagsFlyout } from '../actions/tags/edit_tags_flyout';
|
|||
import { useAssigneesAction } from '../actions/assignees/use_assignees_action';
|
||||
import { EditAssigneesFlyout } from '../actions/assignees/edit_assignees_flyout';
|
||||
import { useCopyIDAction } from '../actions/copy_id/use_copy_id_action';
|
||||
import { useShouldDisableStatus } from '../actions/status/use_should_disable_status';
|
||||
|
||||
const ActionColumnComponent: React.FC<{ theCase: CaseUI; disableActions: boolean }> = ({
|
||||
theCase,
|
||||
|
@ -38,6 +39,12 @@ const ActionColumnComponent: React.FC<{ theCase: CaseUI; disableActions: boolean
|
|||
const closePopover = useCallback(() => setIsPopoverOpen(false), []);
|
||||
const refreshCases = useRefreshCases();
|
||||
|
||||
const shouldDisable = useShouldDisableStatus();
|
||||
|
||||
const shouldDisableStatus = useMemo(() => {
|
||||
return shouldDisable([theCase]);
|
||||
}, [theCase, shouldDisable]);
|
||||
|
||||
const deleteAction = useDeleteAction({
|
||||
isDisabled: false,
|
||||
onAction: closePopover,
|
||||
|
@ -83,7 +90,7 @@ const ActionColumnComponent: React.FC<{ theCase: CaseUI; disableActions: boolean
|
|||
{ id: 0, items: mainPanelItems, title: i18n.ACTIONS },
|
||||
];
|
||||
|
||||
if (canUpdate) {
|
||||
if (!shouldDisableStatus) {
|
||||
mainPanelItems.push({
|
||||
name: (
|
||||
<FormattedMessage
|
||||
|
@ -97,7 +104,8 @@ const ActionColumnComponent: React.FC<{ theCase: CaseUI; disableActions: boolean
|
|||
key: `case-action-status-panel-${theCase.id}`,
|
||||
'data-test-subj': `case-action-status-panel-${theCase.id}`,
|
||||
});
|
||||
|
||||
}
|
||||
if (severityAction.canUpdateSeverity) {
|
||||
mainPanelItems.push({
|
||||
name: (
|
||||
<FormattedMessage
|
||||
|
@ -137,13 +145,14 @@ const ActionColumnComponent: React.FC<{ theCase: CaseUI; disableActions: boolean
|
|||
mainPanelItems.push(deleteAction.getAction([theCase]));
|
||||
}
|
||||
|
||||
if (canUpdate) {
|
||||
if (statusAction.canUpdateStatus || !shouldDisableStatus) {
|
||||
panelsToBuild.push({
|
||||
id: 1,
|
||||
title: i18n.STATUS,
|
||||
items: statusAction.getActions([theCase]),
|
||||
});
|
||||
|
||||
}
|
||||
if (severityAction.canUpdateSeverity) {
|
||||
panelsToBuild.push({
|
||||
id: 2,
|
||||
title: i18n.SEVERITY,
|
||||
|
@ -162,6 +171,7 @@ const ActionColumnComponent: React.FC<{ theCase: CaseUI; disableActions: boolean
|
|||
statusAction,
|
||||
tagsAction,
|
||||
theCase,
|
||||
shouldDisableStatus,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
@ -232,7 +242,7 @@ interface UseBulkActionsProps {
|
|||
|
||||
export const useActions = ({ disableActions }: UseBulkActionsProps): UseBulkActionsReturnValue => {
|
||||
const { permissions } = useCasesContext();
|
||||
const shouldShowActions = permissions.update || permissions.delete;
|
||||
const shouldShowActions = permissions.update || permissions.delete || permissions.reopenCase;
|
||||
|
||||
return {
|
||||
actions: shouldShowActions
|
||||
|
|
|
@ -17,10 +17,12 @@ import {
|
|||
createAppMockRenderer,
|
||||
noDeleteCasesPermissions,
|
||||
onlyDeleteCasesPermission,
|
||||
noReopenCasesPermissions,
|
||||
onlyReopenCasesPermission,
|
||||
} from '../../common/mock';
|
||||
import { useBulkActions } from './use_bulk_actions';
|
||||
import * as api from '../../containers/api';
|
||||
import { basicCase } from '../../containers/mock';
|
||||
import { basicCase, basicCaseClosed } from '../../containers/mock';
|
||||
|
||||
jest.mock('../../containers/api');
|
||||
jest.mock('../../containers/user_profiles/api');
|
||||
|
@ -117,7 +119,7 @@ describe('useBulkActions', () => {
|
|||
"items": Array [
|
||||
Object {
|
||||
"data-test-subj": "cases-bulk-action-status-open",
|
||||
"disabled": true,
|
||||
"disabled": false,
|
||||
"icon": "empty",
|
||||
"key": "cases-bulk-action-status-open",
|
||||
"name": "Open",
|
||||
|
@ -523,5 +525,72 @@ describe('useBulkActions', () => {
|
|||
expect(res.queryByTestId('bulk-actions-separator')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows the correct actions with no reopen permissions', async () => {
|
||||
appMockRender = createAppMockRenderer({ permissions: noReopenCasesPermissions() });
|
||||
const { result, waitFor: waitForHook } = renderHook(
|
||||
() => useBulkActions({ onAction, onActionSuccess, selectedCases: [basicCaseClosed] }),
|
||||
{
|
||||
wrapper: appMockRender.AppWrapper,
|
||||
}
|
||||
);
|
||||
|
||||
const modals = result.current.modals;
|
||||
const panels = result.current.panels;
|
||||
|
||||
const res = appMockRender.render(
|
||||
<>
|
||||
<EuiContextMenu initialPanelId={0} panels={panels} />
|
||||
{modals}
|
||||
</>
|
||||
);
|
||||
|
||||
await waitForHook(() => {
|
||||
expect(res.queryByTestId('case-bulk-action-status')).toBeInTheDocument();
|
||||
res.queryByTestId('case-bulk-action-status')?.click();
|
||||
});
|
||||
|
||||
await waitForHook(() => {
|
||||
expect(res.queryByTestId('cases-bulk-action-status-open')).toBeDisabled();
|
||||
expect(res.queryByTestId('cases-bulk-action-status-in-progress')).toBeDisabled();
|
||||
expect(res.queryByTestId('cases-bulk-action-status-closed')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows the correct actions with reopen permissions', async () => {
|
||||
appMockRender = createAppMockRenderer({ permissions: onlyReopenCasesPermission() });
|
||||
const { result } = renderHook(
|
||||
() => useBulkActions({ onAction, onActionSuccess, selectedCases: [basicCaseClosed] }),
|
||||
{
|
||||
wrapper: appMockRender.AppWrapper,
|
||||
}
|
||||
);
|
||||
|
||||
const { modals, flyouts, panels } = result.current;
|
||||
const renderResult = appMockRender.render(
|
||||
<>
|
||||
<EuiContextMenu initialPanelId={0} panels={panels} />
|
||||
{modals}
|
||||
{flyouts}
|
||||
</>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(renderResult.queryByTestId('case-bulk-action-status')).toBeInTheDocument();
|
||||
expect(renderResult.queryByTestId('case-bulk-action-severity')).toBeInTheDocument();
|
||||
expect(renderResult.queryByTestId('bulk-actions-separator')).not.toBeInTheDocument();
|
||||
expect(renderResult.queryByTestId('case-bulk-action-delete')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
userEvent.click(renderResult.getByTestId('case-bulk-action-status'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(renderResult.queryByTestId('cases-bulk-action-status-open')).not.toBeDisabled();
|
||||
expect(
|
||||
renderResult.queryByTestId('cases-bulk-action-status-in-progress')
|
||||
).not.toBeDisabled();
|
||||
expect(renderResult.queryByTestId('cases-bulk-action-status-closed')).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -76,9 +76,6 @@ export const useBulkActions = ({
|
|||
|
||||
const panels = useMemo((): EuiContextMenuPanelDescriptor[] => {
|
||||
const mainPanelItems: EuiContextMenuPanelItemDescriptor[] = [];
|
||||
const panelsToBuild: EuiContextMenuPanelDescriptor[] = [
|
||||
{ id: 0, items: mainPanelItems, title: i18n.ACTIONS },
|
||||
];
|
||||
|
||||
if (canUpdate) {
|
||||
mainPanelItems.push({
|
||||
|
@ -119,7 +116,13 @@ export const useBulkActions = ({
|
|||
if (canDelete) {
|
||||
mainPanelItems.push(deleteAction.getAction(selectedCases));
|
||||
}
|
||||
|
||||
const panelsToBuild: EuiContextMenuPanelDescriptor[] = [
|
||||
{
|
||||
id: 0,
|
||||
items: [...mainPanelItems], // Create a new array instead of using reference
|
||||
title: i18n.ACTIONS,
|
||||
},
|
||||
];
|
||||
if (canUpdate) {
|
||||
panelsToBuild.push({
|
||||
id: 1,
|
||||
|
|
|
@ -94,7 +94,9 @@ export const CasesTableUtilityBar: FunctionComponent<Props> = React.memo(
|
|||
* Granular permission check for each action is performed
|
||||
* in the useBulkActions hook.
|
||||
*/
|
||||
const showBulkActions = (permissions.update || permissions.delete) && selectedCases.length > 0;
|
||||
const showBulkActions =
|
||||
(permissions.update || permissions.delete || permissions.reopenCase) &&
|
||||
selectedCases.length > 0;
|
||||
|
||||
const visibleCases =
|
||||
pagination?.pageSize && totalCases > pagination.pageSize ? pagination.pageSize : totalCases;
|
||||
|
|
|
@ -39,7 +39,7 @@ const CasesAppComponent: React.FC<CasesAppProps> = ({
|
|||
getFilesClient,
|
||||
owner: [APP_OWNER],
|
||||
useFetchAlertData: () => [false, {}],
|
||||
permissions: userCapabilities.generalCases,
|
||||
permissions: userCapabilities.generalCasesV2,
|
||||
basePath: '/',
|
||||
features: { alerts: { enabled: true, sync: false } },
|
||||
})}
|
||||
|
|
|
@ -21,15 +21,15 @@ jest.mock('../../common/lib/kibana');
|
|||
const useKibanaMock = useKibana as jest.MockedFunction<typeof useKibana>;
|
||||
|
||||
const hasAll = {
|
||||
securitySolutionCases: allCasesCapabilities(),
|
||||
observabilityCases: allCasesCapabilities(),
|
||||
generalCases: allCasesCapabilities(),
|
||||
securitySolutionCasesV2: allCasesCapabilities(),
|
||||
observabilityCasesV2: allCasesCapabilities(),
|
||||
generalCasesV2: allCasesCapabilities(),
|
||||
};
|
||||
|
||||
const secAllObsReadGenNone = {
|
||||
securitySolutionCases: allCasesCapabilities(),
|
||||
observabilityCases: readCasesCapabilities(),
|
||||
generalCases: noCasesCapabilities(),
|
||||
securitySolutionCasesV2: allCasesCapabilities(),
|
||||
observabilityCasesV2: readCasesCapabilities(),
|
||||
generalCasesV2: noCasesCapabilities(),
|
||||
};
|
||||
|
||||
const unrelatedFeatures = {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { APP_ID, FEATURE_ID } from '../../../common/constants';
|
||||
import { APP_ID, FEATURE_ID_V2 } from '../../../common/constants';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import type { CasesPermissions } from '../../containers/types';
|
||||
import { allCasePermissions } from '../../utils/permissions';
|
||||
|
@ -25,7 +25,7 @@ export const useAvailableCasesOwners = (
|
|||
|
||||
return Object.entries(kibanaCapabilities).reduce(
|
||||
(availableOwners: string[], [featureId, kibanaCapability]) => {
|
||||
if (!featureId.endsWith('Cases')) {
|
||||
if (!featureId.endsWith('CasesV2')) {
|
||||
return availableOwners;
|
||||
}
|
||||
for (const cap of capabilities) {
|
||||
|
@ -42,9 +42,9 @@ export const useAvailableCasesOwners = (
|
|||
};
|
||||
|
||||
const getOwnerFromFeatureID = (featureID: string) => {
|
||||
if (featureID === FEATURE_ID) {
|
||||
if (featureID === FEATURE_ID_V2) {
|
||||
return APP_ID;
|
||||
}
|
||||
|
||||
return featureID.replace('Cases', '');
|
||||
return featureID.replace('CasesV2', '');
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiButtonEmpty, useEuiTheme } from '@elastic/eui';
|
||||
import type { CaseStatuses } from '../../../common/types/domain';
|
||||
|
@ -23,6 +23,7 @@ import { useRefreshCaseViewPage } from '../case_view/use_on_refresh_case_view_pa
|
|||
import { useCasesContext } from '../cases_context/use_cases_context';
|
||||
import { useCasesFeatures } from '../../common/use_cases_features';
|
||||
import { useGetCaseConnectors } from '../../containers/use_get_case_connectors';
|
||||
import { useShouldDisableStatus } from '../actions/status/use_should_disable_status';
|
||||
|
||||
export interface CaseActionBarProps {
|
||||
caseData: CaseUI;
|
||||
|
@ -67,6 +68,11 @@ const CaseActionBarComponent: React.FC<CaseActionBarProps> = ({
|
|||
[caseData.settings, onUpdateField]
|
||||
);
|
||||
|
||||
const shouldDisableStatusFn = useShouldDisableStatus();
|
||||
const isStatusMenuDisabled = useMemo(() => {
|
||||
return shouldDisableStatusFn([caseData]);
|
||||
}, [caseData, shouldDisableStatusFn]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="l" justifyContent="flexEnd" data-test-subj="case-action-bar-wrapper">
|
||||
<EuiFlexItem
|
||||
|
@ -83,7 +89,7 @@ const CaseActionBarComponent: React.FC<CaseActionBarProps> = ({
|
|||
<ActionBarStatusItem title={i18n.STATUS} data-test-subj="case-view-status">
|
||||
<StatusContextMenu
|
||||
currentStatus={caseData.status}
|
||||
disabled={!permissions.update}
|
||||
disabled={isStatusMenuDisabled}
|
||||
isLoading={isLoading}
|
||||
onStatusChanged={onStatusChanged}
|
||||
/>
|
||||
|
|
|
@ -10,17 +10,24 @@ import { mount } from 'enzyme';
|
|||
|
||||
import { CaseStatuses } from '../../../common/types/domain';
|
||||
import { StatusContextMenu } from './status_context_menu';
|
||||
import { TestProviders } from '../../common/mock';
|
||||
import { useShouldDisableStatus } from '../actions/status/use_should_disable_status';
|
||||
|
||||
describe('SyncAlertsSwitch', () => {
|
||||
jest.mock('../actions/status/use_should_disable_status');
|
||||
|
||||
describe('StatusContextMenu', () => {
|
||||
const onStatusChanged = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(useShouldDisableStatus as jest.Mock).mockReturnValue(() => false);
|
||||
});
|
||||
|
||||
it('renders', async () => {
|
||||
const wrapper = mount(
|
||||
<StatusContextMenu currentStatus={CaseStatuses.open} onStatusChanged={onStatusChanged} />
|
||||
<TestProviders>
|
||||
<StatusContextMenu currentStatus={CaseStatuses.open} onStatusChanged={onStatusChanged} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find(`[data-test-subj="case-view-status-dropdown"]`).exists()).toBeTruthy();
|
||||
|
@ -28,11 +35,13 @@ describe('SyncAlertsSwitch', () => {
|
|||
|
||||
it('renders a simple status badge when disabled', async () => {
|
||||
const wrapper = mount(
|
||||
<StatusContextMenu
|
||||
disabled={true}
|
||||
currentStatus={CaseStatuses.open}
|
||||
onStatusChanged={onStatusChanged}
|
||||
/>
|
||||
<TestProviders>
|
||||
<StatusContextMenu
|
||||
disabled={true}
|
||||
currentStatus={CaseStatuses.open}
|
||||
onStatusChanged={onStatusChanged}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find(`[data-test-subj="case-view-status-dropdown"]`).exists()).toBeFalsy();
|
||||
|
@ -41,7 +50,9 @@ describe('SyncAlertsSwitch', () => {
|
|||
|
||||
it('renders the current status correctly', async () => {
|
||||
const wrapper = mount(
|
||||
<StatusContextMenu currentStatus={CaseStatuses.closed} onStatusChanged={onStatusChanged} />
|
||||
<TestProviders>
|
||||
<StatusContextMenu currentStatus={CaseStatuses.closed} onStatusChanged={onStatusChanged} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find(`[data-test-subj="case-view-status-dropdown"]`).first().text()).toBe(
|
||||
|
@ -51,7 +62,9 @@ describe('SyncAlertsSwitch', () => {
|
|||
|
||||
it('changes the status', async () => {
|
||||
const wrapper = mount(
|
||||
<StatusContextMenu currentStatus={CaseStatuses.open} onStatusChanged={onStatusChanged} />
|
||||
<TestProviders>
|
||||
<StatusContextMenu currentStatus={CaseStatuses.open} onStatusChanged={onStatusChanged} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper.find(`[data-test-subj="case-view-status-dropdown"] button`).simulate('click');
|
||||
|
@ -62,14 +75,61 @@ describe('SyncAlertsSwitch', () => {
|
|||
expect(onStatusChanged).toHaveBeenCalledWith('in-progress');
|
||||
});
|
||||
|
||||
it('does not call onStatusChanged if selection is same as current status', async () => {
|
||||
it('does not render the button at all if the status cannot change', async () => {
|
||||
(useShouldDisableStatus as jest.Mock).mockReturnValue(() => true);
|
||||
const wrapper = mount(
|
||||
<StatusContextMenu currentStatus={CaseStatuses.open} onStatusChanged={onStatusChanged} />
|
||||
<TestProviders>
|
||||
<StatusContextMenu currentStatus={CaseStatuses.open} onStatusChanged={onStatusChanged} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper.find(`[data-test-subj="case-view-status-dropdown"] button`).simulate('click');
|
||||
wrapper.find(`[data-test-subj="case-view-status-dropdown-open"] button`).simulate('click');
|
||||
expect(wrapper.find(`[data-test-subj="case-view-status-dropdown-open"] button`)).toHaveLength(
|
||||
0
|
||||
);
|
||||
|
||||
expect(onStatusChanged).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('updates menu items when shouldDisableStatus changes', async () => {
|
||||
const mockShouldDisableStatus = jest.fn().mockReturnValue(false);
|
||||
(useShouldDisableStatus as jest.Mock).mockReturnValue(mockShouldDisableStatus);
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<StatusContextMenu currentStatus={CaseStatuses.open} onStatusChanged={onStatusChanged} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper.find(`[data-test-subj="case-view-status-dropdown"] button`).simulate('click');
|
||||
|
||||
expect(mockShouldDisableStatus).toHaveBeenCalledWith([{ status: CaseStatuses.open }]);
|
||||
});
|
||||
|
||||
it('handles all statuses being disabled', async () => {
|
||||
(useShouldDisableStatus as jest.Mock).mockReturnValue(() => true);
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<StatusContextMenu currentStatus={CaseStatuses.open} onStatusChanged={onStatusChanged} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper.find(`[data-test-subj="case-view-status-dropdown"] button`).simulate('click');
|
||||
expect(wrapper.find('EuiContextMenuItem').prop('onClick')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('correctly evaluates each status option', async () => {
|
||||
(useShouldDisableStatus as jest.Mock).mockReturnValue(false);
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<StatusContextMenu currentStatus={CaseStatuses.open} onStatusChanged={onStatusChanged} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="case-view-status-dropdown"] button`).exists()
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,6 +12,7 @@ import type { CaseStatuses } from '../../../common/types/domain';
|
|||
import { caseStatuses } from '../../../common/types/domain';
|
||||
import { StatusPopoverButton } from '../status';
|
||||
import { CHANGE_STATUS } from '../all_cases/translations';
|
||||
import { useShouldDisableStatus } from '../actions/status/use_should_disable_status';
|
||||
|
||||
interface Props {
|
||||
currentStatus: CaseStatuses;
|
||||
|
@ -27,6 +28,7 @@ const StatusContextMenuComponent: React.FC<Props> = ({
|
|||
onStatusChanged,
|
||||
}) => {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const shouldDisableStatus = useShouldDisableStatus();
|
||||
const togglePopover = useCallback(
|
||||
() => setIsPopoverOpen((prevPopoverStatus) => !prevPopoverStatus),
|
||||
[]
|
||||
|
@ -57,17 +59,19 @@ const StatusContextMenuComponent: React.FC<Props> = ({
|
|||
|
||||
const panelItems = useMemo(
|
||||
() =>
|
||||
caseStatuses.map((status: CaseStatuses) => (
|
||||
<EuiContextMenuItem
|
||||
data-test-subj={`case-view-status-dropdown-${status}`}
|
||||
icon={status === currentStatus ? 'check' : 'empty'}
|
||||
key={status}
|
||||
onClick={() => onContextMenuItemClick(status)}
|
||||
>
|
||||
<Status status={status} />
|
||||
</EuiContextMenuItem>
|
||||
)),
|
||||
[currentStatus, onContextMenuItemClick]
|
||||
caseStatuses
|
||||
.filter((_: CaseStatuses) => !shouldDisableStatus([{ status: currentStatus }]))
|
||||
.map((status: CaseStatuses) => (
|
||||
<EuiContextMenuItem
|
||||
data-test-subj={`case-view-status-dropdown-${status}`}
|
||||
icon={status === currentStatus ? 'check' : 'empty'}
|
||||
key={status}
|
||||
onClick={() => onContextMenuItemClick(status)}
|
||||
>
|
||||
<Status status={status} />
|
||||
</EuiContextMenuItem>
|
||||
)),
|
||||
[currentStatus, onContextMenuItemClick, shouldDisableStatus]
|
||||
);
|
||||
|
||||
if (disabled) {
|
||||
|
|
|
@ -98,6 +98,8 @@ export const CasesProvider: FC<
|
|||
read: permissions.read,
|
||||
settings: permissions.settings,
|
||||
update: permissions.update,
|
||||
reopenCase: permissions.reopenCase,
|
||||
createComment: permissions.createComment,
|
||||
},
|
||||
basePath,
|
||||
/**
|
||||
|
@ -127,6 +129,8 @@ export const CasesProvider: FC<
|
|||
permissions.read,
|
||||
permissions.settings,
|
||||
permissions.update,
|
||||
permissions.reopenCase,
|
||||
permissions.createComment,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -107,19 +107,9 @@ describe('AddFile', () => {
|
|||
expect(await screen.findByTestId('cases-files-add')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('AddFile is not rendered if user has no create permission', async () => {
|
||||
it('AddFile is not rendered if user has no createComment permission', async () => {
|
||||
appMockRender = createAppMockRenderer({
|
||||
permissions: buildCasesPermissions({ create: false }),
|
||||
});
|
||||
|
||||
appMockRender.render(<AddFile caseId={'foobar'} />);
|
||||
|
||||
expect(screen.queryByTestId('cases-files-add')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('AddFile is not rendered if user has no update permission', async () => {
|
||||
appMockRender = createAppMockRenderer({
|
||||
permissions: buildCasesPermissions({ update: false }),
|
||||
permissions: buildCasesPermissions({ createComment: false }),
|
||||
});
|
||||
|
||||
appMockRender.render(<AddFile caseId={'foobar'} />);
|
||||
|
|
|
@ -107,7 +107,7 @@ const AddFileComponent: React.FC<AddFileProps> = ({ caseId }) => {
|
|||
[caseId, createAttachments, owner, refreshAttachmentsTable, showDangerToast, showSuccessToast]
|
||||
);
|
||||
|
||||
return permissions.create && permissions.update ? (
|
||||
return permissions.createComment ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="cases-files-add"
|
||||
|
|
|
@ -262,13 +262,13 @@ describe('RecentCases', () => {
|
|||
it('sets all available solutions correctly', () => {
|
||||
appMockRender = createAppMockRenderer({ owner: [] });
|
||||
/**
|
||||
* We set securitySolutionCases capability to not have
|
||||
* We set securitySolutionCasesV2 capability to not have
|
||||
* any access to cases. This tests that we get the owners
|
||||
* that have at least read access.
|
||||
*/
|
||||
appMockRender.coreStart.application.capabilities = {
|
||||
...appMockRender.coreStart.application.capabilities,
|
||||
securitySolutionCases: noCasesCapabilities(),
|
||||
securitySolutionCasesV2: noCasesCapabilities(),
|
||||
};
|
||||
|
||||
appMockRender.render(<RecentCases {...{ ...defaultProps, maxCasesToShow: 2 }} />);
|
||||
|
|
|
@ -16,7 +16,6 @@ import { getManualAlertIdsWithNoRuleId } from './helpers';
|
|||
import type { UserActionTreeProps } from './types';
|
||||
import { useUserActionsHandler } from './use_user_actions_handler';
|
||||
import { NEW_COMMENT_ID } from './constants';
|
||||
import { useCasesContext } from '../cases_context/use_cases_context';
|
||||
import { UserToolTip } from '../user_profiles/user_tooltip';
|
||||
import { Username } from '../user_profiles/username';
|
||||
import { HoverableAvatar } from '../user_profiles/hoverable_avatar';
|
||||
|
@ -25,6 +24,7 @@ import { useUserActionsPagination } from './use_user_actions_pagination';
|
|||
import { useLastPageUserActions } from './use_user_actions_last_page';
|
||||
import { ShowMoreButton } from './show_more_button';
|
||||
import { useLastPage } from './use_last_page';
|
||||
import { useUserPermissions } from './use_user_permissions';
|
||||
|
||||
const getIconsCss = (hasNextPage: boolean | undefined, euiTheme: EuiThemeComputed<{}>): string => {
|
||||
const customSize = hasNextPage
|
||||
|
@ -108,10 +108,10 @@ export const UserActions = React.memo((props: UserActionTreeProps) => {
|
|||
|
||||
const [loadingAlertData, manualAlertsData] = useFetchAlertData(alertIdsWithoutRuleInfo);
|
||||
|
||||
const { permissions } = useCasesContext();
|
||||
const { getCanAddUserComments } = useUserPermissions();
|
||||
|
||||
// add-comment markdown is not visible in History filter
|
||||
const showCommentEditor = permissions.create && userActivityQueryParams.type !== 'action';
|
||||
const shouldShowCommentEditor = getCanAddUserComments(userActivityQueryParams);
|
||||
|
||||
const {
|
||||
commentRefs,
|
||||
|
@ -136,7 +136,7 @@ export const UserActions = React.memo((props: UserActionTreeProps) => {
|
|||
[caseId, handleUpdate, handleManageMarkdownEditId, statusActionButton, commentRefs]
|
||||
);
|
||||
|
||||
const bottomActions = showCommentEditor
|
||||
const bottomActions = shouldShowCommentEditor
|
||||
? [
|
||||
{
|
||||
username: (
|
||||
|
|
|
@ -0,0 +1,259 @@
|
|||
/*
|
||||
* 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 { renderHook } from '@testing-library/react-hooks';
|
||||
import { useCasesContext } from '../cases_context/use_cases_context';
|
||||
import { useUserPermissions } from './use_user_permissions';
|
||||
import type { UserActivityParams } from '../user_actions_activity_bar/types';
|
||||
|
||||
jest.mock('../cases_context/use_cases_context');
|
||||
const mockUseCasesContext = useCasesContext as jest.Mock;
|
||||
|
||||
describe('useUserPermissions', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('canUpdate permission', () => {
|
||||
it('should return true when user has update permission', () => {
|
||||
mockUseCasesContext.mockReturnValue({
|
||||
permissions: {
|
||||
update: true,
|
||||
reopenCase: false,
|
||||
createComment: false,
|
||||
all: false,
|
||||
read: true,
|
||||
create: false,
|
||||
delete: false,
|
||||
push: false,
|
||||
connectors: true,
|
||||
settings: false,
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useUserPermissions());
|
||||
expect(result.current.canUpdate).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when user lacks update permission', () => {
|
||||
mockUseCasesContext.mockReturnValue({
|
||||
permissions: {
|
||||
update: false,
|
||||
reopenCase: true,
|
||||
createComment: true,
|
||||
all: false,
|
||||
read: true,
|
||||
create: false,
|
||||
delete: false,
|
||||
push: false,
|
||||
connectors: true,
|
||||
settings: false,
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useUserPermissions());
|
||||
expect(result.current.canUpdate).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('canReopenCase permission', () => {
|
||||
it('should return true when user has reopenCase permission', () => {
|
||||
mockUseCasesContext.mockReturnValue({
|
||||
permissions: {
|
||||
update: false,
|
||||
reopenCase: true,
|
||||
createComment: false,
|
||||
all: false,
|
||||
read: true,
|
||||
create: false,
|
||||
delete: false,
|
||||
push: false,
|
||||
connectors: true,
|
||||
settings: false,
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useUserPermissions());
|
||||
expect(result.current.canReopenCase).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when user lacks reopenCase permission', () => {
|
||||
mockUseCasesContext.mockReturnValue({
|
||||
permissions: {
|
||||
update: true,
|
||||
reopenCase: false,
|
||||
createComment: true,
|
||||
all: false,
|
||||
read: true,
|
||||
create: false,
|
||||
delete: false,
|
||||
push: false,
|
||||
connectors: true,
|
||||
settings: false,
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useUserPermissions());
|
||||
expect(result.current.canReopenCase).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCanAddUserComments permission', () => {
|
||||
it('should return false when activity type is "action" regardless of createComment permission', () => {
|
||||
mockUseCasesContext.mockReturnValue({
|
||||
permissions: {
|
||||
update: false,
|
||||
reopenCase: false,
|
||||
createComment: true,
|
||||
all: false,
|
||||
read: true,
|
||||
create: false,
|
||||
delete: false,
|
||||
push: false,
|
||||
connectors: true,
|
||||
settings: false,
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useUserPermissions());
|
||||
const userActivityParams: UserActivityParams = {
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
sortOrder: 'asc',
|
||||
type: 'action',
|
||||
};
|
||||
|
||||
expect(result.current.getCanAddUserComments(userActivityParams)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when type is not "action" and user has createComment permission', () => {
|
||||
mockUseCasesContext.mockReturnValue({
|
||||
permissions: {
|
||||
update: false,
|
||||
reopenCase: false,
|
||||
createComment: true,
|
||||
all: false,
|
||||
read: true,
|
||||
create: false,
|
||||
delete: false,
|
||||
push: false,
|
||||
connectors: true,
|
||||
settings: false,
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useUserPermissions());
|
||||
const userActivityParams: UserActivityParams = {
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
sortOrder: 'asc',
|
||||
type: 'user',
|
||||
};
|
||||
|
||||
expect(result.current.getCanAddUserComments(userActivityParams)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when type is not "action" but user lacks createComment permission', () => {
|
||||
mockUseCasesContext.mockReturnValue({
|
||||
permissions: {
|
||||
update: true,
|
||||
reopenCase: true,
|
||||
createComment: false,
|
||||
all: false,
|
||||
read: true,
|
||||
create: false,
|
||||
delete: false,
|
||||
push: false,
|
||||
connectors: true,
|
||||
settings: false,
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useUserPermissions());
|
||||
const userActivityParams: UserActivityParams = {
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
sortOrder: 'asc',
|
||||
type: 'user',
|
||||
};
|
||||
|
||||
expect(result.current.getCanAddUserComments(userActivityParams)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should maintain stable references to memoized values when permissions do not change', () => {
|
||||
const permissions = {
|
||||
update: true,
|
||||
reopenCase: true,
|
||||
createComment: true,
|
||||
all: false,
|
||||
read: true,
|
||||
create: false,
|
||||
delete: false,
|
||||
push: false,
|
||||
connectors: true,
|
||||
settings: false,
|
||||
};
|
||||
|
||||
mockUseCasesContext.mockReturnValue({ permissions });
|
||||
|
||||
const { result, rerender } = renderHook(() => useUserPermissions());
|
||||
|
||||
const initialCanUpdate = result.current.canUpdate;
|
||||
const initialCanReopenCase = result.current.canReopenCase;
|
||||
const initialGetCanAddUserComments = result.current.getCanAddUserComments;
|
||||
|
||||
rerender();
|
||||
|
||||
expect(result.current.canUpdate).toBe(initialCanUpdate);
|
||||
expect(result.current.canReopenCase).toBe(initialCanReopenCase);
|
||||
expect(result.current.getCanAddUserComments).toBe(initialGetCanAddUserComments);
|
||||
});
|
||||
|
||||
it('should update memoized values when permissions change', () => {
|
||||
const initialPermissions = {
|
||||
update: true,
|
||||
reopenCase: true,
|
||||
createComment: true,
|
||||
all: false,
|
||||
read: true,
|
||||
create: false,
|
||||
delete: false,
|
||||
push: false,
|
||||
connectors: true,
|
||||
settings: false,
|
||||
};
|
||||
|
||||
mockUseCasesContext.mockReturnValue({ permissions: initialPermissions });
|
||||
|
||||
const { result, rerender } = renderHook(() => useUserPermissions());
|
||||
|
||||
const initialCanUpdate = result.current.canUpdate;
|
||||
const initialCanReopenCase = result.current.canReopenCase;
|
||||
const initialGetCanAddUserComments = result.current.getCanAddUserComments;
|
||||
|
||||
const newPermissions = {
|
||||
update: false,
|
||||
reopenCase: false,
|
||||
createComment: true,
|
||||
all: false,
|
||||
read: true,
|
||||
create: false,
|
||||
delete: false,
|
||||
push: false,
|
||||
connectors: true,
|
||||
settings: false,
|
||||
};
|
||||
|
||||
mockUseCasesContext.mockReturnValue({ permissions: newPermissions });
|
||||
rerender();
|
||||
|
||||
expect(result.current.canUpdate).not.toBe(initialCanUpdate);
|
||||
expect(result.current.canReopenCase).not.toBe(initialCanReopenCase);
|
||||
expect(result.current.getCanAddUserComments).toBe(initialGetCanAddUserComments);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 { useCallback } from 'react';
|
||||
import { useCasesContext } from '../cases_context/use_cases_context';
|
||||
import type { UserActivityParams } from '../user_actions_activity_bar/types';
|
||||
|
||||
export const useUserPermissions = () => {
|
||||
const { permissions } = useCasesContext();
|
||||
|
||||
/**
|
||||
* Determines if a user has the capability to update the case. Reopening a case is not part of this capability.
|
||||
*/
|
||||
|
||||
const canUpdate = permissions.update;
|
||||
|
||||
/**
|
||||
* Determines if a user has the capability to change the case from closed => open or closed => in progress
|
||||
*/
|
||||
|
||||
const canReopenCase = permissions.reopenCase;
|
||||
|
||||
/**
|
||||
* Determines if a user has the capability to add comments and attachments
|
||||
*/
|
||||
const getCanAddUserComments = useCallback(
|
||||
(userActivityQueryParams: UserActivityParams) => {
|
||||
if (userActivityQueryParams.type === 'action') return false;
|
||||
return permissions.createComment;
|
||||
},
|
||||
[permissions.createComment]
|
||||
);
|
||||
|
||||
return { getCanAddUserComments, canReopenCase, canUpdate };
|
||||
};
|
|
@ -69,7 +69,7 @@ describe('useGetCases', () => {
|
|||
|
||||
appMockRender.coreStart.application.capabilities = {
|
||||
...appMockRender.coreStart.application.capabilities,
|
||||
observabilityCases: {
|
||||
observabilityCasesV2: {
|
||||
create_cases: true,
|
||||
read_cases: true,
|
||||
update_cases: true,
|
||||
|
@ -78,7 +78,7 @@ describe('useGetCases', () => {
|
|||
delete_cases: true,
|
||||
cases_settings: true,
|
||||
},
|
||||
securitySolutionCases: {
|
||||
securitySolutionCasesV2: {
|
||||
create_cases: true,
|
||||
read_cases: true,
|
||||
update_cases: true,
|
||||
|
|
|
@ -50,6 +50,8 @@ const helpersMock: jest.Mocked<CasesPublicStart['helpers']> = {
|
|||
push: false,
|
||||
connectors: false,
|
||||
settings: false,
|
||||
createComment: false,
|
||||
reopenCase: false,
|
||||
}),
|
||||
getRuleIdFromEvent: jest.fn(),
|
||||
groupAlertsByRule: jest.fn(),
|
||||
|
|
|
@ -2520,6 +2520,90 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`audit_logger log function event structure creates the correct audit event for operation: "reopenCase" with an error and entity 1`] = `
|
||||
Object {
|
||||
"error": Object {
|
||||
"code": "Error",
|
||||
"message": "an error",
|
||||
},
|
||||
"event": Object {
|
||||
"action": "case_reopen",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"outcome": "failure",
|
||||
"type": Array [
|
||||
"change",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "1",
|
||||
"type": "cases",
|
||||
},
|
||||
},
|
||||
"message": "Failed attempt to update cases [id=1] as owner \\"awesome\\"",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`audit_logger log function event structure creates the correct audit event for operation: "reopenCase" with an error but no entity 1`] = `
|
||||
Object {
|
||||
"error": Object {
|
||||
"code": "Error",
|
||||
"message": "an error",
|
||||
},
|
||||
"event": Object {
|
||||
"action": "case_reopen",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"outcome": "failure",
|
||||
"type": Array [
|
||||
"change",
|
||||
],
|
||||
},
|
||||
"message": "Failed attempt to update a case as any owners",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`audit_logger log function event structure creates the correct audit event for operation: "reopenCase" without an error but with an entity 1`] = `
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_reopen",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"outcome": "unknown",
|
||||
"type": Array [
|
||||
"change",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "5",
|
||||
"type": "cases",
|
||||
},
|
||||
},
|
||||
"message": "User is updating cases [id=5] as owner \\"super\\"",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`audit_logger log function event structure creates the correct audit event for operation: "reopenCase" without an error or entity 1`] = `
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_reopen",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"outcome": "unknown",
|
||||
"type": Array [
|
||||
"change",
|
||||
],
|
||||
},
|
||||
"message": "User is updating a case as any owners",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`audit_logger log function event structure creates the correct audit event for operation: "resolveCase" with an error and entity 1`] = `
|
||||
Object {
|
||||
"error": Object {
|
||||
|
|
150
x-pack/plugins/cases/server/authorization/__snapshots__/authorization.test.ts.snap
generated
Normal file
150
x-pack/plugins/cases/server/authorization/__snapshots__/authorization.test.ts.snap
generated
Normal file
|
@ -0,0 +1,150 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`authorization ensureAuthorized with operation arrays handles multiple operations successfully when authorized 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_create",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"outcome": "unknown",
|
||||
"type": Array [
|
||||
"creation",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "1",
|
||||
"type": "cases",
|
||||
},
|
||||
},
|
||||
"message": "User is creating cases [id=1] as owner \\"a\\"",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_get",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"outcome": "success",
|
||||
"type": Array [
|
||||
"access",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "1",
|
||||
"type": "cases",
|
||||
},
|
||||
},
|
||||
"message": "User has accessed cases [id=1] as owner \\"a\\"",
|
||||
},
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`authorization ensureAuthorized with operation arrays logs each operation separately 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_create",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"outcome": "unknown",
|
||||
"type": Array [
|
||||
"creation",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "1",
|
||||
"type": "cases",
|
||||
},
|
||||
},
|
||||
"message": "User is creating cases [id=1] as owner \\"a\\"",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_get",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"outcome": "success",
|
||||
"type": Array [
|
||||
"access",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "1",
|
||||
"type": "cases",
|
||||
},
|
||||
},
|
||||
"message": "User has accessed cases [id=1] as owner \\"a\\"",
|
||||
},
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`authorization ensureAuthorized with operation arrays throws on first unauthorized operation in array 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"error": Object {
|
||||
"code": "Error",
|
||||
"message": "Unauthorized to create, access case with owners: \\"a\\"",
|
||||
},
|
||||
"event": Object {
|
||||
"action": "case_create",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"outcome": "failure",
|
||||
"type": Array [
|
||||
"creation",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "1",
|
||||
"type": "cases",
|
||||
},
|
||||
},
|
||||
"message": "Failed attempt to create cases [id=1] as owner \\"a\\"",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Object {
|
||||
"error": Object {
|
||||
"code": "Error",
|
||||
"message": "Unauthorized to create, access case with owners: \\"a\\"",
|
||||
},
|
||||
"event": Object {
|
||||
"action": "case_get",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"outcome": "failure",
|
||||
"type": Array [
|
||||
"access",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "1",
|
||||
"type": "cases",
|
||||
},
|
||||
},
|
||||
"message": "Failed attempt to access cases [id=1] as owner \\"a\\"",
|
||||
},
|
||||
],
|
||||
]
|
||||
`;
|
|
@ -82,15 +82,18 @@ export class AuthorizationAuditLogger {
|
|||
operation,
|
||||
}: {
|
||||
owners: string[];
|
||||
operation: OperationDetails;
|
||||
operation: OperationDetails | OperationDetails[];
|
||||
}) {
|
||||
const ownerMsg = owners.length <= 0 ? 'of any owner' : `with owners: "${owners.join(', ')}"`;
|
||||
const operations = Array.isArray(operation) ? operation : [operation];
|
||||
const operationVerbs = [...new Set(operations.map((op) => op.verbs.present))].join(', ');
|
||||
const operationDocTypes = [...new Set(operations.map((op) => op.docType))].join(', ');
|
||||
/**
|
||||
* This will take the form:
|
||||
* `Unauthorized to create case with owners: "securitySolution, observability"`
|
||||
* `Unauthorized to access cases of any owner`
|
||||
*/
|
||||
return `Unauthorized to ${operation.verbs.present} ${operation.docType} ${ownerMsg}`;
|
||||
return `Unauthorized to ${operationVerbs} ${operationDocTypes} ${ownerMsg}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1459,4 +1459,80 @@ describe('authorization', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ensureAuthorized with operation arrays', () => {
|
||||
let auth: Authorization;
|
||||
let securityStart: ReturnType<typeof securityMock.createStart>;
|
||||
let featuresStart: jest.Mocked<FeaturesPluginStart>;
|
||||
let spacesStart: jest.Mocked<SpacesPluginStart>;
|
||||
|
||||
beforeEach(async () => {
|
||||
securityStart = securityMock.createStart();
|
||||
securityStart.authz.mode.useRbacForRequest.mockReturnValue(true);
|
||||
securityStart.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue(
|
||||
jest.fn(async () => ({ hasAllRequested: true }))
|
||||
);
|
||||
|
||||
featuresStart = featuresPluginMock.createStart();
|
||||
featuresStart.getKibanaFeatures.mockReturnValue([
|
||||
{ id: '1', cases: ['a'] },
|
||||
] as unknown as KibanaFeature[]);
|
||||
|
||||
spacesStart = createSpacesDisabledFeaturesMock();
|
||||
|
||||
auth = await Authorization.create({
|
||||
request,
|
||||
securityAuth: securityStart.authz,
|
||||
spaces: spacesStart,
|
||||
features: featuresStart,
|
||||
auditLogger: new AuthorizationAuditLogger(mockLogger),
|
||||
logger: loggingSystemMock.createLogger(),
|
||||
});
|
||||
});
|
||||
|
||||
it('handles multiple operations successfully when authorized', async () => {
|
||||
await expect(
|
||||
auth.ensureAuthorized({
|
||||
entities: [{ id: '1', owner: 'a' }],
|
||||
operation: [Operations.createCase, Operations.getCase],
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
|
||||
expect(mockLogger.log.mock.calls).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('throws on first unauthorized operation in array', async () => {
|
||||
securityStart.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue(
|
||||
jest.fn(async () => ({ hasAllRequested: false }))
|
||||
);
|
||||
|
||||
await expect(
|
||||
auth.ensureAuthorized({
|
||||
entities: [{ id: '1', owner: 'a' }],
|
||||
operation: [Operations.createCase, Operations.getCase],
|
||||
})
|
||||
).rejects.toThrow('Unauthorized to create, access case with owners: "a"');
|
||||
|
||||
expect(mockLogger.log.mock.calls).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('logs each operation separately', async () => {
|
||||
await auth.ensureAuthorized({
|
||||
entities: [{ id: '1', owner: 'a' }],
|
||||
operation: [Operations.createCase, Operations.getCase],
|
||||
});
|
||||
|
||||
expect(mockLogger.log).toHaveBeenCalledTimes(2);
|
||||
expect(mockLogger.log.mock.calls).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('handles empty operation array', async () => {
|
||||
await expect(
|
||||
auth.ensureAuthorized({
|
||||
entities: [{ id: '1', owner: 'a' }],
|
||||
operation: [],
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -108,18 +108,17 @@ export class Authorization {
|
|||
operation,
|
||||
}: {
|
||||
entities: OwnerEntity[];
|
||||
operation: OperationDetails;
|
||||
operation: OperationDetails | OperationDetails[];
|
||||
}) {
|
||||
const uniqueOwners = Array.from(new Set(entities.map((entity) => entity.owner)));
|
||||
const operations = Array.isArray(operation) ? operation : [operation];
|
||||
try {
|
||||
const uniqueOwners = Array.from(new Set(entities.map((entity) => entity.owner)));
|
||||
|
||||
await this._ensureAuthorized(uniqueOwners, operation);
|
||||
await this._ensureAuthorized(uniqueOwners, operations);
|
||||
} catch (error) {
|
||||
this.logSavedObjects({ entities, operation, error });
|
||||
this.logSavedObjects({ entities, operation: operations, error });
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logSavedObjects({ entities, operation });
|
||||
this.logSavedObjects({ entities, operation: operations });
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -177,11 +176,15 @@ export class Authorization {
|
|||
error,
|
||||
}: {
|
||||
entities: OwnerEntity[];
|
||||
operation: OperationDetails;
|
||||
operation: OperationDetails | OperationDetails[];
|
||||
error?: Error;
|
||||
}) {
|
||||
const operations = Array.isArray(operation) ? operation : [operation];
|
||||
|
||||
for (const entity of entities) {
|
||||
this.auditLogger.log({ operation, error, entity });
|
||||
for (const op of operations) {
|
||||
this.auditLogger.log({ operation: op, error, entity });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -197,15 +200,13 @@ export class Authorization {
|
|||
}
|
||||
}
|
||||
|
||||
private async _ensureAuthorized(owners: string[], operation: OperationDetails) {
|
||||
private async _ensureAuthorized(owners: string[], operations: OperationDetails[]) {
|
||||
const { securityAuth } = this;
|
||||
const areAllOwnersAvailable = owners.every((owner) => this.featureCaseOwners.has(owner));
|
||||
|
||||
if (securityAuth && this.shouldCheckAuthorization()) {
|
||||
const requiredPrivileges: string[] = owners.map((owner) =>
|
||||
securityAuth.actions.cases.get(owner, operation.name)
|
||||
const requiredPrivileges: string[] = operations.flatMap((operation) =>
|
||||
owners.map((owner) => securityAuth.actions.cases.get(owner, operation.name))
|
||||
);
|
||||
|
||||
const checkPrivileges = securityAuth.checkPrivilegesDynamicallyWithRequest(this.request);
|
||||
const { hasAllRequested } = await checkPrivileges({
|
||||
kibana: requiredPrivileges,
|
||||
|
@ -219,14 +220,20 @@ export class Authorization {
|
|||
* as Privileged.
|
||||
* This check will ensure we don't accidentally let these through
|
||||
*/
|
||||
throw Boom.forbidden(AuthorizationAuditLogger.createFailureMessage({ owners, operation }));
|
||||
throw Boom.forbidden(
|
||||
AuthorizationAuditLogger.createFailureMessage({ owners, operation: operations })
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasAllRequested) {
|
||||
throw Boom.forbidden(AuthorizationAuditLogger.createFailureMessage({ owners, operation }));
|
||||
throw Boom.forbidden(
|
||||
AuthorizationAuditLogger.createFailureMessage({ owners, operation: operations })
|
||||
);
|
||||
}
|
||||
} else if (!areAllOwnersAvailable) {
|
||||
throw Boom.forbidden(AuthorizationAuditLogger.createFailureMessage({ owners, operation }));
|
||||
throw Boom.forbidden(
|
||||
AuthorizationAuditLogger.createFailureMessage({ owners, operation: operations })
|
||||
);
|
||||
}
|
||||
|
||||
// else security is disabled so let the operation proceed
|
||||
|
@ -288,7 +295,6 @@ export class Authorization {
|
|||
const { hasAllRequested, username, privileges } = await checkPrivileges({
|
||||
kibana: [...requiredPrivileges.keys()],
|
||||
});
|
||||
|
||||
return {
|
||||
hasAllRequested,
|
||||
username,
|
||||
|
|
|
@ -59,7 +59,7 @@ const EVENT_TYPES: Record<string, ArrayElement<EcsEvent['type']>> = {
|
|||
};
|
||||
|
||||
/**
|
||||
* These values need to match the respective values in this file: x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts
|
||||
* These values need to match the respective values in this file: x-pack/packages/security/authorization_core/src/privileges/feature_privilege_builder/cases.ts
|
||||
* These are shared between find, get, get all, and delete/delete all
|
||||
* There currently isn't a use case for a user to delete one comment but not all or differentiating between get, get all,
|
||||
* and find operations from a privilege stand point.
|
||||
|
@ -182,6 +182,14 @@ const CaseOperations = {
|
|||
docType: 'cases',
|
||||
savedObjectType: CASE_SAVED_OBJECT,
|
||||
},
|
||||
[WriteOperations.ReopenCase]: {
|
||||
ecsType: EVENT_TYPES.change,
|
||||
name: WriteOperations.ReopenCase as const,
|
||||
action: 'case_reopen',
|
||||
verbs: updateVerbs,
|
||||
docType: 'case',
|
||||
savedObjectType: CASE_SAVED_OBJECT,
|
||||
},
|
||||
};
|
||||
|
||||
const ConfigurationOperations = {
|
||||
|
|
|
@ -63,6 +63,7 @@ export enum WriteOperations {
|
|||
UpdateComment = 'updateComment',
|
||||
CreateConfiguration = 'createConfiguration',
|
||||
UpdateConfiguration = 'updateConfiguration',
|
||||
ReopenCase = 'reopenCase',
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -75,7 +76,7 @@ export interface OperationDetails {
|
|||
ecsType: ArrayElement<EcsEvent['type']>;
|
||||
/**
|
||||
* The name of the operation to authorize against for the privilege check.
|
||||
* These values need to match one of the operation strings defined here: x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts
|
||||
* These values need to match one of the operation strings defined here: x-pack/packages/security/authorization_core/src/privileges/feature_privilege_builder/cases.ts
|
||||
*
|
||||
* To avoid the authorization strings getting too large, new operations should generally fit within one of the
|
||||
* CasesSupportedOperations. In the situation where a new one is needed we'll have to add it to the security plugin.
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CustomFieldTypes } from '../../../common/types/domain';
|
||||
import { CustomFieldTypes, CaseStatuses } from '../../../common/types/domain';
|
||||
import {
|
||||
MAX_CATEGORY_LENGTH,
|
||||
MAX_DESCRIPTION_LENGTH,
|
||||
|
@ -19,6 +19,7 @@ import {
|
|||
} from '../../../common/constants';
|
||||
import { mockCases } from '../../mocks';
|
||||
import { createCasesClientMock, createCasesClientMockArgs } from '../mocks';
|
||||
import { Operations } from '../../authorization';
|
||||
import { bulkUpdate } from './bulk_update';
|
||||
|
||||
describe('update', () => {
|
||||
|
@ -1628,5 +1629,135 @@ describe('update', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Authorization', () => {
|
||||
const clientArgs = createCasesClientMockArgs();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
clientArgs.services.caseService.getCases.mockResolvedValue({ saved_objects: mockCases });
|
||||
clientArgs.services.caseService.getAllCaseComments.mockResolvedValue({
|
||||
saved_objects: [],
|
||||
total: 0,
|
||||
per_page: 10,
|
||||
page: 1,
|
||||
});
|
||||
clientArgs.services.attachmentService.getter.getCaseCommentStats.mockResolvedValue(
|
||||
new Map()
|
||||
);
|
||||
});
|
||||
|
||||
it('checks authorization for updateCase operation', async () => {
|
||||
clientArgs.services.caseService.patchCases.mockResolvedValue({
|
||||
saved_objects: [{ ...mockCases[0] }],
|
||||
});
|
||||
|
||||
await bulkUpdate(
|
||||
{
|
||||
cases: [
|
||||
{
|
||||
id: mockCases[0].id,
|
||||
version: mockCases[0].version ?? '',
|
||||
title: 'Updated title',
|
||||
},
|
||||
],
|
||||
},
|
||||
clientArgs,
|
||||
casesClientMock
|
||||
);
|
||||
|
||||
expect(clientArgs.authorization.ensureAuthorized).toHaveBeenCalledWith({
|
||||
entities: [{ id: mockCases[0].id, owner: mockCases[0].attributes.owner }],
|
||||
operation: [Operations.updateCase],
|
||||
});
|
||||
});
|
||||
|
||||
it('checks authorization for both reopenCase and updateCase operations when reopening a case', async () => {
|
||||
// Mock a closed case
|
||||
const closedCase = {
|
||||
...mockCases[0],
|
||||
attributes: {
|
||||
...mockCases[0].attributes,
|
||||
status: CaseStatuses.closed,
|
||||
},
|
||||
};
|
||||
clientArgs.services.caseService.getCases.mockResolvedValue({ saved_objects: [closedCase] });
|
||||
|
||||
clientArgs.services.caseService.patchCases.mockResolvedValue({
|
||||
saved_objects: [{ ...closedCase }],
|
||||
});
|
||||
|
||||
await bulkUpdate(
|
||||
{
|
||||
cases: [
|
||||
{
|
||||
id: closedCase.id,
|
||||
version: closedCase.version ?? '',
|
||||
status: CaseStatuses.open,
|
||||
},
|
||||
],
|
||||
},
|
||||
clientArgs,
|
||||
casesClientMock
|
||||
);
|
||||
|
||||
expect(clientArgs.authorization.ensureAuthorized).not.toThrow();
|
||||
});
|
||||
|
||||
it('throws when user is not authorized to update case', async () => {
|
||||
const error = new Error('Unauthorized');
|
||||
clientArgs.authorization.ensureAuthorized.mockRejectedValue(error);
|
||||
|
||||
await expect(
|
||||
bulkUpdate(
|
||||
{
|
||||
cases: [
|
||||
{
|
||||
id: mockCases[0].id,
|
||||
version: mockCases[0].version ?? '',
|
||||
title: 'Updated title',
|
||||
},
|
||||
],
|
||||
},
|
||||
clientArgs,
|
||||
casesClientMock
|
||||
)
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Failed to update case, ids: [{\\"id\\":\\"mock-id-1\\",\\"version\\":\\"WzAsMV0=\\"}]: Error: Unauthorized"`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when user is not authorized to reopen case', async () => {
|
||||
const closedCase = {
|
||||
...mockCases[0],
|
||||
attributes: {
|
||||
...mockCases[0].attributes,
|
||||
status: CaseStatuses.closed,
|
||||
},
|
||||
};
|
||||
clientArgs.services.caseService.getCases.mockResolvedValue({ saved_objects: [closedCase] });
|
||||
|
||||
const error = new Error('Unauthorized to reopen case');
|
||||
clientArgs.authorization.ensureAuthorized.mockRejectedValueOnce(error); // Reject reopenCase
|
||||
|
||||
await expect(
|
||||
bulkUpdate(
|
||||
{
|
||||
cases: [
|
||||
{
|
||||
id: closedCase.id,
|
||||
version: closedCase.version ?? '',
|
||||
status: CaseStatuses.open,
|
||||
},
|
||||
],
|
||||
},
|
||||
clientArgs,
|
||||
casesClientMock
|
||||
)
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Failed to update case, ids: [{\\"id\\":\\"mock-id-1\\",\\"version\\":\\"WzAsMV0=\\"}]: Error: Unauthorized to reopen case"`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -272,9 +272,11 @@ function partitionPatchRequest(
|
|||
conflictedCases: CasePatchRequest[];
|
||||
// This will be a deduped array of case IDs with their corresponding owner
|
||||
casesToAuthorize: OwnerEntity[];
|
||||
reopenedCases: CasePatchRequest[];
|
||||
} {
|
||||
const nonExistingCases: CasePatchRequest[] = [];
|
||||
const conflictedCases: CasePatchRequest[] = [];
|
||||
const reopenedCases: CasePatchRequest[] = [];
|
||||
const casesToAuthorize: Map<string, OwnerEntity> = new Map<string, OwnerEntity>();
|
||||
|
||||
for (const reqCase of patchReqCases) {
|
||||
|
@ -286,6 +288,13 @@ function partitionPatchRequest(
|
|||
conflictedCases.push(reqCase);
|
||||
// let's try to authorize the conflicted case even though we'll fail after afterwards just in case
|
||||
casesToAuthorize.set(foundCase.id, { id: foundCase.id, owner: foundCase.attributes.owner });
|
||||
} else if (
|
||||
reqCase.status != null &&
|
||||
foundCase.attributes.status !== reqCase.status &&
|
||||
foundCase.attributes.status === CaseStatuses.closed
|
||||
) {
|
||||
// Track cases that are closed and a user is attempting to reopen
|
||||
reopenedCases.push(reqCase);
|
||||
} else {
|
||||
casesToAuthorize.set(foundCase.id, { id: foundCase.id, owner: foundCase.attributes.owner });
|
||||
}
|
||||
|
@ -294,6 +303,7 @@ function partitionPatchRequest(
|
|||
return {
|
||||
nonExistingCases,
|
||||
conflictedCases,
|
||||
reopenedCases,
|
||||
casesToAuthorize: Array.from(casesToAuthorize.values()),
|
||||
};
|
||||
}
|
||||
|
@ -344,14 +354,17 @@ export const bulkUpdate = async (
|
|||
return acc;
|
||||
}, new Map<string, CaseSavedObjectTransformed>());
|
||||
|
||||
const { nonExistingCases, conflictedCases, casesToAuthorize } = partitionPatchRequest(
|
||||
casesMap,
|
||||
query.cases
|
||||
);
|
||||
const { nonExistingCases, conflictedCases, casesToAuthorize, reopenedCases } =
|
||||
partitionPatchRequest(casesMap, query.cases);
|
||||
|
||||
const operationsToAuthorize =
|
||||
reopenedCases.length > 0
|
||||
? [Operations.reopenCase, Operations.updateCase]
|
||||
: [Operations.updateCase];
|
||||
|
||||
await authorization.ensureAuthorized({
|
||||
entities: casesToAuthorize,
|
||||
operation: Operations.updateCase,
|
||||
operation: operationsToAuthorize,
|
||||
});
|
||||
|
||||
if (nonExistingCases.length > 0) {
|
||||
|
|
|
@ -36,6 +36,7 @@ describe('getCasesConnectorType', () => {
|
|||
'cases:my-owner/updateComment',
|
||||
'cases:my-owner/deleteComment',
|
||||
'cases:my-owner/findConfigurations',
|
||||
'cases:my-owner/reopenCase',
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -356,6 +357,7 @@ describe('getCasesConnectorType', () => {
|
|||
'cases:securitySolution/updateComment',
|
||||
'cases:securitySolution/deleteComment',
|
||||
'cases:securitySolution/findConfigurations',
|
||||
'cases:securitySolution/reopenCase',
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -376,6 +378,7 @@ describe('getCasesConnectorType', () => {
|
|||
'cases:observability/updateComment',
|
||||
'cases:observability/deleteComment',
|
||||
'cases:observability/findConfigurations',
|
||||
'cases:observability/reopenCase',
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -396,6 +399,7 @@ describe('getCasesConnectorType', () => {
|
|||
'cases:securitySolution/updateComment',
|
||||
'cases:securitySolution/deleteComment',
|
||||
'cases:securitySolution/findConfigurations',
|
||||
'cases:securitySolution/reopenCase',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -507,6 +507,7 @@ describe('utils', () => {
|
|||
'cases:my-owner/updateComment',
|
||||
'cases:my-owner/deleteComment',
|
||||
'cases:my-owner/findConfigurations',
|
||||
'cases:my-owner/reopenCase',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -109,7 +109,7 @@ export const buildCustomFieldsForRequest = (
|
|||
export const constructRequiredKibanaPrivileges = (owner: string): string[] => {
|
||||
/**
|
||||
* Kibana features privileges are defined in
|
||||
* x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts
|
||||
* x-pack/packages/security/authorization_core/src/privileges/feature_privilege_builder/cases.ts
|
||||
*/
|
||||
return [
|
||||
`cases:${owner}/createCase`,
|
||||
|
@ -120,5 +120,6 @@ export const constructRequiredKibanaPrivileges = (owner: string): string[] => {
|
|||
`cases:${owner}/updateComment`,
|
||||
`cases:${owner}/deleteComment`,
|
||||
`cases:${owner}/findConfigurations`,
|
||||
`cases:${owner}/reopenCase`,
|
||||
];
|
||||
};
|
||||
|
|
18
x-pack/plugins/cases/server/features/constants.ts
Normal file
18
x-pack/plugins/cases/server/features/constants.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Unique sub privilege ids for cases.
|
||||
* @description When upgrading (creating new versions), the sub-privileges
|
||||
* do not need to be versioned as they are appended to the top level privilege id which is the only id
|
||||
* that will need to be versioned
|
||||
*/
|
||||
|
||||
export const CASES_DELETE_SUB_PRIVILEGE_ID = 'cases_delete';
|
||||
export const CASES_SETTINGS_SUB_PRIVILEGE_ID = 'cases_settings';
|
||||
export const CASES_CREATE_COMMENT_SUB_PRIVILEGE_ID = 'create_comment';
|
||||
export const CASES_REOPEN_SUB_PRIVILEGE_ID = 'case_reopen';
|
15
x-pack/plugins/cases/server/features/index.ts
Normal file
15
x-pack/plugins/cases/server/features/index.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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 type { KibanaFeatureConfig } from '@kbn/features-plugin/common';
|
||||
import { getV1 } from './v1';
|
||||
import { getV2 } from './v2';
|
||||
|
||||
export const getCasesKibanaFeatures = (): {
|
||||
v1: KibanaFeatureConfig;
|
||||
v2: KibanaFeatureConfig;
|
||||
} => ({ v1: getV1(), v2: getV2() });
|
|
@ -12,8 +12,9 @@ import { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/s
|
|||
import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server';
|
||||
|
||||
import { KibanaFeatureScope } from '@kbn/features-plugin/common';
|
||||
import { APP_ID, FEATURE_ID } from '../common/constants';
|
||||
import { createUICapabilities, getApiTags } from '../common';
|
||||
import { APP_ID, FEATURE_ID, FEATURE_ID_V2 } from '../../common/constants';
|
||||
import { createUICapabilities, getApiTags } from '../../common';
|
||||
import { CASES_DELETE_SUB_PRIVILEGE_ID, CASES_SETTINGS_SUB_PRIVILEGE_ID } from './constants';
|
||||
|
||||
/**
|
||||
* The order of appearance in the feature privilege page
|
||||
|
@ -23,14 +24,24 @@ import { createUICapabilities, getApiTags } from '../common';
|
|||
|
||||
const FEATURE_ORDER = 3100;
|
||||
|
||||
export const getCasesKibanaFeature = (): KibanaFeatureConfig => {
|
||||
export const getV1 = (): KibanaFeatureConfig => {
|
||||
const capabilities = createUICapabilities();
|
||||
const apiTags = getApiTags(APP_ID);
|
||||
|
||||
return {
|
||||
deprecated: {
|
||||
notice: i18n.translate('xpack.cases.features.casesFeature.deprecationMessage', {
|
||||
defaultMessage:
|
||||
'The {currentId} permissions are deprecated, please see {casesFeatureIdV2}.',
|
||||
values: {
|
||||
currentId: FEATURE_ID,
|
||||
casesFeatureIdV2: FEATURE_ID_V2,
|
||||
},
|
||||
}),
|
||||
},
|
||||
id: FEATURE_ID,
|
||||
name: i18n.translate('xpack.cases.features.casesFeatureName', {
|
||||
defaultMessage: 'Cases',
|
||||
name: i18n.translate('xpack.cases.features.casesFeatureNameDeprecated', {
|
||||
defaultMessage: 'Cases (Deprecated)',
|
||||
}),
|
||||
category: DEFAULT_APP_CATEGORIES.management,
|
||||
scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security],
|
||||
|
@ -42,12 +53,14 @@ export const getCasesKibanaFeature = (): KibanaFeatureConfig => {
|
|||
cases: [APP_ID],
|
||||
privileges: {
|
||||
all: {
|
||||
api: apiTags.all,
|
||||
api: [...apiTags.all, ...apiTags.createComment],
|
||||
cases: {
|
||||
create: [APP_ID],
|
||||
read: [APP_ID],
|
||||
update: [APP_ID],
|
||||
push: [APP_ID],
|
||||
createComment: [APP_ID],
|
||||
reopenCase: [APP_ID],
|
||||
},
|
||||
management: {
|
||||
insightsAndAlerting: [APP_ID],
|
||||
|
@ -57,6 +70,15 @@ export const getCasesKibanaFeature = (): KibanaFeatureConfig => {
|
|||
read: [...filesSavedObjectTypes],
|
||||
},
|
||||
ui: capabilities.all,
|
||||
replacedBy: {
|
||||
default: [{ feature: FEATURE_ID_V2, privileges: ['all'] }],
|
||||
minimal: [
|
||||
{
|
||||
feature: FEATURE_ID_V2,
|
||||
privileges: ['minimal_all', 'create_comment', 'case_reopen'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
read: {
|
||||
api: apiTags.read,
|
||||
|
@ -71,6 +93,10 @@ export const getCasesKibanaFeature = (): KibanaFeatureConfig => {
|
|||
read: [...filesSavedObjectTypes],
|
||||
},
|
||||
ui: capabilities.read,
|
||||
replacedBy: {
|
||||
default: [{ feature: FEATURE_ID_V2, privileges: ['read'] }],
|
||||
minimal: [{ feature: FEATURE_ID_V2, privileges: ['minimal_read'] }],
|
||||
},
|
||||
},
|
||||
},
|
||||
subFeatures: [
|
||||
|
@ -84,7 +110,7 @@ export const getCasesKibanaFeature = (): KibanaFeatureConfig => {
|
|||
privileges: [
|
||||
{
|
||||
api: apiTags.delete,
|
||||
id: 'cases_delete',
|
||||
id: CASES_DELETE_SUB_PRIVILEGE_ID,
|
||||
name: i18n.translate('xpack.cases.features.deleteSubFeatureDetails', {
|
||||
defaultMessage: 'Delete cases and comments',
|
||||
}),
|
||||
|
@ -97,6 +123,9 @@ export const getCasesKibanaFeature = (): KibanaFeatureConfig => {
|
|||
delete: [APP_ID],
|
||||
},
|
||||
ui: capabilities.delete,
|
||||
replacedBy: [
|
||||
{ feature: FEATURE_ID_V2, privileges: [CASES_DELETE_SUB_PRIVILEGE_ID] },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -111,7 +140,7 @@ export const getCasesKibanaFeature = (): KibanaFeatureConfig => {
|
|||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
id: 'cases_settings',
|
||||
id: CASES_SETTINGS_SUB_PRIVILEGE_ID,
|
||||
name: i18n.translate('xpack.cases.features.casesSettingsSubFeatureDetails', {
|
||||
defaultMessage: 'Edit case settings',
|
||||
}),
|
||||
|
@ -124,6 +153,9 @@ export const getCasesKibanaFeature = (): KibanaFeatureConfig => {
|
|||
settings: [APP_ID],
|
||||
},
|
||||
ui: capabilities.settings,
|
||||
replacedBy: [
|
||||
{ feature: FEATURE_ID_V2, privileges: [CASES_SETTINGS_SUB_PRIVILEGE_ID] },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
195
x-pack/plugins/cases/server/features/v2.ts
Normal file
195
x-pack/plugins/cases/server/features/v2.ts
Normal file
|
@ -0,0 +1,195 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
import type { KibanaFeatureConfig } from '@kbn/features-plugin/common';
|
||||
import { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/saved_objects';
|
||||
import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server';
|
||||
|
||||
import { KibanaFeatureScope } from '@kbn/features-plugin/common';
|
||||
import { APP_ID, FEATURE_ID_V2 } from '../../common/constants';
|
||||
import { createUICapabilities, getApiTags } from '../../common';
|
||||
import {
|
||||
CASES_DELETE_SUB_PRIVILEGE_ID,
|
||||
CASES_SETTINGS_SUB_PRIVILEGE_ID,
|
||||
CASES_CREATE_COMMENT_SUB_PRIVILEGE_ID,
|
||||
CASES_REOPEN_SUB_PRIVILEGE_ID,
|
||||
} from './constants';
|
||||
|
||||
/**
|
||||
* The order of appearance in the feature privilege page
|
||||
* under the management section. Cases should be under
|
||||
* the Actions and Connectors feature
|
||||
*/
|
||||
|
||||
const FEATURE_ORDER = 3100;
|
||||
|
||||
export const getV2 = (): KibanaFeatureConfig => {
|
||||
const capabilities = createUICapabilities();
|
||||
const apiTags = getApiTags(APP_ID);
|
||||
|
||||
return {
|
||||
id: FEATURE_ID_V2,
|
||||
name: i18n.translate('xpack.cases.features.casesFeatureName', {
|
||||
defaultMessage: 'Cases',
|
||||
}),
|
||||
category: DEFAULT_APP_CATEGORIES.management,
|
||||
scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security],
|
||||
app: [],
|
||||
order: FEATURE_ORDER,
|
||||
management: {
|
||||
insightsAndAlerting: [APP_ID],
|
||||
},
|
||||
cases: [APP_ID],
|
||||
privileges: {
|
||||
all: {
|
||||
api: apiTags.all,
|
||||
cases: {
|
||||
create: [APP_ID],
|
||||
read: [APP_ID],
|
||||
update: [APP_ID],
|
||||
push: [APP_ID],
|
||||
},
|
||||
management: {
|
||||
insightsAndAlerting: [APP_ID],
|
||||
},
|
||||
savedObject: {
|
||||
all: [...filesSavedObjectTypes],
|
||||
read: [...filesSavedObjectTypes],
|
||||
},
|
||||
ui: capabilities.all,
|
||||
},
|
||||
read: {
|
||||
api: apiTags.read,
|
||||
cases: {
|
||||
read: [APP_ID],
|
||||
},
|
||||
management: {
|
||||
insightsAndAlerting: [APP_ID],
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [...filesSavedObjectTypes],
|
||||
},
|
||||
ui: capabilities.read,
|
||||
},
|
||||
},
|
||||
subFeatures: [
|
||||
{
|
||||
name: i18n.translate('xpack.cases.features.deleteSubFeatureName', {
|
||||
defaultMessage: 'Delete',
|
||||
}),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
api: apiTags.delete,
|
||||
id: CASES_DELETE_SUB_PRIVILEGE_ID,
|
||||
name: i18n.translate('xpack.cases.features.deleteSubFeatureDetails', {
|
||||
defaultMessage: 'Delete cases and comments',
|
||||
}),
|
||||
includeIn: 'all',
|
||||
savedObject: {
|
||||
all: [...filesSavedObjectTypes],
|
||||
read: [...filesSavedObjectTypes],
|
||||
},
|
||||
cases: {
|
||||
delete: [APP_ID],
|
||||
},
|
||||
ui: capabilities.delete,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.cases.features.casesSettingsSubFeatureName', {
|
||||
defaultMessage: 'Case settings',
|
||||
}),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
id: CASES_SETTINGS_SUB_PRIVILEGE_ID,
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.cases.features.addCommentsSubFeatureName', {
|
||||
defaultMessage: 'Create comments & attachments',
|
||||
}),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
api: apiTags.createComment,
|
||||
id: CASES_CREATE_COMMENT_SUB_PRIVILEGE_ID,
|
||||
name: i18n.translate('xpack.cases.features.addCommentsSubFeatureDetails', {
|
||||
defaultMessage: 'Add comments to cases',
|
||||
}),
|
||||
includeIn: 'all',
|
||||
savedObject: {
|
||||
all: [...filesSavedObjectTypes],
|
||||
read: [...filesSavedObjectTypes],
|
||||
},
|
||||
cases: {
|
||||
createComment: [APP_ID],
|
||||
},
|
||||
ui: capabilities.createComment,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.cases.features.reopenCaseSubFeatureName', {
|
||||
defaultMessage: 'Re-open',
|
||||
}),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
id: CASES_REOPEN_SUB_PRIVILEGE_ID,
|
||||
name: i18n.translate('xpack.cases.features.reopenCaseSubFeatureDetails', {
|
||||
defaultMessage: 'Re-open closed cases',
|
||||
}),
|
||||
includeIn: 'all',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
cases: {
|
||||
reopenCase: [APP_ID],
|
||||
},
|
||||
ui: capabilities.reopenCase,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
|
@ -30,7 +30,7 @@ import type {
|
|||
CasesServerStartDependencies,
|
||||
} from './types';
|
||||
import { CasesClientFactory } from './client/factory';
|
||||
import { getCasesKibanaFeature } from './features';
|
||||
import { getCasesKibanaFeatures } from './features';
|
||||
import { registerRoutes } from './routes/api/register_routes';
|
||||
import { getExternalRoutes } from './routes/api/get_external_routes';
|
||||
import { createCasesTelemetry, scheduleCasesTelemetryTask } from './telemetry';
|
||||
|
@ -92,7 +92,11 @@ export class CasePlugin
|
|||
this.lensEmbeddableFactory = plugins.lens.lensEmbeddableFactory;
|
||||
|
||||
if (this.caseConfig.stack.enabled) {
|
||||
plugins.features.registerKibanaFeature(getCasesKibanaFeature());
|
||||
// V1 is deprecated, but has to be maintained for the time being
|
||||
// https://github.com/elastic/kibana/pull/186800#issue-2369812818
|
||||
const casesFeatures = getCasesKibanaFeatures();
|
||||
plugins.features.registerKibanaFeature(casesFeatures.v1);
|
||||
plugins.features.registerKibanaFeature(casesFeatures.v2);
|
||||
}
|
||||
|
||||
registerSavedObjects({
|
||||
|
|
|
@ -188,6 +188,7 @@ export interface FeatureKibanaPrivileges {
|
|||
read?: readonly string[];
|
||||
/**
|
||||
* List of case owners which users should have update access to when granted this privilege.
|
||||
* This privilege does NOT provide access to re-opening a case. Please see `reopenCase` for said functionality.
|
||||
* @example
|
||||
* ```ts
|
||||
* {
|
||||
|
@ -216,6 +217,26 @@ export interface FeatureKibanaPrivileges {
|
|||
* ```
|
||||
*/
|
||||
settings?: readonly string[];
|
||||
/**
|
||||
* List of case owners whose users should have createComment access when granted this privilege.
|
||||
* @example
|
||||
* ```ts
|
||||
* {
|
||||
* createComment: ['securitySolution']
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
createComment?: readonly string[];
|
||||
/**
|
||||
* List of case owners whose users should have reopenCase access when granted this privilege.
|
||||
* @example
|
||||
* ```ts
|
||||
* {
|
||||
* reopenCase: ['securitySolution']
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
reopenCase?: readonly string[];
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -557,9 +557,11 @@ Array [
|
|||
"cases": Object {
|
||||
"all": Array [],
|
||||
"create": Array [],
|
||||
"createComment": Array [],
|
||||
"delete": Array [],
|
||||
"push": Array [],
|
||||
"read": Array [],
|
||||
"reopenCase": Array [],
|
||||
"settings": Array [],
|
||||
"update": Array [],
|
||||
},
|
||||
|
@ -716,9 +718,11 @@ Array [
|
|||
"cases": Object {
|
||||
"all": Array [],
|
||||
"create": Array [],
|
||||
"createComment": Array [],
|
||||
"delete": Array [],
|
||||
"push": Array [],
|
||||
"read": Array [],
|
||||
"reopenCase": Array [],
|
||||
"settings": Array [],
|
||||
"update": Array [],
|
||||
},
|
||||
|
@ -1050,9 +1054,11 @@ Array [
|
|||
"cases": Object {
|
||||
"all": Array [],
|
||||
"create": Array [],
|
||||
"createComment": Array [],
|
||||
"delete": Array [],
|
||||
"push": Array [],
|
||||
"read": Array [],
|
||||
"reopenCase": Array [],
|
||||
"settings": Array [],
|
||||
"update": Array [],
|
||||
},
|
||||
|
@ -1190,9 +1196,11 @@ Array [
|
|||
"cases": Object {
|
||||
"all": Array [],
|
||||
"create": Array [],
|
||||
"createComment": Array [],
|
||||
"delete": Array [],
|
||||
"push": Array [],
|
||||
"read": Array [],
|
||||
"reopenCase": Array [],
|
||||
"settings": Array [],
|
||||
"update": Array [],
|
||||
},
|
||||
|
@ -1349,9 +1357,11 @@ Array [
|
|||
"cases": Object {
|
||||
"all": Array [],
|
||||
"create": Array [],
|
||||
"createComment": Array [],
|
||||
"delete": Array [],
|
||||
"push": Array [],
|
||||
"read": Array [],
|
||||
"reopenCase": Array [],
|
||||
"settings": Array [],
|
||||
"update": Array [],
|
||||
},
|
||||
|
@ -1683,9 +1693,11 @@ Array [
|
|||
"cases": Object {
|
||||
"all": Array [],
|
||||
"create": Array [],
|
||||
"createComment": Array [],
|
||||
"delete": Array [],
|
||||
"push": Array [],
|
||||
"read": Array [],
|
||||
"reopenCase": Array [],
|
||||
"settings": Array [],
|
||||
"update": Array [],
|
||||
},
|
||||
|
|
|
@ -78,6 +78,8 @@ describe('featurePrivilegeIterator', () => {
|
|||
delete: ['cases-delete-type'],
|
||||
push: ['cases-push-type'],
|
||||
settings: ['cases-settings-type'],
|
||||
createComment: ['cases-create-comment-type'],
|
||||
reopenCase: ['cases-reopen-type'],
|
||||
},
|
||||
ui: ['ui-action'],
|
||||
},
|
||||
|
@ -148,6 +150,8 @@ describe('featurePrivilegeIterator', () => {
|
|||
delete: ['cases-delete-type'],
|
||||
push: ['cases-push-type'],
|
||||
settings: ['cases-settings-type'],
|
||||
createComment: ['cases-create-comment-type'],
|
||||
reopenCase: ['cases-reopen-type'],
|
||||
},
|
||||
ui: ['ui-action'],
|
||||
},
|
||||
|
@ -217,6 +221,8 @@ describe('featurePrivilegeIterator', () => {
|
|||
delete: ['cases-delete-type'],
|
||||
push: ['cases-push-type'],
|
||||
settings: ['cases-settings-type'],
|
||||
createComment: ['cases-create-comment-type'],
|
||||
reopenCase: ['cases-reopen-type'],
|
||||
},
|
||||
ui: ['ui-action'],
|
||||
},
|
||||
|
@ -288,6 +294,8 @@ describe('featurePrivilegeIterator', () => {
|
|||
delete: ['cases-delete-type'],
|
||||
push: ['cases-push-type'],
|
||||
settings: ['cases-settings-type'],
|
||||
createComment: ['cases-create-comment-type'],
|
||||
reopenCase: ['cases-reopen-type'],
|
||||
},
|
||||
ui: ['ui-action'],
|
||||
},
|
||||
|
@ -329,6 +337,8 @@ describe('featurePrivilegeIterator', () => {
|
|||
delete: ['cases-delete-type'],
|
||||
push: ['cases-push-type'],
|
||||
settings: ['cases-settings-type'],
|
||||
createComment: ['cases-create-comment-type'],
|
||||
reopenCase: ['cases-reopen-type'],
|
||||
},
|
||||
ui: ['ui-action'],
|
||||
},
|
||||
|
@ -391,6 +401,8 @@ describe('featurePrivilegeIterator', () => {
|
|||
delete: ['cases-delete-sub-type'],
|
||||
push: ['cases-push-sub-type'],
|
||||
settings: ['cases-settings-sub-type'],
|
||||
createComment: ['cases-create-comment-type'],
|
||||
reopenCase: ['cases-reopen-type'],
|
||||
},
|
||||
ui: ['ui-sub-type'],
|
||||
},
|
||||
|
@ -438,6 +450,8 @@ describe('featurePrivilegeIterator', () => {
|
|||
delete: ['cases-delete-type'],
|
||||
push: ['cases-push-type'],
|
||||
settings: ['cases-settings-type'],
|
||||
createComment: ['cases-create-comment-type'],
|
||||
reopenCase: ['cases-reopen-type'],
|
||||
},
|
||||
ui: ['ui-action'],
|
||||
},
|
||||
|
@ -506,6 +520,8 @@ describe('featurePrivilegeIterator', () => {
|
|||
delete: ['cases-delete-type'],
|
||||
push: ['cases-push-type'],
|
||||
settings: ['cases-settings-type'],
|
||||
createComment: ['cases-create-comment-type'],
|
||||
reopenCase: ['cases-reopen-type'],
|
||||
},
|
||||
ui: ['ui-action'],
|
||||
},
|
||||
|
@ -568,6 +584,8 @@ describe('featurePrivilegeIterator', () => {
|
|||
delete: ['cases-delete-sub-type'],
|
||||
push: ['cases-push-sub-type'],
|
||||
settings: ['cases-settings-sub-type'],
|
||||
createComment: ['cases-create-comment-type'],
|
||||
reopenCase: ['cases-reopen-type'],
|
||||
},
|
||||
ui: ['ui-sub-type'],
|
||||
},
|
||||
|
@ -615,6 +633,8 @@ describe('featurePrivilegeIterator', () => {
|
|||
delete: ['cases-delete-type'],
|
||||
push: ['cases-push-type'],
|
||||
settings: ['cases-settings-type'],
|
||||
createComment: ['cases-create-comment-type'],
|
||||
reopenCase: ['cases-reopen-type'],
|
||||
},
|
||||
ui: ['ui-action'],
|
||||
},
|
||||
|
@ -683,6 +703,8 @@ describe('featurePrivilegeIterator', () => {
|
|||
delete: ['cases-delete-type'],
|
||||
push: ['cases-push-type'],
|
||||
settings: ['cases-settings-type'],
|
||||
createComment: ['cases-create-comment-type'],
|
||||
reopenCase: ['cases-reopen-type'],
|
||||
},
|
||||
ui: ['ui-action'],
|
||||
},
|
||||
|
@ -746,6 +768,8 @@ describe('featurePrivilegeIterator', () => {
|
|||
delete: ['cases-delete-sub-type'],
|
||||
push: ['cases-push-sub-type'],
|
||||
settings: ['cases-settings-sub-type'],
|
||||
createComment: ['cases-create-comment-sub-type'],
|
||||
reopenCase: ['cases-reopen-sub-type'],
|
||||
},
|
||||
ui: ['ui-sub-type'],
|
||||
},
|
||||
|
@ -796,6 +820,8 @@ describe('featurePrivilegeIterator', () => {
|
|||
delete: ['cases-delete-type', 'cases-delete-sub-type'],
|
||||
push: ['cases-push-type', 'cases-push-sub-type'],
|
||||
settings: ['cases-settings-type', 'cases-settings-sub-type'],
|
||||
createComment: ['cases-create-comment-type', 'cases-create-comment-sub-type'],
|
||||
reopenCase: ['cases-reopen-type', 'cases-reopen-sub-type'],
|
||||
},
|
||||
ui: ['ui-action', 'ui-sub-type'],
|
||||
},
|
||||
|
@ -832,6 +858,8 @@ describe('featurePrivilegeIterator', () => {
|
|||
delete: ['cases-delete-sub-type'],
|
||||
push: ['cases-push-sub-type'],
|
||||
settings: ['cases-settings-sub-type'],
|
||||
createComment: ['cases-create-comment-sub-type'],
|
||||
reopenCase: ['cases-reopen-sub-type'],
|
||||
},
|
||||
ui: ['ui-action', 'ui-sub-type'],
|
||||
},
|
||||
|
@ -875,6 +903,8 @@ describe('featurePrivilegeIterator', () => {
|
|||
delete: ['cases-delete-type'],
|
||||
push: ['cases-push-type'],
|
||||
settings: ['cases-settings-type'],
|
||||
createComment: ['cases-create-comment-type'],
|
||||
reopenCase: ['cases-reopen-type'],
|
||||
},
|
||||
ui: ['ui-action'],
|
||||
},
|
||||
|
@ -980,6 +1010,8 @@ describe('featurePrivilegeIterator', () => {
|
|||
delete: ['cases-delete-type'],
|
||||
push: ['cases-push-type'],
|
||||
settings: ['cases-settings-type'],
|
||||
createComment: ['cases-create-comment-type'],
|
||||
reopenCase: ['cases-reopen-type'],
|
||||
},
|
||||
ui: ['ui-action'],
|
||||
},
|
||||
|
@ -1015,6 +1047,8 @@ describe('featurePrivilegeIterator', () => {
|
|||
delete: [],
|
||||
push: [],
|
||||
settings: [],
|
||||
createComment: [],
|
||||
reopenCase: [],
|
||||
},
|
||||
ui: ['ui-action'],
|
||||
},
|
||||
|
@ -1056,6 +1090,8 @@ describe('featurePrivilegeIterator', () => {
|
|||
delete: ['cases-delete-type'],
|
||||
push: ['cases-push-type'],
|
||||
settings: ['cases-settings-type'],
|
||||
createComment: ['cases-create-comment-type'],
|
||||
reopenCase: ['cases-reopen-type'],
|
||||
},
|
||||
ui: ['ui-action'],
|
||||
},
|
||||
|
@ -1119,6 +1155,8 @@ describe('featurePrivilegeIterator', () => {
|
|||
delete: ['cases-delete-sub-type'],
|
||||
push: ['cases-push-sub-type'],
|
||||
settings: ['cases-settings-sub-type'],
|
||||
createComment: ['cases-create-comment-sub-type'],
|
||||
reopenCase: ['cases-reopen-sub-type'],
|
||||
},
|
||||
ui: ['ui-sub-type'],
|
||||
},
|
||||
|
@ -1169,6 +1207,8 @@ describe('featurePrivilegeIterator', () => {
|
|||
delete: ['cases-delete-type', 'cases-delete-sub-type'],
|
||||
push: ['cases-push-type', 'cases-push-sub-type'],
|
||||
settings: ['cases-settings-type', 'cases-settings-sub-type'],
|
||||
createComment: ['cases-create-comment-type', 'cases-create-comment-sub-type'],
|
||||
reopenCase: ['cases-reopen-type', 'cases-reopen-sub-type'],
|
||||
},
|
||||
ui: ['ui-action', 'ui-sub-type'],
|
||||
},
|
||||
|
@ -1362,6 +1402,8 @@ describe('featurePrivilegeIterator', () => {
|
|||
delete: ['cases-delete-sub-type'],
|
||||
push: ['cases-push-sub-type'],
|
||||
settings: ['cases-settings-sub-type'],
|
||||
createComment: ['cases-create-comment-sub-type'],
|
||||
reopenCase: ['cases-reopen-sub-type'],
|
||||
},
|
||||
ui: ['ui-sub-type'],
|
||||
},
|
||||
|
@ -1412,6 +1454,8 @@ describe('featurePrivilegeIterator', () => {
|
|||
delete: ['cases-delete-sub-type'],
|
||||
push: ['cases-push-sub-type'],
|
||||
settings: ['cases-settings-sub-type'],
|
||||
createComment: ['cases-create-comment-sub-type'],
|
||||
reopenCase: ['cases-reopen-sub-type'],
|
||||
},
|
||||
ui: ['ui-sub-type'],
|
||||
},
|
||||
|
@ -1448,6 +1492,8 @@ describe('featurePrivilegeIterator', () => {
|
|||
delete: ['cases-delete-sub-type'],
|
||||
push: ['cases-push-sub-type'],
|
||||
settings: ['cases-settings-sub-type'],
|
||||
createComment: ['cases-create-comment-sub-type'],
|
||||
reopenCase: ['cases-reopen-sub-type'],
|
||||
},
|
||||
ui: ['ui-sub-type'],
|
||||
},
|
||||
|
@ -1489,6 +1535,8 @@ describe('featurePrivilegeIterator', () => {
|
|||
delete: ['cases-delete-type'],
|
||||
push: ['cases-push-type'],
|
||||
settings: ['cases-settings-type'],
|
||||
createComment: ['cases-create-comment-type'],
|
||||
reopenCase: ['cases-reopen-type'],
|
||||
},
|
||||
ui: ['ui-action'],
|
||||
},
|
||||
|
@ -1580,6 +1628,8 @@ describe('featurePrivilegeIterator', () => {
|
|||
delete: ['cases-delete-type'],
|
||||
push: ['cases-push-type'],
|
||||
settings: ['cases-settings-type'],
|
||||
createComment: ['cases-create-comment-type'],
|
||||
reopenCase: ['cases-reopen-type'],
|
||||
},
|
||||
ui: ['ui-action'],
|
||||
},
|
||||
|
@ -1615,6 +1665,8 @@ describe('featurePrivilegeIterator', () => {
|
|||
delete: [],
|
||||
push: [],
|
||||
settings: [],
|
||||
createComment: [],
|
||||
reopenCase: [],
|
||||
},
|
||||
ui: ['ui-action'],
|
||||
},
|
||||
|
|
|
@ -151,6 +151,14 @@ function mergeWithSubFeatures(
|
|||
mergedConfig.cases?.settings ?? [],
|
||||
subFeaturePrivilege.cases?.settings ?? []
|
||||
),
|
||||
createComment: mergeArrays(
|
||||
mergedConfig.cases?.createComment ?? [],
|
||||
subFeaturePrivilege.cases?.createComment ?? []
|
||||
),
|
||||
reopenCase: mergeArrays(
|
||||
mergedConfig.cases?.reopenCase ?? [],
|
||||
subFeaturePrivilege.cases?.reopenCase ?? []
|
||||
),
|
||||
};
|
||||
}
|
||||
return mergedConfig;
|
||||
|
|
|
@ -83,6 +83,8 @@ const casesSchemaObject = schema.maybe(
|
|||
delete: schema.maybe(casesSchema),
|
||||
push: schema.maybe(casesSchema),
|
||||
settings: schema.maybe(casesSchema),
|
||||
createComment: schema.maybe(casesSchema),
|
||||
reopenCase: schema.maybe(casesSchema),
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ import {
|
|||
ALERT_STATUS,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import type { FieldFormatsRegistry } from '@kbn/field-formats-plugin/common';
|
||||
import { APP_ID as CASE_APP_ID, FEATURE_ID as CASE_GENERAL_ID } from '@kbn/cases-plugin/common';
|
||||
import { APP_ID as CASE_APP_ID, FEATURE_ID_V2 as CASE_GENERAL_ID } from '@kbn/cases-plugin/common';
|
||||
import { MANAGEMENT_APP_ID } from '@kbn/deeplinks-management/constants';
|
||||
import { getAlertFlyout } from './use_alerts_flyout';
|
||||
import {
|
||||
|
|
|
@ -120,6 +120,8 @@ describe('AddToCaseAction', function () {
|
|||
push: false,
|
||||
connectors: false,
|
||||
settings: false,
|
||||
createComment: false,
|
||||
reopenCase: false,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
|
|
@ -63,7 +63,9 @@ export {
|
|||
getProbabilityFromProgressiveLoadingQuality,
|
||||
} from './progressive_loading';
|
||||
|
||||
/** @deprecated deprecated in 8.17. Please use casesFeatureIdV2 instead */
|
||||
export const casesFeatureId = 'observabilityCases';
|
||||
export const casesFeatureIdV2 = 'observabilityCasesV2';
|
||||
export const sloFeatureId = 'slo';
|
||||
// The ID of the observability app. Should more appropriately be called
|
||||
// 'observability' but it's used in telemetry by applicationUsage so we don't
|
||||
|
|
|
@ -159,7 +159,7 @@ export function AlertActions({
|
|||
);
|
||||
|
||||
const actionsMenuItems = [
|
||||
...(userCasesPermissions.create && userCasesPermissions.read
|
||||
...(userCasesPermissions.createComment && userCasesPermissions.read
|
||||
? [
|
||||
<EuiContextMenuItem
|
||||
data-test-subj="add-to-existing-case-action"
|
||||
|
|
|
@ -28,6 +28,8 @@ const defaultProps: CasesProps = {
|
|||
update: true,
|
||||
connectors: true,
|
||||
settings: true,
|
||||
reopenCase: true,
|
||||
createComment: true,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -45,5 +47,7 @@ CasesPageWithNoPermissions.args = {
|
|||
update: false,
|
||||
connectors: false,
|
||||
settings: false,
|
||||
reopenCase: false,
|
||||
createComment: false,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
* 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 { DEFAULT_APP_CATEGORIES } from '@kbn/core/server';
|
||||
import { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/saved_objects';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { KibanaFeatureConfig, KibanaFeatureScope } from '@kbn/features-plugin/common';
|
||||
import { CasesUiCapabilities, CasesApiTags } from '@kbn/cases-plugin/common';
|
||||
import { casesFeatureId, casesFeatureIdV2, observabilityFeatureId } from '../../common';
|
||||
|
||||
export const getCasesFeature = (
|
||||
casesCapabilities: CasesUiCapabilities,
|
||||
casesApiTags: CasesApiTags
|
||||
): KibanaFeatureConfig => ({
|
||||
deprecated: {
|
||||
// TODO: Add docLinks to link to documentation about the deprecation
|
||||
notice: i18n.translate(
|
||||
'xpack.observability.featureRegistry.linkObservabilityTitle.deprecationMessage',
|
||||
{
|
||||
defaultMessage:
|
||||
'The {currentId} permissions are deprecated, please see {casesFeatureIdV2}.',
|
||||
values: {
|
||||
currentId: casesFeatureId,
|
||||
casesFeatureIdV2,
|
||||
},
|
||||
}
|
||||
),
|
||||
},
|
||||
id: casesFeatureId,
|
||||
name: i18n.translate('xpack.observability.featureRegistry.linkObservabilityTitleDeprecated', {
|
||||
defaultMessage: 'Cases (Deprecated)',
|
||||
}),
|
||||
order: 1100,
|
||||
category: DEFAULT_APP_CATEGORIES.observability,
|
||||
scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security],
|
||||
app: [casesFeatureId, 'kibana'],
|
||||
catalogue: [observabilityFeatureId],
|
||||
cases: [observabilityFeatureId],
|
||||
privileges: {
|
||||
all: {
|
||||
api: [...casesApiTags.all, ...casesApiTags.createComment],
|
||||
app: [casesFeatureId, 'kibana'],
|
||||
catalogue: [observabilityFeatureId],
|
||||
cases: {
|
||||
create: [observabilityFeatureId],
|
||||
read: [observabilityFeatureId],
|
||||
update: [observabilityFeatureId],
|
||||
push: [observabilityFeatureId],
|
||||
createComment: [observabilityFeatureId],
|
||||
reopenCase: [observabilityFeatureId],
|
||||
},
|
||||
savedObject: {
|
||||
all: [...filesSavedObjectTypes],
|
||||
read: [...filesSavedObjectTypes],
|
||||
},
|
||||
ui: casesCapabilities.all,
|
||||
replacedBy: {
|
||||
default: [{ feature: casesFeatureIdV2, privileges: ['all'] }],
|
||||
minimal: [
|
||||
{
|
||||
feature: casesFeatureIdV2,
|
||||
privileges: ['minimal_all', 'create_comment', 'case_reopen'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
read: {
|
||||
api: casesApiTags.read,
|
||||
app: [casesFeatureId, 'kibana'],
|
||||
catalogue: [observabilityFeatureId],
|
||||
cases: {
|
||||
read: [observabilityFeatureId],
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [...filesSavedObjectTypes],
|
||||
},
|
||||
ui: casesCapabilities.read,
|
||||
replacedBy: {
|
||||
default: [{ feature: casesFeatureIdV2, privileges: ['read'] }],
|
||||
minimal: [{ feature: casesFeatureIdV2, privileges: ['minimal_read'] }],
|
||||
},
|
||||
},
|
||||
},
|
||||
subFeatures: [
|
||||
{
|
||||
name: i18n.translate('xpack.observability.featureRegistry.deleteSubFeatureName', {
|
||||
defaultMessage: 'Delete',
|
||||
}),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
api: casesApiTags.delete,
|
||||
id: 'cases_delete',
|
||||
name: i18n.translate('xpack.observability.featureRegistry.deleteSubFeatureDetails', {
|
||||
defaultMessage: 'Delete cases and comments',
|
||||
}),
|
||||
includeIn: 'all',
|
||||
savedObject: {
|
||||
all: [...filesSavedObjectTypes],
|
||||
read: [...filesSavedObjectTypes],
|
||||
},
|
||||
cases: {
|
||||
delete: [observabilityFeatureId],
|
||||
},
|
||||
ui: casesCapabilities.delete,
|
||||
replacedBy: [{ feature: casesFeatureIdV2, privileges: ['cases_delete'] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
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,
|
||||
replacedBy: [{ feature: casesFeatureIdV2, privileges: ['cases_settings'] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
* 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 { DEFAULT_APP_CATEGORIES } from '@kbn/core/server';
|
||||
import { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/saved_objects';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { KibanaFeatureConfig, KibanaFeatureScope } from '@kbn/features-plugin/common';
|
||||
import { CasesUiCapabilities, CasesApiTags } from '@kbn/cases-plugin/common';
|
||||
import { casesFeatureIdV2, casesFeatureId, observabilityFeatureId } from '../../common';
|
||||
|
||||
export const getCasesFeatureV2 = (
|
||||
casesCapabilities: CasesUiCapabilities,
|
||||
casesApiTags: CasesApiTags
|
||||
): KibanaFeatureConfig => ({
|
||||
id: casesFeatureIdV2,
|
||||
name: i18n.translate('xpack.observability.featureRegistry.linkObservabilityTitle', {
|
||||
defaultMessage: 'Cases',
|
||||
}),
|
||||
order: 1100,
|
||||
category: DEFAULT_APP_CATEGORIES.observability,
|
||||
scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security],
|
||||
app: [casesFeatureId, 'kibana'],
|
||||
catalogue: [observabilityFeatureId],
|
||||
cases: [observabilityFeatureId],
|
||||
privileges: {
|
||||
all: {
|
||||
api: casesApiTags.all,
|
||||
app: [casesFeatureId, 'kibana'],
|
||||
catalogue: [observabilityFeatureId],
|
||||
cases: {
|
||||
create: [observabilityFeatureId],
|
||||
read: [observabilityFeatureId],
|
||||
update: [observabilityFeatureId],
|
||||
push: [observabilityFeatureId],
|
||||
},
|
||||
savedObject: {
|
||||
all: [...filesSavedObjectTypes],
|
||||
read: [...filesSavedObjectTypes],
|
||||
},
|
||||
ui: casesCapabilities.all,
|
||||
},
|
||||
read: {
|
||||
api: casesApiTags.read,
|
||||
app: [casesFeatureId, 'kibana'],
|
||||
catalogue: [observabilityFeatureId],
|
||||
cases: {
|
||||
read: [observabilityFeatureId],
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [...filesSavedObjectTypes],
|
||||
},
|
||||
ui: casesCapabilities.read,
|
||||
},
|
||||
},
|
||||
subFeatures: [
|
||||
{
|
||||
name: i18n.translate('xpack.observability.featureRegistry.deleteSubFeatureName', {
|
||||
defaultMessage: 'Delete',
|
||||
}),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
api: casesApiTags.delete,
|
||||
id: 'cases_delete',
|
||||
name: i18n.translate('xpack.observability.featureRegistry.deleteSubFeatureDetails', {
|
||||
defaultMessage: 'Delete cases and comments',
|
||||
}),
|
||||
includeIn: 'all',
|
||||
savedObject: {
|
||||
all: [...filesSavedObjectTypes],
|
||||
read: [...filesSavedObjectTypes],
|
||||
},
|
||||
cases: {
|
||||
delete: [observabilityFeatureId],
|
||||
},
|
||||
ui: casesCapabilities.delete,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.observability.featureRegistry.addCommentsSubFeatureName', {
|
||||
defaultMessage: 'Create comments & attachments',
|
||||
}),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
api: casesApiTags.createComment,
|
||||
id: 'create_comment',
|
||||
name: i18n.translate(
|
||||
'xpack.observability.featureRegistry.addCommentsSubFeatureDetails',
|
||||
{
|
||||
defaultMessage: 'Add comments to cases',
|
||||
}
|
||||
),
|
||||
includeIn: 'all',
|
||||
savedObject: {
|
||||
all: [...filesSavedObjectTypes],
|
||||
read: [...filesSavedObjectTypes],
|
||||
},
|
||||
cases: {
|
||||
createComment: [observabilityFeatureId],
|
||||
},
|
||||
ui: casesCapabilities.createComment,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.observability.featureRegistry.reopenCaseSubFeatureName', {
|
||||
defaultMessage: 'Re-open',
|
||||
}),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
id: 'case_reopen',
|
||||
name: i18n.translate(
|
||||
'xpack.observability.featureRegistry.reopenCaseSubFeatureDetails',
|
||||
{
|
||||
defaultMessage: 'Re-open closed cases',
|
||||
}
|
||||
),
|
||||
includeIn: 'all',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
cases: {
|
||||
reopenCase: [observabilityFeatureId],
|
||||
},
|
||||
ui: casesCapabilities.reopenCase,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
|
@ -21,7 +21,6 @@ import {
|
|||
} from '@kbn/core/server';
|
||||
import { LogsExplorerLocatorParams, LOGS_EXPLORER_LOCATOR_ID } from '@kbn/deeplinks-observability';
|
||||
import { FeaturesPluginSetup } from '@kbn/features-plugin/server';
|
||||
import { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/saved_objects';
|
||||
import type { GuidedOnboardingPluginSetup } from '@kbn/guided-onboarding-plugin/server';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
|
@ -41,7 +40,7 @@ import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
|
|||
import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server';
|
||||
import { KibanaFeatureScope } from '@kbn/features-plugin/common';
|
||||
import { ObservabilityConfig } from '.';
|
||||
import { casesFeatureId, observabilityFeatureId } from '../common';
|
||||
import { observabilityFeatureId } from '../common';
|
||||
import {
|
||||
kubernetesGuideConfig,
|
||||
kubernetesGuideId,
|
||||
|
@ -58,6 +57,8 @@ import { registerRoutes } from './routes/register_routes';
|
|||
import { threshold } from './saved_objects/threshold';
|
||||
import { AlertDetailsContextualInsightsService } from './services';
|
||||
import { uiSettings } from './ui_settings';
|
||||
import { getCasesFeature } from './features/cases_v1';
|
||||
import { getCasesFeatureV2 } from './features/cases_v2';
|
||||
|
||||
export type ObservabilityPluginSetup = ReturnType<ObservabilityPlugin['setup']>;
|
||||
|
||||
|
@ -110,112 +111,8 @@ export class ObservabilityPlugin implements Plugin<ObservabilityPluginSetup> {
|
|||
|
||||
const alertDetailsContextualInsightsService = new AlertDetailsContextualInsightsService();
|
||||
|
||||
plugins.features.registerKibanaFeature({
|
||||
id: casesFeatureId,
|
||||
name: i18n.translate('xpack.observability.featureRegistry.linkObservabilityTitle', {
|
||||
defaultMessage: 'Cases',
|
||||
}),
|
||||
order: 1100,
|
||||
category: DEFAULT_APP_CATEGORIES.observability,
|
||||
scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security],
|
||||
app: [casesFeatureId, 'kibana'],
|
||||
catalogue: [observabilityFeatureId],
|
||||
cases: [observabilityFeatureId],
|
||||
privileges: {
|
||||
all: {
|
||||
api: casesApiTags.all,
|
||||
app: [casesFeatureId, 'kibana'],
|
||||
catalogue: [observabilityFeatureId],
|
||||
cases: {
|
||||
create: [observabilityFeatureId],
|
||||
read: [observabilityFeatureId],
|
||||
update: [observabilityFeatureId],
|
||||
push: [observabilityFeatureId],
|
||||
},
|
||||
savedObject: {
|
||||
all: [...filesSavedObjectTypes],
|
||||
read: [...filesSavedObjectTypes],
|
||||
},
|
||||
ui: casesCapabilities.all,
|
||||
},
|
||||
read: {
|
||||
api: casesApiTags.read,
|
||||
app: [casesFeatureId, 'kibana'],
|
||||
catalogue: [observabilityFeatureId],
|
||||
cases: {
|
||||
read: [observabilityFeatureId],
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [...filesSavedObjectTypes],
|
||||
},
|
||||
ui: casesCapabilities.read,
|
||||
},
|
||||
},
|
||||
subFeatures: [
|
||||
{
|
||||
name: i18n.translate('xpack.observability.featureRegistry.deleteSubFeatureName', {
|
||||
defaultMessage: 'Delete',
|
||||
}),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
api: casesApiTags.delete,
|
||||
id: 'cases_delete',
|
||||
name: i18n.translate(
|
||||
'xpack.observability.featureRegistry.deleteSubFeatureDetails',
|
||||
{
|
||||
defaultMessage: 'Delete cases and comments',
|
||||
}
|
||||
),
|
||||
includeIn: 'all',
|
||||
savedObject: {
|
||||
all: [...filesSavedObjectTypes],
|
||||
read: [...filesSavedObjectTypes],
|
||||
},
|
||||
cases: {
|
||||
delete: [observabilityFeatureId],
|
||||
},
|
||||
ui: casesCapabilities.delete,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
plugins.features.registerKibanaFeature(getCasesFeature(casesCapabilities, casesApiTags));
|
||||
plugins.features.registerKibanaFeature(getCasesFeatureV2(casesCapabilities, casesApiTags));
|
||||
|
||||
let annotationsApiPromise: Promise<AnnotationsAPI> | undefined;
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import { AlertConsumers } from '@kbn/rule-data-utils';
|
|||
|
||||
export const observabilityFeatureId = 'observability';
|
||||
export const observabilityAppId = 'observability-overview';
|
||||
export const casesFeatureId = 'observabilityCases';
|
||||
export const casesFeatureId = 'observabilityCasesV2';
|
||||
export const sloFeatureId = 'slo';
|
||||
|
||||
// SLO alerts table in slo detail page
|
||||
|
|
|
@ -14,6 +14,8 @@ export const noCasesPermissions = () => ({
|
|||
push: false,
|
||||
connectors: false,
|
||||
settings: false,
|
||||
createComment: false,
|
||||
reopenCase: false,
|
||||
});
|
||||
|
||||
export const allCasesPermissions = () => ({
|
||||
|
@ -25,4 +27,6 @@ export const allCasesPermissions = () => ({
|
|||
push: true,
|
||||
connectors: true,
|
||||
settings: true,
|
||||
createComment: true,
|
||||
reopenCase: true,
|
||||
});
|
||||
|
|
|
@ -94,7 +94,7 @@ const roles = [
|
|||
applications: [
|
||||
{
|
||||
application: 'kibana-.kibana',
|
||||
privileges: ['feature_securitySolutionCases.a;;'],
|
||||
privileges: ['feature_securitySolutionCasesV2.a;;'],
|
||||
resources: ['*'],
|
||||
},
|
||||
],
|
||||
|
@ -184,7 +184,7 @@ const roles = [
|
|||
applications: [
|
||||
{
|
||||
application: 'kibana-.kibana',
|
||||
privileges: ['feature_securitySolutionCases.a;;'],
|
||||
privileges: ['feature_securitySolutionCasesV2.a;;'],
|
||||
resources: ['space:default'],
|
||||
},
|
||||
],
|
||||
|
|
|
@ -21,7 +21,7 @@ export const APP_ID = 'securitySolution' as const;
|
|||
export const APP_UI_ID = 'securitySolutionUI' as const;
|
||||
export const ASSISTANT_FEATURE_ID = 'securitySolutionAssistant' as const;
|
||||
export const ATTACK_DISCOVERY_FEATURE_ID = 'securitySolutionAttackDiscovery' as const;
|
||||
export const CASES_FEATURE_ID = 'securitySolutionCases' as const;
|
||||
export const CASES_FEATURE_ID = 'securitySolutionCasesV2' as const;
|
||||
export const SERVER_APP_ID = 'siem' as const;
|
||||
export const APP_NAME = 'Security' as const;
|
||||
export const APP_ICON = 'securityAnalyticsApp' as const;
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
"siem": ["read", "read_alerts"],
|
||||
"securitySolutionAssistant": ["none"],
|
||||
"securitySolutionAttackDiscovery": ["none"],
|
||||
"securitySolutionCases": ["read"],
|
||||
"securitySolutionCasesV2": ["read"],
|
||||
"actions": ["read"],
|
||||
"builtInAlerts": ["read"]
|
||||
},
|
||||
|
@ -79,7 +79,7 @@
|
|||
"siem": ["all", "read_alerts", "crud_alerts"],
|
||||
"securitySolutionAssistant": ["all"],
|
||||
"securitySolutionAttackDiscovery": ["all"],
|
||||
"securitySolutionCases": ["all"],
|
||||
"securitySolutionCasesV2": ["all"],
|
||||
"actions": ["read"],
|
||||
"builtInAlerts": ["all"]
|
||||
},
|
||||
|
@ -128,7 +128,7 @@
|
|||
"siem": ["all", "read_alerts", "crud_alerts"],
|
||||
"securitySolutionAssistant": ["all"],
|
||||
"securitySolutionAttackDiscovery": ["all"],
|
||||
"securitySolutionCases": ["all"],
|
||||
"securitySolutionCasesV2": ["all"],
|
||||
"builtInAlerts": ["all"]
|
||||
},
|
||||
"spaces": ["*"],
|
||||
|
|
|
@ -33,8 +33,8 @@ const TakeActionComponent: React.FC<Props> = ({ attackDiscovery, replacements })
|
|||
const { cases } = useKibana().services;
|
||||
const userCasesPermissions = cases.helpers.canUseCases([APP_ID]);
|
||||
const canUserCreateAndReadCases = useCallback(
|
||||
() => userCasesPermissions.create && userCasesPermissions.read,
|
||||
[userCasesPermissions.create, userCasesPermissions.read]
|
||||
() => userCasesPermissions.createComment && userCasesPermissions.read,
|
||||
[userCasesPermissions.createComment, userCasesPermissions.read]
|
||||
);
|
||||
const { disabled: addToCaseDisabled, onAddToNewCase } = useAddToNewCase({
|
||||
canUserCreateAndReadCases,
|
||||
|
|
|
@ -15,6 +15,8 @@ export const noCasesCapabilities = (): CasesCapabilities => ({
|
|||
push_cases: false,
|
||||
cases_connectors: false,
|
||||
cases_settings: false,
|
||||
case_reopen: false,
|
||||
create_comment: false,
|
||||
});
|
||||
|
||||
export const readCasesCapabilities = (): CasesCapabilities => ({
|
||||
|
@ -25,6 +27,8 @@ export const readCasesCapabilities = (): CasesCapabilities => ({
|
|||
push_cases: false,
|
||||
cases_connectors: true,
|
||||
cases_settings: false,
|
||||
case_reopen: false,
|
||||
create_comment: false,
|
||||
});
|
||||
|
||||
export const allCasesCapabilities = (): CasesCapabilities => ({
|
||||
|
@ -35,6 +39,8 @@ export const allCasesCapabilities = (): CasesCapabilities => ({
|
|||
push_cases: true,
|
||||
cases_connectors: true,
|
||||
cases_settings: true,
|
||||
case_reopen: true,
|
||||
create_comment: true,
|
||||
});
|
||||
|
||||
export const noCasesPermissions = (): CasesPermissions => ({
|
||||
|
@ -46,6 +52,8 @@ export const noCasesPermissions = (): CasesPermissions => ({
|
|||
push: false,
|
||||
connectors: false,
|
||||
settings: false,
|
||||
reopenCase: false,
|
||||
createComment: false,
|
||||
});
|
||||
|
||||
export const readCasesPermissions = (): CasesPermissions => ({
|
||||
|
@ -57,6 +65,8 @@ export const readCasesPermissions = (): CasesPermissions => ({
|
|||
push: false,
|
||||
connectors: true,
|
||||
settings: false,
|
||||
reopenCase: false,
|
||||
createComment: false,
|
||||
});
|
||||
|
||||
export const writeCasesPermissions = (): CasesPermissions => ({
|
||||
|
@ -68,6 +78,8 @@ export const writeCasesPermissions = (): CasesPermissions => ({
|
|||
push: true,
|
||||
connectors: true,
|
||||
settings: true,
|
||||
reopenCase: true,
|
||||
createComment: true,
|
||||
});
|
||||
|
||||
export const allCasesPermissions = (): CasesPermissions => ({
|
||||
|
@ -79,4 +91,6 @@ export const allCasesPermissions = (): CasesPermissions => ({
|
|||
push: true,
|
||||
connectors: true,
|
||||
settings: true,
|
||||
reopenCase: true,
|
||||
createComment: true,
|
||||
});
|
||||
|
|
|
@ -59,7 +59,7 @@ export const useAddToExistingCase = ({
|
|||
disabled:
|
||||
lensAttributes == null ||
|
||||
timeRange == null ||
|
||||
!userCasesPermissions.create ||
|
||||
!userCasesPermissions.createComment ||
|
||||
!userCasesPermissions.read,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -60,7 +60,7 @@ export const useAddToNewCase = ({
|
|||
disabled:
|
||||
lensAttributes == null ||
|
||||
timeRange == null ||
|
||||
!userCasesPermissions.create ||
|
||||
!userCasesPermissions.createComment ||
|
||||
!userCasesPermissions.read,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -432,9 +432,9 @@ describe('Security links', () => {
|
|||
|
||||
describe('hasCapabilities', () => {
|
||||
const siemShow = 'siem.show';
|
||||
const createCases = 'securitySolutionCases.create_cases';
|
||||
const readCases = 'securitySolutionCases.read_cases';
|
||||
const pushCases = 'securitySolutionCases.push_cases';
|
||||
const createCases = 'securitySolutionCasesV2.create_cases';
|
||||
const readCases = 'securitySolutionCasesV2.read_cases';
|
||||
const pushCases = 'securitySolutionCasesV2.push_cases';
|
||||
|
||||
it('returns false when capabilities is an empty array', () => {
|
||||
expect(hasCapabilities(createCapabilities(), [])).toBeFalsy();
|
||||
|
@ -461,7 +461,7 @@ describe('Security links', () => {
|
|||
hasCapabilities(
|
||||
createCapabilities({
|
||||
siem: { show: true },
|
||||
securitySolutionCases: { create_cases: false },
|
||||
securitySolutionCasesV2: { create_cases: false },
|
||||
}),
|
||||
[siemShow, createCases]
|
||||
)
|
||||
|
@ -473,7 +473,7 @@ describe('Security links', () => {
|
|||
hasCapabilities(
|
||||
createCapabilities({
|
||||
siem: { show: false },
|
||||
securitySolutionCases: { create_cases: true },
|
||||
securitySolutionCasesV2: { create_cases: true },
|
||||
}),
|
||||
[siemShow, createCases]
|
||||
)
|
||||
|
@ -485,7 +485,7 @@ describe('Security links', () => {
|
|||
hasCapabilities(
|
||||
createCapabilities({
|
||||
siem: { show: true },
|
||||
securitySolutionCases: { create_cases: false },
|
||||
securitySolutionCasesV2: { create_cases: false },
|
||||
}),
|
||||
[readCases, createCases]
|
||||
)
|
||||
|
@ -497,7 +497,7 @@ describe('Security links', () => {
|
|||
hasCapabilities(
|
||||
createCapabilities({
|
||||
siem: { show: true },
|
||||
securitySolutionCases: { read_cases: true, create_cases: true },
|
||||
securitySolutionCasesV2: { read_cases: true, create_cases: true },
|
||||
}),
|
||||
[[readCases, createCases]]
|
||||
)
|
||||
|
@ -509,7 +509,7 @@ describe('Security links', () => {
|
|||
hasCapabilities(
|
||||
createCapabilities({
|
||||
siem: { show: false },
|
||||
securitySolutionCases: { read_cases: false, create_cases: true },
|
||||
securitySolutionCasesV2: { read_cases: false, create_cases: true },
|
||||
}),
|
||||
[siemShow, [readCases, createCases]]
|
||||
)
|
||||
|
@ -521,7 +521,7 @@ describe('Security links', () => {
|
|||
hasCapabilities(
|
||||
createCapabilities({
|
||||
siem: { show: true },
|
||||
securitySolutionCases: { read_cases: false, create_cases: true },
|
||||
securitySolutionCasesV2: { read_cases: false, create_cases: true },
|
||||
}),
|
||||
[siemShow, [readCases, createCases]]
|
||||
)
|
||||
|
@ -533,7 +533,7 @@ describe('Security links', () => {
|
|||
hasCapabilities(
|
||||
createCapabilities({
|
||||
siem: { show: true },
|
||||
securitySolutionCases: { read_cases: false, create_cases: true, push_cases: false },
|
||||
securitySolutionCasesV2: { read_cases: false, create_cases: true, push_cases: false },
|
||||
}),
|
||||
[
|
||||
[siemShow, pushCases],
|
||||
|
|
|
@ -88,6 +88,8 @@ jest.mock('../../../../common/lib/kibana', () => {
|
|||
update: true,
|
||||
delete: true,
|
||||
push: true,
|
||||
createComment: true,
|
||||
reopenCase: true,
|
||||
}),
|
||||
getRuleIdFromEvent: jest.fn(),
|
||||
},
|
||||
|
|
|
@ -142,7 +142,7 @@ export const useAddToCaseActions = ({
|
|||
const addToCaseActionItems: AlertTableContextMenuItem[] = useMemo(() => {
|
||||
if (
|
||||
(isActiveTimelines || isInDetections) &&
|
||||
userCasesPermissions.create &&
|
||||
userCasesPermissions.createComment &&
|
||||
userCasesPermissions.read &&
|
||||
isAlert
|
||||
) {
|
||||
|
@ -169,14 +169,14 @@ export const useAddToCaseActions = ({
|
|||
}
|
||||
return [];
|
||||
}, [
|
||||
isActiveTimelines,
|
||||
isInDetections,
|
||||
userCasesPermissions.createComment,
|
||||
userCasesPermissions.read,
|
||||
isAlert,
|
||||
ariaLabel,
|
||||
handleAddToExistingCaseClick,
|
||||
handleAddToNewCaseClick,
|
||||
userCasesPermissions.create,
|
||||
userCasesPermissions.read,
|
||||
isInDetections,
|
||||
isActiveTimelines,
|
||||
isAlert,
|
||||
]);
|
||||
|
||||
return {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue