[NP] add http resources sub-service (#61797)

* add HttpResources basic implementation

* expose http resources to plugins

* add mocks

* move http resources to a separate service

* hide rendering service

* adopt internal types

* expose HttpResources service to plugins

* update platform mocks

* plugins start using HttpResources API

* remove RenderingServiceSetup export

* RenderingServiceSetup --> InternalRenderingServiceSetup

* improve types

* remove httpRespources leftovers from http service

* remove rendering types from RequestHanlderContext

* fix security plugin tests

* add unit tests for httpResources service

* add unit tests

* remove outdated cache-control header

* restructure http resources service

* merge getUiPlugins and discover

* static route declaration shouldnt require auth & validate

* update docs

* use HttpResources service instad of rendering

* address comments

* update docs

* roll back unnecessary changes

* use getVars for rendering

* dont pass app. it is not public API

* remove static registers

* update migration guide
This commit is contained in:
Mikhail Shustov 2020-04-16 16:09:39 +02:00 committed by GitHub
parent 721e4fae1b
commit af09fedaf2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
78 changed files with 1485 additions and 522 deletions

View file

@ -9,5 +9,7 @@
<b>Signature:</b>
```typescript
http: HttpServiceSetup;
http: HttpServiceSetup & {
resources: HttpResources;
};
```

View file

@ -20,7 +20,7 @@ export interface CoreSetup<TPluginsStart extends object = object, TStart = unkno
| [context](./kibana-plugin-core-server.coresetup.context.md) | <code>ContextSetup</code> | [ContextSetup](./kibana-plugin-core-server.contextsetup.md) |
| [elasticsearch](./kibana-plugin-core-server.coresetup.elasticsearch.md) | <code>ElasticsearchServiceSetup</code> | [ElasticsearchServiceSetup](./kibana-plugin-core-server.elasticsearchservicesetup.md) |
| [getStartServices](./kibana-plugin-core-server.coresetup.getstartservices.md) | <code>StartServicesAccessor&lt;TPluginsStart, TStart&gt;</code> | [StartServicesAccessor](./kibana-plugin-core-server.startservicesaccessor.md) |
| [http](./kibana-plugin-core-server.coresetup.http.md) | <code>HttpServiceSetup</code> | [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) |
| [http](./kibana-plugin-core-server.coresetup.http.md) | <code>HttpServiceSetup &amp; {</code><br/><code> resources: HttpResources;</code><br/><code> }</code> | [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) |
| [metrics](./kibana-plugin-core-server.coresetup.metrics.md) | <code>MetricsServiceSetup</code> | [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) |
| [savedObjects](./kibana-plugin-core-server.coresetup.savedobjects.md) | <code>SavedObjectsServiceSetup</code> | [SavedObjectsServiceSetup](./kibana-plugin-core-server.savedobjectsservicesetup.md) |
| [status](./kibana-plugin-core-server.coresetup.status.md) | <code>StatusServiceSetup</code> | [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) |

View file

@ -0,0 +1,20 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [HttpResources](./kibana-plugin-core-server.httpresources.md)
## HttpResources interface
HttpResources service is responsible for serving static &amp; dynamic assets for Kibana application via HTTP. Provides API allowing plug-ins to respond with: - a pre-configured HTML page bootstrapping Kibana client app - custom HTML page - custom JS script file.
<b>Signature:</b>
```typescript
export interface HttpResources
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [register](./kibana-plugin-core-server.httpresources.register.md) | <code>&lt;P, Q, B&gt;(route: RouteConfig&lt;P, Q, B, 'get'&gt;, handler: HttpResourcesRequestHandler&lt;P, Q, B&gt;) =&gt; void</code> | To register a route handler executing passed function to form response. |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [HttpResources](./kibana-plugin-core-server.httpresources.md) &gt; [register](./kibana-plugin-core-server.httpresources.register.md)
## HttpResources.register property
To register a route handler executing passed function to form response.
<b>Signature:</b>
```typescript
register: <P, Q, B>(route: RouteConfig<P, Q, B, 'get'>, handler: HttpResourcesRequestHandler<P, Q, B>) => void;
```

View file

@ -0,0 +1,18 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [HttpResourcesRenderOptions](./kibana-plugin-core-server.httpresourcesrenderoptions.md) &gt; [headers](./kibana-plugin-core-server.httpresourcesrenderoptions.headers.md)
## HttpResourcesRenderOptions.headers property
HTTP Headers with additional information about response.
<b>Signature:</b>
```typescript
headers?: ResponseHeaders;
```
## Remarks
All HTML pages are already pre-configured with `content-security-policy` header that cannot be overridden.

View file

@ -0,0 +1,20 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [HttpResourcesRenderOptions](./kibana-plugin-core-server.httpresourcesrenderoptions.md)
## HttpResourcesRenderOptions interface
Allows to configure HTTP response parameters
<b>Signature:</b>
```typescript
export interface HttpResourcesRenderOptions
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [headers](./kibana-plugin-core-server.httpresourcesrenderoptions.headers.md) | <code>ResponseHeaders</code> | HTTP Headers with additional information about response. |

View file

@ -0,0 +1,18 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [HttpResourcesRequestHandler](./kibana-plugin-core-server.httpresourcesrequesthandler.md)
## HttpResourcesRequestHandler type
Extended version of [RequestHandler](./kibana-plugin-core-server.requesthandler.md) having access to [HttpResourcesServiceToolkit](./kibana-plugin-core-server.httpresourcesservicetoolkit.md) to respond with HTML or JS resources.
<b>Signature:</b>
```typescript
export declare type HttpResourcesRequestHandler<P = unknown, Q = unknown, B = unknown> = RequestHandler<P, Q, B, 'get', KibanaResponseFactory & HttpResourcesServiceToolkit>;
```
## Example
\`\`\`<!-- -->typescript httpResources.register(<!-- -->{ path: '/login', validate: { params: schema.object(<!-- -->{ id: schema.string() }<!-- -->), }<!-- -->, }<!-- -->, async (context, request, response) =<!-- -->&gt; { //.. return response.renderCoreApp(); }<!-- -->);

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [HttpResourcesResponseOptions](./kibana-plugin-core-server.httpresourcesresponseoptions.md)
## HttpResourcesResponseOptions type
HTTP Resources response parameters
<b>Signature:</b>
```typescript
export declare type HttpResourcesResponseOptions = HttpResponseOptions;
```

View file

@ -0,0 +1,23 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [HttpResourcesServiceToolkit](./kibana-plugin-core-server.httpresourcesservicetoolkit.md)
## HttpResourcesServiceToolkit interface
Extended set of [KibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) helpers used to respond with HTML or JS resource.
<b>Signature:</b>
```typescript
export interface HttpResourcesServiceToolkit
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [renderAnonymousCoreApp](./kibana-plugin-core-server.httpresourcesservicetoolkit.renderanonymouscoreapp.md) | <code>(options?: HttpResourcesRenderOptions) =&gt; Promise&lt;IKibanaResponse&gt;</code> | To respond with HTML page bootstrapping Kibana application without retrieving user-specific information. |
| [renderCoreApp](./kibana-plugin-core-server.httpresourcesservicetoolkit.rendercoreapp.md) | <code>(options?: HttpResourcesRenderOptions) =&gt; Promise&lt;IKibanaResponse&gt;</code> | To respond with HTML page bootstrapping Kibana application. |
| [renderHtml](./kibana-plugin-core-server.httpresourcesservicetoolkit.renderhtml.md) | <code>(options: HttpResourcesResponseOptions) =&gt; IKibanaResponse</code> | To respond with a custom HTML page. |
| [renderJs](./kibana-plugin-core-server.httpresourcesservicetoolkit.renderjs.md) | <code>(options: HttpResourcesResponseOptions) =&gt; IKibanaResponse</code> | To respond with a custom JS script file. |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [HttpResourcesServiceToolkit](./kibana-plugin-core-server.httpresourcesservicetoolkit.md) &gt; [renderAnonymousCoreApp](./kibana-plugin-core-server.httpresourcesservicetoolkit.renderanonymouscoreapp.md)
## HttpResourcesServiceToolkit.renderAnonymousCoreApp property
To respond with HTML page bootstrapping Kibana application without retrieving user-specific information.
<b>Signature:</b>
```typescript
renderAnonymousCoreApp: (options?: HttpResourcesRenderOptions) => Promise<IKibanaResponse>;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [HttpResourcesServiceToolkit](./kibana-plugin-core-server.httpresourcesservicetoolkit.md) &gt; [renderCoreApp](./kibana-plugin-core-server.httpresourcesservicetoolkit.rendercoreapp.md)
## HttpResourcesServiceToolkit.renderCoreApp property
To respond with HTML page bootstrapping Kibana application.
<b>Signature:</b>
```typescript
renderCoreApp: (options?: HttpResourcesRenderOptions) => Promise<IKibanaResponse>;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [HttpResourcesServiceToolkit](./kibana-plugin-core-server.httpresourcesservicetoolkit.md) &gt; [renderHtml](./kibana-plugin-core-server.httpresourcesservicetoolkit.renderhtml.md)
## HttpResourcesServiceToolkit.renderHtml property
To respond with a custom HTML page.
<b>Signature:</b>
```typescript
renderHtml: (options: HttpResourcesResponseOptions) => IKibanaResponse;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [HttpResourcesServiceToolkit](./kibana-plugin-core-server.httpresourcesservicetoolkit.md) &gt; [renderJs](./kibana-plugin-core-server.httpresourcesservicetoolkit.renderjs.md)
## HttpResourcesServiceToolkit.renderJs property
To respond with a custom JS script file.
<b>Signature:</b>
```typescript
renderJs: (options: HttpResourcesResponseOptions) => IKibanaResponse;
```

View file

@ -9,5 +9,5 @@ Wrap a router handler to catch and converts legacy boom errors to proper custom
<b>Signature:</b>
```typescript
handleLegacyErrors: <P, Q, B>(handler: RequestHandler<P, Q, B>) => RequestHandler<P, Q, B>;
handleLegacyErrors: RequestHandlerWrapper;
```

View file

@ -18,7 +18,7 @@ export interface IRouter
| --- | --- | --- |
| [delete](./kibana-plugin-core-server.irouter.delete.md) | <code>RouteRegistrar&lt;'delete'&gt;</code> | Register a route handler for <code>DELETE</code> request. |
| [get](./kibana-plugin-core-server.irouter.get.md) | <code>RouteRegistrar&lt;'get'&gt;</code> | Register a route handler for <code>GET</code> request. |
| [handleLegacyErrors](./kibana-plugin-core-server.irouter.handlelegacyerrors.md) | <code>&lt;P, Q, B&gt;(handler: RequestHandler&lt;P, Q, B&gt;) =&gt; RequestHandler&lt;P, Q, B&gt;</code> | Wrap a router handler to catch and converts legacy boom errors to proper custom errors. |
| [handleLegacyErrors](./kibana-plugin-core-server.irouter.handlelegacyerrors.md) | <code>RequestHandlerWrapper</code> | Wrap a router handler to catch and converts legacy boom errors to proper custom errors. |
| [patch](./kibana-plugin-core-server.irouter.patch.md) | <code>RouteRegistrar&lt;'patch'&gt;</code> | Register a route handler for <code>PATCH</code> request. |
| [post](./kibana-plugin-core-server.irouter.post.md) | <code>RouteRegistrar&lt;'post'&gt;</code> | Register a route handler for <code>POST</code> request. |
| [put](./kibana-plugin-core-server.irouter.put.md) | <code>RouteRegistrar&lt;'put'&gt;</code> | Register a route handler for <code>PUT</code> request. |

View file

@ -1,19 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [IScopedRenderingClient](./kibana-plugin-core-server.iscopedrenderingclient.md)
## IScopedRenderingClient interface
<b>Signature:</b>
```typescript
export interface IScopedRenderingClient
```
## Methods
| Method | Description |
| --- | --- |
| [render(options)](./kibana-plugin-core-server.iscopedrenderingclient.render.md) | Generate a <code>KibanaResponse</code> which renders an HTML page bootstrapped with the <code>core</code> bundle. Intended as a response body for HTTP route handlers. |

View file

@ -1,41 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [IScopedRenderingClient](./kibana-plugin-core-server.iscopedrenderingclient.md) &gt; [render](./kibana-plugin-core-server.iscopedrenderingclient.render.md)
## IScopedRenderingClient.render() method
Generate a `KibanaResponse` which renders an HTML page bootstrapped with the `core` bundle. Intended as a response body for HTTP route handlers.
<b>Signature:</b>
```typescript
render(options?: Pick<IRenderOptions, 'includeUserSettings'>): Promise<string>;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| options | <code>Pick&lt;IRenderOptions, 'includeUserSettings'&gt;</code> | |
<b>Returns:</b>
`Promise<string>`
## Example
```ts
router.get(
{ path: '/', validate: false },
(context, request, response) =>
response.ok({
body: await context.core.rendering.render(),
headers: {
'content-security-policy': context.core.http.csp.header,
},
})
);
```

View file

@ -20,4 +20,5 @@ export interface LegacyServiceSetupDeps
| --- | --- | --- |
| [core](./kibana-plugin-core-server.legacyservicesetupdeps.core.md) | <code>LegacyCoreSetup</code> | |
| [plugins](./kibana-plugin-core-server.legacyservicesetupdeps.plugins.md) | <code>Record&lt;string, unknown&gt;</code> | |
| [uiPlugins](./kibana-plugin-core-server.legacyservicesetupdeps.uiplugins.md) | <code>UiPlugins</code> | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [LegacyServiceSetupDeps](./kibana-plugin-core-server.legacyservicesetupdeps.md) &gt; [uiPlugins](./kibana-plugin-core-server.legacyservicesetupdeps.uiplugins.md)
## LegacyServiceSetupDeps.uiPlugins property
<b>Signature:</b>
```typescript
uiPlugins: UiPlugins;
```

View file

@ -80,6 +80,9 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [EnvironmentMode](./kibana-plugin-core-server.environmentmode.md) | |
| [ErrorHttpResponseOptions](./kibana-plugin-core-server.errorhttpresponseoptions.md) | HTTP response parameters |
| [FakeRequest](./kibana-plugin-core-server.fakerequest.md) | Fake request object created manually by Kibana plugins. |
| [HttpResources](./kibana-plugin-core-server.httpresources.md) | HttpResources service is responsible for serving static &amp; dynamic assets for Kibana application via HTTP. Provides API allowing plug-ins to respond with: - a pre-configured HTML page bootstrapping Kibana client app - custom HTML page - custom JS script file. |
| [HttpResourcesRenderOptions](./kibana-plugin-core-server.httpresourcesrenderoptions.md) | Allows to configure HTTP response parameters |
| [HttpResourcesServiceToolkit](./kibana-plugin-core-server.httpresourcesservicetoolkit.md) | Extended set of [KibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) helpers used to respond with HTML or JS resource. |
| [HttpResponseOptions](./kibana-plugin-core-server.httpresponseoptions.md) | HTTP response parameters |
| [HttpServerInfo](./kibana-plugin-core-server.httpserverinfo.md) | |
| [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) | Kibana HTTP Service provides own abstraction for work with HTTP stack. Plugins don't have direct access to <code>hapi</code> server and its primitives anymore. Moreover, plugins shouldn't rely on the fact that HTTP Service uses one or another library under the hood. This gives the platform flexibility to upgrade or changing our internal HTTP stack without breaking plugins. If the HTTP Service lacks functionality you need, we are happy to discuss and support your needs. |
@ -92,7 +95,6 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [IndexSettingsDeprecationInfo](./kibana-plugin-core-server.indexsettingsdeprecationinfo.md) | |
| [IRenderOptions](./kibana-plugin-core-server.irenderoptions.md) | |
| [IRouter](./kibana-plugin-core-server.irouter.md) | Registers route handlers for specified resource path and method. See [RouteConfig](./kibana-plugin-core-server.routeconfig.md) and [RequestHandler](./kibana-plugin-core-server.requesthandler.md) for more information about arguments to route registrations. |
| [IScopedRenderingClient](./kibana-plugin-core-server.iscopedrenderingclient.md) | |
| [IUiSettingsClient](./kibana-plugin-core-server.iuisettingsclient.md) | Server-side client that provides access to the advanced settings stored in elasticsearch. The settings provide control over the behavior of the Kibana application. For example, a user can specify how to display numeric or date fields. Users can adjust the settings via Management UI. |
| [KibanaRequestEvents](./kibana-plugin-core-server.kibanarequestevents.md) | Request events. |
| [KibanaRequestRoute](./kibana-plugin-core-server.kibanarequestroute.md) | Request specific route information exposed to a handler. |
@ -118,7 +120,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [PluginConfigDescriptor](./kibana-plugin-core-server.pluginconfigdescriptor.md) | Describes a plugin configuration properties. |
| [PluginInitializerContext](./kibana-plugin-core-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. |
| [PluginManifest](./kibana-plugin-core-server.pluginmanifest.md) | Describes the set of required and optional properties plugin can define in its mandatory JSON manifest file. |
| [RequestHandlerContext](./kibana-plugin-core-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.<!-- -->Provides the following clients and services: - [rendering](./kibana-plugin-core-server.iscopedrenderingclient.md) - Rendering client which uses the data of the incoming request - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.dataClient](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request |
| [RequestHandlerContext](./kibana-plugin-core-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.<!-- -->Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.dataClient](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request |
| [RouteConfig](./kibana-plugin-core-server.routeconfig.md) | Route specific configuration. |
| [RouteConfigOptions](./kibana-plugin-core-server.routeconfigoptions.md) | Additional route options. |
| [RouteConfigOptionsBody](./kibana-plugin-core-server.routeconfigoptionsbody.md) | Additional body options for a route |
@ -216,6 +218,8 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [HandlerFunction](./kibana-plugin-core-server.handlerfunction.md) | A function that accepts a context object and an optional number of additional arguments. Used for the generic types in [IContextContainer](./kibana-plugin-core-server.icontextcontainer.md) |
| [HandlerParameters](./kibana-plugin-core-server.handlerparameters.md) | Extracts the types of the additional arguments of a [HandlerFunction](./kibana-plugin-core-server.handlerfunction.md)<!-- -->, excluding the [HandlerContextType](./kibana-plugin-core-server.handlercontexttype.md)<!-- -->. |
| [Headers](./kibana-plugin-core-server.headers.md) | Http request headers to read. |
| [HttpResourcesRequestHandler](./kibana-plugin-core-server.httpresourcesrequesthandler.md) | Extended version of [RequestHandler](./kibana-plugin-core-server.requesthandler.md) having access to [HttpResourcesServiceToolkit](./kibana-plugin-core-server.httpresourcesservicetoolkit.md) to respond with HTML or JS resources. |
| [HttpResourcesResponseOptions](./kibana-plugin-core-server.httpresourcesresponseoptions.md) | HTTP Resources response parameters |
| [HttpResponsePayload](./kibana-plugin-core-server.httpresponsepayload.md) | Data send to the client as a response payload. |
| [IBasePath](./kibana-plugin-core-server.ibasepath.md) | Access or manipulate the Kibana base path[BasePath](./kibana-plugin-core-server.basepath.md) |
| [IClusterClient](./kibana-plugin-core-server.iclusterclient.md) | Represents an Elasticsearch cluster API client created by the platform. It 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>).<!-- -->See [ClusterClient](./kibana-plugin-core-server.clusterclient.md)<!-- -->. |
@ -245,6 +249,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [RequestHandler](./kibana-plugin-core-server.requesthandler.md) | A function executed when route path matched requested resource path. Request handler is expected to return a result of one of [KibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) functions. |
| [RequestHandlerContextContainer](./kibana-plugin-core-server.requesthandlercontextcontainer.md) | An object that handles registration of http request context providers. |
| [RequestHandlerContextProvider](./kibana-plugin-core-server.requesthandlercontextprovider.md) | Context provider for request handler. Extends request context object with provided functionality or data. |
| [RequestHandlerWrapper](./kibana-plugin-core-server.requesthandlerwrapper.md) | Type-safe wrapper for [RequestHandler](./kibana-plugin-core-server.requesthandler.md) function. |
| [ResponseError](./kibana-plugin-core-server.responseerror.md) | Error message and optional data send to the client in case of error. |
| [ResponseErrorAttributes](./kibana-plugin-core-server.responseerrorattributes.md) | Additional data to provide error details. |
| [ResponseHeaders](./kibana-plugin-core-server.responseheaders.md) | Http response headers to set. |

View file

@ -9,7 +9,7 @@ A function executed when route path matched requested resource path. Request han
<b>Signature:</b>
```typescript
export declare type RequestHandler<P = unknown, Q = unknown, B = unknown, Method extends RouteMethod = any> = (context: RequestHandlerContext, request: KibanaRequest<P, Q, B, Method>, response: KibanaResponseFactory) => IKibanaResponse<any> | Promise<IKibanaResponse<any>>;
export declare type RequestHandler<P = unknown, Q = unknown, B = unknown, Method extends RouteMethod = any, ResponseFactory extends KibanaResponseFactory = KibanaResponseFactory> = (context: RequestHandlerContext, request: KibanaRequest<P, Q, B, Method>, response: ResponseFactory) => IKibanaResponse<any> | Promise<IKibanaResponse<any>>;
```
## Example

View file

@ -8,7 +8,6 @@
```typescript
core: {
rendering: IScopedRenderingClient;
savedObjects: {
client: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;

View file

@ -6,7 +6,7 @@
Plugin specific context passed to a route handler.
Provides the following clients and services: - [rendering](./kibana-plugin-core-server.iscopedrenderingclient.md) - Rendering client which uses the data of the incoming request - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.dataClient](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request
Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.dataClient](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-core-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request
<b>Signature:</b>
@ -18,5 +18,5 @@ export interface RequestHandlerContext
| Property | Type | Description |
| --- | --- | --- |
| [core](./kibana-plugin-core-server.requesthandlercontext.core.md) | <code>{</code><br/><code> rendering: IScopedRenderingClient;</code><br/><code> savedObjects: {</code><br/><code> client: SavedObjectsClientContract;</code><br/><code> typeRegistry: ISavedObjectTypeRegistry;</code><br/><code> };</code><br/><code> elasticsearch: {</code><br/><code> dataClient: IScopedClusterClient;</code><br/><code> adminClient: IScopedClusterClient;</code><br/><code> };</code><br/><code> uiSettings: {</code><br/><code> client: IUiSettingsClient;</code><br/><code> };</code><br/><code> }</code> | |
| [core](./kibana-plugin-core-server.requesthandlercontext.core.md) | <code>{</code><br/><code> savedObjects: {</code><br/><code> client: SavedObjectsClientContract;</code><br/><code> typeRegistry: ISavedObjectTypeRegistry;</code><br/><code> };</code><br/><code> elasticsearch: {</code><br/><code> dataClient: IScopedClusterClient;</code><br/><code> adminClient: IScopedClusterClient;</code><br/><code> };</code><br/><code> uiSettings: {</code><br/><code> client: IUiSettingsClient;</code><br/><code> };</code><br/><code> }</code> | |

View file

@ -0,0 +1,27 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [RequestHandlerWrapper](./kibana-plugin-core-server.requesthandlerwrapper.md)
## RequestHandlerWrapper type
Type-safe wrapper for [RequestHandler](./kibana-plugin-core-server.requesthandler.md) function.
<b>Signature:</b>
```typescript
export declare type RequestHandlerWrapper = <P, Q, B, Method extends RouteMethod = any, ResponseFactory extends KibanaResponseFactory = KibanaResponseFactory>(handler: RequestHandler<P, Q, B, Method, ResponseFactory>) => RequestHandler<P, Q, B, Method, ResponseFactory>;
```
## Example
```typescript
export const wrapper: RequestHandlerWrapper = handler => {
return async (context, request, response) => {
// do some logic
...
};
}
```

View file

@ -9,9 +9,5 @@ Http response headers to set.
<b>Signature:</b>
```typescript
export declare type ResponseHeaders = {
[header in KnownHeaders]?: string | string[];
} & {
[header: string]: string | string[];
};
export declare type ResponseHeaders = Record<KnownHeaders, string | string[]> | Record<string, string | string[]>;
```

View file

@ -1252,26 +1252,27 @@ import { npStart: { plugins } } from 'ui/new_platform';
In server code, `core` can be accessed from either `server.newPlatform` or `kbnServer.newPlatform`. There are not currently very many services available on the server-side:
| Legacy Platform | New Platform | Notes |
| ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| `server.config()` | [`initializerContext.config.create()`](/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.config.md) | Must also define schema. See _[how to configure plugin](#configure-plugin)_ |
| `server.route` | [`core.http.createRouter`](/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.createrouter.md) | [Examples](./MIGRATION_EXAMPLES.md#route-registration) |
| `server.renderApp()` / `server.renderAppWithDefaultConfig()` | [`context.rendering.render()`](/docs/development/core/server/kibana-plugin-core-server.iscopedrenderingclient.render.md) | [Examples](./MIGRATION_EXAMPLES.md#render-html-content) |
| `request.getBasePath()` | [`core.http.basePath.get`](/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.basepath.md) | |
| Legacy Platform | New Platform | Notes |
| ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| `server.config()` | [`initializerContext.config.create()`](/docs/development/core/server/kibana-plugin-core-server.plugininitializercontext.config.md) | Must also define schema. See _[how to configure plugin](#configure-plugin)_ |
| `server.route` | [`core.http.createRouter`](/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.createrouter.md) | [Examples](./MIGRATION_EXAMPLES.md#route-registration) |
| `server.renderApp()` | [`response.renderCoreApp()`](docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.rendercoreapp.md) | [Examples](./MIGRATION_EXAMPLES.md#render-html-content) |
| `server.renderAppWithDefaultConfig()` | [`response.renderAnonymousCoreApp()`](docs/development/core/server/kibana-plugin-core-server.httpresourcesservicetoolkit.renderanonymouscoreapp.md) | [Examples](./MIGRATION_EXAMPLES.md#render-html-content) |
| `request.getBasePath()` | [`core.http.basePath.get`](/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.basepath.md) | |
| `server.plugins.elasticsearch.getCluster('data')` | [`context.core.elasticsearch.dataClient`](/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.md) | |
| `server.plugins.elasticsearch.getCluster('admin')` | [`context.core.elasticsearch.adminClient`](/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.md) | |
| `server.plugins.elasticsearch.createCluster(...)` | [`core.elasticsearch.legacy.createClient`](/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.legacy.md) | |
| `server.savedObjects.setScopedSavedObjectsClientFactory` | [`core.savedObjects.setClientFactoryProvider`](/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.setclientfactoryprovider.md) | |
| `server.savedObjects.addScopedSavedObjectsClientWrapperFactory` | [`core.savedObjects.addClientWrapper`](/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.addclientwrapper.md) | |
| `server.plugins.elasticsearch.createCluster(...)` | [`core.elasticsearch.legacy.createClient`](/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.legacy.md) | |
| `server.savedObjects.setScopedSavedObjectsClientFactory` | [`core.savedObjects.setClientFactoryProvider`](/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.setclientfactoryprovider.md) | |
| `server.savedObjects.addScopedSavedObjectsClientWrapperFactory` | [`core.savedObjects.addClientWrapper`](/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.addclientwrapper.md) | |
| `server.savedObjects.getSavedObjectsRepository` | [`core.savedObjects.createInternalRepository`](/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.createinternalrepository.md) [`core.savedObjects.createScopedRepository`](/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.createscopedrepository.md) | |
| `server.savedObjects.getScopedSavedObjectsClient` | [`core.savedObjects.getScopedClient`](/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.getscopedclient.md) | |
| `request.getSavedObjectsClient` | [`context.core.savedObjects.client`](/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md) | |
| `server.savedObjects.getScopedSavedObjectsClient` | [`core.savedObjects.getScopedClient`](/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicestart.getscopedclient.md) | |
| `request.getSavedObjectsClient` | [`context.core.savedObjects.client`](/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md) | |
| `request.getUiSettingsService` | [`context.core.uiSettings.client`](/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.md) | |
| `kibana.Plugin.deprecations` | [Handle plugin configuration deprecations](#handle-plugin-config-deprecations) and [`PluginConfigDescriptor.deprecations`](docs/development/core/server/kibana-plugin-core-server.pluginconfigdescriptor.md) | Deprecations from New Platform are not applied to legacy configuration |
| `kibana.Plugin.savedObjectSchemas` | [`core.savedObjects.registerType`](docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md) | [Examples](./MIGRATION_EXAMPLES.md#saved-objects-types) |
| `kibana.Plugin.mappings` | [`core.savedObjects.registerType`](docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md) | [Examples](./MIGRATION_EXAMPLES.md#saved-objects-types) |
| `kibana.Plugin.migrations` | [`core.savedObjects.registerType`](docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md) | [Examples](./MIGRATION_EXAMPLES.md#saved-objects-types) |
| `kibana.Plugin.savedObjectsManagement` | [`core.savedObjects.registerType`](docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md) | [Examples](./MIGRATION_EXAMPLES.md#saved-objects-types) |
| `kibana.Plugin.deprecations` | [Handle plugin configuration deprecations](#handle-plugin-config-deprecations) and [`PluginConfigDescriptor.deprecations`](docs/development/core/server/kibana-plugin-core-server.pluginconfigdescriptor.md) | Deprecations from New Platform are not applied to legacy configuration |
| `kibana.Plugin.savedObjectSchemas` | [`core.savedObjects.registerType`](docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md) | [Examples](./MIGRATION_EXAMPLES.md#saved-objects-types) |
| `kibana.Plugin.mappings` | [`core.savedObjects.registerType`](docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md) | [Examples](./MIGRATION_EXAMPLES.md#saved-objects-types) |
| `kibana.Plugin.migrations` | [`core.savedObjects.registerType`](docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md) | [Examples](./MIGRATION_EXAMPLES.md#saved-objects-types) |
| `kibana.Plugin.savedObjectsManagement` | [`core.savedObjects.registerType`](docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md) | [Examples](./MIGRATION_EXAMPLES.md#saved-objects-types) |
_See also: [Server's CoreSetup API Docs](/docs/development/core/server/kibana-plugin-core-server.coresetup.md)_
@ -1494,8 +1495,9 @@ The above example looks in the new platform as
```
The [request handler context](/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md) exposed the next scoped **core** services:
| Legacy Platform | New Platform |
| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------|
| Legacy Platform | New Platform |
| --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `request.getSavedObjectsClient` | [`context.savedObjects.client`](/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md) |
| `server.plugins.elasticsearch.getCluster('admin')` | [`context.elasticsearch.adminClient`](/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.md) |
| `server.plugins.elasticsearch.getCluster('data')` | [`context.elasticsearch.dataClient`](/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.md) |

View file

@ -700,21 +700,15 @@ application.register({
## Render HTML Content
You can return a blank HTML page bootstrapped with the core application bundle from an HTTP route handler
via the `rendering` context. You may wish to do this if you are rendering a chromeless application with a
via the `httpResources` service. You may wish to do this if you are rendering a chromeless application with a
custom application route or have other custom rendering needs.
```ts
router.get(
```typescript
httpResources.register(
{ path: '/chromeless', validate: false },
(context, request, response) => {
const { http, rendering } = context.core;
return response.ok({
body: await rendering.render(), // generates an HTML document
headers: {
'content-security-policy': http.csp.header,
},
});
//... some logic
return response.renderCoreApp();
}
);
```
@ -724,18 +718,12 @@ comprises all UI Settings that are *user provided*, then injected into the page.
You may wish to exclude fetching this data if not authorized or to slim the page
size.
```ts
router.get(
{ path: '/', validate: false },
```typescript
httpResources.register(
{ path: '/', validate: false, options: { authRequired: false } },
(context, request, response) => {
const { http, rendering } = context.core;
return response.ok({
body: await rendering.render({ includeUserSettings: false }),
headers: {
'content-security-policy': http.csp.header,
},
});
//... some logic
return response.renderAnonymousCoreApp();
}
);
```

View file

@ -38,6 +38,7 @@ export {
LifecycleResponseFactory,
RedirectResponseOptions,
RequestHandler,
RequestHandlerWrapper,
ResponseError,
ResponseErrorAttributes,
ResponseHeaders,

View file

@ -148,7 +148,7 @@ function findHeadersIntersection(
log: Logger
) {
Object.keys(headers).forEach(headerName => {
if (responseHeaders[headerName] !== undefined) {
if (Reflect.has(responseHeaders, headerName)) {
log.warn(`onPreResponseHandler rewrote a response header [${headerName}].`);
}
});

View file

@ -18,20 +18,10 @@
*/
import Boom from 'boom';
import { KibanaRequest } from './request';
import { KibanaResponseFactory } from './response';
import { RequestHandler } from './router';
import { RequestHandlerContext } from '../../../server';
import { RouteMethod } from './route';
import { RequestHandlerWrapper } from './router';
export const wrapErrors = <P, Q, B>(
handler: RequestHandler<P, Q, B, RouteMethod>
): RequestHandler<P, Q, B, RouteMethod> => {
return async (
context: RequestHandlerContext,
request: KibanaRequest<P, Q, B, RouteMethod>,
response: KibanaResponseFactory
) => {
export const wrapErrors: RequestHandlerWrapper = handler => {
return async (context, request, response) => {
try {
return await handler(context, request, response);
} catch (e) {

View file

@ -56,9 +56,9 @@ export type Headers = { [header in KnownHeaders]?: string | string[] | undefined
* Http response headers to set.
* @public
*/
export type ResponseHeaders = { [header in KnownHeaders]?: string | string[] } & {
[header: string]: string | string[];
};
export type ResponseHeaders =
| Record<KnownHeaders, string | string[]>
| Record<string, string | string[]>;
const normalizeHeaderField = (field: string) => field.trim().toLowerCase();

View file

@ -18,7 +18,7 @@
*/
export { Headers, filterHeaders, ResponseHeaders, KnownHeaders } from './headers';
export { Router, RequestHandler, IRouter, RouteRegistrar } from './router';
export { Router, RequestHandler, RequestHandlerWrapper, IRouter, RouteRegistrar } from './router';
export {
KibanaRequest,
KibanaRequestEvents,

View file

@ -98,7 +98,7 @@ export interface IRouter {
* Wrap a router handler to catch and converts legacy boom errors to proper custom errors.
* @param handler {@link RequestHandler} - a route handler to wrap
*/
handleLegacyErrors: <P, Q, B>(handler: RequestHandler<P, Q, B>) => RequestHandler<P, Q, B>;
handleLegacyErrors: RequestHandlerWrapper;
/**
* Returns all routes registered with this router.
@ -237,9 +237,7 @@ export class Router implements IRouter {
return [...this.routes];
}
public handleLegacyErrors<P, Q, B>(handler: RequestHandler<P, Q, B>): RequestHandler<P, Q, B> {
return wrapErrors(handler);
}
public handleLegacyErrors = wrapErrors;
private async handle<P, Q, B>({
routeSchemas,
@ -316,9 +314,33 @@ export type RequestHandler<
P = unknown,
Q = unknown,
B = unknown,
Method extends RouteMethod = any
Method extends RouteMethod = any,
ResponseFactory extends KibanaResponseFactory = KibanaResponseFactory
> = (
context: RequestHandlerContext,
request: KibanaRequest<P, Q, B, Method>,
response: KibanaResponseFactory
response: ResponseFactory
) => IKibanaResponse<any> | Promise<IKibanaResponse<any>>;
/**
* Type-safe wrapper for {@link RequestHandler} function.
* @example
* ```typescript
* export const wrapper: RequestHandlerWrapper = handler => {
* return async (context, request, response) => {
* // do some logic
* ...
* };
* }
* ```
* @public
*/
export type RequestHandlerWrapper = <
P,
Q,
B,
Method extends RouteMethod = any,
ResponseFactory extends KibanaResponseFactory = KibanaResponseFactory
>(
handler: RequestHandler<P, Q, B, Method, ResponseFactory>
) => RequestHandler<P, Q, B, Method, ResponseFactory>;

View file

@ -0,0 +1,50 @@
/*
* 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 { httpServerMock } from '../http/http_server.mocks';
import { HttpResources, HttpResourcesServiceToolkit } from './types';
const createHttpResourcesMock = (): jest.Mocked<HttpResources> => ({
register: jest.fn(),
});
function createInternalHttpResourcesSetup() {
return {
createRegistrar: createHttpResourcesMock,
};
}
function createHttpResourcesResponseFactory() {
const mocked: jest.Mocked<HttpResourcesServiceToolkit> = {
renderCoreApp: jest.fn(),
renderAnonymousCoreApp: jest.fn(),
renderHtml: jest.fn(),
renderJs: jest.fn(),
};
return {
...httpServerMock.createResponseFactory(),
...mocked,
};
}
export const httpResourcesMock = {
createRegistrar: createHttpResourcesMock,
createSetupContract: createInternalHttpResourcesSetup,
createResponseFactory: createHttpResourcesResponseFactory,
};

View file

@ -0,0 +1,258 @@
/*
* 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 { IRouter, RouteConfig } from '../http';
import { coreMock } from '../mocks';
import { mockCoreContext } from '../core_context.mock';
import { httpServiceMock } from '../http/http_service.mock';
import { httpServerMock } from '../http/http_server.mocks';
import { renderingMock } from '../rendering/rendering_service.mock';
import { HttpResourcesService, SetupDeps } from './http_resources_service';
import { httpResourcesMock } from './http_resources_service.mock';
const coreContext = mockCoreContext.create();
describe('HttpResources service', () => {
let service: HttpResourcesService;
let setupDeps: SetupDeps;
let router: jest.Mocked<IRouter>;
const kibanaRequest = httpServerMock.createKibanaRequest();
const context = { core: coreMock.createRequestHandlerContext() };
describe('#createRegistrar', () => {
beforeEach(() => {
setupDeps = {
http: httpServiceMock.createSetupContract(),
rendering: renderingMock.createSetupContract(),
};
service = new HttpResourcesService(coreContext);
router = httpServiceMock.createRouter();
});
describe('register', () => {
describe('renderCoreApp', () => {
it('formats successful response', async () => {
const routeConfig: RouteConfig<any, any, any, 'get'> = { path: '/', validate: false };
const { createRegistrar } = await service.setup(setupDeps);
const { register } = createRegistrar(router);
register(routeConfig, async (ctx, req, res) => {
return res.renderCoreApp();
});
const [[, routeHandler]] = router.get.mock.calls;
const responseFactory = httpResourcesMock.createResponseFactory();
await routeHandler(context, kibanaRequest, responseFactory);
expect(setupDeps.rendering.render).toHaveBeenCalledWith(
kibanaRequest,
context.core.uiSettings.client,
{
includeUserSettings: true,
}
);
});
it('can attach headers, except the CSP header', async () => {
const routeConfig: RouteConfig<any, any, any, 'get'> = { path: '/', validate: false };
const { createRegistrar } = await service.setup(setupDeps);
const { register } = createRegistrar(router);
register(routeConfig, async (ctx, req, res) => {
return res.renderCoreApp({
headers: {
'content-security-policy': "script-src 'unsafe-eval'",
'x-kibana': '42',
},
});
});
const [[, routeHandler]] = router.get.mock.calls;
const responseFactory = httpResourcesMock.createResponseFactory();
await routeHandler(context, kibanaRequest, responseFactory);
expect(responseFactory.ok).toHaveBeenCalledWith({
body: '<body />',
headers: {
'x-kibana': '42',
'content-security-policy':
"script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'",
},
});
});
});
describe('renderAnonymousCoreApp', () => {
it('formats successful response', async () => {
const routeConfig: RouteConfig<any, any, any, 'get'> = { path: '/', validate: false };
const { createRegistrar } = await service.setup(setupDeps);
const { register } = createRegistrar(router);
register(routeConfig, async (ctx, req, res) => {
return res.renderAnonymousCoreApp();
});
const [[, routeHandler]] = router.get.mock.calls;
const responseFactory = httpResourcesMock.createResponseFactory();
await routeHandler(context, kibanaRequest, responseFactory);
expect(setupDeps.rendering.render).toHaveBeenCalledWith(
kibanaRequest,
context.core.uiSettings.client,
{
includeUserSettings: false,
}
);
});
it('can attach headers, except the CSP header', async () => {
const routeConfig: RouteConfig<any, any, any, 'get'> = { path: '/', validate: false };
const { createRegistrar } = await service.setup(setupDeps);
const { register } = createRegistrar(router);
register(routeConfig, async (ctx, req, res) => {
return res.renderAnonymousCoreApp({
headers: {
'content-security-policy': "script-src 'unsafe-eval'",
'x-kibana': '42',
},
});
});
const [[, routeHandler]] = router.get.mock.calls;
const responseFactory = httpResourcesMock.createResponseFactory();
await routeHandler(context, kibanaRequest, responseFactory);
expect(responseFactory.ok).toHaveBeenCalledWith({
body: '<body />',
headers: {
'x-kibana': '42',
'content-security-policy':
"script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'",
},
});
});
});
describe('renderHtml', () => {
it('formats successful response', async () => {
const htmlBody = '<html><body /></html>';
const routeConfig: RouteConfig<any, any, any, 'get'> = { path: '/', validate: false };
const { createRegistrar } = await service.setup(setupDeps);
const { register } = createRegistrar(router);
register(routeConfig, async (ctx, req, res) => {
return res.renderHtml({ body: htmlBody });
});
const [[, routeHandler]] = router.get.mock.calls;
const responseFactory = httpResourcesMock.createResponseFactory();
await routeHandler(context, kibanaRequest, responseFactory);
expect(responseFactory.ok).toHaveBeenCalledWith({
body: htmlBody,
headers: {
'content-type': 'text/html',
'content-security-policy':
"script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'",
},
});
});
it('can attach headers, except the CSP & "content-type" headers', async () => {
const htmlBody = '<html><body /></html>';
const routeConfig: RouteConfig<any, any, any, 'get'> = { path: '/', validate: false };
const { createRegistrar } = await service.setup(setupDeps);
const { register } = createRegistrar(router);
register(routeConfig, async (ctx, req, res) => {
return res.renderHtml({
body: htmlBody,
headers: {
'content-type': 'text/html5',
'content-security-policy': "script-src 'unsafe-eval'",
'x-kibana': '42',
},
});
});
const [[, routeHandler]] = router.get.mock.calls;
const responseFactory = httpResourcesMock.createResponseFactory();
await routeHandler(context, kibanaRequest, responseFactory);
expect(responseFactory.ok).toHaveBeenCalledWith({
body: htmlBody,
headers: {
'content-type': 'text/html',
'x-kibana': '42',
'content-security-policy':
"script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'",
},
});
});
});
describe('renderJs', () => {
it('formats successful response', async () => {
const jsBody = 'alert(1);';
const routeConfig: RouteConfig<any, any, any, 'get'> = { path: '/', validate: false };
const { createRegistrar } = await service.setup(setupDeps);
const { register } = createRegistrar(router);
register(routeConfig, async (ctx, req, res) => {
return res.renderJs({ body: jsBody });
});
const [[, routeHandler]] = router.get.mock.calls;
const responseFactory = httpResourcesMock.createResponseFactory();
await routeHandler(context, kibanaRequest, responseFactory);
expect(responseFactory.ok).toHaveBeenCalledWith({
body: jsBody,
headers: {
'content-type': 'text/javascript',
'content-security-policy':
"script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'",
},
});
});
it('can attach headers, except the CSP & "content-type" headers', async () => {
const jsBody = 'alert(1);';
const routeConfig: RouteConfig<any, any, any, 'get'> = { path: '/', validate: false };
const { createRegistrar } = await service.setup(setupDeps);
const { register } = createRegistrar(router);
register(routeConfig, async (ctx, req, res) => {
return res.renderJs({
body: jsBody,
headers: {
'content-type': 'text/html',
'content-security-policy': "script-src 'unsafe-eval'",
'x-kibana': '42',
},
});
});
const [[, routeHandler]] = router.get.mock.calls;
const responseFactory = httpResourcesMock.createResponseFactory();
await routeHandler(context, kibanaRequest, responseFactory);
expect(responseFactory.ok).toHaveBeenCalledWith({
body: jsBody,
headers: {
'content-type': 'text/javascript',
'x-kibana': '42',
'content-security-policy':
"script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'",
},
});
});
});
});
});
});

View file

@ -0,0 +1,130 @@
/*
* 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 { RequestHandlerContext } from 'src/core/server';
import { CoreContext } from '../core_context';
import {
IRouter,
RouteConfig,
InternalHttpServiceSetup,
KibanaRequest,
KibanaResponseFactory,
} from '../http';
import { Logger } from '../logging';
import { InternalRenderingServiceSetup } from '../rendering';
import { CoreService } from '../../types';
import {
InternalHttpResourcesSetup,
HttpResources,
HttpResourcesResponseOptions,
HttpResourcesRenderOptions,
HttpResourcesRequestHandler,
HttpResourcesServiceToolkit,
} from './types';
export interface SetupDeps {
http: InternalHttpServiceSetup;
rendering: InternalRenderingServiceSetup;
}
export class HttpResourcesService implements CoreService<InternalHttpResourcesSetup> {
private readonly logger: Logger;
constructor(core: CoreContext) {
this.logger = core.logger.get('http-resources');
}
setup(deps: SetupDeps) {
this.logger.debug('setting up HttpResourcesService');
return {
createRegistrar: this.createRegistrar.bind(this, deps),
};
}
start() {}
stop() {}
private createRegistrar(deps: SetupDeps, router: IRouter): HttpResources {
return {
register: <P, Q, B>(
route: RouteConfig<P, Q, B, 'get'>,
handler: HttpResourcesRequestHandler<P, Q, B>
) => {
return router.get<P, Q, B>(route, (context, request, response) => {
return handler(context, request, {
...response,
...this.createResponseToolkit(deps, context, request, response),
});
});
},
};
}
private createResponseToolkit(
deps: SetupDeps,
context: RequestHandlerContext,
request: KibanaRequest,
response: KibanaResponseFactory
): HttpResourcesServiceToolkit {
const cspHeader = deps.http.csp.header;
return {
async renderCoreApp(options: HttpResourcesRenderOptions = {}) {
const body = await deps.rendering.render(request, context.core.uiSettings.client, {
includeUserSettings: true,
});
return response.ok({
body,
headers: { ...options.headers, 'content-security-policy': cspHeader },
});
},
async renderAnonymousCoreApp(options: HttpResourcesRenderOptions = {}) {
const body = await deps.rendering.render(request, context.core.uiSettings.client, {
includeUserSettings: false,
});
return response.ok({
body,
headers: { ...options.headers, 'content-security-policy': cspHeader },
});
},
renderHtml(options: HttpResourcesResponseOptions) {
return response.ok({
body: options.body,
headers: {
...options.headers,
'content-type': 'text/html',
'content-security-policy': cspHeader,
},
});
},
renderJs(options: HttpResourcesResponseOptions) {
return response.ok({
body: options.body,
headers: {
...options.headers,
'content-type': 'text/javascript',
'content-security-policy': cspHeader,
},
});
},
};
}
}

View file

@ -0,0 +1,29 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { HttpResourcesService } from './http_resources_service';
export {
HttpResourcesRenderOptions,
HttpResourcesResponseOptions,
HttpResourcesServiceToolkit,
HttpResourcesRequestHandler,
HttpResources,
InternalHttpResourcesSetup,
} from './types';

View file

@ -0,0 +1,203 @@
/*
* 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 { schema } from '@kbn/config-schema';
import * as kbnTestServer from '../../../../test_utils/kbn_server';
describe('http resources service', () => {
describe('register', () => {
let root: ReturnType<typeof kbnTestServer.createRoot>;
const defaultCspRules = "script-src 'self'";
beforeEach(async () => {
root = kbnTestServer.createRoot({
csp: {
rules: [defaultCspRules],
},
});
}, 30000);
afterEach(async () => {
await root.shutdown();
});
describe('renderAnonymousCoreApp', () => {
it('renders core application', async () => {
const { http, httpResources } = await root.setup();
const router = http.createRouter('');
const resources = httpResources.createRegistrar(router);
resources.register({ path: '/render-core', validate: false }, (context, req, res) =>
res.renderAnonymousCoreApp()
);
await root.start();
const response = await kbnTestServer.request.get(root, '/render-core').expect(200);
expect(response.text.length).toBeGreaterThan(0);
});
it('attaches CSP header', async () => {
const { http, httpResources } = await root.setup();
const router = http.createRouter('');
const resources = httpResources.createRegistrar(router);
resources.register({ path: '/render-core', validate: false }, (context, req, res) =>
res.renderAnonymousCoreApp()
);
await root.start();
const response = await kbnTestServer.request.get(root, '/render-core').expect(200);
expect(response.header['content-security-policy']).toBe(defaultCspRules);
});
it('can attach headers, except the CSP header', async () => {
const { http, httpResources } = await root.setup();
const router = http.createRouter('');
const resources = httpResources.createRegistrar(router);
resources.register({ path: '/render-core', validate: false }, (context, req, res) =>
res.renderAnonymousCoreApp({
headers: {
'content-security-policy': "script-src 'unsafe-eval'",
'x-kibana': '42',
},
})
);
await root.start();
const response = await kbnTestServer.request.get(root, '/render-core').expect(200);
expect(response.header['content-security-policy']).toBe(defaultCspRules);
expect(response.header['x-kibana']).toBe('42');
});
});
describe('custom renders', () => {
it('renders html', async () => {
const { http, httpResources } = await root.setup();
const router = http.createRouter('');
const resources = httpResources.createRegistrar(router);
const htmlBody = `
<!DOCTYPE html>
<html>
<body>
<p>HTML body</p>
</body>
</html>
`;
resources.register({ path: '/render-html', validate: false }, (context, req, res) =>
res.renderHtml({ body: htmlBody })
);
await root.start();
const response = await kbnTestServer.request.get(root, '/render-html').expect(200);
expect(response.text).toBe(htmlBody);
expect(response.header['content-type']).toBe('text/html; charset=utf-8');
});
it('renders javascript', async () => {
const { http, httpResources } = await root.setup();
const router = http.createRouter('');
const resources = httpResources.createRegistrar(router);
const jsBody = 'window.alert("from js body");';
resources.register({ path: '/render-js', validate: false }, (context, req, res) =>
res.renderJs({ body: jsBody })
);
await root.start();
const response = await kbnTestServer.request.get(root, '/render-js').expect(200);
expect(response.text).toBe(jsBody);
expect(response.header['content-type']).toBe('text/javascript; charset=utf-8');
});
it('attaches CSP header', async () => {
const { http, httpResources } = await root.setup();
const router = http.createRouter('');
const resources = httpResources.createRegistrar(router);
const htmlBody = `
<!DOCTYPE html>
<html>
<body>
<p>HTML body</p>
</body>
</html>
`;
resources.register({ path: '/render-html', validate: false }, (context, req, res) =>
res.renderHtml({ body: htmlBody })
);
await root.start();
const response = await kbnTestServer.request.get(root, '/render-html').expect(200);
expect(response.header['content-security-policy']).toBe(defaultCspRules);
});
it('can attach headers, except the CSP & "content-type" headers', async () => {
const { http, httpResources } = await root.setup();
const router = http.createRouter('');
const resources = httpResources.createRegistrar(router);
resources.register({ path: '/render-core', validate: false }, (context, req, res) =>
res.renderHtml({
body: '<html><p>Hi</p></html>',
headers: {
'content-security-policy': "script-src 'unsafe-eval'",
'content-type': 'text/html',
'x-kibana': '42',
},
})
);
await root.start();
const response = await kbnTestServer.request.get(root, '/render-core').expect(200);
expect(response.header['content-security-policy']).toBe(defaultCspRules);
expect(response.header['x-kibana']).toBe('42');
});
it('can adjust route config', async () => {
const { http, httpResources } = await root.setup();
const router = http.createRouter('');
const resources = httpResources.createRegistrar(router);
const validate = {
params: schema.object({
id: schema.string(),
}),
};
resources.register({ path: '/render-js-with-param/{id}', validate }, (context, req, res) =>
res.renderJs({ body: `window.alert(${req.params.id});` })
);
await root.start();
const response = await kbnTestServer.request
.get(root, '/render-js-with-param/42')
.expect(200);
expect(response.text).toBe('window.alert(42);');
});
});
});
});

View file

@ -0,0 +1,116 @@
/*
* 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 {
IRouter,
RouteConfig,
IKibanaResponse,
ResponseHeaders,
HttpResponseOptions,
KibanaResponseFactory,
RequestHandler,
} from '../http';
/**
* Allows to configure HTTP response parameters
* @public
*/
export interface HttpResourcesRenderOptions {
/**
* HTTP Headers with additional information about response.
* @remarks
* All HTML pages are already pre-configured with `content-security-policy` header that cannot be overridden.
* */
headers?: ResponseHeaders;
}
/**
* HTTP Resources response parameters
* @public
*/
export type HttpResourcesResponseOptions = HttpResponseOptions;
/**
* Extended set of {@link KibanaResponseFactory} helpers used to respond with HTML or JS resource.
* @public
*/
export interface HttpResourcesServiceToolkit {
/** To respond with HTML page bootstrapping Kibana application. */
renderCoreApp: (options?: HttpResourcesRenderOptions) => Promise<IKibanaResponse>;
/** To respond with HTML page bootstrapping Kibana application without retrieving user-specific information. */
renderAnonymousCoreApp: (options?: HttpResourcesRenderOptions) => Promise<IKibanaResponse>;
/** To respond with a custom HTML page. */
renderHtml: (options: HttpResourcesResponseOptions) => IKibanaResponse;
/** To respond with a custom JS script file. */
renderJs: (options: HttpResourcesResponseOptions) => IKibanaResponse;
}
/**
* Extended version of {@link RequestHandler} having access to {@link HttpResourcesServiceToolkit}
* to respond with HTML or JS resources.
* @param context {@link RequestHandlerContext} - the core context exposed for this request.
* @param request {@link KibanaRequest} - object containing information about requested resource,
* such as path, method, headers, parameters, query, body, etc.
* @param response {@link KibanaResponseFactory} {@libk HttpResourcesServiceToolkit} - a set of helper functions used to respond to a request.
*
* @example
* ```typescript
* httpResources.register({
* path: '/login',
* validate: {
* params: schema.object({ id: schema.string() }),
* },
* },
* async (context, request, response) => {
* //..
* return response.renderCoreApp();
* });
* @public
*/
export type HttpResourcesRequestHandler<P = unknown, Q = unknown, B = unknown> = RequestHandler<
P,
Q,
B,
'get',
KibanaResponseFactory & HttpResourcesServiceToolkit
>;
/**
* Allows to configure HTTP response parameters
* @internal
*/
export interface InternalHttpResourcesSetup {
createRegistrar(router: IRouter): HttpResources;
}
/**
* HttpResources service is responsible for serving static & dynamic assets for Kibana application via HTTP.
* Provides API allowing plug-ins to respond with:
* - a pre-configured HTML page bootstrapping Kibana client app
* - custom HTML page
* - custom JS script file.
* @public
*/
export interface HttpResources {
/** To register a route handler executing passed function to form response. */
register: <P, Q, B>(
route: RouteConfig<P, Q, B, 'get'>,
handler: HttpResourcesRequestHandler<P, Q, B>
) => void;
}

View file

@ -47,7 +47,8 @@ import {
} from './elasticsearch';
import { HttpServiceSetup } from './http';
import { IScopedRenderingClient } from './rendering';
import { HttpResources } from './http_resources';
import { PluginsServiceSetup, PluginsServiceStart, PluginOpaqueId } from './plugins';
import { ContextSetup } from './context';
import { IUiSettingsClient, UiSettingsServiceSetup, UiSettingsServiceStart } from './ui_settings';
@ -146,6 +147,7 @@ export {
OnPreResponseInfo,
RedirectResponseOptions,
RequestHandler,
RequestHandlerWrapper,
RequestHandlerContextContainer,
RequestHandlerContextProvider,
ResponseError,
@ -175,7 +177,15 @@ export {
DestructiveRouteMethod,
SafeRouteMethod,
} from './http';
export { RenderingServiceSetup, IRenderOptions } from './rendering';
export {
HttpResourcesRenderOptions,
HttpResourcesResponseOptions,
HttpResourcesServiceToolkit,
HttpResourcesRequestHandler,
} from './http_resources';
export { IRenderOptions } from './rendering';
export { Logger, LoggerFactory, LogMeta, LogRecord, LogLevel } from './logging';
export {
@ -313,8 +323,6 @@ export {
* Plugin specific context passed to a route handler.
*
* Provides the following clients and services:
* - {@link IScopedRenderingClient | rendering} - Rendering client
* which uses the data of the incoming request
* - {@link SavedObjectsClient | savedObjects.client} - Saved Objects client
* which uses the credentials of the incoming request
* - {@link ISavedObjectTypeRegistry | savedObjects.typeRegistry} - Type registry containing
@ -330,7 +338,6 @@ export {
*/
export interface RequestHandlerContext {
core: {
rendering: IScopedRenderingClient;
savedObjects: {
client: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
@ -362,7 +369,10 @@ export interface CoreSetup<TPluginsStart extends object = object, TStart = unkno
/** {@link ElasticsearchServiceSetup} */
elasticsearch: ElasticsearchServiceSetup;
/** {@link HttpServiceSetup} */
http: HttpServiceSetup;
http: HttpServiceSetup & {
/** {@link HttpResources} */
resources: HttpResources;
};
/** {@link MetricsServiceSetup} */
metrics: MetricsServiceSetup;
/** {@link SavedObjectsServiceSetup} */
@ -410,7 +420,7 @@ export {
CapabilitiesSetup,
CapabilitiesStart,
ContextSetup,
IScopedRenderingClient,
HttpResources,
PluginsServiceSetup,
PluginsServiceStart,
PluginOpaqueId,

View file

@ -31,6 +31,8 @@ import {
import { InternalUiSettingsServiceSetup, InternalUiSettingsServiceStart } from './ui_settings';
import { UuidServiceSetup } from './uuid';
import { InternalMetricsServiceSetup } from './metrics';
import { InternalRenderingServiceSetup } from './rendering';
import { InternalHttpResourcesSetup } from './http_resources';
import { InternalStatusServiceSetup } from './status';
/** @internal */
@ -44,6 +46,8 @@ export interface InternalCoreSetup {
status: InternalStatusServiceSetup;
uiSettings: InternalUiSettingsServiceSetup;
uuid: UuidServiceSetup;
rendering: InternalRenderingServiceSetup;
httpResources: InternalHttpResourcesSetup;
}
/**

View file

@ -41,6 +41,7 @@ import { httpServiceMock } from '../http/http_service.mock';
import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock';
import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.mock';
import { capabilitiesServiceMock } from '../capabilities/capabilities_service.mock';
import { httpResourcesMock } from '../http_resources/http_resources_service.mock';
import { setupMock as renderingServiceMock } from '../rendering/__mocks__/rendering_service';
import { uuidServiceMock } from '../uuid/uuid_service.mock';
import { metricsServiceMock } from '../metrics/metrics_service.mock';
@ -86,23 +87,11 @@ beforeEach(() => {
getAuthHeaders: () => undefined,
} as any,
},
httpResources: httpResourcesMock.createSetupContract(),
savedObjects: savedObjectsServiceMock.createInternalSetupContract(),
plugins: {
initialized: true,
contracts: new Map([['plugin-id', 'plugin-value']]),
uiPlugins: {
public: new Map([['plugin-id', {} as DiscoveredPlugin]]),
internal: new Map([
[
'plugin-id',
{
publicTargetDir: 'path/to/target/public',
publicAssetsDir: '/plugins/name/assets/',
},
],
]),
browserConfigs: new Map(),
},
},
rendering: renderingServiceMock,
metrics: metricsServiceMock.createInternalSetupContract(),
@ -110,6 +99,19 @@ beforeEach(() => {
status: statusServiceMock.createInternalSetupContract(),
},
plugins: { 'plugin-id': 'plugin-value' },
uiPlugins: {
public: new Map([['plugin-id', {} as DiscoveredPlugin]]),
internal: new Map([
[
'plugin-id',
{
publicTargetDir: 'path/to/target/public',
publicAssetsDir: '/plugins/name/assets/',
},
],
]),
browserConfigs: new Map(),
},
};
startDeps = {

View file

@ -269,6 +269,7 @@ export class LegacyService implements CoreService {
uiSettings: { asScopedToClient: startDeps.core.uiSettings.asScopedToClient },
};
const router = setupDeps.core.http.createRouter('', this.legacyId);
const coreSetup: CoreSetup = {
capabilities: setupDeps.core.capabilities,
context: setupDeps.core.context,
@ -283,7 +284,8 @@ export class LegacyService implements CoreService {
null,
this.legacyId
),
createRouter: () => setupDeps.core.http.createRouter('', this.legacyId),
createRouter: () => router,
resources: setupDeps.core.httpResources.createRegistrar(router),
registerOnPreAuth: setupDeps.core.http.registerOnPreAuth,
registerAuth: setupDeps.core.http.registerAuth,
registerOnPostAuth: setupDeps.core.http.registerOnPostAuth,
@ -342,7 +344,7 @@ export class LegacyService implements CoreService {
},
hapiServer: setupDeps.core.http.server,
kibanaMigrator: startDeps.core.savedObjects.migrator,
uiPlugins: setupDeps.core.plugins.uiPlugins,
uiPlugins: setupDeps.uiPlugins,
elasticsearch: setupDeps.core.elasticsearch,
rendering: setupDeps.core.rendering,
uiSettings: setupDeps.core.uiSettings,

View file

@ -22,8 +22,8 @@ import { Server } from 'hapi';
import { ChromeNavLink } from '../../public';
import { KibanaRequest, LegacyRequest } from '../http';
import { InternalCoreSetup, InternalCoreStart } from '../internal_types';
import { PluginsServiceSetup, PluginsServiceStart } from '../plugins';
import { RenderingServiceSetup } from '../rendering';
import { PluginsServiceSetup, PluginsServiceStart, UiPlugins } from '../plugins';
import { InternalRenderingServiceSetup } from '../rendering';
import { SavedObjectsLegacyUiExports } from '../types';
/**
@ -34,7 +34,7 @@ export type LegacyVars = Record<string, any>;
type LegacyCoreSetup = InternalCoreSetup & {
plugins: PluginsServiceSetup;
rendering: RenderingServiceSetup;
rendering: InternalRenderingServiceSetup;
};
type LegacyCoreStart = InternalCoreStart & { plugins: PluginsServiceStart };
@ -173,6 +173,7 @@ export type LegacyUiExports = SavedObjectsLegacyUiExports & {
export interface LegacyServiceSetupDeps {
core: LegacyCoreSetup;
plugins: Record<string, unknown>;
uiPlugins: UiPlugins;
}
/**

View file

@ -23,10 +23,12 @@ import { CspConfig } from './csp';
import { loggingServiceMock } from './logging/logging_service.mock';
import { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service.mock';
import { httpServiceMock } from './http/http_service.mock';
import { httpResourcesMock } from './http_resources/http_resources_service.mock';
import { contextServiceMock } from './context/context_service.mock';
import { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock';
import { savedObjectsClientMock } from './saved_objects/service/saved_objects_client.mock';
import { typeRegistryMock as savedObjectsTypeRegistryMock } from './saved_objects/saved_objects_type_registry.mock';
import { renderingMock } from './rendering/rendering_service.mock';
import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock';
import { SharedGlobalConfig } from './plugins';
import { InternalCoreSetup, InternalCoreStart } from './internal_types';
@ -36,6 +38,7 @@ import { uuidServiceMock } from './uuid/uuid_service.mock';
import { statusServiceMock } from './status/status_service.mock';
export { httpServerMock } from './http/http_server.mocks';
export { httpResourcesMock } from './http_resources/http_resources_service.mock';
export { sessionStorageMock } from './http/cookie_session_storage.mocks';
export { configServiceMock } from './config/config_service.mock';
export { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service.mock';
@ -45,6 +48,7 @@ export { savedObjectsRepositoryMock } from './saved_objects/service/lib/reposito
export { typeRegistryMock as savedObjectsTypeRegistryMock } from './saved_objects/saved_objects_type_registry.mock';
export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock';
export { metricsServiceMock } from './metrics/metrics_service.mock';
export { renderingMock } from './rendering/rendering_service.mock';
export function pluginInitializerContextConfigMock<T>(config: T) {
const globalConfig: SharedGlobalConfig = {
@ -120,6 +124,7 @@ function createCoreSetupMock({
get: httpService.auth.get,
isAuthenticated: httpService.auth.isAuthenticated,
},
resources: httpResourcesMock.createRegistrar(),
getServerInfo: httpService.getServerInfo,
};
httpMock.createRouter.mockImplementation(() => httpService.createRouter(''));
@ -167,6 +172,8 @@ function createInternalCoreSetupMock() {
savedObjects: savedObjectsServiceMock.createInternalSetupContract(),
status: statusServiceMock.createInternalSetupContract(),
uuid: uuidServiceMock.createSetupContract(),
httpResources: httpResourcesMock.createSetupContract(),
rendering: renderingMock.createSetupContract(),
uiSettings: uiSettingsServiceMock.createSetupContract(),
};
return setupDeps;
@ -184,9 +191,6 @@ function createInternalCoreStartMock() {
function createCoreRequestHandlerContextMock() {
return {
rendering: {
render: jest.fn(),
},
savedObjects: {
client: savedObjectsClientMock.create(),
typeRegistry: savedObjectsTypeRegistryMock.create(),

View file

@ -17,7 +17,12 @@
* under the License.
*/
export { PluginsService, PluginsServiceSetup, PluginsServiceStart } from './plugins_service';
export {
PluginsService,
PluginsServiceSetup,
PluginsServiceStart,
UiPlugins,
} from './plugins_service';
export { config } from './plugins_config';
/** @internal */
export { isNewPlatformPlugin } from './discovery';

View file

@ -136,6 +136,8 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>(
deps: PluginsServiceSetupDeps,
plugin: PluginWrapper<TPlugin, TPluginDependencies>
): CoreSetup {
const router = deps.http.createRouter('', plugin.opaqueId);
return {
capabilities: {
registerProvider: deps.capabilities.registerProvider,
@ -155,7 +157,8 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>(
null,
plugin.opaqueId
),
createRouter: () => deps.http.createRouter('', plugin.opaqueId),
createRouter: () => router,
resources: deps.httpResources.createRegistrar(router),
registerOnPreAuth: deps.http.registerOnPreAuth,
registerAuth: deps.http.registerAuth,
registerOnPostAuth: deps.http.registerOnPostAuth,

View file

@ -23,14 +23,10 @@ type PluginsServiceMock = jest.Mocked<PublicMethodsOf<PluginsService>>;
const createSetupContractMock = (): PluginsServiceSetup => ({
contracts: new Map(),
uiPlugins: {
browserConfigs: new Map(),
internal: new Map(),
public: new Map(),
},
initialized: true,
});
const createStartContractMock = () => ({ contracts: new Map() });
const createServiceMock = (): PluginsServiceMock => ({
discover: jest.fn(),
setup: jest.fn().mockResolvedValue(createSetupContractMock()),
@ -38,8 +34,17 @@ const createServiceMock = (): PluginsServiceMock => ({
stop: jest.fn(),
});
function createUiPlugins() {
return {
browserConfigs: new Map(),
internal: new Map(),
public: new Map(),
};
}
export const pluginServiceMock = {
create: createServiceMock,
createSetupContract: createSetupContractMock,
createStartContract: createStartContractMock,
createUiPlugins,
};

View file

@ -120,6 +120,7 @@ describe('PluginsService', () => {
pluginsService = new PluginsService({ coreId, env, logger, configService });
[mockPluginSystem] = MockPluginsSystem.mock.instances as any;
mockPluginSystem.uiPlugins.mockReturnValue(new Map());
});
afterEach(() => {
@ -202,7 +203,6 @@ describe('PluginsService', () => {
.mockImplementation(path => Promise.resolve(!path.includes('disabled')));
mockPluginSystem.setupPlugins.mockResolvedValue(new Map());
mockPluginSystem.uiPlugins.mockReturnValue(new Map());
mockDiscover.mockReturnValue({
error$: from([]),
@ -234,8 +234,6 @@ describe('PluginsService', () => {
const setup = await pluginsService.setup(setupDeps);
expect(setup.contracts).toBeInstanceOf(Map);
expect(setup.uiPlugins.public).toBeInstanceOf(Map);
expect(setup.uiPlugins.internal).toBeInstanceOf(Map);
expect(mockPluginSystem.addPlugin).not.toHaveBeenCalled();
expect(mockPluginSystem.setupPlugins).toHaveBeenCalledTimes(1);
expect(mockPluginSystem.setupPlugins).toHaveBeenCalledWith(setupDeps);
@ -273,7 +271,8 @@ describe('PluginsService', () => {
plugin$: from([firstPlugin, secondPlugin]),
});
await expect(pluginsService.discover()).resolves.toBeUndefined();
const { pluginTree } = await pluginsService.discover();
expect(pluginTree).toBeUndefined();
expect(mockDiscover).toHaveBeenCalledTimes(1);
expect(mockPluginSystem.addPlugin).toHaveBeenCalledTimes(2);
@ -308,7 +307,8 @@ describe('PluginsService', () => {
plugin$: from([firstPlugin, secondPlugin, thirdPlugin, lastPlugin, missingDepsPlugin]),
});
await expect(pluginsService.discover()).resolves.toBeUndefined();
const { pluginTree } = await pluginsService.discover();
expect(pluginTree).toBeUndefined();
expect(mockDiscover).toHaveBeenCalledTimes(1);
expect(mockPluginSystem.addPlugin).toHaveBeenCalledTimes(4);
@ -466,12 +466,8 @@ describe('PluginsService', () => {
});
mockPluginSystem.uiPlugins.mockReturnValue(new Map([pluginToDiscoveredEntry(plugin)]));
await pluginsService.discover();
const {
uiPlugins: { browserConfigs },
} = await pluginsService.setup(setupDeps);
const uiConfig$ = browserConfigs.get('plugin-with-expose');
const { uiPlugins } = await pluginsService.discover();
const uiConfig$ = uiPlugins.browserConfigs.get('plugin-with-expose');
expect(uiConfig$).toBeDefined();
const uiConfig = await uiConfig$!.pipe(take(1)).toPromise();
@ -506,12 +502,8 @@ describe('PluginsService', () => {
});
mockPluginSystem.uiPlugins.mockReturnValue(new Map([pluginToDiscoveredEntry(plugin)]));
await pluginsService.discover();
const {
uiPlugins: { browserConfigs },
} = await pluginsService.setup(setupDeps);
expect([...browserConfigs.entries()]).toHaveLength(0);
const { uiPlugins } = await pluginsService.discover();
expect([...uiPlugins.browserConfigs.entries()]).toHaveLength(0);
});
});
@ -539,8 +531,7 @@ describe('PluginsService', () => {
describe('uiPlugins.internal', () => {
it('includes disabled plugins', async () => {
config$.next({ plugins: { initialize: true }, plugin1: { enabled: false } });
await pluginsService.discover();
const { uiPlugins } = await pluginsService.setup(setupDeps);
const { uiPlugins } = await pluginsService.discover();
expect(uiPlugins.internal).toMatchInlineSnapshot(`
Map {
"plugin-1" => Object {

View file

@ -39,23 +39,25 @@ export interface PluginsServiceSetup {
initialized: boolean;
/** Setup contracts returned by plugins. */
contracts: Map<PluginName, unknown>;
uiPlugins: {
/**
* Paths to all discovered ui plugin entrypoints on the filesystem, even if
* disabled.
*/
internal: Map<PluginName, InternalPluginInfo>;
}
/**
* Information needed by client-side to load plugins and wire dependencies.
*/
public: Map<PluginName, DiscoveredPlugin>;
/** @internal */
export interface UiPlugins {
/**
* Paths to all discovered ui plugin entrypoints on the filesystem, even if
* disabled.
*/
internal: Map<PluginName, InternalPluginInfo>;
/**
* Configuration for plugins to be exposed to the client-side.
*/
browserConfigs: Map<PluginName, Observable<unknown>>;
};
/**
* Information needed by client-side to load plugins and wire dependencies.
*/
public: Map<PluginName, DiscoveredPlugin>;
/**
* Configuration for plugins to be exposed to the client-side.
*/
browserConfigs: Map<PluginName, Observable<unknown>>;
}
/** @internal */
@ -97,8 +99,17 @@ export class PluginsService implements CoreService<PluginsServiceSetup, PluginsS
await this.handleDiscoveryErrors(error$);
await this.handleDiscoveredPlugins(plugin$);
// Return dependency tree
return this.pluginsSystem.getPluginDependencies();
const uiPlugins = this.pluginsSystem.uiPlugins();
return {
// Return dependency tree
pluginTree: this.pluginsSystem.getPluginDependencies(),
uiPlugins: {
internal: this.uiPluginInternalInfo,
public: uiPlugins,
browserConfigs: this.generateUiPluginsConfigs(uiPlugins),
},
};
}
public async setup(deps: PluginsServiceSetupDeps) {
@ -115,15 +126,9 @@ export class PluginsService implements CoreService<PluginsServiceSetup, PluginsS
this.log.info('Plugin initialization disabled.');
}
const uiPlugins = this.pluginsSystem.uiPlugins();
return {
initialized: initialize,
contracts,
uiPlugins: {
internal: this.uiPluginInternalInfo,
public: uiPlugins,
browserConfigs: this.generateUiPluginsConfigs(uiPlugins),
},
};
}

View file

@ -24,12 +24,12 @@ import { legacyServiceMock } from '../../legacy/legacy_service.mock';
const context = mockCoreContext.create();
const http = httpServiceMock.createSetupContract();
const plugins = pluginServiceMock.createSetupContract();
const uiPlugins = pluginServiceMock.createUiPlugins();
const legacyPlugins = legacyServiceMock.createDiscoverPlugins();
export const mockRenderingServiceParams = context;
export const mockRenderingSetupDeps = {
http,
legacyPlugins,
plugins,
uiPlugins,
};

View file

@ -18,12 +18,12 @@
*/
import { RenderingService as Service } from '../rendering_service';
import { RenderingServiceSetup } from '../types';
import { InternalRenderingServiceSetup } from '../types';
import { mockRenderingServiceParams } from './params';
type IRenderingService = PublicMethodsOf<Service>;
export const setupMock: jest.Mocked<RenderingServiceSetup> = {
export const setupMock: jest.Mocked<InternalRenderingServiceSetup> = {
render: jest.fn(),
};
export const mockSetup = jest.fn().mockResolvedValue(setupMock);

View file

@ -0,0 +1,31 @@
/*
* 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 { InternalRenderingServiceSetup } from './types';
function createRenderingSetup() {
const mocked: jest.Mocked<InternalRenderingServiceSetup> = {
render: jest.fn().mockResolvedValue('<body />'),
};
return mocked;
}
export const renderingMock = {
createSetupContract: createRenderingSetup,
};

View file

@ -22,7 +22,7 @@ import { load } from 'cheerio';
import { httpServerMock } from '../http/http_server.mocks';
import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock';
import { mockRenderingServiceParams, mockRenderingSetupDeps } from './__mocks__/params';
import { RenderingServiceSetup } from './types';
import { InternalRenderingServiceSetup } from './types';
import { RenderingService } from './rendering_service';
const INJECTED_METADATA = {
@ -62,15 +62,9 @@ describe('RenderingService', () => {
});
describe('setup()', () => {
it('creates instance of RenderingServiceSetup', async () => {
const rendering = await service.setup(mockRenderingSetupDeps);
expect(rendering.render).toBeInstanceOf(Function);
});
describe('render()', () => {
let uiSettings: ReturnType<typeof uiSettingsServiceMock.createClient>;
let render: RenderingServiceSetup['render'];
let render: InternalRenderingServiceSetup['render'];
beforeEach(async () => {
uiSettings = uiSettingsServiceMock.createClient();
@ -78,6 +72,13 @@ describe('RenderingService', () => {
registered: { name: 'title' },
});
render = (await service.setup(mockRenderingSetupDeps)).render;
await service.start({
legacy: {
legacyInternals: {
getVars: () => ({}),
},
},
} as any);
});
it('renders "core" page', async () => {

View file

@ -23,41 +23,37 @@ import { take } from 'rxjs/operators';
import { i18n } from '@kbn/i18n';
import { UiPlugins } from '../plugins';
import { CoreService } from '../../types';
import { CoreContext } from '../core_context';
import { Template } from './views';
import { LegacyService } from '../legacy';
import {
IRenderOptions,
RenderingSetupDeps,
RenderingServiceSetup,
InternalRenderingServiceSetup,
RenderingMetadata,
} from './types';
/** @internal */
export class RenderingService implements CoreService<RenderingServiceSetup> {
export class RenderingService implements CoreService<InternalRenderingServiceSetup> {
private legacyInternals?: LegacyService['legacyInternals'];
constructor(private readonly coreContext: CoreContext) {}
public async setup({
http,
legacyPlugins,
plugins,
}: RenderingSetupDeps): Promise<RenderingServiceSetup> {
async function getUiConfig(pluginId: string) {
const browserConfig = plugins.uiPlugins.browserConfigs.get(pluginId);
return ((await browserConfig?.pipe(take(1)).toPromise()) ?? {}) as Record<string, any>;
}
uiPlugins,
}: RenderingSetupDeps): Promise<InternalRenderingServiceSetup> {
return {
render: async (
request,
uiSettings,
{
app = { getId: () => 'core' },
includeUserSettings = true,
vars = {},
}: IRenderOptions = {}
{ app = { getId: () => 'core' }, includeUserSettings = true, vars }: IRenderOptions = {}
) => {
if (!this.legacyInternals) {
throw new Error('Cannot render before "start"');
}
const { env } = this.coreContext;
const basePath = http.basePath.get(request);
const serverBasePath = http.basePath.serverBasePath;
@ -87,12 +83,12 @@ export class RenderingService implements CoreService<RenderingServiceSetup> {
translationsUrl: `${basePath}/translations/${i18n.getLocale()}.json`,
},
csp: { warnLegacyBrowsers: http.csp.warnLegacyBrowsers },
vars,
vars: vars ?? (await this.legacyInternals!.getVars('core', request)),
uiPlugins: await Promise.all(
[...plugins.uiPlugins.public].map(async ([id, plugin]) => ({
[...uiPlugins.public].map(async ([id, plugin]) => ({
id,
plugin,
config: await getUiConfig(id),
config: await this.getUiConfig(uiPlugins, id),
}))
),
legacyMetadata: {
@ -116,7 +112,15 @@ export class RenderingService implements CoreService<RenderingServiceSetup> {
};
}
public async start() {}
public async start({ legacy }: { legacy: LegacyService }) {
this.legacyInternals = legacy.legacyInternals;
}
public async stop() {}
private async getUiConfig(uiPlugins: UiPlugins, pluginId: string) {
const browserConfig = uiPlugins.browserConfigs.get(pluginId);
return ((await browserConfig?.pipe(take(1)).toPromise()) ?? {}) as Record<string, any>;
}
}

View file

@ -23,7 +23,7 @@ import { Env } from '../config';
import { ICspConfig } from '../csp';
import { InternalHttpServiceSetup, KibanaRequest, LegacyRequest } from '../http';
import { LegacyNavLink, LegacyServiceDiscoverPlugins } from '../legacy';
import { PluginsServiceSetup, DiscoveredPlugin } from '../plugins';
import { UiPlugins, DiscoveredPlugin } from '../plugins';
import { IUiSettingsClient, UserProvidedValues } from '../ui_settings';
/** @internal */
@ -75,7 +75,7 @@ export interface RenderingMetadata {
export interface RenderingSetupDeps {
http: InternalHttpServiceSetup;
legacyPlugins: LegacyServiceDiscoverPlugins;
plugins: PluginsServiceSetup;
uiPlugins: UiPlugins;
}
/** @public */
@ -102,31 +102,8 @@ export interface IRenderOptions {
vars?: Record<string, any>;
}
/** @public */
export interface IScopedRenderingClient {
/**
* Generate a `KibanaResponse` which renders an HTML page bootstrapped
* with the `core` bundle. Intended as a response body for HTTP route handlers.
*
* @example
* ```ts
* router.get(
* { path: '/', validate: false },
* (context, request, response) =>
* response.ok({
* body: await context.core.rendering.render(),
* headers: {
* 'content-security-policy': context.core.http.csp.header,
* },
* })
* );
* ```
*/
render(options?: Pick<IRenderOptions, 'includeUserSettings'>): Promise<string>;
}
/** @internal */
export interface RenderingServiceSetup {
export interface InternalRenderingServiceSetup {
/**
* Generate a `KibanaResponse` which renders an HTML page bootstrapped
* with the `core` bundle or the ID of another specified legacy bundle.

View file

@ -632,7 +632,9 @@ export interface CoreSetup<TPluginsStart extends object = object, TStart = unkno
// (undocumented)
getStartServices: StartServicesAccessor<TPluginsStart, TStart>;
// (undocumented)
http: HttpServiceSetup;
http: HttpServiceSetup & {
resources: HttpResources;
};
// (undocumented)
metrics: MetricsServiceSetup;
// (undocumented)
@ -861,6 +863,30 @@ export type Headers = {
[header: string]: string | string[] | undefined;
};
// @public
export interface HttpResources {
register: <P, Q, B>(route: RouteConfig<P, Q, B, 'get'>, handler: HttpResourcesRequestHandler<P, Q, B>) => void;
}
// @public
export interface HttpResourcesRenderOptions {
headers?: ResponseHeaders;
}
// @public
export type HttpResourcesRequestHandler<P = unknown, Q = unknown, B = unknown> = RequestHandler<P, Q, B, 'get', KibanaResponseFactory & HttpResourcesServiceToolkit>;
// @public
export type HttpResourcesResponseOptions = HttpResponseOptions;
// @public
export interface HttpResourcesServiceToolkit {
renderAnonymousCoreApp: (options?: HttpResourcesRenderOptions) => Promise<IKibanaResponse>;
renderCoreApp: (options?: HttpResourcesRenderOptions) => Promise<IKibanaResponse>;
renderHtml: (options: HttpResourcesResponseOptions) => IKibanaResponse;
renderJs: (options: HttpResourcesResponseOptions) => IKibanaResponse;
}
// @public
export interface HttpResponseOptions {
body?: HttpResponsePayload;
@ -989,7 +1015,7 @@ export interface IRouter {
//
// @internal
getRoutes: () => RouterRoute[];
handleLegacyErrors: <P, Q, B>(handler: RequestHandler<P, Q, B>) => RequestHandler<P, Q, B>;
handleLegacyErrors: RequestHandlerWrapper;
patch: RouteRegistrar<'patch'>;
post: RouteRegistrar<'post'>;
put: RouteRegistrar<'put'>;
@ -1008,11 +1034,6 @@ export type ISavedObjectTypeRegistry = Omit<SavedObjectTypeRegistry, 'registerTy
// @public
export type IScopedClusterClient = Pick<ScopedClusterClient, 'callAsCurrentUser' | 'callAsInternalUser'>;
// @public (undocumented)
export interface IScopedRenderingClient {
render(options?: Pick<IRenderOptions, 'includeUserSettings'>): Promise<string>;
}
// @public
export interface IUiSettingsClient {
get: <T = any>(key: string) => Promise<T>;
@ -1150,6 +1171,10 @@ export interface LegacyServiceSetupDeps {
core: LegacyCoreSetup;
// (undocumented)
plugins: Record<string, unknown>;
// Warning: (ae-forgotten-export) The symbol "UiPlugins" needs to be exported by the entry point index.d.ts
//
// (undocumented)
uiPlugins: UiPlugins;
}
// @public @deprecated (undocumented)
@ -1466,12 +1491,6 @@ export type PluginOpaqueId = symbol;
export interface PluginsServiceSetup {
contracts: Map<PluginName, unknown>;
initialized: boolean;
// (undocumented)
uiPlugins: {
internal: Map<PluginName, InternalPluginInfo>;
public: Map<PluginName, DiscoveredPlugin>;
browserConfigs: Map<PluginName, Observable<unknown>>;
};
}
// @internal (undocumented)
@ -1496,19 +1515,13 @@ export type RedirectResponseOptions = HttpResponseOptions & {
};
};
// @internal (undocumented)
export interface RenderingServiceSetup {
render<R extends KibanaRequest | LegacyRequest>(request: R, uiSettings: IUiSettingsClient, options?: IRenderOptions): Promise<string>;
}
// @public
export type RequestHandler<P = unknown, Q = unknown, B = unknown, Method extends RouteMethod = any> = (context: RequestHandlerContext, request: KibanaRequest<P, Q, B, Method>, response: KibanaResponseFactory) => IKibanaResponse<any> | Promise<IKibanaResponse<any>>;
export type RequestHandler<P = unknown, Q = unknown, B = unknown, Method extends RouteMethod = any, ResponseFactory extends KibanaResponseFactory = KibanaResponseFactory> = (context: RequestHandlerContext, request: KibanaRequest<P, Q, B, Method>, response: ResponseFactory) => IKibanaResponse<any> | Promise<IKibanaResponse<any>>;
// @public
export interface RequestHandlerContext {
// (undocumented)
core: {
rendering: IScopedRenderingClient;
savedObjects: {
client: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
@ -1529,6 +1542,9 @@ export type RequestHandlerContextContainer = IContextContainer<RequestHandler<an
// @public
export type RequestHandlerContextProvider<TContextName extends keyof RequestHandlerContext> = IContextProvider<RequestHandler<any, any, any>, TContextName>;
// @public
export type RequestHandlerWrapper = <P, Q, B, Method extends RouteMethod = any, ResponseFactory extends KibanaResponseFactory = KibanaResponseFactory>(handler: RequestHandler<P, Q, B, Method, ResponseFactory>) => RequestHandler<P, Q, B, Method, ResponseFactory>;
// @public
export function resolveSavedObjectsImportErrors({ readStream, objectLimit, retries, savedObjectsClient, supportedTypes, namespace, }: SavedObjectsResolveImportErrorsOptions): Promise<SavedObjectsImportResponse>;
@ -1542,11 +1558,7 @@ export type ResponseError = string | Error | {
export type ResponseErrorAttributes = Record<string, any>;
// @public
export type ResponseHeaders = {
[header in KnownHeaders]?: string | string[];
} & {
[header: string]: string | string[];
};
export type ResponseHeaders = Record<KnownHeaders, string | string[]> | Record<string, string | string[]>;
// @public
export interface RouteConfig<P, Q, B, Method extends RouteMethod> {
@ -2463,7 +2475,6 @@ export const validBodyOutput: readonly ["data", "stream"];
// src/core/server/legacy/types.ts:164:3 - (ae-forgotten-export) The symbol "LegacyNavLinkSpec" needs to be exported by the entry point index.d.ts
// src/core/server/legacy/types.ts:165:3 - (ae-forgotten-export) The symbol "LegacyAppSpec" needs to be exported by the entry point index.d.ts
// src/core/server/legacy/types.ts:166:16 - (ae-forgotten-export) The symbol "LegacyPluginSpec" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/plugins_service.ts:47:5 - (ae-forgotten-export) The symbol "InternalPluginInfo" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/types.ts:230:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/types.ts:230:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/types.ts:232:3 - (ae-forgotten-export) The symbol "PathConfigType" needs to be exported by the entry point index.d.ts

View file

@ -46,7 +46,10 @@ const rawConfigService = rawConfigServiceMock.create({});
beforeEach(() => {
mockConfigService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true }));
mockPluginsService.discover.mockResolvedValue(new Map());
mockPluginsService.discover.mockResolvedValue({
pluginTree: new Map(),
uiPlugins: { internal: new Map(), public: new Map(), browserConfigs: new Map() },
});
});
afterEach(() => {
@ -88,7 +91,10 @@ test('injects legacy dependency to context#setup()', async () => {
[pluginA, []],
[pluginB, [pluginA]],
]);
mockPluginsService.discover.mockResolvedValue(pluginDependencies);
mockPluginsService.discover.mockResolvedValue({
pluginTree: pluginDependencies,
uiPlugins: { internal: new Map(), public: new Map(), browserConfigs: new Map() },
});
await server.setup();

View file

@ -29,7 +29,8 @@ import {
import { CoreApp } from './core_app';
import { ElasticsearchService } from './elasticsearch';
import { HttpService } from './http';
import { RenderingService, RenderingServiceSetup } from './rendering';
import { HttpResourcesService } from './http_resources';
import { RenderingService } from './rendering';
import { LegacyService, ensureValidConfiguration } from './legacy';
import { Logger, LoggerFactory } from './logging';
import { UiSettingsService } from './ui_settings';
@ -71,6 +72,7 @@ export class Server {
private readonly uiSettings: UiSettingsService;
private readonly uuid: UuidService;
private readonly metrics: MetricsService;
private readonly httpResources: HttpResourcesService;
private readonly status: StatusService;
private readonly coreApp: CoreApp;
@ -99,13 +101,14 @@ export class Server {
this.metrics = new MetricsService(core);
this.status = new StatusService(core);
this.coreApp = new CoreApp(core);
this.httpResources = new HttpResourcesService(core);
}
public async setup() {
this.log.debug('setting up server');
// Discover any plugins before continuing. This allows other systems to utilize the plugin dependency graph.
const pluginDependencies = await this.plugins.discover();
const { pluginTree, uiPlugins } = await this.plugins.discover();
const legacyPlugins = await this.legacy.discoverPlugins();
// Immediately terminate in case of invalid configuration
@ -117,10 +120,7 @@ export class Server {
// 1) Can access context from any NP plugin
// 2) Can register context providers that will only be available to other legacy plugins and will not leak into
// New Platform plugins.
pluginDependencies: new Map([
...pluginDependencies,
[this.legacy.legacyId, [...pluginDependencies.keys()]],
]),
pluginDependencies: new Map([...pluginTree, [this.legacy.legacyId, [...pluginTree.keys()]]]),
});
const uuidSetup = await this.uuid.setup();
@ -148,6 +148,17 @@ export class Server {
const metricsSetup = await this.metrics.setup({ http: httpSetup });
const renderingSetup = await this.rendering.setup({
http: httpSetup,
legacyPlugins,
uiPlugins,
});
const httpResourcesSetup = this.httpResources.setup({
http: httpSetup,
rendering: renderingSetup,
});
const statusSetup = this.status.setup({
elasticsearch: elasticsearchServiceSetup,
savedObjects: savedObjectsSetup,
@ -158,28 +169,25 @@ export class Server {
context: contextServiceSetup,
elasticsearch: elasticsearchServiceSetup,
http: httpSetup,
metrics: metricsSetup,
savedObjects: savedObjectsSetup,
status: statusSetup,
uiSettings: uiSettingsSetup,
uuid: uuidSetup,
metrics: metricsSetup,
rendering: renderingSetup,
httpResources: httpResourcesSetup,
};
const pluginsSetup = await this.plugins.setup(coreSetup);
this.pluginsInitialized = pluginsSetup.initialized;
const renderingSetup = await this.rendering.setup({
http: httpSetup,
legacyPlugins,
plugins: pluginsSetup,
});
await this.legacy.setup({
core: { ...coreSetup, plugins: pluginsSetup, rendering: renderingSetup },
plugins: mapToObject(pluginsSetup.contracts),
uiPlugins,
});
this.registerCoreContext(coreSetup, renderingSetup);
this.registerCoreContext(coreSetup);
this.coreApp.setup(coreSetup);
return coreSetup;
@ -212,7 +220,9 @@ export class Server {
});
await this.http.start();
await this.rendering.start();
await this.rendering.start({
legacy: this.legacy,
});
await this.metrics.start();
return this.coreStart;
@ -232,7 +242,7 @@ export class Server {
await this.status.stop();
}
private registerCoreContext(coreSetup: InternalCoreSetup, rendering: RenderingServiceSetup) {
private registerCoreContext(coreSetup: InternalCoreSetup) {
coreSetup.http.registerRouteHandlerContext(
coreId,
'core',
@ -241,13 +251,6 @@ export class Server {
const uiSettingsClient = coreSetup.uiSettings.asScopedToClient(savedObjectsClient);
return {
rendering: {
render: async (options = {}) =>
rendering.render(req, uiSettingsClient, {
...options,
vars: await this.legacy.legacyInternals!.getVars('core', req),
}),
},
savedObjects: {
client: savedObjectsClient,
typeRegistry: this.coreStart!.savedObjects.getTypeRegistry(),

View file

@ -41,10 +41,11 @@ import {
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { LegacyConfig, ILegacyService, ILegacyInternals } from '../../core/server/legacy';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { UiPlugins } from '../../core/server/plugins';
import { ApmOssPlugin } from '../core_plugins/apm_oss';
import { CallClusterWithRequest, ElasticsearchPlugin } from '../core_plugins/elasticsearch';
import { UsageCollectionSetup } from '../../plugins/usage_collection/server';
import { Capabilities } from '../../core/server';
import { UiSettingsServiceFactoryOptions } from '../../legacy/ui/ui_settings/ui_settings_service_factory';
import { HomeServerPluginSetup } from '../../plugins/home/server';
@ -111,7 +112,7 @@ export interface KibanaCore {
kibanaMigrator: LegacyServiceStartDeps['core']['savedObjects']['migrator'];
legacy: ILegacyInternals;
rendering: LegacyServiceSetupDeps['core']['rendering'];
uiPlugins: LegacyServiceSetupDeps['core']['plugins']['uiPlugins'];
uiPlugins: UiPlugins;
uiSettings: LegacyServiceSetupDeps['core']['uiSettings'];
savedObjectsClientProvider: LegacyServiceStartDeps['core']['savedObjects']['clientProvider'];
};

View file

@ -34,7 +34,7 @@ export const createGotoRoute = ({
shortUrlLookup: ShortUrlLookupService;
http: CoreSetup['http'];
}) => {
router.get(
http.resources.register(
{
path: getGotoPath('{urlId}'),
validate: {
@ -63,14 +63,8 @@ export const createGotoRoute = ({
},
});
}
const body = await context.core.rendering.render();
return response.ok({
headers: {
'content-security-policy': http.csp.header,
},
body,
});
return response.renderCoreApp();
})
);
};

View file

@ -17,15 +17,13 @@
* under the License.
*/
import { Plugin, CoreSetup, IRenderOptions } from 'kibana/server';
import { Plugin, CoreSetup } from 'kibana/server';
import { schema } from '@kbn/config-schema';
export class RenderingPlugin implements Plugin {
public setup(core: CoreSetup) {
const router = core.http.createRouter();
router.get(
core.http.resources.register(
{
path: '/render/{id}',
validate: {
@ -41,18 +39,12 @@ export class RenderingPlugin implements Plugin {
},
},
async (context, req, res) => {
const { id } = req.params;
const { includeUserSettings } = req.query;
const app = { getId: () => id! };
const options: Partial<IRenderOptions> = { app, includeUserSettings };
const body = await context.core.rendering.render(options);
return res.ok({
body,
headers: {
'content-security-policy': core.http.csp.header,
},
});
if (includeUserSettings) {
return res.renderCoreApp();
}
return res.renderAnonymousCoreApp();
}
);
}

View file

@ -157,12 +157,12 @@ export class Plugin {
defineRoutes({
router: core.http.createRouter(),
basePath: core.http.basePath,
httpResources: core.http.resources,
logger: this.initializerContext.logger.get('routes'),
clusterClient: this.clusterClient,
config,
authc,
authz,
csp: core.http.csp,
license,
});

View file

@ -11,17 +11,6 @@ import { defineCommonRoutes } from './common';
import { defineOIDCRoutes } from './oidc';
import { RouteDefinitionParams } from '..';
export function createCustomResourceResponse(body: string, contentType: string, cspHeader: string) {
return {
body,
headers: {
'content-type': contentType,
'content-security-policy': cspHeader,
},
statusCode: 200,
};
}
export function defineAuthenticationRoutes(params: RouteDefinitionParams) {
defineSessionRoutes(params);
defineCommonRoutes(params);

View file

@ -8,7 +8,6 @@ import { schema } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n';
import { KibanaRequest, KibanaResponseFactory } from '../../../../../../src/core/server';
import { OIDCLogin } from '../../authentication';
import { createCustomResourceResponse } from '.';
import { createLicensedRouteHandler } from '../licensed_route_handler';
import { wrapIntoCustomErrorResponse } from '../../errors';
import {
@ -20,7 +19,13 @@ import { RouteDefinitionParams } from '..';
/**
* Defines routes required for SAML authentication.
*/
export function defineOIDCRoutes({ router, logger, authc, csp, basePath }: RouteDefinitionParams) {
export function defineOIDCRoutes({
router,
httpResources,
logger,
authc,
basePath,
}: RouteDefinitionParams) {
// Generate two identical routes with new and deprecated URL and issue a warning if route with deprecated URL is ever used.
for (const path of ['/api/security/oidc/implicit', '/api/security/v1/oidc/implicit']) {
/**
@ -28,7 +33,7 @@ export function defineOIDCRoutes({ router, logger, authc, csp, basePath }: Route
* is used, so that we can extract authentication response from URL fragment and send it to
* the `/api/security/oidc/callback` route.
*/
router.get(
httpResources.register(
{
path,
validate: false,
@ -42,18 +47,14 @@ export function defineOIDCRoutes({ router, logger, authc, csp, basePath }: Route
{ tags: ['deprecation'] }
);
}
return response.custom(
createCustomResourceResponse(
`
<!DOCTYPE html>
<title>Kibana OpenID Connect Login</title>
<link rel="icon" href="data:,">
<script src="${serverBasePath}/internal/security/oidc/implicit.js"></script>
`,
'text/html',
csp.header
)
);
return response.renderHtml({
body: `
<!DOCTYPE html>
<title>Kibana OpenID Connect Login</title>
<link rel="icon" href="data:,">
<script src="${serverBasePath}/internal/security/oidc/implicit.js"></script>
`,
});
}
);
}
@ -63,7 +64,7 @@ export function defineOIDCRoutes({ router, logger, authc, csp, basePath }: Route
* that extracts fragment part from the URL and send it to the `/api/security/oidc/callback` route.
* We need this separate endpoint because of default CSP policy that forbids inline scripts.
*/
router.get(
httpResources.register(
{
path: '/internal/security/oidc/implicit.js',
validate: false,
@ -71,17 +72,13 @@ export function defineOIDCRoutes({ router, logger, authc, csp, basePath }: Route
},
(context, request, response) => {
const serverBasePath = basePath.serverBasePath;
return response.custom(
createCustomResourceResponse(
`
return response.renderJs({
body: `
window.location.replace(
'${serverBasePath}/api/security/oidc/callback?authenticationResponseURI=' + encodeURIComponent(window.location.href)
);
`,
'text/javascript',
csp.header
)
);
});
}
);
@ -155,7 +152,9 @@ export function defineOIDCRoutes({ router, logger, authc, csp, basePath }: Route
}
if (!loginAttempt) {
return response.badRequest({ body: 'Unrecognized login attempt.' });
return response.badRequest({
body: 'Unrecognized login attempt.',
});
}
return performOIDCLogin(request, response, loginAttempt);

View file

@ -7,14 +7,19 @@
import { schema } from '@kbn/config-schema';
import { SAMLLogin } from '../../authentication';
import { SAMLAuthenticationProvider } from '../../authentication/providers';
import { createCustomResourceResponse } from '.';
import { RouteDefinitionParams } from '..';
/**
* Defines routes required for SAML authentication.
*/
export function defineSAMLRoutes({ router, logger, authc, csp, basePath }: RouteDefinitionParams) {
router.get(
export function defineSAMLRoutes({
router,
httpResources,
logger,
authc,
basePath,
}: RouteDefinitionParams) {
httpResources.register(
{
path: '/internal/security/saml/capture-url-fragment',
validate: false,
@ -22,39 +27,30 @@ export function defineSAMLRoutes({ router, logger, authc, csp, basePath }: Route
},
(context, request, response) => {
// We're also preventing `favicon.ico` request since it can cause new SAML handshake.
return response.custom(
createCustomResourceResponse(
`
return response.renderHtml({
body: `
<!DOCTYPE html>
<title>Kibana SAML Login</title>
<link rel="icon" href="data:,">
<script src="${basePath.serverBasePath}/internal/security/saml/capture-url-fragment.js"></script>
`,
'text/html',
csp.header
)
);
});
}
);
router.get(
httpResources.register(
{
path: '/internal/security/saml/capture-url-fragment.js',
validate: false,
options: { authRequired: false },
},
(context, request, response) => {
return response.custom(
createCustomResourceResponse(
`
return response.renderJs({
body: `
window.location.replace(
'${basePath.serverBasePath}/internal/security/saml/start?redirectURLFragment=' + encodeURIComponent(window.location.hash)
);
`,
'text/javascript',
csp.header
)
);
});
}
);

View file

@ -8,6 +8,7 @@ import {
elasticsearchServiceMock,
httpServiceMock,
loggingServiceMock,
httpResourcesMock,
} from '../../../../../src/core/server/mocks';
import { authenticationMock } from '../authentication/index.mock';
import { authorizationMock } from '../authorization/index.mock';
@ -27,5 +28,6 @@ export const routeDefinitionParamsMock = {
authc: authenticationMock.create(),
authz: authorizationMock.create(),
license: licenseMock.create(),
httpResources: httpResourcesMock.createRegistrar(),
}),
};

View file

@ -4,7 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { CoreSetup, IClusterClient, IRouter, Logger } from '../../../../../src/core/server';
import {
CoreSetup,
HttpResources,
IClusterClient,
IRouter,
Logger,
} from '../../../../../src/core/server';
import { SecurityLicense } from '../../common/licensing';
import { Authentication } from '../authentication';
import { Authorization } from '../authorization';
@ -24,7 +30,7 @@ import { defineViewRoutes } from './views';
export interface RouteDefinitionParams {
router: IRouter;
basePath: CoreSetup['http']['basePath'];
csp: CoreSetup['http']['csp'];
httpResources: HttpResources;
logger: Logger;
clusterClient: IClusterClient;
config: ConfigType;

View file

@ -9,11 +9,8 @@ import { RouteDefinitionParams } from '..';
/**
* Defines routes required for the Account Management view.
*/
export function defineAccountManagementRoutes({ router, csp }: RouteDefinitionParams) {
router.get({ path: '/security/account', validate: false }, async (context, request, response) => {
return response.ok({
body: await context.core.rendering.render({ includeUserSettings: true }),
headers: { 'content-security-policy': csp.header },
});
});
export function defineAccountManagementRoutes({ httpResources }: RouteDefinitionParams) {
httpResources.register({ path: '/security/account', validate: false }, (context, req, res) =>
res.renderCoreApp()
);
}

View file

@ -17,7 +17,8 @@ describe('View routes', () => {
defineViewRoutes(routeParamsMock);
expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(`
expect(routeParamsMock.httpResources.register.mock.calls.map(([{ path }]) => path))
.toMatchInlineSnapshot(`
Array [
"/security/account",
"/security/logged_out",
@ -25,6 +26,9 @@ describe('View routes', () => {
"/security/overwritten_session",
]
`);
expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(
`Array []`
);
});
it('registers Login routes if `basic` provider is enabled', () => {
@ -35,16 +39,21 @@ describe('View routes', () => {
defineViewRoutes(routeParamsMock);
expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(`
expect(routeParamsMock.httpResources.register.mock.calls.map(([{ path }]) => path))
.toMatchInlineSnapshot(`
Array [
"/login",
"/internal/security/login_state",
"/security/account",
"/security/logged_out",
"/logout",
"/security/overwritten_session",
]
`);
expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(`
Array [
"/internal/security/login_state",
]
`);
});
it('registers Login routes if `token` provider is enabled', () => {
@ -55,16 +64,21 @@ describe('View routes', () => {
defineViewRoutes(routeParamsMock);
expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(`
expect(routeParamsMock.httpResources.register.mock.calls.map(([{ path }]) => path))
.toMatchInlineSnapshot(`
Array [
"/login",
"/internal/security/login_state",
"/security/account",
"/security/logged_out",
"/logout",
"/security/overwritten_session",
]
`);
expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(`
Array [
"/internal/security/login_state",
]
`);
});
it('registers Login routes if Login Selector is enabled even if both `token` and `basic` providers are not enabled', () => {
@ -75,15 +89,20 @@ describe('View routes', () => {
defineViewRoutes(routeParamsMock);
expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(`
expect(routeParamsMock.httpResources.register.mock.calls.map(([{ path }]) => path))
.toMatchInlineSnapshot(`
Array [
"/login",
"/internal/security/login_state",
"/security/account",
"/security/logged_out",
"/logout",
"/security/overwritten_session",
]
`);
expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(`
Array [
"/internal/security/login_state",
]
`);
});
});

View file

@ -4,20 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
RequestHandler,
RouteConfig,
kibanaResponseFactory,
} from '../../../../../../src/core/server';
import { HttpResourcesRequestHandler, RouteConfig } from '../../../../../../src/core/server';
import { Authentication } from '../../authentication';
import { defineLoggedOutRoutes } from './logged_out';
import { coreMock, httpServerMock } from '../../../../../../src/core/server/mocks';
import { httpServerMock, httpResourcesMock } from '../../../../../../src/core/server/mocks';
import { routeDefinitionParamsMock } from '../index.mock';
describe('LoggedOut view routes', () => {
let authc: jest.Mocked<Authentication>;
let routeHandler: RequestHandler<any, any, any, 'get'>;
let routeHandler: HttpResourcesRequestHandler<any, any, any>;
let routeConfig: RouteConfig<any, any, any, 'get'>;
beforeEach(() => {
const routeParamsMock = routeDefinitionParamsMock.create();
@ -28,7 +24,7 @@ describe('LoggedOut view routes', () => {
const [
loggedOutRouteConfig,
loggedOutRouteHandler,
] = routeParamsMock.router.get.mock.calls.find(
] = routeParamsMock.httpResources.register.mock.calls.find(
([{ path }]) => path === '/security/logged_out'
)!;
@ -51,9 +47,11 @@ describe('LoggedOut view routes', () => {
const request = httpServerMock.createKibanaRequest();
await expect(routeHandler({} as any, request, kibanaResponseFactory)).resolves.toEqual({
options: { headers: { location: '/mock-server-basepath/' } },
status: 302,
const responseFactory = httpResourcesMock.createResponseFactory();
await routeHandler({} as any, request, responseFactory);
expect(responseFactory.redirected).toHaveBeenCalledWith({
headers: { location: '/mock-server-basepath/' },
});
expect(authc.getSessionInfo).toHaveBeenCalledWith(request);
@ -63,21 +61,10 @@ describe('LoggedOut view routes', () => {
authc.getSessionInfo.mockResolvedValue(null);
const request = httpServerMock.createKibanaRequest();
const contextMock = coreMock.createRequestHandlerContext();
await expect(
routeHandler({ core: contextMock } as any, request, kibanaResponseFactory)
).resolves.toEqual({
options: {
headers: {
'content-security-policy':
"script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'",
},
},
status: 200,
});
const responseFactory = httpResourcesMock.createResponseFactory();
await routeHandler({} as any, request, responseFactory);
expect(authc.getSessionInfo).toHaveBeenCalledWith(request);
expect(contextMock.rendering.render).toHaveBeenCalledWith({ includeUserSettings: false });
expect(responseFactory.renderAnonymousCoreApp).toHaveBeenCalledWith();
});
});

View file

@ -16,13 +16,12 @@ import { RouteDefinitionParams } from '..';
* Defines routes required for the Logged Out view.
*/
export function defineLoggedOutRoutes({
router,
logger,
authc,
csp,
httpResources,
basePath,
}: RouteDefinitionParams) {
router.get(
httpResources.register(
{
path: '/security/logged_out',
validate: false,
@ -39,10 +38,7 @@ export function defineLoggedOutRoutes({
});
}
return response.ok({
body: await context.core.rendering.render({ includeUserSettings: false }),
headers: { 'content-security-policy': csp.header },
});
return response.renderAnonymousCoreApp();
}
);
}

View file

@ -7,26 +7,34 @@
import { URL } from 'url';
import { Type } from '@kbn/config-schema';
import {
RequestHandler,
RouteConfig,
kibanaResponseFactory,
HttpResources,
HttpResourcesRequestHandler,
IRouter,
RequestHandler,
kibanaResponseFactory,
RouteConfig,
} from '../../../../../../src/core/server';
import { SecurityLicense } from '../../../common/licensing';
import { LoginState } from '../../../common/login_state';
import { ConfigType } from '../../config';
import { defineLoginRoutes } from './login';
import { coreMock, httpServerMock } from '../../../../../../src/core/server/mocks';
import {
coreMock,
httpServerMock,
httpResourcesMock,
} from '../../../../../../src/core/server/mocks';
import { routeDefinitionParamsMock } from '../index.mock';
describe('Login view routes', () => {
let httpResources: jest.Mocked<HttpResources>;
let router: jest.Mocked<IRouter>;
let license: jest.Mocked<SecurityLicense>;
let config: ConfigType;
beforeEach(() => {
const routeParamsMock = routeDefinitionParamsMock.create();
router = routeParamsMock.router;
httpResources = routeParamsMock.httpResources;
license = routeParamsMock.license;
config = routeParamsMock.config;
@ -34,10 +42,10 @@ describe('Login view routes', () => {
});
describe('View route', () => {
let routeHandler: RequestHandler<any, any, any, 'get'>;
let routeHandler: HttpResourcesRequestHandler<any, any, any>;
let routeConfig: RouteConfig<any, any, any, 'get'>;
beforeEach(() => {
const [loginRouteConfig, loginRouteHandler] = router.get.mock.calls.find(
const [loginRouteConfig, loginRouteHandler] = httpResources.register.mock.calls.find(
([{ path }]) => path === '/login'
)!;
@ -96,9 +104,11 @@ describe('Login view routes', () => {
'https://kibana.co'
);
license.getFeatures.mockReturnValue({ showLogin: true } as any);
await expect(routeHandler({} as any, request, kibanaResponseFactory)).resolves.toEqual({
options: { headers: { location: `${expectedLocation}` } },
status: 302,
const responseFactory = httpResourcesMock.createResponseFactory();
await routeHandler({} as any, request, responseFactory);
expect(responseFactory.redirected).toHaveBeenCalledWith({
headers: { location: `${expectedLocation}` },
});
// Redirect if `showLogin` is `false` even if user is not authenticated.
@ -108,9 +118,12 @@ describe('Login view routes', () => {
'https://kibana.co'
);
license.getFeatures.mockReturnValue({ showLogin: false } as any);
await expect(routeHandler({} as any, request, kibanaResponseFactory)).resolves.toEqual({
options: { headers: { location: `${expectedLocation}` } },
status: 302,
responseFactory.redirected.mockClear();
await routeHandler({} as any, request, responseFactory);
expect(responseFactory.redirected).toHaveBeenCalledWith({
headers: { location: `${expectedLocation}` },
});
}
});
@ -121,19 +134,9 @@ describe('Login view routes', () => {
const request = httpServerMock.createKibanaRequest({ auth: { isAuthenticated: false } });
const contextMock = coreMock.createRequestHandlerContext();
await expect(
routeHandler({ core: contextMock } as any, request, kibanaResponseFactory)
).resolves.toEqual({
options: {
headers: {
'content-security-policy':
"script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'",
},
},
status: 200,
});
expect(contextMock.rendering.render).toHaveBeenCalledWith({ includeUserSettings: false });
const responseFactory = httpResourcesMock.createResponseFactory();
await routeHandler({ core: contextMock } as any, request, responseFactory);
expect(responseFactory.renderAnonymousCoreApp).toHaveBeenCalledWith();
});
});

View file

@ -16,11 +16,11 @@ export function defineLoginRoutes({
config,
router,
logger,
csp,
httpResources,
basePath,
license,
}: RouteDefinitionParams) {
router.get(
httpResources.register(
{
path: '/login',
validate: {
@ -45,10 +45,7 @@ export function defineLoginRoutes({
});
}
return response.ok({
body: await context.core.rendering.render({ includeUserSettings: false }),
headers: { 'content-security-policy': csp.header },
});
return response.renderAnonymousCoreApp();
}
);

View file

@ -9,18 +9,9 @@ import { RouteDefinitionParams } from '..';
/**
* Defines routes required for the Logout out view.
*/
export function defineLogoutRoutes({ router, csp }: RouteDefinitionParams) {
router.get(
{
path: '/logout',
validate: false,
options: { authRequired: false },
},
async (context, request, response) => {
return response.ok({
body: await context.core.rendering.render({ includeUserSettings: false }),
headers: { 'content-security-policy': csp.header },
});
}
export function defineLogoutRoutes({ httpResources }: RouteDefinitionParams) {
httpResources.register(
{ path: '/logout', validate: false, options: { authRequired: false } },
(context, request, response) => response.renderAnonymousCoreApp()
);
}

View file

@ -9,14 +9,9 @@ import { RouteDefinitionParams } from '..';
/**
* Defines routes required for the Overwritten Session view.
*/
export function defineOverwrittenSessionRoutes({ router, csp }: RouteDefinitionParams) {
router.get(
export function defineOverwrittenSessionRoutes({ httpResources }: RouteDefinitionParams) {
httpResources.register(
{ path: '/security/overwritten_session', validate: false },
async (context, request, response) => {
return response.ok({
body: await context.core.rendering.render({ includeUserSettings: true }),
headers: { 'content-security-policy': csp.header },
});
}
(context, req, res) => res.renderCoreApp()
);
}

View file

@ -115,10 +115,8 @@ export class Plugin {
const savedObjectsService = new SpacesSavedObjectsService();
savedObjectsService.setup({ core, spacesService });
const viewRouter = core.http.createRouter();
initSpacesViewsRoutes({
viewRouter,
cspHeader: core.http.csp.header,
httpResources: core.http.resources,
});
const externalRouter = core.http.createRouter();

View file

@ -4,26 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { IRouter } from 'src/core/server';
import { HttpResources } from 'src/core/server';
export interface ViewRouteDeps {
viewRouter: IRouter;
cspHeader: string;
httpResources: HttpResources;
}
export function initSpacesViewsRoutes(deps: ViewRouteDeps) {
deps.viewRouter.get(
{
path: '/spaces/space_selector',
validate: false,
},
async (context, request, response) => {
return response.ok({
headers: {
'Content-Security-Policy': deps.cspHeader,
},
body: await context.core.rendering.render({ includeUserSettings: true }),
});
}
deps.httpResources.register(
{ path: '/spaces/space_selector', validate: false },
(context, request, response) => response.renderCoreApp()
);
}