From 0caa6cdfa71813a913d2516c5c483d94c0db665f Mon Sep 17 00:00:00 2001 From: Milton Hultgren Date: Wed, 14 Aug 2024 18:00:07 +0200 Subject: [PATCH] [kbn/server-route-repository] Add zod support (#190244) This PR adds support for using `zod` as the validation library alongside of `io-ts`. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../index.ts | 2 + .../src/typings.ts | 25 +- .../tsconfig.json | 1 + .../kbn-server-route-repository/README.md | 25 +- packages/kbn-server-route-repository/index.ts | 4 +- .../src/decode_request_params.test.ts | 85 +--- .../src/decode_request_params.ts | 24 +- .../src/make_zod_validation_object.test.ts | 50 ++ .../src/make_zod_validation_object.ts | 27 ++ .../src/register_routes.test.ts | 444 +++++++++++++----- .../src/register_routes.ts | 31 +- .../src/route_validation_object.ts | 20 - .../strip_nullish_request_parameters.test.ts | 35 ++ .../src/strip_nullish_request_parameters.ts | 26 + .../src/test_types.ts | 102 +++- .../src/validate_and_decode_params.test.ts | 69 +++ .../src/validate_and_decode_params.ts | 35 ++ .../src/validation_objects.ts | 27 ++ .../kbn-server-route-repository/tsconfig.json | 2 +- .../apm_routes/register_apm_server_routes.ts | 21 +- .../server/routes/register_routes.ts | 12 +- .../server/routes/register_routes.ts | 12 +- .../server/routes/register_routes.ts | 12 +- .../slo/server/routes/register_routes.ts | 12 +- 24 files changed, 800 insertions(+), 303 deletions(-) create mode 100644 packages/kbn-server-route-repository/src/make_zod_validation_object.test.ts create mode 100644 packages/kbn-server-route-repository/src/make_zod_validation_object.ts delete mode 100644 packages/kbn-server-route-repository/src/route_validation_object.ts create mode 100644 packages/kbn-server-route-repository/src/strip_nullish_request_parameters.test.ts create mode 100644 packages/kbn-server-route-repository/src/strip_nullish_request_parameters.ts create mode 100644 packages/kbn-server-route-repository/src/validate_and_decode_params.test.ts create mode 100644 packages/kbn-server-route-repository/src/validate_and_decode_params.ts create mode 100644 packages/kbn-server-route-repository/src/validation_objects.ts diff --git a/packages/kbn-server-route-repository-utils/index.ts b/packages/kbn-server-route-repository-utils/index.ts index bfcc03c0e64c5..ce2139badd8e6 100644 --- a/packages/kbn-server-route-repository-utils/index.ts +++ b/packages/kbn-server-route-repository-utils/index.ts @@ -24,4 +24,6 @@ export type { DefaultClientOptions, DefaultRouteCreateOptions, DefaultRouteHandlerResources, + IoTsParamsObject, + ZodParamsObject, } from './src/typings'; diff --git a/packages/kbn-server-route-repository-utils/src/typings.ts b/packages/kbn-server-route-repository-utils/src/typings.ts index 0539d9ea1d38e..ff93e760a580a 100644 --- a/packages/kbn-server-route-repository-utils/src/typings.ts +++ b/packages/kbn-server-route-repository-utils/src/typings.ts @@ -9,13 +9,14 @@ import type { HttpFetchOptions } from '@kbn/core-http-browser'; import type { IKibanaResponse } from '@kbn/core-http-server'; import type { - RequestHandlerContext, + KibanaRequest, + KibanaResponseFactory, Logger, + RequestHandlerContext, RouteConfigOptions, RouteMethod, - KibanaRequest, - KibanaResponseFactory, } from '@kbn/core/server'; +import { z } from '@kbn/zod'; import * as t from 'io-ts'; import { RequiredKeys } from 'utility-types'; @@ -30,7 +31,13 @@ type WithoutIncompatibleMethods = Omit t.Encoder; }; -export type RouteParamsRT = WithoutIncompatibleMethods< +export type ZodParamsObject = z.ZodObject<{ + path?: any; + query?: any; + body?: any; +}>; + +export type IoTsParamsObject = WithoutIncompatibleMethods< t.Type<{ path?: any; query?: any; @@ -38,6 +45,8 @@ export type RouteParamsRT = WithoutIncompatibleMethods< }> >; +export type RouteParamsRT = IoTsParamsObject | ZodParamsObject; + export interface RouteState { [endpoint: string]: ServerRoute; } @@ -82,6 +91,10 @@ type ClientRequestParamsOfType = ? MaybeOptional<{ params: t.OutputOf; }> + : TRouteParamsRT extends z.Schema + ? MaybeOptional<{ + params: z.TypeOf; + }> : {}; type DecodedRequestParamsOfType = @@ -89,6 +102,10 @@ type DecodedRequestParamsOfType = ? MaybeOptional<{ params: t.TypeOf; }> + : TRouteParamsRT extends z.Schema + ? MaybeOptional<{ + params: z.TypeOf; + }> : {}; export type EndpointOf = diff --git a/packages/kbn-server-route-repository-utils/tsconfig.json b/packages/kbn-server-route-repository-utils/tsconfig.json index 0f3dd221ec6b7..cb5d9846f6cc8 100644 --- a/packages/kbn-server-route-repository-utils/tsconfig.json +++ b/packages/kbn-server-route-repository-utils/tsconfig.json @@ -19,5 +19,6 @@ "@kbn/core-http-browser", "@kbn/core-http-server", "@kbn/core", + "@kbn/zod", ] } diff --git a/packages/kbn-server-route-repository/README.md b/packages/kbn-server-route-repository/README.md index f46a8f3ee3677..f42bfb4390a20 100644 --- a/packages/kbn-server-route-repository/README.md +++ b/packages/kbn-server-route-repository/README.md @@ -127,22 +127,22 @@ The client translates the endpoint and the options (including request parameters ## Request parameter validation -When creating your routes, you can also provide an `io-ts` codec to be used when validating incoming requests. +When creating your routes, you can provide either a `zod` schema or an `io-ts` codec to be used when validating incoming requests. ```javascript -import * as t from 'io-ts'; +import { z } from '@kbn/zod'; const myRoute = createMyPluginServerRoute({ - endpoint: 'GET /internal/my_plugin/route/{my_path_param}', - params: t.type({ - path: t.type({ - my_path_param: t.string, + endpoint: 'POST /internal/my_plugin/route/{my_path_param}', + params: z.object({ + path: z.object({ + my_path_param: z.string(), }), - query: t.type({ - my_query_param: t.string, + query: z.object({ + my_query_param: z.string(), }), - body: t.type({ - my_body_param: t.string, + body: z.object({ + my_body_param: z.string(), }), }), handler: async (resources) => { @@ -162,7 +162,7 @@ The `params` object is added to the route resources. When calling this endpoint, it will look like this: ```javascript -client('GET /internal/my_plugin/route/{my_path_param}', { +client('POST /internal/my_plugin/route/{my_path_param}', { params: { path: { my_path_param: 'some_path_value', @@ -179,6 +179,9 @@ client('GET /internal/my_plugin/route/{my_path_param}', { Where the shape of `params` is typed to match the expected shape, meaning you don't need to manually use the codec when calling the route. +> When using `zod` you also opt into the Kibana platforms automatic OpenAPI specification generation tooling. +> By adding `server.oas.enabled: true` to your `kibana.yml` and visiting `/api/oas?pluginId=yourPluginId` you can see the generated specification. + ## Public routes To define a public route, you need to change the endpoint path and add a version. diff --git a/packages/kbn-server-route-repository/index.ts b/packages/kbn-server-route-repository/index.ts index e4e43523d25c3..5a2bcb89ec3a4 100644 --- a/packages/kbn-server-route-repository/index.ts +++ b/packages/kbn-server-route-repository/index.ts @@ -9,7 +9,8 @@ export { formatRequest, parseEndpoint } from '@kbn/server-route-repository-utils'; export { createServerRouteFactory } from './src/create_server_route_factory'; export { decodeRequestParams } from './src/decode_request_params'; -export { routeValidationObject } from './src/route_validation_object'; +export { stripNullishRequestParameters } from './src/strip_nullish_request_parameters'; +export { passThroughValidationObject } from './src/validation_objects'; export { registerRoutes } from './src/register_routes'; export type { @@ -24,4 +25,5 @@ export type { RouteState, DefaultRouteCreateOptions, DefaultRouteHandlerResources, + IoTsParamsObject, } from '@kbn/server-route-repository-utils'; diff --git a/packages/kbn-server-route-repository/src/decode_request_params.test.ts b/packages/kbn-server-route-repository/src/decode_request_params.test.ts index 0cb8d280f28e4..9041f68643317 100644 --- a/packages/kbn-server-route-repository/src/decode_request_params.test.ts +++ b/packages/kbn-server-route-repository/src/decode_request_params.test.ts @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { jsonRt } from '@kbn/io-ts-utils'; + import * as t from 'io-ts'; import { decodeRequestParams } from './decode_request_params'; @@ -14,10 +14,9 @@ describe('decodeRequestParams', () => { const decode = () => { return decodeRequestParams( { - params: { + path: { serviceName: 'opbeans-java', }, - body: null, query: { start: '', }, @@ -48,11 +47,10 @@ describe('decodeRequestParams', () => { const decode = () => { return decodeRequestParams( { - params: { + path: { serviceName: 'opbeans-java', extraKey: '', }, - body: null, query: { start: '', }, @@ -74,81 +72,4 @@ describe('decodeRequestParams', () => { path.extraKey" `); }); - - it('returns the decoded output', () => { - const decode = () => { - return decodeRequestParams( - { - params: {}, - query: { - _inspect: 'true', - }, - body: null, - }, - t.type({ - query: t.type({ - _inspect: jsonRt.pipe(t.boolean), - }), - }) - ); - }; - - expect(decode).not.toThrow(); - - expect(decode()).toEqual({ - query: { - _inspect: true, - }, - }); - }); - - it('strips empty params', () => { - const decode = () => { - return decodeRequestParams( - { - params: {}, - query: {}, - body: {}, - }, - t.type({ - body: t.any, - }) - ); - }; - - expect(decode).not.toThrow(); - - expect(decode()).toEqual({}); - }); - - it('allows excess keys in an any type', () => { - const decode = () => { - return decodeRequestParams( - { - params: {}, - query: {}, - body: { - body: { - query: 'foo', - }, - }, - }, - t.type({ - body: t.type({ - body: t.any, - }), - }) - ); - }; - - expect(decode).not.toThrow(); - - expect(decode()).toEqual({ - body: { - body: { - query: 'foo', - }, - }, - }); - }); }); diff --git a/packages/kbn-server-route-repository/src/decode_request_params.ts b/packages/kbn-server-route-repository/src/decode_request_params.ts index bae6e4d4a0e12..fe7c93b2e7397 100644 --- a/packages/kbn-server-route-repository/src/decode_request_params.ts +++ b/packages/kbn-server-route-repository/src/decode_request_params.ts @@ -7,32 +7,16 @@ */ import Boom from '@hapi/boom'; import { formatErrors, strictKeysRt } from '@kbn/io-ts-utils'; -import { RouteParamsRT } from '@kbn/server-route-repository-utils'; +import { IoTsParamsObject } from '@kbn/server-route-repository-utils'; import { isLeft } from 'fp-ts/lib/Either'; import * as t from 'io-ts'; -import { isEmpty, isPlainObject, omitBy } from 'lodash'; -interface KibanaRequestParams { - body: unknown; - query: unknown; - params: unknown; -} - -export function decodeRequestParams( - params: KibanaRequestParams, +export function decodeRequestParams( + params: Partial<{ path: any; query: any; body: any }>, paramsRt: T ): t.OutputOf { - const paramMap = omitBy( - { - path: params.params, - body: params.body, - query: params.query, - }, - (val) => val === null || val === undefined || (isPlainObject(val) && isEmpty(val)) - ); - // decode = validate - const result = strictKeysRt(paramsRt).decode(paramMap); + const result = strictKeysRt(paramsRt).decode(params); if (isLeft(result)) { throw Boom.badRequest(formatErrors(result.left)); diff --git a/packages/kbn-server-route-repository/src/make_zod_validation_object.test.ts b/packages/kbn-server-route-repository/src/make_zod_validation_object.test.ts new file mode 100644 index 0000000000000..b06bfebe613c5 --- /dev/null +++ b/packages/kbn-server-route-repository/src/make_zod_validation_object.test.ts @@ -0,0 +1,50 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { z } from '@kbn/zod'; +import { makeZodValidationObject } from './make_zod_validation_object'; +import { noParamsValidationObject } from './validation_objects'; + +describe('makeZodValidationObject', () => { + it('translate path to params', () => { + const schema = z.object({ + path: z.object({}), + }); + + expect(makeZodValidationObject(schema)).toMatchObject({ + params: expect.anything(), + }); + }); + + it('makes all object types strict', () => { + const schema = z.object({ + path: z.object({}), + query: z.object({}), + body: z.string(), + }); + + const pathStrictSpy = jest.spyOn(schema.shape.path, 'strict'); + const queryStrictSpy = jest.spyOn(schema.shape.query, 'strict'); + + expect(makeZodValidationObject(schema)).toEqual({ + params: pathStrictSpy.mock.results[0].value, + query: queryStrictSpy.mock.results[0].value, + body: schema.shape.body, + }); + }); + + it('sets key to strict empty if schema is missing key', () => { + const schema = z.object({}); + + expect(makeZodValidationObject(schema)).toStrictEqual({ + params: noParamsValidationObject.params, + query: noParamsValidationObject.query, + body: noParamsValidationObject.body, + }); + }); +}); diff --git a/packages/kbn-server-route-repository/src/make_zod_validation_object.ts b/packages/kbn-server-route-repository/src/make_zod_validation_object.ts new file mode 100644 index 0000000000000..5e3634f6f0333 --- /dev/null +++ b/packages/kbn-server-route-repository/src/make_zod_validation_object.ts @@ -0,0 +1,27 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { ZodObject, ZodAny } from '@kbn/zod'; +import { ZodParamsObject } from '@kbn/server-route-repository-utils'; +import { noParamsValidationObject } from './validation_objects'; + +export function makeZodValidationObject(params: ZodParamsObject) { + return { + params: params.shape.path ? asStrict(params.shape.path) : noParamsValidationObject.params, + query: params.shape.query ? asStrict(params.shape.query) : noParamsValidationObject.query, + body: params.shape.body ? asStrict(params.shape.body) : noParamsValidationObject.body, + }; +} + +function asStrict(schema: ZodAny) { + if (schema instanceof ZodObject) { + return schema.strict(); + } else { + return schema; + } +} diff --git a/packages/kbn-server-route-repository/src/register_routes.test.ts b/packages/kbn-server-route-repository/src/register_routes.test.ts index 39180a093835a..8be38bb5891cc 100644 --- a/packages/kbn-server-route-repository/src/register_routes.test.ts +++ b/packages/kbn-server-route-repository/src/register_routes.test.ts @@ -6,59 +6,39 @@ * Side Public License, v 1. */ -import * as t from 'io-ts'; import { CoreSetup, kibanaResponseFactory } from '@kbn/core/server'; import { loggerMock } from '@kbn/logging-mocks'; -import { registerRoutes } from './register_routes'; -import { routeValidationObject } from './route_validation_object'; +import { z } from '@kbn/zod'; +import * as t from 'io-ts'; import { NEVER } from 'rxjs'; +import * as makeZodValidationObject from './make_zod_validation_object'; +import { registerRoutes } from './register_routes'; +import { passThroughValidationObject, noParamsValidationObject } from './validation_objects'; describe('registerRoutes', () => { - const get = jest.fn(); - - const getAddVersion = jest.fn(); - const getWithVersion = jest.fn((_options) => { + const post = jest.fn(); + const postAddVersion = jest.fn(); + const postWithVersion = jest.fn((_options) => { return { - addVersion: getAddVersion, + addVersion: postAddVersion, }; }); - const createRouter = jest.fn().mockReturnValue({ - get, + post, versioned: { - get: getWithVersion, + post: postWithVersion, }, }); - - const internalOptions = { - internal: true, - }; - const publicOptions = { - public: true, - }; - - const internalHandler = jest.fn().mockResolvedValue('internal'); - const publicHandler = jest - .fn() - .mockResolvedValue( - kibanaResponseFactory.custom({ statusCode: 201, body: { message: 'public' } }) - ); - const errorHandler = jest.fn().mockRejectedValue(new Error('error')); - + const coreSetup = { + http: { + createRouter, + }, + } as unknown as CoreSetup; const mockLogger = loggerMock.create(); const mockService = jest.fn(); const mockContext = {}; const mockRequest = { - body: { - bodyParam: 'body', - }, - query: { - queryParam: 'query', - }, - params: { - pathParam: 'path', - }, events: { aborted$: NEVER, }, @@ -66,99 +46,129 @@ describe('registerRoutes', () => { beforeEach(() => { jest.clearAllMocks(); + }); - const coreSetup = { - http: { - createRouter, - }, - } as unknown as CoreSetup; - - const paramsRt = t.type({ - body: t.type({ - bodyParam: t.string, - }), - query: t.type({ - queryParam: t.string, - }), - path: t.type({ - pathParam: t.string, - }), - }); - - registerRoutes({ - core: coreSetup, - repository: { - 'GET /internal/app/feature': { - endpoint: 'GET /internal/app/feature', - handler: internalHandler, - params: paramsRt, - options: internalOptions, - }, - 'GET /api/app/feature version': { - endpoint: 'GET /api/app/feature version', - handler: publicHandler, - params: paramsRt, - options: publicOptions, - }, - 'GET /internal/app/feature/error': { - endpoint: 'GET /internal/app/feature/error', - handler: errorHandler, - params: paramsRt, - options: internalOptions, + it('creates a router and defines the routes', () => { + callRegisterRoutes({ + 'POST /internal/route': { + endpoint: 'POST /internal/route', + handler: jest.fn(), + options: { + internal: true, }, }, - dependencies: { - aService: mockService, + 'POST /api/public_route version': { + endpoint: 'POST /api/public_route version', + handler: jest.fn(), + options: { + public: true, + }, }, - logger: mockLogger, }); - }); - it('creates a router and defines the routes', () => { expect(createRouter).toHaveBeenCalledTimes(1); - expect(get).toHaveBeenCalledTimes(2); + expect(post).toHaveBeenCalledTimes(1); - const [internalRoute] = get.mock.calls[0]; - expect(internalRoute.path).toEqual('/internal/app/feature'); - expect(internalRoute.options).toEqual(internalOptions); - expect(internalRoute.validate).toEqual(routeValidationObject); + const [internalRoute] = post.mock.calls[0]; + expect(internalRoute.path).toEqual('/internal/route'); + expect(internalRoute.options).toEqual({ + internal: true, + }); + expect(internalRoute.validate).toEqual(noParamsValidationObject); - expect(getWithVersion).toHaveBeenCalledTimes(1); - const [publicRoute] = getWithVersion.mock.calls[0]; - expect(publicRoute.path).toEqual('/api/app/feature'); - expect(publicRoute.options).toEqual(publicOptions); + expect(postWithVersion).toHaveBeenCalledTimes(1); + const [publicRoute] = postWithVersion.mock.calls[0]; + expect(publicRoute.path).toEqual('/api/public_route'); + expect(publicRoute.options).toEqual({ + public: true, + }); expect(publicRoute.access).toEqual('public'); - expect(getAddVersion).toHaveBeenCalledTimes(1); - const [versionedRoute] = getAddVersion.mock.calls[0]; + expect(postAddVersion).toHaveBeenCalledTimes(1); + const [versionedRoute] = postAddVersion.mock.calls[0]; expect(versionedRoute.version).toEqual('version'); expect(versionedRoute.validate).toEqual({ - request: routeValidationObject, + request: noParamsValidationObject, }); }); + it('does not allow any params if no schema is provided', () => { + const pathDoesNotAllowExcessKeys = () => { + noParamsValidationObject.params.parse({ + unexpectedKey: 'not_allowed', + }); + }; + const queryDoesNotAllowExcessKeys = () => { + noParamsValidationObject.query.parse({ + unexpectedKey: 'not_allowed', + }); + }; + const bodyDoesNotAllowExcessKeys = () => { + noParamsValidationObject.body.parse({ + unexpectedKey: 'not_allowed', + }); + }; + + expect(pathDoesNotAllowExcessKeys).toThrowErrorMatchingInlineSnapshot(` + "[ + { + \\"code\\": \\"unrecognized_keys\\", + \\"keys\\": [ + \\"unexpectedKey\\" + ], + \\"path\\": [], + \\"message\\": \\"Unrecognized key(s) in object: 'unexpectedKey'\\" + } + ]" + `); + expect(queryDoesNotAllowExcessKeys).toThrowErrorMatchingInlineSnapshot(` + "[ + { + \\"code\\": \\"unrecognized_keys\\", + \\"keys\\": [ + \\"unexpectedKey\\" + ], + \\"path\\": [], + \\"message\\": \\"Unrecognized key(s) in object: 'unexpectedKey'\\" + } + ]" + `); + expect(bodyDoesNotAllowExcessKeys).toThrowErrorMatchingInlineSnapshot(` + "[ + { + \\"code\\": \\"unrecognized_keys\\", + \\"keys\\": [ + \\"unexpectedKey\\" + ], + \\"path\\": [], + \\"message\\": \\"Unrecognized key(s) in object: 'unexpectedKey'\\" + } + ]" + `); + }); + it('calls the route handler with all dependencies', async () => { - const [_, internalRouteHandler] = get.mock.calls[0]; - await internalRouteHandler(mockContext, mockRequest, kibanaResponseFactory); + const handler = jest.fn(); + + callRegisterRoutes({ + 'POST /internal/route': { + endpoint: 'POST /internal/route', + handler, + }, + }); - const [args] = internalHandler.mock.calls[0]; + const [_, wrappedHandler] = post.mock.calls[0]; + await wrappedHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(handler).toBeCalledTimes(1); + const [args] = handler.mock.calls[0]; expect(Object.keys(args).sort()).toEqual( ['aService', 'request', 'response', 'context', 'params', 'logger'].sort() ); const { params, logger, aService, request, response, context } = args; - expect(params).toEqual({ - body: { - bodyParam: 'body', - }, - query: { - queryParam: 'query', - }, - path: { - pathParam: 'path', - }, - }); + expect(params).toEqual(undefined); expect(request).toBe(mockRequest); expect(response).toBe(kibanaResponseFactory); expect(context).toBe(mockContext); @@ -167,38 +177,226 @@ describe('registerRoutes', () => { }); it('wraps a plain route handler result into a response', async () => { - const [_, internalRouteHandler] = get.mock.calls[0]; - const internalResult = await internalRouteHandler( - mockContext, - mockRequest, - kibanaResponseFactory - ); + const handler = jest.fn().mockResolvedValue('result'); - expect(internalHandler).toHaveBeenCalledTimes(1); - expect(internalResult).toEqual({ + callRegisterRoutes({ + 'POST /internal/route': { + endpoint: 'POST /internal/route', + handler, + }, + }); + + const [_, wrappedHandler] = post.mock.calls[0]; + const result = await wrappedHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(handler).toHaveBeenCalledTimes(1); + expect(result).toEqual({ status: 200, - payload: 'internal', - options: { body: 'internal' }, + payload: 'result', + options: { body: 'result' }, }); }); it('allows for route handlers to define a custom response', async () => { - const [_, publicRouteHandler] = getAddVersion.mock.calls[0]; - const publicResult = await publicRouteHandler(mockContext, mockRequest, kibanaResponseFactory); + const handler = jest + .fn() + .mockResolvedValue( + kibanaResponseFactory.custom({ statusCode: 201, body: { message: 'result' } }) + ); + + callRegisterRoutes({ + 'POST /internal/route': { + endpoint: 'POST /internal/route', + handler, + }, + }); - expect(publicHandler).toHaveBeenCalledTimes(1); - expect(publicResult).toEqual({ status: 201, payload: { message: 'public' }, options: {} }); + const [_, wrappedHandler] = post.mock.calls[0]; + const result = await wrappedHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(handler).toHaveBeenCalledTimes(1); + expect(result).toEqual({ status: 201, payload: { message: 'result' }, options: {} }); }); it('translates errors thrown in a route handler to an error response', async () => { - const [_, errorRouteHandler] = get.mock.calls[1]; - const errorResult = await errorRouteHandler(mockContext, mockRequest, kibanaResponseFactory); + const handler = jest.fn().mockRejectedValue(new Error('error')); + + callRegisterRoutes({ + 'POST /internal/route': { + endpoint: 'POST /internal/route', + handler, + }, + }); - expect(errorHandler).toHaveBeenCalledTimes(1); - expect(errorResult).toEqual({ + const [_, wrappedHandler] = post.mock.calls[0]; + const error = await wrappedHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(handler).toHaveBeenCalledTimes(1); + expect(error).toEqual({ status: 500, payload: { message: 'error', attributes: { data: {} } }, options: {}, }); }); + + describe('when using zod', () => { + const makeZodValidationObjectSpy = jest.spyOn( + makeZodValidationObject, + 'makeZodValidationObject' + ); + + const zodParamsRt = z.object({ + body: z.object({ + bodyParam: z.string(), + }), + query: z.object({ + queryParam: z.string(), + }), + path: z.object({ + pathParam: z.string(), + }), + }); + + it('uses Core validation', () => { + callRegisterRoutes({ + 'POST /internal/route': { + endpoint: 'POST /internal/route', + params: zodParamsRt, + handler: jest.fn, + }, + }); + + const [internalRoute] = post.mock.calls[0]; + expect(makeZodValidationObjectSpy).toHaveBeenCalledWith(zodParamsRt); + expect(internalRoute.validate).toEqual(makeZodValidationObjectSpy.mock.results[0].value); + }); + + it('passes on params', async () => { + const handler = jest.fn(); + callRegisterRoutes({ + 'POST /internal/route': { + endpoint: 'POST /internal/route', + params: zodParamsRt, + handler, + }, + }); + + const [_, wrappedHandler] = post.mock.calls[0]; + + await wrappedHandler( + mockContext, + { + ...mockRequest, + params: { + pathParam: 'path', + }, + query: { + queryParam: 'query', + }, + body: { + bodyParam: 'body', + }, + }, + kibanaResponseFactory + ); + + expect(handler).toBeCalledTimes(1); + const [args] = handler.mock.calls[0]; + const { params } = args; + expect(params).toEqual({ + path: { + pathParam: 'path', + }, + query: { + queryParam: 'query', + }, + body: { + bodyParam: 'body', + }, + }); + }); + }); + + describe('when using io-ts', () => { + const iotsParamsRt = t.type({ + body: t.type({ + bodyParam: t.string, + }), + query: t.type({ + queryParam: t.string, + }), + path: t.type({ + pathParam: t.string, + }), + }); + + it('bypasses Core validation', () => { + callRegisterRoutes({ + 'POST /internal/route': { + endpoint: 'POST /internal/route', + params: iotsParamsRt, + handler: jest.fn, + }, + }); + + const [internalRoute] = post.mock.calls[0]; + expect(internalRoute.validate).toEqual(passThroughValidationObject); + }); + + it('decodes params', async () => { + const handler = jest.fn(); + callRegisterRoutes({ + 'POST /internal/route': { + endpoint: 'POST /internal/route', + params: iotsParamsRt, + handler, + }, + }); + + const [_, wrappedHandler] = post.mock.calls[0]; + + await wrappedHandler( + mockContext, + { + ...mockRequest, + params: { + pathParam: 'path', + }, + query: { + queryParam: 'query', + }, + body: { + bodyParam: 'body', + }, + }, + kibanaResponseFactory + ); + + expect(handler).toBeCalledTimes(1); + const [args] = handler.mock.calls[0]; + const { params } = args; + expect(params).toEqual({ + path: { + pathParam: 'path', + }, + query: { + queryParam: 'query', + }, + body: { + bodyParam: 'body', + }, + }); + }); + }); + + function callRegisterRoutes(repository: any) { + registerRoutes({ + core: coreSetup, + logger: mockLogger, + dependencies: { + aService: mockService, + }, + repository, + }); + } }); diff --git a/packages/kbn-server-route-repository/src/register_routes.ts b/packages/kbn-server-route-repository/src/register_routes.ts index fcf3c5c3281ee..56f42fb499225 100644 --- a/packages/kbn-server-route-repository/src/register_routes.ts +++ b/packages/kbn-server-route-repository/src/register_routes.ts @@ -5,6 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ + import { errors } from '@elastic/elasticsearch'; import { isBoom } from '@hapi/boom'; import type { RequestHandlerContext } from '@kbn/core-http-request-handler-context-server'; @@ -12,15 +13,17 @@ import type { KibanaRequest, KibanaResponseFactory } from '@kbn/core-http-server import { isKibanaResponse } from '@kbn/core-http-server'; import type { CoreSetup } from '@kbn/core-lifecycle-server'; import type { Logger } from '@kbn/logging'; -import * as t from 'io-ts'; -import { merge, pick } from 'lodash'; import { ServerRoute, ServerRouteCreateOptions, + ZodParamsObject, parseEndpoint, } from '@kbn/server-route-repository-utils'; -import { decodeRequestParams } from './decode_request_params'; -import { routeValidationObject } from './route_validation_object'; +import { isZod } from '@kbn/zod'; +import { merge } from 'lodash'; +import { passThroughValidationObject, noParamsValidationObject } from './validation_objects'; +import { validateAndDecodeParams } from './validate_and_decode_params'; +import { makeZodValidationObject } from './make_zod_validation_object'; const CLIENT_CLOSED_REQUEST = { statusCode: 499, @@ -55,12 +58,7 @@ export function registerRoutes>({ response: KibanaResponseFactory ) => { try { - const runtimeType = params || t.strict({}); - - const validatedParams = decodeRequestParams( - pick(request, 'params', 'body', 'query'), - runtimeType - ); + const validatedParams = validateAndDecodeParams(request, params); const { aborted, result } = await Promise.race([ handler({ @@ -122,12 +120,21 @@ export function registerRoutes>({ logger.debug(`Registering endpoint ${endpoint}`); + let validationObject; + if (params === undefined) { + validationObject = noParamsValidationObject; + } else if (isZod(params)) { + validationObject = makeZodValidationObject(params as ZodParamsObject); + } else { + validationObject = passThroughValidationObject; + } + if (!version) { router[method]( { path: pathname, options, - validate: routeValidationObject, + validate: validationObject, }, wrappedHandler ); @@ -140,7 +147,7 @@ export function registerRoutes>({ { version, validate: { - request: routeValidationObject, + request: validationObject, }, }, wrappedHandler diff --git a/packages/kbn-server-route-repository/src/route_validation_object.ts b/packages/kbn-server-route-repository/src/route_validation_object.ts deleted file mode 100644 index 9d033e31fc324..0000000000000 --- a/packages/kbn-server-route-repository/src/route_validation_object.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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 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 or the Server - * Side Public License, v 1. - */ -import { schema } from '@kbn/config-schema'; - -const anyObject = schema.object({}, { unknowns: 'allow' }); - -export const routeValidationObject = { - // `body` can be null, but `validate` expects non-nullable types - // if any validation is defined. Not having validation currently - // means we don't get the payload. See - // https://github.com/elastic/kibana/issues/50179 - body: schema.nullable(schema.oneOf([anyObject, schema.string()])), - params: anyObject, - query: anyObject, -}; diff --git a/packages/kbn-server-route-repository/src/strip_nullish_request_parameters.test.ts b/packages/kbn-server-route-repository/src/strip_nullish_request_parameters.test.ts new file mode 100644 index 0000000000000..b76c28048aeb3 --- /dev/null +++ b/packages/kbn-server-route-repository/src/strip_nullish_request_parameters.test.ts @@ -0,0 +1,35 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { stripNullishRequestParameters } from './strip_nullish_request_parameters'; + +describe('stripNullishRequestParameters', () => { + it('translate params to path', () => { + expect( + stripNullishRequestParameters({ + params: { + something: 'test', + }, + }) + ).toEqual({ + path: { + something: 'test', + }, + }); + }); + + it('removes invalid values', () => { + expect( + stripNullishRequestParameters({ + params: undefined, + query: null, + body: {}, + }) + ).toEqual({}); + }); +}); diff --git a/packages/kbn-server-route-repository/src/strip_nullish_request_parameters.ts b/packages/kbn-server-route-repository/src/strip_nullish_request_parameters.ts new file mode 100644 index 0000000000000..06668ac55a95a --- /dev/null +++ b/packages/kbn-server-route-repository/src/strip_nullish_request_parameters.ts @@ -0,0 +1,26 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { omitBy, isPlainObject, isEmpty } from 'lodash'; + +interface KibanaRequestParams { + body?: unknown; + query?: unknown; + params?: unknown; +} + +export function stripNullishRequestParameters(params: KibanaRequestParams) { + return omitBy<{ path: any; body: any; query: any }>( + { + path: params.params, + query: params.query, + body: params.body, + }, + (val) => val === null || val === undefined || (isPlainObject(val) && isEmpty(val)) + ); +} diff --git a/packages/kbn-server-route-repository/src/test_types.ts b/packages/kbn-server-route-repository/src/test_types.ts index 16447a6ef000f..ce4fbb6303c5c 100644 --- a/packages/kbn-server-route-repository/src/test_types.ts +++ b/packages/kbn-server-route-repository/src/test_types.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ import * as t from 'io-ts'; +import { z } from '@kbn/zod'; import { kibanaResponseFactory } from '@kbn/core/server'; import { EndpointOf, ReturnOf, RouteRepositoryClient } from '@kbn/server-route-repository-utils'; import { createServerRouteFactory } from './create_server_route_factory'; @@ -39,6 +40,18 @@ createServerRouteFactory<{}, {}>()({ }, }); +createServerRouteFactory<{}, {}>()({ + endpoint: 'GET /internal/endpoint_with_params', + params: z.object({ + path: z.object({ + serviceName: z.string(), + }), + }), + handler: async (resources) => { + assertType<{ params: { path: { serviceName: string } } }>(resources); + }, +}); + // Resources should be passed to the request handler. createServerRouteFactory<{ context: { getSpaceId: () => string } }, {}>()({ endpoint: 'GET /internal/endpoint_with_params', @@ -53,6 +66,19 @@ createServerRouteFactory<{ context: { getSpaceId: () => string } }, {}>()({ }, }); +createServerRouteFactory<{ context: { getSpaceId: () => string } }, {}>()({ + endpoint: 'GET /internal/endpoint_with_params', + params: z.object({ + path: z.object({ + serviceName: z.string(), + }), + }), + handler: async ({ context }) => { + const spaceId = context.getSpaceId(); + assertType(spaceId); + }, +}); + // Create options are available when registering a route. createServerRouteFactory<{}, { options: { tags: string[] } }>()({ endpoint: 'GET /internal/endpoint_with_params', @@ -125,6 +151,36 @@ const repository = { }; }, }), + ...createServerRoute({ + endpoint: 'GET /internal/endpoint_with_params_zod', + params: z.object({ + path: z.object({ + serviceName: z.string(), + }), + }), + handler: async () => { + return { + yesParamsForMe: true, + }; + }, + }), + ...createServerRoute({ + endpoint: 'GET /internal/endpoint_with_optional_params_zod', + params: z + .object({ + path: z + .object({ + serviceName: z.string(), + }) + .partial(), + }) + .partial(), + handler: async () => { + return { + someParamsForMe: true, + }; + }, + }), ...createServerRoute({ endpoint: 'GET /internal/endpoint_returning_result', handler: async () => { @@ -153,6 +209,10 @@ assertType>>([ 'GET /internal/endpoint_with_params', 'GET /internal/endpoint_without_params', 'GET /internal/endpoint_with_optional_params', + 'GET /internal/endpoint_with_params_zod', + 'GET /internal/endpoint_with_optional_params_zod', + 'GET /internal/endpoint_returning_result', + 'GET /internal/endpoint_returning_kibana_response', ]); // @ts-expect-error Type '"this_endpoint_does_not_exist"' is not assignable to type '"endpoint_without_params" | "endpoint_with_params" | "endpoint_with_optional_params"' @@ -208,11 +268,23 @@ client('GET /internal/endpoint_with_params', { timeout: 1, }); +client('GET /internal/endpoint_with_params_zod', { + params: { + // @ts-expect-error property 'serviceName' is missing in type '{}' + path: {}, + }, + timeout: 1, +}); + // Params are optional if the codec has no required keys client('GET /internal/endpoint_with_optional_params', { timeout: 1, }); +client('GET /internal/endpoint_with_optional_params_zod', { + timeout: 1, +}); + // If optional, an error will still occur if the params do not match client('GET /internal/endpoint_with_optional_params', { timeout: 1, @@ -222,6 +294,14 @@ client('GET /internal/endpoint_with_optional_params', { }, }); +client('GET /internal/endpoint_with_optional_params_zod', { + timeout: 1, + params: { + // @ts-expect-error Object literal may only specify known properties, and 'path' does not exist in type + path: '', + }, +}); + // The return type is correctly inferred client('GET /internal/endpoint_with_params', { params: { @@ -241,6 +321,24 @@ client('GET /internal/endpoint_with_params', { }>(res); }); +client('GET /internal/endpoint_with_params_zod', { + params: { + path: { + serviceName: '', + }, + }, + timeout: 1, +}).then((res) => { + assertType<{ + noParamsForMe: boolean; + // @ts-expect-error Property 'noParamsForMe' is missing in type + }>(res); + + assertType<{ + yesParamsForMe: boolean; + }>(res); +}); + client('GET /internal/endpoint_returning_result', { timeout: 1, }).then((res) => { @@ -261,7 +359,7 @@ client('GET /internal/endpoint_returning_kibana_response', { assertType<{ path: { serviceName: string } }>( decodeRequestParams( { - params: { + path: { serviceName: 'serviceName', }, body: undefined, @@ -275,7 +373,7 @@ assertType<{ path: { serviceName: boolean } }>( // @ts-expect-error The types of 'path.serviceName' are incompatible between these types. decodeRequestParams( { - params: { + path: { serviceName: 'serviceName', }, body: undefined, diff --git a/packages/kbn-server-route-repository/src/validate_and_decode_params.test.ts b/packages/kbn-server-route-repository/src/validate_and_decode_params.test.ts new file mode 100644 index 0000000000000..a60c6322f1d7a --- /dev/null +++ b/packages/kbn-server-route-repository/src/validate_and_decode_params.test.ts @@ -0,0 +1,69 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { KibanaRequest } from '@kbn/core-http-server'; +import { z } from '@kbn/zod'; +import * as t from 'io-ts'; +import { validateAndDecodeParams } from './validate_and_decode_params'; + +describe('validateAndDecodeParams', () => { + it('does nothing if no schema is provided', () => { + const request = {} as KibanaRequest; + expect(validateAndDecodeParams(request, undefined)).toEqual(undefined); + }); + + it('only does formatting when using zod', () => { + const request = { + params: { + my_path_param: 'test', + }, + query: {}, + } as KibanaRequest; + + expect(validateAndDecodeParams(request, z.object({}))).toEqual({ + path: { + my_path_param: 'test', + }, + }); + }); + + it('additionally performs validation when using zod', () => { + const schema = t.type({ + path: t.type({ + my_path_param: t.string, + }), + }); + + const validRequest = { + params: { + my_path_param: 'test', + }, + query: {}, + } as KibanaRequest; + + expect(validateAndDecodeParams(validRequest, schema)).toEqual({ + path: { + my_path_param: 'test', + }, + }); + + const invalidRequest = { + params: { + my_unexpected_param: 'test', + }, + } as KibanaRequest; + const shouldThrow = () => { + return validateAndDecodeParams(invalidRequest, schema); + }; + + expect(shouldThrow).toThrowErrorMatchingInlineSnapshot(` + "Failed to validate: + in /path/my_path_param: undefined does not match expected type string" + `); + }); +}); diff --git a/packages/kbn-server-route-repository/src/validate_and_decode_params.ts b/packages/kbn-server-route-repository/src/validate_and_decode_params.ts new file mode 100644 index 0000000000000..36faf00e40afd --- /dev/null +++ b/packages/kbn-server-route-repository/src/validate_and_decode_params.ts @@ -0,0 +1,35 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { KibanaRequest } from '@kbn/core-http-server'; +import { ZodParamsObject, IoTsParamsObject } from '@kbn/server-route-repository-utils'; +import { isZod } from '@kbn/zod'; +import { decodeRequestParams } from './decode_request_params'; +import { stripNullishRequestParameters } from './strip_nullish_request_parameters'; + +export function validateAndDecodeParams( + request: KibanaRequest, + paramsSchema: ZodParamsObject | IoTsParamsObject | undefined +) { + if (paramsSchema === undefined) { + return undefined; + } + + const params = stripNullishRequestParameters({ + params: request.params, + body: request.body, + query: request.query, + }); + + if (isZod(paramsSchema)) { + // Already validated by platform + return params; + } + + return decodeRequestParams(params, paramsSchema); +} diff --git a/packages/kbn-server-route-repository/src/validation_objects.ts b/packages/kbn-server-route-repository/src/validation_objects.ts new file mode 100644 index 0000000000000..9b4bab477ae3c --- /dev/null +++ b/packages/kbn-server-route-repository/src/validation_objects.ts @@ -0,0 +1,27 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { z } from '@kbn/zod'; + +export const passThroughValidationObject = { + body: z.any(), + params: z.any(), + query: z.any(), +}; + +export const noParamsValidationObject = { + params: z.object({}).strict(), + query: z.object({}).strict(), + body: z.union([ + // If the route uses POST, the body should be empty object or null + z.object({}).strict(), + z.null(), + // If the route uses GET, body is undefined, + z.undefined(), + ]), +}; diff --git a/packages/kbn-server-route-repository/tsconfig.json b/packages/kbn-server-route-repository/tsconfig.json index 5202a5bf0c422..bb8a0847c39b6 100644 --- a/packages/kbn-server-route-repository/tsconfig.json +++ b/packages/kbn-server-route-repository/tsconfig.json @@ -12,7 +12,6 @@ "**/*.ts" ], "kbn_references": [ - "@kbn/config-schema", "@kbn/io-ts-utils", "@kbn/core-http-request-handler-context-server", "@kbn/core-http-server", @@ -21,6 +20,7 @@ "@kbn/core", "@kbn/logging-mocks", "@kbn/server-route-repository-utils", + "@kbn/zod", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/observability_solution/apm/server/routes/apm_routes/register_apm_server_routes.ts b/x-pack/plugins/observability_solution/apm/server/routes/apm_routes/register_apm_server_routes.ts index 36f6d107653b8..5a2af3e7dc066 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/apm_routes/register_apm_server_routes.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/apm_routes/register_apm_server_routes.ts @@ -10,12 +10,16 @@ import * as t from 'io-ts'; import { Logger, KibanaRequest, KibanaResponseFactory, RouteRegistrar } from '@kbn/core/server'; import { errors } from '@elastic/elasticsearch'; import agent from 'elastic-apm-node'; -import { ServerRouteRepository } from '@kbn/server-route-repository'; +import { + IoTsParamsObject, + ServerRouteRepository, + stripNullishRequestParameters, +} from '@kbn/server-route-repository'; import { merge } from 'lodash'; import { decodeRequestParams, parseEndpoint, - routeValidationObject, + passThroughValidationObject, } from '@kbn/server-route-repository'; import { jsonRt, mergeRt } from '@kbn/io-ts-utils'; import { InspectResponse } from '@kbn/observability-plugin/typings/common'; @@ -24,7 +28,6 @@ import { VersionedRouteRegistrar } from '@kbn/core-http-server'; import { IRuleDataClient } from '@kbn/rule-registry-plugin/server'; import type { APMIndices } from '@kbn/apm-data-access-plugin/server'; import { ApmFeatureFlags } from '../../../common/apm_feature_flags'; -import { pickKeys } from '../../../common/utils/pick_keys'; import type { APMCore, MinimalApmPluginRequestHandlerContext, @@ -94,10 +97,14 @@ export function registerRoutes({ inspectableEsQueriesMap.set(request, []); try { - const runtimeType = params ? mergeRt(params, inspectRt) : inspectRt; + const runtimeType = params ? mergeRt(params as IoTsParamsObject, inspectRt) : inspectRt; const validatedParams = decodeRequestParams( - pickKeys(request, 'params', 'body', 'query'), + stripNullishRequestParameters({ + params: request.params, + body: request.body, + query: request.query, + }), runtimeType ); @@ -210,7 +217,7 @@ export function registerRoutes({ { path: pathname, options, - validate: routeValidationObject, + validate: passThroughValidationObject, }, wrappedHandler ); @@ -228,7 +235,7 @@ export function registerRoutes({ { version, validate: { - request: routeValidationObject, + request: passThroughValidationObject, }, }, wrappedHandler diff --git a/x-pack/plugins/observability_solution/dataset_quality/server/routes/register_routes.ts b/x-pack/plugins/observability_solution/dataset_quality/server/routes/register_routes.ts index 94eb3103c39d4..4e1970d7fc887 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/server/routes/register_routes.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/server/routes/register_routes.ts @@ -8,10 +8,12 @@ import { errors } from '@elastic/elasticsearch'; import Boom from '@hapi/boom'; import { CoreSetup, Logger, RouteRegistrar } from '@kbn/core/server'; import { + IoTsParamsObject, ServerRouteRepository, decodeRequestParams, + stripNullishRequestParameters, parseEndpoint, - routeValidationObject, + passThroughValidationObject, } from '@kbn/server-route-repository'; import * as t from 'io-ts'; import { DatasetQualityRequestHandlerContext } from '../types'; @@ -43,18 +45,18 @@ export function registerRoutes({ (router[method] as RouteRegistrar)( { path: pathname, - validate: routeValidationObject, + validate: passThroughValidationObject, options, }, async (context, request, response) => { try { const decodedParams = decodeRequestParams( - { + stripNullishRequestParameters({ params: request.params, body: request.body, query: request.query, - }, - params ?? t.strict({}) + }), + (params as IoTsParamsObject) ?? t.strict({}) ); const data = (await handler({ diff --git a/x-pack/plugins/observability_solution/observability/server/routes/register_routes.ts b/x-pack/plugins/observability_solution/observability/server/routes/register_routes.ts index f0f41a70bec81..b885050eb64c7 100644 --- a/x-pack/plugins/observability_solution/observability/server/routes/register_routes.ts +++ b/x-pack/plugins/observability_solution/observability/server/routes/register_routes.ts @@ -10,9 +10,11 @@ import { RulesClientApi } from '@kbn/alerting-plugin/server/types'; import { CoreSetup, KibanaRequest, Logger, RouteRegistrar } from '@kbn/core/server'; import { RuleDataPluginService } from '@kbn/rule-registry-plugin/server'; import { + IoTsParamsObject, decodeRequestParams, + stripNullishRequestParameters, parseEndpoint, - routeValidationObject, + passThroughValidationObject, } from '@kbn/server-route-repository'; import { SpacesPluginStart } from '@kbn/spaces-plugin/server'; import axios from 'axios'; @@ -57,18 +59,18 @@ export function registerRoutes({ config, repository, core, logger, dependencies (router[method] as RouteRegistrar)( { path: pathname, - validate: routeValidationObject, + validate: passThroughValidationObject, options, }, async (context, request, response) => { try { const decodedParams = decodeRequestParams( - { + stripNullishRequestParameters({ params: request.params, body: request.body, query: request.query, - }, - params ?? t.strict({}) + }), + (params as IoTsParamsObject) ?? t.strict({}) ); const data = await handler({ diff --git a/x-pack/plugins/observability_solution/observability_onboarding/server/routes/register_routes.ts b/x-pack/plugins/observability_solution/observability_onboarding/server/routes/register_routes.ts index 9603d97e63faa..8fe51623510eb 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/server/routes/register_routes.ts +++ b/x-pack/plugins/observability_solution/observability_onboarding/server/routes/register_routes.ts @@ -9,10 +9,12 @@ import Boom from '@hapi/boom'; import type { IKibanaResponse } from '@kbn/core/server'; import { CoreSetup, Logger, RouteRegistrar } from '@kbn/core/server'; import { + IoTsParamsObject, ServerRouteRepository, decodeRequestParams, + stripNullishRequestParameters, parseEndpoint, - routeValidationObject, + passThroughValidationObject, } from '@kbn/server-route-repository'; import * as t from 'io-ts'; import { ObservabilityOnboardingConfig } from '..'; @@ -52,18 +54,18 @@ export function registerRoutes({ (router[method] as RouteRegistrar)( { path: pathname, - validate: routeValidationObject, + validate: passThroughValidationObject, options, }, async (context, request, response) => { try { const decodedParams = decodeRequestParams( - { + stripNullishRequestParameters({ params: request.params, body: request.body, query: request.query, - }, - params ?? t.strict({}) + }), + (params as IoTsParamsObject) ?? t.strict({}) ); const data = (await handler({ diff --git a/x-pack/plugins/observability_solution/slo/server/routes/register_routes.ts b/x-pack/plugins/observability_solution/slo/server/routes/register_routes.ts index 8ddabede0d190..6a7d2d08bdd1b 100644 --- a/x-pack/plugins/observability_solution/slo/server/routes/register_routes.ts +++ b/x-pack/plugins/observability_solution/slo/server/routes/register_routes.ts @@ -14,9 +14,11 @@ import { RuleRegistryPluginSetupContract, } from '@kbn/rule-registry-plugin/server'; import { + IoTsParamsObject, decodeRequestParams, + stripNullishRequestParameters, parseEndpoint, - routeValidationObject, + passThroughValidationObject, } from '@kbn/server-route-repository'; import { SpacesPluginStart } from '@kbn/spaces-plugin/server'; import axios from 'axios'; @@ -59,18 +61,18 @@ export function registerRoutes({ config, repository, core, logger, dependencies (router[method] as RouteRegistrar)( { path: pathname, - validate: routeValidationObject, + validate: passThroughValidationObject, options, }, async (context, request, response) => { try { const decodedParams = decodeRequestParams( - { + stripNullishRequestParameters({ params: request.params, body: request.body, query: request.query, - }, - params ?? t.strict({}) + }), + (params as IoTsParamsObject) ?? t.strict({}) ); const data = await handler({