Hide remote indices role configuration when not supported (#155490)

Resolves #155389

## Summary

Adds feature flag to automatically hide remote index privileges section
when not supported by cluster.
This commit is contained in:
Thom Heymann 2023-04-21 15:19:25 +01:00 committed by GitHub
parent f95ebdfb31
commit cfc01d5444
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 130 additions and 119 deletions

View file

@ -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<{

View file

@ -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('<EditRolePage />', () => {
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('<EditRolePage />', () => {
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('<EditRolePage />', () => {
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('<EditRolePage />', () => {
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('<EditRolePage />', () => {
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('<EditRolePage />', () => {
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('<EditRolePage />', () => {
});
});
it('hides remote index privileges section when not supported', async () => {
const wrapper = mountWithIntl(
<KibanaContextProvider services={coreStart}>
<EditRolePage {...getProps({ action: 'edit', canUseRemoteIndices: false })} />
</KibanaContextProvider>
);
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);

View file

@ -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<CheckRoleMappingFeaturesResponse>('/internal/security/_check_role_mapping_features')
);
}
function useRunAsUsers(
userAPIClient: PublicMethodsOf<UserAPIClient>,
fatalErrors: FatalErrorsSetup
@ -311,6 +319,7 @@ export const EditRolePage: FunctionComponent<Props> = ({
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<Props> = ({
}
}, [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<Props> = ({
builtinESPrivileges={builtInESPrivileges}
license={license}
docLinks={docLinks}
canUseRemoteIndices={featureCheckState.value?.canUseRemoteIndices}
/>
</div>
);

View file

@ -200,89 +200,5 @@ exports[`it renders without crashing 1`] = `
}
}
/>
<EuiSpacer />
<EuiSpacer />
<EuiTitle
size="xs"
>
<h3>
<FormattedMessage
defaultMessage="Remote index privileges"
id="xpack.security.management.editRole.elasticSearchPrivileges.remoteIndexPrivilegesTitle"
values={Object {}}
/>
</h3>
</EuiTitle>
<EuiSpacer
size="s"
/>
<EuiText
color="subdued"
size="s"
>
<p>
<FormattedMessage
defaultMessage="Control access to the data in remote clusters. "
id="xpack.security.management.editRole.elasticSearchPrivileges.controlAccessToRemoteClusterDataDescription"
values={Object {}}
/>
<EuiLink
className="editRole__learnMore"
href="https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-privileges.html#privileges-list-indices"
target="_blank"
>
<FormattedMessage
defaultMessage="Learn more"
id="xpack.security.management.editRole.elasticSearchPrivileges.learnMoreLinkText"
values={Object {}}
/>
</EuiLink>
</p>
</EuiText>
<IndexPrivileges
availableIndexPrivileges={
Array [
"all",
"read",
"write",
"index",
]
}
editable={true}
indexType="remote_indices"
indicesAPIClient={
Object {
"getFields": [MockFunction],
}
}
license={
Object {
"features$": Observable {
"_subscribe": [Function],
},
"getFeatures": [MockFunction],
"hasAtLeast": [MockFunction],
"isEnabled": [MockFunction],
"isLicenseAvailable": [MockFunction],
}
}
onChange={[MockFunction]}
role={
Object {
"elasticsearch": Object {
"cluster": Array [],
"indices": Array [],
"run_as": Array [],
},
"kibana": Array [],
"name": "",
}
}
validator={
RoleValidator {
"shouldValidate": undefined,
}
}
/>
</CollapsiblePanel>
`;

View file

@ -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(<ElasticsearchPrivileges {...getProps()} />);
expect(wrapper.find('IndexPrivileges[indexType="remote_indices"]')).toHaveLength(0);
});
test('it renders remote index privileges section when `canUseRemoteIndices` is enabled', () => {
const wrapper = shallowWithIntl(<ElasticsearchPrivileges {...getProps()} canUseRemoteIndices />);
expect(wrapper.find('IndexPrivileges[indexType="remote_indices"]')).toHaveLength(1);
});

View file

@ -40,6 +40,7 @@ interface Props {
validator: RoleValidator;
builtinESPrivileges: BuiltinESPrivileges;
indexPatterns: string[];
canUseRemoteIndices?: boolean;
}
export class ElasticsearchPrivileges extends Component<Props, {}> {
@ -62,6 +63,7 @@ export class ElasticsearchPrivileges extends Component<Props, {}> {
indexPatterns,
license,
builtinESPrivileges,
canUseRemoteIndices,
} = this.props;
return (
@ -170,37 +172,42 @@ export class ElasticsearchPrivileges extends Component<Props, {}> {
availableIndexPrivileges={builtinESPrivileges.index}
editable={editable}
/>
<EuiSpacer />
<EuiSpacer />
<EuiTitle size="xs">
<h3>
<FormattedMessage
id="xpack.security.management.editRole.elasticSearchPrivileges.remoteIndexPrivilegesTitle"
defaultMessage="Remote index privileges"
{canUseRemoteIndices && (
<>
<EuiSpacer />
<EuiSpacer />
<EuiTitle size="xs">
<h3>
<FormattedMessage
id="xpack.security.management.editRole.elasticSearchPrivileges.remoteIndexPrivilegesTitle"
defaultMessage="Remote index 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.indicesPrivileges)}
</p>
</EuiText>
<IndexPrivileges
indexType="remote_indices"
role={role}
indicesAPIClient={indicesAPIClient}
validator={validator}
license={license}
onChange={onChange}
availableIndexPrivileges={builtinESPrivileges.index}
editable={editable}
/>
</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.indicesPrivileges)}
</p>
</EuiText>
<IndexPrivileges
indexType="remote_indices"
role={role}
indicesAPIClient={indicesAPIClient}
validator={validator}
license={license}
onChange={onChange}
availableIndexPrivileges={builtinESPrivileges.index}
editable={editable}
/>
</>
)}
</Fragment>
);
};

View file

@ -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,
},
},
}

View file

@ -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,
};
}