[New platform] HTTP & Security integration (#34631) (#35181)

* 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:
Mikhail Shustov 2019-04-16 22:36:00 +02:00 committed by GitHub
parent 4f574e1b4b
commit 69c5551d7d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
63 changed files with 1815 additions and 27 deletions

View file

@ -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>;
```

View file

@ -0,0 +1,11 @@
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [AuthToolkit](./kibana-plugin-server.authtoolkit.md) &gt; [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;
```

View file

@ -0,0 +1,20 @@
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [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) =&gt; AuthResult</code> | Authentication is successful with given credentials, allow request to pass through |
| [redirected](./kibana-plugin-server.authtoolkit.redirected.md) | <code>(url: string) =&gt; 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/>` }) =&gt; AuthResult</code> | Authentication is unsuccessful, fail the request with specified error. |

View file

@ -0,0 +1,11 @@
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [AuthToolkit](./kibana-plugin-server.authtoolkit.md) &gt; [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;
```

View file

@ -0,0 +1,13 @@
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [AuthToolkit](./kibana-plugin-server.authtoolkit.md) &gt; [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;
```

View file

@ -0,0 +1,10 @@
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md)
## HttpServiceSetup type
<b>Signature:</b>
```typescript
export declare type HttpServiceSetup = HttpServerInfo;
```

View file

@ -0,0 +1,9 @@
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [KibanaRequest](./kibana-plugin-server.kibanarequest.md) &gt; [body](./kibana-plugin-server.kibanarequest.body.md)
## KibanaRequest.body property
<b>Signature:</b>
```typescript
readonly body: Body;
```

View file

@ -0,0 +1,23 @@
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [KibanaRequest](./kibana-plugin-server.kibanarequest.md) &gt; [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&lt;P, Q, B&gt; &#124; undefined</code> | |
<b>Returns:</b>
`KibanaRequest<P["type"], Q["type"], B["type"]>`

View file

@ -0,0 +1,20 @@
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [KibanaRequest](./kibana-plugin-server.kibanarequest.md) &gt; [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>`

View file

@ -0,0 +1,9 @@
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [KibanaRequest](./kibana-plugin-server.kibanarequest.md) &gt; [headers](./kibana-plugin-server.kibanarequest.headers.md)
## KibanaRequest.headers property
<b>Signature:</b>
```typescript
readonly headers: Headers;
```

View file

@ -0,0 +1,28 @@
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [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) | | |

View file

@ -0,0 +1,9 @@
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [KibanaRequest](./kibana-plugin-server.kibanarequest.md) &gt; [params](./kibana-plugin-server.kibanarequest.params.md)
## KibanaRequest.params property
<b>Signature:</b>
```typescript
readonly params: Params;
```

View file

@ -0,0 +1,9 @@
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [KibanaRequest](./kibana-plugin-server.kibanarequest.md) &gt; [path](./kibana-plugin-server.kibanarequest.path.md)
## KibanaRequest.path property
<b>Signature:</b>
```typescript
readonly path: string;
```

View file

@ -0,0 +1,9 @@
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [KibanaRequest](./kibana-plugin-server.kibanarequest.md) &gt; [query](./kibana-plugin-server.kibanarequest.query.md)
## KibanaRequest.query property
<b>Signature:</b>
```typescript
readonly query: Query;
```

View file

@ -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. |

View file

@ -0,0 +1,10 @@
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [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>;
```

View file

@ -0,0 +1,20 @@
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [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>() =&gt; OnRequestResult</code> | To pass request to the next handler |
| [redirected](./kibana-plugin-server.onrequesttoolkit.redirected.md) | <code>(url: string) =&gt; 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/>` }) =&gt; OnRequestResult</code> | Fail the request with specified error. |

View file

@ -0,0 +1,11 @@
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [OnRequestToolkit](./kibana-plugin-server.onrequesttoolkit.md) &gt; [next](./kibana-plugin-server.onrequesttoolkit.next.md)
## OnRequestToolkit.next property
To pass request to the next handler
<b>Signature:</b>
```typescript
next: () => OnRequestResult;
```

View file

@ -0,0 +1,11 @@
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [OnRequestToolkit](./kibana-plugin-server.onrequesttoolkit.md) &gt; [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;
```

View file

@ -0,0 +1,13 @@
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [OnRequestToolkit](./kibana-plugin-server.onrequesttoolkit.md) &gt; [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;
```

View file

@ -0,0 +1,12 @@
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [PluginSetupContext](./kibana-plugin-server.pluginsetupcontext.md) &gt; [http](./kibana-plugin-server.pluginsetupcontext.http.md)
## PluginSetupContext.http property
<b>Signature:</b>
```typescript
http: {
registerAuth: HttpServiceSetup['registerAuth'];
registerOnRequest: HttpServiceSetup['registerOnRequest'];
};
```

View file

@ -15,4 +15,5 @@ export interface PluginSetupContext
| Property | Type | Description |
| --- | --- | --- |
| [elasticsearch](./kibana-plugin-server.pluginsetupcontext.elasticsearch.md) | <code>{`<p/>` adminClient$: Observable&lt;ClusterClient&gt;;`<p/>` dataClient$: Observable&lt;ClusterClient&gt;;`<p/>` }</code> | |
| [http](./kibana-plugin-server.pluginsetupcontext.http.md) | <code>{`<p/>` registerAuth: HttpServiceSetup['registerAuth'];`<p/>` registerOnRequest: HttpServiceSetup['registerOnRequest'];`<p/>` }</code> | |

View file

@ -0,0 +1,23 @@
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [Router](./kibana-plugin-server.router.md) &gt; [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&lt;P, Q, B&gt;</code> | |
| handler | <code>RequestHandler&lt;P, Q, B&gt;</code> | |
<b>Returns:</b>
`void`

View file

@ -0,0 +1,23 @@
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [Router](./kibana-plugin-server.router.md) &gt; [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&lt;P, Q, B&gt;</code> | |
| handler | <code>RequestHandler&lt;P, Q, B&gt;</code> | |
<b>Returns:</b>
`void`

View file

@ -0,0 +1,17 @@
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [Router](./kibana-plugin-server.router.md) &gt; [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.

View file

@ -0,0 +1,28 @@
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [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&lt;Readonly&lt;RouterRoute&gt;&gt;</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 |

View file

@ -0,0 +1,9 @@
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [Router](./kibana-plugin-server.router.md) &gt; [path](./kibana-plugin-server.router.path.md)
## Router.path property
<b>Signature:</b>
```typescript
readonly path: string;
```

View file

@ -0,0 +1,23 @@
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [Router](./kibana-plugin-server.router.md) &gt; [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&lt;P, Q, B&gt;</code> | |
| handler | <code>RequestHandler&lt;P, Q, B&gt;</code> | |
<b>Returns:</b>
`void`

View file

@ -0,0 +1,23 @@
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [Router](./kibana-plugin-server.router.md) &gt; [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&lt;P, Q, B&gt;</code> | |
| handler | <code>RequestHandler&lt;P, Q, B&gt;</code> | |
<b>Returns:</b>
`void`

View file

@ -0,0 +1,9 @@
[Home](./index) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [Router](./kibana-plugin-server.router.md) &gt; [routes](./kibana-plugin-server.router.routes.md)
## Router.routes property
<b>Signature:</b>
```typescript
routes: Array<Readonly<RouterRoute>>;
```

View file

@ -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",

View 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);
},
};
}

View 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=/',
]);
});
});
});

View file

@ -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();
});

View file

@ -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');
}
}

View file

@ -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;
};

View file

@ -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 */

View file

@ -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';

View file

@ -0,0 +1,7 @@
{
"id": "dummy-on-request",
"version": "0.0.1",
"kibanaVersion": "kibana",
"ui": false,
"server": true
}

View file

@ -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();

View file

@ -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();
});
}
}

View file

@ -0,0 +1,7 @@
{
"id": "dummy-security",
"version": "0.0.1",
"kibanaVersion": "kibana",
"ui": false,
"server": true
}

View file

@ -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();

View file

@ -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';
},
};
}
}

View 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);
});
});
});
});

View 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);
});
});

View 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 });
}
};
}

View 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);
});
});

View 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 });
}
};
}

View file

@ -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[]) {

View file

@ -30,6 +30,7 @@ export interface RouterRoute {
handler: (req: Request, responseToolkit: ResponseToolkit) => Promise<ResponseObject>;
}
/** @public */
export class Router {
public routes: Array<Readonly<RouterRoute>> = [];

View 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>;
}

View file

@ -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 };

View file

@ -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)

View file

@ -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',

View file

@ -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,
});

View file

@ -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,
},
};
}

View file

@ -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 */

View file

@ -70,6 +70,7 @@ export class Server {
const pluginsSetup = await this.plugins.setup({
elasticsearch: elasticsearchServiceSetup,
http: httpSetup,
});
await this.legacy.setup({

View file

@ -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;
};

View file

@ -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,
},

View file

@ -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));
}
/**

View file

@ -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"