Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

No longer use generated IDs for X-Opaque-ID / requestId #123197

Closed
wants to merge 11 commits into from

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ export declare class KibanaRequest<Params = unknown, Query = unknown, Body = unk
| [body](./kibana-plugin-core-server.kibanarequest.body.md) | | Body | |
| [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 <code>HttpFetchOptions#asSystemRequest</code> option. |
| [opaqueId?](./kibana-plugin-core-server.kibanarequest.opaqueid.md) | | string | <i>(Optional)</i> The (optional) opaqueId of this request. |
| [params](./kibana-plugin-core-server.kibanarequest.params.md) | | Params | |
| [query](./kibana-plugin-core-server.kibanarequest.query.md) | | Query | |
| [rewrittenUrl?](./kibana-plugin-core-server.kibanarequest.rewrittenurl.md) | | URL | <i>(Optional)</i> URL rewritten in onPreRouting request interceptor. |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [KibanaRequest](./kibana-plugin-core-server.kibanarequest.md) &gt; [opaqueId](./kibana-plugin-core-server.kibanarequest.opaqueid.md)

## KibanaRequest.opaqueId property

The (optional) opaqueId of this request.

<b>Signature:</b>

```typescript
readonly opaqueId?: string;
```

## Remarks

This value is sourced from the incoming request's `X-Opaque-Id` header which is not guaranteed to be unique per request. When present, it should contain a userId in any format.

32 changes: 12 additions & 20 deletions packages/kbn-server-http-tools/src/get_request_id.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,14 @@

import { getRequestId } from './get_request_id';

jest.mock('uuid', () => ({
v4: jest.fn().mockReturnValue('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'),
}));

describe('getRequestId', () => {
describe('when allowFromAnyIp is true', () => {
it('generates a UUID if no x-opaque-id header is present', () => {
it('returns undefined 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'
);
expect(getRequestId(request, { allowFromAnyIp: true, ipAllowlist: [] })).toBeUndefined();
});

it('uses x-opaque-id header value if present', () => {
Expand All @@ -39,14 +33,12 @@ describe('getRequestId', () => {

describe('when allowFromAnyIp is false', () => {
describe('and ipAllowlist is empty', () => {
it('generates a UUID even if x-opaque-id header is present', () => {
it('returns undefined 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'
);
expect(getRequestId(request, { allowFromAnyIp: false, ipAllowlist: [] })).toBeUndefined();
});
});

Expand All @@ -61,24 +53,24 @@ describe('getRequestId', () => {
);
});

it('generates a UUID if request comes from untrusted IP address', () => {
it('does not use x-opaque-id header 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'
);
expect(
getRequestId(request, { allowFromAnyIp: false, ipAllowlist: ['1.1.1.1'] })
).toBeUndefined();
});

it('generates UUID if request comes from trusted IP address but no x-opaque-id header is present', () => {
it('returns undefined 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'
);
expect(
getRequestId(request, { allowFromAnyIp: false, ipAllowlist: ['1.1.1.1'] })
).toBeUndefined();
});
});
});
Expand Down
12 changes: 8 additions & 4 deletions packages/kbn-server-http-tools/src/get_request_id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,20 @@
*/

import { Request } from '@hapi/hapi';
import uuid from 'uuid';

/**
* Return the requestId for this request from its `x-opaque-id` header,
* depending on the `server.requestId` configuration, or undefined
* if the value should not be used.
*/
export function getRequestId(
request: Request,
{ allowFromAnyIp, ipAllowlist }: { allowFromAnyIp: boolean; ipAllowlist: string[] }
): string {
): string | undefined {
const remoteAddress = request.raw.req.socket?.remoteAddress;
return allowFromAnyIp ||
// socket may be undefined in integration tests that connect via the http listener directly
(remoteAddress && ipAllowlist.includes(remoteAddress))
? request.headers['x-opaque-id'] ?? uuid.v4()
: uuid.v4();
? request.headers['x-opaque-id']
: undefined;
pgayvallet marked this conversation as resolved.
Show resolved Hide resolved
}
5 changes: 3 additions & 2 deletions src/core/server/elasticsearch/client/cluster_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,12 +137,13 @@ export class ClusterClient implements ICustomClusterClient {
let scopedHeaders: Headers;
if (isRealRequest(request)) {
const requestHeaders = ensureRawRequest(request).headers ?? {};
const requestIdHeaders = isKibanaRequest(request) ? { 'x-opaque-id': request.id } : {};
const opaqueIdHeaders =
isKibanaRequest(request) && request.opaqueId ? { 'x-opaque-id': request.opaqueId } : {};
const authHeaders = this.authHeaders ? this.authHeaders.get(request) : {};

scopedHeaders = {
...filterHeaders(requestHeaders, this.config.requestHeadersWhitelist),
...requestIdHeaders,
...opaqueIdHeaders,
...authHeaders,
};
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ export class ExecutionContextService

constructor(private readonly coreContext: CoreContext) {
this.log = coreContext.logger.get('execution_context');
this.contextStore = new AsyncLocalStorage<IExecutionContextContainer>();
this.requestIdStore = new AsyncLocalStorage<{ requestId: string }>();
this.contextStore = new AsyncLocalStorage();
this.requestIdStore = new AsyncLocalStorage();
}

setup(): InternalExecutionContextSetup {
Expand Down Expand Up @@ -154,7 +154,7 @@ export class ExecutionContextService

private getAsHeader(): string | undefined {
if (!this.enabled) return;
// requestId may not be present in the case of FakeRequest
// requestId may not be present if unspecified by the client or in the case of FakeRequest
const requestId = this.requestIdStore.getStore()?.requestId ?? 'unknownId';
const executionContext = this.contextStore.getStore()?.toString();
const executionContextStr = executionContext ? `;kibana:${executionContext}` : '';
Comment on lines 156 to 160
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(This is correlated with the change in src/core/server/elasticsearch/client/cluster_client.ts)

ATM we kinda have an inconsistency between when EC is enabled or not, as when EC is enabled, the x-opaque-id value will be coming from EC.getAsHeader, and when it's not, the value is generated within ClusterClient.getScopedHeaders

ATM (with the PR's current change), the value matrix looks like:

EC enabled request.opaqueId present EC present x-opaque-id sent to ES
true true true '${opaqueId};kibana:${executionContext}'
true true false '${opaqueId}'
true false true 'unknownId;kibana:${executionContext}'
true false false 'unknownId'
false true true N/A
false true false '${opaqueId}'
false false true N/A
false false false undefined

I got a few questions here:

  • is this unknownId default value for opaqueId acceptable?
  • When EC is disabled, the header is populated by ClusterClient.getScopedHeaders. ATM when request.opaqueId is not present, we're not sending any x-opaque-id header to ES. Do we want to change that to send kibana (or any constant value we agreed on from the previous point) instead?
  • (follow-up of next point) Should we decide to have the EC service the single source of truth for the x-opaque-id header and have getAsHeader always return a value even when disabled
    • this would require the requestIdStore to always be populated even when EC is disabled, which could be a performance issue as the whole purpose of allowing to disable the EC was for performances reasons

@mshustov @lukeelmers what do you think about this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we fine with this format? Do we just want to use kibana:${executionContext} in that case instead?

Worth noting, ES doesn't parse x-opaque-id value, so we can use any format. I used kibana static value to simplify parsing when an incoming request contains x-opaque-id. If you find it easier to use kibana;${executionContext}, we can change the format.

ATM when request.opaqueId is not present, we're not sending any x-opaque-id header to ES. Do we want to change that to send kibana (or any constant value we agreed on from the previous point) instead?

@pgomulka what is better for the depreciation service of Elasticsearch?

Should we decide to have the EC service the single source of truth for the x-opaque-id header and have getAsHeader always return a value even when disabled

I didn't put this logic in the EC service to have a proper separation of concerns. The fact we put execution_context in x-opaque-id header of outbound requests is an implementation detail. When the buggage header is available, we will use it for context propagation. Having said that, I'd rather keep this logic on the elasticsearch service level. But we can refactor it a bit.

Copy link

@pgomulka pgomulka Jan 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ATM when request.opaqueId is not present, we're not sending any x-opaque-id header to ES. Do we want to change that to send kibana (or any constant value we agreed on from the previous point) instead?

@pgomulka what is better for the depreciation service of Elasticsearch?

either way is good. As long as it is constant (or from a finite set). If Kibana wants to send a constant x-opaque-id-from-kibana it would help identifying its requests

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used kibana static value to simplify parsing when an incoming request contains x-opaque-id. If you find it easier to use kibana;${executionContext}, we can change the format.

Let's not overcomplicate the PR for no good reasons. The current format is good enough to address #120124, let's not change it for now. We'll reevaluate when we'll use the baggage header anyway.

Expand Down
4 changes: 3 additions & 1 deletion src/core/server/http/http_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,9 @@ export class HttpServer {
const parentContext = executionContext?.getParentContextFrom(request.headers);
if (parentContext) executionContext?.set(parentContext);

executionContext?.setRequestId(requestId);
if (requestId) {
executionContext?.setRequestId(requestId);
}
pgayvallet marked this conversation as resolved.
Show resolved Hide resolved

request.app = {
...(request.app ?? {}),
Expand Down
2 changes: 1 addition & 1 deletion src/core/server/http/integration_tests/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ describe('KibanaRequest', () => {
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 } });
return res.ok({ body: { requestId: req.opaqueId } });
});
await server.start();

Expand Down
12 changes: 6 additions & 6 deletions src/core/server/http/router/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,31 +16,31 @@ import { httpServerMock } from '../http_server.mocks';
import { schema } from '@kbn/config-schema';

describe('KibanaRequest', () => {
describe('id property', () => {
describe('opaqueId 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');
expect(kibanaRequest.opaqueId).toEqual('fakeId');
});

it('generates a new UUID if request.app property is not present', () => {
it('is undefined 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');
expect(kibanaRequest.opaqueId).toBeUndefined();
});

it('generates a new UUID if request.app.requestId property is not present', () => {
it('is undefined 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');
expect(kibanaRequest.opaqueId).toBeUndefined();
});
});

Expand Down
23 changes: 11 additions & 12 deletions src/core/server/http/router/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export interface KibanaRouteOptions extends RouteOptionsApp {
* @internal
*/
export interface KibanaRequestState extends RequestApplicationState {
requestId: string;
requestId?: string;
requestUuid: string;
rewrittenUrl?: URL;
traceId?: string;
Expand Down Expand Up @@ -127,21 +127,20 @@ export class KibanaRequest<
const body = routeValidator.getBody(req.payload, 'request body');
return { query, params, body };
}

/**
* A identifier to identify this request.
* The (optional) opaqueId of 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.
* @remarks This value is sourced from the incoming request's `X-Opaque-Id` header
* which is not guaranteed to be unique per request. When present, it should
* contain a userId in any format.
*/
public readonly id: string;
public readonly opaqueId?: string;
/**
* A UUID to identify this request.
*
* @remarks
* This value is NOT sourced from the incoming request's `X-Opaque-Id` header. it
* is always a UUID uniquely identifying the request.
* @remarks This value is NOT sourced from the incoming request's `X-Opaque-Id` header.
* it is always a UUID uniquely identifying the request.
*/
public readonly uuid: string;
/** a WHATWG URL standard object. */
Expand Down Expand Up @@ -186,11 +185,11 @@ export class KibanaRequest<
// until that time we have to expose all the headers
private readonly withoutSecretHeaders: boolean
) {
// The `requestId` and `requestUuid` properties will not be populated for requests that are 'faked' by internal systems that leverage
// The `opaqueId` and `requestUuid` properties will not be populated for requests that are 'faked' by internal systems that leverage
// KibanaRequest in conjunction with scoped Elasticsearch and SavedObjectsClient in order to pass credentials.
// In these cases, the ids default to a newly generated UUID.
const appState = request.app as KibanaRequestState | undefined;
this.id = appState?.requestId ?? uuid.v4();
this.opaqueId = appState?.requestId;
this.uuid = appState?.requestUuid ?? uuid.v4();
this.rewrittenUrl = appState?.rewrittenUrl;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -488,8 +488,7 @@ describe(`POST ${URL}`, () => {
const mockUuid = jest.requireMock('uuid');
mockUuid.v4 = jest
.fn()
.mockReturnValueOnce('foo') // a uuid.v4() is generated for the request.id
.mockReturnValueOnce('foo') // another uuid.v4() is used for the request.uuid
.mockReturnValueOnce('foo') // a uuid.v4() is used for the request.uuid
.mockReturnValueOnce('new-id-1')
.mockReturnValueOnce('new-id-2');
savedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [mockIndexPattern] });
Expand Down
2 changes: 1 addition & 1 deletion src/core/server/server.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1350,8 +1350,8 @@ export class KibanaRequest<Params = unknown, Query = unknown, Body = unknown, Me
// @internal
static from<P, Q, B>(req: Request_2, routeSchemas?: RouteValidator<P, Q, B> | RouteValidatorFullConfig<P, Q, B>, withoutSecretHeaders?: boolean): KibanaRequest<P, Q, B, any>;
readonly headers: Headers_2;
readonly id: string;
readonly isSystemRequest: boolean;
readonly opaqueId?: string;
// (undocumented)
readonly params: Params;
// (undocumented)
Expand Down
4 changes: 2 additions & 2 deletions src/plugins/data/server/search/expressions/esaggs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ describe('esaggs expression function - server', () => {
jest.clearAllMocks();
mockHandlers = {
abortSignal: jest.fn() as unknown as jest.Mocked<AbortSignal>,
getKibanaRequest: jest.fn().mockReturnValue({ id: 'hi' } as KibanaRequest),
getKibanaRequest: jest.fn().mockReturnValue({ opaqueId: 'hi' } as KibanaRequest),
getSearchContext: jest.fn(),
getSearchSessionId: jest.fn().mockReturnValue('abc123'),
getExecutionContext: jest.fn(),
Expand All @@ -81,7 +81,7 @@ describe('esaggs expression function - server', () => {
test('calls getStartDependencies with the KibanaRequest', async () => {
await definition().fn(null, args, mockHandlers).toPromise();

expect(getStartDependencies).toHaveBeenCalledWith({ id: 'hi' });
expect(getStartDependencies).toHaveBeenCalledWith({ opaqueId: 'hi' });
});

test('calls indexPatterns.create with the values provided by the subexpression arg', async () => {
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/security/server/audit/audit_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ export class AuditService {
session_id: sessionId,
...event.kibana,
},
trace: { id: request.id },
trace: { id: request.opaqueId },
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@elastic/kibana-security request.id was renamed to request.opaqueId, but is now optional, as we're no longer generating a value when not provided by the client / when the configuration forbid its usage.

Is this change fine, or should we fallback to request.uuid when request.opaqueId is empty?

Copy link
Contributor

@mshustov mshustov Jan 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JFYI: Kibana sets tracing fields starting from #118466 The value from meta override them (sorry, I need to update the test suite name 😅 )

test('format() meta can not override tracing properties', () => {

maybe audit logging should rely on the trace field supplied by the Kibana server and not overwrite them?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#118466 was merged on 8.1 only, and not backported though, right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, it wasn't. because it's not a bug fix

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@elastic/kibana-security request.id was renamed to request.opaqueId, but is now optional, as we're no longer generating a value when not provided by the client / when the configuration forbid its usage.

Is this change fine, or should we fallback to request.uuid when request.opaqueId is empty?

I responded to the linked issue in #120124 (comment).

I'm not on board with this change in general as it hamstrings auditing functionality. If we have to go through with this though, we at least need the ability to correlate audit records within the Kibana audit log which originate from the same request. In that case it would be better to fall back to request.uuid.

});
},
});
Expand Down