mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Cases][ResponseOps] Add support for deletion sub feature privilege (#135487)
* Starting conversion to permissions from userCanCrud
* Migrating userCanCrud to context
* Fixing tests
* Fix type error
* Removing missed userCanCrud
* Fixing tests and addressing permissions.all feedback
* Fixing test
* Adding deletion sub feature priv
* Fixing type errors
* Fixing tests and adding more granular permissions
* Trying to get plugin tests to work
* Removing unnecessary tests
* First pass at fixing tests
* Moving createUICapabilities to a normal function
* Adding more tests for permissions
* Fixing tests
* Fixing and adding more tests
* Addressing feedback and fixing tests
* Reverting permissions.all changes except delete
* Revert "Reverting permissions.all changes except delete"
This reverts commit 609c150b7d
.
* Fixing test
* Adjusting permissions for add to new or existing case
* Switching a few all permissions to create and read
* check permisions inside of actions menu
* Addressing initial feedback
* Adding functional tests for deletion
* Changing deletion text
* Addressing feedback and fixing tests
* Fixing deeplinks to allow create when no delete
* Addressing feedback
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
99a01902ca
commit
0f3e46749b
126 changed files with 2909 additions and 1063 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -372,6 +372,7 @@
|
|||
/x-pack/test/cases_api_integration/ @elastic/response-ops
|
||||
/x-pack/test/functional/services/cases/ @elastic/response-ops
|
||||
/x-pack/test/functional_with_es_ssl/apps/cases/ @elastic/response-ops
|
||||
/x-pack/test/api_integration/apis/cases @elastic/response-ops
|
||||
|
||||
# Enterprise Search
|
||||
/x-pack/plugins/enterprise_search @elastic/enterprise-search-frontend
|
||||
|
|
|
@ -106,6 +106,7 @@ export const MAX_ALERTS_PER_CASE = 1000 as const;
|
|||
*/
|
||||
export const SECURITY_SOLUTION_OWNER = 'securitySolution' as const;
|
||||
export const OBSERVABILITY_OWNER = 'observability' as const;
|
||||
export const GENERAL_CASES_OWNER = APP_ID;
|
||||
|
||||
export const OWNER_INFO = {
|
||||
[SECURITY_SOLUTION_OWNER]: {
|
||||
|
@ -116,6 +117,10 @@ export const OWNER_INFO = {
|
|||
label: 'Observability',
|
||||
iconType: 'logoObservability',
|
||||
},
|
||||
[GENERAL_CASES_OWNER]: {
|
||||
label: 'Stack',
|
||||
iconType: 'casesApp',
|
||||
},
|
||||
} as const;
|
||||
|
||||
/**
|
||||
|
@ -150,3 +155,12 @@ export const CASES_TELEMETRY_TASK_NAME = 'cases-telemetry-task';
|
|||
*/
|
||||
export const CASE_TELEMETRY_SAVED_OBJECT = 'cases-telemetry';
|
||||
export const CASE_TELEMETRY_SAVED_OBJECT_ID = 'cases-telemetry';
|
||||
|
||||
/**
|
||||
* Cases UI Capabilities
|
||||
*/
|
||||
export const CREATE_CASES_CAPABILITY = 'create_cases' as const;
|
||||
export const READ_CASES_CAPABILITY = 'read_cases' as const;
|
||||
export const UPDATE_CASES_CAPABILITY = 'update_cases' as const;
|
||||
export const DELETE_CASES_CAPABILITY = 'delete_cases' as const;
|
||||
export const PUSH_CASES_CAPABILITY = 'push_cases' as const;
|
||||
|
|
|
@ -15,12 +15,27 @@
|
|||
// For example, constants below could eventually be in a "kbn-cases-constants" instead.
|
||||
// See: https://docs.elastic.dev/kibana-dev-docs/key-concepts/platform-intro#public-plugin-api
|
||||
|
||||
export { CASES_URL, SECURITY_SOLUTION_OWNER } from './constants';
|
||||
export {
|
||||
CASES_URL,
|
||||
SECURITY_SOLUTION_OWNER,
|
||||
CREATE_CASES_CAPABILITY,
|
||||
DELETE_CASES_CAPABILITY,
|
||||
PUSH_CASES_CAPABILITY,
|
||||
READ_CASES_CAPABILITY,
|
||||
UPDATE_CASES_CAPABILITY,
|
||||
} from './constants';
|
||||
|
||||
export { CommentType, CaseStatuses, getCasesFromAlertsUrl, throwErrors } from './api';
|
||||
|
||||
export type { Case, Ecs, CasesFeatures, CaseViewRefreshPropInterface } from './ui/types';
|
||||
export type {
|
||||
Case,
|
||||
Ecs,
|
||||
CasesFeatures,
|
||||
CaseViewRefreshPropInterface,
|
||||
CasesPermissions,
|
||||
} from './ui/types';
|
||||
|
||||
export { StatusAll } from './ui/types';
|
||||
|
||||
export { getCreateConnectorUrl, getAllConnectorsUrl } from './utils/connectors_api';
|
||||
export { createUICapabilities } from './utils/capabilities';
|
||||
|
|
|
@ -6,6 +6,12 @@
|
|||
*/
|
||||
|
||||
import type { SavedObjectsResolveResponse } from '@kbn/core/public';
|
||||
import {
|
||||
CREATE_CASES_CAPABILITY,
|
||||
DELETE_CASES_CAPABILITY,
|
||||
READ_CASES_CAPABILITY,
|
||||
UPDATE_CASES_CAPABILITY,
|
||||
} from '..';
|
||||
import {
|
||||
CasePatchRequest,
|
||||
CaseStatuses,
|
||||
|
@ -24,6 +30,7 @@ import {
|
|||
CommentResponseExternalReferenceType,
|
||||
CommentResponseTypePersistableState,
|
||||
} from '../api';
|
||||
import { PUSH_CASES_CAPABILITY } from '../constants';
|
||||
import { SnakeToCamelCase } from '../types';
|
||||
|
||||
type DeepRequired<T> = { [K in keyof T]: DeepRequired<T[K]> } & Required<T>;
|
||||
|
@ -229,3 +236,20 @@ export interface Ecs {
|
|||
export type CaseActionConnector = ActionConnector;
|
||||
|
||||
export type UseFetchAlertData = (alertIds: string[]) => [boolean, Record<string, unknown>];
|
||||
|
||||
export interface CasesPermissions {
|
||||
all: boolean;
|
||||
create: boolean;
|
||||
read: boolean;
|
||||
update: boolean;
|
||||
delete: boolean;
|
||||
push: boolean;
|
||||
}
|
||||
|
||||
export interface CasesCapabilities {
|
||||
[CREATE_CASES_CAPABILITY]: boolean;
|
||||
[READ_CASES_CAPABILITY]: boolean;
|
||||
[UPDATE_CASES_CAPABILITY]: boolean;
|
||||
[DELETE_CASES_CAPABILITY]: boolean;
|
||||
[PUSH_CASES_CAPABILITY]: boolean;
|
||||
}
|
||||
|
|
29
x-pack/plugins/cases/common/utils/capabilities.ts
Normal file
29
x-pack/plugins/cases/common/utils/capabilities.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 {
|
||||
CREATE_CASES_CAPABILITY,
|
||||
DELETE_CASES_CAPABILITY,
|
||||
PUSH_CASES_CAPABILITY,
|
||||
READ_CASES_CAPABILITY,
|
||||
UPDATE_CASES_CAPABILITY,
|
||||
} from '../constants';
|
||||
|
||||
/**
|
||||
* Return the UI capabilities for each type of operation. These strings must match the values defined in the UI
|
||||
* here: x-pack/plugins/cases/public/client/helpers/capabilities.ts
|
||||
*/
|
||||
export const createUICapabilities = () => ({
|
||||
all: [
|
||||
CREATE_CASES_CAPABILITY,
|
||||
READ_CASES_CAPABILITY,
|
||||
UPDATE_CASES_CAPABILITY,
|
||||
PUSH_CASES_CAPABILITY,
|
||||
] as const,
|
||||
read: [READ_CASES_CAPABILITY] as const,
|
||||
delete: [DELETE_CASES_CAPABILITY] as const,
|
||||
});
|
|
@ -6,3 +6,4 @@
|
|||
*/
|
||||
|
||||
export * from './connectors_api';
|
||||
export * from './capabilities';
|
||||
|
|
|
@ -6,124 +6,91 @@
|
|||
*/
|
||||
|
||||
import type { ApplicationStart } from '@kbn/core/public';
|
||||
import {
|
||||
allCasesCapabilities,
|
||||
allCasesPermissions,
|
||||
noCasesCapabilities,
|
||||
noCasesPermissions,
|
||||
readCasesCapabilities,
|
||||
readCasesPermissions,
|
||||
writeCasesCapabilities,
|
||||
writeCasesPermissions,
|
||||
} from '../../common/mock';
|
||||
import { canUseCases } from './can_use_cases';
|
||||
|
||||
type CasesCapabilities = Pick<
|
||||
ApplicationStart['capabilities'],
|
||||
'securitySolutionCases' | 'observabilityCases'
|
||||
'securitySolutionCases' | 'observabilityCases' | 'generalCases'
|
||||
>;
|
||||
|
||||
const hasAll: CasesCapabilities = {
|
||||
securitySolutionCases: {
|
||||
crud_cases: true,
|
||||
read_cases: true,
|
||||
},
|
||||
observabilityCases: {
|
||||
crud_cases: true,
|
||||
read_cases: true,
|
||||
},
|
||||
securitySolutionCases: allCasesCapabilities(),
|
||||
observabilityCases: allCasesCapabilities(),
|
||||
generalCases: allCasesCapabilities(),
|
||||
};
|
||||
|
||||
const hasNone: CasesCapabilities = {
|
||||
securitySolutionCases: {
|
||||
crud_cases: false,
|
||||
read_cases: false,
|
||||
},
|
||||
observabilityCases: {
|
||||
crud_cases: false,
|
||||
read_cases: false,
|
||||
},
|
||||
securitySolutionCases: noCasesCapabilities(),
|
||||
observabilityCases: noCasesCapabilities(),
|
||||
generalCases: noCasesCapabilities(),
|
||||
};
|
||||
|
||||
const hasSecurity = {
|
||||
securitySolutionCases: {
|
||||
crud_cases: true,
|
||||
read_cases: true,
|
||||
},
|
||||
observabilityCases: {
|
||||
crud_cases: false,
|
||||
read_cases: false,
|
||||
},
|
||||
const hasSecurity: CasesCapabilities = {
|
||||
securitySolutionCases: allCasesCapabilities(),
|
||||
observabilityCases: noCasesCapabilities(),
|
||||
generalCases: noCasesCapabilities(),
|
||||
};
|
||||
|
||||
const hasObservability = {
|
||||
securitySolutionCases: {
|
||||
crud_cases: false,
|
||||
read_cases: false,
|
||||
},
|
||||
observabilityCases: {
|
||||
crud_cases: true,
|
||||
read_cases: true,
|
||||
},
|
||||
const hasObservability: CasesCapabilities = {
|
||||
securitySolutionCases: noCasesCapabilities(),
|
||||
observabilityCases: allCasesCapabilities(),
|
||||
generalCases: noCasesCapabilities(),
|
||||
};
|
||||
|
||||
const hasObservabilityCrudTrue = {
|
||||
securitySolutionCases: {
|
||||
crud_cases: false,
|
||||
read_cases: false,
|
||||
},
|
||||
observabilityCases: {
|
||||
crud_cases: true,
|
||||
read_cases: false,
|
||||
},
|
||||
const hasObservabilityWriteTrue: CasesCapabilities = {
|
||||
securitySolutionCases: noCasesCapabilities(),
|
||||
observabilityCases: writeCasesCapabilities(),
|
||||
generalCases: noCasesCapabilities(),
|
||||
};
|
||||
|
||||
const hasSecurityCrudTrue = {
|
||||
securitySolutionCases: {
|
||||
crud_cases: false,
|
||||
read_cases: false,
|
||||
},
|
||||
observabilityCases: {
|
||||
crud_cases: true,
|
||||
read_cases: false,
|
||||
},
|
||||
const hasSecurityWriteTrue: CasesCapabilities = {
|
||||
securitySolutionCases: writeCasesCapabilities(),
|
||||
observabilityCases: noCasesCapabilities(),
|
||||
generalCases: noCasesCapabilities(),
|
||||
};
|
||||
|
||||
const hasObservabilityReadTrue = {
|
||||
securitySolutionCases: {
|
||||
crud_cases: false,
|
||||
read_cases: false,
|
||||
},
|
||||
observabilityCases: {
|
||||
crud_cases: false,
|
||||
read_cases: true,
|
||||
},
|
||||
const hasObservabilityReadTrue: CasesCapabilities = {
|
||||
securitySolutionCases: noCasesCapabilities(),
|
||||
observabilityCases: readCasesCapabilities(),
|
||||
generalCases: noCasesCapabilities(),
|
||||
};
|
||||
|
||||
const hasSecurityReadTrue = {
|
||||
securitySolutionCases: {
|
||||
crud_cases: false,
|
||||
read_cases: true,
|
||||
},
|
||||
observabilityCases: {
|
||||
crud_cases: false,
|
||||
read_cases: false,
|
||||
},
|
||||
const hasSecurityReadTrue: CasesCapabilities = {
|
||||
securitySolutionCases: readCasesCapabilities(),
|
||||
observabilityCases: noCasesCapabilities(),
|
||||
generalCases: noCasesCapabilities(),
|
||||
};
|
||||
|
||||
const hasSecurityAsCrudAndObservabilityAsRead = {
|
||||
securitySolutionCases: {
|
||||
crud_cases: true,
|
||||
},
|
||||
observabilityCases: {
|
||||
read_cases: true,
|
||||
},
|
||||
const hasSecurityWriteAndObservabilityRead: CasesCapabilities = {
|
||||
securitySolutionCases: writeCasesCapabilities(),
|
||||
observabilityCases: readCasesCapabilities(),
|
||||
generalCases: noCasesCapabilities(),
|
||||
};
|
||||
|
||||
describe('canUseCases', () => {
|
||||
it.each([hasAll, hasSecurity, hasObservability, hasSecurityAsCrudAndObservabilityAsRead])(
|
||||
'returns true for both crud and read, if a user has access to both on any solution',
|
||||
it.each([hasAll, hasSecurity, hasObservability, hasSecurityWriteAndObservabilityRead])(
|
||||
'returns true for all permissions, if a user has access to both on any solution',
|
||||
(capability) => {
|
||||
const permissions = canUseCases(capability)();
|
||||
expect(permissions).toStrictEqual({ crud: true, read: true });
|
||||
expect(permissions).toStrictEqual(allCasesPermissions());
|
||||
}
|
||||
);
|
||||
|
||||
it.each([hasObservabilityCrudTrue, hasSecurityCrudTrue])(
|
||||
'returns true for only crud, if a user has access to only crud on any solution',
|
||||
it.each([hasObservabilityWriteTrue, hasSecurityWriteTrue])(
|
||||
'returns true for only write, if a user has access to only write on any solution',
|
||||
(capability) => {
|
||||
const permissions = canUseCases(capability)();
|
||||
expect(permissions).toStrictEqual({ crud: true, read: false });
|
||||
expect(permissions).toStrictEqual(writeCasesPermissions());
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -131,15 +98,15 @@ describe('canUseCases', () => {
|
|||
'returns true for only read, if a user has access to only read on any solution',
|
||||
(capability) => {
|
||||
const permissions = canUseCases(capability)();
|
||||
expect(permissions).toStrictEqual({ crud: false, read: true });
|
||||
expect(permissions).toStrictEqual(readCasesPermissions());
|
||||
}
|
||||
);
|
||||
|
||||
it.each([hasNone, {}])(
|
||||
'returns false for both, if a user has access to no solution',
|
||||
'returns false for all permissions, if a user has access to no solution',
|
||||
(capability) => {
|
||||
const permissions = canUseCases(capability)();
|
||||
expect(permissions).toStrictEqual({ crud: false, read: false });
|
||||
expect(permissions).toStrictEqual(noCasesPermissions());
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
@ -6,9 +6,19 @@
|
|||
*/
|
||||
|
||||
import type { ApplicationStart } from '@kbn/core/public';
|
||||
import { OBSERVABILITY_OWNER, SECURITY_SOLUTION_OWNER } from '../../../common/constants';
|
||||
import {
|
||||
FEATURE_ID,
|
||||
GENERAL_CASES_OWNER,
|
||||
OBSERVABILITY_OWNER,
|
||||
SECURITY_SOLUTION_OWNER,
|
||||
} from '../../../common/constants';
|
||||
import { getUICapabilities } from './capabilities';
|
||||
import { CasesPermissions } from '../../../common';
|
||||
|
||||
export type CasesOwners = typeof SECURITY_SOLUTION_OWNER | typeof OBSERVABILITY_OWNER;
|
||||
export type CasesOwners =
|
||||
| typeof SECURITY_SOLUTION_OWNER
|
||||
| typeof OBSERVABILITY_OWNER
|
||||
| typeof GENERAL_CASES_OWNER;
|
||||
|
||||
/*
|
||||
* Returns an object denoting the current user's ability to read and crud cases.
|
||||
|
@ -16,14 +26,44 @@ export type CasesOwners = typeof SECURITY_SOLUTION_OWNER | typeof OBSERVABILITY_
|
|||
* then crud or read is set to true.
|
||||
* Permissions for a specific owners can be found by passing an owner array
|
||||
*/
|
||||
|
||||
export const canUseCases =
|
||||
(capabilities: Partial<ApplicationStart['capabilities']>) =>
|
||||
(
|
||||
owners: CasesOwners[] = [OBSERVABILITY_OWNER, SECURITY_SOLUTION_OWNER]
|
||||
): { crud: boolean; read: boolean } => ({
|
||||
crud:
|
||||
(capabilities && owners.some((owner) => capabilities[`${owner}Cases`]?.crud_cases)) ?? false,
|
||||
read:
|
||||
(capabilities && owners.some((owner) => capabilities[`${owner}Cases`]?.read_cases)) ?? false,
|
||||
});
|
||||
owners: CasesOwners[] = [OBSERVABILITY_OWNER, SECURITY_SOLUTION_OWNER, GENERAL_CASES_OWNER]
|
||||
): CasesPermissions => {
|
||||
const aggregatedPermissions = owners.reduce<CasesPermissions>(
|
||||
(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;
|
||||
acc.delete = acc.delete || userCapabilitiesForOwner.delete;
|
||||
acc.push = acc.push || userCapabilitiesForOwner.push;
|
||||
const allFromAcc = acc.create && acc.read && acc.update && acc.delete && acc.push;
|
||||
acc.all = acc.all || userCapabilitiesForOwner.all || allFromAcc;
|
||||
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
all: false,
|
||||
create: false,
|
||||
read: false,
|
||||
update: false,
|
||||
delete: false,
|
||||
push: false,
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
...aggregatedPermissions,
|
||||
};
|
||||
};
|
||||
|
||||
const getFeatureID = (owner: CasesOwners) => {
|
||||
if (owner === GENERAL_CASES_OWNER) {
|
||||
return FEATURE_ID;
|
||||
}
|
||||
|
||||
return `${owner}Cases`;
|
||||
};
|
||||
|
|
104
x-pack/plugins/cases/public/client/helpers/capabilities.test.ts
Normal file
104
x-pack/plugins/cases/public/client/helpers/capabilities.test.ts
Normal file
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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 { getUICapabilities } from './capabilities';
|
||||
|
||||
describe('getUICapabilities', () => {
|
||||
it('returns false for all fields when the feature cannot be found', () => {
|
||||
expect(getUICapabilities(undefined)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"all": false,
|
||||
"create": false,
|
||||
"delete": false,
|
||||
"push": false,
|
||||
"read": false,
|
||||
"update": false,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns false for all fields when the capabilities are not passed in', () => {
|
||||
expect(getUICapabilities()).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"all": false,
|
||||
"create": false,
|
||||
"delete": false,
|
||||
"push": false,
|
||||
"read": false,
|
||||
"update": false,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns true for create when it is set to true in the ui capabilities', () => {
|
||||
expect(getUICapabilities({ create_cases: true })).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"all": false,
|
||||
"create": true,
|
||||
"delete": false,
|
||||
"push": false,
|
||||
"read": false,
|
||||
"update": false,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns false for all fields when the ui capabilities are false', () => {
|
||||
expect(
|
||||
getUICapabilities({
|
||||
create_cases: false,
|
||||
read_cases: false,
|
||||
update_cases: false,
|
||||
delete_cases: false,
|
||||
push_cases: false,
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"all": false,
|
||||
"create": false,
|
||||
"delete": false,
|
||||
"push": false,
|
||||
"read": false,
|
||||
"update": false,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns false for all fields when the ui capabilities is an empty object', () => {
|
||||
expect(getUICapabilities({})).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"all": false,
|
||||
"create": false,
|
||||
"delete": false,
|
||||
"push": false,
|
||||
"read": false,
|
||||
"update": false,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns false for the all field when a single field is false', () => {
|
||||
expect(
|
||||
getUICapabilities({
|
||||
create_cases: false,
|
||||
read_cases: true,
|
||||
update_cases: true,
|
||||
delete_cases: true,
|
||||
push_cases: true,
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"all": false,
|
||||
"create": false,
|
||||
"delete": true,
|
||||
"push": true,
|
||||
"read": true,
|
||||
"update": true,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -5,19 +5,31 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export interface CasesPermissions {
|
||||
all: boolean;
|
||||
read: boolean;
|
||||
}
|
||||
import { CasesPermissions } from '../../../common';
|
||||
import {
|
||||
CREATE_CASES_CAPABILITY,
|
||||
DELETE_CASES_CAPABILITY,
|
||||
PUSH_CASES_CAPABILITY,
|
||||
READ_CASES_CAPABILITY,
|
||||
UPDATE_CASES_CAPABILITY,
|
||||
} from '../../../common/constants';
|
||||
|
||||
export const getUICapabilities = (
|
||||
featureCapabilities: Partial<Record<string, boolean | Record<string, boolean>>>
|
||||
featureCapabilities?: Partial<Record<string, boolean | Record<string, boolean>>>
|
||||
): CasesPermissions => {
|
||||
const read = !!featureCapabilities?.read_cases;
|
||||
const all = !!featureCapabilities?.crud_cases;
|
||||
const create = !!featureCapabilities?.[CREATE_CASES_CAPABILITY];
|
||||
const read = !!featureCapabilities?.[READ_CASES_CAPABILITY];
|
||||
const update = !!featureCapabilities?.[UPDATE_CASES_CAPABILITY];
|
||||
const deletePriv = !!featureCapabilities?.[DELETE_CASES_CAPABILITY];
|
||||
const push = !!featureCapabilities?.[PUSH_CASES_CAPABILITY];
|
||||
const all = create && read && update && deletePriv && push;
|
||||
|
||||
return {
|
||||
all,
|
||||
create,
|
||||
read,
|
||||
update,
|
||||
delete: deletePriv,
|
||||
push,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -9,7 +9,7 @@ import React from 'react';
|
|||
import { renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import { useApplicationCapabilities } from './hooks';
|
||||
import { TestProviders } from '../../mock';
|
||||
import { allCasesPermissions, TestProviders } from '../../mock';
|
||||
|
||||
describe('hooks', () => {
|
||||
describe('useApplicationCapabilities', () => {
|
||||
|
@ -23,7 +23,7 @@ describe('hooks', () => {
|
|||
|
||||
expect(result.current).toEqual({
|
||||
actions: { crud: true, read: true },
|
||||
generalCases: { all: true, read: true },
|
||||
generalCases: allCasesPermissions(),
|
||||
visualize: { crud: true, read: true },
|
||||
dashboard: { crud: true, read: true },
|
||||
});
|
||||
|
|
|
@ -12,13 +12,14 @@ import { i18n } from '@kbn/i18n';
|
|||
|
||||
import { AuthenticatedUser } from '@kbn/security-plugin/common/model';
|
||||
import { NavigateToAppOptions } from '@kbn/core/public';
|
||||
import { CasesPermissions, getUICapabilities } from '../../../client/helpers/capabilities';
|
||||
import { getUICapabilities } from '../../../client/helpers/capabilities';
|
||||
import { convertToCamelCase } from '../../../api/utils';
|
||||
import {
|
||||
FEATURE_ID,
|
||||
DEFAULT_DATE_FORMAT,
|
||||
DEFAULT_DATE_FORMAT_TZ,
|
||||
} from '../../../../common/constants';
|
||||
import { CasesPermissions } from '../../../../common';
|
||||
import { StartServices } from '../../../types';
|
||||
import { useUiSetting, useKibana } from './kibana_react';
|
||||
|
||||
|
@ -187,7 +188,11 @@ export const useApplicationCapabilities = (): UseApplicationCapabilities => {
|
|||
actions: { crud: !!capabilities.actions?.save, read: !!capabilities.actions?.show },
|
||||
generalCases: {
|
||||
all: permissions.all,
|
||||
create: permissions.create,
|
||||
read: permissions.read,
|
||||
update: permissions.update,
|
||||
delete: permissions.delete,
|
||||
push: permissions.push,
|
||||
},
|
||||
visualize: { crud: !!capabilities.visualize?.save, read: !!capabilities.visualize?.show },
|
||||
dashboard: {
|
||||
|
@ -203,7 +208,11 @@ export const useApplicationCapabilities = (): UseApplicationCapabilities => {
|
|||
capabilities.visualize?.save,
|
||||
capabilities.visualize?.show,
|
||||
permissions.all,
|
||||
permissions.create,
|
||||
permissions.read,
|
||||
permissions.update,
|
||||
permissions.delete,
|
||||
permissions.push,
|
||||
]
|
||||
);
|
||||
};
|
||||
|
|
|
@ -53,7 +53,13 @@ export const createStartServicesMock = (): StartServices => {
|
|||
services.application.capabilities = {
|
||||
...services.application.capabilities,
|
||||
actions: { save: true, show: true },
|
||||
generalCases: { crud_cases: true, read_cases: true },
|
||||
generalCases: {
|
||||
create_cases: true,
|
||||
read_cases: true,
|
||||
update_cases: true,
|
||||
delete_cases: true,
|
||||
push_cases: true,
|
||||
},
|
||||
visualize: { save: true, show: true },
|
||||
dashboard: { show: true, createNew: true },
|
||||
};
|
||||
|
|
|
@ -14,7 +14,7 @@ import { render as reactRender, RenderOptions, RenderResult } from '@testing-lib
|
|||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { SECURITY_SOLUTION_OWNER } from '../../../common/constants';
|
||||
import { CasesFeatures } from '../../../common/ui/types';
|
||||
import { CasesCapabilities, CasesFeatures, CasesPermissions } from '../../../common/ui/types';
|
||||
import { CasesProvider } from '../../components/cases_context';
|
||||
import {
|
||||
createKibanaContextProviderMock,
|
||||
|
@ -23,7 +23,6 @@ import {
|
|||
import { FieldHook } from '../shared_imports';
|
||||
import { StartServices } from '../../types';
|
||||
import { ReleasePhase } from '../../components/types';
|
||||
import { CasesPermissions } from '../../client/helpers/capabilities';
|
||||
import { ExternalReferenceAttachmentTypeRegistry } from '../../client/attachment_framework/external_reference_registry';
|
||||
import { PersistableStateAttachmentTypeRegistry } from '../../client/attachment_framework/persistable_state_registry';
|
||||
|
||||
|
@ -101,19 +100,66 @@ export const testQueryClient = new QueryClient({
|
|||
},
|
||||
});
|
||||
|
||||
export const buildCasesPermissions = (overrides: Partial<CasesPermissions> = {}) => {
|
||||
export const allCasesPermissions = () => buildCasesPermissions();
|
||||
export const noCasesPermissions = () =>
|
||||
buildCasesPermissions({ read: false, create: false, update: false, delete: false, push: false });
|
||||
export const readCasesPermissions = () =>
|
||||
buildCasesPermissions({ read: true, create: false, update: false, delete: false, push: false });
|
||||
export const noCreateCasesPermissions = () => buildCasesPermissions({ create: false });
|
||||
export const noUpdateCasesPermissions = () => buildCasesPermissions({ update: false });
|
||||
export const noPushCasesPermissions = () => buildCasesPermissions({ push: false });
|
||||
export const noDeleteCasesPermissions = () => buildCasesPermissions({ delete: false });
|
||||
export const writeCasesPermissions = () => buildCasesPermissions({ read: false });
|
||||
|
||||
export const buildCasesPermissions = (overrides: Partial<Omit<CasesPermissions, 'all'>> = {}) => {
|
||||
const create = overrides.create ?? true;
|
||||
const read = overrides.read ?? true;
|
||||
const all = overrides.all ?? true;
|
||||
const update = overrides.update ?? true;
|
||||
const deletePermissions = overrides.delete ?? true;
|
||||
const push = overrides.push ?? true;
|
||||
const all = create && read && update && deletePermissions && push;
|
||||
|
||||
return {
|
||||
all,
|
||||
create,
|
||||
read,
|
||||
update,
|
||||
delete: deletePermissions,
|
||||
push,
|
||||
};
|
||||
};
|
||||
|
||||
export const allCasesPermissions = () => buildCasesPermissions();
|
||||
export const noCasesPermissions = () => buildCasesPermissions({ read: false, all: false });
|
||||
export const readCasesPermissions = () => buildCasesPermissions({ all: false });
|
||||
export const allCasesCapabilities = () => buildCasesCapabilities();
|
||||
export const noCasesCapabilities = () =>
|
||||
buildCasesCapabilities({
|
||||
create_cases: false,
|
||||
read_cases: false,
|
||||
update_cases: false,
|
||||
delete_cases: false,
|
||||
push_cases: false,
|
||||
});
|
||||
export const readCasesCapabilities = () =>
|
||||
buildCasesCapabilities({
|
||||
create_cases: false,
|
||||
update_cases: false,
|
||||
delete_cases: false,
|
||||
push_cases: false,
|
||||
});
|
||||
export const writeCasesCapabilities = () => {
|
||||
return buildCasesCapabilities({
|
||||
read_cases: false,
|
||||
});
|
||||
};
|
||||
|
||||
export const buildCasesCapabilities = (overrides?: Partial<CasesCapabilities>) => {
|
||||
return {
|
||||
create_cases: overrides?.create_cases ?? true,
|
||||
read_cases: overrides?.read_cases ?? true,
|
||||
update_cases: overrides?.update_cases ?? true,
|
||||
delete_cases: overrides?.delete_cases ?? true,
|
||||
push_cases: overrides?.push_cases ?? true,
|
||||
};
|
||||
};
|
||||
|
||||
export const createAppMockRenderer = ({
|
||||
features,
|
||||
|
|
|
@ -10,7 +10,7 @@ import { mount } from 'enzyme';
|
|||
import { waitFor, act } from '@testing-library/react';
|
||||
import { noop } from 'lodash/fp';
|
||||
|
||||
import { TestProviders } from '../../common/mock';
|
||||
import { noCreateCasesPermissions, TestProviders } from '../../common/mock';
|
||||
|
||||
import { CommentRequest, CommentType } from '../../../common/api';
|
||||
import { SECURITY_SOLUTION_OWNER } from '../../../common/constants';
|
||||
|
@ -113,13 +113,13 @@ describe('AddComment ', () => {
|
|||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should hide the component when the user does not have crud permissions', () => {
|
||||
it('should hide the component when the user does not have create permissions', () => {
|
||||
useCreateAttachmentsMock.mockImplementation(() => ({
|
||||
...defaultResponse,
|
||||
isLoading: true,
|
||||
}));
|
||||
const wrapper = mount(
|
||||
<TestProviders permissions={{ all: false, read: false }}>
|
||||
<TestProviders permissions={noCreateCasesPermissions()}>
|
||||
<AddComment {...{ ...addCommentProps }} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
|
|
@ -147,7 +147,7 @@ export const AddComment = React.memo(
|
|||
return (
|
||||
<span id="add-comment-permLink">
|
||||
{isLoading && showLoading && <MySpinner data-test-subj="loading-spinner" size="xl" />}
|
||||
{permissions.all && (
|
||||
{permissions.create && (
|
||||
<Form form={form}>
|
||||
<UseField
|
||||
path={fieldName}
|
||||
|
|
|
@ -13,7 +13,12 @@ import { renderHook } from '@testing-library/react-hooks';
|
|||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import '../../common/mock/match_media';
|
||||
import { AppMockRenderer, createAppMockRenderer, TestProviders } from '../../common/mock';
|
||||
import {
|
||||
AppMockRenderer,
|
||||
createAppMockRenderer,
|
||||
noDeleteCasesPermissions,
|
||||
TestProviders,
|
||||
} from '../../common/mock';
|
||||
import { casesStatus, useGetCasesMockState, mockCase, connectorsMock } from '../../containers/mock';
|
||||
|
||||
import { StatusAll } from '../../../common/ui/types';
|
||||
|
@ -503,6 +508,20 @@ describe('AllCasesListGeneric', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should not render table utility bar when the user does not have permissions to delete', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders permissions={noDeleteCasesPermissions()}>
|
||||
<AllCasesList isSelectorView={true} />
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find('[data-test-subj="case-table-selected-case-count"]').exists()).toBe(
|
||||
false
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="case-table-bulk-actions"]').exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render metrics when isSelectorView=false', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
|
|
|
@ -185,7 +185,11 @@ export const AllCasesList = React.memo<AllCasesListProps>(
|
|||
[deselectCases, setFilterOptions, refreshCases, setQueryParams]
|
||||
);
|
||||
|
||||
const showActions = permissions.all && !isSelectorView;
|
||||
/**
|
||||
* At the time of changing this from all to delete the only bulk action we have is to delete. When we add more
|
||||
* actions we'll need to revisit this to allow more granular checks around the bulk actions.
|
||||
*/
|
||||
const showActions = permissions.delete && !isSelectorView;
|
||||
|
||||
const columns = useCasesColumns({
|
||||
filterStatus: filterOptions.status ?? StatusAll,
|
||||
|
|
|
@ -319,7 +319,7 @@ export const useCasesColumns = ({
|
|||
return (
|
||||
<StatusContextMenu
|
||||
currentStatus={theCase.status}
|
||||
disabled={!permissions.all || isLoadingUpdateCase}
|
||||
disabled={!permissions.update || isLoadingUpdateCase}
|
||||
onStatusChanged={(status) =>
|
||||
handleDispatchUpdate({
|
||||
updateKey: 'status',
|
||||
|
@ -372,7 +372,7 @@ export const useCasesColumns = ({
|
|||
},
|
||||
]
|
||||
: []),
|
||||
...(permissions.all && !isSelectorView
|
||||
...(permissions.delete && !isSelectorView
|
||||
? [
|
||||
{
|
||||
name: (
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
|
||||
import { AppMockRenderer, buildCasesPermissions, createAppMockRenderer } from '../../common/mock';
|
||||
import { CasesTableHeader } from './header';
|
||||
|
||||
describe('CasesTableHeader', () => {
|
||||
let appMockRender: AppMockRenderer;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
appMockRender = createAppMockRenderer();
|
||||
});
|
||||
|
||||
it('displays the create new case button when the user has create privileges', () => {
|
||||
appMockRender = createAppMockRenderer({
|
||||
permissions: buildCasesPermissions({ update: false, create: true }),
|
||||
});
|
||||
const result = appMockRender.render(<CasesTableHeader actionsErrors={[]} />);
|
||||
|
||||
expect(result.getByTestId('createNewCaseBtn')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not display the create new case button when the user does not have create privileges', () => {
|
||||
appMockRender = createAppMockRenderer({
|
||||
permissions: buildCasesPermissions({ create: false }),
|
||||
});
|
||||
const result = appMockRender.render(<CasesTableHeader actionsErrors={[]} />);
|
||||
|
||||
expect(result.queryByTestId('createNewCaseBtn')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays the configure button when the user has update privileges', () => {
|
||||
appMockRender = createAppMockRenderer({
|
||||
permissions: buildCasesPermissions({ create: false, update: true }),
|
||||
});
|
||||
const result = appMockRender.render(<CasesTableHeader actionsErrors={[]} />);
|
||||
|
||||
expect(result.getByTestId('configure-case-button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not display the configure button when the user does not have update privileges', () => {
|
||||
appMockRender = createAppMockRenderer({
|
||||
permissions: buildCasesPermissions({ update: false }),
|
||||
});
|
||||
const result = appMockRender.render(<CasesTableHeader actionsErrors={[]} />);
|
||||
|
||||
expect(result.queryByTestId('configure-case-button')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -6,12 +6,11 @@
|
|||
*/
|
||||
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { EuiFlexGroup } from '@elastic/eui';
|
||||
import { HeaderPage } from '../header_page';
|
||||
import * as i18n from './translations';
|
||||
import { ErrorMessage } from '../use_push_to_service/callout/types';
|
||||
import { NavButtons } from './nav_buttons';
|
||||
import { useCasesContext } from '../cases_context/use_cases_context';
|
||||
|
||||
interface OwnProps {
|
||||
actionsErrors: ErrorMessage[];
|
||||
|
@ -20,8 +19,6 @@ interface OwnProps {
|
|||
type Props = OwnProps;
|
||||
|
||||
export const CasesTableHeader: FunctionComponent<Props> = ({ actionsErrors }) => {
|
||||
const { permissions } = useCasesContext();
|
||||
|
||||
return (
|
||||
<HeaderPage title={i18n.PAGE_TITLE} border data-test-subj="cases-all-title">
|
||||
<EuiFlexGroup
|
||||
|
@ -30,11 +27,7 @@ export const CasesTableHeader: FunctionComponent<Props> = ({ actionsErrors }) =>
|
|||
wrap={true}
|
||||
data-test-subj="all-cases-header"
|
||||
>
|
||||
{permissions.all ? (
|
||||
<EuiFlexItem>
|
||||
<NavButtons actionsErrors={actionsErrors} />
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
<NavButtons actionsErrors={actionsErrors} />
|
||||
</EuiFlexGroup>
|
||||
</HeaderPage>
|
||||
);
|
||||
|
|
|
@ -10,7 +10,12 @@ import { mount } from 'enzyme';
|
|||
import { waitFor } from '@testing-library/react';
|
||||
|
||||
import { AllCases } from '.';
|
||||
import { TestProviders } from '../../common/mock';
|
||||
import {
|
||||
AppMockRenderer,
|
||||
createAppMockRenderer,
|
||||
noCreateCasesPermissions,
|
||||
TestProviders,
|
||||
} from '../../common/mock';
|
||||
import { useGetReporters } from '../../containers/use_get_reporters';
|
||||
import { useGetActionLicense } from '../../containers/use_get_action_license';
|
||||
import { casesStatus, connectorsMock, useGetCasesMockState } from '../../containers/mock';
|
||||
|
@ -79,8 +84,39 @@ describe('AllCases', () => {
|
|||
useGetCasesMock.mockReturnValue(defaultGetCases);
|
||||
});
|
||||
|
||||
let appMockRender: AppMockRenderer;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
appMockRender = createAppMockRenderer();
|
||||
});
|
||||
|
||||
describe('empty table', () => {
|
||||
beforeEach(() => {
|
||||
useGetCasesMock.mockReturnValue({
|
||||
...defaultGetCases,
|
||||
data: {
|
||||
...defaultGetCases.data,
|
||||
cases: [],
|
||||
total: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the create new case link when the user has create privileges', async () => {
|
||||
const result = appMockRender.render(<AllCases />);
|
||||
await waitFor(() => {
|
||||
expect(result.getByTestId('cases-table-add-case')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render the create new case link when the user does not have create privileges', async () => {
|
||||
appMockRender = createAppMockRenderer({ permissions: noCreateCasesPermissions() });
|
||||
const result = appMockRender.render(<AllCases />);
|
||||
await waitFor(() => {
|
||||
expect(result.queryByTestId('cases-table-add-case')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the stats', async () => {
|
||||
|
|
|
@ -13,6 +13,7 @@ import * as i18n from './translations';
|
|||
import { ConfigureCaseButton, LinkButton } from '../links';
|
||||
import { ErrorMessage } from '../use_push_to_service/callout/types';
|
||||
import { useCreateCaseNavigation } from '../../common/navigation';
|
||||
import { useCasesContext } from '../cases_context/use_cases_context';
|
||||
|
||||
const ButtonFlexGroup = styled(EuiFlexGroup)`
|
||||
${({ theme }) => css`
|
||||
|
@ -31,6 +32,7 @@ interface OwnProps {
|
|||
type Props = OwnProps;
|
||||
|
||||
export const NavButtons: FunctionComponent<Props> = ({ actionsErrors }) => {
|
||||
const { permissions } = useCasesContext();
|
||||
const { getCreateCaseUrl, navigateToCreateCase } = useCreateCaseNavigation();
|
||||
const navigateToCreateCaseClick = useCallback(
|
||||
(e) => {
|
||||
|
@ -39,29 +41,40 @@ export const NavButtons: FunctionComponent<Props> = ({ actionsErrors }) => {
|
|||
},
|
||||
[navigateToCreateCase]
|
||||
);
|
||||
|
||||
if (!permissions.create && !permissions.update) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ButtonFlexGroup responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ConfigureCaseButton
|
||||
label={i18n.CONFIGURE_CASES_BUTTON}
|
||||
isDisabled={!isEmpty(actionsErrors)}
|
||||
showToolTip={!isEmpty(actionsErrors)}
|
||||
msgTooltip={!isEmpty(actionsErrors) ? <>{actionsErrors[0].description}</> : <></>}
|
||||
titleTooltip={!isEmpty(actionsErrors) ? actionsErrors[0].title : ''}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<LinkButton
|
||||
fill
|
||||
onClick={navigateToCreateCaseClick}
|
||||
href={getCreateCaseUrl()}
|
||||
iconType="plusInCircle"
|
||||
data-test-subj="createNewCaseBtn"
|
||||
>
|
||||
{i18n.CREATE_CASE_TITLE}
|
||||
</LinkButton>
|
||||
</EuiFlexItem>
|
||||
</ButtonFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<ButtonFlexGroup responsive={false}>
|
||||
{permissions.update && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<ConfigureCaseButton
|
||||
label={i18n.CONFIGURE_CASES_BUTTON}
|
||||
isDisabled={!isEmpty(actionsErrors)}
|
||||
showToolTip={!isEmpty(actionsErrors)}
|
||||
msgTooltip={!isEmpty(actionsErrors) ? <>{actionsErrors[0].description}</> : <></>}
|
||||
titleTooltip={!isEmpty(actionsErrors) ? actionsErrors[0].title : ''}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{permissions.create && (
|
||||
<EuiFlexItem>
|
||||
<LinkButton
|
||||
fill
|
||||
onClick={navigateToCreateCaseClick}
|
||||
href={getCreateCaseUrl()}
|
||||
iconType="plusInCircle"
|
||||
data-test-subj="createNewCaseBtn"
|
||||
>
|
||||
{i18n.CREATE_CASE_TITLE}
|
||||
</LinkButton>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</ButtonFlexGroup>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
||||
NavButtons.displayName = 'NavButtons';
|
||||
|
|
|
@ -11,7 +11,7 @@ import userEvent from '@testing-library/user-event';
|
|||
import React from 'react';
|
||||
import AllCasesSelectorModal from '.';
|
||||
import { Case, CaseStatuses, StatusAll } from '../../../../common';
|
||||
import { AppMockRenderer, createAppMockRenderer } from '../../../common/mock';
|
||||
import { allCasesPermissions, AppMockRenderer, createAppMockRenderer } from '../../../common/mock';
|
||||
import { useCasesToast } from '../../../common/use_cases_toast';
|
||||
import { alertComment } from '../../../containers/mock';
|
||||
import { useCreateAttachments } from '../../../containers/use_create_attachments';
|
||||
|
@ -64,10 +64,7 @@ describe('use cases add to existing case modal hook', () => {
|
|||
externalReferenceAttachmentTypeRegistry,
|
||||
persistableStateAttachmentTypeRegistry,
|
||||
owner: ['test'],
|
||||
permissions: {
|
||||
all: true,
|
||||
read: true,
|
||||
},
|
||||
permissions: allCasesPermissions(),
|
||||
appId: 'test',
|
||||
appTitle: 'jest',
|
||||
basePath: '/jest',
|
||||
|
|
|
@ -109,11 +109,11 @@ export const CasesTable: FunctionComponent<CasesTableProps> = ({
|
|||
<EuiEmptyPrompt
|
||||
title={<h3>{i18n.NO_CASES}</h3>}
|
||||
titleSize="xs"
|
||||
body={permissions.all ? i18n.NO_CASES_BODY : i18n.NO_CASES_BODY_READ_ONLY}
|
||||
body={permissions.create ? i18n.NO_CASES_BODY : i18n.NO_CASES_BODY_READ_ONLY}
|
||||
actions={
|
||||
permissions.all && (
|
||||
permissions.create && (
|
||||
<LinkButton
|
||||
isDisabled={!permissions.all}
|
||||
isDisabled={!permissions.create}
|
||||
fill
|
||||
size="s"
|
||||
onClick={navigateToCreateCaseClick}
|
||||
|
|
|
@ -10,9 +10,14 @@ import React from 'react';
|
|||
import { MemoryRouterProps } from 'react-router';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { readCasesPermissions, TestProviders } from '../../common/mock';
|
||||
import {
|
||||
noCreateCasesPermissions,
|
||||
noUpdateCasesPermissions,
|
||||
readCasesPermissions,
|
||||
TestProviders,
|
||||
} from '../../common/mock';
|
||||
import { CasesRoutes } from './routes';
|
||||
import { CasesPermissions } from '../../client/helpers/capabilities';
|
||||
import { CasesPermissions } from '../../../common';
|
||||
|
||||
jest.mock('../all_cases', () => ({
|
||||
AllCases: () => <div>{'All cases'}</div>,
|
||||
|
@ -85,8 +90,8 @@ describe('Cases routes', () => {
|
|||
expect(screen.getByText('Create case')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the no privileges page if user is read only', () => {
|
||||
renderWithRouter(['/cases/create'], readCasesPermissions());
|
||||
it('shows the no privileges page if the user does not have create privileges', () => {
|
||||
renderWithRouter(['/cases/create'], noCreateCasesPermissions());
|
||||
expect(screen.getByText('Privileges required')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
@ -97,8 +102,8 @@ describe('Cases routes', () => {
|
|||
expect(screen.getByText('Configure cases')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the no privileges page if user is read only', () => {
|
||||
renderWithRouter(['/cases/configure'], readCasesPermissions());
|
||||
it('shows the no privileges page if the user does not have update privileges', () => {
|
||||
renderWithRouter(['/cases/configure'], noUpdateCasesPermissions());
|
||||
expect(screen.getByText('Privileges required')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -58,7 +58,7 @@ const CasesRoutesComponent: React.FC<CasesRoutesProps> = ({
|
|||
</Route>
|
||||
|
||||
<Route path={getCreateCasePath(basePath)}>
|
||||
{permissions.all ? (
|
||||
{permissions.create ? (
|
||||
<CreateCase
|
||||
onSuccess={onCreateCaseSuccess}
|
||||
onCancel={navigateToAllCases}
|
||||
|
@ -70,7 +70,7 @@ const CasesRoutesComponent: React.FC<CasesRoutesProps> = ({
|
|||
</Route>
|
||||
|
||||
<Route path={getCasesConfigurePath(basePath)}>
|
||||
{permissions.all ? (
|
||||
{permissions.update ? (
|
||||
<ConfigureCases />
|
||||
) : (
|
||||
<NoPrivilegesPage pageName={i18n.CONFIGURE_CASES_PAGE_NAME} />
|
||||
|
|
|
@ -7,8 +7,13 @@
|
|||
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import { OBSERVABILITY_OWNER, SECURITY_SOLUTION_OWNER } from '../../../common/constants';
|
||||
import { APP_ID, OBSERVABILITY_OWNER, SECURITY_SOLUTION_OWNER } from '../../../common/constants';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import {
|
||||
allCasesCapabilities,
|
||||
noCasesCapabilities,
|
||||
readCasesCapabilities,
|
||||
} from '../../common/mock';
|
||||
import { useAvailableCasesOwners } from './use_available_owners';
|
||||
|
||||
jest.mock('../../common/lib/kibana');
|
||||
|
@ -16,30 +21,19 @@ jest.mock('../../common/lib/kibana');
|
|||
const useKibanaMock = useKibana as jest.MockedFunction<typeof useKibana>;
|
||||
|
||||
const hasAll = {
|
||||
securitySolutionCases: {
|
||||
crud_cases: true,
|
||||
read_cases: true,
|
||||
},
|
||||
observabilityCases: {
|
||||
crud_cases: true,
|
||||
read_cases: true,
|
||||
},
|
||||
securitySolutionCases: allCasesCapabilities(),
|
||||
observabilityCases: allCasesCapabilities(),
|
||||
generalCases: allCasesCapabilities(),
|
||||
};
|
||||
|
||||
const hasSecurityAsCrudAndObservabilityAsRead = {
|
||||
securitySolutionCases: {
|
||||
crud_cases: true,
|
||||
},
|
||||
observabilityCases: {
|
||||
read_cases: true,
|
||||
},
|
||||
const secAllObsReadGenNone = {
|
||||
securitySolutionCases: allCasesCapabilities(),
|
||||
observabilityCases: readCasesCapabilities(),
|
||||
generalCases: noCasesCapabilities(),
|
||||
};
|
||||
|
||||
const unrelatedFeatures = {
|
||||
bogusCapability: {
|
||||
crud_cases: true,
|
||||
read_cases: true,
|
||||
},
|
||||
bogusCapability: allCasesCapabilities(),
|
||||
};
|
||||
|
||||
const mockKibana = (permissionType: unknown = hasAll) => {
|
||||
|
@ -57,7 +51,7 @@ describe('useAvailableCasesOwners correctly grabs user case permissions', () =>
|
|||
mockKibana();
|
||||
const { result } = renderHook(useAvailableCasesOwners);
|
||||
|
||||
expect(result.current).toEqual([SECURITY_SOLUTION_OWNER, OBSERVABILITY_OWNER]);
|
||||
expect(result.current).toEqual([SECURITY_SOLUTION_OWNER, OBSERVABILITY_OWNER, APP_ID]);
|
||||
});
|
||||
|
||||
it('returns no owner types if user has access to none', () => {
|
||||
|
@ -68,17 +62,17 @@ describe('useAvailableCasesOwners correctly grabs user case permissions', () =>
|
|||
});
|
||||
|
||||
it('returns only the permission it should have with CRUD as default', () => {
|
||||
mockKibana(hasSecurityAsCrudAndObservabilityAsRead);
|
||||
mockKibana(secAllObsReadGenNone);
|
||||
const { result } = renderHook(useAvailableCasesOwners);
|
||||
|
||||
expect(result.current).toEqual([SECURITY_SOLUTION_OWNER]);
|
||||
});
|
||||
|
||||
it('returns only the permission it should have with READ as default', () => {
|
||||
mockKibana(hasSecurityAsCrudAndObservabilityAsRead);
|
||||
const { result } = renderHook(() => useAvailableCasesOwners('read'));
|
||||
mockKibana(secAllObsReadGenNone);
|
||||
const { result } = renderHook(() => useAvailableCasesOwners(['read']));
|
||||
|
||||
expect(result.current).toEqual([OBSERVABILITY_OWNER]);
|
||||
expect(result.current).toEqual([SECURITY_SOLUTION_OWNER, OBSERVABILITY_OWNER]);
|
||||
});
|
||||
|
||||
it('returns no owners when the capabilities does not contain valid entries', () => {
|
||||
|
|
|
@ -5,27 +5,45 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { APP_ID, FEATURE_ID } from '../../../common/constants';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { CasesPermissions } from '../../containers/types';
|
||||
|
||||
type Capability = Omit<keyof CasesPermissions, 'all'>;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param level : 'crud' | 'read' (default: 'crud')
|
||||
*
|
||||
* `securitySolution` owner uses cases capability feature id: 'securitySolutionCases'; //owner
|
||||
* `observability` owner uses cases capability feature id: 'observabilityCases';
|
||||
* both solutions use `crud_cases` and `read_cases` capability names
|
||||
* @param capabilities : specifies the requirements for a valid owner, an owner will be included if it has the specified
|
||||
* capabilities
|
||||
**/
|
||||
|
||||
export const useAvailableCasesOwners = (level: 'crud' | 'read' = 'crud'): string[] => {
|
||||
const { capabilities } = useKibana().services.application;
|
||||
const capabilityName = `${level}_cases`;
|
||||
return Object.entries(capabilities).reduce(
|
||||
(availableOwners: string[], [featureId, capability]) => {
|
||||
if (featureId.endsWith('Cases') && !!capability[capabilityName]) {
|
||||
availableOwners.push(featureId.replace('Cases', ''));
|
||||
export const useAvailableCasesOwners = (
|
||||
capabilities: Capability[] = ['create', 'read', 'update', 'delete', 'push']
|
||||
): string[] => {
|
||||
const { capabilities: kibanaCapabilities } = useKibana().services.application;
|
||||
|
||||
return Object.entries(kibanaCapabilities).reduce(
|
||||
(availableOwners: string[], [featureId, kibananCapability]) => {
|
||||
if (!featureId.endsWith('Cases')) {
|
||||
return availableOwners;
|
||||
}
|
||||
for (const cap of capabilities) {
|
||||
const hasCapability = !!kibananCapability[`${cap}_cases`];
|
||||
if (!hasCapability) {
|
||||
return availableOwners;
|
||||
}
|
||||
}
|
||||
availableOwners.push(getOwnerFromFeatureID(featureId));
|
||||
return availableOwners;
|
||||
},
|
||||
[]
|
||||
);
|
||||
};
|
||||
|
||||
const getOwnerFromFeatureID = (featureID: string) => {
|
||||
if (featureID === FEATURE_ID) {
|
||||
return APP_ID;
|
||||
}
|
||||
|
||||
return featureID.replace('Cases', '');
|
||||
};
|
||||
|
|
|
@ -10,6 +10,7 @@ import { useCallback, useEffect } from 'react';
|
|||
import * as i18n from './translations';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { useCasesContext } from '../cases_context/use_cases_context';
|
||||
import { isReadOnlyPermissions } from '../../utils/permissions';
|
||||
|
||||
/**
|
||||
* This component places a read-only icon badge in the header if user only has read permissions
|
||||
|
@ -20,7 +21,7 @@ export function useReadonlyHeader() {
|
|||
|
||||
// if the user is read only then display the glasses badge in the global navigation header
|
||||
const setBadge = useCallback(() => {
|
||||
if (!permissions.all && permissions.read) {
|
||||
if (isReadOnlyPermissions(permissions)) {
|
||||
chrome.setBadge({
|
||||
text: i18n.READ_ONLY_BADGE_TEXT,
|
||||
tooltip: i18n.READ_ONLY_BADGE_TOOLTIP,
|
||||
|
|
|
@ -9,7 +9,7 @@ import React from 'react';
|
|||
import { mount } from 'enzyme';
|
||||
|
||||
import { useDeleteCases } from '../../containers/use_delete_cases';
|
||||
import { TestProviders } from '../../common/mock';
|
||||
import { noDeleteCasesPermissions, TestProviders } from '../../common/mock';
|
||||
import { basicCase, basicPush } from '../../containers/mock';
|
||||
import { Actions } from './actions';
|
||||
import * as i18n from '../case_view/translations';
|
||||
|
@ -67,6 +67,17 @@ describe('CaseView actions', () => {
|
|||
expect(handleToggleModal).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not show trash icon when user does not have deletion privileges', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders permissions={noDeleteCasesPermissions()}>
|
||||
<Actions {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="confirm-delete-case-modal"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('button[data-test-subj="property-actions-ellipses"]').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('toggle delete modal and confirm', () => {
|
||||
useDeleteCasesMock.mockImplementation(() => ({
|
||||
...defaultDeleteState,
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import React, { useMemo } from 'react';
|
||||
import { EuiFlexItem } from '@elastic/eui';
|
||||
import * as i18n from '../case_view/translations';
|
||||
import { useDeleteCases } from '../../containers/use_delete_cases';
|
||||
import { ConfirmDeleteCaseModal } from '../confirm_delete_case';
|
||||
|
@ -14,6 +15,7 @@ import { PropertyActions } from '../property_actions';
|
|||
import { Case } from '../../../common/ui/types';
|
||||
import { CaseService } from '../../containers/use_get_case_user_actions';
|
||||
import { useAllCasesNavigation } from '../../common/navigation';
|
||||
import { useCasesContext } from '../cases_context/use_cases_context';
|
||||
|
||||
interface CaseViewActions {
|
||||
caseData: Case;
|
||||
|
@ -25,14 +27,19 @@ const ActionsComponent: React.FC<CaseViewActions> = ({ caseData, currentExternal
|
|||
const { handleToggleModal, handleOnDeleteConfirm, isDeleted, isDisplayConfirmDeleteModal } =
|
||||
useDeleteCases();
|
||||
const { navigateToAllCases } = useAllCasesNavigation();
|
||||
const { permissions } = useCasesContext();
|
||||
|
||||
const propertyActions = useMemo(
|
||||
() => [
|
||||
{
|
||||
iconType: 'trash',
|
||||
label: i18n.DELETE_CASE(),
|
||||
onClick: handleToggleModal,
|
||||
},
|
||||
...(permissions.delete
|
||||
? [
|
||||
{
|
||||
iconType: 'trash',
|
||||
label: i18n.DELETE_CASE(),
|
||||
onClick: handleToggleModal,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(currentExternalIncident != null && !isEmpty(currentExternalIncident?.externalUrl)
|
||||
? [
|
||||
{
|
||||
|
@ -43,15 +50,20 @@ const ActionsComponent: React.FC<CaseViewActions> = ({ caseData, currentExternal
|
|||
]
|
||||
: []),
|
||||
],
|
||||
[handleToggleModal, currentExternalIncident]
|
||||
[handleToggleModal, currentExternalIncident, permissions.delete]
|
||||
);
|
||||
|
||||
if (isDeleted) {
|
||||
navigateToAllCases();
|
||||
return null;
|
||||
}
|
||||
|
||||
if (propertyActions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexItem grow={false} data-test-subj="case-view-actions">
|
||||
<PropertyActions propertyActions={propertyActions} />
|
||||
<ConfirmDeleteCaseModal
|
||||
caseTitle={caseData.title}
|
||||
|
@ -59,7 +71,7 @@ const ActionsComponent: React.FC<CaseViewActions> = ({ caseData, currentExternal
|
|||
onCancel={handleToggleModal}
|
||||
onConfirm={handleOnDeleteConfirm.bind(null, [{ id: caseData.id, title: caseData.title }])}
|
||||
/>
|
||||
</>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
||||
ActionsComponent.displayName = 'Actions';
|
||||
|
|
|
@ -7,11 +7,17 @@
|
|||
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { render } from '@testing-library/react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { basicCase, caseUserActions, getAlertUserAction } from '../../containers/mock';
|
||||
import { CaseActionBar, CaseActionBarProps } from '.';
|
||||
import { TestProviders } from '../../common/mock';
|
||||
import {
|
||||
allCasesPermissions,
|
||||
noDeleteCasesPermissions,
|
||||
noUpdateCasesPermissions,
|
||||
TestProviders,
|
||||
} from '../../common/mock';
|
||||
import { useGetCaseUserActions } from '../../containers/use_get_case_user_actions';
|
||||
import { useRefreshCaseViewPage } from '../case_view/use_on_refresh_case_view_page';
|
||||
|
||||
|
@ -188,4 +194,56 @@ describe('CaseActionBar', () => {
|
|||
|
||||
expect(getByText('Case opened')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show the change status text when the user has update privileges', () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<CaseActionBar {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(screen.getByTitle('Change status')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not show the change status text when the user does not have update privileges', () => {
|
||||
render(
|
||||
<TestProviders permissions={noUpdateCasesPermissions()}>
|
||||
<CaseActionBar {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(screen.queryByTitle('Change status')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not show the sync alerts toggle when the user does not have update privileges', () => {
|
||||
const { queryByText } = render(
|
||||
<TestProviders permissions={noUpdateCasesPermissions()}>
|
||||
<CaseActionBar {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(queryByText('Sync alerts')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not show the delete item in the menu when the user does not have delete privileges', () => {
|
||||
const { queryByText, queryByTestId } = render(
|
||||
<TestProviders permissions={noDeleteCasesPermissions()}>
|
||||
<CaseActionBar {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(queryByTestId('property-actions-ellipses')).not.toBeInTheDocument();
|
||||
expect(queryByText('Delete case')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show the the delete item in the menu when the user does have delete privileges', () => {
|
||||
const { queryByText } = render(
|
||||
<TestProviders permissions={allCasesPermissions()}>
|
||||
<CaseActionBar {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
userEvent.click(screen.getByTestId('property-actions-ellipses'));
|
||||
expect(queryByText('Delete case')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -107,7 +107,7 @@ const CaseActionBarComponent: React.FC<CaseActionBarProps> = ({
|
|||
<EuiDescriptionListDescription>
|
||||
<StatusContextMenu
|
||||
currentStatus={caseData.status}
|
||||
disabled={!permissions.all || isLoading}
|
||||
disabled={!permissions.update || isLoading}
|
||||
onStatusChanged={onStatusChanged}
|
||||
/>
|
||||
</EuiDescriptionListDescription>
|
||||
|
@ -134,7 +134,7 @@ const CaseActionBarComponent: React.FC<CaseActionBarProps> = ({
|
|||
responsive={false}
|
||||
justifyContent="spaceBetween"
|
||||
>
|
||||
{permissions.all && isSyncAlertsEnabled && (
|
||||
{permissions.update && isSyncAlertsEnabled && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiDescriptionListTitle>
|
||||
<EuiFlexGroup
|
||||
|
@ -172,14 +172,7 @@ const CaseActionBarComponent: React.FC<CaseActionBarProps> = ({
|
|||
</EuiButtonEmpty>
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
{permissions.all && (
|
||||
<EuiFlexItem grow={false} data-test-subj="case-view-actions">
|
||||
<Actions
|
||||
caseData={caseData}
|
||||
currentExternalIncident={currentExternalIncident}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<Actions caseData={caseData} currentExternalIncident={currentExternalIncident} />
|
||||
</EuiFlexGroup>
|
||||
</EuiDescriptionList>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -13,7 +13,11 @@ import {
|
|||
getAlertUserAction,
|
||||
} from '../../../containers/mock';
|
||||
import React from 'react';
|
||||
import { AppMockRenderer, createAppMockRenderer } from '../../../common/mock';
|
||||
import {
|
||||
AppMockRenderer,
|
||||
createAppMockRenderer,
|
||||
noUpdateCasesPermissions,
|
||||
} from '../../../common/mock';
|
||||
import { CaseViewActivity } from './case_view_activity';
|
||||
import { ConnectorTypes } from '../../../../common/api/connectors';
|
||||
import { Case } from '../../../../common';
|
||||
|
@ -110,6 +114,30 @@ describe('Case View Page activity tab', () => {
|
|||
expect(useGetCaseUserActionsMock).toHaveBeenCalledWith(caseData.id, caseData.connector.id);
|
||||
});
|
||||
|
||||
it('should not render the case view status button when the user does not have update permissions', () => {
|
||||
appMockRender = createAppMockRenderer({ permissions: noUpdateCasesPermissions() });
|
||||
|
||||
const result = appMockRender.render(<CaseViewActivity {...caseProps} />);
|
||||
expect(result.getByTestId('case-view-activity')).toBeTruthy();
|
||||
expect(result.getByTestId('user-actions')).toBeTruthy();
|
||||
expect(result.getByTestId('case-tags')).toBeTruthy();
|
||||
expect(result.getByTestId('connector-edit-header')).toBeTruthy();
|
||||
expect(result.queryByTestId('case-view-status-action-button')).not.toBeInTheDocument();
|
||||
expect(useGetCaseUserActionsMock).toHaveBeenCalledWith(caseData.id, caseData.connector.id);
|
||||
});
|
||||
|
||||
it('should disable the severity selector when the user does not have update permissions', () => {
|
||||
appMockRender = createAppMockRenderer({ permissions: noUpdateCasesPermissions() });
|
||||
|
||||
const result = appMockRender.render(<CaseViewActivity {...caseProps} />);
|
||||
expect(result.getByTestId('case-view-activity')).toBeTruthy();
|
||||
expect(result.getByTestId('user-actions')).toBeTruthy();
|
||||
expect(result.getByTestId('case-tags')).toBeTruthy();
|
||||
expect(result.getByTestId('connector-edit-header')).toBeTruthy();
|
||||
expect(result.getByTestId('case-severity-selection')).toBeDisabled();
|
||||
expect(useGetCaseUserActionsMock).toHaveBeenCalledWith(caseData.id, caseData.connector.id);
|
||||
});
|
||||
|
||||
it('should show a loading when is fetching data is true and hide the user actions activity', () => {
|
||||
useGetCaseUserActionsMock.mockReturnValue({
|
||||
...defaultUseGetCaseUserActions,
|
||||
|
|
|
@ -133,7 +133,7 @@ export const CaseViewActivity = ({
|
|||
onShowAlertDetails={onShowAlertDetails}
|
||||
onUpdateField={onUpdateField}
|
||||
statusActionButton={
|
||||
permissions.all ? (
|
||||
permissions.update ? (
|
||||
<StatusActionButton
|
||||
status={caseData.status}
|
||||
onStatusChanged={changeStatus}
|
||||
|
@ -149,7 +149,7 @@ export const CaseViewActivity = ({
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={2}>
|
||||
<SeveritySidebarSelector
|
||||
isDisabled={!permissions.all}
|
||||
isDisabled={!permissions.update}
|
||||
isLoading={isLoading}
|
||||
selectedSeverity={caseData.severity}
|
||||
onSeverityChange={onUpdateSeverity}
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
casesContextReducer,
|
||||
getInitialCasesContextState,
|
||||
} from './cases_context_reducer';
|
||||
import { CasesFeaturesAllRequired, CasesFeatures } from '../../containers/types';
|
||||
import { CasesFeaturesAllRequired, CasesFeatures, CasesPermissions } from '../../containers/types';
|
||||
import { CasesGlobalComponents } from './cases_global_components';
|
||||
import { ReleasePhase } from '../types';
|
||||
import { ExternalReferenceAttachmentTypeRegistry } from '../../client/attachment_framework/external_reference_registry';
|
||||
|
@ -30,10 +30,7 @@ export interface CasesContextValue {
|
|||
owner: string[];
|
||||
appId: string;
|
||||
appTitle: string;
|
||||
permissions: {
|
||||
all: boolean;
|
||||
read: boolean;
|
||||
};
|
||||
permissions: CasesPermissions;
|
||||
basePath: string;
|
||||
features: CasesFeaturesAllRequired;
|
||||
releasePhase: ReleasePhase;
|
||||
|
|
|
@ -10,7 +10,7 @@ import { ReactWrapper, mount } from 'enzyme';
|
|||
import { waitFor } from '@testing-library/react';
|
||||
|
||||
import { ConfigureCases } from '.';
|
||||
import { noCasesPermissions, TestProviders } from '../../common/mock';
|
||||
import { noUpdateCasesPermissions, TestProviders } from '../../common/mock';
|
||||
import { Connectors } from './connectors';
|
||||
import { ClosureOptions } from './closure_options';
|
||||
|
||||
|
@ -188,10 +188,10 @@ describe('ConfigureCases', () => {
|
|||
expect(wrapper.find('[data-test-subj="edit-connector-flyout"]').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('it disables correctly when the user cannot crud', () => {
|
||||
test('it disables correctly when the user cannot update', () => {
|
||||
const newWrapper = mount(<ConfigureCases />, {
|
||||
wrappingComponent: TestProviders,
|
||||
wrappingComponentProps: { permissions: noCasesPermissions() },
|
||||
wrappingComponentProps: { permissions: noUpdateCasesPermissions() },
|
||||
});
|
||||
|
||||
expect(newWrapper.find('button[data-test-subj="dropdown-connectors"]').prop('disabled')).toBe(
|
||||
|
|
|
@ -225,7 +225,7 @@ export const ConfigureCases: React.FC = React.memo(() => {
|
|||
<SectionWrapper>
|
||||
<ClosureOptions
|
||||
closureTypeSelected={closureType}
|
||||
disabled={persistLoading || isLoadingConnectors || !permissions.all}
|
||||
disabled={persistLoading || isLoadingConnectors || !permissions.update}
|
||||
onChangeClosureType={onChangeClosureType}
|
||||
/>
|
||||
</SectionWrapper>
|
||||
|
@ -233,13 +233,13 @@ export const ConfigureCases: React.FC = React.memo(() => {
|
|||
<Connectors
|
||||
actionTypes={actionTypes}
|
||||
connectors={connectors ?? []}
|
||||
disabled={persistLoading || isLoadingConnectors || !permissions.all}
|
||||
disabled={persistLoading || isLoadingConnectors || !permissions.update}
|
||||
handleShowEditFlyout={onClickUpdateConnector}
|
||||
isLoading={isLoadingAny}
|
||||
mappings={mappings}
|
||||
onChangeConnector={onChangeConnector}
|
||||
selectedConnector={connector}
|
||||
updateConnectorDisabled={updateConnectorDisabled || !permissions.all}
|
||||
updateConnectorDisabled={updateConnectorDisabled || !permissions.update}
|
||||
/>
|
||||
</SectionWrapper>
|
||||
{ConnectorAddFlyout}
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
AppMockRenderer,
|
||||
createAppMockRenderer,
|
||||
readCasesPermissions,
|
||||
noPushCasesPermissions,
|
||||
TestProviders,
|
||||
} from '../../common/mock';
|
||||
import { basicCase, basicPush, caseUserActions, connectorsMock } from '../../containers/mock';
|
||||
|
@ -362,6 +363,16 @@ describe('EditConnector ', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('does not show the push button if the user does not have push permissions', async () => {
|
||||
const defaultProps = getDefaultProps();
|
||||
|
||||
appMockRender = createAppMockRenderer({ permissions: noPushCasesPermissions() });
|
||||
const result = appMockRender.render(<EditConnector {...defaultProps} />);
|
||||
await waitFor(() => {
|
||||
expect(result.queryByTestId('has-data-to-push-button')).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not show the edit connectors pencil if the user does not have read access to actions', async () => {
|
||||
const defaultProps = getDefaultProps();
|
||||
const props = { ...defaultProps, connectors: [] };
|
||||
|
@ -376,4 +387,16 @@ describe('EditConnector ', () => {
|
|||
expect(result.queryByTestId('connector-edit')).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not show the edit connectors pencil if the user does not have push permissions', async () => {
|
||||
const defaultProps = getDefaultProps();
|
||||
const props = { ...defaultProps, connectors: [] };
|
||||
appMockRender = createAppMockRenderer({ permissions: noPushCasesPermissions() });
|
||||
|
||||
const result = appMockRender.render(<EditConnector {...props} />);
|
||||
await waitFor(() => {
|
||||
expect(result.getByTestId('connector-edit-header')).toBeInTheDocument();
|
||||
expect(result.queryByTestId('connector-edit')).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -288,7 +288,7 @@ export const EditConnector = React.memo(
|
|||
<h4>{i18n.CONNECTORS}</h4>
|
||||
</EuiFlexItem>
|
||||
{isLoading && <EuiLoadingSpinner data-test-subj="connector-loading" />}
|
||||
{!isLoading && !editConnector && permissions.all && actionsReadCapabilities && (
|
||||
{!isLoading && !editConnector && permissions.push && actionsReadCapabilities && (
|
||||
<EuiFlexItem data-test-subj="connector-edit" grow={false}>
|
||||
<EuiButtonIcon
|
||||
data-test-subj="connector-edit-button"
|
||||
|
@ -316,7 +316,7 @@ export const EditConnector = React.memo(
|
|||
connectors,
|
||||
dataTestSubj: 'caseConnectors',
|
||||
defaultValue: selectedConnector,
|
||||
disabled: !permissions.all,
|
||||
disabled: !permissions.push,
|
||||
idAria: 'caseConnectors',
|
||||
isEdit: editConnector,
|
||||
isLoading,
|
||||
|
@ -372,7 +372,7 @@ export const EditConnector = React.memo(
|
|||
{pushCallouts == null &&
|
||||
!isLoading &&
|
||||
!editConnector &&
|
||||
permissions.all &&
|
||||
permissions.push &&
|
||||
actionsReadCapabilities && (
|
||||
<EuiFlexItem data-test-subj="has-data-to-push-button" grow={false}>
|
||||
<span>{pushButton}</span>
|
||||
|
|
|
@ -44,7 +44,11 @@ exports[`EditableTitle renders 1`] = `
|
|||
],
|
||||
"permissions": Object {
|
||||
"all": true,
|
||||
"create": true,
|
||||
"delete": true,
|
||||
"push": true,
|
||||
"read": true,
|
||||
"update": true,
|
||||
},
|
||||
"persistableStateAttachmentTypeRegistry": PersistableStateAttachmentTypeRegistry {
|
||||
"collection": Map {},
|
||||
|
|
|
@ -44,7 +44,11 @@ exports[`HeaderPage it renders 1`] = `
|
|||
],
|
||||
"permissions": Object {
|
||||
"all": true,
|
||||
"create": true,
|
||||
"delete": true,
|
||||
"push": true,
|
||||
"read": true,
|
||||
"update": true,
|
||||
},
|
||||
"persistableStateAttachmentTypeRegistry": PersistableStateAttachmentTypeRegistry {
|
||||
"collection": Map {},
|
||||
|
|
|
@ -118,7 +118,7 @@ const EditableTitleComponent: React.FC<EditableTitleProps> = ({ onSubmit, isLoad
|
|||
) : (
|
||||
<Title title={title} releasePhase={releasePhase}>
|
||||
{isLoading && <MySpinner data-test-subj="editable-title-loading" />}
|
||||
{!isLoading && permissions.all && (
|
||||
{!isLoading && permissions.update && (
|
||||
<MyEuiButtonIcon
|
||||
aria-label={i18n.EDIT_TITLE_ARIA(title as string)}
|
||||
iconType="pencil"
|
||||
|
|
|
@ -25,7 +25,7 @@ describe('NoCases', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('displays a message without a link to create a case when the user does not have write permissions', () => {
|
||||
it('displays a message without a link to create a case when the user does not have create permissions', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders permissions={readCasesPermissions()}>
|
||||
<NoCases />
|
||||
|
|
|
@ -24,7 +24,7 @@ const NoCasesComponent = () => {
|
|||
[navigateToCreateCase]
|
||||
);
|
||||
|
||||
return permissions.all ? (
|
||||
return permissions.create ? (
|
||||
<>
|
||||
<span>{i18n.NO_CASES}</span>
|
||||
<LinkAnchor
|
||||
|
|
|
@ -107,7 +107,7 @@ describe('TagList ', () => {
|
|||
expect(wrapper.find(`[data-test-subj="tag-pepsi"]`).last().exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('does not render when the user does not have write permissions', () => {
|
||||
it('does not render when the user does not have update permissions', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders permissions={readCasesPermissions()}>
|
||||
<TagList {...defaultProps} />
|
||||
|
|
|
@ -104,7 +104,7 @@ export const TagList = React.memo(({ isLoading, onSubmit, tags }: TagListProps)
|
|||
<h4>{i18n.TAGS}</h4>
|
||||
</EuiFlexItem>
|
||||
{isLoading && <EuiLoadingSpinner data-test-subj="tag-list-loading" />}
|
||||
{!isLoading && permissions.all && (
|
||||
{!isLoading && permissions.update && (
|
||||
<EuiFlexItem data-test-subj="tag-list-edit" grow={false}>
|
||||
<EuiButtonIcon
|
||||
data-test-subj="tag-list-edit-button"
|
||||
|
|
|
@ -11,7 +11,7 @@ import { render, screen } from '@testing-library/react';
|
|||
|
||||
import '../../common/mock/match_media';
|
||||
import { usePushToService, ReturnUsePushToService, UsePushToService } from '.';
|
||||
import { readCasesPermissions, TestProviders } from '../../common/mock';
|
||||
import { noPushCasesPermissions, readCasesPermissions, TestProviders } from '../../common/mock';
|
||||
import { CaseStatuses, ConnectorTypes } from '../../../common/api';
|
||||
import { usePostPushToService } from '../../containers/use_post_push_to_service';
|
||||
import { basicPush, actionLicenses, connectorsMock } from '../../containers/mock';
|
||||
|
@ -280,6 +280,24 @@ describe('usePushToService', () => {
|
|||
});
|
||||
|
||||
describe('user does not have write permissions', () => {
|
||||
it('disables the push button when the user does not have push permissions', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<UsePushToService, ReturnUsePushToService>(
|
||||
() => usePushToService(defaultArgs),
|
||||
{
|
||||
wrapper: ({ children }) => (
|
||||
<TestProviders permissions={noPushCasesPermissions()}> {children}</TestProviders>
|
||||
),
|
||||
}
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
|
||||
const { getByTestId } = render(result.current.pushButton);
|
||||
|
||||
expect(getByTestId('push-to-external-service')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not display a message when user does not have a premium license', async () => {
|
||||
useFetchActionLicenseMock.mockImplementation(() => ({
|
||||
isLoading: false,
|
||||
|
|
|
@ -76,7 +76,7 @@ export const usePushToService = ({
|
|||
|
||||
// these message require that the user do some sort of write action as a result of the message, readonly users won't
|
||||
// be able to perform such an action so let's not display the error to the user in that situation
|
||||
if (!permissions.all) {
|
||||
if (!permissions.update) {
|
||||
return errors;
|
||||
}
|
||||
|
||||
|
@ -114,7 +114,7 @@ export const usePushToService = ({
|
|||
|
||||
return errors;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [actionLicense, caseStatus, connectors.length, connector, loadingLicense, permissions.all]);
|
||||
}, [actionLicense, caseStatus, connectors.length, connector, loadingLicense, permissions.update]);
|
||||
|
||||
const pushToServiceButton = useMemo(
|
||||
() => (
|
||||
|
@ -126,7 +126,7 @@ export const usePushToService = ({
|
|||
isLoading ||
|
||||
loadingLicense ||
|
||||
errorsMsg.length > 0 ||
|
||||
!permissions.all ||
|
||||
!permissions.push ||
|
||||
!isValidConnector ||
|
||||
!hasDataToPush
|
||||
}
|
||||
|
@ -146,13 +146,13 @@ export const usePushToService = ({
|
|||
hasDataToPush,
|
||||
isLoading,
|
||||
loadingLicense,
|
||||
permissions.all,
|
||||
permissions.push,
|
||||
isValidConnector,
|
||||
]
|
||||
);
|
||||
|
||||
const objToReturn = useMemo(() => {
|
||||
const hidePushButton = errorsMsg.length > 0 || !hasDataToPush || !permissions.all;
|
||||
const hidePushButton = errorsMsg.length > 0 || !hasDataToPush || !permissions.push;
|
||||
|
||||
return {
|
||||
pushButton: hidePushButton ? (
|
||||
|
@ -184,7 +184,7 @@ export const usePushToService = ({
|
|||
hasLicenseError,
|
||||
onEditClick,
|
||||
pushToServiceButton,
|
||||
permissions.all,
|
||||
permissions.push,
|
||||
]);
|
||||
|
||||
return objToReturn;
|
||||
|
|
|
@ -233,7 +233,7 @@ export const UserActions = React.memo(
|
|||
|
||||
const { permissions } = useCasesContext();
|
||||
|
||||
const bottomActions = permissions.all
|
||||
const bottomActions = permissions.create
|
||||
? [
|
||||
{
|
||||
username: (
|
||||
|
|
|
@ -6,11 +6,15 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { mount, ReactWrapper } from 'enzyme';
|
||||
import { UserActionPropertyActions } from './property_actions';
|
||||
import { render } from '@testing-library/react';
|
||||
import { UserActionPropertyActions, UserActionPropertyActionsProps } from './property_actions';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { TestProviders } from '../../common/mock';
|
||||
import {
|
||||
noCreateCasesPermissions,
|
||||
noDeleteCasesPermissions,
|
||||
noUpdateCasesPermissions,
|
||||
TestProviders,
|
||||
} from '../../common/mock';
|
||||
|
||||
jest.mock('../../common/lib/kibana');
|
||||
|
||||
|
@ -28,67 +32,123 @@ const props = {
|
|||
};
|
||||
|
||||
describe('UserActionPropertyActions ', () => {
|
||||
let wrapper: ReactWrapper;
|
||||
|
||||
beforeAll(() => {
|
||||
wrapper = mount(
|
||||
<TestProviders>
|
||||
<UserActionPropertyActions {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders', async () => {
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="user-action-title-loading"]').first().exists()
|
||||
).toBeFalsy();
|
||||
render(
|
||||
<TestProviders>
|
||||
<UserActionPropertyActions {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="property-actions"]').first().exists()).toBeTruthy();
|
||||
expect(screen.queryByTestId('user-action-title-loading')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('property-actions')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the edit and quote buttons', async () => {
|
||||
wrapper.find('[data-test-subj="property-actions-ellipses"]').first().simulate('click');
|
||||
wrapper.find('[data-test-subj="property-actions-pencil"]').exists();
|
||||
wrapper.find('[data-test-subj="property-actions-quote"]').exists();
|
||||
const renderResult = render(
|
||||
<TestProviders>
|
||||
<UserActionPropertyActions {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
userEvent.click(renderResult.getByTestId('property-actions-ellipses'));
|
||||
expect(screen.getByTestId('property-actions-pencil')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('property-actions-quote')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('quote click calls onQuote', async () => {
|
||||
wrapper.find('[data-test-subj="property-actions-ellipses"]').first().simulate('click');
|
||||
wrapper.find('[data-test-subj="property-actions-quote"]').first().simulate('click');
|
||||
const renderResult = render(
|
||||
<TestProviders>
|
||||
<UserActionPropertyActions {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
userEvent.click(renderResult.getByTestId('property-actions-ellipses'));
|
||||
userEvent.click(renderResult.getByTestId('property-actions-quote'));
|
||||
|
||||
expect(onQuote).toHaveBeenCalledWith(props.id);
|
||||
});
|
||||
|
||||
it('pencil click calls onEdit', async () => {
|
||||
wrapper.find('[data-test-subj="property-actions-ellipses"]').first().simulate('click');
|
||||
wrapper.find('[data-test-subj="property-actions-pencil"]').first().simulate('click');
|
||||
const renderResult = render(
|
||||
<TestProviders>
|
||||
<UserActionPropertyActions {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
userEvent.click(renderResult.getByTestId('property-actions-ellipses'));
|
||||
userEvent.click(renderResult.getByTestId('property-actions-pencil'));
|
||||
expect(onEdit).toHaveBeenCalledWith(props.id);
|
||||
});
|
||||
|
||||
it('shows the spinner when loading', async () => {
|
||||
wrapper = mount(
|
||||
render(
|
||||
<TestProviders>
|
||||
<UserActionPropertyActions {...props} isLoading={true} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="user-action-title-loading"]').first().exists()
|
||||
).toBeTruthy();
|
||||
|
||||
expect(wrapper.find('[data-test-subj="property-actions"]').first().exists()).toBeFalsy();
|
||||
expect(screen.getByTestId('user-action-title-loading')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('property-actions')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('Delete button', () => {
|
||||
const onDelete = jest.fn();
|
||||
const deleteProps = {
|
||||
...props,
|
||||
onDelete,
|
||||
deleteLabel: 'delete me',
|
||||
deleteConfirmlabel: 'confirm delete me',
|
||||
};
|
||||
describe('deletion props', () => {
|
||||
let onDelete: jest.Mock;
|
||||
let deleteProps: UserActionPropertyActionsProps;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
onDelete = jest.fn();
|
||||
deleteProps = {
|
||||
...props,
|
||||
onDelete,
|
||||
deleteLabel: 'delete me',
|
||||
deleteConfirmTitle: 'confirm delete me',
|
||||
};
|
||||
});
|
||||
|
||||
it('does not show the delete icon when the user does not have delete permissions', () => {
|
||||
const renderResult = render(
|
||||
<TestProviders permissions={noDeleteCasesPermissions()}>
|
||||
<UserActionPropertyActions {...deleteProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
userEvent.click(renderResult.getByTestId('property-actions-ellipses'));
|
||||
expect(renderResult.queryByTestId('property-actions-trash')).not.toBeInTheDocument();
|
||||
expect(renderResult.queryByTestId('property-actions-pencil')).toBeInTheDocument();
|
||||
expect(renderResult.queryByTestId('property-actions-quote')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show the pencil icon when the user does not have update permissions', () => {
|
||||
const renderResult = render(
|
||||
<TestProviders permissions={noUpdateCasesPermissions()}>
|
||||
<UserActionPropertyActions {...deleteProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
userEvent.click(renderResult.getByTestId('property-actions-ellipses'));
|
||||
expect(renderResult.queryByTestId('property-actions-trash')).toBeInTheDocument();
|
||||
expect(renderResult.queryByTestId('property-actions-pencil')).not.toBeInTheDocument();
|
||||
expect(renderResult.queryByTestId('property-actions-quote')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show the quote icon when the user does not have create permissions', () => {
|
||||
const renderResult = render(
|
||||
<TestProviders permissions={noCreateCasesPermissions()}>
|
||||
<UserActionPropertyActions {...deleteProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
userEvent.click(renderResult.getByTestId('property-actions-ellipses'));
|
||||
expect(renderResult.queryByTestId('property-actions-trash')).toBeInTheDocument();
|
||||
expect(renderResult.queryByTestId('property-actions-pencil')).toBeInTheDocument();
|
||||
expect(renderResult.queryByTestId('property-actions-quote')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the delete button', () => {
|
||||
const renderResult = render(
|
||||
<TestProviders>
|
||||
|
|
|
@ -13,7 +13,7 @@ import { useLensOpenVisualization } from '../markdown_editor/plugins/lens/use_le
|
|||
import { CANCEL_BUTTON, CONFIRM_BUTTON } from './translations';
|
||||
import { useCasesContext } from '../cases_context/use_cases_context';
|
||||
|
||||
interface UserActionPropertyActionsProps {
|
||||
export interface UserActionPropertyActionsProps {
|
||||
id: string;
|
||||
editLabel: string;
|
||||
deleteLabel?: string;
|
||||
|
@ -59,47 +59,56 @@ const UserActionPropertyActionsComponent = ({
|
|||
setShowDeleteConfirm(false);
|
||||
}, []);
|
||||
|
||||
const propertyActions = useMemo(
|
||||
() =>
|
||||
[
|
||||
permissions.all
|
||||
? [
|
||||
{
|
||||
iconType: 'pencil',
|
||||
label: editLabel,
|
||||
onClick: onEditClick,
|
||||
},
|
||||
...(deleteLabel && onDelete
|
||||
? [
|
||||
{
|
||||
iconType: 'trash',
|
||||
label: deleteLabel,
|
||||
onClick: onDeleteClick,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
iconType: 'quote',
|
||||
label: quoteLabel,
|
||||
onClick: onQuoteClick,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
canUseEditor && actionConfig ? [actionConfig] : [],
|
||||
].flat(),
|
||||
[
|
||||
permissions.all,
|
||||
editLabel,
|
||||
onEditClick,
|
||||
deleteLabel,
|
||||
onDelete,
|
||||
onDeleteClick,
|
||||
quoteLabel,
|
||||
onQuoteClick,
|
||||
canUseEditor,
|
||||
actionConfig,
|
||||
]
|
||||
);
|
||||
const propertyActions = useMemo(() => {
|
||||
const showEditPencilIcon = permissions.update;
|
||||
const showTrashIcon = permissions.delete && deleteLabel && onDelete;
|
||||
const showQuoteIcon = permissions.create;
|
||||
const showLensEditor = permissions.update && canUseEditor && actionConfig;
|
||||
|
||||
return [
|
||||
...(showEditPencilIcon
|
||||
? [
|
||||
{
|
||||
iconType: 'pencil',
|
||||
label: editLabel,
|
||||
onClick: onEditClick,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(showTrashIcon
|
||||
? [
|
||||
{
|
||||
iconType: 'trash',
|
||||
label: deleteLabel,
|
||||
onClick: onDeleteClick,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(showQuoteIcon
|
||||
? [
|
||||
{
|
||||
iconType: 'quote',
|
||||
label: quoteLabel,
|
||||
onClick: onQuoteClick,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(showLensEditor ? [actionConfig] : []),
|
||||
];
|
||||
}, [
|
||||
permissions.update,
|
||||
permissions.delete,
|
||||
permissions.create,
|
||||
deleteLabel,
|
||||
onDelete,
|
||||
canUseEditor,
|
||||
actionConfig,
|
||||
editLabel,
|
||||
onEditClick,
|
||||
onDeleteClick,
|
||||
quoteLabel,
|
||||
onQuoteClick,
|
||||
]);
|
||||
|
||||
if (!propertyActions.length) {
|
||||
return null;
|
||||
|
|
|
@ -28,6 +28,14 @@ const hooksMock: jest.Mocked<CasesUiStart['hooks']> = {
|
|||
|
||||
const helpersMock: jest.Mocked<CasesUiStart['helpers']> = {
|
||||
canUseCases: jest.fn(),
|
||||
getUICapabilities: jest.fn().mockReturnValue({
|
||||
all: false,
|
||||
create: false,
|
||||
read: false,
|
||||
update: false,
|
||||
delete: false,
|
||||
push: false,
|
||||
}),
|
||||
getRuleIdFromEvent: jest.fn(),
|
||||
groupAlertsByRule: jest.fn(),
|
||||
};
|
||||
|
|
|
@ -24,6 +24,7 @@ import { getCasesContextLazy } from './client/ui/get_cases_context';
|
|||
import { getCreateCaseFlyoutLazy } from './client/ui/get_create_case_flyout';
|
||||
import { getRecentCasesLazy } from './client/ui/get_recent_cases';
|
||||
import { groupAlertsByRule } from './client/helpers/group_alerts_by_rule';
|
||||
import { getUICapabilities } from './client/helpers/capabilities';
|
||||
import { ExternalReferenceAttachmentTypeRegistry } from './client/attachment_framework/external_reference_registry';
|
||||
import { PersistableStateAttachmentTypeRegistry } from './client/attachment_framework/persistable_state_registry';
|
||||
|
||||
|
@ -150,6 +151,7 @@ export class CasesUiPlugin
|
|||
},
|
||||
helpers: {
|
||||
canUseCases: canUseCases(core.application.capabilities),
|
||||
getUICapabilities,
|
||||
getRuleIdFromEvent,
|
||||
groupAlertsByRule,
|
||||
},
|
||||
|
|
|
@ -29,7 +29,7 @@ import type {
|
|||
} from '../common/api';
|
||||
import type { UseCasesAddToExistingCaseModal } from './components/all_cases/selector_modal/use_cases_add_to_existing_case_modal';
|
||||
import type { UseCasesAddToNewCaseFlyout } from './components/create/flyout/use_cases_add_to_new_case_flyout';
|
||||
import type { CasesOwners } from './client/helpers/can_use_cases';
|
||||
import { canUseCases } from './client/helpers/can_use_cases';
|
||||
import { getRuleIdFromEvent } from './client/helpers/get_rule_id_from_event';
|
||||
import type { GetCasesContextProps } from './client/ui/get_cases_context';
|
||||
import type { GetCasesProps } from './client/ui/get_cases';
|
||||
|
@ -38,6 +38,7 @@ import type { GetCreateCaseFlyoutProps } from './client/ui/get_create_case_flyou
|
|||
import type { GetRecentCasesProps } from './client/ui/get_recent_cases';
|
||||
import type { Cases, CasesStatus, CasesMetrics } from '../common/ui';
|
||||
import { groupAlertsByRule } from './client/helpers/group_alerts_by_rule';
|
||||
import { getUICapabilities } from './client/helpers/capabilities';
|
||||
import type { AttachmentFramework } from './client/attachment_framework/types';
|
||||
import { ExternalReferenceAttachmentTypeRegistry } from './client/attachment_framework/external_reference_registry';
|
||||
import { PersistableStateAttachmentTypeRegistry } from './client/attachment_framework/persistable_state_registry';
|
||||
|
@ -137,7 +138,8 @@ export interface CasesUiStart {
|
|||
* @param owners an array of CaseOwners that should be queried for permission
|
||||
* @returns An object denoting the case permissions of the current user
|
||||
*/
|
||||
canUseCases: (owners?: CasesOwners[]) => { crud: boolean; read: boolean };
|
||||
canUseCases: ReturnType<typeof canUseCases>;
|
||||
getUICapabilities: typeof getUICapabilities;
|
||||
getRuleIdFromEvent: typeof getRuleIdFromEvent;
|
||||
groupAlertsByRule: typeof groupAlertsByRule;
|
||||
};
|
||||
|
|
19
x-pack/plugins/cases/public/utils/permissions.ts
Normal file
19
x-pack/plugins/cases/public/utils/permissions.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 { CasesPermissions } from '../../common';
|
||||
|
||||
export const isReadOnlyPermissions = (permissions: CasesPermissions) => {
|
||||
return (
|
||||
!permissions.all &&
|
||||
!permissions.create &&
|
||||
!permissions.update &&
|
||||
!permissions.delete &&
|
||||
!permissions.push &&
|
||||
permissions.read
|
||||
);
|
||||
};
|
|
@ -11,6 +11,7 @@ import { KibanaFeatureConfig } from '@kbn/features-plugin/common';
|
|||
import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server';
|
||||
|
||||
import { APP_ID, FEATURE_ID } from '../common/constants';
|
||||
import { createUICapabilities } from '../common';
|
||||
|
||||
/**
|
||||
* The order of appearance in the feature privilege page
|
||||
|
@ -20,44 +21,81 @@ import { APP_ID, FEATURE_ID } from '../common/constants';
|
|||
|
||||
const FEATURE_ORDER = 3100;
|
||||
|
||||
export const getCasesKibanaFeature = (): KibanaFeatureConfig => ({
|
||||
id: FEATURE_ID,
|
||||
name: i18n.translate('xpack.cases.features.casesFeatureName', {
|
||||
defaultMessage: 'Cases',
|
||||
}),
|
||||
category: DEFAULT_APP_CATEGORIES.management,
|
||||
app: [],
|
||||
order: FEATURE_ORDER,
|
||||
management: {
|
||||
insightsAndAlerting: [APP_ID],
|
||||
},
|
||||
cases: [APP_ID],
|
||||
privileges: {
|
||||
all: {
|
||||
cases: {
|
||||
all: [APP_ID],
|
||||
},
|
||||
management: {
|
||||
insightsAndAlerting: [APP_ID],
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['crud_cases', 'read_cases'],
|
||||
export const getCasesKibanaFeature = (): KibanaFeatureConfig => {
|
||||
const capabilities = createUICapabilities();
|
||||
|
||||
return {
|
||||
id: FEATURE_ID,
|
||||
name: i18n.translate('xpack.cases.features.casesFeatureName', {
|
||||
defaultMessage: 'Cases',
|
||||
}),
|
||||
category: DEFAULT_APP_CATEGORIES.management,
|
||||
app: [],
|
||||
order: FEATURE_ORDER,
|
||||
management: {
|
||||
insightsAndAlerting: [APP_ID],
|
||||
},
|
||||
read: {
|
||||
cases: {
|
||||
read: [APP_ID],
|
||||
cases: [APP_ID],
|
||||
privileges: {
|
||||
all: {
|
||||
cases: {
|
||||
create: [APP_ID],
|
||||
read: [APP_ID],
|
||||
update: [APP_ID],
|
||||
push: [APP_ID],
|
||||
},
|
||||
management: {
|
||||
insightsAndAlerting: [APP_ID],
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: capabilities.all,
|
||||
},
|
||||
management: {
|
||||
insightsAndAlerting: [APP_ID],
|
||||
read: {
|
||||
cases: {
|
||||
read: [APP_ID],
|
||||
},
|
||||
management: {
|
||||
insightsAndAlerting: [APP_ID],
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: capabilities.read,
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['read_cases'],
|
||||
},
|
||||
},
|
||||
});
|
||||
subFeatures: [
|
||||
{
|
||||
name: i18n.translate('xpack.cases.features.deleteSubFeatureName', {
|
||||
defaultMessage: 'Delete',
|
||||
}),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
api: [],
|
||||
id: 'cases_delete',
|
||||
name: i18n.translate('xpack.cases.features.deleteSubFeatureDetails', {
|
||||
defaultMessage: 'Delete cases and comments',
|
||||
}),
|
||||
includeIn: 'all',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
cases: {
|
||||
delete: [APP_ID],
|
||||
},
|
||||
ui: capabilities.delete,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
|
|
@ -12,6 +12,7 @@ import { sampleAttribute } from '../../configurations/test_data/sample_attribute
|
|||
import * as pluginHook from '../../../../../hooks/use_plugin_context';
|
||||
import { TypedLensByValueInput } from '@kbn/lens-plugin/public';
|
||||
import { ExpViewActionMenuContent } from './action_menu';
|
||||
import { noCasesPermissions as mockUseGetCasesPermissions } from '../../../../../utils/cases_permissions';
|
||||
|
||||
jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({
|
||||
appMountParameters: {
|
||||
|
@ -19,7 +20,15 @@ jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({
|
|||
},
|
||||
} as any);
|
||||
|
||||
jest.mock('../../../../../hooks/use_get_user_cases_permissions', () => ({
|
||||
useGetUserCasesPermissions: jest.fn(() => mockUseGetCasesPermissions()),
|
||||
}));
|
||||
|
||||
describe('Action Menu', function () {
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be able to click open in lens', async function () {
|
||||
const { findByText, core } = render(
|
||||
<ExpViewActionMenuContent
|
||||
|
|
|
@ -12,12 +12,18 @@ import { ExploratoryView } from './exploratory_view';
|
|||
import * as obsvDataViews from '../../../utils/observability_data_views/observability_data_views';
|
||||
import * as pluginHook from '../../../hooks/use_plugin_context';
|
||||
import { createStubIndexPattern } from '@kbn/data-plugin/common/stubs';
|
||||
import { noCasesPermissions as mockUseGetCasesPermissions } from '../../../utils/cases_permissions';
|
||||
|
||||
jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({
|
||||
appMountParameters: {
|
||||
setHeaderActionMenu: jest.fn(),
|
||||
},
|
||||
} as any);
|
||||
|
||||
jest.mock('../../../hooks/use_get_user_cases_permissions', () => ({
|
||||
useGetUserCasesPermissions: jest.fn(() => mockUseGetCasesPermissions()),
|
||||
}));
|
||||
|
||||
describe('ExploratoryView', () => {
|
||||
mockAppDataView();
|
||||
|
||||
|
|
|
@ -11,8 +11,12 @@ import { fireEvent } from '@testing-library/dom';
|
|||
import { AddToCaseAction } from './add_to_case_action';
|
||||
import * as useCaseHook from '../hooks/use_add_to_case';
|
||||
import * as datePicker from '../components/date_range_picker';
|
||||
import * as useGetUserCasesPermissionsModule from '../../../../hooks/use_get_user_cases_permissions';
|
||||
import moment from 'moment';
|
||||
import { noCasesPermissions as mockUseGetCasesPermissions } from '../../../../utils/cases_permissions';
|
||||
|
||||
jest.mock('../../../../hooks/use_get_user_cases_permissions', () => ({
|
||||
useGetUserCasesPermissions: jest.fn(() => mockUseGetCasesPermissions()),
|
||||
}));
|
||||
|
||||
describe('AddToCaseAction', function () {
|
||||
beforeEach(() => {
|
||||
|
@ -82,10 +86,6 @@ describe('AddToCaseAction', function () {
|
|||
});
|
||||
|
||||
it('should be able to click add to case button', async function () {
|
||||
const mockUseGetUserCasesPermissions = jest
|
||||
.spyOn(useGetUserCasesPermissionsModule, 'useGetUserCasesPermissions')
|
||||
.mockImplementation(() => ({ crud: false, read: false }));
|
||||
|
||||
const initSeries = {
|
||||
data: [
|
||||
{
|
||||
|
@ -113,11 +113,13 @@ describe('AddToCaseAction', function () {
|
|||
owner: ['observability'],
|
||||
permissions: {
|
||||
all: false,
|
||||
create: false,
|
||||
read: false,
|
||||
update: false,
|
||||
delete: false,
|
||||
push: false,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
mockUseGetUserCasesPermissions.mockRestore();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -39,8 +39,7 @@ export function AddToCaseAction({
|
|||
timeRange,
|
||||
}: AddToCaseProps) {
|
||||
const kServices = useKibana<ObservabilityAppServices>().services;
|
||||
const userPermissions = useGetUserCasesPermissions();
|
||||
const casesPermissions = { all: userPermissions.crud, read: userPermissions.read };
|
||||
const userCasesPermissions = useGetUserCasesPermissions();
|
||||
|
||||
const {
|
||||
cases,
|
||||
|
@ -77,7 +76,7 @@ export function AddToCaseAction({
|
|||
});
|
||||
|
||||
const getAllCasesSelectorModalProps: GetAllCasesSelectorModalProps = {
|
||||
permissions: casesPermissions,
|
||||
permissions: userCasesPermissions,
|
||||
onRowClick: onCaseClicked,
|
||||
owner: [owner],
|
||||
onClose: () => {
|
||||
|
|
|
@ -25,8 +25,7 @@ export interface UseAddToCaseActions {
|
|||
export const useBulkAddToCaseActions = ({ onClose, onSuccess }: UseAddToCaseActions = {}) => {
|
||||
const { cases: casesUi } = useKibana<ObservabilityAppServices>().services;
|
||||
|
||||
const casePermissions = useGetUserCasesPermissions();
|
||||
const hasWritePermissions = casePermissions?.crud ?? false;
|
||||
const userCasesPermissions = useGetUserCasesPermissions();
|
||||
|
||||
const createCaseFlyout = casesUi.hooks.getUseCasesAddToNewCaseFlyout({
|
||||
onClose,
|
||||
|
@ -38,7 +37,7 @@ export const useBulkAddToCaseActions = ({ onClose, onSuccess }: UseAddToCaseActi
|
|||
});
|
||||
|
||||
return useMemo(() => {
|
||||
return hasWritePermissions
|
||||
return userCasesPermissions.create && userCasesPermissions.read
|
||||
? [
|
||||
{
|
||||
label: ADD_TO_NEW_CASE,
|
||||
|
@ -68,5 +67,11 @@ export const useBulkAddToCaseActions = ({ onClose, onSuccess }: UseAddToCaseActi
|
|||
},
|
||||
]
|
||||
: [];
|
||||
}, [casesUi.helpers, createCaseFlyout, hasWritePermissions, selectCaseModal]);
|
||||
}, [
|
||||
casesUi.helpers,
|
||||
createCaseFlyout,
|
||||
userCasesPermissions.create,
|
||||
userCasesPermissions.read,
|
||||
selectCaseModal,
|
||||
]);
|
||||
};
|
||||
|
|
|
@ -1,79 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { applicationServiceMock } from '@kbn/core/public/mocks';
|
||||
import { casesFeatureId } from '../../common';
|
||||
import { useGetUserCasesPermissions } from './use_get_user_cases_permissions';
|
||||
import { kibanaStartMock } from '../utils/kibana_react.mock';
|
||||
|
||||
let mockUseKibanaReturnValue = kibanaStartMock.startContract();
|
||||
|
||||
jest.mock('../utils/kibana_react', () => ({
|
||||
__esModule: true,
|
||||
useKibana: jest.fn(() => mockUseKibanaReturnValue),
|
||||
}));
|
||||
|
||||
describe('useGetUserCasesPermissions', function () {
|
||||
it('returns expected permissions when capabilities entry exists', () => {
|
||||
mockUseKibanaReturnValue = {
|
||||
...mockUseKibanaReturnValue,
|
||||
services: {
|
||||
...mockUseKibanaReturnValue.services,
|
||||
application: {
|
||||
...mockUseKibanaReturnValue.services.application,
|
||||
capabilities: {
|
||||
...applicationServiceMock.createStartContract().capabilities,
|
||||
[casesFeatureId]: { crud_cases: false, read_cases: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const { result } = renderHook(() => useGetUserCasesPermissions(), {});
|
||||
expect(result.current?.read).toBe(true);
|
||||
expect(result.current?.crud).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when capabilities entry permissions are missing', () => {
|
||||
mockUseKibanaReturnValue = {
|
||||
...mockUseKibanaReturnValue,
|
||||
services: {
|
||||
...mockUseKibanaReturnValue.services,
|
||||
application: {
|
||||
...mockUseKibanaReturnValue.services.application,
|
||||
capabilities: {
|
||||
...applicationServiceMock.createStartContract().capabilities,
|
||||
[casesFeatureId]: {
|
||||
/* intentionally empty */
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const { result } = renderHook(() => useGetUserCasesPermissions(), {});
|
||||
expect(result.current?.read).toBe(false);
|
||||
expect(result.current?.crud).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when capabilities entry is missing entirely', () => {
|
||||
mockUseKibanaReturnValue = {
|
||||
...mockUseKibanaReturnValue,
|
||||
services: {
|
||||
...mockUseKibanaReturnValue.services,
|
||||
application: {
|
||||
...mockUseKibanaReturnValue.services.application,
|
||||
capabilities: {
|
||||
...applicationServiceMock.createStartContract().capabilities,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const { result } = renderHook(() => useGetUserCasesPermissions(), {});
|
||||
expect(result.current?.read).toBe(false);
|
||||
expect(result.current?.crud).toBe(false);
|
||||
});
|
||||
});
|
|
@ -6,35 +6,42 @@
|
|||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { CasesPermissions } from '@kbn/cases-plugin/common';
|
||||
import { useKibana } from '../utils/kibana_react';
|
||||
import { casesFeatureId } from '../../common';
|
||||
|
||||
export interface UseGetUserCasesPermissions {
|
||||
crud: boolean;
|
||||
read: boolean;
|
||||
}
|
||||
|
||||
export function useGetUserCasesPermissions() {
|
||||
const [casesPermissions, setCasesPermissions] = useState<UseGetUserCasesPermissions>({
|
||||
crud: false,
|
||||
const [casesPermissions, setCasesPermissions] = useState<CasesPermissions>({
|
||||
all: false,
|
||||
read: false,
|
||||
create: false,
|
||||
update: false,
|
||||
delete: false,
|
||||
push: false,
|
||||
});
|
||||
const uiCapabilities = useKibana().services.application.capabilities;
|
||||
|
||||
const casesCapabilities = useKibana().services.cases.helpers.getUICapabilities(
|
||||
uiCapabilities[casesFeatureId]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const capabilitiesCanUserCRUD: boolean =
|
||||
typeof uiCapabilities[casesFeatureId]?.crud_cases === 'boolean'
|
||||
? (uiCapabilities[casesFeatureId].crud_cases as boolean)
|
||||
: false;
|
||||
const capabilitiesCanUserRead: boolean =
|
||||
typeof uiCapabilities[casesFeatureId]?.read_cases === 'boolean'
|
||||
? (uiCapabilities[casesFeatureId].read_cases as boolean)
|
||||
: false;
|
||||
setCasesPermissions({
|
||||
crud: capabilitiesCanUserCRUD,
|
||||
read: capabilitiesCanUserRead,
|
||||
all: casesCapabilities.all,
|
||||
create: casesCapabilities.create,
|
||||
read: casesCapabilities.read,
|
||||
update: casesCapabilities.update,
|
||||
delete: casesCapabilities.delete,
|
||||
push: casesCapabilities.push,
|
||||
});
|
||||
}, [uiCapabilities]);
|
||||
}, [
|
||||
casesCapabilities.all,
|
||||
casesCapabilities.create,
|
||||
casesCapabilities.read,
|
||||
casesCapabilities.update,
|
||||
casesCapabilities.delete,
|
||||
casesCapabilities.push,
|
||||
]);
|
||||
|
||||
return casesPermissions;
|
||||
}
|
||||
|
|
|
@ -5,16 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
const casesUiStartMock = {
|
||||
createStart() {
|
||||
return {
|
||||
getCases: jest.fn(),
|
||||
getAllCasesSelectorModal: jest.fn(),
|
||||
getCreateCaseFlyout: jest.fn(),
|
||||
getRecentCases: jest.fn(),
|
||||
};
|
||||
},
|
||||
};
|
||||
import { mockCasesContract } from '@kbn/cases-plugin/public/mocks';
|
||||
|
||||
const embeddableStartMock = {
|
||||
createStart() {
|
||||
|
@ -60,7 +51,7 @@ const triggersActionsUiStartMock = {
|
|||
export const observabilityPublicPluginsStartMock = {
|
||||
createStart() {
|
||||
return {
|
||||
cases: casesUiStartMock.createStart(),
|
||||
cases: mockCasesContract(),
|
||||
embeddable: embeddableStartMock.createStart(),
|
||||
triggersActionsUi: triggersActionsUiStartMock.createStart(),
|
||||
data: null,
|
||||
|
|
|
@ -219,8 +219,7 @@ function AlertsPage() {
|
|||
const hasData = hasAnyData === true || (isAllRequestsComplete === false ? undefined : false);
|
||||
|
||||
const CasesContext = cases.ui.getCasesContext();
|
||||
const userPermissions = useGetUserCasesPermissions();
|
||||
const casesPermissions = { all: userPermissions.crud, read: userPermissions.read };
|
||||
const userCasesPermissions = useGetUserCasesPermissions();
|
||||
|
||||
if (!hasAnyData && !isAllRequestsComplete) {
|
||||
return <LoadingObservability />;
|
||||
|
@ -266,7 +265,7 @@ function AlertsPage() {
|
|||
<EuiFlexItem>
|
||||
<CasesContext
|
||||
owner={[observabilityFeatureId]}
|
||||
permissions={casesPermissions}
|
||||
permissions={userCasesPermissions}
|
||||
features={{ alerts: { sync: false } }}
|
||||
>
|
||||
<AlertsTableTGrid
|
||||
|
|
|
@ -168,7 +168,7 @@ function ObservabilityActions({
|
|||
setActionsPopover((current) => (current ? null : id));
|
||||
}, []);
|
||||
|
||||
const casePermissions = useGetUserCasesPermissions();
|
||||
const userCasesPermissions = useGetUserCasesPermissions();
|
||||
const ruleId = alert.fields['kibana.alert.rule.uuid'] ?? null;
|
||||
const linkToRule = ruleId ? http.basePath.prepend(paths.observability.ruleDetails(ruleId)) : null;
|
||||
const caseAttachments: CaseAttachments = useMemo(() => {
|
||||
|
@ -201,7 +201,7 @@ function ObservabilityActions({
|
|||
|
||||
const actionsMenuItems = useMemo(() => {
|
||||
return [
|
||||
...(casePermissions.crud
|
||||
...(userCasesPermissions.create && userCasesPermissions.read
|
||||
? [
|
||||
<EuiContextMenuItem
|
||||
data-test-subj="add-to-existing-case-action"
|
||||
|
@ -246,7 +246,8 @@ function ObservabilityActions({
|
|||
],
|
||||
];
|
||||
}, [
|
||||
casePermissions.crud,
|
||||
userCasesPermissions.create,
|
||||
userCasesPermissions.read,
|
||||
handleAddToExistingCaseClick,
|
||||
handleAddToNewCaseClick,
|
||||
linkToRule,
|
||||
|
@ -332,7 +333,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) {
|
|||
storage.get(stateStorageKey)
|
||||
);
|
||||
|
||||
const casePermissions = useGetUserCasesPermissions();
|
||||
const userCasesPermissions = useGetUserCasesPermissions();
|
||||
|
||||
const hasAlertsCrudPermissions = useCallback(
|
||||
({ ruleConsumer, ruleProducer }: { ruleConsumer: string; ruleProducer?: string }) => {
|
||||
|
@ -415,7 +416,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) {
|
|||
return {
|
||||
appId: observabilityAppId,
|
||||
casesOwner: observabilityFeatureId,
|
||||
casePermissions,
|
||||
casePermissions: userCasesPermissions,
|
||||
type,
|
||||
columns: (tGridState?.columns ?? columns).map(addDisplayNames),
|
||||
deletedEventIds,
|
||||
|
@ -464,7 +465,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) {
|
|||
unit: (totalAlerts: number) => translations.alertsTable.showingAlertsTitle(totalAlerts),
|
||||
};
|
||||
}, [
|
||||
casePermissions,
|
||||
userCasesPermissions,
|
||||
tGridState?.columns,
|
||||
tGridState?.sort,
|
||||
deletedEventIds,
|
||||
|
|
|
@ -7,18 +7,18 @@
|
|||
|
||||
import React, { Suspense, useCallback, useState } from 'react';
|
||||
|
||||
import { CasesPermissions } from '@kbn/cases-plugin/common';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { CASES_OWNER, CASES_PATH } from './constants';
|
||||
import { usePluginContext } from '../../hooks/use_plugin_context';
|
||||
import { LazyAlertsFlyout } from '../..';
|
||||
import { useFetchAlertDetail } from './use_fetch_alert_detail';
|
||||
import { useFetchAlertData } from './use_fetch_alert_data';
|
||||
import { UseGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions';
|
||||
import { paths } from '../../config';
|
||||
import { ObservabilityAppServices } from '../../application/types';
|
||||
|
||||
interface CasesProps {
|
||||
permissions: UseGetUserCasesPermissions;
|
||||
permissions: CasesPermissions;
|
||||
}
|
||||
export const Cases = React.memo<CasesProps>(({ permissions }) => {
|
||||
const {
|
||||
|
@ -30,7 +30,6 @@ export const Cases = React.memo<CasesProps>(({ permissions }) => {
|
|||
} = useKibana<ObservabilityAppServices>().services;
|
||||
const { observabilityRuleTypeRegistry } = usePluginContext();
|
||||
const [selectedAlertId, setSelectedAlertId] = useState<string>('');
|
||||
const casesPermissions = { all: permissions.crud, read: permissions.read };
|
||||
|
||||
const handleFlyoutClose = useCallback(() => {
|
||||
setSelectedAlertId('');
|
||||
|
@ -51,7 +50,7 @@ export const Cases = React.memo<CasesProps>(({ permissions }) => {
|
|||
)}
|
||||
{cases.ui.getCases({
|
||||
basePath: CASES_PATH,
|
||||
permissions: casesPermissions,
|
||||
permissions,
|
||||
owner: [CASES_OWNER],
|
||||
features: { alerts: { sync: false } },
|
||||
useFetchAlertData,
|
||||
|
|
|
@ -19,7 +19,7 @@ import { getNoDataConfig } from '../../utils/no_data_config';
|
|||
import { ObservabilityAppServices } from '../../application/types';
|
||||
|
||||
export const CasesPage = React.memo(() => {
|
||||
const userPermissions = useGetUserCasesPermissions();
|
||||
const userCasesPermissions = useGetUserCasesPermissions();
|
||||
const { docLinks, http } = useKibana<ObservabilityAppServices>().services;
|
||||
const { ObservabilityPageTemplate } = usePluginContext();
|
||||
|
||||
|
@ -38,13 +38,13 @@ export const CasesPage = React.memo(() => {
|
|||
docsLink: docLinks.links.observability.guide,
|
||||
});
|
||||
|
||||
return userPermissions.read ? (
|
||||
return userCasesPermissions.read ? (
|
||||
<ObservabilityPageTemplate
|
||||
isPageDataLoaded={Boolean(hasAnyData || isAllRequestsComplete)}
|
||||
data-test-subj={noDataConfig ? 'noDataPage' : undefined}
|
||||
noDataConfig={noDataConfig}
|
||||
>
|
||||
<Cases permissions={userPermissions} />
|
||||
<Cases permissions={userCasesPermissions} />
|
||||
</ObservabilityPageTemplate>
|
||||
) : (
|
||||
<CaseFeatureNoPermissions />
|
||||
|
|
|
@ -127,8 +127,7 @@ export function OverviewPage({ routeParams }: Props) {
|
|||
}, []);
|
||||
|
||||
const CasesContext = cases.ui.getCasesContext();
|
||||
const userPermissions = useGetUserCasesPermissions();
|
||||
const casesPermissions = { all: userPermissions.crud, read: userPermissions.read };
|
||||
const userCasesPermissions = useGetUserCasesPermissions();
|
||||
|
||||
useEffect(() => {
|
||||
if (hasAnyData !== true) {
|
||||
|
@ -200,7 +199,7 @@ export function OverviewPage({ routeParams }: Props) {
|
|||
>
|
||||
<CasesContext
|
||||
owner={[observabilityFeatureId]}
|
||||
permissions={casesPermissions}
|
||||
permissions={userCasesPermissions}
|
||||
features={{ alerts: { sync: false } }}
|
||||
>
|
||||
<AlertsTableTGrid
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export const noCasesPermissions = () => ({
|
||||
all: false,
|
||||
create: false,
|
||||
read: false,
|
||||
update: false,
|
||||
delete: false,
|
||||
push: false,
|
||||
});
|
|
@ -14,6 +14,7 @@ import {
|
|||
} from '@kbn/core/server';
|
||||
import { RuleRegistryPluginSetupContract } from '@kbn/rule-registry-plugin/server';
|
||||
import { PluginSetupContract as FeaturesSetup } from '@kbn/features-plugin/server';
|
||||
import { createUICapabilities } from '@kbn/cases-plugin/common';
|
||||
import { ObservabilityConfig } from '.';
|
||||
import {
|
||||
bootstrapAnnotations,
|
||||
|
@ -40,6 +41,8 @@ export class ObservabilityPlugin implements Plugin<ObservabilityPluginSetup> {
|
|||
public setup(core: CoreSetup, plugins: PluginSetup) {
|
||||
const config = this.initContext.config.get<ObservabilityConfig>();
|
||||
|
||||
const casesCapabilities = createUICapabilities();
|
||||
|
||||
plugins.features.registerKibanaFeature({
|
||||
id: casesFeatureId,
|
||||
name: i18n.translate('xpack.observability.featureRegistry.linkObservabilityTitle', {
|
||||
|
@ -55,14 +58,17 @@ export class ObservabilityPlugin implements Plugin<ObservabilityPluginSetup> {
|
|||
app: [casesFeatureId, 'kibana'],
|
||||
catalogue: [observabilityFeatureId],
|
||||
cases: {
|
||||
all: [observabilityFeatureId],
|
||||
create: [observabilityFeatureId],
|
||||
read: [observabilityFeatureId],
|
||||
update: [observabilityFeatureId],
|
||||
push: [observabilityFeatureId],
|
||||
},
|
||||
api: [],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['crud_cases', 'read_cases'], // uiCapabilities[casesFeatureId].crud_cases or read_cases
|
||||
ui: casesCapabilities.all,
|
||||
},
|
||||
read: {
|
||||
app: [casesFeatureId, 'kibana'],
|
||||
|
@ -75,9 +81,42 @@ export class ObservabilityPlugin implements Plugin<ObservabilityPluginSetup> {
|
|||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['read_cases'], // uiCapabilities[uiCapabilities[casesFeatureId]].read_cases
|
||||
ui: casesCapabilities.read,
|
||||
},
|
||||
},
|
||||
subFeatures: [
|
||||
{
|
||||
name: i18n.translate('xpack.observability.featureRegistry.deleteSubFeatureName', {
|
||||
defaultMessage: 'Delete',
|
||||
}),
|
||||
privilegeGroups: [
|
||||
{
|
||||
groupType: 'independent',
|
||||
privileges: [
|
||||
{
|
||||
api: [],
|
||||
id: 'cases_delete',
|
||||
name: i18n.translate(
|
||||
'xpack.observability.featureRegistry.deleteSubFeatureDetails',
|
||||
{
|
||||
defaultMessage: 'Delete cases and comments',
|
||||
}
|
||||
),
|
||||
includeIn: 'all',
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
cases: {
|
||||
delete: [observabilityFeatureId],
|
||||
},
|
||||
ui: casesCapabilities.delete,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let annotationsApiPromise: Promise<AnnotationsAPI> | undefined;
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import type { TestCaseWithoutTimeline } from '../../objects/case';
|
||||
import { ALL_CASES_NAME } from '../../screens/all_cases';
|
||||
import { ALL_CASES_CREATE_NEW_CASE_BTN, ALL_CASES_NAME } from '../../screens/all_cases';
|
||||
|
||||
import { goToCreateNewCase } from '../../tasks/all_cases';
|
||||
import { cleanKibana, deleteCases } from '../../tasks/common';
|
||||
|
@ -31,12 +31,21 @@ import {
|
|||
secAllUser,
|
||||
secReadCasesAllUser,
|
||||
secReadCasesAll,
|
||||
secAllCasesNoDelete,
|
||||
secAllCasesNoDeleteUser,
|
||||
secAllCasesOnlyReadDeleteUser,
|
||||
secAllCasesOnlyReadDelete,
|
||||
} from '../../tasks/privileges';
|
||||
|
||||
import { CASES_URL } from '../../urls/navigation';
|
||||
import { openSourcerer } from '../../tasks/sourcerer';
|
||||
const usersToCreate = [secAllUser, secReadCasesAllUser];
|
||||
const rolesToCreate = [secAll, secReadCasesAll];
|
||||
const usersToCreate = [
|
||||
secAllUser,
|
||||
secReadCasesAllUser,
|
||||
secAllCasesNoDeleteUser,
|
||||
secAllCasesOnlyReadDeleteUser,
|
||||
];
|
||||
const rolesToCreate = [secAll, secReadCasesAll, secAllCasesNoDelete, secAllCasesOnlyReadDelete];
|
||||
// needed to generate index pattern
|
||||
const visitSecuritySolution = () => {
|
||||
visitHostDetailsPage();
|
||||
|
@ -51,6 +60,7 @@ const testCase: TestCaseWithoutTimeline = {
|
|||
reporter: 'elastic',
|
||||
owner: 'securitySolution',
|
||||
};
|
||||
|
||||
describe('Cases privileges', () => {
|
||||
before(() => {
|
||||
cleanKibana();
|
||||
|
@ -67,7 +77,7 @@ describe('Cases privileges', () => {
|
|||
deleteCases();
|
||||
});
|
||||
|
||||
for (const user of [secAllUser, secReadCasesAllUser]) {
|
||||
for (const user of [secAllUser, secReadCasesAllUser, secAllCasesNoDeleteUser]) {
|
||||
it(`User ${user.username} with role(s) ${user.roles.join()} can create a case`, () => {
|
||||
loginWithUser(user);
|
||||
visitWithUser(CASES_URL, user);
|
||||
|
@ -80,4 +90,12 @@ describe('Cases privileges', () => {
|
|||
cy.get(ALL_CASES_NAME).should('have.text', testCase.name);
|
||||
});
|
||||
}
|
||||
|
||||
for (const user of [secAllCasesOnlyReadDeleteUser]) {
|
||||
it(`User ${user.username} with role(s) ${user.roles.join()} cannot create a case`, () => {
|
||||
loginWithUser(user);
|
||||
visitWithUser(CASES_URL, user);
|
||||
cy.get(ALL_CASES_CREATE_NEW_CASE_BTN).should('not.exist');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -110,6 +110,68 @@ export const secReadCasesAllUser: User = {
|
|||
roles: [secReadCasesAll.name],
|
||||
};
|
||||
|
||||
export const secAllCasesOnlyReadDelete: Role = {
|
||||
name: 'sec_all_cases_only_read_delete',
|
||||
privileges: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['all'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
feature: {
|
||||
siem: ['all'],
|
||||
securitySolutionCases: ['cases_read', 'cases_delete'],
|
||||
actions: ['all'],
|
||||
actionsSimulators: ['all'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const secAllCasesOnlyReadDeleteUser: User = {
|
||||
username: 'sec_all_cases_only_read_delete_user',
|
||||
password: 'password',
|
||||
roles: [secAllCasesOnlyReadDelete.name],
|
||||
};
|
||||
|
||||
export const secAllCasesNoDelete: Role = {
|
||||
name: 'sec_all_cases_no_delete',
|
||||
privileges: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['all'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
feature: {
|
||||
siem: ['all'],
|
||||
securitySolutionCases: ['minimal_all'],
|
||||
actions: ['all'],
|
||||
actionsSimulators: ['all'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const secAllCasesNoDeleteUser: User = {
|
||||
username: 'sec_all_cases_no_delete_user',
|
||||
password: 'password',
|
||||
roles: [secAllCasesNoDelete.name],
|
||||
};
|
||||
|
||||
const getUserInfo = (user: User): UserInfo => ({
|
||||
username: user.username,
|
||||
full_name: user.username.replace('_', ' '),
|
||||
|
|
|
@ -57,8 +57,7 @@ const StartAppComponent: FC<StartAppComponent> = ({
|
|||
cases,
|
||||
} = useKibana().services;
|
||||
const [darkMode] = useUiSetting$<boolean>(DEFAULT_DARK_MODE);
|
||||
const userPermissions = useGetUserCasesPermissions();
|
||||
const casesPermissions = { all: userPermissions.crud, read: userPermissions.read };
|
||||
const userCasesPermissions = useGetUserCasesPermissions();
|
||||
const CasesContext = cases.ui.getCasesContext();
|
||||
return (
|
||||
<EuiErrorBoundary>
|
||||
|
@ -71,7 +70,7 @@ const StartAppComponent: FC<StartAppComponent> = ({
|
|||
<UserPrivilegesProvider kibanaCapabilities={capabilities}>
|
||||
<ManageUserInfo>
|
||||
<ReactQueryClientProvider>
|
||||
<CasesContext owner={[APP_ID]} permissions={casesPermissions}>
|
||||
<CasesContext owner={[APP_ID]} permissions={userCasesPermissions}>
|
||||
<PageRouter
|
||||
history={history}
|
||||
onAppLeave={onAppLeave}
|
||||
|
|
|
@ -4,11 +4,12 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { getDeepLinks } from '.';
|
||||
import { getDeepLinks, hasFeaturesCapability } from '.';
|
||||
import type { AppDeepLink, Capabilities } from '@kbn/core/public';
|
||||
import { SecurityPageName } from '../types';
|
||||
import { mockGlobalState } from '../../common/mock';
|
||||
import { CASES_FEATURE_ID, SERVER_APP_ID } from '../../../common/constants';
|
||||
import { createCapabilities } from '../../common/links/test_utils';
|
||||
|
||||
const findDeepLink = (id: string, deepLinks: AppDeepLink[]): AppDeepLink | null =>
|
||||
deepLinks.reduce((deepLinkFound: AppDeepLink | null, deepLink) => {
|
||||
|
@ -28,6 +29,14 @@ const basicLicense = 'basic';
|
|||
const platinumLicense = 'platinum';
|
||||
|
||||
describe('deepLinks', () => {
|
||||
describe('hasFeaturesCapability', () => {
|
||||
it('returns true when features is undefined', () => {
|
||||
expect(
|
||||
hasFeaturesCapability(undefined, createCapabilities({ siem: { show: true } }))
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a all basic license deep links in the premium deep links', () => {
|
||||
const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense);
|
||||
const platinumLinks = getDeepLinks(mockGlobalState.app.enableExperimental, platinumLicense);
|
||||
|
@ -68,7 +77,7 @@ describe('deepLinks', () => {
|
|||
|
||||
it('should return case links for basic license with only read_cases capabilities', () => {
|
||||
const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense, {
|
||||
[CASES_FEATURE_ID]: { read_cases: true, crud_cases: false },
|
||||
[CASES_FEATURE_ID]: { read_cases: true },
|
||||
[SERVER_APP_ID]: { show: true },
|
||||
} as unknown as Capabilities);
|
||||
expect(findDeepLink(SecurityPageName.case, basicLinks)).toBeTruthy();
|
||||
|
@ -76,15 +85,21 @@ describe('deepLinks', () => {
|
|||
|
||||
it('should return case links with NO deepLinks for basic license with only read_cases capabilities', () => {
|
||||
const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense, {
|
||||
[CASES_FEATURE_ID]: { read_cases: true, crud_cases: false },
|
||||
[CASES_FEATURE_ID]: { read_cases: true },
|
||||
[SERVER_APP_ID]: { show: true },
|
||||
} as unknown as Capabilities);
|
||||
expect(findDeepLink(SecurityPageName.case, basicLinks)?.deepLinks?.length === 0).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return case links with deepLinks for basic license with crud_cases capabilities', () => {
|
||||
it('should return case links with deepLinks for basic license with permissive capabilities', () => {
|
||||
const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense, {
|
||||
[CASES_FEATURE_ID]: { read_cases: true, crud_cases: true },
|
||||
[CASES_FEATURE_ID]: {
|
||||
create_cases: true,
|
||||
read_cases: true,
|
||||
update_cases: true,
|
||||
delete_cases: true,
|
||||
push_cases: true,
|
||||
},
|
||||
[SERVER_APP_ID]: { show: true },
|
||||
} as unknown as Capabilities);
|
||||
|
||||
|
@ -93,17 +108,29 @@ describe('deepLinks', () => {
|
|||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return case links with deepLinks for basic license with crud_cases capabilities and security disabled', () => {
|
||||
it('should return case links with deepLinks for basic license with permissive capabilities and security disabled', () => {
|
||||
const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, platinumLicense, {
|
||||
[CASES_FEATURE_ID]: { read_cases: true, crud_cases: true },
|
||||
[CASES_FEATURE_ID]: {
|
||||
create_cases: true,
|
||||
read_cases: true,
|
||||
update_cases: true,
|
||||
delete_cases: true,
|
||||
push_cases: true,
|
||||
},
|
||||
[SERVER_APP_ID]: { show: false },
|
||||
} as unknown as Capabilities);
|
||||
expect(findDeepLink(SecurityPageName.case, basicLinks)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return NO case links for basic license with NO read_cases capabilities', () => {
|
||||
it('should return NO case links for basic license with NO cases capabilities', () => {
|
||||
const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense, {
|
||||
[CASES_FEATURE_ID]: { read_cases: false, crud_cases: false },
|
||||
[CASES_FEATURE_ID]: {
|
||||
create_cases: false,
|
||||
read_cases: false,
|
||||
update_cases: false,
|
||||
delete_cases: false,
|
||||
push_cases: false,
|
||||
},
|
||||
[SERVER_APP_ID]: { show: true },
|
||||
} as unknown as Capabilities);
|
||||
expect(findDeepLink(SecurityPageName.case, basicLinks)).toBeFalsy();
|
||||
|
|
|
@ -7,9 +7,15 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { get } from 'lodash';
|
||||
import type { LicenseType } from '@kbn/licensing-plugin/common/types';
|
||||
import { getCasesDeepLinks } from '@kbn/cases-plugin/public';
|
||||
import {
|
||||
CREATE_CASES_CAPABILITY,
|
||||
DELETE_CASES_CAPABILITY,
|
||||
PUSH_CASES_CAPABILITY,
|
||||
READ_CASES_CAPABILITY,
|
||||
UPDATE_CASES_CAPABILITY,
|
||||
} from '@kbn/cases-plugin/common';
|
||||
import type { AppDeepLink, AppUpdater, Capabilities } from '@kbn/core/public';
|
||||
import { AppNavLinkStatus } from '@kbn/core/public';
|
||||
import type { Subject, Subscription } from 'rxjs';
|
||||
|
@ -65,20 +71,36 @@ import {
|
|||
RESPONSE_ACTIONS_PATH,
|
||||
} from '../../../common/constants';
|
||||
import type { ExperimentalFeatures } from '../../../common/experimental_features';
|
||||
import { subscribeAppLinks } from '../../common/links';
|
||||
import { hasCapabilities, subscribeAppLinks } from '../../common/links';
|
||||
import type { AppLinkItems } from '../../common/links/types';
|
||||
|
||||
const FEATURE = {
|
||||
export const FEATURE = {
|
||||
general: `${SERVER_APP_ID}.show`,
|
||||
casesRead: `${CASES_FEATURE_ID}.read_cases`,
|
||||
casesCrud: `${CASES_FEATURE_ID}.crud_cases`,
|
||||
casesCreate: `${CASES_FEATURE_ID}.${CREATE_CASES_CAPABILITY}`,
|
||||
casesRead: `${CASES_FEATURE_ID}.${READ_CASES_CAPABILITY}`,
|
||||
casesUpdate: `${CASES_FEATURE_ID}.${UPDATE_CASES_CAPABILITY}`,
|
||||
casesDelete: `${CASES_FEATURE_ID}.${DELETE_CASES_CAPABILITY}`,
|
||||
casesPush: `${CASES_FEATURE_ID}.${PUSH_CASES_CAPABILITY}`,
|
||||
} as const;
|
||||
|
||||
type Feature = typeof FEATURE[keyof typeof FEATURE];
|
||||
type FeatureKey = typeof FEATURE[keyof typeof FEATURE];
|
||||
|
||||
/**
|
||||
* The format of defining features supports OR and AND mechanism. To specify features in an OR fashion
|
||||
* they can be defined in a single level array like: [requiredFeature1, requiredFeature2]. If either of these features
|
||||
* is satisfied the deeplinks would be included. To require that the features be AND'd together a second level array
|
||||
* can be specified: [feature1, [feature2, feature3]] this would result in feature1 || (feature2 && feature3). To specify
|
||||
* features that all must be and'd together an example would be: [[feature1, feature2]], this would result in the boolean
|
||||
* operation feature1 && feature2.
|
||||
*
|
||||
* The final format is to specify a single feature, this would be like: features: feature1, which is the same as
|
||||
* features: [feature1]
|
||||
*/
|
||||
type Features = FeatureKey | Array<FeatureKey | FeatureKey[]>;
|
||||
|
||||
type SecuritySolutionDeepLink = AppDeepLink & {
|
||||
isPremium?: boolean;
|
||||
features?: Feature[];
|
||||
features?: Features;
|
||||
/**
|
||||
* Displays deep link when feature flag is enabled.
|
||||
*/
|
||||
|
@ -417,11 +439,11 @@ export const securitySolutionsDeepLinks: SecuritySolutionDeepLink[] = [
|
|||
features: [FEATURE.casesRead],
|
||||
},
|
||||
[SecurityPageName.caseConfigure]: {
|
||||
features: [FEATURE.casesCrud],
|
||||
features: [FEATURE.casesUpdate],
|
||||
isPremium: true,
|
||||
},
|
||||
[SecurityPageName.caseCreate]: {
|
||||
features: [FEATURE.casesCrud],
|
||||
features: [FEATURE.casesCreate],
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
@ -525,14 +547,15 @@ export function getDeepLinks(
|
|||
return filterDeepLinks(securitySolutionsDeepLinks);
|
||||
}
|
||||
|
||||
function hasFeaturesCapability(
|
||||
features: Feature[] | undefined,
|
||||
export function hasFeaturesCapability(
|
||||
features: Features | undefined,
|
||||
capabilities: Capabilities
|
||||
): boolean {
|
||||
if (!features) {
|
||||
return true;
|
||||
}
|
||||
return features.some((featureKey) => get(capabilities, featureKey, false));
|
||||
|
||||
return hasCapabilities(features, capabilities);
|
||||
}
|
||||
|
||||
export function isPremiumLicense(licenseType?: LicenseType): boolean {
|
||||
|
|
|
@ -5,6 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
CREATE_CASES_CAPABILITY,
|
||||
READ_CASES_CAPABILITY,
|
||||
UPDATE_CASES_CAPABILITY,
|
||||
} from '@kbn/cases-plugin/common';
|
||||
import { getCasesDeepLinks } from '@kbn/cases-plugin/public';
|
||||
import { CASES_FEATURE_ID, CASES_PATH, SecurityPageName } from '../../common/constants';
|
||||
import type { LinkItem } from '../common/links/types';
|
||||
|
@ -16,16 +21,16 @@ export const getCasesLinkItems = (): LinkItem => {
|
|||
[SecurityPageName.case]: {
|
||||
globalNavEnabled: true,
|
||||
globalNavOrder: 5,
|
||||
capabilities: [`${CASES_FEATURE_ID}.read_cases`],
|
||||
capabilities: [`${CASES_FEATURE_ID}.${READ_CASES_CAPABILITY}`],
|
||||
},
|
||||
[SecurityPageName.caseConfigure]: {
|
||||
capabilities: [`${CASES_FEATURE_ID}.crud_cases`],
|
||||
capabilities: [`${CASES_FEATURE_ID}.${UPDATE_CASES_CAPABILITY}`],
|
||||
licenseType: 'gold',
|
||||
sideNavDisabled: true,
|
||||
hideTimeline: true,
|
||||
},
|
||||
[SecurityPageName.caseCreate]: {
|
||||
capabilities: [`${CASES_FEATURE_ID}.crud_cases`],
|
||||
capabilities: [`${CASES_FEATURE_ID}.${CREATE_CASES_CAPABILITY}`],
|
||||
sideNavDisabled: true,
|
||||
hideTimeline: true,
|
||||
},
|
||||
|
|
|
@ -42,8 +42,7 @@ const TimelineDetailsPanel = () => {
|
|||
const CaseContainerComponent: React.FC = () => {
|
||||
const { cases } = useKibana().services;
|
||||
const { getAppUrl, navigateTo } = useNavigation();
|
||||
const userPermissions = useGetUserCasesPermissions();
|
||||
const casesPermissions = { all: userPermissions.crud, read: userPermissions.read };
|
||||
const userCasesPermissions = useGetUserCasesPermissions();
|
||||
const dispatch = useDispatch();
|
||||
const { formatUrl: detectionsFormatUrl, search: detectionsUrlSearch } = useFormatUrl(
|
||||
SecurityPageName.rules
|
||||
|
@ -145,7 +144,7 @@ const CaseContainerComponent: React.FC = () => {
|
|||
},
|
||||
},
|
||||
useFetchAlertData,
|
||||
permissions: casesPermissions,
|
||||
permissions: userCasesPermissions,
|
||||
})}
|
||||
</CaseDetailsRefreshContext.Provider>
|
||||
<SpyRoute pageName={SecurityPageName.case} />
|
||||
|
|
66
x-pack/plugins/security_solution/public/cases_test_utils.ts
Normal file
66
x-pack/plugins/security_solution/public/cases_test_utils.ts
Normal file
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const noCasesCapabilities = () => ({
|
||||
create_cases: false,
|
||||
read_cases: false,
|
||||
update_cases: false,
|
||||
delete_cases: false,
|
||||
push_cases: false,
|
||||
});
|
||||
|
||||
export const readCasesCapabilities = () => ({
|
||||
create_cases: false,
|
||||
read_cases: true,
|
||||
update_cases: false,
|
||||
delete_cases: false,
|
||||
push_cases: false,
|
||||
});
|
||||
|
||||
export const allCasesCapabilities = () => ({
|
||||
create_cases: true,
|
||||
read_cases: true,
|
||||
update_cases: true,
|
||||
delete_cases: true,
|
||||
push_cases: true,
|
||||
});
|
||||
|
||||
export const noCasesPermissions = () => ({
|
||||
all: false,
|
||||
create: false,
|
||||
read: false,
|
||||
update: false,
|
||||
delete: false,
|
||||
push: false,
|
||||
});
|
||||
|
||||
export const readCasesPermissions = () => ({
|
||||
all: false,
|
||||
create: false,
|
||||
read: true,
|
||||
update: false,
|
||||
delete: false,
|
||||
push: false,
|
||||
});
|
||||
|
||||
export const writeCasesPermissions = () => ({
|
||||
all: false,
|
||||
create: true,
|
||||
read: false,
|
||||
update: true,
|
||||
delete: true,
|
||||
push: true,
|
||||
});
|
||||
|
||||
export const allCasesPermissions = () => ({
|
||||
all: true,
|
||||
create: true,
|
||||
read: true,
|
||||
update: true,
|
||||
delete: true,
|
||||
push: true,
|
||||
});
|
|
@ -12,6 +12,7 @@ import { TestProviders } from '../../mock';
|
|||
import { useKibana as mockUseKibana } from '../../lib/kibana/__mocks__';
|
||||
import { useGetUserCasesPermissions } from '../../lib/kibana';
|
||||
import { RelatedCases } from './related_cases';
|
||||
import { noCasesPermissions, readCasesPermissions } from '../../../cases_test_utils';
|
||||
|
||||
const mockedUseKibana = mockUseKibana();
|
||||
const mockGetRelatedCases = jest.fn();
|
||||
|
@ -42,9 +43,7 @@ const eventId = '1c84d9bff4884dabe6aa1bb15f08433463b848d9269e587078dc56669550d27
|
|||
describe('Related Cases', () => {
|
||||
describe('When user does not have cases read permissions', () => {
|
||||
test('should not show related cases when user does not have permissions', () => {
|
||||
(useGetUserCasesPermissions as jest.Mock).mockReturnValue({
|
||||
read: false,
|
||||
});
|
||||
(useGetUserCasesPermissions as jest.Mock).mockReturnValue(noCasesPermissions());
|
||||
render(
|
||||
<TestProviders>
|
||||
<RelatedCases eventId={eventId} />
|
||||
|
@ -56,9 +55,7 @@ describe('Related Cases', () => {
|
|||
});
|
||||
describe('When user does have case read permissions', () => {
|
||||
beforeEach(() => {
|
||||
(useGetUserCasesPermissions as jest.Mock).mockReturnValue({
|
||||
read: true,
|
||||
});
|
||||
(useGetUserCasesPermissions as jest.Mock).mockReturnValue(readCasesPermissions());
|
||||
});
|
||||
|
||||
describe('When related cases are unable to be retrieved', () => {
|
||||
|
|
|
@ -25,11 +25,11 @@ export const RelatedCases: React.FC<Props> = React.memo(({ eventId, isReadOnly }
|
|||
services: { cases },
|
||||
} = useKibana();
|
||||
const toasts = useToasts();
|
||||
const casePermissions = useGetUserCasesPermissions();
|
||||
const userCasesPermissions = useGetUserCasesPermissions();
|
||||
const [relatedCases, setRelatedCases] = useState<RelatedCaseList>([]);
|
||||
const [areCasesLoading, setAreCasesLoading] = useState(true);
|
||||
const [hasError, setHasError] = useState<boolean>(false);
|
||||
const hasCasesReadPermissions = casePermissions.read;
|
||||
const hasCasesReadPermissions = userCasesPermissions.read;
|
||||
|
||||
const getRelatedCases = useCallback(async () => {
|
||||
let relatedCaseList: RelatedCaseList = [];
|
||||
|
|
|
@ -20,6 +20,12 @@ import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental
|
|||
import { TestProviders } from '../../../mock';
|
||||
import { CASES_FEATURE_ID } from '../../../../../common/constants';
|
||||
import { useCanSeeHostIsolationExceptionsMenu } from '../../../../management/pages/host_isolation_exceptions/view/hooks';
|
||||
import {
|
||||
noCasesPermissions,
|
||||
readCasesCapabilities,
|
||||
readCasesPermissions,
|
||||
} from '../../../../cases_test_utils';
|
||||
import { mockCasesContract } from '@kbn/cases-plugin/public/mocks';
|
||||
|
||||
jest.mock('../../../lib/kibana/kibana_react');
|
||||
jest.mock('../../../lib/kibana');
|
||||
|
@ -85,8 +91,12 @@ describe('useSecuritySolutionNavigation', () => {
|
|||
(useRouteSpy as jest.Mock).mockReturnValue(mockRouteSpy);
|
||||
(useCanSeeHostIsolationExceptionsMenu as jest.Mock).mockReturnValue(true);
|
||||
|
||||
const cases = mockCasesContract();
|
||||
cases.helpers.getUICapabilities.mockReturnValue(readCasesPermissions());
|
||||
|
||||
(useKibana as jest.Mock).mockReturnValue({
|
||||
services: {
|
||||
cases,
|
||||
application: {
|
||||
navigateToApp: jest.fn(),
|
||||
getUrlForApp: (appId: string, options?: { path?: string; deepLinkId?: boolean }) =>
|
||||
|
@ -96,7 +106,7 @@ describe('useSecuritySolutionNavigation', () => {
|
|||
show: true,
|
||||
crud: true,
|
||||
},
|
||||
[CASES_FEATURE_ID]: { read_cases: true, crud_cases: false },
|
||||
[CASES_FEATURE_ID]: readCasesCapabilities(),
|
||||
},
|
||||
},
|
||||
chrome: {
|
||||
|
@ -154,10 +164,7 @@ describe('useSecuritySolutionNavigation', () => {
|
|||
describe('Permission gated routes', () => {
|
||||
describe('cases', () => {
|
||||
it('should display the cases navigation item when the user has read permissions', () => {
|
||||
(useGetUserCasesPermissions as jest.Mock).mockReturnValue({
|
||||
crud: true,
|
||||
read: true,
|
||||
});
|
||||
(useGetUserCasesPermissions as jest.Mock).mockReturnValue(readCasesPermissions());
|
||||
|
||||
const { result } = renderHook<{}, KibanaPageTemplateProps['solutionNav']>(
|
||||
() => useSecuritySolutionNavigation(),
|
||||
|
@ -182,10 +189,7 @@ describe('useSecuritySolutionNavigation', () => {
|
|||
});
|
||||
|
||||
it('should not display the cases navigation item when the user does not have read permissions', () => {
|
||||
(useGetUserCasesPermissions as jest.Mock).mockReturnValue({
|
||||
crud: false,
|
||||
read: false,
|
||||
});
|
||||
(useGetUserCasesPermissions as jest.Mock).mockReturnValue(noCasesPermissions());
|
||||
|
||||
const { result } = renderHook<{}, KibanaPageTemplateProps['solutionNav']>(
|
||||
() => useSecuritySolutionNavigation(),
|
||||
|
|
|
@ -23,6 +23,7 @@ import { cloneDeep } from 'lodash';
|
|||
import { useKibana } from '../../lib/kibana/kibana_react';
|
||||
import { CASES_FEATURE_ID } from '../../../../common/constants';
|
||||
import { mockCasesContract } from '@kbn/cases-plugin/public/mocks';
|
||||
import { allCasesCapabilities, allCasesPermissions } from '../../../cases_test_utils';
|
||||
jest.mock('react-router-dom', () => {
|
||||
const actual = jest.requireActual('react-router-dom');
|
||||
return {
|
||||
|
@ -70,6 +71,9 @@ describe('VisualizationActions', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
const cases = mockCasesContract();
|
||||
cases.helpers.getUICapabilities.mockReturnValue(allCasesPermissions());
|
||||
|
||||
(useKibana as jest.Mock).mockReturnValue({
|
||||
services: {
|
||||
lens: {
|
||||
|
@ -88,7 +92,7 @@ describe('VisualizationActions', () => {
|
|||
},
|
||||
},
|
||||
application: {
|
||||
capabilities: { [CASES_FEATURE_ID]: { crud_cases: true, read_cases: true } },
|
||||
capabilities: { [CASES_FEATURE_ID]: allCasesCapabilities() },
|
||||
getUrlForApp: jest.fn(),
|
||||
navigateToApp: jest.fn(),
|
||||
},
|
||||
|
|
|
@ -9,6 +9,11 @@ import { useKibana as mockUseKibana } from '../../lib/kibana/__mocks__';
|
|||
import { kpiHostMetricLensAttributes } from './lens_attributes/hosts/kpi_host_metric';
|
||||
import { useAddToExistingCase } from './use_add_to_existing_case';
|
||||
import { useGetUserCasesPermissions } from '../../lib/kibana';
|
||||
import {
|
||||
allCasesPermissions,
|
||||
readCasesPermissions,
|
||||
writeCasesPermissions,
|
||||
} from '../../../cases_test_utils';
|
||||
|
||||
const mockedUseKibana = mockUseKibana();
|
||||
const mockGetUseCasesAddToExistingCaseModal = jest.fn();
|
||||
|
@ -41,10 +46,7 @@ describe('useAddToExistingCase', () => {
|
|||
};
|
||||
|
||||
beforeEach(() => {
|
||||
(useGetUserCasesPermissions as jest.Mock).mockReturnValue({
|
||||
crud: true,
|
||||
read: true,
|
||||
});
|
||||
(useGetUserCasesPermissions as jest.Mock).mockReturnValue(allCasesPermissions());
|
||||
});
|
||||
|
||||
it('getUseCasesAddToExistingCaseModal with attachments', () => {
|
||||
|
@ -62,11 +64,21 @@ describe('useAddToExistingCase', () => {
|
|||
expect(result.current.disabled).toEqual(false);
|
||||
});
|
||||
|
||||
it("button disabled if user Can't Crud", () => {
|
||||
(useGetUserCasesPermissions as jest.Mock).mockReturnValue({
|
||||
crud: false,
|
||||
read: true,
|
||||
});
|
||||
it("disables the button if the user can't create but can read", () => {
|
||||
(useGetUserCasesPermissions as jest.Mock).mockReturnValue(readCasesPermissions());
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useAddToExistingCase({
|
||||
lensAttributes: kpiHostMetricLensAttributes,
|
||||
timeRange,
|
||||
onAddToCaseClicked: mockOnAddToCaseClicked,
|
||||
})
|
||||
);
|
||||
expect(result.current.disabled).toEqual(true);
|
||||
});
|
||||
|
||||
it("disables the button if the user can't read but can create", () => {
|
||||
(useGetUserCasesPermissions as jest.Mock).mockReturnValue(writeCasesPermissions());
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useAddToExistingCase({
|
||||
|
|
|
@ -24,7 +24,7 @@ export const useAddToExistingCase = ({
|
|||
lensAttributes: LensAttributes | null;
|
||||
timeRange: { from: string; to: string } | null;
|
||||
}) => {
|
||||
const userPermissions = useGetUserCasesPermissions();
|
||||
const userCasesPermissions = useGetUserCasesPermissions();
|
||||
const { cases } = useKibana().services;
|
||||
const attachments = useMemo(() => {
|
||||
return [
|
||||
|
@ -53,6 +53,10 @@ export const useAddToExistingCase = ({
|
|||
|
||||
return {
|
||||
onAddToExistingCaseClicked,
|
||||
disabled: lensAttributes == null || timeRange == null || !userPermissions.crud,
|
||||
disabled:
|
||||
lensAttributes == null ||
|
||||
timeRange == null ||
|
||||
!userCasesPermissions.create ||
|
||||
!userCasesPermissions.read,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -9,6 +9,11 @@ import { useKibana as mockUseKibana } from '../../lib/kibana/__mocks__';
|
|||
import { kpiHostMetricLensAttributes } from './lens_attributes/hosts/kpi_host_metric';
|
||||
import { useAddToNewCase } from './use_add_to_new_case';
|
||||
import { useGetUserCasesPermissions } from '../../lib/kibana';
|
||||
import {
|
||||
allCasesPermissions,
|
||||
readCasesPermissions,
|
||||
writeCasesPermissions,
|
||||
} from '../../../cases_test_utils';
|
||||
|
||||
jest.mock('../../lib/kibana/kibana_react');
|
||||
|
||||
|
@ -41,10 +46,7 @@ describe('useAddToNewCase', () => {
|
|||
to: '2022-03-07T15:59:59.999Z',
|
||||
};
|
||||
beforeEach(() => {
|
||||
(useGetUserCasesPermissions as jest.Mock).mockReturnValue({
|
||||
crud: true,
|
||||
read: true,
|
||||
});
|
||||
(useGetUserCasesPermissions as jest.Mock).mockReturnValue(allCasesPermissions());
|
||||
});
|
||||
|
||||
it('getUseCasesAddToNewCaseFlyout with attachments', () => {
|
||||
|
@ -60,11 +62,20 @@ describe('useAddToNewCase', () => {
|
|||
expect(result.current.disabled).toEqual(false);
|
||||
});
|
||||
|
||||
it("button disabled if user Can't Crud", () => {
|
||||
(useGetUserCasesPermissions as jest.Mock).mockReturnValue({
|
||||
crud: false,
|
||||
read: true,
|
||||
});
|
||||
it("disables the button if the user can't create but can read", () => {
|
||||
(useGetUserCasesPermissions as jest.Mock).mockReturnValue(readCasesPermissions());
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useAddToNewCase({
|
||||
lensAttributes: kpiHostMetricLensAttributes,
|
||||
timeRange,
|
||||
})
|
||||
);
|
||||
expect(result.current.disabled).toEqual(true);
|
||||
});
|
||||
|
||||
it("disables the button if the user can't read but can create", () => {
|
||||
(useGetUserCasesPermissions as jest.Mock).mockReturnValue(writeCasesPermissions());
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useAddToNewCase({
|
||||
|
|
|
@ -23,7 +23,7 @@ export interface UseAddToNewCaseProps {
|
|||
const owner = APP_ID;
|
||||
|
||||
export const useAddToNewCase = ({ onClick, timeRange, lensAttributes }: UseAddToNewCaseProps) => {
|
||||
const userPermissions = useGetUserCasesPermissions();
|
||||
const userCasesPermissions = useGetUserCasesPermissions();
|
||||
const { cases } = useKibana().services;
|
||||
const attachments = useMemo(() => {
|
||||
return [
|
||||
|
@ -52,6 +52,10 @@ export const useAddToNewCase = ({ onClick, timeRange, lensAttributes }: UseAddTo
|
|||
|
||||
return {
|
||||
onAddToNewCaseClicked,
|
||||
disabled: lensAttributes == null || timeRange == null || !userPermissions.crud,
|
||||
disabled:
|
||||
lensAttributes == null ||
|
||||
timeRange == null ||
|
||||
!userCasesPermissions.create ||
|
||||
!userCasesPermissions.read,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -14,6 +14,7 @@ import { camelCase, isArray, isObject } from 'lodash';
|
|||
import { set } from '@elastic/safer-lodash-set';
|
||||
import type { AuthenticatedUser } from '@kbn/security-plugin/common/model';
|
||||
import type { NavigateToAppOptions } from '@kbn/core/public';
|
||||
import type { CasesPermissions } from '@kbn/cases-plugin/common/ui';
|
||||
import {
|
||||
APP_UI_ID,
|
||||
CASES_FEATURE_ID,
|
||||
|
@ -146,24 +147,37 @@ export const useCurrentUser = (): AuthenticatedElasticUser | null => {
|
|||
return user;
|
||||
};
|
||||
|
||||
export interface UseGetUserCasesPermissions {
|
||||
crud: boolean;
|
||||
read: boolean;
|
||||
}
|
||||
|
||||
export const useGetUserCasesPermissions = () => {
|
||||
const [casesPermissions, setCasesPermissions] = useState<UseGetUserCasesPermissions>({
|
||||
crud: false,
|
||||
const [casesPermissions, setCasesPermissions] = useState<CasesPermissions>({
|
||||
all: false,
|
||||
create: false,
|
||||
read: false,
|
||||
update: false,
|
||||
delete: false,
|
||||
push: false,
|
||||
});
|
||||
const uiCapabilities = useKibana().services.application.capabilities;
|
||||
const casesCapabilities = useKibana().services.cases.helpers.getUICapabilities(
|
||||
uiCapabilities[CASES_FEATURE_ID]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setCasesPermissions({
|
||||
crud: !!uiCapabilities[CASES_FEATURE_ID]?.crud_cases,
|
||||
read: !!uiCapabilities[CASES_FEATURE_ID]?.read_cases,
|
||||
all: casesCapabilities.all,
|
||||
create: casesCapabilities.create,
|
||||
read: casesCapabilities.read,
|
||||
update: casesCapabilities.update,
|
||||
delete: casesCapabilities.delete,
|
||||
push: casesCapabilities.push,
|
||||
});
|
||||
}, [uiCapabilities]);
|
||||
}, [
|
||||
casesCapabilities.all,
|
||||
casesCapabilities.create,
|
||||
casesCapabilities.read,
|
||||
casesCapabilities.update,
|
||||
casesCapabilities.delete,
|
||||
casesCapabilities.push,
|
||||
]);
|
||||
|
||||
return casesPermissions;
|
||||
};
|
||||
|
|
|
@ -40,6 +40,8 @@ import { MlLocatorDefinition } from '@kbn/ml-plugin/public';
|
|||
import type { EuiTheme } from '@kbn/kibana-react-plugin/common';
|
||||
import { MockUrlService } from '@kbn/share-plugin/common/mocks';
|
||||
import { fleetMock } from '@kbn/fleet-plugin/public/mocks';
|
||||
import { mockCasesContract } from '@kbn/cases-plugin/public/mocks';
|
||||
import { noCasesPermissions } from '../../../cases_test_utils';
|
||||
import { triggersActionsUiMock } from '@kbn/triggers-actions-ui-plugin/public/mocks';
|
||||
|
||||
const mockUiSettings: Record<string, unknown> = {
|
||||
|
@ -98,17 +100,13 @@ export const createStartServicesMock = (
|
|||
const locator = urlService.locators.create(new MlLocatorDefinition());
|
||||
const fleet = fleetMock.createStartMock();
|
||||
const unifiedSearch = unifiedSearchPluginMock.createStartContract();
|
||||
const cases = mockCasesContract();
|
||||
cases.helpers.getUICapabilities.mockReturnValue(noCasesPermissions());
|
||||
const triggersActionsUi = triggersActionsUiMock.createStart();
|
||||
|
||||
return {
|
||||
...core,
|
||||
cases: {
|
||||
getAllCases: jest.fn(),
|
||||
getCaseView: jest.fn(),
|
||||
getConfigureCases: jest.fn(),
|
||||
getCreateCase: jest.fn(),
|
||||
getRecentCases: jest.fn(),
|
||||
},
|
||||
cases,
|
||||
unifiedSearch,
|
||||
data: {
|
||||
...data,
|
||||
|
|
|
@ -18,7 +18,9 @@ import {
|
|||
needsUrlState,
|
||||
updateAppLinks,
|
||||
useLinkExists,
|
||||
hasCapabilities,
|
||||
} from './links';
|
||||
import { createCapabilities } from './test_utils';
|
||||
|
||||
const defaultAppLinks: AppLinkItems = [
|
||||
{
|
||||
|
@ -288,4 +290,118 @@ describe('Security app links', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasCapabilities', () => {
|
||||
const siemShow = 'siem.show';
|
||||
const createCases = 'securitySolutionCases.create_cases';
|
||||
const readCases = 'securitySolutionCases.read_cases';
|
||||
const pushCases = 'securitySolutionCases.push_cases';
|
||||
|
||||
it('returns false when capabilities is an empty array', () => {
|
||||
expect(hasCapabilities([], createCapabilities())).toBeFalsy();
|
||||
});
|
||||
|
||||
it('returns true when the capability requested is specified as a single value', () => {
|
||||
expect(hasCapabilities(siemShow, createCapabilities({ siem: { show: true } }))).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns true when the capability requested is a single entry in an array', () => {
|
||||
expect(
|
||||
hasCapabilities([siemShow], createCapabilities({ siem: { show: true } }))
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it("returns true when the capability requested is a single entry in an AND'd array format", () => {
|
||||
expect(
|
||||
hasCapabilities([[siemShow]], createCapabilities({ siem: { show: true } }))
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns true when only one requested capability is found in an OR situation', () => {
|
||||
expect(
|
||||
hasCapabilities(
|
||||
[siemShow, createCases],
|
||||
createCapabilities({
|
||||
siem: { show: true },
|
||||
securitySolutionCases: { create_cases: false },
|
||||
})
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns true when only the create_cases requested capability is found in an OR situation', () => {
|
||||
expect(
|
||||
hasCapabilities(
|
||||
[siemShow, createCases],
|
||||
createCapabilities({
|
||||
siem: { show: false },
|
||||
securitySolutionCases: { create_cases: true },
|
||||
})
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns false when none of the requested capabilities are found in an OR situation', () => {
|
||||
expect(
|
||||
hasCapabilities(
|
||||
[readCases, createCases],
|
||||
createCapabilities({
|
||||
siem: { show: true },
|
||||
securitySolutionCases: { create_cases: false },
|
||||
})
|
||||
)
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it('returns true when all of the requested capabilities are found in an AND situation', () => {
|
||||
expect(
|
||||
hasCapabilities(
|
||||
[[readCases, createCases]],
|
||||
createCapabilities({
|
||||
siem: { show: true },
|
||||
securitySolutionCases: { read_cases: true, create_cases: true },
|
||||
})
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns false when neither the single OR capability is found nor all of the AND capabilities', () => {
|
||||
expect(
|
||||
hasCapabilities(
|
||||
[siemShow, [readCases, createCases]],
|
||||
createCapabilities({
|
||||
siem: { show: false },
|
||||
securitySolutionCases: { read_cases: false, create_cases: true },
|
||||
})
|
||||
)
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it('returns true when the single OR capability is found when using an OR with an AND format', () => {
|
||||
expect(
|
||||
hasCapabilities(
|
||||
[siemShow, [readCases, createCases]],
|
||||
createCapabilities({
|
||||
siem: { show: true },
|
||||
securitySolutionCases: { read_cases: false, create_cases: true },
|
||||
})
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it("returns false when the AND'd expressions are not satisfied", () => {
|
||||
expect(
|
||||
hasCapabilities(
|
||||
[
|
||||
[siemShow, pushCases],
|
||||
[readCases, createCases],
|
||||
],
|
||||
createCapabilities({
|
||||
siem: { show: true },
|
||||
securitySolutionCases: { read_cases: false, create_cases: true, push_cases: false },
|
||||
})
|
||||
)
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import type { Capabilities } from '@kbn/core/public';
|
||||
import { get } from 'lodash';
|
||||
import { get, isArray } from 'lodash';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import type { SecurityPageName } from '../../../common/constants';
|
||||
|
@ -173,9 +173,35 @@ const getFilteredAppLinks = (
|
|||
return acc;
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* The format of defining features supports OR and AND mechanism. To specify features in an OR fashion
|
||||
* they can be defined in a single level array like: [requiredFeature1, requiredFeature2]. If either of these features
|
||||
* is satisfied the links would be included. To require that the features be AND'd together a second level array
|
||||
* can be specified: [feature1, [feature2, feature3]] this would result in feature1 || (feature2 && feature3).
|
||||
*
|
||||
* The final format is to specify a single feature, this would be like: features: feature1, which is the same as
|
||||
* features: [feature1]
|
||||
*/
|
||||
type LinkCapabilities = string | Array<string | string[]>;
|
||||
|
||||
// It checks if the user has at least one of the link capabilities needed
|
||||
const hasCapabilities = (linkCapabilities: string[], userCapabilities: Capabilities): boolean =>
|
||||
linkCapabilities.some((linkCapability) => get(userCapabilities, linkCapability, false));
|
||||
export const hasCapabilities = <T>(
|
||||
linkCapabilities: LinkCapabilities,
|
||||
userCapabilities: Capabilities
|
||||
): boolean => {
|
||||
if (!isArray(linkCapabilities)) {
|
||||
return !!get(userCapabilities, linkCapabilities, false);
|
||||
} else {
|
||||
return linkCapabilities.some((linkCapabilityKeyOr) => {
|
||||
if (isArray(linkCapabilityKeyOr)) {
|
||||
return linkCapabilityKeyOr.every((linkCapabilityKeyAnd) =>
|
||||
get(userCapabilities, linkCapabilityKeyAnd, false)
|
||||
);
|
||||
}
|
||||
return get(userCapabilities, linkCapabilityKeyOr, false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const isLinkAllowed = (
|
||||
link: LinkItem,
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Capabilities } from '@kbn/core/public';
|
||||
|
||||
interface FeatureCap {
|
||||
[key: string]: Record<string, boolean | Record<string, boolean>>;
|
||||
}
|
||||
|
||||
export const createCapabilities = (capabilities?: FeatureCap): Capabilities => {
|
||||
return {
|
||||
navLinks: {},
|
||||
management: {},
|
||||
catalogue: {},
|
||||
...capabilities,
|
||||
};
|
||||
};
|
|
@ -38,9 +38,18 @@ export interface LinkItem {
|
|||
experimentalKey?: keyof ExperimentalFeatures;
|
||||
/**
|
||||
* Capabilities strings (using object dot notation) to enable the link.
|
||||
* Uses "or" conditional, only one enabled capability is needed to activate the link
|
||||
*
|
||||
* The format of defining features supports OR and AND mechanism. To specify features in an OR fashion
|
||||
* they can be defined in a single level array like: [requiredFeature1, requiredFeature2]. If either of these features
|
||||
* is satisfied the deeplinks would be included. To require that the features be AND'd together a second level array
|
||||
* can be specified: [feature1, [feature2, feature3]] this would result in feature1 || (feature2 && feature3). To specify
|
||||
* features that all must be and'd together an example would be: [[feature1, feature2]], this would result in the boolean
|
||||
* operation feature1 && feature2.
|
||||
*
|
||||
* The final format is to specify a single feature, this would be like: features: feature1, which is the same as
|
||||
* features: [feature1]
|
||||
*/
|
||||
capabilities?: string[];
|
||||
capabilities?: string | Array<string | string[]>;
|
||||
/**
|
||||
* Categories to display in the navigation
|
||||
*/
|
||||
|
|
|
@ -63,8 +63,12 @@ jest.mock('../../../../common/lib/kibana', () => ({
|
|||
},
|
||||
}),
|
||||
useGetUserCasesPermissions: jest.fn().mockReturnValue({
|
||||
crud: true,
|
||||
all: true,
|
||||
create: true,
|
||||
read: true,
|
||||
update: true,
|
||||
delete: true,
|
||||
push: true,
|
||||
}),
|
||||
}));
|
||||
|
||||
|
|
|
@ -34,8 +34,7 @@ export const useAddToCaseActions = ({
|
|||
timelineId,
|
||||
}: UseAddToCaseActions) => {
|
||||
const { cases: casesUi } = useKibana().services;
|
||||
const casePermissions = useGetUserCasesPermissions();
|
||||
const hasWritePermissions = casePermissions.crud;
|
||||
const userCasesPermissions = useGetUserCasesPermissions();
|
||||
|
||||
const isAlert = useMemo(() => {
|
||||
return ecsData?.event?.kind?.includes('signal');
|
||||
|
@ -84,7 +83,8 @@ export const useAddToCaseActions = ({
|
|||
TimelineId.detectionsRulesDetailsPage,
|
||||
TimelineId.active,
|
||||
].includes(timelineId as TimelineId) &&
|
||||
hasWritePermissions &&
|
||||
userCasesPermissions.create &&
|
||||
userCasesPermissions.read &&
|
||||
isAlert
|
||||
) {
|
||||
return [
|
||||
|
@ -113,7 +113,8 @@ export const useAddToCaseActions = ({
|
|||
ariaLabel,
|
||||
handleAddToExistingCaseClick,
|
||||
handleAddToNewCaseClick,
|
||||
hasWritePermissions,
|
||||
userCasesPermissions.create,
|
||||
userCasesPermissions.read,
|
||||
timelineId,
|
||||
isAlert,
|
||||
]);
|
||||
|
|
|
@ -19,8 +19,7 @@ export interface UseAddToCaseActions {
|
|||
export const useBulkAddToCaseActions = ({ onClose, onSuccess }: UseAddToCaseActions = {}) => {
|
||||
const { cases: casesUi } = useKibana().services;
|
||||
|
||||
const casePermissions = useGetUserCasesPermissions();
|
||||
const hasWritePermissions = casePermissions.crud;
|
||||
const userCasesPermissions = useGetUserCasesPermissions();
|
||||
|
||||
const createCaseFlyout = casesUi.hooks.getUseCasesAddToNewCaseFlyout({
|
||||
onClose,
|
||||
|
@ -32,7 +31,7 @@ export const useBulkAddToCaseActions = ({ onClose, onSuccess }: UseAddToCaseActi
|
|||
});
|
||||
|
||||
return useMemo(() => {
|
||||
return hasWritePermissions
|
||||
return userCasesPermissions.create && userCasesPermissions.read
|
||||
? [
|
||||
{
|
||||
label: ADD_TO_NEW_CASE,
|
||||
|
@ -58,5 +57,11 @@ export const useBulkAddToCaseActions = ({ onClose, onSuccess }: UseAddToCaseActi
|
|||
},
|
||||
]
|
||||
: [];
|
||||
}, [casesUi.helpers, createCaseFlyout, hasWritePermissions, selectCaseModal]);
|
||||
}, [
|
||||
casesUi.helpers,
|
||||
createCaseFlyout,
|
||||
userCasesPermissions.create,
|
||||
userCasesPermissions.read,
|
||||
selectCaseModal,
|
||||
]);
|
||||
};
|
||||
|
|
|
@ -35,6 +35,7 @@ import {
|
|||
} from '../../../common/utils/endpoint_alert_check';
|
||||
import { HostStatus } from '../../../../common/endpoint/types';
|
||||
import { getUserPrivilegesMockDefaultValue } from '../../../common/components/user_privileges/__mocks__';
|
||||
import { allCasesPermissions } from '../../../cases_test_utils';
|
||||
|
||||
jest.mock('../../../common/components/user_privileges');
|
||||
|
||||
|
@ -43,7 +44,7 @@ jest.mock('../user_info', () => ({
|
|||
}));
|
||||
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
(useGetUserCasesPermissions as jest.Mock).mockReturnValue({ crud: true });
|
||||
(useGetUserCasesPermissions as jest.Mock).mockReturnValue(allCasesPermissions());
|
||||
|
||||
jest.mock('../../containers/detection_engine/alerts/use_alerts_privileges', () => ({
|
||||
useAlertsPrivileges: jest.fn().mockReturnValue({ hasIndexWrite: true, hasKibanaCRUD: true }),
|
||||
|
|
|
@ -18,6 +18,11 @@ import {
|
|||
getField,
|
||||
} from './helpers';
|
||||
import type { StartedSubPlugins } from './types';
|
||||
import {
|
||||
allCasesCapabilities,
|
||||
noCasesCapabilities,
|
||||
readCasesCapabilities,
|
||||
} from './cases_test_utils';
|
||||
|
||||
describe('public helpers parseRoute', () => {
|
||||
it('should properly parse hash route', () => {
|
||||
|
@ -76,7 +81,7 @@ describe('#getSubPluginRoutesByCapabilities', () => {
|
|||
it('cases routes should return NoPrivilegesPage component when cases plugin is NOT available ', () => {
|
||||
const routes = getSubPluginRoutesByCapabilities(mockSubPlugins, {
|
||||
[SERVER_APP_ID]: { show: true, crud: false },
|
||||
[CASES_FEATURE_ID]: { read_cases: false, crud_cases: false },
|
||||
[CASES_FEATURE_ID]: noCasesCapabilities(),
|
||||
} as unknown as Capabilities);
|
||||
const casesRoute = routes.find((r) => r.path === 'cases');
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
@ -91,7 +96,7 @@ describe('#getSubPluginRoutesByCapabilities', () => {
|
|||
it('alerts should return NoPrivilegesPage component when siem plugin is NOT available ', () => {
|
||||
const routes = getSubPluginRoutesByCapabilities(mockSubPlugins, {
|
||||
[SERVER_APP_ID]: { show: false, crud: false },
|
||||
[CASES_FEATURE_ID]: { read_cases: true, crud_cases: false },
|
||||
[CASES_FEATURE_ID]: readCasesCapabilities(),
|
||||
} as unknown as Capabilities);
|
||||
const alertsRoute = routes.find((r) => r.path === 'alerts');
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
@ -106,7 +111,7 @@ describe('#getSubPluginRoutesByCapabilities', () => {
|
|||
it('should return NoPrivilegesPage for each route when both plugins are NOT available ', () => {
|
||||
const routes = getSubPluginRoutesByCapabilities(mockSubPlugins, {
|
||||
[SERVER_APP_ID]: { show: false, crud: false },
|
||||
[CASES_FEATURE_ID]: { read_cases: false, crud_cases: false },
|
||||
[CASES_FEATURE_ID]: noCasesCapabilities(),
|
||||
} as unknown as Capabilities);
|
||||
const casesRoute = routes.find((r) => r.path === 'cases');
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
@ -134,7 +139,7 @@ describe('#isSubPluginAvailable', () => {
|
|||
expect(
|
||||
isSubPluginAvailable('pluginKey', {
|
||||
[SERVER_APP_ID]: { show: true, crud: true },
|
||||
[CASES_FEATURE_ID]: { read_cases: false, crud_cases: false },
|
||||
[CASES_FEATURE_ID]: noCasesCapabilities(),
|
||||
} as unknown as Capabilities)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
@ -143,7 +148,7 @@ describe('#isSubPluginAvailable', () => {
|
|||
expect(
|
||||
isSubPluginAvailable('pluginKey', {
|
||||
[SERVER_APP_ID]: { show: true, crud: false },
|
||||
[CASES_FEATURE_ID]: { read_cases: false, crud_cases: false },
|
||||
[CASES_FEATURE_ID]: noCasesCapabilities(),
|
||||
} as unknown as Capabilities)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
@ -152,7 +157,7 @@ describe('#isSubPluginAvailable', () => {
|
|||
expect(
|
||||
isSubPluginAvailable('pluginKey', {
|
||||
[SERVER_APP_ID]: { show: false, crud: false },
|
||||
[CASES_FEATURE_ID]: { read_cases: false, crud_cases: false },
|
||||
[CASES_FEATURE_ID]: noCasesCapabilities(),
|
||||
} as unknown as Capabilities)
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
@ -161,7 +166,7 @@ describe('#isSubPluginAvailable', () => {
|
|||
expect(
|
||||
isSubPluginAvailable('cases', {
|
||||
[SERVER_APP_ID]: { show: false, crud: false },
|
||||
[CASES_FEATURE_ID]: { read_cases: true, crud_cases: true },
|
||||
[CASES_FEATURE_ID]: allCasesCapabilities(),
|
||||
} as unknown as Capabilities)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
@ -170,7 +175,7 @@ describe('#isSubPluginAvailable', () => {
|
|||
expect(
|
||||
isSubPluginAvailable('cases', {
|
||||
[SERVER_APP_ID]: { show: false, crud: false },
|
||||
[CASES_FEATURE_ID]: { read_cases: true, crud_cases: false },
|
||||
[CASES_FEATURE_ID]: readCasesCapabilities(),
|
||||
} as unknown as Capabilities)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
@ -179,7 +184,7 @@ describe('#isSubPluginAvailable', () => {
|
|||
expect(
|
||||
isSubPluginAvailable('pluginKey', {
|
||||
[SERVER_APP_ID]: { show: false, crud: false },
|
||||
[CASES_FEATURE_ID]: { read_cases: false, crud_cases: false },
|
||||
[CASES_FEATURE_ID]: noCasesCapabilities(),
|
||||
} as unknown as Capabilities)
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
@ -189,7 +194,7 @@ describe('RedirectRoute', () => {
|
|||
it('RedirectRoute should redirect to overview page when siem and case privileges are all', () => {
|
||||
const mockCapabilitities = {
|
||||
[SERVER_APP_ID]: { show: true, crud: true },
|
||||
[CASES_FEATURE_ID]: { read_cases: true, crud_cases: true },
|
||||
[CASES_FEATURE_ID]: allCasesCapabilities(),
|
||||
} as unknown as Capabilities;
|
||||
expect(shallow(<RedirectRoute capabilities={mockCapabilitities} />)).toMatchInlineSnapshot(`
|
||||
<Redirect
|
||||
|
@ -201,7 +206,7 @@ describe('RedirectRoute', () => {
|
|||
it('RedirectRoute should redirect to overview page when siem and case privileges are read', () => {
|
||||
const mockCapabilitities = {
|
||||
[SERVER_APP_ID]: { show: true, crud: false },
|
||||
[CASES_FEATURE_ID]: { read_cases: true, crud_cases: false },
|
||||
[CASES_FEATURE_ID]: readCasesCapabilities(),
|
||||
} as unknown as Capabilities;
|
||||
expect(shallow(<RedirectRoute capabilities={mockCapabilitities} />)).toMatchInlineSnapshot(`
|
||||
<Redirect
|
||||
|
@ -213,7 +218,7 @@ describe('RedirectRoute', () => {
|
|||
it('RedirectRoute should redirect to overview page when siem and case privileges are off', () => {
|
||||
const mockCapabilitities = {
|
||||
[SERVER_APP_ID]: { show: false, crud: false },
|
||||
[CASES_FEATURE_ID]: { read_cases: false, crud_cases: false },
|
||||
[CASES_FEATURE_ID]: noCasesCapabilities(),
|
||||
} as unknown as Capabilities;
|
||||
expect(shallow(<RedirectRoute capabilities={mockCapabilitities} />)).toMatchInlineSnapshot(`
|
||||
<Redirect
|
||||
|
@ -225,7 +230,7 @@ describe('RedirectRoute', () => {
|
|||
it('RedirectRoute should redirect to overview page when siem privilege is read and case privilege is all', () => {
|
||||
const mockCapabilitities = {
|
||||
[SERVER_APP_ID]: { show: true, crud: false },
|
||||
[CASES_FEATURE_ID]: { read_cases: true, crud_cases: true },
|
||||
[CASES_FEATURE_ID]: allCasesCapabilities(),
|
||||
} as unknown as Capabilities;
|
||||
expect(shallow(<RedirectRoute capabilities={mockCapabilitities} />)).toMatchInlineSnapshot(`
|
||||
<Redirect
|
||||
|
@ -237,7 +242,7 @@ describe('RedirectRoute', () => {
|
|||
it('RedirectRoute should redirect to overview page when siem privilege is read and case privilege is read', () => {
|
||||
const mockCapabilitities = {
|
||||
[SERVER_APP_ID]: { show: true, crud: false },
|
||||
[CASES_FEATURE_ID]: { read_cases: true, crud_cases: true },
|
||||
[CASES_FEATURE_ID]: allCasesCapabilities(),
|
||||
} as unknown as Capabilities;
|
||||
expect(shallow(<RedirectRoute capabilities={mockCapabilitities} />)).toMatchInlineSnapshot(`
|
||||
<Redirect
|
||||
|
@ -249,7 +254,7 @@ describe('RedirectRoute', () => {
|
|||
it('RedirectRoute should redirect to cases page when siem privilege is none and case privilege is read', () => {
|
||||
const mockCapabilitities = {
|
||||
[SERVER_APP_ID]: { show: false, crud: false },
|
||||
[CASES_FEATURE_ID]: { read_cases: true, crud_cases: false },
|
||||
[CASES_FEATURE_ID]: readCasesCapabilities(),
|
||||
} as unknown as Capabilities;
|
||||
expect(shallow(<RedirectRoute capabilities={mockCapabilitities} />)).toMatchInlineSnapshot(`
|
||||
<Redirect
|
||||
|
@ -261,7 +266,7 @@ describe('RedirectRoute', () => {
|
|||
it('RedirectRoute should redirect to cases page when siem privilege is none and case privilege is all', () => {
|
||||
const mockCapabilitities = {
|
||||
[SERVER_APP_ID]: { show: false, crud: false },
|
||||
[CASES_FEATURE_ID]: { read_cases: true, crud_cases: true },
|
||||
[CASES_FEATURE_ID]: allCasesCapabilities(),
|
||||
} as unknown as Capabilities;
|
||||
expect(shallow(<RedirectRoute capabilities={mockCapabilitities} />)).toMatchInlineSnapshot(`
|
||||
<Redirect
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue