From 6c20c08402229506c4401f964bca67680ecbbf57 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Tue, 14 Jul 2020 17:50:01 -0600 Subject: [PATCH 01/11] Add IP type to kbn/config-schema --- packages/kbn-config-schema/src/index.ts | 7 ++ packages/kbn-config-schema/src/types/index.ts | 1 + .../src/types/ip_type.test.ts | 71 +++++++++++++++++++ .../kbn-config-schema/src/types/ip_type.ts | 46 ++++++++++++ 4 files changed, 125 insertions(+) create mode 100644 packages/kbn-config-schema/src/types/ip_type.test.ts create mode 100644 packages/kbn-config-schema/src/types/ip_type.ts diff --git a/packages/kbn-config-schema/src/index.ts b/packages/kbn-config-schema/src/index.ts index 2319fe4395e3f6..78b70743a2703a 100644 --- a/packages/kbn-config-schema/src/index.ts +++ b/packages/kbn-config-schema/src/index.ts @@ -34,6 +34,8 @@ import { ConditionalTypeValue, DurationOptions, DurationType, + IpOptions, + IpType, LiteralType, MapOfOptions, MapOfType, @@ -107,6 +109,10 @@ function never(): Type { return new NeverType(); } +function ip(options?: IpOptions): Type { + return new IpType(options); +} + /** * Create an optional type */ @@ -207,6 +213,7 @@ export const schema = { conditional, contextRef, duration, + ip, literal, mapOf, maybe, diff --git a/packages/kbn-config-schema/src/types/index.ts b/packages/kbn-config-schema/src/types/index.ts index c7900e1923e786..27be0a5060b420 100644 --- a/packages/kbn-config-schema/src/types/index.ts +++ b/packages/kbn-config-schema/src/types/index.ts @@ -36,3 +36,4 @@ export { StringOptions, StringType } from './string_type'; export { UnionType } from './union_type'; export { URIOptions, URIType } from './uri_type'; export { NeverType } from './never_type'; +export { IpType, IpOptions } from './ip_type'; diff --git a/packages/kbn-config-schema/src/types/ip_type.test.ts b/packages/kbn-config-schema/src/types/ip_type.test.ts new file mode 100644 index 00000000000000..2db3069f5d715c --- /dev/null +++ b/packages/kbn-config-schema/src/types/ip_type.test.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '..'; + +const { ip } = schema; + +describe('ip validation', () => { + test('accepts ipv4', () => { + expect(ip().validate('1.1.1.1')).toEqual('1.1.1.1'); + }); + test('accepts ipv6', () => { + expect(ip().validate('1200:0000:AB00:1234:0000:2552:7777:1313')).toEqual( + '1200:0000:AB00:1234:0000:2552:7777:1313' + ); + }); + test('rejects ipv6 when not specified', () => { + expect(() => + ip({ versions: ['ipv4'] }).validate('1200:0000:AB00:1234:0000:2552:7777:1313') + ).toThrowErrorMatchingInlineSnapshot(`"value must be a valid ipv4 address"`); + }); + test('rejects ipv4 when not specified', () => { + expect(() => ip({ versions: ['ipv6'] }).validate('1.1.1.1')).toThrowErrorMatchingInlineSnapshot( + `"value must be a valid ipv6 address"` + ); + }); + test('rejects invalid ip addresses', () => { + expect(() => ip().validate('1.1.1.1/24')).toThrowErrorMatchingInlineSnapshot( + `"value must be a valid ipv4 or ipv6 address"` + ); + expect(() => ip().validate('99999.1.1.1')).toThrowErrorMatchingInlineSnapshot( + `"value must be a valid ipv4 or ipv6 address"` + ); + expect(() => + ip().validate('ZZZZ:0000:AB00:1234:0000:2552:7777:1313') + ).toThrowErrorMatchingInlineSnapshot(`"value must be a valid ipv4 or ipv6 address"`); + expect(() => ip().validate('blah 1234')).toThrowErrorMatchingInlineSnapshot( + `"value must be a valid ipv4 or ipv6 address"` + ); + }); +}); + +test('returns error when not string', () => { + expect(() => ip().validate(123)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [number]"` + ); + + expect(() => ip().validate([1, 2, 3])).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [Array]"` + ); + + expect(() => ip().validate(/abc/)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [string] but got [RegExp]"` + ); +}); diff --git a/packages/kbn-config-schema/src/types/ip_type.ts b/packages/kbn-config-schema/src/types/ip_type.ts new file mode 100644 index 00000000000000..2bc1a8ea3b2112 --- /dev/null +++ b/packages/kbn-config-schema/src/types/ip_type.ts @@ -0,0 +1,46 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import typeDetect from 'type-detect'; +import { internals } from '../internals'; +import { Type, TypeOptions } from './type'; + +export type IpVersion = 'ipv4' | 'ipv6'; +export type IpOptions = TypeOptions & { + /** + * IP versions to accept, defaults to ['ipv4', 'ipv6']. + */ + versions: IpVersion[]; +}; + +export class IpType extends Type { + constructor(options: IpOptions = { versions: ['ipv4', 'ipv6'] }) { + const schema = internals.string().ip({ version: options.versions, cidr: 'forbidden' }); + super(schema, options); + } + + protected handleError(type: string, { value, version }: Record) { + switch (type) { + case 'string.base': + return `expected value of type [string] but got [${typeDetect(value)}]`; + case 'string.ipVersion': + return `value must be a valid ${version.join(' or ')} address`; + } + } +} From b638bcdc092290072360f89f62f12267beb89cc2 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Tue, 14 Jul 2020 17:50:11 -0600 Subject: [PATCH 02/11] Add support for reading request ID from X-Opaque-Id header --- ...ver.httpservicesetup.registeronpostauth.md | 2 +- ...rver.httpservicesetup.registeronpreauth.md | 2 +- ...ana-plugin-core-server.kibanarequest.id.md | 18 ++++ ...kibana-plugin-core-server.kibanarequest.md | 1 + .../core/server/kibana-plugin-core-server.md | 2 +- ...plugin-core-server.onpreresponsetoolkit.md | 2 +- docs/setup/settings.asciidoc | 6 ++ .../legacy/cluster_client.test.ts | 16 +++- .../elasticsearch/legacy/cluster_client.ts | 8 +- .../__snapshots__/http_config.test.ts.snap | 4 + .../http/cookie_session_storage.test.ts | 4 + src/core/server/http/http_config.test.ts | 56 ++++++++++++ src/core/server/http/http_config.ts | 15 ++++ src/core/server/http/http_server.mocks.ts | 13 ++- src/core/server/http/http_server.test.ts | 6 +- src/core/server/http/http_server.ts | 24 ++++- src/core/server/http/http_tools.test.ts | 89 ++++++++++++++++++- src/core/server/http/http_tools.ts | 8 ++ .../http/integration_tests/request.test.ts | 20 +++++ .../server/http/lifecycle_handlers.test.ts | 15 ++-- src/core/server/http/router/index.ts | 3 +- src/core/server/http/router/request.ts | 24 ++++- src/core/server/http/test_utils.ts | 4 + src/core/server/server.api.md | 1 + .../server/client/audit_trail_client.test.ts | 15 +++- .../server/client/audit_trail_client.ts | 1 + x-pack/plugins/audit_trail/server/types.ts | 1 + 27 files changed, 332 insertions(+), 28 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.kibanarequest.id.md diff --git a/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.registeronpostauth.md b/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.registeronpostauth.md index eff53b7b75fa59..41b82f428948a0 100644 --- a/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.registeronpostauth.md +++ b/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.registeronpostauth.md @@ -14,5 +14,5 @@ registerOnPostAuth: (handler: OnPostAuthHandler) => void; ## Remarks -The auth state is available at stage via http.auth.get(..) Can register any number of registerOnPreRouting, which are called in sequence (from the first registered to the last). See [OnPostAuthHandler](./kibana-plugin-core-server.onpostauthhandler.md). +The auth state is available at stage via http.auth.get(..) Can register any number of registerOnPostAuth, which are called in sequence (from the first registered to the last). See [OnPostAuthHandler](./kibana-plugin-core-server.onpostauthhandler.md). diff --git a/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.registeronpreauth.md b/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.registeronpreauth.md index ce4cacb1c87490..57b1833df5e032 100644 --- a/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.registeronpreauth.md +++ b/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.registeronpreauth.md @@ -14,5 +14,5 @@ registerOnPreAuth: (handler: OnPreAuthHandler) => void; ## Remarks -Can register any number of registerOnPostAuth, which are called in sequence (from the first registered to the last). See [OnPreRoutingHandler](./kibana-plugin-core-server.onpreroutinghandler.md). +Can register any number of registerOnPreAuth, which are called in sequence (from the first registered to the last). See [OnPreAuthHandler](./kibana-plugin-core-server.onpreauthhandler.md). diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanarequest.id.md b/docs/development/core/server/kibana-plugin-core-server.kibanarequest.id.md new file mode 100644 index 00000000000000..8cad5972cc164c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.kibanarequest.id.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [KibanaRequest](./kibana-plugin-core-server.kibanarequest.md) > [id](./kibana-plugin-core-server.kibanarequest.id.md) + +## KibanaRequest.id property + +A identifier to identify this request. + +Signature: + +```typescript +readonly id: string; +``` + +## Remarks + +Depending on the user's configuration, this value may be sourced from the incoming request's `X-Opaque-Id` header which is not guaranteed to be unique per request. + diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanarequest.md b/docs/development/core/server/kibana-plugin-core-server.kibanarequest.md index 3e6fba835c396f..05e408ab499952 100644 --- a/docs/development/core/server/kibana-plugin-core-server.kibanarequest.md +++ b/docs/development/core/server/kibana-plugin-core-server.kibanarequest.md @@ -26,6 +26,7 @@ export declare class KibanaRequestBody | | | [events](./kibana-plugin-core-server.kibanarequest.events.md) | | KibanaRequestEvents | Request events [KibanaRequestEvents](./kibana-plugin-core-server.kibanarequestevents.md) | | [headers](./kibana-plugin-core-server.kibanarequest.headers.md) | | Headers | Readonly copy of incoming request headers. | +| [id](./kibana-plugin-core-server.kibanarequest.id.md) | | string | A identifier to identify this request. | | [isSystemRequest](./kibana-plugin-core-server.kibanarequest.issystemrequest.md) | | boolean | Whether or not the request is a "system request" rather than an application-level request. Can be set on the client using the HttpFetchOptions#asSystemRequest option. | | [params](./kibana-plugin-core-server.kibanarequest.params.md) | | Params | | | [query](./kibana-plugin-core-server.kibanarequest.query.md) | | Query | | diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index a665327454c1a6..61ffc532f0de5b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -122,7 +122,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [OnPreAuthToolkit](./kibana-plugin-core-server.onpreauthtoolkit.md) | A tool set defining an outcome of OnPreAuth interceptor for incoming request. | | [OnPreResponseExtensions](./kibana-plugin-core-server.onpreresponseextensions.md) | Additional data to extend a response. | | [OnPreResponseInfo](./kibana-plugin-core-server.onpreresponseinfo.md) | Response status code. | -| [OnPreResponseToolkit](./kibana-plugin-core-server.onpreresponsetoolkit.md) | A tool set defining an outcome of OnPreRouting interceptor for incoming request. | +| [OnPreResponseToolkit](./kibana-plugin-core-server.onpreresponsetoolkit.md) | A tool set defining an outcome of OnPreResponse interceptor for incoming request. | | [OnPreRoutingToolkit](./kibana-plugin-core-server.onpreroutingtoolkit.md) | A tool set defining an outcome of OnPreRouting interceptor for incoming request. | | [OpsMetrics](./kibana-plugin-core-server.opsmetrics.md) | Regroups metrics gathered by all the collectors. This contains metrics about the os/runtime, the kibana process and the http server. | | [OpsOsMetrics](./kibana-plugin-core-server.opsosmetrics.md) | OS related metrics | diff --git a/docs/development/core/server/kibana-plugin-core-server.onpreresponsetoolkit.md b/docs/development/core/server/kibana-plugin-core-server.onpreresponsetoolkit.md index 306c375ba4a3c3..44da09d0cc68eb 100644 --- a/docs/development/core/server/kibana-plugin-core-server.onpreresponsetoolkit.md +++ b/docs/development/core/server/kibana-plugin-core-server.onpreresponsetoolkit.md @@ -4,7 +4,7 @@ ## OnPreResponseToolkit interface -A tool set defining an outcome of OnPreRouting interceptor for incoming request. +A tool set defining an outcome of OnPreResponse interceptor for incoming request. Signature: diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 01a9e96484965a..257633e590cb25 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -476,6 +476,12 @@ identifies this {kib} instance. *Default: `"your-hostname"`* | {kib} is served by a back end server. This setting specifies the port to use. *Default: `5601`* +| `server.requestOpaqueId.allowFromAnyIp:` + | Sets whether or not the X-Opaque-Id header should be trusted from any IP address for identifying requests in logs and forwarded to Elasticsearch. + +| `server.requestOpaqueId.ipAllowlist:` + | A list of IPv4 and IPv6 address which the `X-Opaque-Id` header should be trusted from. Normally this would be set to the IP addresses of the load balancers or reverse-proxy that end users use to access Kibana. If any are set, `server.requestOpaqueId.allowFromAnyIp` must also be set to `false.` + | `server.rewriteBasePath:` | Specifies whether {kib} should rewrite requests that are prefixed with `server.basePath` or require that they diff --git a/src/core/server/elasticsearch/legacy/cluster_client.test.ts b/src/core/server/elasticsearch/legacy/cluster_client.test.ts index fd57d06e61eee3..73d941053e84b7 100644 --- a/src/core/server/elasticsearch/legacy/cluster_client.test.ts +++ b/src/core/server/elasticsearch/legacy/cluster_client.test.ts @@ -349,6 +349,20 @@ describe('#asScoped', () => { ); }); + test('passes x-opaque-id header with request id', () => { + clusterClient.asScoped( + httpServerMock.createKibanaRequest({ kibanaRequestState: { requestId: 'alpha' } }) + ); + + expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); + expect(MockScopedClusterClient).toHaveBeenCalledWith( + expect.any(Function), + expect.any(Function), + { 'x-opaque-id': 'alpha' }, + expect.any(Object) + ); + }); + test('both scoped and internal API caller fail if cluster client is closed', async () => { clusterClient.asScoped( httpServerMock.createRawRequest({ headers: { zero: '0', one: '1', two: '2', three: '3' } }) @@ -482,7 +496,7 @@ describe('#asScoped', () => { expect(MockScopedClusterClient).toHaveBeenCalledWith( expect.any(Function), expect.any(Function), - {}, + expect.objectContaining({ 'x-opaque-id': expect.any(String) }), auditor ); }); diff --git a/src/core/server/elasticsearch/legacy/cluster_client.ts b/src/core/server/elasticsearch/legacy/cluster_client.ts index 7a39113d25a14d..38ed060298cc4d 100644 --- a/src/core/server/elasticsearch/legacy/cluster_client.ts +++ b/src/core/server/elasticsearch/legacy/cluster_client.ts @@ -205,7 +205,10 @@ export class LegacyClusterClient implements ILegacyClusterClient { return new LegacyScopedClusterClient( this.callAsInternalUser, this.callAsCurrentUser, - filterHeaders(this.getHeaders(request), this.config.requestHeadersWhitelist), + filterHeaders(this.getHeaders(request), [ + 'x-opaque-id', + ...this.config.requestHeadersWhitelist, + ]), this.getScopedAuditor(request) ); } @@ -255,7 +258,8 @@ export class LegacyClusterClient implements ILegacyClusterClient { } const authHeaders = this.getAuthHeaders(request); const headers = ensureRawRequest(request).headers; + const requestIdHeaders = request instanceof KibanaRequest ? { 'x-opaque-id': request.id } : {}; - return { ...headers, ...authHeaders }; + return { ...headers, ...authHeaders, ...requestIdHeaders }; } } diff --git a/src/core/server/http/__snapshots__/http_config.test.ts.snap b/src/core/server/http/__snapshots__/http_config.test.ts.snap index d48ead3cec8e13..23cd45548790ef 100644 --- a/src/core/server/http/__snapshots__/http_config.test.ts.snap +++ b/src/core/server/http/__snapshots__/http_config.test.ts.snap @@ -39,6 +39,10 @@ Object { }, "name": "kibana-hostname", "port": 5601, + "requestOpaqueId": Object { + "allowFromAnyIp": true, + "ipAllowlist": Array [], + }, "rewriteBasePath": false, "socketTimeout": 120000, "ssl": Object { diff --git a/src/core/server/http/cookie_session_storage.test.ts b/src/core/server/http/cookie_session_storage.test.ts index 1fb2b5693bb614..e743e419405406 100644 --- a/src/core/server/http/cookie_session_storage.test.ts +++ b/src/core/server/http/cookie_session_storage.test.ts @@ -63,6 +63,10 @@ configService.atPath.mockReturnValue( whitelist: [], }, customResponseHeaders: {}, + requestOpaqueId: { + allowFromAnyIp: true, + ipAllowlist: [], + }, } as any) ); diff --git a/src/core/server/http/http_config.test.ts b/src/core/server/http/http_config.test.ts index 0698f118be03fa..e71d4cb47c171f 100644 --- a/src/core/server/http/http_config.test.ts +++ b/src/core/server/http/http_config.test.ts @@ -54,6 +54,62 @@ test('throws if invalid hostname', () => { expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot(); }); +describe('requestOpaqueId', () => { + test('accepts valid ip addresses', () => { + const { + requestOpaqueId: { ipAllowlist }, + } = config.schema.validate({ + requestOpaqueId: { + allowFromAnyIp: false, + ipAllowlist: ['0.0.0.0', '123.123.123.123', '1200:0000:AB00:1234:0000:2552:7777:1313'], + }, + }); + expect(ipAllowlist).toMatchInlineSnapshot(` + Array [ + "0.0.0.0", + "123.123.123.123", + "1200:0000:AB00:1234:0000:2552:7777:1313", + ] + `); + }); + + test('rejects invalid ip addresses', () => { + expect(() => { + config.schema.validate({ + requestOpaqueId: { + allowFromAnyIp: false, + ipAllowlist: ['1200:0000:AB00:1234:O000:2552:7777:1313', '[2001:db8:0:1]:80'], + }, + }); + }).toThrowErrorMatchingInlineSnapshot( + `"[requestOpaqueId.ipAllowlist.0]: value must be a valid ipv4 or ipv6 address"` + ); + }); + + test('rejects if allowFromAnyIp is `true` and `ipAllowlist` is non-empty', () => { + expect(() => { + config.schema.validate({ + requestOpaqueId: { + allowFromAnyIp: true, + ipAllowlist: ['0.0.0.0', '123.123.123.123', '1200:0000:AB00:1234:0000:2552:7777:1313'], + }, + }); + }).toThrowErrorMatchingInlineSnapshot( + `"[requestOpaqueId]: allowFromAnyIp must be set to 'false' if any values are specified in ipAllowlist"` + ); + + expect(() => { + config.schema.validate({ + requestOpaqueId: { + ipAllowlist: ['0.0.0.0', '123.123.123.123', '1200:0000:AB00:1234:0000:2552:7777:1313'], + }, + }); + }).toThrowErrorMatchingInlineSnapshot( + `"[requestOpaqueId]: allowFromAnyIp must be set to 'false' if any values are specified in ipAllowlist"` + ); + }); +}); + test('can specify max payload as string', () => { const obj = { maxPayload: '2mb', diff --git a/src/core/server/http/http_config.ts b/src/core/server/http/http_config.ts index 83a2e712b424fd..9ba5f027245c3e 100644 --- a/src/core/server/http/http_config.ts +++ b/src/core/server/http/http_config.ts @@ -101,6 +101,19 @@ export const config = { { defaultValue: [] } ), }), + requestOpaqueId: schema.object( + { + allowFromAnyIp: schema.boolean({ defaultValue: true }), + ipAllowlist: schema.arrayOf(schema.ip(), { defaultValue: [] }), + }, + { + validate(value) { + if (value.allowFromAnyIp === true && value.ipAllowlist?.length > 0) { + return `allowFromAnyIp must be set to 'false' if any values are specified in ipAllowlist`; + } + }, + } + ), }, { validate: (rawConfig) => { @@ -144,6 +157,7 @@ export class HttpConfig { public compression: { enabled: boolean; referrerWhitelist?: string[] }; public csp: ICspConfig; public xsrf: { disableProtection: boolean; whitelist: string[] }; + public requestOpaqueId: { allowFromAnyIp: boolean; ipAllowlist: string[] }; /** * @internal @@ -172,6 +186,7 @@ export class HttpConfig { this.compression = rawHttpConfig.compression; this.csp = new CspConfig(rawCspConfig); this.xsrf = rawHttpConfig.xsrf; + this.requestOpaqueId = rawHttpConfig.requestOpaqueId; } } diff --git a/src/core/server/http/http_server.mocks.ts b/src/core/server/http/http_server.mocks.ts index 7d37af833d4c17..4afd238c7afc7e 100644 --- a/src/core/server/http/http_server.mocks.ts +++ b/src/core/server/http/http_server.mocks.ts @@ -29,7 +29,8 @@ import { RouteMethod, KibanaResponseFactory, RouteValidationSpec, - KibanaRouteState, + KibanaRouteOptions, + KibanaRequestState, } from './router'; import { OnPreResponseToolkit } from './lifecycle/on_pre_response'; import { OnPostAuthToolkit } from './lifecycle/on_post_auth'; @@ -45,7 +46,8 @@ interface RequestFixtureOptions

{ method?: RouteMethod; socket?: Socket; routeTags?: string[]; - kibanaRouteState?: KibanaRouteState; + kibanaRouteOptions?: KibanaRouteOptions; + kibanaRequestState?: KibanaRequestState; routeAuthRequired?: false; validation?: { params?: RouteValidationSpec

; @@ -65,13 +67,15 @@ function createKibanaRequestMock

({ routeTags, routeAuthRequired, validation = {}, - kibanaRouteState = { xsrfRequired: true }, + kibanaRouteOptions = { xsrfRequired: true }, + kibanaRequestState = { requestId: '123' }, auth = { isAuthenticated: true }, }: RequestFixtureOptions = {}) { const queryString = stringify(query, { sort: false }); return KibanaRequest.from( createRawRequestMock({ + app: kibanaRequestState, auth, headers, params, @@ -86,7 +90,7 @@ function createKibanaRequestMock

({ search: queryString ? `?${queryString}` : queryString, }, route: { - settings: { tags: routeTags, auth: routeAuthRequired, app: kibanaRouteState }, + settings: { tags: routeTags, auth: routeAuthRequired, app: kibanaRouteOptions }, }, raw: { req: { socket }, @@ -128,6 +132,7 @@ function createRawRequestMock(customization: DeepPartial = {}) { raw: { req: { url: '/', + socket: {}, }, }, }, diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index 601eba835a54e8..90f5eac4968e65 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -68,7 +68,11 @@ beforeEach(() => { port: 10002, ssl: { enabled: false }, compression: { enabled: true }, - } as HttpConfig; + requestOpaqueId: { + allowFromAnyIp: true, + ipAllowlist: [], + }, + } as any; configWithSSL = { ...config, diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 9c16162d693348..1fe8a0d4b417fa 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -22,13 +22,19 @@ import url from 'url'; import { Logger, LoggerFactory } from '../logging'; import { HttpConfig } from './http_config'; -import { createServer, getListenerOptions, getServerOptions } from './http_tools'; +import { createServer, getListenerOptions, getServerOptions, getRequestId } from './http_tools'; import { adoptToHapiAuthFormat, AuthenticationHandler } from './lifecycle/auth'; import { adoptToHapiOnPreAuth, OnPreAuthHandler } from './lifecycle/on_pre_auth'; import { adoptToHapiOnPostAuthFormat, OnPostAuthHandler } from './lifecycle/on_post_auth'; import { adoptToHapiOnRequest, OnPreRoutingHandler } from './lifecycle/on_pre_routing'; import { adoptToHapiOnPreResponseFormat, OnPreResponseHandler } from './lifecycle/on_pre_response'; -import { IRouter, RouteConfigOptions, KibanaRouteState, isSafeMethod } from './router'; +import { + IRouter, + RouteConfigOptions, + KibanaRouteOptions, + KibanaRequestState, + isSafeMethod, +} from './router'; import { SessionStorageCookieOptions, createCookieSessionStorageFactory, @@ -115,6 +121,7 @@ export class HttpServer { const basePathService = new BasePath(config.basePath); this.setupBasePathRewrite(config, basePathService); this.setupConditionalCompression(config); + this.setupRequestStateAssignment(config); return { registerRouter: this.registerRouter.bind(this), @@ -164,7 +171,7 @@ export class HttpServer { const { authRequired, tags, body = {} } = route.options; const { accepts: allow, maxBytes, output, parse } = body; - const kibanaRouteState: KibanaRouteState = { + const kibanaRouteOptions: KibanaRouteOptions = { xsrfRequired: route.options.xsrfRequired ?? !isSafeMethod(route.method), }; @@ -174,7 +181,7 @@ export class HttpServer { path: route.path, options: { auth: this.getAuthOption(authRequired), - app: kibanaRouteState, + app: kibanaRouteOptions, tags: tags ? Array.from(tags) : undefined, // TODO: This 'validate' section can be removed once the legacy platform is completely removed. // We are telling Hapi that NP routes can accept any payload, so that it can bypass the default @@ -270,6 +277,15 @@ export class HttpServer { } } + private setupRequestStateAssignment(config: HttpConfig) { + this.server!.ext('onRequest', (request, responseToolkit) => { + request.app = { + requestId: getRequestId(request, config.requestOpaqueId), + } as KibanaRequestState; + return responseToolkit.continue; + }); + } + private registerOnPreAuth(fn: OnPreAuthHandler) { if (this.server === undefined) { throw new Error('Server is not created yet'); diff --git a/src/core/server/http/http_tools.test.ts b/src/core/server/http/http_tools.test.ts index f09d862f9edace..4d5f077f6b4c5b 100644 --- a/src/core/server/http/http_tools.test.ts +++ b/src/core/server/http/http_tools.test.ts @@ -26,11 +26,20 @@ jest.mock('fs', () => { }; }); +jest.mock('uuid', () => ({ + v4: jest.fn().mockReturnValue('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'), +})); + import supertest from 'supertest'; import { Request, ResponseToolkit } from 'hapi'; import Joi from 'joi'; -import { defaultValidationErrorHandler, HapiValidationError, getServerOptions } from './http_tools'; +import { + defaultValidationErrorHandler, + HapiValidationError, + getServerOptions, + getRequestId, +} from './http_tools'; import { HttpServer } from './http_server'; import { HttpConfig, config } from './http_config'; import { Router } from './router'; @@ -94,7 +103,11 @@ describe('timeouts', () => { maxPayload: new ByteSizeValue(1024), ssl: {}, compression: { enabled: true }, - } as HttpConfig); + requestOpaqueId: { + allowFromAnyIp: true, + ipAllowlist: [], + }, + } as any); registerRouter(router); await server.start(); @@ -173,3 +186,75 @@ describe('getServerOptions', () => { `); }); }); + +describe('getRequestId', () => { + describe('when allowFromAnyIp is true', () => { + it('generates a UUID if no x-opaque-id header is present', () => { + const request = { + headers: {}, + raw: { req: { socket: { remoteAddress: '1.1.1.1' } } }, + } as any; + expect(getRequestId(request, { allowFromAnyIp: true, ipAllowlist: [] })).toEqual( + 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' + ); + }); + + it('uses x-opaque-id header value if present', () => { + const request = { + headers: { + 'x-opaque-id': 'id from header', + raw: { req: { socket: { remoteAddress: '1.1.1.1' } } }, + }, + } as any; + expect(getRequestId(request, { allowFromAnyIp: true, ipAllowlist: [] })).toEqual( + 'id from header' + ); + }); + }); + + describe('when allowFromAnyIp is false', () => { + describe('and ipAllowlist is empty', () => { + it('generates a UUID even if x-opaque-id header is present', () => { + const request = { + headers: { 'x-opaque-id': 'id from header' }, + raw: { req: { socket: { remoteAddress: '1.1.1.1' } } }, + } as any; + expect(getRequestId(request, { allowFromAnyIp: false, ipAllowlist: [] })).toEqual( + 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' + ); + }); + }); + + describe('and ipAllowlist is not empty', () => { + it('uses x-opaque-id header if request comes from trusted IP address', () => { + const request = { + headers: { 'x-opaque-id': 'id from header' }, + raw: { req: { socket: { remoteAddress: '1.1.1.1' } } }, + } as any; + expect(getRequestId(request, { allowFromAnyIp: false, ipAllowlist: ['1.1.1.1'] })).toEqual( + 'id from header' + ); + }); + + it('generates a UUID if request comes from untrusted IP address', () => { + const request = { + headers: { 'x-opaque-id': 'id from header' }, + raw: { req: { socket: { remoteAddress: '5.5.5.5' } } }, + } as any; + expect(getRequestId(request, { allowFromAnyIp: false, ipAllowlist: ['1.1.1.1'] })).toEqual( + 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' + ); + }); + + it('generates UUID if request comes from trusted IP address but no x-opaque-id header is present', () => { + const request = { + headers: {}, + raw: { req: { socket: { remoteAddress: '1.1.1.1' } } }, + } as any; + expect(getRequestId(request, { allowFromAnyIp: false, ipAllowlist: ['1.1.1.1'] })).toEqual( + 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' + ); + }); + }); + }); +}); diff --git a/src/core/server/http/http_tools.ts b/src/core/server/http/http_tools.ts index 4e47cf492e287e..5849e81d3ae885 100644 --- a/src/core/server/http/http_tools.ts +++ b/src/core/server/http/http_tools.ts @@ -21,6 +21,7 @@ import { Lifecycle, Request, ResponseToolkit, Server, ServerOptions, Util } from import Hoek from 'hoek'; import { ServerOptions as TLSOptions } from 'https'; import { ValidationError } from 'joi'; +import uuid from 'uuid'; import { HttpConfig } from './http_config'; import { validateObject } from './prototype_pollution'; @@ -169,3 +170,10 @@ export function defaultValidationErrorHandler( throw err; } + +export function getRequestId(request: Request, options: HttpConfig['requestOpaqueId']): string { + return options.allowFromAnyIp || + options.ipAllowlist.includes(request.raw.req.socket.remoteAddress!) + ? request.headers['x-opaque-id'] ?? uuid.v4() + : uuid.v4(); +} diff --git a/src/core/server/http/integration_tests/request.test.ts b/src/core/server/http/integration_tests/request.test.ts index 2d018f7f464b5d..5528b2f28abb02 100644 --- a/src/core/server/http/integration_tests/request.test.ts +++ b/src/core/server/http/integration_tests/request.test.ts @@ -197,4 +197,24 @@ describe('KibanaRequest', () => { }); }); }); + + describe('request id', () => { + it('accepts x-opaque-id header case-insensitively', async () => { + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + router.get({ path: '/', validate: false }, async (context, req, res) => { + return res.ok({ body: { requestId: req.id } }); + }); + await server.start(); + + const st = supertest(innerServer.listener); + + const resp1 = await st.get('/').set({ 'x-opaque-id': 'alpha' }).expect(200); + expect(resp1.body).toEqual({ requestId: 'alpha' }); + const resp2 = await st.get('/').set({ 'X-Opaque-Id': 'beta' }).expect(200); + expect(resp2.body).toEqual({ requestId: 'beta' }); + const resp3 = await st.get('/').set({ 'X-OPAQUE-ID': 'gamma' }).expect(200); + expect(resp3.body).toEqual({ requestId: 'gamma' }); + }); + }); }); diff --git a/src/core/server/http/lifecycle_handlers.test.ts b/src/core/server/http/lifecycle_handlers.test.ts index a80e432e0d4cb7..fdcf2a173b906a 100644 --- a/src/core/server/http/lifecycle_handlers.test.ts +++ b/src/core/server/http/lifecycle_handlers.test.ts @@ -24,7 +24,7 @@ import { } from './lifecycle_handlers'; import { httpServerMock } from './http_server.mocks'; import { HttpConfig } from './http_config'; -import { KibanaRequest, RouteMethod, KibanaRouteState } from './router'; +import { KibanaRequest, RouteMethod, KibanaRouteOptions } from './router'; const createConfig = (partial: Partial): HttpConfig => partial as HttpConfig; @@ -32,14 +32,19 @@ const forgeRequest = ({ headers = {}, path = '/', method = 'get', - kibanaRouteState, + kibanaRouteOptions, }: Partial<{ headers: Record; path: string; method: RouteMethod; - kibanaRouteState: KibanaRouteState; + kibanaRouteOptions: KibanaRouteOptions; }>): KibanaRequest => { - return httpServerMock.createKibanaRequest({ headers, path, method, kibanaRouteState }); + return httpServerMock.createKibanaRequest({ + headers, + path, + method, + kibanaRouteOptions, + }); }; describe('xsrf post-auth handler', () => { @@ -154,7 +159,7 @@ describe('xsrf post-auth handler', () => { method: 'post', headers: {}, path: '/some-path', - kibanaRouteState: { + kibanaRouteOptions: { xsrfRequired: false, }, }); diff --git a/src/core/server/http/router/index.ts b/src/core/server/http/router/index.ts index 83ceff4a25d86d..27aee8fb8ebd8c 100644 --- a/src/core/server/http/router/index.ts +++ b/src/core/server/http/router/index.ts @@ -24,7 +24,8 @@ export { KibanaRequestEvents, KibanaRequestRoute, KibanaRequestRouteOptions, - KibanaRouteState, + KibanaRouteOptions, + KibanaRequestState, isRealRequest, LegacyRequest, ensureRawRequest, diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts index fefd75ad9710e9..cac05b6873f9f5 100644 --- a/src/core/server/http/router/request.ts +++ b/src/core/server/http/router/request.ts @@ -18,7 +18,7 @@ */ import { Url } from 'url'; -import { Request, ApplicationState } from 'hapi'; +import { Request, RouteOptionsApp, ApplicationState } from 'hapi'; import { Observable, fromEvent, merge } from 'rxjs'; import { shareReplay, first, takeUntil } from 'rxjs/operators'; import { RecursiveReadonly } from '@kbn/utility-types'; @@ -34,9 +34,17 @@ const requestSymbol = Symbol('request'); /** * @internal */ -export interface KibanaRouteState extends ApplicationState { +export interface KibanaRouteOptions extends RouteOptionsApp { xsrfRequired: boolean; } + +/** + * @internal + */ +export interface KibanaRequestState extends ApplicationState { + requestId: string; +} + /** * Route options: If 'GET' or 'OPTIONS' method, body options won't be returned. * @public @@ -124,6 +132,15 @@ export class KibanaRequest< return { query, params, body }; } + /** + * A identifier to identify this request. + * + * @remarks + * Depending on the user's configuration, this value may be sourced from the + * incoming request's `X-Opaque-Id` header which is not guaranteed to be unique + * per request. + */ + public readonly id: string; /** a WHATWG URL standard object. */ public readonly url: Url; /** matched route details */ @@ -161,6 +178,7 @@ export class KibanaRequest< // until that time we have to expose all the headers private readonly withoutSecretHeaders: boolean ) { + this.id = (request.app as KibanaRequestState).requestId; this.url = request.url; this.headers = deepFreeze({ ...request.headers }); this.isSystemRequest = @@ -201,7 +219,7 @@ export class KibanaRequest< const options = ({ authRequired: this.getAuthRequired(request), // some places in LP call KibanaRequest.from(request) manually. remove fallback to true before v8 - xsrfRequired: (request.route.settings.app as KibanaRouteState)?.xsrfRequired ?? true, + xsrfRequired: (request.route.settings.app as KibanaRouteOptions)?.xsrfRequired ?? true, tags: request.route.settings.tags || [], body: isSafeMethod(method) ? undefined diff --git a/src/core/server/http/test_utils.ts b/src/core/server/http/test_utils.ts index bda66e1de8168f..b6a7be3f0e63c2 100644 --- a/src/core/server/http/test_utils.ts +++ b/src/core/server/http/test_utils.ts @@ -46,6 +46,10 @@ configService.atPath.mockReturnValue( whitelist: [], }, customResponseHeaders: {}, + requestOpaqueId: { + allowFromAnyIp: true, + ipAllowlist: [], + }, } as any) ); diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index a0e16602ba4bfe..d604b9dbc2e4a3 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -960,6 +960,7 @@ export class KibanaRequest(req: Request, routeSchemas?: RouteValidator | RouteValidatorFullConfig, withoutSecretHeaders?: boolean): KibanaRequest; readonly headers: Headers; + readonly id: string; readonly isSystemRequest: boolean; // (undocumented) readonly params: Params; diff --git a/x-pack/plugins/audit_trail/server/client/audit_trail_client.test.ts b/x-pack/plugins/audit_trail/server/client/audit_trail_client.test.ts index cdc0aa4cfd7e74..45a192e40c87bf 100644 --- a/x-pack/plugins/audit_trail/server/client/audit_trail_client.test.ts +++ b/x-pack/plugins/audit_trail/server/client/audit_trail_client.test.ts @@ -23,7 +23,11 @@ describe('AuditTrailClient', () => { beforeEach(() => { event$ = new Subject(); - client = new AuditTrailClient(httpServerMock.createKibanaRequest(), event$, deps); + client = new AuditTrailClient( + httpServerMock.createKibanaRequest({ kibanaRequestState: { requestId: 'request id alpha' } }), + event$, + deps + ); }); afterEach(() => { @@ -40,6 +44,15 @@ describe('AuditTrailClient', () => { client.add({ message: 'message', type: 'type' }); }); + it('populates requestId', (done) => { + client.withAuditScope('scope_name'); + event$.subscribe((event) => { + expect(event.requestId).toBe('request id alpha'); + done(); + }); + client.add({ message: 'message', type: 'type' }); + }); + it('throws an exception if tries to re-write a scope', () => { client.withAuditScope('scope_name'); expect(() => client.withAuditScope('another_scope_name')).toThrowErrorMatchingInlineSnapshot( diff --git a/x-pack/plugins/audit_trail/server/client/audit_trail_client.ts b/x-pack/plugins/audit_trail/server/client/audit_trail_client.ts index f12977cddaf0bf..e5022234af9d7a 100644 --- a/x-pack/plugins/audit_trail/server/client/audit_trail_client.ts +++ b/x-pack/plugins/audit_trail/server/client/audit_trail_client.ts @@ -41,6 +41,7 @@ export class AuditTrailClient implements Auditor { user: user?.username, space: spaceId, scope: this.scope, + requestId: this.request.id, }); } } diff --git a/x-pack/plugins/audit_trail/server/types.ts b/x-pack/plugins/audit_trail/server/types.ts index d0eb0e7eaa981c..1b7afb09f06290 100644 --- a/x-pack/plugins/audit_trail/server/types.ts +++ b/x-pack/plugins/audit_trail/server/types.ts @@ -13,4 +13,5 @@ export interface AuditEvent { scope?: string; user?: string; space?: string; + requestId?: string; } From 891f3a723c86d0e025e3f6e0e566fea7b08d9fc7 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Thu, 30 Jul 2020 11:43:04 -0600 Subject: [PATCH 03/11] Default allowFromAnyIp to false --- src/core/server/http/__snapshots__/http_config.test.ts.snap | 2 +- src/core/server/http/http_config.test.ts | 1 + src/core/server/http/http_config.ts | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core/server/http/__snapshots__/http_config.test.ts.snap b/src/core/server/http/__snapshots__/http_config.test.ts.snap index 23cd45548790ef..5971c971fdc567 100644 --- a/src/core/server/http/__snapshots__/http_config.test.ts.snap +++ b/src/core/server/http/__snapshots__/http_config.test.ts.snap @@ -40,7 +40,7 @@ Object { "name": "kibana-hostname", "port": 5601, "requestOpaqueId": Object { - "allowFromAnyIp": true, + "allowFromAnyIp": false, "ipAllowlist": Array [], }, "rewriteBasePath": false, diff --git a/src/core/server/http/http_config.test.ts b/src/core/server/http/http_config.test.ts index e71d4cb47c171f..48330b361ed7ea 100644 --- a/src/core/server/http/http_config.test.ts +++ b/src/core/server/http/http_config.test.ts @@ -101,6 +101,7 @@ describe('requestOpaqueId', () => { expect(() => { config.schema.validate({ requestOpaqueId: { + allowFromAnyIp: true, ipAllowlist: ['0.0.0.0', '123.123.123.123', '1200:0000:AB00:1234:0000:2552:7777:1313'], }, }); diff --git a/src/core/server/http/http_config.ts b/src/core/server/http/http_config.ts index 8cc8efef0db111..61f1fc66a28d4a 100644 --- a/src/core/server/http/http_config.ts +++ b/src/core/server/http/http_config.ts @@ -89,7 +89,7 @@ export const config = { }), requestOpaqueId: schema.object( { - allowFromAnyIp: schema.boolean({ defaultValue: true }), + allowFromAnyIp: schema.boolean({ defaultValue: false }), ipAllowlist: schema.arrayOf(schema.ip(), { defaultValue: [] }), }, { From 7ee769ff545b1c6d368e9e5cd7d855966fac3b41 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Thu, 30 Jul 2020 14:11:27 -0600 Subject: [PATCH 04/11] Fix fake requests in tests --- x-pack/plugins/alerts/server/alerts_client_factory.test.ts | 1 + x-pack/plugins/reporting/server/export_types/csv/execute_job.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts index 16b5af499bb90f..4b4574f1e5a132 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts @@ -44,6 +44,7 @@ const alertsClientFactoryParams: jest.Mocked = { features, }; const fakeRequest = ({ + app: {}, headers: {}, getBasePath: () => '', path: '/', diff --git a/x-pack/plugins/reporting/server/export_types/csv/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv/execute_job.ts index f0c41a6a49703d..77c2d8805355eb 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/execute_job.ts @@ -50,6 +50,7 @@ const getRequest = async (headers: string | undefined, crypto: Crypto, logger: L path: '/', route: { settings: {} }, url: { href: '/' }, + app: {}, raw: { req: { url: '/' } }, } as Hapi.Request); }; From 4eb455dd2474d93090824e6f5c89fa41c69b9078 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Fri, 31 Jul 2020 11:33:15 -0600 Subject: [PATCH 05/11] Default to `internal` for fake requests --- src/core/server/http/router/request.ts | 7 ++++++- .../server/lib/__tests__/replace_injected_vars.js | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts index 485cd6c33fa526..7e3d6f88797696 100644 --- a/src/core/server/http/router/request.ts +++ b/src/core/server/http/router/request.ts @@ -178,7 +178,12 @@ export class KibanaRequest< // until that time we have to expose all the headers private readonly withoutSecretHeaders: boolean ) { - this.id = (request.app as KibanaRequestState).requestId; + // The `requestId` property will not be populated for requests that are 'faked' by internal systems that leverage + // KibanaRequest in conjunction with scoped Elaticcsearch and SavedObjectsClient in order to pass credentials. + // In these cases, the id defaults to `internal`. + // This should be solved as part of https://github.com/elastic/kibana/issues/39430. + this.id = (request.app as KibanaRequestState | undefined)?.requestId ?? 'internal'; + this.url = request.url; this.headers = deepFreeze({ ...request.headers }); this.isSystemRequest = diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/replace_injected_vars.js b/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/replace_injected_vars.js index f2dda63e689b97..ce6e20bd874b29 100644 --- a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/replace_injected_vars.js +++ b/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/replace_injected_vars.js @@ -14,6 +14,7 @@ const buildRequest = (path = '/app/kibana') => { const get = sinon.stub(); return { + app: {}, path, route: { settings: {} }, headers: {}, From 46b069fe2bb12fbb05ab4b00db1517894760b515 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Mon, 3 Aug 2020 14:45:24 -0600 Subject: [PATCH 06/11] Add support for new ES client --- .../client/cluster_client.test.ts | 54 +++++++++++++++++-- .../elasticsearch/client/cluster_client.ts | 15 +++--- 2 files changed, 59 insertions(+), 10 deletions(-) diff --git a/src/core/server/elasticsearch/client/cluster_client.test.ts b/src/core/server/elasticsearch/client/cluster_client.test.ts index 85517b80745f17..121ef3aa42d513 100644 --- a/src/core/server/elasticsearch/client/cluster_client.test.ts +++ b/src/core/server/elasticsearch/client/cluster_client.test.ts @@ -96,7 +96,7 @@ describe('ClusterClient', () => { expect(scopedClusterClient.asCurrentUser).toBe(scopedClient.child.mock.results[0].value); }); - it('returns a distinct scoped cluster client on each call', () => { + it('returns a distinct scoped cluster client on each call', () => { const clusterClient = new ClusterClient(createConfig(), logger, getAuthHeaders); const request = httpServerMock.createKibanaRequest(); @@ -127,7 +127,7 @@ describe('ClusterClient', () => { expect(scopedClient.child).toHaveBeenCalledTimes(1); expect(scopedClient.child).toHaveBeenCalledWith({ - headers: { foo: 'bar' }, + headers: { foo: 'bar', 'x-opaque-id': expect.any(String) }, }); }); @@ -147,7 +147,7 @@ describe('ClusterClient', () => { expect(scopedClient.child).toHaveBeenCalledTimes(1); expect(scopedClient.child).toHaveBeenCalledWith({ - headers: { authorization: 'auth' }, + headers: { authorization: 'auth', 'x-opaque-id': expect.any(String) }, }); }); @@ -171,7 +171,7 @@ describe('ClusterClient', () => { expect(scopedClient.child).toHaveBeenCalledTimes(1); expect(scopedClient.child).toHaveBeenCalledWith({ - headers: { authorization: 'auth' }, + headers: { authorization: 'auth', 'x-opaque-id': expect.any(String) }, }); }); @@ -195,6 +195,26 @@ describe('ClusterClient', () => { headers: { foo: 'bar', hello: 'dolly', + 'x-opaque-id': expect.any(String), + }, + }); + }); + + it('adds the x-opaque-id header based on the request id', () => { + const config = createConfig(); + getAuthHeaders.mockReturnValue({}); + + const clusterClient = new ClusterClient(config, logger, getAuthHeaders); + const request = httpServerMock.createKibanaRequest({ + kibanaRequestState: { requestId: 'my-fake-id' }, + }); + + clusterClient.asScoped(request); + + expect(scopedClient.child).toHaveBeenCalledTimes(1); + expect(scopedClient.child).toHaveBeenCalledWith({ + headers: { + 'x-opaque-id': 'my-fake-id', }, }); }); @@ -221,6 +241,7 @@ describe('ClusterClient', () => { headers: { foo: 'auth', hello: 'dolly', + 'x-opaque-id': expect.any(String), }, }); }); @@ -247,6 +268,31 @@ describe('ClusterClient', () => { headers: { foo: 'request', hello: 'dolly', + 'x-opaque-id': expect.any(String), + }, + }); + }); + + it('respect the precedence of x-opaque-id header over config headers', () => { + const config = createConfig({ + customHeaders: { + 'x-opaque-id': 'from config', + }, + }); + getAuthHeaders.mockReturnValue({}); + + const clusterClient = new ClusterClient(config, logger, getAuthHeaders); + const request = httpServerMock.createKibanaRequest({ + headers: { foo: 'request' }, + kibanaRequestState: { requestId: 'from request' }, + }); + + clusterClient.asScoped(request); + + expect(scopedClient.child).toHaveBeenCalledTimes(1); + expect(scopedClient.child).toHaveBeenCalledWith({ + headers: { + 'x-opaque-id': 'from request', }, }); }); diff --git a/src/core/server/elasticsearch/client/cluster_client.ts b/src/core/server/elasticsearch/client/cluster_client.ts index d9a0e6fe3f2384..c2b3806638b0a3 100644 --- a/src/core/server/elasticsearch/client/cluster_client.ts +++ b/src/core/server/elasticsearch/client/cluster_client.ts @@ -20,7 +20,7 @@ import { Client } from '@elastic/elasticsearch'; import { Logger } from '../../logging'; import { GetAuthHeaders, isRealRequest, Headers } from '../../http'; -import { ensureRawRequest, filterHeaders } from '../../http/router'; +import { ensureRawRequest, filterHeaders, KibanaRequest } from '../../http/router'; import { ScopeableRequest } from '../types'; import { ElasticsearchClient } from './types'; import { configureClient } from './configure_client'; @@ -95,12 +95,15 @@ export class ClusterClient implements ICustomClusterClient { private getScopedHeaders(request: ScopeableRequest): Headers { let scopedHeaders: Headers; if (isRealRequest(request)) { - const authHeaders = this.getAuthHeaders(request); const requestHeaders = ensureRawRequest(request).headers; - scopedHeaders = filterHeaders( - { ...requestHeaders, ...authHeaders }, - this.config.requestHeadersWhitelist - ); + const requestIdHeaders = + request instanceof KibanaRequest ? { 'x-opaque-id': request.id } : {}; + const authHeaders = this.getAuthHeaders(request); + + scopedHeaders = filterHeaders({ ...requestHeaders, ...requestIdHeaders, ...authHeaders }, [ + 'x-opaque-id', + ...this.config.requestHeadersWhitelist, + ]); } else { scopedHeaders = filterHeaders(request?.headers ?? {}, this.config.requestHeadersWhitelist); } From 074dea50508fc40dd5a00a5f7105c8f079afdc72 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Mon, 3 Aug 2020 14:45:28 -0600 Subject: [PATCH 07/11] Fix integration tests --- .../integration_tests/core_services.test.ts | 10 ++++-- .../lifecycle_handlers.test.ts | 4 +++ src/core/server/http/router/request.test.ts | 33 +++++++++++++++++++ src/core/server/http/router/request.ts | 6 ++-- 4 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/core/server/http/integration_tests/core_services.test.ts b/src/core/server/http/integration_tests/core_services.test.ts index 6a00db5a6cc4a6..2b9193a280aeca 100644 --- a/src/core/server/http/integration_tests/core_services.test.ts +++ b/src/core/server/http/integration_tests/core_services.test.ts @@ -406,7 +406,10 @@ describe('http service', () => { // client contains authHeaders for BWC with legacy platform. const [client] = MockLegacyScopedClusterClient.mock.calls; const [, , clientHeaders] = client; - expect(clientHeaders).toEqual(authHeaders); + expect(clientHeaders).toEqual({ + ...authHeaders, + 'x-opaque-id': expect.any(String), + }); }); it('passes request authorization header to Elasticsearch if registerAuth was not set', async () => { @@ -430,7 +433,10 @@ describe('http service', () => { const [client] = MockLegacyScopedClusterClient.mock.calls; const [, , clientHeaders] = client; - expect(clientHeaders).toEqual({ authorization: authorizationHeader }); + expect(clientHeaders).toEqual({ + authorization: authorizationHeader, + 'x-opaque-id': expect.any(String), + }); }); it('forwards 401 errors returned from elasticsearch', async () => { diff --git a/src/core/server/http/integration_tests/lifecycle_handlers.test.ts b/src/core/server/http/integration_tests/lifecycle_handlers.test.ts index 2120fb5b881de9..ba57667753c1d6 100644 --- a/src/core/server/http/integration_tests/lifecycle_handlers.test.ts +++ b/src/core/server/http/integration_tests/lifecycle_handlers.test.ts @@ -63,6 +63,10 @@ describe('core lifecycle handlers', () => { 'some-header': 'some-value', }, xsrf: { disableProtection: false, whitelist: [whitelistedTestPath] }, + requestOpaqueId: { + allowFromAnyIp: true, + ipAllowlist: [], + }, } as any) ); server = createHttpServer({ configService }); diff --git a/src/core/server/http/router/request.test.ts b/src/core/server/http/router/request.test.ts index fb999dc60e39c5..e741121f3d70c2 100644 --- a/src/core/server/http/router/request.test.ts +++ b/src/core/server/http/router/request.test.ts @@ -16,12 +16,45 @@ * specific language governing permissions and limitations * under the License. */ + +jest.mock('uuid', () => ({ + v4: jest.fn().mockReturnValue('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'), +})); + import { RouteOptions } from 'hapi'; import { KibanaRequest } from './request'; import { httpServerMock } from '../http_server.mocks'; import { schema } from '@kbn/config-schema'; describe('KibanaRequest', () => { + describe('id property', () => { + it('uses the request.app.requestId property if present', () => { + const request = httpServerMock.createRawRequest({ + app: { requestId: 'fakeId' }, + }); + const kibanaRequest = KibanaRequest.from(request); + expect(kibanaRequest.id).toEqual('fakeId'); + }); + + it('generates a new UUID if request.app property is not present', () => { + // Undefined app property + const request = httpServerMock.createRawRequest({ + app: undefined, + }); + const kibanaRequest = KibanaRequest.from(request); + expect(kibanaRequest.id).toEqual('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'); + }); + + it('generates a new UUID if request.app.requestId property is not present', () => { + // Undefined app.requestId property + const request = httpServerMock.createRawRequest({ + app: {}, + }); + const kibanaRequest = KibanaRequest.from(request); + expect(kibanaRequest.id).toEqual('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'); + }); + }); + describe('get all headers', () => { it('returns all headers', () => { const request = httpServerMock.createRawRequest({ diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts index ccebaa5ebb85bb..5d8743713c3251 100644 --- a/src/core/server/http/router/request.ts +++ b/src/core/server/http/router/request.ts @@ -18,6 +18,7 @@ */ import { Url } from 'url'; +import uuid from 'uuid'; import { Request, RouteOptionsApp, ApplicationState } from 'hapi'; import { Observable, fromEvent, merge } from 'rxjs'; import { shareReplay, first, takeUntil } from 'rxjs/operators'; @@ -190,9 +191,8 @@ export class KibanaRequest< ) { // The `requestId` property will not be populated for requests that are 'faked' by internal systems that leverage // KibanaRequest in conjunction with scoped Elaticcsearch and SavedObjectsClient in order to pass credentials. - // In these cases, the id defaults to `internal`. - // This should be solved as part of https://github.com/elastic/kibana/issues/39430. - this.id = (request.app as KibanaRequestState | undefined)?.requestId ?? 'internal'; + // In these cases, the id defaults to a newly generated UUID. + this.id = (request.app as KibanaRequestState | undefined)?.requestId ?? uuid.v4(); this.url = request.url; this.headers = deepFreeze({ ...request.headers }); From 9028c0a3609fa2bbffd365c2d8ece7f7a09e284f Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Mon, 3 Aug 2020 17:15:27 -0600 Subject: [PATCH 08/11] Handle undefined socket --- src/core/server/http/http_tools.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/core/server/http/http_tools.ts b/src/core/server/http/http_tools.ts index 5849e81d3ae885..106a6c1b480920 100644 --- a/src/core/server/http/http_tools.ts +++ b/src/core/server/http/http_tools.ts @@ -173,7 +173,9 @@ export function defaultValidationErrorHandler( export function getRequestId(request: Request, options: HttpConfig['requestOpaqueId']): string { return options.allowFromAnyIp || - options.ipAllowlist.includes(request.raw.req.socket.remoteAddress!) + // socket may be undefined in integration tests that connect via the http listener directly + (request.raw.req.socket?.remoteAddress && + options.ipAllowlist.includes(request.raw.req.socket.remoteAddress)) ? request.headers['x-opaque-id'] ?? uuid.v4() : uuid.v4(); } From 1144ebedc97b5b071039d1a8b28d1ffd49b90006 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Thu, 6 Aug 2020 15:05:48 -0600 Subject: [PATCH 09/11] Rename server.requestOpaqueId to server.requestId --- docs/setup/settings.asciidoc | 6 +++--- .../__snapshots__/http_config.test.ts.snap | 2 +- .../server/http/cookie_session_storage.test.ts | 2 +- src/core/server/http/http_config.test.ts | 18 +++++++++--------- src/core/server/http/http_config.ts | 6 +++--- src/core/server/http/http_server.test.ts | 2 +- src/core/server/http/http_server.ts | 2 +- src/core/server/http/http_tools.test.ts | 2 +- src/core/server/http/http_tools.ts | 2 +- .../lifecycle_handlers.test.ts | 2 +- src/core/server/http/test_utils.ts | 2 +- 11 files changed, 23 insertions(+), 23 deletions(-) diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 257633e590cb25..018cc656362b8e 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -476,11 +476,11 @@ identifies this {kib} instance. *Default: `"your-hostname"`* | {kib} is served by a back end server. This setting specifies the port to use. *Default: `5601`* -| `server.requestOpaqueId.allowFromAnyIp:` +| `server.requestId.allowFromAnyIp:` | Sets whether or not the X-Opaque-Id header should be trusted from any IP address for identifying requests in logs and forwarded to Elasticsearch. -| `server.requestOpaqueId.ipAllowlist:` - | A list of IPv4 and IPv6 address which the `X-Opaque-Id` header should be trusted from. Normally this would be set to the IP addresses of the load balancers or reverse-proxy that end users use to access Kibana. If any are set, `server.requestOpaqueId.allowFromAnyIp` must also be set to `false.` +| `server.requestId.ipAllowlist:` + | A list of IPv4 and IPv6 address which the `X-Opaque-Id` header should be trusted from. Normally this would be set to the IP addresses of the load balancers or reverse-proxy that end users use to access Kibana. If any are set, `server.requestId.allowFromAnyIp` must also be set to `false.` | `server.rewriteBasePath:` | Specifies whether {kib} should diff --git a/src/core/server/http/__snapshots__/http_config.test.ts.snap b/src/core/server/http/__snapshots__/http_config.test.ts.snap index 5971c971fdc567..e9b818fe859eca 100644 --- a/src/core/server/http/__snapshots__/http_config.test.ts.snap +++ b/src/core/server/http/__snapshots__/http_config.test.ts.snap @@ -39,7 +39,7 @@ Object { }, "name": "kibana-hostname", "port": 5601, - "requestOpaqueId": Object { + "requestId": Object { "allowFromAnyIp": false, "ipAllowlist": Array [], }, diff --git a/src/core/server/http/cookie_session_storage.test.ts b/src/core/server/http/cookie_session_storage.test.ts index e743e419405406..8e5dec7d4eadd7 100644 --- a/src/core/server/http/cookie_session_storage.test.ts +++ b/src/core/server/http/cookie_session_storage.test.ts @@ -63,7 +63,7 @@ configService.atPath.mockReturnValue( whitelist: [], }, customResponseHeaders: {}, - requestOpaqueId: { + requestId: { allowFromAnyIp: true, ipAllowlist: [], }, diff --git a/src/core/server/http/http_config.test.ts b/src/core/server/http/http_config.test.ts index 48330b361ed7ea..58e6699582e138 100644 --- a/src/core/server/http/http_config.test.ts +++ b/src/core/server/http/http_config.test.ts @@ -54,12 +54,12 @@ test('throws if invalid hostname', () => { expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot(); }); -describe('requestOpaqueId', () => { +describe('requestId', () => { test('accepts valid ip addresses', () => { const { - requestOpaqueId: { ipAllowlist }, + requestId: { ipAllowlist }, } = config.schema.validate({ - requestOpaqueId: { + requestId: { allowFromAnyIp: false, ipAllowlist: ['0.0.0.0', '123.123.123.123', '1200:0000:AB00:1234:0000:2552:7777:1313'], }, @@ -76,37 +76,37 @@ describe('requestOpaqueId', () => { test('rejects invalid ip addresses', () => { expect(() => { config.schema.validate({ - requestOpaqueId: { + requestId: { allowFromAnyIp: false, ipAllowlist: ['1200:0000:AB00:1234:O000:2552:7777:1313', '[2001:db8:0:1]:80'], }, }); }).toThrowErrorMatchingInlineSnapshot( - `"[requestOpaqueId.ipAllowlist.0]: value must be a valid ipv4 or ipv6 address"` + `"[requestId.ipAllowlist.0]: value must be a valid ipv4 or ipv6 address"` ); }); test('rejects if allowFromAnyIp is `true` and `ipAllowlist` is non-empty', () => { expect(() => { config.schema.validate({ - requestOpaqueId: { + requestId: { allowFromAnyIp: true, ipAllowlist: ['0.0.0.0', '123.123.123.123', '1200:0000:AB00:1234:0000:2552:7777:1313'], }, }); }).toThrowErrorMatchingInlineSnapshot( - `"[requestOpaqueId]: allowFromAnyIp must be set to 'false' if any values are specified in ipAllowlist"` + `"[requestId]: allowFromAnyIp must be set to 'false' if any values are specified in ipAllowlist"` ); expect(() => { config.schema.validate({ - requestOpaqueId: { + requestId: { allowFromAnyIp: true, ipAllowlist: ['0.0.0.0', '123.123.123.123', '1200:0000:AB00:1234:0000:2552:7777:1313'], }, }); }).toThrowErrorMatchingInlineSnapshot( - `"[requestOpaqueId]: allowFromAnyIp must be set to 'false' if any values are specified in ipAllowlist"` + `"[requestId]: allowFromAnyIp must be set to 'false' if any values are specified in ipAllowlist"` ); }); }); diff --git a/src/core/server/http/http_config.ts b/src/core/server/http/http_config.ts index 61f1fc66a28d4a..7d41b4ea9e915b 100644 --- a/src/core/server/http/http_config.ts +++ b/src/core/server/http/http_config.ts @@ -87,7 +87,7 @@ export const config = { { defaultValue: [] } ), }), - requestOpaqueId: schema.object( + requestId: schema.object( { allowFromAnyIp: schema.boolean({ defaultValue: false }), ipAllowlist: schema.arrayOf(schema.ip(), { defaultValue: [] }), @@ -143,7 +143,7 @@ export class HttpConfig { public compression: { enabled: boolean; referrerWhitelist?: string[] }; public csp: ICspConfig; public xsrf: { disableProtection: boolean; whitelist: string[] }; - public requestOpaqueId: { allowFromAnyIp: boolean; ipAllowlist: string[] }; + public requestId: { allowFromAnyIp: boolean; ipAllowlist: string[] }; /** * @internal @@ -172,7 +172,7 @@ export class HttpConfig { this.compression = rawHttpConfig.compression; this.csp = new CspConfig(rawCspConfig); this.xsrf = rawHttpConfig.xsrf; - this.requestOpaqueId = rawHttpConfig.requestOpaqueId; + this.requestId = rawHttpConfig.requestId; } } diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index 86c0a6f4bf31b3..e8961d8af7d60c 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -68,7 +68,7 @@ beforeEach(() => { port: 10002, ssl: { enabled: false }, compression: { enabled: true }, - requestOpaqueId: { + requestId: { allowFromAnyIp: true, ipAllowlist: [], }, diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 403625dd79ed9c..d09af8f5dee995 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -296,7 +296,7 @@ export class HttpServer { private setupRequestStateAssignment(config: HttpConfig) { this.server!.ext('onRequest', (request, responseToolkit) => { request.app = { - requestId: getRequestId(request, config.requestOpaqueId), + requestId: getRequestId(request, config.requestId), } as KibanaRequestState; return responseToolkit.continue; }); diff --git a/src/core/server/http/http_tools.test.ts b/src/core/server/http/http_tools.test.ts index 4d5f077f6b4c5b..bdeca3a87799a6 100644 --- a/src/core/server/http/http_tools.test.ts +++ b/src/core/server/http/http_tools.test.ts @@ -103,7 +103,7 @@ describe('timeouts', () => { maxPayload: new ByteSizeValue(1024), ssl: {}, compression: { enabled: true }, - requestOpaqueId: { + requestId: { allowFromAnyIp: true, ipAllowlist: [], }, diff --git a/src/core/server/http/http_tools.ts b/src/core/server/http/http_tools.ts index 106a6c1b480920..71900ab982f3de 100644 --- a/src/core/server/http/http_tools.ts +++ b/src/core/server/http/http_tools.ts @@ -171,7 +171,7 @@ export function defaultValidationErrorHandler( throw err; } -export function getRequestId(request: Request, options: HttpConfig['requestOpaqueId']): string { +export function getRequestId(request: Request, options: HttpConfig['requestId']): string { return options.allowFromAnyIp || // socket may be undefined in integration tests that connect via the http listener directly (request.raw.req.socket?.remoteAddress && diff --git a/src/core/server/http/integration_tests/lifecycle_handlers.test.ts b/src/core/server/http/integration_tests/lifecycle_handlers.test.ts index 785e162b018537..a1401ba73813b3 100644 --- a/src/core/server/http/integration_tests/lifecycle_handlers.test.ts +++ b/src/core/server/http/integration_tests/lifecycle_handlers.test.ts @@ -62,7 +62,7 @@ describe('core lifecycle handlers', () => { 'some-header': 'some-value', }, xsrf: { disableProtection: false, whitelist: [whitelistedTestPath] }, - requestOpaqueId: { + requestId: { allowFromAnyIp: true, ipAllowlist: [], }, diff --git a/src/core/server/http/test_utils.ts b/src/core/server/http/test_utils.ts index b6a7be3f0e63c2..c3afae108027e1 100644 --- a/src/core/server/http/test_utils.ts +++ b/src/core/server/http/test_utils.ts @@ -46,7 +46,7 @@ configService.atPath.mockReturnValue( whitelist: [], }, customResponseHeaders: {}, - requestOpaqueId: { + requestId: { allowFromAnyIp: true, ipAllowlist: [], }, From 275b58c9397eefc8cfe3bfd9f342689da7793eb6 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Wed, 19 Aug 2020 13:03:34 -0600 Subject: [PATCH 10/11] Review nits --- .../server/elasticsearch/client/cluster_client.ts | 5 ++--- .../server/elasticsearch/legacy/cluster_client.ts | 11 +++++------ src/core/server/http/http_server.ts | 1 + src/core/server/http/index.ts | 1 + src/core/server/http/router/index.ts | 1 + src/core/server/http/router/request.ts | 6 +++++- 6 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/core/server/elasticsearch/client/cluster_client.ts b/src/core/server/elasticsearch/client/cluster_client.ts index c2b3806638b0a3..396d4d7ebe64b4 100644 --- a/src/core/server/elasticsearch/client/cluster_client.ts +++ b/src/core/server/elasticsearch/client/cluster_client.ts @@ -19,7 +19,7 @@ import { Client } from '@elastic/elasticsearch'; import { Logger } from '../../logging'; -import { GetAuthHeaders, isRealRequest, Headers } from '../../http'; +import { GetAuthHeaders, Headers, isKibanaRequest, isRealRequest } from '../../http'; import { ensureRawRequest, filterHeaders, KibanaRequest } from '../../http/router'; import { ScopeableRequest } from '../types'; import { ElasticsearchClient } from './types'; @@ -96,8 +96,7 @@ export class ClusterClient implements ICustomClusterClient { let scopedHeaders: Headers; if (isRealRequest(request)) { const requestHeaders = ensureRawRequest(request).headers; - const requestIdHeaders = - request instanceof KibanaRequest ? { 'x-opaque-id': request.id } : {}; + const requestIdHeaders = isKibanaRequest(request) ? { 'x-opaque-id': request.id } : {}; const authHeaders = this.getAuthHeaders(request); scopedHeaders = filterHeaders({ ...requestHeaders, ...requestIdHeaders, ...authHeaders }, [ diff --git a/src/core/server/elasticsearch/legacy/cluster_client.ts b/src/core/server/elasticsearch/legacy/cluster_client.ts index 38e2db3270f913..81cbb5a10d7c66 100644 --- a/src/core/server/elasticsearch/legacy/cluster_client.ts +++ b/src/core/server/elasticsearch/legacy/cluster_client.ts @@ -20,7 +20,7 @@ import { Client } from 'elasticsearch'; import { get } from 'lodash'; import { LegacyElasticsearchErrorHelpers } from './errors'; -import { GetAuthHeaders, isRealRequest, KibanaRequest } from '../../http'; +import { GetAuthHeaders, KibanaRequest, isKibanaRequest, isRealRequest } from '../../http'; import { AuditorFactory } from '../../audit_trail'; import { filterHeaders, ensureRawRequest } from '../../http/router'; import { Logger } from '../../logging'; @@ -218,8 +218,7 @@ export class LegacyClusterClient implements ILegacyClusterClient { private getScopedAuditor(request?: ScopeableRequest) { // TODO: support alternative credential owners from outside of Request context in #39430 if (request && isRealRequest(request)) { - const kibanaRequest = - request instanceof KibanaRequest ? request : KibanaRequest.from(request); + const kibanaRequest = isKibanaRequest(request) ? request : KibanaRequest.from(request); const auditorFactory = this.getAuditorFactory(); return auditorFactory.asScoped(kibanaRequest); } @@ -259,9 +258,9 @@ export class LegacyClusterClient implements ILegacyClusterClient { return request && request.headers ? request.headers : {}; } const authHeaders = this.getAuthHeaders(request); - const headers = ensureRawRequest(request).headers; - const requestIdHeaders = request instanceof KibanaRequest ? { 'x-opaque-id': request.id } : {}; + const requestHeaders = ensureRawRequest(request).headers; + const requestIdHeaders = isKibanaRequest(request) ? { 'x-opaque-id': request.id } : {}; - return { ...headers, ...authHeaders, ...requestIdHeaders }; + return { ...requestHeaders, ...requestIdHeaders, ...authHeaders }; } } diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 931736f15a20bb..7609f23fe0c51d 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -313,6 +313,7 @@ export class HttpServer { private setupRequestStateAssignment(config: HttpConfig) { this.server!.ext('onRequest', (request, responseToolkit) => { request.app = { + ...(request.app ?? {}), requestId: getRequestId(request, config.requestId), } as KibanaRequestState; return responseToolkit.continue; diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index e91f7d93758429..7513e609660852 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -24,6 +24,7 @@ export { AuthStatus, GetAuthState, IsAuthenticated } from './auth_state_storage' export { CustomHttpResponseOptions, IKibanaSocket, + isKibanaRequest, isRealRequest, Headers, HttpResponseOptions, diff --git a/src/core/server/http/router/index.ts b/src/core/server/http/router/index.ts index 27aee8fb8ebd8c..e09833ef6b2da6 100644 --- a/src/core/server/http/router/index.ts +++ b/src/core/server/http/router/index.ts @@ -26,6 +26,7 @@ export { KibanaRequestRouteOptions, KibanaRouteOptions, KibanaRequestState, + isKibanaRequest, isRealRequest, LegacyRequest, ensureRawRequest, diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts index b3ca935ffa7096..76f8761a7e998b 100644 --- a/src/core/server/http/router/request.ts +++ b/src/core/server/http/router/request.ts @@ -299,7 +299,11 @@ export class KibanaRequest< export const ensureRawRequest = (request: KibanaRequest | LegacyRequest) => isKibanaRequest(request) ? request[requestSymbol] : request; -function isKibanaRequest(request: unknown): request is KibanaRequest { +/** + * Checks if an incoming request is a {@link KibanaRequest} + * @internal + */ +export function isKibanaRequest(request: unknown): request is KibanaRequest { return request instanceof KibanaRequest; } From ec9a96da630d4eaf1b290ba25f886e5416c05c64 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Wed, 19 Aug 2020 13:38:37 -0600 Subject: [PATCH 11/11] rm unused import --- src/core/server/elasticsearch/client/cluster_client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/server/elasticsearch/client/cluster_client.ts b/src/core/server/elasticsearch/client/cluster_client.ts index 396d4d7ebe64b4..ffe0c10321fff6 100644 --- a/src/core/server/elasticsearch/client/cluster_client.ts +++ b/src/core/server/elasticsearch/client/cluster_client.ts @@ -20,7 +20,7 @@ import { Client } from '@elastic/elasticsearch'; import { Logger } from '../../logging'; import { GetAuthHeaders, Headers, isKibanaRequest, isRealRequest } from '../../http'; -import { ensureRawRequest, filterHeaders, KibanaRequest } from '../../http/router'; +import { ensureRawRequest, filterHeaders } from '../../http/router'; import { ScopeableRequest } from '../types'; import { ElasticsearchClient } from './types'; import { configureClient } from './configure_client';