mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Security Solutions] Add PLI authorisation for Cases Connector (#161343)
## Summary * Create a new capability called `cases_connectors` which will control the access to the cases connector feature. Note that for users to have access to this feature they also need to be authorized for cases feature and actions feature. * Create a new API tag `casesGetConnectorsConfigure` to restrict access to the Get Connectors APIs. ## Authorization For the authorization of users we use a) a new UI capability b) a new API access tag and c) the existing Cases RBAC. The Cases feature privilege in Security solution is constructed based on the configuration provided by the security serverless plugin. The UI capability, the API tag, and the cases operations will be added/removed depending on the configuration. ### UI capability We include the `CASES_CONNECTORS_CAPABILITY` which will be used by the UI to show/hide various UI components responsible for the case connectors feature. ### APIs There are two APIs that use connectors in Cases. The [Get Connectors API](https://www.elastic.co/guide/en/kibana/current/case-apis.html#findCaseConnectors) which returns all supported connectors by Cases and the [Push Case API](https://www.elastic.co/guide/en/kibana/current/case-apis.html#pushCaseDefaultSpace) that push a case to an external service. #### Get Connectors API The Get Connectors API does not interact with any of the cases' saved objects. It uses the `actionsClient`, provided by the actions plugin, to get all connectors and filter out the ones supported by cases. For that reason, an API tag called `GET_CONNECTORS_CONFIGURE_API_TAG` is added to the API to control access. If the user has access to any of the Cases kibana privilege features (Security, Observability, or Stack) it will have access to the API. This is an expected behavior and in the Security serverless project, only one Case feature will be available. #### Push Case API The Push Case API already authorizes users by using the Cases RBAC. The user should have the `push` operation set in the Cases Kibana feature privilege to be able to use the API. ## Permissions <meta charset="utf-8"><b style="font-weight:normal;" id="docs-internal-guid-d1fea174-7fff-4f03-ed2e-9fc3ad3ed789"><div dir="ltr" style="margin-left:0pt;" align="left"> Cases | Actions | Case Connectors | Outcome -- | -- | -- | -- read | all | all | See the connector but cannot edit (current behavior) read | all | none | Hide the connectors in Cases read | read | all | See the connector but cannot edit (current behavior) read | read | none | Hide the connectors in Cases all | all | all | Full access all | all | none | Hide the connectors in Cases all | read | all | See the connector but cannot edit (current behavior) all | read | none | Hide the connectors in Cases </div><br /></b> When the Actions is set to `none` all connector features are hidden ### How to test it? #### ESS * Run ESS and check if it still works as expected for all combinations of cases and actions permissions. #### Serverless * Run Serverless with security essentials (serverless.security.yml) and check if it works as expected for all combinations of cases and actions permissions. ``` xpack.serverless.security.productTypes: [ { product_line: 'security', product_tier: 'essentials' } ] ``` * Run Serverless with security complete (config/serverless.security.yml) and check if it works as expected for all combinations of cases and actions permissions. ``` xpack.serverless.security.productTypes: [ { product_line: 'security', product_tier: 'complete' }, ] ``` ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Christos Nasikas <christos.nasikas@elastic.co>
This commit is contained in:
parent
527c2d5884
commit
aa42bccd40
41 changed files with 478 additions and 96 deletions
|
@ -158,6 +158,7 @@ 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;
|
||||
export const CASES_CONNECTORS_CAPABILITY = 'cases_connectors' as const;
|
||||
|
||||
/**
|
||||
* Cases API Tags
|
||||
|
@ -173,6 +174,11 @@ export const SUGGEST_USER_PROFILES_API_TAG = 'casesSuggestUserProfiles';
|
|||
*/
|
||||
export const BULK_GET_USER_PROFILES_API_TAG = 'bulkGetUserProfiles';
|
||||
|
||||
/**
|
||||
* This tag is registered for the connectors (configure) get API
|
||||
*/
|
||||
export const GET_CONNECTORS_CONFIGURE_API_TAG = 'casesGetConnectorsConfigure';
|
||||
|
||||
/**
|
||||
* User profiles
|
||||
*/
|
||||
|
|
|
@ -12,7 +12,8 @@ import type {
|
|||
READ_CASES_CAPABILITY,
|
||||
UPDATE_CASES_CAPABILITY,
|
||||
} from '..';
|
||||
import type { PUSH_CASES_CAPABILITY } from '../constants';
|
||||
import type { CaseMetricsFeature, CasesMetricsResponse, SingleCaseMetricsResponse } from '../api';
|
||||
import type { CASES_CONNECTORS_CAPABILITY, PUSH_CASES_CAPABILITY } from '../constants';
|
||||
import type { SnakeToCamelCase } from '../types';
|
||||
import type {
|
||||
CaseSeverity,
|
||||
|
@ -285,6 +286,7 @@ export interface CasesPermissions {
|
|||
update: boolean;
|
||||
delete: boolean;
|
||||
push: boolean;
|
||||
connectors: boolean;
|
||||
}
|
||||
|
||||
export interface CasesCapabilities {
|
||||
|
@ -293,4 +295,5 @@ export interface CasesCapabilities {
|
|||
[UPDATE_CASES_CAPABILITY]: boolean;
|
||||
[DELETE_CASES_CAPABILITY]: boolean;
|
||||
[PUSH_CASES_CAPABILITY]: boolean;
|
||||
[CASES_CONNECTORS_CAPABILITY]: boolean;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ Object {
|
|||
"all": Array [
|
||||
"casesSuggestUserProfiles",
|
||||
"bulkGetUserProfiles",
|
||||
"casesGetConnectorsConfigure",
|
||||
"casesFilesCasesCreate",
|
||||
"casesFilesCasesRead",
|
||||
],
|
||||
|
@ -14,6 +15,7 @@ Object {
|
|||
"read": Array [
|
||||
"casesSuggestUserProfiles",
|
||||
"bulkGetUserProfiles",
|
||||
"casesGetConnectorsConfigure",
|
||||
"casesFilesCasesRead",
|
||||
],
|
||||
}
|
||||
|
@ -24,6 +26,7 @@ Object {
|
|||
"all": Array [
|
||||
"casesSuggestUserProfiles",
|
||||
"bulkGetUserProfiles",
|
||||
"casesGetConnectorsConfigure",
|
||||
"observabilityFilesCasesCreate",
|
||||
"observabilityFilesCasesRead",
|
||||
],
|
||||
|
@ -33,6 +36,7 @@ Object {
|
|||
"read": Array [
|
||||
"casesSuggestUserProfiles",
|
||||
"bulkGetUserProfiles",
|
||||
"casesGetConnectorsConfigure",
|
||||
"observabilityFilesCasesRead",
|
||||
],
|
||||
}
|
||||
|
@ -43,6 +47,7 @@ Object {
|
|||
"all": Array [
|
||||
"casesSuggestUserProfiles",
|
||||
"bulkGetUserProfiles",
|
||||
"casesGetConnectorsConfigure",
|
||||
"securitySolutionFilesCasesCreate",
|
||||
"securitySolutionFilesCasesRead",
|
||||
],
|
||||
|
@ -52,6 +57,7 @@ Object {
|
|||
"read": Array [
|
||||
"casesSuggestUserProfiles",
|
||||
"bulkGetUserProfiles",
|
||||
"casesGetConnectorsConfigure",
|
||||
"securitySolutionFilesCasesRead",
|
||||
],
|
||||
}
|
||||
|
|
|
@ -5,7 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { BULK_GET_USER_PROFILES_API_TAG, SUGGEST_USER_PROFILES_API_TAG } from '../constants';
|
||||
import {
|
||||
BULK_GET_USER_PROFILES_API_TAG,
|
||||
GET_CONNECTORS_CONFIGURE_API_TAG,
|
||||
SUGGEST_USER_PROFILES_API_TAG,
|
||||
} from '../constants';
|
||||
import { HttpApiTagOperation } from '../constants/types';
|
||||
import type { Owner } from '../constants/types';
|
||||
import { constructFilesHttpOperationTag } from '../files';
|
||||
|
@ -16,8 +20,19 @@ export const getApiTags = (owner: Owner) => {
|
|||
const read = constructFilesHttpOperationTag(owner, HttpApiTagOperation.Read);
|
||||
|
||||
return {
|
||||
all: [SUGGEST_USER_PROFILES_API_TAG, BULK_GET_USER_PROFILES_API_TAG, create, read] as const,
|
||||
read: [SUGGEST_USER_PROFILES_API_TAG, BULK_GET_USER_PROFILES_API_TAG, read] as const,
|
||||
all: [
|
||||
SUGGEST_USER_PROFILES_API_TAG,
|
||||
BULK_GET_USER_PROFILES_API_TAG,
|
||||
GET_CONNECTORS_CONFIGURE_API_TAG,
|
||||
create,
|
||||
read,
|
||||
] as const,
|
||||
read: [
|
||||
SUGGEST_USER_PROFILES_API_TAG,
|
||||
BULK_GET_USER_PROFILES_API_TAG,
|
||||
GET_CONNECTORS_CONFIGURE_API_TAG,
|
||||
read,
|
||||
] as const,
|
||||
delete: [deleteTag] as const,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
CASES_CONNECTORS_CAPABILITY,
|
||||
CREATE_CASES_CAPABILITY,
|
||||
DELETE_CASES_CAPABILITY,
|
||||
PUSH_CASES_CAPABILITY,
|
||||
|
@ -23,7 +24,8 @@ export const createUICapabilities = () => ({
|
|||
READ_CASES_CAPABILITY,
|
||||
UPDATE_CASES_CAPABILITY,
|
||||
PUSH_CASES_CAPABILITY,
|
||||
CASES_CONNECTORS_CAPABILITY,
|
||||
] as const,
|
||||
read: [READ_CASES_CAPABILITY] as const,
|
||||
read: [READ_CASES_CAPABILITY, CASES_CONNECTORS_CAPABILITY] as const,
|
||||
delete: [DELETE_CASES_CAPABILITY] as const,
|
||||
});
|
||||
|
|
|
@ -11,8 +11,8 @@ import {
|
|||
allCasesPermissions,
|
||||
noCasesCapabilities,
|
||||
noCasesPermissions,
|
||||
readCasesCapabilities,
|
||||
readCasesPermissions,
|
||||
readCasesCapabilities,
|
||||
writeCasesCapabilities,
|
||||
writeCasesPermissions,
|
||||
} from '../../common/mock';
|
||||
|
@ -77,6 +77,12 @@ const hasSecurityWriteAndObservabilityRead: CasesCapabilities = {
|
|||
generalCases: noCasesCapabilities(),
|
||||
};
|
||||
|
||||
const hasSecurityConnectors: CasesCapabilities = {
|
||||
securitySolutionCases: readCasesCapabilities(),
|
||||
observabilityCases: noCasesCapabilities(),
|
||||
generalCases: noCasesCapabilities(),
|
||||
};
|
||||
|
||||
describe('canUseCases', () => {
|
||||
it.each([hasAll, hasSecurity, hasObservability, hasSecurityWriteAndObservabilityRead])(
|
||||
'returns true for all permissions, if a user has access to both on any solution',
|
||||
|
@ -109,4 +115,12 @@ describe('canUseCases', () => {
|
|||
expect(permissions).toStrictEqual(noCasesPermissions());
|
||||
}
|
||||
);
|
||||
|
||||
it.each([hasSecurityConnectors])(
|
||||
'returns true for only connectors, if a user has access to only connectors on any solution',
|
||||
(capability) => {
|
||||
const permissions = canUseCases(capability)();
|
||||
expect(permissions).toStrictEqual(readCasesPermissions());
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
@ -40,8 +40,10 @@ export const canUseCases =
|
|||
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;
|
||||
const allFromAcc =
|
||||
acc.create && acc.read && acc.update && acc.delete && acc.push && acc.connectors;
|
||||
acc.all = acc.all || userCapabilitiesForOwner.all || allFromAcc;
|
||||
acc.connectors = acc.connectors || userCapabilitiesForOwner.connectors;
|
||||
|
||||
return acc;
|
||||
},
|
||||
|
@ -52,6 +54,7 @@ export const canUseCases =
|
|||
update: false,
|
||||
delete: false,
|
||||
push: false,
|
||||
connectors: false,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ describe('getUICapabilities', () => {
|
|||
expect(getUICapabilities(undefined)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"all": false,
|
||||
"connectors": false,
|
||||
"create": false,
|
||||
"delete": false,
|
||||
"push": false,
|
||||
|
@ -25,6 +26,7 @@ describe('getUICapabilities', () => {
|
|||
expect(getUICapabilities()).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"all": false,
|
||||
"connectors": false,
|
||||
"create": false,
|
||||
"delete": false,
|
||||
"push": false,
|
||||
|
@ -38,6 +40,7 @@ describe('getUICapabilities', () => {
|
|||
expect(getUICapabilities({ create_cases: true })).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"all": false,
|
||||
"connectors": false,
|
||||
"create": true,
|
||||
"delete": false,
|
||||
"push": false,
|
||||
|
@ -55,10 +58,12 @@ describe('getUICapabilities', () => {
|
|||
update_cases: false,
|
||||
delete_cases: false,
|
||||
push_cases: false,
|
||||
cases_connectors: false,
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"all": false,
|
||||
"connectors": false,
|
||||
"create": false,
|
||||
"delete": false,
|
||||
"push": false,
|
||||
|
@ -72,6 +77,7 @@ describe('getUICapabilities', () => {
|
|||
expect(getUICapabilities({})).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"all": false,
|
||||
"connectors": false,
|
||||
"create": false,
|
||||
"delete": false,
|
||||
"push": false,
|
||||
|
@ -89,10 +95,35 @@ describe('getUICapabilities', () => {
|
|||
update_cases: true,
|
||||
delete_cases: true,
|
||||
push_cases: true,
|
||||
cases_connectors: true,
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"all": false,
|
||||
"connectors": true,
|
||||
"create": false,
|
||||
"delete": true,
|
||||
"push": true,
|
||||
"read": true,
|
||||
"update": true,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns false for the all field when cases_connectors is false', () => {
|
||||
expect(
|
||||
getUICapabilities({
|
||||
create_cases: false,
|
||||
read_cases: true,
|
||||
update_cases: true,
|
||||
delete_cases: true,
|
||||
push_cases: true,
|
||||
cases_connectors: false,
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"all": false,
|
||||
"connectors": false,
|
||||
"create": false,
|
||||
"delete": true,
|
||||
"push": true,
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import type { CasesPermissions } from '../../../common';
|
||||
import {
|
||||
CASES_CONNECTORS_CAPABILITY,
|
||||
CREATE_CASES_CAPABILITY,
|
||||
DELETE_CASES_CAPABILITY,
|
||||
PUSH_CASES_CAPABILITY,
|
||||
|
@ -22,7 +23,8 @@ export const getUICapabilities = (
|
|||
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;
|
||||
const connectors = !!featureCapabilities?.[CASES_CONNECTORS_CAPABILITY];
|
||||
const all = create && read && update && deletePriv && push && connectors;
|
||||
|
||||
return {
|
||||
all,
|
||||
|
@ -31,5 +33,6 @@ export const getUICapabilities = (
|
|||
update,
|
||||
delete: deletePriv,
|
||||
push,
|
||||
connectors,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -193,6 +193,7 @@ export const useApplicationCapabilities = (): UseApplicationCapabilities => {
|
|||
update: permissions.update,
|
||||
delete: permissions.delete,
|
||||
push: permissions.push,
|
||||
connectors: permissions.connectors,
|
||||
},
|
||||
visualize: { crud: !!capabilities.visualize?.save, read: !!capabilities.visualize?.show },
|
||||
dashboard: {
|
||||
|
@ -213,6 +214,7 @@ export const useApplicationCapabilities = (): UseApplicationCapabilities => {
|
|||
permissions.update,
|
||||
permissions.delete,
|
||||
permissions.push,
|
||||
permissions.connectors,
|
||||
]
|
||||
);
|
||||
};
|
||||
|
|
|
@ -74,6 +74,7 @@ export const createStartServicesMock = ({ license }: StartServiceArgs = {}): Sta
|
|||
update_cases: true,
|
||||
delete_cases: true,
|
||||
push_cases: true,
|
||||
cases_connectors: true,
|
||||
},
|
||||
visualize: { save: true, show: true },
|
||||
dashboard: { show: true, createNew: true },
|
||||
|
|
|
@ -9,9 +9,23 @@ import type { CasesCapabilities, CasesPermissions } from '../../containers/types
|
|||
|
||||
export const allCasesPermissions = () => buildCasesPermissions();
|
||||
export const noCasesPermissions = () =>
|
||||
buildCasesPermissions({ read: false, create: false, update: false, delete: false, push: false });
|
||||
buildCasesPermissions({
|
||||
read: false,
|
||||
create: false,
|
||||
update: false,
|
||||
delete: false,
|
||||
push: false,
|
||||
connectors: false,
|
||||
});
|
||||
export const readCasesPermissions = () =>
|
||||
buildCasesPermissions({ read: true, create: false, update: false, delete: false, push: false });
|
||||
buildCasesPermissions({
|
||||
read: true,
|
||||
create: false,
|
||||
update: false,
|
||||
delete: false,
|
||||
push: false,
|
||||
connectors: true,
|
||||
});
|
||||
export const noCreateCasesPermissions = () => buildCasesPermissions({ create: false });
|
||||
export const noUpdateCasesPermissions = () => buildCasesPermissions({ update: false });
|
||||
export const noPushCasesPermissions = () => buildCasesPermissions({ push: false });
|
||||
|
@ -19,6 +33,7 @@ export const noDeleteCasesPermissions = () => buildCasesPermissions({ delete: fa
|
|||
export const writeCasesPermissions = () => buildCasesPermissions({ read: false });
|
||||
export const onlyDeleteCasesPermission = () =>
|
||||
buildCasesPermissions({ read: false, create: false, update: false, delete: true, push: false });
|
||||
export const noConnectorsCasePermission = () => buildCasesPermissions({ connectors: false });
|
||||
|
||||
export const buildCasesPermissions = (overrides: Partial<Omit<CasesPermissions, 'all'>> = {}) => {
|
||||
const create = overrides.create ?? true;
|
||||
|
@ -26,6 +41,7 @@ export const buildCasesPermissions = (overrides: Partial<Omit<CasesPermissions,
|
|||
const update = overrides.update ?? true;
|
||||
const deletePermissions = overrides.delete ?? true;
|
||||
const push = overrides.push ?? true;
|
||||
const connectors = overrides.connectors ?? true;
|
||||
const all = create && read && update && deletePermissions && push;
|
||||
|
||||
return {
|
||||
|
@ -35,6 +51,7 @@ export const buildCasesPermissions = (overrides: Partial<Omit<CasesPermissions,
|
|||
update,
|
||||
delete: deletePermissions,
|
||||
push,
|
||||
connectors,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -46,6 +63,7 @@ export const noCasesCapabilities = () =>
|
|||
update_cases: false,
|
||||
delete_cases: false,
|
||||
push_cases: false,
|
||||
cases_connectors: false,
|
||||
});
|
||||
export const readCasesCapabilities = () =>
|
||||
buildCasesCapabilities({
|
||||
|
@ -67,5 +85,6 @@ export const buildCasesCapabilities = (overrides?: Partial<CasesCapabilities>) =
|
|||
update_cases: overrides?.update_cases ?? true,
|
||||
delete_cases: overrides?.delete_cases ?? true,
|
||||
push_cases: overrides?.push_cases ?? true,
|
||||
cases_connectors: overrides?.cases_connectors ?? true,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -12,8 +12,12 @@ import { render, screen } from '@testing-library/react';
|
|||
|
||||
import type { Props } from './connectors';
|
||||
import { Connectors } from './connectors';
|
||||
import type { AppMockRenderer } from '../../common/mock';
|
||||
import { createAppMockRenderer, TestProviders } from '../../common/mock';
|
||||
import {
|
||||
type AppMockRenderer,
|
||||
noConnectorsCasePermission,
|
||||
createAppMockRenderer,
|
||||
TestProviders,
|
||||
} from '../../common/mock';
|
||||
import { ConnectorsDropdown } from './connectors_dropdown';
|
||||
import { connectors, actionTypes } from './__mock__';
|
||||
import { ConnectorTypes } from '../../../common/types/domain';
|
||||
|
@ -161,4 +165,14 @@ describe('Connectors', () => {
|
|||
).toBeInTheDocument();
|
||||
expect(result.queryByTestId('case-connectors-dropdown')).toBe(null);
|
||||
});
|
||||
|
||||
it('shows the actions permission message if the user does not have access to case connector', async () => {
|
||||
appMockRender = createAppMockRenderer({ permissions: noConnectorsCasePermission() });
|
||||
|
||||
const result = appMockRender.render(<Connectors {...props} />);
|
||||
expect(
|
||||
result.getByTestId('configure-case-connector-permissions-error-msg')
|
||||
).toBeInTheDocument();
|
||||
expect(result.queryByTestId('case-connectors-dropdown')).toBe(null);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -27,6 +27,7 @@ import { ConnectorTypes } from '../../../common/types/domain';
|
|||
import { DeprecatedCallout } from '../connectors/deprecated_callout';
|
||||
import { isDeprecatedConnector } from '../utils';
|
||||
import { useApplicationCapabilities } from '../../common/lib/kibana';
|
||||
import { useCasesContext } from '../cases_context/use_cases_context';
|
||||
|
||||
const EuiFormRowExtended = styled(EuiFormRow)`
|
||||
.euiFormRow__labelWrapper {
|
||||
|
@ -63,6 +64,8 @@ const ConnectorsComponent: React.FC<Props> = ({
|
|||
() => connectors.find((c) => c.id === selectedConnector.id),
|
||||
[connectors, selectedConnector.id]
|
||||
);
|
||||
const { permissions } = useCasesContext();
|
||||
const canUseConnectors = permissions.connectors && actions.read;
|
||||
|
||||
const connectorsName = connector?.name ?? 'none';
|
||||
|
||||
|
@ -105,7 +108,7 @@ const ConnectorsComponent: React.FC<Props> = ({
|
|||
>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem grow={false}>
|
||||
{actions.read ? (
|
||||
{canUseConnectors ? (
|
||||
<ConnectorsDropdown
|
||||
connectors={connectors}
|
||||
disabled={disabled}
|
||||
|
|
|
@ -22,7 +22,11 @@ import { incidentTypes, severity, choices } from '../connectors/mock';
|
|||
import type { FormProps } from './schema';
|
||||
import { schema } from './schema';
|
||||
import type { AppMockRenderer } from '../../common/mock';
|
||||
import { createAppMockRenderer, TestProviders } from '../../common/mock';
|
||||
import {
|
||||
noConnectorsCasePermission,
|
||||
createAppMockRenderer,
|
||||
TestProviders,
|
||||
} from '../../common/mock';
|
||||
import { useCaseConfigure } from '../../containers/configure/use_configure';
|
||||
import { useCaseConfigureResponse } from '../configure_cases/__mock__';
|
||||
|
||||
|
@ -190,4 +194,16 @@ describe('Connector', () => {
|
|||
expect(result.getByTestId('create-case-connector-permissions-error-msg')).toBeInTheDocument();
|
||||
expect(result.queryByTestId('caseConnectors')).toBe(null);
|
||||
});
|
||||
|
||||
it('shows the actions permission message if the user does not have access to case connector', async () => {
|
||||
appMockRender = createAppMockRenderer({ permissions: noConnectorsCasePermission() });
|
||||
|
||||
const result = appMockRender.render(
|
||||
<MockHookWrapperComponent>
|
||||
<Connector {...defaultProps} />
|
||||
</MockHookWrapperComponent>
|
||||
);
|
||||
expect(result.getByTestId('create-case-connector-permissions-error-msg')).toBeInTheDocument();
|
||||
expect(result.queryByTestId('caseConnectors')).toBe(null);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,6 +18,7 @@ import { useCaseConfigure } from '../../containers/configure/use_configure';
|
|||
import { getConnectorById, getConnectorsFormValidators } from '../utils';
|
||||
import { useApplicationCapabilities } from '../../common/lib/kibana';
|
||||
import * as i18n from '../../common/translations';
|
||||
import { useCasesContext } from '../cases_context/use_cases_context';
|
||||
|
||||
interface Props {
|
||||
connectors: ActionConnector[];
|
||||
|
@ -30,6 +31,8 @@ const ConnectorComponent: React.FC<Props> = ({ connectors, isLoading, isLoadingC
|
|||
const connector = getConnectorById(connectorId, connectors) ?? null;
|
||||
const { connector: configurationConnector } = useCaseConfigure();
|
||||
const { actions } = useApplicationCapabilities();
|
||||
const { permissions } = useCasesContext();
|
||||
const hasReadPermissions = permissions.connectors && actions.read;
|
||||
|
||||
const defaultConnectorId = useMemo(() => {
|
||||
return connectors.some((c) => c.id === configurationConnector.id)
|
||||
|
@ -42,7 +45,7 @@ const ConnectorComponent: React.FC<Props> = ({ connectors, isLoading, isLoadingC
|
|||
connectors,
|
||||
});
|
||||
|
||||
if (!actions.read) {
|
||||
if (!hasReadPermissions) {
|
||||
return (
|
||||
<EuiText data-test-subj="create-case-connector-permissions-error-msg" size="s">
|
||||
<span>{i18n.READ_ACTIONS_PERMISSIONS_ERROR_MSG}</span>
|
||||
|
|
|
@ -11,12 +11,14 @@ import userEvent from '@testing-library/user-event';
|
|||
|
||||
import type { EditConnectorProps } from '.';
|
||||
import { EditConnector } from '.';
|
||||
import type { AppMockRenderer } from '../../common/mock';
|
||||
|
||||
import {
|
||||
type AppMockRenderer,
|
||||
createAppMockRenderer,
|
||||
readCasesPermissions,
|
||||
noPushCasesPermissions,
|
||||
TestProviders,
|
||||
noConnectorsCasePermission,
|
||||
} from '../../common/mock';
|
||||
import { basicCase, connectorsMock } from '../../containers/mock';
|
||||
import { getCaseConnectorsMockResponse } from '../../common/mock/connectors';
|
||||
|
@ -274,6 +276,17 @@ describe('EditConnector ', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('does not show the callout if the user does not have access to cases connectors', async () => {
|
||||
const props = { ...defaultProps, connectors: [] };
|
||||
appMockRender = createAppMockRenderer({ permissions: noConnectorsCasePermission() });
|
||||
|
||||
const result = appMockRender.render(<EditConnector {...props} />);
|
||||
await waitFor(() => {
|
||||
expect(result.getByTestId('edit-connector-permissions-error-msg')).toBeInTheDocument();
|
||||
expect(result.queryByTestId('push-callouts')).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not show the connectors previewer if the user does not have read access to actions', async () => {
|
||||
const props = { ...defaultProps, connectors: [] };
|
||||
appMockRender.coreStart.application.capabilities = {
|
||||
|
@ -285,6 +298,14 @@ describe('EditConnector ', () => {
|
|||
expect(result.queryByTestId('connector-fields-preview')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show the connectors previewer if the user does not have access to cases connectors', async () => {
|
||||
const props = { ...defaultProps, connectors: [] };
|
||||
appMockRender = createAppMockRenderer({ permissions: noConnectorsCasePermission() });
|
||||
|
||||
const result = appMockRender.render(<EditConnector {...props} />);
|
||||
expect(result.queryByTestId('connector-fields-preview')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show the connectors form if the user does not have read access to actions', async () => {
|
||||
const props = { ...defaultProps, connectors: [] };
|
||||
appMockRender.coreStart.application.capabilities = {
|
||||
|
@ -296,6 +317,14 @@ describe('EditConnector ', () => {
|
|||
expect(result.queryByTestId('edit-connector-fields-form-flex-item')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show the connectors form if the user does not have access to cases connectors', async () => {
|
||||
const props = { ...defaultProps, connectors: [] };
|
||||
appMockRender = createAppMockRenderer({ permissions: noConnectorsCasePermission() });
|
||||
|
||||
const result = appMockRender.render(<EditConnector {...props} />);
|
||||
expect(result.queryByTestId('edit-connector-fields-form-flex-item')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show the push button if the user does not have read access to actions', async () => {
|
||||
appMockRender.coreStart.application.capabilities = {
|
||||
...appMockRender.coreStart.application.capabilities,
|
||||
|
@ -317,6 +346,15 @@ describe('EditConnector ', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('does not show the push button if the user does not have access to cases actions', async () => {
|
||||
appMockRender = createAppMockRenderer({ permissions: noConnectorsCasePermission() });
|
||||
|
||||
const result = appMockRender.render(<EditConnector {...defaultProps} />);
|
||||
await waitFor(() => {
|
||||
expect(result.queryByTestId('push-to-external-service')).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not show the edit connectors pencil if the user does not have read access to actions', async () => {
|
||||
const props = { ...defaultProps, connectors: [] };
|
||||
appMockRender.coreStart.application.capabilities = {
|
||||
|
@ -332,6 +370,20 @@ describe('EditConnector ', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('does not show the edit connectors pencil if the user does not have access to case connectors', async () => {
|
||||
const props = { ...defaultProps, connectors: [] };
|
||||
appMockRender = createAppMockRenderer({
|
||||
permissions: noConnectorsCasePermission(),
|
||||
});
|
||||
|
||||
appMockRender.render(<EditConnector {...props} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('connector-edit-header')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('connector-edit-button')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not show the edit connectors pencil if the user does not have push permissions', async () => {
|
||||
const props = { ...defaultProps, connectors: [] };
|
||||
appMockRender = createAppMockRenderer({ permissions: noPushCasesPermissions() });
|
||||
|
|
|
@ -21,6 +21,7 @@ import { PushButton } from './push_button';
|
|||
import { PushCallouts } from './push_callouts';
|
||||
import { ConnectorsForm } from './connectors_form';
|
||||
import { ConnectorFieldsPreviewForm } from '../connectors/fields_preview_form';
|
||||
import { useCasesContext } from '../cases_context/use_cases_context';
|
||||
|
||||
export interface EditConnectorProps {
|
||||
caseData: CaseUI;
|
||||
|
@ -45,7 +46,8 @@ export const EditConnector = React.memo(
|
|||
const [isEdit, setIsEdit] = useState(false);
|
||||
|
||||
const { actions } = useApplicationCapabilities();
|
||||
const hasActionsReadPermissions = actions.read;
|
||||
const { permissions } = useCasesContext();
|
||||
const canUseConnectors = permissions.connectors && actions.read;
|
||||
|
||||
const onEditClick = useCallback(() => setIsEdit(true), []);
|
||||
const onCancelConnector = useCallback(() => setIsEdit(false), []);
|
||||
|
@ -102,7 +104,7 @@ export const EditConnector = React.memo(
|
|||
<EuiFlexItem grow={false} data-test-subj="connector-edit-header">
|
||||
<h4>{i18n.CONNECTORS}</h4>
|
||||
</EuiFlexItem>
|
||||
{!isLoading && !isEdit && hasPushPermissions && hasActionsReadPermissions ? (
|
||||
{!isLoading && !isEdit && hasPushPermissions && canUseConnectors ? (
|
||||
<EuiFlexItem data-test-subj="connector-edit" grow={false}>
|
||||
<EuiButtonIcon
|
||||
data-test-subj="connector-edit-button"
|
||||
|
@ -115,7 +117,7 @@ export const EditConnector = React.memo(
|
|||
</EuiFlexGroup>
|
||||
<EuiHorizontalRule margin="xs" />
|
||||
<EuiFlexGroup data-test-subj="edit-connectors" direction="column" alignItems="stretch">
|
||||
{!isLoading && !isEdit && hasErrorMessages && hasActionsReadPermissions && (
|
||||
{!isLoading && !isEdit && hasErrorMessages && canUseConnectors && (
|
||||
<EuiFlexItem data-test-subj="push-callouts">
|
||||
<PushCallouts
|
||||
errorsMsg={errorsMsg}
|
||||
|
@ -125,18 +127,18 @@ export const EditConnector = React.memo(
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{!hasActionsReadPermissions && (
|
||||
{!canUseConnectors && (
|
||||
<EuiText data-test-subj="edit-connector-permissions-error-msg" size="s">
|
||||
<span>{i18n.READ_ACTIONS_PERMISSIONS_ERROR_MSG}</span>
|
||||
</EuiText>
|
||||
)}
|
||||
{hasActionsReadPermissions && !isEdit && (
|
||||
{canUseConnectors && !isEdit && (
|
||||
<ConnectorFieldsPreviewForm
|
||||
connector={caseActionConnector}
|
||||
fields={caseConnectorFields}
|
||||
/>
|
||||
)}
|
||||
{hasActionsReadPermissions && isEdit && (
|
||||
{canUseConnectors && isEdit && (
|
||||
<ConnectorsForm
|
||||
caseData={caseData}
|
||||
caseConnectors={caseConnectors}
|
||||
|
@ -146,25 +148,21 @@ export const EditConnector = React.memo(
|
|||
onSubmit={onSubmitConnector}
|
||||
/>
|
||||
)}
|
||||
{!hasErrorMessages &&
|
||||
!isLoading &&
|
||||
!isEdit &&
|
||||
hasPushPermissions &&
|
||||
hasActionsReadPermissions && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<span>
|
||||
<PushButton
|
||||
hasBeenPushed={hasBeenPushed}
|
||||
disabled={disablePushButton}
|
||||
isLoading={isLoadingPushToService}
|
||||
pushToService={handlePushToService}
|
||||
errorsMsg={errorsMsg}
|
||||
showTooltip={errorsMsg.length > 0 || !needsToBePushed || !hasPushPermissions}
|
||||
connectorName={connectorWithName.name}
|
||||
/>
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{!hasErrorMessages && !isLoading && !isEdit && hasPushPermissions && canUseConnectors && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<span>
|
||||
<PushButton
|
||||
hasBeenPushed={hasBeenPushed}
|
||||
disabled={disablePushButton}
|
||||
isLoading={isLoadingPushToService}
|
||||
pushToService={handlePushToService}
|
||||
errorsMsg={errorsMsg}
|
||||
showTooltip={errorsMsg.length > 0 || !needsToBePushed || !hasPushPermissions}
|
||||
connectorName={connectorWithName.name}
|
||||
/>
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -11,14 +11,17 @@ import { useApplicationCapabilities, useToasts } from '../../common/lib/kibana';
|
|||
import * as i18n from './translations';
|
||||
import { casesQueriesKeys } from '../constants';
|
||||
import type { ServerError } from '../../types';
|
||||
import { useCasesContext } from '../../components/cases_context/use_cases_context';
|
||||
|
||||
export function useGetSupportedActionConnectors() {
|
||||
const toasts = useToasts();
|
||||
const { actions } = useApplicationCapabilities();
|
||||
const { permissions } = useCasesContext();
|
||||
|
||||
return useQuery(
|
||||
casesQueriesKeys.connectorsList(),
|
||||
async ({ signal }) => {
|
||||
if (!actions.read) {
|
||||
if (!actions.read || !permissions.connectors) {
|
||||
return [];
|
||||
}
|
||||
return getSupportedActionConnectors({ signal });
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import React from 'react';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import * as api from './api';
|
||||
import { TestProviders } from '../../common/mock';
|
||||
import { noConnectorsCasePermission, TestProviders } from '../../common/mock';
|
||||
import { useApplicationCapabilities, useToasts } from '../../common/lib/kibana';
|
||||
import { useGetSupportedActionConnectors } from './use_get_supported_action_connectors';
|
||||
|
||||
|
@ -65,4 +65,20 @@ describe('useConnectors', () => {
|
|||
expect(spyOnFetchConnectors).not.toHaveBeenCalled();
|
||||
expect(result.current.data).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not fetch connectors when the user does not has access to connectors', async () => {
|
||||
const spyOnFetchConnectors = jest.spyOn(api, 'getSupportedActionConnectors');
|
||||
useApplicationCapabilitiesMock().actions = { crud: true, read: true };
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() => useGetSupportedActionConnectors(), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestProviders permissions={noConnectorsCasePermission()}>{children}</TestProviders>
|
||||
),
|
||||
});
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(spyOnFetchConnectors).not.toHaveBeenCalled();
|
||||
expect(result.current.data).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2458,7 +2458,7 @@ Object {
|
|||
"type": "cases",
|
||||
},
|
||||
},
|
||||
"message": "Failed attempt to update cases [id=1] as owner \\"awesome\\"",
|
||||
"message": "Failed attempt to push cases [id=1] as owner \\"awesome\\"",
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -2478,7 +2478,7 @@ Object {
|
|||
"change",
|
||||
],
|
||||
},
|
||||
"message": "Failed attempt to update a case as any owners",
|
||||
"message": "Failed attempt to push a case as any owners",
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -2500,7 +2500,7 @@ Object {
|
|||
"type": "cases",
|
||||
},
|
||||
},
|
||||
"message": "User is updating cases [id=5] as owner \\"super\\"",
|
||||
"message": "User is pushing cases [id=5] as owner \\"super\\"",
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -2516,7 +2516,7 @@ Object {
|
|||
"change",
|
||||
],
|
||||
},
|
||||
"message": "User is updating a case as any owners",
|
||||
"message": "User is pushing a case as any owners",
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
|
@ -39,6 +39,12 @@ const updateVerbs: Verbs = {
|
|||
past: 'updated',
|
||||
};
|
||||
|
||||
const pushVerbs: Verbs = {
|
||||
present: 'push',
|
||||
progressive: 'pushing',
|
||||
past: 'pushed',
|
||||
};
|
||||
|
||||
const deleteVerbs: Verbs = {
|
||||
present: 'delete',
|
||||
progressive: 'deleting',
|
||||
|
@ -164,7 +170,7 @@ const CaseOperations = {
|
|||
ecsType: EVENT_TYPES.change,
|
||||
name: WriteOperations.PushCase as const,
|
||||
action: 'case_push',
|
||||
verbs: updateVerbs,
|
||||
verbs: pushVerbs,
|
||||
docType: 'case',
|
||||
savedObjectType: CASE_SAVED_OBJECT,
|
||||
},
|
||||
|
|
|
@ -15,6 +15,9 @@ import { createCasesRoute } from '../create_cases_route';
|
|||
export const getConnectorsRoute = createCasesRoute({
|
||||
method: 'get',
|
||||
path: `${CASE_CONFIGURE_CONNECTORS_URL}/_find`,
|
||||
routerOptions: {
|
||||
tags: ['access:casesGetConnectorsConfigure'],
|
||||
},
|
||||
handler: async ({ context, response }) => {
|
||||
try {
|
||||
const caseContext = await context.cases;
|
||||
|
|
|
@ -40,7 +40,7 @@ interface CaseRouteHandlerArguments<P, Q, B> {
|
|||
kibanaVersion: PluginInitializerContext['env']['packageInfo']['version'];
|
||||
}
|
||||
|
||||
type CaseRouteTags = 'access:casesSuggestUserProfiles';
|
||||
type CaseRouteTags = 'access:casesSuggestUserProfiles' | 'access:casesGetConnectorsConfigure';
|
||||
|
||||
export interface CaseRoute<P = unknown, Q = unknown, B = unknown> {
|
||||
method: 'get' | 'post' | 'put' | 'delete' | 'patch';
|
||||
|
|
|
@ -110,6 +110,7 @@ describe('AddToCaseAction', function () {
|
|||
update: false,
|
||||
delete: false,
|
||||
push: false,
|
||||
connectors: false,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
|
|
@ -18,6 +18,7 @@ export function useGetUserCasesPermissions() {
|
|||
update: false,
|
||||
delete: false,
|
||||
push: false,
|
||||
connectors: false,
|
||||
});
|
||||
const uiCapabilities = useKibana().services.application.capabilities;
|
||||
|
||||
|
@ -33,6 +34,7 @@ export function useGetUserCasesPermissions() {
|
|||
update: casesCapabilities.update,
|
||||
delete: casesCapabilities.delete,
|
||||
push: casesCapabilities.push,
|
||||
connectors: casesCapabilities.connectors,
|
||||
});
|
||||
}, [
|
||||
casesCapabilities.all,
|
||||
|
@ -41,6 +43,7 @@ export function useGetUserCasesPermissions() {
|
|||
casesCapabilities.update,
|
||||
casesCapabilities.delete,
|
||||
casesCapabilities.push,
|
||||
casesCapabilities.connectors,
|
||||
]);
|
||||
|
||||
return casesPermissions;
|
||||
|
|
|
@ -19,7 +19,15 @@ export default {
|
|||
const Template: ComponentStory<typeof Component> = (props: CasesProps) => <Component {...props} />;
|
||||
|
||||
const defaultProps: CasesProps = {
|
||||
permissions: { read: true, all: true, create: true, delete: true, push: true, update: true },
|
||||
permissions: {
|
||||
read: true,
|
||||
all: true,
|
||||
create: true,
|
||||
delete: true,
|
||||
push: true,
|
||||
update: true,
|
||||
connectors: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const CasesPageWithAllPermissions = Template.bind({});
|
||||
|
@ -34,5 +42,6 @@ CasesPageWithNoPermissions.args = {
|
|||
delete: false,
|
||||
push: false,
|
||||
update: false,
|
||||
connectors: false,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -19,6 +19,7 @@ export function useGetUserCasesPermissions() {
|
|||
update: false,
|
||||
delete: false,
|
||||
push: false,
|
||||
connectors: false,
|
||||
});
|
||||
const uiCapabilities = useKibana().services.application!.capabilities;
|
||||
|
||||
|
@ -35,6 +36,7 @@ export function useGetUserCasesPermissions() {
|
|||
update: casesCapabilities.update,
|
||||
delete: casesCapabilities.delete,
|
||||
push: casesCapabilities.push,
|
||||
connectors: casesCapabilities.connectors,
|
||||
});
|
||||
}, [
|
||||
casesCapabilities.all,
|
||||
|
@ -43,6 +45,7 @@ export function useGetUserCasesPermissions() {
|
|||
casesCapabilities.update,
|
||||
casesCapabilities.delete,
|
||||
casesCapabilities.push,
|
||||
casesCapabilities.connectors,
|
||||
]);
|
||||
|
||||
return casesPermissions;
|
||||
|
|
|
@ -12,4 +12,5 @@ export const noCasesPermissions = () => ({
|
|||
update: false,
|
||||
delete: false,
|
||||
push: false,
|
||||
connectors: false,
|
||||
});
|
||||
|
|
|
@ -11,6 +11,7 @@ export const noCasesCapabilities = () => ({
|
|||
update_cases: false,
|
||||
delete_cases: false,
|
||||
push_cases: false,
|
||||
cases_connector: false,
|
||||
});
|
||||
|
||||
export const readCasesCapabilities = () => ({
|
||||
|
@ -19,6 +20,7 @@ export const readCasesCapabilities = () => ({
|
|||
update_cases: false,
|
||||
delete_cases: false,
|
||||
push_cases: false,
|
||||
cases_connector: true,
|
||||
});
|
||||
|
||||
export const allCasesCapabilities = () => ({
|
||||
|
@ -27,6 +29,7 @@ export const allCasesCapabilities = () => ({
|
|||
update_cases: true,
|
||||
delete_cases: true,
|
||||
push_cases: true,
|
||||
cases_connector: true,
|
||||
});
|
||||
|
||||
export const noCasesPermissions = () => ({
|
||||
|
@ -36,6 +39,7 @@ export const noCasesPermissions = () => ({
|
|||
update: false,
|
||||
delete: false,
|
||||
push: false,
|
||||
connectors: false,
|
||||
});
|
||||
|
||||
export const readCasesPermissions = () => ({
|
||||
|
@ -45,6 +49,7 @@ export const readCasesPermissions = () => ({
|
|||
update: false,
|
||||
delete: false,
|
||||
push: false,
|
||||
connectors: true,
|
||||
});
|
||||
|
||||
export const writeCasesPermissions = () => ({
|
||||
|
@ -54,6 +59,7 @@ export const writeCasesPermissions = () => ({
|
|||
update: true,
|
||||
delete: true,
|
||||
push: true,
|
||||
connectors: true,
|
||||
});
|
||||
|
||||
export const allCasesPermissions = () => ({
|
||||
|
@ -63,4 +69,5 @@ export const allCasesPermissions = () => ({
|
|||
update: true,
|
||||
delete: true,
|
||||
push: true,
|
||||
connectors: true,
|
||||
});
|
||||
|
|
|
@ -161,6 +161,7 @@ export const useGetUserCasesPermissions = () => {
|
|||
update: false,
|
||||
delete: false,
|
||||
push: false,
|
||||
connectors: false,
|
||||
});
|
||||
const uiCapabilities = useKibana().services.application.capabilities;
|
||||
const casesCapabilities = useKibana().services.cases.helpers.getUICapabilities(
|
||||
|
@ -175,6 +176,7 @@ export const useGetUserCasesPermissions = () => {
|
|||
update: casesCapabilities.update,
|
||||
delete: casesCapabilities.delete,
|
||||
push: casesCapabilities.push,
|
||||
connectors: casesCapabilities.connectors,
|
||||
});
|
||||
}, [
|
||||
casesCapabilities.all,
|
||||
|
@ -183,6 +185,7 @@ export const useGetUserCasesPermissions = () => {
|
|||
casesCapabilities.update,
|
||||
casesCapabilities.delete,
|
||||
casesCapabilities.push,
|
||||
casesCapabilities.connectors,
|
||||
]);
|
||||
|
||||
return casesPermissions;
|
||||
|
|
|
@ -13,6 +13,10 @@ import {
|
|||
createUICapabilities as createCasesUICapabilities,
|
||||
getApiTags as getCasesApiTags,
|
||||
} from '@kbn/cases-plugin/common';
|
||||
import {
|
||||
CASES_CONNECTORS_CAPABILITY,
|
||||
GET_CONNECTORS_CONFIGURE_API_TAG,
|
||||
} from '@kbn/cases-plugin/common/constants';
|
||||
import type { AppFeaturesCasesConfig, BaseKibanaFeatureConfig } from './types';
|
||||
import { APP_ID, CASES_FEATURE_ID } from '../../../common/constants';
|
||||
import { CasesSubFeatureId } from './security_cases_kibana_sub_features';
|
||||
|
@ -21,48 +25,66 @@ import { AppFeatureCasesKey } from '../../../common/types/app_features';
|
|||
const casesCapabilities = createCasesUICapabilities();
|
||||
const casesApiTags = getCasesApiTags(APP_ID);
|
||||
|
||||
export const getCasesBaseKibanaFeature = (): BaseKibanaFeatureConfig => ({
|
||||
id: CASES_FEATURE_ID,
|
||||
name: i18n.translate('xpack.securitySolution.featureRegistry.linkSecuritySolutionCaseTitle', {
|
||||
defaultMessage: 'Cases',
|
||||
}),
|
||||
order: 1100,
|
||||
category: DEFAULT_APP_CATEGORIES.security,
|
||||
app: [CASES_FEATURE_ID, 'kibana'],
|
||||
catalogue: [APP_ID],
|
||||
cases: [APP_ID],
|
||||
privileges: {
|
||||
all: {
|
||||
api: casesApiTags.all,
|
||||
app: [CASES_FEATURE_ID, 'kibana'],
|
||||
catalogue: [APP_ID],
|
||||
cases: {
|
||||
create: [APP_ID],
|
||||
read: [APP_ID],
|
||||
update: [APP_ID],
|
||||
push: [APP_ID],
|
||||
export const getCasesBaseKibanaFeature = (): BaseKibanaFeatureConfig => {
|
||||
// On SecuritySolution essentials cases does not have the connector feature
|
||||
const casesAllUICapabilities = casesCapabilities.all.filter(
|
||||
(capability) => capability !== CASES_CONNECTORS_CAPABILITY
|
||||
);
|
||||
|
||||
const casesReadUICapabilities = casesCapabilities.read.filter(
|
||||
(capability) => capability !== CASES_CONNECTORS_CAPABILITY
|
||||
);
|
||||
|
||||
const casesAllAPICapabilities = casesApiTags.all.filter(
|
||||
(capability) => capability !== GET_CONNECTORS_CONFIGURE_API_TAG
|
||||
);
|
||||
|
||||
const casesReadAPICapabilities = casesApiTags.read.filter(
|
||||
(capability) => capability !== GET_CONNECTORS_CONFIGURE_API_TAG
|
||||
);
|
||||
|
||||
return {
|
||||
id: CASES_FEATURE_ID,
|
||||
name: i18n.translate('xpack.securitySolution.featureRegistry.linkSecuritySolutionCaseTitle', {
|
||||
defaultMessage: 'Cases',
|
||||
}),
|
||||
order: 1100,
|
||||
category: DEFAULT_APP_CATEGORIES.security,
|
||||
app: [CASES_FEATURE_ID, 'kibana'],
|
||||
catalogue: [APP_ID],
|
||||
cases: [APP_ID],
|
||||
privileges: {
|
||||
all: {
|
||||
api: casesAllAPICapabilities,
|
||||
app: [CASES_FEATURE_ID, 'kibana'],
|
||||
catalogue: [APP_ID],
|
||||
cases: {
|
||||
create: [APP_ID],
|
||||
read: [APP_ID],
|
||||
update: [APP_ID],
|
||||
},
|
||||
savedObject: {
|
||||
all: [...filesSavedObjectTypes],
|
||||
read: [...filesSavedObjectTypes],
|
||||
},
|
||||
ui: casesAllUICapabilities,
|
||||
},
|
||||
savedObject: {
|
||||
all: [...filesSavedObjectTypes],
|
||||
read: [...filesSavedObjectTypes],
|
||||
read: {
|
||||
api: casesReadAPICapabilities,
|
||||
app: [CASES_FEATURE_ID, 'kibana'],
|
||||
catalogue: [APP_ID],
|
||||
cases: {
|
||||
read: [APP_ID],
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [...filesSavedObjectTypes],
|
||||
},
|
||||
ui: casesReadUICapabilities,
|
||||
},
|
||||
ui: casesCapabilities.all,
|
||||
},
|
||||
read: {
|
||||
api: casesApiTags.read,
|
||||
app: [CASES_FEATURE_ID, 'kibana'],
|
||||
catalogue: [APP_ID],
|
||||
cases: {
|
||||
read: [APP_ID],
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [...filesSavedObjectTypes],
|
||||
},
|
||||
ui: casesCapabilities.read,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const getCasesBaseKibanaSubFeatureIds = (): CasesSubFeatureId[] => [
|
||||
CasesSubFeatureId.deleteCases,
|
||||
|
@ -79,6 +101,18 @@ export const getCasesBaseKibanaSubFeatureIds = (): CasesSubFeatureId[] => [
|
|||
*/
|
||||
export const getCasesAppFeaturesConfig = (): AppFeaturesCasesConfig => ({
|
||||
[AppFeatureCasesKey.casesConnectors]: {
|
||||
// TODO: Add cases connector configuration privileges
|
||||
privileges: {
|
||||
all: {
|
||||
api: [GET_CONNECTORS_CONFIGURE_API_TAG], // Add cases connector get connectors API privileges
|
||||
ui: [CASES_CONNECTORS_CAPABILITY], // Add cases connector UI privileges
|
||||
cases: {
|
||||
push: [APP_ID], // Add cases connector push privileges
|
||||
},
|
||||
},
|
||||
read: {
|
||||
api: [GET_CONNECTORS_CONFIGURE_API_TAG], // Add cases connector get connectors API privileges
|
||||
ui: [CASES_CONNECTORS_CAPABILITY], // Add cases connector UI privileges
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -44,6 +44,30 @@ export const noCasesPrivilegesSpace1: Role = {
|
|||
},
|
||||
};
|
||||
|
||||
export const noCasesConnectors: Role = {
|
||||
name: 'no_cases_connectors',
|
||||
privileges: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['all'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
feature: {
|
||||
testNoCasesConnectorFixture: ['all'],
|
||||
actions: ['all'],
|
||||
actionsSimulators: ['all'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const globalRead: Role = {
|
||||
name: 'global_read',
|
||||
privileges: {
|
||||
|
@ -353,6 +377,7 @@ export const securitySolutionOnlyAllSpacesRole: Role = {
|
|||
export const roles = [
|
||||
noKibanaPrivileges,
|
||||
noCasesPrivilegesSpace1,
|
||||
noCasesConnectors,
|
||||
globalRead,
|
||||
securitySolutionOnlyAll,
|
||||
securitySolutionOnlyRead,
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
securitySolutionOnlyReadAlerts,
|
||||
securitySolutionOnlyReadNoIndexAlerts,
|
||||
securitySolutionOnlyReadDelete,
|
||||
noCasesConnectors as noCasesConnectorRole,
|
||||
} from './roles';
|
||||
import { User } from './types';
|
||||
|
||||
|
@ -126,6 +127,12 @@ export const noCasesPrivilegesSpace1: User = {
|
|||
roles: [noCasesPrivilegesSpace1Role.name],
|
||||
};
|
||||
|
||||
export const noCasesConnectors: User = {
|
||||
username: 'no_cases_connectors',
|
||||
password: 'no_cases_connectors',
|
||||
roles: [noCasesConnectorRole.name],
|
||||
};
|
||||
|
||||
/**
|
||||
* These users will have access to all spaces.
|
||||
*/
|
||||
|
@ -154,4 +161,5 @@ export const users = [
|
|||
noKibanaPrivileges,
|
||||
noCasesPrivilegesSpace1,
|
||||
testDisabled,
|
||||
noCasesConnectors,
|
||||
];
|
||||
|
|
|
@ -40,6 +40,45 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
|
|||
|
||||
registerRoutes(core, this.log);
|
||||
registerCaseFixtureFileKinds(deps.files);
|
||||
|
||||
/**
|
||||
* Kibana features
|
||||
*/
|
||||
|
||||
deps.features.registerKibanaFeature({
|
||||
id: 'testNoCasesConnectorFixture',
|
||||
name: 'TestNoCasesConnectorFixture',
|
||||
app: ['kibana'],
|
||||
category: { id: 'cases-fixtures', label: 'Cases Fixtures' },
|
||||
cases: ['testNoCasesConnectorFixture'],
|
||||
privileges: {
|
||||
all: {
|
||||
api: [],
|
||||
app: ['kibana'],
|
||||
cases: {
|
||||
create: ['testNoCasesConnectorFixture'],
|
||||
read: ['testNoCasesConnectorFixture'],
|
||||
update: ['testNoCasesConnectorFixture'],
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
},
|
||||
read: {
|
||||
app: ['kibana'],
|
||||
cases: {
|
||||
read: ['testNoCasesConnectorFixture'],
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public start(core: CoreStart, plugins: FixtureStartDeps) {}
|
||||
|
|
|
@ -31,7 +31,7 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
|
|||
cases: ['observabilityFixture'],
|
||||
privileges: {
|
||||
all: {
|
||||
api: ['casesSuggestUserProfiles', 'bulkGetUserProfiles'],
|
||||
api: ['casesSuggestUserProfiles', 'bulkGetUserProfiles', 'casesGetConnectorsConfigure'],
|
||||
app: ['kibana'],
|
||||
cases: {
|
||||
all: ['observabilityFixture'],
|
||||
|
@ -43,7 +43,7 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
|
|||
ui: [],
|
||||
},
|
||||
read: {
|
||||
api: ['casesSuggestUserProfiles', 'bulkGetUserProfiles'],
|
||||
api: ['casesSuggestUserProfiles', 'bulkGetUserProfiles', 'casesGetConnectorsConfigure'],
|
||||
app: ['kibana'],
|
||||
cases: {
|
||||
read: ['observabilityFixture'],
|
||||
|
|
|
@ -39,7 +39,7 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
|
|||
cases: ['securitySolutionFixture'],
|
||||
privileges: {
|
||||
all: {
|
||||
api: ['casesSuggestUserProfiles', 'bulkGetUserProfiles'],
|
||||
api: ['casesSuggestUserProfiles', 'bulkGetUserProfiles', 'casesGetConnectorsConfigure'],
|
||||
app: ['kibana'],
|
||||
cases: {
|
||||
create: ['securitySolutionFixture'],
|
||||
|
@ -54,7 +54,7 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
|
|||
ui: [],
|
||||
},
|
||||
read: {
|
||||
api: ['casesSuggestUserProfiles', 'bulkGetUserProfiles'],
|
||||
api: ['casesSuggestUserProfiles', 'bulkGetUserProfiles', 'casesGetConnectorsConfigure'],
|
||||
app: ['kibana'],
|
||||
cases: {
|
||||
read: ['securitySolutionFixture'],
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { ConnectorTypes } from '@kbn/cases-plugin/common/types/domain';
|
||||
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
|
||||
import { postCaseReq } from '../../../../common/lib/mock';
|
||||
import {
|
||||
|
|
|
@ -54,6 +54,7 @@ import {
|
|||
} from '../../../../common/lib/api';
|
||||
import {
|
||||
globalRead,
|
||||
noCasesConnectors,
|
||||
noKibanaPrivileges,
|
||||
obsOnlyRead,
|
||||
obsSecRead,
|
||||
|
@ -840,6 +841,24 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
|
||||
expect(theCase.status).to.eql('open');
|
||||
});
|
||||
|
||||
it('should return 403 when the user does not have access to push', async () => {
|
||||
const { postedCase } = await createCaseWithConnector({
|
||||
supertest,
|
||||
serviceNowSimulatorURL,
|
||||
actionsRemover,
|
||||
configureReq: { owner: 'testNoCasesConnectorFixture' },
|
||||
createCaseReq: { ...getPostCaseRequest(), owner: 'testNoCasesConnectorFixture' },
|
||||
});
|
||||
|
||||
await pushCase({
|
||||
supertest: supertestWithoutAuth,
|
||||
caseId: postedCase.id,
|
||||
connectorId: postedCase.connector.id,
|
||||
expectedHttpCode: 403,
|
||||
auth: { user: noCasesConnectors, space: null },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
|
||||
import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib';
|
||||
import {
|
||||
|
@ -20,10 +20,12 @@ import {
|
|||
getCaseConnectors,
|
||||
getCasesWebhookConnector,
|
||||
} from '../../../../common/lib/api';
|
||||
import { noCasesConnectors } from '../../../../common/lib/authentication/users';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext): void => {
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const actionsRemover = new ActionsRemover(supertest);
|
||||
|
||||
describe('get_connectors', () => {
|
||||
|
@ -184,5 +186,13 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return 403 when the user does not have access to the case connectors', async () => {
|
||||
await getCaseConnectors({
|
||||
supertest: supertestWithoutAuth,
|
||||
auth: { user: noCasesConnectors, space: null },
|
||||
expectedHttpCode: 403,
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -42,6 +42,7 @@ const permissions = {
|
|||
update: true,
|
||||
delete: true,
|
||||
push: true,
|
||||
connectors: true,
|
||||
};
|
||||
|
||||
const attachments = [{ type: AttachmentType.user as const, comment: 'test' }];
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue