diff --git a/docs/development/core/server/kibana-plugin-server.authenticationhandler.md b/docs/development/core/server/kibana-plugin-server.authenticationhandler.md index cb09dc0cd6051d..482ef44a6e4612 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 d0f1e07c474846..eabee65cb14519 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 d7e2e39b44d4cb..cc9f14c57fe4c1 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 8c547ca2a42a9e..8cb25af29e4baa 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']; }; diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.md b/docs/development/core/server/kibana-plugin-server.coresetup.md index fe6d3ee71edc8f..7b46817842defc 100644 --- a/docs/development/core/server/kibana-plugin-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-server.coresetup.md @@ -17,5 +17,5 @@ export interface CoreSetup | Property | Type | Description | | --- | --- | --- | | [elasticsearch](./kibana-plugin-server.coresetup.elasticsearch.md) | {`

` adminClient$: Observable<ClusterClient>;`

` dataClient$: Observable<ClusterClient>;`

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

` registerAuth: HttpServiceSetup['registerAuth'];`

` registerOnRequest: HttpServiceSetup['registerOnRequest'];`

` getBasePathFor: HttpServiceSetup['getBasePathFor'];`

` setBasePathFor: HttpServiceSetup['setBasePathFor'];`

` } | | +| [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 f7f7707b5657e2..f93e4c073eb210 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 00000000000000..d552ba55a2b0e6 --- /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 c078a5fdc983f3..843f2a65e95627 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -34,7 +34,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | [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. | +| [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) | | @@ -49,7 +50,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ElasticsearchClientConfig](./kibana-plugin-server.elasticsearchclientconfig.md) | | | [Headers](./kibana-plugin-server.headers.md) | | | [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) | | -| [OnRequestHandler](./kibana-plugin-server.onrequesthandler.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 00000000000000..83de25e3f3d6dd --- /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 00000000000000..276643c4f9d1a9 --- /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 00000000000000..26b4562974e412 --- /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 00000000000000..23cef2f97e32b2 --- /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 00000000000000..a7767dac727a01 --- /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 00000000000000..606ed21dc6463b --- /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 00000000000000..066a95f9fa7c75 --- /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 976e3b1a2db875..86369f70ac1d97 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 00000000000000..65c0512b9367b2 --- /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 447d9b3fb9be5a..b267a03b6f9348 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 5d90e399db6762..00000000000000 --- 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 e6a79a13dd4363..00000000000000 --- 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 311398845bd59a..00000000000000 --- 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 0f20cbdb18d968..00000000000000 --- 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 00000000000000..eafe755b79eea0 --- /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 21e11934719725..0f1a85a336b048 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -505,7 +505,7 @@ describe('with `basepath: /bar` and `rewriteBasePath: true`', () => { res.ok({ key: 'value:/foo' }) ); - const { registerRouter, server: innerServer } = await server.setup(config); + const { registerRouter, server: innerServer } = await server.setup(configWithBasePath); registerRouter(router); await server.start(configWithBasePath); @@ -601,9 +601,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(); @@ -621,11 +621,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(); }); @@ -653,11 +653,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(); }); @@ -708,3 +708,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(config); + 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(config); + 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(config); + 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(config); + 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(config); + 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(config); + + 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(config); + 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(config); + + 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 6dbae8a14d6015..db2233b59a2e87 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 { @@ -62,7 +80,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; @@ -103,16 +125,23 @@ export class HttpServer { const serverOptions = getServerOptions(config); this.server = createServer(serverOptions); + 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. @@ -126,14 +155,16 @@ export class HttpServer { } this.log.debug('starting http server'); - this.setupBasePathRewrite(this.server, config); - 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, + }, }); } } @@ -157,13 +188,13 @@ export class HttpServer { this.server = undefined; } - private setupBasePathRewrite(server: Server, config: HttpConfig) { + private setupBasePathRewrite(config: HttpConfig) { if (config.basePath === undefined || !config.rewriteBasePath) { return; } const basePath = config.basePath; - server.ext('onRequest', (request, responseToolkit) => { + 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, '') || '/'; @@ -173,18 +204,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 }); }); } @@ -195,12 +218,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( @@ -223,7 +254,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 289eae0990531f..6d49ac3010ef02 100644 --- a/src/core/server/http/http_service.mock.ts +++ b/src/core/server/http/http_service.mock.ts @@ -23,13 +23,18 @@ import { HttpService } from './http_service'; 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, + auth: { + get: jest.fn(), + isAuthenticated: jest.fn(), + }, }; return setupContract; }; diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index 465c5cb6a859b0..ece61579314b1e 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 9913f9914a0a8f..93fe20a80e1209 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 8205d21c5ff59e..ffe77e0120fe46 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 bc4410c7732888..7644b8a35ef772 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 00000000000000..c0843a6bc764b6 --- /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 00000000000000..83900ba5ad89d2 --- /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 00000000000000..317dd2f621f74e --- /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 168b4f513400fe..00000000000000 --- 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 03b62f4948306d..4b7c3193e2ea74 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 64ed67e8f940b1..bbc45258b1e1af 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 a640a413fd81b0..2a1a169e0931dd 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 d3223674eb00c2..e144c0f2568f8b 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']; }; diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index e533ac0cfe9acd..b2a88bbd32760c 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, }, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 821d85a3812f3e..745fbdb7d9d99f 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -25,11 +25,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; @@ -83,8 +83,9 @@ export interface CoreSetup { }; // (undocumented) http: { + registerOnPreAuth: HttpServiceSetup['registerOnPreAuth']; registerAuth: HttpServiceSetup['registerAuth']; - registerOnRequest: HttpServiceSetup['registerOnRequest']; + registerOnPostAuth: HttpServiceSetup['registerOnPostAuth']; getBasePathFor: HttpServiceSetup['getBasePathFor']; setBasePathFor: HttpServiceSetup['setBasePathFor']; }; @@ -176,6 +177,8 @@ export class KibanaRequest { readonly query: Query; // (undocumented) unstable_getIncomingMessage(): import("http").IncomingMessage; + // (undocumented) + readonly url: Url; } // @public @@ -247,19 +250,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 diff --git a/yarn.lock b/yarn.lock index 5ef2e44bc39b09..395f28efc2e17c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8471,9 +8471,9 @@ core-js@^2.5.3, core-js@^2.5.7: integrity sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw== core-js@^2.6.5: - version "2.6.8" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.8.tgz#dc3a1e633a04267944e0cb850d3880f340248139" - integrity sha512-RWlREFU74TEkdXzyl1bka66O3kYp8jeTXrvJZDzVVMH8AiHUSOFpL1yfhQJ+wHocAm1m+4971W1PPzfLuCv1vg== + version "2.6.9" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2" + integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A== core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2"