Fix resolve index API to not throw 500 when encountering no_such_remote_cluster_exception (#204802)

## Summary

Fixes https://github.com/elastic/kibana/issues/197747.

Updates the `/internal/index-pattern-management/resolve_index/{query}`
route to properly handle `no_such_remote_cluster_exception` and return
`404` rather than `500` server error.

Adds unit tests for the route handler.

### Checklist

Check the PR satisfies following conditions. 

Reviewers should verify this PR satisfies this list as well.

- [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

---------

Co-authored-by: Julia Rechkunova <julia.rechkunova@gmail.com>
This commit is contained in:
Lukas Olson 2024-12-19 14:55:29 -07:00 committed by GitHub
parent 9ad31d0863
commit 0952f6e0f5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 255 additions and 2 deletions

View file

@ -0,0 +1,243 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { MockedKeys } from '@kbn/utility-types-jest';
import { CoreSetup, RequestHandlerContext } from '@kbn/core/server';
import { coreMock, httpServerMock } from '@kbn/core/server/mocks';
import { registerResolveIndexRoute } from './resolve_index';
const mockResponseIndices = {
indices: [
{
name: 'kibana_sample_data_logs',
attributes: ['open'],
},
],
aliases: [],
data_streams: [],
};
const mockResponseEmpty = {
indices: [],
aliases: [],
data_streams: [],
};
const mockError403 = {
meta: {
body: {
error: {
root_cause: [
{
type: 'no_such_remote_cluster_exception',
reason: 'no such remote cluster: [cluster1]',
},
],
type: 'security_exception',
reason:
'action [indices:admin/resolve/index] is unauthorized for user [elastic] with effective roles [superuser], this action is granted by the index privileges [view_index_metadata,manage,read,all]',
caused_by: {
type: 'no_such_remote_cluster_exception',
reason: 'no such remote cluster: [cluster1]',
},
},
status: 403,
},
statusCode: 403,
},
};
const mockError404 = {
meta: {
body: {
error: {
root_cause: [
{
type: 'index_not_found_exception',
reason: 'no such index [asdf]',
'resource.type': 'index_or_alias',
'resource.id': 'asdf',
index_uuid: '_na_',
index: 'asdf',
},
],
type: 'index_not_found_exception',
reason: 'no such index [asdf]',
'resource.type': 'index_or_alias',
'resource.id': 'asdf',
index_uuid: '_na_',
index: 'asdf',
},
status: 404,
},
statusCode: 404,
},
};
describe('resolve_index route', () => {
let mockCoreSetup: MockedKeys<CoreSetup>;
beforeEach(() => {
mockCoreSetup = coreMock.createSetup();
});
it('handler calls /_resolve/index with the given request', async () => {
const mockClient = {
indices: {
resolveIndex: jest.fn().mockResolvedValue(mockResponseIndices),
},
};
const mockContext = {
core: {
elasticsearch: { client: { asCurrentUser: mockClient } },
},
};
const mockRequest = httpServerMock.createKibanaRequest({
params: {
query: 'kibana_sample_data_logs',
},
});
const mockResponse = httpServerMock.createResponseFactory();
registerResolveIndexRoute(mockCoreSetup.http.createRouter());
const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value;
const handler = mockRouter.get.mock.calls[0][1];
await handler(mockContext as unknown as RequestHandlerContext, mockRequest, mockResponse);
expect(mockClient.indices.resolveIndex.mock.calls[0][0]).toMatchInlineSnapshot(`
Object {
"expand_wildcards": "open",
"name": "kibana_sample_data_logs",
}
`);
expect(mockResponse.ok).toBeCalled();
expect(mockResponse.ok.mock.calls[0][0]).toEqual({ body: mockResponseIndices });
});
it('should return 200 for a search for indices with wildcard', async () => {
const mockClient = {
indices: {
resolveIndex: jest.fn().mockResolvedValue(mockResponseEmpty),
},
};
const mockContext = {
core: {
elasticsearch: { client: { asCurrentUser: mockClient } },
},
};
const mockRequest = httpServerMock.createKibanaRequest({
params: {
query: 'asdf*',
},
});
const mockResponse = httpServerMock.createResponseFactory();
registerResolveIndexRoute(mockCoreSetup.http.createRouter());
const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value;
const handler = mockRouter.get.mock.calls[0][1];
await handler(mockContext as unknown as RequestHandlerContext, mockRequest, mockResponse);
expect(mockClient.indices.resolveIndex.mock.calls[0][0]).toMatchInlineSnapshot(`
Object {
"expand_wildcards": "open",
"name": "asdf*",
}
`);
expect(mockResponse.ok).toBeCalled();
expect(mockResponse.ok.mock.calls[0][0]).toEqual({ body: mockResponseEmpty });
});
it('returns 404 when hitting a 403 from Elasticsearch', async () => {
const mockClient = {
indices: {
resolveIndex: jest.fn().mockRejectedValue(mockError403),
},
};
const mockContext = {
core: {
elasticsearch: { client: { asCurrentUser: mockClient } },
},
};
const mockRequest = httpServerMock.createKibanaRequest({
params: {
query: 'cluster1:filebeat-*,cluster2:filebeat-*',
},
});
const mockResponse = httpServerMock.createResponseFactory();
registerResolveIndexRoute(mockCoreSetup.http.createRouter());
const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value;
const handler = mockRouter.get.mock.calls[0][1];
await handler(mockContext as unknown as RequestHandlerContext, mockRequest, mockResponse);
expect(mockClient.indices.resolveIndex.mock.calls[0][0]).toMatchInlineSnapshot(`
Object {
"expand_wildcards": "open",
"name": "cluster1:filebeat-*,cluster2:filebeat-*",
}
`);
expect(mockResponse.notFound).toBeCalled();
expect(mockResponse.notFound.mock.calls[0][0]).toMatchInlineSnapshot(`
Object {
"body": Object {
"message": "action [indices:admin/resolve/index] is unauthorized for user [elastic] with effective roles [superuser], this action is granted by the index privileges [view_index_metadata,manage,read,all]",
},
}
`);
});
it('returns 404 when hitting a 404 from Elasticsearch', async () => {
const mockClient = {
indices: {
resolveIndex: jest.fn().mockRejectedValue(mockError404),
},
};
const mockContext = {
core: {
elasticsearch: { client: { asCurrentUser: mockClient } },
},
};
const mockRequest = httpServerMock.createKibanaRequest({
params: {
query: 'asdf',
},
});
const mockResponse = httpServerMock.createResponseFactory();
registerResolveIndexRoute(mockCoreSetup.http.createRouter());
const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value;
const handler = mockRouter.get.mock.calls[0][1];
await handler(mockContext as unknown as RequestHandlerContext, mockRequest, mockResponse);
expect(mockClient.indices.resolveIndex.mock.calls[0][0]).toMatchInlineSnapshot(`
Object {
"expand_wildcards": "open",
"name": "asdf",
}
`);
expect(mockResponse.notFound).toBeCalled();
expect(mockResponse.notFound.mock.calls[0][0]).toMatchInlineSnapshot(`
Object {
"body": Object {
"message": "no such index [asdf]",
},
}
`);
});
});

View file

@ -47,7 +47,9 @@ export function registerResolveIndexRoute(router: IRouter): void {
});
return res.ok({ body });
} catch (e) {
if (e?.meta.statusCode === 404) {
// 403: no_such_remote_cluster_exception
// 404: index_not_found_exception
if ([403, 404].includes(e?.meta.statusCode)) {
return res.notFound({ body: { message: e.meta?.body?.error?.reason } });
} else {
throw getKbnServerError(e);

View file

@ -22,10 +22,18 @@ export default function ({ getService }: FtrProviderContext) {
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.expect(200));
it('should return 404 for an exact match index', () =>
it('should return 404 when no indices match', () =>
supertest
.get(`/internal/index-pattern-management/resolve_index/test`)
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.expect(404));
it('should return 404 when cluster is not found', () =>
supertest
.get(
`/internal/index-pattern-management/resolve_index/cluster1:filebeat-*,cluster2:filebeat-*`
)
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
.expect(404));
});
}