mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
# Backport This will backport the following commits from `main` to `8.x`: - [[Cases] Cases assignees sub feature (#201654)](https://github.com/elastic/kibana/pull/201654) - [[Cases] Fix an issue with the reopen case permission, add integration tests for failing case (#201517)](https://github.com/elastic/kibana/pull/201517) <!--- Backport version: 9.6.4 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Kevin Qualters","email":"56408403+kqualters-elastic@users.noreply.github.com"},"sourceCommit":{"committedDate":"2025-01-30T16:04:38Z","message":"[Cases] Cases assignees sub feature (#201654)\n\n## Summary\r\n\r\nThis pr implements a new cases assignee sub-feature, allowing users to\r\ncontrol a role's ability to change the assignee of a case. With the\r\npermission enabled, they can assign any user to any case, with it\r\ndisabled, the assignees component is hidden.\r\n\r\nRead only + enabled:\r\n\r\n\r\n\r\n\r\nAll + assign disabled:\r\n\r\n\r\n\r\n\r\n\r\n### Checklist\r\n\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: Elastic Machine <elasticmachine@users.noreply.github.com>","sha":"0e7c608ed3d62852b72eaf45e65e347a03bd08d6","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Team:ResponseOps","backport missing","release_note:feature","Team:Threat Hunting:Investigations","backport:prev-minor","ci:project-deploy-observability","Team:obs-ux-management","v9.1.0","v8.19.0"],"title":"[Cases] Cases assignees sub feature","number":201654,"url":"https://github.com/elastic/kibana/pull/201654","mergeCommit":{"message":"[Cases] Cases assignees sub feature (#201654)\n\n## Summary\r\n\r\nThis pr implements a new cases assignee sub-feature, allowing users to\r\ncontrol a role's ability to change the assignee of a case. With the\r\npermission enabled, they can assign any user to any case, with it\r\ndisabled, the assignees component is hidden.\r\n\r\nRead only + enabled:\r\n\r\n\r\n\r\n\r\nAll + assign disabled:\r\n\r\n\r\n\r\n\r\n\r\n### Checklist\r\n\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: Elastic Machine <elasticmachine@users.noreply.github.com>","sha":"0e7c608ed3d62852b72eaf45e65e347a03bd08d6"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"9.1","label":"v9.1.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.19","label":"v8.19.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"url":"https://github.com/elastic/kibana/pull/209435","number":209435,"branch":"8.18","state":"OPEN"}]}] BACKPORT--> --------- Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
0f56a7cdcd
commit
bd53593617
105 changed files with 2425 additions and 218 deletions
|
@ -18,6 +18,7 @@ Array [
|
|||
"cases:observability/updateConfiguration",
|
||||
"cases:observability/createComment",
|
||||
"cases:observability/reopenCase",
|
||||
"cases:observability/assignCase",
|
||||
]
|
||||
`;
|
||||
|
||||
|
|
|
@ -130,6 +130,7 @@ describe(`cases`, () => {
|
|||
"cases:security/updateConfiguration",
|
||||
"cases:security/createComment",
|
||||
"cases:security/reopenCase",
|
||||
"cases:security/assignCase",
|
||||
"cases:obs/getCase",
|
||||
"cases:obs/getComment",
|
||||
"cases:obs/getTags",
|
||||
|
@ -187,6 +188,7 @@ describe(`cases`, () => {
|
|||
"cases:security/updateConfiguration",
|
||||
"cases:security/createComment",
|
||||
"cases:security/reopenCase",
|
||||
"cases:security/assignCase",
|
||||
"cases:other-security/pushCase",
|
||||
"cases:other-security/createCase",
|
||||
"cases:other-security/getCase",
|
||||
|
@ -203,6 +205,7 @@ describe(`cases`, () => {
|
|||
"cases:other-security/updateConfiguration",
|
||||
"cases:other-security/createComment",
|
||||
"cases:other-security/reopenCase",
|
||||
"cases:other-security/assignCase",
|
||||
"cases:obs/getCase",
|
||||
"cases:obs/getComment",
|
||||
"cases:obs/getTags",
|
||||
|
|
|
@ -37,6 +37,7 @@ const deleteOperations = ['deleteCase', 'deleteComment'] as const;
|
|||
const settingsOperations = ['createConfiguration', 'updateConfiguration'] as const;
|
||||
const createCommentOperations = ['createComment'] as const;
|
||||
const reopenOperations = ['reopenCase'] as const;
|
||||
const assignOperations = ['assignCase'] as const;
|
||||
const allOperations = [
|
||||
...pushOperations,
|
||||
...createOperations,
|
||||
|
@ -46,6 +47,7 @@ const allOperations = [
|
|||
...settingsOperations,
|
||||
...createCommentOperations,
|
||||
...reopenOperations,
|
||||
...assignOperations,
|
||||
] as const;
|
||||
|
||||
export class FeaturePrivilegeCasesBuilder extends BaseFeaturePrivilegeBuilder {
|
||||
|
@ -71,6 +73,7 @@ export class FeaturePrivilegeCasesBuilder extends BaseFeaturePrivilegeBuilder {
|
|||
...getCasesPrivilege(settingsOperations, privilegeDefinition.cases?.settings),
|
||||
...getCasesPrivilege(createCommentOperations, privilegeDefinition.cases?.createComment),
|
||||
...getCasesPrivilege(reopenOperations, privilegeDefinition.cases?.reopenCase),
|
||||
...getCasesPrivilege(assignOperations, privilegeDefinition.cases?.assign),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,9 +12,11 @@ import { CASE_VIEW_PAGE_TABS } from '../types';
|
|||
*/
|
||||
|
||||
export const APP_ID = 'cases' as const;
|
||||
/** @deprecated Please use FEATURE_ID_V2 instead */
|
||||
/** @deprecated Please use FEATURE_ID_V3 instead */
|
||||
export const FEATURE_ID = 'generalCases' as const;
|
||||
/** @deprecated Please use FEATURE_ID_V3 instead */
|
||||
export const FEATURE_ID_V2 = 'generalCasesV2' as const;
|
||||
export const FEATURE_ID_V3 = 'generalCasesV3' 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;
|
||||
|
|
|
@ -186,6 +186,7 @@ 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;
|
||||
export const ASSIGN_CASE_CAPABILITY = 'cases_assign' as const;
|
||||
|
||||
/**
|
||||
* Cases API Tags
|
||||
|
|
|
@ -58,6 +58,7 @@ export {
|
|||
CASES_SETTINGS_CAPABILITY,
|
||||
CREATE_COMMENT_CAPABILITY,
|
||||
CASES_REOPEN_CAPABILITY,
|
||||
ASSIGN_CASE_CAPABILITY,
|
||||
} from './constants';
|
||||
|
||||
export type { AttachmentAttributes } from './types/domain';
|
||||
|
|
|
@ -13,6 +13,7 @@ import type {
|
|||
UPDATE_CASES_CAPABILITY,
|
||||
CREATE_COMMENT_CAPABILITY,
|
||||
CASES_REOPEN_CAPABILITY,
|
||||
ASSIGN_CASE_CAPABILITY,
|
||||
} from '..';
|
||||
import type {
|
||||
CASES_CONNECTORS_CAPABILITY,
|
||||
|
@ -325,6 +326,7 @@ export interface CasesPermissions {
|
|||
settings: boolean;
|
||||
reopenCase: boolean;
|
||||
createComment: boolean;
|
||||
assign: boolean;
|
||||
}
|
||||
|
||||
export interface CasesCapabilities {
|
||||
|
@ -337,4 +339,5 @@ export interface CasesCapabilities {
|
|||
[CASES_SETTINGS_CAPABILITY]: boolean;
|
||||
[CREATE_COMMENT_CAPABILITY]: boolean;
|
||||
[CASES_REOPEN_CAPABILITY]: boolean;
|
||||
[ASSIGN_CASE_CAPABILITY]: boolean;
|
||||
}
|
||||
|
|
|
@ -18,6 +18,9 @@ describe('createUICapabilities', () => {
|
|||
"push_cases",
|
||||
"cases_connectors",
|
||||
],
|
||||
"assignCase": Array [
|
||||
"cases_assign",
|
||||
],
|
||||
"createComment": Array [
|
||||
"create_comment",
|
||||
],
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
CASES_SETTINGS_CAPABILITY,
|
||||
CASES_REOPEN_CAPABILITY,
|
||||
CREATE_COMMENT_CAPABILITY,
|
||||
ASSIGN_CASE_CAPABILITY,
|
||||
} from '../constants';
|
||||
|
||||
export interface CasesUiCapabilities {
|
||||
|
@ -24,6 +25,7 @@ export interface CasesUiCapabilities {
|
|||
settings: readonly string[];
|
||||
reopenCase: readonly string[];
|
||||
createComment: readonly string[];
|
||||
assignCase: readonly string[];
|
||||
}
|
||||
/**
|
||||
* Return the UI capabilities for each type of operation. These strings must match the values defined in the UI
|
||||
|
@ -42,4 +44,5 @@ export const createUICapabilities = (): CasesUiCapabilities => ({
|
|||
settings: [CASES_SETTINGS_CAPABILITY] as const,
|
||||
reopenCase: [CASES_REOPEN_CAPABILITY] as const,
|
||||
createComment: [CREATE_COMMENT_CAPABILITY] as const,
|
||||
assignCase: [ASSIGN_CASE_CAPABILITY] as const,
|
||||
});
|
||||
|
|
|
@ -20,67 +20,67 @@ import { canUseCases } from './can_use_cases';
|
|||
|
||||
type CasesCapabilities = Pick<
|
||||
ApplicationStart['capabilities'],
|
||||
'securitySolutionCasesV2' | 'observabilityCasesV2' | 'generalCasesV2'
|
||||
'securitySolutionCasesV3' | 'observabilityCasesV3' | 'generalCasesV3'
|
||||
>;
|
||||
|
||||
const hasAll: CasesCapabilities = {
|
||||
securitySolutionCasesV2: allCasesCapabilities(),
|
||||
observabilityCasesV2: allCasesCapabilities(),
|
||||
generalCasesV2: allCasesCapabilities(),
|
||||
securitySolutionCasesV3: allCasesCapabilities(),
|
||||
observabilityCasesV3: allCasesCapabilities(),
|
||||
generalCasesV3: allCasesCapabilities(),
|
||||
};
|
||||
|
||||
const hasNone: CasesCapabilities = {
|
||||
securitySolutionCasesV2: noCasesCapabilities(),
|
||||
observabilityCasesV2: noCasesCapabilities(),
|
||||
generalCasesV2: noCasesCapabilities(),
|
||||
securitySolutionCasesV3: noCasesCapabilities(),
|
||||
observabilityCasesV3: noCasesCapabilities(),
|
||||
generalCasesV3: noCasesCapabilities(),
|
||||
};
|
||||
|
||||
const hasSecurity: CasesCapabilities = {
|
||||
securitySolutionCasesV2: allCasesCapabilities(),
|
||||
observabilityCasesV2: noCasesCapabilities(),
|
||||
generalCasesV2: noCasesCapabilities(),
|
||||
securitySolutionCasesV3: allCasesCapabilities(),
|
||||
observabilityCasesV3: noCasesCapabilities(),
|
||||
generalCasesV3: noCasesCapabilities(),
|
||||
};
|
||||
|
||||
const hasObservability: CasesCapabilities = {
|
||||
securitySolutionCasesV2: noCasesCapabilities(),
|
||||
observabilityCasesV2: allCasesCapabilities(),
|
||||
generalCasesV2: noCasesCapabilities(),
|
||||
securitySolutionCasesV3: noCasesCapabilities(),
|
||||
observabilityCasesV3: allCasesCapabilities(),
|
||||
generalCasesV3: noCasesCapabilities(),
|
||||
};
|
||||
|
||||
const hasObservabilityWriteTrue: CasesCapabilities = {
|
||||
securitySolutionCasesV2: noCasesCapabilities(),
|
||||
observabilityCasesV2: writeCasesCapabilities(),
|
||||
generalCasesV2: noCasesCapabilities(),
|
||||
securitySolutionCasesV3: noCasesCapabilities(),
|
||||
observabilityCasesV3: writeCasesCapabilities(),
|
||||
generalCasesV3: noCasesCapabilities(),
|
||||
};
|
||||
|
||||
const hasSecurityWriteTrue: CasesCapabilities = {
|
||||
securitySolutionCasesV2: writeCasesCapabilities(),
|
||||
observabilityCasesV2: noCasesCapabilities(),
|
||||
generalCasesV2: noCasesCapabilities(),
|
||||
securitySolutionCasesV3: writeCasesCapabilities(),
|
||||
observabilityCasesV3: noCasesCapabilities(),
|
||||
generalCasesV3: noCasesCapabilities(),
|
||||
};
|
||||
|
||||
const hasObservabilityReadTrue: CasesCapabilities = {
|
||||
securitySolutionCasesV2: noCasesCapabilities(),
|
||||
observabilityCasesV2: readCasesCapabilities(),
|
||||
generalCasesV2: noCasesCapabilities(),
|
||||
securitySolutionCasesV3: noCasesCapabilities(),
|
||||
observabilityCasesV3: readCasesCapabilities(),
|
||||
generalCasesV3: noCasesCapabilities(),
|
||||
};
|
||||
|
||||
const hasSecurityReadTrue: CasesCapabilities = {
|
||||
securitySolutionCasesV2: readCasesCapabilities(),
|
||||
observabilityCasesV2: noCasesCapabilities(),
|
||||
generalCasesV2: noCasesCapabilities(),
|
||||
securitySolutionCasesV3: readCasesCapabilities(),
|
||||
observabilityCasesV3: noCasesCapabilities(),
|
||||
generalCasesV3: noCasesCapabilities(),
|
||||
};
|
||||
|
||||
const hasSecurityWriteAndObservabilityRead: CasesCapabilities = {
|
||||
securitySolutionCasesV2: writeCasesCapabilities(),
|
||||
observabilityCasesV2: readCasesCapabilities(),
|
||||
generalCasesV2: noCasesCapabilities(),
|
||||
securitySolutionCasesV3: writeCasesCapabilities(),
|
||||
observabilityCasesV3: readCasesCapabilities(),
|
||||
generalCasesV3: noCasesCapabilities(),
|
||||
};
|
||||
|
||||
const hasSecurityConnectors: CasesCapabilities = {
|
||||
securitySolutionCasesV2: readCasesCapabilities(),
|
||||
observabilityCasesV2: noCasesCapabilities(),
|
||||
generalCasesV2: noCasesCapabilities(),
|
||||
securitySolutionCasesV3: readCasesCapabilities(),
|
||||
observabilityCasesV3: noCasesCapabilities(),
|
||||
generalCasesV3: noCasesCapabilities(),
|
||||
};
|
||||
|
||||
describe('canUseCases', () => {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import type { ApplicationStart } from '@kbn/core/public';
|
||||
import {
|
||||
FEATURE_ID_V2,
|
||||
FEATURE_ID_V3,
|
||||
GENERAL_CASES_OWNER,
|
||||
OBSERVABILITY_OWNER,
|
||||
SECURITY_SOLUTION_OWNER,
|
||||
|
@ -32,9 +32,9 @@ export const canUseCases =
|
|||
owners: CasesOwners[] = [OBSERVABILITY_OWNER, SECURITY_SOLUTION_OWNER, GENERAL_CASES_OWNER]
|
||||
): CasesPermissions => {
|
||||
const aggregatedPermissions = owners.reduce<CasesPermissions>(
|
||||
// eslint-disable-next-line complexity
|
||||
(acc, owner) => {
|
||||
const userCapabilitiesForOwner = getUICapabilities(capabilities[getFeatureID(owner)]);
|
||||
|
||||
acc.create = acc.create || userCapabilitiesForOwner.create;
|
||||
acc.read = acc.read || userCapabilitiesForOwner.read;
|
||||
acc.update = acc.update || userCapabilitiesForOwner.update;
|
||||
|
@ -44,6 +44,7 @@ export const canUseCases =
|
|||
acc.settings = acc.settings || userCapabilitiesForOwner.settings;
|
||||
acc.reopenCase = acc.reopenCase || userCapabilitiesForOwner.reopenCase;
|
||||
acc.createComment = acc.createComment || userCapabilitiesForOwner.createComment;
|
||||
acc.assign = acc.assign || userCapabilitiesForOwner.assign;
|
||||
|
||||
const allFromAcc =
|
||||
acc.create &&
|
||||
|
@ -54,7 +55,8 @@ export const canUseCases =
|
|||
acc.connectors &&
|
||||
acc.settings &&
|
||||
acc.reopenCase &&
|
||||
acc.createComment;
|
||||
acc.createComment &&
|
||||
acc.assign;
|
||||
|
||||
acc.all = acc.all || userCapabilitiesForOwner.all || allFromAcc;
|
||||
|
||||
|
@ -71,6 +73,7 @@ export const canUseCases =
|
|||
settings: false,
|
||||
reopenCase: false,
|
||||
createComment: false,
|
||||
assign: false,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -81,8 +84,8 @@ export const canUseCases =
|
|||
|
||||
const getFeatureID = (owner: CasesOwners) => {
|
||||
if (owner === GENERAL_CASES_OWNER) {
|
||||
return FEATURE_ID_V2;
|
||||
return FEATURE_ID_V3;
|
||||
}
|
||||
|
||||
return `${owner}CasesV2`;
|
||||
return `${owner}CasesV3`;
|
||||
};
|
||||
|
|
|
@ -12,6 +12,7 @@ describe('getUICapabilities', () => {
|
|||
expect(getUICapabilities(undefined)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"all": false,
|
||||
"assign": false,
|
||||
"connectors": false,
|
||||
"create": false,
|
||||
"createComment": false,
|
||||
|
@ -29,6 +30,7 @@ describe('getUICapabilities', () => {
|
|||
expect(getUICapabilities()).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"all": false,
|
||||
"assign": false,
|
||||
"connectors": false,
|
||||
"create": false,
|
||||
"createComment": false,
|
||||
|
@ -46,6 +48,7 @@ describe('getUICapabilities', () => {
|
|||
expect(getUICapabilities({ create_cases: true })).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"all": false,
|
||||
"assign": false,
|
||||
"connectors": false,
|
||||
"create": true,
|
||||
"createComment": false,
|
||||
|
@ -72,6 +75,7 @@ describe('getUICapabilities', () => {
|
|||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"all": false,
|
||||
"assign": false,
|
||||
"connectors": false,
|
||||
"create": false,
|
||||
"createComment": false,
|
||||
|
@ -89,6 +93,7 @@ describe('getUICapabilities', () => {
|
|||
expect(getUICapabilities({})).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"all": false,
|
||||
"assign": false,
|
||||
"connectors": false,
|
||||
"create": false,
|
||||
"createComment": false,
|
||||
|
@ -115,6 +120,7 @@ describe('getUICapabilities', () => {
|
|||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"all": false,
|
||||
"assign": false,
|
||||
"connectors": true,
|
||||
"create": false,
|
||||
"createComment": false,
|
||||
|
@ -142,6 +148,7 @@ describe('getUICapabilities', () => {
|
|||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"all": false,
|
||||
"assign": false,
|
||||
"connectors": false,
|
||||
"create": true,
|
||||
"createComment": false,
|
||||
|
@ -169,6 +176,7 @@ describe('getUICapabilities', () => {
|
|||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"all": false,
|
||||
"assign": false,
|
||||
"connectors": true,
|
||||
"create": true,
|
||||
"createComment": false,
|
||||
|
@ -186,6 +194,7 @@ describe('getUICapabilities', () => {
|
|||
expect(getUICapabilities({ cases_settings: true })).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"all": false,
|
||||
"assign": false,
|
||||
"connectors": false,
|
||||
"create": false,
|
||||
"createComment": false,
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
UPDATE_CASES_CAPABILITY,
|
||||
CASES_REOPEN_CAPABILITY,
|
||||
CREATE_COMMENT_CAPABILITY,
|
||||
ASSIGN_CASE_CAPABILITY,
|
||||
} from '../../../common/constants';
|
||||
|
||||
export const getUICapabilities = (
|
||||
|
@ -30,6 +31,7 @@ export const getUICapabilities = (
|
|||
const settings = !!featureCapabilities?.[CASES_SETTINGS_CAPABILITY];
|
||||
const reopenCase = !!featureCapabilities?.[CASES_REOPEN_CAPABILITY];
|
||||
const createComment = !!featureCapabilities?.[CREATE_COMMENT_CAPABILITY];
|
||||
const assignCases = !!featureCapabilities?.[ASSIGN_CASE_CAPABILITY];
|
||||
|
||||
const all =
|
||||
create &&
|
||||
|
@ -40,7 +42,8 @@ export const getUICapabilities = (
|
|||
connectors &&
|
||||
settings &&
|
||||
reopenCase &&
|
||||
createComment;
|
||||
createComment &&
|
||||
assignCases;
|
||||
|
||||
return {
|
||||
all,
|
||||
|
@ -53,5 +56,6 @@ export const getUICapabilities = (
|
|||
settings,
|
||||
reopenCase,
|
||||
createComment,
|
||||
assign: assignCases,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -20,7 +20,7 @@ describe('hooks', () => {
|
|||
|
||||
expect(result.current).toEqual({
|
||||
actions: { crud: true, read: true },
|
||||
generalCasesV2: allCasesPermissions(),
|
||||
generalCasesV3: 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_V2,
|
||||
FEATURE_ID_V3,
|
||||
DEFAULT_DATE_FORMAT,
|
||||
DEFAULT_DATE_FORMAT_TZ,
|
||||
} from '../../../../common/constants';
|
||||
|
@ -166,7 +166,7 @@ interface Capabilities {
|
|||
}
|
||||
interface UseApplicationCapabilities {
|
||||
actions: Capabilities;
|
||||
generalCasesV2: CasesPermissions;
|
||||
generalCasesV3: 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_V2];
|
||||
const casesCapabilities = capabilities[FEATURE_ID_V3];
|
||||
const permissions = getUICapabilities(casesCapabilities);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
actions: { crud: !!capabilities.actions?.save, read: !!capabilities.actions?.show },
|
||||
generalCasesV2: {
|
||||
generalCasesV3: {
|
||||
all: permissions.all,
|
||||
create: permissions.create,
|
||||
read: permissions.read,
|
||||
|
@ -195,6 +195,7 @@ export const useApplicationCapabilities = (): UseApplicationCapabilities => {
|
|||
settings: permissions.settings,
|
||||
reopenCase: permissions.reopenCase,
|
||||
createComment: permissions.createComment,
|
||||
assign: permissions.assign,
|
||||
},
|
||||
visualize: { crud: !!capabilities.visualize?.save, read: !!capabilities.visualize?.show },
|
||||
dashboard: {
|
||||
|
@ -219,6 +220,7 @@ export const useApplicationCapabilities = (): UseApplicationCapabilities => {
|
|||
permissions.settings,
|
||||
permissions.reopenCase,
|
||||
permissions.createComment,
|
||||
permissions.assign,
|
||||
]
|
||||
);
|
||||
};
|
||||
|
|
|
@ -83,7 +83,7 @@ export const createStartServicesMock = ({ license }: StartServiceArgs = {}): Sta
|
|||
services.application.capabilities = {
|
||||
...services.application.capabilities,
|
||||
actions: { save: true, show: true },
|
||||
generalCasesV2: {
|
||||
generalCasesV3: {
|
||||
create_cases: true,
|
||||
read_cases: true,
|
||||
update_cases: true,
|
||||
|
@ -93,6 +93,7 @@ export const createStartServicesMock = ({ license }: StartServiceArgs = {}): Sta
|
|||
cases_settings: true,
|
||||
case_reopen: true,
|
||||
create_comment: true,
|
||||
cases_assign: true,
|
||||
},
|
||||
visualize: { save: true, show: true },
|
||||
dashboard: { show: true, createNew: true },
|
||||
|
|
|
@ -19,6 +19,7 @@ export const noCasesPermissions = () =>
|
|||
settings: false,
|
||||
createComment: false,
|
||||
reopenCase: false,
|
||||
assign: false,
|
||||
});
|
||||
|
||||
export const readCasesPermissions = () =>
|
||||
|
@ -32,12 +33,14 @@ export const readCasesPermissions = () =>
|
|||
settings: false,
|
||||
createComment: false,
|
||||
reopenCase: false,
|
||||
assign: false,
|
||||
});
|
||||
export const noCreateCasesPermissions = () => buildCasesPermissions({ create: false });
|
||||
export const noCreateCommentCasesPermissions = () =>
|
||||
buildCasesPermissions({ createComment: false });
|
||||
export const noUpdateCasesPermissions = () =>
|
||||
buildCasesPermissions({ update: false, reopenCase: false });
|
||||
export const noAssignCasesPermissions = () => buildCasesPermissions({ assign: false });
|
||||
export const noPushCasesPermissions = () => buildCasesPermissions({ push: false });
|
||||
export const noDeleteCasesPermissions = () => buildCasesPermissions({ delete: false });
|
||||
export const noReopenCasesPermissions = () => buildCasesPermissions({ reopenCase: false });
|
||||
|
@ -51,6 +54,7 @@ export const onlyCreateCommentPermissions = () =>
|
|||
push: false,
|
||||
createComment: true,
|
||||
reopenCase: false,
|
||||
assign: false,
|
||||
});
|
||||
export const onlyDeleteCasesPermission = () =>
|
||||
buildCasesPermissions({
|
||||
|
@ -61,6 +65,7 @@ export const onlyDeleteCasesPermission = () =>
|
|||
push: false,
|
||||
createComment: false,
|
||||
reopenCase: false,
|
||||
assign: false,
|
||||
});
|
||||
// In practice, a real life user should never have this configuration, but testing for thoroughness
|
||||
export const onlyReopenCasesPermission = () =>
|
||||
|
@ -72,6 +77,7 @@ export const onlyReopenCasesPermission = () =>
|
|||
push: false,
|
||||
createComment: false,
|
||||
reopenCase: true,
|
||||
assign: false,
|
||||
});
|
||||
export const noConnectorsCasePermission = () => buildCasesPermissions({ connectors: false });
|
||||
export const noCasesSettingsPermission = () => buildCasesPermissions({ settings: false });
|
||||
|
@ -87,6 +93,7 @@ export const buildCasesPermissions = (overrides: Partial<Omit<CasesPermissions,
|
|||
const settings = overrides.settings ?? true;
|
||||
const reopenCase = overrides.reopenCase ?? true;
|
||||
const createComment = overrides.createComment ?? true;
|
||||
const assign = overrides.assign ?? true;
|
||||
const all =
|
||||
create &&
|
||||
read &&
|
||||
|
@ -96,6 +103,7 @@ export const buildCasesPermissions = (overrides: Partial<Omit<CasesPermissions,
|
|||
settings &&
|
||||
connectors &&
|
||||
reopenCase &&
|
||||
assign &&
|
||||
createComment;
|
||||
|
||||
return {
|
||||
|
@ -109,6 +117,7 @@ export const buildCasesPermissions = (overrides: Partial<Omit<CasesPermissions,
|
|||
settings,
|
||||
reopenCase,
|
||||
createComment,
|
||||
assign,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -124,6 +133,7 @@ export const noCasesCapabilities = () =>
|
|||
cases_settings: false,
|
||||
create_comment: false,
|
||||
case_reopen: false,
|
||||
cases_assign: false,
|
||||
});
|
||||
export const readCasesCapabilities = () =>
|
||||
buildCasesCapabilities({
|
||||
|
@ -134,6 +144,7 @@ export const readCasesCapabilities = () =>
|
|||
cases_settings: false,
|
||||
create_comment: false,
|
||||
case_reopen: false,
|
||||
cases_assign: false,
|
||||
});
|
||||
export const writeCasesCapabilities = () => {
|
||||
return buildCasesCapabilities({
|
||||
|
@ -152,5 +163,6 @@ export const buildCasesCapabilities = (overrides?: Partial<CasesCapabilities>) =
|
|||
cases_settings: overrides?.cases_settings ?? true,
|
||||
create_comment: overrides?.create_comment ?? true,
|
||||
case_reopen: overrides?.case_reopen ?? true,
|
||||
cases_assign: overrides?.cases_assign ?? true,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -20,7 +20,10 @@ export interface UseCasesFeatures {
|
|||
}
|
||||
|
||||
export const useCasesFeatures = (): UseCasesFeatures => {
|
||||
const { features } = useCasesContext();
|
||||
const {
|
||||
features,
|
||||
permissions: { assign },
|
||||
} = useCasesContext();
|
||||
const { isAtLeastPlatinum } = useLicense();
|
||||
const hasLicenseGreaterThanPlatinum = isAtLeastPlatinum();
|
||||
|
||||
|
@ -37,11 +40,17 @@ export const useCasesFeatures = (): UseCasesFeatures => {
|
|||
*/
|
||||
isSyncAlertsEnabled: !features.alerts.enabled ? false : features.alerts.sync,
|
||||
metricsFeatures: features.metrics,
|
||||
caseAssignmentAuthorized: hasLicenseGreaterThanPlatinum,
|
||||
caseAssignmentAuthorized: hasLicenseGreaterThanPlatinum && assign,
|
||||
pushToServiceAuthorized: hasLicenseGreaterThanPlatinum,
|
||||
observablesAuthorized: hasLicenseGreaterThanPlatinum,
|
||||
}),
|
||||
[features.alerts.enabled, features.alerts.sync, features.metrics, hasLicenseGreaterThanPlatinum]
|
||||
[
|
||||
features.alerts.enabled,
|
||||
features.alerts.sync,
|
||||
features.metrics,
|
||||
hasLicenseGreaterThanPlatinum,
|
||||
assign,
|
||||
]
|
||||
);
|
||||
|
||||
return casesFeatures;
|
||||
|
|
|
@ -34,7 +34,8 @@ export const useItemsAction = <T,>({
|
|||
const [isFlyoutOpen, setIsFlyoutOpen] = useState<boolean>(false);
|
||||
const [selectedCasesToEdit, setSelectedCasesToEdit] = useState<CasesUI>([]);
|
||||
const canUpdateStatus = permissions.update;
|
||||
const isActionDisabled = isDisabled || !canUpdateStatus;
|
||||
const canUpdateAssignee = permissions.assign;
|
||||
const isActionDisabled = isDisabled || (!canUpdateStatus && !canUpdateAssignee);
|
||||
|
||||
const onFlyoutClosed = useCallback(() => setIsFlyoutOpen(false), []);
|
||||
const openFlyout = useCallback(
|
||||
|
|
|
@ -47,6 +47,7 @@ export interface AllCasesListProps {
|
|||
export const AllCasesList = React.memo<AllCasesListProps>(
|
||||
({ hiddenStatuses = [], isSelectorView = false, onRowClick }) => {
|
||||
const { owner, permissions } = useCasesContext();
|
||||
|
||||
const availableSolutions = useAvailableCasesOwners(getAllPermissionsExceptFrom('delete'));
|
||||
const isLoading = useIsLoadingCases();
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
|
|
@ -396,6 +396,7 @@ describe('useActions', () => {
|
|||
connectors: true,
|
||||
settings: false,
|
||||
createComment: false,
|
||||
assign: false,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -431,6 +432,7 @@ describe('useActions', () => {
|
|||
connectors: true,
|
||||
settings: false,
|
||||
createComment: false,
|
||||
assign: false,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -466,6 +468,7 @@ describe('useActions', () => {
|
|||
connectors: true,
|
||||
settings: false,
|
||||
createComment: false,
|
||||
assign: false,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ const ActionColumnComponent: React.FC<{ theCase: CaseUI; disableActions: boolean
|
|||
const tooglePopover = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [isPopoverOpen]);
|
||||
const closePopover = useCallback(() => setIsPopoverOpen(false), []);
|
||||
const refreshCases = useRefreshCases();
|
||||
const { permissions } = useCasesContext();
|
||||
|
||||
const shouldDisable = useShouldDisableStatus();
|
||||
|
||||
|
@ -83,6 +84,7 @@ const ActionColumnComponent: React.FC<{ theCase: CaseUI; disableActions: boolean
|
|||
|
||||
const canDelete = deleteAction.canDelete;
|
||||
const canUpdate = statusAction.canUpdateStatus;
|
||||
const canAssign = permissions.assign;
|
||||
|
||||
const panels = useMemo((): EuiContextMenuPanelDescriptor[] => {
|
||||
const mainPanelItems: EuiContextMenuPanelItemDescriptor[] = [];
|
||||
|
@ -136,6 +138,9 @@ const ActionColumnComponent: React.FC<{ theCase: CaseUI; disableActions: boolean
|
|||
|
||||
if (canUpdate) {
|
||||
mainPanelItems.push(tagsAction.getAction([theCase]));
|
||||
}
|
||||
|
||||
if (canAssign) {
|
||||
mainPanelItems.push(assigneesAction.getAction([theCase]));
|
||||
}
|
||||
|
||||
|
@ -164,6 +169,7 @@ const ActionColumnComponent: React.FC<{ theCase: CaseUI; disableActions: boolean
|
|||
}, [
|
||||
assigneesAction,
|
||||
canDelete,
|
||||
canAssign,
|
||||
canUpdate,
|
||||
copyIDAction,
|
||||
deleteAction,
|
||||
|
@ -242,7 +248,8 @@ interface UseBulkActionsProps {
|
|||
|
||||
export const useActions = ({ disableActions }: UseBulkActionsProps): UseBulkActionsReturnValue => {
|
||||
const { permissions } = useCasesContext();
|
||||
const shouldShowActions = permissions.update || permissions.delete || permissions.reopenCase;
|
||||
const shouldShowActions =
|
||||
permissions.update || permissions.delete || permissions.reopenCase || permissions.assign;
|
||||
|
||||
return {
|
||||
actions: shouldShowActions
|
||||
|
|
|
@ -18,6 +18,7 @@ import { useStatusAction } from '../actions/status/use_status_action';
|
|||
import { EditTagsFlyout } from '../actions/tags/edit_tags_flyout';
|
||||
import { useTagsAction } from '../actions/tags/use_tags_action';
|
||||
import { ConfirmDeleteCaseModal } from '../confirm_delete_case';
|
||||
import { useCasesContext } from '../cases_context/use_cases_context';
|
||||
import { useAssigneesAction } from '../actions/assignees/use_assignees_action';
|
||||
import { EditAssigneesFlyout } from '../actions/assignees/edit_assignees_flyout';
|
||||
import * as i18n from './translations';
|
||||
|
@ -40,6 +41,7 @@ export const useBulkActions = ({
|
|||
onActionSuccess,
|
||||
}: UseBulkActionsProps): UseBulkActionsReturnValue => {
|
||||
const isDisabled = selectedCases.length === 0;
|
||||
const { permissions } = useCasesContext();
|
||||
|
||||
const deleteAction = useDeleteAction({
|
||||
isDisabled,
|
||||
|
@ -73,6 +75,7 @@ export const useBulkActions = ({
|
|||
|
||||
const canDelete = deleteAction.canDelete;
|
||||
const canUpdate = statusAction.canUpdateStatus;
|
||||
const canAssign = permissions.assign;
|
||||
|
||||
const panels = useMemo((): EuiContextMenuPanelDescriptor[] => {
|
||||
const mainPanelItems: EuiContextMenuPanelItemDescriptor[] = [];
|
||||
|
@ -110,6 +113,9 @@ export const useBulkActions = ({
|
|||
|
||||
if (canUpdate) {
|
||||
mainPanelItems.push(tagsAction.getAction(selectedCases));
|
||||
}
|
||||
|
||||
if (canAssign) {
|
||||
mainPanelItems.push(assigneesAction.getAction(selectedCases));
|
||||
}
|
||||
|
||||
|
@ -141,6 +147,7 @@ export const useBulkActions = ({
|
|||
}, [
|
||||
canDelete,
|
||||
canUpdate,
|
||||
canAssign,
|
||||
deleteAction,
|
||||
isDisabled,
|
||||
selectedCases,
|
||||
|
|
|
@ -209,6 +209,44 @@ describe('Severity form field', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('does show the bulk actions with only assign permissions', async () => {
|
||||
appMockRender = createAppMockRenderer({
|
||||
permissions: {
|
||||
...noCasesPermissions(),
|
||||
assign: true,
|
||||
},
|
||||
});
|
||||
appMockRender.render(<CasesTableUtilityBar {...props} />);
|
||||
|
||||
expect(await screen.findByTestId('case-table-bulk-actions-link-icon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows bulk actions when only assignCase and update permissions are present', async () => {
|
||||
appMockRender = createAppMockRenderer({
|
||||
permissions: {
|
||||
...noCasesPermissions(),
|
||||
assign: true,
|
||||
update: true,
|
||||
},
|
||||
});
|
||||
appMockRender.render(<CasesTableUtilityBar {...props} />);
|
||||
|
||||
expect(await screen.findByTestId('case-table-bulk-actions-link-icon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows bulk actions when only assignCase and delete permissions are present', async () => {
|
||||
appMockRender = createAppMockRenderer({
|
||||
permissions: {
|
||||
...noCasesPermissions(),
|
||||
assign: true,
|
||||
delete: true,
|
||||
},
|
||||
});
|
||||
appMockRender.render(<CasesTableUtilityBar {...props} />);
|
||||
|
||||
expect(await screen.findByTestId('case-table-bulk-actions-link-icon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('Maximum number of cases', () => {
|
||||
const newProps = {
|
||||
...props,
|
||||
|
|
|
@ -95,7 +95,7 @@ export const CasesTableUtilityBar: FunctionComponent<Props> = React.memo(
|
|||
* in the useBulkActions hook.
|
||||
*/
|
||||
const showBulkActions =
|
||||
(permissions.update || permissions.delete || permissions.reopenCase) &&
|
||||
(permissions.update || permissions.delete || permissions.reopenCase || permissions.assign) &&
|
||||
selectedCases.length > 0;
|
||||
|
||||
const visibleCases =
|
||||
|
|
|
@ -39,7 +39,7 @@ const CasesAppComponent: React.FC<CasesAppProps> = ({
|
|||
getFilesClient,
|
||||
owner: [APP_OWNER],
|
||||
useFetchAlertData: () => [false, {}],
|
||||
permissions: userCapabilities.generalCasesV2,
|
||||
permissions: userCapabilities.generalCasesV3,
|
||||
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 = {
|
||||
securitySolutionCasesV2: allCasesCapabilities(),
|
||||
observabilityCasesV2: allCasesCapabilities(),
|
||||
generalCasesV2: allCasesCapabilities(),
|
||||
securitySolutionCasesV3: allCasesCapabilities(),
|
||||
observabilityCasesV3: allCasesCapabilities(),
|
||||
generalCasesV3: allCasesCapabilities(),
|
||||
};
|
||||
|
||||
const secAllObsReadGenNone = {
|
||||
securitySolutionCasesV2: allCasesCapabilities(),
|
||||
observabilityCasesV2: readCasesCapabilities(),
|
||||
generalCasesV2: noCasesCapabilities(),
|
||||
securitySolutionCasesV3: allCasesCapabilities(),
|
||||
observabilityCasesV3: readCasesCapabilities(),
|
||||
generalCasesV3: noCasesCapabilities(),
|
||||
};
|
||||
|
||||
const unrelatedFeatures = {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { APP_ID, FEATURE_ID_V2 } from '../../../common/constants';
|
||||
import { APP_ID, FEATURE_ID_V3 } from '../../../common/constants';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import type { CasesPermissions } from '../../containers/types';
|
||||
import { allCasePermissions } from '../../utils/permissions';
|
||||
|
@ -22,14 +22,14 @@ export const useAvailableCasesOwners = (
|
|||
capabilities: Capability[] = allCasePermissions
|
||||
): string[] => {
|
||||
const { capabilities: kibanaCapabilities } = useKibana().services.application;
|
||||
|
||||
return Object.entries(kibanaCapabilities).reduce(
|
||||
(availableOwners: string[], [featureId, kibanaCapability]) => {
|
||||
if (!featureId.endsWith('CasesV2')) {
|
||||
if (!featureId.endsWith('CasesV3')) {
|
||||
return availableOwners;
|
||||
}
|
||||
for (const cap of capabilities) {
|
||||
const hasCapability = !!kibanaCapability[`${cap}_cases`];
|
||||
const hasCapability =
|
||||
!!kibanaCapability[`${cap}_cases`] || !!kibanaCapability[`cases_${cap}`];
|
||||
if (!hasCapability) {
|
||||
return availableOwners;
|
||||
}
|
||||
|
@ -42,9 +42,9 @@ export const useAvailableCasesOwners = (
|
|||
};
|
||||
|
||||
const getOwnerFromFeatureID = (featureID: string) => {
|
||||
if (featureID === FEATURE_ID_V2) {
|
||||
if (featureID === FEATURE_ID_V3) {
|
||||
return APP_ID;
|
||||
}
|
||||
|
||||
return featureID.replace('CasesV2', '');
|
||||
return featureID.replace('CasesV3', '');
|
||||
};
|
||||
|
|
|
@ -11,7 +11,7 @@ import { userProfiles, userProfilesMap } from '../../../containers/user_profiles
|
|||
import { fireEvent, screen, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import type { AppMockRenderer } from '../../../common/mock';
|
||||
import { createAppMockRenderer, noUpdateCasesPermissions } from '../../../common/mock';
|
||||
import { createAppMockRenderer, noAssignCasesPermissions } from '../../../common/mock';
|
||||
import type { AssignUsersProps } from './assign_users';
|
||||
import { AssignUsers } from './assign_users';
|
||||
import { waitForEuiPopoverClose, waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl';
|
||||
|
@ -49,15 +49,15 @@ describe('AssignUsers', () => {
|
|||
expect(screen.getByText('No users are assigned')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show the suggest users edit button when the user does not have update permissions', () => {
|
||||
appMockRender = createAppMockRenderer({ permissions: noUpdateCasesPermissions() });
|
||||
it('does not show the suggest users edit button when the user does not have assign permissions', () => {
|
||||
appMockRender = createAppMockRenderer({ permissions: noAssignCasesPermissions() });
|
||||
appMockRender.render(<AssignUsers {...defaultProps} />);
|
||||
|
||||
expect(screen.queryByText('case-view-assignees-edit')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show the assign users link when the user does not have update permissions', () => {
|
||||
appMockRender = createAppMockRenderer({ permissions: noUpdateCasesPermissions() });
|
||||
it('does not show the assign users link when the user does not have assign permissions', () => {
|
||||
appMockRender = createAppMockRenderer({ permissions: noAssignCasesPermissions() });
|
||||
appMockRender.render(<AssignUsers {...defaultProps} />);
|
||||
|
||||
expect(screen.queryByTestId('assign yourself')).not.toBeInTheDocument();
|
||||
|
|
|
@ -180,7 +180,7 @@ const AssignUsersComponent: React.FC<AssignUsersProps> = ({
|
|||
<SidebarTitle title={i18n.ASSIGNEES} />
|
||||
</EuiFlexItem>
|
||||
{isLoading && <EuiLoadingSpinner data-test-subj="case-view-assignees-button-loading" />}
|
||||
{!isLoading && permissions.update && (
|
||||
{!isLoading && permissions.assign && (
|
||||
<EuiFlexItem data-test-subj="case-view-assignees-edit" grow={false}>
|
||||
<SuggestUsersPopover
|
||||
assignedUsersWithProfiles={assigneesWithProfiles}
|
||||
|
|
|
@ -100,6 +100,7 @@ export const CasesProvider: FC<
|
|||
update: permissions.update,
|
||||
reopenCase: permissions.reopenCase,
|
||||
createComment: permissions.createComment,
|
||||
assign: permissions.assign,
|
||||
},
|
||||
basePath,
|
||||
/**
|
||||
|
@ -131,6 +132,7 @@ export const CasesProvider: FC<
|
|||
permissions.update,
|
||||
permissions.reopenCase,
|
||||
permissions.createComment,
|
||||
permissions.assign,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import { DEFAULT_FILTER_OPTIONS, DEFAULT_QUERY_PARAMS } from './constants';
|
|||
import { useGetCases } from './use_get_cases';
|
||||
import * as api from './api';
|
||||
import type { AppMockRenderer } from '../common/mock';
|
||||
import { createAppMockRenderer } from '../common/mock';
|
||||
import { createAppMockRenderer, allCasesCapabilities } from '../common/mock';
|
||||
import { useToasts } from '../common/lib/kibana/hooks';
|
||||
import { OWNERS } from '../../common/constants';
|
||||
|
||||
|
@ -69,24 +69,9 @@ describe('useGetCases', () => {
|
|||
|
||||
appMockRender.coreStart.application.capabilities = {
|
||||
...appMockRender.coreStart.application.capabilities,
|
||||
observabilityCasesV2: {
|
||||
create_cases: true,
|
||||
read_cases: true,
|
||||
update_cases: true,
|
||||
push_cases: true,
|
||||
cases_connectors: true,
|
||||
delete_cases: true,
|
||||
cases_settings: true,
|
||||
},
|
||||
securitySolutionCasesV2: {
|
||||
create_cases: true,
|
||||
read_cases: true,
|
||||
update_cases: true,
|
||||
push_cases: true,
|
||||
cases_connectors: true,
|
||||
delete_cases: true,
|
||||
cases_settings: true,
|
||||
},
|
||||
generalCasesV3: allCasesCapabilities(),
|
||||
observabilityCasesV3: allCasesCapabilities(),
|
||||
securitySolutionCasesV3: allCasesCapabilities(),
|
||||
};
|
||||
|
||||
const spyOnGetCases = jest.spyOn(api, 'getCases');
|
||||
|
@ -107,6 +92,12 @@ describe('useGetCases', () => {
|
|||
|
||||
it('should set only the available owners when no owner is provided', async () => {
|
||||
appMockRender = createAppMockRenderer({ owner: [] });
|
||||
|
||||
appMockRender.coreStart.application.capabilities = {
|
||||
...appMockRender.coreStart.application.capabilities,
|
||||
generalCasesV3: allCasesCapabilities(),
|
||||
};
|
||||
|
||||
const spyOnGetCases = jest.spyOn(api, 'getCases');
|
||||
|
||||
renderHook(() => useGetCases(), {
|
||||
|
|
|
@ -10,7 +10,15 @@ import { getAllPermissionsExceptFrom, isReadOnlyPermissions } from './permission
|
|||
|
||||
describe('permissions', () => {
|
||||
describe('isReadOnlyPermissions', () => {
|
||||
const tests = [['update'], ['create'], ['delete'], ['push'], ['all']];
|
||||
const tests = [
|
||||
['update'],
|
||||
['create'],
|
||||
['delete'],
|
||||
['push'],
|
||||
['all'],
|
||||
['assign'],
|
||||
['createComment'],
|
||||
];
|
||||
|
||||
it('returns true if the user has only read permissions', async () => {
|
||||
expect(isReadOnlyPermissions(readCasesPermissions())).toBe(true);
|
||||
|
@ -31,7 +39,13 @@ describe('permissions', () => {
|
|||
|
||||
describe('getAllPermissionsExceptFrom', () => {
|
||||
it('returns the correct permissions', async () => {
|
||||
expect(getAllPermissionsExceptFrom('create')).toEqual(['read', 'update', 'delete', 'push']);
|
||||
expect(getAllPermissionsExceptFrom('create')).toEqual([
|
||||
'read',
|
||||
'update',
|
||||
'delete',
|
||||
'push',
|
||||
'assign',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,13 +14,22 @@ export const isReadOnlyPermissions = (permissions: CasesPermissions) => {
|
|||
!permissions.update &&
|
||||
!permissions.delete &&
|
||||
!permissions.push &&
|
||||
!permissions.assign &&
|
||||
!permissions.createComment &&
|
||||
permissions.read
|
||||
);
|
||||
};
|
||||
|
||||
type CasePermission = Exclude<keyof CasesPermissions, 'all'>;
|
||||
|
||||
export const allCasePermissions: CasePermission[] = ['create', 'read', 'update', 'delete', 'push'];
|
||||
export const allCasePermissions: CasePermission[] = [
|
||||
'create',
|
||||
'read',
|
||||
'update',
|
||||
'delete',
|
||||
'push',
|
||||
'assign',
|
||||
];
|
||||
|
||||
export const getAllPermissionsExceptFrom = (capToExclude: CasePermission): CasePermission[] =>
|
||||
allCasePermissions.filter((permission) => permission !== capToExclude) as CasePermission[];
|
||||
|
|
|
@ -1,5 +1,89 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`audit_logger log function event structure creates the correct audit event for operation: "assignCase" with an error and entity 1`] = `
|
||||
Object {
|
||||
"error": Object {
|
||||
"code": "Error",
|
||||
"message": "an error",
|
||||
},
|
||||
"event": Object {
|
||||
"action": "cases_assign",
|
||||
"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: "assignCase" with an error but no entity 1`] = `
|
||||
Object {
|
||||
"error": Object {
|
||||
"code": "Error",
|
||||
"message": "an error",
|
||||
},
|
||||
"event": Object {
|
||||
"action": "cases_assign",
|
||||
"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: "assignCase" without an error but with an entity 1`] = `
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "cases_assign",
|
||||
"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: "assignCase" without an error or entity 1`] = `
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "cases_assign",
|
||||
"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: "bulkCreateAttachments" with an error and entity 1`] = `
|
||||
Object {
|
||||
"error": Object {
|
||||
|
|
|
@ -190,6 +190,14 @@ const CaseOperations = {
|
|||
docType: 'case',
|
||||
savedObjectType: CASE_SAVED_OBJECT,
|
||||
},
|
||||
[WriteOperations.AssignCase]: {
|
||||
ecsType: EVENT_TYPES.change,
|
||||
name: WriteOperations.AssignCase as const,
|
||||
action: 'cases_assign',
|
||||
verbs: updateVerbs,
|
||||
docType: 'case',
|
||||
savedObjectType: CASE_SAVED_OBJECT,
|
||||
},
|
||||
};
|
||||
|
||||
const ConfigurationOperations = {
|
||||
|
|
|
@ -64,6 +64,7 @@ export enum WriteOperations {
|
|||
CreateConfiguration = 'createConfiguration',
|
||||
UpdateConfiguration = 'updateConfiguration',
|
||||
ReopenCase = 'reopenCase',
|
||||
AssignCase = 'assignCase',
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -416,6 +416,65 @@ describe('bulkCreate', () => {
|
|||
{ id: 'mock-saved-object-id', owner: 'securitySolution' },
|
||||
{ id: 'mock-saved-object-id', owner: 'cases' },
|
||||
],
|
||||
operation: [
|
||||
{
|
||||
action: 'cases_assign',
|
||||
docType: 'case',
|
||||
ecsType: 'change',
|
||||
name: 'assignCase',
|
||||
savedObjectType: 'cases',
|
||||
verbs: { past: 'updated', present: 'update', progressive: 'updating' },
|
||||
},
|
||||
{
|
||||
action: 'case_create',
|
||||
docType: 'case',
|
||||
ecsType: 'creation',
|
||||
name: 'createCase',
|
||||
savedObjectType: 'cases',
|
||||
verbs: { past: 'created', present: 'create', progressive: 'creating' },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('validates with assign+create operations when cases have assignees', async () => {
|
||||
await bulkCreate(
|
||||
{ cases: [getCases()[0], getCases({ owner: 'cases' })[0]] },
|
||||
clientArgs,
|
||||
casesClientMock
|
||||
);
|
||||
|
||||
expect(clientArgs.authorization.ensureAuthorized).toHaveBeenCalledWith({
|
||||
entities: [
|
||||
{ id: 'mock-saved-object-id', owner: 'securitySolution' },
|
||||
{ id: 'mock-saved-object-id', owner: 'cases' },
|
||||
],
|
||||
operation: [
|
||||
{
|
||||
action: 'cases_assign',
|
||||
docType: 'case',
|
||||
ecsType: 'change',
|
||||
name: 'assignCase',
|
||||
savedObjectType: 'cases',
|
||||
verbs: { past: 'updated', present: 'update', progressive: 'updating' },
|
||||
},
|
||||
{
|
||||
action: 'case_create',
|
||||
docType: 'case',
|
||||
ecsType: 'creation',
|
||||
name: 'createCase',
|
||||
savedObjectType: 'cases',
|
||||
verbs: { past: 'created', present: 'create', progressive: 'creating' },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('validates with only create operation when cases have no assignees', async () => {
|
||||
await bulkCreate({ cases: [getCases({ assignees: [] })[0]] }, clientArgs, casesClientMock);
|
||||
|
||||
expect(clientArgs.authorization.ensureAuthorized).toHaveBeenCalledWith({
|
||||
entities: [{ id: 'mock-saved-object-id', owner: 'securitySolution' }],
|
||||
operation: {
|
||||
action: 'case_create',
|
||||
docType: 'case',
|
||||
|
|
|
@ -54,10 +54,20 @@ export const bulkCreate = async (
|
|||
|
||||
const casesWithIds = getCaseWithIds(decodedData);
|
||||
|
||||
await auth.ensureAuthorized({
|
||||
operation: Operations.createCase,
|
||||
entities: casesWithIds.map((theCase) => ({ owner: theCase.owner, id: theCase.id })),
|
||||
});
|
||||
if (
|
||||
casesWithIds.filter((theCase) => theCase.assignees && theCase.assignees.length !== 0).length >
|
||||
0
|
||||
) {
|
||||
await auth.ensureAuthorized({
|
||||
operation: [Operations.assignCase, Operations.createCase],
|
||||
entities: casesWithIds.map((theCase) => ({ owner: theCase.owner, id: theCase.id })),
|
||||
});
|
||||
} else {
|
||||
await auth.ensureAuthorized({
|
||||
operation: Operations.createCase,
|
||||
entities: casesWithIds.map((theCase) => ({ owner: theCase.owner, id: theCase.id })),
|
||||
});
|
||||
}
|
||||
|
||||
const hasPlatinumLicenseOrGreater = await licensingService.isAtLeastPlatinum();
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
import { mockCases } from '../../mocks';
|
||||
import { createCasesClientMock, createCasesClientMockArgs } from '../mocks';
|
||||
import { Operations } from '../../authorization';
|
||||
import { bulkUpdate } from './bulk_update';
|
||||
import { bulkUpdate, getOperationsToAuthorize } from './bulk_update';
|
||||
|
||||
describe('update', () => {
|
||||
const cases = {
|
||||
|
@ -315,6 +315,90 @@ describe('update', () => {
|
|||
'Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: The length of the field assignees is too long. Array must be of length <= 10.'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns only updateCase operation when no reopened cases or changed assignees', () => {
|
||||
const operations = getOperationsToAuthorize({
|
||||
reopenedCases: [],
|
||||
changedAssignees: [],
|
||||
allCases: cases.cases,
|
||||
});
|
||||
expect(operations).toEqual([Operations.updateCase]);
|
||||
});
|
||||
|
||||
it('returns only assignCase operation when all cases are assignee changes', () => {
|
||||
const operations = getOperationsToAuthorize({
|
||||
reopenedCases: [],
|
||||
changedAssignees: cases.cases,
|
||||
allCases: cases.cases,
|
||||
});
|
||||
expect(operations).toEqual([Operations.assignCase]);
|
||||
});
|
||||
|
||||
it('returns only reopenCase operation when all cases are being reopened', () => {
|
||||
const operations = getOperationsToAuthorize({
|
||||
reopenedCases: cases.cases,
|
||||
changedAssignees: [],
|
||||
allCases: cases.cases,
|
||||
});
|
||||
expect(operations).toEqual([Operations.reopenCase]);
|
||||
});
|
||||
|
||||
it('returns assignCase and updateCase when some cases have non-assignee changes', () => {
|
||||
const case2 = { id: 'case-2', version: '1' };
|
||||
const operations = getOperationsToAuthorize({
|
||||
reopenedCases: [],
|
||||
changedAssignees: cases.cases,
|
||||
allCases: [...cases.cases, case2],
|
||||
});
|
||||
expect(operations).toEqual([Operations.assignCase, Operations.updateCase]);
|
||||
});
|
||||
|
||||
it('returns reopenCase and updateCase when some cases have non-reopen changes', () => {
|
||||
const case2 = { id: 'case-2', version: '1' };
|
||||
const operations = getOperationsToAuthorize({
|
||||
reopenedCases: cases.cases,
|
||||
changedAssignees: [],
|
||||
allCases: [...cases.cases, case2],
|
||||
});
|
||||
expect(operations).toEqual([Operations.reopenCase, Operations.updateCase]);
|
||||
});
|
||||
|
||||
it('returns all operations when cases have mixed changes', () => {
|
||||
const case2 = { id: 'case-2', version: '1' };
|
||||
const case3 = { id: 'case-3', version: '1' };
|
||||
const operations = getOperationsToAuthorize({
|
||||
reopenedCases: cases.cases,
|
||||
changedAssignees: [case2],
|
||||
allCases: [...cases.cases, case2, case3],
|
||||
});
|
||||
expect(operations).toEqual([
|
||||
Operations.reopenCase,
|
||||
Operations.assignCase,
|
||||
Operations.updateCase,
|
||||
]);
|
||||
});
|
||||
|
||||
it('handles empty casesToAuthorize array', () => {
|
||||
const operations = getOperationsToAuthorize({
|
||||
reopenedCases: [],
|
||||
changedAssignees: [],
|
||||
allCases: [],
|
||||
});
|
||||
expect(operations).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns only combined operations when all cases have both reopen and assignee changes', () => {
|
||||
const operations = getOperationsToAuthorize({
|
||||
reopenedCases: cases.cases,
|
||||
changedAssignees: cases.cases,
|
||||
allCases: cases.cases,
|
||||
});
|
||||
expect(operations).toEqual([
|
||||
Operations.reopenCase,
|
||||
Operations.assignCase,
|
||||
Operations.updateCase,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Category', () => {
|
||||
|
@ -1514,6 +1598,59 @@ describe('update', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('throws an error if the case is not found', async () => {
|
||||
clientArgsMock.services.caseService.getCases.mockResolvedValue({ saved_objects: [] });
|
||||
|
||||
await expect(
|
||||
bulkUpdate(
|
||||
{
|
||||
cases: [
|
||||
{
|
||||
id: mockCases[0].id,
|
||||
version: mockCases[0].version ?? '',
|
||||
status: CaseStatuses.open,
|
||||
},
|
||||
],
|
||||
},
|
||||
clientArgsMock,
|
||||
casesClientMock
|
||||
)
|
||||
).rejects.toThrow(
|
||||
'Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: These cases mock-id-1 do not exist. Please check you have the correct ids.'
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error if the case is not found and the SO clients returns an SO object', async () => {
|
||||
clientArgsMock.services.caseService.getCases.mockResolvedValue({
|
||||
saved_objects: [
|
||||
{
|
||||
type: 'cases',
|
||||
id: 'mock-id-1',
|
||||
references: [],
|
||||
error: { error: 'Non found', message: 'Non found', statusCode: 404 },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await expect(
|
||||
bulkUpdate(
|
||||
{
|
||||
cases: [
|
||||
{
|
||||
id: mockCases[0].id,
|
||||
version: mockCases[0].version ?? '',
|
||||
status: CaseStatuses.open,
|
||||
},
|
||||
],
|
||||
},
|
||||
clientArgsMock,
|
||||
casesClientMock
|
||||
)
|
||||
).rejects.toThrow(
|
||||
'Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: These cases mock-id-1 do not exist. Please check you have the correct ids.'
|
||||
);
|
||||
});
|
||||
|
||||
describe('Validate max user actions per page', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
@ -1674,7 +1811,7 @@ describe('update', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('checks authorization for both reopenCase and updateCase operations when reopening a case', async () => {
|
||||
it('checks authorization for only reopenCase', async () => {
|
||||
// Mock a closed case
|
||||
const closedCase = {
|
||||
...mockCases[0],
|
||||
|
@ -1683,6 +1820,7 @@ describe('update', () => {
|
|||
status: CaseStatuses.closed,
|
||||
},
|
||||
};
|
||||
|
||||
clientArgs.services.caseService.getCases.mockResolvedValue({ saved_objects: [closedCase] });
|
||||
|
||||
clientArgs.services.caseService.patchCases.mockResolvedValue({
|
||||
|
@ -1703,7 +1841,10 @@ describe('update', () => {
|
|||
casesClientMock
|
||||
);
|
||||
|
||||
expect(clientArgs.authorization.ensureAuthorized).not.toThrow();
|
||||
expect(clientArgs.authorization.ensureAuthorized).toHaveBeenCalledWith({
|
||||
entities: [{ id: closedCase.id, owner: closedCase.attributes.owner }],
|
||||
operation: [Operations.reopenCase],
|
||||
});
|
||||
});
|
||||
|
||||
it('throws when user is not authorized to update case', async () => {
|
||||
|
@ -1728,38 +1869,6 @@ describe('update', () => {
|
|||
`"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"`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,13 +14,14 @@ import type {
|
|||
SavedObjectsFindResult,
|
||||
SavedObjectsUpdateResponse,
|
||||
} from '@kbn/core/server';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
import { nodeBuilder } from '@kbn/es-query';
|
||||
|
||||
import type { AlertService, CasesService, CaseUserActionService } from '../../services';
|
||||
import type { UpdateAlertStatusRequest } from '../alerts/types';
|
||||
import type { CasesClient, CasesClientArgs } from '..';
|
||||
import type { OwnerEntity } from '../../authorization';
|
||||
import type { OwnerEntity, OperationDetails } from '../../authorization';
|
||||
import type { PatchCasesArgs } from '../../services/cases/types';
|
||||
import type { UserActionEvent, UserActionsDict } from '../../services/user_actions/types';
|
||||
|
||||
|
@ -273,10 +274,12 @@ function partitionPatchRequest(
|
|||
// This will be a deduped array of case IDs with their corresponding owner
|
||||
casesToAuthorize: OwnerEntity[];
|
||||
reopenedCases: CasePatchRequest[];
|
||||
changedAssignees: CasePatchRequest[];
|
||||
} {
|
||||
const nonExistingCases: CasePatchRequest[] = [];
|
||||
const conflictedCases: CasePatchRequest[] = [];
|
||||
const reopenedCases: CasePatchRequest[] = [];
|
||||
const changedAssignees: CasePatchRequest[] = [];
|
||||
const casesToAuthorize: Map<string, OwnerEntity> = new Map<string, OwnerEntity>();
|
||||
|
||||
for (const reqCase of patchReqCases) {
|
||||
|
@ -295,19 +298,62 @@ function partitionPatchRequest(
|
|||
) {
|
||||
// Track cases that are closed and a user is attempting to reopen
|
||||
reopenedCases.push(reqCase);
|
||||
casesToAuthorize.set(foundCase.id, { id: foundCase.id, owner: foundCase.attributes.owner });
|
||||
} else {
|
||||
casesToAuthorize.set(foundCase.id, { id: foundCase.id, owner: foundCase.attributes.owner });
|
||||
}
|
||||
if (reqCase.assignees) {
|
||||
if (
|
||||
!isEqual(
|
||||
reqCase.assignees.map(({ uid }) => uid),
|
||||
foundCase?.attributes.assignees.map(({ uid }) => uid)
|
||||
) &&
|
||||
foundCase
|
||||
) {
|
||||
changedAssignees.push(reqCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
nonExistingCases,
|
||||
conflictedCases,
|
||||
reopenedCases,
|
||||
changedAssignees,
|
||||
casesToAuthorize: Array.from(casesToAuthorize.values()),
|
||||
};
|
||||
}
|
||||
|
||||
export function getOperationsToAuthorize({
|
||||
reopenedCases,
|
||||
changedAssignees,
|
||||
allCases,
|
||||
}: {
|
||||
reopenedCases: CasePatchRequest[];
|
||||
changedAssignees: CasePatchRequest[];
|
||||
allCases: CasePatchRequest[];
|
||||
}): OperationDetails[] {
|
||||
const operations: OperationDetails[] = [];
|
||||
const onlyAssigneeOperations =
|
||||
reopenedCases.length === 0 && changedAssignees.length === allCases.length;
|
||||
const onlyReopenOperations =
|
||||
changedAssignees.length === 0 && reopenedCases.length === allCases.length;
|
||||
|
||||
if (reopenedCases.length > 0) {
|
||||
operations.push(Operations.reopenCase);
|
||||
}
|
||||
|
||||
if (changedAssignees.length > 0) {
|
||||
operations.push(Operations.assignCase);
|
||||
}
|
||||
|
||||
if (!onlyAssigneeOperations && !onlyReopenOperations) {
|
||||
operations.push(Operations.updateCase);
|
||||
}
|
||||
|
||||
return operations;
|
||||
}
|
||||
|
||||
export interface UpdateRequestWithOriginalCase {
|
||||
updateReq: CasePatchRequest;
|
||||
originalCase: CaseSavedObjectTransformed;
|
||||
|
@ -354,13 +400,14 @@ export const bulkUpdate = async (
|
|||
return acc;
|
||||
}, new Map<string, CaseSavedObjectTransformed>());
|
||||
|
||||
const { nonExistingCases, conflictedCases, casesToAuthorize, reopenedCases } =
|
||||
const { nonExistingCases, conflictedCases, casesToAuthorize, reopenedCases, changedAssignees } =
|
||||
partitionPatchRequest(casesMap, query.cases);
|
||||
|
||||
const operationsToAuthorize =
|
||||
reopenedCases.length > 0
|
||||
? [Operations.reopenCase, Operations.updateCase]
|
||||
: [Operations.updateCase];
|
||||
const operationsToAuthorize = getOperationsToAuthorize({
|
||||
reopenedCases,
|
||||
changedAssignees,
|
||||
allCases: query.cases,
|
||||
});
|
||||
|
||||
await authorization.ensureAuthorized({
|
||||
entities: casesToAuthorize,
|
||||
|
|
|
@ -116,6 +116,51 @@ describe('create', () => {
|
|||
`Failed to create case: Error: In order to assign users to cases, you must be subscribed to an Elastic Platinum license`
|
||||
);
|
||||
});
|
||||
|
||||
it('validates with assign+create operations when cases have assignees', async () => {
|
||||
clientArgs.services.licensingService.isAtLeastPlatinum.mockResolvedValue(true);
|
||||
await create(theCase, clientArgs, casesClientMock);
|
||||
|
||||
expect(clientArgs.authorization.ensureAuthorized).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
operation: [
|
||||
{
|
||||
action: 'cases_assign',
|
||||
docType: 'case',
|
||||
ecsType: 'change',
|
||||
name: 'assignCase',
|
||||
savedObjectType: 'cases',
|
||||
verbs: { past: 'updated', present: 'update', progressive: 'updating' },
|
||||
},
|
||||
{
|
||||
action: 'case_create',
|
||||
docType: 'case',
|
||||
ecsType: 'creation',
|
||||
name: 'createCase',
|
||||
savedObjectType: 'cases',
|
||||
verbs: { past: 'created', present: 'create', progressive: 'creating' },
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('validates with only create operation when cases have no assignees', async () => {
|
||||
await create({ ...theCase, assignees: [] }, clientArgs, casesClientMock);
|
||||
|
||||
expect(clientArgs.authorization.ensureAuthorized).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
operation: {
|
||||
action: 'case_create',
|
||||
docType: 'case',
|
||||
ecsType: 'creation',
|
||||
name: 'createCase',
|
||||
savedObjectType: 'cases',
|
||||
verbs: { past: 'created', present: 'create', progressive: 'creating' },
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Attributes', () => {
|
||||
|
|
|
@ -52,11 +52,17 @@ export const create = async (
|
|||
validateCustomFields(customFieldsValidationParams);
|
||||
|
||||
const savedObjectID = SavedObjectsUtils.generateId();
|
||||
|
||||
await auth.ensureAuthorized({
|
||||
operation: Operations.createCase,
|
||||
entities: [{ owner: query.owner, id: savedObjectID }],
|
||||
});
|
||||
if (query.assignees && query.assignees.length > 0) {
|
||||
await auth.ensureAuthorized({
|
||||
operation: [Operations.assignCase, Operations.createCase],
|
||||
entities: [{ owner: query.owner, id: savedObjectID }],
|
||||
});
|
||||
} else {
|
||||
await auth.ensureAuthorized({
|
||||
operation: Operations.createCase,
|
||||
entities: [{ owner: query.owner, id: savedObjectID }],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign users to a case is only available to Platinum+
|
||||
|
|
|
@ -37,6 +37,7 @@ describe('getCasesConnectorType', () => {
|
|||
'cases:my-owner/deleteComment',
|
||||
'cases:my-owner/findConfigurations',
|
||||
'cases:my-owner/reopenCase',
|
||||
'cases:my-owner/assignCase',
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -358,6 +359,7 @@ describe('getCasesConnectorType', () => {
|
|||
'cases:securitySolution/deleteComment',
|
||||
'cases:securitySolution/findConfigurations',
|
||||
'cases:securitySolution/reopenCase',
|
||||
'cases:securitySolution/assignCase',
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -379,6 +381,7 @@ describe('getCasesConnectorType', () => {
|
|||
'cases:observability/deleteComment',
|
||||
'cases:observability/findConfigurations',
|
||||
'cases:observability/reopenCase',
|
||||
'cases:observability/assignCase',
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -400,6 +403,7 @@ describe('getCasesConnectorType', () => {
|
|||
'cases:securitySolution/deleteComment',
|
||||
'cases:securitySolution/findConfigurations',
|
||||
'cases:securitySolution/reopenCase',
|
||||
'cases:securitySolution/assignCase',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -508,6 +508,7 @@ describe('utils', () => {
|
|||
'cases:my-owner/deleteComment',
|
||||
'cases:my-owner/findConfigurations',
|
||||
'cases:my-owner/reopenCase',
|
||||
'cases:my-owner/assignCase',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -121,5 +121,6 @@ export const constructRequiredKibanaPrivileges = (owner: string): string[] => {
|
|||
`cases:${owner}/deleteComment`,
|
||||
`cases:${owner}/findConfigurations`,
|
||||
`cases:${owner}/reopenCase`,
|
||||
`cases:${owner}/assignCase`,
|
||||
];
|
||||
};
|
||||
|
|
|
@ -16,3 +16,4 @@ 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';
|
||||
export const CASES_ASSIGN_SUB_PRIVILEGE_ID = 'cases_assign';
|
||||
|
|
|
@ -8,8 +8,10 @@
|
|||
import type { KibanaFeatureConfig } from '@kbn/features-plugin/common';
|
||||
import { getV1 } from './v1';
|
||||
import { getV2 } from './v2';
|
||||
import { getV3 } from './v3';
|
||||
|
||||
export const getCasesKibanaFeatures = (): {
|
||||
v1: KibanaFeatureConfig;
|
||||
v2: KibanaFeatureConfig;
|
||||
} => ({ v1: getV1(), v2: getV2() });
|
||||
v3: KibanaFeatureConfig;
|
||||
} => ({ v1: getV1(), v2: getV2(), v3: getV3() });
|
||||
|
|
|
@ -12,7 +12,7 @@ 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, FEATURE_ID_V2 } from '../../common/constants';
|
||||
import { APP_ID, FEATURE_ID, FEATURE_ID_V3 } from '../../common/constants';
|
||||
import { createUICapabilities, getApiTags } from '../../common';
|
||||
import { CASES_DELETE_SUB_PRIVILEGE_ID, CASES_SETTINGS_SUB_PRIVILEGE_ID } from './constants';
|
||||
|
||||
|
@ -35,7 +35,7 @@ export const getV1 = (): KibanaFeatureConfig => {
|
|||
'The {currentId} permissions are deprecated, please see {casesFeatureIdV2}.',
|
||||
values: {
|
||||
currentId: FEATURE_ID,
|
||||
casesFeatureIdV2: FEATURE_ID_V2,
|
||||
casesFeatureIdV2: FEATURE_ID_V3,
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
@ -61,6 +61,7 @@ export const getV1 = (): KibanaFeatureConfig => {
|
|||
push: [APP_ID],
|
||||
createComment: [APP_ID],
|
||||
reopenCase: [APP_ID],
|
||||
assign: [APP_ID],
|
||||
},
|
||||
management: {
|
||||
insightsAndAlerting: [APP_ID],
|
||||
|
@ -69,13 +70,18 @@ export const getV1 = (): KibanaFeatureConfig => {
|
|||
all: [...filesSavedObjectTypes],
|
||||
read: [...filesSavedObjectTypes],
|
||||
},
|
||||
ui: capabilities.all,
|
||||
ui: [
|
||||
...capabilities.all,
|
||||
...capabilities.createComment,
|
||||
...capabilities.reopenCase,
|
||||
...capabilities.assignCase,
|
||||
],
|
||||
replacedBy: {
|
||||
default: [{ feature: FEATURE_ID_V2, privileges: ['all'] }],
|
||||
default: [{ feature: FEATURE_ID_V3, privileges: ['all'] }],
|
||||
minimal: [
|
||||
{
|
||||
feature: FEATURE_ID_V2,
|
||||
privileges: ['minimal_all', 'create_comment', 'case_reopen'],
|
||||
feature: FEATURE_ID_V3,
|
||||
privileges: ['minimal_all', 'create_comment', 'case_reopen', 'cases_assign'],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -94,8 +100,8 @@ export const getV1 = (): KibanaFeatureConfig => {
|
|||
},
|
||||
ui: capabilities.read,
|
||||
replacedBy: {
|
||||
default: [{ feature: FEATURE_ID_V2, privileges: ['read'] }],
|
||||
minimal: [{ feature: FEATURE_ID_V2, privileges: ['minimal_read'] }],
|
||||
default: [{ feature: FEATURE_ID_V3, privileges: ['read'] }],
|
||||
minimal: [{ feature: FEATURE_ID_V3, privileges: ['minimal_read'] }],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -124,7 +130,7 @@ export const getV1 = (): KibanaFeatureConfig => {
|
|||
},
|
||||
ui: capabilities.delete,
|
||||
replacedBy: [
|
||||
{ feature: FEATURE_ID_V2, privileges: [CASES_DELETE_SUB_PRIVILEGE_ID] },
|
||||
{ feature: FEATURE_ID_V3, privileges: [CASES_DELETE_SUB_PRIVILEGE_ID] },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
@ -154,7 +160,7 @@ export const getV1 = (): KibanaFeatureConfig => {
|
|||
},
|
||||
ui: capabilities.settings,
|
||||
replacedBy: [
|
||||
{ feature: FEATURE_ID_V2, privileges: [CASES_SETTINGS_SUB_PRIVILEGE_ID] },
|
||||
{ feature: FEATURE_ID_V3, privileges: [CASES_SETTINGS_SUB_PRIVILEGE_ID] },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
@ -12,7 +12,7 @@ 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_V2 } from '../../common/constants';
|
||||
import { APP_ID, FEATURE_ID_V2, FEATURE_ID_V3 } from '../../common/constants';
|
||||
import { createUICapabilities, getApiTags } from '../../common';
|
||||
import {
|
||||
CASES_DELETE_SUB_PRIVILEGE_ID,
|
||||
|
@ -34,6 +34,16 @@ export const getV2 = (): KibanaFeatureConfig => {
|
|||
const apiTags = getApiTags(APP_ID);
|
||||
|
||||
return {
|
||||
deprecated: {
|
||||
notice: i18n.translate('xpack.cases.features.casesFeatureV2.deprecationMessage', {
|
||||
defaultMessage:
|
||||
'The {currentId} permissions are deprecated, please see {casesFeatureIdV3}.',
|
||||
values: {
|
||||
currentId: FEATURE_ID_V2,
|
||||
casesFeatureIdV3: FEATURE_ID_V3,
|
||||
},
|
||||
}),
|
||||
},
|
||||
id: FEATURE_ID_V2,
|
||||
name: i18n.translate('xpack.cases.features.casesFeatureName', {
|
||||
defaultMessage: 'Cases',
|
||||
|
@ -54,6 +64,7 @@ export const getV2 = (): KibanaFeatureConfig => {
|
|||
read: [APP_ID],
|
||||
update: [APP_ID],
|
||||
push: [APP_ID],
|
||||
assign: [APP_ID],
|
||||
},
|
||||
management: {
|
||||
insightsAndAlerting: [APP_ID],
|
||||
|
@ -62,7 +73,16 @@ export const getV2 = (): KibanaFeatureConfig => {
|
|||
all: [...filesSavedObjectTypes],
|
||||
read: [...filesSavedObjectTypes],
|
||||
},
|
||||
ui: capabilities.all,
|
||||
ui: [...capabilities.all, ...capabilities.assignCase],
|
||||
replacedBy: {
|
||||
default: [{ feature: FEATURE_ID_V3, privileges: ['all'] }],
|
||||
minimal: [
|
||||
{
|
||||
feature: FEATURE_ID_V3,
|
||||
privileges: ['minimal_all', 'cases_assign'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
read: {
|
||||
api: apiTags.read,
|
||||
|
@ -77,6 +97,10 @@ export const getV2 = (): KibanaFeatureConfig => {
|
|||
read: [...filesSavedObjectTypes],
|
||||
},
|
||||
ui: capabilities.read,
|
||||
replacedBy: {
|
||||
default: [{ feature: FEATURE_ID_V3, privileges: ['read'] }],
|
||||
minimal: [{ feature: FEATURE_ID_V3, privileges: ['minimal_read'] }],
|
||||
},
|
||||
},
|
||||
},
|
||||
subFeatures: [
|
||||
|
@ -103,6 +127,9 @@ export const getV2 = (): KibanaFeatureConfig => {
|
|||
delete: [APP_ID],
|
||||
},
|
||||
ui: capabilities.delete,
|
||||
replacedBy: [
|
||||
{ feature: FEATURE_ID_V3, privileges: [CASES_DELETE_SUB_PRIVILEGE_ID] },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -130,6 +157,9 @@ export const getV2 = (): KibanaFeatureConfig => {
|
|||
settings: [APP_ID],
|
||||
},
|
||||
ui: capabilities.settings,
|
||||
replacedBy: [
|
||||
{ feature: FEATURE_ID_V3, privileges: [CASES_SETTINGS_SUB_PRIVILEGE_ID] },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -158,6 +188,9 @@ export const getV2 = (): KibanaFeatureConfig => {
|
|||
createComment: [APP_ID],
|
||||
},
|
||||
ui: capabilities.createComment,
|
||||
replacedBy: [
|
||||
{ feature: FEATURE_ID_V3, privileges: [CASES_CREATE_COMMENT_SUB_PRIVILEGE_ID] },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -185,6 +218,9 @@ export const getV2 = (): KibanaFeatureConfig => {
|
|||
reopenCase: [APP_ID],
|
||||
},
|
||||
ui: capabilities.reopenCase,
|
||||
replacedBy: [
|
||||
{ feature: FEATURE_ID_V3, privileges: [CASES_REOPEN_SUB_PRIVILEGE_ID] },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
223
x-pack/platform/plugins/shared/cases/server/features/v3.ts
Normal file
223
x-pack/platform/plugins/shared/cases/server/features/v3.ts
Normal file
|
@ -0,0 +1,223 @@
|
|||
/*
|
||||
* 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_V3 } 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,
|
||||
CASES_ASSIGN_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 getV3 = (): KibanaFeatureConfig => {
|
||||
const capabilities = createUICapabilities();
|
||||
const apiTags = getApiTags(APP_ID);
|
||||
|
||||
return {
|
||||
id: FEATURE_ID_V3,
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.cases.features.assignUsersSubFeatureName', {
|
||||
defaultMessage: 'Assign users',
|
||||
}),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
id: CASES_ASSIGN_SUB_PRIVILEGE_ID,
|
||||
name: i18n.translate('xpack.cases.features.assignUsersSubFeatureName', {
|
||||
defaultMessage: 'Assign users to cases',
|
||||
}),
|
||||
includeIn: 'all',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
cases: {
|
||||
assign: [APP_ID],
|
||||
},
|
||||
ui: capabilities.assignCase,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
|
@ -100,6 +100,7 @@ export class CasePlugin
|
|||
const casesFeatures = getCasesKibanaFeatures();
|
||||
plugins.features.registerKibanaFeature(casesFeatures.v1);
|
||||
plugins.features.registerKibanaFeature(casesFeatures.v2);
|
||||
plugins.features.registerKibanaFeature(casesFeatures.v3);
|
||||
}
|
||||
|
||||
registerSavedObjects({
|
||||
|
|
|
@ -238,6 +238,16 @@ export interface FeatureKibanaPrivileges {
|
|||
* ```
|
||||
*/
|
||||
reopenCase?: readonly string[];
|
||||
/**
|
||||
* List of case owners whose users should have assignCase access when granted this privilege.
|
||||
* @example
|
||||
* ```ts
|
||||
* {
|
||||
* assign: ['securitySolution']
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
assign?: readonly string[];
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -558,6 +558,7 @@ Array [
|
|||
],
|
||||
"cases": Object {
|
||||
"all": Array [],
|
||||
"assign": Array [],
|
||||
"create": Array [],
|
||||
"createComment": Array [],
|
||||
"delete": Array [],
|
||||
|
@ -722,6 +723,7 @@ Array [
|
|||
],
|
||||
"cases": Object {
|
||||
"all": Array [],
|
||||
"assign": Array [],
|
||||
"create": Array [],
|
||||
"createComment": Array [],
|
||||
"delete": Array [],
|
||||
|
@ -1075,6 +1077,7 @@ Array [
|
|||
],
|
||||
"cases": Object {
|
||||
"all": Array [],
|
||||
"assign": Array [],
|
||||
"create": Array [],
|
||||
"createComment": Array [],
|
||||
"delete": Array [],
|
||||
|
@ -1222,6 +1225,7 @@ Array [
|
|||
],
|
||||
"cases": Object {
|
||||
"all": Array [],
|
||||
"assign": Array [],
|
||||
"create": Array [],
|
||||
"createComment": Array [],
|
||||
"delete": Array [],
|
||||
|
@ -1386,6 +1390,7 @@ Array [
|
|||
],
|
||||
"cases": Object {
|
||||
"all": Array [],
|
||||
"assign": Array [],
|
||||
"create": Array [],
|
||||
"createComment": Array [],
|
||||
"delete": Array [],
|
||||
|
@ -1739,6 +1744,7 @@ Array [
|
|||
],
|
||||
"cases": Object {
|
||||
"all": Array [],
|
||||
"assign": Array [],
|
||||
"create": Array [],
|
||||
"createComment": Array [],
|
||||
"delete": Array [],
|
||||
|
|
|
@ -80,6 +80,7 @@ describe('featurePrivilegeIterator', () => {
|
|||
settings: ['cases-settings-type'],
|
||||
createComment: ['cases-create-comment-type'],
|
||||
reopenCase: ['cases-reopen-type'],
|
||||
assign: ['cases-assign-type'],
|
||||
},
|
||||
ui: ['ui-action'],
|
||||
},
|
||||
|
@ -152,6 +153,7 @@ describe('featurePrivilegeIterator', () => {
|
|||
settings: ['cases-settings-type'],
|
||||
createComment: ['cases-create-comment-type'],
|
||||
reopenCase: ['cases-reopen-type'],
|
||||
assign: ['cases-assign-type'],
|
||||
},
|
||||
ui: ['ui-action'],
|
||||
},
|
||||
|
@ -223,6 +225,7 @@ describe('featurePrivilegeIterator', () => {
|
|||
settings: ['cases-settings-type'],
|
||||
createComment: ['cases-create-comment-type'],
|
||||
reopenCase: ['cases-reopen-type'],
|
||||
assign: ['cases-assign-type'],
|
||||
},
|
||||
ui: ['ui-action'],
|
||||
},
|
||||
|
@ -296,6 +299,7 @@ describe('featurePrivilegeIterator', () => {
|
|||
settings: ['cases-settings-type'],
|
||||
createComment: ['cases-create-comment-type'],
|
||||
reopenCase: ['cases-reopen-type'],
|
||||
assign: ['cases-assign-type'],
|
||||
},
|
||||
ui: ['ui-action'],
|
||||
},
|
||||
|
@ -339,6 +343,7 @@ describe('featurePrivilegeIterator', () => {
|
|||
settings: ['cases-settings-type'],
|
||||
createComment: ['cases-create-comment-type'],
|
||||
reopenCase: ['cases-reopen-type'],
|
||||
assign: ['cases-assign-type'],
|
||||
},
|
||||
ui: ['ui-action'],
|
||||
},
|
||||
|
@ -403,6 +408,7 @@ describe('featurePrivilegeIterator', () => {
|
|||
settings: ['cases-settings-sub-type'],
|
||||
createComment: ['cases-create-comment-type'],
|
||||
reopenCase: ['cases-reopen-type'],
|
||||
assign: ['cases-assign-type'],
|
||||
},
|
||||
ui: ['ui-sub-type'],
|
||||
},
|
||||
|
@ -452,6 +458,7 @@ describe('featurePrivilegeIterator', () => {
|
|||
settings: ['cases-settings-type'],
|
||||
createComment: ['cases-create-comment-type'],
|
||||
reopenCase: ['cases-reopen-type'],
|
||||
assign: ['cases-assign-type'],
|
||||
},
|
||||
ui: ['ui-action'],
|
||||
},
|
||||
|
@ -522,6 +529,7 @@ describe('featurePrivilegeIterator', () => {
|
|||
settings: ['cases-settings-type'],
|
||||
createComment: ['cases-create-comment-type'],
|
||||
reopenCase: ['cases-reopen-type'],
|
||||
assign: ['cases-assign-type'],
|
||||
},
|
||||
ui: ['ui-action'],
|
||||
},
|
||||
|
@ -586,6 +594,7 @@ describe('featurePrivilegeIterator', () => {
|
|||
settings: ['cases-settings-sub-type'],
|
||||
createComment: ['cases-create-comment-type'],
|
||||
reopenCase: ['cases-reopen-type'],
|
||||
assign: ['cases-assign-type'],
|
||||
},
|
||||
ui: ['ui-sub-type'],
|
||||
},
|
||||
|
@ -635,6 +644,7 @@ describe('featurePrivilegeIterator', () => {
|
|||
settings: ['cases-settings-type'],
|
||||
createComment: ['cases-create-comment-type'],
|
||||
reopenCase: ['cases-reopen-type'],
|
||||
assign: ['cases-assign-type'],
|
||||
},
|
||||
ui: ['ui-action'],
|
||||
},
|
||||
|
@ -705,6 +715,7 @@ describe('featurePrivilegeIterator', () => {
|
|||
settings: ['cases-settings-type'],
|
||||
createComment: ['cases-create-comment-type'],
|
||||
reopenCase: ['cases-reopen-type'],
|
||||
assign: ['cases-assign-type'],
|
||||
},
|
||||
ui: ['ui-action'],
|
||||
},
|
||||
|
@ -770,6 +781,7 @@ describe('featurePrivilegeIterator', () => {
|
|||
settings: ['cases-settings-sub-type'],
|
||||
createComment: ['cases-create-comment-sub-type'],
|
||||
reopenCase: ['cases-reopen-sub-type'],
|
||||
assign: ['cases-assign-sub-type'],
|
||||
},
|
||||
ui: ['ui-sub-type'],
|
||||
},
|
||||
|
@ -822,6 +834,7 @@ describe('featurePrivilegeIterator', () => {
|
|||
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'],
|
||||
assign: ['cases-assign-type', 'cases-assign-sub-type'],
|
||||
},
|
||||
ui: ['ui-action', 'ui-sub-type'],
|
||||
},
|
||||
|
@ -860,6 +873,7 @@ describe('featurePrivilegeIterator', () => {
|
|||
settings: ['cases-settings-sub-type'],
|
||||
createComment: ['cases-create-comment-sub-type'],
|
||||
reopenCase: ['cases-reopen-sub-type'],
|
||||
assign: ['cases-assign-sub-type'],
|
||||
},
|
||||
ui: ['ui-action', 'ui-sub-type'],
|
||||
},
|
||||
|
@ -905,6 +919,7 @@ describe('featurePrivilegeIterator', () => {
|
|||
settings: ['cases-settings-type'],
|
||||
createComment: ['cases-create-comment-type'],
|
||||
reopenCase: ['cases-reopen-type'],
|
||||
assign: ['cases-assign-type'],
|
||||
},
|
||||
ui: ['ui-action'],
|
||||
},
|
||||
|
@ -1012,6 +1027,7 @@ describe('featurePrivilegeIterator', () => {
|
|||
settings: ['cases-settings-type'],
|
||||
createComment: ['cases-create-comment-type'],
|
||||
reopenCase: ['cases-reopen-type'],
|
||||
assign: ['cases-assign-type'],
|
||||
},
|
||||
ui: ['ui-action'],
|
||||
},
|
||||
|
@ -1049,6 +1065,7 @@ describe('featurePrivilegeIterator', () => {
|
|||
settings: [],
|
||||
createComment: [],
|
||||
reopenCase: [],
|
||||
assign: [],
|
||||
},
|
||||
ui: ['ui-action'],
|
||||
},
|
||||
|
@ -1092,6 +1109,7 @@ describe('featurePrivilegeIterator', () => {
|
|||
settings: ['cases-settings-type'],
|
||||
createComment: ['cases-create-comment-type'],
|
||||
reopenCase: ['cases-reopen-type'],
|
||||
assign: ['cases-assign-type'],
|
||||
},
|
||||
ui: ['ui-action'],
|
||||
},
|
||||
|
@ -1157,6 +1175,7 @@ describe('featurePrivilegeIterator', () => {
|
|||
settings: ['cases-settings-sub-type'],
|
||||
createComment: ['cases-create-comment-sub-type'],
|
||||
reopenCase: ['cases-reopen-sub-type'],
|
||||
assign: ['cases-assign-sub-type'],
|
||||
},
|
||||
ui: ['ui-sub-type'],
|
||||
},
|
||||
|
@ -1209,6 +1228,7 @@ describe('featurePrivilegeIterator', () => {
|
|||
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'],
|
||||
assign: ['cases-assign-type', 'cases-assign-sub-type'],
|
||||
},
|
||||
ui: ['ui-action', 'ui-sub-type'],
|
||||
},
|
||||
|
@ -1456,6 +1476,7 @@ describe('featurePrivilegeIterator', () => {
|
|||
settings: ['cases-settings-sub-type'],
|
||||
createComment: ['cases-create-comment-sub-type'],
|
||||
reopenCase: ['cases-reopen-sub-type'],
|
||||
assign: [],
|
||||
},
|
||||
ui: ['ui-sub-type'],
|
||||
},
|
||||
|
@ -1494,6 +1515,7 @@ describe('featurePrivilegeIterator', () => {
|
|||
settings: ['cases-settings-sub-type'],
|
||||
createComment: ['cases-create-comment-sub-type'],
|
||||
reopenCase: ['cases-reopen-sub-type'],
|
||||
assign: [],
|
||||
},
|
||||
ui: ['ui-sub-type'],
|
||||
},
|
||||
|
@ -1630,6 +1652,7 @@ describe('featurePrivilegeIterator', () => {
|
|||
settings: ['cases-settings-type'],
|
||||
createComment: ['cases-create-comment-type'],
|
||||
reopenCase: ['cases-reopen-type'],
|
||||
assign: [],
|
||||
},
|
||||
ui: ['ui-action'],
|
||||
},
|
||||
|
@ -1667,6 +1690,7 @@ describe('featurePrivilegeIterator', () => {
|
|||
settings: [],
|
||||
createComment: [],
|
||||
reopenCase: [],
|
||||
assign: [],
|
||||
},
|
||||
ui: ['ui-action'],
|
||||
},
|
||||
|
|
|
@ -180,6 +180,10 @@ function mergeWithSubFeatures(
|
|||
mergedConfig.cases?.reopenCase ?? [],
|
||||
subFeaturePrivilege.cases?.reopenCase ?? []
|
||||
),
|
||||
assign: mergeArrays(
|
||||
mergedConfig.cases?.assign ?? [],
|
||||
subFeaturePrivilege.cases?.assign ?? []
|
||||
),
|
||||
};
|
||||
}
|
||||
return mergedConfig;
|
||||
|
|
|
@ -92,6 +92,7 @@ const casesSchemaObject = schema.maybe(
|
|||
settings: schema.maybe(casesSchema),
|
||||
createComment: schema.maybe(casesSchema),
|
||||
reopenCase: schema.maybe(casesSchema),
|
||||
assign: schema.maybe(casesSchema),
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
@ -122,6 +122,7 @@ describe('AddToCaseAction', function () {
|
|||
settings: false,
|
||||
createComment: false,
|
||||
reopenCase: false,
|
||||
assign: false,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
|
|
@ -66,6 +66,7 @@ export {
|
|||
/** @deprecated deprecated in 8.17. Please use casesFeatureIdV2 instead */
|
||||
export const casesFeatureId = 'observabilityCases';
|
||||
export const casesFeatureIdV2 = 'observabilityCasesV2';
|
||||
export const casesFeatureIdV3 = 'observabilityCasesV3';
|
||||
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
|
||||
|
|
|
@ -30,6 +30,7 @@ const defaultProps: CasesProps = {
|
|||
settings: true,
|
||||
reopenCase: true,
|
||||
createComment: true,
|
||||
assign: true,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -49,5 +50,6 @@ CasesPageWithNoPermissions.args = {
|
|||
settings: false,
|
||||
reopenCase: false,
|
||||
createComment: false,
|
||||
assign: false,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -10,7 +10,7 @@ import { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/s
|
|||
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';
|
||||
import { casesFeatureId, casesFeatureIdV3, observabilityFeatureId } from '../../common';
|
||||
|
||||
export const getCasesFeature = (
|
||||
casesCapabilities: CasesUiCapabilities,
|
||||
|
@ -22,10 +22,10 @@ export const getCasesFeature = (
|
|||
'xpack.observability.featureRegistry.linkObservabilityTitle.deprecationMessage',
|
||||
{
|
||||
defaultMessage:
|
||||
'The {currentId} permissions are deprecated, please see {casesFeatureIdV2}.',
|
||||
'The {currentId} permissions are deprecated, please see {casesFeatureIdV3}.',
|
||||
values: {
|
||||
currentId: casesFeatureId,
|
||||
casesFeatureIdV2,
|
||||
casesFeatureIdV3,
|
||||
},
|
||||
}
|
||||
),
|
||||
|
@ -52,18 +52,24 @@ export const getCasesFeature = (
|
|||
push: [observabilityFeatureId],
|
||||
createComment: [observabilityFeatureId],
|
||||
reopenCase: [observabilityFeatureId],
|
||||
assign: [observabilityFeatureId],
|
||||
},
|
||||
savedObject: {
|
||||
all: [...filesSavedObjectTypes],
|
||||
read: [...filesSavedObjectTypes],
|
||||
},
|
||||
ui: casesCapabilities.all,
|
||||
ui: [
|
||||
...casesCapabilities.all,
|
||||
...casesCapabilities.createComment,
|
||||
...casesCapabilities.reopenCase,
|
||||
...casesCapabilities.assignCase,
|
||||
],
|
||||
replacedBy: {
|
||||
default: [{ feature: casesFeatureIdV2, privileges: ['all'] }],
|
||||
default: [{ feature: casesFeatureIdV3, privileges: ['all'] }],
|
||||
minimal: [
|
||||
{
|
||||
feature: casesFeatureIdV2,
|
||||
privileges: ['minimal_all', 'create_comment', 'case_reopen'],
|
||||
feature: casesFeatureIdV3,
|
||||
privileges: ['minimal_all', 'create_comment', 'case_reopen', 'cases_assign'],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -81,8 +87,8 @@ export const getCasesFeature = (
|
|||
},
|
||||
ui: casesCapabilities.read,
|
||||
replacedBy: {
|
||||
default: [{ feature: casesFeatureIdV2, privileges: ['read'] }],
|
||||
minimal: [{ feature: casesFeatureIdV2, privileges: ['minimal_read'] }],
|
||||
default: [{ feature: casesFeatureIdV3, privileges: ['read'] }],
|
||||
minimal: [{ feature: casesFeatureIdV3, privileges: ['minimal_read'] }],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -110,7 +116,7 @@ export const getCasesFeature = (
|
|||
delete: [observabilityFeatureId],
|
||||
},
|
||||
ui: casesCapabilities.delete,
|
||||
replacedBy: [{ feature: casesFeatureIdV2, privileges: ['cases_delete'] }],
|
||||
replacedBy: [{ feature: casesFeatureIdV3, privileges: ['cases_delete'] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -141,7 +147,7 @@ export const getCasesFeature = (
|
|||
settings: [observabilityFeatureId],
|
||||
},
|
||||
ui: casesCapabilities.settings,
|
||||
replacedBy: [{ feature: casesFeatureIdV2, privileges: ['cases_settings'] }],
|
||||
replacedBy: [{ feature: casesFeatureIdV3, privileges: ['cases_settings'] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -10,12 +10,26 @@ import { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/s
|
|||
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';
|
||||
import {
|
||||
casesFeatureIdV2,
|
||||
casesFeatureId,
|
||||
observabilityFeatureId,
|
||||
casesFeatureIdV3,
|
||||
} from '../../common';
|
||||
|
||||
export const getCasesFeatureV2 = (
|
||||
casesCapabilities: CasesUiCapabilities,
|
||||
casesApiTags: CasesApiTags
|
||||
): KibanaFeatureConfig => ({
|
||||
deprecated: {
|
||||
notice: i18n.translate('xpack.observability.featureRegistry.casesFeature.deprecationMessage', {
|
||||
defaultMessage: 'The {currentId} permissions are deprecated, please see {casesFeatureIdV3}.',
|
||||
values: {
|
||||
currentId: casesFeatureIdV2,
|
||||
casesFeatureIdV3,
|
||||
},
|
||||
}),
|
||||
},
|
||||
id: casesFeatureIdV2,
|
||||
name: i18n.translate('xpack.observability.featureRegistry.linkObservabilityTitle', {
|
||||
defaultMessage: 'Cases',
|
||||
|
@ -36,12 +50,22 @@ export const getCasesFeatureV2 = (
|
|||
read: [observabilityFeatureId],
|
||||
update: [observabilityFeatureId],
|
||||
push: [observabilityFeatureId],
|
||||
assign: [observabilityFeatureId],
|
||||
},
|
||||
savedObject: {
|
||||
all: [...filesSavedObjectTypes],
|
||||
read: [...filesSavedObjectTypes],
|
||||
},
|
||||
ui: casesCapabilities.all,
|
||||
ui: [...casesCapabilities.all, ...casesCapabilities.assignCase],
|
||||
replacedBy: {
|
||||
default: [{ feature: casesFeatureIdV3, privileges: ['all'] }],
|
||||
minimal: [
|
||||
{
|
||||
feature: casesFeatureIdV3,
|
||||
privileges: ['minimal_all', 'cases_assign'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
read: {
|
||||
api: casesApiTags.read,
|
||||
|
@ -55,6 +79,10 @@ export const getCasesFeatureV2 = (
|
|||
read: [...filesSavedObjectTypes],
|
||||
},
|
||||
ui: casesCapabilities.read,
|
||||
replacedBy: {
|
||||
default: [{ feature: casesFeatureIdV3, privileges: ['read'] }],
|
||||
minimal: [{ feature: casesFeatureIdV3, privileges: ['minimal_read'] }],
|
||||
},
|
||||
},
|
||||
},
|
||||
subFeatures: [
|
||||
|
@ -81,6 +109,7 @@ export const getCasesFeatureV2 = (
|
|||
delete: [observabilityFeatureId],
|
||||
},
|
||||
ui: casesCapabilities.delete,
|
||||
replacedBy: [{ feature: casesFeatureIdV3, privileges: ['cases_delete'] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -111,6 +140,7 @@ export const getCasesFeatureV2 = (
|
|||
settings: [observabilityFeatureId],
|
||||
},
|
||||
ui: casesCapabilities.settings,
|
||||
replacedBy: [{ feature: casesFeatureIdV3, privileges: ['cases_settings'] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -142,6 +172,7 @@ export const getCasesFeatureV2 = (
|
|||
createComment: [observabilityFeatureId],
|
||||
},
|
||||
ui: casesCapabilities.createComment,
|
||||
replacedBy: [{ feature: casesFeatureIdV3, privileges: ['create_comment'] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -172,6 +203,7 @@ export const getCasesFeatureV2 = (
|
|||
reopenCase: [observabilityFeatureId],
|
||||
},
|
||||
ui: casesCapabilities.reopenCase,
|
||||
replacedBy: [{ feature: casesFeatureIdV3, privileges: ['case_reopen'] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -0,0 +1,208 @@
|
|||
/*
|
||||
* 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 { casesFeatureIdV3, casesFeatureId, observabilityFeatureId } from '../../common';
|
||||
|
||||
export const getCasesFeatureV3 = (
|
||||
casesCapabilities: CasesUiCapabilities,
|
||||
casesApiTags: CasesApiTags
|
||||
): KibanaFeatureConfig => ({
|
||||
id: casesFeatureIdV3,
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.observability.features.assignUsersSubFeatureName', {
|
||||
defaultMessage: 'Assign users',
|
||||
}),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
id: 'cases_assign',
|
||||
name: i18n.translate('xpack.observability.features.assignUsersSubFeatureName', {
|
||||
defaultMessage: 'Assign users to cases',
|
||||
}),
|
||||
includeIn: 'all',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
cases: {
|
||||
assign: [observabilityFeatureId],
|
||||
},
|
||||
ui: casesCapabilities.assignCase,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
|
@ -51,6 +51,7 @@ import { uiSettings } from './ui_settings';
|
|||
import { OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES } from '../common/constants';
|
||||
import { getCasesFeature } from './features/cases_v1';
|
||||
import { getCasesFeatureV2 } from './features/cases_v2';
|
||||
import { getCasesFeatureV3 } from './features/cases_v3';
|
||||
|
||||
export type ObservabilityPluginSetup = ReturnType<ObservabilityPlugin['setup']>;
|
||||
|
||||
|
@ -103,6 +104,7 @@ export class ObservabilityPlugin
|
|||
|
||||
plugins.features.registerKibanaFeature(getCasesFeature(casesCapabilities, casesApiTags));
|
||||
plugins.features.registerKibanaFeature(getCasesFeatureV2(casesCapabilities, casesApiTags));
|
||||
plugins.features.registerKibanaFeature(getCasesFeatureV3(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 = 'observabilityCasesV2';
|
||||
export const casesFeatureId = 'observabilityCasesV3';
|
||||
export const sloFeatureId = 'slo';
|
||||
|
||||
// SLO alerts table in slo detail page
|
||||
|
|
|
@ -16,6 +16,7 @@ export const noCasesPermissions = () => ({
|
|||
settings: false,
|
||||
createComment: false,
|
||||
reopenCase: false,
|
||||
assign: false,
|
||||
});
|
||||
|
||||
export const allCasesPermissions = () => ({
|
||||
|
@ -29,4 +30,5 @@ export const allCasesPermissions = () => ({
|
|||
settings: true,
|
||||
createComment: true,
|
||||
reopenCase: true,
|
||||
assign: true,
|
||||
});
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { getCasesFeature, getCasesV2Feature, getCasesV3Feature } from './src/cases';
|
||||
export { getSecurityFeature, getSecurityV2Feature } from './src/security';
|
||||
export { getCasesFeature, getCasesV2Feature } from './src/cases';
|
||||
export { getAssistantFeature } from './src/assistant';
|
||||
export { getAttackDiscoveryFeature } from './src/attack_discovery';
|
||||
export { getTimelineFeature } from './src/timeline';
|
||||
|
|
|
@ -17,6 +17,11 @@ import {
|
|||
getCasesBaseKibanaSubFeatureIdsV2,
|
||||
getCasesSubFeaturesMapV2,
|
||||
} from './v2_features/kibana_sub_features';
|
||||
import { getCasesBaseKibanaFeatureV3 } from './v3_features/kibana_features';
|
||||
import {
|
||||
getCasesBaseKibanaSubFeatureIdsV3,
|
||||
getCasesSubFeaturesMapV3,
|
||||
} from './v3_features/kibana_sub_features';
|
||||
|
||||
/**
|
||||
* @deprecated Use getCasesV2Feature instead
|
||||
|
@ -36,3 +41,11 @@ export const getCasesV2Feature = (
|
|||
baseKibanaSubFeatureIds: getCasesBaseKibanaSubFeatureIdsV2(),
|
||||
subFeaturesMap: getCasesSubFeaturesMapV2(params),
|
||||
});
|
||||
|
||||
export const getCasesV3Feature = (
|
||||
params: CasesFeatureParams
|
||||
): ProductFeatureParams<CasesSubFeatureId> => ({
|
||||
baseKibanaFeature: getCasesBaseKibanaFeatureV3(params),
|
||||
baseKibanaSubFeatureIds: getCasesBaseKibanaSubFeatureIdsV3(),
|
||||
subFeaturesMap: getCasesSubFeaturesMapV3(params),
|
||||
});
|
||||
|
|
|
@ -10,7 +10,7 @@ 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 { APP_ID, CASES_FEATURE_ID, CASES_FEATURE_ID_V3 } from '../../constants';
|
||||
import type { CasesFeatureParams } from '../types';
|
||||
|
||||
/**
|
||||
|
@ -30,7 +30,7 @@ export const getCasesBaseKibanaFeature = ({
|
|||
'The {currentId} permissions are deprecated, please see {casesFeatureIdV2}.',
|
||||
values: {
|
||||
currentId: CASES_FEATURE_ID,
|
||||
casesFeatureIdV2: CASES_FEATURE_ID_V2,
|
||||
casesFeatureIdV2: CASES_FEATURE_ID_V3,
|
||||
},
|
||||
}
|
||||
),
|
||||
|
@ -60,18 +60,24 @@ export const getCasesBaseKibanaFeature = ({
|
|||
push: [APP_ID],
|
||||
createComment: [APP_ID],
|
||||
reopenCase: [APP_ID],
|
||||
assign: [APP_ID],
|
||||
},
|
||||
savedObject: {
|
||||
all: [...savedObjects.files],
|
||||
read: [...savedObjects.files],
|
||||
},
|
||||
ui: uiCapabilities.all,
|
||||
ui: [
|
||||
...uiCapabilities.all,
|
||||
...uiCapabilities.createComment,
|
||||
...uiCapabilities.reopenCase,
|
||||
...uiCapabilities.assignCase,
|
||||
],
|
||||
replacedBy: {
|
||||
default: [{ feature: CASES_FEATURE_ID_V2, privileges: ['all'] }],
|
||||
default: [{ feature: CASES_FEATURE_ID_V3, privileges: ['all'] }],
|
||||
minimal: [
|
||||
{
|
||||
feature: CASES_FEATURE_ID_V2,
|
||||
privileges: ['minimal_all', 'create_comment', 'case_reopen'],
|
||||
feature: CASES_FEATURE_ID_V3,
|
||||
privileges: ['minimal_all', 'create_comment', 'case_reopen', 'cases_assign'],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -89,8 +95,8 @@ export const getCasesBaseKibanaFeature = ({
|
|||
},
|
||||
ui: uiCapabilities.read,
|
||||
replacedBy: {
|
||||
default: [{ feature: CASES_FEATURE_ID_V2, privileges: ['read'] }],
|
||||
minimal: [{ feature: CASES_FEATURE_ID_V2, privileges: ['minimal_read'] }],
|
||||
default: [{ feature: CASES_FEATURE_ID_V3, privileges: ['read'] }],
|
||||
minimal: [{ feature: CASES_FEATURE_ID_V3, privileges: ['minimal_read'] }],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import type { SubFeatureConfig } from '@kbn/features-plugin/common';
|
||||
import { CasesSubFeatureId } from '../../product_features_keys';
|
||||
import { APP_ID, CASES_FEATURE_ID_V2 } from '../../constants';
|
||||
import { APP_ID, CASES_FEATURE_ID_V3 } from '../../constants';
|
||||
import type { CasesFeatureParams } from '../types';
|
||||
|
||||
/**
|
||||
|
@ -56,7 +56,7 @@ export const getCasesSubFeaturesMap = ({
|
|||
delete: [APP_ID],
|
||||
},
|
||||
ui: uiCapabilities.delete,
|
||||
replacedBy: [{ feature: CASES_FEATURE_ID_V2, privileges: ['cases_delete'] }],
|
||||
replacedBy: [{ feature: CASES_FEATURE_ID_V3, privileges: ['cases_delete'] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -91,7 +91,7 @@ export const getCasesSubFeaturesMap = ({
|
|||
settings: [APP_ID],
|
||||
},
|
||||
ui: uiCapabilities.settings,
|
||||
replacedBy: [{ feature: CASES_FEATURE_ID_V2, privileges: ['cases_settings'] }],
|
||||
replacedBy: [{ feature: CASES_FEATURE_ID_V3, privileges: ['cases_settings'] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -10,7 +10,12 @@ 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_V2, CASES_FEATURE_ID } from '../../constants';
|
||||
import {
|
||||
APP_ID,
|
||||
CASES_FEATURE_ID_V2,
|
||||
CASES_FEATURE_ID,
|
||||
CASES_FEATURE_ID_V3,
|
||||
} from '../../constants';
|
||||
import type { CasesFeatureParams } from '../types';
|
||||
|
||||
export const getCasesBaseKibanaFeatureV2 = ({
|
||||
|
@ -19,6 +24,19 @@ export const getCasesBaseKibanaFeatureV2 = ({
|
|||
savedObjects,
|
||||
}: CasesFeatureParams): BaseKibanaFeatureConfig => {
|
||||
return {
|
||||
deprecated: {
|
||||
notice: i18n.translate(
|
||||
'securitySolutionPackages.features.featureRegistry.casesFeature.deprecationMessage',
|
||||
{
|
||||
defaultMessage:
|
||||
'The {currentId} permissions are deprecated, please see {casesFeatureIdV3}.',
|
||||
values: {
|
||||
currentId: CASES_FEATURE_ID_V2,
|
||||
casesFeatureIdV3: CASES_FEATURE_ID_V3,
|
||||
},
|
||||
}
|
||||
),
|
||||
},
|
||||
id: CASES_FEATURE_ID_V2,
|
||||
name: i18n.translate(
|
||||
'securitySolutionPackages.features.featureRegistry.linkSecuritySolutionCaseTitle',
|
||||
|
@ -42,12 +60,22 @@ export const getCasesBaseKibanaFeatureV2 = ({
|
|||
read: [APP_ID],
|
||||
update: [APP_ID],
|
||||
push: [APP_ID],
|
||||
assign: [APP_ID],
|
||||
},
|
||||
savedObject: {
|
||||
all: [...savedObjects.files],
|
||||
read: [...savedObjects.files],
|
||||
},
|
||||
ui: uiCapabilities.all,
|
||||
ui: [...uiCapabilities.all, ...uiCapabilities.assignCase],
|
||||
replacedBy: {
|
||||
default: [{ feature: CASES_FEATURE_ID_V3, privileges: ['all'] }],
|
||||
minimal: [
|
||||
{
|
||||
feature: CASES_FEATURE_ID_V3,
|
||||
privileges: ['minimal_all', 'cases_assign'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
read: {
|
||||
api: apiTags.read,
|
||||
|
@ -61,6 +89,10 @@ export const getCasesBaseKibanaFeatureV2 = ({
|
|||
read: [...savedObjects.files],
|
||||
},
|
||||
ui: uiCapabilities.read,
|
||||
replacedBy: {
|
||||
default: [{ feature: CASES_FEATURE_ID_V3, privileges: ['read'] }],
|
||||
minimal: [{ feature: CASES_FEATURE_ID_V3, privileges: ['minimal_read'] }],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
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 { APP_ID, CASES_FEATURE_ID_V3 } from '../../constants';
|
||||
import type { CasesFeatureParams } from '../types';
|
||||
|
||||
/**
|
||||
|
@ -57,6 +57,7 @@ export const getCasesSubFeaturesMapV2 = ({
|
|||
delete: [APP_ID],
|
||||
},
|
||||
ui: uiCapabilities.delete,
|
||||
replacedBy: [{ feature: CASES_FEATURE_ID_V3, privileges: ['cases_delete'] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -91,6 +92,7 @@ export const getCasesSubFeaturesMapV2 = ({
|
|||
settings: [APP_ID],
|
||||
},
|
||||
ui: uiCapabilities.settings,
|
||||
replacedBy: [{ feature: CASES_FEATURE_ID_V3, privileges: ['cases_settings'] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -128,6 +130,7 @@ export const getCasesSubFeaturesMapV2 = ({
|
|||
createComment: [APP_ID],
|
||||
},
|
||||
ui: uiCapabilities.createComment,
|
||||
replacedBy: [{ feature: CASES_FEATURE_ID_V3, privileges: ['create_comment'] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -161,6 +164,7 @@ export const getCasesSubFeaturesMapV2 = ({
|
|||
reopenCase: [APP_ID],
|
||||
},
|
||||
ui: uiCapabilities.reopenCase,
|
||||
replacedBy: [{ feature: CASES_FEATURE_ID_V3, privileges: ['case_reopen'] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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_V3, CASES_FEATURE_ID } from '../../constants';
|
||||
import type { CasesFeatureParams } from '../types';
|
||||
|
||||
export const getCasesBaseKibanaFeatureV3 = ({
|
||||
uiCapabilities,
|
||||
apiTags,
|
||||
savedObjects,
|
||||
}: CasesFeatureParams): BaseKibanaFeatureConfig => {
|
||||
return {
|
||||
id: CASES_FEATURE_ID_V3,
|
||||
name: i18n.translate(
|
||||
'securitySolutionPackages.features.featureRegistry.linkSecuritySolutionCaseTitle',
|
||||
{
|
||||
defaultMessage: 'Cases',
|
||||
}
|
||||
),
|
||||
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,
|
||||
app: [CASES_FEATURE_ID, 'kibana'],
|
||||
catalogue: [APP_ID],
|
||||
cases: {
|
||||
create: [APP_ID],
|
||||
read: [APP_ID],
|
||||
update: [APP_ID],
|
||||
push: [APP_ID],
|
||||
},
|
||||
savedObject: {
|
||||
all: [...savedObjects.files],
|
||||
read: [...savedObjects.files],
|
||||
},
|
||||
ui: uiCapabilities.all,
|
||||
},
|
||||
read: {
|
||||
api: apiTags.read,
|
||||
app: [CASES_FEATURE_ID, 'kibana'],
|
||||
catalogue: [APP_ID],
|
||||
cases: {
|
||||
read: [APP_ID],
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [...savedObjects.files],
|
||||
},
|
||||
ui: uiCapabilities.read,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
|
@ -0,0 +1,204 @@
|
|||
/*
|
||||
* 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 getCasesBaseKibanaSubFeatureIdsV3 = (): CasesSubFeatureId[] => [
|
||||
CasesSubFeatureId.deleteCases,
|
||||
CasesSubFeatureId.casesSettings,
|
||||
CasesSubFeatureId.createComment,
|
||||
CasesSubFeatureId.reopenCase,
|
||||
CasesSubFeatureId.assignUsers,
|
||||
];
|
||||
|
||||
/**
|
||||
* Defines all the Security Solution Cases subFeatures available.
|
||||
* The order of the subFeatures is the order they will be displayed
|
||||
*/
|
||||
export const getCasesSubFeaturesMapV3 = ({
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const casesAssignUsersCasesSubFeature: SubFeatureConfig = {
|
||||
name: i18n.translate('securitySolutionPackages.features.assignUsersSubFeatureName', {
|
||||
defaultMessage: 'Assign users',
|
||||
}),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
id: 'cases_assign',
|
||||
name: i18n.translate('securitySolutionPackages.features.assignUsersSubFeatureName', {
|
||||
defaultMessage: 'Assign users to cases',
|
||||
}),
|
||||
includeIn: 'all',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
cases: {
|
||||
assign: [APP_ID],
|
||||
},
|
||||
ui: uiCapabilities.assignCase,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return new Map<CasesSubFeatureId, SubFeatureConfig>([
|
||||
[CasesSubFeatureId.deleteCases, deleteCasesSubFeature],
|
||||
[CasesSubFeatureId.casesSettings, casesSettingsCasesSubFeature],
|
||||
[CasesSubFeatureId.createComment, casesAddCommentsCasesSubFeature],
|
||||
[CasesSubFeatureId.reopenCase, casesreopenCaseSubFeature],
|
||||
[CasesSubFeatureId.assignUsers, casesAssignUsersCasesSubFeature],
|
||||
]);
|
||||
};
|
|
@ -20,6 +20,9 @@ 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;
|
||||
|
||||
// New version created in 8.18 for case assignees
|
||||
export const CASES_FEATURE_ID_V3 = 'securitySolutionCasesV3' as const;
|
||||
|
||||
export const SECURITY_SOLUTION_CASES_APP_ID = 'securitySolutionCases' as const;
|
||||
|
||||
export const ASSISTANT_FEATURE_ID = 'securitySolutionAssistant' as const;
|
||||
|
|
|
@ -177,6 +177,7 @@ export enum CasesSubFeatureId {
|
|||
casesSettings = 'casesSettingsSubFeature',
|
||||
createComment = 'createCommentSubFeature',
|
||||
reopenCase = 'reopenCaseSubFeature',
|
||||
assignUsers = 'assignUsersSubFeature',
|
||||
}
|
||||
|
||||
/** Sub-features IDs for Security Assistant */
|
||||
|
|
|
@ -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 = 'securitySolutionCasesV2' as const;
|
||||
export const CASES_FEATURE_ID = 'securitySolutionCasesV3' as const;
|
||||
export const TIMELINE_FEATURE_ID = 'securitySolutionTimeline' as const;
|
||||
export const NOTES_FEATURE_ID = 'securitySolutionNotes' as const;
|
||||
export const SERVER_APP_ID = 'siem' as const;
|
||||
|
|
|
@ -17,6 +17,7 @@ export const noCasesCapabilities = (): CasesCapabilities => ({
|
|||
cases_settings: false,
|
||||
case_reopen: false,
|
||||
create_comment: false,
|
||||
cases_assign: false,
|
||||
});
|
||||
|
||||
export const readCasesCapabilities = (): CasesCapabilities => ({
|
||||
|
@ -29,6 +30,7 @@ export const readCasesCapabilities = (): CasesCapabilities => ({
|
|||
cases_settings: false,
|
||||
case_reopen: false,
|
||||
create_comment: false,
|
||||
cases_assign: false,
|
||||
});
|
||||
|
||||
export const allCasesCapabilities = (): CasesCapabilities => ({
|
||||
|
@ -41,6 +43,7 @@ export const allCasesCapabilities = (): CasesCapabilities => ({
|
|||
cases_settings: true,
|
||||
case_reopen: true,
|
||||
create_comment: true,
|
||||
cases_assign: true,
|
||||
});
|
||||
|
||||
export const noCasesPermissions = (): CasesPermissions => ({
|
||||
|
@ -54,6 +57,7 @@ export const noCasesPermissions = (): CasesPermissions => ({
|
|||
settings: false,
|
||||
reopenCase: false,
|
||||
createComment: false,
|
||||
assign: false,
|
||||
});
|
||||
|
||||
export const readCasesPermissions = (): CasesPermissions => ({
|
||||
|
@ -67,6 +71,7 @@ export const readCasesPermissions = (): CasesPermissions => ({
|
|||
settings: false,
|
||||
reopenCase: false,
|
||||
createComment: false,
|
||||
assign: false,
|
||||
});
|
||||
|
||||
export const writeCasesPermissions = (): CasesPermissions => ({
|
||||
|
@ -80,6 +85,7 @@ export const writeCasesPermissions = (): CasesPermissions => ({
|
|||
settings: true,
|
||||
reopenCase: true,
|
||||
createComment: true,
|
||||
assign: true,
|
||||
});
|
||||
|
||||
export const allCasesPermissions = (): CasesPermissions => ({
|
||||
|
@ -93,4 +99,5 @@ export const allCasesPermissions = (): CasesPermissions => ({
|
|||
settings: true,
|
||||
reopenCase: true,
|
||||
createComment: true,
|
||||
assign: true,
|
||||
});
|
||||
|
|
|
@ -55,7 +55,7 @@ export const getEndpointOperationsAnalyst: () => Omit<Role, 'name'> = () => {
|
|||
fleet: ['all'],
|
||||
fleetv2: ['all'],
|
||||
osquery: ['all'],
|
||||
securitySolutionCasesV2: ['all'],
|
||||
securitySolutionCasesV3: ['all'],
|
||||
builtinAlerts: ['all'],
|
||||
siemV2: [
|
||||
'all',
|
||||
|
|
|
@ -37,7 +37,7 @@ export const getNoResponseActionsRole: () => Omit<Role, 'name'> = () => ({
|
|||
advancedSettings: ['all'],
|
||||
dev_tools: ['all'],
|
||||
fleet: ['all'],
|
||||
generalCasesV2: ['all'],
|
||||
generalCasesV3: ['all'],
|
||||
indexPatterns: ['all'],
|
||||
osquery: ['all'],
|
||||
savedObjectsManagement: ['all'],
|
||||
|
|
|
@ -36,6 +36,11 @@ jest.mock('@kbn/security-solution-features/product_features', () => ({
|
|||
baseKibanaSubFeatureIds: [],
|
||||
subFeaturesMap: new Map(),
|
||||
})),
|
||||
getCasesV3Feature: jest.fn(() => ({
|
||||
baseKibanaFeature: {},
|
||||
baseKibanaSubFeatureIds: [],
|
||||
subFeaturesMap: new Map(),
|
||||
})),
|
||||
getAssistantFeature: jest.fn(() => ({
|
||||
baseKibanaFeature: {},
|
||||
baseKibanaSubFeatureIds: [],
|
||||
|
|
|
@ -45,6 +45,7 @@ jest.mock('@kbn/security-solution-features/product_features', () => ({
|
|||
getAssistantFeature: () => mockGetFeature(),
|
||||
getCasesFeature: () => mockGetFeature(),
|
||||
getCasesV2Feature: () => mockGetFeature(),
|
||||
getCasesV3Feature: () => mockGetFeature(),
|
||||
getSecurityFeature: () => mockGetFeature(),
|
||||
getSecurityV2Feature: () => mockGetFeature(),
|
||||
getTimelineFeature: () => mockGetFeature(),
|
||||
|
@ -60,8 +61,8 @@ describe('ProductFeaturesService', () => {
|
|||
const experimentalFeatures = {} as ExperimentalFeatures;
|
||||
new ProductFeaturesService(loggerMock.create(), experimentalFeatures);
|
||||
|
||||
expect(mockGetFeature).toHaveBeenCalledTimes(8);
|
||||
expect(MockedProductFeatures).toHaveBeenCalledTimes(8);
|
||||
expect(mockGetFeature).toHaveBeenCalledTimes(9);
|
||||
expect(MockedProductFeatures).toHaveBeenCalledTimes(9);
|
||||
});
|
||||
|
||||
it('should init all ProductFeatures when initialized', () => {
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
getCasesFeature,
|
||||
getSecurityFeature,
|
||||
getCasesV2Feature,
|
||||
getCasesV3Feature,
|
||||
getSecurityV2Feature,
|
||||
getTimelineFeature,
|
||||
getNotesFeature,
|
||||
|
@ -46,6 +47,7 @@ export class ProductFeaturesService {
|
|||
private securityV2ProductFeatures: ProductFeatures;
|
||||
private casesProductFeatures: ProductFeatures;
|
||||
private casesProductV2Features: ProductFeatures;
|
||||
private casesProductFeaturesV3: ProductFeatures;
|
||||
private securityAssistantProductFeatures: ProductFeatures;
|
||||
private attackDiscoveryProductFeatures: ProductFeatures;
|
||||
private timelineProductFeatures: ProductFeatures;
|
||||
|
@ -103,6 +105,18 @@ export class ProductFeaturesService {
|
|||
casesV2Feature.baseKibanaSubFeatureIds
|
||||
);
|
||||
|
||||
const casesV3Feature = getCasesV3Feature({
|
||||
uiCapabilities: casesUiCapabilities,
|
||||
apiTags: casesApiTags,
|
||||
savedObjects: { files: filesSavedObjectTypes },
|
||||
});
|
||||
this.casesProductFeaturesV3 = new ProductFeatures(
|
||||
this.logger,
|
||||
casesV3Feature.subFeaturesMap,
|
||||
casesV3Feature.baseKibanaFeature,
|
||||
casesV3Feature.baseKibanaSubFeatureIds
|
||||
);
|
||||
|
||||
const assistantFeature = getAssistantFeature(this.experimentalFeatures);
|
||||
this.securityAssistantProductFeatures = new ProductFeatures(
|
||||
this.logger,
|
||||
|
@ -148,6 +162,7 @@ export class ProductFeaturesService {
|
|||
this.securityV2ProductFeatures.init(featuresSetup);
|
||||
this.casesProductFeatures.init(featuresSetup);
|
||||
this.casesProductV2Features.init(featuresSetup);
|
||||
this.casesProductFeaturesV3.init(featuresSetup);
|
||||
this.securityAssistantProductFeatures.init(featuresSetup);
|
||||
this.attackDiscoveryProductFeatures.init(featuresSetup);
|
||||
this.timelineProductFeatures.init(featuresSetup);
|
||||
|
@ -162,6 +177,7 @@ export class ProductFeaturesService {
|
|||
const casesProductFeaturesConfig = configurator.cases();
|
||||
this.casesProductFeatures.setConfig(casesProductFeaturesConfig);
|
||||
this.casesProductV2Features.setConfig(casesProductFeaturesConfig);
|
||||
this.casesProductFeaturesV3.setConfig(casesProductFeaturesConfig);
|
||||
|
||||
const securityAssistantProductFeaturesConfig = configurator.securityAssistant();
|
||||
this.securityAssistantProductFeatures.setConfig(securityAssistantProductFeaturesConfig);
|
||||
|
|
|
@ -136,6 +136,81 @@ export const secCasesV2All: Role = {
|
|||
},
|
||||
};
|
||||
|
||||
export const secCasesV3All: Role = {
|
||||
name: 'sec_cases_v3_all_role_api_int',
|
||||
privileges: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['all'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
feature: {
|
||||
siem: ['all'],
|
||||
securitySolutionCasesV3: ['all'],
|
||||
actions: ['all'],
|
||||
actionsSimulators: ['all'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const secCasesV2NoReopenWithCreateComment: Role = {
|
||||
name: 'sec_cases_v2_no_reopen_role_api_int',
|
||||
privileges: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['all'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
feature: {
|
||||
siem: ['all'],
|
||||
securitySolutionCasesV2: ['read', 'update', 'create', 'cases_delete', 'create_comment'],
|
||||
actions: ['all'],
|
||||
actionsSimulators: ['all'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const secCasesV2NoCreateCommentWithReopen: Role = {
|
||||
name: 'sec_cases_v2_create_comment_no_reopen_role_api_int',
|
||||
privileges: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['all'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
feature: {
|
||||
siem: ['all'],
|
||||
securitySolutionCasesV2: ['read', 'update', 'create', 'delete', 'case_reopen'],
|
||||
actions: ['all'],
|
||||
actionsSimulators: ['all'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const secAllSpace1: Role = {
|
||||
name: 'sec_all_role_space1_api_int',
|
||||
privileges: {
|
||||
|
@ -434,6 +509,131 @@ export const casesV2All: Role = {
|
|||
},
|
||||
};
|
||||
|
||||
export const casesV3All: Role = {
|
||||
name: 'cases_v3_all_role_api_int',
|
||||
privileges: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['all'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: [],
|
||||
feature: {
|
||||
generalCasesV3: ['all'],
|
||||
actions: ['all'],
|
||||
actionsSimulators: ['all'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const casesV3NoAssignee: Role = {
|
||||
name: 'cases_v3_no_assignee_role_api_int',
|
||||
privileges: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['all'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: [],
|
||||
feature: {
|
||||
generalCasesV3: ['minimal_read', 'cases_delete', 'case_reopen', 'create_comment'],
|
||||
actions: ['all'],
|
||||
actionsSimulators: ['all'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const casesV3ReadAndAssignee: Role = {
|
||||
name: 'cases_v3_read_assignee_role_api_int',
|
||||
privileges: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['all'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: [],
|
||||
feature: {
|
||||
generalCasesV3: ['minimal_read', 'cases_assign'],
|
||||
actions: ['all'],
|
||||
actionsSimulators: ['all'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const casesV2NoReopenWithCreateComment: Role = {
|
||||
name: 'cases_v2_no_reopen_role_api_int',
|
||||
privileges: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['all'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: [],
|
||||
feature: {
|
||||
generalCasesV2: ['read', 'update', 'create', 'cases_delete', 'create_comment'],
|
||||
actions: ['all'],
|
||||
actionsSimulators: ['all'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const casesV2NoCreateCommentWithReopen: Role = {
|
||||
name: 'cases_v2_no_create_comment_role_api_int',
|
||||
privileges: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['all'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: [],
|
||||
feature: {
|
||||
generalCasesV2: ['read', 'update', 'create', 'cases_delete', 'case_reopen'],
|
||||
actions: ['all'],
|
||||
actionsSimulators: ['all'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const casesRead: Role = {
|
||||
name: 'cases_read_role_api_int',
|
||||
privileges: {
|
||||
|
@ -583,6 +783,87 @@ export const obsCasesV2All: Role = {
|
|||
},
|
||||
};
|
||||
|
||||
export const obsCasesV3All: Role = {
|
||||
name: 'obs_cases_v3_all_role_api_int',
|
||||
privileges: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['all'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: [],
|
||||
feature: {
|
||||
observabilityCasesV3: ['all'],
|
||||
actions: ['all'],
|
||||
actionsSimulators: ['all'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const obsCasesV2NoReopenWithCreateComment: Role = {
|
||||
name: 'obs_cases_v2_no_reopen_role_api_int',
|
||||
privileges: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['all'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: [],
|
||||
feature: {
|
||||
observabilityCasesV2: [
|
||||
'read',
|
||||
'cases_update',
|
||||
'create',
|
||||
'cases_delete',
|
||||
'create_comment',
|
||||
],
|
||||
actions: ['all'],
|
||||
actionsSimulators: ['all'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const obsCasesV2NoCreateCommentWithReopen: Role = {
|
||||
name: 'obs_cases_v2_no_create_comment_role_api_int',
|
||||
privileges: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['all'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
spaces: ['*'],
|
||||
base: [],
|
||||
feature: {
|
||||
observabilityCasesV2: ['read', 'update', 'create', 'cases_delete', 'case_reopen'],
|
||||
actions: ['all'],
|
||||
actionsSimulators: ['all'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const obsCasesRead: Role = {
|
||||
name: 'obs_cases_read_role_api_int',
|
||||
privileges: {
|
||||
|
@ -613,6 +894,9 @@ export const roles = [
|
|||
secAllCasesNoDelete,
|
||||
secAll,
|
||||
secCasesV2All,
|
||||
secCasesV3All,
|
||||
secCasesV2NoReopenWithCreateComment,
|
||||
secCasesV2NoCreateCommentWithReopen,
|
||||
secAllSpace1,
|
||||
secAllCasesRead,
|
||||
secAllCasesNone,
|
||||
|
@ -625,11 +909,19 @@ export const roles = [
|
|||
casesNoDelete,
|
||||
casesAll,
|
||||
casesV2All,
|
||||
casesV3All,
|
||||
casesV3NoAssignee,
|
||||
casesV3ReadAndAssignee,
|
||||
casesV2NoReopenWithCreateComment,
|
||||
casesV2NoCreateCommentWithReopen,
|
||||
casesRead,
|
||||
obsCasesOnlyDelete,
|
||||
obsCasesOnlyReadDelete,
|
||||
obsCasesNoDelete,
|
||||
obsCasesAll,
|
||||
obsCasesV2All,
|
||||
obsCasesV3All,
|
||||
obsCasesV2NoReopenWithCreateComment,
|
||||
obsCasesV2NoCreateCommentWithReopen,
|
||||
obsCasesRead,
|
||||
];
|
||||
|
|
|
@ -9,18 +9,23 @@ import { User } from '../../../../cases_api_integration/common/lib/authenticatio
|
|||
import {
|
||||
casesAll,
|
||||
casesV2All,
|
||||
casesV3All,
|
||||
casesV3NoAssignee,
|
||||
casesV3ReadAndAssignee,
|
||||
casesNoDelete,
|
||||
casesOnlyDelete,
|
||||
casesOnlyReadDelete,
|
||||
casesRead,
|
||||
obsCasesAll,
|
||||
obsCasesV2All,
|
||||
obsCasesV3All,
|
||||
obsCasesNoDelete,
|
||||
obsCasesOnlyDelete,
|
||||
obsCasesOnlyReadDelete,
|
||||
obsCasesRead,
|
||||
secAll,
|
||||
secCasesV2All,
|
||||
secCasesV3All,
|
||||
secAllCasesNoDelete,
|
||||
secAllCasesNone,
|
||||
secAllCasesOnlyDelete,
|
||||
|
@ -31,6 +36,12 @@ import {
|
|||
secReadCasesAll,
|
||||
secReadCasesNone,
|
||||
secReadCasesRead,
|
||||
casesV2NoReopenWithCreateComment,
|
||||
obsCasesV2NoReopenWithCreateComment,
|
||||
secCasesV2NoReopenWithCreateComment,
|
||||
secCasesV2NoCreateCommentWithReopen,
|
||||
casesV2NoCreateCommentWithReopen,
|
||||
obsCasesV2NoCreateCommentWithReopen,
|
||||
} from './roles';
|
||||
|
||||
/**
|
||||
|
@ -67,6 +78,24 @@ export const secCasesV2AllUser: User = {
|
|||
roles: [secCasesV2All.name],
|
||||
};
|
||||
|
||||
export const secCasesV3AllUser: User = {
|
||||
username: 'sec_cases_v3_all_user_api_int',
|
||||
password: 'password',
|
||||
roles: [secCasesV3All.name],
|
||||
};
|
||||
|
||||
export const secCasesV2NoReopenWithCreateCommentUser: User = {
|
||||
username: 'sec_cases_v2_no_reopen_with_create_comment_user_api_int',
|
||||
password: 'password',
|
||||
roles: [secCasesV2NoReopenWithCreateComment.name],
|
||||
};
|
||||
|
||||
export const secCasesV2NoCreateCommentWithReopenUser: User = {
|
||||
username: 'sec_cases_v2_no_create_comment_with_reopen_user_api_int',
|
||||
password: 'password',
|
||||
roles: [secCasesV2NoCreateCommentWithReopen.name],
|
||||
};
|
||||
|
||||
export const secAllSpace1User: User = {
|
||||
username: 'sec_all_space1_user_api_int',
|
||||
password: 'password',
|
||||
|
@ -143,6 +172,36 @@ export const casesV2AllUser: User = {
|
|||
roles: [casesV2All.name],
|
||||
};
|
||||
|
||||
export const casesV3AllUser: User = {
|
||||
username: 'cases_v3_all_user_api_int',
|
||||
password: 'password',
|
||||
roles: [casesV3All.name],
|
||||
};
|
||||
|
||||
export const casesV3NoAssigneeUser: User = {
|
||||
username: 'cases_v3_no_assignee_user_api_int',
|
||||
password: 'password',
|
||||
roles: [casesV3NoAssignee.name],
|
||||
};
|
||||
|
||||
export const casesV3ReadAndAssignUser: User = {
|
||||
username: 'cases_v3_read_and_assignee_user_api_int',
|
||||
password: 'password',
|
||||
roles: [casesV3ReadAndAssignee.name],
|
||||
};
|
||||
|
||||
export const casesV2NoReopenWithCreateCommentUser: User = {
|
||||
username: 'cases_v2_no_reopen_with_create_comment_user_api_int',
|
||||
password: 'password',
|
||||
roles: [casesV2NoReopenWithCreateComment.name],
|
||||
};
|
||||
|
||||
export const casesV2NoCreateCommentWithReopenUser: User = {
|
||||
username: 'cases_v2_no_create_comment_with_reopen_user_api_int',
|
||||
password: 'password',
|
||||
roles: [casesV2NoCreateCommentWithReopen.name],
|
||||
};
|
||||
|
||||
export const casesReadUser: User = {
|
||||
username: 'cases_read_user_api_int',
|
||||
password: 'password',
|
||||
|
@ -183,6 +242,24 @@ export const obsCasesV2AllUser: User = {
|
|||
roles: [obsCasesV2All.name],
|
||||
};
|
||||
|
||||
export const obsCasesV3AllUser: User = {
|
||||
username: 'obs_cases_v3_all_user_api_int',
|
||||
password: 'password',
|
||||
roles: [obsCasesV3All.name],
|
||||
};
|
||||
|
||||
export const obsCasesV2NoReopenWithCreateCommentUser: User = {
|
||||
username: 'obs_cases_v2_no_reopen_with_create_comment_user_api_int',
|
||||
password: 'password',
|
||||
roles: [obsCasesV2NoReopenWithCreateComment.name],
|
||||
};
|
||||
|
||||
export const obsCasesV2NoCreateCommentWithReopenUser: User = {
|
||||
username: 'obs_cases_v2_no_create_comment_with_reopen_user_api_int',
|
||||
password: 'password',
|
||||
roles: [obsCasesV2NoCreateCommentWithReopen.name],
|
||||
};
|
||||
|
||||
export const obsCasesReadUser: User = {
|
||||
username: 'obs_cases_read_user_api_int',
|
||||
password: 'password',
|
||||
|
@ -211,6 +288,9 @@ export const users = [
|
|||
secAllCasesNoDeleteUser,
|
||||
secAllUser,
|
||||
secCasesV2AllUser,
|
||||
secCasesV3AllUser,
|
||||
secCasesV2NoReopenWithCreateCommentUser,
|
||||
secCasesV2NoCreateCommentWithReopenUser,
|
||||
secAllSpace1User,
|
||||
secAllCasesReadUser,
|
||||
secAllCasesNoneUser,
|
||||
|
@ -223,12 +303,20 @@ export const users = [
|
|||
casesNoDeleteUser,
|
||||
casesAllUser,
|
||||
casesV2AllUser,
|
||||
casesV3AllUser,
|
||||
casesV3NoAssigneeUser,
|
||||
casesV3ReadAndAssignUser,
|
||||
casesV2NoReopenWithCreateCommentUser,
|
||||
casesV2NoCreateCommentWithReopenUser,
|
||||
casesReadUser,
|
||||
obsCasesOnlyDeleteUser,
|
||||
obsCasesOnlyReadDeleteUser,
|
||||
obsCasesNoDeleteUser,
|
||||
obsCasesAllUser,
|
||||
obsCasesV2AllUser,
|
||||
obsCasesV3AllUser,
|
||||
obsCasesV2NoReopenWithCreateCommentUser,
|
||||
obsCasesV2NoCreateCommentWithReopenUser,
|
||||
obsCasesReadUser,
|
||||
obsSecCasesAllUser,
|
||||
obsSecCasesReadUser,
|
||||
|
|
|
@ -20,14 +20,19 @@ import {
|
|||
getCase,
|
||||
createComment,
|
||||
updateCaseStatus,
|
||||
updateCaseAssignee,
|
||||
} from '../../../cases_api_integration/common/lib/api';
|
||||
import {
|
||||
casesAllUser,
|
||||
casesV2AllUser,
|
||||
casesV3AllUser,
|
||||
casesV3NoAssigneeUser,
|
||||
casesV3ReadAndAssignUser,
|
||||
casesNoDeleteUser,
|
||||
casesOnlyDeleteUser,
|
||||
obsCasesAllUser,
|
||||
obsCasesV2AllUser,
|
||||
obsCasesV3AllUser,
|
||||
obsCasesNoDeleteUser,
|
||||
obsCasesOnlyDeleteUser,
|
||||
secAllCasesNoDeleteUser,
|
||||
|
@ -36,12 +41,20 @@ import {
|
|||
secAllCasesReadUser,
|
||||
secAllUser,
|
||||
secCasesV2AllUser,
|
||||
secCasesV3AllUser,
|
||||
secReadCasesAllUser,
|
||||
secReadCasesNoneUser,
|
||||
secReadCasesReadUser,
|
||||
secReadUser,
|
||||
casesV2NoReopenWithCreateCommentUser,
|
||||
casesV2NoCreateCommentWithReopenUser,
|
||||
obsCasesV2NoReopenWithCreateCommentUser,
|
||||
obsCasesV2NoCreateCommentWithReopenUser,
|
||||
secCasesV2NoReopenWithCreateCommentUser,
|
||||
secCasesV2NoCreateCommentWithReopenUser,
|
||||
} from './common/users';
|
||||
import { getPostCaseRequest } from '../../../cases_api_integration/common/lib/mock';
|
||||
import { suggestUserProfiles } from '../../../cases_api_integration/common/lib/api/user_profiles';
|
||||
|
||||
export default ({ getService }: FtrProviderContext): void => {
|
||||
describe('feature privilege', () => {
|
||||
|
@ -63,9 +76,13 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
{ user: obsCasesAllUser, owner: OBSERVABILITY_APP_ID },
|
||||
{ user: obsCasesV2AllUser, owner: OBSERVABILITY_APP_ID },
|
||||
{ user: obsCasesNoDeleteUser, owner: OBSERVABILITY_APP_ID },
|
||||
{ user: secCasesV3AllUser, owner: SECURITY_SOLUTION_APP_ID },
|
||||
{ user: casesV3AllUser, owner: CASES_APP_ID },
|
||||
{ user: obsCasesV3AllUser, owner: OBSERVABILITY_APP_ID },
|
||||
{ user: casesV3NoAssigneeUser, owner: CASES_APP_ID },
|
||||
]) {
|
||||
it(`User ${user.username} with role(s) ${user.roles.join()} can create a case`, async () => {
|
||||
await createCase(supertestWithoutAuth, getPostCaseRequest({ owner }), 200, {
|
||||
await createCase(supertest, getPostCaseRequest({ owner }), 200, {
|
||||
user,
|
||||
space: null,
|
||||
});
|
||||
|
@ -83,6 +100,10 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
{ user: obsCasesAllUser, owner: OBSERVABILITY_APP_ID },
|
||||
{ user: obsCasesV2AllUser, owner: OBSERVABILITY_APP_ID },
|
||||
{ user: obsCasesNoDeleteUser, owner: OBSERVABILITY_APP_ID },
|
||||
{ user: secCasesV3AllUser, owner: SECURITY_SOLUTION_APP_ID },
|
||||
{ user: casesV3AllUser, owner: CASES_APP_ID },
|
||||
{ user: obsCasesV3AllUser, owner: OBSERVABILITY_APP_ID },
|
||||
{ user: casesV3NoAssigneeUser, owner: CASES_APP_ID },
|
||||
]) {
|
||||
it(`User ${user.username} with role(s) ${user.roles.join()} can get a case`, async () => {
|
||||
const caseInfo = await createCase(supertest, getPostCaseRequest({ owner }));
|
||||
|
@ -105,6 +126,7 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
{ user: secReadCasesNoneUser, owner: SECURITY_SOLUTION_APP_ID },
|
||||
{ user: casesOnlyDeleteUser, owner: CASES_APP_ID },
|
||||
{ user: obsCasesOnlyDeleteUser, owner: OBSERVABILITY_APP_ID },
|
||||
{ user: casesV3NoAssigneeUser, owner: CASES_APP_ID },
|
||||
]) {
|
||||
it(`User ${
|
||||
user.username
|
||||
|
@ -145,6 +167,10 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
{ user: obsCasesAllUser, owner: OBSERVABILITY_APP_ID },
|
||||
{ user: obsCasesV2AllUser, owner: OBSERVABILITY_APP_ID },
|
||||
{ user: obsCasesOnlyDeleteUser, owner: OBSERVABILITY_APP_ID },
|
||||
{ user: secCasesV3AllUser, owner: SECURITY_SOLUTION_APP_ID },
|
||||
{ user: casesV3AllUser, owner: CASES_APP_ID },
|
||||
{ user: obsCasesV3AllUser, owner: OBSERVABILITY_APP_ID },
|
||||
{ user: casesV3NoAssigneeUser, owner: CASES_APP_ID },
|
||||
]) {
|
||||
it(`User ${user.username} with role(s) ${user.roles.join()} can delete a case`, async () => {
|
||||
const caseInfo = await createCase(supertest, getPostCaseRequest({ owner }));
|
||||
|
@ -183,6 +209,9 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
{ user: obsCasesV2AllUser, owner: OBSERVABILITY_APP_ID },
|
||||
{ user: casesAllUser, owner: CASES_APP_ID },
|
||||
{ user: casesV2AllUser, owner: CASES_APP_ID },
|
||||
{ user: secCasesV3AllUser, owner: SECURITY_SOLUTION_APP_ID },
|
||||
{ user: casesV3AllUser, owner: CASES_APP_ID },
|
||||
{ user: obsCasesV3AllUser, owner: OBSERVABILITY_APP_ID },
|
||||
]) {
|
||||
it(`User ${user.username} with role(s) ${user.roles.join()} can reopen a case`, async () => {
|
||||
const caseInfo = await createCase(supertest, getPostCaseRequest({ owner }));
|
||||
|
@ -190,7 +219,14 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
supertest: supertestWithoutAuth,
|
||||
caseId: caseInfo.id,
|
||||
status: 'closed' as CaseStatuses,
|
||||
version: '2',
|
||||
version: caseInfo.version,
|
||||
expectedHttpCode: 200,
|
||||
auth: { user, space: null },
|
||||
});
|
||||
|
||||
const updatedCase = await getCase({
|
||||
supertest: supertestWithoutAuth,
|
||||
caseId: caseInfo.id,
|
||||
expectedHttpCode: 200,
|
||||
auth: { user, space: null },
|
||||
});
|
||||
|
@ -199,13 +235,110 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
supertest: supertestWithoutAuth,
|
||||
caseId: caseInfo.id,
|
||||
status: 'open' as CaseStatuses,
|
||||
version: '3',
|
||||
version: updatedCase.version,
|
||||
expectedHttpCode: 200,
|
||||
auth: { user, space: null },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
for (const { user, owner, userWithFullPerms } of [
|
||||
{
|
||||
user: casesV2NoCreateCommentWithReopenUser,
|
||||
owner: CASES_APP_ID,
|
||||
userWithFullPerms: casesV3AllUser,
|
||||
},
|
||||
{
|
||||
user: obsCasesV2NoCreateCommentWithReopenUser,
|
||||
owner: OBSERVABILITY_APP_ID,
|
||||
userWithFullPerms: obsCasesV3AllUser,
|
||||
},
|
||||
{
|
||||
user: secCasesV2NoCreateCommentWithReopenUser,
|
||||
owner: SECURITY_SOLUTION_APP_ID,
|
||||
userWithFullPerms: secCasesV3AllUser,
|
||||
},
|
||||
{ user: casesV3NoAssigneeUser, owner: CASES_APP_ID, userWithFullPerms: casesV3AllUser },
|
||||
]) {
|
||||
it(`User ${
|
||||
user.username
|
||||
} with role(s) ${user.roles.join()} can reopen a case, if it's closed`, async () => {
|
||||
const caseInfo = await createCase(supertest, getPostCaseRequest({ owner }));
|
||||
await updateCaseStatus({
|
||||
supertest: supertestWithoutAuth,
|
||||
caseId: caseInfo.id,
|
||||
status: 'closed' as CaseStatuses,
|
||||
version: caseInfo.version,
|
||||
expectedHttpCode: 200,
|
||||
auth: { user: userWithFullPerms, space: null },
|
||||
});
|
||||
|
||||
const updatedCase = await getCase({
|
||||
supertest: supertestWithoutAuth,
|
||||
caseId: caseInfo.id,
|
||||
expectedHttpCode: 200,
|
||||
auth: { user, space: null },
|
||||
});
|
||||
|
||||
await updateCaseStatus({
|
||||
supertest: supertestWithoutAuth,
|
||||
caseId: caseInfo.id,
|
||||
status: 'open' as CaseStatuses,
|
||||
version: updatedCase.version,
|
||||
expectedHttpCode: 200,
|
||||
auth: { user, space: null },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
for (const { user, owner, userWithFullPerms } of [
|
||||
{
|
||||
user: casesV2NoReopenWithCreateCommentUser,
|
||||
owner: CASES_APP_ID,
|
||||
userWithFullPerms: casesV3AllUser,
|
||||
},
|
||||
{
|
||||
user: obsCasesV2NoReopenWithCreateCommentUser,
|
||||
owner: OBSERVABILITY_APP_ID,
|
||||
userWithFullPerms: obsCasesV3AllUser,
|
||||
},
|
||||
{
|
||||
user: secCasesV2NoReopenWithCreateCommentUser,
|
||||
owner: SECURITY_SOLUTION_APP_ID,
|
||||
userWithFullPerms: secCasesV3AllUser,
|
||||
},
|
||||
]) {
|
||||
it(`User ${
|
||||
user.username
|
||||
} with role(s) ${user.roles.join()} CANNOT reopen a case`, async () => {
|
||||
const caseInfo = await createCase(supertest, getPostCaseRequest({ owner }));
|
||||
await updateCaseStatus({
|
||||
supertest: supertestWithoutAuth,
|
||||
caseId: caseInfo.id,
|
||||
status: 'closed' as CaseStatuses,
|
||||
version: caseInfo.version,
|
||||
expectedHttpCode: 200,
|
||||
auth: { user: userWithFullPerms, space: null },
|
||||
});
|
||||
|
||||
const updatedCase = await getCase({
|
||||
supertest: supertestWithoutAuth,
|
||||
caseId: caseInfo.id,
|
||||
expectedHttpCode: 200,
|
||||
auth: { user, space: null },
|
||||
});
|
||||
|
||||
await updateCaseStatus({
|
||||
supertest: supertestWithoutAuth,
|
||||
caseId: caseInfo.id,
|
||||
status: 'open' as CaseStatuses,
|
||||
version: updatedCase.version,
|
||||
expectedHttpCode: 403,
|
||||
auth: { user, space: null },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
for (const { user, owner } of [
|
||||
{ user: secAllUser, owner: SECURITY_SOLUTION_APP_ID },
|
||||
{ user: secCasesV2AllUser, owner: SECURITY_SOLUTION_APP_ID },
|
||||
|
@ -213,6 +346,13 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
{ user: obsCasesV2AllUser, owner: OBSERVABILITY_APP_ID },
|
||||
{ user: casesAllUser, owner: CASES_APP_ID },
|
||||
{ user: casesV2AllUser, owner: CASES_APP_ID },
|
||||
{ user: casesV2NoReopenWithCreateCommentUser, owner: CASES_APP_ID },
|
||||
{ user: obsCasesV2NoReopenWithCreateCommentUser, owner: OBSERVABILITY_APP_ID },
|
||||
{ user: secCasesV2NoReopenWithCreateCommentUser, owner: SECURITY_SOLUTION_APP_ID },
|
||||
{ user: secCasesV3AllUser, owner: SECURITY_SOLUTION_APP_ID },
|
||||
{ user: casesV3AllUser, owner: CASES_APP_ID },
|
||||
{ user: obsCasesV3AllUser, owner: OBSERVABILITY_APP_ID },
|
||||
{ user: casesV3NoAssigneeUser, owner: CASES_APP_ID },
|
||||
]) {
|
||||
it(`User ${user.username} with role(s) ${user.roles.join()} can add comments`, async () => {
|
||||
const caseInfo = await createCase(supertest, getPostCaseRequest({ owner }));
|
||||
|
@ -230,5 +370,109 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
for (const { user, owner, userWithFullPerms } of [
|
||||
{ user: casesV3NoAssigneeUser, owner: CASES_APP_ID, userWithFullPerms: casesV3AllUser },
|
||||
{
|
||||
user: casesV2NoCreateCommentWithReopenUser,
|
||||
owner: CASES_APP_ID,
|
||||
userWithFullPerms: casesV3AllUser,
|
||||
},
|
||||
{
|
||||
user: obsCasesV2NoCreateCommentWithReopenUser,
|
||||
owner: OBSERVABILITY_APP_ID,
|
||||
userWithFullPerms: obsCasesV3AllUser,
|
||||
},
|
||||
{
|
||||
user: secCasesV2NoCreateCommentWithReopenUser,
|
||||
owner: SECURITY_SOLUTION_APP_ID,
|
||||
userWithFullPerms: secCasesV3AllUser,
|
||||
},
|
||||
{ user: secReadUser, owner: SECURITY_SOLUTION_APP_ID, userWithFullPerms: secAllUser },
|
||||
{ user: casesOnlyDeleteUser, owner: CASES_APP_ID, userWithFullPerms: casesAllUser },
|
||||
{
|
||||
user: obsCasesOnlyDeleteUser,
|
||||
owner: OBSERVABILITY_APP_ID,
|
||||
userWithFullPerms: obsCasesAllUser,
|
||||
},
|
||||
]) {
|
||||
it(`User ${
|
||||
user.username
|
||||
} with role(s) ${user.roles.join()} CANNOT change assignee`, async () => {
|
||||
const caseInfo = await createCase(supertest, getPostCaseRequest({ owner }));
|
||||
const [{ uid: assigneeId }] = await suggestUserProfiles({
|
||||
supertest: supertestWithoutAuth,
|
||||
req: { name: userWithFullPerms.username, owners: [owner], size: 1 },
|
||||
auth: { user: userWithFullPerms, space: null },
|
||||
});
|
||||
await updateCaseAssignee({
|
||||
supertest: supertestWithoutAuth,
|
||||
caseId: caseInfo.id,
|
||||
assigneeId,
|
||||
expectedHttpCode: 403,
|
||||
auth: { user, space: null },
|
||||
version: caseInfo.version,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
for (const { user, owner } of [
|
||||
{ user: casesV3ReadAndAssignUser, owner: CASES_APP_ID },
|
||||
{ user: secAllUser, owner: SECURITY_SOLUTION_APP_ID },
|
||||
{ user: casesAllUser, owner: CASES_APP_ID },
|
||||
{ user: casesV2AllUser, owner: CASES_APP_ID },
|
||||
{ user: secCasesV2AllUser, owner: SECURITY_SOLUTION_APP_ID },
|
||||
{ user: obsCasesV2AllUser, owner: OBSERVABILITY_APP_ID },
|
||||
{ user: obsCasesAllUser, owner: OBSERVABILITY_APP_ID },
|
||||
{ user: secCasesV2AllUser, owner: SECURITY_SOLUTION_APP_ID },
|
||||
{ user: obsCasesAllUser, owner: OBSERVABILITY_APP_ID },
|
||||
{ user: obsCasesV2AllUser, owner: OBSERVABILITY_APP_ID },
|
||||
{ user: secCasesV3AllUser, owner: SECURITY_SOLUTION_APP_ID },
|
||||
{ user: casesV3AllUser, owner: CASES_APP_ID },
|
||||
{ user: obsCasesV3AllUser, owner: OBSERVABILITY_APP_ID },
|
||||
]) {
|
||||
it(`User ${
|
||||
user.username
|
||||
} with role(s) ${user.roles.join()} CAN change assignee`, async () => {
|
||||
const caseInfo = await createCase(supertest, getPostCaseRequest({ owner }));
|
||||
const [{ uid: assigneeId }] = await suggestUserProfiles({
|
||||
supertest: supertestWithoutAuth,
|
||||
req: { name: user.username, owners: [owner], size: 1 },
|
||||
auth: { user, space: null },
|
||||
});
|
||||
await updateCaseAssignee({
|
||||
supertest: supertestWithoutAuth,
|
||||
caseId: caseInfo.id,
|
||||
assigneeId,
|
||||
expectedHttpCode: 200,
|
||||
version: caseInfo.version,
|
||||
auth: { user, space: null },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
for (const { user, owner } of [
|
||||
{ user: casesV2NoCreateCommentWithReopenUser, owner: CASES_APP_ID },
|
||||
{ user: obsCasesV2NoCreateCommentWithReopenUser, owner: OBSERVABILITY_APP_ID },
|
||||
{ user: secCasesV2NoCreateCommentWithReopenUser, owner: SECURITY_SOLUTION_APP_ID },
|
||||
]) {
|
||||
it(`User ${
|
||||
user.username
|
||||
} with role(s) ${user.roles.join()} CANNOT add comments`, async () => {
|
||||
const caseInfo = await createCase(supertest, getPostCaseRequest({ owner }));
|
||||
const comment: UserCommentAttachmentPayload = {
|
||||
comment: 'test',
|
||||
owner,
|
||||
type: AttachmentType.user,
|
||||
};
|
||||
await createComment({
|
||||
params: comment,
|
||||
supertest: supertestWithoutAuth,
|
||||
caseId: caseInfo.id,
|
||||
expectedHttpCode: 403,
|
||||
auth: { user, space: null },
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -113,7 +113,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
'guidedOnboardingFeature',
|
||||
'monitoring',
|
||||
'observabilityAIAssistant',
|
||||
'observabilityCasesV2',
|
||||
'observabilityCasesV3',
|
||||
'savedObjectsManagement',
|
||||
'savedQueryManagement',
|
||||
'savedObjectsTagging',
|
||||
|
@ -121,7 +121,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
'apm',
|
||||
'stackAlerts',
|
||||
'canvas',
|
||||
'generalCasesV2',
|
||||
'generalCasesV3',
|
||||
'infrastructure',
|
||||
'inventory',
|
||||
'logs',
|
||||
|
@ -137,7 +137,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
'slo',
|
||||
'securitySolutionAssistant',
|
||||
'securitySolutionAttackDiscovery',
|
||||
'securitySolutionCasesV2',
|
||||
'securitySolutionCasesV3',
|
||||
'securitySolutionTimeline',
|
||||
'securitySolutionNotes',
|
||||
'fleet',
|
||||
|
@ -170,7 +170,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
'guidedOnboardingFeature',
|
||||
'monitoring',
|
||||
'observabilityAIAssistant',
|
||||
'observabilityCasesV2',
|
||||
'observabilityCasesV3',
|
||||
'savedObjectsManagement',
|
||||
'savedQueryManagement',
|
||||
'savedObjectsTagging',
|
||||
|
@ -178,7 +178,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
'apm',
|
||||
'stackAlerts',
|
||||
'canvas',
|
||||
'generalCasesV2',
|
||||
'generalCasesV3',
|
||||
'infrastructure',
|
||||
'inventory',
|
||||
'logs',
|
||||
|
@ -195,7 +195,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
'slo',
|
||||
'securitySolutionAssistant',
|
||||
'securitySolutionAttackDiscovery',
|
||||
'securitySolutionCasesV2',
|
||||
'securitySolutionCasesV3',
|
||||
'securitySolutionTimeline',
|
||||
'securitySolutionNotes',
|
||||
'fleet',
|
||||
|
|
|
@ -40,6 +40,17 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
'create_comment',
|
||||
'case_reopen',
|
||||
],
|
||||
generalCasesV3: [
|
||||
'all',
|
||||
'read',
|
||||
'minimal_all',
|
||||
'minimal_read',
|
||||
'cases_delete',
|
||||
'cases_settings',
|
||||
'create_comment',
|
||||
'case_reopen',
|
||||
'cases_assign',
|
||||
],
|
||||
observabilityCases: [
|
||||
'all',
|
||||
'read',
|
||||
|
@ -58,6 +69,17 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
'create_comment',
|
||||
'case_reopen',
|
||||
],
|
||||
observabilityCasesV3: [
|
||||
'all',
|
||||
'read',
|
||||
'minimal_all',
|
||||
'minimal_read',
|
||||
'cases_delete',
|
||||
'cases_settings',
|
||||
'create_comment',
|
||||
'case_reopen',
|
||||
'cases_assign',
|
||||
],
|
||||
observabilityAIAssistant: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
slo: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
searchPlayground: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
|
@ -164,6 +186,17 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
'create_comment',
|
||||
'case_reopen',
|
||||
],
|
||||
securitySolutionCasesV3: [
|
||||
'all',
|
||||
'read',
|
||||
'minimal_all',
|
||||
'minimal_read',
|
||||
'cases_delete',
|
||||
'cases_settings',
|
||||
'create_comment',
|
||||
'case_reopen',
|
||||
'cases_assign',
|
||||
],
|
||||
securitySolutionTimeline: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
securitySolutionNotes: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
infrastructure: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
|
|
|
@ -33,8 +33,10 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
maps: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
generalCases: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
generalCasesV2: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
generalCasesV3: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
observabilityCases: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
observabilityCasesV2: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
observabilityCasesV3: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
observabilityAIAssistant: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
slo: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
canvas: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
|
@ -53,6 +55,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
securitySolutionAttackDiscovery: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
securitySolutionCases: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
securitySolutionCasesV2: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
securitySolutionCasesV3: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
securitySolutionNotes: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
securitySolutionTimeline: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
searchPlayground: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
|
@ -133,6 +136,17 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
'create_comment',
|
||||
'case_reopen',
|
||||
],
|
||||
generalCasesV3: [
|
||||
'all',
|
||||
'read',
|
||||
'minimal_all',
|
||||
'minimal_read',
|
||||
'cases_delete',
|
||||
'cases_settings',
|
||||
'create_comment',
|
||||
'case_reopen',
|
||||
'cases_assign',
|
||||
],
|
||||
observabilityCases: [
|
||||
'all',
|
||||
'read',
|
||||
|
@ -151,6 +165,17 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
'create_comment',
|
||||
'case_reopen',
|
||||
],
|
||||
observabilityCasesV3: [
|
||||
'all',
|
||||
'read',
|
||||
'minimal_all',
|
||||
'minimal_read',
|
||||
'cases_delete',
|
||||
'cases_settings',
|
||||
'create_comment',
|
||||
'case_reopen',
|
||||
'cases_assign',
|
||||
],
|
||||
observabilityAIAssistant: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
slo: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
searchPlayground: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
|
@ -257,6 +282,17 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
'create_comment',
|
||||
'case_reopen',
|
||||
],
|
||||
securitySolutionCasesV3: [
|
||||
'all',
|
||||
'read',
|
||||
'minimal_all',
|
||||
'minimal_read',
|
||||
'cases_delete',
|
||||
'cases_settings',
|
||||
'create_comment',
|
||||
'case_reopen',
|
||||
'cases_assign',
|
||||
],
|
||||
securitySolutionTimeline: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
securitySolutionNotes: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
infrastructure: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
|
|
|
@ -37,7 +37,7 @@ const secAll: Role = {
|
|||
{
|
||||
feature: {
|
||||
siem: ['all'],
|
||||
securitySolutionCasesV2: ['all'],
|
||||
securitySolutionCasesV3: ['all'],
|
||||
actions: ['all'],
|
||||
actionsSimulators: ['all'],
|
||||
},
|
||||
|
@ -68,7 +68,7 @@ const secRead: Role = {
|
|||
{
|
||||
feature: {
|
||||
siem: ['read'],
|
||||
securitySolutionCasesV2: ['read'],
|
||||
securitySolutionCasesV3: ['read'],
|
||||
actions: ['all'],
|
||||
actionsSimulators: ['all'],
|
||||
},
|
||||
|
|
|
@ -10,7 +10,7 @@ import { Case, CaseStatuses } from '@kbn/cases-plugin/common/types/domain';
|
|||
import {
|
||||
CasePostRequest,
|
||||
CasesFindResponse,
|
||||
CasePatchRequest,
|
||||
CasesPatchRequest,
|
||||
} from '@kbn/cases-plugin/common/types/api';
|
||||
import type SuperTest from 'supertest';
|
||||
import { ToolingLog } from '@kbn/tooling-log';
|
||||
|
@ -106,21 +106,60 @@ export const updateCaseStatus = async ({
|
|||
}: {
|
||||
supertest: SuperTest.Agent;
|
||||
caseId: string;
|
||||
version?: string;
|
||||
version: string;
|
||||
status?: CaseStatuses;
|
||||
expectedHttpCode?: number;
|
||||
auth?: { user: User; space: string | null };
|
||||
}) => {
|
||||
const updateRequest: CasePatchRequest = {
|
||||
status,
|
||||
version,
|
||||
id: caseId,
|
||||
const updateRequest: CasesPatchRequest = {
|
||||
cases: [
|
||||
{
|
||||
status,
|
||||
version,
|
||||
id: caseId,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { body: updatedCase } = await supertest
|
||||
.patch(`/api/cases/${caseId}`)
|
||||
.patch(`${getSpaceUrlPrefix(auth?.space)}${CASES_URL}`)
|
||||
.auth(auth.user.username, auth.user.password)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.send(updateRequest);
|
||||
.send(updateRequest)
|
||||
.expect(expectedHttpCode);
|
||||
return updatedCase;
|
||||
};
|
||||
|
||||
export const updateCaseAssignee = async ({
|
||||
supertest,
|
||||
caseId,
|
||||
version = '2',
|
||||
assigneeId,
|
||||
expectedHttpCode = 204,
|
||||
auth = { user: superUser, space: null },
|
||||
}: {
|
||||
supertest: SuperTest.Agent;
|
||||
caseId: string;
|
||||
version?: string;
|
||||
assigneeId: string;
|
||||
expectedHttpCode?: number;
|
||||
auth?: { user: User; space: string | null };
|
||||
}) => {
|
||||
const updateRequest: CasesPatchRequest = {
|
||||
cases: [
|
||||
{
|
||||
version,
|
||||
assignees: [{ uid: assigneeId }],
|
||||
id: caseId,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { body: updatedCase } = await supertest
|
||||
.patch(`${getSpaceUrlPrefix(auth?.space)}${CASES_URL}`)
|
||||
.auth(auth.user.username, auth.user.password)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.send(updateRequest)
|
||||
.expect(expectedHttpCode);
|
||||
return updatedCase;
|
||||
};
|
||||
|
|
|
@ -161,6 +161,29 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Assign users',
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
id: 'cases_assign',
|
||||
name: 'Assign users to cases',
|
||||
includeIn: 'all',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
cases: {
|
||||
assign: ['securitySolutionFixture'],
|
||||
},
|
||||
ui: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
|
|
@ -150,7 +150,7 @@ export function MachineLearningSecurityCommonProvider({ getService }: FtrProvide
|
|||
savedObjectsManagement: ['all'],
|
||||
advancedSettings: ['all'],
|
||||
indexPatterns: ['all'],
|
||||
generalCasesV2: ['all'],
|
||||
generalCasesV3: ['all'],
|
||||
ml: ['none'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
|
@ -179,7 +179,7 @@ export function MachineLearningSecurityCommonProvider({ getService }: FtrProvide
|
|||
savedObjectsManagement: ['all'],
|
||||
advancedSettings: ['all'],
|
||||
indexPatterns: ['all'],
|
||||
generalCasesV2: ['all'],
|
||||
generalCasesV3: ['all'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
|
|
|
@ -58,7 +58,7 @@ export function ObservabilityUsersProvider({ getPageObject, getService }: FtrPro
|
|||
*/
|
||||
const defineBasicObservabilityRole = (
|
||||
features: Partial<{
|
||||
observabilityCasesV2: string[];
|
||||
observabilityCasesV3: string[];
|
||||
apm: string[];
|
||||
logs: string[];
|
||||
infrastructure: string[];
|
||||
|
|
|
@ -25,7 +25,7 @@ export const casesReadDelete: Role = {
|
|||
kibana: [
|
||||
{
|
||||
feature: {
|
||||
generalCasesV2: ['minimal_read', 'cases_delete'],
|
||||
generalCasesV3: ['minimal_read', 'cases_delete'],
|
||||
actions: ['all'],
|
||||
actionsSimulators: ['all'],
|
||||
},
|
||||
|
@ -49,7 +49,7 @@ export const casesNoDelete: Role = {
|
|||
kibana: [
|
||||
{
|
||||
feature: {
|
||||
generalCasesV2: ['minimal_all'],
|
||||
generalCasesV3: ['minimal_all'],
|
||||
actions: ['all'],
|
||||
actionsSimulators: ['all'],
|
||||
},
|
||||
|
@ -97,7 +97,7 @@ export const casesAll: Role = {
|
|||
kibana: [
|
||||
{
|
||||
feature: {
|
||||
generalCasesV2: ['all'],
|
||||
generalCasesV3: ['all'],
|
||||
actions: ['all'],
|
||||
actionsSimulators: ['all'],
|
||||
},
|
||||
|
|
|
@ -44,6 +44,7 @@ const permissions = {
|
|||
settings: true,
|
||||
createComment: true,
|
||||
reopenCase: true,
|
||||
assign: true,
|
||||
};
|
||||
|
||||
const attachments = [{ type: AttachmentType.user as const, comment: 'test' }];
|
||||
|
|
|
@ -43,7 +43,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
await esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs');
|
||||
await observability.users.setTestUserRole(
|
||||
observability.users.defineBasicObservabilityRole({
|
||||
observabilityCasesV2: ['all'],
|
||||
observabilityCasesV3: ['all'],
|
||||
logs: ['all'],
|
||||
})
|
||||
);
|
||||
|
@ -96,7 +96,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
await esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs');
|
||||
await observability.users.setTestUserRole(
|
||||
observability.users.defineBasicObservabilityRole({
|
||||
observabilityCasesV2: ['read'],
|
||||
observabilityCasesV3: ['read'],
|
||||
logs: ['all'],
|
||||
})
|
||||
);
|
||||
|
|
|
@ -29,7 +29,7 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => {
|
|||
before(async () => {
|
||||
await observability.users.setTestUserRole(
|
||||
observability.users.defineBasicObservabilityRole({
|
||||
observabilityCasesV2: ['all'],
|
||||
observabilityCasesV3: ['all'],
|
||||
logs: ['all'],
|
||||
})
|
||||
);
|
||||
|
@ -75,7 +75,7 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => {
|
|||
before(async () => {
|
||||
await observability.users.setTestUserRole(
|
||||
observability.users.defineBasicObservabilityRole({
|
||||
observabilityCasesV2: ['read'],
|
||||
observabilityCasesV3: ['read'],
|
||||
logs: ['all'],
|
||||
})
|
||||
);
|
||||
|
|
|
@ -33,7 +33,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
before(async () => {
|
||||
await observability.users.setTestUserRole(
|
||||
observability.users.defineBasicObservabilityRole({
|
||||
observabilityCasesV2: ['all'],
|
||||
observabilityCasesV3: ['all'],
|
||||
logs: ['all'],
|
||||
})
|
||||
);
|
||||
|
|
|
@ -181,8 +181,11 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
"case_4_feature_a",
|
||||
"case_4_feature_b",
|
||||
"generalCases",
|
||||
"generalCasesV2",
|
||||
"observabilityCases",
|
||||
"observabilityCasesV2",
|
||||
"securitySolutionCases",
|
||||
"securitySolutionCasesV2",
|
||||
"siem",
|
||||
]
|
||||
`);
|
||||
|
|
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