Unify response interface in handler and request interceptors (#42442)
* add response factory to the interceptors * adopt x-pack code to the changes * Add a separate response factory for lifecycles. Only route handler can respond with 2xx response. Interceptors may redirect or reject an incoming request. * re-generate docs * response.internal --> response.internalError * use internalError for exceptions in authenticator * before Security plugin proxied ES error status code. now sets explicitly. * provide error via message field of error response for BWC * update docs * add customError response * restore integration test and update unit tests * update docs * support Hapi error format for BWC * add a couple of tests
This commit is contained in:
parent
35528af8ef
commit
06adc737d9
|
@ -8,5 +8,5 @@
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type AuthenticationHandler = (request: KibanaRequest, t: AuthToolkit) => AuthResult | Promise<AuthResult>;
|
||||
export declare type AuthenticationHandler = (request: KibanaRequest, response: LifecycleResponseFactory, toolkit: AuthToolkit) => AuthResult | KibanaResponse | Promise<AuthResult | KibanaResponse>;
|
||||
```
|
||||
|
|
|
@ -17,6 +17,4 @@ export interface AuthToolkit
|
|||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [authenticated](./kibana-plugin-server.authtoolkit.authenticated.md) | <code>(data?: AuthResultParams) => AuthResult</code> | Authentication is successful with given credentials, allow request to pass through |
|
||||
| [redirected](./kibana-plugin-server.authtoolkit.redirected.md) | <code>(url: string) => AuthResult</code> | Authentication requires to interrupt request handling and redirect to a configured url |
|
||||
| [rejected](./kibana-plugin-server.authtoolkit.rejected.md) | <code>(error: Error, options?: {</code><br/><code> statusCode?: number;</code><br/><code> }) => AuthResult</code> | Authentication is unsuccessful, fail the request with specified error. |
|
||||
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
redirected: (url: string) => AuthResult;
|
||||
```
|
|
@ -1,15 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [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.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
rejected: (error: Error, options?: {
|
||||
statusCode?: number;
|
||||
}) => AuthResult;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [LifecycleResponseFactory](./kibana-plugin-server.lifecycleresponsefactory.md)
|
||||
|
||||
## LifecycleResponseFactory type
|
||||
|
||||
Creates an object containing redirection or error response with error details, HTTP headers, and other data transmitted to the client.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type LifecycleResponseFactory = typeof lifecycleResponseFactory;
|
||||
```
|
|
@ -120,6 +120,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| [IsAuthenticated](./kibana-plugin-server.isauthenticated.md) | Return authentication status for a request. |
|
||||
| [KibanaResponseFactory](./kibana-plugin-server.kibanaresponsefactory.md) | Creates an object containing request response payload, HTTP headers, error details, and other data transmitted to the client. |
|
||||
| [KnownHeaders](./kibana-plugin-server.knownheaders.md) | Set of well-known HTTP headers. |
|
||||
| [LifecycleResponseFactory](./kibana-plugin-server.lifecycleresponsefactory.md) | Creates an object containing redirection or error response with error details, HTTP headers, and other data transmitted to the client. |
|
||||
| [OnPostAuthHandler](./kibana-plugin-server.onpostauthhandler.md) | |
|
||||
| [OnPreAuthHandler](./kibana-plugin-server.onpreauthhandler.md) | |
|
||||
| [PluginInitializer](./kibana-plugin-server.plugininitializer.md) | The <code>plugin</code> export at the root of a plugin's <code>server</code> directory should conform to this interface. |
|
||||
|
|
|
@ -8,5 +8,5 @@
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type OnPostAuthHandler<Params = any, Query = any, Body = any> = (request: KibanaRequest<Params, Query, Body>, t: OnPostAuthToolkit) => OnPostAuthResult | Promise<OnPostAuthResult>;
|
||||
export declare type OnPostAuthHandler = (request: KibanaRequest, response: LifecycleResponseFactory, toolkit: OnPostAuthToolkit) => OnPostAuthResult | KibanaResponse | Promise<OnPostAuthResult | KibanaResponse>;
|
||||
```
|
||||
|
|
|
@ -17,6 +17,4 @@ export interface OnPostAuthToolkit
|
|||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [next](./kibana-plugin-server.onpostauthtoolkit.next.md) | <code>() => OnPostAuthResult</code> | To pass request to the next handler |
|
||||
| [redirected](./kibana-plugin-server.onpostauthtoolkit.redirected.md) | <code>(url: string) => OnPostAuthResult</code> | To interrupt request handling and redirect to a configured url |
|
||||
| [rejected](./kibana-plugin-server.onpostauthtoolkit.rejected.md) | <code>(error: Error, options?: {</code><br/><code> statusCode?: number;</code><br/><code> }) => OnPostAuthResult</code> | Fail the request with specified error. |
|
||||
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPostAuthToolkit](./kibana-plugin-server.onpostauthtoolkit.md) > [redirected](./kibana-plugin-server.onpostauthtoolkit.redirected.md)
|
||||
|
||||
## OnPostAuthToolkit.redirected property
|
||||
|
||||
To interrupt request handling and redirect to a configured url
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
redirected: (url: string) => OnPostAuthResult;
|
||||
```
|
|
@ -1,15 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPostAuthToolkit](./kibana-plugin-server.onpostauthtoolkit.md) > [rejected](./kibana-plugin-server.onpostauthtoolkit.rejected.md)
|
||||
|
||||
## OnPostAuthToolkit.rejected property
|
||||
|
||||
Fail the request with specified error.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
rejected: (error: Error, options?: {
|
||||
statusCode?: number;
|
||||
}) => OnPostAuthResult;
|
||||
```
|
|
@ -8,5 +8,5 @@
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type OnPreAuthHandler<Params = any, Query = any, Body = any> = (request: KibanaRequest<Params, Query, Body>, t: OnPreAuthToolkit) => OnPreAuthResult | Promise<OnPreAuthResult>;
|
||||
export declare type OnPreAuthHandler = (request: KibanaRequest, response: LifecycleResponseFactory, toolkit: OnPreAuthToolkit) => OnPreAuthResult | KibanaResponse | Promise<OnPreAuthResult | KibanaResponse>;
|
||||
```
|
||||
|
|
|
@ -17,6 +17,5 @@ export interface OnPreAuthToolkit
|
|||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [next](./kibana-plugin-server.onpreauthtoolkit.next.md) | <code>() => OnPreAuthResult</code> | To pass request to the next handler |
|
||||
| [redirected](./kibana-plugin-server.onpreauthtoolkit.redirected.md) | <code>(url: string, options?: {</code><br/><code> forward: boolean;</code><br/><code> }) => OnPreAuthResult</code> | To interrupt request handling and redirect to a configured url. If "options.forwarded" = true, request will be forwarded to another url right on the server. |
|
||||
| [rejected](./kibana-plugin-server.onpreauthtoolkit.rejected.md) | <code>(error: Error, options?: {</code><br/><code> statusCode?: number;</code><br/><code> }) => OnPreAuthResult</code> | Fail the request with specified error. |
|
||||
| [rewriteUrl](./kibana-plugin-server.onpreauthtoolkit.rewriteurl.md) | <code>(url: string) => OnPreAuthResult</code> | Rewrite requested resources url before is was authenticated and routed to a handler |
|
||||
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md) > [redirected](./kibana-plugin-server.onpreauthtoolkit.redirected.md)
|
||||
|
||||
## OnPreAuthToolkit.redirected property
|
||||
|
||||
To interrupt request handling and redirect to a configured url. If "options.forwarded" = true, request will be forwarded to another url right on the server.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
redirected: (url: string, options?: {
|
||||
forward: boolean;
|
||||
}) => OnPreAuthResult;
|
||||
```
|
|
@ -1,15 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md) > [rejected](./kibana-plugin-server.onpreauthtoolkit.rejected.md)
|
||||
|
||||
## OnPreAuthToolkit.rejected property
|
||||
|
||||
Fail the request with specified error.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
rejected: (error: Error, options?: {
|
||||
statusCode?: number;
|
||||
}) => OnPreAuthResult;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md) > [rewriteUrl](./kibana-plugin-server.onpreauthtoolkit.rewriteurl.md)
|
||||
|
||||
## OnPreAuthToolkit.rewriteUrl property
|
||||
|
||||
Rewrite requested resources url before is was authenticated and routed to a handler
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
rewriteUrl: (url: string) => OnPreAuthResult;
|
||||
```
|
|
@ -16,5 +16,5 @@ constructor(path: string);
|
|||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| path | <code>string</code> | a router path, set as the very first path segment for all registered routes. |
|
||||
| path | <code>string</code> | |
|
||||
|
||||
|
|
|
@ -23,7 +23,6 @@ export declare class Router
|
|||
| Property | Modifiers | Type | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| [path](./kibana-plugin-server.router.path.md) | | <code>string</code> | |
|
||||
| [routes](./kibana-plugin-server.router.routes.md) | | <code>Array<Readonly<RouterRoute>></code> | |
|
||||
|
||||
## Methods
|
||||
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [routes](./kibana-plugin-server.router.routes.md)
|
||||
|
||||
## Router.routes property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
routes: Array<Readonly<RouterRoute>>;
|
||||
```
|
|
@ -16,14 +16,19 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { Request, ResponseToolkit } from 'hapi';
|
||||
import { Request } from 'hapi';
|
||||
import { merge } from 'lodash';
|
||||
|
||||
import querystring from 'querystring';
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
import { KibanaRequest, RouteMethod } from './router';
|
||||
import {
|
||||
KibanaRequest,
|
||||
LifecycleResponseFactory,
|
||||
RouteMethod,
|
||||
KibanaResponseFactory,
|
||||
} from './router';
|
||||
|
||||
interface RequestFixtureOptions {
|
||||
headers?: Record<string, string>;
|
||||
|
@ -97,12 +102,35 @@ function createRawRequestMock(customization: DeepPartial<Request> = {}) {
|
|||
) as Request;
|
||||
}
|
||||
|
||||
function createRawResponseToolkitMock(customization: DeepPartial<ResponseToolkit> = {}) {
|
||||
return merge({}, customization) as ResponseToolkit;
|
||||
}
|
||||
const createResponseFactoryMock = (): jest.Mocked<KibanaResponseFactory> => ({
|
||||
ok: jest.fn(),
|
||||
accepted: jest.fn(),
|
||||
noContent: jest.fn(),
|
||||
custom: jest.fn(),
|
||||
redirected: jest.fn(),
|
||||
badRequest: jest.fn(),
|
||||
unauthorized: jest.fn(),
|
||||
forbidden: jest.fn(),
|
||||
notFound: jest.fn(),
|
||||
conflict: jest.fn(),
|
||||
internalError: jest.fn(),
|
||||
customError: jest.fn(),
|
||||
});
|
||||
|
||||
const createLifecycleResponseFactoryMock = (): jest.Mocked<LifecycleResponseFactory> => ({
|
||||
redirected: jest.fn(),
|
||||
badRequest: jest.fn(),
|
||||
unauthorized: jest.fn(),
|
||||
forbidden: jest.fn(),
|
||||
notFound: jest.fn(),
|
||||
conflict: jest.fn(),
|
||||
internalError: jest.fn(),
|
||||
customError: jest.fn(),
|
||||
});
|
||||
|
||||
export const httpServerMock = {
|
||||
createKibanaRequest: createKibanaRequestMock,
|
||||
createRawRequest: createRawRequestMock,
|
||||
createRawResponseToolkit: createRawResponseToolkitMock,
|
||||
createResponseFactory: createResponseFactoryMock,
|
||||
createLifecycleResponseFactory: createLifecycleResponseFactoryMock,
|
||||
};
|
||||
|
|
|
@ -18,8 +18,6 @@
|
|||
*/
|
||||
|
||||
import { Server } from 'http';
|
||||
import request from 'request';
|
||||
import Boom from 'boom';
|
||||
|
||||
jest.mock('fs', () => ({
|
||||
readFileSync: jest.fn(),
|
||||
|
@ -40,16 +38,6 @@ const cookieOptions = {
|
|||
isSecure: false,
|
||||
};
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
roles?: string[];
|
||||
}
|
||||
|
||||
interface StorageData {
|
||||
value: User;
|
||||
expires: number;
|
||||
}
|
||||
|
||||
const chance = new Chance();
|
||||
|
||||
let server: HttpServer;
|
||||
|
@ -159,7 +147,9 @@ test('invalid params', async () => {
|
|||
.expect(400)
|
||||
.then(res => {
|
||||
expect(res.body).toEqual({
|
||||
error: '[request params.test]: expected value of type [number] but got [string]',
|
||||
error: 'Bad Request',
|
||||
statusCode: 400,
|
||||
message: '[request params.test]: expected value of type [number] but got [string]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -222,7 +212,9 @@ test('invalid query', async () => {
|
|||
.expect(400)
|
||||
.then(res => {
|
||||
expect(res.body).toEqual({
|
||||
error: '[request query.bar]: expected value of type [number] but got [string]',
|
||||
error: 'Bad Request',
|
||||
statusCode: 400,
|
||||
message: '[request query.bar]: expected value of type [number] but got [string]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -290,7 +282,9 @@ test('invalid body', async () => {
|
|||
.expect(400)
|
||||
.then(res => {
|
||||
expect(res.body).toEqual({
|
||||
error: '[request body.bar]: expected value of type [number] but got [string]',
|
||||
error: 'Bad Request',
|
||||
statusCode: 400,
|
||||
message: '[request body.bar]: expected value of type [number] but got [string]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -498,78 +492,12 @@ test('returns server and connection options on start', async () => {
|
|||
expect(innerServer).toBe((server as any).server);
|
||||
});
|
||||
|
||||
test('registers registerOnPostAuth interceptor several times', async () => {
|
||||
const { registerOnPostAuth } = await server.setup(config);
|
||||
const doRegister = () => registerOnPostAuth(() => null as any);
|
||||
|
||||
doRegister();
|
||||
expect(doRegister).not.toThrowError();
|
||||
});
|
||||
|
||||
test('throws an error if starts without set up', async () => {
|
||||
await expect(server.start()).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Http server is not setup up yet"`
|
||||
);
|
||||
});
|
||||
|
||||
test('enables auth for a route by default if registerAuth has been called', async () => {
|
||||
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
|
||||
|
||||
const router = new Router('');
|
||||
router.get({ path: '/', validate: false }, (req, res) =>
|
||||
res.ok({ authRequired: req.route.options.authRequired })
|
||||
);
|
||||
registerRouter(router);
|
||||
|
||||
const authenticate = jest.fn().mockImplementation((req, t) => t.authenticated());
|
||||
registerAuth(authenticate);
|
||||
|
||||
await server.start();
|
||||
await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(200, { authRequired: true });
|
||||
|
||||
expect(authenticate).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('supports disabling auth for a route explicitly', async () => {
|
||||
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
|
||||
|
||||
const router = new Router('');
|
||||
router.get({ path: '/', validate: false, options: { authRequired: false } }, (req, res) =>
|
||||
res.ok({ authRequired: req.route.options.authRequired })
|
||||
);
|
||||
registerRouter(router);
|
||||
const authenticate = jest.fn();
|
||||
registerAuth(authenticate);
|
||||
|
||||
await server.start();
|
||||
await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(200, { authRequired: false });
|
||||
|
||||
expect(authenticate).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('supports enabling auth for a route explicitly', async () => {
|
||||
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
|
||||
|
||||
const router = new Router('');
|
||||
router.get({ path: '/', validate: false, options: { authRequired: true } }, (req, res) =>
|
||||
res.ok({ authRequired: req.route.options.authRequired })
|
||||
);
|
||||
registerRouter(router);
|
||||
const authenticate = jest.fn().mockImplementation((req, t) => t.authenticated({}));
|
||||
await registerAuth(authenticate);
|
||||
|
||||
await server.start();
|
||||
await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(200, { authRequired: true });
|
||||
|
||||
expect(authenticate).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('allows attaching metadata to attach meta-data tag strings to a route', async () => {
|
||||
const tags = ['my:tag'];
|
||||
const { registerRouter, server: innerServer } = await server.setup(config);
|
||||
|
@ -629,307 +557,6 @@ describe('setup contract', () => {
|
|||
expect(create()).rejects.toThrowError('A cookieSessionStorageFactory was already created');
|
||||
});
|
||||
});
|
||||
describe('#registerAuth', () => {
|
||||
it('registers auth request interceptor only once', async () => {
|
||||
const { registerAuth } = await server.setup(config);
|
||||
const doRegister = () => registerAuth(() => null as any);
|
||||
|
||||
doRegister();
|
||||
expect(doRegister).toThrowError('Auth interceptor was already registered');
|
||||
});
|
||||
|
||||
it('may grant access to a resource', async () => {
|
||||
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
|
||||
const router = new Router('');
|
||||
router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' }));
|
||||
registerRouter(router);
|
||||
|
||||
await registerAuth((req, t) => t.authenticated());
|
||||
await server.start();
|
||||
|
||||
await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(200, { content: 'ok' });
|
||||
});
|
||||
|
||||
it('supports rejecting a request from an unauthenticated user', async () => {
|
||||
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
|
||||
const router = new Router('');
|
||||
router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' }));
|
||||
registerRouter(router);
|
||||
|
||||
await registerAuth((req, t) => t.rejected(Boom.unauthorized()));
|
||||
await server.start();
|
||||
|
||||
await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(401);
|
||||
});
|
||||
|
||||
it('supports redirecting', async () => {
|
||||
const redirectTo = '/redirect-url';
|
||||
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
|
||||
const router = new Router('');
|
||||
router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' }));
|
||||
registerRouter(router);
|
||||
|
||||
await registerAuth((req, t) => {
|
||||
return t.redirected(redirectTo);
|
||||
});
|
||||
await server.start();
|
||||
|
||||
const response = await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(302);
|
||||
expect(response.header.location).toBe(redirectTo);
|
||||
});
|
||||
|
||||
it(`doesn't expose internal error details`, async () => {
|
||||
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
|
||||
const router = new Router('');
|
||||
router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' }));
|
||||
registerRouter(router);
|
||||
|
||||
await registerAuth((req, t) => {
|
||||
throw new Error('sensitive info');
|
||||
});
|
||||
await server.start();
|
||||
|
||||
await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect({
|
||||
statusCode: 500,
|
||||
error: 'Internal Server Error',
|
||||
message: 'An internal server error occurred',
|
||||
});
|
||||
});
|
||||
|
||||
it('allows manipulating cookies via cookie session storage', async () => {
|
||||
const router = new Router('');
|
||||
router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' }));
|
||||
|
||||
const {
|
||||
createCookieSessionStorageFactory,
|
||||
registerAuth,
|
||||
registerRouter,
|
||||
server: innerServer,
|
||||
} = await server.setup(config);
|
||||
const sessionStorageFactory = await createCookieSessionStorageFactory<StorageData>(
|
||||
cookieOptions
|
||||
);
|
||||
registerAuth((req, t) => {
|
||||
const user = { id: '42' };
|
||||
const sessionStorage = sessionStorageFactory.asScoped(req);
|
||||
sessionStorage.set({ value: user, expires: Date.now() + 1000 });
|
||||
return t.authenticated({ state: user });
|
||||
});
|
||||
registerRouter(router);
|
||||
await server.start();
|
||||
|
||||
const response = await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(200, { content: 'ok' });
|
||||
|
||||
expect(response.header['set-cookie']).toBeDefined();
|
||||
const cookies = response.header['set-cookie'];
|
||||
expect(cookies).toHaveLength(1);
|
||||
|
||||
const sessionCookie = request.cookie(cookies[0]);
|
||||
if (!sessionCookie) {
|
||||
throw new Error('session cookie expected to be defined');
|
||||
}
|
||||
expect(sessionCookie).toBeDefined();
|
||||
expect(sessionCookie.key).toBe('sid');
|
||||
expect(sessionCookie.value).toBeDefined();
|
||||
expect(sessionCookie.path).toBe('/');
|
||||
expect(sessionCookie.httpOnly).toBe(true);
|
||||
});
|
||||
|
||||
it('allows manipulating cookies from route handler', async () => {
|
||||
const {
|
||||
createCookieSessionStorageFactory,
|
||||
registerAuth,
|
||||
registerRouter,
|
||||
server: innerServer,
|
||||
} = await server.setup(config);
|
||||
const sessionStorageFactory = await createCookieSessionStorageFactory<StorageData>(
|
||||
cookieOptions
|
||||
);
|
||||
registerAuth((req, t) => {
|
||||
const user = { id: '42' };
|
||||
const sessionStorage = sessionStorageFactory.asScoped(req);
|
||||
sessionStorage.set({ value: user, expires: Date.now() + 1000 });
|
||||
return t.authenticated();
|
||||
});
|
||||
|
||||
const router = new Router('');
|
||||
router.get({ path: '/', validate: false }, (req, res) => res.ok({ content: 'ok' }));
|
||||
router.get({ path: '/with-cookie', validate: false }, (req, res) => {
|
||||
const sessionStorage = sessionStorageFactory.asScoped(req);
|
||||
sessionStorage.clear();
|
||||
return res.ok({ content: 'ok' });
|
||||
});
|
||||
registerRouter(router);
|
||||
|
||||
await server.start();
|
||||
|
||||
const responseToSetCookie = await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(200);
|
||||
|
||||
expect(responseToSetCookie.header['set-cookie']).toBeDefined();
|
||||
|
||||
const responseToResetCookie = await supertest(innerServer.listener)
|
||||
.get('/with-cookie')
|
||||
.expect(200);
|
||||
|
||||
expect(responseToResetCookie.header['set-cookie']).toEqual([
|
||||
'sid=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Path=/',
|
||||
]);
|
||||
});
|
||||
|
||||
it.skip('is the only place with access to the authorization header', async () => {
|
||||
const token = 'Basic: user:password';
|
||||
const {
|
||||
registerAuth,
|
||||
registerOnPreAuth,
|
||||
registerOnPostAuth,
|
||||
registerRouter,
|
||||
server: innerServer,
|
||||
} = await server.setup(config);
|
||||
|
||||
let fromRegisterOnPreAuth;
|
||||
await registerOnPreAuth((req, t) => {
|
||||
fromRegisterOnPreAuth = req.headers.authorization;
|
||||
return t.next();
|
||||
});
|
||||
|
||||
let fromRegisterAuth;
|
||||
await registerAuth((req, t) => {
|
||||
fromRegisterAuth = req.headers.authorization;
|
||||
return t.authenticated();
|
||||
});
|
||||
|
||||
let fromRegisterOnPostAuth;
|
||||
await registerOnPostAuth((req, t) => {
|
||||
fromRegisterOnPostAuth = req.headers.authorization;
|
||||
return t.next();
|
||||
});
|
||||
|
||||
let fromRouteHandler;
|
||||
const router = new Router('');
|
||||
router.get({ path: '/', validate: false }, (req, res) => {
|
||||
fromRouteHandler = req.headers.authorization;
|
||||
return res.ok({ content: 'ok' });
|
||||
});
|
||||
registerRouter(router);
|
||||
|
||||
await server.start();
|
||||
|
||||
await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.set('Authorization', token)
|
||||
.expect(200);
|
||||
|
||||
expect(fromRegisterOnPreAuth).toEqual({});
|
||||
expect(fromRegisterAuth).toEqual({ authorization: token });
|
||||
expect(fromRegisterOnPostAuth).toEqual({});
|
||||
expect(fromRouteHandler).toEqual({});
|
||||
});
|
||||
|
||||
it('attach security header to a successful response', async () => {
|
||||
const authResponseHeader = {
|
||||
'www-authenticate': 'Negotiate ade0234568a4209af8bc0280289eca',
|
||||
};
|
||||
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
|
||||
|
||||
await registerAuth((req, t) => {
|
||||
return t.authenticated({ responseHeaders: authResponseHeader });
|
||||
});
|
||||
|
||||
const router = new Router('/');
|
||||
router.get({ path: '/', validate: false }, (req, res) => res.ok({ header: 'ok' }));
|
||||
registerRouter(router);
|
||||
|
||||
await server.start();
|
||||
|
||||
const response = await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(200);
|
||||
|
||||
expect(response.header['www-authenticate']).toBe(authResponseHeader['www-authenticate']);
|
||||
});
|
||||
|
||||
it('attach security header to an error response', async () => {
|
||||
const authResponseHeader = {
|
||||
'www-authenticate': 'Negotiate ade0234568a4209af8bc0280289eca',
|
||||
};
|
||||
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
|
||||
|
||||
await registerAuth((req, t) => {
|
||||
return t.authenticated({ responseHeaders: authResponseHeader });
|
||||
});
|
||||
|
||||
const router = new Router('/');
|
||||
router.get({ path: '/', validate: false }, (req, res) => res.badRequest(new Error('reason')));
|
||||
registerRouter(router);
|
||||
|
||||
await server.start();
|
||||
|
||||
const response = await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(400);
|
||||
|
||||
expect(response.header['www-authenticate']).toBe(authResponseHeader['www-authenticate']);
|
||||
});
|
||||
|
||||
// TODO un-skip when NP ResponseFactory supports configuring custom headers
|
||||
it.skip('logs warning if Auth Security Header rewrites response header for success response', async () => {
|
||||
const authResponseHeader = {
|
||||
'www-authenticate': 'Negotiate ade0234568a4209af8bc0280289eca',
|
||||
};
|
||||
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
|
||||
|
||||
await registerAuth((req, t) => {
|
||||
return t.authenticated({ responseHeaders: authResponseHeader });
|
||||
});
|
||||
|
||||
const router = new Router('/');
|
||||
router.get({ path: '/', validate: false }, (req, res) => res.ok({}));
|
||||
registerRouter(router);
|
||||
|
||||
await server.start();
|
||||
|
||||
await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(200);
|
||||
|
||||
expect(loggingServiceMock.collect(logger).warn).toMatchInlineSnapshot();
|
||||
});
|
||||
|
||||
it.skip('logs warning if Auth Security Header rewrites response header for error response', async () => {
|
||||
const authResponseHeader = {
|
||||
'www-authenticate': 'Negotiate ade0234568a4209af8bc0280289eca',
|
||||
};
|
||||
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
|
||||
|
||||
await registerAuth((req, t) => {
|
||||
return t.authenticated({ responseHeaders: authResponseHeader });
|
||||
});
|
||||
|
||||
const router = new Router('/');
|
||||
router.get({ path: '/', validate: false }, (req, res) => res.badRequest(new Error('reason')));
|
||||
registerRouter(router);
|
||||
|
||||
await server.start();
|
||||
|
||||
await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(400);
|
||||
|
||||
expect(loggingServiceMock.collect(logger).warn).toMatchInlineSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#auth.isAuthenticated()', () => {
|
||||
it('returns true if has been authorized', async () => {
|
||||
|
@ -943,7 +570,7 @@ describe('setup contract', () => {
|
|||
);
|
||||
registerRouter(router);
|
||||
|
||||
await registerAuth((req, t) => t.authenticated());
|
||||
await registerAuth((req, res, toolkit) => toolkit.authenticated());
|
||||
|
||||
await server.start();
|
||||
await supertest(innerServer.listener)
|
||||
|
@ -962,7 +589,7 @@ describe('setup contract', () => {
|
|||
);
|
||||
registerRouter(router);
|
||||
|
||||
await registerAuth((req, t) => t.authenticated());
|
||||
await registerAuth((req, res, toolkit) => toolkit.authenticated());
|
||||
|
||||
await server.start();
|
||||
await supertest(innerServer.listener)
|
||||
|
@ -997,9 +624,9 @@ describe('setup contract', () => {
|
|||
auth,
|
||||
} = await server.setup(config);
|
||||
const sessionStorageFactory = await createCookieSessionStorageFactory(cookieOptions);
|
||||
registerAuth((req, t) => {
|
||||
registerAuth((req, res, toolkit) => {
|
||||
sessionStorageFactory.asScoped(req).set({ value: user, expires: Date.now() + 1000 });
|
||||
return t.authenticated({ state: user });
|
||||
return toolkit.authenticated({ state: user });
|
||||
});
|
||||
|
||||
const router = new Router('');
|
||||
|
|
|
@ -281,14 +281,14 @@ export class HttpServer {
|
|||
return;
|
||||
}
|
||||
|
||||
this.registerOnPreAuth((request, toolkit) => {
|
||||
this.registerOnPreAuth((request, response, toolkit) => {
|
||||
const oldUrl = request.url.href!;
|
||||
const newURL = basePathService.remove(oldUrl);
|
||||
const shouldRedirect = newURL !== oldUrl;
|
||||
if (shouldRedirect) {
|
||||
return toolkit.redirected(newURL, { forward: true });
|
||||
return toolkit.rewriteUrl(newURL);
|
||||
}
|
||||
return toolkit.rejected(new Error('not found'), { statusCode: 404 });
|
||||
return response.notFound(new Error('not found'));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -304,7 +304,7 @@ export class HttpServer {
|
|||
throw new Error('Server is not created yet');
|
||||
}
|
||||
|
||||
this.server.ext('onPostAuth', adoptToHapiOnPostAuthFormat(fn));
|
||||
this.server.ext('onPostAuth', adoptToHapiOnPostAuthFormat(fn, this.log));
|
||||
}
|
||||
|
||||
private registerOnPreAuth(fn: OnPreAuthHandler) {
|
||||
|
@ -312,7 +312,7 @@ export class HttpServer {
|
|||
throw new Error('Server is not created yet');
|
||||
}
|
||||
|
||||
this.server.ext('onRequest', adoptToHapiOnPreAuthFormat(fn));
|
||||
this.server.ext('onRequest', adoptToHapiOnPreAuthFormat(fn, this.log));
|
||||
}
|
||||
|
||||
private async createCookieSessionStorageFactory<T>(
|
||||
|
@ -345,20 +345,24 @@ export class HttpServer {
|
|||
this.authRegistered = true;
|
||||
|
||||
this.server.auth.scheme('login', () => ({
|
||||
authenticate: adoptToHapiAuthFormat(fn, (req, { state, requestHeaders, responseHeaders }) => {
|
||||
this.authState.set(req, state);
|
||||
authenticate: adoptToHapiAuthFormat(
|
||||
fn,
|
||||
this.log,
|
||||
(req, { state, requestHeaders, responseHeaders }) => {
|
||||
this.authState.set(req, state);
|
||||
|
||||
if (responseHeaders) {
|
||||
this.authResponseHeaders.set(req, responseHeaders);
|
||||
}
|
||||
if (responseHeaders) {
|
||||
this.authResponseHeaders.set(req, responseHeaders);
|
||||
}
|
||||
|
||||
if (requestHeaders) {
|
||||
this.authRequestHeaders.set(req, requestHeaders);
|
||||
// we mutate headers only for the backward compatibility with the legacy platform.
|
||||
// where some plugin read directly from headers to identify whether a user is authenticated.
|
||||
Object.assign(req.headers, requestHeaders);
|
||||
if (requestHeaders) {
|
||||
this.authRequestHeaders.set(req, requestHeaders);
|
||||
// we mutate headers only for the backward compatibility with the legacy platform.
|
||||
// where some plugin read directly from headers to identify whether a user is authenticated.
|
||||
Object.assign(req.headers, requestHeaders);
|
||||
}
|
||||
}
|
||||
}),
|
||||
),
|
||||
}));
|
||||
this.server.auth.strategy('session', 'login');
|
||||
|
||||
|
|
|
@ -18,11 +18,9 @@
|
|||
*/
|
||||
|
||||
import { Server } from 'hapi';
|
||||
import { HttpService } from './http_service';
|
||||
import { HttpServiceSetup } from './http_service';
|
||||
import { HttpService, HttpServiceSetup } from './http_service';
|
||||
import { OnPreAuthToolkit } from './lifecycle/on_pre_auth';
|
||||
import { AuthToolkit } from './lifecycle/auth';
|
||||
import { OnPostAuthToolkit } from './lifecycle/on_post_auth';
|
||||
import { sessionStorageMock } from './cookie_session_storage.mocks';
|
||||
|
||||
type ServiceSetupMockType = jest.Mocked<HttpServiceSetup> & {
|
||||
|
@ -72,20 +70,11 @@ const createHttpServiceMock = () => {
|
|||
|
||||
const createOnPreAuthToolkitMock = (): jest.Mocked<OnPreAuthToolkit> => ({
|
||||
next: jest.fn(),
|
||||
redirected: jest.fn(),
|
||||
rejected: jest.fn(),
|
||||
rewriteUrl: jest.fn(),
|
||||
});
|
||||
|
||||
const createAuthToolkitMock = (): jest.Mocked<AuthToolkit> => ({
|
||||
authenticated: jest.fn(),
|
||||
redirected: jest.fn(),
|
||||
rejected: jest.fn(),
|
||||
});
|
||||
|
||||
const createOnPostAuthToolkitMock = (): jest.Mocked<OnPostAuthToolkit> => ({
|
||||
next: jest.fn(),
|
||||
redirected: jest.fn(),
|
||||
rejected: jest.fn(),
|
||||
});
|
||||
|
||||
export const httpServiceMock = {
|
||||
|
@ -94,5 +83,4 @@ export const httpServiceMock = {
|
|||
createSetupContract: createSetupContractMock,
|
||||
createOnPreAuthToolkit: createOnPreAuthToolkitMock,
|
||||
createAuthToolkit: createAuthToolkitMock,
|
||||
createOnPostAuthToolkit: createOnPostAuthToolkitMock,
|
||||
};
|
||||
|
|
|
@ -31,6 +31,7 @@ export {
|
|||
KibanaRequestRoute,
|
||||
KnownHeaders,
|
||||
LegacyRequest,
|
||||
LifecycleResponseFactory,
|
||||
RedirectResponseOptions,
|
||||
RequestHandler,
|
||||
ResponseError,
|
||||
|
|
308
src/core/server/http/integration_tests/core_services.test.ts
Normal file
308
src/core/server/http/integration_tests/core_services.test.ts
Normal file
|
@ -0,0 +1,308 @@
|
|||
/*
|
||||
* 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 { Request } from 'hapi';
|
||||
import { first } from 'rxjs/operators';
|
||||
import { clusterClientMock } from './core_service.test.mocks';
|
||||
|
||||
import { Router } from '../router';
|
||||
import * as kbnTestServer from '../../../../test_utils/kbn_server';
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
roles?: string[];
|
||||
}
|
||||
|
||||
interface StorageData {
|
||||
value: User;
|
||||
expires: number;
|
||||
}
|
||||
|
||||
describe('http service', () => {
|
||||
describe('legacy server', () => {
|
||||
describe('#registerAuth()', () => {
|
||||
const sessionDurationMs = 1000;
|
||||
const cookieOptions = {
|
||||
name: 'sid',
|
||||
encryptionKey: 'something_at_least_32_characters',
|
||||
validate: (session: StorageData) => true,
|
||||
isSecure: false,
|
||||
path: '/',
|
||||
};
|
||||
|
||||
let root: ReturnType<typeof kbnTestServer.createRoot>;
|
||||
beforeEach(async () => {
|
||||
root = kbnTestServer.createRoot();
|
||||
}, 30000);
|
||||
|
||||
afterEach(async () => {
|
||||
clusterClientMock.mockClear();
|
||||
await root.shutdown();
|
||||
});
|
||||
|
||||
it('runs auth for legacy routes and proxy request to legacy server route handlers', async () => {
|
||||
const { http } = await root.setup();
|
||||
const sessionStorageFactory = await http.createCookieSessionStorageFactory<StorageData>(
|
||||
cookieOptions
|
||||
);
|
||||
http.registerAuth((req, res, toolkit) => {
|
||||
if (req.headers.authorization) {
|
||||
const user = { id: '42' };
|
||||
const sessionStorage = sessionStorageFactory.asScoped(req);
|
||||
sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs });
|
||||
return toolkit.authenticated({ state: user });
|
||||
} else {
|
||||
return res.unauthorized();
|
||||
}
|
||||
});
|
||||
await root.start();
|
||||
|
||||
const legacyUrl = '/legacy';
|
||||
const kbnServer = kbnTestServer.getKbnServer(root);
|
||||
kbnServer.server.route({
|
||||
method: 'GET',
|
||||
path: legacyUrl,
|
||||
handler: () => 'ok from legacy server',
|
||||
});
|
||||
|
||||
const response = await kbnTestServer.request
|
||||
.get(root, legacyUrl)
|
||||
.expect(200, 'ok from legacy server');
|
||||
|
||||
expect(response.header['set-cookie']).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('passes authHeaders as request headers to the legacy platform', async () => {
|
||||
const token = 'Basic: name:password';
|
||||
const { http } = await root.setup();
|
||||
const sessionStorageFactory = await http.createCookieSessionStorageFactory<StorageData>(
|
||||
cookieOptions
|
||||
);
|
||||
http.registerAuth((req, res, toolkit) => {
|
||||
if (req.headers.authorization) {
|
||||
const user = { id: '42' };
|
||||
const sessionStorage = sessionStorageFactory.asScoped(req);
|
||||
sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs });
|
||||
return toolkit.authenticated({
|
||||
state: user,
|
||||
requestHeaders: {
|
||||
authorization: token,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return res.unauthorized();
|
||||
}
|
||||
});
|
||||
await root.start();
|
||||
|
||||
const legacyUrl = '/legacy';
|
||||
const kbnServer = kbnTestServer.getKbnServer(root);
|
||||
kbnServer.server.route({
|
||||
method: 'GET',
|
||||
path: legacyUrl,
|
||||
handler: (req: Request) => ({
|
||||
authorization: req.headers.authorization,
|
||||
custom: req.headers.custom,
|
||||
}),
|
||||
});
|
||||
|
||||
await kbnTestServer.request
|
||||
.get(root, legacyUrl)
|
||||
.set({ custom: 'custom-header' })
|
||||
.expect(200, { authorization: token, custom: 'custom-header' });
|
||||
});
|
||||
|
||||
it('passes associated auth state to Legacy platform', async () => {
|
||||
const user = { id: '42' };
|
||||
|
||||
const { http } = await root.setup();
|
||||
const sessionStorageFactory = await http.createCookieSessionStorageFactory<StorageData>(
|
||||
cookieOptions
|
||||
);
|
||||
http.registerAuth((req, res, toolkit) => {
|
||||
if (req.headers.authorization) {
|
||||
const sessionStorage = sessionStorageFactory.asScoped(req);
|
||||
sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs });
|
||||
return toolkit.authenticated({ state: user });
|
||||
} else {
|
||||
return res.unauthorized();
|
||||
}
|
||||
});
|
||||
await root.start();
|
||||
|
||||
const legacyUrl = '/legacy';
|
||||
const kbnServer = kbnTestServer.getKbnServer(root);
|
||||
kbnServer.server.route({
|
||||
method: 'GET',
|
||||
path: legacyUrl,
|
||||
handler: kbnServer.newPlatform.setup.core.http.auth.get,
|
||||
});
|
||||
|
||||
const response = await kbnTestServer.request.get(root, legacyUrl).expect(200);
|
||||
expect(response.body.state).toEqual(user);
|
||||
expect(response.body.status).toEqual('authenticated');
|
||||
|
||||
expect(response.header['set-cookie']).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('attach security header to a successful response handled by Legacy platform', async () => {
|
||||
const authResponseHeader = {
|
||||
'www-authenticate': 'Negotiate ade0234568a4209af8bc0280289eca',
|
||||
};
|
||||
const { http } = await root.setup();
|
||||
const { registerAuth } = http;
|
||||
|
||||
await registerAuth((req, res, toolkit) => {
|
||||
return toolkit.authenticated({ responseHeaders: authResponseHeader });
|
||||
});
|
||||
|
||||
await root.start();
|
||||
|
||||
const kbnServer = kbnTestServer.getKbnServer(root);
|
||||
kbnServer.server.route({
|
||||
method: 'GET',
|
||||
path: '/legacy',
|
||||
handler: () => 'ok',
|
||||
});
|
||||
|
||||
const response = await kbnTestServer.request.get(root, '/legacy').expect(200);
|
||||
expect(response.header['www-authenticate']).toBe(authResponseHeader['www-authenticate']);
|
||||
});
|
||||
|
||||
it('attach security header to an error response handled by Legacy platform', async () => {
|
||||
const authResponseHeader = {
|
||||
'www-authenticate': 'Negotiate ade0234568a4209af8bc0280289eca',
|
||||
};
|
||||
const { http } = await root.setup();
|
||||
const { registerAuth } = http;
|
||||
|
||||
await registerAuth((req, res, toolkit) => {
|
||||
return toolkit.authenticated({ responseHeaders: authResponseHeader });
|
||||
});
|
||||
|
||||
await root.start();
|
||||
|
||||
const kbnServer = kbnTestServer.getKbnServer(root);
|
||||
kbnServer.server.route({
|
||||
method: 'GET',
|
||||
path: '/legacy',
|
||||
handler: () => {
|
||||
throw Boom.badRequest();
|
||||
},
|
||||
});
|
||||
|
||||
const response = await kbnTestServer.request.get(root, '/legacy').expect(400);
|
||||
expect(response.header['www-authenticate']).toBe(authResponseHeader['www-authenticate']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#basePath()', () => {
|
||||
let root: ReturnType<typeof kbnTestServer.createRoot>;
|
||||
beforeEach(async () => {
|
||||
root = kbnTestServer.createRoot();
|
||||
}, 30000);
|
||||
|
||||
afterEach(async () => await root.shutdown());
|
||||
it('basePath information for an incoming request is available in legacy server', async () => {
|
||||
const reqBasePath = '/requests-specific-base-path';
|
||||
const { http } = await root.setup();
|
||||
http.registerOnPreAuth((req, res, toolkit) => {
|
||||
http.basePath.set(req, reqBasePath);
|
||||
return toolkit.next();
|
||||
});
|
||||
|
||||
await root.start();
|
||||
|
||||
const legacyUrl = '/legacy';
|
||||
const kbnServer = kbnTestServer.getKbnServer(root);
|
||||
kbnServer.server.route({
|
||||
method: 'GET',
|
||||
path: legacyUrl,
|
||||
handler: kbnServer.newPlatform.setup.core.http.basePath.get,
|
||||
});
|
||||
|
||||
await kbnTestServer.request.get(root, legacyUrl).expect(200, reqBasePath);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('elasticsearch', () => {
|
||||
let root: ReturnType<typeof kbnTestServer.createRoot>;
|
||||
beforeEach(async () => {
|
||||
root = kbnTestServer.createRoot();
|
||||
}, 30000);
|
||||
|
||||
afterEach(async () => {
|
||||
clusterClientMock.mockClear();
|
||||
await root.shutdown();
|
||||
});
|
||||
it('rewrites authorization header via authHeaders to make a request to Elasticsearch', async () => {
|
||||
const authHeaders = { authorization: 'Basic: user:password' };
|
||||
const { http, elasticsearch } = await root.setup();
|
||||
const { registerAuth, registerRouter } = http;
|
||||
|
||||
await registerAuth((req, res, toolkit) =>
|
||||
toolkit.authenticated({ requestHeaders: authHeaders })
|
||||
);
|
||||
|
||||
const router = new Router('/new-platform');
|
||||
router.get({ path: '/', validate: false }, async (req, res) => {
|
||||
const client = await elasticsearch.dataClient$.pipe(first()).toPromise();
|
||||
client.asScoped(req);
|
||||
return res.ok({ header: 'ok' });
|
||||
});
|
||||
registerRouter(router);
|
||||
|
||||
await root.start();
|
||||
|
||||
await kbnTestServer.request.get(root, '/new-platform/').expect(200);
|
||||
expect(clusterClientMock).toBeCalledTimes(1);
|
||||
const [firstCall] = clusterClientMock.mock.calls;
|
||||
const [, , headers] = firstCall;
|
||||
expect(headers).toEqual(authHeaders);
|
||||
});
|
||||
|
||||
it('passes request authorization header to Elasticsearch if registerAuth was not set', async () => {
|
||||
const authorizationHeader = 'Basic: username:password';
|
||||
const { http, elasticsearch } = await root.setup();
|
||||
const { registerRouter } = http;
|
||||
|
||||
const router = new Router('/new-platform');
|
||||
router.get({ path: '/', validate: false }, async (req, res) => {
|
||||
const client = await elasticsearch.dataClient$.pipe(first()).toPromise();
|
||||
client.asScoped(req);
|
||||
return res.ok({ header: 'ok' });
|
||||
});
|
||||
registerRouter(router);
|
||||
|
||||
await root.start();
|
||||
|
||||
await kbnTestServer.request
|
||||
.get(root, '/new-platform/')
|
||||
.set('Authorization', authorizationHeader)
|
||||
.expect(200);
|
||||
|
||||
expect(clusterClientMock).toBeCalledTimes(1);
|
||||
const [firstCall] = clusterClientMock.mock.calls;
|
||||
const [, , headers] = firstCall;
|
||||
expect(headers).toEqual({
|
||||
authorization: authorizationHeader,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,428 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import Boom from 'boom';
|
||||
import { Request } from 'hapi';
|
||||
import { first } from 'rxjs/operators';
|
||||
import { clusterClientMock } from './http_service.test.mocks';
|
||||
|
||||
import { Router } from '../router';
|
||||
import * as kbnTestServer from '../../../../test_utils/kbn_server';
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
roles?: string[];
|
||||
}
|
||||
|
||||
interface StorageData {
|
||||
value: User;
|
||||
expires: number;
|
||||
}
|
||||
|
||||
describe('http service', () => {
|
||||
describe('setup contract', () => {
|
||||
describe('#registerAuth()', () => {
|
||||
const sessionDurationMs = 1000;
|
||||
const cookieOptions = {
|
||||
name: 'sid',
|
||||
encryptionKey: 'something_at_least_32_characters',
|
||||
validate: (session: StorageData) => true,
|
||||
isSecure: false,
|
||||
path: '/',
|
||||
};
|
||||
|
||||
let root: ReturnType<typeof kbnTestServer.createRoot>;
|
||||
beforeEach(async () => {
|
||||
root = kbnTestServer.createRoot();
|
||||
}, 30000);
|
||||
|
||||
afterEach(async () => {
|
||||
clusterClientMock.mockClear();
|
||||
await root.shutdown();
|
||||
});
|
||||
|
||||
it('runs auth for legacy routes and proxy request to legacy server route handlers', async () => {
|
||||
const { http } = await root.setup();
|
||||
const sessionStorageFactory = await http.createCookieSessionStorageFactory<StorageData>(
|
||||
cookieOptions
|
||||
);
|
||||
http.registerAuth((req, t) => {
|
||||
if (req.headers.authorization) {
|
||||
const user = { id: '42' };
|
||||
const sessionStorage = sessionStorageFactory.asScoped(req);
|
||||
sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs });
|
||||
return t.authenticated({ state: user });
|
||||
} else {
|
||||
return t.rejected(Boom.unauthorized());
|
||||
}
|
||||
});
|
||||
await root.start();
|
||||
|
||||
const legacyUrl = '/legacy';
|
||||
const kbnServer = kbnTestServer.getKbnServer(root);
|
||||
kbnServer.server.route({
|
||||
method: 'GET',
|
||||
path: legacyUrl,
|
||||
handler: () => 'ok from legacy server',
|
||||
});
|
||||
|
||||
const response = await kbnTestServer.request
|
||||
.get(root, legacyUrl)
|
||||
.expect(200, 'ok from legacy server');
|
||||
|
||||
expect(response.header['set-cookie']).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('passes authHeaders as request headers to the legacy platform', async () => {
|
||||
const token = 'Basic: name:password';
|
||||
const { http } = await root.setup();
|
||||
const sessionStorageFactory = await http.createCookieSessionStorageFactory<StorageData>(
|
||||
cookieOptions
|
||||
);
|
||||
http.registerAuth((req, t) => {
|
||||
if (req.headers.authorization) {
|
||||
const user = { id: '42' };
|
||||
const sessionStorage = sessionStorageFactory.asScoped(req);
|
||||
sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs });
|
||||
return t.authenticated({
|
||||
state: user,
|
||||
requestHeaders: {
|
||||
authorization: token,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return t.rejected(Boom.unauthorized());
|
||||
}
|
||||
});
|
||||
await root.start();
|
||||
|
||||
const legacyUrl = '/legacy';
|
||||
const kbnServer = kbnTestServer.getKbnServer(root);
|
||||
kbnServer.server.route({
|
||||
method: 'GET',
|
||||
path: legacyUrl,
|
||||
handler: (req: Request) => ({
|
||||
authorization: req.headers.authorization,
|
||||
custom: req.headers.custom,
|
||||
}),
|
||||
});
|
||||
|
||||
await kbnTestServer.request
|
||||
.get(root, legacyUrl)
|
||||
.set({ custom: 'custom-header' })
|
||||
.expect(200, { authorization: token, custom: 'custom-header' });
|
||||
});
|
||||
|
||||
it('passes associated auth state to Legacy platform', async () => {
|
||||
const user = { id: '42' };
|
||||
|
||||
const { http } = await root.setup();
|
||||
const sessionStorageFactory = await http.createCookieSessionStorageFactory<StorageData>(
|
||||
cookieOptions
|
||||
);
|
||||
http.registerAuth((req, t) => {
|
||||
if (req.headers.authorization) {
|
||||
const sessionStorage = sessionStorageFactory.asScoped(req);
|
||||
sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs });
|
||||
return t.authenticated({ state: user });
|
||||
} else {
|
||||
return t.rejected(Boom.unauthorized());
|
||||
}
|
||||
});
|
||||
await root.start();
|
||||
|
||||
const legacyUrl = '/legacy';
|
||||
const kbnServer = kbnTestServer.getKbnServer(root);
|
||||
kbnServer.server.route({
|
||||
method: 'GET',
|
||||
path: legacyUrl,
|
||||
handler: kbnServer.newPlatform.setup.core.http.auth.get,
|
||||
});
|
||||
|
||||
const response = await kbnTestServer.request.get(root, legacyUrl).expect(200);
|
||||
expect(response.body.state).toEqual(user);
|
||||
expect(response.body.status).toEqual('authenticated');
|
||||
|
||||
expect(response.header['set-cookie']).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('rewrites authorization header via authHeaders to make a request to Elasticsearch', async () => {
|
||||
const authHeaders = { authorization: 'Basic: user:password' };
|
||||
const { http, elasticsearch } = await root.setup();
|
||||
const { registerAuth, registerRouter } = http;
|
||||
|
||||
await registerAuth((req, t) => {
|
||||
return t.authenticated({ requestHeaders: authHeaders });
|
||||
});
|
||||
|
||||
const router = new Router('/new-platform');
|
||||
router.get({ path: '/', validate: false }, async (req, res) => {
|
||||
const client = await elasticsearch.dataClient$.pipe(first()).toPromise();
|
||||
client.asScoped(req);
|
||||
return res.ok({ header: 'ok' });
|
||||
});
|
||||
registerRouter(router);
|
||||
|
||||
await root.start();
|
||||
|
||||
await kbnTestServer.request.get(root, '/new-platform/').expect(200);
|
||||
expect(clusterClientMock).toBeCalledTimes(1);
|
||||
const [firstCall] = clusterClientMock.mock.calls;
|
||||
const [, , headers] = firstCall;
|
||||
expect(headers).toEqual(authHeaders);
|
||||
});
|
||||
|
||||
it('passes request authorization header to Elasticsearch if registerAuth was not set', async () => {
|
||||
const authorizationHeader = 'Basic: username:password';
|
||||
const { http, elasticsearch } = await root.setup();
|
||||
const { registerRouter } = http;
|
||||
|
||||
const router = new Router('/new-platform');
|
||||
router.get({ path: '/', validate: false }, async (req, res) => {
|
||||
const client = await elasticsearch.dataClient$.pipe(first()).toPromise();
|
||||
client.asScoped(req);
|
||||
return res.ok({ header: 'ok' });
|
||||
});
|
||||
registerRouter(router);
|
||||
|
||||
await root.start();
|
||||
|
||||
await kbnTestServer.request
|
||||
.get(root, '/new-platform/')
|
||||
.set('Authorization', authorizationHeader)
|
||||
.expect(200);
|
||||
|
||||
expect(clusterClientMock).toBeCalledTimes(1);
|
||||
const [firstCall] = clusterClientMock.mock.calls;
|
||||
const [, , headers] = firstCall;
|
||||
expect(headers).toEqual({
|
||||
authorization: authorizationHeader,
|
||||
});
|
||||
});
|
||||
|
||||
it('attach security header to a successful response handled by Legacy platform', async () => {
|
||||
const authResponseHeader = {
|
||||
'www-authenticate': 'Negotiate ade0234568a4209af8bc0280289eca',
|
||||
};
|
||||
const { http } = await root.setup();
|
||||
const { registerAuth } = http;
|
||||
|
||||
await registerAuth((req, t) => {
|
||||
return t.authenticated({ responseHeaders: authResponseHeader });
|
||||
});
|
||||
|
||||
await root.start();
|
||||
|
||||
const kbnServer = kbnTestServer.getKbnServer(root);
|
||||
kbnServer.server.route({
|
||||
method: 'GET',
|
||||
path: '/legacy',
|
||||
handler: () => 'ok',
|
||||
});
|
||||
|
||||
const response = await kbnTestServer.request.get(root, '/legacy').expect(200);
|
||||
expect(response.header['www-authenticate']).toBe(authResponseHeader['www-authenticate']);
|
||||
});
|
||||
|
||||
it('attach security header to an error response handled by Legacy platform', async () => {
|
||||
const authResponseHeader = {
|
||||
'www-authenticate': 'Negotiate ade0234568a4209af8bc0280289eca',
|
||||
};
|
||||
const { http } = await root.setup();
|
||||
const { registerAuth } = http;
|
||||
|
||||
await registerAuth((req, t) => {
|
||||
return t.authenticated({ responseHeaders: authResponseHeader });
|
||||
});
|
||||
|
||||
await root.start();
|
||||
|
||||
const kbnServer = kbnTestServer.getKbnServer(root);
|
||||
kbnServer.server.route({
|
||||
method: 'GET',
|
||||
path: '/legacy',
|
||||
handler: () => {
|
||||
throw Boom.badRequest();
|
||||
},
|
||||
});
|
||||
|
||||
const response = await kbnTestServer.request.get(root, '/legacy').expect(400);
|
||||
expect(response.header['www-authenticate']).toBe(authResponseHeader['www-authenticate']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#registerOnPostAuth()', () => {
|
||||
let root: ReturnType<typeof kbnTestServer.createRoot>;
|
||||
beforeEach(async () => {
|
||||
root = kbnTestServer.createRoot();
|
||||
}, 30000);
|
||||
afterEach(async () => await root.shutdown());
|
||||
|
||||
it('supports passing request through to the route handler', async () => {
|
||||
const router = new Router('/new-platform');
|
||||
router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' }));
|
||||
|
||||
const { http } = await root.setup();
|
||||
http.registerOnPostAuth((req, t) => t.next());
|
||||
http.registerOnPostAuth(async (req, t) => {
|
||||
await Promise.resolve();
|
||||
return t.next();
|
||||
});
|
||||
http.registerRouter(router);
|
||||
await root.start();
|
||||
|
||||
await kbnTestServer.request.get(root, '/new-platform/').expect(200, { content: 'ok' });
|
||||
});
|
||||
|
||||
it('supports redirecting to configured url', async () => {
|
||||
const redirectTo = '/redirect-url';
|
||||
const { http } = await root.setup();
|
||||
http.registerOnPostAuth(async (req, t) => t.redirected(redirectTo));
|
||||
await root.start();
|
||||
|
||||
const response = await kbnTestServer.request.get(root, '/new-platform/').expect(302);
|
||||
expect(response.header.location).toBe(redirectTo);
|
||||
});
|
||||
|
||||
it('fails a request with configured error and status code', async () => {
|
||||
const { http } = await root.setup();
|
||||
http.registerOnPostAuth(async (req, t) =>
|
||||
t.rejected(new Error('unexpected error'), { statusCode: 400 })
|
||||
);
|
||||
await root.start();
|
||||
|
||||
await kbnTestServer.request
|
||||
.get(root, '/new-platform/')
|
||||
.expect(400, { statusCode: 400, error: 'Bad Request', message: 'unexpected error' });
|
||||
});
|
||||
|
||||
it(`doesn't expose internal error details`, async () => {
|
||||
const { http } = await root.setup();
|
||||
http.registerOnPostAuth(async (req, t) => {
|
||||
throw new Error('sensitive info');
|
||||
});
|
||||
await root.start();
|
||||
|
||||
await kbnTestServer.request.get(root, '/new-platform/').expect({
|
||||
statusCode: 500,
|
||||
error: 'Internal Server Error',
|
||||
message: 'An internal server error occurred',
|
||||
});
|
||||
});
|
||||
|
||||
it(`doesn't share request object between interceptors`, async () => {
|
||||
const { http } = await root.setup();
|
||||
http.registerOnPostAuth(async (req, t) => {
|
||||
// @ts-ignore. don't complain customField is not defined on Request type
|
||||
req.customField = { value: 42 };
|
||||
return t.next();
|
||||
});
|
||||
http.registerOnPostAuth((req, t) => {
|
||||
// @ts-ignore don't complain customField is not defined on Request type
|
||||
if (typeof req.customField !== 'undefined') {
|
||||
throw new Error('Request object was mutated');
|
||||
}
|
||||
return t.next();
|
||||
});
|
||||
const router = new Router('/new-platform');
|
||||
router.get({ path: '/', validate: false }, async (req, res) =>
|
||||
// @ts-ignore. don't complain customField is not defined on Request type
|
||||
res.ok({ customField: String(req.customField) })
|
||||
);
|
||||
http.registerRouter(router);
|
||||
await root.start();
|
||||
|
||||
await kbnTestServer.request
|
||||
.get(root, '/new-platform/')
|
||||
.expect(200, { customField: 'undefined' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('#registerOnPostAuth() toolkit', () => {
|
||||
let root: ReturnType<typeof kbnTestServer.createRoot>;
|
||||
beforeEach(async () => {
|
||||
root = kbnTestServer.createRoot();
|
||||
}, 30000);
|
||||
|
||||
afterEach(async () => await root.shutdown());
|
||||
it('supports Url change on the flight', async () => {
|
||||
const { http } = await root.setup();
|
||||
http.registerOnPreAuth((req, t) => {
|
||||
return t.redirected('/new-platform/new-url', { forward: true });
|
||||
});
|
||||
|
||||
const router = new Router('/new-platform');
|
||||
router.get({ path: '/new-url', validate: false }, async (req, res) =>
|
||||
res.ok({ key: 'new-url-reached' })
|
||||
);
|
||||
http.registerRouter(router);
|
||||
|
||||
await root.start();
|
||||
|
||||
await kbnTestServer.request.get(root, '/').expect(200, { key: 'new-url-reached' });
|
||||
});
|
||||
|
||||
it('url re-write works for legacy server as well', async () => {
|
||||
const { http } = await root.setup();
|
||||
const newUrl = '/new-url';
|
||||
http.registerOnPreAuth((req, t) => {
|
||||
return t.redirected(newUrl, { forward: true });
|
||||
});
|
||||
|
||||
await root.start();
|
||||
const kbnServer = kbnTestServer.getKbnServer(root);
|
||||
kbnServer.server.route({
|
||||
method: 'GET',
|
||||
path: newUrl,
|
||||
handler: () => 'ok-from-legacy',
|
||||
});
|
||||
|
||||
await kbnTestServer.request.get(root, '/').expect(200, 'ok-from-legacy');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#basePath()', () => {
|
||||
let root: ReturnType<typeof kbnTestServer.createRoot>;
|
||||
beforeEach(async () => {
|
||||
root = kbnTestServer.createRoot();
|
||||
}, 30000);
|
||||
|
||||
afterEach(async () => await root.shutdown());
|
||||
it('basePath information for an incoming request is available in legacy server', async () => {
|
||||
const reqBasePath = '/requests-specific-base-path';
|
||||
const { http } = await root.setup();
|
||||
http.registerOnPreAuth((req, t) => {
|
||||
http.basePath.set(req, reqBasePath);
|
||||
return t.next();
|
||||
});
|
||||
|
||||
await root.start();
|
||||
|
||||
const legacyUrl = '/legacy';
|
||||
const kbnServer = kbnTestServer.getKbnServer(root);
|
||||
kbnServer.server.route({
|
||||
method: 'GET',
|
||||
path: legacyUrl,
|
||||
handler: kbnServer.newPlatform.setup.core.http.basePath.get,
|
||||
});
|
||||
|
||||
await kbnTestServer.request.get(root, legacyUrl).expect(200, reqBasePath);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
923
src/core/server/http/integration_tests/lifecycle.test.ts
Normal file
923
src/core/server/http/integration_tests/lifecycle.test.ts
Normal file
|
@ -0,0 +1,923 @@
|
|||
/*
|
||||
* 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 supertest from 'supertest';
|
||||
import { ByteSizeValue } from '@kbn/config-schema';
|
||||
import request from 'request';
|
||||
|
||||
import { HttpConfig, Router } from '..';
|
||||
import { ensureRawRequest } from '../router';
|
||||
import { HttpServer } from '../http_server';
|
||||
|
||||
import { LoggerFactory } from '../../logging';
|
||||
import { loggingServiceMock } from '../../logging/logging_service.mock';
|
||||
|
||||
let server: HttpServer;
|
||||
let logger: LoggerFactory;
|
||||
|
||||
const config = {
|
||||
host: '127.0.0.1',
|
||||
maxPayload: new ByteSizeValue(1024),
|
||||
port: 10001,
|
||||
ssl: { enabled: false },
|
||||
} as HttpConfig;
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
roles?: string[];
|
||||
}
|
||||
|
||||
interface StorageData {
|
||||
value: User;
|
||||
expires: number;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
logger = loggingServiceMock.create();
|
||||
server = new HttpServer(logger, 'tests');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await server.stop();
|
||||
});
|
||||
|
||||
describe('OnPreAuth', () => {
|
||||
it('supports registering request inceptors', async () => {
|
||||
const router = new Router('/');
|
||||
|
||||
router.get({ path: '/', validate: false }, (req, res) => res.ok('ok'));
|
||||
|
||||
const { registerRouter, registerOnPreAuth, server: innerServer } = await server.setup(config);
|
||||
registerRouter(router);
|
||||
|
||||
const callingOrder: string[] = [];
|
||||
registerOnPreAuth((req, res, t) => {
|
||||
callingOrder.push('first');
|
||||
return t.next();
|
||||
});
|
||||
|
||||
registerOnPreAuth((req, res, t) => {
|
||||
callingOrder.push('second');
|
||||
return t.next();
|
||||
});
|
||||
await server.start();
|
||||
|
||||
await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(200, 'ok');
|
||||
|
||||
expect(callingOrder).toEqual(['first', 'second']);
|
||||
});
|
||||
|
||||
it('supports request forwarding to specified url', async () => {
|
||||
const router = new Router('/');
|
||||
|
||||
router.get({ path: '/initial', validate: false }, (req, res) => res.ok('initial'));
|
||||
router.get({ path: '/redirectUrl', validate: false }, (req, res) => res.ok('redirected'));
|
||||
|
||||
const { registerRouter, registerOnPreAuth, server: innerServer } = await server.setup(config);
|
||||
registerRouter(router);
|
||||
|
||||
let urlBeforeForwarding;
|
||||
registerOnPreAuth((req, res, t) => {
|
||||
urlBeforeForwarding = ensureRawRequest(req).raw.req.url;
|
||||
return t.rewriteUrl('/redirectUrl');
|
||||
});
|
||||
|
||||
let urlAfterForwarding;
|
||||
registerOnPreAuth((req, res, t) => {
|
||||
// used by legacy platform
|
||||
urlAfterForwarding = ensureRawRequest(req).raw.req.url;
|
||||
return t.next();
|
||||
});
|
||||
|
||||
await server.start();
|
||||
|
||||
await supertest(innerServer.listener)
|
||||
.get('/initial')
|
||||
.expect(200, 'redirected');
|
||||
|
||||
expect(urlBeforeForwarding).toBe('/initial');
|
||||
expect(urlAfterForwarding).toBe('/redirectUrl');
|
||||
});
|
||||
|
||||
it('supports redirection from the interceptor', async () => {
|
||||
const router = new Router('/');
|
||||
const redirectUrl = '/redirectUrl';
|
||||
router.get({ path: '/initial', validate: false }, (req, res) => res.ok('initial'));
|
||||
|
||||
const { registerRouter, registerOnPreAuth, server: innerServer } = await server.setup(config);
|
||||
registerRouter(router);
|
||||
|
||||
registerOnPreAuth((req, res, t) =>
|
||||
res.redirected(undefined, {
|
||||
headers: {
|
||||
location: redirectUrl,
|
||||
},
|
||||
})
|
||||
);
|
||||
await server.start();
|
||||
|
||||
const result = await supertest(innerServer.listener)
|
||||
.get('/initial')
|
||||
.expect(302);
|
||||
|
||||
expect(result.header.location).toBe(redirectUrl);
|
||||
});
|
||||
|
||||
it('supports rejecting request and adjusting response headers', async () => {
|
||||
const router = new Router('/');
|
||||
router.get({ path: '/', validate: false }, (req, res) => res.ok(undefined));
|
||||
|
||||
const { registerRouter, registerOnPreAuth, server: innerServer } = await server.setup(config);
|
||||
registerRouter(router);
|
||||
|
||||
registerOnPreAuth((req, res, t) =>
|
||||
res.unauthorized('not found error', {
|
||||
headers: {
|
||||
'www-authenticate': 'challenge',
|
||||
},
|
||||
})
|
||||
);
|
||||
await server.start();
|
||||
|
||||
const result = await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(401);
|
||||
|
||||
expect(result.header['www-authenticate']).toBe('challenge');
|
||||
});
|
||||
|
||||
it("doesn't expose error details if interceptor throws", async () => {
|
||||
const router = new Router('/');
|
||||
router.get({ path: '/', validate: false }, (req, res) => res.ok(undefined));
|
||||
|
||||
const { registerRouter, registerOnPreAuth, server: innerServer } = await server.setup(config);
|
||||
registerRouter(router);
|
||||
|
||||
registerOnPreAuth((req, res, t) => {
|
||||
throw new Error('reason');
|
||||
});
|
||||
await server.start();
|
||||
|
||||
const result = await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(500);
|
||||
|
||||
expect(result.body.message).toBe('An internal server error occurred.');
|
||||
expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
[Error: reason],
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns internal error if interceptor returns unexpected result', async () => {
|
||||
const router = new Router('/');
|
||||
router.get({ path: '/', validate: false }, (req, res) => res.ok('ok'));
|
||||
|
||||
const { registerRouter, registerOnPreAuth, server: innerServer } = await server.setup(config);
|
||||
registerRouter(router);
|
||||
|
||||
registerOnPreAuth((req, res, t) => ({} as any));
|
||||
await server.start();
|
||||
|
||||
const result = await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(500);
|
||||
|
||||
expect(result.body.message).toBe('An internal server error occurred.');
|
||||
expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
[Error: Unexpected result from OnPreAuth. Expected OnPreAuthResult or KibanaResponse, but given: [object Object].],
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it(`doesn't share request object between interceptors`, async () => {
|
||||
const { registerRouter, registerOnPreAuth, server: innerServer } = await server.setup(config);
|
||||
registerOnPreAuth((req, res, t) => {
|
||||
// @ts-ignore. don't complain customField is not defined on Request type
|
||||
req.customField = { value: 42 };
|
||||
return t.next();
|
||||
});
|
||||
registerOnPreAuth((req, res, t) => {
|
||||
// @ts-ignore don't complain customField is not defined on Request type
|
||||
if (typeof req.customField !== 'undefined') {
|
||||
throw new Error('Request object was mutated');
|
||||
}
|
||||
return t.next();
|
||||
});
|
||||
const router = new Router('/');
|
||||
router.get({ path: '/', validate: false }, async (req, res) =>
|
||||
// @ts-ignore. don't complain customField is not defined on Request type
|
||||
res.ok({ customField: String(req.customField) })
|
||||
);
|
||||
registerRouter(router);
|
||||
await server.start();
|
||||
|
||||
await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(200, { customField: 'undefined' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('OnPostAuth', () => {
|
||||
it('supports registering request inceptors', async () => {
|
||||
const router = new Router('/');
|
||||
|
||||
router.get({ path: '/', validate: false }, (req, res) => res.ok('ok'));
|
||||
|
||||
const { registerRouter, registerOnPostAuth, server: innerServer } = await server.setup(config);
|
||||
registerRouter(router);
|
||||
|
||||
const callingOrder: string[] = [];
|
||||
registerOnPostAuth((req, res, t) => {
|
||||
callingOrder.push('first');
|
||||
return t.next();
|
||||
});
|
||||
|
||||
registerOnPostAuth((req, res, t) => {
|
||||
callingOrder.push('second');
|
||||
return t.next();
|
||||
});
|
||||
await server.start();
|
||||
|
||||
await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(200, 'ok');
|
||||
|
||||
expect(callingOrder).toEqual(['first', 'second']);
|
||||
});
|
||||
|
||||
it('supports redirection from the interceptor', async () => {
|
||||
const router = new Router('/');
|
||||
const redirectUrl = '/redirectUrl';
|
||||
router.get({ path: '/initial', validate: false }, (req, res) => res.ok('initial'));
|
||||
|
||||
const { registerRouter, registerOnPostAuth, server: innerServer } = await server.setup(config);
|
||||
registerRouter(router);
|
||||
|
||||
registerOnPostAuth((req, res, t) =>
|
||||
res.redirected(undefined, {
|
||||
headers: {
|
||||
location: redirectUrl,
|
||||
},
|
||||
})
|
||||
);
|
||||
await server.start();
|
||||
|
||||
const result = await supertest(innerServer.listener)
|
||||
.get('/initial')
|
||||
.expect(302);
|
||||
|
||||
expect(result.header.location).toBe(redirectUrl);
|
||||
});
|
||||
|
||||
it('supports rejecting request and adjusting response headers', async () => {
|
||||
const router = new Router('/');
|
||||
router.get({ path: '/', validate: false }, (req, res) => res.ok(undefined));
|
||||
|
||||
const { registerRouter, registerOnPostAuth, server: innerServer } = await server.setup(config);
|
||||
registerRouter(router);
|
||||
|
||||
registerOnPostAuth((req, res, t) =>
|
||||
res.unauthorized('not found error', {
|
||||
headers: {
|
||||
'www-authenticate': 'challenge',
|
||||
},
|
||||
})
|
||||
);
|
||||
await server.start();
|
||||
|
||||
const result = await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(401);
|
||||
|
||||
expect(result.header['www-authenticate']).toBe('challenge');
|
||||
});
|
||||
|
||||
it("doesn't expose error details if interceptor throws", async () => {
|
||||
const router = new Router('/');
|
||||
router.get({ path: '/', validate: false }, (req, res) => res.ok(undefined));
|
||||
|
||||
const { registerRouter, registerOnPostAuth, server: innerServer } = await server.setup(config);
|
||||
registerRouter(router);
|
||||
|
||||
registerOnPostAuth((req, res, t) => {
|
||||
throw new Error('reason');
|
||||
});
|
||||
await server.start();
|
||||
|
||||
const result = await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(500);
|
||||
|
||||
expect(result.body.message).toBe('An internal server error occurred.');
|
||||
expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
[Error: reason],
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns internal error if interceptor returns unexpected result', async () => {
|
||||
const router = new Router('/');
|
||||
router.get({ path: '/', validate: false }, (req, res) => res.ok('ok'));
|
||||
|
||||
const { registerRouter, registerOnPostAuth, server: innerServer } = await server.setup(config);
|
||||
registerRouter(router);
|
||||
|
||||
registerOnPostAuth((req, res, t) => ({} as any));
|
||||
await server.start();
|
||||
|
||||
const result = await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(500);
|
||||
|
||||
expect(result.body.message).toBe('An internal server error occurred.');
|
||||
expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
[Error: Unexpected result from OnPostAuth. Expected OnPostAuthResult or KibanaResponse, but given: [object Object].],
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it(`doesn't share request object between interceptors`, async () => {
|
||||
const { registerRouter, registerOnPostAuth, server: innerServer } = await server.setup(config);
|
||||
registerOnPostAuth((req, res, t) => {
|
||||
// @ts-ignore. don't complain customField is not defined on Request type
|
||||
req.customField = { value: 42 };
|
||||
return t.next();
|
||||
});
|
||||
registerOnPostAuth((req, res, t) => {
|
||||
// @ts-ignore don't complain customField is not defined on Request type
|
||||
if (typeof req.customField !== 'undefined') {
|
||||
throw new Error('Request object was mutated');
|
||||
}
|
||||
return t.next();
|
||||
});
|
||||
const router = new Router('/');
|
||||
router.get({ path: '/', validate: false }, async (req, res) =>
|
||||
// @ts-ignore. don't complain customField is not defined on Request type
|
||||
res.ok({ customField: String(req.customField) })
|
||||
);
|
||||
registerRouter(router);
|
||||
await server.start();
|
||||
|
||||
await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(200, { customField: 'undefined' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Auth', () => {
|
||||
const cookieOptions = {
|
||||
name: 'sid',
|
||||
encryptionKey: 'something_at_least_32_characters',
|
||||
validate: () => true,
|
||||
isSecure: false,
|
||||
};
|
||||
|
||||
it('registers auth request interceptor only once', async () => {
|
||||
const { registerAuth } = await server.setup(config);
|
||||
const doRegister = () => registerAuth(() => null as any);
|
||||
|
||||
doRegister();
|
||||
expect(doRegister).toThrowError('Auth interceptor was already registered');
|
||||
});
|
||||
|
||||
it('may grant access to a resource', async () => {
|
||||
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
|
||||
const router = new Router('');
|
||||
router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' }));
|
||||
registerRouter(router);
|
||||
|
||||
registerAuth((req, res, t) => t.authenticated());
|
||||
await server.start();
|
||||
|
||||
await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(200, { content: 'ok' });
|
||||
});
|
||||
|
||||
it('enables auth for a route by default if registerAuth has been called', async () => {
|
||||
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
|
||||
|
||||
const router = new Router('');
|
||||
router.get({ path: '/', validate: false }, (req, res) =>
|
||||
res.ok({ authRequired: req.route.options.authRequired })
|
||||
);
|
||||
registerRouter(router);
|
||||
|
||||
const authenticate = jest.fn().mockImplementation((req, res, t) => t.authenticated());
|
||||
registerAuth(authenticate);
|
||||
|
||||
await server.start();
|
||||
await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(200, { authRequired: true });
|
||||
|
||||
expect(authenticate).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('supports disabling auth for a route explicitly', async () => {
|
||||
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
|
||||
|
||||
const router = new Router('');
|
||||
router.get({ path: '/', validate: false, options: { authRequired: false } }, (req, res) =>
|
||||
res.ok({ authRequired: req.route.options.authRequired })
|
||||
);
|
||||
registerRouter(router);
|
||||
const authenticate = jest.fn();
|
||||
registerAuth(authenticate);
|
||||
|
||||
await server.start();
|
||||
await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(200, { authRequired: false });
|
||||
|
||||
expect(authenticate).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('supports enabling auth for a route explicitly', async () => {
|
||||
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
|
||||
|
||||
const router = new Router('');
|
||||
router.get({ path: '/', validate: false, options: { authRequired: true } }, (req, res) =>
|
||||
res.ok({ authRequired: req.route.options.authRequired })
|
||||
);
|
||||
registerRouter(router);
|
||||
const authenticate = jest.fn().mockImplementation((req, res, t) => t.authenticated({}));
|
||||
await registerAuth(authenticate);
|
||||
|
||||
await server.start();
|
||||
await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(200, { authRequired: true });
|
||||
|
||||
expect(authenticate).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('supports rejecting a request from an unauthenticated user', async () => {
|
||||
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
|
||||
const router = new Router('');
|
||||
router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' }));
|
||||
registerRouter(router);
|
||||
|
||||
registerAuth((req, res) => res.unauthorized());
|
||||
await server.start();
|
||||
|
||||
await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(401);
|
||||
});
|
||||
|
||||
it('supports redirecting', async () => {
|
||||
const redirectTo = '/redirect-url';
|
||||
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
|
||||
const router = new Router('');
|
||||
router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' }));
|
||||
registerRouter(router);
|
||||
|
||||
registerAuth((req, res) =>
|
||||
res.redirected(undefined, {
|
||||
headers: {
|
||||
location: redirectTo,
|
||||
},
|
||||
})
|
||||
);
|
||||
await server.start();
|
||||
|
||||
const response = await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(302);
|
||||
expect(response.header.location).toBe(redirectTo);
|
||||
});
|
||||
|
||||
it(`doesn't expose internal error details`, async () => {
|
||||
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
|
||||
const router = new Router('');
|
||||
router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' }));
|
||||
registerRouter(router);
|
||||
|
||||
registerAuth((req, t) => {
|
||||
throw new Error('reason');
|
||||
});
|
||||
await server.start();
|
||||
|
||||
const result = await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(500);
|
||||
|
||||
expect(result.body.message).toBe('An internal server error occurred.');
|
||||
expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
[Error: reason],
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('allows manipulating cookies via cookie session storage', async () => {
|
||||
const router = new Router('');
|
||||
router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' }));
|
||||
|
||||
const {
|
||||
createCookieSessionStorageFactory,
|
||||
registerAuth,
|
||||
registerRouter,
|
||||
server: innerServer,
|
||||
} = await server.setup(config);
|
||||
const sessionStorageFactory = await createCookieSessionStorageFactory<StorageData>(
|
||||
cookieOptions
|
||||
);
|
||||
registerAuth((req, res, toolkit) => {
|
||||
const user = { id: '42' };
|
||||
const sessionStorage = sessionStorageFactory.asScoped(req);
|
||||
sessionStorage.set({ value: user, expires: Date.now() + 1000 });
|
||||
return toolkit.authenticated({ state: user });
|
||||
});
|
||||
registerRouter(router);
|
||||
await server.start();
|
||||
|
||||
const response = await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(200, { content: 'ok' });
|
||||
|
||||
expect(response.header['set-cookie']).toBeDefined();
|
||||
const cookies = response.header['set-cookie'];
|
||||
expect(cookies).toHaveLength(1);
|
||||
|
||||
const sessionCookie = request.cookie(cookies[0]);
|
||||
if (!sessionCookie) {
|
||||
throw new Error('session cookie expected to be defined');
|
||||
}
|
||||
expect(sessionCookie).toBeDefined();
|
||||
expect(sessionCookie.key).toBe('sid');
|
||||
expect(sessionCookie.value).toBeDefined();
|
||||
expect(sessionCookie.path).toBe('/');
|
||||
expect(sessionCookie.httpOnly).toBe(true);
|
||||
});
|
||||
|
||||
it('allows manipulating cookies from route handler', async () => {
|
||||
const {
|
||||
createCookieSessionStorageFactory,
|
||||
registerAuth,
|
||||
registerRouter,
|
||||
server: innerServer,
|
||||
} = await server.setup(config);
|
||||
const sessionStorageFactory = await createCookieSessionStorageFactory<StorageData>(
|
||||
cookieOptions
|
||||
);
|
||||
registerAuth((req, res, toolkit) => {
|
||||
const user = { id: '42' };
|
||||
const sessionStorage = sessionStorageFactory.asScoped(req);
|
||||
sessionStorage.set({ value: user, expires: Date.now() + 1000 });
|
||||
return toolkit.authenticated();
|
||||
});
|
||||
|
||||
const router = new Router('');
|
||||
router.get({ path: '/', validate: false }, (req, res) => res.ok({ content: 'ok' }));
|
||||
router.get({ path: '/with-cookie', validate: false }, (req, res) => {
|
||||
const sessionStorage = sessionStorageFactory.asScoped(req);
|
||||
sessionStorage.clear();
|
||||
return res.ok({ content: 'ok' });
|
||||
});
|
||||
registerRouter(router);
|
||||
|
||||
await server.start();
|
||||
|
||||
const responseToSetCookie = await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(200);
|
||||
|
||||
expect(responseToSetCookie.header['set-cookie']).toBeDefined();
|
||||
|
||||
const responseToResetCookie = await supertest(innerServer.listener)
|
||||
.get('/with-cookie')
|
||||
.expect(200);
|
||||
|
||||
expect(responseToResetCookie.header['set-cookie']).toEqual([
|
||||
'sid=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Path=/',
|
||||
]);
|
||||
});
|
||||
|
||||
it.skip('is the only place with access to the authorization header', async () => {
|
||||
const token = 'Basic: user:password';
|
||||
const {
|
||||
registerAuth,
|
||||
registerOnPreAuth,
|
||||
registerOnPostAuth,
|
||||
registerRouter,
|
||||
server: innerServer,
|
||||
} = await server.setup(config);
|
||||
|
||||
let fromRegisterOnPreAuth;
|
||||
await registerOnPreAuth((req, res, toolkit) => {
|
||||
fromRegisterOnPreAuth = req.headers.authorization;
|
||||
return toolkit.next();
|
||||
});
|
||||
|
||||
let fromRegisterAuth;
|
||||
registerAuth((req, res, toolkit) => {
|
||||
fromRegisterAuth = req.headers.authorization;
|
||||
return toolkit.authenticated();
|
||||
});
|
||||
|
||||
let fromRegisterOnPostAuth;
|
||||
await registerOnPostAuth((req, res, toolkit) => {
|
||||
fromRegisterOnPostAuth = req.headers.authorization;
|
||||
return toolkit.next();
|
||||
});
|
||||
|
||||
let fromRouteHandler;
|
||||
const router = new Router('');
|
||||
router.get({ path: '/', validate: false }, (req, res) => {
|
||||
fromRouteHandler = req.headers.authorization;
|
||||
return res.ok({ content: 'ok' });
|
||||
});
|
||||
registerRouter(router);
|
||||
|
||||
await server.start();
|
||||
|
||||
await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.set('Authorization', token)
|
||||
.expect(200);
|
||||
|
||||
expect(fromRegisterOnPreAuth).toEqual({});
|
||||
expect(fromRegisterAuth).toEqual({ authorization: token });
|
||||
expect(fromRegisterOnPostAuth).toEqual({});
|
||||
expect(fromRouteHandler).toEqual({});
|
||||
});
|
||||
|
||||
it('attach security header to a successful response', async () => {
|
||||
const authResponseHeader = {
|
||||
'www-authenticate': 'Negotiate ade0234568a4209af8bc0280289eca',
|
||||
};
|
||||
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
|
||||
|
||||
registerAuth((req, res, toolkit) => {
|
||||
return toolkit.authenticated({ responseHeaders: authResponseHeader });
|
||||
});
|
||||
|
||||
const router = new Router('/');
|
||||
router.get({ path: '/', validate: false }, (req, res) => res.ok({ header: 'ok' }));
|
||||
registerRouter(router);
|
||||
|
||||
await server.start();
|
||||
|
||||
const response = await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(200);
|
||||
|
||||
expect(response.header['www-authenticate']).toBe(authResponseHeader['www-authenticate']);
|
||||
});
|
||||
|
||||
it('attach security header to an error response', async () => {
|
||||
const authResponseHeader = {
|
||||
'www-authenticate': 'Negotiate ade0234568a4209af8bc0280289eca',
|
||||
};
|
||||
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
|
||||
|
||||
registerAuth((req, res, toolkit) => {
|
||||
return toolkit.authenticated({ responseHeaders: authResponseHeader });
|
||||
});
|
||||
|
||||
const router = new Router('/');
|
||||
router.get({ path: '/', validate: false }, (req, res) => res.badRequest(new Error('reason')));
|
||||
registerRouter(router);
|
||||
|
||||
await server.start();
|
||||
|
||||
const response = await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(400);
|
||||
|
||||
expect(response.header['www-authenticate']).toBe(authResponseHeader['www-authenticate']);
|
||||
});
|
||||
|
||||
it('logs warning if Auth Security Header rewrites response header for success response', async () => {
|
||||
const authResponseHeader = {
|
||||
'www-authenticate': 'from auth interceptor',
|
||||
};
|
||||
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
|
||||
|
||||
registerAuth((req, res, toolkit) => {
|
||||
return toolkit.authenticated({ responseHeaders: authResponseHeader });
|
||||
});
|
||||
|
||||
const router = new Router('/');
|
||||
router.get({ path: '/', validate: false }, (req, res) =>
|
||||
res.ok(
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
'www-authenticate': 'from handler',
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
registerRouter(router);
|
||||
|
||||
await server.start();
|
||||
|
||||
const response = await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(200);
|
||||
|
||||
expect(response.header['www-authenticate']).toBe('from auth interceptor');
|
||||
expect(loggingServiceMock.collect(logger).warn).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
"Server rewrites a response header [www-authenticate].",
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('logs warning if Auth Security Header rewrites response header for error response', async () => {
|
||||
const authResponseHeader = {
|
||||
'www-authenticate': 'from auth interceptor',
|
||||
};
|
||||
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
|
||||
|
||||
registerAuth((req, res, toolkit) => {
|
||||
return toolkit.authenticated({ responseHeaders: authResponseHeader });
|
||||
});
|
||||
|
||||
const router = new Router('/');
|
||||
router.get({ path: '/', validate: false }, (req, res) =>
|
||||
res.badRequest('reason', {
|
||||
headers: {
|
||||
'www-authenticate': 'from handler',
|
||||
},
|
||||
})
|
||||
);
|
||||
registerRouter(router);
|
||||
|
||||
await server.start();
|
||||
|
||||
const response = await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(400);
|
||||
|
||||
expect(response.header['www-authenticate']).toBe('from auth interceptor');
|
||||
expect(loggingServiceMock.collect(logger).warn).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
"Server rewrites a response header [www-authenticate].",
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('supports redirection from the interceptor', async () => {
|
||||
const router = new Router('/');
|
||||
const redirectUrl = '/redirectUrl';
|
||||
router.get({ path: '/initial', validate: false }, (req, res) => res.ok('initial'));
|
||||
|
||||
const { registerRouter, registerOnPostAuth, server: innerServer } = await server.setup(config);
|
||||
registerRouter(router);
|
||||
|
||||
registerOnPostAuth((req, res, t) =>
|
||||
res.redirected(undefined, {
|
||||
headers: {
|
||||
location: redirectUrl,
|
||||
},
|
||||
})
|
||||
);
|
||||
await server.start();
|
||||
|
||||
const result = await supertest(innerServer.listener)
|
||||
.get('/initial')
|
||||
.expect(302);
|
||||
|
||||
expect(result.header.location).toBe(redirectUrl);
|
||||
});
|
||||
|
||||
it('supports rejecting request and adjusting response headers', async () => {
|
||||
const router = new Router('/');
|
||||
router.get({ path: '/', validate: false }, (req, res) => res.ok(undefined));
|
||||
|
||||
const { registerRouter, registerOnPostAuth, server: innerServer } = await server.setup(config);
|
||||
registerRouter(router);
|
||||
|
||||
registerOnPostAuth((req, res, t) =>
|
||||
res.unauthorized('not found error', {
|
||||
headers: {
|
||||
'www-authenticate': 'challenge',
|
||||
},
|
||||
})
|
||||
);
|
||||
await server.start();
|
||||
|
||||
const result = await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(401);
|
||||
|
||||
expect(result.header['www-authenticate']).toBe('challenge');
|
||||
});
|
||||
|
||||
it("doesn't expose error details if interceptor throws", async () => {
|
||||
const router = new Router('/');
|
||||
router.get({ path: '/', validate: false }, (req, res) => res.ok(undefined));
|
||||
|
||||
const { registerRouter, registerOnPostAuth, server: innerServer } = await server.setup(config);
|
||||
registerRouter(router);
|
||||
|
||||
registerOnPostAuth((req, res, t) => {
|
||||
throw new Error('reason');
|
||||
});
|
||||
await server.start();
|
||||
|
||||
const result = await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(500);
|
||||
|
||||
expect(result.body.message).toBe('An internal server error occurred.');
|
||||
expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
[Error: reason],
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns internal error if interceptor returns unexpected result', async () => {
|
||||
const router = new Router('/');
|
||||
router.get({ path: '/', validate: false }, (req, res) => res.ok('ok'));
|
||||
|
||||
const { registerRouter, registerOnPostAuth, server: innerServer } = await server.setup(config);
|
||||
registerRouter(router);
|
||||
|
||||
registerOnPostAuth((req, res, t) => ({} as any));
|
||||
await server.start();
|
||||
|
||||
const result = await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(500);
|
||||
|
||||
expect(result.body.message).toBe('An internal server error occurred.');
|
||||
expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
[Error: Unexpected result from OnPostAuth. Expected OnPostAuthResult or KibanaResponse, but given: [object Object].],
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
it(`doesn't share request object between interceptors`, async () => {
|
||||
const { registerRouter, registerOnPostAuth, server: innerServer } = await server.setup(config);
|
||||
registerOnPostAuth((req, res, t) => {
|
||||
// @ts-ignore. don't complain customField is not defined on Request type
|
||||
req.customField = { value: 42 };
|
||||
return t.next();
|
||||
});
|
||||
registerOnPostAuth((req, res, t) => {
|
||||
// @ts-ignore don't complain customField is not defined on Request type
|
||||
if (typeof req.customField !== 'undefined') {
|
||||
throw new Error('Request object was mutated');
|
||||
}
|
||||
return t.next();
|
||||
});
|
||||
const router = new Router('/');
|
||||
router.get({ path: '/', validate: false }, async (req, res) =>
|
||||
// @ts-ignore. don't complain customField is not defined on Request type
|
||||
res.ok({ customField: String(req.customField) })
|
||||
);
|
||||
registerRouter(router);
|
||||
await server.start();
|
||||
|
||||
await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(200, { customField: 'undefined' });
|
||||
});
|
||||
});
|
|
@ -63,7 +63,7 @@ describe('Handler', () => {
|
|||
.get('/')
|
||||
.expect(500);
|
||||
|
||||
expect(result.body.error).toBe('An internal server error occurred.');
|
||||
expect(result.body.message).toBe('An internal server error occurred.');
|
||||
expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
|
@ -88,7 +88,7 @@ describe('Handler', () => {
|
|||
.get('/')
|
||||
.expect(500);
|
||||
|
||||
expect(result.body.error).toBe('An internal server error occurred.');
|
||||
expect(result.body.message).toBe('An internal server error occurred.');
|
||||
expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
|
@ -111,7 +111,7 @@ describe('Handler', () => {
|
|||
.get('/')
|
||||
.expect(500);
|
||||
|
||||
expect(result.body.error).toBe('An internal server error occurred.');
|
||||
expect(result.body.message).toBe('An internal server error occurred.');
|
||||
expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
|
@ -146,7 +146,9 @@ describe('Handler', () => {
|
|||
.expect(400);
|
||||
|
||||
expect(result.body).toEqual({
|
||||
error: '[request query.page]: expected value of type [number] but got [string]',
|
||||
error: 'Bad Request',
|
||||
message: '[request query.page]: expected value of type [number] but got [string]',
|
||||
statusCode: 400,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -531,7 +533,7 @@ describe('Response factory', () => {
|
|||
.get('/')
|
||||
.expect(500);
|
||||
|
||||
expect(result.body.error).toBe('An internal server error occurred.');
|
||||
expect(result.body.message).toBe('An internal server error occurred.');
|
||||
expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
|
@ -559,7 +561,11 @@ describe('Response factory', () => {
|
|||
.get('/')
|
||||
.expect(400);
|
||||
|
||||
expect(result.body).toEqual({ error: 'some message' });
|
||||
expect(result.body).toEqual({
|
||||
error: 'Bad Request',
|
||||
message: 'some message',
|
||||
statusCode: 400,
|
||||
});
|
||||
});
|
||||
|
||||
it('400 Bad request with default message', async () => {
|
||||
|
@ -577,7 +583,11 @@ describe('Response factory', () => {
|
|||
.get('/')
|
||||
.expect(400);
|
||||
|
||||
expect(result.body).toEqual({ error: 'Bad Request' });
|
||||
expect(result.body).toEqual({
|
||||
error: 'Bad Request',
|
||||
message: 'Bad Request',
|
||||
statusCode: 400,
|
||||
});
|
||||
});
|
||||
|
||||
it('400 Bad request with additional data', async () => {
|
||||
|
@ -596,10 +606,12 @@ describe('Response factory', () => {
|
|||
.expect(400);
|
||||
|
||||
expect(result.body).toEqual({
|
||||
error: 'some message',
|
||||
error: 'Bad Request',
|
||||
message: 'some message',
|
||||
meta: {
|
||||
data: ['good', 'bad'],
|
||||
},
|
||||
statusCode: 400,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -623,7 +635,7 @@ describe('Response factory', () => {
|
|||
.get('/')
|
||||
.expect(401);
|
||||
|
||||
expect(result.body.error).toBe('no access');
|
||||
expect(result.body.message).toBe('no access');
|
||||
expect(result.header['www-authenticate']).toBe('challenge');
|
||||
});
|
||||
|
||||
|
@ -642,7 +654,7 @@ describe('Response factory', () => {
|
|||
.get('/')
|
||||
.expect(401);
|
||||
|
||||
expect(result.body.error).toBe('Unauthorized');
|
||||
expect(result.body.message).toBe('Unauthorized');
|
||||
});
|
||||
|
||||
it('403 Forbidden', async () => {
|
||||
|
@ -661,7 +673,7 @@ describe('Response factory', () => {
|
|||
.get('/')
|
||||
.expect(403);
|
||||
|
||||
expect(result.body.error).toBe('reason');
|
||||
expect(result.body.message).toBe('reason');
|
||||
});
|
||||
|
||||
it('403 Forbidden with default message', async () => {
|
||||
|
@ -679,7 +691,7 @@ describe('Response factory', () => {
|
|||
.get('/')
|
||||
.expect(403);
|
||||
|
||||
expect(result.body.error).toBe('Forbidden');
|
||||
expect(result.body.message).toBe('Forbidden');
|
||||
});
|
||||
|
||||
it('404 Not Found', async () => {
|
||||
|
@ -698,7 +710,7 @@ describe('Response factory', () => {
|
|||
.get('/')
|
||||
.expect(404);
|
||||
|
||||
expect(result.body.error).toBe('file is not found');
|
||||
expect(result.body.message).toBe('file is not found');
|
||||
});
|
||||
|
||||
it('404 Not Found with default message', async () => {
|
||||
|
@ -716,7 +728,7 @@ describe('Response factory', () => {
|
|||
.get('/')
|
||||
.expect(404);
|
||||
|
||||
expect(result.body.error).toBe('Not Found');
|
||||
expect(result.body.message).toBe('Not Found');
|
||||
});
|
||||
|
||||
it('409 Conflict', async () => {
|
||||
|
@ -735,7 +747,7 @@ describe('Response factory', () => {
|
|||
.get('/')
|
||||
.expect(409);
|
||||
|
||||
expect(result.body.error).toBe('stale version');
|
||||
expect(result.body.message).toBe('stale version');
|
||||
});
|
||||
|
||||
it('409 Conflict with default message', async () => {
|
||||
|
@ -753,7 +765,116 @@ describe('Response factory', () => {
|
|||
.get('/')
|
||||
.expect(409);
|
||||
|
||||
expect(result.body.error).toBe('Conflict');
|
||||
expect(result.body.message).toBe('Conflict');
|
||||
});
|
||||
|
||||
it('Custom error response', async () => {
|
||||
const router = new Router('/');
|
||||
|
||||
router.get({ path: '/', validate: false }, (req, res) => {
|
||||
const error = new Error('some message');
|
||||
return res.customError(error, {
|
||||
statusCode: 418,
|
||||
});
|
||||
});
|
||||
|
||||
const { registerRouter, server: innerServer } = await server.setup(config);
|
||||
registerRouter(router);
|
||||
await server.start();
|
||||
|
||||
const result = await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(418);
|
||||
|
||||
expect(result.body).toEqual({
|
||||
error: "I'm a teapot",
|
||||
message: 'some message',
|
||||
statusCode: 418,
|
||||
});
|
||||
});
|
||||
|
||||
it('Custom error response for server error', async () => {
|
||||
const router = new Router('/');
|
||||
|
||||
router.get({ path: '/', validate: false }, (req, res) => {
|
||||
const error = new Error('some message');
|
||||
|
||||
return res.customError(error, {
|
||||
statusCode: 500,
|
||||
});
|
||||
});
|
||||
|
||||
const { registerRouter, server: innerServer } = await server.setup(config);
|
||||
registerRouter(router);
|
||||
await server.start();
|
||||
|
||||
const result = await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(500);
|
||||
|
||||
expect(result.body).toEqual({
|
||||
error: 'Internal Server Error',
|
||||
message: 'some message',
|
||||
statusCode: 500,
|
||||
});
|
||||
});
|
||||
|
||||
it('Custom error response for Boom server error', async () => {
|
||||
const router = new Router('/');
|
||||
|
||||
router.get({ path: '/', validate: false }, (req, res) => {
|
||||
const error = new Error('some message');
|
||||
|
||||
return res.customError(Boom.boomify(error), {
|
||||
statusCode: 500,
|
||||
});
|
||||
});
|
||||
|
||||
const { registerRouter, server: innerServer } = await server.setup(config);
|
||||
registerRouter(router);
|
||||
await server.start();
|
||||
|
||||
const result = await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(500);
|
||||
|
||||
expect(result.body).toEqual({
|
||||
error: 'Internal Server Error',
|
||||
message: 'some message',
|
||||
statusCode: 500,
|
||||
});
|
||||
});
|
||||
|
||||
it('Custom error response requires error status code', async () => {
|
||||
const router = new Router('/');
|
||||
|
||||
router.get({ path: '/', validate: false }, (req, res) => {
|
||||
const error = new Error('some message');
|
||||
return res.customError(error, {
|
||||
statusCode: 200,
|
||||
});
|
||||
});
|
||||
|
||||
const { registerRouter, server: innerServer } = await server.setup(config);
|
||||
registerRouter(router);
|
||||
await server.start();
|
||||
|
||||
const result = await supertest(innerServer.listener)
|
||||
.get('/')
|
||||
.expect(500);
|
||||
|
||||
expect(result.body).toEqual({
|
||||
error: 'Internal Server Error',
|
||||
message: 'An internal server error occurred.',
|
||||
statusCode: 500,
|
||||
});
|
||||
expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
[Error: Unexpected Http status code. Expected from 400 to 599, but given: 200],
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -849,7 +970,7 @@ describe('Response factory', () => {
|
|||
.get('/')
|
||||
.expect(401);
|
||||
|
||||
expect(result.body.error).toBe('unauthorized');
|
||||
expect(result.body.message).toBe('unauthorized');
|
||||
});
|
||||
|
||||
it('creates error response with additional data', async () => {
|
||||
|
@ -876,8 +997,10 @@ describe('Response factory', () => {
|
|||
.expect(401);
|
||||
|
||||
expect(result.body).toEqual({
|
||||
error: 'unauthorized',
|
||||
error: 'Unauthorized',
|
||||
message: 'unauthorized',
|
||||
meta: { errorCode: 'K401' },
|
||||
statusCode: 401,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -905,8 +1028,10 @@ describe('Response factory', () => {
|
|||
.expect(401);
|
||||
|
||||
expect(result.body).toEqual({
|
||||
error: 'unauthorized',
|
||||
error: 'Unauthorized',
|
||||
message: 'unauthorized',
|
||||
meta: { errorCode: 'K401' },
|
||||
statusCode: 401,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -928,7 +1053,7 @@ describe('Response factory', () => {
|
|||
.get('/')
|
||||
.expect(401);
|
||||
|
||||
expect(result.body.error).toBe('Unauthorized');
|
||||
expect(result.body.message).toBe('Unauthorized');
|
||||
});
|
||||
|
||||
it("Doesn't log details of created 500 Server error response", async () => {
|
||||
|
@ -948,7 +1073,7 @@ describe('Response factory', () => {
|
|||
.get('/')
|
||||
.expect(500);
|
||||
|
||||
expect(result.body.error).toBe('reason');
|
||||
expect(result.body.message).toBe('reason');
|
||||
expect(loggingServiceMock.collect(logger).error).toHaveLength(0);
|
||||
});
|
||||
|
||||
|
@ -972,7 +1097,7 @@ describe('Response factory', () => {
|
|||
.get('/')
|
||||
.expect(500);
|
||||
|
||||
expect(result.body.error).toBe('An internal server error occurred.');
|
||||
expect(result.body.message).toBe('An internal server error occurred.');
|
||||
expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
|
@ -999,7 +1124,7 @@ describe('Response factory', () => {
|
|||
.get('/')
|
||||
.expect(500);
|
||||
|
||||
expect(result.body.error).toBe('An internal server error occurred.');
|
||||
expect(result.body.message).toBe('An internal server error occurred.');
|
||||
expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
|
@ -1025,7 +1150,7 @@ describe('Response factory', () => {
|
|||
.get('/')
|
||||
.expect(500);
|
||||
|
||||
expect(result.body.error).toBe('An internal server error occurred.');
|
||||
expect(result.body.message).toBe('An internal server error occurred.');
|
||||
expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
|
@ -1051,7 +1176,7 @@ describe('Response factory', () => {
|
|||
.get('/')
|
||||
.expect(500);
|
||||
|
||||
expect(result.body.error).toBe('An internal server error occurred.');
|
||||
expect(result.body.message).toBe('An internal server error occurred.');
|
||||
expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
|
|
|
@ -1,110 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
import { adoptToHapiAuthFormat } from './auth';
|
||||
import { httpServerMock } from '../http_server.mocks';
|
||||
|
||||
describe('adoptToHapiAuthFormat', () => {
|
||||
it('allows to associate arbitrary data with an incoming request', async () => {
|
||||
const authData = {
|
||||
state: { foo: 'bar' },
|
||||
requestHeaders: { authorization: 'baz' },
|
||||
};
|
||||
const authenticatedMock = jest.fn();
|
||||
const onSuccessMock = jest.fn();
|
||||
const onAuth = adoptToHapiAuthFormat((req, t) => t.authenticated(authData), onSuccessMock);
|
||||
await onAuth(
|
||||
httpServerMock.createRawRequest(),
|
||||
httpServerMock.createRawResponseToolkit({
|
||||
authenticated: authenticatedMock,
|
||||
})
|
||||
);
|
||||
|
||||
expect(authenticatedMock).toBeCalledTimes(1);
|
||||
expect(authenticatedMock).toBeCalledWith({ credentials: authData.state });
|
||||
|
||||
expect(onSuccessMock).toBeCalledTimes(1);
|
||||
const [[, onSuccessData]] = onSuccessMock.mock.calls;
|
||||
expect(onSuccessData).toEqual(authData);
|
||||
});
|
||||
|
||||
it('Should allow redirecting to specified url', async () => {
|
||||
const redirectUrl = '/docs';
|
||||
const onSuccessMock = jest.fn();
|
||||
const onAuth = adoptToHapiAuthFormat((req, t) => t.redirected(redirectUrl), onSuccessMock);
|
||||
const takeoverSymbol = {};
|
||||
const redirectMock = jest.fn(() => ({ takeover: () => takeoverSymbol }));
|
||||
const result = await onAuth(
|
||||
httpServerMock.createRawRequest(),
|
||||
httpServerMock.createRawResponseToolkit({
|
||||
redirect: redirectMock,
|
||||
})
|
||||
);
|
||||
|
||||
expect(redirectMock).toBeCalledWith(redirectUrl);
|
||||
expect(result).toBe(takeoverSymbol);
|
||||
expect(onSuccessMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Should allow to specify statusCode and message for Boom error', async () => {
|
||||
const onSuccessMock = jest.fn();
|
||||
const onAuth = adoptToHapiAuthFormat(
|
||||
(req, t) => t.rejected(new Error('not found'), { statusCode: 404 }),
|
||||
onSuccessMock
|
||||
);
|
||||
const result = (await onAuth(
|
||||
httpServerMock.createRawRequest(),
|
||||
httpServerMock.createRawResponseToolkit()
|
||||
)) as Boom;
|
||||
|
||||
expect(result).toBeInstanceOf(Boom);
|
||||
expect(result.message).toBe('not found');
|
||||
expect(result.output.statusCode).toBe(404);
|
||||
expect(onSuccessMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Should return Boom.internal error error if interceptor throws', async () => {
|
||||
const onAuth = adoptToHapiAuthFormat((req, t) => {
|
||||
throw new Error('unknown error');
|
||||
});
|
||||
const result = (await onAuth(
|
||||
httpServerMock.createRawRequest(),
|
||||
httpServerMock.createRawResponseToolkit()
|
||||
)) as Boom;
|
||||
|
||||
expect(result).toBeInstanceOf(Boom);
|
||||
expect(result.message).toBe('unknown error');
|
||||
expect(result.output.statusCode).toBe(500);
|
||||
});
|
||||
|
||||
it('Should return Boom.internal error if interceptor returns unexpected result', async () => {
|
||||
const onAuth = adoptToHapiAuthFormat(async (req, t) => undefined as any);
|
||||
const result = (await onAuth(
|
||||
httpServerMock.createRawRequest(),
|
||||
httpServerMock.createRawResponseToolkit()
|
||||
)) 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);
|
||||
});
|
||||
});
|
|
@ -16,33 +16,25 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import Boom from 'boom';
|
||||
import { noop } from 'lodash';
|
||||
import { Lifecycle, Request, ResponseToolkit } from 'hapi';
|
||||
import { KibanaRequest } from '../router';
|
||||
import { Logger } from '../../logging';
|
||||
import {
|
||||
HapiResponseAdapter,
|
||||
KibanaRequest,
|
||||
KibanaResponse,
|
||||
lifecycleResponseFactory,
|
||||
LifecycleResponseFactory,
|
||||
} from '../router';
|
||||
|
||||
enum ResultType {
|
||||
authenticated = 'authenticated',
|
||||
redirected = 'redirected',
|
||||
rejected = 'rejected',
|
||||
}
|
||||
|
||||
interface Authenticated extends AuthResultParams {
|
||||
type: ResultType.authenticated;
|
||||
}
|
||||
|
||||
interface Redirected {
|
||||
type: ResultType.redirected;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface Rejected {
|
||||
type: ResultType.rejected;
|
||||
error: Error;
|
||||
statusCode?: number;
|
||||
}
|
||||
|
||||
type AuthResult = Authenticated | Rejected | Redirected;
|
||||
type AuthResult = Authenticated;
|
||||
|
||||
const authResult = {
|
||||
authenticated(data: Partial<AuthResultParams> = {}): AuthResult {
|
||||
|
@ -53,28 +45,8 @@ const authResult = {
|
|||
responseHeaders: data.responseHeaders,
|
||||
};
|
||||
},
|
||||
redirected(url: string): AuthResult {
|
||||
return { type: ResultType.redirected, url };
|
||||
},
|
||||
rejected(error: Error, options: { statusCode?: number } = {}): AuthResult {
|
||||
return { type: ResultType.rejected, error, statusCode: options.statusCode };
|
||||
},
|
||||
isValid(candidate: any): candidate is AuthResult {
|
||||
return (
|
||||
candidate &&
|
||||
(candidate.type === ResultType.authenticated ||
|
||||
candidate.type === ResultType.rejected ||
|
||||
candidate.type === ResultType.redirected)
|
||||
);
|
||||
},
|
||||
isAuthenticated(result: AuthResult): result is Authenticated {
|
||||
return result.type === ResultType.authenticated;
|
||||
},
|
||||
isRedirected(result: AuthResult): result is Redirected {
|
||||
return result.type === ResultType.redirected;
|
||||
},
|
||||
isRejected(result: AuthResult): result is Rejected {
|
||||
return result.type === ResultType.rejected;
|
||||
return result && result.type === ResultType.authenticated;
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -113,55 +85,53 @@ export interface AuthResultParams {
|
|||
export interface AuthToolkit {
|
||||
/** Authentication is successful with given credentials, allow request to pass through */
|
||||
authenticated: (data?: AuthResultParams) => AuthResult;
|
||||
/** Authentication requires to interrupt request handling and redirect to a configured url */
|
||||
redirected: (url: string) => AuthResult;
|
||||
/** Authentication is unsuccessful, fail the request with specified error. */
|
||||
rejected: (error: Error, options?: { statusCode?: number }) => AuthResult;
|
||||
}
|
||||
|
||||
const toolkit: AuthToolkit = {
|
||||
authenticated: authResult.authenticated,
|
||||
redirected: authResult.redirected,
|
||||
rejected: authResult.rejected,
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type AuthenticationHandler = (
|
||||
request: KibanaRequest,
|
||||
t: AuthToolkit
|
||||
) => AuthResult | Promise<AuthResult>;
|
||||
response: LifecycleResponseFactory,
|
||||
toolkit: AuthToolkit
|
||||
) => AuthResult | KibanaResponse | Promise<AuthResult | KibanaResponse>;
|
||||
|
||||
/** @public */
|
||||
export function adoptToHapiAuthFormat(
|
||||
fn: AuthenticationHandler,
|
||||
onSuccess: (req: Request, data: AuthResultParams) => void = noop
|
||||
log: Logger,
|
||||
onSuccess: (req: Request, data: AuthResultParams) => void = () => undefined
|
||||
) {
|
||||
return async function interceptAuth(
|
||||
req: Request,
|
||||
h: ResponseToolkit
|
||||
request: Request,
|
||||
responseToolkit: ResponseToolkit
|
||||
): Promise<Lifecycle.ReturnValue> {
|
||||
const hapiResponseAdapter = new HapiResponseAdapter(responseToolkit);
|
||||
try {
|
||||
const result = await fn(KibanaRequest.from(req, undefined, false), toolkit);
|
||||
if (!authResult.isValid(result)) {
|
||||
throw new Error(
|
||||
`Unexpected result from Authenticate. Expected AuthResult, but given: ${result}.`
|
||||
);
|
||||
const result = await fn(
|
||||
KibanaRequest.from(request, undefined, false),
|
||||
lifecycleResponseFactory,
|
||||
toolkit
|
||||
);
|
||||
if (result instanceof KibanaResponse) {
|
||||
return hapiResponseAdapter.handle(result);
|
||||
}
|
||||
if (authResult.isAuthenticated(result)) {
|
||||
onSuccess(req, {
|
||||
onSuccess(request, {
|
||||
state: result.state,
|
||||
requestHeaders: result.requestHeaders,
|
||||
responseHeaders: result.responseHeaders,
|
||||
});
|
||||
return h.authenticated({ credentials: result.state || {} });
|
||||
return responseToolkit.authenticated({ credentials: result.state || {} });
|
||||
}
|
||||
if (authResult.isRedirected(result)) {
|
||||
return h.redirect(result.url).takeover();
|
||||
}
|
||||
const { error, statusCode } = result;
|
||||
return Boom.boomify(error, { statusCode });
|
||||
throw new Error(
|
||||
`Unexpected result from Authenticate. Expected AuthResult or KibanaResponse, but given: ${result}.`
|
||||
);
|
||||
} catch (error) {
|
||||
return Boom.internal(error.message, { statusCode: 500 });
|
||||
log.error(error);
|
||||
return hapiResponseAdapter.toInternalError();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,95 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
import { adoptToHapiOnPostAuthFormat } from './on_post_auth';
|
||||
import { httpServerMock } from '../http_server.mocks';
|
||||
|
||||
describe('adoptToHapiOnPostAuthFormat', () => {
|
||||
it('Should allow passing request to the next handler', async () => {
|
||||
const continueSymbol = Symbol();
|
||||
const onPostAuth = adoptToHapiOnPostAuthFormat((req, t) => t.next());
|
||||
const result = await onPostAuth(
|
||||
httpServerMock.createRawRequest(),
|
||||
httpServerMock.createRawResponseToolkit({
|
||||
['continue']: continueSymbol,
|
||||
})
|
||||
);
|
||||
|
||||
expect(result).toBe(continueSymbol);
|
||||
});
|
||||
|
||||
it('Should support redirecting to specified url', async () => {
|
||||
const redirectUrl = '/docs';
|
||||
const onPostAuth = adoptToHapiOnPostAuthFormat((req, t) => t.redirected(redirectUrl));
|
||||
const takeoverSymbol = {};
|
||||
const redirectMock = jest.fn(() => ({ takeover: () => takeoverSymbol }));
|
||||
const result = await onPostAuth(
|
||||
httpServerMock.createRawRequest(),
|
||||
httpServerMock.createRawResponseToolkit({
|
||||
redirect: redirectMock,
|
||||
})
|
||||
);
|
||||
|
||||
expect(redirectMock).toBeCalledWith(redirectUrl);
|
||||
expect(result).toBe(takeoverSymbol);
|
||||
});
|
||||
|
||||
it('Should support specifying statusCode and message for Boom error', async () => {
|
||||
const onPostAuth = adoptToHapiOnPostAuthFormat((req, t) => {
|
||||
return t.rejected(new Error('unexpected result'), { statusCode: 501 });
|
||||
});
|
||||
const result = (await onPostAuth(
|
||||
httpServerMock.createRawRequest(),
|
||||
httpServerMock.createRawResponseToolkit()
|
||||
)) as Boom;
|
||||
|
||||
expect(result).toBeInstanceOf(Boom);
|
||||
expect(result.message).toBe('unexpected result');
|
||||
expect(result.output.statusCode).toBe(501);
|
||||
});
|
||||
|
||||
it('Should return Boom.internal error if interceptor throws', async () => {
|
||||
const onPostAuth = adoptToHapiOnPostAuthFormat((req, t) => {
|
||||
throw new Error('unknown error');
|
||||
});
|
||||
const result = (await onPostAuth(
|
||||
httpServerMock.createRawRequest(),
|
||||
httpServerMock.createRawResponseToolkit()
|
||||
)) as Boom;
|
||||
|
||||
expect(result).toBeInstanceOf(Boom);
|
||||
expect(result.message).toBe('unknown error');
|
||||
expect(result.output.statusCode).toBe(500);
|
||||
});
|
||||
|
||||
it('Should return Boom.internal error if interceptor returns unexpected result', async () => {
|
||||
const onPostAuth = adoptToHapiOnPostAuthFormat((req, toolkit) => undefined as any);
|
||||
const result = (await onPostAuth(
|
||||
httpServerMock.createRawRequest(),
|
||||
httpServerMock.createRawResponseToolkit()
|
||||
)) as Boom;
|
||||
|
||||
expect(result).toBeInstanceOf(Boom);
|
||||
expect(result.message).toMatchInlineSnapshot(
|
||||
`"Unexpected result from OnPostAuth. Expected OnPostAuthResult, but given: undefined."`
|
||||
);
|
||||
expect(result.output.statusCode).toBe(500);
|
||||
});
|
||||
});
|
|
@ -17,59 +17,32 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
import { Lifecycle, Request, ResponseToolkit } from 'hapi';
|
||||
import { KibanaRequest } from '../router';
|
||||
import { Lifecycle, Request, ResponseToolkit as HapiResponseToolkit } from 'hapi';
|
||||
import { Logger } from '../../logging';
|
||||
import {
|
||||
HapiResponseAdapter,
|
||||
KibanaRequest,
|
||||
KibanaResponse,
|
||||
lifecycleResponseFactory,
|
||||
LifecycleResponseFactory,
|
||||
} from '../router';
|
||||
|
||||
enum ResultType {
|
||||
next = 'next',
|
||||
redirected = 'redirected',
|
||||
rejected = 'rejected',
|
||||
}
|
||||
|
||||
interface Next {
|
||||
type: ResultType.next;
|
||||
}
|
||||
|
||||
interface Redirected {
|
||||
type: ResultType.redirected;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface Rejected {
|
||||
type: ResultType.rejected;
|
||||
error: Error;
|
||||
statusCode?: number;
|
||||
}
|
||||
|
||||
type OnPostAuthResult = Next | Rejected | Redirected;
|
||||
type OnPostAuthResult = Next;
|
||||
|
||||
const postAuthResult = {
|
||||
next(): OnPostAuthResult {
|
||||
return { type: ResultType.next };
|
||||
},
|
||||
redirected(url: string): OnPostAuthResult {
|
||||
return { type: ResultType.redirected, url };
|
||||
},
|
||||
rejected(error: Error, options: { statusCode?: number } = {}): OnPostAuthResult {
|
||||
return { type: ResultType.rejected, error, statusCode: options.statusCode };
|
||||
},
|
||||
isValid(candidate: any): candidate is OnPostAuthResult {
|
||||
return (
|
||||
candidate &&
|
||||
(candidate.type === ResultType.next ||
|
||||
candidate.type === ResultType.rejected ||
|
||||
candidate.type === ResultType.redirected)
|
||||
);
|
||||
},
|
||||
isNext(result: OnPostAuthResult): result is Next {
|
||||
return result.type === ResultType.next;
|
||||
},
|
||||
isRedirected(result: OnPostAuthResult): result is Redirected {
|
||||
return result.type === ResultType.redirected;
|
||||
},
|
||||
isRejected(result: OnPostAuthResult): result is Rejected {
|
||||
return result.type === ResultType.rejected;
|
||||
return result && result.type === ResultType.next;
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -80,51 +53,46 @@ const postAuthResult = {
|
|||
export interface OnPostAuthToolkit {
|
||||
/** To pass request to the next handler */
|
||||
next: () => OnPostAuthResult;
|
||||
/** To interrupt request handling and redirect to a configured url */
|
||||
redirected: (url: string) => OnPostAuthResult;
|
||||
/** Fail the request with specified error. */
|
||||
rejected: (error: Error, options?: { statusCode?: number }) => OnPostAuthResult;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type OnPostAuthHandler<Params = any, Query = any, Body = any> = (
|
||||
request: KibanaRequest<Params, Query, Body>,
|
||||
t: OnPostAuthToolkit
|
||||
) => OnPostAuthResult | Promise<OnPostAuthResult>;
|
||||
export type OnPostAuthHandler = (
|
||||
request: KibanaRequest,
|
||||
response: LifecycleResponseFactory,
|
||||
toolkit: OnPostAuthToolkit
|
||||
) => OnPostAuthResult | KibanaResponse | Promise<OnPostAuthResult | KibanaResponse>;
|
||||
|
||||
const toolkit: OnPostAuthToolkit = {
|
||||
next: postAuthResult.next,
|
||||
redirected: postAuthResult.redirected,
|
||||
rejected: postAuthResult.rejected,
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
* Adopt custom request interceptor to Hapi lifecycle system.
|
||||
* @param fn - an extension point allowing to perform custom logic for
|
||||
* incoming HTTP requests.
|
||||
*/
|
||||
export function adoptToHapiOnPostAuthFormat(fn: OnPostAuthHandler) {
|
||||
export function adoptToHapiOnPostAuthFormat(fn: OnPostAuthHandler, log: Logger) {
|
||||
return async function interceptRequest(
|
||||
request: Request,
|
||||
h: ResponseToolkit
|
||||
responseToolkit: HapiResponseToolkit
|
||||
): Promise<Lifecycle.ReturnValue> {
|
||||
const hapiResponseAdapter = new HapiResponseAdapter(responseToolkit);
|
||||
try {
|
||||
const result = await fn(KibanaRequest.from(request), toolkit);
|
||||
if (!postAuthResult.isValid(result)) {
|
||||
throw new Error(
|
||||
`Unexpected result from OnPostAuth. Expected OnPostAuthResult, but given: ${result}.`
|
||||
);
|
||||
const result = await fn(KibanaRequest.from(request), lifecycleResponseFactory, toolkit);
|
||||
if (result instanceof KibanaResponse) {
|
||||
return hapiResponseAdapter.handle(result);
|
||||
}
|
||||
if (postAuthResult.isNext(result)) {
|
||||
return h.continue;
|
||||
return responseToolkit.continue;
|
||||
}
|
||||
if (postAuthResult.isRedirected(result)) {
|
||||
return h.redirect(result.url).takeover();
|
||||
}
|
||||
const { error, statusCode } = result;
|
||||
return Boom.boomify(error, { statusCode });
|
||||
|
||||
throw new Error(
|
||||
`Unexpected result from OnPostAuth. Expected OnPostAuthResult or KibanaResponse, but given: ${result}.`
|
||||
);
|
||||
} catch (error) {
|
||||
return Boom.internal(error.message, { statusCode: 500 });
|
||||
log.error(error);
|
||||
return hapiResponseAdapter.toInternalError();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,115 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
import { adoptToHapiOnPreAuthFormat } from './on_pre_auth';
|
||||
import { httpServerMock } from '../http_server.mocks';
|
||||
|
||||
describe('adoptToHapiOnPreAuthFormat', () => {
|
||||
it('Should allow passing request to the next handler', async () => {
|
||||
const continueSymbol = Symbol();
|
||||
const onPreAuth = adoptToHapiOnPreAuthFormat((req, t) => t.next());
|
||||
const result = await onPreAuth(
|
||||
httpServerMock.createRawRequest(),
|
||||
httpServerMock.createRawResponseToolkit({
|
||||
['continue']: continueSymbol,
|
||||
})
|
||||
);
|
||||
|
||||
expect(result).toBe(continueSymbol);
|
||||
});
|
||||
|
||||
it('Should support redirecting to specified url', async () => {
|
||||
const redirectUrl = '/docs';
|
||||
const onPreAuth = adoptToHapiOnPreAuthFormat((req, t) => t.redirected(redirectUrl));
|
||||
const takeoverSymbol = {};
|
||||
const redirectMock = jest.fn(() => ({ takeover: () => takeoverSymbol }));
|
||||
const result = await onPreAuth(
|
||||
httpServerMock.createRawRequest(),
|
||||
httpServerMock.createRawResponseToolkit({
|
||||
redirect: redirectMock,
|
||||
})
|
||||
);
|
||||
|
||||
expect(redirectMock).toBeCalledWith(redirectUrl);
|
||||
expect(result).toBe(takeoverSymbol);
|
||||
});
|
||||
|
||||
it('Should support request forwarding to specified url', async () => {
|
||||
const redirectUrl = '/docs';
|
||||
const onPreAuth = adoptToHapiOnPreAuthFormat((req, t) =>
|
||||
t.redirected(redirectUrl, { forward: true })
|
||||
);
|
||||
const continueSymbol = Symbol();
|
||||
const setUrl = jest.fn();
|
||||
const mockedRequest = httpServerMock.createRawRequest({ setUrl });
|
||||
const result = await onPreAuth(
|
||||
mockedRequest,
|
||||
httpServerMock.createRawResponseToolkit({
|
||||
['continue']: continueSymbol,
|
||||
})
|
||||
);
|
||||
|
||||
expect(setUrl).toBeCalledWith(redirectUrl);
|
||||
expect(mockedRequest.raw.req.url).toBe(redirectUrl);
|
||||
expect(result).toBe(continueSymbol);
|
||||
});
|
||||
|
||||
it('Should support specifying statusCode and message for Boom error', async () => {
|
||||
const onPreAuth = adoptToHapiOnPreAuthFormat((req, t) => {
|
||||
return t.rejected(new Error('unexpected result'), { statusCode: 501 });
|
||||
});
|
||||
const result = (await onPreAuth(
|
||||
httpServerMock.createRawRequest(),
|
||||
httpServerMock.createRawResponseToolkit()
|
||||
)) as Boom;
|
||||
|
||||
expect(result).toBeInstanceOf(Boom);
|
||||
expect(result.message).toBe('unexpected result');
|
||||
expect(result.output.statusCode).toBe(501);
|
||||
});
|
||||
|
||||
it('Should return Boom.internal error if interceptor throws', async () => {
|
||||
const onPreAuth = adoptToHapiOnPreAuthFormat((req, t) => {
|
||||
throw new Error('unknown error');
|
||||
});
|
||||
const result = (await onPreAuth(
|
||||
httpServerMock.createRawRequest(),
|
||||
httpServerMock.createRawResponseToolkit()
|
||||
)) as Boom;
|
||||
|
||||
expect(result).toBeInstanceOf(Boom);
|
||||
expect(result.message).toBe('unknown error');
|
||||
expect(result.output.statusCode).toBe(500);
|
||||
});
|
||||
|
||||
it('Should return Boom.internal error if interceptor returns unexpected result', async () => {
|
||||
const onPreAuth = adoptToHapiOnPreAuthFormat((req, toolkit) => undefined as any);
|
||||
const result = (await onPreAuth(
|
||||
httpServerMock.createRawRequest(),
|
||||
httpServerMock.createRawResponseToolkit()
|
||||
)) as Boom;
|
||||
|
||||
expect(result).toBeInstanceOf(Boom);
|
||||
expect(result.message).toMatchInlineSnapshot(
|
||||
`"Unexpected result from OnPreAuth. Expected OnPreAuthResult, but given: undefined."`
|
||||
);
|
||||
expect(result.output.statusCode).toBe(500);
|
||||
});
|
||||
});
|
|
@ -17,60 +17,44 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
import { Lifecycle, Request, ResponseToolkit } from 'hapi';
|
||||
import { KibanaRequest } from '../router';
|
||||
import { Lifecycle, Request, ResponseToolkit as HapiResponseToolkit } from 'hapi';
|
||||
import { Logger } from '../../logging';
|
||||
import {
|
||||
HapiResponseAdapter,
|
||||
KibanaRequest,
|
||||
KibanaResponse,
|
||||
lifecycleResponseFactory,
|
||||
LifecycleResponseFactory,
|
||||
} from '../router';
|
||||
|
||||
enum ResultType {
|
||||
next = 'next',
|
||||
redirected = 'redirected',
|
||||
rejected = 'rejected',
|
||||
rewriteUrl = 'rewriteUrl',
|
||||
}
|
||||
|
||||
interface Next {
|
||||
type: ResultType.next;
|
||||
}
|
||||
|
||||
interface Redirected {
|
||||
type: ResultType.redirected;
|
||||
interface RewriteUrl {
|
||||
type: ResultType.rewriteUrl;
|
||||
url: string;
|
||||
forward?: boolean;
|
||||
}
|
||||
|
||||
interface Rejected {
|
||||
type: ResultType.rejected;
|
||||
error: Error;
|
||||
statusCode?: number;
|
||||
}
|
||||
|
||||
type OnPreAuthResult = Next | Rejected | Redirected;
|
||||
type OnPreAuthResult = Next | RewriteUrl;
|
||||
|
||||
const preAuthResult = {
|
||||
next(): OnPreAuthResult {
|
||||
return { type: ResultType.next };
|
||||
},
|
||||
redirected(url: string, options: { forward?: boolean } = {}): OnPreAuthResult {
|
||||
return { type: ResultType.redirected, url, forward: options.forward };
|
||||
},
|
||||
rejected(error: Error, options: { statusCode?: number } = {}): OnPreAuthResult {
|
||||
return { type: ResultType.rejected, error, statusCode: options.statusCode };
|
||||
},
|
||||
isValid(candidate: any): candidate is OnPreAuthResult {
|
||||
return (
|
||||
candidate &&
|
||||
(candidate.type === ResultType.next ||
|
||||
candidate.type === ResultType.rejected ||
|
||||
candidate.type === ResultType.redirected)
|
||||
);
|
||||
rewriteUrl(url: string): OnPreAuthResult {
|
||||
return { type: ResultType.rewriteUrl, url };
|
||||
},
|
||||
isNext(result: OnPreAuthResult): result is Next {
|
||||
return result.type === ResultType.next;
|
||||
return result && result.type === ResultType.next;
|
||||
},
|
||||
isRedirected(result: OnPreAuthResult): result is Redirected {
|
||||
return result.type === ResultType.redirected;
|
||||
},
|
||||
isRejected(result: OnPreAuthResult): result is Rejected {
|
||||
return result.type === ResultType.rejected;
|
||||
isRewriteUrl(result: OnPreAuthResult): result is RewriteUrl {
|
||||
return result && result.type === ResultType.rewriteUrl;
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -81,26 +65,21 @@ const preAuthResult = {
|
|||
export interface OnPreAuthToolkit {
|
||||
/** To pass request to the next handler */
|
||||
next: () => OnPreAuthResult;
|
||||
/**
|
||||
* To interrupt request handling and redirect to a configured url.
|
||||
* If "options.forwarded" = true, request will be forwarded to another url right on the server.
|
||||
* */
|
||||
redirected: (url: string, options?: { forward: boolean }) => OnPreAuthResult;
|
||||
/** Fail the request with specified error. */
|
||||
rejected: (error: Error, options?: { statusCode?: number }) => OnPreAuthResult;
|
||||
/** Rewrite requested resources url before is was authenticated and routed to a handler */
|
||||
rewriteUrl: (url: string) => OnPreAuthResult;
|
||||
}
|
||||
|
||||
const toolkit: OnPreAuthToolkit = {
|
||||
next: preAuthResult.next,
|
||||
redirected: preAuthResult.redirected,
|
||||
rejected: preAuthResult.rejected,
|
||||
rewriteUrl: preAuthResult.rewriteUrl,
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type OnPreAuthHandler<Params = any, Query = any, Body = any> = (
|
||||
request: KibanaRequest<Params, Query, Body>,
|
||||
t: OnPreAuthToolkit
|
||||
) => OnPreAuthResult | Promise<OnPreAuthResult>;
|
||||
export type OnPreAuthHandler = (
|
||||
request: KibanaRequest,
|
||||
response: LifecycleResponseFactory,
|
||||
toolkit: OnPreAuthToolkit
|
||||
) => OnPreAuthResult | KibanaResponse | Promise<OnPreAuthResult | KibanaResponse>;
|
||||
|
||||
/**
|
||||
* @public
|
||||
|
@ -108,38 +87,36 @@ export type OnPreAuthHandler<Params = any, Query = any, Body = any> = (
|
|||
* @param fn - an extension point allowing to perform custom logic for
|
||||
* incoming HTTP requests.
|
||||
*/
|
||||
export function adoptToHapiOnPreAuthFormat(fn: OnPreAuthHandler) {
|
||||
export function adoptToHapiOnPreAuthFormat(fn: OnPreAuthHandler, log: Logger) {
|
||||
return async function interceptPreAuthRequest(
|
||||
request: Request,
|
||||
h: ResponseToolkit
|
||||
responseToolkit: HapiResponseToolkit
|
||||
): Promise<Lifecycle.ReturnValue> {
|
||||
const hapiResponseAdapter = new HapiResponseAdapter(responseToolkit);
|
||||
|
||||
try {
|
||||
const result = await fn(KibanaRequest.from(request), toolkit);
|
||||
|
||||
if (!preAuthResult.isValid(result)) {
|
||||
throw new Error(
|
||||
`Unexpected result from OnPreAuth. Expected OnPreAuthResult, but given: ${result}.`
|
||||
);
|
||||
const result = await fn(KibanaRequest.from(request), lifecycleResponseFactory, toolkit);
|
||||
if (result instanceof KibanaResponse) {
|
||||
return hapiResponseAdapter.handle(result);
|
||||
}
|
||||
|
||||
if (preAuthResult.isNext(result)) {
|
||||
return h.continue;
|
||||
return responseToolkit.continue;
|
||||
}
|
||||
|
||||
if (preAuthResult.isRedirected(result)) {
|
||||
const { url, forward } = result;
|
||||
if (forward) {
|
||||
request.setUrl(url);
|
||||
// We should update raw request as well since it can be proxied to the old platform
|
||||
request.raw.req.url = url;
|
||||
return h.continue;
|
||||
}
|
||||
return h.redirect(url).takeover();
|
||||
if (preAuthResult.isRewriteUrl(result)) {
|
||||
const { url } = result;
|
||||
request.setUrl(url);
|
||||
// We should update raw request as well since it can be proxied to the old platform
|
||||
request.raw.req.url = url;
|
||||
return responseToolkit.continue;
|
||||
}
|
||||
|
||||
const { error, statusCode } = result;
|
||||
return Boom.boomify(error, { statusCode });
|
||||
throw new Error(
|
||||
`Unexpected result from OnPreAuth. Expected OnPreAuthResult or KibanaResponse, but given: ${result}.`
|
||||
);
|
||||
} catch (error) {
|
||||
return Boom.internal(error.message, { statusCode: 500 });
|
||||
log.error(error);
|
||||
return hapiResponseAdapter.toInternalError();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ export {
|
|||
ensureRawRequest,
|
||||
} from './request';
|
||||
export { RouteMethod, RouteConfig, RouteConfigOptions } from './route';
|
||||
export { HapiResponseAdapter } from './response_adapter';
|
||||
export {
|
||||
CustomHttpResponseOptions,
|
||||
HttpResponseOptions,
|
||||
|
@ -34,6 +35,9 @@ export {
|
|||
RedirectResponseOptions,
|
||||
ResponseError,
|
||||
ResponseErrorMeta,
|
||||
KibanaResponse,
|
||||
kibanaResponseFactory,
|
||||
KibanaResponseFactory,
|
||||
lifecycleResponseFactory,
|
||||
LifecycleResponseFactory,
|
||||
} from './response';
|
||||
|
|
|
@ -44,7 +44,7 @@ export type ResponseError =
|
|||
* A response data object, expected to returned as a result of {@link RequestHandler} execution
|
||||
* @internal
|
||||
*/
|
||||
export class KibanaResponse<T extends HttpResponsePayload | ResponseError> {
|
||||
export class KibanaResponse<T extends HttpResponsePayload | ResponseError = any> {
|
||||
constructor(
|
||||
readonly status: number,
|
||||
readonly payload?: T,
|
||||
|
@ -85,6 +85,118 @@ export type RedirectResponseOptions = HttpResponseOptions & {
|
|||
};
|
||||
};
|
||||
|
||||
const successResponseFactory = {
|
||||
/**
|
||||
* The request has succeeded.
|
||||
* Status code: `200`.
|
||||
* @param payload - {@link HttpResponsePayload} payload to send to the client
|
||||
* @param options - {@link HttpResponseOptions} configures HTTP response parameters.
|
||||
*/
|
||||
ok: (payload: HttpResponsePayload, options: HttpResponseOptions = {}) =>
|
||||
new KibanaResponse(200, payload, options),
|
||||
|
||||
/**
|
||||
* The request has been accepted for processing.
|
||||
* Status code: `202`.
|
||||
* @param payload - {@link HttpResponsePayload} payload to send to the client
|
||||
* @param options - {@link HttpResponseOptions} configures HTTP response parameters.
|
||||
*/
|
||||
accepted: (payload?: HttpResponsePayload, options: HttpResponseOptions = {}) =>
|
||||
new KibanaResponse(202, payload, options),
|
||||
|
||||
/**
|
||||
* The server has successfully fulfilled the request and that there is no additional content to send in the response payload body.
|
||||
* Status code: `204`.
|
||||
* @param options - {@link HttpResponseOptions} configures HTTP response parameters.
|
||||
*/
|
||||
noContent: (options: HttpResponseOptions = {}) => new KibanaResponse(204, undefined, options),
|
||||
};
|
||||
|
||||
const redirectionResponseFactory = {
|
||||
/**
|
||||
* Redirect to a different URI.
|
||||
* Status code: `302`.
|
||||
* @param payload - payload to send to the client
|
||||
* @param options - {@link RedirectResponseOptions} configures HTTP response parameters.
|
||||
* Expects `location` header to be set.
|
||||
*/
|
||||
redirected: (payload: HttpResponsePayload, options: RedirectResponseOptions) =>
|
||||
new KibanaResponse(302, payload, options),
|
||||
};
|
||||
|
||||
const errorResponseFactory = {
|
||||
/**
|
||||
* The server cannot process the request due to something that is perceived to be a client error.
|
||||
* Status code: `400`.
|
||||
* @param error - {@link ResponseError} Error object containing message and other error details to pass to the client
|
||||
* @param options - {@link HttpResponseOptions} configures HTTP response parameters.
|
||||
*/
|
||||
badRequest: (error: ResponseError = 'Bad Request', options: HttpResponseOptions = {}) =>
|
||||
new KibanaResponse(400, error, options),
|
||||
|
||||
/**
|
||||
* The request cannot be applied because it lacks valid authentication credentials for the target resource.
|
||||
* Status code: `401`.
|
||||
* @param error - {@link ResponseError} Error object containing message and other error details to pass to the client
|
||||
* @param options - {@link HttpResponseOptions} configures HTTP response parameters.
|
||||
*/
|
||||
unauthorized: (error: ResponseError = 'Unauthorized', options: HttpResponseOptions = {}) =>
|
||||
new KibanaResponse(401, error, options),
|
||||
|
||||
/**
|
||||
* Server cannot grant access to a resource.
|
||||
* Status code: `403`.
|
||||
* @param error - {@link ResponseError} Error object containing message and other error details to pass to the client
|
||||
* @param options - {@link HttpResponseOptions} configures HTTP response parameters.
|
||||
*/
|
||||
forbidden: (error: ResponseError = 'Forbidden', options: HttpResponseOptions = {}) =>
|
||||
new KibanaResponse(403, error, options),
|
||||
|
||||
/**
|
||||
* Server cannot find a current representation for the target resource.
|
||||
* Status code: `404`.
|
||||
* @param error - {@link ResponseError} Error object containing message and other error details to pass to the client
|
||||
* @param options - {@link HttpResponseOptions} configures HTTP response parameters.
|
||||
*/
|
||||
notFound: (error: ResponseError = 'Not Found', options: HttpResponseOptions = {}) =>
|
||||
new KibanaResponse(404, error, options),
|
||||
|
||||
/**
|
||||
* The request could not be completed due to a conflict with the current state of the target resource.
|
||||
* Status code: `409`.
|
||||
* @param error - {@link ResponseError} Error object containing message and other error details to pass to the client
|
||||
* @param options - {@link HttpResponseOptions} configures HTTP response parameters.
|
||||
*/
|
||||
conflict: (error: ResponseError = 'Conflict', options: HttpResponseOptions = {}) =>
|
||||
new KibanaResponse(409, error, options),
|
||||
|
||||
// Server error
|
||||
/**
|
||||
* The server encountered an unexpected condition that prevented it from fulfilling the request.
|
||||
* Status code: `500`.
|
||||
* @param error - {@link ResponseError} Error object containing message and other error details to pass to the client
|
||||
* @param options - {@link HttpResponseOptions} configures HTTP response parameters.
|
||||
*/
|
||||
internalError: (error: ResponseError = 'Internal Error', options: HttpResponseOptions = {}) =>
|
||||
new KibanaResponse(500, error, options),
|
||||
|
||||
/**
|
||||
* Creates an error response with defined status code and payload.
|
||||
* @param error - {@link ResponseError} Error object containing message and other error details to pass to the client
|
||||
* @param options - {@link CustomHttpResponseOptions} configures HTTP response parameters.
|
||||
*/
|
||||
customError: (error: ResponseError, options: CustomHttpResponseOptions) => {
|
||||
if (!options || !options.statusCode) {
|
||||
throw new Error(`options.statusCode is expected to be set. given options: ${options}`);
|
||||
}
|
||||
if (options.statusCode < 400 || options.statusCode >= 600) {
|
||||
throw new Error(
|
||||
`Unexpected Http status code. Expected from 400 to 599, but given: ${options.statusCode}`
|
||||
);
|
||||
}
|
||||
return new KibanaResponse(options.statusCode, error, options);
|
||||
},
|
||||
};
|
||||
/**
|
||||
* Set of helpers used to create `KibanaResponse` to form HTTP response on an incoming request.
|
||||
* Should be returned as a result of {@link RequestHandler} execution.
|
||||
|
@ -178,32 +290,9 @@ export type RedirectResponseOptions = HttpResponseOptions & {
|
|||
* @public
|
||||
*/
|
||||
export const kibanaResponseFactory = {
|
||||
// Success
|
||||
/**
|
||||
* The request has succeeded.
|
||||
* Status code: `200`.
|
||||
* @param payload - {@link HttpResponsePayload} payload to send to the client
|
||||
* @param options - {@link HttpResponseOptions} configures HTTP response parameters.
|
||||
*/
|
||||
ok: (payload: HttpResponsePayload, options: HttpResponseOptions = {}) =>
|
||||
new KibanaResponse(200, payload, options),
|
||||
|
||||
/**
|
||||
* The request has been accepted for processing.
|
||||
* Status code: `202`.
|
||||
* @param payload - {@link HttpResponsePayload} payload to send to the client
|
||||
* @param options - {@link HttpResponseOptions} configures HTTP response parameters.
|
||||
*/
|
||||
accepted: (payload?: HttpResponsePayload, options: HttpResponseOptions = {}) =>
|
||||
new KibanaResponse(202, payload, options),
|
||||
|
||||
/**
|
||||
* The server has successfully fulfilled the request and that there is no additional content to send in the response payload body.
|
||||
* Status code: `204`.
|
||||
* @param options - {@link HttpResponseOptions} configures HTTP response parameters.
|
||||
*/
|
||||
noContent: (options: HttpResponseOptions = {}) => new KibanaResponse(204, undefined, options),
|
||||
|
||||
...successResponseFactory,
|
||||
...redirectionResponseFactory,
|
||||
...errorResponseFactory,
|
||||
/**
|
||||
* Creates a response with defined status code and payload.
|
||||
* @param payload - {@link HttpResponsePayload} payload to send to the client
|
||||
|
@ -216,73 +305,11 @@ export const kibanaResponseFactory = {
|
|||
const { statusCode: code, ...rest } = options;
|
||||
return new KibanaResponse(code, payload, rest);
|
||||
},
|
||||
};
|
||||
|
||||
// Redirection
|
||||
/**
|
||||
* Redirect to a different URI.
|
||||
* Status code: `302`.
|
||||
* @param payload - payload to send to the client
|
||||
* @param options - {@link RedirectResponseOptions} configures HTTP response parameters.
|
||||
* Expects `location` header to be set.
|
||||
*/
|
||||
redirected: (payload: HttpResponsePayload, options: RedirectResponseOptions) =>
|
||||
new KibanaResponse(302, payload, options),
|
||||
|
||||
// Client error
|
||||
/**
|
||||
* The server cannot process the request due to something that is perceived to be a client error.
|
||||
* Status code: `400`.
|
||||
* @param error - {@link ResponseError} Error object containing message and other error details to pass to the client
|
||||
* @param options - {@link HttpResponseOptions} configures HTTP response parameters.
|
||||
*/
|
||||
badRequest: (error: ResponseError = 'Bad Request', options: HttpResponseOptions = {}) =>
|
||||
new KibanaResponse(400, error, options),
|
||||
|
||||
/**
|
||||
* The request cannot be applied because it lacks valid authentication credentials for the target resource.
|
||||
* Status code: `401`.
|
||||
* @param error - {@link ResponseError} Error object containing message and other error details to pass to the client
|
||||
* @param options - {@link HttpResponseOptions} configures HTTP response parameters.
|
||||
*/
|
||||
unauthorized: (error: ResponseError = 'Unauthorized', options: HttpResponseOptions = {}) =>
|
||||
new KibanaResponse(401, error, options),
|
||||
|
||||
/**
|
||||
* Server cannot grant access to a resource.
|
||||
* Status code: `403`.
|
||||
* @param error - {@link ResponseError} Error object containing message and other error details to pass to the client
|
||||
* @param options - {@link HttpResponseOptions} configures HTTP response parameters.
|
||||
*/
|
||||
forbidden: (error: ResponseError = 'Forbidden', options: HttpResponseOptions = {}) =>
|
||||
new KibanaResponse(403, error, options),
|
||||
|
||||
/**
|
||||
* Server cannot find a current representation for the target resource.
|
||||
* Status code: `404`.
|
||||
* @param error - {@link ResponseError} Error object containing message and other error details to pass to the client
|
||||
* @param options - {@link HttpResponseOptions} configures HTTP response parameters.
|
||||
*/
|
||||
notFound: (error: ResponseError = 'Not Found', options: HttpResponseOptions = {}) =>
|
||||
new KibanaResponse(404, error, options),
|
||||
|
||||
/**
|
||||
* The request could not be completed due to a conflict with the current state of the target resource.
|
||||
* Status code: `409`.
|
||||
* @param error - {@link ResponseError} Error object containing message and other error details to pass to the client
|
||||
* @param options - {@link HttpResponseOptions} configures HTTP response parameters.
|
||||
*/
|
||||
conflict: (error: ResponseError = 'Conflict', options: HttpResponseOptions = {}) =>
|
||||
new KibanaResponse(409, error, options),
|
||||
|
||||
// Server error
|
||||
/**
|
||||
* The server encountered an unexpected condition that prevented it from fulfilling the request.
|
||||
* Status code: `500`.
|
||||
* @param error - {@link ResponseError} Error object containing message and other error details to pass to the client
|
||||
* @param options - {@link HttpResponseOptions} configures HTTP response parameters.
|
||||
*/
|
||||
internal: (error: ResponseError = 'Internal Error', options: HttpResponseOptions = {}) =>
|
||||
new KibanaResponse(500, error, options),
|
||||
export const lifecycleResponseFactory = {
|
||||
...redirectionResponseFactory,
|
||||
...errorResponseFactory,
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -290,3 +317,9 @@ export const kibanaResponseFactory = {
|
|||
* @public
|
||||
*/
|
||||
export type KibanaResponseFactory = typeof kibanaResponseFactory;
|
||||
|
||||
/**
|
||||
* Creates an object containing redirection or error response with error details, HTTP headers, and other data transmitted to the client.
|
||||
* @public
|
||||
*/
|
||||
export type LifecycleResponseFactory = typeof lifecycleResponseFactory;
|
||||
|
|
|
@ -18,13 +18,20 @@
|
|||
*/
|
||||
import { ResponseObject as HapiResponseObject, ResponseToolkit as HapiResponseToolkit } from 'hapi';
|
||||
import typeDetect from 'type-detect';
|
||||
import Boom from 'boom';
|
||||
|
||||
import { HttpResponsePayload, KibanaResponse, ResponseError } from './response';
|
||||
import { HttpResponsePayload, KibanaResponse, ResponseError, ResponseErrorMeta } from './response';
|
||||
|
||||
declare module 'boom' {
|
||||
interface Payload {
|
||||
meta?: ResponseErrorMeta;
|
||||
}
|
||||
}
|
||||
|
||||
function setHeaders(response: HapiResponseObject, headers: Record<string, string | string[]> = {}) {
|
||||
Object.entries(headers).forEach(([header, value]) => {
|
||||
if (value !== undefined) {
|
||||
// Hapi typings for header accept only string, although string[] is a valid value
|
||||
// Hapi typings for header accept only strings, although string[] is a valid value
|
||||
response.header(header, value as any);
|
||||
}
|
||||
});
|
||||
|
@ -40,14 +47,22 @@ const statusHelpers = {
|
|||
export class HapiResponseAdapter {
|
||||
constructor(private readonly responseToolkit: HapiResponseToolkit) {}
|
||||
public toBadRequest(message: string) {
|
||||
return this.responseToolkit.response({ error: message }).code(400);
|
||||
const error = Boom.badRequest();
|
||||
error.output.payload.message = message;
|
||||
return error;
|
||||
}
|
||||
|
||||
public toInternalError() {
|
||||
return this.responseToolkit.response({ error: 'An internal server error occurred.' }).code(500);
|
||||
const error = new Boom('', {
|
||||
statusCode: 500,
|
||||
});
|
||||
|
||||
error.output.payload.message = 'An internal server error occurred.';
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
public handle(kibanaResponse: KibanaResponse<any>) {
|
||||
public handle(kibanaResponse: KibanaResponse) {
|
||||
if (!(kibanaResponse instanceof KibanaResponse)) {
|
||||
throw new Error(
|
||||
`Unexpected result from Route Handler. Expected KibanaResponse, but given: ${typeDetect(
|
||||
|
@ -56,28 +71,30 @@ export class HapiResponseAdapter {
|
|||
);
|
||||
}
|
||||
|
||||
const response = this.toHapiResponse(kibanaResponse);
|
||||
setHeaders(response, kibanaResponse.options.headers);
|
||||
return response;
|
||||
return this.toHapiResponse(kibanaResponse);
|
||||
}
|
||||
|
||||
private toHapiResponse(kibanaResponse: KibanaResponse<any>) {
|
||||
private toHapiResponse(kibanaResponse: KibanaResponse) {
|
||||
if (statusHelpers.isError(kibanaResponse.status)) {
|
||||
return this.toError(kibanaResponse);
|
||||
}
|
||||
if (statusHelpers.isSuccess(kibanaResponse.status)) {
|
||||
return this.toSuccess(kibanaResponse);
|
||||
}
|
||||
if (statusHelpers.isRedirect(kibanaResponse.status)) {
|
||||
return this.toRedirect(kibanaResponse);
|
||||
}
|
||||
if (statusHelpers.isError(kibanaResponse.status)) {
|
||||
return this.toError(kibanaResponse);
|
||||
}
|
||||
throw new Error(
|
||||
`Unexpected Http status code. Expected from 100 to 599, but given: ${kibanaResponse.status}.`
|
||||
);
|
||||
}
|
||||
|
||||
private toSuccess(kibanaResponse: KibanaResponse<HttpResponsePayload>) {
|
||||
return this.responseToolkit.response(kibanaResponse.payload).code(kibanaResponse.status);
|
||||
const response = this.responseToolkit
|
||||
.response(kibanaResponse.payload)
|
||||
.code(kibanaResponse.status);
|
||||
setHeaders(response, kibanaResponse.options.headers);
|
||||
return response;
|
||||
}
|
||||
|
||||
private toRedirect(kibanaResponse: KibanaResponse<HttpResponsePayload>) {
|
||||
|
@ -86,20 +103,33 @@ export class HapiResponseAdapter {
|
|||
throw new Error("expected 'location' header to be set");
|
||||
}
|
||||
|
||||
return this.responseToolkit
|
||||
const response = this.responseToolkit
|
||||
.response(kibanaResponse.payload)
|
||||
.redirect(headers.location)
|
||||
.code(kibanaResponse.status);
|
||||
.code(kibanaResponse.status)
|
||||
.takeover();
|
||||
|
||||
setHeaders(response, kibanaResponse.options.headers);
|
||||
return response;
|
||||
}
|
||||
|
||||
private toError(kibanaResponse: KibanaResponse<ResponseError>) {
|
||||
const { payload } = kibanaResponse;
|
||||
return this.responseToolkit
|
||||
.response({
|
||||
error: getErrorMessage(payload),
|
||||
meta: getErrorMeta(payload),
|
||||
})
|
||||
.code(kibanaResponse.status);
|
||||
// we use for BWC with Boom payload for error responses - {error: string, message: string, statusCode: string}
|
||||
const error = new Boom('', {
|
||||
statusCode: kibanaResponse.status,
|
||||
});
|
||||
|
||||
error.output.payload.message = getErrorMessage(payload);
|
||||
error.output.payload.meta = getErrorMeta(payload);
|
||||
|
||||
const headers = kibanaResponse.options.headers;
|
||||
if (headers) {
|
||||
// Hapi typings for header accept only strings, although string[] is a valid value
|
||||
error.output.headers = headers as any;
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
import { ObjectType, TypeOf, Type } from '@kbn/config-schema';
|
||||
import { Request, ResponseObject, ResponseToolkit } from 'hapi';
|
||||
import Boom from 'boom';
|
||||
|
||||
import { Logger } from '../../logging';
|
||||
import { KibanaRequest } from './request';
|
||||
|
@ -30,7 +31,11 @@ interface RouterRoute {
|
|||
method: RouteMethod;
|
||||
path: string;
|
||||
options: RouteConfigOptions;
|
||||
handler: (req: Request, responseToolkit: ResponseToolkit, log: Logger) => Promise<ResponseObject>;
|
||||
handler: (
|
||||
request: Request,
|
||||
responseToolkit: ResponseToolkit,
|
||||
log: Logger
|
||||
) => Promise<ResponseObject | Boom<any>>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -47,10 +52,8 @@ interface RouterRoute {
|
|||
* @public
|
||||
* */
|
||||
export class Router {
|
||||
public routes: Array<Readonly<RouterRoute>> = [];
|
||||
/**
|
||||
* @param path - a router path, set as the very first path segment for all registered routes.
|
||||
*/
|
||||
private routes: Array<Readonly<RouterRoute>> = [];
|
||||
|
||||
constructor(readonly path: string) {}
|
||||
|
||||
/**
|
||||
|
|
|
@ -78,6 +78,7 @@ export {
|
|||
IsAuthenticated,
|
||||
KibanaRequest,
|
||||
KibanaRequestRoute,
|
||||
LifecycleResponseFactory,
|
||||
KnownHeaders,
|
||||
LegacyRequest,
|
||||
OnPreAuthHandler,
|
||||
|
|
|
@ -25,9 +25,10 @@ import { Url } from 'url';
|
|||
export type APICaller = (endpoint: string, clientParams: Record<string, any>, options?: CallAPIOptions) => Promise<unknown>;
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "AuthResult" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// @public (undocumented)
|
||||
export type AuthenticationHandler = (request: KibanaRequest, t: AuthToolkit) => AuthResult | Promise<AuthResult>;
|
||||
export type AuthenticationHandler = (request: KibanaRequest, response: LifecycleResponseFactory, toolkit: AuthToolkit) => AuthResult | KibanaResponse | Promise<AuthResult | KibanaResponse>;
|
||||
|
||||
// @public
|
||||
export type AuthHeaders = Record<string, string | string[]>;
|
||||
|
@ -49,10 +50,6 @@ export enum AuthStatus {
|
|||
// @public
|
||||
export interface AuthToolkit {
|
||||
authenticated: (data?: AuthResultParams) => 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
|
||||
|
@ -303,9 +300,6 @@ export type KibanaResponseFactory = typeof kibanaResponseFactory;
|
|||
|
||||
// @public
|
||||
export const kibanaResponseFactory: {
|
||||
ok: (payload: HttpResponsePayload, options?: HttpResponseOptions) => KibanaResponse<string | Record<string, any> | Buffer | Stream>;
|
||||
accepted: (payload?: HttpResponsePayload, options?: HttpResponseOptions) => KibanaResponse<string | Record<string, any> | Buffer | Stream>;
|
||||
noContent: (options?: HttpResponseOptions) => KibanaResponse<undefined>;
|
||||
custom: (payload: string | Error | Record<string, any> | Buffer | Stream | {
|
||||
message: string | Error;
|
||||
meta?: ResponseErrorMeta | undefined;
|
||||
|
@ -313,13 +307,17 @@ export const kibanaResponseFactory: {
|
|||
message: string | Error;
|
||||
meta?: ResponseErrorMeta | undefined;
|
||||
}>;
|
||||
redirected: (payload: HttpResponsePayload, options: RedirectResponseOptions) => KibanaResponse<string | Record<string, any> | Buffer | Stream>;
|
||||
badRequest: (error?: ResponseError, options?: HttpResponseOptions) => KibanaResponse<ResponseError>;
|
||||
unauthorized: (error?: ResponseError, options?: HttpResponseOptions) => KibanaResponse<ResponseError>;
|
||||
forbidden: (error?: ResponseError, options?: HttpResponseOptions) => KibanaResponse<ResponseError>;
|
||||
notFound: (error?: ResponseError, options?: HttpResponseOptions) => KibanaResponse<ResponseError>;
|
||||
conflict: (error?: ResponseError, options?: HttpResponseOptions) => KibanaResponse<ResponseError>;
|
||||
internal: (error?: ResponseError, options?: HttpResponseOptions) => KibanaResponse<ResponseError>;
|
||||
internalError: (error?: ResponseError, options?: HttpResponseOptions) => KibanaResponse<ResponseError>;
|
||||
customError: (error: ResponseError, options: CustomHttpResponseOptions) => KibanaResponse<ResponseError>;
|
||||
redirected: (payload: HttpResponsePayload, options: RedirectResponseOptions) => KibanaResponse<string | Record<string, any> | Buffer | Stream>;
|
||||
ok: (payload: HttpResponsePayload, options?: HttpResponseOptions) => KibanaResponse<string | Record<string, any> | Buffer | Stream>;
|
||||
accepted: (payload?: HttpResponsePayload, options?: HttpResponseOptions) => KibanaResponse<string | Record<string, any> | Buffer | Stream>;
|
||||
noContent: (options?: HttpResponseOptions) => KibanaResponse<undefined>;
|
||||
};
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "KnownKeys" needs to be exported by the entry point index.d.ts
|
||||
|
@ -331,6 +329,11 @@ export type KnownHeaders = KnownKeys<IncomingHttpHeaders>;
|
|||
export interface LegacyRequest extends Request {
|
||||
}
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "lifecycleResponseFactory" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// @public
|
||||
export type LifecycleResponseFactory = typeof lifecycleResponseFactory;
|
||||
|
||||
// @public
|
||||
export interface Logger {
|
||||
debug(message: string, meta?: LogMeta): void;
|
||||
|
@ -403,31 +406,22 @@ export interface LogRecord {
|
|||
// Warning: (ae-forgotten-export) The symbol "OnPostAuthResult" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// @public (undocumented)
|
||||
export type OnPostAuthHandler<Params = any, Query = any, Body = any> = (request: KibanaRequest<Params, Query, Body>, t: OnPostAuthToolkit) => OnPostAuthResult | Promise<OnPostAuthResult>;
|
||||
export type OnPostAuthHandler = (request: KibanaRequest, response: LifecycleResponseFactory, toolkit: OnPostAuthToolkit) => OnPostAuthResult | KibanaResponse | Promise<OnPostAuthResult | KibanaResponse>;
|
||||
|
||||
// @public
|
||||
export interface OnPostAuthToolkit {
|
||||
next: () => OnPostAuthResult;
|
||||
redirected: (url: string) => OnPostAuthResult;
|
||||
rejected: (error: Error, options?: {
|
||||
statusCode?: number;
|
||||
}) => OnPostAuthResult;
|
||||
}
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "OnPreAuthResult" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// @public (undocumented)
|
||||
export type OnPreAuthHandler<Params = any, Query = any, Body = any> = (request: KibanaRequest<Params, Query, Body>, t: OnPreAuthToolkit) => OnPreAuthResult | Promise<OnPreAuthResult>;
|
||||
export type OnPreAuthHandler = (request: KibanaRequest, response: LifecycleResponseFactory, toolkit: OnPreAuthToolkit) => OnPreAuthResult | KibanaResponse | Promise<OnPreAuthResult | KibanaResponse>;
|
||||
|
||||
// @public
|
||||
export interface OnPreAuthToolkit {
|
||||
next: () => OnPreAuthResult;
|
||||
redirected: (url: string, options?: {
|
||||
forward: boolean;
|
||||
}) => OnPreAuthResult;
|
||||
rejected: (error: Error, options?: {
|
||||
statusCode?: number;
|
||||
}) => OnPreAuthResult;
|
||||
rewriteUrl: (url: string) => OnPreAuthResult;
|
||||
}
|
||||
|
||||
// @public
|
||||
|
@ -549,16 +543,14 @@ export class Router {
|
|||
constructor(path: string);
|
||||
delete<P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>): void;
|
||||
get<P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>): void;
|
||||
// Warning: (ae-forgotten-export) The symbol "RouterRoute" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// @internal
|
||||
getRoutes(): Readonly<RouterRoute>[];
|
||||
// (undocumented)
|
||||
readonly path: string;
|
||||
post<P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>): void;
|
||||
put<P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>): void;
|
||||
// Warning: (ae-forgotten-export) The symbol "RouterRoute" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
routes: Array<Readonly<RouterRoute>>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
|
@ -1029,7 +1021,6 @@ export interface SessionStorageFactory<T> {
|
|||
|
||||
// Warnings were encountered during analysis:
|
||||
//
|
||||
// src/core/server/http/router/response.ts:188:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts
|
||||
// src/core/server/plugins/plugins_service.ts:39:5 - (ae-forgotten-export) The symbol "DiscoveredPluginInternal" needs to be exported by the entry point index.d.ts
|
||||
// src/core/server/plugins/types.ts:162:10 - (ae-forgotten-export) The symbol "EnvironmentMode" needs to be exported by the entry point index.d.ts
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import { SpacesServiceSetup } from '../../new_platform/spaces_service/spaces_ser
|
|||
import {
|
||||
elasticsearchServiceMock,
|
||||
httpServiceMock,
|
||||
httpServerMock,
|
||||
} from '../../../../../../../src/core/server/mocks';
|
||||
import * as kbnTestServer from '../../../../../../../src/test_utils/kbn_server';
|
||||
import { HttpServiceSetup } from 'src/core/server';
|
||||
|
@ -184,17 +185,21 @@ describe('onPostAuthRequestInterceptor', () => {
|
|||
httpMock.basePath.set = jest.fn().mockImplementation((req: any, newPath: string) => {
|
||||
basePath = newPath;
|
||||
});
|
||||
const preAuthToolkit = httpServiceMock.createOnPreAuthToolkit();
|
||||
preAuthToolkit.rewriteUrl.mockImplementation(url => {
|
||||
path = url;
|
||||
return null as any;
|
||||
});
|
||||
httpMock.registerOnPreAuth = jest.fn().mockImplementation(async handler => {
|
||||
const preAuthRequest = {
|
||||
path,
|
||||
url: parse(path),
|
||||
};
|
||||
await handler(preAuthRequest, {
|
||||
redirected: jest.fn().mockImplementation(url => {
|
||||
path = url;
|
||||
}),
|
||||
next: jest.fn(),
|
||||
});
|
||||
await handler(
|
||||
preAuthRequest,
|
||||
httpServerMock.createLifecycleResponseFactory(),
|
||||
preAuthToolkit
|
||||
);
|
||||
});
|
||||
|
||||
const service = new SpacesService(log, configFn().get('server.basePath'));
|
||||
|
|
|
@ -3,7 +3,12 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { KibanaRequest, OnPreAuthToolkit, HttpServiceSetup } from 'src/core/server';
|
||||
import {
|
||||
KibanaRequest,
|
||||
OnPreAuthToolkit,
|
||||
LifecycleResponseFactory,
|
||||
HttpServiceSetup,
|
||||
} from 'src/core/server';
|
||||
import { KibanaConfig } from 'src/legacy/server/kbn_server';
|
||||
import { format } from 'url';
|
||||
import { DEFAULT_SPACE_ID } from '../../../common/constants';
|
||||
|
@ -19,6 +24,7 @@ export function initSpacesOnRequestInterceptor({ config, http }: OnRequestInterc
|
|||
|
||||
http.registerOnPreAuth(async function spacesOnPreAuthHandler(
|
||||
request: KibanaRequest,
|
||||
response: LifecycleResponseFactory,
|
||||
toolkit: OnPreAuthToolkit
|
||||
) {
|
||||
const path = request.url.pathname;
|
||||
|
@ -41,7 +47,7 @@ export function initSpacesOnRequestInterceptor({ config, http }: OnRequestInterc
|
|||
};
|
||||
});
|
||||
|
||||
return toolkit.redirected(newUrl, { forward: true });
|
||||
return toolkit.rewriteUrl(newUrl);
|
||||
}
|
||||
|
||||
return toolkit.next();
|
||||
|
|
|
@ -32,7 +32,6 @@ import {
|
|||
} from '../../../../../src/core/server';
|
||||
import { AuthenticatedUser } from '../../common/model';
|
||||
import { ConfigType, createConfig$ } from '../config';
|
||||
import { getErrorStatusCode } from '../errors';
|
||||
import { LegacyAPI } from '../plugin';
|
||||
import { AuthenticationResult } from './authentication_result';
|
||||
import { setupAuthentication } from '.';
|
||||
|
@ -132,21 +131,23 @@ describe('setupAuthentication()', () => {
|
|||
|
||||
it('replies with no credentials when security is disabled in elasticsearch', async () => {
|
||||
const mockRequest = httpServerMock.createKibanaRequest();
|
||||
const mockResponse = httpServerMock.createLifecycleResponseFactory();
|
||||
|
||||
mockXpackInfo.feature.mockReturnValue(mockXPackFeature({ isEnabled: false }));
|
||||
|
||||
await authHandler(mockRequest, mockAuthToolkit);
|
||||
await authHandler(mockRequest, mockResponse, mockAuthToolkit);
|
||||
|
||||
expect(mockAuthToolkit.authenticated).toHaveBeenCalledTimes(1);
|
||||
expect(mockAuthToolkit.authenticated).toHaveBeenCalledWith();
|
||||
expect(mockAuthToolkit.redirected).not.toHaveBeenCalled();
|
||||
expect(mockAuthToolkit.rejected).not.toHaveBeenCalled();
|
||||
expect(mockResponse.redirected).not.toHaveBeenCalled();
|
||||
expect(mockResponse.internalError).not.toHaveBeenCalled();
|
||||
|
||||
expect(authenticate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('continues request with credentials on success', async () => {
|
||||
const mockRequest = httpServerMock.createKibanaRequest();
|
||||
const mockResponse = httpServerMock.createLifecycleResponseFactory();
|
||||
const mockUser = mockAuthenticatedUser();
|
||||
const mockAuthHeaders = { authorization: 'Basic xxx' };
|
||||
|
||||
|
@ -154,15 +155,15 @@ describe('setupAuthentication()', () => {
|
|||
AuthenticationResult.succeeded(mockUser, { authHeaders: mockAuthHeaders })
|
||||
);
|
||||
|
||||
await authHandler(mockRequest, mockAuthToolkit);
|
||||
await authHandler(mockRequest, mockResponse, mockAuthToolkit);
|
||||
|
||||
expect(mockAuthToolkit.authenticated).toHaveBeenCalledTimes(1);
|
||||
expect(mockAuthToolkit.authenticated).toHaveBeenCalledWith({
|
||||
state: mockUser,
|
||||
requestHeaders: mockAuthHeaders,
|
||||
});
|
||||
expect(mockAuthToolkit.redirected).not.toHaveBeenCalled();
|
||||
expect(mockAuthToolkit.rejected).not.toHaveBeenCalled();
|
||||
expect(mockResponse.redirected).not.toHaveBeenCalled();
|
||||
expect(mockResponse.internalError).not.toHaveBeenCalled();
|
||||
|
||||
expect(authenticate).toHaveBeenCalledTimes(1);
|
||||
expect(authenticate).toHaveBeenCalledWith(mockRequest);
|
||||
|
@ -170,6 +171,7 @@ describe('setupAuthentication()', () => {
|
|||
|
||||
it('returns authentication response headers on success if any', async () => {
|
||||
const mockRequest = httpServerMock.createKibanaRequest();
|
||||
const mockResponse = httpServerMock.createLifecycleResponseFactory();
|
||||
const mockUser = mockAuthenticatedUser();
|
||||
const mockAuthHeaders = { authorization: 'Basic xxx' };
|
||||
const mockAuthResponseHeaders = { 'WWW-Authenticate': 'Negotiate' };
|
||||
|
@ -181,7 +183,7 @@ describe('setupAuthentication()', () => {
|
|||
})
|
||||
);
|
||||
|
||||
await authHandler(mockRequest, mockAuthToolkit);
|
||||
await authHandler(mockRequest, mockResponse, mockAuthToolkit);
|
||||
|
||||
expect(mockAuthToolkit.authenticated).toHaveBeenCalledTimes(1);
|
||||
expect(mockAuthToolkit.authenticated).toHaveBeenCalledWith({
|
||||
|
@ -189,53 +191,66 @@ describe('setupAuthentication()', () => {
|
|||
requestHeaders: mockAuthHeaders,
|
||||
responseHeaders: mockAuthResponseHeaders,
|
||||
});
|
||||
expect(mockAuthToolkit.redirected).not.toHaveBeenCalled();
|
||||
expect(mockAuthToolkit.rejected).not.toHaveBeenCalled();
|
||||
expect(mockResponse.redirected).not.toHaveBeenCalled();
|
||||
expect(mockResponse.internalError).not.toHaveBeenCalled();
|
||||
|
||||
expect(authenticate).toHaveBeenCalledTimes(1);
|
||||
expect(authenticate).toHaveBeenCalledWith(mockRequest);
|
||||
});
|
||||
|
||||
it('redirects user if redirection is requested by the authenticator', async () => {
|
||||
const mockResponse = httpServerMock.createLifecycleResponseFactory();
|
||||
authenticate.mockResolvedValue(AuthenticationResult.redirectTo('/some/url'));
|
||||
|
||||
await authHandler(httpServerMock.createKibanaRequest(), mockAuthToolkit);
|
||||
await authHandler(httpServerMock.createKibanaRequest(), mockResponse, mockAuthToolkit);
|
||||
|
||||
expect(mockAuthToolkit.redirected).toHaveBeenCalledTimes(1);
|
||||
expect(mockAuthToolkit.redirected).toHaveBeenCalledWith('/some/url');
|
||||
expect(mockResponse.redirected).toHaveBeenCalledTimes(1);
|
||||
expect(mockResponse.redirected).toHaveBeenCalledWith(undefined, {
|
||||
headers: { location: '/some/url' },
|
||||
});
|
||||
expect(mockAuthToolkit.authenticated).not.toHaveBeenCalled();
|
||||
expect(mockAuthToolkit.rejected).not.toHaveBeenCalled();
|
||||
expect(mockResponse.internalError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects with `Internal Server Error` when `authenticate` throws unhandled exception', async () => {
|
||||
it('rejects with `Internal Server Error` and log error when `authenticate` throws unhandled exception', async () => {
|
||||
const mockResponse = httpServerMock.createLifecycleResponseFactory();
|
||||
authenticate.mockRejectedValue(new Error('something went wrong'));
|
||||
|
||||
await authHandler(httpServerMock.createKibanaRequest(), mockAuthToolkit);
|
||||
await authHandler(httpServerMock.createKibanaRequest(), mockResponse, mockAuthToolkit);
|
||||
|
||||
expect(mockAuthToolkit.rejected).toHaveBeenCalledTimes(1);
|
||||
const [[error]] = mockAuthToolkit.rejected.mock.calls;
|
||||
expect(error.message).toBe('something went wrong');
|
||||
expect(getErrorStatusCode(error)).toBe(500);
|
||||
expect(mockResponse.internalError).toHaveBeenCalledTimes(1);
|
||||
const [[error]] = mockResponse.internalError.mock.calls;
|
||||
expect(error).toBeUndefined();
|
||||
|
||||
expect(mockAuthToolkit.authenticated).not.toHaveBeenCalled();
|
||||
expect(mockAuthToolkit.redirected).not.toHaveBeenCalled();
|
||||
expect(mockResponse.redirected).not.toHaveBeenCalled();
|
||||
expect(loggingServiceMock.collect(mockSetupAuthenticationParams.loggers).error)
|
||||
.toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
[Error: something went wrong],
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('rejects with wrapped original error when `authenticate` fails to authenticate user', async () => {
|
||||
it('rejects with original `badRequest` error when `authenticate` fails to authenticate user', async () => {
|
||||
const mockResponse = httpServerMock.createLifecycleResponseFactory();
|
||||
const esError = Boom.badRequest('some message');
|
||||
authenticate.mockResolvedValue(AuthenticationResult.failed(esError));
|
||||
|
||||
await authHandler(httpServerMock.createKibanaRequest(), mockAuthToolkit);
|
||||
await authHandler(httpServerMock.createKibanaRequest(), mockResponse, mockAuthToolkit);
|
||||
|
||||
expect(mockAuthToolkit.rejected).toHaveBeenCalledTimes(1);
|
||||
const [[error]] = mockAuthToolkit.rejected.mock.calls;
|
||||
expect(mockResponse.customError).toHaveBeenCalledTimes(1);
|
||||
const [[error]] = mockResponse.customError.mock.calls;
|
||||
expect(error).toBe(esError);
|
||||
|
||||
expect(mockAuthToolkit.authenticated).not.toHaveBeenCalled();
|
||||
expect(mockAuthToolkit.redirected).not.toHaveBeenCalled();
|
||||
expect(mockResponse.redirected).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('includes `WWW-Authenticate` header if `authenticate` fails to authenticate user and provides challenges', async () => {
|
||||
const mockResponse = httpServerMock.createLifecycleResponseFactory();
|
||||
const originalError = Boom.unauthorized('some message');
|
||||
originalError.output.headers['WWW-Authenticate'] = [
|
||||
'Basic realm="Access to prod", charset="UTF-8"',
|
||||
|
@ -248,29 +263,30 @@ describe('setupAuthentication()', () => {
|
|||
})
|
||||
);
|
||||
|
||||
await authHandler(httpServerMock.createKibanaRequest(), mockAuthToolkit);
|
||||
await authHandler(httpServerMock.createKibanaRequest(), mockResponse, mockAuthToolkit);
|
||||
|
||||
expect(mockAuthToolkit.rejected).toHaveBeenCalledTimes(1);
|
||||
const [[error]] = mockAuthToolkit.rejected.mock.calls;
|
||||
expect(error.message).toBe(originalError.message);
|
||||
expect((error as Boom).output.headers).toEqual({ 'WWW-Authenticate': 'Negotiate' });
|
||||
expect(mockResponse.customError).toHaveBeenCalledTimes(1);
|
||||
const [[error, options]] = mockResponse.customError.mock.calls;
|
||||
expect(error).toBe(originalError);
|
||||
expect(options!.headers).toEqual({ 'WWW-Authenticate': 'Negotiate' });
|
||||
|
||||
expect(mockAuthToolkit.authenticated).not.toHaveBeenCalled();
|
||||
expect(mockAuthToolkit.redirected).not.toHaveBeenCalled();
|
||||
expect(mockResponse.redirected).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns `unauthorized` when authentication can not be handled', async () => {
|
||||
const mockResponse = httpServerMock.createLifecycleResponseFactory();
|
||||
authenticate.mockResolvedValue(AuthenticationResult.notHandled());
|
||||
|
||||
await authHandler(httpServerMock.createKibanaRequest(), mockAuthToolkit);
|
||||
await authHandler(httpServerMock.createKibanaRequest(), mockResponse, mockAuthToolkit);
|
||||
|
||||
expect(mockAuthToolkit.rejected).toHaveBeenCalledTimes(1);
|
||||
const [[error]] = mockAuthToolkit.rejected.mock.calls;
|
||||
expect(error.message).toBe('Unauthorized');
|
||||
expect(getErrorStatusCode(error)).toBe(401);
|
||||
expect(mockResponse.unauthorized).toHaveBeenCalledTimes(1);
|
||||
const [[error]] = mockResponse.unauthorized.mock.calls;
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
|
||||
expect(mockAuthToolkit.authenticated).not.toHaveBeenCalled();
|
||||
expect(mockAuthToolkit.redirected).not.toHaveBeenCalled();
|
||||
expect(mockResponse.redirected).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -3,8 +3,6 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
import {
|
||||
ClusterClient,
|
||||
CoreSetup,
|
||||
|
@ -13,7 +11,7 @@ import {
|
|||
} from '../../../../../src/core/server';
|
||||
import { AuthenticatedUser } from '../../common/model';
|
||||
import { ConfigType } from '../config';
|
||||
import { getErrorStatusCode, wrapError } from '../errors';
|
||||
import { getErrorStatusCode } from '../errors';
|
||||
import { Authenticator, ProviderSession } from './authenticator';
|
||||
import { LegacyAPI } from '../plugin';
|
||||
import { createAPIKey, CreateAPIKeyOptions } from './api_keys';
|
||||
|
@ -77,7 +75,7 @@ export async function setupAuthentication({
|
|||
|
||||
authLogger.debug('Successfully initialized authenticator.');
|
||||
|
||||
core.http.registerAuth(async (request, t) => {
|
||||
core.http.registerAuth(async (request, response, t) => {
|
||||
// If security is disabled continue with no user credentials and delete the client cookie as well.
|
||||
if (isSecurityFeatureDisabled()) {
|
||||
return t.authenticated();
|
||||
|
@ -88,7 +86,7 @@ export async function setupAuthentication({
|
|||
authenticationResult = await authenticator.authenticate(request);
|
||||
} catch (err) {
|
||||
authLogger.error(err);
|
||||
return t.rejected(wrapError(err));
|
||||
return response.internalError();
|
||||
}
|
||||
|
||||
if (authenticationResult.succeeded()) {
|
||||
|
@ -105,28 +103,34 @@ export async function setupAuthentication({
|
|||
// authentication (username and password) or arbitrary external page managed by 3rd party
|
||||
// Identity Provider for SSO authentication mechanisms. Authentication provider is the one who
|
||||
// decides what location user should be redirected to.
|
||||
return t.redirected(authenticationResult.redirectURL!);
|
||||
return response.redirected(undefined, {
|
||||
headers: {
|
||||
location: authenticationResult.redirectURL!,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let error;
|
||||
if (authenticationResult.failed()) {
|
||||
authLogger.info(`Authentication attempt failed: ${authenticationResult.error!.message}`);
|
||||
error = wrapError(authenticationResult.error);
|
||||
|
||||
const authResponseHeaders = authenticationResult.authResponseHeaders;
|
||||
for (const [headerName, headerValue] of Object.entries(authResponseHeaders || {})) {
|
||||
if (error.output.headers[headerName] !== undefined) {
|
||||
authLogger.warn(`Server rewrites a error response header [${headerName}].`);
|
||||
}
|
||||
// Hapi typings don't support headers that are `string[]`.
|
||||
error.output.headers[headerName] = headerValue as any;
|
||||
const error = authenticationResult.error!;
|
||||
// proxy Elasticsearch "native" errors
|
||||
const statusCode = getErrorStatusCode(error);
|
||||
if (typeof statusCode === 'number') {
|
||||
return response.customError(error, {
|
||||
statusCode,
|
||||
headers: authenticationResult.authResponseHeaders,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
authLogger.info('Could not handle authentication attempt');
|
||||
error = Boom.unauthorized();
|
||||
|
||||
return response.unauthorized(undefined, {
|
||||
headers: authenticationResult.authResponseHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
return t.rejected(error);
|
||||
authLogger.info('Could not handle authentication attempt');
|
||||
return response.unauthorized(undefined, {
|
||||
headers: authenticationResult.authResponseHeaders,
|
||||
});
|
||||
});
|
||||
|
||||
authLogger.debug('Successfully registered core authentication handler.');
|
||||
|
|
Loading…
Reference in a new issue