diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_api_client.ts b/x-pack/plugins/security/public/management/role_mappings/role_mappings_api_client.ts index 79e2335919b1a..5465bc24b7e31 100644 --- a/x-pack/plugins/security/public/management/role_mappings/role_mappings_api_client.ts +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_api_client.ts @@ -9,11 +9,12 @@ import type { HttpStart } from '@kbn/core/public'; import type { RoleMapping } from '../../../common/model'; -interface CheckRoleMappingFeaturesResponse { +export interface CheckRoleMappingFeaturesResponse { canManageRoleMappings: boolean; canUseInlineScripts: boolean; canUseStoredScripts: boolean; hasCompatibleRealms: boolean; + canUseRemoteIndices: boolean; } type DeleteRoleMappingsResponse = Array<{ diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx index 090c7e6854aeb..52e3d768ef07e 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx @@ -140,11 +140,13 @@ function getProps({ role, canManageSpaces = true, spacesEnabled = true, + canUseRemoteIndices = true, }: { action: 'edit' | 'clone'; role?: Role; canManageSpaces?: boolean; spacesEnabled?: boolean; + canUseRemoteIndices?: boolean; }) { const rolesAPIClient = rolesAPIClientMock.create(); rolesAPIClient.getRole.mockResolvedValue(role); @@ -171,12 +173,15 @@ function getProps({ const { fatalErrors } = coreMock.createSetup(); const { http, docLinks, notifications } = coreMock.createStart(); http.get.mockImplementation(async (path: any) => { - if (!spacesEnabled) { - throw { response: { status: 404 } }; // eslint-disable-line no-throw-literal - } if (path === '/api/spaces/space') { + if (!spacesEnabled) { + throw { response: { status: 404 } }; // eslint-disable-line no-throw-literal + } return buildSpaces(); } + if (path === '/internal/security/_check_role_mapping_features') { + return { canUseRemoteIndices }; + } }); return { @@ -265,6 +270,8 @@ describe('', () => { expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); expect(wrapper.find('input[data-test-subj="roleFormNameInput"]').prop('disabled')).toBe(true); + expect(wrapper.find('IndexPrivileges[indexType="indices"]')).toHaveLength(1); + expect(wrapper.find('IndexPrivileges[indexType="remote_indices"]')).toHaveLength(1); expectReadOnlyFormButtons(wrapper); }); @@ -291,6 +298,8 @@ describe('', () => { expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); expect(wrapper.find('input[data-test-subj="roleFormNameInput"]').prop('disabled')).toBe(true); + expect(wrapper.find('IndexPrivileges[indexType="indices"]')).toHaveLength(1); + expect(wrapper.find('IndexPrivileges[indexType="remote_indices"]')).toHaveLength(1); expectSaveFormButtons(wrapper); }); @@ -308,6 +317,8 @@ describe('', () => { expect(wrapper.find('input[data-test-subj="roleFormNameInput"]').prop('disabled')).toBe( false ); + expect(wrapper.find('IndexPrivileges[indexType="indices"]')).toHaveLength(1); + expect(wrapper.find('IndexPrivileges[indexType="remote_indices"]')).toHaveLength(1); expectSaveFormButtons(wrapper); }); @@ -480,6 +491,8 @@ describe('', () => { expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); expect(wrapper.find('input[data-test-subj="roleFormNameInput"]').prop('disabled')).toBe(true); + expect(wrapper.find('IndexPrivileges[indexType="indices"]')).toHaveLength(1); + expect(wrapper.find('IndexPrivileges[indexType="remote_indices"]')).toHaveLength(1); expectReadOnlyFormButtons(wrapper); }); @@ -507,6 +520,8 @@ describe('', () => { expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); expect(wrapper.find('input[data-test-subj="roleFormNameInput"]').prop('disabled')).toBe(true); + expect(wrapper.find('IndexPrivileges[indexType="indices"]')).toHaveLength(1); + expect(wrapper.find('IndexPrivileges[indexType="remote_indices"]')).toHaveLength(1); expectSaveFormButtons(wrapper); }); @@ -524,6 +539,8 @@ describe('', () => { expect(wrapper.find('input[data-test-subj="roleFormNameInput"]').prop('disabled')).toBe( false ); + expect(wrapper.find('IndexPrivileges[indexType="indices"]')).toHaveLength(1); + expect(wrapper.find('IndexPrivileges[indexType="remote_indices"]')).toHaveLength(1); expectSaveFormButtons(wrapper); }); @@ -612,6 +629,19 @@ describe('', () => { }); }); + it('hides remote index privileges section when not supported', async () => { + const wrapper = mountWithIntl( + + + + ); + + await waitForRender(wrapper); + + expect(wrapper.find('IndexPrivileges[indexType="indices"]')).toHaveLength(1); + expect(wrapper.find('IndexPrivileges[indexType="remote_indices"]')).toHaveLength(0); + }); + it('registers fatal error if features endpoint fails unexpectedly', async () => { const error = { response: { status: 500 } }; const getFeatures = jest.fn().mockRejectedValue(error); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx index e5e5736ca5833..9388ab92a0a76 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx @@ -21,6 +21,7 @@ import { } from '@elastic/eui'; import type { ChangeEvent, FocusEvent, FunctionComponent, HTMLProps } from 'react'; import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'; +import useAsync from 'react-use/lib/useAsync'; import type { IHttpFetchError } from '@kbn/core-http-browser'; import type { @@ -56,6 +57,7 @@ import { prepareRoleClone, } from '../../../../common/model'; import { useCapabilities } from '../../../components/use_capabilities'; +import type { CheckRoleMappingFeaturesResponse } from '../../role_mappings/role_mappings_api_client'; import type { UserAPIClient } from '../../users'; import type { IndicesAPIClient } from '../indices_api_client'; import { KibanaPrivileges } from '../model'; @@ -86,6 +88,12 @@ interface Props { spacesApiUi?: SpacesApiUi; } +function useFeatureCheck(http: HttpStart) { + return useAsync(() => + http.get('/internal/security/_check_role_mapping_features') + ); +} + function useRunAsUsers( userAPIClient: PublicMethodsOf, fatalErrors: FatalErrorsSetup @@ -311,6 +319,7 @@ export const EditRolePage: FunctionComponent = ({ const privileges = usePrivileges(privilegesAPIClient, fatalErrors); const spaces = useSpaces(http, fatalErrors); const features = useFeatures(getFeatures, fatalErrors); + const featureCheckState = useFeatureCheck(http); const [role, setRole] = useRole( rolesAPIClient, fatalErrors, @@ -329,7 +338,15 @@ export const EditRolePage: FunctionComponent = ({ } }, [hasReadOnlyPrivileges, isEditingExistingRole]); // eslint-disable-line react-hooks/exhaustive-deps - if (!role || !runAsUsers || !indexPatternsTitles || !privileges || !spaces || !features) { + if ( + !role || + !runAsUsers || + !indexPatternsTitles || + !privileges || + !spaces || + !features || + !featureCheckState.value + ) { return null; } @@ -457,6 +474,7 @@ export const EditRolePage: FunctionComponent = ({ builtinESPrivileges={builtInESPrivileges} license={license} docLinks={docLinks} + canUseRemoteIndices={featureCheckState.value?.canUseRemoteIndices} /> ); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap index 988f463f49fd1..8f160c6e57abc 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap @@ -200,89 +200,5 @@ exports[`it renders without crashing 1`] = ` } } /> - - - -

- -

-
- - -

- - - - -

-
- `; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.test.tsx index 13a4143890a86..1a1486f6d82e3 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.test.tsx @@ -63,8 +63,13 @@ test('it renders index privileges section', () => { expect(wrapper.find('IndexPrivileges[indexType="indices"]')).toHaveLength(1); }); -test('it renders remote index privileges section', () => { +test('it does not render remote index privileges section by default', () => { const wrapper = shallowWithIntl(); + expect(wrapper.find('IndexPrivileges[indexType="remote_indices"]')).toHaveLength(0); +}); + +test('it renders remote index privileges section when `canUseRemoteIndices` is enabled', () => { + const wrapper = shallowWithIntl(); expect(wrapper.find('IndexPrivileges[indexType="remote_indices"]')).toHaveLength(1); }); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.tsx index 88e0c953711fa..e963c4eda6d92 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.tsx @@ -40,6 +40,7 @@ interface Props { validator: RoleValidator; builtinESPrivileges: BuiltinESPrivileges; indexPatterns: string[]; + canUseRemoteIndices?: boolean; } export class ElasticsearchPrivileges extends Component { @@ -62,6 +63,7 @@ export class ElasticsearchPrivileges extends Component { indexPatterns, license, builtinESPrivileges, + canUseRemoteIndices, } = this.props; return ( @@ -170,37 +172,42 @@ export class ElasticsearchPrivileges extends Component { availableIndexPrivileges={builtinESPrivileges.index} editable={editable} /> - - - -

- -

-
- - -

- + + + + +

+ +

+ + + +

+ + {this.learnMore(docLinks.links.security.indicesPrivileges)} +

+
+ - {this.learnMore(docLinks.links.security.indicesPrivileges)} -

-
- + + )} ); }; diff --git a/x-pack/plugins/security/server/routes/role_mapping/feature_check.test.ts b/x-pack/plugins/security/server/routes/role_mapping/feature_check.test.ts index 0efe93d21c1b2..ce0a38ef73039 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/feature_check.test.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/feature_check.test.ts @@ -21,6 +21,9 @@ interface TestOptions { } const defaultXpackUsageResponse = { + remote_clusters: { + size: 0, + }, security: { realms: { native: { @@ -94,6 +97,7 @@ describe('GET role mappings feature check', () => { canUseInlineScripts: true, canUseStoredScripts: true, hasCompatibleRealms: true, + canUseRemoteIndices: true, }, }, }); @@ -117,10 +121,31 @@ describe('GET role mappings feature check', () => { canUseInlineScripts: true, canUseStoredScripts: true, hasCompatibleRealms: true, + canUseRemoteIndices: true, }, }, }); + getFeatureCheckTest( + 'indicates canUseRemoteIndices=false when cluster does not support remote indices', + { + xpackUsageResponse: () => ({ + ...defaultXpackUsageResponse, + remote_clusters: undefined, + }), + asserts: { + statusCode: 200, + result: { + canManageRoleMappings: true, + canUseInlineScripts: true, + canUseStoredScripts: true, + hasCompatibleRealms: true, + canUseRemoteIndices: false, + }, + }, + } + ); + getFeatureCheckTest('disallows stored scripts when disabled', { nodeSettingsResponse: () => ({ nodes: { @@ -140,6 +165,7 @@ describe('GET role mappings feature check', () => { canUseInlineScripts: true, canUseStoredScripts: false, hasCompatibleRealms: true, + canUseRemoteIndices: true, }, }, }); @@ -163,12 +189,14 @@ describe('GET role mappings feature check', () => { canUseInlineScripts: false, canUseStoredScripts: true, hasCompatibleRealms: true, + canUseRemoteIndices: true, }, }, }); getFeatureCheckTest('indicates incompatible realms when only native and file are enabled', { xpackUsageResponse: () => ({ + ...defaultXpackUsageResponse, security: { realms: { native: { @@ -189,6 +217,7 @@ describe('GET role mappings feature check', () => { canUseInlineScripts: true, canUseStoredScripts: true, hasCompatibleRealms: false, + canUseRemoteIndices: true, }, }, }); @@ -219,6 +248,7 @@ describe('GET role mappings feature check', () => { canUseInlineScripts: true, canUseStoredScripts: true, hasCompatibleRealms: false, + canUseRemoteIndices: false, }, }, } diff --git a/x-pack/plugins/security/server/routes/role_mapping/feature_check.ts b/x-pack/plugins/security/server/routes/role_mapping/feature_check.ts index 9715f92cb5a37..309cdfbeab456 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/feature_check.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/feature_check.ts @@ -24,6 +24,9 @@ interface NodeSettingsResponse { } interface XPackUsageResponse { + remote_clusters?: { + size: number; + }; security: { realms: { [realmName: string]: { @@ -128,6 +131,7 @@ async function getEnabledRoleMappingsFeatures(esClient: ElasticsearchClient, log hasCompatibleRealms, canUseStoredScripts, canUseInlineScripts, + canUseRemoteIndices: !!xpackUsage.remote_clusters, }; }