From f3f1bd2ec603867cf1f6da8bf9ec9f8b3f258369 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Sat, 21 Dec 2019 00:02:35 +0000 Subject: [PATCH] [NP] Allow custom validations in HTTP Routes apart from @kbn/config-schema (#51919) (#53714) * [NP] Allow custom validations in HTTP Routes apart from @kbn/config-schema * API docs * Allow validate function in the route handler (run-code validation) * Prefix RouteXXX + Params and Body Validation Aliases * Fix test broken by lodash * Update API docs * Add default types for simpler manual declaration * Add run-time validation of the RouteValidateSpec * Expose RouteValidationError instead of SchemaTypeError * RouteValidator as a class to match config-schema interface * Test for not-inline handler (need to check IRouter for #47047) * Add preValidation of the input for a safer custom validation * Better types for RouteHandlers * [NP] Move route validation to RouteValidator wrapper * Use the class only internally but maintain the same API * Fix types * Ensure RouteValidator instance in KibanaRequest.from * Fix validator.tests (Buffer.from instead of new Buffer) * Default precheck should allow null values * Also allow undefined in preChecks * MR feedback fixes * Provide RouteValidationResolver to the validation function * Add functional tests * Fix new functional tests * Fix validator additional test * Fix test with new resolver * Remove unused import * Rename ValidationResolver to ValidationResultFactory and change the interface to look more like the KibanaResponseFactory Co-authored-by: Elastic Machine Co-authored-by: Elastic Machine --- ...lugin-server.irouter.handlelegacyerrors.md | 2 +- .../server/kibana-plugin-server.irouter.md | 2 +- .../core/server/kibana-plugin-server.md | 8 +- .../kibana-plugin-server.requesthandler.md | 2 +- .../kibana-plugin-server.routeconfig.md | 4 +- ...bana-plugin-server.routeconfig.validate.md | 2 +- .../kibana-plugin-server.routeregistrar.md | 2 +- .../kibana-plugin-server.routeschemas.body.md | 11 - .../kibana-plugin-server.routeschemas.md | 22 -- ...ibana-plugin-server.routeschemas.params.md | 11 - ...kibana-plugin-server.routeschemas.query.md | 11 - ...rver.routevalidationerror._constructor_.md | 21 ++ ...bana-plugin-server.routevalidationerror.md | 20 ++ ...a-plugin-server.routevalidationfunction.md | 42 +++ ...routevalidationresultfactory.badrequest.md | 13 + ...gin-server.routevalidationresultfactory.md | 23 ++ ...-server.routevalidationresultfactory.ok.md | 13 + ...ibana-plugin-server.routevalidationspec.md | 15 + ...plugin-server.routevalidatorconfig.body.md | 13 + ...bana-plugin-server.routevalidatorconfig.md | 22 ++ ...ugin-server.routevalidatorconfig.params.md | 13 + ...lugin-server.routevalidatorconfig.query.md | 13 + ...-plugin-server.routevalidatorfullconfig.md | 13 + ...ana-plugin-server.routevalidatoroptions.md | 20 ++ ...gin-server.routevalidatoroptions.unsafe.md | 17 ++ packages/kbn-config-schema/src/index.ts | 1 + src/core/server/http/http_server.mocks.ts | 2 +- src/core/server/http/http_server.test.ts | 233 ++++++++++++++- src/core/server/http/index.ts | 8 +- .../http/integration_tests/router.test.ts | 110 +++++++ src/core/server/http/router/error_wrapper.ts | 5 +- src/core/server/http/router/index.ts | 11 +- src/core/server/http/router/request.test.ts | 53 ++++ src/core/server/http/router/request.ts | 58 ++-- src/core/server/http/router/route.ts | 27 +- src/core/server/http/router/router.test.ts | 10 +- src/core/server/http/router/router.ts | 92 ++---- .../server/http/router/validator/index.ts | 29 ++ .../http/router/validator/validator.test.ts | 135 +++++++++ .../server/http/router/validator/validator.ts | 280 ++++++++++++++++++ .../http/router/validator/validator_error.ts | 34 +++ src/core/server/index.ts | 8 +- src/core/server/server.api.md | 63 +++- .../tutorials/lib/tutorials_registry_types.ts | 14 +- .../plugins/core_plugin_b/server/plugin.ts | 20 ++ .../core_plugins/server_plugins.ts | 24 ++ .../apm/server/routes/create_api/index.ts | 2 +- .../lib/adapters/framework/adapter_types.ts | 8 +- .../framework/kibana_framework_adapter.ts | 9 +- .../server/routes/catch_error_handler.ts | 7 +- .../register_privileges_with_cluster.test.ts | 2 +- .../server/routes/authentication/saml.ts | 4 +- .../routes/authorization/roles/delete.ts | 4 +- .../server/routes/authorization/roles/get.ts | 4 +- .../server/routes/licensed_route_handler.ts | 11 +- .../routes/api/external/copy_to_space.test.ts | 27 +- .../server/routes/api/external/delete.test.ts | 11 +- .../server/routes/api/external/post.test.ts | 9 +- .../server/routes/api/external/put.test.ts | 9 +- .../routes/lib/licensed_route_handler.ts | 11 +- 60 files changed, 1369 insertions(+), 301 deletions(-) delete mode 100644 docs/development/core/server/kibana-plugin-server.routeschemas.body.md delete mode 100644 docs/development/core/server/kibana-plugin-server.routeschemas.md delete mode 100644 docs/development/core/server/kibana-plugin-server.routeschemas.params.md delete mode 100644 docs/development/core/server/kibana-plugin-server.routeschemas.query.md create mode 100644 docs/development/core/server/kibana-plugin-server.routevalidationerror._constructor_.md create mode 100644 docs/development/core/server/kibana-plugin-server.routevalidationerror.md create mode 100644 docs/development/core/server/kibana-plugin-server.routevalidationfunction.md create mode 100644 docs/development/core/server/kibana-plugin-server.routevalidationresultfactory.badrequest.md create mode 100644 docs/development/core/server/kibana-plugin-server.routevalidationresultfactory.md create mode 100644 docs/development/core/server/kibana-plugin-server.routevalidationresultfactory.ok.md create mode 100644 docs/development/core/server/kibana-plugin-server.routevalidationspec.md create mode 100644 docs/development/core/server/kibana-plugin-server.routevalidatorconfig.body.md create mode 100644 docs/development/core/server/kibana-plugin-server.routevalidatorconfig.md create mode 100644 docs/development/core/server/kibana-plugin-server.routevalidatorconfig.params.md create mode 100644 docs/development/core/server/kibana-plugin-server.routevalidatorconfig.query.md create mode 100644 docs/development/core/server/kibana-plugin-server.routevalidatorfullconfig.md create mode 100644 docs/development/core/server/kibana-plugin-server.routevalidatoroptions.md create mode 100644 docs/development/core/server/kibana-plugin-server.routevalidatoroptions.unsafe.md create mode 100644 src/core/server/http/router/validator/index.ts create mode 100644 src/core/server/http/router/validator/validator.test.ts create mode 100644 src/core/server/http/router/validator/validator.ts create mode 100644 src/core/server/http/router/validator/validator_error.ts diff --git a/docs/development/core/server/kibana-plugin-server.irouter.handlelegacyerrors.md b/docs/development/core/server/kibana-plugin-server.irouter.handlelegacyerrors.md index 2367420068064..ff71f13466cf8 100644 --- a/docs/development/core/server/kibana-plugin-server.irouter.handlelegacyerrors.md +++ b/docs/development/core/server/kibana-plugin-server.irouter.handlelegacyerrors.md @@ -9,5 +9,5 @@ Wrap a router handler to catch and converts legacy boom errors to proper custom Signature: ```typescript -handleLegacyErrors:

(handler: RequestHandler) => RequestHandler; +handleLegacyErrors: (handler: RequestHandler) => RequestHandler; ``` diff --git a/docs/development/core/server/kibana-plugin-server.irouter.md b/docs/development/core/server/kibana-plugin-server.irouter.md index 73e96191e02e7..a6536d2ed6763 100644 --- a/docs/development/core/server/kibana-plugin-server.irouter.md +++ b/docs/development/core/server/kibana-plugin-server.irouter.md @@ -18,7 +18,7 @@ export interface IRouter | --- | --- | --- | | [delete](./kibana-plugin-server.irouter.delete.md) | RouteRegistrar<'delete'> | Register a route handler for DELETE request. | | [get](./kibana-plugin-server.irouter.get.md) | RouteRegistrar<'get'> | Register a route handler for GET request. | -| [handleLegacyErrors](./kibana-plugin-server.irouter.handlelegacyerrors.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(handler: RequestHandler<P, Q, B>) => RequestHandler<P, Q, B> | Wrap a router handler to catch and converts legacy boom errors to proper custom errors. | +| [handleLegacyErrors](./kibana-plugin-server.irouter.handlelegacyerrors.md) | <P, Q, B>(handler: RequestHandler<P, Q, B>) => RequestHandler<P, Q, B> | Wrap a router handler to catch and converts legacy boom errors to proper custom errors. | | [patch](./kibana-plugin-server.irouter.patch.md) | RouteRegistrar<'patch'> | Register a route handler for PATCH request. | | [post](./kibana-plugin-server.irouter.post.md) | RouteRegistrar<'post'> | Register a route handler for POST request. | | [put](./kibana-plugin-server.irouter.put.md) | RouteRegistrar<'put'> | Register a route handler for PUT request. | diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index a39c27a758f9d..9c8aafb158bfd 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -21,6 +21,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [CspConfig](./kibana-plugin-server.cspconfig.md) | CSP configuration for use in Kibana. | | [ElasticsearchErrorHelpers](./kibana-plugin-server.elasticsearcherrorhelpers.md) | Helpers for working with errors returned from the Elasticsearch service.Since the internal data of errors are subject to change, consumers of the Elasticsearch service should always use these helpers to classify errors instead of checking error internals such as body.error.header[WWW-Authenticate] | | [KibanaRequest](./kibana-plugin-server.kibanarequest.md) | Kibana specific abstraction for an incoming request. | +| [RouteValidationError](./kibana-plugin-server.routevalidationerror.md) | Error to return when the validation is not successful. | | [SavedObjectsClient](./kibana-plugin-server.savedobjectsclient.md) | | | [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) | | | [SavedObjectsRepository](./kibana-plugin-server.savedobjectsrepository.md) | | @@ -94,7 +95,9 @@ The plugin integrates with the core system via lifecycle events: `setup` | [RouteConfig](./kibana-plugin-server.routeconfig.md) | Route specific configuration. | | [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md) | Additional route options. | | [RouteConfigOptionsBody](./kibana-plugin-server.routeconfigoptionsbody.md) | Additional body options for a route | -| [RouteSchemas](./kibana-plugin-server.routeschemas.md) | RouteSchemas contains the schemas for validating the different parts of a request. | +| [RouteValidationResultFactory](./kibana-plugin-server.routevalidationresultfactory.md) | Validation result factory to be used in the custom validation function to return the valid data or validation errorsSee [RouteValidationFunction](./kibana-plugin-server.routevalidationfunction.md). | +| [RouteValidatorConfig](./kibana-plugin-server.routevalidatorconfig.md) | The configuration object to the RouteValidator class. Set params, query and/or body to specify the validation logic to follow for that property. | +| [RouteValidatorOptions](./kibana-plugin-server.routevalidatoroptions.md) | Additional options for the RouteValidator class to modify its default behaviour. | | [SavedObject](./kibana-plugin-server.savedobject.md) | | | [SavedObjectAttributes](./kibana-plugin-server.savedobjectattributes.md) | The data for a Saved Object is stored as an object in the attributes property. | | [SavedObjectReference](./kibana-plugin-server.savedobjectreference.md) | A reference to another saved object. | @@ -200,6 +203,9 @@ The plugin integrates with the core system via lifecycle events: `setup` | [RouteContentType](./kibana-plugin-server.routecontenttype.md) | The set of supported parseable Content-Types | | [RouteMethod](./kibana-plugin-server.routemethod.md) | The set of common HTTP methods supported by Kibana routing. | | [RouteRegistrar](./kibana-plugin-server.routeregistrar.md) | Route handler common definition | +| [RouteValidationFunction](./kibana-plugin-server.routevalidationfunction.md) | The custom validation function if @kbn/config-schema is not a valid solution for your specific plugin requirements. | +| [RouteValidationSpec](./kibana-plugin-server.routevalidationspec.md) | Allowed property validation options: either @kbn/config-schema validations or custom validation functionsSee [RouteValidationFunction](./kibana-plugin-server.routevalidationfunction.md) for custom validation. | +| [RouteValidatorFullConfig](./kibana-plugin-server.routevalidatorfullconfig.md) | Route validations config and options merged into one object | | [SavedObjectAttribute](./kibana-plugin-server.savedobjectattribute.md) | Type definition for a Saved Object attribute value | | [SavedObjectAttributeSingle](./kibana-plugin-server.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-server.savedobjectattribute.md) | | [SavedObjectsClientContract](./kibana-plugin-server.savedobjectsclientcontract.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state.\#\# SavedObjectsClient errorsSince the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to to application code. Ideally, all errors will be either:1. Caused by bad implementation (ie. undefined is not a function) and as such unpredictable 2. An error that has been classified and decorated appropriately by the decorators in [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md)Type 1 errors are inevitable, but since all expected/handle-able errors should be Type 2 the isXYZError() helpers exposed at SavedObjectsErrorHelpers should be used to understand and manage error responses from the SavedObjectsClient.Type 2 errors are decorated versions of the source error, so if the elasticsearch client threw an error it will be decorated based on its type. That means that rather than looking for error.body.error.type or doing substring checks on error.body.error.reason, just use the helpers to understand the meaning of the error:\`\`\`js if (SavedObjectsErrorHelpers.isNotFoundError(error)) { // handle 404 }if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { // 401 handling should be automatic, but in case you wanted to know }// always rethrow the error unless you handle it throw error; \`\`\`\#\#\# 404s from missing indexFrom the perspective of application code and APIs the SavedObjectsClient is a black box that persists objects. One of the internal details that users have no control over is that we use an elasticsearch index for persistance and that index might be missing.At the time of writing we are in the process of transitioning away from the operating assumption that the SavedObjects index is always available. Part of this transition is handling errors resulting from an index missing. These used to trigger a 500 error in most cases, and in others cause 404s with different error messages.From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The object the request/call was targeting could not be found. This is why \#14141 takes special care to ensure that 404 errors are generic and don't distinguish between index missing or document missing.\#\#\# 503s from missing indexUnlike all other methods, create requests are supposed to succeed even when the Kibana index does not exist because it will be automatically created by elasticsearch. When that is not the case it is because Elasticsearch's action.auto_create_index setting prevents it from being created automatically so we throw a special 503 with the intention of informing the user that their Elasticsearch settings need to be updated.See [SavedObjectsClient](./kibana-plugin-server.savedobjectsclient.md) See [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) | diff --git a/docs/development/core/server/kibana-plugin-server.requesthandler.md b/docs/development/core/server/kibana-plugin-server.requesthandler.md index 79abfd4293e9f..9fc183ffc334b 100644 --- a/docs/development/core/server/kibana-plugin-server.requesthandler.md +++ b/docs/development/core/server/kibana-plugin-server.requesthandler.md @@ -9,7 +9,7 @@ A function executed when route path matched requested resource path. Request han Signature: ```typescript -export declare type RequestHandler

| Type, Method extends RouteMethod = any> = (context: RequestHandlerContext, request: KibanaRequest, TypeOf, TypeOf, Method>, response: KibanaResponseFactory) => IKibanaResponse | Promise>; +export declare type RequestHandler

= (context: RequestHandlerContext, request: KibanaRequest, response: KibanaResponseFactory) => IKibanaResponse | Promise>; ``` ## Example diff --git a/docs/development/core/server/kibana-plugin-server.routeconfig.md b/docs/development/core/server/kibana-plugin-server.routeconfig.md index 1970b23c7ec09..4beb12f0d056e 100644 --- a/docs/development/core/server/kibana-plugin-server.routeconfig.md +++ b/docs/development/core/server/kibana-plugin-server.routeconfig.md @@ -9,7 +9,7 @@ Route specific configuration. Signature: ```typescript -export interface RouteConfig

| Type, Method extends RouteMethod> +export interface RouteConfig ``` ## Properties @@ -18,5 +18,5 @@ export interface RouteConfig

RouteConfigOptions<Method> | Additional route options [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md). | | [path](./kibana-plugin-server.routeconfig.path.md) | string | The endpoint \_within\_ the router path to register the route. | -| [validate](./kibana-plugin-server.routeconfig.validate.md) | RouteSchemas<P, Q, B> | false | A schema created with @kbn/config-schema that every request will be validated against. | +| [validate](./kibana-plugin-server.routeconfig.validate.md) | RouteValidatorFullConfig<P, Q, B> | false | A schema created with @kbn/config-schema that every request will be validated against. | diff --git a/docs/development/core/server/kibana-plugin-server.routeconfig.validate.md b/docs/development/core/server/kibana-plugin-server.routeconfig.validate.md index e1ec743ae71cc..23a72fc3c68b3 100644 --- a/docs/development/core/server/kibana-plugin-server.routeconfig.validate.md +++ b/docs/development/core/server/kibana-plugin-server.routeconfig.validate.md @@ -9,7 +9,7 @@ A schema created with `@kbn/config-schema` that every request will be validated Signature: ```typescript -validate: RouteSchemas | false; +validate: RouteValidatorFullConfig | false; ``` ## Remarks diff --git a/docs/development/core/server/kibana-plugin-server.routeregistrar.md b/docs/development/core/server/kibana-plugin-server.routeregistrar.md index 0f5f49636fdd5..901d260fee21d 100644 --- a/docs/development/core/server/kibana-plugin-server.routeregistrar.md +++ b/docs/development/core/server/kibana-plugin-server.routeregistrar.md @@ -9,5 +9,5 @@ Route handler common definition Signature: ```typescript -export declare type RouteRegistrar =

| Type>(route: RouteConfig, handler: RequestHandler) => void; +export declare type RouteRegistrar = (route: RouteConfig, handler: RequestHandler) => void; ``` diff --git a/docs/development/core/server/kibana-plugin-server.routeschemas.body.md b/docs/development/core/server/kibana-plugin-server.routeschemas.body.md deleted file mode 100644 index 78a9d25c25d9d..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.routeschemas.body.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteSchemas](./kibana-plugin-server.routeschemas.md) > [body](./kibana-plugin-server.routeschemas.body.md) - -## RouteSchemas.body property - -Signature: - -```typescript -body?: B; -``` diff --git a/docs/development/core/server/kibana-plugin-server.routeschemas.md b/docs/development/core/server/kibana-plugin-server.routeschemas.md deleted file mode 100644 index 77b980551a8ff..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.routeschemas.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteSchemas](./kibana-plugin-server.routeschemas.md) - -## RouteSchemas interface - -RouteSchemas contains the schemas for validating the different parts of a request. - -Signature: - -```typescript -export interface RouteSchemas

| Type> -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [body](./kibana-plugin-server.routeschemas.body.md) | B | | -| [params](./kibana-plugin-server.routeschemas.params.md) | P | | -| [query](./kibana-plugin-server.routeschemas.query.md) | Q | | - diff --git a/docs/development/core/server/kibana-plugin-server.routeschemas.params.md b/docs/development/core/server/kibana-plugin-server.routeschemas.params.md deleted file mode 100644 index 3dbf9fed94dc0..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.routeschemas.params.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteSchemas](./kibana-plugin-server.routeschemas.md) > [params](./kibana-plugin-server.routeschemas.params.md) - -## RouteSchemas.params property - -Signature: - -```typescript -params?: P; -``` diff --git a/docs/development/core/server/kibana-plugin-server.routeschemas.query.md b/docs/development/core/server/kibana-plugin-server.routeschemas.query.md deleted file mode 100644 index 5be5830cb4bc8..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.routeschemas.query.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteSchemas](./kibana-plugin-server.routeschemas.md) > [query](./kibana-plugin-server.routeschemas.query.md) - -## RouteSchemas.query property - -Signature: - -```typescript -query?: Q; -``` diff --git a/docs/development/core/server/kibana-plugin-server.routevalidationerror._constructor_.md b/docs/development/core/server/kibana-plugin-server.routevalidationerror._constructor_.md new file mode 100644 index 0000000000000..551e13faaf154 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routevalidationerror._constructor_.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteValidationError](./kibana-plugin-server.routevalidationerror.md) > [(constructor)](./kibana-plugin-server.routevalidationerror._constructor_.md) + +## RouteValidationError.(constructor) + +Constructs a new instance of the `RouteValidationError` class + +Signature: + +```typescript +constructor(error: Error | string, path?: string[]); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| error | Error | string | | +| path | string[] | | + diff --git a/docs/development/core/server/kibana-plugin-server.routevalidationerror.md b/docs/development/core/server/kibana-plugin-server.routevalidationerror.md new file mode 100644 index 0000000000000..71bd72dca2eab --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routevalidationerror.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteValidationError](./kibana-plugin-server.routevalidationerror.md) + +## RouteValidationError class + +Error to return when the validation is not successful. + +Signature: + +```typescript +export declare class RouteValidationError extends SchemaTypeError +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(error, path)](./kibana-plugin-server.routevalidationerror._constructor_.md) | | Constructs a new instance of the RouteValidationError class | + diff --git a/docs/development/core/server/kibana-plugin-server.routevalidationfunction.md b/docs/development/core/server/kibana-plugin-server.routevalidationfunction.md new file mode 100644 index 0000000000000..34fa096aaae78 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routevalidationfunction.md @@ -0,0 +1,42 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteValidationFunction](./kibana-plugin-server.routevalidationfunction.md) + +## RouteValidationFunction type + +The custom validation function if @kbn/config-schema is not a valid solution for your specific plugin requirements. + +Signature: + +```typescript +export declare type RouteValidationFunction = (data: any, validationResult: RouteValidationResultFactory) => { + value: T; + error?: never; +} | { + value?: never; + error: RouteValidationError; +}; +``` + +## Example + +The validation should look something like: + +```typescript +interface MyExpectedBody { + bar: string; + baz: number; +} + +const myBodyValidation: RouteValidationFunction = (data, validationResult) => { + const { ok, badRequest } = validationResult; + const { bar, baz } = data || {}; + if (typeof bar === 'string' && typeof baz === 'number') { + return ok({ bar, baz }); + } else { + return badRequest('Wrong payload', ['body']); + } +} + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.routevalidationresultfactory.badrequest.md b/docs/development/core/server/kibana-plugin-server.routevalidationresultfactory.badrequest.md new file mode 100644 index 0000000000000..36ea6103fb352 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routevalidationresultfactory.badrequest.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteValidationResultFactory](./kibana-plugin-server.routevalidationresultfactory.md) > [badRequest](./kibana-plugin-server.routevalidationresultfactory.badrequest.md) + +## RouteValidationResultFactory.badRequest property + +Signature: + +```typescript +badRequest: (error: Error | string, path?: string[]) => { + error: RouteValidationError; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-server.routevalidationresultfactory.md b/docs/development/core/server/kibana-plugin-server.routevalidationresultfactory.md new file mode 100644 index 0000000000000..5f44b490e9a17 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routevalidationresultfactory.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteValidationResultFactory](./kibana-plugin-server.routevalidationresultfactory.md) + +## RouteValidationResultFactory interface + +Validation result factory to be used in the custom validation function to return the valid data or validation errors + +See [RouteValidationFunction](./kibana-plugin-server.routevalidationfunction.md). + +Signature: + +```typescript +export interface RouteValidationResultFactory +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [badRequest](./kibana-plugin-server.routevalidationresultfactory.badrequest.md) | (error: Error | string, path?: string[]) => {
error: RouteValidationError;
} | | +| [ok](./kibana-plugin-server.routevalidationresultfactory.ok.md) | <T>(value: T) => {
value: T;
} | | + diff --git a/docs/development/core/server/kibana-plugin-server.routevalidationresultfactory.ok.md b/docs/development/core/server/kibana-plugin-server.routevalidationresultfactory.ok.md new file mode 100644 index 0000000000000..eca6a31bd547f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routevalidationresultfactory.ok.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteValidationResultFactory](./kibana-plugin-server.routevalidationresultfactory.md) > [ok](./kibana-plugin-server.routevalidationresultfactory.ok.md) + +## RouteValidationResultFactory.ok property + +Signature: + +```typescript +ok: (value: T) => { + value: T; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-server.routevalidationspec.md b/docs/development/core/server/kibana-plugin-server.routevalidationspec.md new file mode 100644 index 0000000000000..f5fc06544043f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routevalidationspec.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteValidationSpec](./kibana-plugin-server.routevalidationspec.md) + +## RouteValidationSpec type + +Allowed property validation options: either @kbn/config-schema validations or custom validation functions + +See [RouteValidationFunction](./kibana-plugin-server.routevalidationfunction.md) for custom validation. + +Signature: + +```typescript +export declare type RouteValidationSpec = ObjectType | Type | RouteValidationFunction; +``` diff --git a/docs/development/core/server/kibana-plugin-server.routevalidatorconfig.body.md b/docs/development/core/server/kibana-plugin-server.routevalidatorconfig.body.md new file mode 100644 index 0000000000000..8b5d2c0413087 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routevalidatorconfig.body.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteValidatorConfig](./kibana-plugin-server.routevalidatorconfig.md) > [body](./kibana-plugin-server.routevalidatorconfig.body.md) + +## RouteValidatorConfig.body property + +Validation logic for the body payload + +Signature: + +```typescript +body?: RouteValidationSpec; +``` diff --git a/docs/development/core/server/kibana-plugin-server.routevalidatorconfig.md b/docs/development/core/server/kibana-plugin-server.routevalidatorconfig.md new file mode 100644 index 0000000000000..4637da7741d80 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routevalidatorconfig.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteValidatorConfig](./kibana-plugin-server.routevalidatorconfig.md) + +## RouteValidatorConfig interface + +The configuration object to the RouteValidator class. Set `params`, `query` and/or `body` to specify the validation logic to follow for that property. + +Signature: + +```typescript +export interface RouteValidatorConfig +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [body](./kibana-plugin-server.routevalidatorconfig.body.md) | RouteValidationSpec<B> | Validation logic for the body payload | +| [params](./kibana-plugin-server.routevalidatorconfig.params.md) | RouteValidationSpec<P> | Validation logic for the URL params | +| [query](./kibana-plugin-server.routevalidatorconfig.query.md) | RouteValidationSpec<Q> | Validation logic for the Query params | + diff --git a/docs/development/core/server/kibana-plugin-server.routevalidatorconfig.params.md b/docs/development/core/server/kibana-plugin-server.routevalidatorconfig.params.md new file mode 100644 index 0000000000000..11de25ff3b19f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routevalidatorconfig.params.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteValidatorConfig](./kibana-plugin-server.routevalidatorconfig.md) > [params](./kibana-plugin-server.routevalidatorconfig.params.md) + +## RouteValidatorConfig.params property + +Validation logic for the URL params + +Signature: + +```typescript +params?: RouteValidationSpec

; +``` diff --git a/docs/development/core/server/kibana-plugin-server.routevalidatorconfig.query.md b/docs/development/core/server/kibana-plugin-server.routevalidatorconfig.query.md new file mode 100644 index 0000000000000..510325c2dfff7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routevalidatorconfig.query.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteValidatorConfig](./kibana-plugin-server.routevalidatorconfig.md) > [query](./kibana-plugin-server.routevalidatorconfig.query.md) + +## RouteValidatorConfig.query property + +Validation logic for the Query params + +Signature: + +```typescript +query?: RouteValidationSpec; +``` diff --git a/docs/development/core/server/kibana-plugin-server.routevalidatorfullconfig.md b/docs/development/core/server/kibana-plugin-server.routevalidatorfullconfig.md new file mode 100644 index 0000000000000..0f3785b954a3a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routevalidatorfullconfig.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteValidatorFullConfig](./kibana-plugin-server.routevalidatorfullconfig.md) + +## RouteValidatorFullConfig type + +Route validations config and options merged into one object + +Signature: + +```typescript +export declare type RouteValidatorFullConfig = RouteValidatorConfig & RouteValidatorOptions; +``` diff --git a/docs/development/core/server/kibana-plugin-server.routevalidatoroptions.md b/docs/development/core/server/kibana-plugin-server.routevalidatoroptions.md new file mode 100644 index 0000000000000..00b029d9928e3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routevalidatoroptions.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteValidatorOptions](./kibana-plugin-server.routevalidatoroptions.md) + +## RouteValidatorOptions interface + +Additional options for the RouteValidator class to modify its default behaviour. + +Signature: + +```typescript +export interface RouteValidatorOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [unsafe](./kibana-plugin-server.routevalidatoroptions.unsafe.md) | {
params?: boolean;
query?: boolean;
body?: boolean;
} | Set the unsafe config to avoid running some additional internal \*safe\* validations on top of your custom validation | + diff --git a/docs/development/core/server/kibana-plugin-server.routevalidatoroptions.unsafe.md b/docs/development/core/server/kibana-plugin-server.routevalidatoroptions.unsafe.md new file mode 100644 index 0000000000000..0406a372c4e9d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routevalidatoroptions.unsafe.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteValidatorOptions](./kibana-plugin-server.routevalidatoroptions.md) > [unsafe](./kibana-plugin-server.routevalidatoroptions.unsafe.md) + +## RouteValidatorOptions.unsafe property + +Set the `unsafe` config to avoid running some additional internal \*safe\* validations on top of your custom validation + +Signature: + +```typescript +unsafe?: { + params?: boolean; + query?: boolean; + body?: boolean; + }; +``` diff --git a/packages/kbn-config-schema/src/index.ts b/packages/kbn-config-schema/src/index.ts index 56b3096433c24..fc3e3c541846a 100644 --- a/packages/kbn-config-schema/src/index.ts +++ b/packages/kbn-config-schema/src/index.ts @@ -59,6 +59,7 @@ import { export { ObjectType, TypeOf, Type }; export { ByteSizeValue } from './byte_size_value'; +export { SchemaTypeError, ValidationError } from './errors'; function any(options?: TypeOptions) { return new AnyType(options); diff --git a/src/core/server/http/http_server.mocks.ts b/src/core/server/http/http_server.mocks.ts index 8469a1d23a44b..ba742292e9e83 100644 --- a/src/core/server/http/http_server.mocks.ts +++ b/src/core/server/http/http_server.mocks.ts @@ -77,7 +77,7 @@ function createKibanaRequestMock({ body: schema.object({}, { allowUnknowns: true }), query: schema.object({}, { allowUnknowns: true }), } - ) as KibanaRequest, Readonly<{}>, Readonly<{}>>; + ); } type DeepPartial = T extends any[] diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index ce6d41494c171..32aba3c48a5fd 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -27,10 +27,18 @@ import supertest from 'supertest'; import { ByteSizeValue, schema } from '@kbn/config-schema'; import { HttpConfig } from './http_config'; -import { Router } from './router'; +import { + Router, + KibanaRequest, + KibanaResponseFactory, + RequestHandler, + RouteValidationResultFactory, + RouteValidationFunction, +} from './router'; import { loggingServiceMock } from '../logging/logging_service.mock'; import { HttpServer } from './http_server'; import { Readable } from 'stream'; +import { RequestHandlerContext } from 'kibana/server'; const cookieOptions = { name: 'sid', @@ -288,6 +296,229 @@ test('valid body', async () => { }); }); +test('valid body with validate function', async () => { + const router = new Router('/foo', logger, enhanceWithContext); + + router.post( + { + path: '/', + validate: { + body: ({ bar, baz } = {}, { ok, badRequest }) => { + if (typeof bar === 'string' && typeof baz === 'number') { + return ok({ bar, baz }); + } else { + return badRequest('Wrong payload', ['body']); + } + }, + }, + }, + (context, req, res) => { + return res.ok({ body: req.body }); + } + ); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + + await server.start(); + + await supertest(innerServer.listener) + .post('/foo/') + .send({ + bar: 'test', + baz: 123, + }) + .expect(200) + .then(res => { + expect(res.body).toEqual({ bar: 'test', baz: 123 }); + }); +}); + +test('not inline validation - specifying params', async () => { + const router = new Router('/foo', logger, enhanceWithContext); + + const bodyValidation = ( + { bar, baz }: any = {}, + { ok, badRequest }: RouteValidationResultFactory + ) => { + if (typeof bar === 'string' && typeof baz === 'number') { + return ok({ bar, baz }); + } else { + return badRequest('Wrong payload', ['body']); + } + }; + + router.post( + { + path: '/', + validate: { + body: bodyValidation, + }, + }, + (context, req, res) => { + return res.ok({ body: req.body }); + } + ); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + + await server.start(); + + await supertest(innerServer.listener) + .post('/foo/') + .send({ + bar: 'test', + baz: 123, + }) + .expect(200) + .then(res => { + expect(res.body).toEqual({ bar: 'test', baz: 123 }); + }); +}); + +test('not inline validation - specifying validation handler', async () => { + const router = new Router('/foo', logger, enhanceWithContext); + + const bodyValidation: RouteValidationFunction<{ bar: string; baz: number }> = ( + { bar, baz } = {}, + { ok, badRequest } + ) => { + if (typeof bar === 'string' && typeof baz === 'number') { + return ok({ bar, baz }); + } else { + return badRequest('Wrong payload', ['body']); + } + }; + + router.post( + { + path: '/', + validate: { + body: bodyValidation, + }, + }, + (context, req, res) => { + return res.ok({ body: req.body }); + } + ); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + + await server.start(); + + await supertest(innerServer.listener) + .post('/foo/') + .send({ + bar: 'test', + baz: 123, + }) + .expect(200) + .then(res => { + expect(res.body).toEqual({ bar: 'test', baz: 123 }); + }); +}); + +// https://github.com/elastic/kibana/issues/47047 +test('not inline handler - KibanaRequest', async () => { + const router = new Router('/foo', logger, enhanceWithContext); + + const handler = ( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ) => { + const body = { + bar: req.body.bar.toUpperCase(), + baz: req.body.baz.toString(), + }; + + return res.ok({ body }); + }; + + router.post( + { + path: '/', + validate: { + body: ({ bar, baz } = {}, { ok, badRequest }) => { + if (typeof bar === 'string' && typeof baz === 'number') { + return ok({ bar, baz }); + } else { + return badRequest('Wrong payload', ['body']); + } + }, + }, + }, + handler + ); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + + await server.start(); + + await supertest(innerServer.listener) + .post('/foo/') + .send({ + bar: 'test', + baz: 123, + }) + .expect(200) + .then(res => { + expect(res.body).toEqual({ bar: 'TEST', baz: '123' }); + }); +}); + +test('not inline handler - RequestHandler', async () => { + const router = new Router('/foo', logger, enhanceWithContext); + + const handler: RequestHandler = ( + context, + req, + res + ) => { + const body = { + bar: req.body.bar.toUpperCase(), + baz: req.body.baz.toString(), + }; + + return res.ok({ body }); + }; + + router.post( + { + path: '/', + validate: { + body: ({ bar, baz } = {}, { ok, badRequest }) => { + if (typeof bar === 'string' && typeof baz === 'number') { + return ok({ bar, baz }); + } else { + return badRequest('Wrong payload', ['body']); + } + }, + }, + }, + handler + ); + + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + + await server.start(); + + await supertest(innerServer.listener) + .post('/foo/') + .send({ + bar: 'test', + baz: 123, + }) + .expect(200) + .then(res => { + expect(res.body).toEqual({ bar: 'TEST', baz: '123' }); + }); +}); + test('invalid body', async () => { const router = new Router('/foo', logger, enhanceWithContext); diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index 21de3945f1044..55ba813484268 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -47,10 +47,16 @@ export { RouteMethod, RouteRegistrar, RouteConfigOptions, - RouteSchemas, RouteConfigOptionsBody, RouteContentType, validBodyOutput, + RouteValidatorConfig, + RouteValidationSpec, + RouteValidationFunction, + RouteValidatorOptions, + RouteValidationError, + RouteValidatorFullConfig, + RouteValidationResultFactory, } from './router'; export { BasePathProxyServer } from './base_path_proxy_server'; export { OnPreAuthHandler, OnPreAuthToolkit } from './lifecycle/on_pre_auth'; diff --git a/src/core/server/http/integration_tests/router.test.ts b/src/core/server/http/integration_tests/router.test.ts index 6117190c57ba8..c3b9b20d84865 100644 --- a/src/core/server/http/integration_tests/router.test.ts +++ b/src/core/server/http/integration_tests/router.test.ts @@ -642,6 +642,116 @@ describe('Response factory', () => { }); }); + it('validate function in body', async () => { + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/foo'); + + router.post( + { + path: '/', + validate: { + body: ({ bar, baz } = {}, { ok, badRequest }) => { + if (typeof bar === 'string' && typeof baz === 'number') { + return ok({ bar, baz }); + } else { + return badRequest('Wrong payload', ['body']); + } + }, + }, + }, + (context, req, res) => { + return res.ok({ body: req.body }); + } + ); + + await server.start(); + + await supertest(innerServer.listener) + .post('/foo/') + .send({ + bar: 'test', + baz: 123, + }) + .expect(200) + .then(res => { + expect(res.body).toEqual({ bar: 'test', baz: 123 }); + }); + + await supertest(innerServer.listener) + .post('/foo/') + .send({ + bar: 'test', + baz: '123', + }) + .expect(400) + .then(res => { + expect(res.body).toEqual({ + error: 'Bad Request', + message: '[request body.body]: Wrong payload', + statusCode: 400, + }); + }); + }); + + it('@kbn/config-schema validation in body', async () => { + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/foo'); + + router.post( + { + path: '/', + validate: { + body: schema.object({ + bar: schema.string(), + baz: schema.number(), + }), + }, + }, + (context, req, res) => { + return res.ok({ body: req.body }); + } + ); + + await server.start(); + + await supertest(innerServer.listener) + .post('/foo/') + .send({ + bar: 'test', + baz: 123, + }) + .expect(200) + .then(res => { + expect(res.body).toEqual({ bar: 'test', baz: 123 }); + }); + + await supertest(innerServer.listener) + .post('/foo/') + .send({ + bar: 'test', + baz: '123', // Automatic casting happens + }) + .expect(200) + .then(res => { + expect(res.body).toEqual({ bar: 'test', baz: 123 }); + }); + + await supertest(innerServer.listener) + .post('/foo/') + .send({ + bar: 'test', + baz: 'test', // Can't cast it into number + }) + .expect(400) + .then(res => { + expect(res.body).toEqual({ + error: 'Bad Request', + message: '[request body.baz]: expected value of type [number] but got [string]', + statusCode: 400, + }); + }); + }); + it('401 Unauthorized', async () => { const { server: innerServer, createRouter } = await server.setup(setupDeps); const router = createRouter('/'); diff --git a/src/core/server/http/router/error_wrapper.ts b/src/core/server/http/router/error_wrapper.ts index c4b4d3840d1b9..8f895753c38c3 100644 --- a/src/core/server/http/router/error_wrapper.ts +++ b/src/core/server/http/router/error_wrapper.ts @@ -18,19 +18,18 @@ */ import Boom from 'boom'; -import { ObjectType, TypeOf } from '@kbn/config-schema'; import { KibanaRequest } from './request'; import { KibanaResponseFactory } from './response'; import { RequestHandler } from './router'; import { RequestHandlerContext } from '../../../server'; import { RouteMethod } from './route'; -export const wrapErrors =

( +export const wrapErrors = ( handler: RequestHandler ): RequestHandler => { return async ( context: RequestHandlerContext, - request: KibanaRequest, TypeOf, TypeOf, RouteMethod>, + request: KibanaRequest, response: KibanaResponseFactory ) => { try { diff --git a/src/core/server/http/router/index.ts b/src/core/server/http/router/index.ts index 35bfb3ba9c33a..084d30d694474 100644 --- a/src/core/server/http/router/index.ts +++ b/src/core/server/http/router/index.ts @@ -31,7 +31,6 @@ export { RouteMethod, RouteConfig, RouteConfigOptions, - RouteSchemas, RouteContentType, RouteConfigOptionsBody, validBodyOutput, @@ -55,3 +54,13 @@ export { } from './response'; export { IKibanaSocket } from './socket'; + +export { + RouteValidatorConfig, + RouteValidationSpec, + RouteValidationFunction, + RouteValidatorOptions, + RouteValidationError, + RouteValidatorFullConfig, + RouteValidationResultFactory, +} from './validator'; diff --git a/src/core/server/http/router/request.test.ts b/src/core/server/http/router/request.test.ts index ebb7ffa7a6fc9..51162a2c258e9 100644 --- a/src/core/server/http/router/request.test.ts +++ b/src/core/server/http/router/request.test.ts @@ -18,6 +18,7 @@ */ import { KibanaRequest } from './request'; import { httpServerMock } from '../http_server.mocks'; +import { schema } from '@kbn/config-schema'; describe('KibanaRequest', () => { describe('get all headers', () => { @@ -64,4 +65,56 @@ describe('KibanaRequest', () => { }); }); }); + + describe('RouteSchema type inferring', () => { + it('should work with config-schema', () => { + const body = Buffer.from('body!'); + const request = { + ...httpServerMock.createRawRequest({ + params: { id: 'params' }, + query: { search: 'query' }, + }), + payload: body, // Set outside because the mock is using `merge` by lodash and breaks the Buffer into arrays + } as any; + const kibanaRequest = KibanaRequest.from(request, { + params: schema.object({ id: schema.string() }), + query: schema.object({ search: schema.string() }), + body: schema.buffer(), + }); + expect(kibanaRequest.params).toStrictEqual({ id: 'params' }); + expect(kibanaRequest.params.id.toUpperCase()).toEqual('PARAMS'); // infers it's a string + expect(kibanaRequest.query).toStrictEqual({ search: 'query' }); + expect(kibanaRequest.query.search.toUpperCase()).toEqual('QUERY'); // infers it's a string + expect(kibanaRequest.body).toEqual(body); + expect(kibanaRequest.body.byteLength).toBeGreaterThan(0); // infers it's a buffer + }); + + it('should work with ValidationFunction', () => { + const body = Buffer.from('body!'); + const request = { + ...httpServerMock.createRawRequest({ + params: { id: 'params' }, + query: { search: 'query' }, + }), + payload: body, // Set outside because the mock is using `merge` by lodash and breaks the Buffer into arrays + } as any; + const kibanaRequest = KibanaRequest.from(request, { + params: schema.object({ id: schema.string() }), + query: schema.object({ search: schema.string() }), + body: (data, { ok, badRequest }) => { + if (Buffer.isBuffer(data)) { + return ok(data); + } else { + return badRequest('It should be a Buffer', []); + } + }, + }); + expect(kibanaRequest.params).toStrictEqual({ id: 'params' }); + expect(kibanaRequest.params.id.toUpperCase()).toEqual('PARAMS'); // infers it's a string + expect(kibanaRequest.query).toStrictEqual({ search: 'query' }); + expect(kibanaRequest.query.search.toUpperCase()).toEqual('QUERY'); // infers it's a string + expect(kibanaRequest.body).toEqual(body); + expect(kibanaRequest.body.byteLength).toBeGreaterThan(0); // infers it's a buffer + }); + }); }); diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts index b132899910569..47b001700b015 100644 --- a/src/core/server/http/router/request.ts +++ b/src/core/server/http/router/request.ts @@ -20,13 +20,11 @@ import { Url } from 'url'; import { Request } from 'hapi'; -import { ObjectType, Type, TypeOf } from '@kbn/config-schema'; - -import { Stream } from 'stream'; import { deepFreeze, RecursiveReadonly } from '../../../utils'; import { Headers } from './headers'; -import { RouteMethod, RouteSchemas, RouteConfigOptions, validBodyOutput } from './route'; +import { RouteMethod, RouteConfigOptions, validBodyOutput } from './route'; import { KibanaSocket, IKibanaSocket } from './socket'; +import { RouteValidator, RouteValidatorFullConfig } from './validator'; const requestSymbol = Symbol('request'); @@ -70,12 +68,13 @@ export class KibanaRequest< * instance of a KibanaRequest. * @internal */ - public static from< - P extends ObjectType, - Q extends ObjectType, - B extends ObjectType | Type | Type - >(req: Request, routeSchemas?: RouteSchemas, withoutSecretHeaders: boolean = true) { - const requestParts = KibanaRequest.validate(req, routeSchemas); + public static from( + req: Request, + routeSchemas: RouteValidator | RouteValidatorFullConfig = {}, + withoutSecretHeaders: boolean = true + ) { + const routeValidator = RouteValidator.from(routeSchemas); + const requestParts = KibanaRequest.validate(req, routeValidator); return new KibanaRequest( req, requestParts.params, @@ -91,40 +90,17 @@ export class KibanaRequest< * received in the route handler. * @internal */ - private static validate< - P extends ObjectType, - Q extends ObjectType, - B extends ObjectType | Type | Type - >( + private static validate( req: Request, - routeSchemas: RouteSchemas | undefined + routeValidator: RouteValidator ): { - params: TypeOf

; - query: TypeOf; - body: TypeOf; + params: P; + query: Q; + body: B; } { - if (routeSchemas === undefined) { - return { - body: {}, - params: {}, - query: {}, - }; - } - - const params = - routeSchemas.params === undefined - ? {} - : routeSchemas.params.validate(req.params, {}, 'request params'); - - const query = - routeSchemas.query === undefined - ? {} - : routeSchemas.query.validate(req.query, {}, 'request query'); - - const body = - routeSchemas.body === undefined - ? {} - : routeSchemas.body.validate(req.payload, {}, 'request body'); + const params = routeValidator.getParams(req.params, 'request params'); + const query = routeValidator.getQuery(req.query, 'request query'); + const body = routeValidator.getBody(req.payload, 'request body'); return { query, params, body }; } diff --git a/src/core/server/http/router/route.ts b/src/core/server/http/router/route.ts index 129cf4c922ffd..4439a80b1eac7 100644 --- a/src/core/server/http/router/route.ts +++ b/src/core/server/http/router/route.ts @@ -17,8 +17,7 @@ * under the License. */ -import { ObjectType, Type } from '@kbn/config-schema'; -import { Stream } from 'stream'; +import { RouteValidatorFullConfig } from './validator'; /** * The set of common HTTP methods supported by Kibana routing. @@ -124,12 +123,7 @@ export interface RouteConfigOptions { * Route specific configuration. * @public */ -export interface RouteConfig< - P extends ObjectType, - Q extends ObjectType, - B extends ObjectType | Type | Type, - Method extends RouteMethod -> { +export interface RouteConfig { /** * The endpoint _within_ the router path to register the route. * @@ -201,25 +195,10 @@ export interface RouteConfig< * }); * ``` */ - validate: RouteSchemas | false; + validate: RouteValidatorFullConfig | false; /** * Additional route options {@link RouteConfigOptions}. */ options?: RouteConfigOptions; } - -/** - * RouteSchemas contains the schemas for validating the different parts of a - * request. - * @public - */ -export interface RouteSchemas< - P extends ObjectType, - Q extends ObjectType, - B extends ObjectType | Type | Type -> { - params?: P; - query?: Q; - body?: B; -} diff --git a/src/core/server/http/router/router.test.ts b/src/core/server/http/router/router.test.ts index f5469a95b5106..a936da6a40a9f 100644 --- a/src/core/server/http/router/router.test.ts +++ b/src/core/server/http/router/router.test.ts @@ -20,6 +20,7 @@ import { Router } from './router'; import { loggingServiceMock } from '../../logging/logging_service.mock'; import { schema } from '@kbn/config-schema'; + const logger = loggingServiceMock.create().get(); const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, {}); @@ -38,12 +39,15 @@ describe('Router', () => { const router = new Router('', logger, enhanceWithContext); expect(() => router.get( - // we use 'any' because validate requires @kbn/config-schema usage - { path: '/', validate: { params: { validate: () => 'error' } } } as any, + // we use 'any' because validate requires valid Type or function usage + { + path: '/', + validate: { params: { validate: () => 'error' } } as any, + }, (context, req, res) => res.ok({}) ) ).toThrowErrorMatchingInlineSnapshot( - `"Expected a valid schema declared with '@kbn/config-schema' package at key: [params]."` + `"Expected a valid validation logic declared with '@kbn/config-schema' package or a RouteValidationFunction at key: [params]."` ); }); diff --git a/src/core/server/http/router/router.ts b/src/core/server/http/router/router.ts index 5c52e71cd54bb..bb56ee3727d1a 100644 --- a/src/core/server/http/router/router.ts +++ b/src/core/server/http/router/router.ts @@ -17,24 +17,18 @@ * under the License. */ -import { ObjectType, TypeOf, Type } from '@kbn/config-schema'; import { Request, ResponseObject, ResponseToolkit } from 'hapi'; import Boom from 'boom'; -import { Stream } from 'stream'; +import { Type } from '@kbn/config-schema'; import { Logger } from '../../logging'; import { KibanaRequest } from './request'; import { KibanaResponseFactory, kibanaResponseFactory, IKibanaResponse } from './response'; -import { - RouteConfig, - RouteConfigOptions, - RouteMethod, - RouteSchemas, - validBodyOutput, -} from './route'; +import { RouteConfig, RouteConfigOptions, RouteMethod, validBodyOutput } from './route'; import { HapiResponseAdapter } from './response_adapter'; import { RequestHandlerContext } from '../../../server'; import { wrapErrors } from './error_wrapper'; +import { RouteValidator } from './validator'; interface RouterRoute { method: RouteMethod; @@ -48,11 +42,7 @@ interface RouterRoute { * * @public */ -export type RouteRegistrar = < - P extends ObjectType, - Q extends ObjectType, - B extends ObjectType | Type | Type ->( +export type RouteRegistrar = ( route: RouteConfig, handler: RequestHandler ) => void; @@ -108,9 +98,7 @@ export interface IRouter { * Wrap a router handler to catch and converts legacy boom errors to proper custom errors. * @param handler {@link RequestHandler} - a route handler to wrap */ - handleLegacyErrors:

( - handler: RequestHandler - ) => RequestHandler; + handleLegacyErrors: (handler: RequestHandler) => RequestHandler; /** * Returns all routes registered with this router. @@ -120,12 +108,9 @@ export interface IRouter { getRoutes: () => RouterRoute[]; } -export type ContextEnhancer< - P extends ObjectType, - Q extends ObjectType, - B extends ObjectType, - Method extends RouteMethod -> = (handler: RequestHandler) => RequestHandlerEnhanced; +export type ContextEnhancer = ( + handler: RequestHandler +) => RequestHandlerEnhanced; function getRouteFullPath(routerPath: string, routePath: string) { // If router's path ends with slash and route's path starts with slash, @@ -140,11 +125,10 @@ function getRouteFullPath(routerPath: string, routePath: string) { * @returns Route schemas if `validate` is specified on the route, otherwise * undefined. */ -function routeSchemasFromRouteConfig< - P extends ObjectType, - Q extends ObjectType, - B extends ObjectType | Type | Type ->(route: RouteConfig, routeMethod: RouteMethod) { +function routeSchemasFromRouteConfig( + route: RouteConfig, + routeMethod: RouteMethod +) { // The type doesn't allow `validate` to be undefined, but it can still // happen when it's used from JavaScript. if (route.validate === undefined) { @@ -155,15 +139,17 @@ function routeSchemasFromRouteConfig< if (route.validate !== false) { Object.entries(route.validate).forEach(([key, schema]) => { - if (!(schema instanceof Type)) { + if (!(schema instanceof Type || typeof schema === 'function')) { throw new Error( - `Expected a valid schema declared with '@kbn/config-schema' package at key: [${key}].` + `Expected a valid validation logic declared with '@kbn/config-schema' package or a RouteValidationFunction at key: [${key}].` ); } }); } - return route.validate ? route.validate : undefined; + if (route.validate) { + return RouteValidator.from(route.validate); + } } /** @@ -174,12 +160,7 @@ function routeSchemasFromRouteConfig< */ function validOptions( method: RouteMethod, - routeConfig: RouteConfig< - ObjectType, - ObjectType, - ObjectType | Type | Type, - typeof method - > + routeConfig: RouteConfig ) { const shouldNotHavePayload = ['head', 'get'].includes(method); const { options = {}, validate } = routeConfig; @@ -225,11 +206,7 @@ export class Router implements IRouter { private readonly log: Logger, private readonly enhanceWithContext: ContextEnhancer ) { - const buildMethod = (method: Method) => < - P extends ObjectType, - Q extends ObjectType, - B extends ObjectType | Type | Type - >( + const buildMethod = (method: Method) => ( route: RouteConfig, handler: RequestHandler ) => { @@ -260,17 +237,11 @@ export class Router implements IRouter { return [...this.routes]; } - public handleLegacyErrors

( - handler: RequestHandler - ): RequestHandler { + public handleLegacyErrors(handler: RequestHandler): RequestHandler { return wrapErrors(handler); } - private async handle< - P extends ObjectType, - Q extends ObjectType, - B extends ObjectType | Type | Type - >({ + private async handle({ routeSchemas, request, responseToolkit, @@ -279,9 +250,9 @@ export class Router implements IRouter { request: Request; responseToolkit: ResponseToolkit; handler: RequestHandlerEnhanced; - routeSchemas?: RouteSchemas; + routeSchemas?: RouteValidator; }) { - let kibanaRequest: KibanaRequest, TypeOf, TypeOf, typeof request.method>; + let kibanaRequest: KibanaRequest; const hapiResponseAdapter = new HapiResponseAdapter(responseToolkit); try { kibanaRequest = KibanaRequest.from(request, routeSchemas); @@ -303,12 +274,9 @@ type WithoutHeadArgument = T extends (first: any, ...rest: infer Params) => i ? (...rest: Params) => Return : never; -type RequestHandlerEnhanced< - P extends ObjectType, - Q extends ObjectType, - B extends ObjectType | Type | Type, - Method extends RouteMethod -> = WithoutHeadArgument>; +type RequestHandlerEnhanced = WithoutHeadArgument< + RequestHandler +>; /** * A function executed when route path matched requested resource path. @@ -345,12 +313,12 @@ type RequestHandlerEnhanced< * @public */ export type RequestHandler< - P extends ObjectType, - Q extends ObjectType, - B extends ObjectType | Type | Type, + P = unknown, + Q = unknown, + B = unknown, Method extends RouteMethod = any > = ( context: RequestHandlerContext, - request: KibanaRequest, TypeOf, TypeOf, Method>, + request: KibanaRequest, response: KibanaResponseFactory ) => IKibanaResponse | Promise>; diff --git a/src/core/server/http/router/validator/index.ts b/src/core/server/http/router/validator/index.ts new file mode 100644 index 0000000000000..edb116c40144a --- /dev/null +++ b/src/core/server/http/router/validator/index.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { + RouteValidator, + RouteValidatorConfig, + RouteValidationSpec, + RouteValidationFunction, + RouteValidatorOptions, + RouteValidatorFullConfig, + RouteValidationResultFactory, +} from './validator'; +export { RouteValidationError } from './validator_error'; diff --git a/src/core/server/http/router/validator/validator.test.ts b/src/core/server/http/router/validator/validator.test.ts new file mode 100644 index 0000000000000..729eb1b60c10a --- /dev/null +++ b/src/core/server/http/router/validator/validator.test.ts @@ -0,0 +1,135 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { RouteValidationError, RouteValidator } from './'; +import { schema, Type } from '@kbn/config-schema'; + +describe('Router validator', () => { + it('should validate and infer the type from a function', () => { + const validator = RouteValidator.from({ + params: ({ foo }, validationResult) => { + if (typeof foo === 'string') { + return validationResult.ok({ foo }); + } + return validationResult.badRequest('Not a string', ['foo']); + }, + }); + expect(validator.getParams({ foo: 'bar' })).toStrictEqual({ foo: 'bar' }); + expect(validator.getParams({ foo: 'bar' }).foo.toUpperCase()).toBe('BAR'); // It knows it's a string! :) + expect(() => validator.getParams({ foo: 1 })).toThrowError('[foo]: Not a string'); + expect(() => validator.getParams({})).toThrowError('[foo]: Not a string'); + + expect(() => validator.getParams(undefined)).toThrowError( + "Cannot destructure property `foo` of 'undefined' or 'null'." + ); + expect(() => validator.getParams({}, 'myField')).toThrowError('[myField.foo]: Not a string'); + + expect(validator.getBody(undefined)).toStrictEqual({}); + expect(validator.getQuery(undefined)).toStrictEqual({}); + }); + + it('should validate and infer the type from a function that does not use the resolver', () => { + const validator = RouteValidator.from({ + params: data => { + if (typeof data.foo === 'string') { + return { value: { foo: data.foo as string } }; + } + return { error: new RouteValidationError('Not a string', ['foo']) }; + }, + }); + expect(validator.getParams({ foo: 'bar' })).toStrictEqual({ foo: 'bar' }); + expect(validator.getParams({ foo: 'bar' }).foo.toUpperCase()).toBe('BAR'); // It knows it's a string! :) + expect(() => validator.getParams({ foo: 1 })).toThrowError('[foo]: Not a string'); + expect(() => validator.getParams({})).toThrowError('[foo]: Not a string'); + + expect(() => validator.getParams(undefined)).toThrowError( + `Cannot read property 'foo' of undefined` + ); + expect(() => validator.getParams({}, 'myField')).toThrowError('[myField.foo]: Not a string'); + + expect(validator.getBody(undefined)).toStrictEqual({}); + expect(validator.getQuery(undefined)).toStrictEqual({}); + }); + + it('should validate and infer the type from a config-schema ObjectType', () => { + const schemaValidation = RouteValidator.from({ + params: schema.object({ + foo: schema.string(), + }), + }); + + expect(schemaValidation.getParams({ foo: 'bar' })).toStrictEqual({ foo: 'bar' }); + expect(schemaValidation.getParams({ foo: 'bar' }).foo.toUpperCase()).toBe('BAR'); // It knows it's a string! :) + expect(() => schemaValidation.getParams({ foo: 1 })).toThrowError( + '[foo]: expected value of type [string] but got [number]' + ); + expect(() => schemaValidation.getParams({})).toThrowError( + '[foo]: expected value of type [string] but got [undefined]' + ); + expect(() => schemaValidation.getParams(undefined)).toThrowError( + '[foo]: expected value of type [string] but got [undefined]' + ); + expect(() => schemaValidation.getParams({}, 'myField')).toThrowError( + '[myField.foo]: expected value of type [string] but got [undefined]' + ); + }); + + it('should validate and infer the type from a config-schema non-ObjectType', () => { + const schemaValidation = RouteValidator.from({ params: schema.buffer() }); + + const foo = Buffer.from('hi!'); + expect(schemaValidation.getParams(foo)).toStrictEqual(foo); + expect(schemaValidation.getParams(foo).byteLength).toBeGreaterThan(0); // It knows it's a buffer! :) + expect(() => schemaValidation.getParams({ foo: 1 })).toThrowError( + 'expected value of type [Buffer] but got [Object]' + ); + expect(() => schemaValidation.getParams({})).toThrowError( + 'expected value of type [Buffer] but got [Object]' + ); + expect(() => schemaValidation.getParams(undefined)).toThrowError( + `expected value of type [Buffer] but got [undefined]` + ); + expect(() => schemaValidation.getParams({}, 'myField')).toThrowError( + '[myField]: expected value of type [Buffer] but got [Object]' + ); + }); + + it('should catch the errors thrown by the validate function', () => { + const validator = RouteValidator.from({ + params: data => { + throw new Error('Something went terribly wrong'); + }, + }); + + expect(() => validator.getParams({ foo: 1 })).toThrowError('Something went terribly wrong'); + expect(() => validator.getParams({}, 'myField')).toThrowError( + '[myField]: Something went terribly wrong' + ); + }); + + it('should not accept invalid validation options', () => { + const wrongValidateSpec = RouteValidator.from({ + params: { validate: (data: T): T => data } as Type, + }); + + expect(() => wrongValidateSpec.getParams({ foo: 1 })).toThrowError( + 'The validation rule provided in the handler is not valid' + ); + }); +}); diff --git a/src/core/server/http/router/validator/validator.ts b/src/core/server/http/router/validator/validator.ts new file mode 100644 index 0000000000000..65c0a934e6ef0 --- /dev/null +++ b/src/core/server/http/router/validator/validator.ts @@ -0,0 +1,280 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ValidationError, Type, schema, ObjectType } from '@kbn/config-schema'; +import { Stream } from 'stream'; +import { RouteValidationError } from './validator_error'; + +/** + * Validation result factory to be used in the custom validation function to return the valid data or validation errors + * + * See {@link RouteValidationFunction}. + * + * @public + */ +export interface RouteValidationResultFactory { + ok: (value: T) => { value: T }; + badRequest: (error: Error | string, path?: string[]) => { error: RouteValidationError }; +} + +/** + * The custom validation function if @kbn/config-schema is not a valid solution for your specific plugin requirements. + * + * @example + * + * The validation should look something like: + * ```typescript + * interface MyExpectedBody { + * bar: string; + * baz: number; + * } + * + * const myBodyValidation: RouteValidationFunction = (data, validationResult) => { + * const { ok, badRequest } = validationResult; + * const { bar, baz } = data || {}; + * if (typeof bar === 'string' && typeof baz === 'number') { + * return ok({ bar, baz }); + * } else { + * return badRequest('Wrong payload', ['body']); + * } + * } + * ``` + * + * @public + */ +export type RouteValidationFunction = ( + data: any, + validationResult: RouteValidationResultFactory +) => + | { + value: T; + error?: never; + } + | { + value?: never; + error: RouteValidationError; + }; + +/** + * Allowed property validation options: either @kbn/config-schema validations or custom validation functions + * + * See {@link RouteValidationFunction} for custom validation. + * + * @public + */ +export type RouteValidationSpec = ObjectType | Type | RouteValidationFunction; + +// Ugly as hell but we need this conditional typing to have proper type inference +type RouteValidationResultType | undefined> = NonNullable< + T extends RouteValidationFunction + ? ReturnType['value'] + : T extends Type + ? ReturnType + : undefined +>; + +/** + * The configuration object to the RouteValidator class. + * Set `params`, `query` and/or `body` to specify the validation logic to follow for that property. + * + * @public + */ +export interface RouteValidatorConfig { + /** + * Validation logic for the URL params + * @public + */ + params?: RouteValidationSpec

; + /** + * Validation logic for the Query params + * @public + */ + query?: RouteValidationSpec; + /** + * Validation logic for the body payload + * @public + */ + body?: RouteValidationSpec; +} + +/** + * Additional options for the RouteValidator class to modify its default behaviour. + * + * @public + */ +export interface RouteValidatorOptions { + /** + * Set the `unsafe` config to avoid running some additional internal *safe* validations on top of your custom validation + * @public + */ + unsafe?: { + params?: boolean; + query?: boolean; + body?: boolean; + }; +} + +/** + * Route validations config and options merged into one object + * @public + */ +export type RouteValidatorFullConfig = RouteValidatorConfig & + RouteValidatorOptions; + +/** + * Route validator class to define the validation logic for each new route. + * + * @internal + */ +export class RouteValidator

{ + public static from

( + opts: RouteValidator | RouteValidatorFullConfig + ) { + if (opts instanceof RouteValidator) { + return opts; + } + const { params, query, body, ...options } = opts; + return new RouteValidator({ params, query, body }, options); + } + + private static ResultFactory: RouteValidationResultFactory = { + ok: (value: T) => ({ value }), + badRequest: (error: Error | string, path?: string[]) => ({ + error: new RouteValidationError(error, path), + }), + }; + + private constructor( + private readonly config: RouteValidatorConfig, + private readonly options: RouteValidatorOptions = {} + ) {} + + /** + * Get validated URL params + * @internal + */ + public getParams(data: unknown, namespace?: string): Readonly

{ + return this.validate(this.config.params, this.options.unsafe?.params, data, namespace); + } + + /** + * Get validated query params + * @internal + */ + public getQuery(data: unknown, namespace?: string): Readonly { + return this.validate(this.config.query, this.options.unsafe?.query, data, namespace); + } + + /** + * Get validated body + * @internal + */ + public getBody(data: unknown, namespace?: string): Readonly { + return this.validate(this.config.body, this.options.unsafe?.body, data, namespace); + } + + /** + * Has body validation + * @internal + */ + public hasBody(): boolean { + return typeof this.config.body !== 'undefined'; + } + + private validate( + validationRule?: RouteValidationSpec, + unsafe?: boolean, + data?: unknown, + namespace?: string + ): RouteValidationResultType { + if (typeof validationRule === 'undefined') { + return {}; + } + let precheckedData = this.preValidateSchema(data).validate(data, {}, namespace); + + if (unsafe !== true) { + precheckedData = this.safetyPrechecks(precheckedData, namespace); + } + + const customCheckedData = this.customValidation(validationRule, precheckedData, namespace); + + if (unsafe === true) { + return customCheckedData; + } + + return this.safetyPostchecks(customCheckedData, namespace); + } + + private safetyPrechecks(data: T, namespace?: string): T { + // We can add any pre-validation safety logic in here + return data; + } + + private safetyPostchecks(data: T, namespace?: string): T { + // We can add any post-validation safety logic in here + return data; + } + + private customValidation( + validationRule: RouteValidationSpec, + data?: unknown, + namespace?: string + ): RouteValidationResultType { + if (validationRule instanceof Type) { + return validationRule.validate(data, {}, namespace); + } else if (typeof validationRule === 'function') { + return this.validateFunction(validationRule, data, namespace); + } else { + throw new ValidationError( + new RouteValidationError(`The validation rule provided in the handler is not valid`), + namespace + ); + } + } + + private validateFunction( + validateFn: RouteValidationFunction, + data: unknown, + namespace?: string + ): T { + let result: ReturnType; + try { + result = validateFn(data, RouteValidator.ResultFactory); + } catch (err) { + result = { error: new RouteValidationError(err) }; + } + + if (result.error) { + throw new ValidationError(result.error, namespace); + } + return result.value; + } + + private preValidateSchema(data: any) { + if (Buffer.isBuffer(data)) { + // if options.body.parse !== true + return schema.buffer(); + } else if (data instanceof Stream) { + // if options.body.output === 'stream' + return schema.stream(); + } else { + return schema.maybe(schema.nullable(schema.object({}, { allowUnknowns: true }))); + } + } +} diff --git a/src/core/server/http/router/validator/validator_error.ts b/src/core/server/http/router/validator/validator_error.ts new file mode 100644 index 0000000000000..d306db4ad1cf4 --- /dev/null +++ b/src/core/server/http/router/validator/validator_error.ts @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SchemaTypeError } from '@kbn/config-schema'; + +/** + * Error to return when the validation is not successful. + * @public + */ +export class RouteValidationError extends SchemaTypeError { + constructor(error: Error | string, path: string[] = []) { + super(error, path); + + // Set the prototype explicitly, see: + // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work + Object.setPrototypeOf(this, RouteValidationError.prototype); + } +} diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 060265120b865..878f854f2a517 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -134,10 +134,16 @@ export { RouteRegistrar, RouteMethod, RouteConfigOptions, - RouteSchemas, RouteConfigOptionsBody, RouteContentType, validBodyOutput, + RouteValidatorConfig, + RouteValidationSpec, + RouteValidationFunction, + RouteValidatorOptions, + RouteValidatorFullConfig, + RouteValidationResultFactory, + RouteValidationError, SessionStorage, SessionStorageCookieOptions, SessionCookieValidationResult, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index eb23915122a58..ef5368751c8f5 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -115,6 +115,7 @@ import { RenderSearchTemplateParams } from 'elasticsearch'; import { Request } from 'hapi'; import { ResponseObject } from 'hapi'; import { ResponseToolkit } from 'hapi'; +import { SchemaTypeError } from '@kbn/config-schema'; import { ScrollParams } from 'elasticsearch'; import { SearchParams } from 'elasticsearch'; import { SearchResponse } from 'elasticsearch'; @@ -805,7 +806,7 @@ export interface IRouter { // // @internal getRoutes: () => RouterRoute[]; - handleLegacyErrors:

(handler: RequestHandler) => RequestHandler; + handleLegacyErrors: (handler: RequestHandler) => RequestHandler; patch: RouteRegistrar<'patch'>; post: RouteRegistrar<'post'>; put: RouteRegistrar<'put'>; @@ -841,8 +842,10 @@ export class KibanaRequest | Type>(req: Request, routeSchemas?: RouteSchemas, withoutSecretHeaders?: boolean): KibanaRequest; + static from(req: Request, routeSchemas?: RouteValidator | RouteValidatorFullConfig, withoutSecretHeaders?: boolean): KibanaRequest; readonly headers: Headers; // (undocumented) readonly params: Params; @@ -1159,7 +1162,7 @@ export type RedirectResponseOptions = HttpResponseOptions & { }; // @public -export type RequestHandler

| Type, Method extends RouteMethod = any> = (context: RequestHandlerContext, request: KibanaRequest, TypeOf, TypeOf, Method>, response: KibanaResponseFactory) => IKibanaResponse | Promise>; +export type RequestHandler

= (context: RequestHandlerContext, request: KibanaRequest, response: KibanaResponseFactory) => IKibanaResponse | Promise>; // @public export interface RequestHandlerContext { @@ -1201,10 +1204,10 @@ export type ResponseHeaders = { }; // @public -export interface RouteConfig

| Type, Method extends RouteMethod> { +export interface RouteConfig { options?: RouteConfigOptions; path: string; - validate: RouteSchemas | false; + validate: RouteValidatorFullConfig | false; } // @public @@ -1229,16 +1232,54 @@ export type RouteContentType = 'application/json' | 'application/*+json' | 'appl export type RouteMethod = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options'; // @public -export type RouteRegistrar =

| Type>(route: RouteConfig, handler: RequestHandler) => void; +export type RouteRegistrar = (route: RouteConfig, handler: RequestHandler) => void; // @public -export interface RouteSchemas

| Type> { - // (undocumented) - body?: B; +export class RouteValidationError extends SchemaTypeError { + constructor(error: Error | string, path?: string[]); +} + +// @public +export type RouteValidationFunction = (data: any, validationResult: RouteValidationResultFactory) => { + value: T; + error?: never; +} | { + value?: never; + error: RouteValidationError; +}; + +// @public +export interface RouteValidationResultFactory { // (undocumented) - params?: P; + badRequest: (error: Error | string, path?: string[]) => { + error: RouteValidationError; + }; // (undocumented) - query?: Q; + ok: (value: T) => { + value: T; + }; +} + +// @public +export type RouteValidationSpec = ObjectType | Type | RouteValidationFunction; + +// @public +export interface RouteValidatorConfig { + body?: RouteValidationSpec; + params?: RouteValidationSpec

; + query?: RouteValidationSpec; +} + +// @public +export type RouteValidatorFullConfig = RouteValidatorConfig & RouteValidatorOptions; + +// @public +export interface RouteValidatorOptions { + unsafe?: { + params?: boolean; + query?: boolean; + body?: boolean; + }; } // @public (undocumented) diff --git a/src/plugins/home/server/services/tutorials/lib/tutorials_registry_types.ts b/src/plugins/home/server/services/tutorials/lib/tutorials_registry_types.ts index d081639e5c7e2..951ce935760a1 100644 --- a/src/plugins/home/server/services/tutorials/lib/tutorials_registry_types.ts +++ b/src/plugins/home/server/services/tutorials/lib/tutorials_registry_types.ts @@ -94,17 +94,5 @@ export interface TutorialSchema { savedObjectsInstallMsg: string; } export type TutorialProvider = (context: { [key: string]: unknown }) => TutorialSchema; -export type TutorialContextFactory = ( - req: KibanaRequest< - Readonly<{ - [x: string]: any; - }>, - Readonly<{ - [x: string]: any; - }>, - Readonly<{ - [x: string]: any; - }> - > -) => { [key: string]: unknown }; +export type TutorialContextFactory = (req: KibanaRequest) => { [key: string]: unknown }; export type ScopedTutorialContextFactory = (...args: any[]) => any; diff --git a/test/plugin_functional/plugins/core_plugin_b/server/plugin.ts b/test/plugin_functional/plugins/core_plugin_b/server/plugin.ts index 771e50b22f66b..91a4fe89c1c57 100644 --- a/test/plugin_functional/plugins/core_plugin_b/server/plugin.ts +++ b/test/plugin_functional/plugins/core_plugin_b/server/plugin.ts @@ -18,6 +18,7 @@ */ import { Plugin, CoreSetup } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; import { PluginARequestContext } from '../../core_plugin_a/server'; declare module 'kibana/server' { @@ -34,6 +35,25 @@ export class CorePluginBPlugin implements Plugin { const response = await context.pluginA.ping(); return res.ok({ body: `Pong via plugin A: ${response}` }); }); + + router.post( + { + path: '/core_plugin_b', + validate: { + query: schema.object({ id: schema.string() }), + body: ({ bar, baz } = {}, { ok, badRequest }) => { + if (typeof bar === 'string' && bar === baz) { + return ok({ bar, baz }); + } else { + return badRequest(`bar: ${bar} !== baz: ${baz} or they are not string`); + } + }, + }, + }, + async (context, req, res) => { + return res.ok({ body: `ID: ${req.query.id} - ${req.body.bar.toUpperCase()}` }); + } + ); } public start() {} diff --git a/test/plugin_functional/test_suites/core_plugins/server_plugins.ts b/test/plugin_functional/test_suites/core_plugins/server_plugins.ts index 3881af5642996..d437912c1b69a 100644 --- a/test/plugin_functional/test_suites/core_plugins/server_plugins.ts +++ b/test/plugin_functional/test_suites/core_plugins/server_plugins.ts @@ -29,5 +29,29 @@ export default function({ getService }: PluginFunctionalProviderContext) { .expect(200) .expect('Pong via plugin A: true'); }); + + it('extend request handler context with validation', async () => { + await supertest + .post('/core_plugin_b') + .set('kbn-xsrf', 'anything') + .query({ id: 'TEST' }) + .send({ bar: 'hi!', baz: 'hi!' }) + .expect(200) + .expect('ID: TEST - HI!'); + }); + + it('extend request handler context with validation (400)', async () => { + await supertest + .post('/core_plugin_b') + .set('kbn-xsrf', 'anything') + .query({ id: 'TEST' }) + .send({ bar: 'hi!', baz: 1234 }) + .expect(400) + .expect({ + error: 'Bad Request', + message: '[request body]: bar: hi! !== baz: 1234 or they are not string', + statusCode: 400, + }); + }); }); } diff --git a/x-pack/legacy/plugins/apm/server/routes/create_api/index.ts b/x-pack/legacy/plugins/apm/server/routes/create_api/index.ts index 3e97d851acd29..c1a9838e90406 100644 --- a/x-pack/legacy/plugins/apm/server/routes/create_api/index.ts +++ b/x-pack/legacy/plugins/apm/server/routes/create_api/index.ts @@ -82,7 +82,7 @@ export function createApi() { // 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(anyObject) as typeof anyObject, + body: schema.nullable(anyObject), params: anyObject, query: anyObject } diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts index 3dd81f4a71862..75cb860bd9869 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -6,7 +6,6 @@ import { SearchResponse, GenericParams } from 'elasticsearch'; import { Lifecycle } from 'hapi'; -import { ObjectType } from '@kbn/config-schema'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { RouteMethod, RouteConfig } from '../../../../../../../../src/core/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../../plugins/features/server'; @@ -165,11 +164,6 @@ export interface InfraTSVBSeries { export type InfraTSVBDataPoint = [number, number]; -export type InfraRouteConfig< - params extends ObjectType, - query extends ObjectType, - body extends ObjectType, - method extends RouteMethod -> = { +export type InfraRouteConfig = { method: RouteMethod; } & RouteConfig; diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts index d7dd4ed1c82a4..4409667d8390a 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -10,7 +10,7 @@ import { GenericParams } from 'elasticsearch'; import { GraphQLSchema } from 'graphql'; import { Legacy } from 'kibana'; import { runHttpQuery } from 'apollo-server-core'; -import { schema, TypeOf, ObjectType } from '@kbn/config-schema'; +import { schema, TypeOf } from '@kbn/config-schema'; import { InfraRouteConfig, InfraTSVBResponse, @@ -43,12 +43,7 @@ export class KibanaFramework { this.plugins = plugins; } - public registerRoute< - params extends ObjectType = any, - query extends ObjectType = any, - body extends ObjectType = any, - method extends RouteMethod = any - >( + public registerRoute( config: InfraRouteConfig, handler: RequestHandler ) { diff --git a/x-pack/plugins/canvas/server/routes/catch_error_handler.ts b/x-pack/plugins/canvas/server/routes/catch_error_handler.ts index fb7f4d6ee2600..4717d8762ffe2 100644 --- a/x-pack/plugins/canvas/server/routes/catch_error_handler.ts +++ b/x-pack/plugins/canvas/server/routes/catch_error_handler.ts @@ -4,14 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ObjectType } from '@kbn/config-schema'; import { RequestHandler } from 'src/core/server'; -export const catchErrorHandler: < - P extends ObjectType, - Q extends ObjectType, - B extends ObjectType ->( +export const catchErrorHandler: ( fn: RequestHandler ) => RequestHandler = fn => { return async (context, request, response) => { diff --git a/x-pack/plugins/security/server/authorization/register_privileges_with_cluster.test.ts b/x-pack/plugins/security/server/authorization/register_privileges_with_cluster.test.ts index c18c071ca7675..dc406c17925dd 100644 --- a/x-pack/plugins/security/server/authorization/register_privileges_with_cluster.test.ts +++ b/x-pack/plugins/security/server/authorization/register_privileges_with_cluster.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IClusterClient, Logger } from '../../../../../src/core/server'; +import { IClusterClient, Logger } from 'kibana/server'; import { RawKibanaPrivileges } from '../../common/model'; import { registerPrivilegesWithCluster } from './register_privileges_with_cluster'; diff --git a/x-pack/plugins/security/server/routes/authentication/saml.ts b/x-pack/plugins/security/server/routes/authentication/saml.ts index e45eae827c858..b5b9f27c2713d 100644 --- a/x-pack/plugins/security/server/routes/authentication/saml.ts +++ b/x-pack/plugins/security/server/routes/authentication/saml.ts @@ -60,7 +60,9 @@ export function defineSAMLRoutes({ router, logger, authc, csp, basePath }: Route router.get( { path: '/api/security/saml/start', - validate: { query: schema.object({ redirectURLFragment: schema.string() }) }, + validate: { + query: schema.object({ redirectURLFragment: schema.string() }), + }, options: { authRequired: false }, }, async (context, request, response) => { diff --git a/x-pack/plugins/security/server/routes/authorization/roles/delete.ts b/x-pack/plugins/security/server/routes/authorization/roles/delete.ts index de966d6f2a758..eb56143288747 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/delete.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/delete.ts @@ -13,7 +13,9 @@ export function defineDeleteRolesRoutes({ router, clusterClient }: RouteDefiniti router.delete( { path: '/api/security/role/{name}', - validate: { params: schema.object({ name: schema.string({ minLength: 1 }) }) }, + validate: { + params: schema.object({ name: schema.string({ minLength: 1 }) }), + }, }, createLicensedRouteHandler(async (context, request, response) => { try { diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get.ts b/x-pack/plugins/security/server/routes/authorization/roles/get.ts index 8c158bee1a15e..bf1140e2e6fd1 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get.ts @@ -14,7 +14,9 @@ export function defineGetRolesRoutes({ router, authz, clusterClient }: RouteDefi router.get( { path: '/api/security/role/{name}', - validate: { params: schema.object({ name: schema.string({ minLength: 1 }) }) }, + validate: { + params: schema.object({ name: schema.string({ minLength: 1 }) }), + }, }, createLicensedRouteHandler(async (context, request, response) => { try { diff --git a/x-pack/plugins/security/server/routes/licensed_route_handler.ts b/x-pack/plugins/security/server/routes/licensed_route_handler.ts index 1194e3d0a83cc..bc5a6a1139215 100644 --- a/x-pack/plugins/security/server/routes/licensed_route_handler.ts +++ b/x-pack/plugins/security/server/routes/licensed_route_handler.ts @@ -4,17 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestHandler } from 'src/core/server'; -import { ObjectType } from '@kbn/config-schema'; +import { RequestHandler } from 'kibana/server'; import { LICENSE_CHECK_STATE } from '../../../licensing/server'; -export const createLicensedRouteHandler = < - P extends ObjectType, - Q extends ObjectType, - B extends ObjectType ->( - handler: RequestHandler -) => { +export const createLicensedRouteHandler = (handler: RequestHandler) => { const licensedRouteHandler: RequestHandler = (context, request, responseToolkit) => { const { license } = context.licensing; const licenseCheck = license.check('security', 'basic'); diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts index 4f78828b14dc2..4d8d08a487e9a 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts @@ -11,7 +11,7 @@ import { mockRouteContext, mockRouteContextWithInvalidLicense, } from '../__fixtures__'; -import { CoreSetup, IRouter, kibanaResponseFactory } from 'src/core/server'; +import { CoreSetup, IRouter, kibanaResponseFactory, RouteValidatorConfig } from 'src/core/server'; import { loggingServiceMock, elasticsearchServiceMock, @@ -22,10 +22,9 @@ import { SpacesService } from '../../../spaces_service'; import { SpacesAuditLogger } from '../../../lib/audit_logger'; import { SpacesClient } from '../../../lib/spaces_client'; import { initCopyToSpacesApi } from './copy_to_space'; -import { ObjectType } from '@kbn/config-schema'; -import { RouteSchemas } from 'src/core/server/http/router/route'; import { spacesConfig } from '../../../lib/__fixtures__'; import { securityMock } from '../../../../../security/server/mocks'; +import { ObjectType } from '@kbn/config-schema'; describe('copy to space', () => { const spacesSavedObjects = createSpaces(); @@ -78,19 +77,11 @@ describe('copy to space', () => { return { copyToSpace: { - routeValidation: ctsRouteDefinition.validate as RouteSchemas< - ObjectType, - ObjectType, - ObjectType - >, + routeValidation: ctsRouteDefinition.validate as RouteValidatorConfig<{}, {}, {}>, routeHandler: ctsRouteHandler, }, resolveConflicts: { - routeValidation: resolveRouteDefinition.validate as RouteSchemas< - ObjectType, - ObjectType, - ObjectType - >, + routeValidation: resolveRouteDefinition.validate as RouteValidatorConfig<{}, {}, {}>, routeHandler: resolveRouteHandler, }, savedObjectsRepositoryMock, @@ -150,7 +141,7 @@ describe('copy to space', () => { const { copyToSpace } = await setup(); expect(() => - copyToSpace.routeValidation.body!.validate(payload) + (copyToSpace.routeValidation.body as ObjectType).validate(payload) ).toThrowErrorMatchingInlineSnapshot(`"[spaces]: duplicate space ids are not allowed"`); }); @@ -163,7 +154,7 @@ describe('copy to space', () => { const { copyToSpace } = await setup(); expect(() => - copyToSpace.routeValidation.body!.validate(payload) + (copyToSpace.routeValidation.body as ObjectType).validate(payload) ).toThrowErrorMatchingInlineSnapshot( `"[spaces.1]: lower case, a-z, 0-9, \\"_\\", and \\"-\\" are allowed"` ); @@ -181,7 +172,7 @@ describe('copy to space', () => { const { copyToSpace } = await setup(); expect(() => - copyToSpace.routeValidation.body!.validate(payload) + (copyToSpace.routeValidation.body as ObjectType).validate(payload) ).toThrowErrorMatchingInlineSnapshot(`"[objects]: duplicate objects are not allowed"`); }); @@ -322,7 +313,7 @@ describe('copy to space', () => { const { resolveConflicts } = await setup(); expect(() => - resolveConflicts.routeValidation.body!.validate(payload) + (resolveConflicts.routeValidation.body as ObjectType).validate(payload) ).toThrowErrorMatchingInlineSnapshot(`"[objects]: duplicate objects are not allowed"`); }); @@ -343,7 +334,7 @@ describe('copy to space', () => { const { resolveConflicts } = await setup(); expect(() => - resolveConflicts.routeValidation.body!.validate(payload) + (resolveConflicts.routeValidation.body as ObjectType).validate(payload) ).toThrowErrorMatchingInlineSnapshot( `"[retries.key(\\"invalid-space-id!@#$%^&*()\\")]: Invalid space id: invalid-space-id!@#$%^&*()"` ); diff --git a/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts b/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts index 86da3023c515e..28d5708a3873c 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts @@ -12,7 +12,7 @@ import { mockRouteContext, mockRouteContextWithInvalidLicense, } from '../__fixtures__'; -import { CoreSetup, IRouter, kibanaResponseFactory } from 'src/core/server'; +import { CoreSetup, IRouter, kibanaResponseFactory, RouteValidatorConfig } from 'src/core/server'; import { loggingServiceMock, elasticsearchServiceMock, @@ -23,10 +23,9 @@ import { SpacesService } from '../../../spaces_service'; import { SpacesAuditLogger } from '../../../lib/audit_logger'; import { SpacesClient } from '../../../lib/spaces_client'; import { initDeleteSpacesApi } from './delete'; -import { RouteSchemas } from 'src/core/server/http/router/route'; -import { ObjectType } from '@kbn/config-schema'; import { spacesConfig } from '../../../lib/__fixtures__'; import { securityMock } from '../../../../../security/server/mocks'; +import { ObjectType } from '@kbn/config-schema'; describe('Spaces Public API', () => { const spacesSavedObjects = createSpaces(); @@ -75,14 +74,16 @@ describe('Spaces Public API', () => { const [routeDefinition, routeHandler] = router.delete.mock.calls[0]; return { - routeValidation: routeDefinition.validate as RouteSchemas, + routeValidation: routeDefinition.validate as RouteValidatorConfig<{}, {}, {}>, routeHandler, }; }; it('requires a space id as part of the path', async () => { const { routeValidation } = await setup(); - expect(() => routeValidation.params!.validate({})).toThrowErrorMatchingInlineSnapshot( + expect(() => + (routeValidation.params as ObjectType).validate({}) + ).toThrowErrorMatchingInlineSnapshot( `"[id]: expected value of type [string] but got [undefined]"` ); }); diff --git a/x-pack/plugins/spaces/server/routes/api/external/post.test.ts b/x-pack/plugins/spaces/server/routes/api/external/post.test.ts index 398b2e37191b6..d82ccaa8ff380 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/post.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/post.test.ts @@ -11,7 +11,7 @@ import { mockRouteContext, mockRouteContextWithInvalidLicense, } from '../__fixtures__'; -import { CoreSetup, kibanaResponseFactory, IRouter } from 'src/core/server'; +import { CoreSetup, kibanaResponseFactory, IRouter, RouteValidatorConfig } from 'src/core/server'; import { loggingServiceMock, elasticsearchServiceMock, @@ -22,10 +22,9 @@ import { SpacesService } from '../../../spaces_service'; import { SpacesAuditLogger } from '../../../lib/audit_logger'; import { SpacesClient } from '../../../lib/spaces_client'; import { initPostSpacesApi } from './post'; -import { RouteSchemas } from 'src/core/server/http/router/route'; -import { ObjectType } from '@kbn/config-schema'; import { spacesConfig } from '../../../lib/__fixtures__'; import { securityMock } from '../../../../../security/server/mocks'; +import { ObjectType } from '@kbn/config-schema'; describe('Spaces Public API', () => { const spacesSavedObjects = createSpaces(); @@ -74,7 +73,7 @@ describe('Spaces Public API', () => { const [routeDefinition, routeHandler] = router.post.mock.calls[0]; return { - routeValidation: routeDefinition.validate as RouteSchemas, + routeValidation: routeDefinition.validate as RouteValidatorConfig<{}, {}, {}>, routeHandler, savedObjectsRepositoryMock, }; @@ -159,7 +158,7 @@ describe('Spaces Public API', () => { const { routeValidation, routeHandler, savedObjectsRepositoryMock } = await setup(); const request = httpServerMock.createKibanaRequest({ - body: routeValidation.body!.validate(payload), + body: (routeValidation.body as ObjectType).validate(payload), method: 'post', }); diff --git a/x-pack/plugins/spaces/server/routes/api/external/put.test.ts b/x-pack/plugins/spaces/server/routes/api/external/put.test.ts index 5c213b7f73f62..15837110f4d92 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/put.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/put.test.ts @@ -12,7 +12,7 @@ import { mockRouteContext, mockRouteContextWithInvalidLicense, } from '../__fixtures__'; -import { CoreSetup, IRouter, kibanaResponseFactory } from 'src/core/server'; +import { CoreSetup, IRouter, kibanaResponseFactory, RouteValidatorConfig } from 'src/core/server'; import { loggingServiceMock, elasticsearchServiceMock, @@ -23,10 +23,9 @@ import { SpacesService } from '../../../spaces_service'; import { SpacesAuditLogger } from '../../../lib/audit_logger'; import { SpacesClient } from '../../../lib/spaces_client'; import { initPutSpacesApi } from './put'; -import { RouteSchemas } from 'src/core/server/http/router/route'; -import { ObjectType } from '@kbn/config-schema'; import { spacesConfig } from '../../../lib/__fixtures__'; import { securityMock } from '../../../../../security/server/mocks'; +import { ObjectType } from '@kbn/config-schema'; describe('PUT /api/spaces/space', () => { const spacesSavedObjects = createSpaces(); @@ -75,7 +74,7 @@ describe('PUT /api/spaces/space', () => { const [routeDefinition, routeHandler] = router.put.mock.calls[0]; return { - routeValidation: routeDefinition.validate as RouteSchemas, + routeValidation: routeDefinition.validate as RouteValidatorConfig<{}, {}, {}>, routeHandler, savedObjectsRepositoryMock, }; @@ -156,7 +155,7 @@ describe('PUT /api/spaces/space', () => { params: { id: payload.id, }, - body: routeValidation.body!.validate(payload), + body: (routeValidation.body as ObjectType).validate(payload), method: 'post', }); diff --git a/x-pack/plugins/spaces/server/routes/lib/licensed_route_handler.ts b/x-pack/plugins/spaces/server/routes/lib/licensed_route_handler.ts index 3838b1d134ea2..13bed5ce58e2b 100644 --- a/x-pack/plugins/spaces/server/routes/lib/licensed_route_handler.ts +++ b/x-pack/plugins/spaces/server/routes/lib/licensed_route_handler.ts @@ -4,17 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestHandler } from 'src/core/server'; -import { ObjectType } from '@kbn/config-schema'; +import { RequestHandler } from 'kibana/server'; import { LICENSE_CHECK_STATE } from '../../../../licensing/server'; -export const createLicensedRouteHandler = < - P extends ObjectType, - Q extends ObjectType, - B extends ObjectType ->( - handler: RequestHandler -) => { +export const createLicensedRouteHandler = (handler: RequestHandler) => { const licensedRouteHandler: RequestHandler = (context, request, responseToolkit) => { const { license } = context.licensing; const licenseCheck = license.check('spaces', 'basic');