mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[Roles] Support for remote_cluster field in ES role definition (#182377)
## Summary
Added support for `remote_cluster` field in ES role definition. The
change is needed for running queries with `ENRICH` keyword that are sent
over CCS.
- Updated GET and PUT APIs, documentation and corresponding data models.
- Added UI section to support the new API features.
- Extracted remote clusters ComboBox to a separate component
[RemoteClusterComboBox](https://github.com/elastic/kibana/pull/182377/files#diff-6b3189b6d802fd2196bcc445dc5c6021af70cf165fe3f8c4d4a5e6a4df651309R22)
to share it between the remote clusters and remote index privilege
views.
d3cf8b9c
-e83d-4ace-ba2e-f8e028977f2d
### Checklist
- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [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
- [x] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
([Report](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/5855))
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
### For maintainers
- [x] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
__Fixes: https://github.com/elastic/kibana/issues/182035__
## Release note
Added support for `remote_cluster` field in ES role definition.
This commit is contained in:
parent
1798e7b7ab
commit
25098d9a72
44 changed files with 1694 additions and 130 deletions
|
@ -26,7 +26,7 @@ To use the create or update role API, you must have the `manage_security` cluste
|
|||
|
||||
`elasticsearch`::
|
||||
(Optional, object) {es} cluster and index privileges. Valid keys include
|
||||
`cluster`, `indices`, `remote_indices`, and `run_as`. For more information, see
|
||||
`cluster`, `indices`, `remote_indices`, `remote_cluster`, and `run_as`. For more information, see
|
||||
{ref}/defining-roles.html[Defining roles].
|
||||
|
||||
`kibana`::
|
||||
|
@ -210,6 +210,12 @@ $ curl -X PUT api/security/role/my_kibana_role
|
|||
"names": [ "remote_index1", "remote_index2" ],
|
||||
"privileges": [ "all" ]
|
||||
}
|
||||
],
|
||||
"remote_cluster": [
|
||||
{
|
||||
"clusters": [ "remote_cluster1" ],
|
||||
"privileges": [ "monitor_enrich" ]
|
||||
}
|
||||
]
|
||||
},
|
||||
"kibana": [
|
||||
|
|
|
@ -16,6 +16,7 @@ export type {
|
|||
RoleIndexPrivilege,
|
||||
RoleKibanaPrivilege,
|
||||
RoleRemoteIndexPrivilege,
|
||||
RoleRemoteClusterPrivilege,
|
||||
FeaturesPrivileges,
|
||||
} from './src/authorization';
|
||||
export type { SecurityLicense, SecurityLicenseFeatures, LoginLayout } from './src/licensing';
|
||||
|
|
|
@ -11,4 +11,5 @@ export type {
|
|||
RoleKibanaPrivilege,
|
||||
RoleIndexPrivilege,
|
||||
RoleRemoteIndexPrivilege,
|
||||
RoleRemoteClusterPrivilege,
|
||||
} from './role';
|
||||
|
|
|
@ -28,11 +28,17 @@ export interface RoleKibanaPrivilege {
|
|||
_reserved?: string[];
|
||||
}
|
||||
|
||||
export interface RoleRemoteClusterPrivilege {
|
||||
clusters: string[];
|
||||
privileges: string[];
|
||||
}
|
||||
|
||||
export interface Role {
|
||||
name: string;
|
||||
description?: string;
|
||||
elasticsearch: {
|
||||
cluster: string[];
|
||||
remote_cluster?: RoleRemoteClusterPrivilege[];
|
||||
indices: RoleIndexPrivilege[];
|
||||
remote_indices?: RoleRemoteIndexPrivilege[];
|
||||
run_as: string[];
|
||||
|
|
|
@ -59,6 +59,11 @@ export interface SecurityLicenseFeatures {
|
|||
*/
|
||||
readonly allowRoleRemoteIndexPrivileges: boolean;
|
||||
|
||||
/**
|
||||
* Indicates whether we allow users to define remote cluster privileges in roles.
|
||||
*/
|
||||
readonly allowRemoteClusterPrivileges: boolean;
|
||||
|
||||
/**
|
||||
* Indicates whether we allow Role-based access control (RBAC).
|
||||
*/
|
||||
|
|
|
@ -22,6 +22,19 @@ export const elasticsearchRoleSchema = schema.object({
|
|||
*/
|
||||
cluster: schema.maybe(schema.arrayOf(schema.string())),
|
||||
|
||||
/**
|
||||
* An optional list of remote cluster privileges. These privileges define the remote cluster level actions that
|
||||
* users with this role are able to execute
|
||||
*/
|
||||
remote_cluster: schema.maybe(
|
||||
schema.arrayOf(
|
||||
schema.object({
|
||||
privileges: schema.arrayOf(schema.string(), { minSize: 1 }),
|
||||
clusters: schema.arrayOf(schema.string(), { minSize: 1 }),
|
||||
})
|
||||
)
|
||||
),
|
||||
|
||||
/**
|
||||
* An optional list of indices permissions entries.
|
||||
*/
|
||||
|
|
|
@ -31,6 +31,7 @@ export type {
|
|||
RoleIndexPrivilege,
|
||||
RoleKibanaPrivilege,
|
||||
RoleRemoteIndexPrivilege,
|
||||
RoleRemoteClusterPrivilege,
|
||||
FeaturesPrivileges,
|
||||
LoginLayout,
|
||||
SecurityLicenseFeatures,
|
||||
|
|
|
@ -26,6 +26,7 @@ describe('license features', function () {
|
|||
allowRoleDocumentLevelSecurity: false,
|
||||
allowRoleFieldLevelSecurity: false,
|
||||
allowRoleRemoteIndexPrivileges: false,
|
||||
allowRemoteClusterPrivileges: false,
|
||||
layout: 'error-es-unavailable',
|
||||
allowRbac: false,
|
||||
allowSubFeaturePrivileges: false,
|
||||
|
@ -50,6 +51,7 @@ describe('license features', function () {
|
|||
allowRoleDocumentLevelSecurity: false,
|
||||
allowRoleFieldLevelSecurity: false,
|
||||
allowRoleRemoteIndexPrivileges: false,
|
||||
allowRemoteClusterPrivileges: false,
|
||||
layout: 'error-xpack-unavailable',
|
||||
allowRbac: false,
|
||||
allowSubFeaturePrivileges: false,
|
||||
|
@ -78,6 +80,7 @@ describe('license features', function () {
|
|||
"allowAuditLogging": false,
|
||||
"allowLogin": false,
|
||||
"allowRbac": false,
|
||||
"allowRemoteClusterPrivileges": false,
|
||||
"allowRoleDocumentLevelSecurity": false,
|
||||
"allowRoleFieldLevelSecurity": false,
|
||||
"allowRoleRemoteIndexPrivileges": false,
|
||||
|
@ -101,6 +104,7 @@ describe('license features', function () {
|
|||
"allowAuditLogging": true,
|
||||
"allowLogin": true,
|
||||
"allowRbac": true,
|
||||
"allowRemoteClusterPrivileges": true,
|
||||
"allowRoleDocumentLevelSecurity": true,
|
||||
"allowRoleFieldLevelSecurity": true,
|
||||
"allowRoleRemoteIndexPrivileges": true,
|
||||
|
@ -137,6 +141,7 @@ describe('license features', function () {
|
|||
allowRoleDocumentLevelSecurity: false,
|
||||
allowRoleFieldLevelSecurity: false,
|
||||
allowRoleRemoteIndexPrivileges: false,
|
||||
allowRemoteClusterPrivileges: false,
|
||||
allowRbac: true,
|
||||
allowSubFeaturePrivileges: false,
|
||||
allowAuditLogging: false,
|
||||
|
@ -164,6 +169,7 @@ describe('license features', function () {
|
|||
allowRoleDocumentLevelSecurity: false,
|
||||
allowRoleFieldLevelSecurity: false,
|
||||
allowRoleRemoteIndexPrivileges: false,
|
||||
allowRemoteClusterPrivileges: false,
|
||||
allowRbac: false,
|
||||
allowSubFeaturePrivileges: false,
|
||||
allowAuditLogging: false,
|
||||
|
@ -190,6 +196,7 @@ describe('license features', function () {
|
|||
allowRoleDocumentLevelSecurity: false,
|
||||
allowRoleFieldLevelSecurity: false,
|
||||
allowRoleRemoteIndexPrivileges: false,
|
||||
allowRemoteClusterPrivileges: false,
|
||||
allowRbac: true,
|
||||
allowSubFeaturePrivileges: false,
|
||||
allowAuditLogging: false,
|
||||
|
@ -216,6 +223,7 @@ describe('license features', function () {
|
|||
allowRoleDocumentLevelSecurity: false,
|
||||
allowRoleFieldLevelSecurity: false,
|
||||
allowRoleRemoteIndexPrivileges: false,
|
||||
allowRemoteClusterPrivileges: false,
|
||||
allowRbac: true,
|
||||
allowSubFeaturePrivileges: true,
|
||||
allowAuditLogging: true,
|
||||
|
@ -242,6 +250,7 @@ describe('license features', function () {
|
|||
allowRoleDocumentLevelSecurity: true,
|
||||
allowRoleFieldLevelSecurity: true,
|
||||
allowRoleRemoteIndexPrivileges: true,
|
||||
allowRemoteClusterPrivileges: true,
|
||||
allowRbac: true,
|
||||
allowSubFeaturePrivileges: true,
|
||||
allowAuditLogging: true,
|
||||
|
|
|
@ -77,6 +77,7 @@ export class SecurityLicenseService {
|
|||
allowRoleDocumentLevelSecurity: false,
|
||||
allowRoleFieldLevelSecurity: false,
|
||||
allowRoleRemoteIndexPrivileges: false,
|
||||
allowRemoteClusterPrivileges: false,
|
||||
allowRbac: false,
|
||||
allowSubFeaturePrivileges: false,
|
||||
allowUserProfileCollaboration: false,
|
||||
|
@ -98,6 +99,7 @@ export class SecurityLicenseService {
|
|||
allowRoleDocumentLevelSecurity: false,
|
||||
allowRoleFieldLevelSecurity: false,
|
||||
allowRoleRemoteIndexPrivileges: false,
|
||||
allowRemoteClusterPrivileges: false,
|
||||
allowRbac: false,
|
||||
allowSubFeaturePrivileges: false,
|
||||
allowUserProfileCollaboration: false,
|
||||
|
@ -119,6 +121,7 @@ export class SecurityLicenseService {
|
|||
allowRoleDocumentLevelSecurity: isLicensePlatinumOrBetter,
|
||||
allowRoleFieldLevelSecurity: isLicensePlatinumOrBetter,
|
||||
allowRoleRemoteIndexPrivileges: isLicensePlatinumOrBetter,
|
||||
allowRemoteClusterPrivileges: isLicensePlatinumOrBetter,
|
||||
allowRbac: true,
|
||||
allowUserProfileCollaboration: isLicenseStandardOrBetter,
|
||||
};
|
||||
|
|
|
@ -8,4 +8,5 @@
|
|||
export interface BuiltinESPrivileges {
|
||||
cluster: string[];
|
||||
index: string[];
|
||||
remote_cluster: string[];
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ export interface CheckRoleMappingFeaturesResponse {
|
|||
canUseStoredScripts: boolean;
|
||||
hasCompatibleRealms: boolean;
|
||||
canUseRemoteIndices: boolean;
|
||||
canUseRemoteClusters: boolean;
|
||||
}
|
||||
|
||||
type DeleteRoleMappingsResponse = Array<{
|
||||
|
|
|
@ -211,7 +211,7 @@ function useRole(
|
|||
? rolesAPIClient.getRole(roleName)
|
||||
: Promise.resolve({
|
||||
name: '',
|
||||
elasticsearch: { cluster: [], indices: [], run_as: [] },
|
||||
elasticsearch: { cluster: [], indices: [], run_as: [], remote_cluster: [] },
|
||||
kibana: [],
|
||||
_unrecognized_applications: [],
|
||||
} as Role);
|
||||
|
@ -529,6 +529,9 @@ export const EditRolePage: FunctionComponent<Props> = ({
|
|||
canUseRemoteIndices={
|
||||
buildFlavor === 'traditional' && featureCheckState.value?.canUseRemoteIndices
|
||||
}
|
||||
canUseRemoteClusters={
|
||||
buildFlavor === 'traditional' && featureCheckState.value?.canUseRemoteClusters
|
||||
}
|
||||
isDarkMode={isDarkMode}
|
||||
buildFlavor={buildFlavor}
|
||||
/>
|
||||
|
|
|
@ -59,6 +59,7 @@ exports[`it renders correctly in serverless mode 1`] = `
|
|||
"elasticsearch": Object {
|
||||
"cluster": Array [],
|
||||
"indices": Array [],
|
||||
"remote_cluster": Array [],
|
||||
"run_as": Array [],
|
||||
},
|
||||
"kibana": Array [],
|
||||
|
@ -141,6 +142,7 @@ exports[`it renders correctly in serverless mode 1`] = `
|
|||
"elasticsearch": Object {
|
||||
"cluster": Array [],
|
||||
"indices": Array [],
|
||||
"remote_cluster": Array [],
|
||||
"run_as": Array [],
|
||||
},
|
||||
"kibana": Array [],
|
||||
|
@ -215,6 +217,7 @@ exports[`it renders without crashing 1`] = `
|
|||
"elasticsearch": Object {
|
||||
"cluster": Array [],
|
||||
"indices": Array [],
|
||||
"remote_cluster": Array [],
|
||||
"run_as": Array [],
|
||||
},
|
||||
"kibana": Array [],
|
||||
|
@ -346,6 +349,7 @@ exports[`it renders without crashing 1`] = `
|
|||
"elasticsearch": Object {
|
||||
"cluster": Array [],
|
||||
"indices": Array [],
|
||||
"remote_cluster": Array [],
|
||||
"run_as": Array [],
|
||||
},
|
||||
"kibana": Array [],
|
||||
|
|
|
@ -36,6 +36,7 @@ exports[`it renders without crashing 1`] = `
|
|||
"elasticsearch": Object {
|
||||
"cluster": Array [],
|
||||
"indices": Array [],
|
||||
"remote_cluster": Array [],
|
||||
"run_as": Array [],
|
||||
},
|
||||
"kibana": Array [],
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`it renders without crashing 1`] = `
|
||||
<RemoteClusterPrivileges
|
||||
availableRemoteClusterPrivileges={
|
||||
Array [
|
||||
"monitor_enrich",
|
||||
]
|
||||
}
|
||||
editable={true}
|
||||
license={
|
||||
Object {
|
||||
"features$": Observable {
|
||||
"_subscribe": [Function],
|
||||
},
|
||||
"getFeatures": [MockFunction],
|
||||
"getUnavailableReason": [MockFunction],
|
||||
"hasAtLeast": [MockFunction],
|
||||
"isEnabled": [MockFunction],
|
||||
"isLicenseAvailable": [MockFunction],
|
||||
}
|
||||
}
|
||||
onChange={[MockFunction]}
|
||||
role={
|
||||
Object {
|
||||
"elasticsearch": Object {
|
||||
"cluster": Array [],
|
||||
"indices": Array [],
|
||||
"remote_cluster": Array [
|
||||
Object {
|
||||
"clusters": Array [
|
||||
"cluster1",
|
||||
"cluster2",
|
||||
],
|
||||
"privileges": Array [
|
||||
"monitor_enrich",
|
||||
],
|
||||
},
|
||||
],
|
||||
"run_as": Array [],
|
||||
},
|
||||
"kibana": Array [],
|
||||
"name": "",
|
||||
}
|
||||
}
|
||||
validator={
|
||||
RoleValidator {
|
||||
"shouldValidate": undefined,
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
|
@ -0,0 +1,116 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`it renders without crashing 1`] = `
|
||||
<Fragment>
|
||||
<EuiSpacer
|
||||
size="m"
|
||||
/>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
className="remote-cluster-privilege-form"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiPanel
|
||||
color="subdued"
|
||||
>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
fullWidth={true}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
isInvalid={false}
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Remote clusters"
|
||||
id="xpack.security.management.editRole.remoteClusterPrivilegeForm.clustersFormRowLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
labelType="label"
|
||||
>
|
||||
<RemoteClusterComboBox
|
||||
data-test-subj="remoteClusterClustersInput0"
|
||||
fullWidth={true}
|
||||
isDisabled={false}
|
||||
onChange={[Function]}
|
||||
onCreateOption={[Function]}
|
||||
placeholder="Add a remote cluster…"
|
||||
remoteClusters={Array []}
|
||||
selectedOptions={
|
||||
Array [
|
||||
Object {
|
||||
"label": "cluster1",
|
||||
},
|
||||
]
|
||||
}
|
||||
type="remote_cluster"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
fullWidth={true}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
isInvalid={false}
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Privileges"
|
||||
id="xpack.security.management.editRole.remoteClusterPrivilegeForm.privilegesFormRowLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
labelType="label"
|
||||
>
|
||||
<EuiComboBox
|
||||
async={false}
|
||||
compressed={false}
|
||||
data-test-subj="remoteClusterPrivilegesInput0"
|
||||
fullWidth={true}
|
||||
isClearable={true}
|
||||
isDisabled={false}
|
||||
onChange={[Function]}
|
||||
optionMatcher={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"label": "monitor_enrich",
|
||||
},
|
||||
]
|
||||
}
|
||||
placeholder="Add an action…"
|
||||
selectedOptions={
|
||||
Array [
|
||||
Object {
|
||||
"label": "monitor_enrich",
|
||||
},
|
||||
]
|
||||
}
|
||||
singleSelection={false}
|
||||
sortMatchesBy="none"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
aria-label="Delete remote cluster privilege"
|
||||
color="danger"
|
||||
data-test-subj="deleteRemoteClusterPrivilegesButton0"
|
||||
iconType="trash"
|
||||
onClick={[MockFunction]}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Fragment>
|
||||
`;
|
|
@ -0,0 +1,146 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`it renders without crashing 1`] = `
|
||||
<EuiComboBox
|
||||
async={false}
|
||||
compressed={false}
|
||||
dat-test-subj="remoteClusterClustersInput0"
|
||||
fullWidth={false}
|
||||
intl={
|
||||
Object {
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"formatDate": [Function],
|
||||
"formatHTMLMessage": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelative": [Function],
|
||||
"formatTime": [Function],
|
||||
"formats": Object {
|
||||
"date": Object {
|
||||
"full": Object {
|
||||
"day": "numeric",
|
||||
"month": "long",
|
||||
"weekday": "long",
|
||||
"year": "numeric",
|
||||
},
|
||||
"long": Object {
|
||||
"day": "numeric",
|
||||
"month": "long",
|
||||
"year": "numeric",
|
||||
},
|
||||
"medium": Object {
|
||||
"day": "numeric",
|
||||
"month": "short",
|
||||
"year": "numeric",
|
||||
},
|
||||
"short": Object {
|
||||
"day": "numeric",
|
||||
"month": "numeric",
|
||||
"year": "2-digit",
|
||||
},
|
||||
},
|
||||
"number": Object {
|
||||
"currency": Object {
|
||||
"style": "currency",
|
||||
},
|
||||
"percent": Object {
|
||||
"style": "percent",
|
||||
},
|
||||
},
|
||||
"relative": Object {
|
||||
"days": Object {
|
||||
"units": "day",
|
||||
},
|
||||
"hours": Object {
|
||||
"units": "hour",
|
||||
},
|
||||
"minutes": Object {
|
||||
"units": "minute",
|
||||
},
|
||||
"months": Object {
|
||||
"units": "month",
|
||||
},
|
||||
"seconds": Object {
|
||||
"units": "second",
|
||||
},
|
||||
"years": Object {
|
||||
"units": "year",
|
||||
},
|
||||
},
|
||||
"time": Object {
|
||||
"full": Object {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
"second": "numeric",
|
||||
"timeZoneName": "short",
|
||||
},
|
||||
"long": Object {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
"second": "numeric",
|
||||
"timeZoneName": "short",
|
||||
},
|
||||
"medium": Object {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
"second": "numeric",
|
||||
},
|
||||
"short": Object {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
},
|
||||
},
|
||||
},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralFormat": [Function],
|
||||
"getRelativeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"now": [Function],
|
||||
"onError": [Function],
|
||||
"textComponent": Symbol(react.fragment),
|
||||
"timeZone": null,
|
||||
}
|
||||
}
|
||||
isClearable={true}
|
||||
optionMatcher={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"label": "test2",
|
||||
},
|
||||
Object {
|
||||
"label": "test3",
|
||||
},
|
||||
Object {
|
||||
"isGroupLabelOption": true,
|
||||
"label": "Incompatible clusters",
|
||||
},
|
||||
Object {
|
||||
"append": <EuiIconTip
|
||||
color="inherit"
|
||||
content={
|
||||
<FormattedMessage
|
||||
defaultMessage="This cluster is configured with the certificate based security model and does not support remote cluster privileges. Connect this cluster with the API key based security model instead to use remote cluster privileges."
|
||||
id="xpack.security.management.editRole.remoteClusterPrivilegeForm.remoteClusterSecurityModelWarning"
|
||||
values={Object {}}
|
||||
/>
|
||||
}
|
||||
type="warning"
|
||||
/>,
|
||||
"disabled": true,
|
||||
"label": "test1",
|
||||
},
|
||||
]
|
||||
}
|
||||
selectedOptions={Array []}
|
||||
singleSelection={false}
|
||||
sortMatchesBy="none"
|
||||
/>
|
||||
`;
|
|
@ -17,6 +17,7 @@ test('it renders without crashing', () => {
|
|||
const role: Role = {
|
||||
name: '',
|
||||
elasticsearch: {
|
||||
remote_cluster: [],
|
||||
cluster: [],
|
||||
indices: [],
|
||||
run_as: [],
|
||||
|
@ -39,6 +40,7 @@ test('it renders fields as disabled when not editable', () => {
|
|||
name: '',
|
||||
elasticsearch: {
|
||||
cluster: [],
|
||||
remote_cluster: [],
|
||||
indices: [],
|
||||
run_as: [],
|
||||
},
|
||||
|
@ -61,6 +63,7 @@ test('it allows for custom cluster privileges', () => {
|
|||
name: '',
|
||||
elasticsearch: {
|
||||
cluster: ['existing-custom', 'monitor'],
|
||||
remote_cluster: [],
|
||||
indices: [],
|
||||
run_as: [],
|
||||
},
|
||||
|
|
|
@ -30,6 +30,7 @@ function getProps() {
|
|||
name: '',
|
||||
elasticsearch: {
|
||||
cluster: [],
|
||||
remote_cluster: [],
|
||||
indices: [],
|
||||
run_as: [],
|
||||
},
|
||||
|
@ -43,6 +44,7 @@ function getProps() {
|
|||
builtinESPrivileges: {
|
||||
cluster: ['all', 'manage', 'monitor'],
|
||||
index: ['all', 'read', 'write', 'index'],
|
||||
remote_cluster: [],
|
||||
},
|
||||
indicesAPIClient: indicesAPIClientMock.create(),
|
||||
docLinks,
|
||||
|
@ -75,13 +77,26 @@ test('it renders remote index privileges section when `canUseRemoteIndices` is e
|
|||
expect(wrapper.find('IndexPrivileges[indexType="remote_indices"]')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('it does not render remote cluster privileges section by default', () => {
|
||||
const wrapper = shallowWithIntl(<ElasticsearchPrivileges {...getProps()} />);
|
||||
expect(wrapper.find('RemoteClusterPrivileges')).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('it renders remote index privileges section when `canUseRemoteClusters` is enabled', () => {
|
||||
const wrapper = shallowWithIntl(<ElasticsearchPrivileges {...getProps()} canUseRemoteClusters />);
|
||||
expect(wrapper.find('RemoteClusterPrivileges')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('it renders fields as disabled when not editable', () => {
|
||||
const wrapper = shallowWithIntl(<ElasticsearchPrivileges {...getProps()} editable={false} />);
|
||||
const wrapper = shallowWithIntl(
|
||||
<ElasticsearchPrivileges {...getProps()} canUseRemoteClusters editable={false} />
|
||||
);
|
||||
expect(wrapper.find('EuiComboBox').prop('isDisabled')).toBe(true);
|
||||
expect(wrapper.find('ClusterPrivileges').prop('editable')).toBe(false);
|
||||
expect(
|
||||
wrapper.find('IndexPrivileges').everyWhere((component) => component.prop('editable'))
|
||||
).toBe(false);
|
||||
expect(wrapper.find('RemoteClusterPrivileges').prop('editable')).toBe(false);
|
||||
});
|
||||
|
||||
test('it renders correctly in serverless mode', () => {
|
||||
|
|
|
@ -25,6 +25,7 @@ import type { PublicMethodsOf } from '@kbn/utility-types';
|
|||
|
||||
import { ClusterPrivileges } from './cluster_privileges';
|
||||
import { IndexPrivileges } from './index_privileges';
|
||||
import { RemoteClusterPrivileges } from './remote_cluster_privileges';
|
||||
import type { BuiltinESPrivileges, Role, SecurityLicense } from '../../../../../../common';
|
||||
import type { IndicesAPIClient } from '../../../indices_api_client';
|
||||
import { CollapsiblePanel } from '../../collapsible_panel';
|
||||
|
@ -43,6 +44,7 @@ interface Props {
|
|||
indexPatterns: string[];
|
||||
remoteClusters?: Cluster[];
|
||||
canUseRemoteIndices?: boolean;
|
||||
canUseRemoteClusters?: boolean;
|
||||
isDarkMode?: boolean;
|
||||
buildFlavor: BuildFlavor;
|
||||
}
|
||||
|
@ -69,6 +71,7 @@ export class ElasticsearchPrivileges extends Component<Props, {}> {
|
|||
license,
|
||||
builtinESPrivileges,
|
||||
canUseRemoteIndices,
|
||||
canUseRemoteClusters,
|
||||
buildFlavor,
|
||||
} = this.props;
|
||||
|
||||
|
@ -219,6 +222,40 @@ export class ElasticsearchPrivileges extends Component<Props, {}> {
|
|||
/>
|
||||
</>
|
||||
)}
|
||||
{buildFlavor === 'traditional' && canUseRemoteClusters && (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<EuiSpacer />
|
||||
|
||||
<EuiTitle size="xs">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.elasticSearchPrivileges.remoteClusterPrivilegesTitle"
|
||||
defaultMessage="Remote cluster privileges"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText size="s" color="subdued">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.elasticSearchPrivileges.controlAccessToRemoteClusterDataDescription"
|
||||
defaultMessage="Control access to the data in remote clusters. "
|
||||
/>
|
||||
{this.learnMore(docLinks.links.security.clusterPrivileges)}
|
||||
</p>
|
||||
</EuiText>
|
||||
<RemoteClusterPrivileges
|
||||
remoteClusters={remoteClusters}
|
||||
role={role}
|
||||
validator={validator}
|
||||
license={license}
|
||||
onChange={onChange}
|
||||
availableRemoteClusterPrivileges={builtinESPrivileges.remote_cluster ?? []}
|
||||
editable={editable}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -107,70 +107,6 @@ test('should not render clusters field for local indices', () => {
|
|||
expect(wrapper.find('[data-test-subj="clustersInput0"]')).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should render clusters field for remote indices', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<IndexPrivilegeForm
|
||||
indexType="remote_indices"
|
||||
indexPrivilege={{
|
||||
clusters: [],
|
||||
names: [],
|
||||
privileges: [],
|
||||
query: '',
|
||||
field_security: {
|
||||
grant: [],
|
||||
},
|
||||
}}
|
||||
remoteClusters={[
|
||||
{
|
||||
name: 'test1',
|
||||
mode: 'proxy',
|
||||
isConnected: false,
|
||||
initialConnectTimeout: '30s',
|
||||
skipUnavailable: false,
|
||||
proxyAddress: 'localhost:9400',
|
||||
proxySocketConnections: 18,
|
||||
connectedSocketsCount: 0,
|
||||
serverName: 'localhost',
|
||||
securityModel: 'certificate',
|
||||
},
|
||||
{
|
||||
name: 'test2',
|
||||
mode: 'proxy',
|
||||
isConnected: false,
|
||||
initialConnectTimeout: '30s',
|
||||
skipUnavailable: false,
|
||||
proxyAddress: 'localhost:9400',
|
||||
proxySocketConnections: 18,
|
||||
connectedSocketsCount: 0,
|
||||
serverName: 'localhost',
|
||||
securityModel: 'api_key',
|
||||
},
|
||||
]}
|
||||
formIndex={0}
|
||||
indexPatterns={[]}
|
||||
indicesAPIClient={indicesAPIClientMock.create()}
|
||||
availableIndexPrivileges={['all', 'read', 'write', 'index']}
|
||||
isRoleReadOnly={false}
|
||||
allowDocumentLevelSecurity
|
||||
allowFieldLevelSecurity
|
||||
validator={new RoleValidator()}
|
||||
onChange={jest.fn()}
|
||||
onDelete={jest.fn()}
|
||||
/>
|
||||
);
|
||||
const clustersInput = wrapper.find('[data-test-subj="clustersInput0"]');
|
||||
expect(clustersInput).toHaveLength(1);
|
||||
expect(clustersInput.prop('options')).toEqual([
|
||||
{ label: 'test2' },
|
||||
{ label: expect.anything(), isGroupLabelOption: true },
|
||||
{
|
||||
label: 'test1',
|
||||
disabled: true,
|
||||
append: expect.anything(),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe('delete button', () => {
|
||||
const props = {
|
||||
indexType: 'indices' as const,
|
||||
|
|
|
@ -12,7 +12,6 @@ import {
|
|||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiIconTip,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiSwitch,
|
||||
|
@ -27,6 +26,7 @@ import type { monaco } from '@kbn/monaco';
|
|||
import type { Cluster } from '@kbn/remote-clusters-plugin/public';
|
||||
import type { PublicMethodsOf } from '@kbn/utility-types';
|
||||
|
||||
import { RemoteClusterComboBox } from './remote_clusters_combo_box';
|
||||
import type { RoleIndexPrivilege, RoleRemoteIndexPrivilege } from '../../../../../../common';
|
||||
import type { IndicesAPIClient } from '../../../indices_api_client';
|
||||
import type { RoleValidator } from '../../validate_role';
|
||||
|
@ -125,45 +125,6 @@ export class IndexPrivilegeForm extends Component<Props, State> {
|
|||
}
|
||||
|
||||
private getPrivilegeForm = () => {
|
||||
const remoteClusterOptions: EuiComboBoxOptionOption[] = [];
|
||||
if (this.props.remoteClusters) {
|
||||
const incompatibleOptions: EuiComboBoxOptionOption[] = [];
|
||||
this.props.remoteClusters.forEach((item, i) => {
|
||||
const disabled = item.securityModel !== 'api_key';
|
||||
if (!disabled) {
|
||||
remoteClusterOptions.push({
|
||||
label: item.name,
|
||||
});
|
||||
} else {
|
||||
incompatibleOptions.push({
|
||||
label: item.name,
|
||||
disabled,
|
||||
append: disabled ? (
|
||||
<EuiIconTip
|
||||
type="warning"
|
||||
color="inherit"
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.indexPrivilegeForm.remoteIndicesSecurityModelWarning"
|
||||
defaultMessage="This cluster is configured with the certificate based security model and does not support remote index privileges. Connect this cluster with the API key based security model instead to use remote index privileges."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
) : undefined,
|
||||
});
|
||||
}
|
||||
});
|
||||
if (incompatibleOptions.length) {
|
||||
remoteClusterOptions.push(
|
||||
{
|
||||
label: 'Incompatible clusters',
|
||||
isGroupLabelOption: true,
|
||||
},
|
||||
...incompatibleOptions
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup>
|
||||
|
@ -181,9 +142,8 @@ export class IndexPrivilegeForm extends Component<Props, State> {
|
|||
this.props.indexPrivilege as RoleRemoteIndexPrivilege
|
||||
)}
|
||||
>
|
||||
<EuiComboBox
|
||||
<RemoteClusterComboBox
|
||||
data-test-subj={`clustersInput${this.props.formIndex}`}
|
||||
options={remoteClusterOptions}
|
||||
selectedOptions={('clusters' in this.props.indexPrivilege &&
|
||||
this.props.indexPrivilege.clusters
|
||||
? this.props.indexPrivilege.clusters
|
||||
|
@ -196,6 +156,8 @@ export class IndexPrivilegeForm extends Component<Props, State> {
|
|||
'xpack.security.management.editRole.indexPrivilegeForm.clustersPlaceholder',
|
||||
{ defaultMessage: 'Add a remote cluster…' }
|
||||
)}
|
||||
remoteClusters={this.props.remoteClusters ?? []}
|
||||
type="remote_indexes"
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
|
|
@ -38,6 +38,7 @@ test('it renders without crashing', async () => {
|
|||
kibana: [],
|
||||
elasticsearch: {
|
||||
cluster: [],
|
||||
remote_cluster: [],
|
||||
indices: [],
|
||||
run_as: [],
|
||||
},
|
||||
|
@ -75,6 +76,7 @@ test('it renders an IndexPrivilegeForm for each index privilege on the role', as
|
|||
kibana: [],
|
||||
elasticsearch: {
|
||||
cluster: [],
|
||||
remote_cluster: [],
|
||||
indices: [
|
||||
{
|
||||
names: ['foo*'],
|
||||
|
@ -129,6 +131,7 @@ test('it renders an IndexPrivilegeForm for each remote index privilege on the ro
|
|||
kibana: [],
|
||||
elasticsearch: {
|
||||
cluster: [],
|
||||
remote_cluster: [],
|
||||
indices: [],
|
||||
remote_indices: [
|
||||
{
|
||||
|
@ -183,6 +186,7 @@ test('it renders fields as disabled when not editable', async () => {
|
|||
kibana: [],
|
||||
elasticsearch: {
|
||||
cluster: [],
|
||||
remote_cluster: [],
|
||||
indices: [
|
||||
{
|
||||
names: ['foo*'],
|
||||
|
|
|
@ -0,0 +1,175 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import type { SecurityLicenseFeatures } from '@kbn/security-plugin-types-common';
|
||||
import { mountWithIntl, shallowWithIntl } from '@kbn/test-jest-helpers';
|
||||
import '@kbn/code-editor-mock/jest_helper';
|
||||
|
||||
import { RemoteClusterPrivileges } from './remote_cluster_privileges';
|
||||
import { RemoteClusterPrivilegesForm } from './remote_cluster_privileges_form';
|
||||
import { licenseMock } from '../../../../../../common/licensing/index.mock';
|
||||
import { RoleValidator } from '../../validate_role';
|
||||
|
||||
test('it renders without crashing', async () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<KibanaContextProvider services={coreMock.createStart()}>
|
||||
<RemoteClusterPrivileges
|
||||
role={{
|
||||
name: '',
|
||||
kibana: [],
|
||||
elasticsearch: {
|
||||
cluster: [],
|
||||
remote_cluster: [
|
||||
{
|
||||
clusters: ['cluster1', 'cluster2'],
|
||||
privileges: ['monitor_enrich'],
|
||||
},
|
||||
],
|
||||
indices: [],
|
||||
run_as: [],
|
||||
},
|
||||
}}
|
||||
onChange={jest.fn()}
|
||||
editable
|
||||
validator={new RoleValidator()}
|
||||
availableRemoteClusterPrivileges={['monitor_enrich']}
|
||||
license={licenseMock.create()}
|
||||
/>
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.children()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('it renders an RemoteClusterPrivilegesForm for each remote cluster privilege on the role', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<KibanaContextProvider services={coreMock.createStart()}>
|
||||
<RemoteClusterPrivileges
|
||||
role={{
|
||||
name: '',
|
||||
kibana: [],
|
||||
elasticsearch: {
|
||||
cluster: [],
|
||||
remote_cluster: [
|
||||
{
|
||||
clusters: ['cluster1', 'cluster2'],
|
||||
privileges: ['monitor_enrich'],
|
||||
},
|
||||
{
|
||||
clusters: ['cluster3', 'cluster4'],
|
||||
privileges: ['monitor_enrich'],
|
||||
},
|
||||
{
|
||||
clusters: ['cluster5', 'cluster6'],
|
||||
privileges: ['monitor_enrich', 'custom-privilege'],
|
||||
},
|
||||
],
|
||||
indices: [],
|
||||
run_as: [],
|
||||
},
|
||||
}}
|
||||
onChange={jest.fn()}
|
||||
editable
|
||||
validator={new RoleValidator()}
|
||||
availableRemoteClusterPrivileges={['monitor_enrich']}
|
||||
license={licenseMock.create()}
|
||||
/>
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.find(RemoteClusterPrivilegesForm)).toHaveLength(3);
|
||||
});
|
||||
|
||||
test('it renders fields as disabled when not editable', async () => {
|
||||
const props = {
|
||||
role: {
|
||||
name: '',
|
||||
kibana: [],
|
||||
elasticsearch: {
|
||||
cluster: [],
|
||||
remote_cluster: [
|
||||
{
|
||||
clusters: ['cluster1', 'cluster2'],
|
||||
privileges: ['monitor_enrich'],
|
||||
},
|
||||
{
|
||||
clusters: ['cluster3', 'cluster4'],
|
||||
privileges: ['monitor_enrich'],
|
||||
},
|
||||
],
|
||||
indices: [],
|
||||
run_as: [],
|
||||
},
|
||||
},
|
||||
onChange: jest.fn(),
|
||||
editable: false,
|
||||
validator: new RoleValidator(),
|
||||
availableRemoteClusterPrivileges: ['monitor_enrich'],
|
||||
license: licenseMock.create(),
|
||||
};
|
||||
const wrapper = mountWithIntl(
|
||||
<KibanaContextProvider services={coreMock.createStart()}>
|
||||
<RemoteClusterPrivileges {...props} />
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('RemoteClusterPrivilegesForm')
|
||||
.everyWhere((component) => component.prop('isRoleReadOnly'))
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('it renders fields as disabled when `allowRemoteClusterPrivileges` is set to false', async () => {
|
||||
const license = licenseMock.create();
|
||||
|
||||
license.getFeatures.mockReturnValue({
|
||||
allowRemoteClusterPrivileges: false,
|
||||
} as SecurityLicenseFeatures);
|
||||
|
||||
const props = {
|
||||
role: {
|
||||
name: '',
|
||||
kibana: [],
|
||||
elasticsearch: {
|
||||
cluster: [],
|
||||
remote_cluster: [
|
||||
{
|
||||
clusters: ['cluster1', 'cluster2'],
|
||||
privileges: ['monitor_enrich'],
|
||||
},
|
||||
{
|
||||
clusters: ['cluster3', 'cluster4'],
|
||||
privileges: ['monitor_enrich'],
|
||||
},
|
||||
],
|
||||
indices: [],
|
||||
run_as: [],
|
||||
},
|
||||
},
|
||||
onChange: jest.fn(),
|
||||
editable: false,
|
||||
validator: new RoleValidator(),
|
||||
availableRemoteClusterPrivileges: ['monitor_enrich'],
|
||||
license: licenseMock.create(),
|
||||
};
|
||||
const wrapper = mountWithIntl(
|
||||
<KibanaContextProvider services={coreMock.createStart()}>
|
||||
<RemoteClusterPrivileges {...props} />
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('RemoteClusterPrivilegesForm')
|
||||
.everyWhere((component) => component.prop('isRoleReadOnly'))
|
||||
).toBe(true);
|
||||
});
|
|
@ -0,0 +1,147 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiSpacer } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { Cluster } from '@kbn/remote-clusters-plugin/public';
|
||||
|
||||
import { RemoteClusterPrivilegesForm } from './remote_cluster_privileges_form';
|
||||
import type { Role, RoleRemoteClusterPrivilege, SecurityLicense } from '../../../../../../common';
|
||||
import { isRoleReadOnly } from '../../../../../../common/model';
|
||||
import type { RoleValidator } from '../../validate_role';
|
||||
|
||||
interface Props {
|
||||
remoteClusters?: Cluster[];
|
||||
role: Role;
|
||||
availableRemoteClusterPrivileges: string[];
|
||||
license: SecurityLicense;
|
||||
onChange: (role: Role) => void;
|
||||
validator: RoleValidator;
|
||||
editable?: boolean;
|
||||
}
|
||||
|
||||
export const RemoteClusterPrivileges: React.FunctionComponent<Props> = ({
|
||||
remoteClusters,
|
||||
license,
|
||||
availableRemoteClusterPrivileges,
|
||||
role,
|
||||
editable,
|
||||
onChange,
|
||||
validator,
|
||||
}) => {
|
||||
const remoteClusterPrivileges = useMemo(() => role.elasticsearch.remote_cluster ?? [], [role]);
|
||||
const remoteClusterPrivilegesDisabled = useMemo(() => {
|
||||
const { allowRemoteClusterPrivileges } = license.getFeatures();
|
||||
|
||||
return !allowRemoteClusterPrivileges;
|
||||
}, [license]);
|
||||
|
||||
const isReadOnly = useMemo(
|
||||
() => !editable || isRoleReadOnly(role) || remoteClusterPrivilegesDisabled,
|
||||
[role, editable, remoteClusterPrivilegesDisabled]
|
||||
);
|
||||
|
||||
const onRoleChange = useCallback(
|
||||
(remoteCluster: RoleRemoteClusterPrivilege[]) => {
|
||||
const roleDraft = {
|
||||
...role,
|
||||
elasticsearch: {
|
||||
...role.elasticsearch,
|
||||
remote_cluster: remoteCluster,
|
||||
},
|
||||
};
|
||||
|
||||
onChange(roleDraft);
|
||||
},
|
||||
[onChange, role]
|
||||
);
|
||||
|
||||
const addRemoteClusterPrivilege = useCallback(() => {
|
||||
const newRemoteClusterPrivileges = [
|
||||
...remoteClusterPrivileges,
|
||||
{
|
||||
clusters: [],
|
||||
privileges: [],
|
||||
},
|
||||
];
|
||||
|
||||
onRoleChange(newRemoteClusterPrivileges);
|
||||
}, [onRoleChange, remoteClusterPrivileges]);
|
||||
|
||||
const onRemoteClusterPrivilegeChange = useCallback(
|
||||
(privilegeIndex: number) => (updatedPrivilege: RoleRemoteClusterPrivilege) => {
|
||||
const newRemoteClusterPrivileges = [...remoteClusterPrivileges];
|
||||
newRemoteClusterPrivileges[privilegeIndex] = updatedPrivilege;
|
||||
|
||||
onRoleChange(newRemoteClusterPrivileges);
|
||||
},
|
||||
[onRoleChange, remoteClusterPrivileges]
|
||||
);
|
||||
|
||||
const onRemoteClusterPrivilegeDelete = useCallback(
|
||||
(privilegeIndex: number) => () => {
|
||||
const newRemoteClusterPrivileges = [...remoteClusterPrivileges];
|
||||
newRemoteClusterPrivileges.splice(privilegeIndex, 1);
|
||||
|
||||
onRoleChange(newRemoteClusterPrivileges);
|
||||
},
|
||||
[onRoleChange, remoteClusterPrivileges]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{remoteClusterPrivileges.map((remoteClusterPrivilege, i) => (
|
||||
<RemoteClusterPrivilegesForm
|
||||
key={i}
|
||||
isRoleReadOnly={isReadOnly}
|
||||
formIndex={i}
|
||||
validator={validator}
|
||||
availableRemoteClusterPrivileges={availableRemoteClusterPrivileges}
|
||||
remoteClusterPrivilege={remoteClusterPrivilege}
|
||||
remoteClusters={remoteClusters}
|
||||
onChange={onRemoteClusterPrivilegeChange(i)}
|
||||
onDelete={onRemoteClusterPrivilegeDelete(i)}
|
||||
/>
|
||||
))}
|
||||
{editable && (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
iconType="plusInCircle"
|
||||
onClick={addRemoteClusterPrivilege}
|
||||
disabled={remoteClusterPrivilegesDisabled}
|
||||
data-test-subj="addRemoteClusterPrivilegesButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.elasticSearchPrivileges.addRemoteClusterPrivilegesButtonLabel"
|
||||
defaultMessage="Add remote cluster privilege"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
{remoteClusterPrivilegesDisabled && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIconTip
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.elasticSearchPrivileges.remoteClusterPrivilegesLicenseMissing"
|
||||
defaultMessage="Your license does not allow configuring remote cluster privileges"
|
||||
/>
|
||||
}
|
||||
position="right"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiButtonIcon } from '@elastic/eui';
|
||||
import type { EuiComboBoxProps } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
import '@kbn/code-editor-mock/jest_helper';
|
||||
import { mountWithIntl, shallowWithIntl } from '@kbn/test-jest-helpers';
|
||||
|
||||
import { RemoteClusterPrivilegesForm } from './remote_cluster_privileges_form';
|
||||
import { RoleValidator } from '../../validate_role';
|
||||
|
||||
test('it renders without crashing', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<RemoteClusterPrivilegesForm
|
||||
remoteClusterPrivilege={{
|
||||
clusters: ['cluster1'],
|
||||
privileges: ['monitor_enrich'],
|
||||
}}
|
||||
formIndex={0}
|
||||
availableRemoteClusterPrivileges={['monitor_enrich']}
|
||||
isRoleReadOnly={false}
|
||||
validator={new RoleValidator()}
|
||||
onChange={jest.fn()}
|
||||
onDelete={jest.fn()}
|
||||
/>
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('it allows for custom remote cluster input', () => {
|
||||
const onChange = jest.fn();
|
||||
const wrapper = mountWithIntl(
|
||||
<RemoteClusterPrivilegesForm
|
||||
remoteClusterPrivilege={{
|
||||
clusters: ['cluster1'],
|
||||
privileges: ['monitor_enrich'],
|
||||
}}
|
||||
formIndex={0}
|
||||
availableRemoteClusterPrivileges={['monitor_enrich']}
|
||||
isRoleReadOnly={false}
|
||||
validator={new RoleValidator()}
|
||||
onChange={onChange}
|
||||
onDelete={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const privilegesSelect = wrapper.find(
|
||||
'EuiComboBox[data-test-subj="remoteClusterClustersInput0"]'
|
||||
);
|
||||
|
||||
(privilegesSelect.props() as any).onCreateOption('custom-cluster');
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ clusters: ['cluster1', 'custom-cluster'] })
|
||||
);
|
||||
});
|
||||
|
||||
test('it does not allow for custom remote cluster privileges', () => {
|
||||
const onChange = jest.fn();
|
||||
const wrapper = mountWithIntl(
|
||||
<RemoteClusterPrivilegesForm
|
||||
remoteClusterPrivilege={{
|
||||
clusters: ['cluster1'],
|
||||
privileges: ['monitor_enrich'],
|
||||
}}
|
||||
formIndex={0}
|
||||
availableRemoteClusterPrivileges={['monitor_enrich']}
|
||||
isRoleReadOnly={false}
|
||||
validator={new RoleValidator()}
|
||||
onChange={onChange}
|
||||
onDelete={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const privilegesSelect = wrapper.find(
|
||||
'EuiComboBox[data-test-subj="remoteClusterPrivilegesInput0"]'
|
||||
);
|
||||
|
||||
expect((privilegesSelect.props() as EuiComboBoxProps<unknown>).onCreateOption).toBe(undefined);
|
||||
});
|
||||
|
||||
test('it allows for custom remote cluster clusters input', () => {
|
||||
const onChange = jest.fn();
|
||||
const wrapper = mountWithIntl(
|
||||
<RemoteClusterPrivilegesForm
|
||||
remoteClusterPrivilege={{
|
||||
clusters: ['cluster1'],
|
||||
privileges: ['monitor_enrich'],
|
||||
}}
|
||||
formIndex={0}
|
||||
availableRemoteClusterPrivileges={['monitor_enrich']}
|
||||
isRoleReadOnly={false}
|
||||
validator={new RoleValidator()}
|
||||
onChange={onChange}
|
||||
onDelete={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const clustersSelect = wrapper.find('EuiComboBox[data-test-subj="remoteClusterClustersInput0"]');
|
||||
|
||||
(clustersSelect.props() as any).onCreateOption('cluster2');
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ clusters: ['cluster1', 'cluster2'] })
|
||||
);
|
||||
});
|
||||
|
||||
test('it renders fields as disabled when isRoleReadOnly is true', () => {
|
||||
const onChange = jest.fn();
|
||||
const wrapper = mountWithIntl(
|
||||
<RemoteClusterPrivilegesForm
|
||||
remoteClusterPrivilege={{
|
||||
clusters: ['cluster1'],
|
||||
privileges: ['monitor_enrich'],
|
||||
}}
|
||||
formIndex={0}
|
||||
availableRemoteClusterPrivileges={['monitor_enrich']}
|
||||
isRoleReadOnly={true}
|
||||
validator={new RoleValidator()}
|
||||
onChange={onChange}
|
||||
onDelete={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const privilegesSelect = wrapper.find(
|
||||
'EuiComboBox[data-test-subj="remoteClusterPrivilegesInput0"]'
|
||||
);
|
||||
expect(privilegesSelect.prop('isDisabled')).toBe(true);
|
||||
|
||||
const clustersSelect = wrapper.find('EuiComboBox[data-test-subj="remoteClusterClustersInput0"]');
|
||||
expect(clustersSelect.prop('isDisabled')).toBe(true);
|
||||
});
|
||||
|
||||
describe('delete button', () => {
|
||||
const props = {
|
||||
remoteClusterPrivilege: {
|
||||
clusters: ['cluster1'],
|
||||
privileges: ['monitor_enrich'],
|
||||
},
|
||||
formIndex: 0,
|
||||
availableRemoteClusterPrivileges: ['monitor_enrich'],
|
||||
isRoleReadOnly: false,
|
||||
validator: new RoleValidator(),
|
||||
onChange: jest.fn(),
|
||||
onDelete: jest.fn(),
|
||||
intl: {} as any,
|
||||
};
|
||||
|
||||
test('it is hidden when isRoleReadOnly is true', () => {
|
||||
const testProps = {
|
||||
...props,
|
||||
isRoleReadOnly: true,
|
||||
};
|
||||
const wrapper = mountWithIntl(<RemoteClusterPrivilegesForm {...testProps} />);
|
||||
expect(wrapper.find(EuiButtonIcon)).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('it is shown when isRoleReadOnly is false', () => {
|
||||
const testProps = {
|
||||
...props,
|
||||
isRoleReadOnly: false,
|
||||
};
|
||||
const wrapper = mountWithIntl(<RemoteClusterPrivilegesForm {...testProps} />);
|
||||
expect(wrapper.find(EuiButtonIcon)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('it invokes onDelete when clicked', () => {
|
||||
const testProps = {
|
||||
...props,
|
||||
isRoleReadOnly: false,
|
||||
};
|
||||
const wrapper = mountWithIntl(<RemoteClusterPrivilegesForm {...testProps} />);
|
||||
wrapper.find(EuiButtonIcon).simulate('click');
|
||||
expect(testProps.onDelete).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiComboBox,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import React, { Fragment, useCallback } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { Cluster } from '@kbn/remote-clusters-plugin/public';
|
||||
|
||||
import { RemoteClusterComboBox } from './remote_clusters_combo_box';
|
||||
import type { RoleRemoteClusterPrivilege } from '../../../../../../common';
|
||||
import type { RoleValidator } from '../../validate_role';
|
||||
|
||||
const fromOption = (option: EuiComboBoxOptionOption) => option.label;
|
||||
const toOption = (value: string): EuiComboBoxOptionOption => ({ label: value });
|
||||
|
||||
interface Props {
|
||||
formIndex: number;
|
||||
remoteClusterPrivilege: RoleRemoteClusterPrivilege;
|
||||
remoteClusters?: Cluster[];
|
||||
availableRemoteClusterPrivileges: string[];
|
||||
onChange: (remoteClusterPrivilege: RoleRemoteClusterPrivilege) => void;
|
||||
onDelete: () => void;
|
||||
isRoleReadOnly: boolean;
|
||||
validator: RoleValidator;
|
||||
}
|
||||
|
||||
export const RemoteClusterPrivilegesForm: React.FunctionComponent<Props> = ({
|
||||
isRoleReadOnly,
|
||||
remoteClusters = [],
|
||||
formIndex,
|
||||
validator,
|
||||
remoteClusterPrivilege,
|
||||
availableRemoteClusterPrivileges,
|
||||
onChange,
|
||||
onDelete,
|
||||
}) => {
|
||||
const onCreateClusterOption = useCallback(
|
||||
(option: string) => {
|
||||
const nextClusters = (remoteClusterPrivilege.clusters ?? []).concat([option]);
|
||||
|
||||
onChange({
|
||||
...remoteClusterPrivilege,
|
||||
clusters: nextClusters,
|
||||
});
|
||||
},
|
||||
[remoteClusterPrivilege, onChange]
|
||||
);
|
||||
|
||||
const onClustersChange = useCallback(
|
||||
(nextOptions: EuiComboBoxOptionOption[]) => {
|
||||
const clusters = nextOptions.map(fromOption);
|
||||
onChange({
|
||||
...remoteClusterPrivilege,
|
||||
clusters,
|
||||
});
|
||||
},
|
||||
[onChange, remoteClusterPrivilege]
|
||||
);
|
||||
|
||||
const onPrivilegeChange = useCallback(
|
||||
(newPrivileges: EuiComboBoxOptionOption[]) => {
|
||||
onChange({
|
||||
...remoteClusterPrivilege,
|
||||
privileges: newPrivileges.map(fromOption),
|
||||
});
|
||||
},
|
||||
[remoteClusterPrivilege, onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
responsive={false}
|
||||
className="remote-cluster-privilege-form"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiPanel color="subdued">
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.remoteClusterPrivilegeForm.clustersFormRowLabel"
|
||||
defaultMessage="Remote clusters"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
{...validator.validateRemoteClusterPrivilegeClusterField(remoteClusterPrivilege)}
|
||||
>
|
||||
<RemoteClusterComboBox
|
||||
data-test-subj={`remoteClusterClustersInput${formIndex}`}
|
||||
selectedOptions={(remoteClusterPrivilege.clusters ?? []).map(toOption)}
|
||||
onCreateOption={onCreateClusterOption}
|
||||
onChange={onClustersChange}
|
||||
isDisabled={isRoleReadOnly}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.security.management.editRole.remoteClusterPrivilegeForm.clustersPlaceholder',
|
||||
{ defaultMessage: 'Add a remote cluster…' }
|
||||
)}
|
||||
remoteClusters={remoteClusters}
|
||||
type="remote_cluster"
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.remoteClusterPrivilegeForm.privilegesFormRowLabel"
|
||||
defaultMessage="Privileges"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
{...validator.validateRemoteClusterPrivilegePrivilegesField(
|
||||
remoteClusterPrivilege
|
||||
)}
|
||||
>
|
||||
<EuiComboBox
|
||||
data-test-subj={`remoteClusterPrivilegesInput${formIndex}`}
|
||||
options={availableRemoteClusterPrivileges.map(toOption)}
|
||||
selectedOptions={remoteClusterPrivilege.privileges.map(toOption)}
|
||||
onChange={onPrivilegeChange}
|
||||
isDisabled={isRoleReadOnly}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.security.management.editRole.remoteClusterPrivilegeForm.privilegesPlaceholder',
|
||||
{ defaultMessage: 'Add an action…' }
|
||||
)}
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
{!isRoleReadOnly && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate(
|
||||
'xpack.security.management.editRole.remoteClusterPrivilegeForm.deleteRemoteClusterPrivilegeAriaLabel',
|
||||
{ defaultMessage: 'Delete remote cluster privilege' }
|
||||
)}
|
||||
color="danger"
|
||||
onClick={onDelete}
|
||||
iconType="trash"
|
||||
data-test-subj={`deleteRemoteClusterPrivilegesButton${formIndex}`}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import '@kbn/code-editor-mock/jest_helper';
|
||||
import { shallowWithIntl } from '@kbn/test-jest-helpers';
|
||||
|
||||
import { RemoteClusterComboBox } from './remote_clusters_combo_box';
|
||||
|
||||
test('it renders without crashing', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<RemoteClusterComboBox
|
||||
type="remote_cluster"
|
||||
dat-test-subj="remoteClusterClustersInput0"
|
||||
remoteClusters={[
|
||||
{
|
||||
name: 'test1',
|
||||
mode: 'proxy',
|
||||
isConnected: false,
|
||||
initialConnectTimeout: '30s',
|
||||
skipUnavailable: false,
|
||||
proxyAddress: 'localhost:9400',
|
||||
proxySocketConnections: 18,
|
||||
connectedSocketsCount: 0,
|
||||
serverName: 'localhost',
|
||||
securityModel: 'certificate',
|
||||
},
|
||||
{
|
||||
name: 'test2',
|
||||
mode: 'proxy',
|
||||
isConnected: false,
|
||||
initialConnectTimeout: '30s',
|
||||
skipUnavailable: false,
|
||||
proxyAddress: 'localhost:9400',
|
||||
proxySocketConnections: 18,
|
||||
connectedSocketsCount: 0,
|
||||
serverName: 'localhost',
|
||||
securityModel: 'api_key',
|
||||
},
|
||||
{
|
||||
name: 'test3',
|
||||
mode: 'proxy',
|
||||
isConnected: false,
|
||||
initialConnectTimeout: '30s',
|
||||
skipUnavailable: false,
|
||||
proxyAddress: 'localhost:9400',
|
||||
proxySocketConnections: 18,
|
||||
connectedSocketsCount: 0,
|
||||
serverName: 'localhost',
|
||||
securityModel: 'api_key',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should render clusters field', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<RemoteClusterComboBox
|
||||
onChange={jest.fn()}
|
||||
type="remote_cluster"
|
||||
remoteClusters={[
|
||||
{
|
||||
name: 'test1',
|
||||
mode: 'proxy',
|
||||
isConnected: false,
|
||||
initialConnectTimeout: '30s',
|
||||
skipUnavailable: false,
|
||||
proxyAddress: 'localhost:9400',
|
||||
proxySocketConnections: 18,
|
||||
connectedSocketsCount: 0,
|
||||
serverName: 'localhost',
|
||||
securityModel: 'certificate',
|
||||
},
|
||||
{
|
||||
name: 'test2',
|
||||
mode: 'proxy',
|
||||
isConnected: false,
|
||||
initialConnectTimeout: '30s',
|
||||
skipUnavailable: false,
|
||||
proxyAddress: 'localhost:9400',
|
||||
proxySocketConnections: 18,
|
||||
connectedSocketsCount: 0,
|
||||
serverName: 'localhost',
|
||||
securityModel: 'api_key',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
const clustersInput = wrapper.find('EuiComboBox');
|
||||
expect(clustersInput.prop('options')).toEqual([
|
||||
{ label: 'test2' },
|
||||
{ label: expect.anything(), isGroupLabelOption: true },
|
||||
{
|
||||
label: 'test1',
|
||||
disabled: true,
|
||||
append: expect.anything(),
|
||||
},
|
||||
]);
|
||||
});
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { EuiComboBoxOptionOption, EuiComboBoxProps } from '@elastic/eui';
|
||||
import { EuiComboBox, EuiIconTip } from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { Cluster } from '@kbn/remote-clusters-plugin/public';
|
||||
|
||||
const API_KEY_SECURITY_MODEL = 'api_key';
|
||||
|
||||
interface Props extends Omit<EuiComboBoxProps<string | number | string[] | undefined>, 'options'> {
|
||||
remoteClusters: Cluster[];
|
||||
type: 'remote_cluster' | 'remote_indexes';
|
||||
}
|
||||
|
||||
export const RemoteClusterComboBox: React.FunctionComponent<Props> = ({
|
||||
remoteClusters,
|
||||
type,
|
||||
...restProps
|
||||
}) => {
|
||||
const remoteClusterOptions = useMemo<EuiComboBoxOptionOption[]>(() => {
|
||||
const { incompatible, remote } = remoteClusters.reduce<{
|
||||
remote: EuiComboBoxOptionOption[];
|
||||
incompatible: EuiComboBoxOptionOption[];
|
||||
}>(
|
||||
(data, item) => {
|
||||
const disabled = item.securityModel !== API_KEY_SECURITY_MODEL;
|
||||
|
||||
if (!disabled) {
|
||||
data.remote.push({ label: item.name });
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
data.incompatible.push({
|
||||
label: item.name,
|
||||
disabled,
|
||||
append: disabled ? (
|
||||
<EuiIconTip
|
||||
type="warning"
|
||||
color="inherit"
|
||||
content={
|
||||
type === 'remote_cluster' ? (
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.remoteClusterPrivilegeForm.remoteClusterSecurityModelWarning"
|
||||
defaultMessage="This cluster is configured with the certificate based security model and does not support remote cluster privileges. Connect this cluster with the API key based security model instead to use remote cluster privileges."
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.editRole.indexPrivilegeForm.remoteIndicesSecurityModelWarning"
|
||||
defaultMessage="This cluster is configured with the certificate based security model and does not support remote index privileges. Connect this cluster with the API key based security model instead to use remote index privileges."
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : undefined,
|
||||
});
|
||||
|
||||
return data;
|
||||
},
|
||||
{
|
||||
incompatible: [],
|
||||
remote: [],
|
||||
}
|
||||
);
|
||||
|
||||
if (incompatible.length) {
|
||||
remote.push(
|
||||
{
|
||||
label: 'Incompatible clusters',
|
||||
isGroupLabelOption: true,
|
||||
},
|
||||
...incompatible
|
||||
);
|
||||
}
|
||||
|
||||
return remote;
|
||||
}, [remoteClusters, type]);
|
||||
|
||||
return <EuiComboBox {...restProps} options={remoteClusterOptions} />;
|
||||
};
|
|
@ -7,7 +7,12 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import type { Role, RoleIndexPrivilege, RoleRemoteIndexPrivilege } from '../../../../common';
|
||||
import type {
|
||||
Role,
|
||||
RoleIndexPrivilege,
|
||||
RoleRemoteClusterPrivilege,
|
||||
RoleRemoteIndexPrivilege,
|
||||
} from '../../../../common';
|
||||
import { MAX_NAME_LENGTH, NAME_REGEX } from '../../../../common/constants';
|
||||
|
||||
interface RoleValidatorOptions {
|
||||
|
@ -81,6 +86,27 @@ export class RoleValidator {
|
|||
return valid();
|
||||
}
|
||||
|
||||
public validateRemoteClusterPrivileges(role: Role): RoleValidationResult {
|
||||
if (!this.shouldValidate) {
|
||||
return valid();
|
||||
}
|
||||
|
||||
const areRemoteClustersInvalid = role.elasticsearch.remote_cluster?.some(
|
||||
(remoteClusterPrivilege) => {
|
||||
return (
|
||||
this.validateRemoteClusterPrivilegeClusterField(remoteClusterPrivilege).isInvalid ||
|
||||
this.validateRemoteClusterPrivilegePrivilegesField(remoteClusterPrivilege).isInvalid
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
if (areRemoteClustersInvalid) {
|
||||
return invalid();
|
||||
}
|
||||
|
||||
return valid();
|
||||
}
|
||||
|
||||
public validateIndexPrivileges(role: Role): RoleValidationResult {
|
||||
if (!this.shouldValidate) {
|
||||
return valid();
|
||||
|
@ -239,6 +265,58 @@ export class RoleValidator {
|
|||
return valid();
|
||||
}
|
||||
|
||||
public validateRemoteClusterPrivilegeClusterField(
|
||||
remoteClusterPrivilege: RoleRemoteClusterPrivilege
|
||||
): RoleValidationResult {
|
||||
if (!this.shouldValidate) {
|
||||
return valid();
|
||||
}
|
||||
|
||||
// Ignore if all other fields are empty
|
||||
if (!remoteClusterPrivilege.privileges.length) {
|
||||
return valid();
|
||||
}
|
||||
|
||||
if (!remoteClusterPrivilege.clusters.length) {
|
||||
return invalid(
|
||||
i18n.translate(
|
||||
'xpack.security.management.editRole.validateRole.oneClusterRequiredWarningMessage',
|
||||
{
|
||||
defaultMessage: 'Enter or select at least one cluster',
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return valid();
|
||||
}
|
||||
|
||||
public validateRemoteClusterPrivilegePrivilegesField(
|
||||
remoteClusterPrivilege: RoleRemoteClusterPrivilege
|
||||
): RoleValidationResult {
|
||||
if (!this.shouldValidate) {
|
||||
return valid();
|
||||
}
|
||||
|
||||
// Ignore if all other fields are empty
|
||||
if (!remoteClusterPrivilege.clusters.length) {
|
||||
return valid();
|
||||
}
|
||||
|
||||
if (!remoteClusterPrivilege.privileges.length) {
|
||||
return invalid(
|
||||
i18n.translate(
|
||||
'xpack.security.management.editRole.validateRole.oneRemoteClusterPrivilegeRequiredWarningMessage',
|
||||
{
|
||||
defaultMessage: 'Enter or select at least one privilege',
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return valid();
|
||||
}
|
||||
|
||||
public validateSelectedSpaces(
|
||||
spaceIds: string[],
|
||||
privilege: string | null
|
||||
|
@ -313,12 +391,15 @@ export class RoleValidator {
|
|||
const { isInvalid: areIndicesInvalid } = this.validateIndexPrivileges(role);
|
||||
const { isInvalid: areRemoteIndicesInvalid } = this.validateRemoteIndexPrivileges(role);
|
||||
const { isInvalid: areSpacePrivilegesInvalid } = this.validateSpacePrivileges(role);
|
||||
const { isInvalid: areRemoteClusterPrivilegesInvalid } =
|
||||
this.validateRemoteClusterPrivileges(role);
|
||||
|
||||
if (
|
||||
isNameInvalid ||
|
||||
areIndicesInvalid ||
|
||||
areRemoteIndicesInvalid ||
|
||||
areSpacePrivilegesInvalid
|
||||
areSpacePrivilegesInvalid ||
|
||||
areRemoteClusterPrivilegesInvalid
|
||||
) {
|
||||
return invalid();
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ const roles = [
|
|||
{
|
||||
name: 'global-base-all',
|
||||
cluster: [],
|
||||
remote_cluster: [],
|
||||
indices: [],
|
||||
applications: [
|
||||
{
|
||||
|
@ -33,6 +34,7 @@ const roles = [
|
|||
{
|
||||
name: 'global-base-read',
|
||||
cluster: [],
|
||||
remote_cluster: [],
|
||||
indices: [],
|
||||
applications: [
|
||||
{
|
||||
|
@ -50,6 +52,7 @@ const roles = [
|
|||
{
|
||||
name: 'global-foo-all',
|
||||
cluster: [],
|
||||
remote_cluster: [],
|
||||
indices: [],
|
||||
applications: [
|
||||
{
|
||||
|
@ -67,6 +70,7 @@ const roles = [
|
|||
{
|
||||
name: 'global-foo-read',
|
||||
cluster: [],
|
||||
remote_cluster: [],
|
||||
indices: [],
|
||||
applications: [
|
||||
{
|
||||
|
@ -84,6 +88,7 @@ const roles = [
|
|||
{
|
||||
name: 'global-malformed',
|
||||
cluster: [],
|
||||
remote_cluster: [],
|
||||
indices: [],
|
||||
applications: [
|
||||
{
|
||||
|
@ -101,6 +106,7 @@ const roles = [
|
|||
{
|
||||
name: 'default-base-all',
|
||||
cluster: [],
|
||||
remote_cluster: [],
|
||||
indices: [],
|
||||
applications: [
|
||||
{
|
||||
|
@ -118,6 +124,7 @@ const roles = [
|
|||
{
|
||||
name: 'default-base-read',
|
||||
cluster: [],
|
||||
remote_cluster: [],
|
||||
indices: [],
|
||||
applications: [
|
||||
{
|
||||
|
@ -135,6 +142,7 @@ const roles = [
|
|||
{
|
||||
name: 'default-foo-all',
|
||||
cluster: [],
|
||||
remote_cluster: [],
|
||||
indices: [],
|
||||
applications: [
|
||||
{
|
||||
|
@ -152,6 +160,7 @@ const roles = [
|
|||
{
|
||||
name: 'default-foo-read',
|
||||
cluster: [],
|
||||
remote_cluster: [],
|
||||
indices: [],
|
||||
applications: [
|
||||
{
|
||||
|
@ -169,6 +178,7 @@ const roles = [
|
|||
{
|
||||
name: 'default-malformed',
|
||||
cluster: [],
|
||||
remote_cluster: [],
|
||||
indices: [],
|
||||
applications: [
|
||||
{
|
||||
|
@ -294,6 +304,7 @@ describe('#transformElasticsearchRoleToRole', () => {
|
|||
const role = {
|
||||
name: 'global-all',
|
||||
cluster: [],
|
||||
remote_cluster: [],
|
||||
indices: [],
|
||||
applications: [
|
||||
{
|
||||
|
|
|
@ -28,6 +28,7 @@ export type ElasticsearchRole = Pick<
|
|||
resources: string[];
|
||||
}>;
|
||||
cluster: Role['elasticsearch']['cluster'];
|
||||
remote_cluster: Role['elasticsearch']['remote_cluster'];
|
||||
indices: Role['elasticsearch']['indices'];
|
||||
remote_indices?: Role['elasticsearch']['remote_indices'];
|
||||
run_as: Role['elasticsearch']['run_as'];
|
||||
|
@ -56,6 +57,7 @@ export function transformElasticsearchRoleToRole(
|
|||
transient_metadata: elasticsearchRole.transient_metadata,
|
||||
elasticsearch: {
|
||||
cluster: elasticsearchRole.cluster,
|
||||
remote_cluster: elasticsearchRole.remote_cluster,
|
||||
indices: elasticsearchRole.indices,
|
||||
remote_indices: elasticsearchRole.remote_indices,
|
||||
run_as: elasticsearchRole.run_as,
|
||||
|
|
|
@ -66,6 +66,7 @@ describe('#getPrivilegeDeprecationsService', () => {
|
|||
"elasticsearch": Object {
|
||||
"cluster": Array [],
|
||||
"indices": Array [],
|
||||
"remote_cluster": undefined,
|
||||
"remote_indices": undefined,
|
||||
"run_as": Array [],
|
||||
},
|
||||
|
@ -139,6 +140,7 @@ describe('#getPrivilegeDeprecationsService', () => {
|
|||
"elasticsearch": Object {
|
||||
"cluster": Array [],
|
||||
"indices": Array [],
|
||||
"remote_cluster": undefined,
|
||||
"remote_indices": undefined,
|
||||
"run_as": Array [],
|
||||
},
|
||||
|
|
|
@ -21,7 +21,10 @@ export function defineGetBuiltinPrivilegesRoutes({ router }: RouteDefinitionPara
|
|||
: [privileges.index];
|
||||
privileges.index = indexPriviledges.filter((privilege) => privilege !== 'none');
|
||||
|
||||
return response.ok({ body: privileges });
|
||||
// TODO: remove hardcoded value once ES returns built-in privileges for remote_cluster
|
||||
const remoteClusterPrivileges = ['monitor_enrich'];
|
||||
|
||||
return response.ok({ body: { ...privileges, remote_cluster: remoteClusterPrivileges } });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -108,10 +108,10 @@ describe('Put payload schema', () => {
|
|||
kibana: [{ spaces: ['foo-*'] }],
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
"[kibana.0.spaces]: types that failed validation:
|
||||
- [kibana.0.spaces.0.0]: expected value to equal [*]
|
||||
- [kibana.0.spaces.1.0]: must be lower case, a-z, 0-9, '_', and '-' are allowed"
|
||||
`);
|
||||
"[kibana.0.spaces]: types that failed validation:
|
||||
- [kibana.0.spaces.0.0]: expected value to equal [*]
|
||||
- [kibana.0.spaces.1.0]: must be lower case, a-z, 0-9, '_', and '-' are allowed"
|
||||
`);
|
||||
});
|
||||
|
||||
test(`can't assign space and global in same entry`, () => {
|
||||
|
@ -120,10 +120,10 @@ describe('Put payload schema', () => {
|
|||
kibana: [{ spaces: ['*', 'foo-space'] }],
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
"[kibana.0.spaces]: types that failed validation:
|
||||
- [kibana.0.spaces.0.1]: expected value to equal [*]
|
||||
- [kibana.0.spaces.1.0]: must be lower case, a-z, 0-9, '_', and '-' are allowed"
|
||||
`);
|
||||
"[kibana.0.spaces]: types that failed validation:
|
||||
- [kibana.0.spaces.0.1]: expected value to equal [*]
|
||||
- [kibana.0.spaces.1.0]: must be lower case, a-z, 0-9, '_', and '-' are allowed"
|
||||
`);
|
||||
});
|
||||
|
||||
test(`only allows known Kibana space base privileges`, () => {
|
||||
|
@ -424,8 +424,72 @@ describe('Put payload schema', () => {
|
|||
`);
|
||||
});
|
||||
|
||||
test('passes through remote_cluster when specified', () => {
|
||||
expect(
|
||||
getPutPayloadSchema(() => basePrivilegeNamesMap).validate({
|
||||
elasticsearch: {
|
||||
remote_cluster: [
|
||||
{
|
||||
privileges: ['monitor_enrich'],
|
||||
clusters: ['my_remote*'],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"elasticsearch": Object {
|
||||
"remote_cluster": Array [
|
||||
Object {
|
||||
"clusters": Array [
|
||||
"my_remote*",
|
||||
],
|
||||
"privileges": Array [
|
||||
"monitor_enrich",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test(`doesn't allow empty privilege for remote_cluster`, () => {
|
||||
expect(() =>
|
||||
getPutPayloadSchema(() => basePrivilegeNamesMap).validate({
|
||||
elasticsearch: {
|
||||
remote_cluster: [
|
||||
{
|
||||
privileges: [],
|
||||
clusters: ['cluster1'],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[elasticsearch.remote_cluster.0.privileges]: array size is [0], but cannot be smaller than [1]"`
|
||||
);
|
||||
});
|
||||
|
||||
test(`doesn't allow empty clusters for remote_cluster`, () => {
|
||||
expect(() =>
|
||||
getPutPayloadSchema(() => basePrivilegeNamesMap).validate({
|
||||
elasticsearch: {
|
||||
remote_cluster: [
|
||||
{
|
||||
privileges: ['enrich_monitor'],
|
||||
clusters: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[elasticsearch.remote_cluster.0.clusters]: array size is [0], but cannot be smaller than [1]"`
|
||||
);
|
||||
});
|
||||
|
||||
// This is important for backwards compatibility
|
||||
test('does not set default value for remote_indices when not specified', () => {
|
||||
test('does not set default value for remote_indices/remote_cluster when not specified', () => {
|
||||
expect(getPutPayloadSchema(() => basePrivilegeNamesMap).validate({})).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"elasticsearch": Object {},
|
||||
|
|
|
@ -19,6 +19,7 @@ export const transformPutPayloadToElasticsearchRole = (
|
|||
) => {
|
||||
const {
|
||||
elasticsearch = {
|
||||
remote_cluster: undefined,
|
||||
cluster: undefined,
|
||||
indices: undefined,
|
||||
remote_indices: undefined,
|
||||
|
@ -34,6 +35,7 @@ export const transformPutPayloadToElasticsearchRole = (
|
|||
...(rolePayload.description && { description: rolePayload.description }),
|
||||
metadata: rolePayload.metadata,
|
||||
cluster: elasticsearch.cluster || [],
|
||||
remote_cluster: elasticsearch.remote_cluster,
|
||||
indices: elasticsearch.indices || [],
|
||||
remote_indices: elasticsearch.remote_indices,
|
||||
run_as: elasticsearch.run_as || [],
|
||||
|
|
|
@ -325,6 +325,7 @@ describe('PUT role', () => {
|
|||
body: {
|
||||
cluster: [],
|
||||
indices: [],
|
||||
remote_cluster: undefined,
|
||||
remote_indices: undefined,
|
||||
run_as: [],
|
||||
applications: [],
|
||||
|
@ -936,5 +937,59 @@ describe('PUT role', () => {
|
|||
result: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
putRoleTest(`creates role with remote_cluster privileges`, {
|
||||
name: 'foo-role-remote-cluster',
|
||||
payload: {
|
||||
kibana: [],
|
||||
elasticsearch: {
|
||||
remote_cluster: [
|
||||
{
|
||||
clusters: ['cluster1', 'cluster2'],
|
||||
privileges: ['monitor_enrich'],
|
||||
},
|
||||
{
|
||||
clusters: ['cluster3', 'cluster4'],
|
||||
privileges: ['monitor_enrich'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
apiResponses: {
|
||||
get: () => ({}),
|
||||
put: () => {},
|
||||
},
|
||||
asserts: {
|
||||
recordSubFeaturePrivilegeUsage: false,
|
||||
apiArguments: {
|
||||
get: [{ name: 'foo-role-remote-cluster' }, { ignore: [404] }],
|
||||
put: [
|
||||
{
|
||||
name: 'foo-role-remote-cluster',
|
||||
body: {
|
||||
applications: [],
|
||||
cluster: [],
|
||||
indices: [],
|
||||
remote_indices: undefined,
|
||||
run_as: [],
|
||||
remote_cluster: [
|
||||
{
|
||||
clusters: ['cluster1', 'cluster2'],
|
||||
privileges: ['monitor_enrich'],
|
||||
},
|
||||
{
|
||||
clusters: ['cluster3', 'cluster4'],
|
||||
privileges: ['monitor_enrich'],
|
||||
},
|
||||
],
|
||||
metadata: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
statusCode: 204,
|
||||
result: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -98,6 +98,7 @@ describe('GET role mappings feature check', () => {
|
|||
canUseStoredScripts: true,
|
||||
hasCompatibleRealms: true,
|
||||
canUseRemoteIndices: true,
|
||||
canUseRemoteClusters: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -122,6 +123,7 @@ describe('GET role mappings feature check', () => {
|
|||
canUseStoredScripts: true,
|
||||
hasCompatibleRealms: true,
|
||||
canUseRemoteIndices: true,
|
||||
canUseRemoteClusters: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -141,6 +143,7 @@ describe('GET role mappings feature check', () => {
|
|||
canUseStoredScripts: true,
|
||||
hasCompatibleRealms: true,
|
||||
canUseRemoteIndices: false,
|
||||
canUseRemoteClusters: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -166,6 +169,7 @@ describe('GET role mappings feature check', () => {
|
|||
canUseStoredScripts: false,
|
||||
hasCompatibleRealms: true,
|
||||
canUseRemoteIndices: true,
|
||||
canUseRemoteClusters: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -190,6 +194,7 @@ describe('GET role mappings feature check', () => {
|
|||
canUseStoredScripts: true,
|
||||
hasCompatibleRealms: true,
|
||||
canUseRemoteIndices: true,
|
||||
canUseRemoteClusters: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -218,6 +223,7 @@ describe('GET role mappings feature check', () => {
|
|||
canUseStoredScripts: true,
|
||||
hasCompatibleRealms: false,
|
||||
canUseRemoteIndices: true,
|
||||
canUseRemoteClusters: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -249,6 +255,7 @@ describe('GET role mappings feature check', () => {
|
|||
canUseStoredScripts: true,
|
||||
hasCompatibleRealms: false,
|
||||
canUseRemoteIndices: false,
|
||||
canUseRemoteClusters: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -131,7 +131,8 @@ async function getEnabledRoleMappingsFeatures(esClient: ElasticsearchClient, log
|
|||
hasCompatibleRealms,
|
||||
canUseStoredScripts,
|
||||
canUseInlineScripts,
|
||||
canUseRemoteIndices: !!xpackUsage.remote_clusters,
|
||||
canUseRemoteIndices: Boolean(xpackUsage.remote_clusters),
|
||||
canUseRemoteClusters: Boolean(xpackUsage.remote_clusters),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -167,6 +167,7 @@ describe('Login view routes', () => {
|
|||
allowRoleDocumentLevelSecurity: true,
|
||||
allowRoleFieldLevelSecurity: false,
|
||||
allowRoleRemoteIndexPrivileges: false,
|
||||
allowRemoteClusterPrivileges: false,
|
||||
layout: 'error-es-unavailable',
|
||||
showLinks: false,
|
||||
showRoleMappingsManagement: true,
|
||||
|
|
|
@ -24,7 +24,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
const sampleOfExpectedIndexPrivileges = ['create', 'index', 'delete'];
|
||||
|
||||
const payload = response.body;
|
||||
expect(Object.keys(payload).sort()).to.eql(['cluster', 'index']);
|
||||
expect(Object.keys(payload).sort()).to.eql(['cluster', 'index', 'remote_cluster']);
|
||||
|
||||
sampleOfExpectedClusterPrivileges.forEach((privilege) =>
|
||||
expect(payload.cluster).to.contain(privilege)
|
||||
|
|
|
@ -17,5 +17,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./field_level_security'));
|
||||
loadTestFile(require.resolve('./user_email'));
|
||||
loadTestFile(require.resolve('./role_mappings'));
|
||||
loadTestFile(require.resolve('./remote_cluster_security_roles'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { keyBy } from 'lodash';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
const EDIT_ROLES_PATH = 'security/roles/edit';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const browser = getService('browser');
|
||||
const log = getService('log');
|
||||
const security = getService('security');
|
||||
const PageObjects = getPageObjects(['security', 'common', 'header', 'discover', 'settings']);
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
|
||||
describe('Remote Cluster Privileges', function () {
|
||||
const customRole = 'rc-custom-role';
|
||||
|
||||
before('initialize tests', async () => {
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/security/dlstest');
|
||||
await browser.setWindowSize(1600, 1000);
|
||||
|
||||
await PageObjects.common.navigateToApp('settings');
|
||||
await PageObjects.settings.createIndexPattern('dlstest', null);
|
||||
|
||||
await security.testUser.setRoles(['cluster_security_manager', 'kibana_admin']);
|
||||
await PageObjects.settings.navigateTo();
|
||||
await PageObjects.security.clickElasticsearchRoles();
|
||||
});
|
||||
|
||||
it(`should add new role ${customRole} with remote cluster privileges`, async function () {
|
||||
await PageObjects.security.addRole(customRole, {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['dlstest'],
|
||||
privileges: ['read', 'view_index_metadata'],
|
||||
},
|
||||
],
|
||||
remote_cluster: [
|
||||
{
|
||||
clusters: ['cluster1', 'cluster2'],
|
||||
privileges: ['monitor_enrich'],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const roles = keyBy(await PageObjects.security.getElasticsearchRoles(), 'rolename');
|
||||
log.debug('actualRoles = %j', roles);
|
||||
expect(roles).to.have.key(customRole);
|
||||
expect(roles[customRole].reserved).to.be(false);
|
||||
});
|
||||
|
||||
it(`should update role ${customRole} with remote cluster privileges`, async function () {
|
||||
await PageObjects.settings.clickLinkText(customRole);
|
||||
const currentUrl = await browser.getCurrentUrl();
|
||||
|
||||
expect(currentUrl).to.contain(EDIT_ROLES_PATH);
|
||||
|
||||
const { clusters: currentClusters, privileges: currentPrivileges } =
|
||||
await PageObjects.security.getRemoteClusterPrivilege(0);
|
||||
|
||||
expect(currentClusters).to.eql(['cluster1', 'cluster2']);
|
||||
expect(currentPrivileges).to.eql(['monitor_enrich']);
|
||||
|
||||
await PageObjects.security.deleteRemoteClusterPrivilege(0);
|
||||
|
||||
await PageObjects.security.addRemoteClusterPrivilege({
|
||||
clusters: ['cluster3', 'cluster4'],
|
||||
privileges: ['monitor_enrich'],
|
||||
});
|
||||
|
||||
await PageObjects.security.saveRole();
|
||||
});
|
||||
|
||||
after('logout', async () => {
|
||||
// NOTE: Logout needs to happen before anything else to avoid flaky behavior
|
||||
await PageObjects.security.forceLogout();
|
||||
await security.role.delete(customRole);
|
||||
await security.testUser.restoreDefaults();
|
||||
});
|
||||
});
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { adminTestUser } from '@kbn/test';
|
||||
import { AuthenticatedUser, Role } from '@kbn/security-plugin/common';
|
||||
import { AuthenticatedUser, Role, RoleRemoteClusterPrivilege } from '@kbn/security-plugin/common';
|
||||
import type { UserFormValues } from '@kbn/security-plugin/public/management/users/edit_user/user_form';
|
||||
import { Key } from 'selenium-webdriver';
|
||||
import { FtrService } from '../ftr_provider_context';
|
||||
|
@ -600,9 +600,54 @@ export class SecurityPageObject extends FtrService {
|
|||
return confirmText;
|
||||
}
|
||||
|
||||
async addRemoteClusterPrivilege(privilege: RoleRemoteClusterPrivilege, index = 0) {
|
||||
this.log.debug('addRemoteClusterPrivilege, index = ', index);
|
||||
|
||||
await this.testSubjects.click('addRemoteClusterPrivilegesButton');
|
||||
|
||||
for (const cluster of privilege.clusters) {
|
||||
await this.comboBox.setCustom(`remoteClusterClustersInput${index}`, cluster);
|
||||
}
|
||||
|
||||
for (const clusterPrivilege of privilege.privileges) {
|
||||
await this.comboBox.setCustom(`remoteClusterPrivilegesInput${index}`, clusterPrivilege);
|
||||
}
|
||||
}
|
||||
|
||||
async saveRole() {
|
||||
this.log.debug('click save button');
|
||||
await this.testSubjects.click('roleFormSaveButton');
|
||||
|
||||
// Signifies that the role management page redirected back to the role grid page,
|
||||
// and successfully refreshed the grid
|
||||
await this.testSubjects.existOrFail('roleRow');
|
||||
}
|
||||
|
||||
async deleteRemoteClusterPrivilege(index: number) {
|
||||
this.log.debug('deleteRemoteClusterPrivilege, index = ', index);
|
||||
|
||||
await this.testSubjects.click(`deleteRemoteClusterPrivilegesButton${index}`);
|
||||
}
|
||||
|
||||
async getRemoteClusterPrivilege(index: number) {
|
||||
this.log.debug('getRemoteClusterPrivilege, index = ', index);
|
||||
const clusterOptions = await this.comboBox.getComboBoxSelectedOptions(
|
||||
`remoteClusterClustersInput${index}`
|
||||
);
|
||||
|
||||
const privilegeOptions = await this.comboBox.getComboBoxSelectedOptions(
|
||||
`remoteClusterPrivilegesInput${index}`
|
||||
);
|
||||
|
||||
return {
|
||||
clusters: clusterOptions,
|
||||
privileges: privilegeOptions,
|
||||
};
|
||||
}
|
||||
|
||||
async addRole(
|
||||
roleName: string,
|
||||
roleObj: { elasticsearch: Pick<Role['elasticsearch'], 'indices'> }
|
||||
roleObj: { elasticsearch: Pick<Role['elasticsearch'], 'indices' | 'remote_cluster'> }
|
||||
) {
|
||||
const self = this;
|
||||
|
||||
|
@ -667,12 +712,18 @@ export class SecurityPageObject extends FtrService {
|
|||
await addGrantedField(roleObj.elasticsearch.indices[0].field_security!.grant!);
|
||||
}
|
||||
|
||||
this.log.debug('click save button');
|
||||
await this.testSubjects.click('roleFormSaveButton');
|
||||
if (roleObj.elasticsearch.remote_cluster) {
|
||||
this.log.debug('adding remote_cluster privileges');
|
||||
|
||||
// Signifies that the role management page redirected back to the role grid page,
|
||||
// and successfully refreshed the grid
|
||||
await this.testSubjects.existOrFail('roleRow');
|
||||
for (const [
|
||||
index,
|
||||
remoteClusterPrivilege,
|
||||
] of roleObj.elasticsearch.remote_cluster.entries()) {
|
||||
await this.addRemoteClusterPrivilege(remoteClusterPrivilege, index);
|
||||
}
|
||||
}
|
||||
|
||||
await this.saveRole();
|
||||
}
|
||||
|
||||
async selectRole(role: string) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue