diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.test.tsx index 221e0ec245fd..2ac99d4f63fb 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.test.tsx @@ -5,11 +5,12 @@ * 2.0. */ -import { EuiButtonIcon, EuiTextArea } from '@elastic/eui'; +import { EuiButtonIcon, EuiComboBox, EuiTextArea } from '@elastic/eui'; import React from 'react'; -import { mountWithIntl, shallowWithIntl } from '@kbn/test/jest'; +import { findTestSubject, mountWithIntl, nextTick, shallowWithIntl } from '@kbn/test/jest'; +import { indicesAPIClientMock } from '../../../index.mock'; import { RoleValidator } from '../../validate_role'; import { IndexPrivilegeForm } from './index_privilege_form'; @@ -25,7 +26,7 @@ test('it renders without crashing', () => { }, formIndex: 0, indexPatterns: [], - availableFields: [], + indicesAPIClient: indicesAPIClientMock.create(), availableIndexPrivileges: ['all', 'read', 'write', 'index'], isRoleReadOnly: false, allowDocumentLevelSecurity: true, @@ -52,7 +53,7 @@ test('it allows for custom index privileges', () => { }, formIndex: 0, indexPatterns: [], - availableFields: [], + indicesAPIClient: indicesAPIClientMock.create(), availableIndexPrivileges: ['all', 'read', 'write', 'index'], isRoleReadOnly: false, allowDocumentLevelSecurity: true, @@ -86,7 +87,7 @@ describe('delete button', () => { }, formIndex: 0, indexPatterns: [], - availableFields: [], + indicesAPIClient: indicesAPIClientMock.create(), availableIndexPrivileges: ['all', 'read', 'write', 'index'], isRoleReadOnly: false, allowDocumentLevelSecurity: true, @@ -138,7 +139,7 @@ describe(`document level security`, () => { }, formIndex: 0, indexPatterns: [], - availableFields: [], + indicesAPIClient: indicesAPIClientMock.create(), availableIndexPrivileges: ['all', 'read', 'write', 'index'], isRoleReadOnly: false, allowDocumentLevelSecurity: true, @@ -197,7 +198,7 @@ describe('field level security', () => { }, formIndex: 0, indexPatterns: [], - availableFields: [], + indicesAPIClient: indicesAPIClientMock.create(), availableIndexPrivileges: ['all', 'read', 'write', 'index'], isRoleReadOnly: false, allowDocumentLevelSecurity: true, @@ -208,19 +209,21 @@ describe('field level security', () => { intl: {} as any, }; - test(`inputs are hidden when FLS is not allowed`, () => { + test(`inputs are hidden when FLS is not allowed, and fields are not queried`, async () => { const testProps = { ...props, allowFieldLevelSecurity: false, }; const wrapper = mountWithIntl(); + await nextTick(); expect(wrapper.find('EuiSwitch[data-test-subj="restrictFieldsQuery0"]')).toHaveLength(0); expect(wrapper.find('.indexPrivilegeForm__grantedFieldsRow')).toHaveLength(0); expect(wrapper.find('.indexPrivilegeForm__deniedFieldsRow')).toHaveLength(0); + expect(testProps.indicesAPIClient.getFields).not.toHaveBeenCalled(); }); - test('only the switch is shown when allowed, and FLS is empty', () => { + test('renders the FLS switch when available, but collapsed when no fields are selected', async () => { const testProps = { ...props, indexPrivilege: { @@ -230,19 +233,126 @@ describe('field level security', () => { }; const wrapper = mountWithIntl(); + await nextTick(); expect(wrapper.find('EuiSwitch[data-test-subj="restrictFieldsQuery0"]')).toHaveLength(1); expect(wrapper.find('.indexPrivilegeForm__grantedFieldsRow')).toHaveLength(0); expect(wrapper.find('.indexPrivilegeForm__deniedFieldsRow')).toHaveLength(0); + expect(testProps.indicesAPIClient.getFields).not.toHaveBeenCalled(); }); - test('inputs are shown when allowed', () => { + test('FLS inputs are shown when allowed', async () => { const testProps = { ...props, }; const wrapper = mountWithIntl(); + await nextTick(); expect(wrapper.find('div.indexPrivilegeForm__grantedFieldsRow')).toHaveLength(1); expect(wrapper.find('div.indexPrivilegeForm__deniedFieldsRow')).toHaveLength(1); + expect(testProps.indicesAPIClient.getFields).not.toHaveBeenCalled(); + }); + + test('does not query for available fields when a request is already in flight', async () => { + jest.useFakeTimers(); + + const testProps = { + ...props, + indexPrivilege: { + ...props.indexPrivilege, + names: ['foo', 'bar-*'], + }, + indicesAPIClient: indicesAPIClientMock.create(), + }; + + testProps.indicesAPIClient.getFields.mockImplementation(async () => { + return new Promise((resolve) => + setTimeout(() => { + resolve(['foo']); + }, 5000) + ); + }); + + const wrapper = mountWithIntl(); + await nextTick(); + expect(wrapper.find('div.indexPrivilegeForm__grantedFieldsRow')).toHaveLength(1); + expect(wrapper.find('div.indexPrivilegeForm__deniedFieldsRow')).toHaveLength(1); + expect(testProps.indicesAPIClient.getFields).toHaveBeenCalledTimes(1); + + findTestSubject(wrapper, 'fieldInput0').simulate('focus'); + jest.advanceTimersByTime(2000); + expect(testProps.indicesAPIClient.getFields).toHaveBeenCalledTimes(1); + + findTestSubject(wrapper, 'fieldInput0').simulate('focus'); + jest.advanceTimersByTime(4000); + expect(testProps.indicesAPIClient.getFields).toHaveBeenCalledTimes(1); + }); + + test('queries for available fields when mounted, and FLS is available', async () => { + const testProps = { + ...props, + indexPrivilege: { + ...props.indexPrivilege, + names: ['foo', 'bar-*'], + }, + indicesAPIClient: indicesAPIClientMock.create(), + }; + + testProps.indicesAPIClient.getFields.mockResolvedValue(['a', 'b', 'c']); + + const wrapper = mountWithIntl(); + await nextTick(); + expect(wrapper.find('div.indexPrivilegeForm__grantedFieldsRow')).toHaveLength(1); + expect(wrapper.find('div.indexPrivilegeForm__deniedFieldsRow')).toHaveLength(1); + expect(testProps.indicesAPIClient.getFields).toHaveBeenCalledTimes(1); + }); + + test('does not query for available fields when mounted, and FLS is unavailable', async () => { + const testProps = { + ...props, + indexPrivilege: { + ...props.indexPrivilege, + names: ['foo', 'bar-*'], + }, + indicesAPIClient: indicesAPIClientMock.create(), + allowFieldLevelSecurity: false, + }; + + testProps.indicesAPIClient.getFields.mockResolvedValue(['a', 'b', 'c']); + + const wrapper = mountWithIntl(); + await nextTick(); + expect(wrapper.find('div.indexPrivilegeForm__grantedFieldsRow')).toHaveLength(0); + expect(wrapper.find('div.indexPrivilegeForm__deniedFieldsRow')).toHaveLength(0); + expect(testProps.indicesAPIClient.getFields).not.toHaveBeenCalled(); + }); + + test('queries for available fields when the set of index patterns change', async () => { + const testProps = { + ...props, + indexPrivilege: { + ...props.indexPrivilege, + names: ['foo', 'bar-*'], + }, + indexPatterns: ['foo', 'bar-*', 'newPattern'], + indicesAPIClient: indicesAPIClientMock.create(), + }; + + testProps.indicesAPIClient.getFields.mockResolvedValue(['a', 'b', 'c']); + + const wrapper = mountWithIntl(); + await nextTick(); + expect(wrapper.find('div.indexPrivilegeForm__grantedFieldsRow')).toHaveLength(1); + expect(wrapper.find('div.indexPrivilegeForm__deniedFieldsRow')).toHaveLength(1); + expect(testProps.indicesAPIClient.getFields).toHaveBeenCalledTimes(1); + + wrapper + .find(EuiComboBox) + .filterWhere((item) => item.props()['data-test-subj'] === 'indicesInput0') + .props().onChange!([{ label: 'newPattern', value: 'newPattern' }]); + + await nextTick(); + expect(testProps.indicesAPIClient.getFields).toHaveBeenCalledTimes(2); + expect(testProps.indicesAPIClient.getFields).toHaveBeenCalledWith('newPattern'); }); test('it displays a warning when no fields are granted', () => { diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.tsx index 697b5c1cac34..811d5590b9a3 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.tsx @@ -23,8 +23,10 @@ import React, { Component, Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import type { RoleIndexPrivilege } from '../../../../../../common/model'; +import type { IndicesAPIClient } from '../../../indices_api_client'; import type { RoleValidator } from '../../validate_role'; const fromOption = (option: any) => option.label; @@ -35,7 +37,7 @@ interface Props { indexPrivilege: RoleIndexPrivilege; indexPatterns: string[]; availableIndexPrivileges: string[]; - availableFields: string[]; + indicesAPIClient: PublicMethodsOf; onChange: (indexPrivilege: RoleIndexPrivilege) => void; onDelete: () => void; isRoleReadOnly: boolean; @@ -50,9 +52,16 @@ interface State { grantedFields: string[]; exceptedFields: string[]; documentQuery?: string; + isFieldListLoading: boolean; + flsOptions: string[]; } export class IndexPrivilegeForm extends Component { + // This is distinct from the field within `this.state`. + // We want to make sure that only one request for fields is in-flight at a time, + // and relying on state for this is error prone. + private isFieldListLoading: boolean = false; + constructor(props: Props) { super(props); @@ -64,9 +73,17 @@ export class IndexPrivilegeForm extends Component { grantedFields: grant, exceptedFields: except, documentQuery: props.indexPrivilege.query, + isFieldListLoading: false, + flsOptions: [], }; } + public componentDidMount() { + if (this.state.fieldSecurityExpanded && this.props.allowFieldLevelSecurity) { + this.loadFLSOptions(this.props.indexPrivilege.names); + } + } + public render() { return ( @@ -149,11 +166,30 @@ export class IndexPrivilegeForm extends Component { ); }; + private loadFLSOptions = (indexNames: string[], force = false) => { + if (!force && (this.isFieldListLoading || indexNames.length === 0)) return; + + this.isFieldListLoading = true; + this.setState({ + isFieldListLoading: true, + }); + + this.props.indicesAPIClient + .getFields(indexNames.join(',')) + .then((fields) => { + this.isFieldListLoading = false; + this.setState({ flsOptions: fields, isFieldListLoading: false }); + }) + .catch(() => { + this.isFieldListLoading = false; + this.setState({ flsOptions: [], isFieldListLoading: false }); + }); + }; + private getFieldLevelControls = () => { const { allowFieldLevelSecurity, allowDocumentLevelSecurity, - availableFields, indexPrivilege, isRoleReadOnly, } = this.props; @@ -210,11 +246,13 @@ export class IndexPrivilegeForm extends Component { @@ -233,11 +271,13 @@ export class IndexPrivilegeForm extends Component { @@ -362,6 +402,11 @@ export class IndexPrivilegeForm extends Component { const hasSavedFieldSecurity = this.state.exceptedFields.length > 0 || this.state.grantedFields.length > 0; + // If turning on, then request available fields + if (willToggleOn) { + this.loadFLSOptions(this.props.indexPrivilege.names); + } + if (willToggleOn && !hasConfiguredFieldSecurity && hasSavedFieldSecurity) { this.props.onChange({ ...this.props.indexPrivilege, @@ -380,13 +425,22 @@ export class IndexPrivilegeForm extends Component { ...this.props.indexPrivilege, names: newIndexPatterns, }); + // If FLS controls are visible, then forcefully request a new set of options + if (this.state.fieldSecurityExpanded) { + this.loadFLSOptions(newIndexPatterns, true); + } }; private onIndexPatternsChange = (newPatterns: EuiComboBoxOptionOption[]) => { + const names = newPatterns.map(fromOption); this.props.onChange({ ...this.props.indexPrivilege, - names: newPatterns.map(fromOption), + names, }); + // If FLS controls are visible, then forcefully request a new set of options + if (this.state.fieldSecurityExpanded) { + this.loadFLSOptions(names, true); + } }; private onPrivilegeChange = (newPrivileges: EuiComboBoxOptionOption[]) => { diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.test.tsx index 5b7d703865b9..c910ec5dda9a 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.test.tsx @@ -56,6 +56,9 @@ test('it renders a IndexPrivilegeForm for each privilege on the role', async () allowRoleDocumentLevelSecurity: true, } as any); + const indicesAPIClient = indicesAPIClientMock.create(); + indicesAPIClient.getFields.mockResolvedValue(['foo']); + const props = { role: { name: '', @@ -80,7 +83,7 @@ test('it renders a IndexPrivilegeForm for each privilege on the role', async () editable: true, validator: new RoleValidator(), availableIndexPrivileges: ['all', 'read', 'write', 'index'], - indicesAPIClient: indicesAPIClientMock.create(), + indicesAPIClient, license, }; const wrapper = mountWithIntl(); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.tsx index 2f6bb73fc62f..d761992c275c 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import _ from 'lodash'; import React, { Component, Fragment } from 'react'; import type { PublicMethodsOf } from '@kbn/utility-types'; @@ -41,18 +40,15 @@ export class IndexPrivileges extends Component { }; } - public componentDidMount() { - this.loadAvailableFields(this.props.role.elasticsearch.indices); - } - public render() { const { indices = [] } = this.props.role.elasticsearch; - const { indexPatterns, license, availableIndexPrivileges } = this.props; + const { indexPatterns, license, availableIndexPrivileges, indicesAPIClient } = this.props; const { allowRoleDocumentLevelSecurity, allowRoleFieldLevelSecurity } = license.getFeatures(); const props = { indexPatterns, + indicesAPIClient, // If editing an existing role while that has been disabled, always show the FLS/DLS fields because currently // a role is only marked as disabled if it has FLS/DLS setup (usually before the user changed to a license that // doesn't permit FLS/DLS). @@ -69,7 +65,6 @@ export class IndexPrivileges extends Component { validator={this.props.validator} availableIndexPrivileges={availableIndexPrivileges} indexPrivilege={indexPrivilege} - availableFields={this.state.availableFields[indexPrivilege.names.join(',')]} onChange={this.onIndexPrivilegeChange(idx)} onDelete={this.onIndexPrivilegeDelete(idx)} /> @@ -116,8 +111,6 @@ export class IndexPrivileges extends Component { indices: newIndices, }, }); - - this.loadAvailableFields(newIndices); }; }; @@ -141,43 +134,4 @@ export class IndexPrivileges extends Component { public isPlaceholderPrivilege = (indexPrivilege: RoleIndexPrivilege) => { return indexPrivilege.names.length === 0; }; - - public loadAvailableFields(privileges: RoleIndexPrivilege[]) { - // readonly roles cannot be edited, and therefore do not need to fetch available fields. - if (isRoleReadOnly(this.props.role)) { - return; - } - - const patterns = privileges.map((index) => index.names.join(',')); - - const cachedPatterns = Object.keys(this.state.availableFields); - const patternsToFetch = _.difference(patterns, cachedPatterns); - - const fetchRequests = patternsToFetch.map(this.loadFieldsForPattern); - - Promise.all(fetchRequests).then((response) => { - this.setState({ - availableFields: { - ...this.state.availableFields, - ...response.reduce((acc, o) => ({ ...acc, ...o }), {}), - }, - }); - }); - } - - public loadFieldsForPattern = async (pattern: string) => { - if (!pattern) { - return { [pattern]: [] }; - } - - try { - return { - [pattern]: await this.props.indicesAPIClient.getFields(pattern), - }; - } catch (e) { - return { - [pattern]: [], - }; - } - }; } diff --git a/x-pack/plugins/security/public/management/roles/indices_api_client.test.ts b/x-pack/plugins/security/public/management/roles/indices_api_client.test.ts new file mode 100644 index 000000000000..feb0c73aa5bb --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/indices_api_client.test.ts @@ -0,0 +1,104 @@ +/* + * 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 { coreMock } from 'src/core/public/mocks'; + +import { IndicesAPIClient } from './indices_api_client'; + +describe('getFields', () => { + it('queries for available fields', async () => { + const { http } = coreMock.createSetup(); + http.get.mockResolvedValue(['foo']); + + const client = new IndicesAPIClient(http); + + const fields = await client.getFields('foo with special characters-&-*'); + + expect(http.get).toHaveBeenCalledTimes(1); + expect(http.get).toHaveBeenCalledWith( + `/internal/security/fields/${encodeURIComponent('foo with special characters-&-*')}` + ); + expect(fields).toEqual(['foo']); + }); + + it('caches results for matching patterns', async () => { + const { http } = coreMock.createSetup(); + http.get.mockResolvedValue(['foo']); + + const client = new IndicesAPIClient(http); + + const fields = await client.getFields('foo'); + + const fields2 = await client.getFields('foo'); + + expect(http.get).toHaveBeenCalledTimes(1); + expect(http.get).toHaveBeenCalledWith(`/internal/security/fields/${encodeURIComponent('foo')}`); + + expect(fields).toEqual(['foo']); + expect(fields2).toEqual(['foo']); + }); + + it('does not cache results for differing patterns', async () => { + const { http } = coreMock.createSetup(); + http.get.mockResolvedValueOnce(['foo']); + http.get.mockResolvedValueOnce(['bar']); + + const client = new IndicesAPIClient(http); + + const fields = await client.getFields('foo'); + + const fields2 = await client.getFields('bar'); + + expect(http.get).toHaveBeenCalledTimes(2); + expect(http.get).toHaveBeenNthCalledWith( + 1, + `/internal/security/fields/${encodeURIComponent('foo')}` + ); + expect(http.get).toHaveBeenNthCalledWith( + 2, + `/internal/security/fields/${encodeURIComponent('bar')}` + ); + + expect(fields).toEqual(['foo']); + expect(fields2).toEqual(['bar']); + }); + + it('does not cache empty results', async () => { + const { http } = coreMock.createSetup(); + http.get.mockResolvedValue([]); + + const client = new IndicesAPIClient(http); + + const fields = await client.getFields('foo'); + + const fields2 = await client.getFields('foo'); + + expect(http.get).toHaveBeenCalledTimes(2); + expect(http.get).toHaveBeenNthCalledWith( + 1, + `/internal/security/fields/${encodeURIComponent('foo')}` + ); + expect(http.get).toHaveBeenNthCalledWith( + 2, + `/internal/security/fields/${encodeURIComponent('foo')}` + ); + + expect(fields).toEqual([]); + expect(fields2).toEqual([]); + }); + + it('throws unexpected errors', async () => { + const { http } = coreMock.createSetup(); + http.get.mockRejectedValue(new Error('AHHHH')); + + const client = new IndicesAPIClient(http); + + await expect(() => client.getFields('foo')).rejects.toThrowErrorMatchingInlineSnapshot( + `"AHHHH"` + ); + }); +}); diff --git a/x-pack/plugins/security/public/management/roles/indices_api_client.ts b/x-pack/plugins/security/public/management/roles/indices_api_client.ts index ebec4d858d7a..5d2ead1961fc 100644 --- a/x-pack/plugins/security/public/management/roles/indices_api_client.ts +++ b/x-pack/plugins/security/public/management/roles/indices_api_client.ts @@ -8,12 +8,20 @@ import type { HttpStart } from 'src/core/public'; export class IndicesAPIClient { + private readonly fieldCache = new Map(); + constructor(private readonly http: HttpStart) {} - async getFields(query: string) { - return ( - (await this.http.get(`/internal/security/fields/${encodeURIComponent(query)}`)) || - [] - ); + async getFields(pattern: string): Promise { + if (pattern && !this.fieldCache.has(pattern)) { + const fields = await this.http.get( + `/internal/security/fields/${encodeURIComponent(pattern)}` + ); + if (Array.isArray(fields) && fields.length > 0) { + this.fieldCache.set(pattern, fields); + } + } + + return this.fieldCache.get(pattern) ?? []; } } diff --git a/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx b/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx index 476aabb845c5..4b10c2d5cf13 100644 --- a/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx @@ -96,7 +96,7 @@ describe('rolesManagementApp', () => { expect(docTitle.reset).not.toHaveBeenCalled(); expect(container).toMatchInlineSnapshot(`
- Role Edit Page: {"action":"edit","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit","search":"","hash":""}}} + Role Edit Page: {"action":"edit","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"fieldCache":{}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit","search":"","hash":""}}}
`); @@ -124,7 +124,7 @@ describe('rolesManagementApp', () => { expect(docTitle.reset).not.toHaveBeenCalled(); expect(container).toMatchInlineSnapshot(`
- Role Edit Page: {"action":"edit","roleName":"role@name","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/role@name","search":"","hash":""}}} + Role Edit Page: {"action":"edit","roleName":"role@name","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"fieldCache":{}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/role@name","search":"","hash":""}}}
`); @@ -149,7 +149,7 @@ describe('rolesManagementApp', () => { expect(docTitle.reset).not.toHaveBeenCalled(); expect(container).toMatchInlineSnapshot(`
- Role Edit Page: {"action":"clone","roleName":"someRoleName","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/clone/someRoleName","search":"","hash":""}}} + Role Edit Page: {"action":"clone","roleName":"someRoleName","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"fieldCache":{}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/clone/someRoleName","search":"","hash":""}}}
`); diff --git a/x-pack/plugins/security/server/routes/indices/get_fields.ts b/x-pack/plugins/security/server/routes/indices/get_fields.ts index 63704682e363..d0370a93b14c 100644 --- a/x-pack/plugins/security/server/routes/indices/get_fields.ts +++ b/x-pack/plugins/security/server/routes/indices/get_fields.ts @@ -25,6 +25,7 @@ export function defineGetFieldsRoutes({ router }: RouteDefinitionParams) { fields: '*', allow_no_indices: false, include_defaults: true, + filter_path: '*.mappings.*.mapping.*.type', }); // The flow is the following (see response format at https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-field-mapping.html): @@ -62,7 +63,17 @@ export function defineGetFieldsRoutes({ router }: RouteDefinitionParams) { body: fields, }); } catch (error) { - return response.customError(wrapIntoCustomErrorResponse(error)); + const customResponse = wrapIntoCustomErrorResponse(error); + + // Elasticsearch returns a 404 response if the provided pattern does not match any indices. + // In this scenario, we want to instead treat this as an empty response. + if (customResponse.statusCode === 404) { + return response.ok({ + body: [], + }); + } + + return response.customError(customResponse); } } ); diff --git a/x-pack/test/api_integration/apis/security/index_fields.ts b/x-pack/test/api_integration/apis/security/index_fields.ts index 6918ec3415da..442740c7666d 100644 --- a/x-pack/test/api_integration/apis/security/index_fields.ts +++ b/x-pack/test/api_integration/apis/security/index_fields.ts @@ -71,6 +71,14 @@ export default function ({ getService }: FtrProviderContext) { expect(actualFields).to.eql(expectedFields); }); + + it('should return an empty result for indices that do not exist', async () => { + await supertest + .get('/internal/security/fields/this-index-name-definitely-does-not-exist-*') + .set('kbn-xsrf', 'xxx') + .send() + .expect(200, []); + }); }); }); }