mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
* [NP] Allow custom validations in HTTP Routes apart from @kbn/config-schema * API docs * Allow validate function in the route handler (run-code validation) * Prefix RouteXXX + Params and Body Validation Aliases * Fix test broken by lodash * Update API docs * Add default types for simpler manual declaration * Add run-time validation of the RouteValidateSpec * Expose RouteValidationError instead of SchemaTypeError * RouteValidator as a class to match config-schema interface * Test for not-inline handler (need to check IRouter for #47047) * Add preValidation of the input for a safer custom validation * Better types for RouteHandlers * [NP] Move route validation to RouteValidator wrapper * Use the class only internally but maintain the same API * Fix types * Ensure RouteValidator instance in KibanaRequest.from * Fix validator.tests (Buffer.from instead of new Buffer) * Default precheck should allow null values * Also allow undefined in preChecks * MR feedback fixes * Provide RouteValidationResolver to the validation function * Add functional tests * Fix new functional tests * Fix validator additional test * Fix test with new resolver * Remove unused import * Rename ValidationResolver to ValidationResultFactory and change the interface to look more like the KibanaResponseFactory Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
63e1e750dd
commit
f3f1bd2ec6
60 changed files with 1369 additions and 301 deletions
|
@ -9,5 +9,5 @@ Wrap a router handler to catch and converts legacy boom errors to proper custom
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
handleLegacyErrors: <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(handler: RequestHandler<P, Q, B>) => RequestHandler<P, Q, B>;
|
||||
handleLegacyErrors: <P, Q, B>(handler: RequestHandler<P, Q, B>) => RequestHandler<P, Q, B>;
|
||||
```
|
||||
|
|
|
@ -18,7 +18,7 @@ export interface IRouter
|
|||
| --- | --- | --- |
|
||||
| [delete](./kibana-plugin-server.irouter.delete.md) | <code>RouteRegistrar<'delete'></code> | Register a route handler for <code>DELETE</code> request. |
|
||||
| [get](./kibana-plugin-server.irouter.get.md) | <code>RouteRegistrar<'get'></code> | Register a route handler for <code>GET</code> request. |
|
||||
| [handleLegacyErrors](./kibana-plugin-server.irouter.handlelegacyerrors.md) | <code><P extends ObjectType, Q extends ObjectType, B extends ObjectType>(handler: RequestHandler<P, Q, B>) => RequestHandler<P, Q, B></code> | Wrap a router handler to catch and converts legacy boom errors to proper custom errors. |
|
||||
| [handleLegacyErrors](./kibana-plugin-server.irouter.handlelegacyerrors.md) | <code><P, Q, B>(handler: RequestHandler<P, Q, B>) => RequestHandler<P, Q, B></code> | Wrap a router handler to catch and converts legacy boom errors to proper custom errors. |
|
||||
| [patch](./kibana-plugin-server.irouter.patch.md) | <code>RouteRegistrar<'patch'></code> | Register a route handler for <code>PATCH</code> request. |
|
||||
| [post](./kibana-plugin-server.irouter.post.md) | <code>RouteRegistrar<'post'></code> | Register a route handler for <code>POST</code> request. |
|
||||
| [put](./kibana-plugin-server.irouter.put.md) | <code>RouteRegistrar<'put'></code> | Register a route handler for <code>PUT</code> request. |
|
||||
|
|
|
@ -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 <code>body.error.header[WWW-Authenticate]</code> |
|
||||
| [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 errors<!-- -->See [RouteValidationFunction](./kibana-plugin-server.routevalidationfunction.md)<!-- -->. |
|
||||
| [RouteValidatorConfig](./kibana-plugin-server.routevalidatorconfig.md) | The configuration object to the RouteValidator class. Set <code>params</code>, <code>query</code> and/or <code>body</code> 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 <code>attributes</code> 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 functions<!-- -->See [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 errors<!-- -->Since 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 <code>isXYZError()</code> helpers exposed at <code>SavedObjectsErrorHelpers</code> should be used to understand and manage error responses from the <code>SavedObjectsClient</code>.<!-- -->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 <code>error.body.error.type</code> or doing substring checks on <code>error.body.error.reason</code>, 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 index<!-- -->From 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 index<!-- -->Unlike 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 <code>action.auto_create_index</code> 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) |
|
||||
|
|
|
@ -9,7 +9,7 @@ A function executed when route path matched requested resource path. Request han
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type RequestHandler<P extends ObjectType, Q extends ObjectType, B extends ObjectType | Type<Buffer> | Type<Stream>, Method extends RouteMethod = any> = (context: RequestHandlerContext, request: KibanaRequest<TypeOf<P>, TypeOf<Q>, TypeOf<B>, Method>, response: KibanaResponseFactory) => IKibanaResponse<any> | Promise<IKibanaResponse<any>>;
|
||||
export declare type RequestHandler<P = unknown, Q = unknown, B = unknown, Method extends RouteMethod = any> = (context: RequestHandlerContext, request: KibanaRequest<P, Q, B, Method>, response: KibanaResponseFactory) => IKibanaResponse<any> | Promise<IKibanaResponse<any>>;
|
||||
```
|
||||
|
||||
## Example
|
||||
|
|
|
@ -9,7 +9,7 @@ Route specific configuration.
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface RouteConfig<P extends ObjectType, Q extends ObjectType, B extends ObjectType | Type<Buffer> | Type<Stream>, Method extends RouteMethod>
|
||||
export interface RouteConfig<P, Q, B, Method extends RouteMethod>
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
@ -18,5 +18,5 @@ export interface RouteConfig<P extends ObjectType, Q extends ObjectType, B exten
|
|||
| --- | --- | --- |
|
||||
| [options](./kibana-plugin-server.routeconfig.options.md) | <code>RouteConfigOptions<Method></code> | Additional route options [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md)<!-- -->. |
|
||||
| [path](./kibana-plugin-server.routeconfig.path.md) | <code>string</code> | The endpoint \_within\_ the router path to register the route. |
|
||||
| [validate](./kibana-plugin-server.routeconfig.validate.md) | <code>RouteSchemas<P, Q, B> | false</code> | A schema created with <code>@kbn/config-schema</code> that every request will be validated against. |
|
||||
| [validate](./kibana-plugin-server.routeconfig.validate.md) | <code>RouteValidatorFullConfig<P, Q, B> | false</code> | A schema created with <code>@kbn/config-schema</code> that every request will be validated against. |
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ A schema created with `@kbn/config-schema` that every request will be validated
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
validate: RouteSchemas<P, Q, B> | false;
|
||||
validate: RouteValidatorFullConfig<P, Q, B> | false;
|
||||
```
|
||||
|
||||
## Remarks
|
||||
|
|
|
@ -9,5 +9,5 @@ Route handler common definition
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type RouteRegistrar<Method extends RouteMethod> = <P extends ObjectType, Q extends ObjectType, B extends ObjectType | Type<Buffer> | Type<Stream>>(route: RouteConfig<P, Q, B, Method>, handler: RequestHandler<P, Q, B, Method>) => void;
|
||||
export declare type RouteRegistrar<Method extends RouteMethod> = <P, Q, B>(route: RouteConfig<P, Q, B, Method>, handler: RequestHandler<P, Q, B, Method>) => void;
|
||||
```
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
body?: B;
|
||||
```
|
|
@ -1,22 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface RouteSchemas<P extends ObjectType, Q extends ObjectType, B extends ObjectType | Type<Buffer> | Type<Stream>>
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [body](./kibana-plugin-server.routeschemas.body.md) | <code>B</code> | |
|
||||
| [params](./kibana-plugin-server.routeschemas.params.md) | <code>P</code> | |
|
||||
| [query](./kibana-plugin-server.routeschemas.query.md) | <code>Q</code> | |
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
params?: P;
|
||||
```
|
|
@ -1,11 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
query?: Q;
|
||||
```
|
|
@ -0,0 +1,21 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
constructor(error: Error | string, path?: string[]);
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| error | <code>Error | string</code> | |
|
||||
| path | <code>string[]</code> | |
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```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 <code>RouteValidationError</code> class |
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type RouteValidationFunction<T> = (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<MyExpectedBody> = (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']);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
badRequest: (error: Error | string, path?: string[]) => {
|
||||
error: RouteValidationError;
|
||||
};
|
||||
```
|
|
@ -0,0 +1,23 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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)<!-- -->.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface RouteValidationResultFactory
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [badRequest](./kibana-plugin-server.routevalidationresultfactory.badrequest.md) | <code>(error: Error | string, path?: string[]) => {</code><br/><code> error: RouteValidationError;</code><br/><code> }</code> | |
|
||||
| [ok](./kibana-plugin-server.routevalidationresultfactory.ok.md) | <code><T>(value: T) => {</code><br/><code> value: T;</code><br/><code> }</code> | |
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
ok: <T>(value: T) => {
|
||||
value: T;
|
||||
};
|
||||
```
|
|
@ -0,0 +1,15 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type RouteValidationSpec<T> = ObjectType | Type<T> | RouteValidationFunction<T>;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
body?: RouteValidationSpec<B>;
|
||||
```
|
|
@ -0,0 +1,22 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface RouteValidatorConfig<P, Q, B>
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [body](./kibana-plugin-server.routevalidatorconfig.body.md) | <code>RouteValidationSpec<B></code> | Validation logic for the body payload |
|
||||
| [params](./kibana-plugin-server.routevalidatorconfig.params.md) | <code>RouteValidationSpec<P></code> | Validation logic for the URL params |
|
||||
| [query](./kibana-plugin-server.routevalidatorconfig.query.md) | <code>RouteValidationSpec<Q></code> | Validation logic for the Query params |
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
params?: RouteValidationSpec<P>;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
query?: RouteValidationSpec<Q>;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type RouteValidatorFullConfig<P, Q, B> = RouteValidatorConfig<P, Q, B> & RouteValidatorOptions;
|
||||
```
|
|
@ -0,0 +1,20 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface RouteValidatorOptions
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [unsafe](./kibana-plugin-server.routevalidatoroptions.unsafe.md) | <code>{</code><br/><code> params?: boolean;</code><br/><code> query?: boolean;</code><br/><code> body?: boolean;</code><br/><code> }</code> | Set the <code>unsafe</code> config to avoid running some additional internal \*safe\* validations on top of your custom validation |
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
unsafe?: {
|
||||
params?: boolean;
|
||||
query?: boolean;
|
||||
body?: boolean;
|
||||
};
|
||||
```
|
|
@ -59,6 +59,7 @@ import {
|
|||
|
||||
export { ObjectType, TypeOf, Type };
|
||||
export { ByteSizeValue } from './byte_size_value';
|
||||
export { SchemaTypeError, ValidationError } from './errors';
|
||||
|
||||
function any(options?: TypeOptions<any>) {
|
||||
return new AnyType(options);
|
||||
|
|
|
@ -77,7 +77,7 @@ function createKibanaRequestMock({
|
|||
body: schema.object({}, { allowUnknowns: true }),
|
||||
query: schema.object({}, { allowUnknowns: true }),
|
||||
}
|
||||
) as KibanaRequest<Readonly<{}>, Readonly<{}>, Readonly<{}>>;
|
||||
);
|
||||
}
|
||||
|
||||
type DeepPartial<T> = T extends any[]
|
||||
|
|
|
@ -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<unknown, unknown, { bar: string; baz: number }>,
|
||||
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<unknown, unknown, { bar: string; baz: number }> = (
|
||||
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);
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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('/');
|
||||
|
|
|
@ -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 = <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(
|
||||
export const wrapErrors = <P, Q, B>(
|
||||
handler: RequestHandler<P, Q, B, RouteMethod>
|
||||
): RequestHandler<P, Q, B, RouteMethod> => {
|
||||
return async (
|
||||
context: RequestHandlerContext,
|
||||
request: KibanaRequest<TypeOf<P>, TypeOf<Q>, TypeOf<B>, RouteMethod>,
|
||||
request: KibanaRequest<P, Q, B, RouteMethod>,
|
||||
response: KibanaResponseFactory
|
||||
) => {
|
||||
try {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<Buffer> | Type<Stream>
|
||||
>(req: Request, routeSchemas?: RouteSchemas<P, Q, B>, withoutSecretHeaders: boolean = true) {
|
||||
const requestParts = KibanaRequest.validate(req, routeSchemas);
|
||||
public static from<P, Q, B>(
|
||||
req: Request,
|
||||
routeSchemas: RouteValidator<P, Q, B> | RouteValidatorFullConfig<P, Q, B> = {},
|
||||
withoutSecretHeaders: boolean = true
|
||||
) {
|
||||
const routeValidator = RouteValidator.from<P, Q, B>(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<Buffer> | Type<Stream>
|
||||
>(
|
||||
private static validate<P, Q, B>(
|
||||
req: Request,
|
||||
routeSchemas: RouteSchemas<P, Q, B> | undefined
|
||||
routeValidator: RouteValidator<P, Q, B>
|
||||
): {
|
||||
params: TypeOf<P>;
|
||||
query: TypeOf<Q>;
|
||||
body: TypeOf<B>;
|
||||
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 };
|
||||
}
|
||||
|
|
|
@ -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<Method extends RouteMethod> {
|
|||
* Route specific configuration.
|
||||
* @public
|
||||
*/
|
||||
export interface RouteConfig<
|
||||
P extends ObjectType,
|
||||
Q extends ObjectType,
|
||||
B extends ObjectType | Type<Buffer> | Type<Stream>,
|
||||
Method extends RouteMethod
|
||||
> {
|
||||
export interface RouteConfig<P, Q, B, Method extends RouteMethod> {
|
||||
/**
|
||||
* The endpoint _within_ the router path to register the route.
|
||||
*
|
||||
|
@ -201,25 +195,10 @@ export interface RouteConfig<
|
|||
* });
|
||||
* ```
|
||||
*/
|
||||
validate: RouteSchemas<P, Q, B> | false;
|
||||
validate: RouteValidatorFullConfig<P, Q, B> | false;
|
||||
|
||||
/**
|
||||
* Additional route options {@link RouteConfigOptions}.
|
||||
*/
|
||||
options?: RouteConfigOptions<Method>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<Buffer> | Type<Stream>
|
||||
> {
|
||||
params?: P;
|
||||
query?: Q;
|
||||
body?: B;
|
||||
}
|
||||
|
|
|
@ -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]."`
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -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<Method extends RouteMethod> = <
|
||||
P extends ObjectType,
|
||||
Q extends ObjectType,
|
||||
B extends ObjectType | Type<Buffer> | Type<Stream>
|
||||
>(
|
||||
export type RouteRegistrar<Method extends RouteMethod> = <P, Q, B>(
|
||||
route: RouteConfig<P, Q, B, Method>,
|
||||
handler: RequestHandler<P, Q, B, Method>
|
||||
) => 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: <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(
|
||||
handler: RequestHandler<P, Q, B>
|
||||
) => RequestHandler<P, Q, B>;
|
||||
handleLegacyErrors: <P, Q, B>(handler: RequestHandler<P, Q, B>) => RequestHandler<P, Q, B>;
|
||||
|
||||
/**
|
||||
* 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<P, Q, B, Method>) => RequestHandlerEnhanced<P, Q, B, Method>;
|
||||
export type ContextEnhancer<P, Q, B, Method extends RouteMethod> = (
|
||||
handler: RequestHandler<P, Q, B, Method>
|
||||
) => RequestHandlerEnhanced<P, Q, B, Method>;
|
||||
|
||||
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<Buffer> | Type<Stream>
|
||||
>(route: RouteConfig<P, Q, B, typeof routeMethod>, routeMethod: RouteMethod) {
|
||||
function routeSchemasFromRouteConfig<P, Q, B>(
|
||||
route: RouteConfig<P, Q, B, typeof routeMethod>,
|
||||
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<Buffer> | Type<Stream>,
|
||||
typeof method
|
||||
>
|
||||
routeConfig: RouteConfig<unknown, unknown, unknown, typeof method>
|
||||
) {
|
||||
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<any, any, any, any>
|
||||
) {
|
||||
const buildMethod = <Method extends RouteMethod>(method: Method) => <
|
||||
P extends ObjectType,
|
||||
Q extends ObjectType,
|
||||
B extends ObjectType | Type<Buffer> | Type<Stream>
|
||||
>(
|
||||
const buildMethod = <Method extends RouteMethod>(method: Method) => <P, Q, B>(
|
||||
route: RouteConfig<P, Q, B, Method>,
|
||||
handler: RequestHandler<P, Q, B, Method>
|
||||
) => {
|
||||
|
@ -260,17 +237,11 @@ export class Router implements IRouter {
|
|||
return [...this.routes];
|
||||
}
|
||||
|
||||
public handleLegacyErrors<P extends ObjectType, Q extends ObjectType, B extends ObjectType>(
|
||||
handler: RequestHandler<P, Q, B>
|
||||
): RequestHandler<P, Q, B> {
|
||||
public handleLegacyErrors<P, Q, B>(handler: RequestHandler<P, Q, B>): RequestHandler<P, Q, B> {
|
||||
return wrapErrors(handler);
|
||||
}
|
||||
|
||||
private async handle<
|
||||
P extends ObjectType,
|
||||
Q extends ObjectType,
|
||||
B extends ObjectType | Type<Buffer> | Type<Stream>
|
||||
>({
|
||||
private async handle<P, Q, B>({
|
||||
routeSchemas,
|
||||
request,
|
||||
responseToolkit,
|
||||
|
@ -279,9 +250,9 @@ export class Router implements IRouter {
|
|||
request: Request;
|
||||
responseToolkit: ResponseToolkit;
|
||||
handler: RequestHandlerEnhanced<P, Q, B, typeof request.method>;
|
||||
routeSchemas?: RouteSchemas<P, Q, B>;
|
||||
routeSchemas?: RouteValidator<P, Q, B>;
|
||||
}) {
|
||||
let kibanaRequest: KibanaRequest<TypeOf<P>, TypeOf<Q>, TypeOf<B>, typeof request.method>;
|
||||
let kibanaRequest: KibanaRequest<P, Q, B, typeof request.method>;
|
||||
const hapiResponseAdapter = new HapiResponseAdapter(responseToolkit);
|
||||
try {
|
||||
kibanaRequest = KibanaRequest.from(request, routeSchemas);
|
||||
|
@ -303,12 +274,9 @@ type WithoutHeadArgument<T> = T extends (first: any, ...rest: infer Params) => i
|
|||
? (...rest: Params) => Return
|
||||
: never;
|
||||
|
||||
type RequestHandlerEnhanced<
|
||||
P extends ObjectType,
|
||||
Q extends ObjectType,
|
||||
B extends ObjectType | Type<Buffer> | Type<Stream>,
|
||||
Method extends RouteMethod
|
||||
> = WithoutHeadArgument<RequestHandler<P, Q, B, Method>>;
|
||||
type RequestHandlerEnhanced<P, Q, B, Method extends RouteMethod> = WithoutHeadArgument<
|
||||
RequestHandler<P, Q, B, Method>
|
||||
>;
|
||||
|
||||
/**
|
||||
* 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<Buffer> | Type<Stream>,
|
||||
P = unknown,
|
||||
Q = unknown,
|
||||
B = unknown,
|
||||
Method extends RouteMethod = any
|
||||
> = (
|
||||
context: RequestHandlerContext,
|
||||
request: KibanaRequest<TypeOf<P>, TypeOf<Q>, TypeOf<B>, Method>,
|
||||
request: KibanaRequest<P, Q, B, Method>,
|
||||
response: KibanaResponseFactory
|
||||
) => IKibanaResponse<any> | Promise<IKibanaResponse<any>>;
|
||||
|
|
29
src/core/server/http/router/validator/index.ts
Normal file
29
src/core/server/http/router/validator/index.ts
Normal file
|
@ -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';
|
135
src/core/server/http/router/validator/validator.test.ts
Normal file
135
src/core/server/http/router/validator/validator.test.ts
Normal file
|
@ -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: <T>(data: T): T => data } as Type<any>,
|
||||
});
|
||||
|
||||
expect(() => wrongValidateSpec.getParams({ foo: 1 })).toThrowError(
|
||||
'The validation rule provided in the handler is not valid'
|
||||
);
|
||||
});
|
||||
});
|
280
src/core/server/http/router/validator/validator.ts
Normal file
280
src/core/server/http/router/validator/validator.ts
Normal file
|
@ -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: <T>(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<MyExpectedBody> = (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<T> = (
|
||||
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<T> = ObjectType | Type<T> | RouteValidationFunction<T>;
|
||||
|
||||
// Ugly as hell but we need this conditional typing to have proper type inference
|
||||
type RouteValidationResultType<T extends RouteValidationSpec<any> | undefined> = NonNullable<
|
||||
T extends RouteValidationFunction<any>
|
||||
? ReturnType<T>['value']
|
||||
: T extends Type<any>
|
||||
? ReturnType<T['validate']>
|
||||
: 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<P, Q, B> {
|
||||
/**
|
||||
* Validation logic for the URL params
|
||||
* @public
|
||||
*/
|
||||
params?: RouteValidationSpec<P>;
|
||||
/**
|
||||
* Validation logic for the Query params
|
||||
* @public
|
||||
*/
|
||||
query?: RouteValidationSpec<Q>;
|
||||
/**
|
||||
* Validation logic for the body payload
|
||||
* @public
|
||||
*/
|
||||
body?: RouteValidationSpec<B>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<P, Q, B> = RouteValidatorConfig<P, Q, B> &
|
||||
RouteValidatorOptions;
|
||||
|
||||
/**
|
||||
* Route validator class to define the validation logic for each new route.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export class RouteValidator<P = {}, Q = {}, B = {}> {
|
||||
public static from<P = {}, Q = {}, B = {}>(
|
||||
opts: RouteValidator<P, Q, B> | RouteValidatorFullConfig<P, Q, B>
|
||||
) {
|
||||
if (opts instanceof RouteValidator) {
|
||||
return opts;
|
||||
}
|
||||
const { params, query, body, ...options } = opts;
|
||||
return new RouteValidator({ params, query, body }, options);
|
||||
}
|
||||
|
||||
private static ResultFactory: RouteValidationResultFactory = {
|
||||
ok: <T>(value: T) => ({ value }),
|
||||
badRequest: (error: Error | string, path?: string[]) => ({
|
||||
error: new RouteValidationError(error, path),
|
||||
}),
|
||||
};
|
||||
|
||||
private constructor(
|
||||
private readonly config: RouteValidatorConfig<P, Q, B>,
|
||||
private readonly options: RouteValidatorOptions = {}
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get validated URL params
|
||||
* @internal
|
||||
*/
|
||||
public getParams(data: unknown, namespace?: string): Readonly<P> {
|
||||
return this.validate(this.config.params, this.options.unsafe?.params, data, namespace);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get validated query params
|
||||
* @internal
|
||||
*/
|
||||
public getQuery(data: unknown, namespace?: string): Readonly<Q> {
|
||||
return this.validate(this.config.query, this.options.unsafe?.query, data, namespace);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get validated body
|
||||
* @internal
|
||||
*/
|
||||
public getBody(data: unknown, namespace?: string): Readonly<B> {
|
||||
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<T>(
|
||||
validationRule?: RouteValidationSpec<T>,
|
||||
unsafe?: boolean,
|
||||
data?: unknown,
|
||||
namespace?: string
|
||||
): RouteValidationResultType<typeof validationRule> {
|
||||
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<T>(data: T, namespace?: string): T {
|
||||
// We can add any pre-validation safety logic in here
|
||||
return data;
|
||||
}
|
||||
|
||||
private safetyPostchecks<T>(data: T, namespace?: string): T {
|
||||
// We can add any post-validation safety logic in here
|
||||
return data;
|
||||
}
|
||||
|
||||
private customValidation<T>(
|
||||
validationRule: RouteValidationSpec<T>,
|
||||
data?: unknown,
|
||||
namespace?: string
|
||||
): RouteValidationResultType<typeof validationRule> {
|
||||
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<T>(
|
||||
validateFn: RouteValidationFunction<T>,
|
||||
data: unknown,
|
||||
namespace?: string
|
||||
): T {
|
||||
let result: ReturnType<typeof validateFn>;
|
||||
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 })));
|
||||
}
|
||||
}
|
||||
}
|
34
src/core/server/http/router/validator/validator_error.ts
Normal file
34
src/core/server/http/router/validator/validator_error.ts
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -134,10 +134,16 @@ export {
|
|||
RouteRegistrar,
|
||||
RouteMethod,
|
||||
RouteConfigOptions,
|
||||
RouteSchemas,
|
||||
RouteConfigOptionsBody,
|
||||
RouteContentType,
|
||||
validBodyOutput,
|
||||
RouteValidatorConfig,
|
||||
RouteValidationSpec,
|
||||
RouteValidationFunction,
|
||||
RouteValidatorOptions,
|
||||
RouteValidatorFullConfig,
|
||||
RouteValidationResultFactory,
|
||||
RouteValidationError,
|
||||
SessionStorage,
|
||||
SessionStorageCookieOptions,
|
||||
SessionCookieValidationResult,
|
||||
|
|
|
@ -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: <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(handler: RequestHandler<P, Q, B>) => RequestHandler<P, Q, B>;
|
||||
handleLegacyErrors: <P, Q, B>(handler: RequestHandler<P, Q, B>) => RequestHandler<P, Q, B>;
|
||||
patch: RouteRegistrar<'patch'>;
|
||||
post: RouteRegistrar<'post'>;
|
||||
put: RouteRegistrar<'put'>;
|
||||
|
@ -841,8 +842,10 @@ export class KibanaRequest<Params = unknown, Query = unknown, Body = unknown, Me
|
|||
constructor(request: Request, params: Params, query: Query, body: Body, withoutSecretHeaders: boolean);
|
||||
// (undocumented)
|
||||
readonly body: Body;
|
||||
// Warning: (ae-forgotten-export) The symbol "RouteValidator" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// @internal
|
||||
static from<P extends ObjectType, Q extends ObjectType, B extends ObjectType | Type<Buffer> | Type<Stream>>(req: Request, routeSchemas?: RouteSchemas<P, Q, B>, withoutSecretHeaders?: boolean): KibanaRequest<P["type"], Q["type"], B["type"], any>;
|
||||
static from<P, Q, B>(req: Request, routeSchemas?: RouteValidator<P, Q, B> | RouteValidatorFullConfig<P, Q, B>, withoutSecretHeaders?: boolean): KibanaRequest<P, Q, B, any>;
|
||||
readonly headers: Headers;
|
||||
// (undocumented)
|
||||
readonly params: Params;
|
||||
|
@ -1159,7 +1162,7 @@ export type RedirectResponseOptions = HttpResponseOptions & {
|
|||
};
|
||||
|
||||
// @public
|
||||
export type RequestHandler<P extends ObjectType, Q extends ObjectType, B extends ObjectType | Type<Buffer> | Type<Stream>, Method extends RouteMethod = any> = (context: RequestHandlerContext, request: KibanaRequest<TypeOf<P>, TypeOf<Q>, TypeOf<B>, Method>, response: KibanaResponseFactory) => IKibanaResponse<any> | Promise<IKibanaResponse<any>>;
|
||||
export type RequestHandler<P = unknown, Q = unknown, B = unknown, Method extends RouteMethod = any> = (context: RequestHandlerContext, request: KibanaRequest<P, Q, B, Method>, response: KibanaResponseFactory) => IKibanaResponse<any> | Promise<IKibanaResponse<any>>;
|
||||
|
||||
// @public
|
||||
export interface RequestHandlerContext {
|
||||
|
@ -1201,10 +1204,10 @@ export type ResponseHeaders = {
|
|||
};
|
||||
|
||||
// @public
|
||||
export interface RouteConfig<P extends ObjectType, Q extends ObjectType, B extends ObjectType | Type<Buffer> | Type<Stream>, Method extends RouteMethod> {
|
||||
export interface RouteConfig<P, Q, B, Method extends RouteMethod> {
|
||||
options?: RouteConfigOptions<Method>;
|
||||
path: string;
|
||||
validate: RouteSchemas<P, Q, B> | false;
|
||||
validate: RouteValidatorFullConfig<P, Q, B> | 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<Method extends RouteMethod> = <P extends ObjectType, Q extends ObjectType, B extends ObjectType | Type<Buffer> | Type<Stream>>(route: RouteConfig<P, Q, B, Method>, handler: RequestHandler<P, Q, B, Method>) => void;
|
||||
export type RouteRegistrar<Method extends RouteMethod> = <P, Q, B>(route: RouteConfig<P, Q, B, Method>, handler: RequestHandler<P, Q, B, Method>) => void;
|
||||
|
||||
// @public
|
||||
export interface RouteSchemas<P extends ObjectType, Q extends ObjectType, B extends ObjectType | Type<Buffer> | Type<Stream>> {
|
||||
export class RouteValidationError extends SchemaTypeError {
|
||||
constructor(error: Error | string, path?: string[]);
|
||||
}
|
||||
|
||||
// @public
|
||||
export type RouteValidationFunction<T> = (data: any, validationResult: RouteValidationResultFactory) => {
|
||||
value: T;
|
||||
error?: never;
|
||||
} | {
|
||||
value?: never;
|
||||
error: RouteValidationError;
|
||||
};
|
||||
|
||||
// @public
|
||||
export interface RouteValidationResultFactory {
|
||||
// (undocumented)
|
||||
body?: B;
|
||||
badRequest: (error: Error | string, path?: string[]) => {
|
||||
error: RouteValidationError;
|
||||
};
|
||||
// (undocumented)
|
||||
params?: P;
|
||||
// (undocumented)
|
||||
query?: Q;
|
||||
ok: <T>(value: T) => {
|
||||
value: T;
|
||||
};
|
||||
}
|
||||
|
||||
// @public
|
||||
export type RouteValidationSpec<T> = ObjectType | Type<T> | RouteValidationFunction<T>;
|
||||
|
||||
// @public
|
||||
export interface RouteValidatorConfig<P, Q, B> {
|
||||
body?: RouteValidationSpec<B>;
|
||||
params?: RouteValidationSpec<P>;
|
||||
query?: RouteValidationSpec<Q>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type RouteValidatorFullConfig<P, Q, B> = RouteValidatorConfig<P, Q, B> & RouteValidatorOptions;
|
||||
|
||||
// @public
|
||||
export interface RouteValidatorOptions {
|
||||
unsafe?: {
|
||||
params?: boolean;
|
||||
query?: boolean;
|
||||
body?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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() {}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
import { SearchResponse, GenericParams } from 'elasticsearch';
|
||||
import { Lifecycle } from 'hapi';
|
||||
import { ObjectType } from '@kbn/config-schema';
|
||||
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
|
||||
import { RouteMethod, RouteConfig } from '../../../../../../../../src/core/server';
|
||||
import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../../plugins/features/server';
|
||||
|
@ -165,11 +164,6 @@ export interface InfraTSVBSeries {
|
|||
|
||||
export type InfraTSVBDataPoint = [number, number];
|
||||
|
||||
export type InfraRouteConfig<
|
||||
params extends ObjectType,
|
||||
query extends ObjectType,
|
||||
body extends ObjectType,
|
||||
method extends RouteMethod
|
||||
> = {
|
||||
export type InfraRouteConfig<params, query, body, method extends RouteMethod> = {
|
||||
method: RouteMethod;
|
||||
} & RouteConfig<params, query, body, method>;
|
||||
|
|
|
@ -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<params = any, query = any, body = any, method extends RouteMethod = any>(
|
||||
config: InfraRouteConfig<params, query, body, method>,
|
||||
handler: RequestHandler<params, query, body>
|
||||
) {
|
||||
|
|
|
@ -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<any>,
|
||||
Q extends ObjectType<any>,
|
||||
B extends ObjectType<any>
|
||||
>(
|
||||
export const catchErrorHandler: <P, Q, B>(
|
||||
fn: RequestHandler<P, Q, B>
|
||||
) => RequestHandler<P, Q, B> = fn => {
|
||||
return async (context, request, response) => {
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<any>,
|
||||
Q extends ObjectType<any>,
|
||||
B extends ObjectType<any>
|
||||
>(
|
||||
handler: RequestHandler<P, Q, B>
|
||||
) => {
|
||||
export const createLicensedRouteHandler = <P, Q, B>(handler: RequestHandler<P, Q, B>) => {
|
||||
const licensedRouteHandler: RequestHandler<P, Q, B> = (context, request, responseToolkit) => {
|
||||
const { license } = context.licensing;
|
||||
const licenseCheck = license.check('security', 'basic');
|
||||
|
|
|
@ -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!@#$%^&*()"`
|
||||
);
|
||||
|
|
|
@ -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<ObjectType, ObjectType, ObjectType>,
|
||||
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]"`
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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<ObjectType, ObjectType, ObjectType>,
|
||||
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',
|
||||
});
|
||||
|
||||
|
|
|
@ -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<ObjectType, ObjectType, ObjectType>,
|
||||
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',
|
||||
});
|
||||
|
||||
|
|
|
@ -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<any>,
|
||||
Q extends ObjectType<any>,
|
||||
B extends ObjectType<any>
|
||||
>(
|
||||
handler: RequestHandler<P, Q, B>
|
||||
) => {
|
||||
export const createLicensedRouteHandler = <P, Q, B>(handler: RequestHandler<P, Q, B>) => {
|
||||
const licensedRouteHandler: RequestHandler<P, Q, B> = (context, request, responseToolkit) => {
|
||||
const { license } = context.licensing;
|
||||
const licenseCheck = license.check('spaces', 'basic');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue