diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.tsx index 292f8244a1740..16bb626ba4e75 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.tsx @@ -82,14 +82,20 @@ export const EnginesList: React.FC = () => { const throttledSearchQuery = useThrottle(searchQuery, INPUT_THROTTLE_DELAY_MS); useEffect(() => { - fetchEngines(); + // Don't fetch engines if we don't have a valid license + if (!isGated) { + fetchEngines(); + } }, [meta.from, meta.size, throttledSearchQuery]); useEffect(() => { // We don't want to trigger loading for each search query change, so we need this // flag to set if the call to backend is first request. - setIsFirstRequest(); + if (!isGated) { + setIsFirstRequest(); + } }, []); + return ( <> {isDeleteModalVisible ? ( @@ -135,7 +141,7 @@ export const EnginesList: React.FC = () => { : [], }} pageViewTelemetry="Engines" - isLoading={isLoading} + isLoading={isLoading && !isGated} > {isGated && ( diff --git a/x-pack/plugins/index_management/__jest__/components/index_table.test.js b/x-pack/plugins/index_management/__jest__/components/index_table.test.js index 0c3487cd7a5d8..c751402d3c711 100644 --- a/x-pack/plugins/index_management/__jest__/components/index_table.test.js +++ b/x-pack/plugins/index_management/__jest__/components/index_table.test.js @@ -58,6 +58,14 @@ for (let i = 0; i < 105; i++) { }); } +const urlServiceMock = { + locators: { + get: () => ({ + navigate: async () => {}, + }), + }, +}; + let component = null; // Resolve outstanding API requests. See https://www.benmvp.com/blog/asynchronous-testing-with-enzyme-react-jest/ @@ -159,6 +167,7 @@ describe('index table', () => { executionContext: executionContextServiceMock.createStartContract(), }, plugins: {}, + url: urlServiceMock, }; component = ( @@ -317,6 +326,7 @@ describe('index table', () => { indexNameLink.simulate('click'); rendered.update(); expect(findTestSubject(rendered, 'indexDetailFlyout').length).toBe(1); + expect(findTestSubject(rendered, 'indexDetailFlyoutDiscover').length).toBe(1); }); test('should show the right context menu options when one index is selected and open', async () => { diff --git a/x-pack/plugins/index_management/public/application/lib/render_discover_link.test.tsx b/x-pack/plugins/index_management/public/application/lib/render_discover_link.test.tsx new file mode 100644 index 0000000000000..e768aae88581c --- /dev/null +++ b/x-pack/plugins/index_management/public/application/lib/render_discover_link.test.tsx @@ -0,0 +1,45 @@ +/* + * 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 { mountWithIntl } from '@kbn/test-jest-helpers'; +import { EuiButtonIcon } from '@elastic/eui'; +import { renderDiscoverLink } from './render_discover_link'; +import { AppContextProvider, AppDependencies } from '../app_context'; + +describe('renderDiscoverLink', () => { + const indexName = 'my-fancy-index'; + + it('calls navigate method when button is clicked', async () => { + const navigateMock = jest.fn(); + const ctx = { + url: { + locators: { + get: () => ({ navigate: navigateMock }), + }, + }, + } as unknown as AppDependencies; + + const component = mountWithIntl( + {renderDiscoverLink(indexName)} + ); + const button = component.find(EuiButtonIcon); + + await button.simulate('click'); + expect(navigateMock).toHaveBeenCalledWith({ dataViewSpec: { title: indexName } }); + }); + + it('does not render a button if locators is not defined', () => { + const ctx = {} as unknown as AppDependencies; + + const component = mountWithIntl( + {renderDiscoverLink(indexName)} + ); + const button = component.find(EuiButtonIcon); + + expect(button).toHaveLength(0); + }); +}); diff --git a/x-pack/plugins/index_management/public/application/lib/render_discover_link.tsx b/x-pack/plugins/index_management/public/application/lib/render_discover_link.tsx new file mode 100644 index 0000000000000..d831f532c9b50 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/lib/render_discover_link.tsx @@ -0,0 +1,46 @@ +/* + * 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 { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { AppContextConsumer } from '../app_context'; + +export const renderDiscoverLink = (indexName: string) => { + return ( + + {(ctx) => { + const locators = ctx?.url?.locators.get('DISCOVER_APP_LOCATOR'); + + if (!locators) { + return null; + } + const onClick = async () => { + await locators.navigate({ dataViewSpec: { title: indexName } }); + }; + return ( + + + + ); + }} + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx index 2e151a0c391da..5e42cecf2034f 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx @@ -24,6 +24,7 @@ import { EuiTitle, } from '@elastic/eui'; +import { renderDiscoverLink } from '../../../../lib/render_discover_link'; import { SectionLoading, reactRouterNavigate } from '../../../../../shared_imports'; import { SectionError, Error, DataHealth } from '../../../../components'; import { useLoadDataStream } from '../../../../services/api'; @@ -265,6 +266,7 @@ export const DataStreamDetailPanel: React.FunctionComponent = ({

{dataStreamName} + {renderDiscoverLink(dataStreamName)} {dataStream && }

diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/detail_panel.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/detail_panel.js index cb6e7d9cceba4..a7cc406a5fb02 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/detail_panel.js +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/detail_panel.js @@ -36,6 +36,7 @@ import { ShowJson } from './show_json'; import { Summary } from './summary'; import { EditSettingsJson } from './edit_settings_json'; import { useServices } from '../../../../app_context'; +import { renderDiscoverLink } from '../../../../lib/render_discover_link'; const tabToHumanizedMap = { [TAB_SUMMARY]: ( @@ -158,6 +159,7 @@ export const DetailPanel = ({ panelType, indexName, index, openDetailPanel, clos

{indexName} + {renderDiscoverLink(indexName)} {renderBadges(index, undefined, extensionsService)}

diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.test.ts index 33d18368f05c9..780804c2fe24d 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.test.ts @@ -30,7 +30,7 @@ describe('fetchNodesFromClusterStats', () => { }, ]; - const esRes = { + const legacyRes = { aggregations: { clusters: { buckets: [ @@ -123,10 +123,109 @@ describe('fetchNodesFromClusterStats', () => { }, }; - it('fetch stats', async () => { + const ecsRes = { + aggregations: { + clusters: { + buckets: [ + { + key: 'NG2d5jHiSBGPE6HLlUN2Bg', + doc_count: 4, + top: { + hits: { + total: { value: 4, relation: 'eq' }, + max_score: null, + hits: [ + { + _index: '.ds-.monitoring-es-8-mb-2023.03.27-000001', + _id: 'CUJ6I4cBwUW49K58n-b9', + _score: null, + _source: { + elasticsearch: { + cluster: { + stats: { + state: { + nodes: { + 'LjJ9FhDATIq9uh1kAa-XPA': { + name: 'instance-0000000000', + ephemeral_id: '3ryJEBWZS1e3x-_K_Yt-ww', + transport_address: '127.0.0.1:9300', + external_id: 'instance-0000000000', + attributes: { + logical_availability_zone: 'zone-0', + 'xpack.installed': 'true', + data: 'hot', + region: 'unknown-region', + availability_zone: 'us-central1-a', + }, + roles: [ + 'data_content', + 'data_hot', + 'ingest', + 'master', + 'remote_cluster_client', + 'transform', + ], + }, + }, + }, + }, + }, + }, + }, + sort: [1679927450602], + }, + { + _index: '.ds-.monitoring-es-8-mb-2023.03.27-000001', + _id: '6kJ6I4cBwUW49K58KuXP', + _score: null, + _source: { + elasticsearch: { + cluster: { + stats: { + state: { + nodes: { + 'LjJ9FhDATIq9uh1kAa-XPA': { + name: 'instance-0000000000', + ephemeral_id: '3ryJEBWZS1e3x-_K_Yt-ww', + transport_address: '127.0.0.1:9300', + external_id: 'instance-0000000000', + attributes: { + logical_availability_zone: 'zone-0', + 'xpack.installed': 'true', + data: 'hot', + region: 'unknown-region', + availability_zone: 'us-central1-a', + }, + roles: [ + 'data_content', + 'data_hot', + 'ingest', + 'master', + 'remote_cluster_client', + 'transform', + ], + }, + }, + }, + }, + }, + }, + }, + sort: [1679927420602], + }, + ], + }, + }, + }, + ], + }, + }, + }; + + it('fetch legacy stats', async () => { esClient.search.mockResponse( // @ts-expect-error not full response interface - esRes + legacyRes ); const result = await fetchNodesFromClusterStats(esClient, clusters); expect(result).toEqual([ @@ -150,11 +249,38 @@ describe('fetchNodesFromClusterStats', () => { ]); }); + it('fetch ecs stats', async () => { + esClient.search.mockResponse( + // @ts-expect-error not full response interface + ecsRes + ); + const result = await fetchNodesFromClusterStats(esClient, clusters); + expect(result).toEqual([ + { + clusterUuid: 'NG2d5jHiSBGPE6HLlUN2Bg', + recentNodes: [ + { + nodeUuid: 'LjJ9FhDATIq9uh1kAa-XPA', + nodeEphemeralId: '3ryJEBWZS1e3x-_K_Yt-ww', + nodeName: 'instance-0000000000', + }, + ], + priorNodes: [ + { + nodeUuid: 'LjJ9FhDATIq9uh1kAa-XPA', + nodeEphemeralId: '3ryJEBWZS1e3x-_K_Yt-ww', + nodeName: 'instance-0000000000', + }, + ], + }, + ]); + }); + it('should call ES with correct query', async () => { let params = null; esClient.search.mockImplementation((...args) => { params = args[0]; - return Promise.resolve(esRes as any); + return Promise.resolve(legacyRes as any); }); await fetchNodesFromClusterStats(esClient, clusters); expect(params).toStrictEqual({ @@ -193,7 +319,7 @@ describe('fetchNodesFromClusterStats', () => { top_hits: { sort: [{ timestamp: { order: 'desc', unmapped_type: 'long' } }], _source: { - includes: ['cluster_state.nodes', 'elasticsearch.cluster.stats.nodes'], + includes: ['cluster_state.nodes', 'elasticsearch.cluster.stats.state.nodes'], }, size: 2, }, @@ -210,7 +336,7 @@ describe('fetchNodesFromClusterStats', () => { let params = null; esClient.search.mockImplementation((...args) => { params = args[0]; - return Promise.resolve(esRes as any); + return Promise.resolve(legacyRes as any); }); await fetchNodesFromClusterStats(esClient, clusters); // @ts-ignore @@ -218,4 +344,74 @@ describe('fetchNodesFromClusterStats', () => { '.monitoring-es-*,metrics-elasticsearch.stack_monitoring.cluster_stats-*' ); }); + + it('ignores buckets with only one document', async () => { + const singleHitRes = { + aggregations: { + clusters: { + buckets: [ + { + key: 'NG2d5jHiSBGPE6HLlUN2Bg', + doc_count: 1, + top: { + hits: { + total: { value: 1, relation: 'eq' }, + max_score: null, + hits: [ + { + _index: '.ds-.monitoring-es-8-mb-2023.03.27-000001', + _id: 'CUJ6I4cBwUW49K58n-b9', + _score: null, + _source: { + elasticsearch: { + cluster: { + stats: { + state: { + nodes: { + 'LjJ9FhDATIq9uh1kAa-XPA': { + name: 'instance-0000000000', + ephemeral_id: '3ryJEBWZS1e3x-_K_Yt-ww', + transport_address: '127.0.0.1:9300', + external_id: 'instance-0000000000', + attributes: { + logical_availability_zone: 'zone-0', + 'xpack.installed': 'true', + data: 'hot', + region: 'unknown-region', + availability_zone: 'us-central1-a', + }, + roles: [ + 'data_content', + 'data_hot', + 'ingest', + 'master', + 'remote_cluster_client', + 'transform', + ], + }, + }, + }, + }, + }, + }, + }, + sort: [1679927450602], + }, + ], + }, + }, + }, + ], + }, + }, + }; + + esClient.search.mockResponse( + // @ts-expect-error not full response interface + singleHitRes + ); + + const result = await fetchNodesFromClusterStats(esClient, clusters); + expect(result).toEqual([]); + }); }); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts index 19b2cdba60b77..cb8f0e4e14b82 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts @@ -87,7 +87,7 @@ export async function fetchNodesFromClusterStats( }, ], _source: { - includes: ['cluster_state.nodes', 'elasticsearch.cluster.stats.nodes'], + includes: ['cluster_state.nodes', 'elasticsearch.cluster.stats.state.nodes'], }, size: 2, }, @@ -117,14 +117,19 @@ export async function fetchNodesFromClusterStats( for (const clusterBucket of clusterBuckets) { const clusterUuid = clusterBucket.key; const hits = clusterBucket.top.hits.hits; + if (hits.length < 2) { + continue; + } const indexName = hits[0]._index; nodes.push({ clusterUuid, recentNodes: formatNode( - hits[0]._source.cluster_state?.nodes || hits[0]._source.elasticsearch.cluster.stats.nodes + hits[0]._source.cluster_state?.nodes || + hits[0]._source.elasticsearch.cluster.stats.state.nodes ), priorNodes: formatNode( - hits[1]._source.cluster_state?.nodes || hits[1]._source.elasticsearch.cluster.stats.nodes + hits[1]._source.cluster_state?.nodes || + hits[1]._source.elasticsearch.cluster.stats.state.nodes ), ccs: indexName.includes(':') ? indexName.split(':')[0] : undefined, }); diff --git a/x-pack/plugins/security/server/authorization/__snapshots__/validate_es_response.test.ts.snap b/x-pack/plugins/security/server/authorization/__snapshots__/validate_es_response.test.ts.snap index b1bc14cc79e64..30517833289d6 100644 --- a/x-pack/plugins/security/server/authorization/__snapshots__/validate_es_response.test.ts.snap +++ b/x-pack/plugins/security/server/authorization/__snapshots__/validate_es_response.test.ts.snap @@ -1,21 +1,21 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`validateEsPrivilegeResponse fails validation when an action is malformed in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: [2]: expected value of type [boolean] but got [string]"`; +exports[`validateEsPrivilegeResponse fails validation when an action is malformed in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: Payload did not match expected action values"`; -exports[`validateEsPrivilegeResponse fails validation when an action is missing in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: Payload did not match expected actions"`; +exports[`validateEsPrivilegeResponse fails validation when an action is missing in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: Payload did not match expected actions"`; -exports[`validateEsPrivilegeResponse fails validation when an expected resource property is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: Payload did not match expected resources"`; +exports[`validateEsPrivilegeResponse fails validation when an expected resource property is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: Payload did not match expected resources"`; -exports[`validateEsPrivilegeResponse fails validation when an extra action is present in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: Payload did not match expected actions"`; +exports[`validateEsPrivilegeResponse fails validation when an extra action is present in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: Payload did not match expected actions"`; -exports[`validateEsPrivilegeResponse fails validation when an extra application is present in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.otherApplication]: definition for this key is missing"`; +exports[`validateEsPrivilegeResponse fails validation when an extra application is present in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: Expected one application but received 2"`; -exports[`validateEsPrivilegeResponse fails validation when an unexpected resource property is present in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: Payload did not match expected resources"`; +exports[`validateEsPrivilegeResponse fails validation when an unexpected resource property is present in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: Payload did not match expected resources"`; -exports[`validateEsPrivilegeResponse fails validation when the "application" property is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: Payload did not match expected resources"`; +exports[`validateEsPrivilegeResponse fails validation when the "application" property is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: Expected one application but received 0"`; -exports[`validateEsPrivilegeResponse fails validation when the requested application is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: Payload did not match expected resources"`; +exports[`validateEsPrivilegeResponse fails validation when the requested application is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: Expected one application but received 0"`; -exports[`validateEsPrivilegeResponse fails validation when the resource propertry is malformed in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: could not parse object value from json input"`; +exports[`validateEsPrivilegeResponse fails validation when the resource property is malformed in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: Payload did not match expected actions"`; -exports[`validateEsPrivilegeResponse fails validation when there are no resource properties in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: Payload did not match expected resources"`; +exports[`validateEsPrivilegeResponse fails validation when there are no resource properties in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: Payload did not match expected resources"`; diff --git a/x-pack/plugins/security/server/authorization/check_privileges.test.ts b/x-pack/plugins/security/server/authorization/check_privileges.test.ts index 6bdca9dd23d89..8141e037c2a90 100644 --- a/x-pack/plugins/security/server/authorization/check_privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges.test.ts @@ -315,7 +315,7 @@ describe('#checkPrivilegesWithRequest.atSpace', () => { }, }); expect(result).toMatchInlineSnapshot( - `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: Payload did not match expected actions]` + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: Payload did not match expected actions]` ); }); @@ -337,7 +337,7 @@ describe('#checkPrivilegesWithRequest.atSpace', () => { }, }); expect(result).toMatchInlineSnapshot( - `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: Payload did not match expected actions]` + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: Payload did not match expected actions]` ); }); }); @@ -1127,7 +1127,7 @@ describe('#checkPrivilegesWithRequest.atSpaces', () => { }, }); expect(result).toMatchInlineSnapshot( - `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: Payload did not match expected actions]` + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: Payload did not match expected actions]` ); }); @@ -1413,7 +1413,7 @@ describe('#checkPrivilegesWithRequest.atSpaces', () => { }, }); expect(result).toMatchInlineSnapshot( - `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: Payload did not match expected actions]` + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: Payload did not match expected actions]` ); }); @@ -1440,7 +1440,7 @@ describe('#checkPrivilegesWithRequest.atSpaces', () => { }, }); expect(result).toMatchInlineSnapshot( - `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: Payload did not match expected actions]` + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: Payload did not match expected actions]` ); }); @@ -1473,7 +1473,7 @@ describe('#checkPrivilegesWithRequest.atSpaces', () => { }, }); expect(result).toMatchInlineSnapshot( - `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: Payload did not match expected resources]` + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: Payload did not match expected resources]` ); }); @@ -1496,7 +1496,7 @@ describe('#checkPrivilegesWithRequest.atSpaces', () => { }, }); expect(result).toMatchInlineSnapshot( - `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: Payload did not match expected resources]` + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: Payload did not match expected resources]` ); }); }); @@ -2335,7 +2335,7 @@ describe('#checkPrivilegesWithRequest.globally', () => { }, }); expect(result).toMatchInlineSnapshot( - `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: Payload did not match expected actions]` + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: Payload did not match expected actions]` ); }); @@ -2453,7 +2453,7 @@ describe('#checkPrivilegesWithRequest.globally', () => { }, }); expect(result).toMatchInlineSnapshot( - `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: Payload did not match expected actions]` + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: Payload did not match expected actions]` ); }); @@ -2474,7 +2474,7 @@ describe('#checkPrivilegesWithRequest.globally', () => { }, }); expect(result).toMatchInlineSnapshot( - `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: Payload did not match expected actions]` + `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: Payload did not match expected actions]` ); }); }); diff --git a/x-pack/plugins/security/server/authorization/validate_es_response.test.ts b/x-pack/plugins/security/server/authorization/validate_es_response.test.ts index d299dfe4edef5..1f387d8c434eb 100644 --- a/x-pack/plugins/security/server/authorization/validate_es_response.test.ts +++ b/x-pack/plugins/security/server/authorization/validate_es_response.test.ts @@ -276,7 +276,7 @@ describe('validateEsPrivilegeResponse', () => { ).toThrowErrorMatchingSnapshot(); }); - it('fails validation when the resource propertry is malformed in the response', () => { + it('fails validation when the resource property is malformed in the response', () => { const response = { ...commonResponse, application: { diff --git a/x-pack/plugins/security/server/authorization/validate_es_response.ts b/x-pack/plugins/security/server/authorization/validate_es_response.ts index 223aee37a9047..52b1777269f35 100644 --- a/x-pack/plugins/security/server/authorization/validate_es_response.ts +++ b/x-pack/plugins/security/server/authorization/validate_es_response.ts @@ -9,10 +9,13 @@ import { schema } from '@kbn/config-schema'; import type { HasPrivilegesResponse } from './types'; -const anyBoolean = schema.boolean(); -const anyBooleanArray = schema.arrayOf(anyBoolean); -const anyString = schema.string(); -const anyObject = schema.object({}, { unknowns: 'allow' }); +const baseResponseSchema = schema.object({ + username: schema.string(), + has_all_requested: schema.boolean(), + application: schema.object({}, { unknowns: 'allow' }), + cluster: schema.object({}, { unknowns: 'allow' }), + index: schema.object({}, { unknowns: 'allow' }), +}); /** * Validates an Elasticsearch "Has privileges" response against the expected application, actions, and resources. @@ -25,9 +28,9 @@ export function validateEsPrivilegeResponse( actions: string[], resources: string[] ) { - const validationSchema = buildValidationSchema(application, actions, resources); try { - validationSchema.validate(response); + baseResponseSchema.validate(response); + validateResponse(response, application, actions, resources); } catch (e) { throw new Error(`Invalid response received from Elasticsearch has_privilege endpoint. ${e}`); } @@ -35,52 +38,45 @@ export function validateEsPrivilegeResponse( return response; } -function buildValidationSchema(application: string, actions: string[], resources: string[]) { - const actionsValidationSchema = schema.object( - {}, - { - unknowns: 'allow', - validate: (value) => { - const actualActions = Object.keys(value).sort(); - if ( - actions.length !== actualActions.length || - ![...actions].sort().every((x, i) => x === actualActions[i]) - ) { - throw new Error('Payload did not match expected actions'); - } - - anyBooleanArray.validate(Object.values(value)); - }, - } - ); +const validateResponse = ( + response: HasPrivilegesResponse, + applicationName: string, + actionNames: string[], + resourceNames: string[] +): void => { + const actualApplicationNames = Object.keys(response.application ?? {}); + if (actualApplicationNames.length !== 1) { + throw new Error(`Expected one application but received ${actualApplicationNames.length}`); + } + if (actualApplicationNames[0] !== applicationName) { + throw new Error( + `Expected application to be ${applicationName} but received ${actualApplicationNames[0]}` + ); + } - const resourcesValidationSchema = schema.object( - {}, - { - unknowns: 'allow', - validate: (value) => { - const actualResources = Object.keys(value).sort(); - if ( - resources.length !== actualResources.length || - ![...resources].sort().every((x, i) => x === actualResources[i]) - ) { - throw new Error('Payload did not match expected resources'); - } + const actualApplication = response.application[applicationName]; + const actualResourceNames = Object.keys(actualApplication).sort(); + if ( + resourceNames.length !== actualResourceNames.length || + ![...resourceNames].sort().every((x, i) => x === actualResourceNames[i]) + ) { + throw new Error('Payload did not match expected resources'); + } - Object.values(value).forEach((actionResult) => { - actionsValidationSchema.validate(actionResult); - }); - }, + const sortedActionNames = [...actionNames].sort(); + Object.values(actualApplication).forEach((actualResource) => { + const actualActionNames = Object.keys(actualResource).sort(); + if ( + actionNames.length !== actualActionNames.length || + !sortedActionNames.every((x, i) => x === actualActionNames[i]) + ) { + throw new Error('Payload did not match expected actions'); } - ); - return schema.object({ - username: anyString, - has_all_requested: anyBoolean, - cluster: anyObject, - application: schema.object({ - [application]: resourcesValidationSchema, - }), - index: anyObject, + Object.values(actualResource).forEach((actualActionValue) => { + if (typeof actualActionValue !== 'boolean') { + throw new Error('Payload did not match expected action values'); + } + }); }); -} +}; diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_authz.ts b/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_authz.ts index 0d578e46fe569..41870a5ab67a6 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_authz.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_authz.ts @@ -38,7 +38,8 @@ export default function ({ getService }: FtrProviderContext) { body: Record | undefined; } - describe('When attempting to call an endpoint api', () => { + // FLAKY: https://github.com/elastic/kibana/issues/153855 + describe.skip('When attempting to call an endpoint api', () => { let indexedData: IndexedHostsAndAlertsResponse; let actionId = ''; let agentId = '';