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 23674200680644..ff71f13466cf88 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 73e96191e02e71..a6536d2ed67634 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 a39c27a758f9d7..9c8aafb158bfdf 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 79abfd4293e9fe..9fc183ffc334b1 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 1970b23c7ec099..4beb12f0d056e5 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 e1ec743ae71cc7..23a72fc3c68b3a 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 0f5f49636fdd5e..901d260fee21da 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 78a9d25c25d9d6..00000000000000 --- 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 77b980551a8ffe..00000000000000 --- 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 3dbf9fed94dc09..00000000000000 --- 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 5be5830cb4bc87..00000000000000 --- 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 00000000000000..551e13faaf1542 --- /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 00000000000000..71bd72dca2eaba --- /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 00000000000000..34fa096aaae785 --- /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 00000000000000..36ea6103fb352d --- /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 00000000000000..5f44b490e9a17a --- /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 00000000000000..eca6a31bd547f5 --- /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 00000000000000..f5fc06544043f2 --- /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 00000000000000..8b5d2c04130871 --- /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 00000000000000..4637da7741d806 --- /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 00000000000000..11de25ff3b19f6 --- /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 00000000000000..510325c2dfff76 --- /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 00000000000000..0f3785b954a3a9 --- /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 00000000000000..00b029d9928e3c --- /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 00000000000000..0406a372c4e9d7 --- /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 56b3096433c247..fc3e3c541846ab 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 8469a1d23a44b8..ba742292e9e836 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 27d9f530050bea..df357aeaf2731d 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 21de3945f10447..55ba813484268e 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 6117190c57ba89..c3b9b20d848657 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 c4b4d3840d1b95..8f895753c38c32 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 35bfb3ba9c33ad..084d30d6944744 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 ebb7ffa7a6fc92..51162a2c258e91 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 b132899910569e..47b001700b0154 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 129cf4c922ffd3..4439a80b1eac71 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 f5469a95b51069..a936da6a40a9f1 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 5c52e71cd54bbf..bb56ee3727d1a2 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 00000000000000..edb116c40144a4 --- /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 00000000000000..729eb1b60c10a8 --- /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 00000000000000..65c0a934e6ef00 --- /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 00000000000000..d306db4ad1cf4f --- /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 060265120b865e..878f854f2a5174 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 85c0af8131ccb6..dc4518ef41253f 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 d081639e5c7e2b..951ce935760a12 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 771e50b22f66ba..91a4fe89c1c57e 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 3881af56429963..d437912c1b69a0 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 3e97d851acd296..c1a9838e90406d 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 5b4ada31a6b0cf..71f6a4db8c30d9 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'; @@ -166,11 +165,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 d7dd4ed1c82a42..4409667d8390a4 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 fb7f4d6ee26006..4717d8762ffe2d 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 c18c071ca76758..dc406c17925dd5 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 06acf5283fe971..465ea61e12a4ec 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 de966d6f2a7586..eb56143288747c 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 8c158bee1a15e5..bf1140e2e6fd10 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 1194e3d0a83cc8..bc5a6a1139215e 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 4f78828b14dc20..4d8d08a487e9a9 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 86da3023c515ef..28d5708a3873ca 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 398b2e37191b6a..d82ccaa8ff3806 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 5c213b7f73f627..15837110f4d927 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 3838b1d134ea2c..13bed5ce58e2b2 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');