diff --git a/docs/development/core/server/kibana-plugin-server.authenticationhandler.md b/docs/development/core/server/kibana-plugin-server.authenticationhandler.md index cb09dc0cd6051..482ef44a6e461 100644 --- a/docs/development/core/server/kibana-plugin-server.authenticationhandler.md +++ b/docs/development/core/server/kibana-plugin-server.authenticationhandler.md @@ -8,5 +8,5 @@ Signature: ```typescript -export declare type AuthenticationHandler = (request: Request, sessionStorage: SessionStorage, t: AuthToolkit) => Promise; +export declare type AuthenticationHandler = (request: Readonly, sessionStorage: SessionStorage, t: AuthToolkit) => AuthResult | Promise; ``` diff --git a/docs/development/core/server/kibana-plugin-server.authtoolkit.authenticated.md b/docs/development/core/server/kibana-plugin-server.authtoolkit.authenticated.md index d0f1e07c47484..eabee65cb1451 100644 --- a/docs/development/core/server/kibana-plugin-server.authtoolkit.authenticated.md +++ b/docs/development/core/server/kibana-plugin-server.authtoolkit.authenticated.md @@ -9,5 +9,5 @@ Authentication is successful with given credentials, allow request to pass throu Signature: ```typescript -authenticated: (credentials: any) => AuthResult; +authenticated: (state: object) => AuthResult; ``` diff --git a/docs/development/core/server/kibana-plugin-server.authtoolkit.md b/docs/development/core/server/kibana-plugin-server.authtoolkit.md index d7e2e39b44d4c..cc9f14c57fe4c 100644 --- a/docs/development/core/server/kibana-plugin-server.authtoolkit.md +++ b/docs/development/core/server/kibana-plugin-server.authtoolkit.md @@ -16,7 +16,7 @@ export interface AuthToolkit | Property | Type | Description | | --- | --- | --- | -| [authenticated](./kibana-plugin-server.authtoolkit.authenticated.md) | (credentials: any) => AuthResult | Authentication is successful with given credentials, allow request to pass through | +| [authenticated](./kibana-plugin-server.authtoolkit.authenticated.md) | (state: object) => AuthResult | Authentication is successful with given credentials, allow request to pass through | | [redirected](./kibana-plugin-server.authtoolkit.redirected.md) | (url: string) => AuthResult | Authentication requires to interrupt request handling and redirect to a configured url | | [rejected](./kibana-plugin-server.authtoolkit.rejected.md) | (error: Error, options?: {`

` statusCode?: number;`

` }) => AuthResult | Authentication is unsuccessful, fail the request with specified error. | diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.http.md b/docs/development/core/server/kibana-plugin-server.coresetup.http.md index 46d44d01bd8b0..81e099b7828fe 100644 --- a/docs/development/core/server/kibana-plugin-server.coresetup.http.md +++ b/docs/development/core/server/kibana-plugin-server.coresetup.http.md @@ -8,8 +8,9 @@ ```typescript http: { + registerOnPreAuth: HttpServiceSetup['registerOnPreAuth']; registerAuth: HttpServiceSetup['registerAuth']; - registerOnRequest: HttpServiceSetup['registerOnRequest']; + registerOnPostAuth: HttpServiceSetup['registerOnPostAuth']; getBasePathFor: HttpServiceSetup['getBasePathFor']; setBasePathFor: HttpServiceSetup['setBasePathFor']; createNewServer: HttpServiceSetup['createNewServer']; diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.md b/docs/development/core/server/kibana-plugin-server.coresetup.md index 287eb5f066bfc..1146355d7af4b 100644 --- a/docs/development/core/server/kibana-plugin-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-server.coresetup.md @@ -9,12 +9,13 @@ Context passed to the plugins `setup` method. Signature: ```typescript -export interface CoreSetup +export interface CoreSetup ``` ## Properties -| Property | Type | Description | -| ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | -| [elasticsearch](./kibana-plugin-server.coresetup.elasticsearch.md) | {`

` adminClient\$: Observable<ClusterClient>;`

` dataClient\$: Observable<ClusterClient>;`

` } | | -| [http](./kibana-plugin-server.coresetup.http.md) | {`

` registerOnPreAuth: HttpServiceSetup['registerOnPreAuth'];`

` registerAuth: HttpServiceSetup['registerAuth'];`

` registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];`

` getBasePathFor: HttpServiceSetup['getBasePathFor'];`

` setBasePathFor: HttpServiceSetup['setBasePathFor'];`

` createNewServer: HttpServiceSetup['createNewServer'];`

` } | | +| Property | Type | Description | +| --- | --- | --- | +| [elasticsearch](./kibana-plugin-server.coresetup.elasticsearch.md) | {`

` adminClient$: Observable<ClusterClient>;`

` dataClient$: Observable<ClusterClient>;`

` } | | +| [http](./kibana-plugin-server.coresetup.http.md) | {`

` registerOnPreAuth: HttpServiceSetup['registerOnPreAuth'];`

` registerAuth: HttpServiceSetup['registerAuth'];`

` registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];`

` getBasePathFor: HttpServiceSetup['getBasePathFor'];`

` setBasePathFor: HttpServiceSetup['setBasePathFor'];`

` } | | + diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.md index f7f7707b5657e..f93e4c073eb21 100644 --- a/docs/development/core/server/kibana-plugin-server.kibanarequest.md +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.md @@ -20,6 +20,7 @@ export declare class KibanaRequestParams | | | [path](./kibana-plugin-server.kibanarequest.path.md) | | string | | | [query](./kibana-plugin-server.kibanarequest.query.md) | | Query | | +| [url](./kibana-plugin-server.kibanarequest.url.md) | | Url | | ## Methods diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.url.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.url.md new file mode 100644 index 0000000000000..d552ba55a2b0e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.url.md @@ -0,0 +1,11 @@ + + +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [url](./kibana-plugin-server.kibanarequest.url.md) + +## KibanaRequest.url property + +Signature: + +```typescript +readonly url: Url; +``` diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index a31166302087e..d003d4bc36879 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -12,44 +12,46 @@ The plugin integrates with the core system via lifecycle events: `setup` ## Classes -| Class | Description | -| -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [ClusterClient](./kibana-plugin-server.clusterclient.md) | Represents an Elasticsearch cluster API client and allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)). | -| [KibanaRequest](./kibana-plugin-server.kibanarequest.md) | | -| [Router](./kibana-plugin-server.router.md) | | -| [ScopedClusterClient](./kibana-plugin-server.scopedclusterclient.md) | Serves the same purpose as "normal" ClusterClient but exposes additional callAsCurrentUser method that doesn't use credentials of the Kibana internal user (as callAsInternalUser does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API | +| Class | Description | +| --- | --- | +| [ClusterClient](./kibana-plugin-server.clusterclient.md) | Represents an Elasticsearch cluster API client and allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)). | +| [KibanaRequest](./kibana-plugin-server.kibanarequest.md) | | +| [Router](./kibana-plugin-server.router.md) | | +| [ScopedClusterClient](./kibana-plugin-server.scopedclusterclient.md) | Serves the same purpose as "normal" ClusterClient but exposes additional callAsCurrentUser method that doesn't use credentials of the Kibana internal user (as callAsInternalUser does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API | ## Interfaces -| Interface | Description | -| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | -| [AuthToolkit](./kibana-plugin-server.authtoolkit.md) | A tool set defining an outcome of Auth interceptor for incoming request. | -| [CallAPIOptions](./kibana-plugin-server.callapioptions.md) | The set of options that defines how API call should be made and result be processed. | -| [CoreSetup](./kibana-plugin-server.coresetup.md) | Context passed to the plugins setup method. | -| [CoreStart](./kibana-plugin-server.corestart.md) | Context passed to the plugins start method. | -| [DiscoveredPlugin](./kibana-plugin-server.discoveredplugin.md) | Small container object used to expose information about discovered plugins that may or may not have been started. | -| [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) | | -| [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) | | -| [HttpServiceStart](./kibana-plugin-server.httpservicestart.md) | | -| [InternalCoreStart](./kibana-plugin-server.internalcorestart.md) | | -| [Logger](./kibana-plugin-server.logger.md) | Logger exposes all the necessary methods to log any type of information and this is the interface used by the logging consumers including plugins. | -| [LoggerFactory](./kibana-plugin-server.loggerfactory.md) | The single purpose of LoggerFactory interface is to define a way to retrieve a context-based logger instance. | -| [LogMeta](./kibana-plugin-server.logmeta.md) | Contextual metadata | -| [OnRequestToolkit](./kibana-plugin-server.onrequesttoolkit.md) | A tool set defining an outcome of OnRequest interceptor for incoming request. | -| [Plugin](./kibana-plugin-server.plugin.md) | The interface that should be returned by a PluginInitializer. | -| [PluginInitializerContext](./kibana-plugin-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. | -| [PluginsServiceSetup](./kibana-plugin-server.pluginsservicesetup.md) | | -| [PluginsServiceStart](./kibana-plugin-server.pluginsservicestart.md) | | +| Interface | Description | +| --- | --- | +| [AuthToolkit](./kibana-plugin-server.authtoolkit.md) | A tool set defining an outcome of Auth interceptor for incoming request. | +| [CallAPIOptions](./kibana-plugin-server.callapioptions.md) | The set of options that defines how API call should be made and result be processed. | +| [CoreSetup](./kibana-plugin-server.coresetup.md) | Context passed to the plugins setup method. | +| [CoreStart](./kibana-plugin-server.corestart.md) | Context passed to the plugins start method. | +| [DiscoveredPlugin](./kibana-plugin-server.discoveredplugin.md) | Small container object used to expose information about discovered plugins that may or may not have been started. | +| [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) | | +| [HttpServiceStart](./kibana-plugin-server.httpservicestart.md) | | +| [InternalCoreStart](./kibana-plugin-server.internalcorestart.md) | | +| [Logger](./kibana-plugin-server.logger.md) | Logger exposes all the necessary methods to log any type of information and this is the interface used by the logging consumers including plugins. | +| [LoggerFactory](./kibana-plugin-server.loggerfactory.md) | The single purpose of LoggerFactory interface is to define a way to retrieve a context-based logger instance. | +| [LogMeta](./kibana-plugin-server.logmeta.md) | Contextual metadata | +| [OnPostAuthToolkit](./kibana-plugin-server.onpostauthtoolkit.md) | A tool set defining an outcome of OnPostAuth interceptor for incoming request. | +| [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md) | A tool set defining an outcome of OnPreAuth interceptor for incoming request. | +| [Plugin](./kibana-plugin-server.plugin.md) | The interface that should be returned by a PluginInitializer. | +| [PluginInitializerContext](./kibana-plugin-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. | +| [PluginsServiceSetup](./kibana-plugin-server.pluginsservicesetup.md) | | +| [PluginsServiceStart](./kibana-plugin-server.pluginsservicestart.md) | | ## Type Aliases -| Type Alias | Description | -| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | -| [APICaller](./kibana-plugin-server.apicaller.md) | | -| [AuthenticationHandler](./kibana-plugin-server.authenticationhandler.md) | | -| [ElasticsearchClientConfig](./kibana-plugin-server.elasticsearchclientconfig.md) | | -| [Headers](./kibana-plugin-server.headers.md) | | -| [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) | | -| [OnRequestHandler](./kibana-plugin-server.onrequesthandler.md) | | -| [PluginInitializer](./kibana-plugin-server.plugininitializer.md) | The plugin export at the root of a plugin's server directory should conform to this interface. | -| [PluginName](./kibana-plugin-server.pluginname.md) | Dedicated type for plugin name/id that is supposed to make Map/Set/Arrays that use it as a key or value more obvious. | +| Type Alias | Description | +| --- | --- | +| [APICaller](./kibana-plugin-server.apicaller.md) | | +| [AuthenticationHandler](./kibana-plugin-server.authenticationhandler.md) | | +| [ElasticsearchClientConfig](./kibana-plugin-server.elasticsearchclientconfig.md) | | +| [Headers](./kibana-plugin-server.headers.md) | | +| [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) | | +| [OnPostAuthHandler](./kibana-plugin-server.onpostauthhandler.md) | | +| [OnPreAuthHandler](./kibana-plugin-server.onpreauthhandler.md) | | +| [PluginInitializer](./kibana-plugin-server.plugininitializer.md) | The plugin export at the root of a plugin's server directory should conform to this interface. | +| [PluginName](./kibana-plugin-server.pluginname.md) | Dedicated type for plugin name/id that is supposed to make Map/Set/Arrays that use it as a key or value more obvious. | + diff --git a/docs/development/core/server/kibana-plugin-server.onpostauthhandler.md b/docs/development/core/server/kibana-plugin-server.onpostauthhandler.md new file mode 100644 index 0000000000000..83de25e3f3d6d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpostauthhandler.md @@ -0,0 +1,12 @@ + + +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPostAuthHandler](./kibana-plugin-server.onpostauthhandler.md) + +## OnPostAuthHandler type + + +Signature: + +```typescript +export declare type OnPostAuthHandler = (request: KibanaRequest, t: OnPostAuthToolkit) => OnPostAuthResult | Promise; +``` diff --git a/docs/development/core/server/kibana-plugin-server.onpostauthtoolkit.md b/docs/development/core/server/kibana-plugin-server.onpostauthtoolkit.md new file mode 100644 index 0000000000000..276643c4f9d1a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpostauthtoolkit.md @@ -0,0 +1,22 @@ + + +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPostAuthToolkit](./kibana-plugin-server.onpostauthtoolkit.md) + +## OnPostAuthToolkit interface + +A tool set defining an outcome of OnPostAuth interceptor for incoming request. + +Signature: + +```typescript +export interface OnPostAuthToolkit +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [next](./kibana-plugin-server.onpostauthtoolkit.next.md) | () => OnPostAuthResult | To pass request to the next handler | +| [redirected](./kibana-plugin-server.onpostauthtoolkit.redirected.md) | (url: string) => OnPostAuthResult | To interrupt request handling and redirect to a configured url | +| [rejected](./kibana-plugin-server.onpostauthtoolkit.rejected.md) | (error: Error, options?: {`

` statusCode?: number;`

` }) => OnPostAuthResult | Fail the request with specified error. | + diff --git a/docs/development/core/server/kibana-plugin-server.onpostauthtoolkit.next.md b/docs/development/core/server/kibana-plugin-server.onpostauthtoolkit.next.md new file mode 100644 index 0000000000000..26b4562974e41 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpostauthtoolkit.next.md @@ -0,0 +1,13 @@ + + +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPostAuthToolkit](./kibana-plugin-server.onpostauthtoolkit.md) > [next](./kibana-plugin-server.onpostauthtoolkit.next.md) + +## OnPostAuthToolkit.next property + +To pass request to the next handler + +Signature: + +```typescript +next: () => OnPostAuthResult; +``` diff --git a/docs/development/core/server/kibana-plugin-server.onpostauthtoolkit.redirected.md b/docs/development/core/server/kibana-plugin-server.onpostauthtoolkit.redirected.md new file mode 100644 index 0000000000000..23cef2f97e32b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpostauthtoolkit.redirected.md @@ -0,0 +1,13 @@ + + +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPostAuthToolkit](./kibana-plugin-server.onpostauthtoolkit.md) > [redirected](./kibana-plugin-server.onpostauthtoolkit.redirected.md) + +## OnPostAuthToolkit.redirected property + +To interrupt request handling and redirect to a configured url + +Signature: + +```typescript +redirected: (url: string) => OnPostAuthResult; +``` diff --git a/docs/development/core/server/kibana-plugin-server.onpostauthtoolkit.rejected.md b/docs/development/core/server/kibana-plugin-server.onpostauthtoolkit.rejected.md new file mode 100644 index 0000000000000..a7767dac727a0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpostauthtoolkit.rejected.md @@ -0,0 +1,15 @@ + + +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPostAuthToolkit](./kibana-plugin-server.onpostauthtoolkit.md) > [rejected](./kibana-plugin-server.onpostauthtoolkit.rejected.md) + +## OnPostAuthToolkit.rejected property + +Fail the request with specified error. + +Signature: + +```typescript +rejected: (error: Error, options?: { + statusCode?: number; + }) => OnPostAuthResult; +``` diff --git a/docs/development/core/server/kibana-plugin-server.onpreauthhandler.md b/docs/development/core/server/kibana-plugin-server.onpreauthhandler.md new file mode 100644 index 0000000000000..606ed21dc6463 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpreauthhandler.md @@ -0,0 +1,12 @@ + + +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreAuthHandler](./kibana-plugin-server.onpreauthhandler.md) + +## OnPreAuthHandler type + + +Signature: + +```typescript +export declare type OnPreAuthHandler = (request: KibanaRequest, t: OnPreAuthToolkit) => OnPreAuthResult | Promise; +``` diff --git a/docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.md b/docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.md new file mode 100644 index 0000000000000..066a95f9fa7c7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.md @@ -0,0 +1,22 @@ + + +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md) + +## OnPreAuthToolkit interface + +A tool set defining an outcome of OnPreAuth interceptor for incoming request. + +Signature: + +```typescript +export interface OnPreAuthToolkit +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [next](./kibana-plugin-server.onpreauthtoolkit.next.md) | () => OnPreAuthResult | To pass request to the next handler | +| [redirected](./kibana-plugin-server.onpreauthtoolkit.redirected.md) | (url: string, options?: {`

` forward: boolean;`

` }) => OnPreAuthResult | To interrupt request handling and redirect to a configured url. If "options.forwarded" = true, request will be forwarded to another url right on the server. | +| [rejected](./kibana-plugin-server.onpreauthtoolkit.rejected.md) | (error: Error, options?: {`

` statusCode?: number;`

` }) => OnPreAuthResult | Fail the request with specified error. | + diff --git a/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.next.md b/docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.next.md similarity index 52% rename from docs/development/core/server/kibana-plugin-server.onrequesttoolkit.next.md rename to docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.next.md index 976e3b1a2db87..86369f70ac1d9 100644 --- a/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.next.md +++ b/docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.next.md @@ -1,13 +1,13 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnRequestToolkit](./kibana-plugin-server.onrequesttoolkit.md) > [next](./kibana-plugin-server.onrequesttoolkit.next.md) +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md) > [next](./kibana-plugin-server.onpreauthtoolkit.next.md) -## OnRequestToolkit.next property +## OnPreAuthToolkit.next property To pass request to the next handler Signature: ```typescript -next: () => OnRequestResult; +next: () => OnPreAuthResult; ``` diff --git a/docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.redirected.md b/docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.redirected.md new file mode 100644 index 0000000000000..65c0512b9367b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.redirected.md @@ -0,0 +1,15 @@ + + +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md) > [redirected](./kibana-plugin-server.onpreauthtoolkit.redirected.md) + +## OnPreAuthToolkit.redirected property + +To interrupt request handling and redirect to a configured url. If "options.forwarded" = true, request will be forwarded to another url right on the server. + +Signature: + +```typescript +redirected: (url: string, options?: { + forward: boolean; + }) => OnPreAuthResult; +``` diff --git a/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.rejected.md b/docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.rejected.md similarity index 59% rename from docs/development/core/server/kibana-plugin-server.onrequesttoolkit.rejected.md rename to docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.rejected.md index 447d9b3fb9be5..b267a03b6f934 100644 --- a/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.rejected.md +++ b/docs/development/core/server/kibana-plugin-server.onpreauthtoolkit.rejected.md @@ -1,8 +1,8 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnRequestToolkit](./kibana-plugin-server.onrequesttoolkit.md) > [rejected](./kibana-plugin-server.onrequesttoolkit.rejected.md) +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md) > [rejected](./kibana-plugin-server.onpreauthtoolkit.rejected.md) -## OnRequestToolkit.rejected property +## OnPreAuthToolkit.rejected property Fail the request with specified error. @@ -11,5 +11,5 @@ Fail the request with specified error. ```typescript rejected: (error: Error, options?: { statusCode?: number; - }) => OnRequestResult; + }) => OnPreAuthResult; ``` diff --git a/docs/development/core/server/kibana-plugin-server.onrequesthandler.md b/docs/development/core/server/kibana-plugin-server.onrequesthandler.md deleted file mode 100644 index 5d90e399db676..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.onrequesthandler.md +++ /dev/null @@ -1,12 +0,0 @@ - - -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnRequestHandler](./kibana-plugin-server.onrequesthandler.md) - -## OnRequestHandler type - - -Signature: - -```typescript -export declare type OnRequestHandler = (req: KibanaRequest, t: OnRequestToolkit) => OnRequestResult | Promise; -``` diff --git a/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.md b/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.md deleted file mode 100644 index e6a79a13dd436..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.md +++ /dev/null @@ -1,23 +0,0 @@ - - -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnRequestToolkit](./kibana-plugin-server.onrequesttoolkit.md) - -## OnRequestToolkit interface - -A tool set defining an outcome of OnRequest interceptor for incoming request. - -Signature: - -```typescript -export interface OnRequestToolkit -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [next](./kibana-plugin-server.onrequesttoolkit.next.md) | () => OnRequestResult | To pass request to the next handler | -| [redirected](./kibana-plugin-server.onrequesttoolkit.redirected.md) | (url: string) => OnRequestResult | To interrupt request handling and redirect to a configured url | -| [rejected](./kibana-plugin-server.onrequesttoolkit.rejected.md) | (error: Error, options?: {`

` statusCode?: number;`

` }) => OnRequestResult | Fail the request with specified error. | -| [setUrl](./kibana-plugin-server.onrequesttoolkit.seturl.md) | (newUrl: string | Url) => void | Change url for an incoming request. | - diff --git a/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.redirected.md b/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.redirected.md deleted file mode 100644 index 311398845bd59..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.redirected.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnRequestToolkit](./kibana-plugin-server.onrequesttoolkit.md) > [redirected](./kibana-plugin-server.onrequesttoolkit.redirected.md) - -## OnRequestToolkit.redirected property - -To interrupt request handling and redirect to a configured url - -Signature: - -```typescript -redirected: (url: string) => OnRequestResult; -``` diff --git a/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.seturl.md b/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.seturl.md deleted file mode 100644 index 0f20cbdb18d96..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.seturl.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnRequestToolkit](./kibana-plugin-server.onrequesttoolkit.md) > [setUrl](./kibana-plugin-server.onrequesttoolkit.seturl.md) - -## OnRequestToolkit.setUrl property - -Change url for an incoming request. - -Signature: - -```typescript -setUrl: (newUrl: string | Url) => void; -``` diff --git a/src/core/server/http/auth_state_storage.ts b/src/core/server/http/auth_state_storage.ts new file mode 100644 index 0000000000000..eafe755b79eea --- /dev/null +++ b/src/core/server/http/auth_state_storage.ts @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Request } from 'hapi'; +import { KibanaRequest } from './router'; + +export enum AuthStatus { + authenticated = 'authenticated', + unauthenticated = 'unauthenticated', + unknown = 'unknown', +} + +const toKey = (request: KibanaRequest | Request) => + request instanceof KibanaRequest ? request.unstable_getIncomingMessage() : request.raw.req; + +export class AuthStateStorage { + private readonly storage = new WeakMap, unknown>(); + constructor(private readonly canBeAuthenticated: () => boolean) {} + public set = (request: KibanaRequest | Request, state: unknown) => { + this.storage.set(toKey(request), state); + }; + public get = (request: KibanaRequest | Request) => { + const key = toKey(request); + const state = this.storage.get(key); + const status: AuthStatus = this.storage.has(key) + ? AuthStatus.authenticated + : this.canBeAuthenticated() + ? AuthStatus.unauthenticated + : AuthStatus.unknown; + + return { status, state }; + }; + public isAuthenticated = (request: KibanaRequest | Request) => { + return this.get(request).status === AuthStatus.authenticated; + }; +} diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index b03986ead2d5b..0ee069282c6a5 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -600,9 +600,9 @@ test('registers auth request interceptor only once', async () => { expect(doRegister()).rejects.toThrowError('Auth interceptor was already registered'); }); -test('registers onRequest interceptor several times', async () => { - const { registerOnRequest } = await server.setup(config); - const doRegister = () => registerOnRequest(() => null as any); +test('registers registerOnPostAuth interceptor several times', async () => { + const { registerOnPostAuth } = await server.setup(config); + const doRegister = () => registerOnPostAuth(() => null as any); doRegister(); expect(doRegister).not.toThrowError(); @@ -620,11 +620,11 @@ test('#getBasePathFor() returns base path associated with an incoming request', setBasePathFor, registerRouter, server: innerServer, - registerOnRequest, + registerOnPostAuth, } = await server.setup(config); const path = '/base-path'; - registerOnRequest((req, t) => { + registerOnPostAuth((req, t) => { setBasePathFor(req, path); return t.next(); }); @@ -652,11 +652,11 @@ test('#getBasePathFor() is based on server base path', async () => { setBasePathFor, registerRouter, server: innerServer, - registerOnRequest, + registerOnPostAuth, } = await server.setup(configWithBasePath); const path = '/base-path'; - registerOnRequest((req, t) => { + registerOnPostAuth((req, t) => { setBasePathFor(req, path); return t.next(); }); @@ -707,3 +707,149 @@ test('#setBasePathFor() cannot be set twice for one request', async () => { `"Request basePath was previously set. Setting multiple times is not supported."` ); }); +const cookieOptions = { + name: 'sid', + encryptionKey: 'something_at_least_32_characters', + validate: () => true, + isSecure: false, +}; + +test('Should enable auth for a route by default if registerAuth has been called', async () => { + const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); + + const router = new Router(''); + router.get({ path: '/', validate: false }, async (req, res) => res.ok({})); + registerRouter(router); + + const authenticate = jest + .fn() + .mockImplementation((req, sessionStorage, t) => t.authenticated({})); + await registerAuth(authenticate, cookieOptions); + + await server.start(); + await supertest(innerServer.listener) + .get('/') + .expect(200); + + expect(authenticate).toHaveBeenCalledTimes(1); +}); + +test('Should support disabling auth for a route', async () => { + const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); + + const router = new Router(''); + router.get({ path: '/', validate: false, authRequired: false }, async (req, res) => res.ok({})); + registerRouter(router); + const authenticate = jest.fn(); + await registerAuth(authenticate, cookieOptions); + + await server.start(); + await supertest(innerServer.listener) + .get('/') + .expect(200); + + expect(authenticate).not.toHaveBeenCalled(); +}); + +describe('#auth.isAuthenticated()', () => { + it('returns true if has been authorized', async () => { + const { registerAuth, registerRouter, server: innerServer, auth } = await server.setup(config); + + const router = new Router(''); + router.get({ path: '/', validate: false }, async (req, res) => + res.ok({ isAuthenticated: auth.isAuthenticated(req) }) + ); + registerRouter(router); + + await registerAuth((req, sessionStorage, t) => t.authenticated({}), cookieOptions); + + await server.start(); + await supertest(innerServer.listener) + .get('/') + .expect(200, { isAuthenticated: true }); + }); + + it('returns false if has not been authorized', async () => { + const { registerAuth, registerRouter, server: innerServer, auth } = await server.setup(config); + + const router = new Router(''); + router.get({ path: '/', validate: false, authRequired: false }, async (req, res) => + res.ok({ isAuthenticated: auth.isAuthenticated(req) }) + ); + registerRouter(router); + + await registerAuth((req, sessionStorage, t) => t.authenticated({}), cookieOptions); + + await server.start(); + await supertest(innerServer.listener) + .get('/') + .expect(200, { isAuthenticated: false }); + }); + + it('returns false if no authorization mechanism has been registered', async () => { + const { registerRouter, server: innerServer, auth } = await server.setup(config); + + const router = new Router(''); + router.get({ path: '/', validate: false, authRequired: false }, async (req, res) => + res.ok({ isAuthenticated: auth.isAuthenticated(req) }) + ); + registerRouter(router); + + await server.start(); + await supertest(innerServer.listener) + .get('/') + .expect(200, { isAuthenticated: false }); + }); +}); + +describe('#auth.get()', () => { + it('Should return authenticated status and allow associate auth state with request', async () => { + const user = { id: '42' }; + const { registerRouter, registerAuth, server: innerServer, auth } = await server.setup(config); + await registerAuth((req, sessionStorage, t) => { + sessionStorage.set({ value: user }); + return t.authenticated(user); + }, cookieOptions); + + const router = new Router(''); + router.get({ path: '/', validate: false }, async (req, res) => res.ok(auth.get(req))); + registerRouter(router); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, { state: user, status: 'authenticated' }); + }); + + it('Should return correct authentication unknown status', async () => { + const { registerRouter, server: innerServer, auth } = await server.setup(config); + const router = new Router(''); + router.get({ path: '/', validate: false }, async (req, res) => res.ok(auth.get(req))); + + registerRouter(router); + await server.start(); + await supertest(innerServer.listener) + .get('/') + .expect(200, { status: 'unknown' }); + }); + + it('Should return correct unauthenticated status', async () => { + const authenticate = jest.fn(); + + const { registerRouter, registerAuth, server: innerServer, auth } = await server.setup(config); + await registerAuth(authenticate, cookieOptions); + const router = new Router(''); + router.get({ path: '/', validate: false, authRequired: false }, async (req, res) => + res.ok(auth.get(req)) + ); + + registerRouter(router); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, { status: 'unauthenticated' }); + + expect(authenticate).not.toHaveBeenCalled(); + }); +}); diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 8dd32dfbf6c2b..03283a72001a3 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -24,33 +24,51 @@ import { Logger } from '../logging'; import { HttpConfig } from './http_config'; import { createServer, getServerOptions } from './http_tools'; import { adoptToHapiAuthFormat, AuthenticationHandler } from './lifecycle/auth'; -import { adoptToHapiOnRequestFormat, OnRequestHandler } from './lifecycle/on_request'; +import { adoptToHapiOnPostAuthFormat, OnPostAuthHandler } from './lifecycle/on_post_auth'; +import { adoptToHapiOnPreAuthFormat, OnPreAuthHandler } from './lifecycle/on_pre_auth'; import { Router, KibanaRequest } from './router'; import { SessionStorageCookieOptions, createCookieSessionStorageFactory, } from './cookie_session_storage'; +import { AuthStateStorage } from './auth_state_storage'; export interface HttpServerSetup { server: Server; options: ServerOptions; registerRouter: (router: Router) => void; /** - * Define custom authentication and/or authorization mechanism for incoming requests. - * Applied to all resources by default. Only one AuthenticationHandler can be registered. + * To define custom authentication and/or authorization mechanism for incoming requests. + * A handler should return a state to associate with the incoming request. + * The state can be retrieved later via http.auth.get(..) + * Only one AuthenticationHandler can be registered. */ registerAuth: ( - authenticationHandler: AuthenticationHandler, + handler: AuthenticationHandler, cookieOptions: SessionStorageCookieOptions - ) => void; + ) => Promise; /** - * Define custom logic to perform for incoming requests. - * Applied to all resources by default. - * Can register any number of OnRequestHandlers, which are called in sequence (from the first registered to the last) + * To define custom logic to perform for incoming requests. Runs the handler before Auth + * hook performs a check that user has access to requested resources, so it's the only + * place when you can forward a request to another URL right on the server. + * Can register any number of registerOnPostAuth, which are called in sequence + * (from the first registered to the last). */ - registerOnRequest: (requestHandler: OnRequestHandler) => void; + registerOnPreAuth: (handler: OnPreAuthHandler) => void; + /** + * To define custom logic to perform for incoming requests. Runs the handler after Auth hook + * did make sure a user has access to the requested resource. + * The auth state is available at stage via http.auth.get(..) + * Can register any number of registerOnPreAuth, which are called in sequence + * (from the first registered to the last). + */ + registerOnPostAuth: (handler: OnPostAuthHandler) => void; getBasePathFor: (request: KibanaRequest | Request) => string; setBasePathFor: (request: KibanaRequest | Request, basePath: string) => void; + auth: { + get: AuthStateStorage['get']; + isAuthenticated: AuthStateStorage['isAuthenticated']; + }; } export class HttpServer { @@ -63,7 +81,11 @@ export class HttpServer { string >(); - constructor(private readonly log: Logger) {} + private readonly authState: AuthStateStorage; + + constructor(private readonly log: Logger) { + this.authState = new AuthStateStorage(() => this.authRegistered); + } public isListening() { return this.server !== undefined && this.server.listener.listening; @@ -105,16 +127,23 @@ export class HttpServer { this.server = createServer(serverOptions); this.config = config; + this.setupBasePathRewrite(config); + return { options: serverOptions, registerRouter: this.registerRouter.bind(this), - registerOnRequest: this.registerOnRequest.bind(this), + registerOnPreAuth: this.registerOnPreAuth.bind(this), + registerOnPostAuth: this.registerOnPostAuth.bind(this), registerAuth: ( fn: AuthenticationHandler, cookieOptions: SessionStorageCookieOptions ) => this.registerAuth(fn, cookieOptions, config.basePath), getBasePathFor: this.getBasePathFor.bind(this, config), setBasePathFor: this.setBasePathFor.bind(this), + auth: { + get: this.authState.get, + isAuthenticated: this.authState.isAuthenticated, + }, // Return server instance with the connection options so that we can properly // bridge core and the "legacy" Kibana internally. Once this bridge isn't // needed anymore we shouldn't return the instance from this method. @@ -128,14 +157,16 @@ export class HttpServer { } this.log.debug('starting http server'); - this.setupBasePathRewrite(this.server); - for (const router of this.registeredRouters) { for (const route of router.getRoutes()) { + const isAuthRequired = Boolean(this.authRegistered && route.authRequired); this.server.route({ handler: route.handler, method: route.method, path: this.getRouteFullPath(router.path, route.path), + options: { + auth: isAuthRequired ? undefined : false, + }, }); } } @@ -155,13 +186,13 @@ export class HttpServer { this.server = undefined; } - private setupBasePathRewrite(server: Server) { - if (this.config!.basePath === undefined || !this.config!.rewriteBasePath) { + private setupBasePathRewrite(config: HttpConfig) { + if (config.basePath === undefined || !config.rewriteBasePath) { return; } - const basePath = this.config!.basePath; - server.ext('onRequest', (request, responseToolkit) => { + const basePath = config.basePath; + this.registerOnPreAuth((request, toolkit) => { const newURL = modifyUrl(request.url.href!, urlParts => { if (urlParts.pathname != null && urlParts.pathname.startsWith(basePath)) { urlParts.pathname = urlParts.pathname.replace(basePath, '') || '/'; @@ -170,18 +201,10 @@ export class HttpServer { } }); if (!newURL) { - return responseToolkit - .response('Not Found') - .code(404) - .takeover(); + return toolkit.rejected(new Error('not found'), { statusCode: 404 }); } - request.setUrl(newURL); - // We should update raw request as well since it can be proxied to the old platform - // where base path isn't expected. - request.raw.req.url = request.url.href; - - return responseToolkit.continue; + return toolkit.redirected(newURL, { forward: true }); }); } @@ -192,12 +215,20 @@ export class HttpServer { return `${routerPath}${routePath.slice(routePathStartIndex)}`; } - private registerOnRequest(fn: OnRequestHandler) { + private registerOnPostAuth(fn: OnPostAuthHandler) { + if (this.server === undefined) { + throw new Error('Server is not created yet'); + } + + this.server.ext('onPostAuth', adoptToHapiOnPostAuthFormat(fn)); + } + + private registerOnPreAuth(fn: OnPreAuthHandler) { if (this.server === undefined) { throw new Error('Server is not created yet'); } - this.server.ext('onRequest', adoptToHapiOnRequestFormat(fn)); + this.server.ext('onRequest', adoptToHapiOnPreAuthFormat(fn)); } private async registerAuth( @@ -220,7 +251,7 @@ export class HttpServer { ); this.server.auth.scheme('login', () => ({ - authenticate: adoptToHapiAuthFormat(fn, sessionStorage), + authenticate: adoptToHapiAuthFormat(fn, sessionStorage, this.authState.set), })); this.server.auth.strategy('session', 'login'); diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts index 54a93d5059599..ff20385792d83 100644 --- a/src/core/server/http/http_service.mock.ts +++ b/src/core/server/http/http_service.mock.ts @@ -19,21 +19,23 @@ import { Server, ServerOptions } from 'hapi'; import { HttpService } from './http_service'; -import { HttpConfig } from './http_config'; -import { HttpServerSetup } from './http_server'; const createSetupContractMock = () => { const setupContract = { options: {} as ServerOptions, + registerOnPreAuth: jest.fn(), registerAuth: jest.fn(), - registerOnRequest: jest.fn(), + registerOnPostAuth: jest.fn(), registerRouter: jest.fn(), getBasePathFor: jest.fn(), setBasePathFor: jest.fn(), // we can mock some hapi server method when we need it server: {} as Server, - createNewServer: async (cfg: Partial): Promise => - ({} as HttpServerSetup), + auth: { + get: jest.fn(), + isAuthenticated: jest.fn(), + }, + createNewServer: jest.fn().mockResolvedValue({}), }; return setupContract; }; diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index 465c5cb6a859b..ece61579314b1 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -21,5 +21,6 @@ export { config, HttpConfig, HttpConfigType } from './http_config'; export { HttpService, HttpServiceSetup, HttpServiceStart } from './http_service'; export { Router, KibanaRequest } from './router'; export { BasePathProxyServer } from './base_path_proxy_server'; +export { OnPreAuthHandler, OnPreAuthToolkit } from './lifecycle/on_pre_auth'; export { AuthenticationHandler, AuthToolkit } from './lifecycle/auth'; -export { OnRequestHandler, OnRequestToolkit } from './lifecycle/on_request'; +export { OnPostAuthHandler, OnPostAuthToolkit } from './lifecycle/on_post_auth'; diff --git a/src/core/server/http/integration_tests/http_service.test.ts b/src/core/server/http/integration_tests/http_service.test.ts index 9913f9914a0a8..93fe20a80e120 100644 --- a/src/core/server/http/integration_tests/http_service.test.ts +++ b/src/core/server/http/integration_tests/http_service.test.ts @@ -16,9 +16,6 @@ * specific language governing permissions and limitations * under the License. */ - -import { parse } from 'url'; - import request from 'request'; import Boom from 'boom'; @@ -64,14 +61,14 @@ describe('http service', () => { if (req.headers.authorization) { const user = { id: '42' }; sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs }); - return t.authenticated({ credentials: user }); + return t.authenticated(user); } else { return t.rejected(Boom.unauthorized()); } }; const { http } = await root.setup(); - http.registerAuth(authenticate, cookieOptions); + await http.registerAuth(authenticate, cookieOptions); http.registerRouter(router); await root.start(); @@ -97,14 +94,14 @@ describe('http service', () => { if (req.headers.authorization) { const user = { id: '42' }; sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs }); - return t.authenticated({ credentials: user }); + return t.authenticated(user); } else { return t.rejected(Boom.unauthorized()); } }; const { http } = await root.setup(); - http.registerAuth(authenticate, cookieOptions); + await http.registerAuth(authenticate, cookieOptions); await root.start(); await kbnTestServer.request @@ -120,7 +117,7 @@ describe('http service', () => { }; const { http } = await root.setup(); - http.registerAuth(authenticate, cookieOptions); + await http.registerAuth(authenticate, cookieOptions); await root.start(); const response = await kbnTestServer.request.get(root, '/').expect(302); @@ -132,14 +129,14 @@ describe('http service', () => { if (req.headers.authorization) { const user = { id: '42' }; sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs }); - return t.authenticated({ credentials: user }); + return t.authenticated(user); } else { return t.rejected(Boom.unauthorized()); } }; const { http } = await root.setup(); - http.registerAuth(authenticate, cookieOptions); + await http.registerAuth(authenticate, cookieOptions); await root.start(); const legacyUrl = '/legacy'; @@ -157,13 +154,43 @@ describe('http service', () => { expect(response.header['set-cookie']).toBe(undefined); }); + it('Should pass associated auth state to Legacy platform', async () => { + const user = { id: '42' }; + const authenticate: AuthenticationHandler = async (req, sessionStorage, t) => { + if (req.headers.authorization) { + sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs }); + return t.authenticated(user); + } else { + return t.rejected(Boom.unauthorized()); + } + }; + + const { http } = await root.setup(); + await http.registerAuth(authenticate, cookieOptions); + await root.start(); + + const legacyUrl = '/legacy'; + const kbnServer = kbnTestServer.getKbnServer(root); + kbnServer.server.route({ + method: 'GET', + path: legacyUrl, + handler: kbnServer.newPlatform.setup.core.http.auth.get, + }); + + const response = await kbnTestServer.request.get(root, legacyUrl).expect(200); + expect(response.body.state).toEqual(user); + expect(response.body.status).toEqual('authenticated'); + + expect(response.header['set-cookie']).toBe(undefined); + }); + it(`Shouldn't expose internal error details`, async () => { const authenticate: AuthenticationHandler = async (req, sessionStorage, t) => { throw new Error('sensitive info'); }; const { http } = await root.setup(); - http.registerAuth(authenticate, cookieOptions); + await http.registerAuth(authenticate, cookieOptions); await root.start(); await kbnTestServer.request.get(root, '/').expect({ @@ -174,7 +201,7 @@ describe('http service', () => { }); }); - describe('#registerOnRequest()', () => { + describe('#registerOnPostAuth()', () => { let root: ReturnType; beforeEach(async () => { root = kbnTestServer.createRoot(); @@ -186,8 +213,8 @@ describe('http service', () => { router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' })); const { http } = await root.setup(); - http.registerOnRequest((req, t) => t.next()); - http.registerOnRequest(async (req, t) => { + http.registerOnPostAuth((req, t) => t.next()); + http.registerOnPostAuth(async (req, t) => { await Promise.resolve(); return t.next(); }); @@ -200,7 +227,7 @@ describe('http service', () => { it('Should support redirecting to configured url', async () => { const redirectTo = '/redirect-url'; const { http } = await root.setup(); - http.registerOnRequest(async (req, t) => t.redirected(redirectTo)); + http.registerOnPostAuth(async (req, t) => t.redirected(redirectTo)); await root.start(); const response = await kbnTestServer.request.get(root, '/').expect(302); @@ -209,7 +236,7 @@ describe('http service', () => { it('Should failing a request with configured error and status code', async () => { const { http } = await root.setup(); - http.registerOnRequest(async (req, t) => + http.registerOnPostAuth(async (req, t) => t.rejected(new Error('unexpected error'), { statusCode: 400 }) ); await root.start(); @@ -221,7 +248,7 @@ describe('http service', () => { it(`Shouldn't expose internal error details`, async () => { const { http } = await root.setup(); - http.registerOnRequest(async (req, t) => { + http.registerOnPostAuth(async (req, t) => { throw new Error('sensitive info'); }); await root.start(); @@ -235,12 +262,12 @@ describe('http service', () => { it(`Shouldn't share request object between interceptors`, async () => { const { http } = await root.setup(); - http.registerOnRequest(async (req, t) => { + http.registerOnPostAuth(async (req, t) => { // @ts-ignore. don't complain customField is not defined on Request type req.customField = { value: 42 }; return t.next(); }); - http.registerOnRequest((req, t) => { + http.registerOnPostAuth((req, t) => { // @ts-ignore don't complain customField is not defined on Request type if (typeof req.customField !== 'undefined') { throw new Error('Request object was mutated'); @@ -259,7 +286,7 @@ describe('http service', () => { }); }); - describe('#registerOnRequest() toolkit', () => { + describe('#registerOnPostAuth() toolkit', () => { let root: ReturnType; beforeEach(async () => { root = kbnTestServer.createRoot(); @@ -268,9 +295,8 @@ describe('http service', () => { afterEach(async () => await root.shutdown()); it('supports Url change on the flight', async () => { const { http } = await root.setup(); - http.registerOnRequest((req, t) => { - t.setUrl(parse('/new-url')); - return t.next(); + http.registerOnPreAuth((req, t) => { + return t.redirected('/new-url', { forward: true }); }); const router = new Router('/'); @@ -287,9 +313,8 @@ describe('http service', () => { it('url re-write works for legacy server as well', async () => { const { http } = await root.setup(); const newUrl = '/new-url'; - http.registerOnRequest((req, t) => { - t.setUrl(newUrl); - return t.next(); + http.registerOnPreAuth((req, t) => { + return t.redirected(newUrl, { forward: true }); }); await root.start(); @@ -314,7 +339,7 @@ describe('http service', () => { it('basePath information for an incoming request is available in legacy server', async () => { const reqBasePath = '/requests-specific-base-path'; const { http } = await root.setup(); - http.registerOnRequest((req, t) => { + http.registerOnPreAuth((req, t) => { http.setBasePathFor(req, reqBasePath); return t.next(); }); diff --git a/src/core/server/http/lifecycle/auth.ts b/src/core/server/http/lifecycle/auth.ts index 8205d21c5ff59..ffe77e0120fe4 100644 --- a/src/core/server/http/lifecycle/auth.ts +++ b/src/core/server/http/lifecycle/auth.ts @@ -17,6 +17,7 @@ * under the License. */ import Boom from 'boom'; +import { noop } from 'lodash'; import { Lifecycle, Request, ResponseToolkit } from 'hapi'; import { SessionStorage, SessionStorageFactory } from '../session_storage'; @@ -26,39 +27,60 @@ enum ResultType { rejected = 'rejected', } -/** @internal */ -class AuthResult { - public static authenticated(credentials: any) { - return new AuthResult(ResultType.authenticated, credentials); - } - public static redirected(url: string) { - return new AuthResult(ResultType.redirected, url); - } - public static rejected(error: Error, options: { statusCode?: number } = {}) { - return new AuthResult(ResultType.rejected, { error, statusCode: options.statusCode }); - } - public static isValidResult(candidate: any) { - return candidate instanceof AuthResult; - } - constructor(private readonly type: ResultType, public readonly payload: any) {} - public isAuthenticated() { - return this.type === ResultType.authenticated; - } - public isRedirected() { - return this.type === ResultType.redirected; - } - public isRejected() { - return this.type === ResultType.rejected; - } +interface Authenticated { + type: ResultType.authenticated; + state: object; } +interface Redirected { + type: ResultType.redirected; + url: string; +} + +interface Rejected { + type: ResultType.rejected; + error: Error; + statusCode?: number; +} + +type AuthResult = Authenticated | Rejected | Redirected; + +const authResult = { + authenticated(state: object): AuthResult { + return { type: ResultType.authenticated, state }; + }, + redirected(url: string): AuthResult { + return { type: ResultType.redirected, url }; + }, + rejected(error: Error, options: { statusCode?: number } = {}): AuthResult { + return { type: ResultType.rejected, error, statusCode: options.statusCode }; + }, + isValid(candidate: any): candidate is AuthResult { + return ( + candidate && + (candidate.type === ResultType.authenticated || + candidate.type === ResultType.rejected || + candidate.type === ResultType.redirected) + ); + }, + isAuthenticated(result: AuthResult): result is Authenticated { + return result.type === ResultType.authenticated; + }, + isRedirected(result: AuthResult): result is Redirected { + return result.type === ResultType.redirected; + }, + isRejected(result: AuthResult): result is Rejected { + return result.type === ResultType.rejected; + }, +}; + /** * @public * A tool set defining an outcome of Auth interceptor for incoming request. */ export interface AuthToolkit { /** Authentication is successful with given credentials, allow request to pass through */ - authenticated: (credentials: any) => AuthResult; + authenticated: (state: object) => AuthResult; /** Authentication requires to interrupt request handling and redirect to a configured url */ redirected: (url: string) => AuthResult; /** Authentication is unsuccessful, fail the request with specified error. */ @@ -66,22 +88,23 @@ export interface AuthToolkit { } const toolkit: AuthToolkit = { - authenticated: AuthResult.authenticated, - redirected: AuthResult.redirected, - rejected: AuthResult.rejected, + authenticated: authResult.authenticated, + redirected: authResult.redirected, + rejected: authResult.rejected, }; /** @public */ export type AuthenticationHandler = ( - request: Request, + request: Readonly, sessionStorage: SessionStorage, t: AuthToolkit -) => Promise; +) => AuthResult | Promise; /** @public */ export function adoptToHapiAuthFormat( fn: AuthenticationHandler, - sessionStorage: SessionStorageFactory + sessionStorage: SessionStorageFactory, + onSuccess: (req: Request, state: unknown) => void = noop ) { return async function interceptAuth( req: Request, @@ -89,22 +112,20 @@ export function adoptToHapiAuthFormat( ): Promise { try { const result = await fn(req, sessionStorage.asScoped(req), toolkit); - - if (AuthResult.isValidResult(result)) { - if (result.isAuthenticated()) { - return h.authenticated({ credentials: result.payload }); - } - if (result.isRedirected()) { - return h.redirect(result.payload).takeover(); - } - if (result.isRejected()) { - const { error, statusCode } = result.payload; - return Boom.boomify(error, { statusCode }); - } + if (!authResult.isValid(result)) { + throw new Error( + `Unexpected result from Authenticate. Expected AuthResult, but given: ${result}.` + ); + } + if (authResult.isAuthenticated(result)) { + onSuccess(req, result.state); + return h.authenticated({ credentials: result.state }); + } + if (authResult.isRedirected(result)) { + return h.redirect(result.url).takeover(); } - throw new Error( - `Unexpected result from Authenticate. Expected AuthResult, but given: ${result}.` - ); + const { error, statusCode } = result; + return Boom.boomify(error, { statusCode }); } catch (error) { return Boom.internal(error.message, { statusCode: 500 }); } diff --git a/src/core/server/http/lifecycle/on_request.test.ts b/src/core/server/http/lifecycle/on_post_auth.test.ts similarity index 71% rename from src/core/server/http/lifecycle/on_request.test.ts rename to src/core/server/http/lifecycle/on_post_auth.test.ts index bc4410c773288..7644b8a35ef77 100644 --- a/src/core/server/http/lifecycle/on_request.test.ts +++ b/src/core/server/http/lifecycle/on_post_auth.test.ts @@ -18,16 +18,16 @@ */ import Boom from 'boom'; -import { adoptToHapiOnRequestFormat } from './on_request'; +import { adoptToHapiOnPostAuthFormat } from './on_post_auth'; const requestMock = {} as any; const createResponseToolkit = (customization = {}): any => ({ ...customization }); -describe('adoptToHapiOnRequestFormat', () => { +describe('adoptToHapiOnPostAuthFormat', () => { it('Should allow passing request to the next handler', async () => { const continueSymbol = {}; - const onRequest = adoptToHapiOnRequestFormat((req, t) => t.next()); - const result = await onRequest( + const onPostAuth = adoptToHapiOnPostAuthFormat((req, t) => t.next()); + const result = await onPostAuth( requestMock, createResponseToolkit({ ['continue']: continueSymbol, @@ -39,10 +39,10 @@ describe('adoptToHapiOnRequestFormat', () => { it('Should support redirecting to specified url', async () => { const redirectUrl = '/docs'; - const onRequest = adoptToHapiOnRequestFormat((req, t) => t.redirected(redirectUrl)); + const onPostAuth = adoptToHapiOnPostAuthFormat((req, t) => t.redirected(redirectUrl)); const takeoverSymbol = {}; const redirectMock = jest.fn(() => ({ takeover: () => takeoverSymbol })); - const result = await onRequest( + const result = await onPostAuth( requestMock, createResponseToolkit({ redirect: redirectMock, @@ -54,10 +54,10 @@ describe('adoptToHapiOnRequestFormat', () => { }); it('Should support specifying statusCode and message for Boom error', async () => { - const onRequest = adoptToHapiOnRequestFormat((req, t) => { + const onPostAuth = adoptToHapiOnPostAuthFormat((req, t) => { return t.rejected(new Error('unexpected result'), { statusCode: 501 }); }); - const result = (await onRequest(requestMock, createResponseToolkit())) as Boom; + const result = (await onPostAuth(requestMock, createResponseToolkit())) as Boom; expect(result).toBeInstanceOf(Boom); expect(result.message).toBe('unexpected result'); @@ -65,10 +65,10 @@ describe('adoptToHapiOnRequestFormat', () => { }); it('Should return Boom.internal error if interceptor throws', async () => { - const onRequest = adoptToHapiOnRequestFormat((req, t) => { + const onPostAuth = adoptToHapiOnPostAuthFormat((req, t) => { throw new Error('unknown error'); }); - const result = (await onRequest(requestMock, createResponseToolkit())) as Boom; + const result = (await onPostAuth(requestMock, createResponseToolkit())) as Boom; expect(result).toBeInstanceOf(Boom); expect(result.message).toBe('unknown error'); @@ -76,12 +76,12 @@ describe('adoptToHapiOnRequestFormat', () => { }); it('Should return Boom.internal error if interceptor returns unexpected result', async () => { - const onRequest = adoptToHapiOnRequestFormat((req, toolkit) => undefined as any); - const result = (await onRequest(requestMock, createResponseToolkit())) as Boom; + const onPostAuth = adoptToHapiOnPostAuthFormat((req, toolkit) => undefined as any); + const result = (await onPostAuth(requestMock, createResponseToolkit())) as Boom; expect(result).toBeInstanceOf(Boom); - expect(result.message).toBe( - 'Unexpected result from OnRequest. Expected OnRequestResult, but given: undefined.' + expect(result.message).toMatchInlineSnapshot( + `"Unexpected result from OnPostAuth. Expected OnPostAuthResult, but given: undefined."` ); expect(result.output.statusCode).toBe(500); }); diff --git a/src/core/server/http/lifecycle/on_post_auth.ts b/src/core/server/http/lifecycle/on_post_auth.ts new file mode 100644 index 0000000000000..c0843a6bc764b --- /dev/null +++ b/src/core/server/http/lifecycle/on_post_auth.ts @@ -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 Boom from 'boom'; +import { Lifecycle, Request, ResponseToolkit } from 'hapi'; +import { KibanaRequest } from '../router'; + +enum ResultType { + next = 'next', + redirected = 'redirected', + rejected = 'rejected', +} + +interface Next { + type: ResultType.next; +} + +interface Redirected { + type: ResultType.redirected; + url: string; +} + +interface Rejected { + type: ResultType.rejected; + error: Error; + statusCode?: number; +} + +type OnPostAuthResult = Next | Rejected | Redirected; + +const postAuthResult = { + next(): OnPostAuthResult { + return { type: ResultType.next }; + }, + redirected(url: string): OnPostAuthResult { + return { type: ResultType.redirected, url }; + }, + rejected(error: Error, options: { statusCode?: number } = {}): OnPostAuthResult { + return { type: ResultType.rejected, error, statusCode: options.statusCode }; + }, + isValid(candidate: any): candidate is OnPostAuthResult { + return ( + candidate && + (candidate.type === ResultType.next || + candidate.type === ResultType.rejected || + candidate.type === ResultType.redirected) + ); + }, + isNext(result: OnPostAuthResult): result is Next { + return result.type === ResultType.next; + }, + isRedirected(result: OnPostAuthResult): result is Redirected { + return result.type === ResultType.redirected; + }, + isRejected(result: OnPostAuthResult): result is Rejected { + return result.type === ResultType.rejected; + }, +}; + +/** + * @public + * A tool set defining an outcome of OnPostAuth interceptor for incoming request. + */ +export interface OnPostAuthToolkit { + /** To pass request to the next handler */ + next: () => OnPostAuthResult; + /** To interrupt request handling and redirect to a configured url */ + redirected: (url: string) => OnPostAuthResult; + /** Fail the request with specified error. */ + rejected: (error: Error, options?: { statusCode?: number }) => OnPostAuthResult; +} + +/** @public */ +export type OnPostAuthHandler = ( + request: KibanaRequest, + t: OnPostAuthToolkit +) => OnPostAuthResult | Promise; + +const toolkit: OnPostAuthToolkit = { + next: postAuthResult.next, + redirected: postAuthResult.redirected, + rejected: postAuthResult.rejected, +}; +/** + * @public + * Adopt custom request interceptor to Hapi lifecycle system. + * @param fn - an extension point allowing to perform custom logic for + * incoming HTTP requests. + */ +export function adoptToHapiOnPostAuthFormat(fn: OnPostAuthHandler) { + return async function interceptRequest( + request: Request, + h: ResponseToolkit + ): Promise { + try { + const result = await fn(KibanaRequest.from(request, undefined), toolkit); + if (!postAuthResult.isValid(result)) { + throw new Error( + `Unexpected result from OnPostAuth. Expected OnPostAuthResult, but given: ${result}.` + ); + } + if (postAuthResult.isNext(result)) { + return h.continue; + } + if (postAuthResult.isRedirected(result)) { + return h.redirect(result.url).takeover(); + } + const { error, statusCode } = result; + return Boom.boomify(error, { statusCode }); + } catch (error) { + return Boom.internal(error.message, { statusCode: 500 }); + } + }; +} diff --git a/src/core/server/http/lifecycle/on_pre_auth.test.ts b/src/core/server/http/lifecycle/on_pre_auth.test.ts new file mode 100644 index 0000000000000..83900ba5ad89d --- /dev/null +++ b/src/core/server/http/lifecycle/on_pre_auth.test.ts @@ -0,0 +1,108 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Boom from 'boom'; +import { adoptToHapiOnPreAuthFormat } from './on_pre_auth'; + +const requestMock = {} as any; +const createResponseToolkit = (customization = {}): any => ({ ...customization }); + +describe('adoptToHapiOnPreAuthFormat', () => { + it('Should allow passing request to the next handler', async () => { + const continueSymbol = {}; + const onPreAuth = adoptToHapiOnPreAuthFormat((req, t) => t.next()); + const result = await onPreAuth( + requestMock, + createResponseToolkit({ + ['continue']: continueSymbol, + }) + ); + + expect(result).toBe(continueSymbol); + }); + + it('Should support redirecting to specified url', async () => { + const redirectUrl = '/docs'; + const onPreAuth = adoptToHapiOnPreAuthFormat((req, t) => t.redirected(redirectUrl)); + const takeoverSymbol = {}; + const redirectMock = jest.fn(() => ({ takeover: () => takeoverSymbol })); + const result = await onPreAuth( + requestMock, + createResponseToolkit({ + redirect: redirectMock, + }) + ); + + expect(redirectMock).toBeCalledWith(redirectUrl); + expect(result).toBe(takeoverSymbol); + }); + + it('Should support request forwarding to specified url', async () => { + const redirectUrl = '/docs'; + const onPreAuth = adoptToHapiOnPreAuthFormat((req, t) => + t.redirected(redirectUrl, { forward: true }) + ); + const continueSymbol = {}; + const setUrl = jest.fn(); + const reqMock = { setUrl, raw: { req: {} } } as any; + const result = await onPreAuth( + reqMock as any, + createResponseToolkit({ + ['continue']: continueSymbol, + }) + ); + + expect(setUrl).toBeCalledWith(redirectUrl); + expect(reqMock.raw.req.url).toBe(redirectUrl); + expect(result).toBe(continueSymbol); + }); + + it('Should support specifying statusCode and message for Boom error', async () => { + const onPreAuth = adoptToHapiOnPreAuthFormat((req, t) => { + return t.rejected(new Error('unexpected result'), { statusCode: 501 }); + }); + const result = (await onPreAuth(requestMock, createResponseToolkit())) as Boom; + + expect(result).toBeInstanceOf(Boom); + expect(result.message).toBe('unexpected result'); + expect(result.output.statusCode).toBe(501); + }); + + it('Should return Boom.internal error if interceptor throws', async () => { + const onPreAuth = adoptToHapiOnPreAuthFormat((req, t) => { + throw new Error('unknown error'); + }); + const result = (await onPreAuth(requestMock, createResponseToolkit())) as Boom; + + expect(result).toBeInstanceOf(Boom); + expect(result.message).toBe('unknown error'); + expect(result.output.statusCode).toBe(500); + }); + + it('Should return Boom.internal error if interceptor returns unexpected result', async () => { + const onPreAuth = adoptToHapiOnPreAuthFormat((req, toolkit) => undefined as any); + const result = (await onPreAuth(requestMock, createResponseToolkit())) as Boom; + + expect(result).toBeInstanceOf(Boom); + expect(result.message).toMatchInlineSnapshot( + `"Unexpected result from OnPreAuth. Expected OnPreAuthResult, but given: undefined."` + ); + expect(result.output.statusCode).toBe(500); + }); +}); diff --git a/src/core/server/http/lifecycle/on_pre_auth.ts b/src/core/server/http/lifecycle/on_pre_auth.ts new file mode 100644 index 0000000000000..317dd2f621f74 --- /dev/null +++ b/src/core/server/http/lifecycle/on_pre_auth.ts @@ -0,0 +1,145 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Boom from 'boom'; +import { Lifecycle, Request, ResponseToolkit } from 'hapi'; +import { KibanaRequest } from '../router'; + +enum ResultType { + next = 'next', + redirected = 'redirected', + rejected = 'rejected', +} + +interface Next { + type: ResultType.next; +} + +interface Redirected { + type: ResultType.redirected; + url: string; + forward?: boolean; +} + +interface Rejected { + type: ResultType.rejected; + error: Error; + statusCode?: number; +} + +type OnPreAuthResult = Next | Rejected | Redirected; + +const preAuthResult = { + next(): OnPreAuthResult { + return { type: ResultType.next }; + }, + redirected(url: string, options: { forward?: boolean } = {}): OnPreAuthResult { + return { type: ResultType.redirected, url, forward: options.forward }; + }, + rejected(error: Error, options: { statusCode?: number } = {}): OnPreAuthResult { + return { type: ResultType.rejected, error, statusCode: options.statusCode }; + }, + isValid(candidate: any): candidate is OnPreAuthResult { + return ( + candidate && + (candidate.type === ResultType.next || + candidate.type === ResultType.rejected || + candidate.type === ResultType.redirected) + ); + }, + isNext(result: OnPreAuthResult): result is Next { + return result.type === ResultType.next; + }, + isRedirected(result: OnPreAuthResult): result is Redirected { + return result.type === ResultType.redirected; + }, + isRejected(result: OnPreAuthResult): result is Rejected { + return result.type === ResultType.rejected; + }, +}; + +/** + * @public + * A tool set defining an outcome of OnPreAuth interceptor for incoming request. + */ +export interface OnPreAuthToolkit { + /** To pass request to the next handler */ + next: () => OnPreAuthResult; + /** + * To interrupt request handling and redirect to a configured url. + * If "options.forwarded" = true, request will be forwarded to another url right on the server. + * */ + redirected: (url: string, options?: { forward: boolean }) => OnPreAuthResult; + /** Fail the request with specified error. */ + rejected: (error: Error, options?: { statusCode?: number }) => OnPreAuthResult; +} + +const toolkit: OnPreAuthToolkit = { + next: preAuthResult.next, + redirected: preAuthResult.redirected, + rejected: preAuthResult.rejected, +}; + +/** @public */ +export type OnPreAuthHandler = ( + request: KibanaRequest, + t: OnPreAuthToolkit +) => OnPreAuthResult | Promise; + +/** + * @public + * Adopt custom request interceptor to Hapi lifecycle system. + * @param fn - an extension point allowing to perform custom logic for + * incoming HTTP requests. + */ +export function adoptToHapiOnPreAuthFormat(fn: OnPreAuthHandler) { + return async function interceptPreAuthRequest( + request: Request, + h: ResponseToolkit + ): Promise { + try { + const result = await fn(KibanaRequest.from(request, undefined), toolkit); + + if (!preAuthResult.isValid(result)) { + throw new Error( + `Unexpected result from OnPreAuth. Expected OnPreAuthResult, but given: ${result}.` + ); + } + if (preAuthResult.isNext(result)) { + return h.continue; + } + + if (preAuthResult.isRedirected(result)) { + const { url, forward } = result; + if (forward) { + request.setUrl(url); + // We should update raw request as well since it can be proxied to the old platform + request.raw.req.url = url; + return h.continue; + } + return h.redirect(url).takeover(); + } + + const { error, statusCode } = result; + return Boom.boomify(error, { statusCode }); + } catch (error) { + return Boom.internal(error.message, { statusCode: 500 }); + } + }; +} diff --git a/src/core/server/http/lifecycle/on_request.ts b/src/core/server/http/lifecycle/on_request.ts deleted file mode 100644 index 168b4f513400f..0000000000000 --- a/src/core/server/http/lifecycle/on_request.ts +++ /dev/null @@ -1,120 +0,0 @@ -/* - * 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 { Url } from 'url'; -import Boom from 'boom'; -import { Lifecycle, Request, ResponseToolkit } from 'hapi'; -import { KibanaRequest } from '../router'; - -enum ResultType { - next = 'next', - redirected = 'redirected', - rejected = 'rejected', -} - -/** @internal */ -class OnRequestResult { - public static next() { - return new OnRequestResult(ResultType.next); - } - public static redirected(url: string) { - return new OnRequestResult(ResultType.redirected, url); - } - public static rejected(error: Error, options: { statusCode?: number } = {}) { - return new OnRequestResult(ResultType.rejected, { error, statusCode: options.statusCode }); - } - public static isValidResult(candidate: any) { - return candidate instanceof OnRequestResult; - } - constructor(private readonly type: ResultType, public readonly payload?: any) {} - public isNext() { - return this.type === ResultType.next; - } - public isRedirected() { - return this.type === ResultType.redirected; - } - public isRejected() { - return this.type === ResultType.rejected; - } -} - -/** - * @public - * A tool set defining an outcome of OnRequest interceptor for incoming request. - */ -export interface OnRequestToolkit { - /** To pass request to the next handler */ - next: () => OnRequestResult; - /** To interrupt request handling and redirect to a configured url */ - redirected: (url: string) => OnRequestResult; - /** Fail the request with specified error. */ - rejected: (error: Error, options?: { statusCode?: number }) => OnRequestResult; - /** Change url for an incoming request. */ - setUrl: (newUrl: string | Url) => void; -} - -/** @public */ -export type OnRequestHandler = ( - req: KibanaRequest, - t: OnRequestToolkit -) => OnRequestResult | Promise; - -/** - * @public - * Adopt custom request interceptor to Hapi lifecycle system. - * @param fn - an extension point allowing to perform custom logic for - * incoming HTTP requests. - */ -export function adoptToHapiOnRequestFormat(fn: OnRequestHandler) { - return async function interceptRequest( - request: Request, - h: ResponseToolkit - ): Promise { - try { - const result = await fn(KibanaRequest.from(request, undefined), { - next: OnRequestResult.next, - redirected: OnRequestResult.redirected, - rejected: OnRequestResult.rejected, - setUrl: (newUrl: string | Url) => { - request.setUrl(newUrl); - // We should update raw request as well since it can be proxied to the old platform - request.raw.req.url = typeof newUrl === 'string' ? newUrl : newUrl.href; - }, - }); - if (OnRequestResult.isValidResult(result)) { - if (result.isNext()) { - return h.continue; - } - if (result.isRedirected()) { - return h.redirect(result.payload).takeover(); - } - if (result.isRejected()) { - const { error, statusCode } = result.payload; - return Boom.boomify(error, { statusCode }); - } - } - - throw new Error( - `Unexpected result from OnRequest. Expected OnRequestResult, but given: ${result}.` - ); - } catch (error) { - return Boom.internal(error.message, { statusCode: 500 }); - } - }; -} diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts index 03b62f4948306..4b7c3193e2ea7 100644 --- a/src/core/server/http/router/request.ts +++ b/src/core/server/http/router/request.ts @@ -17,6 +17,7 @@ * under the License. */ +import { Url } from 'url'; import { ObjectType, TypeOf } from '@kbn/config-schema'; import { Request } from 'hapi'; @@ -70,6 +71,7 @@ export class KibanaRequest { public readonly headers: Headers; public readonly path: string; + public readonly url: Url; constructor( private readonly request: Request, @@ -79,6 +81,7 @@ export class KibanaRequest { ) { this.headers = request.headers; this.path = request.path; + this.url = request.url; } public getFilteredHeaders(headersToKeep: string[]) { diff --git a/src/core/server/http/router/route.ts b/src/core/server/http/router/route.ts index 64ed67e8f940b..bbc45258b1e1a 100644 --- a/src/core/server/http/router/route.ts +++ b/src/core/server/http/router/route.ts @@ -35,6 +35,15 @@ export interface RouteConfig

| false; + + /** + * A flag shows that authentication for a route: + * enabled when true + * disabled when false + * + * Enabled by default. + */ + authRequired?: boolean; } export type RouteValidateFactory< diff --git a/src/core/server/http/router/router.ts b/src/core/server/http/router/router.ts index a640a413fd81b..2a1a169e0931d 100644 --- a/src/core/server/http/router/router.ts +++ b/src/core/server/http/router/router.ts @@ -27,6 +27,7 @@ import { RouteConfig, RouteMethod, RouteSchemas } from './route'; export interface RouterRoute { method: 'GET' | 'POST' | 'PUT' | 'DELETE'; path: string; + authRequired: boolean; handler: (req: Request, responseToolkit: ResponseToolkit) => Promise; } @@ -43,12 +44,14 @@ export class Router { route: RouteConfig, handler: RequestHandler ) { + const { path, authRequired = true } = route; const routeSchemas = this.routeSchemasFromRouteConfig(route, 'GET'); this.routes.push({ handler: async (req, responseToolkit) => await this.handle(routeSchemas, req, responseToolkit, handler), method: 'GET', - path: route.path, + path, + authRequired, }); } @@ -59,12 +62,14 @@ export class Router { route: RouteConfig, handler: RequestHandler ) { + const { path, authRequired = true } = route; const routeSchemas = this.routeSchemasFromRouteConfig(route, 'POST'); this.routes.push({ handler: async (req, responseToolkit) => await this.handle(routeSchemas, req, responseToolkit, handler), method: 'POST', - path: route.path, + path, + authRequired, }); } @@ -75,12 +80,14 @@ export class Router { route: RouteConfig, handler: RequestHandler ) { + const { path, authRequired = true } = route; const routeSchemas = this.routeSchemasFromRouteConfig(route, 'POST'); this.routes.push({ handler: async (req, responseToolkit) => await this.handle(routeSchemas, req, responseToolkit, handler), method: 'PUT', - path: route.path, + path, + authRequired, }); } @@ -91,12 +98,14 @@ export class Router { route: RouteConfig, handler: RequestHandler ) { + const { path, authRequired = true } = route; const routeSchemas = this.routeSchemasFromRouteConfig(route, 'DELETE'); this.routes.push({ handler: async (req, responseToolkit) => await this.handle(routeSchemas, req, responseToolkit, handler), method: 'DELETE', - path: route.path, + path, + authRequired, }); } diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 02452f7b0756f..e8e3563b7ca3a 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -54,8 +54,10 @@ export { AuthenticationHandler, AuthToolkit, KibanaRequest, - OnRequestHandler, - OnRequestToolkit, + OnPreAuthHandler, + OnPreAuthToolkit, + OnPostAuthHandler, + OnPostAuthToolkit, Router, } from './http'; export { Logger, LoggerFactory, LogMeta, LogRecord, LogLevel } from './logging'; @@ -79,8 +81,9 @@ export interface CoreSetup { dataClient$: Observable; }; http: { + registerOnPreAuth: HttpServiceSetup['registerOnPreAuth']; registerAuth: HttpServiceSetup['registerAuth']; - registerOnRequest: HttpServiceSetup['registerOnRequest']; + registerOnPostAuth: HttpServiceSetup['registerOnPostAuth']; getBasePathFor: HttpServiceSetup['getBasePathFor']; setBasePathFor: HttpServiceSetup['setBasePathFor']; createNewServer: HttpServiceSetup['createNewServer']; diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index ddd65b198baa6..9dfc2df1c2d20 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -117,8 +117,9 @@ export function createPluginSetupContext( dataClient$: deps.elasticsearch.dataClient$, }, http: { + registerOnPreAuth: deps.http.registerOnPreAuth, registerAuth: deps.http.registerAuth, - registerOnRequest: deps.http.registerOnRequest, + registerOnPostAuth: deps.http.registerOnPostAuth, getBasePathFor: deps.http.getBasePathFor, setBasePathFor: deps.http.setBasePathFor, createNewServer: deps.http.createNewServer, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index b9148a2b94a57..2d2472103e5ab 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -26,11 +26,11 @@ export type APICaller = (endpoint: string, clientParams: Record // Warning: (ae-forgotten-export) The symbol "AuthResult" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export type AuthenticationHandler = (request: Request, sessionStorage: SessionStorage, t: AuthToolkit) => Promise; +export type AuthenticationHandler = (request: Readonly, sessionStorage: SessionStorage, t: AuthToolkit) => AuthResult | Promise; // @public export interface AuthToolkit { - authenticated: (credentials: any) => AuthResult; + authenticated: (state: object) => AuthResult; redirected: (url: string) => AuthResult; rejected: (error: Error, options?: { statusCode?: number; @@ -84,8 +84,9 @@ export interface CoreSetup { }; // (undocumented) http: { + registerOnPreAuth: HttpServiceSetup['registerOnPreAuth']; registerAuth: HttpServiceSetup['registerAuth']; - registerOnRequest: HttpServiceSetup['registerOnRequest']; + registerOnPostAuth: HttpServiceSetup['registerOnPostAuth']; getBasePathFor: HttpServiceSetup['getBasePathFor']; setBasePathFor: HttpServiceSetup['setBasePathFor']; createNewServer: HttpServiceSetup['createNewServer']; @@ -183,6 +184,8 @@ export class KibanaRequest { readonly query: Query; // (undocumented) unstable_getIncomingMessage(): import("http").IncomingMessage; + // (undocumented) + readonly url: Url; } // @public @@ -254,19 +257,34 @@ export interface LogRecord { timestamp: Date; } -// Warning: (ae-forgotten-export) The symbol "OnRequestResult" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "OnPostAuthResult" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export type OnPostAuthHandler = (request: KibanaRequest, t: OnPostAuthToolkit) => OnPostAuthResult | Promise; + +// @public +export interface OnPostAuthToolkit { + next: () => OnPostAuthResult; + redirected: (url: string) => OnPostAuthResult; + rejected: (error: Error, options?: { + statusCode?: number; + }) => OnPostAuthResult; +} + +// Warning: (ae-forgotten-export) The symbol "OnPreAuthResult" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export type OnRequestHandler = (req: KibanaRequest, t: OnRequestToolkit) => OnRequestResult | Promise; +export type OnPreAuthHandler = (request: KibanaRequest, t: OnPreAuthToolkit) => OnPreAuthResult | Promise; // @public -export interface OnRequestToolkit { - next: () => OnRequestResult; - redirected: (url: string) => OnRequestResult; +export interface OnPreAuthToolkit { + next: () => OnPreAuthResult; + redirected: (url: string, options?: { + forward: boolean; + }) => OnPreAuthResult; rejected: (error: Error, options?: { statusCode?: number; - }) => OnRequestResult; - setUrl: (newUrl: string | Url) => void; + }) => OnPreAuthResult; } // @public