[8.x] [Cases] Cases assignees sub feature (#201654) (#209437)

# 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![image](https://github.com/user-attachments/assets/ba421784-d976-4ae9-a399-e404c26b3842)\r\n\r\n\r\nAll
+ assign
disabled:\r\n\r\n![image](https://github.com/user-attachments/assets/d835b6f9-5a14-4ae0-abed-b3c3252c2692)\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![image](https://github.com/user-attachments/assets/ba421784-d976-4ae9-a399-e404c26b3842)\r\n\r\n\r\nAll
+ assign
disabled:\r\n\r\n![image](https://github.com/user-attachments/assets/d835b6f9-5a14-4ae0-abed-b3c3252c2692)\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:
Kevin Qualters 2025-02-05 12:38:46 -05:00 committed by GitHub
parent 0f56a7cdcd
commit bd53593617
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
105 changed files with 2425 additions and 218 deletions

View file

@ -18,6 +18,7 @@ Array [
"cases:observability/updateConfiguration",
"cases:observability/createComment",
"cases:observability/reopenCase",
"cases:observability/assignCase",
]
`;

View file

@ -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",

View file

@ -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),
]);
}
}

View file

@ -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;

View file

@ -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

View file

@ -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';

View file

@ -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;
}

View file

@ -18,6 +18,9 @@ describe('createUICapabilities', () => {
"push_cases",
"cases_connectors",
],
"assignCase": Array [
"cases_assign",
],
"createComment": Array [
"create_comment",
],

View file

@ -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,
});

View file

@ -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', () => {

View file

@ -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`;
};

View file

@ -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,

View file

@ -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,
};
};

View file

@ -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 },
});

View file

@ -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,
]
);
};

View file

@ -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 },

View file

@ -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,
};
};

View file

@ -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;

View file

@ -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(

View file

@ -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();

View file

@ -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,
},
});

View file

@ -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

View file

@ -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,

View file

@ -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,

View file

@ -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 =

View file

@ -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 } },
})}

View file

@ -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 = {

View file

@ -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', '');
};

View file

@ -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();

View file

@ -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}

View file

@ -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,
]
);

View file

@ -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(), {

View file

@ -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',
]);
});
});
});

View file

@ -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[];

View file

@ -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 {

View file

@ -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 = {

View file

@ -64,6 +64,7 @@ export enum WriteOperations {
CreateConfiguration = 'createConfiguration',
UpdateConfiguration = 'updateConfiguration',
ReopenCase = 'reopenCase',
AssignCase = 'assignCase',
}
/**

View file

@ -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',

View file

@ -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();

View file

@ -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"`
);
});
});
});
});

View file

@ -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,

View file

@ -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', () => {

View file

@ -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+

View file

@ -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',
]);
});
});

View file

@ -508,6 +508,7 @@ describe('utils', () => {
'cases:my-owner/deleteComment',
'cases:my-owner/findConfigurations',
'cases:my-owner/reopenCase',
'cases:my-owner/assignCase',
]);
});
});

View file

@ -121,5 +121,6 @@ export const constructRequiredKibanaPrivileges = (owner: string): string[] => {
`cases:${owner}/deleteComment`,
`cases:${owner}/findConfigurations`,
`cases:${owner}/reopenCase`,
`cases:${owner}/assignCase`,
];
};

View file

@ -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';

View file

@ -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() });

View file

@ -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] },
],
},
],

View file

@ -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] },
],
},
],
},

View 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,
},
],
},
],
},
],
};
};

View file

@ -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({

View file

@ -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[];
};
/**

View file

@ -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 [],

View file

@ -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'],
},

View file

@ -180,6 +180,10 @@ function mergeWithSubFeatures(
mergedConfig.cases?.reopenCase ?? [],
subFeaturePrivilege.cases?.reopenCase ?? []
),
assign: mergeArrays(
mergedConfig.cases?.assign ?? [],
subFeaturePrivilege.cases?.assign ?? []
),
};
}
return mergedConfig;

View file

@ -92,6 +92,7 @@ const casesSchemaObject = schema.maybe(
settings: schema.maybe(casesSchema),
createComment: schema.maybe(casesSchema),
reopenCase: schema.maybe(casesSchema),
assign: schema.maybe(casesSchema),
})
);

View file

@ -122,6 +122,7 @@ describe('AddToCaseAction', function () {
settings: false,
createComment: false,
reopenCase: false,
assign: false,
},
})
);

View file

@ -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

View file

@ -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,
},
};

View file

@ -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'] }],
},
],
},

View file

@ -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'] }],
},
],
},

View file

@ -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,
},
],
},
],
},
],
});

View file

@ -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;

View file

@ -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

View file

@ -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,
});

View file

@ -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';

View file

@ -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),
});

View file

@ -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'] }],
},
},
},

View file

@ -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'] }],
},
],
},

View file

@ -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'] }],
},
},
},
};

View file

@ -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'] }],
},
],
},

View file

@ -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,
},
},
};
};

View file

@ -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],
]);
};

View file

@ -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;

View file

@ -177,6 +177,7 @@ export enum CasesSubFeatureId {
casesSettings = 'casesSettingsSubFeature',
createComment = 'createCommentSubFeature',
reopenCase = 'reopenCaseSubFeature',
assignUsers = 'assignUsersSubFeature',
}
/** Sub-features IDs for Security Assistant */

View file

@ -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;

View file

@ -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,
});

View file

@ -55,7 +55,7 @@ export const getEndpointOperationsAnalyst: () => Omit<Role, 'name'> = () => {
fleet: ['all'],
fleetv2: ['all'],
osquery: ['all'],
securitySolutionCasesV2: ['all'],
securitySolutionCasesV3: ['all'],
builtinAlerts: ['all'],
siemV2: [
'all',

View file

@ -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'],

View file

@ -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: [],

View file

@ -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', () => {

View file

@ -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);

View file

@ -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,
];

View file

@ -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,

View file

@ -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 },
});
});
}
});
};

View file

@ -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',

View file

@ -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'],

View file

@ -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'],

View file

@ -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'],
},

View file

@ -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;
};

View file

@ -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: [],
},
],
},
],
},
],
});

View file

@ -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: ['*'],
},

View file

@ -58,7 +58,7 @@ export function ObservabilityUsersProvider({ getPageObject, getService }: FtrPro
*/
const defineBasicObservabilityRole = (
features: Partial<{
observabilityCasesV2: string[];
observabilityCasesV3: string[];
apm: string[];
logs: string[];
infrastructure: string[];

View file

@ -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'],
},

View file

@ -44,6 +44,7 @@ const permissions = {
settings: true,
createComment: true,
reopenCase: true,
assign: true,
};
const attachments = [{ type: AttachmentType.user as const, comment: 'test' }];

View file

@ -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'],
})
);

View file

@ -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'],
})
);

View file

@ -33,7 +33,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
before(async () => {
await observability.users.setTestUserRole(
observability.users.defineBasicObservabilityRole({
observabilityCasesV2: ['all'],
observabilityCasesV3: ['all'],
logs: ['all'],
})
);

View file

@ -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