Skip to content

Commit df705c4

Browse files
committed
Merge remote-tracking branch 'upstream/7.x' into backport/7.x/pr-72388
2 parents 31fe5db + 939f434 commit df705c4

56 files changed

Lines changed: 854 additions & 291 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@
122122
"@elastic/apm-rum": "^5.2.0",
123123
"@elastic/charts": "19.8.1",
124124
"@elastic/datemath": "5.0.3",
125-
"@elastic/elasticsearch": "7.8.0",
125+
"@elastic/elasticsearch": "7.9.0-rc.2",
126126
"@elastic/ems-client": "7.9.3",
127127
"@elastic/eui": "26.3.1",
128128
"@elastic/filesaver": "1.1.2",

packages/kbn-es/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"license": "Apache-2.0",
66
"private": true,
77
"dependencies": {
8-
"@elastic/elasticsearch": "^7.4.0",
8+
"@elastic/elasticsearch": "7.9.0-rc.1",
99
"@kbn/dev-utils": "1.0.0",
1010
"abort-controller": "^2.0.3",
1111
"chalk": "^2.4.2",
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import {
21+
ResponseError,
22+
ConnectionError,
23+
ConfigurationError,
24+
} from '@elastic/elasticsearch/lib/errors';
25+
import { ApiResponse } from '@elastic/elasticsearch';
26+
import { isResponseError, isUnauthorizedError } from './errors';
27+
28+
const createApiResponseError = ({
29+
statusCode = 200,
30+
headers = {},
31+
body = {},
32+
}: {
33+
statusCode?: number;
34+
headers?: Record<string, string>;
35+
body?: Record<string, any>;
36+
} = {}): ApiResponse => {
37+
return {
38+
body,
39+
statusCode,
40+
headers,
41+
warnings: [],
42+
meta: {} as any,
43+
};
44+
};
45+
46+
describe('isResponseError', () => {
47+
it('returns `true` when the input is a `ResponseError`', () => {
48+
expect(isResponseError(new ResponseError(createApiResponseError()))).toBe(true);
49+
});
50+
51+
it('returns `false` when the input is not a `ResponseError`', () => {
52+
expect(isResponseError(new Error('foo'))).toBe(false);
53+
expect(isResponseError(new ConnectionError('error', createApiResponseError()))).toBe(false);
54+
expect(isResponseError(new ConfigurationError('foo'))).toBe(false);
55+
});
56+
});
57+
58+
describe('isUnauthorizedError', () => {
59+
it('returns true when the input is a `ResponseError` and statusCode === 401', () => {
60+
expect(
61+
isUnauthorizedError(new ResponseError(createApiResponseError({ statusCode: 401 })))
62+
).toBe(true);
63+
});
64+
65+
it('returns false when the input is a `ResponseError` and statusCode !== 401', () => {
66+
expect(
67+
isUnauthorizedError(new ResponseError(createApiResponseError({ statusCode: 200 })))
68+
).toBe(false);
69+
expect(
70+
isUnauthorizedError(new ResponseError(createApiResponseError({ statusCode: 403 })))
71+
).toBe(false);
72+
expect(
73+
isUnauthorizedError(new ResponseError(createApiResponseError({ statusCode: 500 })))
74+
).toBe(false);
75+
});
76+
77+
it('returns `false` when the input is not a `ResponseError`', () => {
78+
expect(isUnauthorizedError(new Error('foo'))).toBe(false);
79+
expect(isUnauthorizedError(new ConnectionError('error', createApiResponseError()))).toBe(false);
80+
expect(isUnauthorizedError(new ConfigurationError('foo'))).toBe(false);
81+
});
82+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import { ResponseError } from '@elastic/elasticsearch/lib/errors';
21+
22+
export type UnauthorizedError = ResponseError & {
23+
statusCode: 401;
24+
};
25+
26+
export function isResponseError(error: any): error is ResponseError {
27+
return Boolean(error.body && error.statusCode && error.headers);
28+
}
29+
30+
export function isUnauthorizedError(error: any): error is UnauthorizedError {
31+
return isResponseError(error) && error.statusCode === 401;
32+
}

src/core/server/elasticsearch/client/mocks.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ describe('Mocked client', () => {
4949
expectMocked(client.close);
5050
});
5151

52+
it('used EventEmitter functions should be mocked', () => {
53+
expectMocked(client.on);
54+
expectMocked(client.off);
55+
expectMocked(client.once);
56+
});
57+
5258
it('`child` should be mocked and return a mocked Client', () => {
5359
expectMocked(client.child);
5460

src/core/server/elasticsearch/client/mocks.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,20 @@ const createInternalClientMock = (): DeeplyMockedKeys<Client> => {
5454

5555
mockify(client, omittedProps);
5656

57-
client.transport = {
57+
// client got some read-only (getter) properties
58+
// so we need to extend it to override the getter-only props.
59+
const mock: any = { ...client };
60+
61+
mock.transport = {
5862
request: jest.fn(),
5963
};
60-
client.close = jest.fn().mockReturnValue(Promise.resolve());
61-
client.child = jest.fn().mockImplementation(() => createInternalClientMock());
64+
mock.close = jest.fn().mockReturnValue(Promise.resolve());
65+
mock.child = jest.fn().mockImplementation(() => createInternalClientMock());
66+
mock.on = jest.fn();
67+
mock.off = jest.fn();
68+
mock.once = jest.fn();
6269

63-
return (client as unknown) as DeeplyMockedKeys<Client>;
70+
return (mock as unknown) as DeeplyMockedKeys<Client>;
6471
};
6572

6673
export type ElasticSearchClientMock = DeeplyMockedKeys<ElasticsearchClient>;

src/core/server/http/integration_tests/core_service.test.mocks.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818
*/
1919
import { elasticsearchServiceMock } from '../../elasticsearch/elasticsearch_service.mock';
2020

21-
export const clusterClientMock = jest.fn();
22-
export const clusterClientInstanceMock = elasticsearchServiceMock.createLegacyScopedClusterClient();
21+
export const MockLegacyScopedClusterClient = jest.fn();
22+
export const legacyClusterClientInstanceMock = elasticsearchServiceMock.createLegacyScopedClusterClient();
2323
jest.doMock('../../elasticsearch/legacy/scoped_cluster_client', () => ({
24-
LegacyScopedClusterClient: clusterClientMock.mockImplementation(() => clusterClientInstanceMock),
24+
LegacyScopedClusterClient: MockLegacyScopedClusterClient.mockImplementation(
25+
() => legacyClusterClientInstanceMock
26+
),
2527
}));
2628

2729
jest.doMock('elasticsearch', () => {
@@ -34,3 +36,12 @@ jest.doMock('elasticsearch', () => {
3436
},
3537
};
3638
});
39+
40+
export const MockElasticsearchClient = jest.fn();
41+
jest.doMock('@elastic/elasticsearch', () => {
42+
const real = jest.requireActual('@elastic/elasticsearch');
43+
return {
44+
...real,
45+
Client: MockElasticsearchClient,
46+
};
47+
});

src/core/server/http/integration_tests/core_services.test.ts

Lines changed: 111 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,21 @@
1717
* under the License.
1818
*/
1919

20-
import { clusterClientMock, clusterClientInstanceMock } from './core_service.test.mocks';
20+
import {
21+
MockLegacyScopedClusterClient,
22+
MockElasticsearchClient,
23+
legacyClusterClientInstanceMock,
24+
} from './core_service.test.mocks';
2125

2226
import Boom from 'boom';
2327
import { Request } from 'hapi';
2428
import { errors as esErrors } from 'elasticsearch';
2529
import { LegacyElasticsearchErrorHelpers } from '../../elasticsearch/legacy';
2630

31+
import { elasticsearchClientMock } from '../../elasticsearch/client/mocks';
32+
import { ResponseError } from '@elastic/elasticsearch/lib/errors';
2733
import * as kbnTestServer from '../../../../test_utils/kbn_server';
34+
import { InternalElasticsearchServiceStart } from '../../elasticsearch';
2835

2936
interface User {
3037
id: string;
@@ -44,6 +51,17 @@ const cookieOptions = {
4451
};
4552

4653
describe('http service', () => {
54+
let esClient: ReturnType<typeof elasticsearchClientMock.createInternalClient>;
55+
56+
beforeEach(async () => {
57+
esClient = elasticsearchClientMock.createInternalClient();
58+
MockElasticsearchClient.mockImplementation(() => esClient);
59+
}, 30000);
60+
61+
afterEach(async () => {
62+
MockElasticsearchClient.mockClear();
63+
});
64+
4765
describe('auth', () => {
4866
let root: ReturnType<typeof kbnTestServer.createRoot>;
4967
beforeEach(async () => {
@@ -200,7 +218,7 @@ describe('http service', () => {
200218
}, 30000);
201219

202220
afterEach(async () => {
203-
clusterClientMock.mockClear();
221+
MockLegacyScopedClusterClient.mockClear();
204222
await root.shutdown();
205223
});
206224

@@ -363,7 +381,7 @@ describe('http service', () => {
363381
}, 30000);
364382

365383
afterEach(async () => {
366-
clusterClientMock.mockClear();
384+
MockLegacyScopedClusterClient.mockClear();
367385
await root.shutdown();
368386
});
369387

@@ -386,7 +404,7 @@ describe('http service', () => {
386404
await kbnTestServer.request.get(root, '/new-platform/').expect(200);
387405

388406
// client contains authHeaders for BWC with legacy platform.
389-
const [client] = clusterClientMock.mock.calls;
407+
const [client] = MockLegacyScopedClusterClient.mock.calls;
390408
const [, , clientHeaders] = client;
391409
expect(clientHeaders).toEqual(authHeaders);
392410
});
@@ -410,7 +428,7 @@ describe('http service', () => {
410428
.set('Authorization', authorizationHeader)
411429
.expect(200);
412430

413-
const [client] = clusterClientMock.mock.calls;
431+
const [client] = MockLegacyScopedClusterClient.mock.calls;
414432
const [, , clientHeaders] = client;
415433
expect(clientHeaders).toEqual({ authorization: authorizationHeader });
416434
});
@@ -426,7 +444,7 @@ describe('http service', () => {
426444
})
427445
);
428446

429-
clusterClientInstanceMock.callAsCurrentUser.mockRejectedValue(authenticationError);
447+
legacyClusterClientInstanceMock.callAsCurrentUser.mockRejectedValue(authenticationError);
430448

431449
const router = createRouter('/new-platform');
432450
router.get({ path: '/', validate: false }, async (context, req, res) => {
@@ -441,4 +459,91 @@ describe('http service', () => {
441459
expect(response.header['www-authenticate']).toEqual('authenticate header');
442460
});
443461
});
462+
463+
describe('elasticsearch client', () => {
464+
let root: ReturnType<typeof kbnTestServer.createRoot>;
465+
466+
beforeEach(async () => {
467+
root = kbnTestServer.createRoot({ plugins: { initialize: false } });
468+
}, 30000);
469+
470+
afterEach(async () => {
471+
MockElasticsearchClient.mockClear();
472+
await root.shutdown();
473+
});
474+
475+
it('forwards unauthorized errors from elasticsearch', async () => {
476+
const { http } = await root.setup();
477+
const { createRouter } = http;
478+
// eslint-disable-next-line prefer-const
479+
let elasticsearch: InternalElasticsearchServiceStart;
480+
481+
esClient.ping.mockImplementation(() =>
482+
elasticsearchClientMock.createClientError(
483+
new ResponseError({
484+
statusCode: 401,
485+
body: {
486+
error: {
487+
type: 'Unauthorized',
488+
},
489+
},
490+
warnings: [],
491+
headers: {
492+
'WWW-Authenticate': 'content',
493+
},
494+
meta: {} as any,
495+
})
496+
)
497+
);
498+
499+
const router = createRouter('/new-platform');
500+
router.get({ path: '/', validate: false }, async (context, req, res) => {
501+
await elasticsearch.client.asScoped(req).asInternalUser.ping();
502+
return res.ok();
503+
});
504+
505+
const coreStart = await root.start();
506+
elasticsearch = coreStart.elasticsearch;
507+
508+
const { header } = await kbnTestServer.request.get(root, '/new-platform/').expect(401);
509+
510+
expect(header['www-authenticate']).toEqual('content');
511+
});
512+
513+
it('uses a default value for `www-authenticate` header when ES 401 does not specify it', async () => {
514+
const { http } = await root.setup();
515+
const { createRouter } = http;
516+
// eslint-disable-next-line prefer-const
517+
let elasticsearch: InternalElasticsearchServiceStart;
518+
519+
esClient.ping.mockImplementation(() =>
520+
elasticsearchClientMock.createClientError(
521+
new ResponseError({
522+
statusCode: 401,
523+
body: {
524+
error: {
525+
type: 'Unauthorized',
526+
},
527+
},
528+
warnings: [],
529+
headers: {},
530+
meta: {} as any,
531+
})
532+
)
533+
);
534+
535+
const router = createRouter('/new-platform');
536+
router.get({ path: '/', validate: false }, async (context, req, res) => {
537+
await elasticsearch.client.asScoped(req).asInternalUser.ping();
538+
return res.ok();
539+
});
540+
541+
const coreStart = await root.start();
542+
elasticsearch = coreStart.elasticsearch;
543+
544+
const { header } = await kbnTestServer.request.get(root, '/new-platform/').expect(401);
545+
546+
expect(header['www-authenticate']).toEqual('Basic realm="Authorization Required"');
547+
});
548+
});
444549
});

0 commit comments

Comments
 (0)