mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[core.http] Add warning header to deprecated endpoints (#205926)
## Summary resolves https://github.com/elastic/kibana/issues/105692 This PR adds a pre response handler that sets a warning header if the requested endpoint is deprecated. ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) ### Identify risks Does this PR introduce any risks? For example, consider risks like hard to test bugs, performance regression, potential of data loss. Describe the risk, its severity, and mitigation for each identified risk. Invite stakeholders and evaluate how to proceed before merging. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
39119b553e
commit
0f67c78659
24 changed files with 427 additions and 39 deletions
|
@ -21,3 +21,4 @@ export { isKibanaRequest, isRealRequest, ensureRawRequest, CoreKibanaRequest } f
|
|||
export { isSafeMethod } from './src/route';
|
||||
export { HapiResponseAdapter } from './src/response_adapter';
|
||||
export { kibanaResponseFactory, lifecycleResponseFactory, KibanaResponse } from './src/response';
|
||||
export { getWarningHeaderMessageFromRouteDeprecation } from './src/get_warning_header_message';
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { RouteDeprecationInfo } from '@kbn/core-http-server';
|
||||
import { getWarningHeaderMessageFromRouteDeprecation } from './get_warning_header_message';
|
||||
|
||||
describe('getWarningHeaderMessageFromRouteDeprecation', () => {
|
||||
it('creates the warning with a default message if the deprecation object does not have one', () => {
|
||||
const kibanaVersion = '12.31.45';
|
||||
const expectedMessage = `299 Kibana-${kibanaVersion} "This endpoint deprecated"`;
|
||||
const deprecationObject: RouteDeprecationInfo = {
|
||||
reason: { type: 'deprecate' },
|
||||
severity: 'warning',
|
||||
documentationUrl: 'fakeurl.com',
|
||||
};
|
||||
expect(getWarningHeaderMessageFromRouteDeprecation(deprecationObject, expectedMessage)).toMatch(
|
||||
expectedMessage
|
||||
);
|
||||
});
|
||||
|
||||
it('creates the warning with the deprecation object message', () => {
|
||||
const kibanaVersion = '12.31.45';
|
||||
const msg = 'Custom deprecation message for this object';
|
||||
const expectedMessage = `299 Kibana-${kibanaVersion} "${msg}"`;
|
||||
const deprecationObject: RouteDeprecationInfo = {
|
||||
reason: { type: 'deprecate' },
|
||||
severity: 'warning',
|
||||
documentationUrl: 'fakeurl.com',
|
||||
message: msg,
|
||||
};
|
||||
expect(getWarningHeaderMessageFromRouteDeprecation(deprecationObject, expectedMessage)).toMatch(
|
||||
expectedMessage
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { RouteDeprecationInfo } from '@kbn/core-http-server';
|
||||
|
||||
export function getWarningHeaderMessageFromRouteDeprecation(
|
||||
deprecationObject: RouteDeprecationInfo,
|
||||
kibanaVersion: string
|
||||
): string {
|
||||
const msg = deprecationObject.message ?? 'This endpoint is deprecated';
|
||||
const warningMessage = `299 Kibana-${kibanaVersion} "${msg}"`;
|
||||
return warningMessage;
|
||||
}
|
|
@ -14,6 +14,7 @@ import { createRequestMock } from '@kbn/hapi-mocks/src/request';
|
|||
import { createFooValidation } from './router.test.util';
|
||||
import { Router, type RouterOptions } from './router';
|
||||
import type { RouteValidatorRequestAndResponses } from '@kbn/core-http-server';
|
||||
import { getEnvOptions, createTestEnv } from '@kbn/config-mocks';
|
||||
|
||||
const mockResponse = {
|
||||
code: jest.fn().mockImplementation(() => mockResponse),
|
||||
|
@ -26,9 +27,12 @@ const mockResponseToolkit = {
|
|||
|
||||
const logger = loggingSystemMock.create().get();
|
||||
const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, {});
|
||||
const options = getEnvOptions();
|
||||
options.cliArgs.dev = false;
|
||||
const env = createTestEnv({ envOptions: options });
|
||||
|
||||
const routerOptions: RouterOptions = {
|
||||
isDev: false,
|
||||
env,
|
||||
versionedRouterOptions: {
|
||||
defaultHandlerResolutionStrategy: 'oldest',
|
||||
useVersionResolutionStrategyForInternalPaths: [],
|
||||
|
@ -273,7 +277,7 @@ describe('Router', () => {
|
|||
|
||||
it('registers pluginId if provided', () => {
|
||||
const pluginId = Symbol('test');
|
||||
const router = new Router('', logger, enhanceWithContext, { pluginId });
|
||||
const router = new Router('', logger, enhanceWithContext, { pluginId, env });
|
||||
expect(router.pluginId).toBe(pluginId);
|
||||
});
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ import type {
|
|||
IKibanaResponse,
|
||||
} from '@kbn/core-http-server';
|
||||
import type { RouteSecurityGetter } from '@kbn/core-http-server';
|
||||
import { Env } from '@kbn/config';
|
||||
import { CoreVersionedRouter } from './versioned_router';
|
||||
import { CoreKibanaRequest, getProtocolFromRequest } from './request';
|
||||
import { kibanaResponseFactory } from './response';
|
||||
|
@ -72,8 +73,7 @@ export type InternalRouterRoute = Omit<RouterRoute, 'handler'> & {
|
|||
|
||||
/** @internal */
|
||||
export interface RouterOptions {
|
||||
/** Whether we are running in development */
|
||||
isDev?: boolean;
|
||||
env: Env;
|
||||
|
||||
/** Plugin for which this router was registered */
|
||||
pluginId?: symbol;
|
||||
|
@ -203,7 +203,7 @@ export class Router<Context extends RequestHandlerContextBase = RequestHandlerCo
|
|||
if (getProtocolFromRequest(request) === 'http2' && kibanaResponse.options.headers) {
|
||||
kibanaResponse.options.headers = stripIllegalHttp2Headers({
|
||||
headers: kibanaResponse.options.headers,
|
||||
isDev: this.options.isDev ?? false,
|
||||
isDev: this.options.env.mode.dev,
|
||||
logger: this.log,
|
||||
requestContext: `${request.route.method} ${request.route.path}`,
|
||||
});
|
||||
|
@ -233,7 +233,7 @@ export class Router<Context extends RequestHandlerContextBase = RequestHandlerCo
|
|||
if (this.versionedRouter === undefined) {
|
||||
this.versionedRouter = CoreVersionedRouter.from({
|
||||
router: this,
|
||||
isDev: this.options.isDev,
|
||||
env: this.options.env,
|
||||
log: this.log,
|
||||
...this.options.versionedRouterOptions,
|
||||
});
|
||||
|
|
|
@ -21,6 +21,12 @@ import { createRequest } from './core_versioned_route.test.util';
|
|||
import { isConfigSchema } from '@kbn/config-schema';
|
||||
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
|
||||
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
|
||||
import { getEnvOptions, createTestEnv } from '@kbn/config-mocks';
|
||||
|
||||
const notDevOptions = getEnvOptions();
|
||||
notDevOptions.cliArgs.dev = false;
|
||||
const notDevEnv = createTestEnv({ envOptions: notDevOptions });
|
||||
const devEnv = createTestEnv();
|
||||
|
||||
describe('Versioned route', () => {
|
||||
let router: Router;
|
||||
|
@ -33,6 +39,7 @@ describe('Versioned route', () => {
|
|||
versionedRouter = CoreVersionedRouter.from({
|
||||
router,
|
||||
log: loggingSystemMock.createLogger(),
|
||||
env: notDevEnv,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -198,7 +205,7 @@ describe('Versioned route', () => {
|
|||
|
||||
it('allows public versions other than "2023-10-31"', () => {
|
||||
expect(() =>
|
||||
CoreVersionedRouter.from({ router, log: loggingSystemMock.createLogger(), isDev: false })
|
||||
CoreVersionedRouter.from({ router, log: loggingSystemMock.createLogger(), env: notDevEnv })
|
||||
.get({ access: 'public', path: '/foo' })
|
||||
.addVersion({ version: '2023-01-31', validate: false }, (ctx, req, res) => res.ok())
|
||||
).not.toThrow();
|
||||
|
@ -297,7 +304,7 @@ describe('Versioned route', () => {
|
|||
beforeEach(() => {
|
||||
versionedRouter = CoreVersionedRouter.from({
|
||||
router,
|
||||
isDev: true,
|
||||
env: devEnv,
|
||||
log: loggingSystemMock.createLogger(),
|
||||
});
|
||||
});
|
||||
|
@ -346,7 +353,7 @@ describe('Versioned route', () => {
|
|||
(router.registerRoute as jest.Mock).mockImplementation((opts) => (handler = opts.handler));
|
||||
versionedRouter = CoreVersionedRouter.from({
|
||||
router,
|
||||
isDev: true,
|
||||
env: devEnv,
|
||||
log: loggingSystemMock.createLogger(),
|
||||
});
|
||||
versionedRouter.post({ path: '/test/{id}', access: 'internal' }).addVersion(
|
||||
|
@ -379,7 +386,7 @@ describe('Versioned route', () => {
|
|||
(router.registerRoute as jest.Mock).mockImplementation((opts) => (handler = opts.handler));
|
||||
versionedRouter = CoreVersionedRouter.from({
|
||||
router,
|
||||
isDev: true,
|
||||
env: devEnv,
|
||||
log: loggingSystemMock.createLogger(),
|
||||
});
|
||||
versionedRouter.post({ path: '/test/{id}', access: 'internal' }).addVersion(
|
||||
|
@ -411,7 +418,7 @@ describe('Versioned route', () => {
|
|||
it('allows using default resolution for specific internal routes', async () => {
|
||||
versionedRouter = CoreVersionedRouter.from({
|
||||
router,
|
||||
isDev: true,
|
||||
env: devEnv,
|
||||
log: loggingSystemMock.createLogger(),
|
||||
useVersionResolutionStrategyForInternalPaths: ['/bypass_me/{id?}'],
|
||||
});
|
||||
|
|
|
@ -25,6 +25,7 @@ import type {
|
|||
} from '@kbn/core-http-server';
|
||||
import { Request } from '@hapi/hapi';
|
||||
import { Logger } from '@kbn/logging';
|
||||
import { Env } from '@kbn/config';
|
||||
import type { HandlerResolutionStrategy, Method, Options } from './types';
|
||||
|
||||
import {
|
||||
|
@ -34,7 +35,7 @@ import {
|
|||
readVersion,
|
||||
removeQueryVersion,
|
||||
} from './route_version_utils';
|
||||
import { injectVersionHeader } from '../util';
|
||||
import { injectResponseHeaders, injectVersionHeader } from '../util';
|
||||
import { validRouteSecurity } from '../security_route_config_validator';
|
||||
|
||||
import { resolvers } from './handler_resolvers';
|
||||
|
@ -44,9 +45,10 @@ import { RequestHandlerEnhanced, Router } from '../router';
|
|||
import { kibanaResponseFactory as responseFactory } from '../response';
|
||||
import { validateHapiRequest } from '../route';
|
||||
import { RouteValidator } from '../validator';
|
||||
import { getWarningHeaderMessageFromRouteDeprecation } from '../get_warning_header_message';
|
||||
|
||||
interface InternalVersionedRouteConfig<M extends RouteMethod> extends VersionedRouteConfig<M> {
|
||||
isDev: boolean;
|
||||
env: Env;
|
||||
useVersionResolutionStrategyForInternalPaths: Map<string, boolean>;
|
||||
defaultHandlerResolutionStrategy: HandlerResolutionStrategy;
|
||||
}
|
||||
|
@ -86,7 +88,7 @@ export class CoreVersionedRoute implements VersionedRoute {
|
|||
|
||||
private useDefaultStrategyForPath: boolean;
|
||||
private isPublic: boolean;
|
||||
private isDev: boolean;
|
||||
private env: Env;
|
||||
private enableQueryVersion: boolean;
|
||||
private defaultSecurityConfig: RouteSecurity | undefined;
|
||||
private defaultHandlerResolutionStrategy: HandlerResolutionStrategy;
|
||||
|
@ -98,13 +100,13 @@ export class CoreVersionedRoute implements VersionedRoute {
|
|||
internalOptions: InternalVersionedRouteConfig<Method>
|
||||
) {
|
||||
const {
|
||||
isDev,
|
||||
env,
|
||||
useVersionResolutionStrategyForInternalPaths,
|
||||
defaultHandlerResolutionStrategy,
|
||||
...options
|
||||
} = internalOptions;
|
||||
this.isPublic = options.access === 'public';
|
||||
this.isDev = isDev;
|
||||
this.env = env;
|
||||
this.defaultHandlerResolutionStrategy = defaultHandlerResolutionStrategy;
|
||||
this.useDefaultStrategyForPath =
|
||||
this.isPublic || useVersionResolutionStrategyForInternalPaths.has(path);
|
||||
|
@ -146,7 +148,7 @@ export class CoreVersionedRoute implements VersionedRoute {
|
|||
if (!maybeVersion) {
|
||||
if (this.useDefaultStrategyForPath) {
|
||||
version = this.getDefaultVersion();
|
||||
} else if (!this.isDev && !this.isPublic) {
|
||||
} else if (!this.env.mode.dev && !this.isPublic) {
|
||||
// When in production, we default internal routes to v1 to allow
|
||||
// gracefully onboarding of un-versioned to versioned routes
|
||||
version = '1';
|
||||
|
@ -211,9 +213,22 @@ export class CoreVersionedRoute implements VersionedRoute {
|
|||
return injectVersionHeader(version, error);
|
||||
}
|
||||
|
||||
const response = await handler.fn(kibanaRequest, responseFactory);
|
||||
let response = await handler.fn(kibanaRequest, responseFactory);
|
||||
|
||||
if (this.isDev && validation?.response?.[response.status]?.body) {
|
||||
// we don't want to overwrite the header value
|
||||
if (handler.options.options?.deprecated && !response.options.headers?.warning) {
|
||||
response = injectResponseHeaders(
|
||||
{
|
||||
warning: getWarningHeaderMessageFromRouteDeprecation(
|
||||
handler.options.options.deprecated,
|
||||
this.env.packageInfo.version
|
||||
),
|
||||
},
|
||||
response
|
||||
);
|
||||
}
|
||||
|
||||
if (this.env.mode.dev && validation?.response?.[response.status]?.body) {
|
||||
const { [response.status]: responseValidation, unsafe } = validation.response;
|
||||
try {
|
||||
const validator = RouteValidator.from({
|
||||
|
@ -235,7 +250,7 @@ export class CoreVersionedRoute implements VersionedRoute {
|
|||
private validateVersion(version: string) {
|
||||
// We do an additional check here while we only have a single allowed public version
|
||||
// for all public Kibana HTTP APIs
|
||||
if (this.isDev && this.isPublic) {
|
||||
if (this.env.mode.dev && this.isPublic) {
|
||||
const message = isAllowedPublicVersion(version);
|
||||
if (message) {
|
||||
throw new Error(message);
|
||||
|
|
|
@ -11,6 +11,7 @@ import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
|
|||
import { Router } from '../router';
|
||||
import { CoreVersionedRouter } from '.';
|
||||
import { createRouter } from './mocks';
|
||||
import { createTestEnv } from '@kbn/config-mocks';
|
||||
|
||||
const pluginId = Symbol('test');
|
||||
describe('Versioned router', () => {
|
||||
|
@ -21,6 +22,7 @@ describe('Versioned router', () => {
|
|||
versionedRouter = CoreVersionedRouter.from({
|
||||
router,
|
||||
log: loggingSystemMock.createLogger(),
|
||||
env: createTestEnv(),
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import type {
|
|||
} from '@kbn/core-http-server';
|
||||
import { omit } from 'lodash';
|
||||
import { Logger } from '@kbn/logging';
|
||||
import { Env } from '@kbn/config';
|
||||
import { CoreVersionedRoute } from './core_versioned_route';
|
||||
import type { HandlerResolutionStrategy, Method } from './types';
|
||||
import type { Router } from '../router';
|
||||
|
@ -30,7 +31,7 @@ export interface VersionedRouterArgs {
|
|||
*/
|
||||
defaultHandlerResolutionStrategy?: HandlerResolutionStrategy;
|
||||
/** Whether Kibana is running in a dev environment */
|
||||
isDev?: boolean;
|
||||
env: Env;
|
||||
/**
|
||||
* List of internal paths that should use the default handler resolution strategy. By default this
|
||||
* is no routes ([]) because ONLY Elastic clients are intended to call internal routes.
|
||||
|
@ -57,14 +58,14 @@ export class CoreVersionedRouter implements VersionedRouter {
|
|||
router,
|
||||
log,
|
||||
defaultHandlerResolutionStrategy,
|
||||
isDev,
|
||||
env,
|
||||
useVersionResolutionStrategyForInternalPaths,
|
||||
}: VersionedRouterArgs) {
|
||||
return new CoreVersionedRouter(
|
||||
router,
|
||||
log,
|
||||
defaultHandlerResolutionStrategy,
|
||||
isDev,
|
||||
env,
|
||||
useVersionResolutionStrategyForInternalPaths
|
||||
);
|
||||
}
|
||||
|
@ -72,7 +73,7 @@ export class CoreVersionedRouter implements VersionedRouter {
|
|||
public readonly router: Router,
|
||||
private readonly log: Logger,
|
||||
public readonly defaultHandlerResolutionStrategy: HandlerResolutionStrategy = 'oldest',
|
||||
public readonly isDev: boolean = false,
|
||||
public readonly env: Env,
|
||||
useVersionResolutionStrategyForInternalPaths: string[] = []
|
||||
) {
|
||||
this.pluginId = this.router.pluginId;
|
||||
|
@ -94,7 +95,7 @@ export class CoreVersionedRouter implements VersionedRouter {
|
|||
defaultHandlerResolutionStrategy: this.defaultHandlerResolutionStrategy,
|
||||
useVersionResolutionStrategyForInternalPaths:
|
||||
this.useVersionResolutionStrategyForInternalPaths,
|
||||
isDev: this.isDev,
|
||||
env: this.env,
|
||||
},
|
||||
});
|
||||
this.routes.add(route);
|
||||
|
|
|
@ -19,7 +19,9 @@
|
|||
"@kbn/core-logging-server-mocks",
|
||||
"@kbn/logging",
|
||||
"@kbn/core-http-common",
|
||||
"@kbn/logging-mocks"
|
||||
"@kbn/logging-mocks",
|
||||
"@kbn/config-mocks",
|
||||
"@kbn/config"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -32,9 +32,14 @@ import { KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils';
|
|||
import moment from 'moment';
|
||||
import { of, Observable, BehaviorSubject } from 'rxjs';
|
||||
import { mockCoreContext } from '@kbn/core-base-server-mocks';
|
||||
import { createTestEnv, getEnvOptions } from '@kbn/config-mocks';
|
||||
|
||||
const options = getEnvOptions();
|
||||
options.cliArgs.dev = false;
|
||||
const env = createTestEnv({ envOptions: options });
|
||||
|
||||
const routerOptions: RouterOptions = {
|
||||
isDev: false,
|
||||
env,
|
||||
versionedRouterOptions: {
|
||||
defaultHandlerResolutionStrategy: 'oldest',
|
||||
useVersionResolutionStrategyForInternalPaths: [],
|
||||
|
|
|
@ -486,7 +486,7 @@ test('passes versioned config to router', async () => {
|
|||
expect.any(Object), // logger
|
||||
expect.any(Function), // context enhancer
|
||||
expect.objectContaining({
|
||||
isDev: true,
|
||||
env,
|
||||
versionedRouterOptions: {
|
||||
defaultHandlerResolutionStrategy: 'newest',
|
||||
useVersionResolutionStrategyForInternalPaths: ['/foo'],
|
||||
|
|
|
@ -147,7 +147,7 @@ export class HttpService
|
|||
this.log,
|
||||
prebootServerRequestHandlerContext.createHandler.bind(null, this.coreContext.coreId),
|
||||
{
|
||||
isDev: this.env.mode.dev,
|
||||
env: this.env,
|
||||
versionedRouterOptions: getVersionedRouterOptions(config),
|
||||
}
|
||||
);
|
||||
|
@ -196,7 +196,7 @@ export class HttpService
|
|||
) => {
|
||||
const enhanceHandler = this.requestHandlerContext!.createHandler.bind(null, pluginId);
|
||||
const router = new Router<Context>(path, this.log, enhanceHandler, {
|
||||
isDev: this.env.mode.dev,
|
||||
env: this.env,
|
||||
versionedRouterOptions: getVersionedRouterOptions(config),
|
||||
pluginId,
|
||||
});
|
||||
|
|
|
@ -66,11 +66,13 @@ export function adoptToHapiOnPreResponseFormat(fn: OnPreResponseHandler, log: Lo
|
|||
|
||||
try {
|
||||
if (response) {
|
||||
const statusCode: number = isBoom(response)
|
||||
const isResponseBoom = isBoom(response);
|
||||
const statusCode: number = isResponseBoom
|
||||
? response.output.statusCode
|
||||
: response.statusCode;
|
||||
const headers: ResponseHeaders = isResponseBoom ? {} : response.headers;
|
||||
|
||||
const result = await fn(CoreKibanaRequest.from(request), { statusCode }, toolkit);
|
||||
const result = await fn(CoreKibanaRequest.from(request), { statusCode, headers }, toolkit);
|
||||
|
||||
if (preResponseResult.isNext(result)) {
|
||||
if (result.headers) {
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
INTERNAL_API_RESTRICTED_LOGGER_NAME,
|
||||
createBuildNrMismatchLoggerPreResponseHandler,
|
||||
createCustomHeadersPreResponseHandler,
|
||||
createDeprecationWarningHeaderPreResponseHandler,
|
||||
createRestrictInternalRoutesPostAuthHandler,
|
||||
createVersionCheckPostAuthHandler,
|
||||
createXsrfPostAuthHandler,
|
||||
|
@ -547,6 +548,50 @@ describe('customHeaders pre-response handler', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('deprecation header pre-response handler', () => {
|
||||
let toolkit: ToolkitMock;
|
||||
|
||||
beforeEach(() => {
|
||||
toolkit = createToolkit();
|
||||
});
|
||||
|
||||
it('adds the deprecation warning header to the request going to a deprecated route', () => {
|
||||
const kibanaVersion = '19.73.41';
|
||||
const deprecationMessage = 'This is a deprecated endpoint message in the tests';
|
||||
const warningHeader = `299 Kibana-${kibanaVersion} "${deprecationMessage}"`;
|
||||
const handler = createDeprecationWarningHeaderPreResponseHandler(kibanaVersion);
|
||||
|
||||
handler(
|
||||
{ route: { options: { deprecated: { message: deprecationMessage } } } } as any,
|
||||
{} as any,
|
||||
toolkit
|
||||
);
|
||||
|
||||
expect(toolkit.next).toHaveBeenCalledTimes(1);
|
||||
expect(toolkit.next).toHaveBeenCalledWith({
|
||||
headers: {
|
||||
warning: warningHeader,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('does not add the deprecation warning header to the request going to a non-deprecated route', () => {
|
||||
const kibanaVersion = '19.73.41';
|
||||
const deprecationMessage = 'This is a deprecated endpoint message in the tests';
|
||||
const warningHeader = `299 Kibana-${kibanaVersion} "${deprecationMessage}"`;
|
||||
const handler = createDeprecationWarningHeaderPreResponseHandler(kibanaVersion);
|
||||
|
||||
handler({ route: { options: { deprecated: {} } } } as any, {} as any, toolkit);
|
||||
|
||||
expect(toolkit.next).toHaveBeenCalledTimes(1);
|
||||
expect(toolkit.next).not.toHaveBeenCalledWith({
|
||||
headers: {
|
||||
warning: warningHeader,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('build number mismatch logger on error pre-response handler', () => {
|
||||
let logger: jest.Mocked<Logger>;
|
||||
|
||||
|
|
|
@ -13,7 +13,10 @@ import type {
|
|||
OnPreResponseInfo,
|
||||
KibanaRequest,
|
||||
} from '@kbn/core-http-server';
|
||||
import { isSafeMethod } from '@kbn/core-http-router-server-internal';
|
||||
import {
|
||||
getWarningHeaderMessageFromRouteDeprecation,
|
||||
isSafeMethod,
|
||||
} from '@kbn/core-http-router-server-internal';
|
||||
import { Logger } from '@kbn/logging';
|
||||
import { KIBANA_BUILD_NR_HEADER } from '@kbn/core-http-common';
|
||||
import { HttpConfig } from './http_config';
|
||||
|
@ -120,6 +123,24 @@ export const createCustomHeadersPreResponseHandler = (config: HttpConfig): OnPre
|
|||
};
|
||||
};
|
||||
|
||||
export const createDeprecationWarningHeaderPreResponseHandler = (
|
||||
kibanaVersion: string
|
||||
): OnPreResponseHandler => {
|
||||
return (request, response, toolkit) => {
|
||||
// we don't want to overwrite the header value
|
||||
if (!request.route.options.deprecated || response.headers?.warning) {
|
||||
return toolkit.next();
|
||||
}
|
||||
const additionalHeaders = {
|
||||
warning: getWarningHeaderMessageFromRouteDeprecation(
|
||||
request.route.options.deprecated,
|
||||
kibanaVersion
|
||||
),
|
||||
};
|
||||
return toolkit.next({ headers: { ...additionalHeaders } });
|
||||
};
|
||||
};
|
||||
|
||||
const shouldLogBuildNumberMismatch = (
|
||||
serverBuild: { number: number; string: string },
|
||||
request: KibanaRequest,
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
createVersionCheckPostAuthHandler,
|
||||
createBuildNrMismatchLoggerPreResponseHandler,
|
||||
createXsrfPostAuthHandler,
|
||||
createDeprecationWarningHeaderPreResponseHandler,
|
||||
} from './lifecycle_handlers';
|
||||
|
||||
export const registerCoreHandlers = (
|
||||
|
@ -27,6 +28,10 @@ export const registerCoreHandlers = (
|
|||
) => {
|
||||
// add headers based on config
|
||||
registrar.registerOnPreResponse(createCustomHeadersPreResponseHandler(config));
|
||||
// add headers for deprecated endpoints
|
||||
registrar.registerOnPreResponse(
|
||||
createDeprecationWarningHeaderPreResponseHandler(env.packageInfo.version)
|
||||
);
|
||||
// add extra request checks stuff
|
||||
registrar.registerOnPostAuth(createXsrfPostAuthHandler(config));
|
||||
if (config.versioned.strictClientVersionCheck !== false) {
|
||||
|
|
|
@ -65,6 +65,8 @@ export interface OnPreResponseExtensions {
|
|||
*/
|
||||
export interface OnPreResponseInfo {
|
||||
statusCode: number;
|
||||
/** So any pre response handler can check the headers if needed, to avoid an overwrite for example */
|
||||
headers?: ResponseHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -128,6 +128,8 @@ export interface RouteDeprecationInfo {
|
|||
documentationUrl: string;
|
||||
/**
|
||||
* The description message to be displayed for the deprecation.
|
||||
* This will also appear in the '299 Kibana-{version} {message}' header warning when someone calls the route.
|
||||
* Keep the message concise to avoid long header values. It is recommended to keep the message under 255 characters.
|
||||
* Check the README for writing deprecations in `src/core/server/deprecations/README.mdx`
|
||||
*/
|
||||
message?: string;
|
||||
|
|
|
@ -22,6 +22,11 @@ import {
|
|||
} from '@kbn/core-http-server-internal';
|
||||
import { mockCoreContext } from '@kbn/core-base-server-mocks';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
import { createTestEnv, getEnvOptions } from '@kbn/config-mocks';
|
||||
|
||||
const options = getEnvOptions();
|
||||
options.cliArgs.dev = false;
|
||||
const env = createTestEnv({ envOptions: options });
|
||||
|
||||
const CSP_CONFIG = cspConfig.schema.validate({});
|
||||
const EXTERNAL_URL_CONFIG = externalUrlConfig.schema.validate({});
|
||||
|
@ -74,7 +79,7 @@ describe('Http2 - Smoke tests', () => {
|
|||
innerServerListener = innerServer.listener;
|
||||
|
||||
const router = new Router('', logger, enhanceWithContext, {
|
||||
isDev: false,
|
||||
env,
|
||||
versionedRouterOptions: {
|
||||
defaultHandlerResolutionStrategy: 'oldest',
|
||||
},
|
||||
|
@ -177,7 +182,7 @@ describe('Http2 - Smoke tests', () => {
|
|||
innerServerListener = innerServer.listener;
|
||||
|
||||
const router = new Router('', logger, enhanceWithContext, {
|
||||
isDev: false,
|
||||
env,
|
||||
versionedRouterOptions: {
|
||||
defaultHandlerResolutionStrategy: 'oldest',
|
||||
},
|
||||
|
|
|
@ -16,6 +16,11 @@ import { Router } from '@kbn/core-http-router-server-internal';
|
|||
import { HttpServer, HttpConfig } from '@kbn/core-http-server-internal';
|
||||
import { mockCoreContext } from '@kbn/core-base-server-mocks';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
import { createTestEnv, getEnvOptions } from '@kbn/config-mocks';
|
||||
|
||||
const options = getEnvOptions();
|
||||
options.cliArgs.dev = false;
|
||||
const env = createTestEnv({ envOptions: options });
|
||||
|
||||
describe('Http server', () => {
|
||||
let server: HttpServer;
|
||||
|
@ -60,7 +65,7 @@ describe('Http server', () => {
|
|||
innerServerListener = innerServer.listener;
|
||||
|
||||
const router = new Router('', logger, enhanceWithContext, {
|
||||
isDev: false,
|
||||
env,
|
||||
versionedRouterOptions: {
|
||||
defaultHandlerResolutionStrategy: 'oldest',
|
||||
},
|
||||
|
|
|
@ -16,6 +16,9 @@ import { contextServiceMock } from '@kbn/core-http-context-server-mocks';
|
|||
import { ensureRawRequest } from '@kbn/core-http-router-server-internal';
|
||||
import { HttpService } from '@kbn/core-http-server-internal';
|
||||
import { createHttpService } from '@kbn/core-http-server-mocks';
|
||||
import { Env } from '@kbn/config';
|
||||
import { REPO_ROOT } from '@kbn/repo-info';
|
||||
import { getEnvOptions } from '@kbn/config-mocks';
|
||||
|
||||
let server: HttpService;
|
||||
|
||||
|
@ -28,6 +31,8 @@ const setupDeps = {
|
|||
executionContext: executionContextServiceMock.createInternalSetupContract(),
|
||||
};
|
||||
|
||||
const kibanaVersion = Env.createDefault(REPO_ROOT, getEnvOptions()).packageInfo.version;
|
||||
|
||||
beforeEach(async () => {
|
||||
logger = loggingSystemMock.create();
|
||||
server = createHttpService({ logger });
|
||||
|
@ -1503,6 +1508,195 @@ describe('runs with default preResponse handlers', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('runs with default preResponse deprecation handlers', () => {
|
||||
const deprecationMessage = 'This is a deprecated endpoint for testing reasons';
|
||||
const warningString = `299 Kibana-${kibanaVersion} "${deprecationMessage}"`;
|
||||
|
||||
it('should handle a deprecated route and include deprecation warning headers', async () => {
|
||||
const { server: innerServer, createRouter } = await server.setup(setupDeps);
|
||||
const router = createRouter('/');
|
||||
|
||||
router.get(
|
||||
{
|
||||
path: '/deprecated',
|
||||
validate: false,
|
||||
options: {
|
||||
deprecated: {
|
||||
documentationUrl: 'https://fake-url.com',
|
||||
reason: { type: 'deprecate' },
|
||||
severity: 'warning',
|
||||
message: deprecationMessage,
|
||||
},
|
||||
},
|
||||
},
|
||||
(context, req, res) => res.ok({})
|
||||
);
|
||||
|
||||
await server.start();
|
||||
|
||||
const response = await supertest(innerServer.listener).get('/deprecated').expect(200);
|
||||
|
||||
expect(response.header.warning).toMatch(warningString);
|
||||
});
|
||||
|
||||
it('should not add a deprecation warning header to a non deprecated route', async () => {
|
||||
const { server: innerServer, createRouter } = await server.setup(setupDeps);
|
||||
const router = createRouter('/');
|
||||
|
||||
router.get(
|
||||
{
|
||||
path: '/test',
|
||||
validate: false,
|
||||
},
|
||||
(context, req, res) => res.ok({})
|
||||
);
|
||||
|
||||
await server.start();
|
||||
|
||||
const response = await supertest(innerServer.listener).get('/test').expect(200);
|
||||
|
||||
expect(response.header.warning).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not overwrite the warning header if it was already set', async () => {
|
||||
const { server: innerServer, createRouter } = await server.setup(setupDeps);
|
||||
const router = createRouter('/');
|
||||
const expectedWarningHeader = 'This should not get overwritten';
|
||||
|
||||
router.get(
|
||||
{
|
||||
path: '/deprecated',
|
||||
validate: false,
|
||||
options: {
|
||||
deprecated: {
|
||||
documentationUrl: 'https://fake-url.com',
|
||||
reason: { type: 'deprecate' },
|
||||
severity: 'warning',
|
||||
message: deprecationMessage,
|
||||
},
|
||||
},
|
||||
},
|
||||
(context, req, res) => res.ok({ headers: { warning: expectedWarningHeader } })
|
||||
);
|
||||
|
||||
await server.start();
|
||||
|
||||
const response = await supertest(innerServer.listener).get('/deprecated').expect(200);
|
||||
expect(response.header.warning).toMatch(expectedWarningHeader);
|
||||
});
|
||||
|
||||
it('should return the warning header in deprecated v1 but not in non deprecated v2', async () => {
|
||||
const { server: innerServer, createRouter } = await server.setup(setupDeps);
|
||||
const router = createRouter('/');
|
||||
|
||||
router.versioned
|
||||
.get({
|
||||
access: 'internal',
|
||||
path: '/test',
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
version: '1',
|
||||
validate: false,
|
||||
options: {
|
||||
deprecated: {
|
||||
documentationUrl: 'https://fake-url.com',
|
||||
reason: { type: 'deprecate' },
|
||||
severity: 'warning',
|
||||
message: deprecationMessage,
|
||||
},
|
||||
},
|
||||
},
|
||||
async (ctx, req, res) => {
|
||||
return res.ok({ body: { v: '1' } });
|
||||
}
|
||||
)
|
||||
.addVersion(
|
||||
{
|
||||
version: '2',
|
||||
validate: false,
|
||||
},
|
||||
async (ctx, req, res) => {
|
||||
return res.ok({ body: { v: '2' } });
|
||||
}
|
||||
);
|
||||
|
||||
await server.start();
|
||||
|
||||
let response = await supertest(innerServer.listener)
|
||||
.get('/test')
|
||||
.set('Elastic-Api-Version', '1')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.v).toMatch('1');
|
||||
expect(response.header.warning).toMatch(warningString);
|
||||
|
||||
response = await supertest(innerServer.listener)
|
||||
.get('/test')
|
||||
.set('Elastic-Api-Version', '2')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.v).toMatch('2');
|
||||
expect(response.header.warning).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not overwrite the warning header if it was already set (versioned)', async () => {
|
||||
const { server: innerServer, createRouter } = await server.setup(setupDeps);
|
||||
const router = createRouter('/');
|
||||
const expectedWarningHeader = 'This should not get overwritten';
|
||||
|
||||
router.versioned
|
||||
.get({
|
||||
access: 'internal',
|
||||
path: '/test',
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
version: '1',
|
||||
validate: false,
|
||||
options: {
|
||||
deprecated: {
|
||||
documentationUrl: 'https://fake-url.com',
|
||||
reason: { type: 'deprecate' },
|
||||
severity: 'warning',
|
||||
message: deprecationMessage,
|
||||
},
|
||||
},
|
||||
},
|
||||
async (ctx, req, res) => {
|
||||
return res.ok({ body: { v: '1' }, headers: { warning: expectedWarningHeader } });
|
||||
}
|
||||
)
|
||||
.addVersion(
|
||||
{
|
||||
version: '2',
|
||||
validate: false,
|
||||
},
|
||||
async (ctx, req, res) => {
|
||||
return res.ok({ body: { v: '2' } });
|
||||
}
|
||||
);
|
||||
|
||||
await server.start();
|
||||
|
||||
let response = await supertest(innerServer.listener)
|
||||
.get('/test')
|
||||
.set('Elastic-Api-Version', '1')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.v).toMatch('1');
|
||||
expect(response.header.warning).toMatch(expectedWarningHeader);
|
||||
|
||||
response = await supertest(innerServer.listener)
|
||||
.get('/test')
|
||||
.set('Elastic-Api-Version', '2')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.v).toMatch('2');
|
||||
expect(response.header.warning).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('run interceptors in the right order', () => {
|
||||
it('with Auth registered', async () => {
|
||||
const {
|
||||
|
|
|
@ -22,6 +22,11 @@ import { Router } from '@kbn/core-http-router-server-internal';
|
|||
import { createHttpService } from '@kbn/core-http-server-mocks';
|
||||
import type { HttpService } from '@kbn/core-http-server-internal';
|
||||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
import { createTestEnv, getEnvOptions } from '@kbn/config-mocks';
|
||||
|
||||
const options = getEnvOptions();
|
||||
options.cliArgs.dev = false;
|
||||
const env = createTestEnv({ envOptions: options });
|
||||
|
||||
let server: HttpService;
|
||||
let logger: ReturnType<typeof loggingSystemMock.create>;
|
||||
|
@ -2266,7 +2271,7 @@ describe('registerRouterAfterListening', () => {
|
|||
const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, {});
|
||||
|
||||
const otherRouter = new Router('/test', loggerMock.create(), enhanceWithContext, {
|
||||
isDev: false,
|
||||
env,
|
||||
versionedRouterOptions: {
|
||||
defaultHandlerResolutionStrategy: 'oldest',
|
||||
},
|
||||
|
@ -2303,7 +2308,7 @@ describe('registerRouterAfterListening', () => {
|
|||
const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, {});
|
||||
|
||||
const otherRouter = new Router('/test', loggerMock.create(), enhanceWithContext, {
|
||||
isDev: false,
|
||||
env,
|
||||
versionedRouterOptions: {
|
||||
defaultHandlerResolutionStrategy: 'oldest',
|
||||
},
|
||||
|
|
|
@ -23,6 +23,11 @@ import {
|
|||
import { isServerTLS, flattenCertificateChain, fetchPeerCertificate } from './tls_utils';
|
||||
import { mockCoreContext } from '@kbn/core-base-server-mocks';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
import { createTestEnv, getEnvOptions } from '@kbn/config-mocks';
|
||||
|
||||
const options = getEnvOptions();
|
||||
options.cliArgs.dev = false;
|
||||
const env = createTestEnv({ envOptions: options });
|
||||
|
||||
const CSP_CONFIG = cspConfig.schema.validate({});
|
||||
const EXTERNAL_URL_CONFIG = externalUrlConfig.schema.validate({});
|
||||
|
@ -70,7 +75,7 @@ describe('HttpServer - TLS config', () => {
|
|||
const listener = innerServer.listener;
|
||||
|
||||
const router = new Router('', logger, enhanceWithContext, {
|
||||
isDev: false,
|
||||
env,
|
||||
versionedRouterOptions: {
|
||||
defaultHandlerResolutionStrategy: 'oldest',
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue