[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:
Pablo Machado 2023-08-07 10:22:10 +02:00 committed by GitHub
parent 527c2d5884
commit aa42bccd40
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 478 additions and 96 deletions

View file

@ -158,6 +158,7 @@ export const READ_CASES_CAPABILITY = 'read_cases' as const;
export const UPDATE_CASES_CAPABILITY = 'update_cases' as const; export const UPDATE_CASES_CAPABILITY = 'update_cases' as const;
export const DELETE_CASES_CAPABILITY = 'delete_cases' as const; export const DELETE_CASES_CAPABILITY = 'delete_cases' as const;
export const PUSH_CASES_CAPABILITY = 'push_cases' as const; export const PUSH_CASES_CAPABILITY = 'push_cases' as const;
export const CASES_CONNECTORS_CAPABILITY = 'cases_connectors' as const;
/** /**
* Cases API Tags * Cases API Tags
@ -173,6 +174,11 @@ export const SUGGEST_USER_PROFILES_API_TAG = 'casesSuggestUserProfiles';
*/ */
export const BULK_GET_USER_PROFILES_API_TAG = 'bulkGetUserProfiles'; 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 * User profiles
*/ */

View file

@ -12,7 +12,8 @@ import type {
READ_CASES_CAPABILITY, READ_CASES_CAPABILITY,
UPDATE_CASES_CAPABILITY, UPDATE_CASES_CAPABILITY,
} from '..'; } 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 { SnakeToCamelCase } from '../types';
import type { import type {
CaseSeverity, CaseSeverity,
@ -285,6 +286,7 @@ export interface CasesPermissions {
update: boolean; update: boolean;
delete: boolean; delete: boolean;
push: boolean; push: boolean;
connectors: boolean;
} }
export interface CasesCapabilities { export interface CasesCapabilities {
@ -293,4 +295,5 @@ export interface CasesCapabilities {
[UPDATE_CASES_CAPABILITY]: boolean; [UPDATE_CASES_CAPABILITY]: boolean;
[DELETE_CASES_CAPABILITY]: boolean; [DELETE_CASES_CAPABILITY]: boolean;
[PUSH_CASES_CAPABILITY]: boolean; [PUSH_CASES_CAPABILITY]: boolean;
[CASES_CONNECTORS_CAPABILITY]: boolean;
} }

View file

@ -5,6 +5,7 @@ Object {
"all": Array [ "all": Array [
"casesSuggestUserProfiles", "casesSuggestUserProfiles",
"bulkGetUserProfiles", "bulkGetUserProfiles",
"casesGetConnectorsConfigure",
"casesFilesCasesCreate", "casesFilesCasesCreate",
"casesFilesCasesRead", "casesFilesCasesRead",
], ],
@ -14,6 +15,7 @@ Object {
"read": Array [ "read": Array [
"casesSuggestUserProfiles", "casesSuggestUserProfiles",
"bulkGetUserProfiles", "bulkGetUserProfiles",
"casesGetConnectorsConfigure",
"casesFilesCasesRead", "casesFilesCasesRead",
], ],
} }
@ -24,6 +26,7 @@ Object {
"all": Array [ "all": Array [
"casesSuggestUserProfiles", "casesSuggestUserProfiles",
"bulkGetUserProfiles", "bulkGetUserProfiles",
"casesGetConnectorsConfigure",
"observabilityFilesCasesCreate", "observabilityFilesCasesCreate",
"observabilityFilesCasesRead", "observabilityFilesCasesRead",
], ],
@ -33,6 +36,7 @@ Object {
"read": Array [ "read": Array [
"casesSuggestUserProfiles", "casesSuggestUserProfiles",
"bulkGetUserProfiles", "bulkGetUserProfiles",
"casesGetConnectorsConfigure",
"observabilityFilesCasesRead", "observabilityFilesCasesRead",
], ],
} }
@ -43,6 +47,7 @@ Object {
"all": Array [ "all": Array [
"casesSuggestUserProfiles", "casesSuggestUserProfiles",
"bulkGetUserProfiles", "bulkGetUserProfiles",
"casesGetConnectorsConfigure",
"securitySolutionFilesCasesCreate", "securitySolutionFilesCasesCreate",
"securitySolutionFilesCasesRead", "securitySolutionFilesCasesRead",
], ],
@ -52,6 +57,7 @@ Object {
"read": Array [ "read": Array [
"casesSuggestUserProfiles", "casesSuggestUserProfiles",
"bulkGetUserProfiles", "bulkGetUserProfiles",
"casesGetConnectorsConfigure",
"securitySolutionFilesCasesRead", "securitySolutionFilesCasesRead",
], ],
} }

View file

@ -5,7 +5,11 @@
* 2.0. * 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 { HttpApiTagOperation } from '../constants/types';
import type { Owner } from '../constants/types'; import type { Owner } from '../constants/types';
import { constructFilesHttpOperationTag } from '../files'; import { constructFilesHttpOperationTag } from '../files';
@ -16,8 +20,19 @@ export const getApiTags = (owner: Owner) => {
const read = constructFilesHttpOperationTag(owner, HttpApiTagOperation.Read); const read = constructFilesHttpOperationTag(owner, HttpApiTagOperation.Read);
return { return {
all: [SUGGEST_USER_PROFILES_API_TAG, BULK_GET_USER_PROFILES_API_TAG, create, read] as const, all: [
read: [SUGGEST_USER_PROFILES_API_TAG, BULK_GET_USER_PROFILES_API_TAG, read] as const, 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, delete: [deleteTag] as const,
}; };
}; };

View file

@ -6,6 +6,7 @@
*/ */
import { import {
CASES_CONNECTORS_CAPABILITY,
CREATE_CASES_CAPABILITY, CREATE_CASES_CAPABILITY,
DELETE_CASES_CAPABILITY, DELETE_CASES_CAPABILITY,
PUSH_CASES_CAPABILITY, PUSH_CASES_CAPABILITY,
@ -23,7 +24,8 @@ export const createUICapabilities = () => ({
READ_CASES_CAPABILITY, READ_CASES_CAPABILITY,
UPDATE_CASES_CAPABILITY, UPDATE_CASES_CAPABILITY,
PUSH_CASES_CAPABILITY, PUSH_CASES_CAPABILITY,
CASES_CONNECTORS_CAPABILITY,
] as const, ] as const,
read: [READ_CASES_CAPABILITY] as const, read: [READ_CASES_CAPABILITY, CASES_CONNECTORS_CAPABILITY] as const,
delete: [DELETE_CASES_CAPABILITY] as const, delete: [DELETE_CASES_CAPABILITY] as const,
}); });

View file

@ -11,8 +11,8 @@ import {
allCasesPermissions, allCasesPermissions,
noCasesCapabilities, noCasesCapabilities,
noCasesPermissions, noCasesPermissions,
readCasesCapabilities,
readCasesPermissions, readCasesPermissions,
readCasesCapabilities,
writeCasesCapabilities, writeCasesCapabilities,
writeCasesPermissions, writeCasesPermissions,
} from '../../common/mock'; } from '../../common/mock';
@ -77,6 +77,12 @@ const hasSecurityWriteAndObservabilityRead: CasesCapabilities = {
generalCases: noCasesCapabilities(), generalCases: noCasesCapabilities(),
}; };
const hasSecurityConnectors: CasesCapabilities = {
securitySolutionCases: readCasesCapabilities(),
observabilityCases: noCasesCapabilities(),
generalCases: noCasesCapabilities(),
};
describe('canUseCases', () => { describe('canUseCases', () => {
it.each([hasAll, hasSecurity, hasObservability, hasSecurityWriteAndObservabilityRead])( it.each([hasAll, hasSecurity, hasObservability, hasSecurityWriteAndObservabilityRead])(
'returns true for all permissions, if a user has access to both on any solution', '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()); 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());
}
);
}); });

View file

@ -40,8 +40,10 @@ export const canUseCases =
acc.update = acc.update || userCapabilitiesForOwner.update; acc.update = acc.update || userCapabilitiesForOwner.update;
acc.delete = acc.delete || userCapabilitiesForOwner.delete; acc.delete = acc.delete || userCapabilitiesForOwner.delete;
acc.push = acc.push || userCapabilitiesForOwner.push; 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.all = acc.all || userCapabilitiesForOwner.all || allFromAcc;
acc.connectors = acc.connectors || userCapabilitiesForOwner.connectors;
return acc; return acc;
}, },
@ -52,6 +54,7 @@ export const canUseCases =
update: false, update: false,
delete: false, delete: false,
push: false, push: false,
connectors: false,
} }
); );

View file

@ -12,6 +12,7 @@ describe('getUICapabilities', () => {
expect(getUICapabilities(undefined)).toMatchInlineSnapshot(` expect(getUICapabilities(undefined)).toMatchInlineSnapshot(`
Object { Object {
"all": false, "all": false,
"connectors": false,
"create": false, "create": false,
"delete": false, "delete": false,
"push": false, "push": false,
@ -25,6 +26,7 @@ describe('getUICapabilities', () => {
expect(getUICapabilities()).toMatchInlineSnapshot(` expect(getUICapabilities()).toMatchInlineSnapshot(`
Object { Object {
"all": false, "all": false,
"connectors": false,
"create": false, "create": false,
"delete": false, "delete": false,
"push": false, "push": false,
@ -38,6 +40,7 @@ describe('getUICapabilities', () => {
expect(getUICapabilities({ create_cases: true })).toMatchInlineSnapshot(` expect(getUICapabilities({ create_cases: true })).toMatchInlineSnapshot(`
Object { Object {
"all": false, "all": false,
"connectors": false,
"create": true, "create": true,
"delete": false, "delete": false,
"push": false, "push": false,
@ -55,10 +58,12 @@ describe('getUICapabilities', () => {
update_cases: false, update_cases: false,
delete_cases: false, delete_cases: false,
push_cases: false, push_cases: false,
cases_connectors: false,
}) })
).toMatchInlineSnapshot(` ).toMatchInlineSnapshot(`
Object { Object {
"all": false, "all": false,
"connectors": false,
"create": false, "create": false,
"delete": false, "delete": false,
"push": false, "push": false,
@ -72,6 +77,7 @@ describe('getUICapabilities', () => {
expect(getUICapabilities({})).toMatchInlineSnapshot(` expect(getUICapabilities({})).toMatchInlineSnapshot(`
Object { Object {
"all": false, "all": false,
"connectors": false,
"create": false, "create": false,
"delete": false, "delete": false,
"push": false, "push": false,
@ -89,10 +95,35 @@ describe('getUICapabilities', () => {
update_cases: true, update_cases: true,
delete_cases: true, delete_cases: true,
push_cases: true, push_cases: true,
cases_connectors: true,
}) })
).toMatchInlineSnapshot(` ).toMatchInlineSnapshot(`
Object { Object {
"all": false, "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, "create": false,
"delete": true, "delete": true,
"push": true, "push": true,

View file

@ -7,6 +7,7 @@
import type { CasesPermissions } from '../../../common'; import type { CasesPermissions } from '../../../common';
import { import {
CASES_CONNECTORS_CAPABILITY,
CREATE_CASES_CAPABILITY, CREATE_CASES_CAPABILITY,
DELETE_CASES_CAPABILITY, DELETE_CASES_CAPABILITY,
PUSH_CASES_CAPABILITY, PUSH_CASES_CAPABILITY,
@ -22,7 +23,8 @@ export const getUICapabilities = (
const update = !!featureCapabilities?.[UPDATE_CASES_CAPABILITY]; const update = !!featureCapabilities?.[UPDATE_CASES_CAPABILITY];
const deletePriv = !!featureCapabilities?.[DELETE_CASES_CAPABILITY]; const deletePriv = !!featureCapabilities?.[DELETE_CASES_CAPABILITY];
const push = !!featureCapabilities?.[PUSH_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 { return {
all, all,
@ -31,5 +33,6 @@ export const getUICapabilities = (
update, update,
delete: deletePriv, delete: deletePriv,
push, push,
connectors,
}; };
}; };

View file

@ -193,6 +193,7 @@ export const useApplicationCapabilities = (): UseApplicationCapabilities => {
update: permissions.update, update: permissions.update,
delete: permissions.delete, delete: permissions.delete,
push: permissions.push, push: permissions.push,
connectors: permissions.connectors,
}, },
visualize: { crud: !!capabilities.visualize?.save, read: !!capabilities.visualize?.show }, visualize: { crud: !!capabilities.visualize?.save, read: !!capabilities.visualize?.show },
dashboard: { dashboard: {
@ -213,6 +214,7 @@ export const useApplicationCapabilities = (): UseApplicationCapabilities => {
permissions.update, permissions.update,
permissions.delete, permissions.delete,
permissions.push, permissions.push,
permissions.connectors,
] ]
); );
}; };

View file

@ -74,6 +74,7 @@ export const createStartServicesMock = ({ license }: StartServiceArgs = {}): Sta
update_cases: true, update_cases: true,
delete_cases: true, delete_cases: true,
push_cases: true, push_cases: true,
cases_connectors: true,
}, },
visualize: { save: true, show: true }, visualize: { save: true, show: true },
dashboard: { show: true, createNew: true }, dashboard: { show: true, createNew: true },

View file

@ -9,9 +9,23 @@ import type { CasesCapabilities, CasesPermissions } from '../../containers/types
export const allCasesPermissions = () => buildCasesPermissions(); export const allCasesPermissions = () => buildCasesPermissions();
export const noCasesPermissions = () => 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 = () => 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 noCreateCasesPermissions = () => buildCasesPermissions({ create: false });
export const noUpdateCasesPermissions = () => buildCasesPermissions({ update: false }); export const noUpdateCasesPermissions = () => buildCasesPermissions({ update: false });
export const noPushCasesPermissions = () => buildCasesPermissions({ push: 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 writeCasesPermissions = () => buildCasesPermissions({ read: false });
export const onlyDeleteCasesPermission = () => export const onlyDeleteCasesPermission = () =>
buildCasesPermissions({ read: false, create: false, update: false, delete: true, push: false }); buildCasesPermissions({ read: false, create: false, update: false, delete: true, push: false });
export const noConnectorsCasePermission = () => buildCasesPermissions({ connectors: false });
export const buildCasesPermissions = (overrides: Partial<Omit<CasesPermissions, 'all'>> = {}) => { export const buildCasesPermissions = (overrides: Partial<Omit<CasesPermissions, 'all'>> = {}) => {
const create = overrides.create ?? true; const create = overrides.create ?? true;
@ -26,6 +41,7 @@ export const buildCasesPermissions = (overrides: Partial<Omit<CasesPermissions,
const update = overrides.update ?? true; const update = overrides.update ?? true;
const deletePermissions = overrides.delete ?? true; const deletePermissions = overrides.delete ?? true;
const push = overrides.push ?? true; const push = overrides.push ?? true;
const connectors = overrides.connectors ?? true;
const all = create && read && update && deletePermissions && push; const all = create && read && update && deletePermissions && push;
return { return {
@ -35,6 +51,7 @@ export const buildCasesPermissions = (overrides: Partial<Omit<CasesPermissions,
update, update,
delete: deletePermissions, delete: deletePermissions,
push, push,
connectors,
}; };
}; };
@ -46,6 +63,7 @@ export const noCasesCapabilities = () =>
update_cases: false, update_cases: false,
delete_cases: false, delete_cases: false,
push_cases: false, push_cases: false,
cases_connectors: false,
}); });
export const readCasesCapabilities = () => export const readCasesCapabilities = () =>
buildCasesCapabilities({ buildCasesCapabilities({
@ -67,5 +85,6 @@ export const buildCasesCapabilities = (overrides?: Partial<CasesCapabilities>) =
update_cases: overrides?.update_cases ?? true, update_cases: overrides?.update_cases ?? true,
delete_cases: overrides?.delete_cases ?? true, delete_cases: overrides?.delete_cases ?? true,
push_cases: overrides?.push_cases ?? true, push_cases: overrides?.push_cases ?? true,
cases_connectors: overrides?.cases_connectors ?? true,
}; };
}; };

View file

@ -12,8 +12,12 @@ import { render, screen } from '@testing-library/react';
import type { Props } from './connectors'; import type { Props } from './connectors';
import { Connectors } from './connectors'; import { Connectors } from './connectors';
import type { AppMockRenderer } from '../../common/mock'; import {
import { createAppMockRenderer, TestProviders } from '../../common/mock'; type AppMockRenderer,
noConnectorsCasePermission,
createAppMockRenderer,
TestProviders,
} from '../../common/mock';
import { ConnectorsDropdown } from './connectors_dropdown'; import { ConnectorsDropdown } from './connectors_dropdown';
import { connectors, actionTypes } from './__mock__'; import { connectors, actionTypes } from './__mock__';
import { ConnectorTypes } from '../../../common/types/domain'; import { ConnectorTypes } from '../../../common/types/domain';
@ -161,4 +165,14 @@ describe('Connectors', () => {
).toBeInTheDocument(); ).toBeInTheDocument();
expect(result.queryByTestId('case-connectors-dropdown')).toBe(null); 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);
});
}); });

View file

@ -27,6 +27,7 @@ import { ConnectorTypes } from '../../../common/types/domain';
import { DeprecatedCallout } from '../connectors/deprecated_callout'; import { DeprecatedCallout } from '../connectors/deprecated_callout';
import { isDeprecatedConnector } from '../utils'; import { isDeprecatedConnector } from '../utils';
import { useApplicationCapabilities } from '../../common/lib/kibana'; import { useApplicationCapabilities } from '../../common/lib/kibana';
import { useCasesContext } from '../cases_context/use_cases_context';
const EuiFormRowExtended = styled(EuiFormRow)` const EuiFormRowExtended = styled(EuiFormRow)`
.euiFormRow__labelWrapper { .euiFormRow__labelWrapper {
@ -63,6 +64,8 @@ const ConnectorsComponent: React.FC<Props> = ({
() => connectors.find((c) => c.id === selectedConnector.id), () => connectors.find((c) => c.id === selectedConnector.id),
[connectors, selectedConnector.id] [connectors, selectedConnector.id]
); );
const { permissions } = useCasesContext();
const canUseConnectors = permissions.connectors && actions.read;
const connectorsName = connector?.name ?? 'none'; const connectorsName = connector?.name ?? 'none';
@ -105,7 +108,7 @@ const ConnectorsComponent: React.FC<Props> = ({
> >
<EuiFlexGroup direction="column"> <EuiFlexGroup direction="column">
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
{actions.read ? ( {canUseConnectors ? (
<ConnectorsDropdown <ConnectorsDropdown
connectors={connectors} connectors={connectors}
disabled={disabled} disabled={disabled}

View file

@ -22,7 +22,11 @@ import { incidentTypes, severity, choices } from '../connectors/mock';
import type { FormProps } from './schema'; import type { FormProps } from './schema';
import { schema } from './schema'; import { schema } from './schema';
import type { AppMockRenderer } from '../../common/mock'; 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 { useCaseConfigure } from '../../containers/configure/use_configure';
import { useCaseConfigureResponse } from '../configure_cases/__mock__'; import { useCaseConfigureResponse } from '../configure_cases/__mock__';
@ -190,4 +194,16 @@ describe('Connector', () => {
expect(result.getByTestId('create-case-connector-permissions-error-msg')).toBeInTheDocument(); expect(result.getByTestId('create-case-connector-permissions-error-msg')).toBeInTheDocument();
expect(result.queryByTestId('caseConnectors')).toBe(null); 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);
});
}); });

View file

@ -18,6 +18,7 @@ import { useCaseConfigure } from '../../containers/configure/use_configure';
import { getConnectorById, getConnectorsFormValidators } from '../utils'; import { getConnectorById, getConnectorsFormValidators } from '../utils';
import { useApplicationCapabilities } from '../../common/lib/kibana'; import { useApplicationCapabilities } from '../../common/lib/kibana';
import * as i18n from '../../common/translations'; import * as i18n from '../../common/translations';
import { useCasesContext } from '../cases_context/use_cases_context';
interface Props { interface Props {
connectors: ActionConnector[]; connectors: ActionConnector[];
@ -30,6 +31,8 @@ const ConnectorComponent: React.FC<Props> = ({ connectors, isLoading, isLoadingC
const connector = getConnectorById(connectorId, connectors) ?? null; const connector = getConnectorById(connectorId, connectors) ?? null;
const { connector: configurationConnector } = useCaseConfigure(); const { connector: configurationConnector } = useCaseConfigure();
const { actions } = useApplicationCapabilities(); const { actions } = useApplicationCapabilities();
const { permissions } = useCasesContext();
const hasReadPermissions = permissions.connectors && actions.read;
const defaultConnectorId = useMemo(() => { const defaultConnectorId = useMemo(() => {
return connectors.some((c) => c.id === configurationConnector.id) return connectors.some((c) => c.id === configurationConnector.id)
@ -42,7 +45,7 @@ const ConnectorComponent: React.FC<Props> = ({ connectors, isLoading, isLoadingC
connectors, connectors,
}); });
if (!actions.read) { if (!hasReadPermissions) {
return ( return (
<EuiText data-test-subj="create-case-connector-permissions-error-msg" size="s"> <EuiText data-test-subj="create-case-connector-permissions-error-msg" size="s">
<span>{i18n.READ_ACTIONS_PERMISSIONS_ERROR_MSG}</span> <span>{i18n.READ_ACTIONS_PERMISSIONS_ERROR_MSG}</span>

View file

@ -11,12 +11,14 @@ import userEvent from '@testing-library/user-event';
import type { EditConnectorProps } from '.'; import type { EditConnectorProps } from '.';
import { EditConnector } from '.'; import { EditConnector } from '.';
import type { AppMockRenderer } from '../../common/mock';
import { import {
type AppMockRenderer,
createAppMockRenderer, createAppMockRenderer,
readCasesPermissions, readCasesPermissions,
noPushCasesPermissions, noPushCasesPermissions,
TestProviders, TestProviders,
noConnectorsCasePermission,
} from '../../common/mock'; } from '../../common/mock';
import { basicCase, connectorsMock } from '../../containers/mock'; import { basicCase, connectorsMock } from '../../containers/mock';
import { getCaseConnectorsMockResponse } from '../../common/mock/connectors'; 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 () => { it('does not show the connectors previewer if the user does not have read access to actions', async () => {
const props = { ...defaultProps, connectors: [] }; const props = { ...defaultProps, connectors: [] };
appMockRender.coreStart.application.capabilities = { appMockRender.coreStart.application.capabilities = {
@ -285,6 +298,14 @@ describe('EditConnector ', () => {
expect(result.queryByTestId('connector-fields-preview')).not.toBeInTheDocument(); 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 () => { it('does not show the connectors form if the user does not have read access to actions', async () => {
const props = { ...defaultProps, connectors: [] }; const props = { ...defaultProps, connectors: [] };
appMockRender.coreStart.application.capabilities = { appMockRender.coreStart.application.capabilities = {
@ -296,6 +317,14 @@ describe('EditConnector ', () => {
expect(result.queryByTestId('edit-connector-fields-form-flex-item')).not.toBeInTheDocument(); 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 () => { 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 = {
...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 () => { it('does not show the edit connectors pencil if the user does not have read access to actions', async () => {
const props = { ...defaultProps, connectors: [] }; const props = { ...defaultProps, connectors: [] };
appMockRender.coreStart.application.capabilities = { 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 () => { it('does not show the edit connectors pencil if the user does not have push permissions', async () => {
const props = { ...defaultProps, connectors: [] }; const props = { ...defaultProps, connectors: [] };
appMockRender = createAppMockRenderer({ permissions: noPushCasesPermissions() }); appMockRender = createAppMockRenderer({ permissions: noPushCasesPermissions() });

View file

@ -21,6 +21,7 @@ import { PushButton } from './push_button';
import { PushCallouts } from './push_callouts'; import { PushCallouts } from './push_callouts';
import { ConnectorsForm } from './connectors_form'; import { ConnectorsForm } from './connectors_form';
import { ConnectorFieldsPreviewForm } from '../connectors/fields_preview_form'; import { ConnectorFieldsPreviewForm } from '../connectors/fields_preview_form';
import { useCasesContext } from '../cases_context/use_cases_context';
export interface EditConnectorProps { export interface EditConnectorProps {
caseData: CaseUI; caseData: CaseUI;
@ -45,7 +46,8 @@ export const EditConnector = React.memo(
const [isEdit, setIsEdit] = useState(false); const [isEdit, setIsEdit] = useState(false);
const { actions } = useApplicationCapabilities(); const { actions } = useApplicationCapabilities();
const hasActionsReadPermissions = actions.read; const { permissions } = useCasesContext();
const canUseConnectors = permissions.connectors && actions.read;
const onEditClick = useCallback(() => setIsEdit(true), []); const onEditClick = useCallback(() => setIsEdit(true), []);
const onCancelConnector = useCallback(() => setIsEdit(false), []); const onCancelConnector = useCallback(() => setIsEdit(false), []);
@ -102,7 +104,7 @@ export const EditConnector = React.memo(
<EuiFlexItem grow={false} data-test-subj="connector-edit-header"> <EuiFlexItem grow={false} data-test-subj="connector-edit-header">
<h4>{i18n.CONNECTORS}</h4> <h4>{i18n.CONNECTORS}</h4>
</EuiFlexItem> </EuiFlexItem>
{!isLoading && !isEdit && hasPushPermissions && hasActionsReadPermissions ? ( {!isLoading && !isEdit && hasPushPermissions && canUseConnectors ? (
<EuiFlexItem data-test-subj="connector-edit" grow={false}> <EuiFlexItem data-test-subj="connector-edit" grow={false}>
<EuiButtonIcon <EuiButtonIcon
data-test-subj="connector-edit-button" data-test-subj="connector-edit-button"
@ -115,7 +117,7 @@ export const EditConnector = React.memo(
</EuiFlexGroup> </EuiFlexGroup>
<EuiHorizontalRule margin="xs" /> <EuiHorizontalRule margin="xs" />
<EuiFlexGroup data-test-subj="edit-connectors" direction="column" alignItems="stretch"> <EuiFlexGroup data-test-subj="edit-connectors" direction="column" alignItems="stretch">
{!isLoading && !isEdit && hasErrorMessages && hasActionsReadPermissions && ( {!isLoading && !isEdit && hasErrorMessages && canUseConnectors && (
<EuiFlexItem data-test-subj="push-callouts"> <EuiFlexItem data-test-subj="push-callouts">
<PushCallouts <PushCallouts
errorsMsg={errorsMsg} errorsMsg={errorsMsg}
@ -125,18 +127,18 @@ export const EditConnector = React.memo(
/> />
</EuiFlexItem> </EuiFlexItem>
)} )}
{!hasActionsReadPermissions && ( {!canUseConnectors && (
<EuiText data-test-subj="edit-connector-permissions-error-msg" size="s"> <EuiText data-test-subj="edit-connector-permissions-error-msg" size="s">
<span>{i18n.READ_ACTIONS_PERMISSIONS_ERROR_MSG}</span> <span>{i18n.READ_ACTIONS_PERMISSIONS_ERROR_MSG}</span>
</EuiText> </EuiText>
)} )}
{hasActionsReadPermissions && !isEdit && ( {canUseConnectors && !isEdit && (
<ConnectorFieldsPreviewForm <ConnectorFieldsPreviewForm
connector={caseActionConnector} connector={caseActionConnector}
fields={caseConnectorFields} fields={caseConnectorFields}
/> />
)} )}
{hasActionsReadPermissions && isEdit && ( {canUseConnectors && isEdit && (
<ConnectorsForm <ConnectorsForm
caseData={caseData} caseData={caseData}
caseConnectors={caseConnectors} caseConnectors={caseConnectors}
@ -146,11 +148,7 @@ export const EditConnector = React.memo(
onSubmit={onSubmitConnector} onSubmit={onSubmitConnector}
/> />
)} )}
{!hasErrorMessages && {!hasErrorMessages && !isLoading && !isEdit && hasPushPermissions && canUseConnectors && (
!isLoading &&
!isEdit &&
hasPushPermissions &&
hasActionsReadPermissions && (
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<span> <span>
<PushButton <PushButton

View file

@ -11,14 +11,17 @@ import { useApplicationCapabilities, useToasts } from '../../common/lib/kibana';
import * as i18n from './translations'; import * as i18n from './translations';
import { casesQueriesKeys } from '../constants'; import { casesQueriesKeys } from '../constants';
import type { ServerError } from '../../types'; import type { ServerError } from '../../types';
import { useCasesContext } from '../../components/cases_context/use_cases_context';
export function useGetSupportedActionConnectors() { export function useGetSupportedActionConnectors() {
const toasts = useToasts(); const toasts = useToasts();
const { actions } = useApplicationCapabilities(); const { actions } = useApplicationCapabilities();
const { permissions } = useCasesContext();
return useQuery( return useQuery(
casesQueriesKeys.connectorsList(), casesQueriesKeys.connectorsList(),
async ({ signal }) => { async ({ signal }) => {
if (!actions.read) { if (!actions.read || !permissions.connectors) {
return []; return [];
} }
return getSupportedActionConnectors({ signal }); return getSupportedActionConnectors({ signal });

View file

@ -8,7 +8,7 @@
import React from 'react'; import React from 'react';
import { renderHook } from '@testing-library/react-hooks'; import { renderHook } from '@testing-library/react-hooks';
import * as api from './api'; import * as api from './api';
import { TestProviders } from '../../common/mock'; import { noConnectorsCasePermission, TestProviders } from '../../common/mock';
import { useApplicationCapabilities, useToasts } from '../../common/lib/kibana'; import { useApplicationCapabilities, useToasts } from '../../common/lib/kibana';
import { useGetSupportedActionConnectors } from './use_get_supported_action_connectors'; import { useGetSupportedActionConnectors } from './use_get_supported_action_connectors';
@ -65,4 +65,20 @@ describe('useConnectors', () => {
expect(spyOnFetchConnectors).not.toHaveBeenCalled(); expect(spyOnFetchConnectors).not.toHaveBeenCalled();
expect(result.current.data).toEqual([]); 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([]);
});
}); });

View file

@ -2458,7 +2458,7 @@ Object {
"type": "cases", "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", "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", "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", "change",
], ],
}, },
"message": "User is updating a case as any owners", "message": "User is pushing a case as any owners",
} }
`; `;

View file

@ -39,6 +39,12 @@ const updateVerbs: Verbs = {
past: 'updated', past: 'updated',
}; };
const pushVerbs: Verbs = {
present: 'push',
progressive: 'pushing',
past: 'pushed',
};
const deleteVerbs: Verbs = { const deleteVerbs: Verbs = {
present: 'delete', present: 'delete',
progressive: 'deleting', progressive: 'deleting',
@ -164,7 +170,7 @@ const CaseOperations = {
ecsType: EVENT_TYPES.change, ecsType: EVENT_TYPES.change,
name: WriteOperations.PushCase as const, name: WriteOperations.PushCase as const,
action: 'case_push', action: 'case_push',
verbs: updateVerbs, verbs: pushVerbs,
docType: 'case', docType: 'case',
savedObjectType: CASE_SAVED_OBJECT, savedObjectType: CASE_SAVED_OBJECT,
}, },

View file

@ -15,6 +15,9 @@ import { createCasesRoute } from '../create_cases_route';
export const getConnectorsRoute = createCasesRoute({ export const getConnectorsRoute = createCasesRoute({
method: 'get', method: 'get',
path: `${CASE_CONFIGURE_CONNECTORS_URL}/_find`, path: `${CASE_CONFIGURE_CONNECTORS_URL}/_find`,
routerOptions: {
tags: ['access:casesGetConnectorsConfigure'],
},
handler: async ({ context, response }) => { handler: async ({ context, response }) => {
try { try {
const caseContext = await context.cases; const caseContext = await context.cases;

View file

@ -40,7 +40,7 @@ interface CaseRouteHandlerArguments<P, Q, B> {
kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; kibanaVersion: PluginInitializerContext['env']['packageInfo']['version'];
} }
type CaseRouteTags = 'access:casesSuggestUserProfiles'; type CaseRouteTags = 'access:casesSuggestUserProfiles' | 'access:casesGetConnectorsConfigure';
export interface CaseRoute<P = unknown, Q = unknown, B = unknown> { export interface CaseRoute<P = unknown, Q = unknown, B = unknown> {
method: 'get' | 'post' | 'put' | 'delete' | 'patch'; method: 'get' | 'post' | 'put' | 'delete' | 'patch';

View file

@ -110,6 +110,7 @@ describe('AddToCaseAction', function () {
update: false, update: false,
delete: false, delete: false,
push: false, push: false,
connectors: false,
}, },
}) })
); );

View file

@ -18,6 +18,7 @@ export function useGetUserCasesPermissions() {
update: false, update: false,
delete: false, delete: false,
push: false, push: false,
connectors: false,
}); });
const uiCapabilities = useKibana().services.application.capabilities; const uiCapabilities = useKibana().services.application.capabilities;
@ -33,6 +34,7 @@ export function useGetUserCasesPermissions() {
update: casesCapabilities.update, update: casesCapabilities.update,
delete: casesCapabilities.delete, delete: casesCapabilities.delete,
push: casesCapabilities.push, push: casesCapabilities.push,
connectors: casesCapabilities.connectors,
}); });
}, [ }, [
casesCapabilities.all, casesCapabilities.all,
@ -41,6 +43,7 @@ export function useGetUserCasesPermissions() {
casesCapabilities.update, casesCapabilities.update,
casesCapabilities.delete, casesCapabilities.delete,
casesCapabilities.push, casesCapabilities.push,
casesCapabilities.connectors,
]); ]);
return casesPermissions; return casesPermissions;

View file

@ -19,7 +19,15 @@ export default {
const Template: ComponentStory<typeof Component> = (props: CasesProps) => <Component {...props} />; const Template: ComponentStory<typeof Component> = (props: CasesProps) => <Component {...props} />;
const defaultProps: CasesProps = { 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({}); export const CasesPageWithAllPermissions = Template.bind({});
@ -34,5 +42,6 @@ CasesPageWithNoPermissions.args = {
delete: false, delete: false,
push: false, push: false,
update: false, update: false,
connectors: false,
}, },
}; };

View file

@ -19,6 +19,7 @@ export function useGetUserCasesPermissions() {
update: false, update: false,
delete: false, delete: false,
push: false, push: false,
connectors: false,
}); });
const uiCapabilities = useKibana().services.application!.capabilities; const uiCapabilities = useKibana().services.application!.capabilities;
@ -35,6 +36,7 @@ export function useGetUserCasesPermissions() {
update: casesCapabilities.update, update: casesCapabilities.update,
delete: casesCapabilities.delete, delete: casesCapabilities.delete,
push: casesCapabilities.push, push: casesCapabilities.push,
connectors: casesCapabilities.connectors,
}); });
}, [ }, [
casesCapabilities.all, casesCapabilities.all,
@ -43,6 +45,7 @@ export function useGetUserCasesPermissions() {
casesCapabilities.update, casesCapabilities.update,
casesCapabilities.delete, casesCapabilities.delete,
casesCapabilities.push, casesCapabilities.push,
casesCapabilities.connectors,
]); ]);
return casesPermissions; return casesPermissions;

View file

@ -12,4 +12,5 @@ export const noCasesPermissions = () => ({
update: false, update: false,
delete: false, delete: false,
push: false, push: false,
connectors: false,
}); });

View file

@ -11,6 +11,7 @@ export const noCasesCapabilities = () => ({
update_cases: false, update_cases: false,
delete_cases: false, delete_cases: false,
push_cases: false, push_cases: false,
cases_connector: false,
}); });
export const readCasesCapabilities = () => ({ export const readCasesCapabilities = () => ({
@ -19,6 +20,7 @@ export const readCasesCapabilities = () => ({
update_cases: false, update_cases: false,
delete_cases: false, delete_cases: false,
push_cases: false, push_cases: false,
cases_connector: true,
}); });
export const allCasesCapabilities = () => ({ export const allCasesCapabilities = () => ({
@ -27,6 +29,7 @@ export const allCasesCapabilities = () => ({
update_cases: true, update_cases: true,
delete_cases: true, delete_cases: true,
push_cases: true, push_cases: true,
cases_connector: true,
}); });
export const noCasesPermissions = () => ({ export const noCasesPermissions = () => ({
@ -36,6 +39,7 @@ export const noCasesPermissions = () => ({
update: false, update: false,
delete: false, delete: false,
push: false, push: false,
connectors: false,
}); });
export const readCasesPermissions = () => ({ export const readCasesPermissions = () => ({
@ -45,6 +49,7 @@ export const readCasesPermissions = () => ({
update: false, update: false,
delete: false, delete: false,
push: false, push: false,
connectors: true,
}); });
export const writeCasesPermissions = () => ({ export const writeCasesPermissions = () => ({
@ -54,6 +59,7 @@ export const writeCasesPermissions = () => ({
update: true, update: true,
delete: true, delete: true,
push: true, push: true,
connectors: true,
}); });
export const allCasesPermissions = () => ({ export const allCasesPermissions = () => ({
@ -63,4 +69,5 @@ export const allCasesPermissions = () => ({
update: true, update: true,
delete: true, delete: true,
push: true, push: true,
connectors: true,
}); });

View file

@ -161,6 +161,7 @@ export const useGetUserCasesPermissions = () => {
update: false, update: false,
delete: false, delete: false,
push: false, push: false,
connectors: false,
}); });
const uiCapabilities = useKibana().services.application.capabilities; const uiCapabilities = useKibana().services.application.capabilities;
const casesCapabilities = useKibana().services.cases.helpers.getUICapabilities( const casesCapabilities = useKibana().services.cases.helpers.getUICapabilities(
@ -175,6 +176,7 @@ export const useGetUserCasesPermissions = () => {
update: casesCapabilities.update, update: casesCapabilities.update,
delete: casesCapabilities.delete, delete: casesCapabilities.delete,
push: casesCapabilities.push, push: casesCapabilities.push,
connectors: casesCapabilities.connectors,
}); });
}, [ }, [
casesCapabilities.all, casesCapabilities.all,
@ -183,6 +185,7 @@ export const useGetUserCasesPermissions = () => {
casesCapabilities.update, casesCapabilities.update,
casesCapabilities.delete, casesCapabilities.delete,
casesCapabilities.push, casesCapabilities.push,
casesCapabilities.connectors,
]); ]);
return casesPermissions; return casesPermissions;

View file

@ -13,6 +13,10 @@ import {
createUICapabilities as createCasesUICapabilities, createUICapabilities as createCasesUICapabilities,
getApiTags as getCasesApiTags, getApiTags as getCasesApiTags,
} from '@kbn/cases-plugin/common'; } 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 type { AppFeaturesCasesConfig, BaseKibanaFeatureConfig } from './types';
import { APP_ID, CASES_FEATURE_ID } from '../../../common/constants'; import { APP_ID, CASES_FEATURE_ID } from '../../../common/constants';
import { CasesSubFeatureId } from './security_cases_kibana_sub_features'; import { CasesSubFeatureId } from './security_cases_kibana_sub_features';
@ -21,7 +25,25 @@ import { AppFeatureCasesKey } from '../../../common/types/app_features';
const casesCapabilities = createCasesUICapabilities(); const casesCapabilities = createCasesUICapabilities();
const casesApiTags = getCasesApiTags(APP_ID); const casesApiTags = getCasesApiTags(APP_ID);
export const getCasesBaseKibanaFeature = (): BaseKibanaFeatureConfig => ({ 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, id: CASES_FEATURE_ID,
name: i18n.translate('xpack.securitySolution.featureRegistry.linkSecuritySolutionCaseTitle', { name: i18n.translate('xpack.securitySolution.featureRegistry.linkSecuritySolutionCaseTitle', {
defaultMessage: 'Cases', defaultMessage: 'Cases',
@ -33,23 +55,22 @@ export const getCasesBaseKibanaFeature = (): BaseKibanaFeatureConfig => ({
cases: [APP_ID], cases: [APP_ID],
privileges: { privileges: {
all: { all: {
api: casesApiTags.all, api: casesAllAPICapabilities,
app: [CASES_FEATURE_ID, 'kibana'], app: [CASES_FEATURE_ID, 'kibana'],
catalogue: [APP_ID], catalogue: [APP_ID],
cases: { cases: {
create: [APP_ID], create: [APP_ID],
read: [APP_ID], read: [APP_ID],
update: [APP_ID], update: [APP_ID],
push: [APP_ID],
}, },
savedObject: { savedObject: {
all: [...filesSavedObjectTypes], all: [...filesSavedObjectTypes],
read: [...filesSavedObjectTypes], read: [...filesSavedObjectTypes],
}, },
ui: casesCapabilities.all, ui: casesAllUICapabilities,
}, },
read: { read: {
api: casesApiTags.read, api: casesReadAPICapabilities,
app: [CASES_FEATURE_ID, 'kibana'], app: [CASES_FEATURE_ID, 'kibana'],
catalogue: [APP_ID], catalogue: [APP_ID],
cases: { cases: {
@ -59,10 +80,11 @@ export const getCasesBaseKibanaFeature = (): BaseKibanaFeatureConfig => ({
all: [], all: [],
read: [...filesSavedObjectTypes], read: [...filesSavedObjectTypes],
}, },
ui: casesCapabilities.read, ui: casesReadUICapabilities,
}, },
}, },
}); };
};
export const getCasesBaseKibanaSubFeatureIds = (): CasesSubFeatureId[] => [ export const getCasesBaseKibanaSubFeatureIds = (): CasesSubFeatureId[] => [
CasesSubFeatureId.deleteCases, CasesSubFeatureId.deleteCases,
@ -79,6 +101,18 @@ export const getCasesBaseKibanaSubFeatureIds = (): CasesSubFeatureId[] => [
*/ */
export const getCasesAppFeaturesConfig = (): AppFeaturesCasesConfig => ({ export const getCasesAppFeaturesConfig = (): AppFeaturesCasesConfig => ({
[AppFeatureCasesKey.casesConnectors]: { [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
},
},
}, },
}); });

View file

@ -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 = { export const globalRead: Role = {
name: 'global_read', name: 'global_read',
privileges: { privileges: {
@ -353,6 +377,7 @@ export const securitySolutionOnlyAllSpacesRole: Role = {
export const roles = [ export const roles = [
noKibanaPrivileges, noKibanaPrivileges,
noCasesPrivilegesSpace1, noCasesPrivilegesSpace1,
noCasesConnectors,
globalRead, globalRead,
securitySolutionOnlyAll, securitySolutionOnlyAll,
securitySolutionOnlyRead, securitySolutionOnlyRead,

View file

@ -21,6 +21,7 @@ import {
securitySolutionOnlyReadAlerts, securitySolutionOnlyReadAlerts,
securitySolutionOnlyReadNoIndexAlerts, securitySolutionOnlyReadNoIndexAlerts,
securitySolutionOnlyReadDelete, securitySolutionOnlyReadDelete,
noCasesConnectors as noCasesConnectorRole,
} from './roles'; } from './roles';
import { User } from './types'; import { User } from './types';
@ -126,6 +127,12 @@ export const noCasesPrivilegesSpace1: User = {
roles: [noCasesPrivilegesSpace1Role.name], 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. * These users will have access to all spaces.
*/ */
@ -154,4 +161,5 @@ export const users = [
noKibanaPrivileges, noKibanaPrivileges,
noCasesPrivilegesSpace1, noCasesPrivilegesSpace1,
testDisabled, testDisabled,
noCasesConnectors,
]; ];

View file

@ -40,6 +40,45 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
registerRoutes(core, this.log); registerRoutes(core, this.log);
registerCaseFixtureFileKinds(deps.files); 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) {} public start(core: CoreStart, plugins: FixtureStartDeps) {}

View file

@ -31,7 +31,7 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
cases: ['observabilityFixture'], cases: ['observabilityFixture'],
privileges: { privileges: {
all: { all: {
api: ['casesSuggestUserProfiles', 'bulkGetUserProfiles'], api: ['casesSuggestUserProfiles', 'bulkGetUserProfiles', 'casesGetConnectorsConfigure'],
app: ['kibana'], app: ['kibana'],
cases: { cases: {
all: ['observabilityFixture'], all: ['observabilityFixture'],
@ -43,7 +43,7 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
ui: [], ui: [],
}, },
read: { read: {
api: ['casesSuggestUserProfiles', 'bulkGetUserProfiles'], api: ['casesSuggestUserProfiles', 'bulkGetUserProfiles', 'casesGetConnectorsConfigure'],
app: ['kibana'], app: ['kibana'],
cases: { cases: {
read: ['observabilityFixture'], read: ['observabilityFixture'],

View file

@ -39,7 +39,7 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
cases: ['securitySolutionFixture'], cases: ['securitySolutionFixture'],
privileges: { privileges: {
all: { all: {
api: ['casesSuggestUserProfiles', 'bulkGetUserProfiles'], api: ['casesSuggestUserProfiles', 'bulkGetUserProfiles', 'casesGetConnectorsConfigure'],
app: ['kibana'], app: ['kibana'],
cases: { cases: {
create: ['securitySolutionFixture'], create: ['securitySolutionFixture'],
@ -54,7 +54,7 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
ui: [], ui: [],
}, },
read: { read: {
api: ['casesSuggestUserProfiles', 'bulkGetUserProfiles'], api: ['casesSuggestUserProfiles', 'bulkGetUserProfiles', 'casesGetConnectorsConfigure'],
app: ['kibana'], app: ['kibana'],
cases: { cases: {
read: ['securitySolutionFixture'], read: ['securitySolutionFixture'],

View file

@ -6,7 +6,7 @@
*/ */
import { ConnectorTypes } from '@kbn/cases-plugin/common/types/domain'; 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 { postCaseReq } from '../../../../common/lib/mock';
import { import {

View file

@ -54,6 +54,7 @@ import {
} from '../../../../common/lib/api'; } from '../../../../common/lib/api';
import { import {
globalRead, globalRead,
noCasesConnectors,
noKibanaPrivileges, noKibanaPrivileges,
obsOnlyRead, obsOnlyRead,
obsSecRead, obsSecRead,
@ -840,6 +841,24 @@ export default ({ getService }: FtrProviderContext): void => {
expect(theCase.status).to.eql('open'); 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 },
});
});
}); });
}); });
}); });

View file

@ -6,7 +6,7 @@
*/ */
import expect from '@kbn/expect'; 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 { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib';
import { import {
@ -20,10 +20,12 @@ import {
getCaseConnectors, getCaseConnectors,
getCasesWebhookConnector, getCasesWebhookConnector,
} from '../../../../common/lib/api'; } from '../../../../common/lib/api';
import { noCasesConnectors } from '../../../../common/lib/authentication/users';
// eslint-disable-next-line import/no-default-export // eslint-disable-next-line import/no-default-export
export default ({ getService }: FtrProviderContext): void => { export default ({ getService }: FtrProviderContext): void => {
const supertest = getService('supertest'); const supertest = getService('supertest');
const supertestWithoutAuth = getService('supertestWithoutAuth');
const actionsRemover = new ActionsRemover(supertest); const actionsRemover = new ActionsRemover(supertest);
describe('get_connectors', () => { 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,
});
});
}); });
}; };

View file

@ -42,6 +42,7 @@ const permissions = {
update: true, update: true,
delete: true, delete: true,
push: true, push: true,
connectors: true,
}; };
const attachments = [{ type: AttachmentType.user as const, comment: 'test' }]; const attachments = [{ type: AttachmentType.user as const, comment: 'test' }];