From d827bfbc02bb0ca182970046b5a4cbc0b7eda88e Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Wed, 3 Apr 2019 18:08:35 +0200 Subject: [PATCH 01/35] Add Auth session --- src/core/server/http/auth_session.ts | 36 ++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/core/server/http/auth_session.ts diff --git a/src/core/server/http/auth_session.ts b/src/core/server/http/auth_session.ts new file mode 100644 index 0000000000000..fac80d6ec7a93 --- /dev/null +++ b/src/core/server/http/auth_session.ts @@ -0,0 +1,36 @@ +/* + * 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'; + +export type ScopedAuthSession = ReturnType; +export class AuthSession { + constructor(private readonly sessionGetter: (request: Request) => Promise) {} + public asScoped(request: Request) { + // NOTE: probably need bind here. request.cookieAuth.set.bind(request.cookieAuth) + return { + // Retrieves session value from the session storage. + get: () => this.sessionGetter(request), + // Puts current session value into the session storage. + set: request.cookieAuth.set, + // Clears current session. + clear: request.cookieAuth.clear, + }; + } +} From 8b15ce164759e0bdabe7de9a1d8990244f145606 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Wed, 3 Apr 2019 18:09:52 +0200 Subject: [PATCH 02/35] add lifecycles --- src/core/server/http/lifecycle/auth.ts | 92 ++++++++++++++++++++ src/core/server/http/lifecycle/on_request.ts | 91 +++++++++++++++++++ 2 files changed, 183 insertions(+) create mode 100644 src/core/server/http/lifecycle/auth.ts create mode 100644 src/core/server/http/lifecycle/on_request.ts diff --git a/src/core/server/http/lifecycle/auth.ts b/src/core/server/http/lifecycle/auth.ts new file mode 100644 index 0000000000000..76dcc2bf31ca4 --- /dev/null +++ b/src/core/server/http/lifecycle/auth.ts @@ -0,0 +1,92 @@ +/* + * 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 { AuthSession, ScopedAuthSession } from '../auth_session'; + +enum ResultType { + authenticated = 'authenticated', + redirected = 'redirected', + rejected = 'rejected', +} + +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) { + return new AuthResult(ResultType.rejected, error); + } + 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; + } +} + +const toolkit = { + authenticated: AuthResult.authenticated, + redirected: AuthResult.redirected, + rejected: AuthResult.rejected, +}; + +export type Authenticate = ( + request: Request, + authSession: ScopedAuthSession, + t: typeof toolkit +) => AuthResult; + +export function adoptToHapiAuthFormat(fn: Authenticate, authSession: AuthSession) { + return async function interceptAuth( + req: Request, + h: ResponseToolkit + ): Promise { + try { + const result = await fn(req, authSession.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 { statusCode } = result.payload; + return Boom.boomify(result.payload, { statusCode }); + } + } + throw new Error( + `Unexpected result from Authenticate. Expected AuthResult, but given: ${result}` + ); + } catch (error) { + return new Boom(error.message, { statusCode: 500 }); + } + }; +} diff --git a/src/core/server/http/lifecycle/on_request.ts b/src/core/server/http/lifecycle/on_request.ts new file mode 100644 index 0000000000000..971e49dcb4030 --- /dev/null +++ b/src/core/server/http/lifecycle/on_request.ts @@ -0,0 +1,91 @@ +/* + * 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', +} + +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) { + return new OnRequestResult(ResultType.rejected, error); + } + 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; + } +} + +const toolkit = { + next: OnRequestResult.next, + redirected: OnRequestResult.redirected, + rejected: OnRequestResult.rejected, +}; + +export type OnRequest = ( + req: KibanaRequest, + t: typeof toolkit +) => OnRequestResult; +export function adoptToHapiOnRequestFormat(fn: OnRequest) { + return async function interceptRequest( + req: Request, + h: ResponseToolkit + ): Promise { + try { + const result = await fn(KibanaRequest.from(req, undefined), toolkit); + if (OnRequestResult.isValidResult(result)) { + if (result.isNext()) { + return h.continue; + } + if (result.isRedirected()) { + return h.redirect(result.payload).takeover(); + } + if (result.isRejected()) { + const { statusCode } = result.payload; + return Boom.boomify(result.payload, { statusCode }); + } + } + + throw new Error( + `Unexpected result from OnRequest. Expected OnRequestResult, but given: ${result}` + ); + } catch (error) { + return new Boom(error.message, { statusCode: 500 }); + } + }; +} From 7d908de70f012c4543351483d738b38b0fb0b54c Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Wed, 3 Apr 2019 18:10:46 +0200 Subject: [PATCH 03/35] add types for hapi-auth-cookie --- package.json | 2 ++ yarn.lock | 9 ++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 5ae2dba7025be..763dffd04f9db 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ }, "resolutions": { "**/@types/node": "10.12.27", + "**/@types/hapi": "^17.0.18", "**/typescript": "^3.3.3333" }, "workspaces": { @@ -283,6 +284,7 @@ "@types/globby": "^8.0.0", "@types/graphql": "^0.13.1", "@types/hapi": "^17.0.18", + "@types/hapi-auth-cookie": "9.1.0", "@types/has-ansi": "^3.0.0", "@types/hoek": "^4.1.3", "@types/humps": "^1.1.2", diff --git a/yarn.lock b/yarn.lock index f0827b5edc8c9..d501d19644f97 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2578,7 +2578,14 @@ resolved "https://registry.yarnpkg.com/@types/handlebars/-/handlebars-4.0.39.tgz#961fb54db68030890942e6aeffe9f93a957807bd" integrity sha512-vjaS7Q0dVqFp85QhyPSZqDKnTTCemcSHNHFvDdalO1s0Ifz5KuE64jQD5xoUkfdWwF4WpqdJEl7LsWH8rzhKJA== -"@types/hapi@^17.0.18": +"@types/hapi-auth-cookie@9.1.0": + version "9.1.0" + resolved "https://registry.yarnpkg.com/@types/hapi-auth-cookie/-/hapi-auth-cookie-9.1.0.tgz#cbcd2236b7d429bd0632a8cc45cfd355fdd7e7a2" + integrity sha512-qsP08L+fNaE2K5dsDVKvHp0AmSBs8m9PD5eWsTdHnkJOk81iD7c0J4GYt/1aDJwZsyx6CgcxpbkPOCwBJmrwAg== + dependencies: + "@types/hapi" "*" + +"@types/hapi@*", "@types/hapi@^17.0.18": version "17.0.18" resolved "https://registry.yarnpkg.com/@types/hapi/-/hapi-17.0.18.tgz#f855fe18766aa2592a3a689c3e6eabe72989ff1a" integrity sha512-sRoDjz1iVOCxTqq+EepzDQI773k2PjboHpvMpp524278grosStxZ5+oooVjNLJZj1iZIbiLeeR5/ZeIRgVXsCg== From 218db726c4a6e44c7d467a59b48ea550ce37ee8b Mon Sep 17 00:00:00 2001 From: restrry Date: Fri, 5 Apr 2019 13:19:49 +0200 Subject: [PATCH 04/35] expose interceptors from http service --- src/core/server/http/http_server.ts | 67 ++++++++++++++++++- src/core/server/http/http_service.mock.ts | 2 + src/core/server/http/lifecycle/auth.ts | 10 +-- .../{auth_session.ts => session_storage.ts} | 8 +-- src/core/server/legacy/legacy_service.ts | 2 +- src/core/server/plugins/plugin_context.ts | 11 +++ src/core/server/plugins/plugins_service.ts | 2 + src/core/server/server.ts | 1 + src/legacy/server/kbn_server.d.ts | 3 +- src/legacy/server/kbn_server.js | 4 +- 10 files changed, 95 insertions(+), 15 deletions(-) rename src/core/server/http/{auth_session.ts => session_storage.ts} (85%) diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 7b7e415415b30..6b7b27bf57a2a 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -18,16 +18,32 @@ */ import { Server, ServerOptions } from 'hapi'; +import hapiAuthCookie from 'hapi-auth-cookie'; import { modifyUrl } from '../../utils'; import { Logger } from '../logging'; import { HttpConfig } from './http_config'; import { createServer, getServerOptions } from './http_tools'; +import { adoptToHapiAuthFormat, Authenticate } from './lifecycle/auth'; +import { adoptToHapiOnRequestFormat, OnRequest } from './lifecycle/on_request'; import { Router } from './router'; +import { SessionStorage } from './session_storage'; + +export interface CookieOptions { + name: string; + password: string; + validate: (sessionValue: any) => boolean | Promise; + isSecure: boolean; + sessionTimeout: number; + path?: string; +} + export interface HttpServerInfo { server: Server; options: ServerOptions; + registerAuth: (fn: Authenticate, cookieOptions: CookieOptions) => void; + registerOnRequest: (fn: OnRequest) => void; } export class HttpServer { @@ -48,7 +64,7 @@ export class HttpServer { this.registeredRouters.add(router); } - public async start(config: HttpConfig) { + public async start(config: HttpConfig): Promise { this.log.debug('starting http server'); const serverOptions = getServerOptions(config); @@ -77,7 +93,12 @@ export class HttpServer { // Return server instance with the connection options so that we can properly // bridge core and the "legacy" Kibana internally. Once this bridge isn't // needed anymore we shouldn't return anything from this method. - return { server: this.server, options: serverOptions }; + return { + server: this.server, + options: serverOptions, + registerOnRequest: this.registerOnRequest, + registerAuth: this.registerAuth, + }; } public async stop() { @@ -127,4 +148,46 @@ export class HttpServer { const routePathStartIndex = routerPath.endsWith('/') && routePath.startsWith('/') ? 1 : 0; return `${routerPath}${routePath.slice(routePathStartIndex)}`; } + + private registerOnRequest = (fn: OnRequest) => { + if (this.server === undefined) { + throw new Error('Server is not created yet'); + } + + this.server.ext('onRequest', adoptToHapiOnRequestFormat(fn)); + }; + + private registerAuth = async (fn: Authenticate, cookieOptions: CookieOptions) => { + if (this.server === undefined) { + throw new Error('Server is not created yet'); + } + + await this.server.register({ plugin: hapiAuthCookie }); + + this.server.auth.strategy('security-cookie', 'cookie', { + cookie: cookieOptions.name, + password: cookieOptions.password, + validateFunc: async (req, session) => ({ valid: await cookieOptions.validate(session) }), + isSecure: cookieOptions.isSecure, + path: cookieOptions.path, + clearInvalid: true, + isHttpOnly: true, + isSameSite: false, + }); + + const sessionStorage = new SessionStorage(request => + this.server!.auth.test('security-cookie', request) + ); + + this.server.auth.scheme('login', () => ({ + authenticate: adoptToHapiAuthFormat(fn, sessionStorage), + })); + this.server.auth.strategy('session', 'login'); + + // The default means that the `session` strategy that is based on `login` schema defined above will be + // automatically assigned to all routes that don't contain an auth config. + // should be applied for all routes if they don't specify auth strategy in route declaration + // https://github.com/hapijs/hapi/blob/master/API.md#-serverauthdefaultoptions + this.server.auth.default('session'); + }; } diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts index bc9a42b26fdff..d8bab493a2ed9 100644 --- a/src/core/server/http/http_service.mock.ts +++ b/src/core/server/http/http_service.mock.ts @@ -25,6 +25,8 @@ const createSetupContractMock = () => { // we can mock some hapi server method when we need it server: {} as Server, options: {} as ServerOptions, + registerAuth: jest.fn(), + registerOnRequest: jest.fn(), }; return setupContract; }; diff --git a/src/core/server/http/lifecycle/auth.ts b/src/core/server/http/lifecycle/auth.ts index 76dcc2bf31ca4..726dad60b6db0 100644 --- a/src/core/server/http/lifecycle/auth.ts +++ b/src/core/server/http/lifecycle/auth.ts @@ -18,7 +18,7 @@ */ import Boom from 'boom'; import { Lifecycle, Request, ResponseToolkit } from 'hapi'; -import { AuthSession, ScopedAuthSession } from '../auth_session'; +import { ScopedSessionStorage, SessionStorage } from '../session_storage'; enum ResultType { authenticated = 'authenticated', @@ -59,17 +59,17 @@ const toolkit = { export type Authenticate = ( request: Request, - authSession: ScopedAuthSession, + sessionStorage: ScopedSessionStorage, t: typeof toolkit -) => AuthResult; +) => Promise; -export function adoptToHapiAuthFormat(fn: Authenticate, authSession: AuthSession) { +export function adoptToHapiAuthFormat(fn: Authenticate, sessionStorage: SessionStorage) { return async function interceptAuth( req: Request, h: ResponseToolkit ): Promise { try { - const result = await fn(req, authSession.asScoped(req), toolkit); + const result = await fn(req, sessionStorage.asScoped(req), toolkit); if (AuthResult.isValidResult(result)) { if (result.isAuthenticated()) { return h.authenticated({ credentials: result.payload }); diff --git a/src/core/server/http/auth_session.ts b/src/core/server/http/session_storage.ts similarity index 85% rename from src/core/server/http/auth_session.ts rename to src/core/server/http/session_storage.ts index fac80d6ec7a93..d2c888cff2f87 100644 --- a/src/core/server/http/auth_session.ts +++ b/src/core/server/http/session_storage.ts @@ -19,8 +19,8 @@ import { Request } from 'hapi'; -export type ScopedAuthSession = ReturnType; -export class AuthSession { +export type ScopedSessionStorage = ReturnType; +export class SessionStorage { constructor(private readonly sessionGetter: (request: Request) => Promise) {} public asScoped(request: Request) { // NOTE: probably need bind here. request.cookieAuth.set.bind(request.cookieAuth) @@ -28,9 +28,9 @@ export class AuthSession { // Retrieves session value from the session storage. get: () => this.sessionGetter(request), // Puts current session value into the session storage. - set: request.cookieAuth.set, + set: (session: object) => request.cookieAuth.set(session), // Clears current session. - clear: request.cookieAuth.clear, + clear: () => request.cookieAuth.clear(), }; } } diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index 8d85516f29aa7..ad092c79fa8c2 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -89,7 +89,6 @@ export class LegacyService implements CoreService { await this.createClusterManager(config); return; } - return await this.createKbnServer(config, deps); }) ) @@ -147,6 +146,7 @@ export class LegacyService implements CoreService { } : { autoListen: false }, handledConfigPaths: await this.coreContext.configService.getUsedPaths(), + http, elasticsearch, plugins, }); diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index b8ce3457ee29a..0a51482085626 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -22,6 +22,7 @@ import { Observable } from 'rxjs'; import { ConfigWithSchema, EnvironmentMode } from '../config'; import { CoreContext } from '../core_context'; import { ClusterClient } from '../elasticsearch'; +import { HttpServiceSetup } from '../http'; import { LoggerFactory } from '../logging'; import { Plugin, PluginManifest } from './plugin'; import { PluginsServiceSetupDeps } from './plugins_service'; @@ -50,6 +51,10 @@ export interface PluginSetupContext { adminClient$: Observable; dataClient$: Observable; }; + http?: { + registerAuth: HttpServiceSetup['registerAuth']; + registerOnRequest: HttpServiceSetup['registerOnRequest']; + }; } /** @@ -129,5 +134,11 @@ export function createPluginSetupContext( adminClient$: deps.elasticsearch.adminClient$, dataClient$: deps.elasticsearch.dataClient$, }, + http: deps.http + ? { + registerAuth: deps.http.registerAuth, + registerOnRequest: deps.http.registerOnRequest, + } + : undefined, }; } diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index 9b31ee77333c8..9335b97a8b801 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -22,6 +22,7 @@ import { filter, first, mergeMap, tap, toArray } from 'rxjs/operators'; import { CoreService } from '../../types'; import { CoreContext } from '../core_context'; import { ElasticsearchServiceSetup } from '../elasticsearch/elasticsearch_service'; +import { HttpServiceSetup } from '../http/http_service'; import { Logger } from '../logging'; import { discover, PluginDiscoveryError, PluginDiscoveryErrorType } from './discovery'; import { DiscoveredPlugin, DiscoveredPluginInternal, Plugin, PluginName } from './plugin'; @@ -40,6 +41,7 @@ export interface PluginsServiceSetup { /** @internal */ export interface PluginsServiceSetupDeps { elasticsearch: ElasticsearchServiceSetup; + http?: HttpServiceSetup; } /** @internal */ diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 26be718bf2d65..08ab584f7d47f 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -70,6 +70,7 @@ export class Server { const pluginsSetup = await this.plugins.setup({ elasticsearch: elasticsearchServiceSetup, + http: httpSetup, }); await this.legacy.setup({ diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index 3bb885dbee756..2e966cf3840b9 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -19,8 +19,8 @@ import { Server } from 'hapi'; +import { ElasticsearchServiceSetup, HttpServiceSetup } from '../../core/server/'; import { ConfigService } from '../../core/server/config'; -import { ElasticsearchServiceSetup } from '../../core/server/elasticsearch'; import { HttpServerInfo } from '../../core/server/http/'; import { PluginsServiceSetup } from '../../core/server/plugins/plugins_service'; import { ApmOssPlugin } from '../core_plugins/apm_oss'; @@ -65,6 +65,7 @@ export default class KbnServer { setup: { core: { elasticsearch: ElasticsearchServiceSetup; + http?: HttpServiceSetup; }; plugins: PluginsServiceSetup; }; diff --git a/src/legacy/server/kbn_server.js b/src/legacy/server/kbn_server.js index c571785dd51e9..c81e7382e1bb7 100644 --- a/src/legacy/server/kbn_server.js +++ b/src/legacy/server/kbn_server.js @@ -54,12 +54,12 @@ export default class KbnServer { this.rootDir = rootDir; this.settings = settings || {}; - const { plugins, elasticsearch, serverOptions, handledConfigPaths } = core; - + const { plugins, http, elasticsearch, serverOptions, handledConfigPaths } = core; this.newPlatform = { setup: { core: { elasticsearch, + http, }, plugins, }, From 4fbc19cd31c0d192b445aa4bbe4f38cb0aa86fc2 Mon Sep 17 00:00:00 2001 From: restrry Date: Fri, 5 Apr 2019 13:24:16 +0200 Subject: [PATCH 05/35] add integration tests --- package.json | 1 + src/core/server/http/index.ts | 2 + .../plugins/dummy_security/kibana.json | 8 ++ .../plugins/dummy_security/server/index.ts | 20 +++ .../plugins/dummy_security/server/plugin.ts | 57 +++++++++ .../integration_tests/http_service.test.ts | 119 ++++++++++++++++++ src/core/server/index.ts | 12 +- src/test_utils/kbn_server.ts | 16 ++- yarn.lock | 22 +++- 9 files changed, 249 insertions(+), 8 deletions(-) create mode 100644 src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/kibana.json create mode 100644 src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/index.ts create mode 100644 src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts create mode 100644 src/core/server/http/integration_tests/http_service.test.ts diff --git a/package.json b/package.json index 763dffd04f9db..aac1be00d70d7 100644 --- a/package.json +++ b/package.json @@ -312,6 +312,7 @@ "@types/react-virtualized": "^9.18.7", "@types/redux": "^3.6.31", "@types/redux-actions": "^2.2.1", + "@types/request": "^2.48.1", "@types/rimraf": "^2.0.2", "@types/semver": "^5.5.0", "@types/sinon": "^5.0.1", diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index 2c0dbf2488373..faa2555c0bed7 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -22,3 +22,5 @@ export { HttpService, HttpServiceSetup } from './http_service'; export { Router, KibanaRequest } from './router'; export { HttpServerInfo } from './http_server'; export { BasePathProxyServer } from './base_path_proxy_server'; +export { Authenticate } from './lifecycle/auth'; +export { OnRequest } from './lifecycle/on_request'; diff --git a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/kibana.json b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/kibana.json new file mode 100644 index 0000000000000..3f5362a5bfa75 --- /dev/null +++ b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/kibana.json @@ -0,0 +1,8 @@ + +{ + "id": "dummy-security", + "version": "0.0.1", + "kibanaVersion": "kibana", + "ui": false, + "server": true +} diff --git a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/index.ts b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/index.ts new file mode 100644 index 0000000000000..dd78ab308a8bc --- /dev/null +++ b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { DummySecurityPlugin } from './plugin'; +export const plugin = () => new DummySecurityPlugin(); diff --git a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts new file mode 100644 index 0000000000000..f6586c50363dd --- /dev/null +++ b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts @@ -0,0 +1,57 @@ +/* + * 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 { Authenticate, CoreSetup } from '../../../../../../../../core/server'; + +export class DummySecurityPlugin { + public setup(core: CoreSetup) { + const authenticate: Authenticate = async (request, sessionStorage, t) => { + if (request.path === '/auth/has_session') { + const prevSession = await sessionStorage.get(); + const userData = prevSession.value; + sessionStorage.set({ value: userData, expires: Date.now() + 1000 }); + + return t.authenticated({ credentials: userData }); + } + + if (request.headers.authorization) { + const user = { id: '42' }; + sessionStorage.set({ value: user, expires: Date.now() + 1000 }); + return t.authenticated({ credentials: user }); + } else { + return t.rejected(Boom.unauthorized()); + } + }; + + const cookieOptions = { + name: 'sid', + password: 'something_at_least_32_characters', + validate: () => true, + isSecure: false, + path: '/', + sessionTimeout: 10000000, + }; + core.http.registerAuth(authenticate, cookieOptions); + return { + dummy() { + return 'Hello from dummy plugin'; + }, + }; + } +} diff --git a/src/core/server/http/integration_tests/http_service.test.ts b/src/core/server/http/integration_tests/http_service.test.ts new file mode 100644 index 0000000000000..8d148c3f3499a --- /dev/null +++ b/src/core/server/http/integration_tests/http_service.test.ts @@ -0,0 +1,119 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import path from 'path'; +import request from 'request'; +import * as kbnTestServer from '../../../../test_utils/kbn_server'; +import { Router } from '../router'; + +const authUrl = '/auth'; +const authHasSessionUrl = '/auth/has_session'; +const dummySecurityPlugin = path.resolve(__dirname, './__fixtures__/plugins/dummy_security'); + +describe('http service', () => { + describe('setup contract', () => { + describe('#registerAuth()', () => { + let root: ReturnType; + beforeAll(async () => { + root = kbnTestServer.createRoot( + { + plugins: { paths: [dummySecurityPlugin] }, + }, + { + dev: true, + } + ); + + const router = new Router(''); + router.get({ path: authUrl, validate: false }, async (req, res) => + res.ok({ content: 'ok' }) + ); + router.get({ path: authHasSessionUrl, validate: false }, async (req, res) => + res.ok({ content: 'ok' }) + ); + // TODO fix me when registerRouter is available before HTTP server is run + (root as any).server.http.registerRouter(router); + + await root.setup(); + }, 30000); + + afterAll(async () => await root.shutdown()); + + it('Should allow to implement custom authentication logic and set the cookie', async () => { + const response = await kbnTestServer.request + .get(root, authUrl) + .expect(200, { content: 'ok' }); + + expect(response.header['set-cookie']).toBeDefined(); + const cookies = response.header['set-cookie']; + expect(cookies).toHaveLength(1); + + const sessionCookie = request.cookie(cookies[0]); + + expect(sessionCookie).toBeDefined(); + expect(sessionCookie!.key).toBe('sid'); + expect(sessionCookie!.value).toBeDefined(); + expect(sessionCookie!.path).toBe('/'); + expect(sessionCookie!.httpOnly).toBe(true); + }); + + it('Should allow to read already set cookie', async () => { + const response = await kbnTestServer.request + .get(root, authUrl) + .expect(200, { content: 'ok' }); + + const cookies = response.header['set-cookie']; + const sessionCookie = request.cookie(cookies[0]); + + const response2 = await kbnTestServer.request + .get(root, authHasSessionUrl) + .set('Cookie', `${sessionCookie!.key}=${sessionCookie!.value}`) + .expect(200, { content: 'ok' }); + + expect(response2.header['set-cookie']).toBeDefined(); + + const cookies2 = response2.header['set-cookie']; + expect(cookies).not.toBe(cookies2); + }); + + it('Should allow to reject a request from an unauthenticated user', async () => { + await kbnTestServer.request + .get(root, authUrl) + .unset('Authorization') + .expect(401); + }); + + it(`Shouldn't affect legacy server routes`, async () => { + const legacyUrl = `${authUrl}/legacy`; + const kbnServer = kbnTestServer.getKbnServer(root); + kbnServer.server.route({ + method: 'GET', + path: legacyUrl, + handler: () => 'ok from legacy server', + }); + + const response = await kbnTestServer.request + .get(root, legacyUrl) + .expect(200, 'ok from legacy server'); + + expect(response.header['set-cookie']).toBe(undefined); + }); + }); + }); +}); diff --git a/src/core/server/index.ts b/src/core/server/index.ts index f0d665875d112..62809d7f305dc 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -17,7 +17,7 @@ * under the License. */ import { ElasticsearchServiceSetup } from './elasticsearch'; -import { HttpServiceSetup } from './http'; +import { Authenticate, HttpServiceSetup, KibanaRequest, OnRequest, Router } from './http'; import { PluginsServiceSetup } from './plugins'; export { bootstrap } from './bootstrap'; @@ -35,3 +35,13 @@ export interface CoreSetup { elasticsearch: ElasticsearchServiceSetup; plugins: PluginsServiceSetup; } + +export { + Router, + Authenticate, + OnRequest, + KibanaRequest, + ElasticsearchServiceSetup, + HttpServiceSetup, + PluginsServiceSetup, +}; diff --git a/src/test_utils/kbn_server.ts b/src/test_utils/kbn_server.ts index 5326b11852354..330c058ed39ae 100644 --- a/src/test_utils/kbn_server.ts +++ b/src/test_utils/kbn_server.ts @@ -32,7 +32,7 @@ import { defaultsDeep, get } from 'lodash'; import { resolve } from 'path'; import { BehaviorSubject } from 'rxjs'; import supertest from 'supertest'; -import { Env } from '../core/server/config'; +import { CliArgs, Env } from '../core/server/config'; import { LegacyObjectToConfigAdapter } from '../core/server/legacy'; import { Root } from '../core/server/root'; @@ -60,7 +60,10 @@ const DEFAULT_SETTINGS_WITH_CORE_PLUGINS = { }, }; -export function createRootWithSettings(...settings: Array>) { +export function createRootWithSettings( + settings: Record, + cliArgs: Partial = {} +) { const env = Env.createDefault({ configs: [], cliArgs: { @@ -72,13 +75,14 @@ export function createRootWithSettings(...settings: Array>) repl: false, basePath: false, optimize: false, + ...cliArgs, }, isDevClusterMaster: false, }); return new Root( new BehaviorSubject( - new LegacyObjectToConfigAdapter(defaultsDeep({}, ...settings, DEFAULTS_SETTINGS)) + new LegacyObjectToConfigAdapter(defaultsDeep({}, settings, DEFAULTS_SETTINGS)) ), env ); @@ -104,8 +108,8 @@ function getSupertest(root: Root, method: HttpMethod, path: string) { * @param {Object} [settings={}] Any config overrides for this instance. * @returns {Root} */ -export function createRoot(settings = {}) { - return createRootWithSettings(settings); +export function createRoot(settings = {}, cliArgs: Partial = {}) { + return createRootWithSettings(settings, cliArgs); } /** @@ -116,7 +120,7 @@ export function createRoot(settings = {}) { * @returns {Root} */ export function createRootWithCorePlugins(settings = {}) { - return createRootWithSettings(settings, DEFAULT_SETTINGS_WITH_CORE_PLUGINS); + return createRootWithSettings(defaultsDeep({}, settings, DEFAULT_SETTINGS_WITH_CORE_PLUGINS)); } /** diff --git a/yarn.lock b/yarn.lock index d501d19644f97..91f0cbbe7db28 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2347,6 +2347,11 @@ resolved "https://registry.yarnpkg.com/@types/boom/-/boom-7.2.0.tgz#19c36cbb5811a7493f0f2e37f31d42b28df1abc1" integrity sha512-HonbGsHFbskh9zRAzA6tabcw18mCOsSEOL2ibGAuVqk6e7nElcRmWO5L4UfIHpDbWBWw+eZYFdsQ1+MEGgpcVA== +"@types/caseless@*": + version "0.12.2" + resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8" + integrity sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w== + "@types/catbox@*": version "10.0.1" resolved "https://registry.yarnpkg.com/@types/catbox/-/catbox-10.0.1.tgz#266679017749041fe9873fee1131dd2aaa04a07e" @@ -2532,7 +2537,7 @@ resolved "https://registry.yarnpkg.com/@types/fetch-mock/-/fetch-mock-7.2.1.tgz#5630999aa75532e00af42a54cbe05e1651f4a080" integrity sha512-zuLhLEK4gOPxhkiUhqbG4p0lKY2ePEE//5NHTTn/vjYl0XWpfk2x0Fw7EWKtCjlggEsuc1GvpasD46X9PSZFaA== -"@types/form-data@^2.2.1": +"@types/form-data@*", "@types/form-data@^2.2.1": version "2.2.1" resolved "https://registry.yarnpkg.com/@types/form-data/-/form-data-2.2.1.tgz#ee2b3b8eaa11c0938289953606b745b738c54b1e" integrity sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ== @@ -3013,6 +3018,16 @@ resolved "https://registry.yarnpkg.com/@types/redux/-/redux-3.6.31.tgz#40eafa7575db36b912ce0059b85de98c205b0708" integrity sha1-QOr6dXXbNrkSzgBZuF3pjCBbBwg= +"@types/request@^2.48.1": + version "2.48.1" + resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.1.tgz#e402d691aa6670fbbff1957b15f1270230ab42fa" + integrity sha512-ZgEZ1TiD+KGA9LiAAPPJL68Id2UWfeSO62ijSXZjFJArVV+2pKcsVHmrcu+1oiE3q6eDGiFiSolRc4JHoerBBg== + dependencies: + "@types/caseless" "*" + "@types/form-data" "*" + "@types/node" "*" + "@types/tough-cookie" "*" + "@types/retry@*", "@types/retry@^0.10.2": version "0.10.2" resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.10.2.tgz#bd1740c4ad51966609b058803ee6874577848b37" @@ -3117,6 +3132,11 @@ resolved "https://registry.yarnpkg.com/@types/tinycolor2/-/tinycolor2-1.4.1.tgz#2f5670c9d1d6e558897a810ed284b44918fc1253" integrity sha512-25L/RL5tqZkquKXVHM1fM2bd23qjfbcPpAZ2N/H05Y45g3UEi+Hw8CbDV28shKY8gH1SHiLpZSxPI1lacqdpGg== +"@types/tough-cookie@*": + version "2.3.5" + resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-2.3.5.tgz#9da44ed75571999b65c37b60c9b2b88db54c585d" + integrity sha512-SCcK7mvGi3+ZNz833RRjFIxrn4gI1PPR3NtuIS+6vMkvmsGjosqTJwRt5bAEFLRz+wtJMWv8+uOnZf2hi2QXTg== + "@types/type-detect@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@types/type-detect/-/type-detect-4.0.1.tgz#3b0f5ac82ea630090cbf57c57a1bf5a63a29b9b6" From ef83bc12d3d1880640644c6d8974cde861bc216e Mon Sep 17 00:00:00 2001 From: restrry Date: Fri, 5 Apr 2019 14:43:56 +0200 Subject: [PATCH 06/35] update tests --- src/core/server/legacy/legacy_service.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index d8dda124ea5bb..e62177d6756e3 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -162,6 +162,7 @@ describe('once LegacyService is set up with connection info', () => { { server: { autoListen: true } }, { elasticsearch: setupDeps.elasticsearch, + http: setupDeps.http, serverOptions: { listener: expect.any(LegacyPlatformProxy), someAnotherOption: 'bar', @@ -187,6 +188,7 @@ describe('once LegacyService is set up with connection info', () => { { server: { autoListen: true } }, { elasticsearch: setupDeps.elasticsearch, + http: setupDeps.http, serverOptions: { listener: expect.any(LegacyPlatformProxy), someAnotherOption: 'bar', From cb2a5e3773c2af7a2af29c6631a25c20c92db0f5 Mon Sep 17 00:00:00 2001 From: restrry Date: Fri, 5 Apr 2019 16:14:43 +0200 Subject: [PATCH 07/35] session storage cleanup --- src/core/server/http/http_server.ts | 30 +--------- src/core/server/http/session_storage.ts | 76 ++++++++++++++++++++----- 2 files changed, 64 insertions(+), 42 deletions(-) diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 6b7b27bf57a2a..7202f2633145a 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -18,7 +18,6 @@ */ import { Server, ServerOptions } from 'hapi'; -import hapiAuthCookie from 'hapi-auth-cookie'; import { modifyUrl } from '../../utils'; import { Logger } from '../logging'; @@ -27,17 +26,7 @@ import { createServer, getServerOptions } from './http_tools'; import { adoptToHapiAuthFormat, Authenticate } from './lifecycle/auth'; import { adoptToHapiOnRequestFormat, OnRequest } from './lifecycle/on_request'; import { Router } from './router'; - -import { SessionStorage } from './session_storage'; - -export interface CookieOptions { - name: string; - password: string; - validate: (sessionValue: any) => boolean | Promise; - isSecure: boolean; - sessionTimeout: number; - path?: string; -} +import { CookieOptions, createCookieSessionStorageFor } from './session_storage'; export interface HttpServerInfo { server: Server; @@ -162,22 +151,7 @@ export class HttpServer { throw new Error('Server is not created yet'); } - await this.server.register({ plugin: hapiAuthCookie }); - - this.server.auth.strategy('security-cookie', 'cookie', { - cookie: cookieOptions.name, - password: cookieOptions.password, - validateFunc: async (req, session) => ({ valid: await cookieOptions.validate(session) }), - isSecure: cookieOptions.isSecure, - path: cookieOptions.path, - clearInvalid: true, - isHttpOnly: true, - isSameSite: false, - }); - - const sessionStorage = new SessionStorage(request => - this.server!.auth.test('security-cookie', request) - ); + const sessionStorage = await createCookieSessionStorageFor(this.server, cookieOptions); this.server.auth.scheme('login', () => ({ authenticate: adoptToHapiAuthFormat(fn, sessionStorage), diff --git a/src/core/server/http/session_storage.ts b/src/core/server/http/session_storage.ts index d2c888cff2f87..8aad623d32ee8 100644 --- a/src/core/server/http/session_storage.ts +++ b/src/core/server/http/session_storage.ts @@ -17,20 +17,68 @@ * under the License. */ -import { Request } from 'hapi'; +import { Request, Server } from 'hapi'; +import hapiAuthCookie from 'hapi-auth-cookie'; -export type ScopedSessionStorage = ReturnType; -export class SessionStorage { - constructor(private readonly sessionGetter: (request: Request) => Promise) {} - public asScoped(request: Request) { - // NOTE: probably need bind here. request.cookieAuth.set.bind(request.cookieAuth) - return { - // Retrieves session value from the session storage. - get: () => this.sessionGetter(request), - // Puts current session value into the session storage. - set: (session: object) => request.cookieAuth.set(session), - // Clears current session. - clear: () => request.cookieAuth.clear(), - }; +export interface CookieOptions { + name: string; + password: string; + validate: (sessionValue: any) => boolean | Promise; + isSecure: boolean; + sessionTimeout: number; + path?: string; +} + +export class ScopedSessionStorage { + constructor( + private readonly sessionGetter: () => Promise, + private readonly request: Request + ) {} + /** + * Retrieves session value from the session storage. + */ + public async get() { + return await this.sessionGetter(); + } + /** + * Puts current session value into the session storage. + * @param sessionValue - value to put store into + */ + public set(sessionValue: object) { + return this.request.cookieAuth.set(sessionValue); } + /** + * Clears current session. + */ + public clear() { + return this.request.cookieAuth.clear(); + } +} + +export interface SessionStorage { + asScoped: (request: Request) => ScopedSessionStorage; +} + +export async function createCookieSessionStorageFor( + server: Server, + cookieOptions: CookieOptions +): Promise { + await server.register({ plugin: hapiAuthCookie }); + + server.auth.strategy('security-cookie', 'cookie', { + cookie: cookieOptions.name, + password: cookieOptions.password, + validateFunc: async (req, session) => ({ valid: await cookieOptions.validate(session) }), + isSecure: cookieOptions.isSecure, + path: cookieOptions.path, + clearInvalid: true, + isHttpOnly: true, + isSameSite: false, + }); + + return { + asScoped(request: Request) { + return new ScopedSessionStorage(() => server.auth.test('security-cookie', request), request); + }, + }; } From f4176247a3a258ffe7b6bb2ec0d02159368930c2 Mon Sep 17 00:00:00 2001 From: restrry Date: Fri, 5 Apr 2019 16:15:16 +0200 Subject: [PATCH 08/35] get SessionStorage type safe --- src/core/server/http/http_server.ts | 6 ++-- .../plugins/dummy_security/server/plugin.ts | 14 ++++++-- src/core/server/http/lifecycle/auth.ts | 6 ++-- src/core/server/http/session_storage.ts | 36 ++++++++++++------- 4 files changed, 41 insertions(+), 21 deletions(-) diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 7202f2633145a..8cc67e18e1b69 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -31,7 +31,7 @@ import { CookieOptions, createCookieSessionStorageFor } from './session_storage' export interface HttpServerInfo { server: Server; options: ServerOptions; - registerAuth: (fn: Authenticate, cookieOptions: CookieOptions) => void; + registerAuth: (fn: Authenticate, cookieOptions: CookieOptions) => void; registerOnRequest: (fn: OnRequest) => void; } @@ -146,12 +146,12 @@ export class HttpServer { this.server.ext('onRequest', adoptToHapiOnRequestFormat(fn)); }; - private registerAuth = async (fn: Authenticate, cookieOptions: CookieOptions) => { + private registerAuth = async (fn: Authenticate, cookieOptions: CookieOptions) => { if (this.server === undefined) { throw new Error('Server is not created yet'); } - const sessionStorage = await createCookieSessionStorageFor(this.server, cookieOptions); + const sessionStorage = await createCookieSessionStorageFor(this.server, cookieOptions); this.server.auth.scheme('login', () => ({ authenticate: adoptToHapiAuthFormat(fn, sessionStorage), diff --git a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts index f6586c50363dd..5c70940a33434 100644 --- a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts +++ b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts @@ -19,9 +19,19 @@ import Boom from 'boom'; import { Authenticate, CoreSetup } from '../../../../../../../../core/server'; +interface User { + id: string; + roles?: string[]; +} + +interface Storage { + value: User; + expires: number; +} + export class DummySecurityPlugin { public setup(core: CoreSetup) { - const authenticate: Authenticate = async (request, sessionStorage, t) => { + const authenticate: Authenticate = async (request, sessionStorage, t) => { if (request.path === '/auth/has_session') { const prevSession = await sessionStorage.get(); const userData = prevSession.value; @@ -42,7 +52,7 @@ export class DummySecurityPlugin { const cookieOptions = { name: 'sid', password: 'something_at_least_32_characters', - validate: () => true, + validate: (session: Storage) => true, isSecure: false, path: '/', sessionTimeout: 10000000, diff --git a/src/core/server/http/lifecycle/auth.ts b/src/core/server/http/lifecycle/auth.ts index 726dad60b6db0..99ad665ca0f98 100644 --- a/src/core/server/http/lifecycle/auth.ts +++ b/src/core/server/http/lifecycle/auth.ts @@ -57,13 +57,13 @@ const toolkit = { rejected: AuthResult.rejected, }; -export type Authenticate = ( +export type Authenticate = ( request: Request, - sessionStorage: ScopedSessionStorage, + sessionStorage: ScopedSessionStorage, t: typeof toolkit ) => Promise; -export function adoptToHapiAuthFormat(fn: Authenticate, sessionStorage: SessionStorage) { +export function adoptToHapiAuthFormat(fn: Authenticate, sessionStorage: SessionStorage) { return async function interceptAuth( req: Request, h: ResponseToolkit diff --git a/src/core/server/http/session_storage.ts b/src/core/server/http/session_storage.ts index 8aad623d32ee8..ca8ac040c56ae 100644 --- a/src/core/server/http/session_storage.ts +++ b/src/core/server/http/session_storage.ts @@ -20,31 +20,31 @@ import { Request, Server } from 'hapi'; import hapiAuthCookie from 'hapi-auth-cookie'; -export interface CookieOptions { +export interface CookieOptions { name: string; password: string; - validate: (sessionValue: any) => boolean | Promise; + validate: (sessionValue: T) => boolean | Promise; isSecure: boolean; sessionTimeout: number; path?: string; } -export class ScopedSessionStorage { +export class ScopedSessionStorage> { constructor( - private readonly sessionGetter: () => Promise, + private readonly sessionGetter: () => Promise, private readonly request: Request ) {} /** * Retrieves session value from the session storage. */ - public async get() { + public async get(): Promise { return await this.sessionGetter(); } /** * Puts current session value into the session storage. * @param sessionValue - value to put store into */ - public set(sessionValue: object) { + public set(sessionValue: T) { return this.request.cookieAuth.set(sessionValue); } /** @@ -55,20 +55,27 @@ export class ScopedSessionStorage { } } -export interface SessionStorage { - asScoped: (request: Request) => ScopedSessionStorage; +export interface SessionStorage { + asScoped: (request: Request) => ScopedSessionStorage; } -export async function createCookieSessionStorageFor( +/** + * Creates object with SessionStorage interface, which abstract the way of + * session storage implementation. + * + * @param server - hapi server to create SessionStorage for + * @param cookieOptions - cookies configuration + */ +export async function createCookieSessionStorageFor( server: Server, - cookieOptions: CookieOptions -): Promise { + cookieOptions: CookieOptions +): Promise> { await server.register({ plugin: hapiAuthCookie }); server.auth.strategy('security-cookie', 'cookie', { cookie: cookieOptions.name, password: cookieOptions.password, - validateFunc: async (req, session) => ({ valid: await cookieOptions.validate(session) }), + validateFunc: async (req, session: T) => ({ valid: await cookieOptions.validate(session) }), isSecure: cookieOptions.isSecure, path: cookieOptions.path, clearInvalid: true, @@ -78,7 +85,10 @@ export async function createCookieSessionStorageFor( return { asScoped(request: Request) { - return new ScopedSessionStorage(() => server.auth.test('security-cookie', request), request); + return new ScopedSessionStorage( + () => server.auth.test('security-cookie', request), + request + ); }, }; } From 2f1dd746132d09f4ca10fede670ff07a598cf3ab Mon Sep 17 00:00:00 2001 From: restrry Date: Tue, 9 Apr 2019 11:10:44 +0200 Subject: [PATCH 09/35] add redirect, clear cookie security integration tests --- .../plugins/dummy_security/server/plugin.ts | 17 ++++- .../integration_tests/http_service.test.ts | 65 +++++++++++++------ 2 files changed, 62 insertions(+), 20 deletions(-) diff --git a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts index 5c70940a33434..439205e778d6f 100644 --- a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts +++ b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts @@ -29,16 +29,31 @@ interface Storage { expires: number; } +export const url = { + auth: '/auth', + authHasSession: '/auth/has_session', + authClearSession: '/auth/clear_session', + authRedirect: '/auth/redirect', +}; + export class DummySecurityPlugin { public setup(core: CoreSetup) { const authenticate: Authenticate = async (request, sessionStorage, t) => { - if (request.path === '/auth/has_session') { + if (request.path === url.authHasSession) { const prevSession = await sessionStorage.get(); const userData = prevSession.value; sessionStorage.set({ value: userData, expires: Date.now() + 1000 }); return t.authenticated({ credentials: userData }); } + if (request.path === url.authClearSession) { + sessionStorage.clear(); + + return t.rejected(Boom.unauthorized()); + } + if (request.path === url.authRedirect) { + return t.redirected('/login'); + } if (request.headers.authorization) { const user = { id: '42' }; 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 8d148c3f3499a..9aece9334ebf1 100644 --- a/src/core/server/http/integration_tests/http_service.test.ts +++ b/src/core/server/http/integration_tests/http_service.test.ts @@ -21,14 +21,12 @@ import path from 'path'; import request from 'request'; import * as kbnTestServer from '../../../../test_utils/kbn_server'; import { Router } from '../router'; - -const authUrl = '/auth'; -const authHasSessionUrl = '/auth/has_session'; -const dummySecurityPlugin = path.resolve(__dirname, './__fixtures__/plugins/dummy_security'); +import { url } from './__fixtures__/plugins/dummy_security/server/plugin'; describe('http service', () => { describe('setup contract', () => { describe('#registerAuth()', () => { + const dummySecurityPlugin = path.resolve(__dirname, './__fixtures__/plugins/dummy_security'); let root: ReturnType; beforeAll(async () => { root = kbnTestServer.createRoot( @@ -41,10 +39,10 @@ describe('http service', () => { ); const router = new Router(''); - router.get({ path: authUrl, validate: false }, async (req, res) => + router.get({ path: url.auth, validate: false }, async (req, res) => res.ok({ content: 'ok' }) ); - router.get({ path: authHasSessionUrl, validate: false }, async (req, res) => + router.get({ path: url.authHasSession, validate: false }, async (req, res) => res.ok({ content: 'ok' }) ); // TODO fix me when registerRouter is available before HTTP server is run @@ -57,7 +55,7 @@ describe('http service', () => { it('Should allow to implement custom authentication logic and set the cookie', async () => { const response = await kbnTestServer.request - .get(root, authUrl) + .get(root, url.auth) .expect(200, { content: 'ok' }); expect(response.header['set-cookie']).toBeDefined(); @@ -65,25 +63,29 @@ describe('http service', () => { expect(cookies).toHaveLength(1); const sessionCookie = request.cookie(cookies[0]); - + if (!sessionCookie) { + throw new Error('session cookie expected to be defined'); + } expect(sessionCookie).toBeDefined(); - expect(sessionCookie!.key).toBe('sid'); - expect(sessionCookie!.value).toBeDefined(); - expect(sessionCookie!.path).toBe('/'); - expect(sessionCookie!.httpOnly).toBe(true); + expect(sessionCookie.key).toBe('sid'); + expect(sessionCookie.value).toBeDefined(); + expect(sessionCookie.path).toBe('/'); + expect(sessionCookie.httpOnly).toBe(true); }); it('Should allow to read already set cookie', async () => { const response = await kbnTestServer.request - .get(root, authUrl) + .get(root, url.auth) .expect(200, { content: 'ok' }); const cookies = response.header['set-cookie']; const sessionCookie = request.cookie(cookies[0]); - + if (!sessionCookie) { + throw new Error('session cookie expected to be defined'); + } const response2 = await kbnTestServer.request - .get(root, authHasSessionUrl) - .set('Cookie', `${sessionCookie!.key}=${sessionCookie!.value}`) + .get(root, url.authHasSession) + .set('Cookie', `${sessionCookie.key}=${sessionCookie.value}`) .expect(200, { content: 'ok' }); expect(response2.header['set-cookie']).toBeDefined(); @@ -94,13 +96,38 @@ describe('http service', () => { it('Should allow to reject a request from an unauthenticated user', async () => { await kbnTestServer.request - .get(root, authUrl) + .get(root, url.auth) .unset('Authorization') .expect(401); }); - it(`Shouldn't affect legacy server routes`, async () => { - const legacyUrl = `${authUrl}/legacy`; + it('Should allow to redirect', async () => { + await kbnTestServer.request.get(root, url.authRedirect).expect(302); + }); + + it('Should allow to clear cookie session storage', async () => { + const response = await kbnTestServer.request + .get(root, url.auth) + .expect(200, { content: 'ok' }); + + const sessionCookie = request.cookie(response.header['set-cookie'][0]); + + if (!sessionCookie) { + throw new Error('session cookie expected to be defined'); + } + + const response2 = await kbnTestServer.request + .get(root, url.authClearSession) + .set('Cookie', `${sessionCookie.key}=${sessionCookie.value}`) + .expect(401); + + expect(response2.header['set-cookie']).toEqual([ + 'sid=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Path=/', + ]); + }); + + it('Should run auth for legacy routes and proxy request to legacy server route handlers', async () => { + const legacyUrl = '/legacy'; const kbnServer = kbnTestServer.getKbnServer(root); kbnServer.server.route({ method: 'GET', From fdc2ff9cf2e9af293b8292d784556df0edfafae4 Mon Sep 17 00:00:00 2001 From: restrry Date: Tue, 9 Apr 2019 13:56:02 +0200 Subject: [PATCH 10/35] add tests for onRequest --- .../server/http/lifecycle/on_request.test.ts | 89 +++++++++++++++++++ src/core/server/http/lifecycle/on_request.ts | 23 +++-- 2 files changed, 107 insertions(+), 5 deletions(-) create mode 100644 src/core/server/http/lifecycle/on_request.test.ts diff --git a/src/core/server/http/lifecycle/on_request.test.ts b/src/core/server/http/lifecycle/on_request.test.ts new file mode 100644 index 0000000000000..b3d273f1c46e4 --- /dev/null +++ b/src/core/server/http/lifecycle/on_request.test.ts @@ -0,0 +1,89 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Boom from 'boom'; +import { adoptToHapiOnRequestFormat } from './on_request'; + +const requestMock = {} as any; +const createResponseToolkit = (customization = {}): any => ({ ...customization }); + +describe('adoptToHapiOnRequestFormat', () => { + it('Should allow to continue request', async () => { + const continueSymbol = {}; + const onRequest = adoptToHapiOnRequestFormat((req, t) => t.next()); + const result = await onRequest( + requestMock, + createResponseToolkit({ + ['continue']: continueSymbol, + }) + ); + + expect(result).toBe(continueSymbol); + }); + + it('Should allow to redirect to specified url', async () => { + const redirectUrl = '/docs'; + const onRequest = adoptToHapiOnRequestFormat((req, t) => t.redirected(redirectUrl)); + const takeoverMock = jest.fn(); + const redirectMock = jest.fn().mockImplementation(() => ({ takeover: takeoverMock })); + await onRequest( + requestMock, + createResponseToolkit({ + redirect: redirectMock, + }) + ); + + expect(redirectMock).toBeCalledTimes(1); + expect(redirectMock).toBeCalledWith(redirectUrl); + expect(takeoverMock).toBeCalledTimes(1); + }); + + it('Should allow to specify statusCode and message for Boom error', async () => { + const onRequest = adoptToHapiOnRequestFormat((req, t) => { + return t.rejected(new Error('unexpected result'), { statusCode: 501 }); + }); + const result = (await onRequest(requestMock, createResponseToolkit())) as Boom; + + expect(result).toBeInstanceOf(Boom); + expect(result.message).toBe('unexpected result'); + expect(result.output.statusCode).toBe(501); + }); + + it('Should return Boom error if interceptor throws', async () => { + const onRequest = adoptToHapiOnRequestFormat((req, t) => { + throw new Error('unknown error'); + }); + const result = (await onRequest(requestMock, createResponseToolkit())) as Boom; + + expect(result).toBeInstanceOf(Boom); + expect(result.message).toBe('unknown error'); + expect(result.output.statusCode).toBe(500); + }); + + it('Should return Boom error if interceptor returns unexpected result', async () => { + const onRequest = adoptToHapiOnRequestFormat((req, toolkit) => undefined as any); + const result = (await onRequest(requestMock, createResponseToolkit())) as Boom; + + expect(result).toBeInstanceOf(Boom); + expect(result.message).toBe( + 'Unexpected result from OnRequest. Expected OnRequestResult, but given: undefined.' + ); + expect(result.output.statusCode).toBe(500); + }); +}); diff --git a/src/core/server/http/lifecycle/on_request.ts b/src/core/server/http/lifecycle/on_request.ts index 971e49dcb4030..6d313e5f7ddf1 100644 --- a/src/core/server/http/lifecycle/on_request.ts +++ b/src/core/server/http/lifecycle/on_request.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ + import Boom from 'boom'; import { Lifecycle, Request, ResponseToolkit } from 'hapi'; import { KibanaRequest } from '../router'; @@ -26,6 +27,9 @@ enum ResultType { rejected = 'rejected', } +interface ErrorParams { + statusCode?: number; +} class OnRequestResult { public static next() { return new OnRequestResult(ResultType.next); @@ -33,8 +37,8 @@ class OnRequestResult { public static redirected(url: string) { return new OnRequestResult(ResultType.redirected, url); } - public static rejected(error: Error) { - return new OnRequestResult(ResultType.rejected, error); + public static rejected(error: Error, { statusCode }: ErrorParams) { + return new OnRequestResult(ResultType.rejected, { error, statusCode }); } public static isValidResult(candidate: any) { return candidate instanceof OnRequestResult; @@ -61,6 +65,15 @@ export type OnRequest = ( req: KibanaRequest, t: typeof toolkit ) => OnRequestResult; + +/** + * Adopt custom request interceptor to Hapi lifecycle system. + * @param fn - an extension point allowing to perform custom logic for + * incoming HTTP requests. Should finish with one of the following commands: + * - t.next(). to pass a request to the next handler. + * - t.redirected(url). to interrupt request handling and redirect to configured url. + * - t.rejected(error). to fail the request with specified error. + */ export function adoptToHapiOnRequestFormat(fn: OnRequest) { return async function interceptRequest( req: Request, @@ -76,13 +89,13 @@ export function adoptToHapiOnRequestFormat(fn: OnRequest) { return h.redirect(result.payload).takeover(); } if (result.isRejected()) { - const { statusCode } = result.payload; - return Boom.boomify(result.payload, { statusCode }); + const { error, statusCode } = result.payload; + return Boom.boomify(error, { statusCode }); } } throw new Error( - `Unexpected result from OnRequest. Expected OnRequestResult, but given: ${result}` + `Unexpected result from OnRequest. Expected OnRequestResult, but given: ${result}.` ); } catch (error) { return new Boom(error.message, { statusCode: 500 }); From 52a45bd16c2a3319122f603d0a292e8c4138cec1 Mon Sep 17 00:00:00 2001 From: restrry Date: Tue, 9 Apr 2019 15:52:34 +0200 Subject: [PATCH 11/35] add tests for onAuth --- src/core/server/http/lifecycle/auth.test.ts | 104 ++++++++++++++++++++ src/core/server/http/lifecycle/auth.ts | 20 ++-- 2 files changed, 117 insertions(+), 7 deletions(-) create mode 100644 src/core/server/http/lifecycle/auth.test.ts diff --git a/src/core/server/http/lifecycle/auth.test.ts b/src/core/server/http/lifecycle/auth.test.ts new file mode 100644 index 0000000000000..3d5e0f1d4ee10 --- /dev/null +++ b/src/core/server/http/lifecycle/auth.test.ts @@ -0,0 +1,104 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Boom from 'boom'; +import { adoptToHapiAuthFormat } from './auth'; + +const SessionStorageMock = { + asScoped: () => null as any, +}; +const requestMock = {} as any; +const createResponseToolkit = (customization = {}): any => ({ ...customization }); + +describe('adoptToHapiAuthFormat', () => { + it('Should allow authenticating a user identity with given credentials', async () => { + const credentials = {}; + const authenticatedMock = jest.fn(); + const onAuth = adoptToHapiAuthFormat( + async (req, sessionStorage, t) => t.authenticated(credentials), + SessionStorageMock + ); + await onAuth( + requestMock, + createResponseToolkit({ + authenticated: authenticatedMock, + }) + ); + + expect(authenticatedMock).toBeCalledTimes(1); + expect(authenticatedMock).toBeCalledWith({ credentials }); + }); + + it('Should allow redirecting to specified url', async () => { + const redirectUrl = '/docs'; + const onAuth = adoptToHapiAuthFormat( + async (req, sessionStorage, t) => t.redirected(redirectUrl), + SessionStorageMock + ); + const takeoverMock = jest.fn(); + const redirectMock = jest.fn().mockImplementation(() => ({ takeover: takeoverMock })); + await onAuth( + requestMock, + createResponseToolkit({ + redirect: redirectMock, + }) + ); + + expect(redirectMock).toBeCalledTimes(1); + expect(redirectMock).toBeCalledWith(redirectUrl); + expect(takeoverMock).toBeCalledTimes(1); + }); + + it('Should allow to specify statusCode and message for Boom error', async () => { + const onAuth = adoptToHapiAuthFormat( + async (req, sessionStorage, t) => t.rejected(new Error('not found'), { statusCode: 404 }), + SessionStorageMock + ); + const result = (await onAuth(requestMock, createResponseToolkit())) as Boom; + + expect(result).toBeInstanceOf(Boom); + expect(result.message).toBe('not found'); + expect(result.output.statusCode).toBe(404); + }); + + it('Should return Boom error if interceptor throws', async () => { + const onAuth = adoptToHapiAuthFormat(async (req, sessionStorage, t) => { + throw new Error('unknown error'); + }, SessionStorageMock); + const result = (await onAuth(requestMock, createResponseToolkit())) as Boom; + + expect(result).toBeInstanceOf(Boom); + expect(result.message).toBe('unknown error'); + expect(result.output.statusCode).toBe(500); + }); + + it('Should return Boom error if interceptor returns unexpected result', async () => { + const onAuth = adoptToHapiAuthFormat( + async (req, sessionStorage, t) => undefined as any, + SessionStorageMock + ); + const result = (await onAuth(requestMock, createResponseToolkit())) as Boom; + + expect(result).toBeInstanceOf(Boom); + expect(result.message).toBe( + 'Unexpected result from Authenticate. Expected AuthResult, but given: undefined.' + ); + expect(result.output.statusCode).toBe(500); + }); +}); diff --git a/src/core/server/http/lifecycle/auth.ts b/src/core/server/http/lifecycle/auth.ts index 99ad665ca0f98..6c42e00bacafe 100644 --- a/src/core/server/http/lifecycle/auth.ts +++ b/src/core/server/http/lifecycle/auth.ts @@ -25,7 +25,9 @@ enum ResultType { redirected = 'redirected', rejected = 'rejected', } - +interface ErrorParams { + statusCode?: number; +} class AuthResult { public static authenticated(credentials: any = {}) { return new AuthResult(ResultType.authenticated, credentials); @@ -33,8 +35,8 @@ class AuthResult { public static redirected(url: string) { return new AuthResult(ResultType.redirected, url); } - public static rejected(error: Error) { - return new AuthResult(ResultType.rejected, error); + public static rejected(error: Error, { statusCode }: ErrorParams) { + return new AuthResult(ResultType.rejected, { error, statusCode }); } public static isValidResult(candidate: any) { return candidate instanceof AuthResult; @@ -63,13 +65,17 @@ export type Authenticate = ( t: typeof toolkit ) => Promise; -export function adoptToHapiAuthFormat(fn: Authenticate, sessionStorage: SessionStorage) { +export function adoptToHapiAuthFormat( + fn: Authenticate, + sessionStorage: SessionStorage +) { return async function interceptAuth( req: Request, h: ResponseToolkit ): Promise { try { const result = await fn(req, sessionStorage.asScoped(req), toolkit); + if (AuthResult.isValidResult(result)) { if (result.isAuthenticated()) { return h.authenticated({ credentials: result.payload }); @@ -78,12 +84,12 @@ export function adoptToHapiAuthFormat(fn: Authenticate, sessionStorage: Se return h.redirect(result.payload).takeover(); } if (result.isRejected()) { - const { statusCode } = result.payload; - return Boom.boomify(result.payload, { statusCode }); + const { error, statusCode } = result.payload; + return Boom.boomify(error, { statusCode }); } } throw new Error( - `Unexpected result from Authenticate. Expected AuthResult, but given: ${result}` + `Unexpected result from Authenticate. Expected AuthResult, but given: ${result}.` ); } catch (error) { return new Boom(error.message, { statusCode: 500 }); From 92c08e5c7a9cf9763d0870fe0dd691bb1d17d0df Mon Sep 17 00:00:00 2001 From: restrry Date: Tue, 9 Apr 2019 17:26:36 +0200 Subject: [PATCH 12/35] register Auth interceptor only once --- src/core/server/http/http_server.test.ts | 11 +++++++++++ src/core/server/http/http_server.ts | 5 +++++ 2 files changed, 16 insertions(+) diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index 89c2c10afcef8..84b9869d4b466 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -571,3 +571,14 @@ test('returns server and connection options on start', async () => { expect(innerServer).toBe((server as any).server); expect(options).toMatchSnapshot(); }); + +test('registers auth request interceptor only once', async () => { + const { registerAuth } = await server.start(config); + const doRegister = () => + registerAuth(() => null as any, { + password: 'asdasdasdasdadakdaksdjhasjdasdh', + } as any); + + await doRegister(); + expect(doRegister()).rejects.toThrowError('Auth hook was already registered'); +}); diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 8cc67e18e1b69..b6c6c8644ad90 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -38,6 +38,7 @@ export interface HttpServerInfo { export class HttpServer { private server?: Server; private registeredRouters: Set = new Set(); + private authRegistered: boolean = false; constructor(private readonly log: Logger) {} @@ -150,6 +151,10 @@ export class HttpServer { if (this.server === undefined) { throw new Error('Server is not created yet'); } + if (this.authRegistered) { + throw new Error('Auth hook was already registered'); + } + this.authRegistered = true; const sessionStorage = await createCookieSessionStorageFor(this.server, cookieOptions); From aa5e08c071b4cf2c379f83aac95d41ec03a9bea2 Mon Sep 17 00:00:00 2001 From: restrry Date: Wed, 10 Apr 2019 08:23:16 +0200 Subject: [PATCH 13/35] refactor redirect tests --- src/core/server/http/lifecycle/auth.test.ts | 9 ++++----- src/core/server/http/lifecycle/on_request.test.ts | 9 ++++----- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/core/server/http/lifecycle/auth.test.ts b/src/core/server/http/lifecycle/auth.test.ts index 3d5e0f1d4ee10..173e2495f371f 100644 --- a/src/core/server/http/lifecycle/auth.test.ts +++ b/src/core/server/http/lifecycle/auth.test.ts @@ -51,18 +51,17 @@ describe('adoptToHapiAuthFormat', () => { async (req, sessionStorage, t) => t.redirected(redirectUrl), SessionStorageMock ); - const takeoverMock = jest.fn(); - const redirectMock = jest.fn().mockImplementation(() => ({ takeover: takeoverMock })); - await onAuth( + const takeoverSymbol = {}; + const redirectMock = jest.fn(() => ({ takeover: () => takeoverSymbol })); + const result = await onAuth( requestMock, createResponseToolkit({ redirect: redirectMock, }) ); - expect(redirectMock).toBeCalledTimes(1); expect(redirectMock).toBeCalledWith(redirectUrl); - expect(takeoverMock).toBeCalledTimes(1); + expect(result).toBe(takeoverSymbol); }); it('Should allow to specify statusCode and message for Boom error', async () => { diff --git a/src/core/server/http/lifecycle/on_request.test.ts b/src/core/server/http/lifecycle/on_request.test.ts index b3d273f1c46e4..3cfe4456d4d77 100644 --- a/src/core/server/http/lifecycle/on_request.test.ts +++ b/src/core/server/http/lifecycle/on_request.test.ts @@ -40,18 +40,17 @@ describe('adoptToHapiOnRequestFormat', () => { it('Should allow to redirect to specified url', async () => { const redirectUrl = '/docs'; const onRequest = adoptToHapiOnRequestFormat((req, t) => t.redirected(redirectUrl)); - const takeoverMock = jest.fn(); - const redirectMock = jest.fn().mockImplementation(() => ({ takeover: takeoverMock })); - await onRequest( + const takeoverSymbol = {}; + const redirectMock = jest.fn(() => ({ takeover: () => takeoverSymbol })); + const result = await onRequest( requestMock, createResponseToolkit({ redirect: redirectMock, }) ); - expect(redirectMock).toBeCalledTimes(1); expect(redirectMock).toBeCalledWith(redirectUrl); - expect(takeoverMock).toBeCalledTimes(1); + expect(result).toBe(takeoverSymbol); }); it('Should allow to specify statusCode and message for Boom error', async () => { From c70df0fb4c11d8bb61eb4c6653d244ad699d7a9a Mon Sep 17 00:00:00 2001 From: restrry Date: Wed, 10 Apr 2019 08:32:40 +0200 Subject: [PATCH 14/35] fix typings, change error message, test suit naming --- src/core/server/http/http_server.test.ts | 4 ++-- src/core/server/http/http_server.ts | 2 +- .../server/http/integration_tests/http_service.test.ts | 10 +++++----- src/core/server/http/lifecycle/auth.ts | 2 +- src/core/server/http/lifecycle/on_request.ts | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index 84b9869d4b466..bbbf449d54ffb 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -576,9 +576,9 @@ test('registers auth request interceptor only once', async () => { const { registerAuth } = await server.start(config); const doRegister = () => registerAuth(() => null as any, { - password: 'asdasdasdasdadakdaksdjhasjdasdh', + password: 'any_password', } as any); await doRegister(); - expect(doRegister()).rejects.toThrowError('Auth hook was already registered'); + expect(doRegister()).rejects.toThrowError('Auth interceptor was already registered'); }); diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index b6c6c8644ad90..718619fbbb6c4 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -152,7 +152,7 @@ export class HttpServer { throw new Error('Server is not created yet'); } if (this.authRegistered) { - throw new Error('Auth hook was already registered'); + throw new Error('Auth interceptor was already registered'); } this.authRegistered = true; 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 9aece9334ebf1..873bf7fb7a538 100644 --- a/src/core/server/http/integration_tests/http_service.test.ts +++ b/src/core/server/http/integration_tests/http_service.test.ts @@ -53,7 +53,7 @@ describe('http service', () => { afterAll(async () => await root.shutdown()); - it('Should allow to implement custom authentication logic and set the cookie', async () => { + it('Should support implementing custom authentication logic and set the cookie', async () => { const response = await kbnTestServer.request .get(root, url.auth) .expect(200, { content: 'ok' }); @@ -73,7 +73,7 @@ describe('http service', () => { expect(sessionCookie.httpOnly).toBe(true); }); - it('Should allow to read already set cookie', async () => { + it('Should support reading already set cookie', async () => { const response = await kbnTestServer.request .get(root, url.auth) .expect(200, { content: 'ok' }); @@ -94,18 +94,18 @@ describe('http service', () => { expect(cookies).not.toBe(cookies2); }); - it('Should allow to reject a request from an unauthenticated user', async () => { + it('Should support rejecting a request from an unauthenticated user', async () => { await kbnTestServer.request .get(root, url.auth) .unset('Authorization') .expect(401); }); - it('Should allow to redirect', async () => { + it('Should support redirecting', async () => { await kbnTestServer.request.get(root, url.authRedirect).expect(302); }); - it('Should allow to clear cookie session storage', async () => { + it('Should support clearing cookie session storage', async () => { const response = await kbnTestServer.request .get(root, url.auth) .expect(200, { content: 'ok' }); diff --git a/src/core/server/http/lifecycle/auth.ts b/src/core/server/http/lifecycle/auth.ts index 6c42e00bacafe..a593959ef6002 100644 --- a/src/core/server/http/lifecycle/auth.ts +++ b/src/core/server/http/lifecycle/auth.ts @@ -35,7 +35,7 @@ class AuthResult { public static redirected(url: string) { return new AuthResult(ResultType.redirected, url); } - public static rejected(error: Error, { statusCode }: ErrorParams) { + public static rejected(error: Error, { statusCode }: ErrorParams = {}) { return new AuthResult(ResultType.rejected, { error, statusCode }); } public static isValidResult(candidate: any) { diff --git a/src/core/server/http/lifecycle/on_request.ts b/src/core/server/http/lifecycle/on_request.ts index 6d313e5f7ddf1..f6f607792d261 100644 --- a/src/core/server/http/lifecycle/on_request.ts +++ b/src/core/server/http/lifecycle/on_request.ts @@ -37,7 +37,7 @@ class OnRequestResult { public static redirected(url: string) { return new OnRequestResult(ResultType.redirected, url); } - public static rejected(error: Error, { statusCode }: ErrorParams) { + public static rejected(error: Error, { statusCode }: ErrorParams = {}) { return new OnRequestResult(ResultType.rejected, { error, statusCode }); } public static isValidResult(candidate: any) { From 01e6426bda444264e88f0b55f501495a965b35a8 Mon Sep 17 00:00:00 2001 From: restrry Date: Wed, 10 Apr 2019 09:40:00 +0200 Subject: [PATCH 15/35] add integration test for session validation --- .../plugins/dummy_security/server/plugin.ts | 12 ++++++--- .../integration_tests/http_service.test.ts | 27 ++++++++++++++++++- src/core/server/http/session_storage.ts | 9 ++++--- 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts index 439205e778d6f..bb16280a079a3 100644 --- a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts +++ b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts @@ -36,28 +36,33 @@ export const url = { authRedirect: '/auth/redirect', }; +export const sessionDurationMs = 30; export class DummySecurityPlugin { public setup(core: CoreSetup) { const authenticate: Authenticate = async (request, sessionStorage, t) => { if (request.path === url.authHasSession) { const prevSession = await sessionStorage.get(); + if (!prevSession) return t.rejected(new Error('invalid session'), { statusCode: 401 }); + const userData = prevSession.value; - sessionStorage.set({ value: userData, expires: Date.now() + 1000 }); + sessionStorage.set({ value: userData, expires: Date.now() + sessionDurationMs }); return t.authenticated({ credentials: userData }); } + if (request.path === url.authClearSession) { sessionStorage.clear(); return t.rejected(Boom.unauthorized()); } + if (request.path === url.authRedirect) { return t.redirected('/login'); } if (request.headers.authorization) { const user = { id: '42' }; - sessionStorage.set({ value: user, expires: Date.now() + 1000 }); + sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs }); return t.authenticated({ credentials: user }); } else { return t.rejected(Boom.unauthorized()); @@ -67,10 +72,9 @@ export class DummySecurityPlugin { const cookieOptions = { name: 'sid', password: 'something_at_least_32_characters', - validate: (session: Storage) => true, + validate: (session: Storage) => session.expires > Date.now(), isSecure: false, path: '/', - sessionTimeout: 10000000, }; core.http.registerAuth(authenticate, cookieOptions); return { 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 873bf7fb7a538..5eec00f35e303 100644 --- a/src/core/server/http/integration_tests/http_service.test.ts +++ b/src/core/server/http/integration_tests/http_service.test.ts @@ -21,7 +21,9 @@ import path from 'path'; import request from 'request'; import * as kbnTestServer from '../../../../test_utils/kbn_server'; import { Router } from '../router'; -import { url } from './__fixtures__/plugins/dummy_security/server/plugin'; +import { url, sessionDurationMs } from './__fixtures__/plugins/dummy_security/server/plugin'; + +const delay = (ms: number) => new Promise(res => setTimeout(res, ms)); describe('http service', () => { describe('setup contract', () => { @@ -94,6 +96,29 @@ describe('http service', () => { expect(cookies).not.toBe(cookies2); }); + it('Should support session validation', async () => { + const response = await kbnTestServer.request + .get(root, url.auth) + .expect(200, { content: 'ok' }); + + const cookies = response.header['set-cookie']; + const sessionCookie = request.cookie(cookies[0]); + if (!sessionCookie) { + throw new Error('session cookie expected to be defined'); + } + + await delay(sessionDurationMs); + + const response2 = await kbnTestServer.request + .get(root, url.authHasSession) + .set('Cookie', `${sessionCookie.key}=${sessionCookie.value}`) + .expect(401, { statusCode: 401, error: 'Unauthorized', message: 'invalid session' }); + + expect(response2.header['set-cookie']).toEqual([ + 'sid=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Path=/', + ]); + }); + it('Should support rejecting a request from an unauthenticated user', async () => { await kbnTestServer.request .get(root, url.auth) diff --git a/src/core/server/http/session_storage.ts b/src/core/server/http/session_storage.ts index ca8ac040c56ae..9036042a694ec 100644 --- a/src/core/server/http/session_storage.ts +++ b/src/core/server/http/session_storage.ts @@ -25,7 +25,6 @@ export interface CookieOptions { password: string; validate: (sessionValue: T) => boolean | Promise; isSecure: boolean; - sessionTimeout: number; path?: string; } @@ -37,8 +36,12 @@ export class ScopedSessionStorage> { /** * Retrieves session value from the session storage. */ - public async get(): Promise { - return await this.sessionGetter(); + public async get(): Promise { + try { + return await this.sessionGetter(); + } catch (error) { + return null; + } } /** * Puts current session value into the session storage. From a9b8c6dc3c80aac740158cf718538e103c8e7ecb Mon Sep 17 00:00:00 2001 From: restrry Date: Wed, 10 Apr 2019 13:24:19 +0200 Subject: [PATCH 16/35] add tests for cookie session storage --- .../server/http/cookie_session_storage.ts | 78 ++++++ .../server/http/cookie_sesson_storage.test.ts | 223 ++++++++++++++++++ src/core/server/http/http_server.ts | 4 +- .../plugins/dummy_security/server/plugin.ts | 20 +- .../integration_tests/http_service.test.ts | 74 +----- src/core/server/http/lifecycle/auth.ts | 6 +- src/core/server/http/session_storage.ts | 77 +----- 7 files changed, 320 insertions(+), 162 deletions(-) create mode 100644 src/core/server/http/cookie_session_storage.ts create mode 100644 src/core/server/http/cookie_sesson_storage.test.ts diff --git a/src/core/server/http/cookie_session_storage.ts b/src/core/server/http/cookie_session_storage.ts new file mode 100644 index 0000000000000..8c11fe02916e7 --- /dev/null +++ b/src/core/server/http/cookie_session_storage.ts @@ -0,0 +1,78 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Request, Server } from 'hapi'; +import hapiAuthCookie from 'hapi-auth-cookie'; +import { SessionStorageFactory, SessionStorage } from './session_storage'; + +export interface CookieOptions { + name: string; + password: string; + validate: (sessionValue: T) => boolean | Promise; + isSecure: boolean; + path?: string; +} + +class ScopedCookieSessionStorage> implements SessionStorage { + constructor(private readonly server: Server, private readonly request: Request) {} + public async get(): Promise { + try { + return await this.server.auth.test('security-cookie', this.request); + } catch (error) { + return null; + } + } + public set(sessionValue: T) { + return this.request.cookieAuth.set(sessionValue); + } + public clear() { + return this.request.cookieAuth.clear(); + } +} + +/** + * Creates SessionStorage factory, which abstract the way of + * session storage implementation and scoping to the incoming requests. + * + * @param server - hapi server to create SessionStorage for + * @param cookieOptions - cookies configuration + */ +export async function createCookieSessionStorageFactory( + server: Server, + cookieOptions: CookieOptions +): Promise> { + await server.register({ plugin: hapiAuthCookie }); + + server.auth.strategy('security-cookie', 'cookie', { + cookie: cookieOptions.name, + password: cookieOptions.password, + validateFunc: async (req, session: T) => ({ valid: await cookieOptions.validate(session) }), + isSecure: cookieOptions.isSecure, + path: cookieOptions.path, + clearInvalid: true, + isHttpOnly: true, + isSameSite: false, + }); + + return { + asScoped(request: Request) { + return new ScopedCookieSessionStorage(server, request); + }, + }; +} diff --git a/src/core/server/http/cookie_sesson_storage.test.ts b/src/core/server/http/cookie_sesson_storage.test.ts new file mode 100644 index 0000000000000..1c54703690de6 --- /dev/null +++ b/src/core/server/http/cookie_sesson_storage.test.ts @@ -0,0 +1,223 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Server } from 'hapi'; +import request from 'request'; + +import { createCookieSessionStorageFactory } from './cookie_session_storage'; + +interface User { + id: string; + roles?: string[]; +} + +interface Storage { + value: User; + expires: number; +} + +function retrieveSessionCookie(cookies: string) { + const sessionCookie = request.cookie(cookies); + if (!sessionCookie) { + throw new Error('session cookie expected to be defined'); + } + return sessionCookie; +} + +const userData = { id: '42' }; +const sessionDurationMs = 30; +const delay = (ms: number) => new Promise(res => setTimeout(res, ms)); +const cookieOptions = { + name: 'sid', + password: 'something_at_least_32_characters', + validate: (session: Storage) => session.expires > Date.now(), + isSecure: false, + path: '/', +}; + +describe('Cookie based SessionStorage', () => { + describe('#set()', () => { + it('Should write to session storage & set cookies', async () => { + const server = new Server(); + const factory = await createCookieSessionStorageFactory(server, cookieOptions); + server.route({ + method: 'GET', + path: '/set', + options: { + handler: async (req, h) => { + const sessionStorage = factory.asScoped(req); + sessionStorage.set({ value: userData, expires: Date.now() + sessionDurationMs }); + return h.response(); + }, + }, + }); + + const response = await server.inject('/set'); + expect(response.statusCode).toBe(200); + + const cookies = response.headers['set-cookie']; + expect(cookies).toBeDefined(); + expect(cookies).toHaveLength(1); + + const sessionCookie = retrieveSessionCookie(cookies[0]); + expect(sessionCookie).toBeDefined(); + expect(sessionCookie.key).toBe('sid'); + expect(sessionCookie.value).toBeDefined(); + expect(sessionCookie.path).toBe('/'); + expect(sessionCookie.httpOnly).toBe(true); + }); + }); + describe('#get()', () => { + it('Should read from session storage', async () => { + const server = new Server(); + const factory = await createCookieSessionStorageFactory(server, cookieOptions); + server.route({ + method: 'GET', + path: '/get', + options: { + handler: async (req, h) => { + const sessionStorage = factory.asScoped(req); + const sessionValue = await sessionStorage.get(); + if (!sessionValue) { + sessionStorage.set({ value: userData, expires: Date.now() + sessionDurationMs }); + return h.response(); + } + return h.response(sessionValue.value); + }, + }, + }); + + const response = await server.inject('/get'); + expect(response.statusCode).toBe(200); + + const cookies = response.headers['set-cookie']; + expect(cookies).toBeDefined(); + expect(cookies).toHaveLength(1); + + const sessionCookie = retrieveSessionCookie(cookies[0]); + + const response2 = await server.inject({ + method: 'GET', + url: '/get', + headers: { cookie: `${sessionCookie.key}=${sessionCookie.value}` }, + }); + expect(response2.statusCode).toBe(200); + expect(response2.result).toEqual(userData); + }); + it('Should return null for empty session', async () => { + const server = new Server(); + const factory = await createCookieSessionStorageFactory(server, cookieOptions); + server.route({ + method: 'GET', + path: '/get-empty', + options: { + handler: async (req, h) => { + const sessionStorage = factory.asScoped(req); + const sessionValue = await sessionStorage.get(); + return h.response(JSON.stringify(sessionValue)); + }, + }, + }); + const response = await server.inject('/get-empty'); + expect(response.statusCode).toBe(200); + expect(response.result).toBe('null'); + + const cookies = response.headers['set-cookie']; + expect(cookies).not.toBeDefined(); + }); + it('Should return null for invalid session & clean cookies', async () => { + const server = new Server(); + const factory = await createCookieSessionStorageFactory(server, cookieOptions); + let setOnce = false; + server.route({ + method: 'GET', + path: '/get-invalid', + options: { + handler: async (req, h) => { + const sessionStorage = factory.asScoped(req); + if (!setOnce) { + setOnce = true; + sessionStorage.set({ value: userData, expires: Date.now() + sessionDurationMs }); + return h.response(); + } + const sessionValue = await sessionStorage.get(); + return h.response(JSON.stringify(sessionValue)); + }, + }, + }); + const response = await server.inject('/get-invalid'); + expect(response.statusCode).toBe(200); + + const cookies = response.headers['set-cookie']; + expect(cookies).toBeDefined(); + + await delay(sessionDurationMs); + + const sessionCookie = retrieveSessionCookie(cookies[0]); + const response2 = await server.inject({ + method: 'GET', + url: '/get-invalid', + headers: { cookie: `${sessionCookie.key}=${sessionCookie.value}` }, + }); + expect(response2.statusCode).toBe(200); + expect(response2.result).toBe('null'); + + const cookies2 = response2.headers['set-cookie']; + expect(cookies2).toEqual([ + 'sid=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Path=/', + ]); + }); + }); + describe('#clear()', () => { + it('Should clear session storage & remove cookies', async () => { + const server = new Server(); + const factory = await createCookieSessionStorageFactory(server, cookieOptions); + server.route({ + method: 'GET', + path: '/clear', + options: { + handler: async (req, h) => { + const sessionStorage = factory.asScoped(req); + if (await sessionStorage.get()) { + sessionStorage.clear(); + return h.response(); + } + sessionStorage.set({ value: userData, expires: Date.now() + sessionDurationMs }); + return h.response(); + }, + }, + }); + const response = await server.inject('/clear'); + const cookies = response.headers['set-cookie']; + + const sessionCookie = retrieveSessionCookie(cookies[0]); + + const response2 = await server.inject({ + method: 'GET', + url: '/clear', + headers: { cookie: `${sessionCookie.key}=${sessionCookie.value}` }, + }); + expect(response2.statusCode).toBe(200); + + const cookies2 = response2.headers['set-cookie']; + expect(cookies2).toEqual([ + 'sid=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Path=/', + ]); + }); + }); +}); diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 718619fbbb6c4..a11e03cb47fca 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -26,7 +26,7 @@ import { createServer, getServerOptions } from './http_tools'; import { adoptToHapiAuthFormat, Authenticate } from './lifecycle/auth'; import { adoptToHapiOnRequestFormat, OnRequest } from './lifecycle/on_request'; import { Router } from './router'; -import { CookieOptions, createCookieSessionStorageFor } from './session_storage'; +import { CookieOptions, createCookieSessionStorageFactory } from './cookie_session_storage'; export interface HttpServerInfo { server: Server; @@ -156,7 +156,7 @@ export class HttpServer { } this.authRegistered = true; - const sessionStorage = await createCookieSessionStorageFor(this.server, cookieOptions); + const sessionStorage = await createCookieSessionStorageFactory(this.server, cookieOptions); this.server.auth.scheme('login', () => ({ authenticate: adoptToHapiAuthFormat(fn, sessionStorage), diff --git a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts index bb16280a079a3..3247c72cdd86e 100644 --- a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts +++ b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts @@ -31,8 +31,6 @@ interface Storage { export const url = { auth: '/auth', - authHasSession: '/auth/has_session', - authClearSession: '/auth/clear_session', authRedirect: '/auth/redirect', }; @@ -40,22 +38,6 @@ export const sessionDurationMs = 30; export class DummySecurityPlugin { public setup(core: CoreSetup) { const authenticate: Authenticate = async (request, sessionStorage, t) => { - if (request.path === url.authHasSession) { - const prevSession = await sessionStorage.get(); - if (!prevSession) return t.rejected(new Error('invalid session'), { statusCode: 401 }); - - const userData = prevSession.value; - sessionStorage.set({ value: userData, expires: Date.now() + sessionDurationMs }); - - return t.authenticated({ credentials: userData }); - } - - if (request.path === url.authClearSession) { - sessionStorage.clear(); - - return t.rejected(Boom.unauthorized()); - } - if (request.path === url.authRedirect) { return t.redirected('/login'); } @@ -72,7 +54,7 @@ export class DummySecurityPlugin { const cookieOptions = { name: 'sid', password: 'something_at_least_32_characters', - validate: (session: Storage) => session.expires > Date.now(), + validate: (session: Storage) => true, isSecure: false, path: '/', }; 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 5eec00f35e303..70cdebf332f2b 100644 --- a/src/core/server/http/integration_tests/http_service.test.ts +++ b/src/core/server/http/integration_tests/http_service.test.ts @@ -21,9 +21,7 @@ import path from 'path'; import request from 'request'; import * as kbnTestServer from '../../../../test_utils/kbn_server'; import { Router } from '../router'; -import { url, sessionDurationMs } from './__fixtures__/plugins/dummy_security/server/plugin'; - -const delay = (ms: number) => new Promise(res => setTimeout(res, ms)); +import { url } from './__fixtures__/plugins/dummy_security/server/plugin'; describe('http service', () => { describe('setup contract', () => { @@ -44,9 +42,6 @@ describe('http service', () => { router.get({ path: url.auth, validate: false }, async (req, res) => res.ok({ content: 'ok' }) ); - router.get({ path: url.authHasSession, validate: false }, async (req, res) => - res.ok({ content: 'ok' }) - ); // TODO fix me when registerRouter is available before HTTP server is run (root as any).server.http.registerRouter(router); @@ -55,7 +50,7 @@ describe('http service', () => { afterAll(async () => await root.shutdown()); - it('Should support implementing custom authentication logic and set the cookie', async () => { + it('Should support implementing custom authentication logic', async () => { const response = await kbnTestServer.request .get(root, url.auth) .expect(200, { content: 'ok' }); @@ -75,50 +70,6 @@ describe('http service', () => { expect(sessionCookie.httpOnly).toBe(true); }); - it('Should support reading already set cookie', async () => { - const response = await kbnTestServer.request - .get(root, url.auth) - .expect(200, { content: 'ok' }); - - const cookies = response.header['set-cookie']; - const sessionCookie = request.cookie(cookies[0]); - if (!sessionCookie) { - throw new Error('session cookie expected to be defined'); - } - const response2 = await kbnTestServer.request - .get(root, url.authHasSession) - .set('Cookie', `${sessionCookie.key}=${sessionCookie.value}`) - .expect(200, { content: 'ok' }); - - expect(response2.header['set-cookie']).toBeDefined(); - - const cookies2 = response2.header['set-cookie']; - expect(cookies).not.toBe(cookies2); - }); - - it('Should support session validation', async () => { - const response = await kbnTestServer.request - .get(root, url.auth) - .expect(200, { content: 'ok' }); - - const cookies = response.header['set-cookie']; - const sessionCookie = request.cookie(cookies[0]); - if (!sessionCookie) { - throw new Error('session cookie expected to be defined'); - } - - await delay(sessionDurationMs); - - const response2 = await kbnTestServer.request - .get(root, url.authHasSession) - .set('Cookie', `${sessionCookie.key}=${sessionCookie.value}`) - .expect(401, { statusCode: 401, error: 'Unauthorized', message: 'invalid session' }); - - expect(response2.header['set-cookie']).toEqual([ - 'sid=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Path=/', - ]); - }); - it('Should support rejecting a request from an unauthenticated user', async () => { await kbnTestServer.request .get(root, url.auth) @@ -130,27 +81,6 @@ describe('http service', () => { await kbnTestServer.request.get(root, url.authRedirect).expect(302); }); - it('Should support clearing cookie session storage', async () => { - const response = await kbnTestServer.request - .get(root, url.auth) - .expect(200, { content: 'ok' }); - - const sessionCookie = request.cookie(response.header['set-cookie'][0]); - - if (!sessionCookie) { - throw new Error('session cookie expected to be defined'); - } - - const response2 = await kbnTestServer.request - .get(root, url.authClearSession) - .set('Cookie', `${sessionCookie.key}=${sessionCookie.value}`) - .expect(401); - - expect(response2.header['set-cookie']).toEqual([ - 'sid=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Path=/', - ]); - }); - it('Should run auth for legacy routes and proxy request to legacy server route handlers', async () => { const legacyUrl = '/legacy'; const kbnServer = kbnTestServer.getKbnServer(root); diff --git a/src/core/server/http/lifecycle/auth.ts b/src/core/server/http/lifecycle/auth.ts index a593959ef6002..f16611bb078e2 100644 --- a/src/core/server/http/lifecycle/auth.ts +++ b/src/core/server/http/lifecycle/auth.ts @@ -18,7 +18,7 @@ */ import Boom from 'boom'; import { Lifecycle, Request, ResponseToolkit } from 'hapi'; -import { ScopedSessionStorage, SessionStorage } from '../session_storage'; +import { SessionStorage, SessionStorageFactory } from '../session_storage'; enum ResultType { authenticated = 'authenticated', @@ -61,13 +61,13 @@ const toolkit = { export type Authenticate = ( request: Request, - sessionStorage: ScopedSessionStorage, + sessionStorage: SessionStorage, t: typeof toolkit ) => Promise; export function adoptToHapiAuthFormat( fn: Authenticate, - sessionStorage: SessionStorage + sessionStorage: SessionStorageFactory ) { return async function interceptAuth( req: Request, diff --git a/src/core/server/http/session_storage.ts b/src/core/server/http/session_storage.ts index 9036042a694ec..4f9d28991fe78 100644 --- a/src/core/server/http/session_storage.ts +++ b/src/core/server/http/session_storage.ts @@ -17,81 +17,26 @@ * under the License. */ -import { Request, Server } from 'hapi'; -import hapiAuthCookie from 'hapi-auth-cookie'; - -export interface CookieOptions { - name: string; - password: string; - validate: (sessionValue: T) => boolean | Promise; - isSecure: boolean; - path?: string; -} - -export class ScopedSessionStorage> { - constructor( - private readonly sessionGetter: () => Promise, - private readonly request: Request - ) {} +import { Request } from 'hapi'; +/** + * Provides an interface to store and retrieve data across requests. + */ +export interface SessionStorage { /** * Retrieves session value from the session storage. */ - public async get(): Promise { - try { - return await this.sessionGetter(); - } catch (error) { - return null; - } - } + get(): Promise; /** * Puts current session value into the session storage. - * @param sessionValue - value to put store into + * @param sessionValue - value to put */ - public set(sessionValue: T) { - return this.request.cookieAuth.set(sessionValue); - } + set(sessionValue: T): void; /** * Clears current session. */ - public clear() { - return this.request.cookieAuth.clear(); - } -} - -export interface SessionStorage { - asScoped: (request: Request) => ScopedSessionStorage; + clear(): void; } -/** - * Creates object with SessionStorage interface, which abstract the way of - * session storage implementation. - * - * @param server - hapi server to create SessionStorage for - * @param cookieOptions - cookies configuration - */ -export async function createCookieSessionStorageFor( - server: Server, - cookieOptions: CookieOptions -): Promise> { - await server.register({ plugin: hapiAuthCookie }); - - server.auth.strategy('security-cookie', 'cookie', { - cookie: cookieOptions.name, - password: cookieOptions.password, - validateFunc: async (req, session: T) => ({ valid: await cookieOptions.validate(session) }), - isSecure: cookieOptions.isSecure, - path: cookieOptions.path, - clearInvalid: true, - isHttpOnly: true, - isSameSite: false, - }); - - return { - asScoped(request: Request) { - return new ScopedSessionStorage( - () => server.auth.test('security-cookie', request), - request - ); - }, - }; +export interface SessionStorageFactory { + asScoped: (request: Request) => SessionStorage; } From 9f6e8bf88ed18ff1dcaa849739a1608d099ddef8 Mon Sep 17 00:00:00 2001 From: restrry Date: Wed, 10 Apr 2019 13:55:50 +0200 Subject: [PATCH 17/35] update docs --- .../kibana-plugin-server.authenticate.md | 10 ++ ....elasticsearchservicesetup.adminclient$.md | 9 ++ ....elasticsearchservicesetup.createclient.md | 9 ++ ...r.elasticsearchservicesetup.dataclient$.md | 9 ++ ...server.elasticsearchservicesetup.legacy.md | 11 ++ ...plugin-server.elasticsearchservicesetup.md | 20 ++++ .../kibana-plugin-server.httpservicesetup.md | 10 ++ ...kibana-plugin-server.kibanarequest.body.md | 9 ++ ...kibana-plugin-server.kibanarequest.from.md | 23 ++++ ...server.kibanarequest.getfilteredheaders.md | 20 ++++ ...ana-plugin-server.kibanarequest.headers.md | 9 ++ .../kibana-plugin-server.kibanarequest.md | 27 +++++ ...bana-plugin-server.kibanarequest.params.md | 9 ++ ...ibana-plugin-server.kibanarequest.query.md | 9 ++ .../core/server/kibana-plugin-server.md | 6 + .../server/kibana-plugin-server.onrequest.md | 10 ++ ...a-plugin-server.pluginsetupcontext.http.md | 12 ++ ...kibana-plugin-server.pluginsetupcontext.md | 1 + .../kibana-plugin-server.router.delete.md | 23 ++++ .../server/kibana-plugin-server.router.get.md | 23 ++++ .../kibana-plugin-server.router.getroutes.md | 17 +++ .../server/kibana-plugin-server.router.md | 28 +++++ .../kibana-plugin-server.router.path.md | 9 ++ .../kibana-plugin-server.router.post.md | 23 ++++ .../server/kibana-plugin-server.router.put.md | 23 ++++ .../kibana-plugin-server.router.routes.md | 9 ++ .../elasticsearch/elasticsearch_service.ts | 1 + src/core/server/http/http_service.ts | 2 +- src/core/server/http/lifecycle/auth.ts | 2 + src/core/server/http/lifecycle/on_request.ts | 2 + src/core/server/http/router/request.ts | 1 + src/core/server/http/router/router.ts | 1 + src/core/server/kibana.api.md | 103 ++++++++++++++++-- 33 files changed, 472 insertions(+), 8 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-server.authenticate.md create mode 100644 docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.adminclient$.md create mode 100644 docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.createclient.md create mode 100644 docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.dataclient$.md create mode 100644 docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.legacy.md create mode 100644 docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.md create mode 100644 docs/development/core/server/kibana-plugin-server.httpservicesetup.md create mode 100644 docs/development/core/server/kibana-plugin-server.kibanarequest.body.md create mode 100644 docs/development/core/server/kibana-plugin-server.kibanarequest.from.md create mode 100644 docs/development/core/server/kibana-plugin-server.kibanarequest.getfilteredheaders.md create mode 100644 docs/development/core/server/kibana-plugin-server.kibanarequest.headers.md create mode 100644 docs/development/core/server/kibana-plugin-server.kibanarequest.md create mode 100644 docs/development/core/server/kibana-plugin-server.kibanarequest.params.md create mode 100644 docs/development/core/server/kibana-plugin-server.kibanarequest.query.md create mode 100644 docs/development/core/server/kibana-plugin-server.onrequest.md create mode 100644 docs/development/core/server/kibana-plugin-server.pluginsetupcontext.http.md create mode 100644 docs/development/core/server/kibana-plugin-server.router.delete.md create mode 100644 docs/development/core/server/kibana-plugin-server.router.get.md create mode 100644 docs/development/core/server/kibana-plugin-server.router.getroutes.md create mode 100644 docs/development/core/server/kibana-plugin-server.router.md create mode 100644 docs/development/core/server/kibana-plugin-server.router.path.md create mode 100644 docs/development/core/server/kibana-plugin-server.router.post.md create mode 100644 docs/development/core/server/kibana-plugin-server.router.put.md create mode 100644 docs/development/core/server/kibana-plugin-server.router.routes.md diff --git a/docs/development/core/server/kibana-plugin-server.authenticate.md b/docs/development/core/server/kibana-plugin-server.authenticate.md new file mode 100644 index 0000000000000..f1c01b23dca71 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.authenticate.md @@ -0,0 +1,10 @@ +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Authenticate](./kibana-plugin-server.authenticate.md) + +## Authenticate type + + +Signature: + +```typescript +export declare type Authenticate = (request: Request, sessionStorage: SessionStorage, t: typeof toolkit) => Promise; +``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.adminclient$.md b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.adminclient$.md new file mode 100644 index 0000000000000..452bf9be914eb --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.adminclient$.md @@ -0,0 +1,9 @@ +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) > [adminClient$](./kibana-plugin-server.elasticsearchservicesetup.adminclient$.md) + +## ElasticsearchServiceSetup.adminClient$ property + +Signature: + +```typescript +readonly adminClient$: Observable; +``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.createclient.md b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.createclient.md new file mode 100644 index 0000000000000..10c6392658fd0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.createclient.md @@ -0,0 +1,9 @@ +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) > [createClient](./kibana-plugin-server.elasticsearchservicesetup.createclient.md) + +## ElasticsearchServiceSetup.createClient property + +Signature: + +```typescript +readonly createClient: (type: string, config: ElasticsearchClientConfig) => ClusterClient; +``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.dataclient$.md b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.dataclient$.md new file mode 100644 index 0000000000000..01d0eb2d6fdbc --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.dataclient$.md @@ -0,0 +1,9 @@ +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) > [dataClient$](./kibana-plugin-server.elasticsearchservicesetup.dataclient$.md) + +## ElasticsearchServiceSetup.dataClient$ property + +Signature: + +```typescript +readonly dataClient$: Observable; +``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.legacy.md b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.legacy.md new file mode 100644 index 0000000000000..d6d2d4911bcce --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.legacy.md @@ -0,0 +1,11 @@ +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) > [legacy](./kibana-plugin-server.elasticsearchservicesetup.legacy.md) + +## ElasticsearchServiceSetup.legacy property + +Signature: + +```typescript +readonly legacy: { + readonly config$: Observable; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.md b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.md new file mode 100644 index 0000000000000..e9fa31b4c3898 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.md @@ -0,0 +1,20 @@ +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) + +## ElasticsearchServiceSetup interface + + +Signature: + +```typescript +export interface ElasticsearchServiceSetup +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [adminClient$](./kibana-plugin-server.elasticsearchservicesetup.adminclient$.md) | Observable<ClusterClient> | | +| [createClient](./kibana-plugin-server.elasticsearchservicesetup.createclient.md) | (type: string, config: ElasticsearchClientConfig) => ClusterClient | | +| [dataClient$](./kibana-plugin-server.elasticsearchservicesetup.dataclient$.md) | Observable<ClusterClient> | | +| [legacy](./kibana-plugin-server.elasticsearchservicesetup.legacy.md) | {`

` readonly config$: Observable<ElasticsearchConfig>;`

` } | | + diff --git a/docs/development/core/server/kibana-plugin-server.httpservicesetup.md b/docs/development/core/server/kibana-plugin-server.httpservicesetup.md new file mode 100644 index 0000000000000..a5517f26bdf96 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpservicesetup.md @@ -0,0 +1,10 @@ +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) + +## HttpServiceSetup type + + +Signature: + +```typescript +export declare type HttpServiceSetup = HttpServerInfo; +``` diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.body.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.body.md new file mode 100644 index 0000000000000..e08f84ea44bba --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.body.md @@ -0,0 +1,9 @@ +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [body](./kibana-plugin-server.kibanarequest.body.md) + +## KibanaRequest.body property + +Signature: + +```typescript +readonly body: Body; +``` diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.from.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.from.md new file mode 100644 index 0000000000000..7d762642bd99f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.from.md @@ -0,0 +1,23 @@ +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [from](./kibana-plugin-server.kibanarequest.from.md) + +## KibanaRequest.from() method + +Factory for creating requests. Validates the request before creating an instance of a KibanaRequest. + +Signature: + +```typescript +static from

(req: Request, routeSchemas: RouteSchemas | undefined): KibanaRequest; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| req | Request | | +| routeSchemas | RouteSchemas<P, Q, B> | undefined | | + +Returns: + +`KibanaRequest` + diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.getfilteredheaders.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.getfilteredheaders.md new file mode 100644 index 0000000000000..defa9b739586f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.getfilteredheaders.md @@ -0,0 +1,20 @@ +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [getFilteredHeaders](./kibana-plugin-server.kibanarequest.getfilteredheaders.md) + +## KibanaRequest.getFilteredHeaders() method + +Signature: + +```typescript +getFilteredHeaders(headersToKeep: string[]): Pick, string>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| headersToKeep | string[] | | + +Returns: + +`Pick, string>` + diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.headers.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.headers.md new file mode 100644 index 0000000000000..f920942db4a67 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.headers.md @@ -0,0 +1,9 @@ +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [headers](./kibana-plugin-server.kibanarequest.headers.md) + +## KibanaRequest.headers property + +Signature: + +```typescript +readonly headers: Headers; +``` diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.md new file mode 100644 index 0000000000000..160b0c712a332 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.md @@ -0,0 +1,27 @@ +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) + +## KibanaRequest class + + +Signature: + +```typescript +export declare class KibanaRequest +``` + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [body](./kibana-plugin-server.kibanarequest.body.md) | | Body | | +| [headers](./kibana-plugin-server.kibanarequest.headers.md) | | Headers | | +| [params](./kibana-plugin-server.kibanarequest.params.md) | | Params | | +| [query](./kibana-plugin-server.kibanarequest.query.md) | | Query | | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [from(req, routeSchemas)](./kibana-plugin-server.kibanarequest.from.md) | static | Factory for creating requests. Validates the request before creating an instance of a KibanaRequest. | +| [getFilteredHeaders(headersToKeep)](./kibana-plugin-server.kibanarequest.getfilteredheaders.md) | | | + diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.params.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.params.md new file mode 100644 index 0000000000000..c8b57329aac0d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.params.md @@ -0,0 +1,9 @@ +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [params](./kibana-plugin-server.kibanarequest.params.md) + +## KibanaRequest.params property + +Signature: + +```typescript +readonly params: Params; +``` diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.query.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.query.md new file mode 100644 index 0000000000000..17cf96f3c3a93 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.query.md @@ -0,0 +1,9 @@ +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [query](./kibana-plugin-server.kibanarequest.query.md) + +## KibanaRequest.query property + +Signature: + +```typescript +readonly query: Query; +``` diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index b1066bff9a1dd..a274eae4f70c5 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -7,6 +7,8 @@ | 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 @@ -15,6 +17,7 @@ | --- | --- | | [CallAPIOptions](./kibana-plugin-server.callapioptions.md) | The set of options that defines how API call should be made and result be processed. | | [CoreSetup](./kibana-plugin-server.coresetup.md) | | +| [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) | | | [Logger](./kibana-plugin-server.logger.md) | Logger exposes all the necessary methods to log any type of information and this is the interface used by the logging consumers including plugins. | | [LoggerFactory](./kibana-plugin-server.loggerfactory.md) | The single purpose of LoggerFactory interface is to define a way to retrieve a context-based logger instance. | | [LogMeta](./kibana-plugin-server.logmeta.md) | Contextual metadata | @@ -26,7 +29,10 @@ | Type Alias | Description | | --- | --- | | [APICaller](./kibana-plugin-server.apicaller.md) | | +| [Authenticate](./kibana-plugin-server.authenticate.md) | | | [ElasticsearchClientConfig](./kibana-plugin-server.elasticsearchclientconfig.md) | | | [Headers](./kibana-plugin-server.headers.md) | | +| [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) | | +| [OnRequest](./kibana-plugin-server.onrequest.md) | | | [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.onrequest.md b/docs/development/core/server/kibana-plugin-server.onrequest.md new file mode 100644 index 0000000000000..d485d3dfd34a9 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onrequest.md @@ -0,0 +1,10 @@ +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnRequest](./kibana-plugin-server.onrequest.md) + +## OnRequest type + + +Signature: + +```typescript +export declare type OnRequest = (req: KibanaRequest, t: typeof toolkit) => OnRequestResult; +``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginsetupcontext.http.md b/docs/development/core/server/kibana-plugin-server.pluginsetupcontext.http.md new file mode 100644 index 0000000000000..d8fc74708f118 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.pluginsetupcontext.http.md @@ -0,0 +1,12 @@ +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginSetupContext](./kibana-plugin-server.pluginsetupcontext.md) > [http](./kibana-plugin-server.pluginsetupcontext.http.md) + +## PluginSetupContext.http property + +Signature: + +```typescript +http?: { + registerAuth: HttpServiceSetup['registerAuth']; + registerOnRequest: HttpServiceSetup['registerOnRequest']; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginsetupcontext.md b/docs/development/core/server/kibana-plugin-server.pluginsetupcontext.md index 6a4e1e16352aa..1df89009327f6 100644 --- a/docs/development/core/server/kibana-plugin-server.pluginsetupcontext.md +++ b/docs/development/core/server/kibana-plugin-server.pluginsetupcontext.md @@ -15,4 +15,5 @@ export interface PluginSetupContext | Property | Type | Description | | --- | --- | --- | | [elasticsearch](./kibana-plugin-server.pluginsetupcontext.elasticsearch.md) | {`

` adminClient$: Observable<ClusterClient>;`

` dataClient$: Observable<ClusterClient>;`

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

` registerAuth: HttpServiceSetup['registerAuth'];`

` registerOnRequest: HttpServiceSetup['registerOnRequest'];`

` } | | diff --git a/docs/development/core/server/kibana-plugin-server.router.delete.md b/docs/development/core/server/kibana-plugin-server.router.delete.md new file mode 100644 index 0000000000000..40457d0645ddb --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.router.delete.md @@ -0,0 +1,23 @@ +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [delete](./kibana-plugin-server.router.delete.md) + +## Router.delete() method + +Register a `DELETE` request with the router + +Signature: + +```typescript +delete

(route: RouteConfig, handler: RequestHandler): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| route | RouteConfig<P, Q, B> | | +| handler | RequestHandler<P, Q, B> | | + +Returns: + +`void` + diff --git a/docs/development/core/server/kibana-plugin-server.router.get.md b/docs/development/core/server/kibana-plugin-server.router.get.md new file mode 100644 index 0000000000000..8ab429d411351 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.router.get.md @@ -0,0 +1,23 @@ +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [get](./kibana-plugin-server.router.get.md) + +## Router.get() method + +Register a `GET` request with the router + +Signature: + +```typescript +get

(route: RouteConfig, handler: RequestHandler): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| route | RouteConfig<P, Q, B> | | +| handler | RequestHandler<P, Q, B> | | + +Returns: + +`void` + diff --git a/docs/development/core/server/kibana-plugin-server.router.getroutes.md b/docs/development/core/server/kibana-plugin-server.router.getroutes.md new file mode 100644 index 0000000000000..eb48ca46d7282 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.router.getroutes.md @@ -0,0 +1,17 @@ +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [getRoutes](./kibana-plugin-server.router.getroutes.md) + +## Router.getRoutes() method + +Returns all routes registered with the this router. + +Signature: + +```typescript +getRoutes(): Readonly[]; +``` +Returns: + +`Readonly[]` + +List of registered routes. + diff --git a/docs/development/core/server/kibana-plugin-server.router.md b/docs/development/core/server/kibana-plugin-server.router.md new file mode 100644 index 0000000000000..8fc634b6a592b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.router.md @@ -0,0 +1,28 @@ +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) + +## Router class + + +Signature: + +```typescript +export declare class Router +``` + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [path](./kibana-plugin-server.router.path.md) | | string | | +| [routes](./kibana-plugin-server.router.routes.md) | | Array<Readonly<RouterRoute>> | | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [delete(route, handler)](./kibana-plugin-server.router.delete.md) | | Register a DELETE request with the router | +| [get(route, handler)](./kibana-plugin-server.router.get.md) | | Register a GET request with the router | +| [getRoutes()](./kibana-plugin-server.router.getroutes.md) | | Returns all routes registered with the this router. | +| [post(route, handler)](./kibana-plugin-server.router.post.md) | | Register a POST request with the router | +| [put(route, handler)](./kibana-plugin-server.router.put.md) | | Register a PUT request with the router | + diff --git a/docs/development/core/server/kibana-plugin-server.router.path.md b/docs/development/core/server/kibana-plugin-server.router.path.md new file mode 100644 index 0000000000000..4344dcf4560e3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.router.path.md @@ -0,0 +1,9 @@ +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [path](./kibana-plugin-server.router.path.md) + +## Router.path property + +Signature: + +```typescript +readonly path: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.router.post.md b/docs/development/core/server/kibana-plugin-server.router.post.md new file mode 100644 index 0000000000000..929af38f0c662 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.router.post.md @@ -0,0 +1,23 @@ +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [post](./kibana-plugin-server.router.post.md) + +## Router.post() method + +Register a `POST` request with the router + +Signature: + +```typescript +post

(route: RouteConfig, handler: RequestHandler): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| route | RouteConfig<P, Q, B> | | +| handler | RequestHandler<P, Q, B> | | + +Returns: + +`void` + diff --git a/docs/development/core/server/kibana-plugin-server.router.put.md b/docs/development/core/server/kibana-plugin-server.router.put.md new file mode 100644 index 0000000000000..902104883262e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.router.put.md @@ -0,0 +1,23 @@ +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [put](./kibana-plugin-server.router.put.md) + +## Router.put() method + +Register a `PUT` request with the router + +Signature: + +```typescript +put

(route: RouteConfig, handler: RequestHandler): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| route | RouteConfig<P, Q, B> | | +| handler | RequestHandler<P, Q, B> | | + +Returns: + +`void` + diff --git a/docs/development/core/server/kibana-plugin-server.router.routes.md b/docs/development/core/server/kibana-plugin-server.router.routes.md new file mode 100644 index 0000000000000..a879bbc733b0c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.router.routes.md @@ -0,0 +1,9 @@ +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [routes](./kibana-plugin-server.router.routes.md) + +## Router.routes property + +Signature: + +```typescript +routes: Array>; +``` diff --git a/src/core/server/elasticsearch/elasticsearch_service.ts b/src/core/server/elasticsearch/elasticsearch_service.ts index 22cf15225b307..036521566a039 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.ts @@ -32,6 +32,7 @@ interface CoreClusterClients { dataClient: ClusterClient; } +/** @public */ export interface ElasticsearchServiceSetup { // Required for the BWC with the legacy Kibana only. readonly legacy: { diff --git a/src/core/server/http/http_service.ts b/src/core/server/http/http_service.ts index 848b3f88e0535..c136a7b20b361 100644 --- a/src/core/server/http/http_service.ts +++ b/src/core/server/http/http_service.ts @@ -27,7 +27,7 @@ import { HttpServer, HttpServerInfo } from './http_server'; import { HttpsRedirectServer } from './https_redirect_server'; import { Router } from './router'; -/** @internal */ +/** @public */ export type HttpServiceSetup = HttpServerInfo; /** @internal */ diff --git a/src/core/server/http/lifecycle/auth.ts b/src/core/server/http/lifecycle/auth.ts index f16611bb078e2..e2022b5168523 100644 --- a/src/core/server/http/lifecycle/auth.ts +++ b/src/core/server/http/lifecycle/auth.ts @@ -59,12 +59,14 @@ const toolkit = { rejected: AuthResult.rejected, }; +/** @public */ export type Authenticate = ( request: Request, sessionStorage: SessionStorage, t: typeof toolkit ) => Promise; +/** @public */ export function adoptToHapiAuthFormat( fn: Authenticate, sessionStorage: SessionStorageFactory diff --git a/src/core/server/http/lifecycle/on_request.ts b/src/core/server/http/lifecycle/on_request.ts index f6f607792d261..27a66da9ba92c 100644 --- a/src/core/server/http/lifecycle/on_request.ts +++ b/src/core/server/http/lifecycle/on_request.ts @@ -61,12 +61,14 @@ const toolkit = { rejected: OnRequestResult.rejected, }; +/** @public */ export type OnRequest = ( req: KibanaRequest, t: typeof toolkit ) => OnRequestResult; /** + * @public * Adopt custom request interceptor to Hapi lifecycle system. * @param fn - an extension point allowing to perform custom logic for * incoming HTTP requests. Should finish with one of the following commands: diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts index 8ee07eac2cca3..a98a346a21c46 100644 --- a/src/core/server/http/router/request.ts +++ b/src/core/server/http/router/request.ts @@ -23,6 +23,7 @@ import { Request } from 'hapi'; import { filterHeaders, Headers } from './headers'; import { RouteSchemas } from './route'; +/** @public */ export class KibanaRequest { /** * Factory for creating requests. Validates the request before creating an diff --git a/src/core/server/http/router/router.ts b/src/core/server/http/router/router.ts index 37bfe053f8181..e75045007e4fd 100644 --- a/src/core/server/http/router/router.ts +++ b/src/core/server/http/router/router.ts @@ -30,6 +30,7 @@ export interface RouterRoute { handler: (req: Request, responseToolkit: ResponseToolkit) => Promise; } +/** @public */ export class Router { public routes: Array> = []; diff --git a/src/core/server/kibana.api.md b/src/core/server/kibana.api.md index f1deeb33e3316..117e03d085538 100644 --- a/src/core/server/kibana.api.md +++ b/src/core/server/kibana.api.md @@ -6,7 +6,12 @@ import { ConfigOptions } from 'elasticsearch'; import { Duration } from 'moment'; +import { ObjectType } from '@kbn/config-schema'; import { Observable } from 'rxjs'; +import { Request } from 'hapi'; +import { ResponseObject } from 'hapi'; +import { ResponseToolkit } from 'hapi'; +import { Schema } from '@kbn/config-schema'; import { Server } from 'hapi'; import { ServerOptions } from 'hapi'; import { Type } from '@kbn/config-schema'; @@ -15,6 +20,13 @@ import { TypeOf } from '@kbn/config-schema'; // @public (undocumented) export type APICaller = (endpoint: string, clientParams: Record, options?: CallAPIOptions) => Promise; +// Warning: (ae-forgotten-export) The symbol "SessionStorage" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "toolkit" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "AuthResult" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export type Authenticate = (request: Request, sessionStorage: SessionStorage, t: typeof toolkit) => Promise; + // Warning: (ae-forgotten-export) The symbol "BootstrapArgs" needs to be exported by the entry point index.d.ts // Warning: (ae-internal-missing-underscore) The name bootstrap should be prefixed with an underscore because the declaration is marked as "@internal" // @@ -39,15 +51,11 @@ export class ClusterClient { // @public (undocumented) export interface CoreSetup { - // Warning: (ae-forgotten-export) The symbol "ElasticsearchServiceSetup" needs to be exported by the entry point index.d.ts - // // (undocumented) elasticsearch: ElasticsearchServiceSetup; - // Warning: (ae-forgotten-export) The symbol "HttpServiceSetup" needs to be exported by the entry point index.d.ts - // // (undocumented) http: HttpServiceSetup; - // Warning: (ae-forgotten-export) The symbol "PluginsServiceSetup" needs to be exported by the entry point index.d.ts + // Warning: (ae-incompatible-release-tags) The symbol "plugins" is marked as @public, but its signature references "PluginsServiceSetup" which is marked as @internal // // (undocumented) plugins: PluginsServiceSetup; @@ -74,9 +82,46 @@ export type ElasticsearchClientConfig = Pick; }; +// @public (undocumented) +export interface ElasticsearchServiceSetup { + // (undocumented) + readonly adminClient$: Observable; + // (undocumented) + readonly createClient: (type: string, config: ElasticsearchClientConfig) => ClusterClient; + // (undocumented) + readonly dataClient$: Observable; + // (undocumented) + readonly legacy: { + readonly config$: Observable; + }; +} + // @public (undocumented) export type Headers = Record; +// Warning: (ae-forgotten-export) The symbol "HttpServerInfo" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export type HttpServiceSetup = HttpServerInfo; + +// @public (undocumented) +export class KibanaRequest { + // (undocumented) + constructor(req: Request, params: Params, query: Query, body: Body); + // (undocumented) + readonly body: Body; + // Warning: (ae-forgotten-export) The symbol "RouteSchemas" needs to be exported by the entry point index.d.ts + static from

(req: Request, routeSchemas: RouteSchemas | undefined): KibanaRequest; + // (undocumented) + getFilteredHeaders(headersToKeep: string[]): Pick, string>; + // (undocumented) + readonly headers: Headers; + // (undocumented) + readonly params: Params; + // (undocumented) + readonly query: Query; + } + // @public export interface Logger { debug(message: string, meta?: LogMeta): void; @@ -150,6 +195,12 @@ export interface LogRecord { timestamp: Date; } +// Warning: (ae-forgotten-export) The symbol "toolkit" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "OnRequestResult" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export type OnRequest = (req: KibanaRequest, t: typeof toolkit_2) => OnRequestResult; + // @public export interface PluginInitializerContext { // (undocumented) @@ -175,8 +226,45 @@ export interface PluginSetupContext { adminClient$: Observable; dataClient$: Observable; }; + // (undocumented) + http?: { + registerAuth: HttpServiceSetup['registerAuth']; + registerOnRequest: HttpServiceSetup['registerOnRequest']; + }; +} + +// Warning: (ae-internal-missing-underscore) The name PluginsServiceSetup should be prefixed with an underscore because the declaration is marked as "@internal" +// +// @internal (undocumented) +export interface PluginsServiceSetup { + // (undocumented) + contracts: Map; + // (undocumented) + uiPlugins: { + public: Map; + internal: Map; + }; } +// @public (undocumented) +export class Router { + // (undocumented) + constructor(path: string); + delete

(route: RouteConfig, handler: RequestHandler): void; + // Warning: (ae-forgotten-export) The symbol "RouteConfig" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "RequestHandler" needs to be exported by the entry point index.d.ts + get

(route: RouteConfig, handler: RequestHandler): void; + getRoutes(): Readonly[]; + // (undocumented) + readonly path: string; + post

(route: RouteConfig, handler: RequestHandler): void; + put

(route: RouteConfig, handler: RequestHandler): void; + // Warning: (ae-forgotten-export) The symbol "RouterRoute" needs to be exported by the entry point index.d.ts + // + // (undocumented) + routes: Array>; + } + // @public export class ScopedClusterClient { // (undocumented) @@ -188,8 +276,9 @@ export class ScopedClusterClient { // Warnings were encountered during analysis: // -// src/core/server/plugins/plugin_context.ts:35:9 - (ae-forgotten-export) The symbol "EnvironmentMode" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/plugin_context.ts:39:9 - (ae-forgotten-export) The symbol "ConfigWithSchema" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/plugin_context.ts:36:9 - (ae-forgotten-export) The symbol "EnvironmentMode" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/plugin_context.ts:40:9 - (ae-forgotten-export) The symbol "ConfigWithSchema" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/plugins_service.ts:34:17 - (ae-forgotten-export) The symbol "DiscoveredPluginInternal" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) From d156d30080c1a01d4b2e63816d22c1313fda7459 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Wed, 10 Apr 2019 15:48:13 +0200 Subject: [PATCH 18/35] add integration tests for onRequest --- src/core/server/http/http_server.test.ts | 8 +++ .../plugins/dummy_on_request/kibana.json | 8 +++ .../plugins/dummy_on_request/server/index.ts | 20 +++++++ .../plugins/dummy_on_request/server/plugin.ts | 44 +++++++++++++++ .../plugins/dummy_security/server/plugin.ts | 3 +- .../integration_tests/http_service.test.ts | 53 +++++++++++++++++-- .../server/http/lifecycle/on_request.test.ts | 6 +-- src/core/server/http/router/request.ts | 2 + 8 files changed, 135 insertions(+), 9 deletions(-) create mode 100644 src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/kibana.json create mode 100644 src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/server/index.ts create mode 100644 src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/server/plugin.ts diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index bbbf449d54ffb..b9e1e6c5c3005 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -582,3 +582,11 @@ test('registers auth request interceptor only once', async () => { await doRegister(); expect(doRegister()).rejects.toThrowError('Auth interceptor was already registered'); }); + +test('registers onRequest interceptor several times', async () => { + const { registerOnRequest } = await server.start(config); + const doRegister = () => registerOnRequest(() => null as any); + + doRegister(); + expect(doRegister).not.toThrowError(); +}); diff --git a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/kibana.json b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/kibana.json new file mode 100644 index 0000000000000..0b467238f1d9b --- /dev/null +++ b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/kibana.json @@ -0,0 +1,8 @@ + +{ + "id": "dummy-on-request", + "version": "0.0.1", + "kibanaVersion": "kibana", + "ui": false, + "server": true +} diff --git a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/server/index.ts b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/server/index.ts new file mode 100644 index 0000000000000..9730472c8f84c --- /dev/null +++ b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/server/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { DummyOnRequestPlugin } from './plugin'; +export const plugin = () => new DummyOnRequestPlugin(); diff --git a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/server/plugin.ts b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/server/plugin.ts new file mode 100644 index 0000000000000..2e75cbc32fe01 --- /dev/null +++ b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/server/plugin.ts @@ -0,0 +1,44 @@ +/* + * 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 { OnRequest, CoreSetup } from '../../../../../..'; + +export const url = { + root: '/', + redirect: '/redirect', + redirectTo: '/redirect-to', + failed: '/failed', +}; + +export class DummyOnRequestPlugin { + public setup(core: CoreSetup) { + const onRequest: OnRequest = (request, t) => { + if (request.path === url.redirect) { + return t.redirected(url.redirectTo); + } + if (request.path === url.failed) { + return t.rejected(new Error('unexpected error'), { statusCode: 400 }); + } + + return t.next(); + }; + + core.http.registerOnRequest(onRequest); + return {}; + } +} diff --git a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts index 3247c72cdd86e..d67eef3599420 100644 --- a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts +++ b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts @@ -32,6 +32,7 @@ interface Storage { export const url = { auth: '/auth', authRedirect: '/auth/redirect', + redirectTo: '/login', }; export const sessionDurationMs = 30; @@ -39,7 +40,7 @@ export class DummySecurityPlugin { public setup(core: CoreSetup) { const authenticate: Authenticate = async (request, sessionStorage, t) => { if (request.path === url.authRedirect) { - return t.redirected('/login'); + return t.redirected(url.redirectTo); } if (request.headers.authorization) { 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 70cdebf332f2b..d5bc6a5e9aa0b 100644 --- a/src/core/server/http/integration_tests/http_service.test.ts +++ b/src/core/server/http/integration_tests/http_service.test.ts @@ -21,7 +21,8 @@ import path from 'path'; import request from 'request'; import * as kbnTestServer from '../../../../test_utils/kbn_server'; import { Router } from '../router'; -import { url } from './__fixtures__/plugins/dummy_security/server/plugin'; +import { url as authUrl } from './__fixtures__/plugins/dummy_security/server/plugin'; +import { url as onReqUrl } from './__fixtures__/plugins/dummy_on_request/server/plugin'; describe('http service', () => { describe('setup contract', () => { @@ -39,7 +40,7 @@ describe('http service', () => { ); const router = new Router(''); - router.get({ path: url.auth, validate: false }, async (req, res) => + router.get({ path: authUrl.auth, validate: false }, async (req, res) => res.ok({ content: 'ok' }) ); // TODO fix me when registerRouter is available before HTTP server is run @@ -52,7 +53,7 @@ describe('http service', () => { it('Should support implementing custom authentication logic', async () => { const response = await kbnTestServer.request - .get(root, url.auth) + .get(root, authUrl.auth) .expect(200, { content: 'ok' }); expect(response.header['set-cookie']).toBeDefined(); @@ -72,13 +73,14 @@ describe('http service', () => { it('Should support rejecting a request from an unauthenticated user', async () => { await kbnTestServer.request - .get(root, url.auth) + .get(root, authUrl.auth) .unset('Authorization') .expect(401); }); it('Should support redirecting', async () => { - await kbnTestServer.request.get(root, url.authRedirect).expect(302); + const response = await kbnTestServer.request.get(root, authUrl.authRedirect).expect(302); + expect(response.header.location).toBe(authUrl.redirectTo); }); it('Should run auth for legacy routes and proxy request to legacy server route handlers', async () => { @@ -97,5 +99,46 @@ describe('http service', () => { expect(response.header['set-cookie']).toBe(undefined); }); }); + + describe('#registerOnRequest()', () => { + const dummyOnRequestPlugin = path.resolve( + __dirname, + './__fixtures__/plugins/dummy_on_request' + ); + let root: ReturnType; + beforeAll(async () => { + root = kbnTestServer.createRoot( + { + plugins: { paths: [dummyOnRequestPlugin] }, + }, + { + dev: true, + } + ); + + const router = new Router(''); + router.get({ path: onReqUrl.root, validate: false }, async (req, res) => + res.ok({ content: 'ok' }) + ); + // TODO fix me when registerRouter is available before HTTP server is run + (root as any).server.http.registerRouter(router); + + await root.setup(); + }, 30000); + + afterAll(async () => await root.shutdown()); + it('Should support passing request through to the route handler', async () => { + await kbnTestServer.request.get(root, onReqUrl.root).expect(200, { content: 'ok' }); + }); + it('Should support redirecting to configured url', async () => { + const response = await kbnTestServer.request.get(root, onReqUrl.redirect).expect(302); + expect(response.header.location).toBe(onReqUrl.redirectTo); + }); + it('Should failing a request with configured error and status code', async () => { + await kbnTestServer.request + .get(root, onReqUrl.failed) + .expect(400, { statusCode: 400, error: 'Bad Request', message: 'unexpected error' }); + }); + }); }); }); diff --git a/src/core/server/http/lifecycle/on_request.test.ts b/src/core/server/http/lifecycle/on_request.test.ts index 3cfe4456d4d77..2a5fb0721d3a9 100644 --- a/src/core/server/http/lifecycle/on_request.test.ts +++ b/src/core/server/http/lifecycle/on_request.test.ts @@ -24,7 +24,7 @@ const requestMock = {} as any; const createResponseToolkit = (customization = {}): any => ({ ...customization }); describe('adoptToHapiOnRequestFormat', () => { - it('Should allow to continue request', async () => { + it('Should allow passing request to the next handler', async () => { const continueSymbol = {}; const onRequest = adoptToHapiOnRequestFormat((req, t) => t.next()); const result = await onRequest( @@ -37,7 +37,7 @@ describe('adoptToHapiOnRequestFormat', () => { expect(result).toBe(continueSymbol); }); - it('Should allow to redirect to specified url', async () => { + it('Should support redirecting to specified url', async () => { const redirectUrl = '/docs'; const onRequest = adoptToHapiOnRequestFormat((req, t) => t.redirected(redirectUrl)); const takeoverSymbol = {}; @@ -53,7 +53,7 @@ describe('adoptToHapiOnRequestFormat', () => { expect(result).toBe(takeoverSymbol); }); - it('Should allow to specify statusCode and message for Boom error', async () => { + it('Should support specifying statusCode and message for Boom error', async () => { const onRequest = adoptToHapiOnRequestFormat((req, t) => { return t.rejected(new Error('unexpected result'), { statusCode: 501 }); }); diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts index a98a346a21c46..69de94e5fc6da 100644 --- a/src/core/server/http/router/request.ts +++ b/src/core/server/http/router/request.ts @@ -69,9 +69,11 @@ export class KibanaRequest { } public readonly headers: Headers; + public readonly path: string; constructor(req: Request, readonly params: Params, readonly query: Query, readonly body: Body) { this.headers = req.headers; + this.path = req.path; } public getFilteredHeaders(headersToKeep: string[]) { From 87b2b0a4377a658b3788b3bc6f91e46687ae2597 Mon Sep 17 00:00:00 2001 From: restrry Date: Wed, 10 Apr 2019 15:49:26 +0200 Subject: [PATCH 19/35] update docs --- .../core/server/kibana-plugin-server.kibanarequest.md | 1 + .../server/kibana-plugin-server.kibanarequest.path.md | 9 +++++++++ src/core/server/kibana.api.md | 2 ++ 3 files changed, 12 insertions(+) create mode 100644 docs/development/core/server/kibana-plugin-server.kibanarequest.path.md diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.md index 160b0c712a332..835e34c9e602a 100644 --- a/docs/development/core/server/kibana-plugin-server.kibanarequest.md +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.md @@ -16,6 +16,7 @@ export declare class KibanaRequest | [body](./kibana-plugin-server.kibanarequest.body.md) | | Body | | | [headers](./kibana-plugin-server.kibanarequest.headers.md) | | Headers | | | [params](./kibana-plugin-server.kibanarequest.params.md) | | Params | | +| [path](./kibana-plugin-server.kibanarequest.path.md) | | string | | | [query](./kibana-plugin-server.kibanarequest.query.md) | | Query | | ## Methods diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.path.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.path.md new file mode 100644 index 0000000000000..269fcfd4e4937 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.path.md @@ -0,0 +1,9 @@ +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [path](./kibana-plugin-server.kibanarequest.path.md) + +## KibanaRequest.path property + +Signature: + +```typescript +readonly path: string; +``` diff --git a/src/core/server/kibana.api.md b/src/core/server/kibana.api.md index d5488e4dd798e..3d17be0a38169 100644 --- a/src/core/server/kibana.api.md +++ b/src/core/server/kibana.api.md @@ -138,6 +138,8 @@ export class KibanaRequest { // (undocumented) readonly params: Params; // (undocumented) + readonly path: string; + // (undocumented) readonly query: Query; } From e74bdb76674b3c2710d3575e5e118ed16c8181f4 Mon Sep 17 00:00:00 2001 From: restrry Date: Wed, 10 Apr 2019 16:15:53 +0200 Subject: [PATCH 20/35] cleanup onRequest integration tests --- .../plugins/dummy_on_request/server/plugin.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/server/plugin.ts b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/server/plugin.ts index 2e75cbc32fe01..fc39ce56998e6 100644 --- a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/server/plugin.ts +++ b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/server/plugin.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { OnRequest, CoreSetup } from '../../../../../..'; +import { CoreSetup } from '../../../../../..'; export const url = { root: '/', @@ -27,18 +27,18 @@ export const url = { export class DummyOnRequestPlugin { public setup(core: CoreSetup) { - const onRequest: OnRequest = (request, t) => { + core.http.registerOnRequest((request, t) => { if (request.path === url.redirect) { return t.redirected(url.redirectTo); } + return t.next(); + }); + + core.http.registerOnRequest((request, t) => { if (request.path === url.failed) { return t.rejected(new Error('unexpected error'), { statusCode: 400 }); } - return t.next(); - }; - - core.http.registerOnRequest(onRequest); - return {}; + }); } } From 41cb8bb83676be1217a2fd1858991b41a8ad74cb Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Thu, 11 Apr 2019 09:41:31 +0200 Subject: [PATCH 21/35] Generate docs for AuthToolkit & OnRequestToolkit --- .../kibana-plugin-server.authenticate.md | 2 +- ...plugin-server.authtoolkit.authenticated.md | 11 ++++++++ .../kibana-plugin-server.authtoolkit.md | 20 +++++++++++++ ...na-plugin-server.authtoolkit.redirected.md | 11 ++++++++ ...bana-plugin-server.authtoolkit.rejected.md | 13 +++++++++ .../core/server/kibana-plugin-server.md | 2 ++ .../server/kibana-plugin-server.onrequest.md | 2 +- .../kibana-plugin-server.onrequesttoolkit.md | 20 +++++++++++++ ...ana-plugin-server.onrequesttoolkit.next.md | 11 ++++++++ ...ugin-server.onrequesttoolkit.redirected.md | 11 ++++++++ ...plugin-server.onrequesttoolkit.rejected.md | 13 +++++++++ src/core/server/http/index.ts | 4 +-- src/core/server/http/lifecycle/auth.ts | 28 +++++++++++++------ src/core/server/http/lifecycle/on_request.ts | 25 ++++++++++++----- src/core/server/index.ts | 20 ++++++------- src/core/server/kibana.api.md | 24 +++++++++++++--- 16 files changed, 184 insertions(+), 33 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-server.authtoolkit.authenticated.md create mode 100644 docs/development/core/server/kibana-plugin-server.authtoolkit.md create mode 100644 docs/development/core/server/kibana-plugin-server.authtoolkit.redirected.md create mode 100644 docs/development/core/server/kibana-plugin-server.authtoolkit.rejected.md create mode 100644 docs/development/core/server/kibana-plugin-server.onrequesttoolkit.md create mode 100644 docs/development/core/server/kibana-plugin-server.onrequesttoolkit.next.md create mode 100644 docs/development/core/server/kibana-plugin-server.onrequesttoolkit.redirected.md create mode 100644 docs/development/core/server/kibana-plugin-server.onrequesttoolkit.rejected.md diff --git a/docs/development/core/server/kibana-plugin-server.authenticate.md b/docs/development/core/server/kibana-plugin-server.authenticate.md index f1c01b23dca71..d6a91170c69be 100644 --- a/docs/development/core/server/kibana-plugin-server.authenticate.md +++ b/docs/development/core/server/kibana-plugin-server.authenticate.md @@ -6,5 +6,5 @@ Signature: ```typescript -export declare type Authenticate = (request: Request, sessionStorage: SessionStorage, t: typeof toolkit) => Promise; +export declare type Authenticate = (request: Request, sessionStorage: SessionStorage, t: AuthToolkit) => 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 new file mode 100644 index 0000000000000..cec5cf405924c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.authtoolkit.authenticated.md @@ -0,0 +1,11 @@ +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthToolkit](./kibana-plugin-server.authtoolkit.md) > [authenticated](./kibana-plugin-server.authtoolkit.authenticated.md) + +## AuthToolkit.authenticated property + +Authentication is successful with given credentials, allow request to pass through + +Signature: + +```typescript +authenticated: (credentials: any) => AuthResult; +``` diff --git a/docs/development/core/server/kibana-plugin-server.authtoolkit.md b/docs/development/core/server/kibana-plugin-server.authtoolkit.md new file mode 100644 index 0000000000000..60590ee448e57 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.authtoolkit.md @@ -0,0 +1,20 @@ +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthToolkit](./kibana-plugin-server.authtoolkit.md) + +## AuthToolkit interface + +A tool set defining an outcome of Auth interceptor for incoming request. + +Signature: + +```typescript +export interface AuthToolkit +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [authenticated](./kibana-plugin-server.authtoolkit.authenticated.md) | (credentials: any) => 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.authtoolkit.redirected.md b/docs/development/core/server/kibana-plugin-server.authtoolkit.redirected.md new file mode 100644 index 0000000000000..1ebe9a8549ff3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.authtoolkit.redirected.md @@ -0,0 +1,11 @@ +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthToolkit](./kibana-plugin-server.authtoolkit.md) > [redirected](./kibana-plugin-server.authtoolkit.redirected.md) + +## AuthToolkit.redirected property + +Authentication requires to interrupt request handling and redirect to a configured url + +Signature: + +```typescript +redirected: (url: string) => AuthResult; +``` diff --git a/docs/development/core/server/kibana-plugin-server.authtoolkit.rejected.md b/docs/development/core/server/kibana-plugin-server.authtoolkit.rejected.md new file mode 100644 index 0000000000000..dffa66531c37d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.authtoolkit.rejected.md @@ -0,0 +1,13 @@ +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthToolkit](./kibana-plugin-server.authtoolkit.md) > [rejected](./kibana-plugin-server.authtoolkit.rejected.md) + +## AuthToolkit.rejected property + +Authentication is unsuccessful, fail the request with specified error. + +Signature: + +```typescript +rejected: (error: Error, options?: { + statusCode?: number; + }) => AuthResult; +``` diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index ed7cf36de82be..c2d234b10abba 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -16,12 +16,14 @@ | Interface | Description | | --- | --- | +| [AuthToolkit](./kibana-plugin-server.authtoolkit.md) | A tool set defining an outcome of Auth interceptor for incoming request. | | [CallAPIOptions](./kibana-plugin-server.callapioptions.md) | The set of options that defines how API call should be made and result be processed. | | [CoreSetup](./kibana-plugin-server.coresetup.md) | | | [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) | | | [Logger](./kibana-plugin-server.logger.md) | Logger exposes all the necessary methods to log any type of information and this is the interface used by the logging consumers including plugins. | | [LoggerFactory](./kibana-plugin-server.loggerfactory.md) | The single purpose of 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. | | [PluginInitializerContext](./kibana-plugin-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. | | [PluginSetupContext](./kibana-plugin-server.pluginsetupcontext.md) | Context passed to the plugins setup method. | diff --git a/docs/development/core/server/kibana-plugin-server.onrequest.md b/docs/development/core/server/kibana-plugin-server.onrequest.md index d485d3dfd34a9..f3c557f1a1019 100644 --- a/docs/development/core/server/kibana-plugin-server.onrequest.md +++ b/docs/development/core/server/kibana-plugin-server.onrequest.md @@ -6,5 +6,5 @@ Signature: ```typescript -export declare type OnRequest = (req: KibanaRequest, t: typeof toolkit) => OnRequestResult; +export declare type OnRequest = (req: KibanaRequest, t: OnRequestToolkit) => OnRequestResult; ``` diff --git a/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.md b/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.md new file mode 100644 index 0000000000000..5fdbebbf37263 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.md @@ -0,0 +1,20 @@ +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnRequestToolkit](./kibana-plugin-server.onrequesttoolkit.md) + +## OnRequestToolkit interface + +A tool set defining an outcome of OnRequest interceptor for incoming request. + +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. | + diff --git a/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.next.md b/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.next.md new file mode 100644 index 0000000000000..4a6aef813a566 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.next.md @@ -0,0 +1,11 @@ +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnRequestToolkit](./kibana-plugin-server.onrequesttoolkit.md) > [next](./kibana-plugin-server.onrequesttoolkit.next.md) + +## OnRequestToolkit.next property + +To pass request to the next handler + +Signature: + +```typescript +next: () => OnRequestResult; +``` diff --git a/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.redirected.md b/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.redirected.md new file mode 100644 index 0000000000000..d2968d7b9c497 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.redirected.md @@ -0,0 +1,11 @@ +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnRequestToolkit](./kibana-plugin-server.onrequesttoolkit.md) > [redirected](./kibana-plugin-server.onrequesttoolkit.redirected.md) + +## OnRequestToolkit.redirected property + +To interrupt request handling and redirect to a configured url + +Signature: + +```typescript +redirected: (url: string) => OnRequestResult; +``` diff --git a/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.rejected.md b/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.rejected.md new file mode 100644 index 0000000000000..c89c6ae5465d7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onrequesttoolkit.rejected.md @@ -0,0 +1,13 @@ +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnRequestToolkit](./kibana-plugin-server.onrequesttoolkit.md) > [rejected](./kibana-plugin-server.onrequesttoolkit.rejected.md) + +## OnRequestToolkit.rejected property + +Fail the request with specified error. + +Signature: + +```typescript +rejected: (error: Error, options?: { + statusCode?: number; + }) => OnRequestResult; +``` diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index faa2555c0bed7..b56c6b9af55b2 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -22,5 +22,5 @@ export { HttpService, HttpServiceSetup } from './http_service'; export { Router, KibanaRequest } from './router'; export { HttpServerInfo } from './http_server'; export { BasePathProxyServer } from './base_path_proxy_server'; -export { Authenticate } from './lifecycle/auth'; -export { OnRequest } from './lifecycle/on_request'; +export { Authenticate, AuthToolkit } from './lifecycle/auth'; +export { OnRequest, OnRequestToolkit } from './lifecycle/on_request'; diff --git a/src/core/server/http/lifecycle/auth.ts b/src/core/server/http/lifecycle/auth.ts index e2022b5168523..828ad70a96c07 100644 --- a/src/core/server/http/lifecycle/auth.ts +++ b/src/core/server/http/lifecycle/auth.ts @@ -25,18 +25,17 @@ enum ResultType { redirected = 'redirected', rejected = 'rejected', } -interface ErrorParams { - statusCode?: number; -} + +/** @internal */ class AuthResult { - public static authenticated(credentials: any = {}) { + 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, { statusCode }: ErrorParams = {}) { - return new AuthResult(ResultType.rejected, { error, statusCode }); + 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; @@ -53,7 +52,20 @@ class AuthResult { } } -const toolkit = { +/** + * @public + * A tool set defining an outcome of Auth interceptor for incoming request. + */ +export interface AuthToolkit { + /** Authentication is successful with given credentials, allow request to pass through */ + authenticated: (credentials: any) => AuthResult; + /** Authentication requires to interrupt request handling and redirect to a configured url */ + redirected: (url: string) => AuthResult; + /** Authentication is unsuccessful, fail the request with specified error. */ + rejected: (error: Error, options?: { statusCode?: number }) => AuthResult; +} + +const toolkit: AuthToolkit = { authenticated: AuthResult.authenticated, redirected: AuthResult.redirected, rejected: AuthResult.rejected, @@ -63,7 +75,7 @@ const toolkit = { export type Authenticate = ( request: Request, sessionStorage: SessionStorage, - t: typeof toolkit + t: AuthToolkit ) => Promise; /** @public */ diff --git a/src/core/server/http/lifecycle/on_request.ts b/src/core/server/http/lifecycle/on_request.ts index 27a66da9ba92c..4587431c228f1 100644 --- a/src/core/server/http/lifecycle/on_request.ts +++ b/src/core/server/http/lifecycle/on_request.ts @@ -27,9 +27,7 @@ enum ResultType { rejected = 'rejected', } -interface ErrorParams { - statusCode?: number; -} +/** @internal */ class OnRequestResult { public static next() { return new OnRequestResult(ResultType.next); @@ -37,8 +35,8 @@ class OnRequestResult { public static redirected(url: string) { return new OnRequestResult(ResultType.redirected, url); } - public static rejected(error: Error, { statusCode }: ErrorParams = {}) { - return new OnRequestResult(ResultType.rejected, { error, statusCode }); + 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; @@ -55,7 +53,20 @@ class OnRequestResult { } } -const toolkit = { +/** + * @public + * A tool set defining an outcome of OnRequest interceptor for incoming request. + */ +export interface OnRequestToolkit { + /** To pass request to the next handler */ + next: () => OnRequestResult; + /** To interrupt request handling and redirect to a configured url */ + redirected: (url: string) => OnRequestResult; + /** Fail the request with specified error. */ + rejected: (error: Error, options?: { statusCode?: number }) => OnRequestResult; +} + +const toolkit: OnRequestToolkit = { next: OnRequestResult.next, redirected: OnRequestResult.redirected, rejected: OnRequestResult.rejected, @@ -64,7 +75,7 @@ const toolkit = { /** @public */ export type OnRequest = ( req: KibanaRequest, - t: typeof toolkit + t: OnRequestToolkit ) => OnRequestResult; /** diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 6da96cf6e2ff7..7a1a50921738d 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -17,7 +17,7 @@ * under the License. */ import { ElasticsearchServiceSetup } from './elasticsearch'; -import { Authenticate, HttpServiceSetup, KibanaRequest, OnRequest, Router } from './http'; +import { HttpServiceSetup } from './http'; import { PluginsServiceSetup } from './plugins'; export { bootstrap } from './bootstrap'; @@ -30,6 +30,14 @@ export { ElasticsearchClientConfig, APICaller, } from './elasticsearch'; +export { + Authenticate, + AuthToolkit, + KibanaRequest, + OnRequest, + OnRequestToolkit, + Router, +} from './http'; export { Logger, LoggerFactory, LogMeta, LogRecord, LogLevel } from './logging'; export { DiscoveredPlugin, @@ -45,12 +53,4 @@ export interface CoreSetup { plugins: PluginsServiceSetup; } -export { - Router, - Authenticate, - OnRequest, - KibanaRequest, - ElasticsearchServiceSetup, - HttpServiceSetup, - PluginsServiceSetup, -}; +export { HttpServiceSetup, ElasticsearchServiceSetup, PluginsServiceSetup }; diff --git a/src/core/server/kibana.api.md b/src/core/server/kibana.api.md index 3d17be0a38169..057722325bf76 100644 --- a/src/core/server/kibana.api.md +++ b/src/core/server/kibana.api.md @@ -21,11 +21,19 @@ import { TypeOf } from '@kbn/config-schema'; export type APICaller = (endpoint: string, clientParams: Record, options?: CallAPIOptions) => Promise; // Warning: (ae-forgotten-export) The symbol "SessionStorage" needs to be exported by the entry point index.d.ts -// Warning: (ae-forgotten-export) The symbol "toolkit" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "AuthResult" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export type Authenticate = (request: Request, sessionStorage: SessionStorage, t: typeof toolkit) => Promise; +export type Authenticate = (request: Request, sessionStorage: SessionStorage, t: AuthToolkit) => Promise; + +// @public +export interface AuthToolkit { + authenticated: (credentials: any) => AuthResult; + redirected: (url: string) => AuthResult; + rejected: (error: Error, options?: { + statusCode?: number; + }) => AuthResult; +} // Warning: (ae-forgotten-export) The symbol "BootstrapArgs" needs to be exported by the entry point index.d.ts // Warning: (ae-internal-missing-underscore) The name bootstrap should be prefixed with an underscore because the declaration is marked as "@internal" @@ -216,11 +224,19 @@ export interface LogRecord { timestamp: Date; } -// Warning: (ae-forgotten-export) The symbol "toolkit" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "OnRequestResult" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export type OnRequest = (req: KibanaRequest, t: typeof toolkit_2) => OnRequestResult; +export type OnRequest = (req: KibanaRequest, t: OnRequestToolkit) => OnRequestResult; + +// @public +export interface OnRequestToolkit { + next: () => OnRequestResult; + redirected: (url: string) => OnRequestResult; + rejected: (error: Error, options?: { + statusCode?: number; + }) => OnRequestResult; +} // @public export interface PluginInitializerContext { From fadf00b55cb8f93412a496632116cdc73a62b07c Mon Sep 17 00:00:00 2001 From: restrry Date: Thu, 11 Apr 2019 10:03:13 +0200 Subject: [PATCH 22/35] add test for an exception in interceptor --- .../plugins/dummy_on_request/server/plugin.ts | 10 +++++++++- .../plugins/dummy_security/server/plugin.ts | 5 +++++ .../http/integration_tests/http_service.test.ts | 15 +++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/server/plugin.ts b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/server/plugin.ts index fc39ce56998e6..0b5e9e9d913b2 100644 --- a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/server/plugin.ts +++ b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/server/plugin.ts @@ -19,10 +19,11 @@ import { CoreSetup } from '../../../../../..'; export const url = { + exception: '/exception', + failed: '/failed', root: '/', redirect: '/redirect', redirectTo: '/redirect-to', - failed: '/failed', }; export class DummyOnRequestPlugin { @@ -40,5 +41,12 @@ export class DummyOnRequestPlugin { } return t.next(); }); + + core.http.registerOnRequest((request, t) => { + if (request.path === url.exception) { + throw new Error('sensitive info'); + } + return t.next(); + }); } } diff --git a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts index d67eef3599420..cb9fe696f331c 100644 --- a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts +++ b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts @@ -32,6 +32,7 @@ interface Storage { export const url = { auth: '/auth', authRedirect: '/auth/redirect', + exception: '/exception', redirectTo: '/login', }; @@ -43,6 +44,10 @@ export class DummySecurityPlugin { return t.redirected(url.redirectTo); } + if (request.path === url.exception) { + throw new Error('sensitive info'); + } + if (request.headers.authorization) { const user = { id: '42' }; sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs }); 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 d5bc6a5e9aa0b..21884418e1ebf 100644 --- a/src/core/server/http/integration_tests/http_service.test.ts +++ b/src/core/server/http/integration_tests/http_service.test.ts @@ -98,6 +98,14 @@ describe('http service', () => { expect(response.header['set-cookie']).toBe(undefined); }); + + it(`Shouldn't expose internal error details`, async () => { + await kbnTestServer.request.get(root, authUrl.exception).expect({ + statusCode: 500, + error: 'Internal Server Error', + message: 'An internal server error occurred', + }); + }); }); describe('#registerOnRequest()', () => { @@ -139,6 +147,13 @@ describe('http service', () => { .get(root, onReqUrl.failed) .expect(400, { statusCode: 400, error: 'Bad Request', message: 'unexpected error' }); }); + it(`Shouldn't expose internal error details`, async () => { + await kbnTestServer.request.get(root, onReqUrl.exception).expect({ + statusCode: 500, + error: 'Internal Server Error', + message: 'An internal server error occurred', + }); + }); }); }); }); From f48e56fcb9fdc5044960f765f10629d674dcfbac Mon Sep 17 00:00:00 2001 From: restrry Date: Thu, 11 Apr 2019 10:40:41 +0200 Subject: [PATCH 23/35] add test OnRequest interceptors dont share request object --- .../plugins/dummy_on_request/server/plugin.ts | 19 +++++++++++++++++++ .../integration_tests/http_service.test.ts | 8 ++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/server/plugin.ts b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/server/plugin.ts index 0b5e9e9d913b2..181a408c4ea07 100644 --- a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/server/plugin.ts +++ b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/server/plugin.ts @@ -21,6 +21,7 @@ import { CoreSetup } from '../../../../../..'; export const url = { exception: '/exception', failed: '/failed', + independentReq: '/independent-request', root: '/', redirect: '/redirect', redirectTo: '/redirect-to', @@ -48,5 +49,23 @@ export class DummyOnRequestPlugin { } return t.next(); }); + + core.http.registerOnRequest((request, t) => { + if (request.path === url.independentReq) { + // @ts-ignore + request.customField = { value: 42 }; + } + return t.next(); + }); + + core.http.registerOnRequest((request, t) => { + if (request.path === url.independentReq) { + // @ts-ignore + if (typeof request.customField !== 'undefined') { + throw new Error('Request object was mutated'); + } + } + return t.next(); + }); } } 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 21884418e1ebf..199e103a3ed92 100644 --- a/src/core/server/http/integration_tests/http_service.test.ts +++ b/src/core/server/http/integration_tests/http_service.test.ts @@ -125,8 +125,9 @@ describe('http service', () => { ); const router = new Router(''); - router.get({ path: onReqUrl.root, validate: false }, async (req, res) => - res.ok({ content: 'ok' }) + // routes with expected success status response should have handlers + [onReqUrl.root, onReqUrl.independentReq].forEach(url => + router.get({ path: url, validate: false }, async (req, res) => res.ok({ content: 'ok' })) ); // TODO fix me when registerRouter is available before HTTP server is run (root as any).server.http.registerRouter(router); @@ -154,6 +155,9 @@ describe('http service', () => { message: 'An internal server error occurred', }); }); + it(`Shouldn't share request object between interceptors`, async () => { + await kbnTestServer.request.get(root, onReqUrl.independentReq).expect(200); + }); }); }); }); From c043c297ce6634881a886b0b685da5fdbe747ef0 Mon Sep 17 00:00:00 2001 From: restrry Date: Thu, 11 Apr 2019 11:24:48 +0200 Subject: [PATCH 24/35] cleanup --- src/core/server/http/lifecycle/auth.test.ts | 4 ++-- src/core/server/http/lifecycle/auth.ts | 2 +- src/core/server/http/lifecycle/on_request.test.ts | 4 ++-- src/core/server/http/lifecycle/on_request.ts | 7 ++----- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/core/server/http/lifecycle/auth.test.ts b/src/core/server/http/lifecycle/auth.test.ts index 173e2495f371f..b8c0c7c5d1d50 100644 --- a/src/core/server/http/lifecycle/auth.test.ts +++ b/src/core/server/http/lifecycle/auth.test.ts @@ -76,7 +76,7 @@ describe('adoptToHapiAuthFormat', () => { expect(result.output.statusCode).toBe(404); }); - it('Should return Boom error if interceptor throws', async () => { + it('Should return Boom.internal error error if interceptor throws', async () => { const onAuth = adoptToHapiAuthFormat(async (req, sessionStorage, t) => { throw new Error('unknown error'); }, SessionStorageMock); @@ -87,7 +87,7 @@ describe('adoptToHapiAuthFormat', () => { expect(result.output.statusCode).toBe(500); }); - it('Should return Boom error if interceptor returns unexpected result', async () => { + it('Should return Boom.internal error if interceptor returns unexpected result', async () => { const onAuth = adoptToHapiAuthFormat( async (req, sessionStorage, t) => undefined as any, SessionStorageMock diff --git a/src/core/server/http/lifecycle/auth.ts b/src/core/server/http/lifecycle/auth.ts index 828ad70a96c07..c242c90e1d893 100644 --- a/src/core/server/http/lifecycle/auth.ts +++ b/src/core/server/http/lifecycle/auth.ts @@ -106,7 +106,7 @@ export function adoptToHapiAuthFormat( `Unexpected result from Authenticate. Expected AuthResult, but given: ${result}.` ); } catch (error) { - return new Boom(error.message, { statusCode: 500 }); + 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_request.test.ts index 2a5fb0721d3a9..bc4410c773288 100644 --- a/src/core/server/http/lifecycle/on_request.test.ts +++ b/src/core/server/http/lifecycle/on_request.test.ts @@ -64,7 +64,7 @@ describe('adoptToHapiOnRequestFormat', () => { expect(result.output.statusCode).toBe(501); }); - it('Should return Boom error if interceptor throws', async () => { + it('Should return Boom.internal error if interceptor throws', async () => { const onRequest = adoptToHapiOnRequestFormat((req, t) => { throw new Error('unknown error'); }); @@ -75,7 +75,7 @@ describe('adoptToHapiOnRequestFormat', () => { expect(result.output.statusCode).toBe(500); }); - it('Should return Boom error if interceptor returns unexpected result', async () => { + 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; diff --git a/src/core/server/http/lifecycle/on_request.ts b/src/core/server/http/lifecycle/on_request.ts index 4587431c228f1..31d8c39219439 100644 --- a/src/core/server/http/lifecycle/on_request.ts +++ b/src/core/server/http/lifecycle/on_request.ts @@ -82,10 +82,7 @@ export type OnRequest = ( * @public * Adopt custom request interceptor to Hapi lifecycle system. * @param fn - an extension point allowing to perform custom logic for - * incoming HTTP requests. Should finish with one of the following commands: - * - t.next(). to pass a request to the next handler. - * - t.redirected(url). to interrupt request handling and redirect to configured url. - * - t.rejected(error). to fail the request with specified error. + * incoming HTTP requests. */ export function adoptToHapiOnRequestFormat(fn: OnRequest) { return async function interceptRequest( @@ -111,7 +108,7 @@ export function adoptToHapiOnRequestFormat(fn: OnRequest) { `Unexpected result from OnRequest. Expected OnRequestResult, but given: ${result}.` ); } catch (error) { - return new Boom(error.message, { statusCode: 500 }); + return Boom.internal(error.message, { statusCode: 500 }); } }; } From 1c5bd1340531398372276ed6fa505f5f4a382fc9 Mon Sep 17 00:00:00 2001 From: restrry Date: Fri, 12 Apr 2019 09:02:41 +0200 Subject: [PATCH 25/35] address comments from @eli --- src/core/server/http/cookie_sesson_storage.test.ts | 2 +- .../plugins/dummy_on_request/kibana.json | 1 - .../plugins/dummy_on_request/server/plugin.ts | 13 +++++++------ .../__fixtures__/plugins/dummy_security/kibana.json | 1 - src/legacy/server/kbn_server.d.ts | 2 +- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/core/server/http/cookie_sesson_storage.test.ts b/src/core/server/http/cookie_sesson_storage.test.ts index 1c54703690de6..f433bf93aa432 100644 --- a/src/core/server/http/cookie_sesson_storage.test.ts +++ b/src/core/server/http/cookie_sesson_storage.test.ts @@ -59,7 +59,7 @@ describe('Cookie based SessionStorage', () => { method: 'GET', path: '/set', options: { - handler: async (req, h) => { + handler: (req, h) => { const sessionStorage = factory.asScoped(req); sessionStorage.set({ value: userData, expires: Date.now() + sessionDurationMs }); return h.response(); diff --git a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/kibana.json b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/kibana.json index 0b467238f1d9b..0499e47abf9c3 100644 --- a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/kibana.json +++ b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/kibana.json @@ -1,4 +1,3 @@ - { "id": "dummy-on-request", "version": "0.0.1", diff --git a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/server/plugin.ts b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/server/plugin.ts index 181a408c4ea07..605247ea93904 100644 --- a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/server/plugin.ts +++ b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/server/plugin.ts @@ -52,18 +52,19 @@ export class DummyOnRequestPlugin { core.http.registerOnRequest((request, t) => { if (request.path === url.independentReq) { - // @ts-ignore + // @ts-ignore. don't complain customField is not defined on Request type request.customField = { value: 42 }; } return t.next(); }); core.http.registerOnRequest((request, t) => { - if (request.path === url.independentReq) { - // @ts-ignore - if (typeof request.customField !== 'undefined') { - throw new Error('Request object was mutated'); - } + if ( + request.path === url.independentReq && + // @ts-ignore don't complain customField is not defined on Request type + typeof request.customField !== 'undefined' + ) { + throw new Error('Request object was mutated'); } return t.next(); }); diff --git a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/kibana.json b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/kibana.json index 3f5362a5bfa75..b6e84959322a9 100644 --- a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/kibana.json +++ b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/kibana.json @@ -1,4 +1,3 @@ - { "id": "dummy-security", "version": "0.0.1", diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index 8f5a18a4da875..75783d1cbd1ee 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -24,7 +24,7 @@ import { HttpServiceSetup, ConfigService, PluginsServiceSetup, -} from '../../core/server/'; +} from '../../core/server'; import { ApmOssPlugin } from '../core_plugins/apm_oss'; import { CallClusterWithRequest, ElasticsearchPlugin } from '../core_plugins/elasticsearch'; From 351f8c5d00c6ddf6e2457a582ab447ce92e901c5 Mon Sep 17 00:00:00 2001 From: restrry Date: Fri, 12 Apr 2019 11:55:50 +0200 Subject: [PATCH 26/35] improve typings for onRequest --- .../__fixtures__/plugins/dummy_on_request/server/plugin.ts | 3 ++- src/core/server/http/lifecycle/on_request.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/server/plugin.ts b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/server/plugin.ts index 605247ea93904..42db041363ea0 100644 --- a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/server/plugin.ts +++ b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_on_request/server/plugin.ts @@ -29,7 +29,8 @@ export const url = { export class DummyOnRequestPlugin { public setup(core: CoreSetup) { - core.http.registerOnRequest((request, t) => { + core.http.registerOnRequest(async (request, t) => { + await Promise.resolve(); if (request.path === url.redirect) { return t.redirected(url.redirectTo); } diff --git a/src/core/server/http/lifecycle/on_request.ts b/src/core/server/http/lifecycle/on_request.ts index 31d8c39219439..5fdc2e2203351 100644 --- a/src/core/server/http/lifecycle/on_request.ts +++ b/src/core/server/http/lifecycle/on_request.ts @@ -76,7 +76,7 @@ const toolkit: OnRequestToolkit = { export type OnRequest = ( req: KibanaRequest, t: OnRequestToolkit -) => OnRequestResult; +) => OnRequestResult | Promise; /** * @public From 1ce33c7640a7135427a0a5cf543151e917e55312 Mon Sep 17 00:00:00 2001 From: restrry Date: Fri, 12 Apr 2019 14:08:40 +0200 Subject: [PATCH 27/35] improve plugin typings --- src/core/server/plugins/plugin_context.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 07c4e676d3204..e0dc3d11baf1a 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -55,7 +55,7 @@ export interface PluginSetupContext { adminClient$: Observable; dataClient$: Observable; }; - http?: { + http: { registerAuth: HttpServiceSetup['registerAuth']; registerOnRequest: HttpServiceSetup['registerOnRequest']; }; @@ -114,6 +114,12 @@ export function createPluginInitializerContext( }; } +// Added to improve http typings as make { http: Required } +// Http service is disabled, when Kibana runs in optimizer mode or as dev cluster managed by cluster master. +// In theory no plugins shouldn't try to access http dependency in this case. +function preventAccess() { + throw new Error('Cannot use http contract when http server not started'); +} /** * This returns a facade for `CoreContext` that will be exposed to the plugin `setup` method. * This facade should be safe to use only within `setup` itself. @@ -143,6 +149,9 @@ export function createPluginSetupContext( registerAuth: deps.http.registerAuth, registerOnRequest: deps.http.registerOnRequest, } - : undefined, + : { + registerAuth: preventAccess, + registerOnRequest: preventAccess, + }, }; } From 85cfd4d13934f8b92c016dee666134ca2b7a55d0 Mon Sep 17 00:00:00 2001 From: restrry Date: Fri, 12 Apr 2019 14:11:42 +0200 Subject: [PATCH 28/35] re-generate docs --- .../development/core/server/kibana-plugin-server.onrequest.md | 2 +- .../server/kibana-plugin-server.pluginsetupcontext.http.md | 2 +- src/core/server/kibana.api.md | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/development/core/server/kibana-plugin-server.onrequest.md b/docs/development/core/server/kibana-plugin-server.onrequest.md index f3c557f1a1019..e252aa0c7c2e4 100644 --- a/docs/development/core/server/kibana-plugin-server.onrequest.md +++ b/docs/development/core/server/kibana-plugin-server.onrequest.md @@ -6,5 +6,5 @@ Signature: ```typescript -export declare type OnRequest = (req: KibanaRequest, t: OnRequestToolkit) => OnRequestResult; +export declare type OnRequest = (req: KibanaRequest, t: OnRequestToolkit) => OnRequestResult | Promise; ``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginsetupcontext.http.md b/docs/development/core/server/kibana-plugin-server.pluginsetupcontext.http.md index d8fc74708f118..d66356117ce24 100644 --- a/docs/development/core/server/kibana-plugin-server.pluginsetupcontext.http.md +++ b/docs/development/core/server/kibana-plugin-server.pluginsetupcontext.http.md @@ -5,7 +5,7 @@ Signature: ```typescript -http?: { +http: { registerAuth: HttpServiceSetup['registerAuth']; registerOnRequest: HttpServiceSetup['registerOnRequest']; }; diff --git a/src/core/server/kibana.api.md b/src/core/server/kibana.api.md index 057722325bf76..7ebc0ccacb86f 100644 --- a/src/core/server/kibana.api.md +++ b/src/core/server/kibana.api.md @@ -227,7 +227,7 @@ export interface LogRecord { // Warning: (ae-forgotten-export) The symbol "OnRequestResult" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export type OnRequest = (req: KibanaRequest, t: OnRequestToolkit) => OnRequestResult; +export type OnRequest = (req: KibanaRequest, t: OnRequestToolkit) => OnRequestResult | Promise; // @public export interface OnRequestToolkit { @@ -264,7 +264,7 @@ export interface PluginSetupContext { dataClient$: Observable; }; // (undocumented) - http?: { + http: { registerAuth: HttpServiceSetup['registerAuth']; registerOnRequest: HttpServiceSetup['registerOnRequest']; }; From 3f500d5f65cc06edb06c86064d9c0b5f4d5f7f96 Mon Sep 17 00:00:00 2001 From: restrry Date: Fri, 12 Apr 2019 16:27:24 +0200 Subject: [PATCH 29/35] only server defines cookie path --- .../server/http/cookie_session_storage.ts | 6 ++--- src/core/server/http/http_server.ts | 27 ++++++++++++------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/core/server/http/cookie_session_storage.ts b/src/core/server/http/cookie_session_storage.ts index 8c11fe02916e7..3b90ba8275e6a 100644 --- a/src/core/server/http/cookie_session_storage.ts +++ b/src/core/server/http/cookie_session_storage.ts @@ -26,7 +26,6 @@ export interface CookieOptions { password: string; validate: (sessionValue: T) => boolean | Promise; isSecure: boolean; - path?: string; } class ScopedCookieSessionStorage> implements SessionStorage { @@ -55,7 +54,8 @@ class ScopedCookieSessionStorage> implements Sessi */ export async function createCookieSessionStorageFactory( server: Server, - cookieOptions: CookieOptions + cookieOptions: CookieOptions, + basePath?: string ): Promise> { await server.register({ plugin: hapiAuthCookie }); @@ -64,7 +64,7 @@ export async function createCookieSessionStorageFactory( password: cookieOptions.password, validateFunc: async (req, session: T) => ({ valid: await cookieOptions.validate(session) }), isSecure: cookieOptions.isSecure, - path: cookieOptions.path, + path: basePath, clearInvalid: true, isHttpOnly: true, isSameSite: false, diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index a11e03cb47fca..ff6467b88ca15 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -37,8 +37,8 @@ export interface HttpServerInfo { export class HttpServer { private server?: Server; - private registeredRouters: Set = new Set(); - private authRegistered: boolean = false; + private registeredRouters = new Set(); + private authRegistered = false; constructor(private readonly log: Logger) {} @@ -86,8 +86,9 @@ export class HttpServer { return { server: this.server, options: serverOptions, - registerOnRequest: this.registerOnRequest, - registerAuth: this.registerAuth, + registerOnRequest: this.registerOnRequest.bind(this), + registerAuth: (fn: Authenticate, cookieOptions: CookieOptions) => + this.registerAuth(fn, cookieOptions, config.basePath), }; } @@ -139,15 +140,19 @@ export class HttpServer { return `${routerPath}${routePath.slice(routePathStartIndex)}`; } - private registerOnRequest = (fn: OnRequest) => { + private registerOnRequest(fn: OnRequest) { if (this.server === undefined) { throw new Error('Server is not created yet'); } this.server.ext('onRequest', adoptToHapiOnRequestFormat(fn)); - }; + } - private registerAuth = async (fn: Authenticate, cookieOptions: CookieOptions) => { + private async registerAuth( + fn: Authenticate, + cookieOptions: CookieOptions, + basePath?: string + ) { if (this.server === undefined) { throw new Error('Server is not created yet'); } @@ -156,7 +161,11 @@ export class HttpServer { } this.authRegistered = true; - const sessionStorage = await createCookieSessionStorageFactory(this.server, cookieOptions); + const sessionStorage = await createCookieSessionStorageFactory( + this.server, + cookieOptions, + basePath + ); this.server.auth.scheme('login', () => ({ authenticate: adoptToHapiAuthFormat(fn, sessionStorage), @@ -168,5 +177,5 @@ export class HttpServer { // should be applied for all routes if they don't specify auth strategy in route declaration // https://github.com/hapijs/hapi/blob/master/API.md#-serverauthdefaultoptions this.server.auth.default('session'); - }; + } } From cd63747f1b5a3d961a408a48097cc1fe0d78ec8a Mon Sep 17 00:00:00 2001 From: restrry Date: Fri, 12 Apr 2019 16:37:39 +0200 Subject: [PATCH 30/35] cookieOptions.password --> cookieOptions.encryptionKey --- src/core/server/http/cookie_session_storage.ts | 4 ++-- src/core/server/http/cookie_sesson_storage.test.ts | 2 +- src/core/server/http/http_server.test.ts | 2 +- .../__fixtures__/plugins/dummy_security/server/plugin.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/core/server/http/cookie_session_storage.ts b/src/core/server/http/cookie_session_storage.ts index 3b90ba8275e6a..d9d7fb775dd2c 100644 --- a/src/core/server/http/cookie_session_storage.ts +++ b/src/core/server/http/cookie_session_storage.ts @@ -23,7 +23,7 @@ import { SessionStorageFactory, SessionStorage } from './session_storage'; export interface CookieOptions { name: string; - password: string; + encryptionKey: string; validate: (sessionValue: T) => boolean | Promise; isSecure: boolean; } @@ -61,7 +61,7 @@ export async function createCookieSessionStorageFactory( server.auth.strategy('security-cookie', 'cookie', { cookie: cookieOptions.name, - password: cookieOptions.password, + password: cookieOptions.encryptionKey, validateFunc: async (req, session: T) => ({ valid: await cookieOptions.validate(session) }), isSecure: cookieOptions.isSecure, path: basePath, diff --git a/src/core/server/http/cookie_sesson_storage.test.ts b/src/core/server/http/cookie_sesson_storage.test.ts index f433bf93aa432..398ef067e0f7a 100644 --- a/src/core/server/http/cookie_sesson_storage.test.ts +++ b/src/core/server/http/cookie_sesson_storage.test.ts @@ -44,7 +44,7 @@ const sessionDurationMs = 30; const delay = (ms: number) => new Promise(res => setTimeout(res, ms)); const cookieOptions = { name: 'sid', - password: 'something_at_least_32_characters', + encryptionKey: 'something_at_least_32_characters', validate: (session: Storage) => session.expires > Date.now(), isSecure: false, path: '/', diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index b9e1e6c5c3005..b8b740796a932 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -576,7 +576,7 @@ test('registers auth request interceptor only once', async () => { const { registerAuth } = await server.start(config); const doRegister = () => registerAuth(() => null as any, { - password: 'any_password', + encryptionKey: 'any_password', } as any); await doRegister(); diff --git a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts index cb9fe696f331c..cb8225ff42876 100644 --- a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts +++ b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts @@ -59,7 +59,7 @@ export class DummySecurityPlugin { const cookieOptions = { name: 'sid', - password: 'something_at_least_32_characters', + encryptionKey: 'something_at_least_32_characters', validate: (session: Storage) => true, isSecure: false, path: '/', From 6a9367097bd64e4a70cc7b117c59a7116e90be35 Mon Sep 17 00:00:00 2001 From: restrry Date: Fri, 12 Apr 2019 16:50:13 +0200 Subject: [PATCH 31/35] CookieOption --> SessionStorageCookieOptions --- src/core/server/http/cookie_session_storage.ts | 4 ++-- src/core/server/http/http_server.ts | 11 +++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/core/server/http/cookie_session_storage.ts b/src/core/server/http/cookie_session_storage.ts index d9d7fb775dd2c..7301de6315606 100644 --- a/src/core/server/http/cookie_session_storage.ts +++ b/src/core/server/http/cookie_session_storage.ts @@ -21,7 +21,7 @@ import { Request, Server } from 'hapi'; import hapiAuthCookie from 'hapi-auth-cookie'; import { SessionStorageFactory, SessionStorage } from './session_storage'; -export interface CookieOptions { +export interface SessionStorageCookieOptions { name: string; encryptionKey: string; validate: (sessionValue: T) => boolean | Promise; @@ -54,7 +54,7 @@ class ScopedCookieSessionStorage> implements Sessi */ export async function createCookieSessionStorageFactory( server: Server, - cookieOptions: CookieOptions, + cookieOptions: SessionStorageCookieOptions, basePath?: string ): Promise> { await server.register({ plugin: hapiAuthCookie }); diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index ff6467b88ca15..8c58cff1e5872 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -26,12 +26,15 @@ import { createServer, getServerOptions } from './http_tools'; import { adoptToHapiAuthFormat, Authenticate } from './lifecycle/auth'; import { adoptToHapiOnRequestFormat, OnRequest } from './lifecycle/on_request'; import { Router } from './router'; -import { CookieOptions, createCookieSessionStorageFactory } from './cookie_session_storage'; +import { + SessionStorageCookieOptions, + createCookieSessionStorageFactory, +} from './cookie_session_storage'; export interface HttpServerInfo { server: Server; options: ServerOptions; - registerAuth: (fn: Authenticate, cookieOptions: CookieOptions) => void; + registerAuth: (fn: Authenticate, cookieOptions: SessionStorageCookieOptions) => void; registerOnRequest: (fn: OnRequest) => void; } @@ -87,7 +90,7 @@ export class HttpServer { server: this.server, options: serverOptions, registerOnRequest: this.registerOnRequest.bind(this), - registerAuth: (fn: Authenticate, cookieOptions: CookieOptions) => + registerAuth: (fn: Authenticate, cookieOptions: SessionStorageCookieOptions) => this.registerAuth(fn, cookieOptions, config.basePath), }; } @@ -150,7 +153,7 @@ export class HttpServer { private async registerAuth( fn: Authenticate, - cookieOptions: CookieOptions, + cookieOptions: SessionStorageCookieOptions, basePath?: string ) { if (this.server === undefined) { From e9e8da0d6389e0581e2fc6aa8d74d2123534fdb4 Mon Sep 17 00:00:00 2001 From: restrry Date: Tue, 16 Apr 2019 10:04:05 +0200 Subject: [PATCH 32/35] address comments @joshdover --- src/core/server/http/http_server.ts | 30 ++++++++++++++----- src/core/server/http/index.ts | 4 +-- .../plugins/dummy_security/server/plugin.ts | 4 +-- src/core/server/http/lifecycle/auth.ts | 4 +-- src/core/server/http/lifecycle/on_request.ts | 4 +-- src/core/server/index.ts | 4 +-- src/core/server/plugins/plugin_context.ts | 2 +- 7 files changed, 33 insertions(+), 19 deletions(-) diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 8c58cff1e5872..7259677d5faca 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -23,8 +23,8 @@ import { modifyUrl } from '../../utils'; import { Logger } from '../logging'; import { HttpConfig } from './http_config'; import { createServer, getServerOptions } from './http_tools'; -import { adoptToHapiAuthFormat, Authenticate } from './lifecycle/auth'; -import { adoptToHapiOnRequestFormat, OnRequest } from './lifecycle/on_request'; +import { adoptToHapiAuthFormat, AuthenticationHandler } from './lifecycle/auth'; +import { adoptToHapiOnRequestFormat, OnRequestHandler } from './lifecycle/on_request'; import { Router } from './router'; import { SessionStorageCookieOptions, @@ -34,8 +34,20 @@ import { export interface HttpServerInfo { server: Server; options: ServerOptions; - registerAuth: (fn: Authenticate, cookieOptions: SessionStorageCookieOptions) => void; - registerOnRequest: (fn: OnRequest) => void; + /** + * Define custom authentication and/or authorization mechanism for incoming requests. + * Applied to all resources by default. Only one AuthenticationHandler can be registered. + */ + registerAuth: ( + authenticationHandler: AuthenticationHandler, + cookieOptions: SessionStorageCookieOptions + ) => void; + /** + * Define custom logic to perform for incoming requests. + * Applied to all resources by default. + * Can register any number of OnRequestHandlers, which are called in sequence (from the first registered to the last) + */ + registerOnRequest: (requestHandler: OnRequestHandler) => void; } export class HttpServer { @@ -90,8 +102,10 @@ export class HttpServer { server: this.server, options: serverOptions, registerOnRequest: this.registerOnRequest.bind(this), - registerAuth: (fn: Authenticate, cookieOptions: SessionStorageCookieOptions) => - this.registerAuth(fn, cookieOptions, config.basePath), + registerAuth: ( + fn: AuthenticationHandler, + cookieOptions: SessionStorageCookieOptions + ) => this.registerAuth(fn, cookieOptions, config.basePath), }; } @@ -143,7 +157,7 @@ export class HttpServer { return `${routerPath}${routePath.slice(routePathStartIndex)}`; } - private registerOnRequest(fn: OnRequest) { + private registerOnRequest(fn: OnRequestHandler) { if (this.server === undefined) { throw new Error('Server is not created yet'); } @@ -152,7 +166,7 @@ export class HttpServer { } private async registerAuth( - fn: Authenticate, + fn: AuthenticationHandler, cookieOptions: SessionStorageCookieOptions, basePath?: string ) { diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index b56c6b9af55b2..9457a3fad3c3c 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -22,5 +22,5 @@ export { HttpService, HttpServiceSetup } from './http_service'; export { Router, KibanaRequest } from './router'; export { HttpServerInfo } from './http_server'; export { BasePathProxyServer } from './base_path_proxy_server'; -export { Authenticate, AuthToolkit } from './lifecycle/auth'; -export { OnRequest, OnRequestToolkit } from './lifecycle/on_request'; +export { AuthenticationHandler, AuthToolkit } from './lifecycle/auth'; +export { OnRequestHandler, OnRequestToolkit } from './lifecycle/on_request'; diff --git a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts index cb8225ff42876..a2abb20900e19 100644 --- a/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts +++ b/src/core/server/http/integration_tests/__fixtures__/plugins/dummy_security/server/plugin.ts @@ -17,7 +17,7 @@ * under the License. */ import Boom from 'boom'; -import { Authenticate, CoreSetup } from '../../../../../../../../core/server'; +import { AuthenticationHandler, CoreSetup } from '../../../../../../../../core/server'; interface User { id: string; @@ -39,7 +39,7 @@ export const url = { export const sessionDurationMs = 30; export class DummySecurityPlugin { public setup(core: CoreSetup) { - const authenticate: Authenticate = async (request, sessionStorage, t) => { + const authenticate: AuthenticationHandler = async (request, sessionStorage, t) => { if (request.path === url.authRedirect) { return t.redirected(url.redirectTo); } diff --git a/src/core/server/http/lifecycle/auth.ts b/src/core/server/http/lifecycle/auth.ts index c242c90e1d893..8205d21c5ff59 100644 --- a/src/core/server/http/lifecycle/auth.ts +++ b/src/core/server/http/lifecycle/auth.ts @@ -72,7 +72,7 @@ const toolkit: AuthToolkit = { }; /** @public */ -export type Authenticate = ( +export type AuthenticationHandler = ( request: Request, sessionStorage: SessionStorage, t: AuthToolkit @@ -80,7 +80,7 @@ export type Authenticate = ( /** @public */ export function adoptToHapiAuthFormat( - fn: Authenticate, + fn: AuthenticationHandler, sessionStorage: SessionStorageFactory ) { return async function interceptAuth( diff --git a/src/core/server/http/lifecycle/on_request.ts b/src/core/server/http/lifecycle/on_request.ts index 5fdc2e2203351..6192a1dae682c 100644 --- a/src/core/server/http/lifecycle/on_request.ts +++ b/src/core/server/http/lifecycle/on_request.ts @@ -73,7 +73,7 @@ const toolkit: OnRequestToolkit = { }; /** @public */ -export type OnRequest = ( +export type OnRequestHandler = ( req: KibanaRequest, t: OnRequestToolkit ) => OnRequestResult | Promise; @@ -84,7 +84,7 @@ export type OnRequest = ( * @param fn - an extension point allowing to perform custom logic for * incoming HTTP requests. */ -export function adoptToHapiOnRequestFormat(fn: OnRequest) { +export function adoptToHapiOnRequestFormat(fn: OnRequestHandler) { return async function interceptRequest( req: Request, h: ResponseToolkit diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 7a1a50921738d..e83abf01044f7 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -31,10 +31,10 @@ export { APICaller, } from './elasticsearch'; export { - Authenticate, + AuthenticationHandler, AuthToolkit, KibanaRequest, - OnRequest, + OnRequestHandler, OnRequestToolkit, Router, } from './http'; diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index e0dc3d11baf1a..95f5579f67ee1 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -116,7 +116,7 @@ export function createPluginInitializerContext( // Added to improve http typings as make { http: Required } // Http service is disabled, when Kibana runs in optimizer mode or as dev cluster managed by cluster master. -// In theory no plugins shouldn't try to access http dependency in this case. +// In theory no plugins shouldn try to access http dependency in this case. function preventAccess() { throw new Error('Cannot use http contract when http server not started'); } From 805b05f71dec408a7c7ca15633cf41268ee9fb38 Mon Sep 17 00:00:00 2001 From: restrry Date: Tue, 16 Apr 2019 18:34:42 +0200 Subject: [PATCH 33/35] resolve conflict leftovers --- .../core/server/kibana-plugin-server.md | 92 +++++++++---------- 1 file changed, 43 insertions(+), 49 deletions(-) diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index dd86aa8decfa9..5a9d57811fac8 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -1,49 +1,43 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) - -## kibana-plugin-server package - -## 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(...)). | -| [ConfigService](./kibana-plugin-server.configservice.md) | | -| [KibanaRequest](./kibana-plugin-server.kibanarequest.md) | | -| [Router](./kibana-plugin-server.router.md) | | -| [ScopedClusterClient](./kibana-plugin-server.scopedclusterclient.md) | Serves the same purpose as "normal" 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) | | -| [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) | | -| [Logger](./kibana-plugin-server.logger.md) | Logger exposes all the necessary methods to log any type of information and this is the interface used by the logging consumers including plugins. | -| [LoggerFactory](./kibana-plugin-server.loggerfactory.md) | The single purpose of LoggerFactory interface is to define a way to retrieve a context-based logger instance. | -| [LogMeta](./kibana-plugin-server.logmeta.md) | Contextual metadata | -<<<<<<< HEAD -| [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. | ->>>>>>> master -| [PluginInitializerContext](./kibana-plugin-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. | -| [PluginSetupContext](./kibana-plugin-server.pluginsetupcontext.md) | Context passed to the plugins setup method. | - -## Type Aliases - -| Type Alias | Description | -| --- | --- | -| [APICaller](./kibana-plugin-server.apicaller.md) | | -| [Authenticate](./kibana-plugin-server.authenticate.md) | | -| [ElasticsearchClientConfig](./kibana-plugin-server.elasticsearchclientconfig.md) | | -| [Headers](./kibana-plugin-server.headers.md) | | -<<<<<<< HEAD -| [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) | | -| [OnRequest](./kibana-plugin-server.onrequest.md) | | -======= -| [PluginInitializer](./kibana-plugin-server.plugininitializer.md) | The plugin export at the root of a plugin's server directory should conform to this interface. | ->>>>>>> master -| [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. | - +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) + +## kibana-plugin-server package + +## 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(...)). | +| [ConfigService](./kibana-plugin-server.configservice.md) | | +| [KibanaRequest](./kibana-plugin-server.kibanarequest.md) | | +| [Router](./kibana-plugin-server.router.md) | | +| [ScopedClusterClient](./kibana-plugin-server.scopedclusterclient.md) | Serves the same purpose as "normal" 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) | | +| [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) | | +| [Logger](./kibana-plugin-server.logger.md) | Logger exposes all the necessary methods to log any type of information and this is the interface used by the logging consumers including plugins. | +| [LoggerFactory](./kibana-plugin-server.loggerfactory.md) | The single purpose of 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. | +| [PluginSetupContext](./kibana-plugin-server.pluginsetupcontext.md) | Context passed to the plugins setup method. | + +## Type Aliases + +| Type Alias | Description | +| --- | --- | +| [APICaller](./kibana-plugin-server.apicaller.md) | | +| [Authenticate](./kibana-plugin-server.authenticate.md) | | +| [ElasticsearchClientConfig](./kibana-plugin-server.elasticsearchclientconfig.md) | | +| [Headers](./kibana-plugin-server.headers.md) | | +| [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) | | +| [OnRequest](./kibana-plugin-server.onrequest.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. | + From 9e2d026cc404b2bc4b9b7be80f8fed70d356db1c Mon Sep 17 00:00:00 2001 From: restrry Date: Tue, 16 Apr 2019 19:07:11 +0200 Subject: [PATCH 34/35] update @types/hapi-auth-cookie deps --- package.json | 2 +- yarn.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index df5c43d3f485f..6f12759f765ef 100644 --- a/package.json +++ b/package.json @@ -287,7 +287,7 @@ "@types/globby": "^8.0.0", "@types/graphql": "^0.13.1", "@types/hapi": "^17.0.18", - "@types/hapi-auth-cookie": "9.1.0", + "@types/hapi-auth-cookie": "^9.1.0", "@types/has-ansi": "^3.0.0", "@types/hoek": "^4.1.3", "@types/humps": "^1.1.2", diff --git a/yarn.lock b/yarn.lock index 88415f0234ca9..c9e1958df0ef8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2945,7 +2945,7 @@ resolved "https://registry.yarnpkg.com/@types/handlebars/-/handlebars-4.0.39.tgz#961fb54db68030890942e6aeffe9f93a957807bd" integrity sha512-vjaS7Q0dVqFp85QhyPSZqDKnTTCemcSHNHFvDdalO1s0Ifz5KuE64jQD5xoUkfdWwF4WpqdJEl7LsWH8rzhKJA== -"@types/hapi-auth-cookie@9.1.0", "@types/hapi-auth-cookie@^9.1.0": +"@types/hapi-auth-cookie@^9.1.0": version "9.1.0" resolved "https://registry.yarnpkg.com/@types/hapi-auth-cookie/-/hapi-auth-cookie-9.1.0.tgz#cbcd2236b7d429bd0632a8cc45cfd355fdd7e7a2" integrity sha512-qsP08L+fNaE2K5dsDVKvHp0AmSBs8m9PD5eWsTdHnkJOk81iD7c0J4GYt/1aDJwZsyx6CgcxpbkPOCwBJmrwAg== From 4a2656baa637e160faeb46f5bc25f2f616a6c9f6 Mon Sep 17 00:00:00 2001 From: restrry Date: Tue, 16 Apr 2019 19:54:05 +0200 Subject: [PATCH 35/35] update docs --- .../kibana-plugin-server.authenticate.md | 10 --- ...ana-plugin-server.authenticationhandler.md | 10 +++ .../core/server/kibana-plugin-server.md | 86 +++++++++---------- .../server/kibana-plugin-server.onrequest.md | 10 --- .../kibana-plugin-server.onrequesthandler.md | 10 +++ 5 files changed, 63 insertions(+), 63 deletions(-) delete mode 100644 docs/development/core/server/kibana-plugin-server.authenticate.md create mode 100644 docs/development/core/server/kibana-plugin-server.authenticationhandler.md delete mode 100644 docs/development/core/server/kibana-plugin-server.onrequest.md create mode 100644 docs/development/core/server/kibana-plugin-server.onrequesthandler.md diff --git a/docs/development/core/server/kibana-plugin-server.authenticate.md b/docs/development/core/server/kibana-plugin-server.authenticate.md deleted file mode 100644 index d6a91170c69be..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.authenticate.md +++ /dev/null @@ -1,10 +0,0 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [Authenticate](./kibana-plugin-server.authenticate.md) - -## Authenticate type - - -Signature: - -```typescript -export declare type Authenticate = (request: Request, sessionStorage: SessionStorage, t: AuthToolkit) => Promise; -``` diff --git a/docs/development/core/server/kibana-plugin-server.authenticationhandler.md b/docs/development/core/server/kibana-plugin-server.authenticationhandler.md new file mode 100644 index 0000000000000..3f087489c1376 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.authenticationhandler.md @@ -0,0 +1,10 @@ +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthenticationHandler](./kibana-plugin-server.authenticationhandler.md) + +## AuthenticationHandler type + + +Signature: + +```typescript +export declare type AuthenticationHandler = (request: Request, sessionStorage: SessionStorage, t: AuthToolkit) => Promise; +``` diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index 5a9d57811fac8..9c1d0b06fa70e 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -1,43 +1,43 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) - -## kibana-plugin-server package - -## 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(...)). | -| [ConfigService](./kibana-plugin-server.configservice.md) | | -| [KibanaRequest](./kibana-plugin-server.kibanarequest.md) | | -| [Router](./kibana-plugin-server.router.md) | | -| [ScopedClusterClient](./kibana-plugin-server.scopedclusterclient.md) | Serves the same purpose as "normal" 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) | | -| [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) | | -| [Logger](./kibana-plugin-server.logger.md) | Logger exposes all the necessary methods to log any type of information and this is the interface used by the logging consumers including plugins. | -| [LoggerFactory](./kibana-plugin-server.loggerfactory.md) | The single purpose of 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. | -| [PluginSetupContext](./kibana-plugin-server.pluginsetupcontext.md) | Context passed to the plugins setup method. | - -## Type Aliases - -| Type Alias | Description | -| --- | --- | -| [APICaller](./kibana-plugin-server.apicaller.md) | | -| [Authenticate](./kibana-plugin-server.authenticate.md) | | -| [ElasticsearchClientConfig](./kibana-plugin-server.elasticsearchclientconfig.md) | | -| [Headers](./kibana-plugin-server.headers.md) | | -| [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) | | -| [OnRequest](./kibana-plugin-server.onrequest.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. | - +[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) + +## kibana-plugin-server package + +## 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(...)). | +| [ConfigService](./kibana-plugin-server.configservice.md) | | +| [KibanaRequest](./kibana-plugin-server.kibanarequest.md) | | +| [Router](./kibana-plugin-server.router.md) | | +| [ScopedClusterClient](./kibana-plugin-server.scopedclusterclient.md) | Serves the same purpose as "normal" 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) | | +| [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) | | +| [Logger](./kibana-plugin-server.logger.md) | Logger exposes all the necessary methods to log any type of information and this is the interface used by the logging consumers including plugins. | +| [LoggerFactory](./kibana-plugin-server.loggerfactory.md) | The single purpose of 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. | +| [PluginSetupContext](./kibana-plugin-server.pluginsetupcontext.md) | Context passed to the plugins setup method. | + +## 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. | + diff --git a/docs/development/core/server/kibana-plugin-server.onrequest.md b/docs/development/core/server/kibana-plugin-server.onrequest.md deleted file mode 100644 index e252aa0c7c2e4..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.onrequest.md +++ /dev/null @@ -1,10 +0,0 @@ -[Home](./index) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnRequest](./kibana-plugin-server.onrequest.md) - -## OnRequest type - - -Signature: - -```typescript -export declare type OnRequest = (req: KibanaRequest, t: OnRequestToolkit) => OnRequestResult | Promise; -``` diff --git a/docs/development/core/server/kibana-plugin-server.onrequesthandler.md b/docs/development/core/server/kibana-plugin-server.onrequesthandler.md new file mode 100644 index 0000000000000..5f093fef4eb20 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onrequesthandler.md @@ -0,0 +1,10 @@ +[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; +```