mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
* Add Auth session * add lifecycles * add types for hapi-auth-cookie * expose interceptors from http service * add integration tests * update tests * session storage cleanup * get SessionStorage type safe * add redirect, clear cookie security integration tests * add tests for onRequest * add tests for onAuth * register Auth interceptor only once * refactor redirect tests * fix typings, change error message, test suit naming * add integration test for session validation * add tests for cookie session storage * update docs * add integration tests for onRequest * update docs * cleanup onRequest integration tests * Generate docs for AuthToolkit & OnRequestToolkit * add test for an exception in interceptor * add test OnRequest interceptors dont share request object * cleanup * address comments from @eli * improve typings for onRequest * improve plugin typings * re-generate docs * only server defines cookie path * cookieOptions.password --> cookieOptions.encryptionKey * CookieOption --> SessionStorageCookieOptions * address comments @joshdover * resolve conflict leftovers * update @types/hapi-auth-cookie deps * update docs
This commit is contained in:
parent
4f574e1b4b
commit
69c5551d7d
63 changed files with 1815 additions and 27 deletions
|
@ -0,0 +1,10 @@
|
|||
[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthenticationHandler](./kibana-plugin-server.authenticationhandler.md)
|
||||
|
||||
## AuthenticationHandler type
|
||||
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type AuthenticationHandler<T> = (request: Request, sessionStorage: SessionStorage<T>, t: AuthToolkit) => Promise<AuthResult>;
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthToolkit](./kibana-plugin-server.authtoolkit.md) > [authenticated](./kibana-plugin-server.authtoolkit.authenticated.md)
|
||||
|
||||
## AuthToolkit.authenticated property
|
||||
|
||||
Authentication is successful with given credentials, allow request to pass through
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
authenticated: (credentials: any) => AuthResult;
|
||||
```
|
|
@ -0,0 +1,20 @@
|
|||
[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthToolkit](./kibana-plugin-server.authtoolkit.md)
|
||||
|
||||
## AuthToolkit interface
|
||||
|
||||
A tool set defining an outcome of Auth interceptor for incoming request.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface AuthToolkit
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [authenticated](./kibana-plugin-server.authtoolkit.authenticated.md) | <code>(credentials: any) => AuthResult</code> | Authentication is successful with given credentials, allow request to pass through |
|
||||
| [redirected](./kibana-plugin-server.authtoolkit.redirected.md) | <code>(url: string) => AuthResult</code> | Authentication requires to interrupt request handling and redirect to a configured url |
|
||||
| [rejected](./kibana-plugin-server.authtoolkit.rejected.md) | <code>(error: Error, options?: {`<p/>` statusCode?: number;`<p/>` }) => AuthResult</code> | Authentication is unsuccessful, fail the request with specified error. |
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthToolkit](./kibana-plugin-server.authtoolkit.md) > [redirected](./kibana-plugin-server.authtoolkit.redirected.md)
|
||||
|
||||
## AuthToolkit.redirected property
|
||||
|
||||
Authentication requires to interrupt request handling and redirect to a configured url
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
redirected: (url: string) => AuthResult;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthToolkit](./kibana-plugin-server.authtoolkit.md) > [rejected](./kibana-plugin-server.authtoolkit.rejected.md)
|
||||
|
||||
## AuthToolkit.rejected property
|
||||
|
||||
Authentication is unsuccessful, fail the request with specified error.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
rejected: (error: Error, options?: {
|
||||
statusCode?: number;
|
||||
}) => AuthResult;
|
||||
```
|
|
@ -0,0 +1,10 @@
|
|||
[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md)
|
||||
|
||||
## HttpServiceSetup type
|
||||
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type HttpServiceSetup = HttpServerInfo;
|
||||
```
|
|
@ -0,0 +1,9 @@
|
|||
[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [body](./kibana-plugin-server.kibanarequest.body.md)
|
||||
|
||||
## KibanaRequest.body property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
readonly body: Body;
|
||||
```
|
|
@ -0,0 +1,23 @@
|
|||
[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [from](./kibana-plugin-server.kibanarequest.from.md)
|
||||
|
||||
## KibanaRequest.from() method
|
||||
|
||||
Factory for creating requests. Validates the request before creating an instance of a KibanaRequest.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
static from<P extends ObjectType, Q extends ObjectType, B extends ObjectType>(req: Request, routeSchemas: RouteSchemas<P, Q, B> | undefined): KibanaRequest<P["type"], Q["type"], B["type"]>;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| req | <code>Request</code> | |
|
||||
| routeSchemas | <code>RouteSchemas<P, Q, B> | undefined</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`KibanaRequest<P["type"], Q["type"], B["type"]>`
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [getFilteredHeaders](./kibana-plugin-server.kibanarequest.getfilteredheaders.md)
|
||||
|
||||
## KibanaRequest.getFilteredHeaders() method
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
getFilteredHeaders(headersToKeep: string[]): Pick<Record<string, string | string[] | undefined>, string>;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| headersToKeep | <code>string[]</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`Pick<Record<string, string | string[] | undefined>, string>`
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [headers](./kibana-plugin-server.kibanarequest.headers.md)
|
||||
|
||||
## KibanaRequest.headers property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
readonly headers: Headers;
|
||||
```
|
|
@ -0,0 +1,28 @@
|
|||
[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md)
|
||||
|
||||
## KibanaRequest class
|
||||
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare class KibanaRequest<Params, Query, Body>
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Modifiers | Type | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| [body](./kibana-plugin-server.kibanarequest.body.md) | | <code>Body</code> | |
|
||||
| [headers](./kibana-plugin-server.kibanarequest.headers.md) | | <code>Headers</code> | |
|
||||
| [params](./kibana-plugin-server.kibanarequest.params.md) | | <code>Params</code> | |
|
||||
| [path](./kibana-plugin-server.kibanarequest.path.md) | | <code>string</code> | |
|
||||
| [query](./kibana-plugin-server.kibanarequest.query.md) | | <code>Query</code> | |
|
||||
|
||||
## Methods
|
||||
|
||||
| Method | Modifiers | Description |
|
||||
| --- | --- | --- |
|
||||
| [from(req, routeSchemas)](./kibana-plugin-server.kibanarequest.from.md) | <code>static</code> | Factory for creating requests. Validates the request before creating an instance of a KibanaRequest. |
|
||||
| [getFilteredHeaders(headersToKeep)](./kibana-plugin-server.kibanarequest.getfilteredheaders.md) | | |
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [params](./kibana-plugin-server.kibanarequest.params.md)
|
||||
|
||||
## KibanaRequest.params property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
readonly params: Params;
|
||||
```
|
|
@ -0,0 +1,9 @@
|
|||
[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [path](./kibana-plugin-server.kibanarequest.path.md)
|
||||
|
||||
## KibanaRequest.path property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
readonly path: string;
|
||||
```
|
|
@ -0,0 +1,9 @@
|
|||
[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [query](./kibana-plugin-server.kibanarequest.query.md)
|
||||
|
||||
## KibanaRequest.query property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
readonly query: Query;
|
||||
```
|
|
@ -8,18 +8,22 @@
|
|||
| --- | --- |
|
||||
| [ClusterClient](./kibana-plugin-server.clusterclient.md) | Represents an Elasticsearch cluster API client and allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via <code>asScoped(...)</code>). |
|
||||
| [ConfigService](./kibana-plugin-server.configservice.md) | |
|
||||
| [KibanaRequest](./kibana-plugin-server.kibanarequest.md) | |
|
||||
| [Router](./kibana-plugin-server.router.md) | |
|
||||
| [ScopedClusterClient](./kibana-plugin-server.scopedclusterclient.md) | Serves the same purpose as "normal" <code>ClusterClient</code> but exposes additional <code>callAsCurrentUser</code> method that doesn't use credentials of the Kibana internal user (as <code>callAsInternalUser</code> does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API |
|
||||
|
||||
## Interfaces
|
||||
|
||||
| Interface | Description |
|
||||
| --- | --- |
|
||||
| [AuthToolkit](./kibana-plugin-server.authtoolkit.md) | A tool set defining an outcome of Auth interceptor for incoming request. |
|
||||
| [CallAPIOptions](./kibana-plugin-server.callapioptions.md) | The set of options that defines how API call should be made and result be processed. |
|
||||
| [CoreSetup](./kibana-plugin-server.coresetup.md) | |
|
||||
| [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) | |
|
||||
| [Logger](./kibana-plugin-server.logger.md) | Logger exposes all the necessary methods to log any type of information and this is the interface used by the logging consumers including plugins. |
|
||||
| [LoggerFactory](./kibana-plugin-server.loggerfactory.md) | The single purpose of <code>LoggerFactory</code> interface is to define a way to retrieve a context-based logger instance. |
|
||||
| [LogMeta](./kibana-plugin-server.logmeta.md) | Contextual metadata |
|
||||
| [OnRequestToolkit](./kibana-plugin-server.onrequesttoolkit.md) | A tool set defining an outcome of OnRequest interceptor for incoming request. |
|
||||
| [Plugin](./kibana-plugin-server.plugin.md) | The interface that should be returned by a <code>PluginInitializer</code>. |
|
||||
| [PluginInitializerContext](./kibana-plugin-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. |
|
||||
| [PluginSetupContext](./kibana-plugin-server.pluginsetupcontext.md) | Context passed to the plugins <code>setup</code> method. |
|
||||
|
@ -29,8 +33,11 @@
|
|||
| Type Alias | Description |
|
||||
| --- | --- |
|
||||
| [APICaller](./kibana-plugin-server.apicaller.md) | |
|
||||
| [AuthenticationHandler](./kibana-plugin-server.authenticationhandler.md) | |
|
||||
| [ElasticsearchClientConfig](./kibana-plugin-server.elasticsearchclientconfig.md) | |
|
||||
| [Headers](./kibana-plugin-server.headers.md) | |
|
||||
| [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) | |
|
||||
| [OnRequestHandler](./kibana-plugin-server.onrequesthandler.md) | |
|
||||
| [PluginInitializer](./kibana-plugin-server.plugininitializer.md) | The <code>plugin</code> export at the root of a plugin's <code>server</code> directory should conform to this interface. |
|
||||
| [PluginName](./kibana-plugin-server.pluginname.md) | Dedicated type for plugin name/id that is supposed to make Map/Set/Arrays that use it as a key or value more obvious. |
|
||||
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnRequestHandler](./kibana-plugin-server.onrequesthandler.md)
|
||||
|
||||
## OnRequestHandler type
|
||||
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type OnRequestHandler<Params = any, Query = any, Body = any> = (req: KibanaRequest<Params, Query, Body>, t: OnRequestToolkit) => OnRequestResult | Promise<OnRequestResult>;
|
||||
```
|
|
@ -0,0 +1,20 @@
|
|||
[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnRequestToolkit](./kibana-plugin-server.onrequesttoolkit.md)
|
||||
|
||||
## OnRequestToolkit interface
|
||||
|
||||
A tool set defining an outcome of OnRequest interceptor for incoming request.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface OnRequestToolkit
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [next](./kibana-plugin-server.onrequesttoolkit.next.md) | <code>() => OnRequestResult</code> | To pass request to the next handler |
|
||||
| [redirected](./kibana-plugin-server.onrequesttoolkit.redirected.md) | <code>(url: string) => OnRequestResult</code> | To interrupt request handling and redirect to a configured url |
|
||||
| [rejected](./kibana-plugin-server.onrequesttoolkit.rejected.md) | <code>(error: Error, options?: {`<p/>` statusCode?: number;`<p/>` }) => OnRequestResult</code> | Fail the request with specified error. |
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnRequestToolkit](./kibana-plugin-server.onrequesttoolkit.md) > [next](./kibana-plugin-server.onrequesttoolkit.next.md)
|
||||
|
||||
## OnRequestToolkit.next property
|
||||
|
||||
To pass request to the next handler
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
next: () => OnRequestResult;
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnRequestToolkit](./kibana-plugin-server.onrequesttoolkit.md) > [redirected](./kibana-plugin-server.onrequesttoolkit.redirected.md)
|
||||
|
||||
## OnRequestToolkit.redirected property
|
||||
|
||||
To interrupt request handling and redirect to a configured url
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
redirected: (url: string) => OnRequestResult;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnRequestToolkit](./kibana-plugin-server.onrequesttoolkit.md) > [rejected](./kibana-plugin-server.onrequesttoolkit.rejected.md)
|
||||
|
||||
## OnRequestToolkit.rejected property
|
||||
|
||||
Fail the request with specified error.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
rejected: (error: Error, options?: {
|
||||
statusCode?: number;
|
||||
}) => OnRequestResult;
|
||||
```
|
|
@ -0,0 +1,12 @@
|
|||
[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginSetupContext](./kibana-plugin-server.pluginsetupcontext.md) > [http](./kibana-plugin-server.pluginsetupcontext.http.md)
|
||||
|
||||
## PluginSetupContext.http property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
http: {
|
||||
registerAuth: HttpServiceSetup['registerAuth'];
|
||||
registerOnRequest: HttpServiceSetup['registerOnRequest'];
|
||||
};
|
||||
```
|
|
@ -15,4 +15,5 @@ export interface PluginSetupContext
|
|||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [elasticsearch](./kibana-plugin-server.pluginsetupcontext.elasticsearch.md) | <code>{`<p/>` adminClient$: Observable<ClusterClient>;`<p/>` dataClient$: Observable<ClusterClient>;`<p/>` }</code> | |
|
||||
| [http](./kibana-plugin-server.pluginsetupcontext.http.md) | <code>{`<p/>` registerAuth: HttpServiceSetup['registerAuth'];`<p/>` registerOnRequest: HttpServiceSetup['registerOnRequest'];`<p/>` }</code> | |
|
||||
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [delete](./kibana-plugin-server.router.delete.md)
|
||||
|
||||
## Router.delete() method
|
||||
|
||||
Register a `DELETE` request with the router
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
delete<P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>): void;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| route | <code>RouteConfig<P, Q, B></code> | |
|
||||
| handler | <code>RequestHandler<P, Q, B></code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`void`
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [get](./kibana-plugin-server.router.get.md)
|
||||
|
||||
## Router.get() method
|
||||
|
||||
Register a `GET` request with the router
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
get<P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>): void;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| route | <code>RouteConfig<P, Q, B></code> | |
|
||||
| handler | <code>RequestHandler<P, Q, B></code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`void`
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [getRoutes](./kibana-plugin-server.router.getroutes.md)
|
||||
|
||||
## Router.getRoutes() method
|
||||
|
||||
Returns all routes registered with the this router.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
getRoutes(): Readonly<RouterRoute>[];
|
||||
```
|
||||
<b>Returns:</b>
|
||||
|
||||
`Readonly<RouterRoute>[]`
|
||||
|
||||
List of registered routes.
|
||||
|
28
docs/development/core/server/kibana-plugin-server.router.md
Normal file
28
docs/development/core/server/kibana-plugin-server.router.md
Normal file
|
@ -0,0 +1,28 @@
|
|||
[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md)
|
||||
|
||||
## Router class
|
||||
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare class Router
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Modifiers | Type | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| [path](./kibana-plugin-server.router.path.md) | | <code>string</code> | |
|
||||
| [routes](./kibana-plugin-server.router.routes.md) | | <code>Array<Readonly<RouterRoute>></code> | |
|
||||
|
||||
## Methods
|
||||
|
||||
| Method | Modifiers | Description |
|
||||
| --- | --- | --- |
|
||||
| [delete(route, handler)](./kibana-plugin-server.router.delete.md) | | Register a <code>DELETE</code> request with the router |
|
||||
| [get(route, handler)](./kibana-plugin-server.router.get.md) | | Register a <code>GET</code> request with the router |
|
||||
| [getRoutes()](./kibana-plugin-server.router.getroutes.md) | | Returns all routes registered with the this router. |
|
||||
| [post(route, handler)](./kibana-plugin-server.router.post.md) | | Register a <code>POST</code> request with the router |
|
||||
| [put(route, handler)](./kibana-plugin-server.router.put.md) | | Register a <code>PUT</code> request with the router |
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [path](./kibana-plugin-server.router.path.md)
|
||||
|
||||
## Router.path property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
readonly path: string;
|
||||
```
|
|
@ -0,0 +1,23 @@
|
|||
[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [post](./kibana-plugin-server.router.post.md)
|
||||
|
||||
## Router.post() method
|
||||
|
||||
Register a `POST` request with the router
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
post<P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>): void;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| route | <code>RouteConfig<P, Q, B></code> | |
|
||||
| handler | <code>RequestHandler<P, Q, B></code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`void`
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [put](./kibana-plugin-server.router.put.md)
|
||||
|
||||
## Router.put() method
|
||||
|
||||
Register a `PUT` request with the router
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
put<P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>): void;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| route | <code>RouteConfig<P, Q, B></code> | |
|
||||
| handler | <code>RequestHandler<P, Q, B></code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
`void`
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [routes](./kibana-plugin-server.router.routes.md)
|
||||
|
||||
## Router.routes property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
routes: Array<Readonly<RouterRoute>>;
|
||||
```
|
|
@ -77,6 +77,7 @@
|
|||
},
|
||||
"resolutions": {
|
||||
"**/@types/node": "10.12.27",
|
||||
"**/@types/hapi": "^17.0.18",
|
||||
"**/typescript": "^3.3.3333"
|
||||
},
|
||||
"workspaces": {
|
||||
|
@ -285,6 +286,7 @@
|
|||
"@types/globby": "^8.0.0",
|
||||
"@types/graphql": "^0.13.1",
|
||||
"@types/hapi": "^17.0.18",
|
||||
"@types/hapi-auth-cookie": "^9.1.0",
|
||||
"@types/has-ansi": "^3.0.0",
|
||||
"@types/hoek": "^4.1.3",
|
||||
"@types/humps": "^1.1.2",
|
||||
|
@ -312,6 +314,7 @@
|
|||
"@types/react-virtualized": "^9.18.7",
|
||||
"@types/redux": "^3.6.31",
|
||||
"@types/redux-actions": "^2.2.1",
|
||||
"@types/request": "^2.48.1",
|
||||
"@types/rimraf": "^2.0.2",
|
||||
"@types/semver": "^5.5.0",
|
||||
"@types/sinon": "^5.0.1",
|
||||
|
|
78
src/core/server/http/cookie_session_storage.ts
Normal file
78
src/core/server/http/cookie_session_storage.ts
Normal file
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* 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 { Request, Server } from 'hapi';
|
||||
import hapiAuthCookie from 'hapi-auth-cookie';
|
||||
import { SessionStorageFactory, SessionStorage } from './session_storage';
|
||||
|
||||
export interface SessionStorageCookieOptions<T> {
|
||||
name: string;
|
||||
encryptionKey: string;
|
||||
validate: (sessionValue: T) => boolean | Promise<boolean>;
|
||||
isSecure: boolean;
|
||||
}
|
||||
|
||||
class ScopedCookieSessionStorage<T extends Record<string, any>> implements SessionStorage<T> {
|
||||
constructor(private readonly server: Server, private readonly request: Request) {}
|
||||
public async get(): Promise<T | null> {
|
||||
try {
|
||||
return await this.server.auth.test('security-cookie', this.request);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
public set(sessionValue: T) {
|
||||
return this.request.cookieAuth.set(sessionValue);
|
||||
}
|
||||
public clear() {
|
||||
return this.request.cookieAuth.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates SessionStorage factory, which abstract the way of
|
||||
* session storage implementation and scoping to the incoming requests.
|
||||
*
|
||||
* @param server - hapi server to create SessionStorage for
|
||||
* @param cookieOptions - cookies configuration
|
||||
*/
|
||||
export async function createCookieSessionStorageFactory<T>(
|
||||
server: Server,
|
||||
cookieOptions: SessionStorageCookieOptions<T>,
|
||||
basePath?: string
|
||||
): Promise<SessionStorageFactory<T>> {
|
||||
await server.register({ plugin: hapiAuthCookie });
|
||||
|
||||
server.auth.strategy('security-cookie', 'cookie', {
|
||||
cookie: cookieOptions.name,
|
||||
password: cookieOptions.encryptionKey,
|
||||
validateFunc: async (req, session: T) => ({ valid: await cookieOptions.validate(session) }),
|
||||
isSecure: cookieOptions.isSecure,
|
||||
path: basePath,
|
||||
clearInvalid: true,
|
||||
isHttpOnly: true,
|
||||
isSameSite: false,
|
||||
});
|
||||
|
||||
return {
|
||||
asScoped(request: Request) {
|
||||
return new ScopedCookieSessionStorage<T>(server, request);
|
||||
},
|
||||
};
|
||||
}
|
223
src/core/server/http/cookie_sesson_storage.test.ts
Normal file
223
src/core/server/http/cookie_sesson_storage.test.ts
Normal file
|
@ -0,0 +1,223 @@
|
|||
/*
|
||||
* 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 { Server } from 'hapi';
|
||||
import request from 'request';
|
||||
|
||||
import { createCookieSessionStorageFactory } from './cookie_session_storage';
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
roles?: string[];
|
||||
}
|
||||
|
||||
interface Storage {
|
||||
value: User;
|
||||
expires: number;
|
||||
}
|
||||
|
||||
function retrieveSessionCookie(cookies: string) {
|
||||
const sessionCookie = request.cookie(cookies);
|
||||
if (!sessionCookie) {
|
||||
throw new Error('session cookie expected to be defined');
|
||||
}
|
||||
return sessionCookie;
|
||||
}
|
||||
|
||||
const userData = { id: '42' };
|
||||
const sessionDurationMs = 30;
|
||||
const delay = (ms: number) => new Promise(res => setTimeout(res, ms));
|
||||
const cookieOptions = {
|
||||
name: 'sid',
|
||||
encryptionKey: 'something_at_least_32_characters',
|
||||
validate: (session: Storage) => session.expires > Date.now(),
|
||||
isSecure: false,
|
||||
path: '/',
|
||||
};
|
||||
|
||||
describe('Cookie based SessionStorage', () => {
|
||||
describe('#set()', () => {
|
||||
it('Should write to session storage & set cookies', async () => {
|
||||
const server = new Server();
|
||||
const factory = await createCookieSessionStorageFactory(server, cookieOptions);
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: '/set',
|
||||
options: {
|
||||
handler: (req, h) => {
|
||||
const sessionStorage = factory.asScoped(req);
|
||||
sessionStorage.set({ value: userData, expires: Date.now() + sessionDurationMs });
|
||||
return h.response();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const response = await server.inject('/set');
|
||||
expect(response.statusCode).toBe(200);
|
||||
|
||||
const cookies = response.headers['set-cookie'];
|
||||
expect(cookies).toBeDefined();
|
||||
expect(cookies).toHaveLength(1);
|
||||
|
||||
const sessionCookie = retrieveSessionCookie(cookies[0]);
|
||||
expect(sessionCookie).toBeDefined();
|
||||
expect(sessionCookie.key).toBe('sid');
|
||||
expect(sessionCookie.value).toBeDefined();
|
||||
expect(sessionCookie.path).toBe('/');
|
||||
expect(sessionCookie.httpOnly).toBe(true);
|
||||
});
|
||||
});
|
||||
describe('#get()', () => {
|
||||
it('Should read from session storage', async () => {
|
||||
const server = new Server();
|
||||
const factory = await createCookieSessionStorageFactory(server, cookieOptions);
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: '/get',
|
||||
options: {
|
||||
handler: async (req, h) => {
|
||||
const sessionStorage = factory.asScoped(req);
|
||||
const sessionValue = await sessionStorage.get();
|
||||
if (!sessionValue) {
|
||||
sessionStorage.set({ value: userData, expires: Date.now() + sessionDurationMs });
|
||||
return h.response();
|
||||
}
|
||||
return h.response(sessionValue.value);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const response = await server.inject('/get');
|
||||
expect(response.statusCode).toBe(200);
|
||||
|
||||
const cookies = response.headers['set-cookie'];
|
||||
expect(cookies).toBeDefined();
|
||||
expect(cookies).toHaveLength(1);
|
||||
|
||||
const sessionCookie = retrieveSessionCookie(cookies[0]);
|
||||
|
||||
const response2 = await server.inject({
|
||||
method: 'GET',
|
||||
url: '/get',
|
||||
headers: { cookie: `${sessionCookie.key}=${sessionCookie.value}` },
|
||||
});
|
||||
expect(response2.statusCode).toBe(200);
|
||||
expect(response2.result).toEqual(userData);
|
||||
});
|
||||
it('Should return null for empty session', async () => {
|
||||
const server = new Server();
|
||||
const factory = await createCookieSessionStorageFactory(server, cookieOptions);
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: '/get-empty',
|
||||
options: {
|
||||
handler: async (req, h) => {
|
||||
const sessionStorage = factory.asScoped(req);
|
||||
const sessionValue = await sessionStorage.get();
|
||||
return h.response(JSON.stringify(sessionValue));
|
||||
},
|
||||
},
|
||||
});
|
||||
const response = await server.inject('/get-empty');
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.result).toBe('null');
|
||||
|
||||
const cookies = response.headers['set-cookie'];
|
||||
expect(cookies).not.toBeDefined();
|
||||
});
|
||||
it('Should return null for invalid session & clean cookies', async () => {
|
||||
const server = new Server();
|
||||
const factory = await createCookieSessionStorageFactory(server, cookieOptions);
|
||||
let setOnce = false;
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: '/get-invalid',
|
||||
options: {
|
||||
handler: async (req, h) => {
|
||||
const sessionStorage = factory.asScoped(req);
|
||||
if (!setOnce) {
|
||||
setOnce = true;
|
||||
sessionStorage.set({ value: userData, expires: Date.now() + sessionDurationMs });
|
||||
return h.response();
|
||||
}
|
||||
const sessionValue = await sessionStorage.get();
|
||||
return h.response(JSON.stringify(sessionValue));
|
||||
},
|
||||
},
|
||||
});
|
||||
const response = await server.inject('/get-invalid');
|
||||
expect(response.statusCode).toBe(200);
|
||||
|
||||
const cookies = response.headers['set-cookie'];
|
||||
expect(cookies).toBeDefined();
|
||||
|
||||
await delay(sessionDurationMs);
|
||||
|
||||
const sessionCookie = retrieveSessionCookie(cookies[0]);
|
||||
const response2 = await server.inject({
|
||||
method: 'GET',
|
||||
url: '/get-invalid',
|
||||
headers: { cookie: `${sessionCookie.key}=${sessionCookie.value}` },
|
||||
});
|
||||
expect(response2.statusCode).toBe(200);
|
||||
expect(response2.result).toBe('null');
|
||||
|
||||
const cookies2 = response2.headers['set-cookie'];
|
||||
expect(cookies2).toEqual([
|
||||
'sid=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Path=/',
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('#clear()', () => {
|
||||
it('Should clear session storage & remove cookies', async () => {
|
||||
const server = new Server();
|
||||
const factory = await createCookieSessionStorageFactory(server, cookieOptions);
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: '/clear',
|
||||
options: {
|
||||
handler: async (req, h) => {
|
||||
const sessionStorage = factory.asScoped(req);
|
||||
if (await sessionStorage.get()) {
|
||||
sessionStorage.clear();
|
||||
return h.response();
|
||||
}
|
||||
sessionStorage.set({ value: userData, expires: Date.now() + sessionDurationMs });
|
||||
return h.response();
|
||||
},
|
||||
},
|
||||
});
|
||||
const response = await server.inject('/clear');
|
||||
const cookies = response.headers['set-cookie'];
|
||||
|
||||
const sessionCookie = retrieveSessionCookie(cookies[0]);
|
||||
|
||||
const response2 = await server.inject({
|
||||
method: 'GET',
|
||||
url: '/clear',
|
||||
headers: { cookie: `${sessionCookie.key}=${sessionCookie.value}` },
|
||||
});
|
||||
expect(response2.statusCode).toBe(200);
|
||||
|
||||
const cookies2 = response2.headers['set-cookie'];
|
||||
expect(cookies2).toEqual([
|
||||
'sid=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Path=/',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -571,3 +571,22 @@ test('returns server and connection options on start', async () => {
|
|||
expect(innerServer).toBe((server as any).server);
|
||||
expect(options).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('registers auth request interceptor only once', async () => {
|
||||
const { registerAuth } = await server.start(config);
|
||||
const doRegister = () =>
|
||||
registerAuth(() => null as any, {
|
||||
encryptionKey: 'any_password',
|
||||
} as any);
|
||||
|
||||
await doRegister();
|
||||
expect(doRegister()).rejects.toThrowError('Auth interceptor was already registered');
|
||||
});
|
||||
|
||||
test('registers onRequest interceptor several times', async () => {
|
||||
const { registerOnRequest } = await server.start(config);
|
||||
const doRegister = () => registerOnRequest(() => null as any);
|
||||
|
||||
doRegister();
|
||||
expect(doRegister).not.toThrowError();
|
||||
});
|
||||
|
|
|
@ -23,16 +23,37 @@ import { modifyUrl } from '../../utils';
|
|||
import { Logger } from '../logging';
|
||||
import { HttpConfig } from './http_config';
|
||||
import { createServer, getServerOptions } from './http_tools';
|
||||
import { adoptToHapiAuthFormat, AuthenticationHandler } from './lifecycle/auth';
|
||||
import { adoptToHapiOnRequestFormat, OnRequestHandler } from './lifecycle/on_request';
|
||||
import { Router } from './router';
|
||||
import {
|
||||
SessionStorageCookieOptions,
|
||||
createCookieSessionStorageFactory,
|
||||
} from './cookie_session_storage';
|
||||
|
||||
export interface HttpServerInfo {
|
||||
server: Server;
|
||||
options: ServerOptions;
|
||||
/**
|
||||
* Define custom authentication and/or authorization mechanism for incoming requests.
|
||||
* Applied to all resources by default. Only one AuthenticationHandler can be registered.
|
||||
*/
|
||||
registerAuth: <T>(
|
||||
authenticationHandler: AuthenticationHandler<T>,
|
||||
cookieOptions: SessionStorageCookieOptions<T>
|
||||
) => void;
|
||||
/**
|
||||
* Define custom logic to perform for incoming requests.
|
||||
* Applied to all resources by default.
|
||||
* Can register any number of OnRequestHandlers, which are called in sequence (from the first registered to the last)
|
||||
*/
|
||||
registerOnRequest: (requestHandler: OnRequestHandler) => void;
|
||||
}
|
||||
|
||||
export class HttpServer {
|
||||
private server?: Server;
|
||||
private registeredRouters: Set<Router> = new Set();
|
||||
private registeredRouters = new Set<Router>();
|
||||
private authRegistered = false;
|
||||
|
||||
constructor(private readonly log: Logger) {}
|
||||
|
||||
|
@ -48,7 +69,7 @@ export class HttpServer {
|
|||
this.registeredRouters.add(router);
|
||||
}
|
||||
|
||||
public async start(config: HttpConfig) {
|
||||
public async start(config: HttpConfig): Promise<HttpServerInfo> {
|
||||
this.log.debug('starting http server');
|
||||
|
||||
const serverOptions = getServerOptions(config);
|
||||
|
@ -77,7 +98,15 @@ export class HttpServer {
|
|||
// Return server instance with the connection options so that we can properly
|
||||
// bridge core and the "legacy" Kibana internally. Once this bridge isn't
|
||||
// needed anymore we shouldn't return anything from this method.
|
||||
return { server: this.server, options: serverOptions };
|
||||
return {
|
||||
server: this.server,
|
||||
options: serverOptions,
|
||||
registerOnRequest: this.registerOnRequest.bind(this),
|
||||
registerAuth: <T>(
|
||||
fn: AuthenticationHandler<T>,
|
||||
cookieOptions: SessionStorageCookieOptions<T>
|
||||
) => this.registerAuth(fn, cookieOptions, config.basePath),
|
||||
};
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
|
@ -127,4 +156,43 @@ export class HttpServer {
|
|||
const routePathStartIndex = routerPath.endsWith('/') && routePath.startsWith('/') ? 1 : 0;
|
||||
return `${routerPath}${routePath.slice(routePathStartIndex)}`;
|
||||
}
|
||||
|
||||
private registerOnRequest(fn: OnRequestHandler) {
|
||||
if (this.server === undefined) {
|
||||
throw new Error('Server is not created yet');
|
||||
}
|
||||
|
||||
this.server.ext('onRequest', adoptToHapiOnRequestFormat(fn));
|
||||
}
|
||||
|
||||
private async registerAuth<T>(
|
||||
fn: AuthenticationHandler<T>,
|
||||
cookieOptions: SessionStorageCookieOptions<T>,
|
||||
basePath?: string
|
||||
) {
|
||||
if (this.server === undefined) {
|
||||
throw new Error('Server is not created yet');
|
||||
}
|
||||
if (this.authRegistered) {
|
||||
throw new Error('Auth interceptor was already registered');
|
||||
}
|
||||
this.authRegistered = true;
|
||||
|
||||
const sessionStorage = await createCookieSessionStorageFactory<T>(
|
||||
this.server,
|
||||
cookieOptions,
|
||||
basePath
|
||||
);
|
||||
|
||||
this.server.auth.scheme('login', () => ({
|
||||
authenticate: adoptToHapiAuthFormat(fn, sessionStorage),
|
||||
}));
|
||||
this.server.auth.strategy('session', 'login');
|
||||
|
||||
// The default means that the `session` strategy that is based on `login` schema defined above will be
|
||||
// automatically assigned to all routes that don't contain an auth config.
|
||||
// should be applied for all routes if they don't specify auth strategy in route declaration
|
||||
// https://github.com/hapijs/hapi/blob/master/API.md#-serverauthdefaultoptions
|
||||
this.server.auth.default('session');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,8 @@ const createSetupContractMock = () => {
|
|||
// we can mock some hapi server method when we need it
|
||||
server: {} as Server,
|
||||
options: {} as ServerOptions,
|
||||
registerAuth: jest.fn(),
|
||||
registerOnRequest: jest.fn(),
|
||||
};
|
||||
return setupContract;
|
||||
};
|
||||
|
|
|
@ -27,7 +27,7 @@ import { HttpServer, HttpServerInfo } from './http_server';
|
|||
import { HttpsRedirectServer } from './https_redirect_server';
|
||||
import { Router } from './router';
|
||||
|
||||
/** @internal */
|
||||
/** @public */
|
||||
export type HttpServiceSetup = HttpServerInfo;
|
||||
|
||||
/** @internal */
|
||||
|
|
|
@ -22,3 +22,5 @@ export { HttpService, HttpServiceSetup } from './http_service';
|
|||
export { Router, KibanaRequest } from './router';
|
||||
export { HttpServerInfo } from './http_server';
|
||||
export { BasePathProxyServer } from './base_path_proxy_server';
|
||||
export { AuthenticationHandler, AuthToolkit } from './lifecycle/auth';
|
||||
export { OnRequestHandler, OnRequestToolkit } from './lifecycle/on_request';
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"id": "dummy-on-request",
|
||||
"version": "0.0.1",
|
||||
"kibanaVersion": "kibana",
|
||||
"ui": false,
|
||||
"server": true
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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 { DummyOnRequestPlugin } from './plugin';
|
||||
export const plugin = () => new DummyOnRequestPlugin();
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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 { CoreSetup } from '../../../../../..';
|
||||
|
||||
export const url = {
|
||||
exception: '/exception',
|
||||
failed: '/failed',
|
||||
independentReq: '/independent-request',
|
||||
root: '/',
|
||||
redirect: '/redirect',
|
||||
redirectTo: '/redirect-to',
|
||||
};
|
||||
|
||||
export class DummyOnRequestPlugin {
|
||||
public setup(core: CoreSetup) {
|
||||
core.http.registerOnRequest(async (request, t) => {
|
||||
await Promise.resolve();
|
||||
if (request.path === url.redirect) {
|
||||
return t.redirected(url.redirectTo);
|
||||
}
|
||||
return t.next();
|
||||
});
|
||||
|
||||
core.http.registerOnRequest((request, t) => {
|
||||
if (request.path === url.failed) {
|
||||
return t.rejected(new Error('unexpected error'), { statusCode: 400 });
|
||||
}
|
||||
return t.next();
|
||||
});
|
||||
|
||||
core.http.registerOnRequest((request, t) => {
|
||||
if (request.path === url.exception) {
|
||||
throw new Error('sensitive info');
|
||||
}
|
||||
return t.next();
|
||||
});
|
||||
|
||||
core.http.registerOnRequest((request, t) => {
|
||||
if (request.path === url.independentReq) {
|
||||
// @ts-ignore. don't complain customField is not defined on Request type
|
||||
request.customField = { value: 42 };
|
||||
}
|
||||
return t.next();
|
||||
});
|
||||
|
||||
core.http.registerOnRequest((request, t) => {
|
||||
if (
|
||||
request.path === url.independentReq &&
|
||||
// @ts-ignore don't complain customField is not defined on Request type
|
||||
typeof request.customField !== 'undefined'
|
||||
) {
|
||||
throw new Error('Request object was mutated');
|
||||
}
|
||||
return t.next();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"id": "dummy-security",
|
||||
"version": "0.0.1",
|
||||
"kibanaVersion": "kibana",
|
||||
"ui": false,
|
||||
"server": true
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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 { DummySecurityPlugin } from './plugin';
|
||||
export const plugin = () => new DummySecurityPlugin();
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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 Boom from 'boom';
|
||||
import { AuthenticationHandler, CoreSetup } from '../../../../../../../../core/server';
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
roles?: string[];
|
||||
}
|
||||
|
||||
interface Storage {
|
||||
value: User;
|
||||
expires: number;
|
||||
}
|
||||
|
||||
export const url = {
|
||||
auth: '/auth',
|
||||
authRedirect: '/auth/redirect',
|
||||
exception: '/exception',
|
||||
redirectTo: '/login',
|
||||
};
|
||||
|
||||
export const sessionDurationMs = 30;
|
||||
export class DummySecurityPlugin {
|
||||
public setup(core: CoreSetup) {
|
||||
const authenticate: AuthenticationHandler<Storage> = async (request, sessionStorage, t) => {
|
||||
if (request.path === url.authRedirect) {
|
||||
return t.redirected(url.redirectTo);
|
||||
}
|
||||
|
||||
if (request.path === url.exception) {
|
||||
throw new Error('sensitive info');
|
||||
}
|
||||
|
||||
if (request.headers.authorization) {
|
||||
const user = { id: '42' };
|
||||
sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs });
|
||||
return t.authenticated({ credentials: user });
|
||||
} else {
|
||||
return t.rejected(Boom.unauthorized());
|
||||
}
|
||||
};
|
||||
|
||||
const cookieOptions = {
|
||||
name: 'sid',
|
||||
encryptionKey: 'something_at_least_32_characters',
|
||||
validate: (session: Storage) => true,
|
||||
isSecure: false,
|
||||
path: '/',
|
||||
};
|
||||
core.http.registerAuth(authenticate, cookieOptions);
|
||||
return {
|
||||
dummy() {
|
||||
return 'Hello from dummy plugin';
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
163
src/core/server/http/integration_tests/http_service.test.ts
Normal file
163
src/core/server/http/integration_tests/http_service.test.ts
Normal file
|
@ -0,0 +1,163 @@
|
|||
/*
|
||||
* 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 path from 'path';
|
||||
import request from 'request';
|
||||
import * as kbnTestServer from '../../../../test_utils/kbn_server';
|
||||
import { Router } from '../router';
|
||||
import { url as authUrl } from './__fixtures__/plugins/dummy_security/server/plugin';
|
||||
import { url as onReqUrl } from './__fixtures__/plugins/dummy_on_request/server/plugin';
|
||||
|
||||
describe('http service', () => {
|
||||
describe('setup contract', () => {
|
||||
describe('#registerAuth()', () => {
|
||||
const dummySecurityPlugin = path.resolve(__dirname, './__fixtures__/plugins/dummy_security');
|
||||
let root: ReturnType<typeof kbnTestServer.createRoot>;
|
||||
beforeAll(async () => {
|
||||
root = kbnTestServer.createRoot(
|
||||
{
|
||||
plugins: { paths: [dummySecurityPlugin] },
|
||||
},
|
||||
{
|
||||
dev: true,
|
||||
}
|
||||
);
|
||||
|
||||
const router = new Router('');
|
||||
router.get({ path: authUrl.auth, validate: false }, async (req, res) =>
|
||||
res.ok({ content: 'ok' })
|
||||
);
|
||||
// TODO fix me when registerRouter is available before HTTP server is run
|
||||
(root as any).server.http.registerRouter(router);
|
||||
|
||||
await root.setup();
|
||||
}, 30000);
|
||||
|
||||
afterAll(async () => await root.shutdown());
|
||||
|
||||
it('Should support implementing custom authentication logic', async () => {
|
||||
const response = await kbnTestServer.request
|
||||
.get(root, authUrl.auth)
|
||||
.expect(200, { content: 'ok' });
|
||||
|
||||
expect(response.header['set-cookie']).toBeDefined();
|
||||
const cookies = response.header['set-cookie'];
|
||||
expect(cookies).toHaveLength(1);
|
||||
|
||||
const sessionCookie = request.cookie(cookies[0]);
|
||||
if (!sessionCookie) {
|
||||
throw new Error('session cookie expected to be defined');
|
||||
}
|
||||
expect(sessionCookie).toBeDefined();
|
||||
expect(sessionCookie.key).toBe('sid');
|
||||
expect(sessionCookie.value).toBeDefined();
|
||||
expect(sessionCookie.path).toBe('/');
|
||||
expect(sessionCookie.httpOnly).toBe(true);
|
||||
});
|
||||
|
||||
it('Should support rejecting a request from an unauthenticated user', async () => {
|
||||
await kbnTestServer.request
|
||||
.get(root, authUrl.auth)
|
||||
.unset('Authorization')
|
||||
.expect(401);
|
||||
});
|
||||
|
||||
it('Should support redirecting', async () => {
|
||||
const response = await kbnTestServer.request.get(root, authUrl.authRedirect).expect(302);
|
||||
expect(response.header.location).toBe(authUrl.redirectTo);
|
||||
});
|
||||
|
||||
it('Should run auth for legacy routes and proxy request to legacy server route handlers', async () => {
|
||||
const legacyUrl = '/legacy';
|
||||
const kbnServer = kbnTestServer.getKbnServer(root);
|
||||
kbnServer.server.route({
|
||||
method: 'GET',
|
||||
path: legacyUrl,
|
||||
handler: () => 'ok from legacy server',
|
||||
});
|
||||
|
||||
const response = await kbnTestServer.request
|
||||
.get(root, legacyUrl)
|
||||
.expect(200, 'ok from legacy server');
|
||||
|
||||
expect(response.header['set-cookie']).toBe(undefined);
|
||||
});
|
||||
|
||||
it(`Shouldn't expose internal error details`, async () => {
|
||||
await kbnTestServer.request.get(root, authUrl.exception).expect({
|
||||
statusCode: 500,
|
||||
error: 'Internal Server Error',
|
||||
message: 'An internal server error occurred',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#registerOnRequest()', () => {
|
||||
const dummyOnRequestPlugin = path.resolve(
|
||||
__dirname,
|
||||
'./__fixtures__/plugins/dummy_on_request'
|
||||
);
|
||||
let root: ReturnType<typeof kbnTestServer.createRoot>;
|
||||
beforeAll(async () => {
|
||||
root = kbnTestServer.createRoot(
|
||||
{
|
||||
plugins: { paths: [dummyOnRequestPlugin] },
|
||||
},
|
||||
{
|
||||
dev: true,
|
||||
}
|
||||
);
|
||||
|
||||
const router = new Router('');
|
||||
// routes with expected success status response should have handlers
|
||||
[onReqUrl.root, onReqUrl.independentReq].forEach(url =>
|
||||
router.get({ path: url, validate: false }, async (req, res) => res.ok({ content: 'ok' }))
|
||||
);
|
||||
// TODO fix me when registerRouter is available before HTTP server is run
|
||||
(root as any).server.http.registerRouter(router);
|
||||
|
||||
await root.setup();
|
||||
}, 30000);
|
||||
|
||||
afterAll(async () => await root.shutdown());
|
||||
it('Should support passing request through to the route handler', async () => {
|
||||
await kbnTestServer.request.get(root, onReqUrl.root).expect(200, { content: 'ok' });
|
||||
});
|
||||
it('Should support redirecting to configured url', async () => {
|
||||
const response = await kbnTestServer.request.get(root, onReqUrl.redirect).expect(302);
|
||||
expect(response.header.location).toBe(onReqUrl.redirectTo);
|
||||
});
|
||||
it('Should failing a request with configured error and status code', async () => {
|
||||
await kbnTestServer.request
|
||||
.get(root, onReqUrl.failed)
|
||||
.expect(400, { statusCode: 400, error: 'Bad Request', message: 'unexpected error' });
|
||||
});
|
||||
it(`Shouldn't expose internal error details`, async () => {
|
||||
await kbnTestServer.request.get(root, onReqUrl.exception).expect({
|
||||
statusCode: 500,
|
||||
error: 'Internal Server Error',
|
||||
message: 'An internal server error occurred',
|
||||
});
|
||||
});
|
||||
it(`Shouldn't share request object between interceptors`, async () => {
|
||||
await kbnTestServer.request.get(root, onReqUrl.independentReq).expect(200);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
103
src/core/server/http/lifecycle/auth.test.ts
Normal file
103
src/core/server/http/lifecycle/auth.test.ts
Normal file
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* 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 Boom from 'boom';
|
||||
import { adoptToHapiAuthFormat } from './auth';
|
||||
|
||||
const SessionStorageMock = {
|
||||
asScoped: () => null as any,
|
||||
};
|
||||
const requestMock = {} as any;
|
||||
const createResponseToolkit = (customization = {}): any => ({ ...customization });
|
||||
|
||||
describe('adoptToHapiAuthFormat', () => {
|
||||
it('Should allow authenticating a user identity with given credentials', async () => {
|
||||
const credentials = {};
|
||||
const authenticatedMock = jest.fn();
|
||||
const onAuth = adoptToHapiAuthFormat(
|
||||
async (req, sessionStorage, t) => t.authenticated(credentials),
|
||||
SessionStorageMock
|
||||
);
|
||||
await onAuth(
|
||||
requestMock,
|
||||
createResponseToolkit({
|
||||
authenticated: authenticatedMock,
|
||||
})
|
||||
);
|
||||
|
||||
expect(authenticatedMock).toBeCalledTimes(1);
|
||||
expect(authenticatedMock).toBeCalledWith({ credentials });
|
||||
});
|
||||
|
||||
it('Should allow redirecting to specified url', async () => {
|
||||
const redirectUrl = '/docs';
|
||||
const onAuth = adoptToHapiAuthFormat(
|
||||
async (req, sessionStorage, t) => t.redirected(redirectUrl),
|
||||
SessionStorageMock
|
||||
);
|
||||
const takeoverSymbol = {};
|
||||
const redirectMock = jest.fn(() => ({ takeover: () => takeoverSymbol }));
|
||||
const result = await onAuth(
|
||||
requestMock,
|
||||
createResponseToolkit({
|
||||
redirect: redirectMock,
|
||||
})
|
||||
);
|
||||
|
||||
expect(redirectMock).toBeCalledWith(redirectUrl);
|
||||
expect(result).toBe(takeoverSymbol);
|
||||
});
|
||||
|
||||
it('Should allow to specify statusCode and message for Boom error', async () => {
|
||||
const onAuth = adoptToHapiAuthFormat(
|
||||
async (req, sessionStorage, t) => t.rejected(new Error('not found'), { statusCode: 404 }),
|
||||
SessionStorageMock
|
||||
);
|
||||
const result = (await onAuth(requestMock, createResponseToolkit())) as Boom;
|
||||
|
||||
expect(result).toBeInstanceOf(Boom);
|
||||
expect(result.message).toBe('not found');
|
||||
expect(result.output.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it('Should return Boom.internal error error if interceptor throws', async () => {
|
||||
const onAuth = adoptToHapiAuthFormat(async (req, sessionStorage, t) => {
|
||||
throw new Error('unknown error');
|
||||
}, SessionStorageMock);
|
||||
const result = (await onAuth(requestMock, createResponseToolkit())) as Boom;
|
||||
|
||||
expect(result).toBeInstanceOf(Boom);
|
||||
expect(result.message).toBe('unknown error');
|
||||
expect(result.output.statusCode).toBe(500);
|
||||
});
|
||||
|
||||
it('Should return Boom.internal error if interceptor returns unexpected result', async () => {
|
||||
const onAuth = adoptToHapiAuthFormat(
|
||||
async (req, sessionStorage, t) => undefined as any,
|
||||
SessionStorageMock
|
||||
);
|
||||
const result = (await onAuth(requestMock, createResponseToolkit())) as Boom;
|
||||
|
||||
expect(result).toBeInstanceOf(Boom);
|
||||
expect(result.message).toBe(
|
||||
'Unexpected result from Authenticate. Expected AuthResult, but given: undefined.'
|
||||
);
|
||||
expect(result.output.statusCode).toBe(500);
|
||||
});
|
||||
});
|
112
src/core/server/http/lifecycle/auth.ts
Normal file
112
src/core/server/http/lifecycle/auth.ts
Normal file
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* 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 Boom from 'boom';
|
||||
import { Lifecycle, Request, ResponseToolkit } from 'hapi';
|
||||
import { SessionStorage, SessionStorageFactory } from '../session_storage';
|
||||
|
||||
enum ResultType {
|
||||
authenticated = 'authenticated',
|
||||
redirected = 'redirected',
|
||||
rejected = 'rejected',
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
class AuthResult {
|
||||
public static authenticated(credentials: any) {
|
||||
return new AuthResult(ResultType.authenticated, credentials);
|
||||
}
|
||||
public static redirected(url: string) {
|
||||
return new AuthResult(ResultType.redirected, url);
|
||||
}
|
||||
public static rejected(error: Error, options: { statusCode?: number } = {}) {
|
||||
return new AuthResult(ResultType.rejected, { error, statusCode: options.statusCode });
|
||||
}
|
||||
public static isValidResult(candidate: any) {
|
||||
return candidate instanceof AuthResult;
|
||||
}
|
||||
constructor(private readonly type: ResultType, public readonly payload: any) {}
|
||||
public isAuthenticated() {
|
||||
return this.type === ResultType.authenticated;
|
||||
}
|
||||
public isRedirected() {
|
||||
return this.type === ResultType.redirected;
|
||||
}
|
||||
public isRejected() {
|
||||
return this.type === ResultType.rejected;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* A tool set defining an outcome of Auth interceptor for incoming request.
|
||||
*/
|
||||
export interface AuthToolkit {
|
||||
/** Authentication is successful with given credentials, allow request to pass through */
|
||||
authenticated: (credentials: any) => AuthResult;
|
||||
/** Authentication requires to interrupt request handling and redirect to a configured url */
|
||||
redirected: (url: string) => AuthResult;
|
||||
/** Authentication is unsuccessful, fail the request with specified error. */
|
||||
rejected: (error: Error, options?: { statusCode?: number }) => AuthResult;
|
||||
}
|
||||
|
||||
const toolkit: AuthToolkit = {
|
||||
authenticated: AuthResult.authenticated,
|
||||
redirected: AuthResult.redirected,
|
||||
rejected: AuthResult.rejected,
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type AuthenticationHandler<T> = (
|
||||
request: Request,
|
||||
sessionStorage: SessionStorage<T>,
|
||||
t: AuthToolkit
|
||||
) => Promise<AuthResult>;
|
||||
|
||||
/** @public */
|
||||
export function adoptToHapiAuthFormat<T = any>(
|
||||
fn: AuthenticationHandler<T>,
|
||||
sessionStorage: SessionStorageFactory<T>
|
||||
) {
|
||||
return async function interceptAuth(
|
||||
req: Request,
|
||||
h: ResponseToolkit
|
||||
): Promise<Lifecycle.ReturnValue> {
|
||||
try {
|
||||
const result = await fn(req, sessionStorage.asScoped(req), toolkit);
|
||||
|
||||
if (AuthResult.isValidResult(result)) {
|
||||
if (result.isAuthenticated()) {
|
||||
return h.authenticated({ credentials: result.payload });
|
||||
}
|
||||
if (result.isRedirected()) {
|
||||
return h.redirect(result.payload).takeover();
|
||||
}
|
||||
if (result.isRejected()) {
|
||||
const { error, statusCode } = result.payload;
|
||||
return Boom.boomify(error, { statusCode });
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`Unexpected result from Authenticate. Expected AuthResult, but given: ${result}.`
|
||||
);
|
||||
} catch (error) {
|
||||
return Boom.internal(error.message, { statusCode: 500 });
|
||||
}
|
||||
};
|
||||
}
|
88
src/core/server/http/lifecycle/on_request.test.ts
Normal file
88
src/core/server/http/lifecycle/on_request.test.ts
Normal file
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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 Boom from 'boom';
|
||||
import { adoptToHapiOnRequestFormat } from './on_request';
|
||||
|
||||
const requestMock = {} as any;
|
||||
const createResponseToolkit = (customization = {}): any => ({ ...customization });
|
||||
|
||||
describe('adoptToHapiOnRequestFormat', () => {
|
||||
it('Should allow passing request to the next handler', async () => {
|
||||
const continueSymbol = {};
|
||||
const onRequest = adoptToHapiOnRequestFormat((req, t) => t.next());
|
||||
const result = await onRequest(
|
||||
requestMock,
|
||||
createResponseToolkit({
|
||||
['continue']: continueSymbol,
|
||||
})
|
||||
);
|
||||
|
||||
expect(result).toBe(continueSymbol);
|
||||
});
|
||||
|
||||
it('Should support redirecting to specified url', async () => {
|
||||
const redirectUrl = '/docs';
|
||||
const onRequest = adoptToHapiOnRequestFormat((req, t) => t.redirected(redirectUrl));
|
||||
const takeoverSymbol = {};
|
||||
const redirectMock = jest.fn(() => ({ takeover: () => takeoverSymbol }));
|
||||
const result = await onRequest(
|
||||
requestMock,
|
||||
createResponseToolkit({
|
||||
redirect: redirectMock,
|
||||
})
|
||||
);
|
||||
|
||||
expect(redirectMock).toBeCalledWith(redirectUrl);
|
||||
expect(result).toBe(takeoverSymbol);
|
||||
});
|
||||
|
||||
it('Should support specifying statusCode and message for Boom error', async () => {
|
||||
const onRequest = adoptToHapiOnRequestFormat((req, t) => {
|
||||
return t.rejected(new Error('unexpected result'), { statusCode: 501 });
|
||||
});
|
||||
const result = (await onRequest(requestMock, createResponseToolkit())) as Boom;
|
||||
|
||||
expect(result).toBeInstanceOf(Boom);
|
||||
expect(result.message).toBe('unexpected result');
|
||||
expect(result.output.statusCode).toBe(501);
|
||||
});
|
||||
|
||||
it('Should return Boom.internal error if interceptor throws', async () => {
|
||||
const onRequest = adoptToHapiOnRequestFormat((req, t) => {
|
||||
throw new Error('unknown error');
|
||||
});
|
||||
const result = (await onRequest(requestMock, createResponseToolkit())) as Boom;
|
||||
|
||||
expect(result).toBeInstanceOf(Boom);
|
||||
expect(result.message).toBe('unknown error');
|
||||
expect(result.output.statusCode).toBe(500);
|
||||
});
|
||||
|
||||
it('Should return Boom.internal error if interceptor returns unexpected result', async () => {
|
||||
const onRequest = adoptToHapiOnRequestFormat((req, toolkit) => undefined as any);
|
||||
const result = (await onRequest(requestMock, createResponseToolkit())) as Boom;
|
||||
|
||||
expect(result).toBeInstanceOf(Boom);
|
||||
expect(result.message).toBe(
|
||||
'Unexpected result from OnRequest. Expected OnRequestResult, but given: undefined.'
|
||||
);
|
||||
expect(result.output.statusCode).toBe(500);
|
||||
});
|
||||
});
|
114
src/core/server/http/lifecycle/on_request.ts
Normal file
114
src/core/server/http/lifecycle/on_request.ts
Normal file
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* 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 Boom from 'boom';
|
||||
import { Lifecycle, Request, ResponseToolkit } from 'hapi';
|
||||
import { KibanaRequest } from '../router';
|
||||
|
||||
enum ResultType {
|
||||
next = 'next',
|
||||
redirected = 'redirected',
|
||||
rejected = 'rejected',
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
class OnRequestResult {
|
||||
public static next() {
|
||||
return new OnRequestResult(ResultType.next);
|
||||
}
|
||||
public static redirected(url: string) {
|
||||
return new OnRequestResult(ResultType.redirected, url);
|
||||
}
|
||||
public static rejected(error: Error, options: { statusCode?: number } = {}) {
|
||||
return new OnRequestResult(ResultType.rejected, { error, statusCode: options.statusCode });
|
||||
}
|
||||
public static isValidResult(candidate: any) {
|
||||
return candidate instanceof OnRequestResult;
|
||||
}
|
||||
constructor(private readonly type: ResultType, public readonly payload?: any) {}
|
||||
public isNext() {
|
||||
return this.type === ResultType.next;
|
||||
}
|
||||
public isRedirected() {
|
||||
return this.type === ResultType.redirected;
|
||||
}
|
||||
public isRejected() {
|
||||
return this.type === ResultType.rejected;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* A tool set defining an outcome of OnRequest interceptor for incoming request.
|
||||
*/
|
||||
export interface OnRequestToolkit {
|
||||
/** To pass request to the next handler */
|
||||
next: () => OnRequestResult;
|
||||
/** To interrupt request handling and redirect to a configured url */
|
||||
redirected: (url: string) => OnRequestResult;
|
||||
/** Fail the request with specified error. */
|
||||
rejected: (error: Error, options?: { statusCode?: number }) => OnRequestResult;
|
||||
}
|
||||
|
||||
const toolkit: OnRequestToolkit = {
|
||||
next: OnRequestResult.next,
|
||||
redirected: OnRequestResult.redirected,
|
||||
rejected: OnRequestResult.rejected,
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type OnRequestHandler<Params = any, Query = any, Body = any> = (
|
||||
req: KibanaRequest<Params, Query, Body>,
|
||||
t: OnRequestToolkit
|
||||
) => OnRequestResult | Promise<OnRequestResult>;
|
||||
|
||||
/**
|
||||
* @public
|
||||
* Adopt custom request interceptor to Hapi lifecycle system.
|
||||
* @param fn - an extension point allowing to perform custom logic for
|
||||
* incoming HTTP requests.
|
||||
*/
|
||||
export function adoptToHapiOnRequestFormat(fn: OnRequestHandler) {
|
||||
return async function interceptRequest(
|
||||
req: Request,
|
||||
h: ResponseToolkit
|
||||
): Promise<Lifecycle.ReturnValue> {
|
||||
try {
|
||||
const result = await fn(KibanaRequest.from(req, undefined), toolkit);
|
||||
if (OnRequestResult.isValidResult(result)) {
|
||||
if (result.isNext()) {
|
||||
return h.continue;
|
||||
}
|
||||
if (result.isRedirected()) {
|
||||
return h.redirect(result.payload).takeover();
|
||||
}
|
||||
if (result.isRejected()) {
|
||||
const { error, statusCode } = result.payload;
|
||||
return Boom.boomify(error, { statusCode });
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Unexpected result from OnRequest. Expected OnRequestResult, but given: ${result}.`
|
||||
);
|
||||
} catch (error) {
|
||||
return Boom.internal(error.message, { statusCode: 500 });
|
||||
}
|
||||
};
|
||||
}
|
|
@ -23,6 +23,7 @@ import { Request } from 'hapi';
|
|||
import { filterHeaders, Headers } from './headers';
|
||||
import { RouteSchemas } from './route';
|
||||
|
||||
/** @public */
|
||||
export class KibanaRequest<Params, Query, Body> {
|
||||
/**
|
||||
* Factory for creating requests. Validates the request before creating an
|
||||
|
@ -68,9 +69,11 @@ export class KibanaRequest<Params, Query, Body> {
|
|||
}
|
||||
|
||||
public readonly headers: Headers;
|
||||
public readonly path: string;
|
||||
|
||||
constructor(req: Request, readonly params: Params, readonly query: Query, readonly body: Body) {
|
||||
this.headers = req.headers;
|
||||
this.path = req.path;
|
||||
}
|
||||
|
||||
public getFilteredHeaders(headersToKeep: string[]) {
|
||||
|
|
|
@ -30,6 +30,7 @@ export interface RouterRoute {
|
|||
handler: (req: Request, responseToolkit: ResponseToolkit) => Promise<ResponseObject>;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export class Router {
|
||||
public routes: Array<Readonly<RouterRoute>> = [];
|
||||
|
||||
|
|
42
src/core/server/http/session_storage.ts
Normal file
42
src/core/server/http/session_storage.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 { Request } from 'hapi';
|
||||
/**
|
||||
* Provides an interface to store and retrieve data across requests.
|
||||
*/
|
||||
export interface SessionStorage<T> {
|
||||
/**
|
||||
* Retrieves session value from the session storage.
|
||||
*/
|
||||
get(): Promise<T | null>;
|
||||
/**
|
||||
* Puts current session value into the session storage.
|
||||
* @param sessionValue - value to put
|
||||
*/
|
||||
set(sessionValue: T): void;
|
||||
/**
|
||||
* Clears current session.
|
||||
*/
|
||||
clear(): void;
|
||||
}
|
||||
|
||||
export interface SessionStorageFactory<T> {
|
||||
asScoped: (request: Request) => SessionStorage<T>;
|
||||
}
|
|
@ -30,6 +30,14 @@ export {
|
|||
ElasticsearchClientConfig,
|
||||
APICaller,
|
||||
} from './elasticsearch';
|
||||
export {
|
||||
AuthenticationHandler,
|
||||
AuthToolkit,
|
||||
KibanaRequest,
|
||||
OnRequestHandler,
|
||||
OnRequestToolkit,
|
||||
Router,
|
||||
} from './http';
|
||||
export { Logger, LoggerFactory, LogMeta, LogRecord, LogLevel } from './logging';
|
||||
|
||||
export {
|
||||
|
@ -48,4 +56,4 @@ export interface CoreSetup {
|
|||
plugins: PluginsServiceSetup;
|
||||
}
|
||||
|
||||
export { ElasticsearchServiceSetup, HttpServiceSetup, PluginsServiceSetup };
|
||||
export { HttpServiceSetup, ElasticsearchServiceSetup, PluginsServiceSetup };
|
||||
|
|
|
@ -6,7 +6,12 @@
|
|||
|
||||
import { ConfigOptions } from 'elasticsearch';
|
||||
import { Duration } from 'moment';
|
||||
import { ObjectType } from '@kbn/config-schema';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Request } from 'hapi';
|
||||
import { ResponseObject } from 'hapi';
|
||||
import { ResponseToolkit } from 'hapi';
|
||||
import { Schema } from '@kbn/config-schema';
|
||||
import { Server } from 'hapi';
|
||||
import { ServerOptions } from 'hapi';
|
||||
import { Type } from '@kbn/config-schema';
|
||||
|
@ -15,6 +20,21 @@ import { TypeOf } from '@kbn/config-schema';
|
|||
// @public (undocumented)
|
||||
export type APICaller = (endpoint: string, clientParams: Record<string, unknown>, options?: CallAPIOptions) => Promise<unknown>;
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "SessionStorage" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-forgotten-export) The symbol "AuthResult" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// @public (undocumented)
|
||||
export type AuthenticationHandler<T> = (request: Request, sessionStorage: SessionStorage<T>, t: AuthToolkit) => Promise<AuthResult>;
|
||||
|
||||
// @public
|
||||
export interface AuthToolkit {
|
||||
authenticated: (credentials: any) => AuthResult;
|
||||
redirected: (url: string) => AuthResult;
|
||||
rejected: (error: Error, options?: {
|
||||
statusCode?: number;
|
||||
}) => AuthResult;
|
||||
}
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "BootstrapArgs" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-internal-missing-underscore) The name bootstrap should be prefixed with an underscore because the declaration is marked as "@internal"
|
||||
//
|
||||
|
@ -61,8 +81,6 @@ export class ConfigService {
|
|||
export interface CoreSetup {
|
||||
// (undocumented)
|
||||
elasticsearch: ElasticsearchServiceSetup;
|
||||
// Warning: (ae-incompatible-release-tags) The symbol "http" is marked as @public, but its signature references "HttpServiceSetup" which is marked as @internal
|
||||
//
|
||||
// (undocumented)
|
||||
http: HttpServiceSetup;
|
||||
// Warning: (ae-incompatible-release-tags) The symbol "plugins" is marked as @public, but its signature references "PluginsServiceSetup" which is marked as @internal
|
||||
|
@ -109,11 +127,30 @@ export interface ElasticsearchServiceSetup {
|
|||
export type Headers = Record<string, string | string[] | undefined>;
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "HttpServerInfo" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-internal-missing-underscore) The name HttpServiceSetup should be prefixed with an underscore because the declaration is marked as "@internal"
|
||||
//
|
||||
// @internal (undocumented)
|
||||
// @public (undocumented)
|
||||
export type HttpServiceSetup = HttpServerInfo;
|
||||
|
||||
// @public (undocumented)
|
||||
export class KibanaRequest<Params, Query, Body> {
|
||||
// (undocumented)
|
||||
constructor(req: Request, params: Params, query: Query, body: Body);
|
||||
// (undocumented)
|
||||
readonly body: Body;
|
||||
// Warning: (ae-forgotten-export) The symbol "RouteSchemas" needs to be exported by the entry point index.d.ts
|
||||
static from<P extends ObjectType, Q extends ObjectType, B extends ObjectType>(req: Request, routeSchemas: RouteSchemas<P, Q, B> | undefined): KibanaRequest<P["type"], Q["type"], B["type"]>;
|
||||
// (undocumented)
|
||||
getFilteredHeaders(headersToKeep: string[]): Pick<Record<string, string | string[] | undefined>, string>;
|
||||
// (undocumented)
|
||||
readonly headers: Headers;
|
||||
// (undocumented)
|
||||
readonly params: Params;
|
||||
// (undocumented)
|
||||
readonly path: string;
|
||||
// (undocumented)
|
||||
readonly query: Query;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface Logger {
|
||||
debug(message: string, meta?: LogMeta): void;
|
||||
|
@ -187,6 +224,20 @@ export interface LogRecord {
|
|||
timestamp: Date;
|
||||
}
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "OnRequestResult" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// @public (undocumented)
|
||||
export type OnRequestHandler<Params = any, Query = any, Body = any> = (req: KibanaRequest<Params, Query, Body>, t: OnRequestToolkit) => OnRequestResult | Promise<OnRequestResult>;
|
||||
|
||||
// @public
|
||||
export interface OnRequestToolkit {
|
||||
next: () => OnRequestResult;
|
||||
redirected: (url: string) => OnRequestResult;
|
||||
rejected: (error: Error, options?: {
|
||||
statusCode?: number;
|
||||
}) => OnRequestResult;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface Plugin<TSetup, TPluginsSetup extends Record<PluginName, unknown> = {}> {
|
||||
// (undocumented)
|
||||
|
@ -223,6 +274,11 @@ export interface PluginSetupContext {
|
|||
adminClient$: Observable<ClusterClient>;
|
||||
dataClient$: Observable<ClusterClient>;
|
||||
};
|
||||
// (undocumented)
|
||||
http: {
|
||||
registerAuth: HttpServiceSetup['registerAuth'];
|
||||
registerOnRequest: HttpServiceSetup['registerOnRequest'];
|
||||
};
|
||||
}
|
||||
|
||||
// Warning: (ae-internal-missing-underscore) The name PluginsServiceSetup should be prefixed with an underscore because the declaration is marked as "@internal"
|
||||
|
@ -238,6 +294,25 @@ export interface PluginsServiceSetup {
|
|||
};
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export class Router {
|
||||
// (undocumented)
|
||||
constructor(path: string);
|
||||
delete<P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>): void;
|
||||
// Warning: (ae-forgotten-export) The symbol "RouteConfig" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-forgotten-export) The symbol "RequestHandler" needs to be exported by the entry point index.d.ts
|
||||
get<P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>): void;
|
||||
getRoutes(): Readonly<RouterRoute>[];
|
||||
// (undocumented)
|
||||
readonly path: string;
|
||||
post<P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>): void;
|
||||
put<P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>): void;
|
||||
// Warning: (ae-forgotten-export) The symbol "RouterRoute" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
routes: Array<Readonly<RouterRoute>>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export class ScopedClusterClient {
|
||||
// (undocumented)
|
||||
|
@ -249,8 +324,8 @@ export class ScopedClusterClient {
|
|||
|
||||
// Warnings were encountered during analysis:
|
||||
//
|
||||
// src/core/server/plugins/plugin_context.ts:35:9 - (ae-forgotten-export) The symbol "EnvironmentMode" needs to be exported by the entry point index.d.ts
|
||||
// src/core/server/plugins/plugins_service.ts:33:24 - (ae-forgotten-export) The symbol "DiscoveredPluginInternal" needs to be exported by the entry point index.d.ts
|
||||
// src/core/server/plugins/plugin_context.ts:36:9 - (ae-forgotten-export) The symbol "EnvironmentMode" needs to be exported by the entry point index.d.ts
|
||||
// src/core/server/plugins/plugins_service.ts:34:24 - (ae-forgotten-export) The symbol "DiscoveredPluginInternal" needs to be exported by the entry point index.d.ts
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
|
||||
|
|
|
@ -162,6 +162,7 @@ describe('once LegacyService is set up with connection info', () => {
|
|||
{ server: { autoListen: true } },
|
||||
{
|
||||
elasticsearch: setupDeps.elasticsearch,
|
||||
http: setupDeps.http,
|
||||
serverOptions: {
|
||||
listener: expect.any(LegacyPlatformProxy),
|
||||
someAnotherOption: 'bar',
|
||||
|
@ -187,6 +188,7 @@ describe('once LegacyService is set up with connection info', () => {
|
|||
{ server: { autoListen: true } },
|
||||
{
|
||||
elasticsearch: setupDeps.elasticsearch,
|
||||
http: setupDeps.http,
|
||||
serverOptions: {
|
||||
listener: expect.any(LegacyPlatformProxy),
|
||||
someAnotherOption: 'bar',
|
||||
|
|
|
@ -89,7 +89,6 @@ export class LegacyService implements CoreService {
|
|||
await this.createClusterManager(config);
|
||||
return;
|
||||
}
|
||||
|
||||
return await this.createKbnServer(config, deps);
|
||||
})
|
||||
)
|
||||
|
@ -148,6 +147,7 @@ export class LegacyService implements CoreService {
|
|||
}
|
||||
: { autoListen: false },
|
||||
handledConfigPaths: await this.coreContext.configService.getUsedPaths(),
|
||||
http,
|
||||
elasticsearch,
|
||||
plugins,
|
||||
});
|
||||
|
|
|
@ -22,6 +22,7 @@ import { Observable } from 'rxjs';
|
|||
import { ConfigWithSchema, EnvironmentMode } from '../config';
|
||||
import { CoreContext } from '../core_context';
|
||||
import { ClusterClient } from '../elasticsearch';
|
||||
import { HttpServiceSetup } from '../http';
|
||||
import { LoggerFactory } from '../logging';
|
||||
import { PluginWrapper, PluginManifest } from './plugin';
|
||||
import { PluginsServiceSetupDeps } from './plugins_service';
|
||||
|
@ -54,6 +55,10 @@ export interface PluginSetupContext {
|
|||
adminClient$: Observable<ClusterClient>;
|
||||
dataClient$: Observable<ClusterClient>;
|
||||
};
|
||||
http: {
|
||||
registerAuth: HttpServiceSetup['registerAuth'];
|
||||
registerOnRequest: HttpServiceSetup['registerOnRequest'];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -109,6 +114,12 @@ export function createPluginInitializerContext(
|
|||
};
|
||||
}
|
||||
|
||||
// Added to improve http typings as make { http: Required<HttpSetup> }
|
||||
// Http service is disabled, when Kibana runs in optimizer mode or as dev cluster managed by cluster master.
|
||||
// In theory no plugins shouldn try to access http dependency in this case.
|
||||
function preventAccess() {
|
||||
throw new Error('Cannot use http contract when http server not started');
|
||||
}
|
||||
/**
|
||||
* This returns a facade for `CoreContext` that will be exposed to the plugin `setup` method.
|
||||
* This facade should be safe to use only within `setup` itself.
|
||||
|
@ -133,5 +144,14 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>(
|
|||
adminClient$: deps.elasticsearch.adminClient$,
|
||||
dataClient$: deps.elasticsearch.dataClient$,
|
||||
},
|
||||
http: deps.http
|
||||
? {
|
||||
registerAuth: deps.http.registerAuth,
|
||||
registerOnRequest: deps.http.registerOnRequest,
|
||||
}
|
||||
: {
|
||||
registerAuth: preventAccess,
|
||||
registerOnRequest: preventAccess,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import { filter, first, mergeMap, tap, toArray } from 'rxjs/operators';
|
|||
import { CoreService } from '../../types';
|
||||
import { CoreContext } from '../core_context';
|
||||
import { ElasticsearchServiceSetup } from '../elasticsearch/elasticsearch_service';
|
||||
import { HttpServiceSetup } from '../http/http_service';
|
||||
import { Logger } from '../logging';
|
||||
import { discover, PluginDiscoveryError, PluginDiscoveryErrorType } from './discovery';
|
||||
import { DiscoveredPlugin, DiscoveredPluginInternal, PluginWrapper, PluginName } from './plugin';
|
||||
|
@ -40,6 +41,7 @@ export interface PluginsServiceSetup {
|
|||
/** @internal */
|
||||
export interface PluginsServiceSetupDeps {
|
||||
elasticsearch: ElasticsearchServiceSetup;
|
||||
http?: HttpServiceSetup;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
|
|
|
@ -70,6 +70,7 @@ export class Server {
|
|||
|
||||
const pluginsSetup = await this.plugins.setup({
|
||||
elasticsearch: elasticsearchServiceSetup,
|
||||
http: httpSetup,
|
||||
});
|
||||
|
||||
await this.legacy.setup({
|
||||
|
|
11
src/legacy/server/kbn_server.d.ts
vendored
11
src/legacy/server/kbn_server.d.ts
vendored
|
@ -19,10 +19,12 @@
|
|||
|
||||
import { Server } from 'hapi';
|
||||
|
||||
import { ConfigService } from '../../core/server/';
|
||||
import { ElasticsearchServiceSetup } from '../../core/server/';
|
||||
import { HttpServiceSetup } from '../../core/server/';
|
||||
import { PluginsServiceSetup } from '../../core/server/';
|
||||
import {
|
||||
ElasticsearchServiceSetup,
|
||||
HttpServiceSetup,
|
||||
ConfigService,
|
||||
PluginsServiceSetup,
|
||||
} from '../../core/server';
|
||||
import { ApmOssPlugin } from '../core_plugins/apm_oss';
|
||||
import { CallClusterWithRequest, ElasticsearchPlugin } from '../core_plugins/elasticsearch';
|
||||
|
||||
|
@ -66,6 +68,7 @@ export default class KbnServer {
|
|||
setup: {
|
||||
core: {
|
||||
elasticsearch: ElasticsearchServiceSetup;
|
||||
http?: HttpServiceSetup;
|
||||
};
|
||||
plugins: PluginsServiceSetup;
|
||||
};
|
||||
|
|
|
@ -54,12 +54,12 @@ export default class KbnServer {
|
|||
this.rootDir = rootDir;
|
||||
this.settings = settings || {};
|
||||
|
||||
const { plugins, elasticsearch, serverOptions, handledConfigPaths } = core;
|
||||
|
||||
const { plugins, http, elasticsearch, serverOptions, handledConfigPaths } = core;
|
||||
this.newPlatform = {
|
||||
setup: {
|
||||
core: {
|
||||
elasticsearch,
|
||||
http,
|
||||
},
|
||||
plugins,
|
||||
},
|
||||
|
|
|
@ -32,7 +32,7 @@ import { defaultsDeep, get } from 'lodash';
|
|||
import { resolve } from 'path';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import supertest from 'supertest';
|
||||
import { Env } from '../core/server/config';
|
||||
import { CliArgs, Env } from '../core/server/config';
|
||||
import { LegacyObjectToConfigAdapter } from '../core/server/legacy';
|
||||
import { Root } from '../core/server/root';
|
||||
|
||||
|
@ -60,7 +60,10 @@ const DEFAULT_SETTINGS_WITH_CORE_PLUGINS = {
|
|||
},
|
||||
};
|
||||
|
||||
export function createRootWithSettings(...settings: Array<Record<string, any>>) {
|
||||
export function createRootWithSettings(
|
||||
settings: Record<string, any>,
|
||||
cliArgs: Partial<CliArgs> = {}
|
||||
) {
|
||||
const env = Env.createDefault({
|
||||
configs: [],
|
||||
cliArgs: {
|
||||
|
@ -72,13 +75,14 @@ export function createRootWithSettings(...settings: Array<Record<string, any>>)
|
|||
repl: false,
|
||||
basePath: false,
|
||||
optimize: false,
|
||||
...cliArgs,
|
||||
},
|
||||
isDevClusterMaster: false,
|
||||
});
|
||||
|
||||
return new Root(
|
||||
new BehaviorSubject(
|
||||
new LegacyObjectToConfigAdapter(defaultsDeep({}, ...settings, DEFAULTS_SETTINGS))
|
||||
new LegacyObjectToConfigAdapter(defaultsDeep({}, settings, DEFAULTS_SETTINGS))
|
||||
),
|
||||
env
|
||||
);
|
||||
|
@ -104,8 +108,8 @@ function getSupertest(root: Root, method: HttpMethod, path: string) {
|
|||
* @param {Object} [settings={}] Any config overrides for this instance.
|
||||
* @returns {Root}
|
||||
*/
|
||||
export function createRoot(settings = {}) {
|
||||
return createRootWithSettings(settings);
|
||||
export function createRoot(settings = {}, cliArgs: Partial<CliArgs> = {}) {
|
||||
return createRootWithSettings(settings, cliArgs);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -116,7 +120,7 @@ export function createRoot(settings = {}) {
|
|||
* @returns {Root}
|
||||
*/
|
||||
export function createRootWithCorePlugins(settings = {}) {
|
||||
return createRootWithSettings(settings, DEFAULT_SETTINGS_WITH_CORE_PLUGINS);
|
||||
return createRootWithSettings(defaultsDeep({}, settings, DEFAULT_SETTINGS_WITH_CORE_PLUGINS));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
26
yarn.lock
26
yarn.lock
|
@ -2440,6 +2440,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/boom/-/boom-7.2.0.tgz#19c36cbb5811a7493f0f2e37f31d42b28df1abc1"
|
||||
integrity sha512-HonbGsHFbskh9zRAzA6tabcw18mCOsSEOL2ibGAuVqk6e7nElcRmWO5L4UfIHpDbWBWw+eZYFdsQ1+MEGgpcVA==
|
||||
|
||||
"@types/caseless@*":
|
||||
version "0.12.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8"
|
||||
integrity sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==
|
||||
|
||||
"@types/catbox@*":
|
||||
version "10.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/catbox/-/catbox-10.0.1.tgz#266679017749041fe9873fee1131dd2aaa04a07e"
|
||||
|
@ -2630,7 +2635,7 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.0.tgz#cbb49815a5e1129d5f23836a98d65d93822409af"
|
||||
integrity sha512-dxdRrUov2HVTbSRFX+7xwUPlbGYVEZK6PrSqClg2QPos3PNe0bCajkDDkDeeC1znjSH03KOEqVbXpnJuWa2wgQ==
|
||||
|
||||
"@types/form-data@^2.2.1":
|
||||
"@types/form-data@*", "@types/form-data@^2.2.1":
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/form-data/-/form-data-2.2.1.tgz#ee2b3b8eaa11c0938289953606b745b738c54b1e"
|
||||
integrity sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ==
|
||||
|
@ -2688,9 +2693,9 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/hapi-auth-cookie/-/hapi-auth-cookie-9.1.0.tgz#cbcd2236b7d429bd0632a8cc45cfd355fdd7e7a2"
|
||||
integrity sha512-qsP08L+fNaE2K5dsDVKvHp0AmSBs8m9PD5eWsTdHnkJOk81iD7c0J4GYt/1aDJwZsyx6CgcxpbkPOCwBJmrwAg==
|
||||
dependencies:
|
||||
"@types/hapi" "^17.0.18"
|
||||
"@types/hapi" "*"
|
||||
|
||||
"@types/hapi@^17.0.18":
|
||||
"@types/hapi@*", "@types/hapi@^17.0.18":
|
||||
version "17.0.18"
|
||||
resolved "https://registry.yarnpkg.com/@types/hapi/-/hapi-17.0.18.tgz#f855fe18766aa2592a3a689c3e6eabe72989ff1a"
|
||||
integrity sha512-sRoDjz1iVOCxTqq+EepzDQI773k2PjboHpvMpp524278grosStxZ5+oooVjNLJZj1iZIbiLeeR5/ZeIRgVXsCg==
|
||||
|
@ -3128,6 +3133,16 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/redux/-/redux-3.6.31.tgz#40eafa7575db36b912ce0059b85de98c205b0708"
|
||||
integrity sha1-QOr6dXXbNrkSzgBZuF3pjCBbBwg=
|
||||
|
||||
"@types/request@^2.48.1":
|
||||
version "2.48.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.1.tgz#e402d691aa6670fbbff1957b15f1270230ab42fa"
|
||||
integrity sha512-ZgEZ1TiD+KGA9LiAAPPJL68Id2UWfeSO62ijSXZjFJArVV+2pKcsVHmrcu+1oiE3q6eDGiFiSolRc4JHoerBBg==
|
||||
dependencies:
|
||||
"@types/caseless" "*"
|
||||
"@types/form-data" "*"
|
||||
"@types/node" "*"
|
||||
"@types/tough-cookie" "*"
|
||||
|
||||
"@types/retry@*", "@types/retry@^0.10.2":
|
||||
version "0.10.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.10.2.tgz#bd1740c4ad51966609b058803ee6874577848b37"
|
||||
|
@ -3232,6 +3247,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/tinycolor2/-/tinycolor2-1.4.1.tgz#2f5670c9d1d6e558897a810ed284b44918fc1253"
|
||||
integrity sha512-25L/RL5tqZkquKXVHM1fM2bd23qjfbcPpAZ2N/H05Y45g3UEi+Hw8CbDV28shKY8gH1SHiLpZSxPI1lacqdpGg==
|
||||
|
||||
"@types/tough-cookie@*":
|
||||
version "2.3.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-2.3.5.tgz#9da44ed75571999b65c37b60c9b2b88db54c585d"
|
||||
integrity sha512-SCcK7mvGi3+ZNz833RRjFIxrn4gI1PPR3NtuIS+6vMkvmsGjosqTJwRt5bAEFLRz+wtJMWv8+uOnZf2hi2QXTg==
|
||||
|
||||
"@types/type-detect@^4.0.1":
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/type-detect/-/type-detect-4.0.1.tgz#3b0f5ac82ea630090cbf57c57a1bf5a63a29b9b6"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue