[7.x] [KP] Separate onPreAuth & onPreRouting http interceptors (#70775) (#71523)

Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com>
Co-authored-by: Josh Dover <me@joshdover.com>
Co-authored-by: Mikhail Shustov <restrry@gmail.com>
This commit is contained in:
Josh Dover 2020-07-13 16:36:56 -06:00 committed by GitHub
parent 2f10543936
commit 3c7a9e1bfb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 630 additions and 122 deletions

View file

@ -88,8 +88,9 @@ async (context, request, response) => {
| [csp](./kibana-plugin-core-server.httpservicesetup.csp.md) | <code>ICspConfig</code> | The CSP config used for Kibana. |
| [getServerInfo](./kibana-plugin-core-server.httpservicesetup.getserverinfo.md) | <code>() =&gt; HttpServerInfo</code> | Provides common [information](./kibana-plugin-core-server.httpserverinfo.md) about the running http server. |
| [registerAuth](./kibana-plugin-core-server.httpservicesetup.registerauth.md) | <code>(handler: AuthenticationHandler) =&gt; void</code> | To define custom authentication and/or authorization mechanism for incoming requests. |
| [registerOnPostAuth](./kibana-plugin-core-server.httpservicesetup.registeronpostauth.md) | <code>(handler: OnPostAuthHandler) =&gt; void</code> | To define custom logic to perform for incoming requests. |
| [registerOnPreAuth](./kibana-plugin-core-server.httpservicesetup.registeronpreauth.md) | <code>(handler: OnPreAuthHandler) =&gt; void</code> | To define custom logic to perform for incoming requests. |
| [registerOnPostAuth](./kibana-plugin-core-server.httpservicesetup.registeronpostauth.md) | <code>(handler: OnPostAuthHandler) =&gt; void</code> | To define custom logic after Auth interceptor did make sure a user has access to the requested resource. |
| [registerOnPreAuth](./kibana-plugin-core-server.httpservicesetup.registeronpreauth.md) | <code>(handler: OnPreAuthHandler) =&gt; void</code> | To define custom logic to perform for incoming requests before the Auth interceptor performs a check that user has access to requested resources. |
| [registerOnPreResponse](./kibana-plugin-core-server.httpservicesetup.registeronpreresponse.md) | <code>(handler: OnPreResponseHandler) =&gt; void</code> | To define custom logic to perform for the server response. |
| [registerOnPreRouting](./kibana-plugin-core-server.httpservicesetup.registeronprerouting.md) | <code>(handler: OnPreRoutingHandler) =&gt; void</code> | To define custom logic to perform for incoming requests before server performs a route lookup. |
| [registerRouteHandlerContext](./kibana-plugin-core-server.httpservicesetup.registerroutehandlercontext.md) | <code>&lt;T extends keyof RequestHandlerContext&gt;(contextName: T, provider: RequestHandlerContextProvider&lt;T&gt;) =&gt; RequestHandlerContextContainer</code> | Register a context provider for a route handler. |

View file

@ -4,7 +4,7 @@
## HttpServiceSetup.registerOnPostAuth property
To define custom logic to perform for incoming requests.
To define custom logic after Auth interceptor did make sure a user has access to the requested resource.
<b>Signature:</b>
@ -14,5 +14,5 @@ registerOnPostAuth: (handler: OnPostAuthHandler) => void;
## Remarks
Runs the handler after Auth interceptor did make sure a user has access to the requested resource. The auth state is available at stage via http.auth.get(..) Can register any number of registerOnPreAuth, which are called in sequence (from the first registered to the last). See [OnPostAuthHandler](./kibana-plugin-core-server.onpostauthhandler.md)<!-- -->.
The auth state is available at stage via http.auth.get(..) Can register any number of registerOnPreRouting, which are called in sequence (from the first registered to the last). See [OnPostAuthHandler](./kibana-plugin-core-server.onpostauthhandler.md)<!-- -->.

View file

@ -4,7 +4,7 @@
## HttpServiceSetup.registerOnPreAuth property
To define custom logic to perform for incoming requests.
To define custom logic to perform for incoming requests before the Auth interceptor performs a check that user has access to requested resources.
<b>Signature:</b>
@ -14,5 +14,5 @@ registerOnPreAuth: (handler: OnPreAuthHandler) => void;
## Remarks
Runs the handler before Auth interceptor performs a check that user has access to requested resources, so it's the only place when you can forward a request to another URL right on the server. Can register any number of registerOnPostAuth, which are called in sequence (from the first registered to the last). See [OnPreAuthHandler](./kibana-plugin-core-server.onpreauthhandler.md)<!-- -->.
Can register any number of registerOnPostAuth, which are called in sequence (from the first registered to the last). See [OnPreRoutingHandler](./kibana-plugin-core-server.onpreroutinghandler.md)<!-- -->.

View file

@ -0,0 +1,18 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) &gt; [registerOnPreRouting](./kibana-plugin-core-server.httpservicesetup.registeronprerouting.md)
## HttpServiceSetup.registerOnPreRouting property
To define custom logic to perform for incoming requests before server performs a route lookup.
<b>Signature:</b>
```typescript
registerOnPreRouting: (handler: OnPreRoutingHandler) => void;
```
## Remarks
It's the only place when you can forward a request to another URL right on the server. Can register any number of registerOnPreRouting, which are called in sequence (from the first registered to the last). See [OnPreRoutingHandler](./kibana-plugin-core-server.onpreroutinghandler.md)<!-- -->.

View file

@ -122,7 +122,8 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [OnPreAuthToolkit](./kibana-plugin-core-server.onpreauthtoolkit.md) | A tool set defining an outcome of OnPreAuth interceptor for incoming request. |
| [OnPreResponseExtensions](./kibana-plugin-core-server.onpreresponseextensions.md) | Additional data to extend a response. |
| [OnPreResponseInfo](./kibana-plugin-core-server.onpreresponseinfo.md) | Response status code. |
| [OnPreResponseToolkit](./kibana-plugin-core-server.onpreresponsetoolkit.md) | A tool set defining an outcome of OnPreAuth interceptor for incoming request. |
| [OnPreResponseToolkit](./kibana-plugin-core-server.onpreresponsetoolkit.md) | A tool set defining an outcome of OnPreRouting interceptor for incoming request. |
| [OnPreRoutingToolkit](./kibana-plugin-core-server.onpreroutingtoolkit.md) | A tool set defining an outcome of OnPreRouting interceptor for incoming request. |
| [OpsMetrics](./kibana-plugin-core-server.opsmetrics.md) | Regroups metrics gathered by all the collectors. This contains metrics about the os/runtime, the kibana process and the http server. |
| [OpsOsMetrics](./kibana-plugin-core-server.opsosmetrics.md) | OS related metrics |
| [OpsProcessMetrics](./kibana-plugin-core-server.opsprocessmetrics.md) | Process related metrics |
@ -256,7 +257,8 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [MutatingOperationRefreshSetting](./kibana-plugin-core-server.mutatingoperationrefreshsetting.md) | Elasticsearch Refresh setting for mutating operation |
| [OnPostAuthHandler](./kibana-plugin-core-server.onpostauthhandler.md) | See [OnPostAuthToolkit](./kibana-plugin-core-server.onpostauthtoolkit.md)<!-- -->. |
| [OnPreAuthHandler](./kibana-plugin-core-server.onpreauthhandler.md) | See [OnPreAuthToolkit](./kibana-plugin-core-server.onpreauthtoolkit.md)<!-- -->. |
| [OnPreResponseHandler](./kibana-plugin-core-server.onpreresponsehandler.md) | See [OnPreAuthToolkit](./kibana-plugin-core-server.onpreauthtoolkit.md)<!-- -->. |
| [OnPreResponseHandler](./kibana-plugin-core-server.onpreresponsehandler.md) | See [OnPreRoutingToolkit](./kibana-plugin-core-server.onpreroutingtoolkit.md)<!-- -->. |
| [OnPreRoutingHandler](./kibana-plugin-core-server.onpreroutinghandler.md) | See [OnPreRoutingToolkit](./kibana-plugin-core-server.onpreroutingtoolkit.md)<!-- -->. |
| [PluginConfigSchema](./kibana-plugin-core-server.pluginconfigschema.md) | Dedicated type for plugin configuration schema. |
| [PluginInitializer](./kibana-plugin-core-server.plugininitializer.md) | The <code>plugin</code> export at the root of a plugin's <code>server</code> directory should conform to this interface. |
| [PluginName](./kibana-plugin-core-server.pluginname.md) | Dedicated type for plugin name/id that is supposed to make Map/Set/Arrays that use it as a key or value more obvious. |

View file

@ -17,5 +17,4 @@ export interface OnPreAuthToolkit
| Property | Type | Description |
| --- | --- | --- |
| [next](./kibana-plugin-core-server.onpreauthtoolkit.next.md) | <code>() =&gt; OnPreAuthResult</code> | To pass request to the next handler |
| [rewriteUrl](./kibana-plugin-core-server.onpreauthtoolkit.rewriteurl.md) | <code>(url: string) =&gt; OnPreAuthResult</code> | Rewrite requested resources url before is was authenticated and routed to a handler |

View file

@ -1,13 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [OnPreAuthToolkit](./kibana-plugin-core-server.onpreauthtoolkit.md) &gt; [rewriteUrl](./kibana-plugin-core-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;
```

View file

@ -4,7 +4,7 @@
## OnPreResponseHandler type
See [OnPreAuthToolkit](./kibana-plugin-core-server.onpreauthtoolkit.md)<!-- -->.
See [OnPreRoutingToolkit](./kibana-plugin-core-server.onpreroutingtoolkit.md)<!-- -->.
<b>Signature:</b>

View file

@ -4,7 +4,7 @@
## OnPreResponseToolkit interface
A tool set defining an outcome of OnPreAuth interceptor for incoming request.
A tool set defining an outcome of OnPreRouting interceptor for incoming request.
<b>Signature:</b>

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [OnPreRoutingHandler](./kibana-plugin-core-server.onpreroutinghandler.md)
## OnPreRoutingHandler type
See [OnPreRoutingToolkit](./kibana-plugin-core-server.onpreroutingtoolkit.md)<!-- -->.
<b>Signature:</b>
```typescript
export declare type OnPreRoutingHandler = (request: KibanaRequest, response: LifecycleResponseFactory, toolkit: OnPreRoutingToolkit) => OnPreRoutingResult | KibanaResponse | Promise<OnPreRoutingResult | KibanaResponse>;
```

View file

@ -0,0 +1,21 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [OnPreRoutingToolkit](./kibana-plugin-core-server.onpreroutingtoolkit.md)
## OnPreRoutingToolkit interface
A tool set defining an outcome of OnPreRouting interceptor for incoming request.
<b>Signature:</b>
```typescript
export interface OnPreRoutingToolkit
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [next](./kibana-plugin-core-server.onpreroutingtoolkit.next.md) | <code>() =&gt; OnPreRoutingResult</code> | To pass request to the next handler |
| [rewriteUrl](./kibana-plugin-core-server.onpreroutingtoolkit.rewriteurl.md) | <code>(url: string) =&gt; OnPreRoutingResult</code> | Rewrite requested resources url before is was authenticated and routed to a handler |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [OnPreRoutingToolkit](./kibana-plugin-core-server.onpreroutingtoolkit.md) &gt; [next](./kibana-plugin-core-server.onpreroutingtoolkit.next.md)
## OnPreRoutingToolkit.next property
To pass request to the next handler
<b>Signature:</b>
```typescript
next: () => OnPreRoutingResult;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [OnPreRoutingToolkit](./kibana-plugin-core-server.onpreroutingtoolkit.md) &gt; [rewriteUrl](./kibana-plugin-core-server.onpreroutingtoolkit.rewriteurl.md)
## OnPreRoutingToolkit.rewriteUrl property
Rewrite requested resources url before is was authenticated and routed to a handler
<b>Signature:</b>
```typescript
rewriteUrl: (url: string) => OnPreRoutingResult;
```

View file

@ -33,7 +33,7 @@ import {
} from './router';
import { OnPreResponseToolkit } from './lifecycle/on_pre_response';
import { OnPostAuthToolkit } from './lifecycle/on_post_auth';
import { OnPreAuthToolkit } from './lifecycle/on_pre_auth';
import { OnPreRoutingToolkit } from './lifecycle/on_pre_routing';
interface RequestFixtureOptions<P = any, Q = any, B = any> {
auth?: { isAuthenticated: boolean };
@ -161,7 +161,7 @@ const createLifecycleResponseFactoryMock = (): jest.Mocked<LifecycleResponseFact
customError: jest.fn(),
});
type ToolkitMock = jest.Mocked<OnPreResponseToolkit & OnPostAuthToolkit & OnPreAuthToolkit>;
type ToolkitMock = jest.Mocked<OnPreResponseToolkit & OnPostAuthToolkit & OnPreRoutingToolkit>;
const createToolkitMock = (): ToolkitMock => {
return {

View file

@ -1089,6 +1089,16 @@ describe('setup contract', () => {
});
});
describe('#registerOnPreRouting', () => {
test('does not throw if called after stop', async () => {
const { registerOnPreRouting } = await server.setup(config);
await server.stop();
expect(() => {
registerOnPreRouting((req, res) => res.unauthorized());
}).not.toThrow();
});
});
describe('#registerOnPreAuth', () => {
test('does not throw if called after stop', async () => {
const { registerOnPreAuth } = await server.setup(config);

View file

@ -24,8 +24,9 @@ import { Logger, LoggerFactory } from '../logging';
import { HttpConfig } from './http_config';
import { createServer, getListenerOptions, getServerOptions } from './http_tools';
import { adoptToHapiAuthFormat, AuthenticationHandler } from './lifecycle/auth';
import { adoptToHapiOnPreAuth, OnPreAuthHandler } from './lifecycle/on_pre_auth';
import { adoptToHapiOnPostAuthFormat, OnPostAuthHandler } from './lifecycle/on_post_auth';
import { adoptToHapiOnPreAuthFormat, OnPreAuthHandler } from './lifecycle/on_pre_auth';
import { adoptToHapiOnRequest, OnPreRoutingHandler } from './lifecycle/on_pre_routing';
import { adoptToHapiOnPreResponseFormat, OnPreResponseHandler } from './lifecycle/on_pre_response';
import { IRouter, RouteConfigOptions, KibanaRouteState, isSafeMethod } from './router';
import {
@ -49,8 +50,9 @@ export interface HttpServerSetup {
basePath: HttpServiceSetup['basePath'];
csp: HttpServiceSetup['csp'];
createCookieSessionStorageFactory: HttpServiceSetup['createCookieSessionStorageFactory'];
registerAuth: HttpServiceSetup['registerAuth'];
registerOnPreRouting: HttpServiceSetup['registerOnPreRouting'];
registerOnPreAuth: HttpServiceSetup['registerOnPreAuth'];
registerAuth: HttpServiceSetup['registerAuth'];
registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];
registerOnPreResponse: HttpServiceSetup['registerOnPreResponse'];
getAuthHeaders: GetAuthHeaders;
@ -64,7 +66,11 @@ export interface HttpServerSetup {
/** @internal */
export type LifecycleRegistrar = Pick<
HttpServerSetup,
'registerAuth' | 'registerOnPreAuth' | 'registerOnPostAuth' | 'registerOnPreResponse'
| 'registerOnPreRouting'
| 'registerOnPreAuth'
| 'registerAuth'
| 'registerOnPostAuth'
| 'registerOnPreResponse'
>;
export class HttpServer {
@ -113,12 +119,13 @@ export class HttpServer {
return {
registerRouter: this.registerRouter.bind(this),
registerStaticDir: this.registerStaticDir.bind(this),
registerOnPreRouting: this.registerOnPreRouting.bind(this),
registerOnPreAuth: this.registerOnPreAuth.bind(this),
registerAuth: this.registerAuth.bind(this),
registerOnPostAuth: this.registerOnPostAuth.bind(this),
registerOnPreResponse: this.registerOnPreResponse.bind(this),
createCookieSessionStorageFactory: <T>(cookieOptions: SessionStorageCookieOptions<T>) =>
this.createCookieSessionStorageFactory(cookieOptions, config.basePath),
registerAuth: this.registerAuth.bind(this),
basePath: basePathService,
csp: config.csp,
auth: {
@ -222,7 +229,7 @@ export class HttpServer {
return;
}
this.registerOnPreAuth((request, response, toolkit) => {
this.registerOnPreRouting((request, response, toolkit) => {
const oldUrl = request.url.href!;
const newURL = basePathService.remove(oldUrl);
const shouldRedirect = newURL !== oldUrl;
@ -263,6 +270,17 @@ export class HttpServer {
}
}
private registerOnPreAuth(fn: OnPreAuthHandler) {
if (this.server === undefined) {
throw new Error('Server is not created yet');
}
if (this.stopped) {
this.log.warn(`registerOnPreAuth called after stop`);
}
this.server.ext('onPreAuth', adoptToHapiOnPreAuth(fn, this.log));
}
private registerOnPostAuth(fn: OnPostAuthHandler) {
if (this.server === undefined) {
throw new Error('Server is not created yet');
@ -274,15 +292,15 @@ export class HttpServer {
this.server.ext('onPostAuth', adoptToHapiOnPostAuthFormat(fn, this.log));
}
private registerOnPreAuth(fn: OnPreAuthHandler) {
private registerOnPreRouting(fn: OnPreRoutingHandler) {
if (this.server === undefined) {
throw new Error('Server is not created yet');
}
if (this.stopped) {
this.log.warn(`registerOnPreAuth called after stop`);
this.log.warn(`registerOnPreRouting called after stop`);
}
this.server.ext('onRequest', adoptToHapiOnPreAuthFormat(fn, this.log));
this.server.ext('onRequest', adoptToHapiOnRequest(fn, this.log));
}
private registerOnPreResponse(fn: OnPreResponseHandler) {

View file

@ -29,7 +29,7 @@ import {
} from './types';
import { HttpService } from './http_service';
import { AuthStatus } from './auth_state_storage';
import { OnPreAuthToolkit } from './lifecycle/on_pre_auth';
import { OnPreRoutingToolkit } from './lifecycle/on_pre_routing';
import { AuthToolkit } from './lifecycle/auth';
import { sessionStorageMock } from './cookie_session_storage.mocks';
import { OnPostAuthToolkit } from './lifecycle/on_post_auth';
@ -87,6 +87,7 @@ const createInternalSetupContractMock = () => {
config: jest.fn().mockReturnValue(configMock.create()),
} as unknown) as jest.MockedClass<Server>,
createCookieSessionStorageFactory: jest.fn(),
registerOnPreRouting: jest.fn(),
registerOnPreAuth: jest.fn(),
registerAuth: jest.fn(),
registerOnPostAuth: jest.fn(),
@ -117,7 +118,8 @@ const createSetupContractMock = () => {
const mock: HttpServiceSetupMock = {
createCookieSessionStorageFactory: internalMock.createCookieSessionStorageFactory,
registerOnPreAuth: internalMock.registerOnPreAuth,
registerOnPreRouting: internalMock.registerOnPreRouting,
registerOnPreAuth: jest.fn(),
registerAuth: internalMock.registerAuth,
registerOnPostAuth: internalMock.registerOnPostAuth,
registerOnPreResponse: internalMock.registerOnPreResponse,
@ -173,7 +175,7 @@ const createHttpServiceMock = () => {
return mocked;
};
const createOnPreAuthToolkitMock = (): jest.Mocked<OnPreAuthToolkit> => ({
const createOnPreAuthToolkitMock = (): jest.Mocked<OnPreRoutingToolkit> => ({
next: jest.fn(),
rewriteUrl: jest.fn(),
});

View file

@ -64,7 +64,7 @@ export {
SafeRouteMethod,
} from './router';
export { BasePathProxyServer } from './base_path_proxy_server';
export { OnPreAuthHandler, OnPreAuthToolkit } from './lifecycle/on_pre_auth';
export { OnPreRoutingHandler, OnPreRoutingToolkit } from './lifecycle/on_pre_routing';
export {
AuthenticationHandler,
AuthHeaders,
@ -78,6 +78,7 @@ export {
AuthResultType,
} from './lifecycle/auth';
export { OnPostAuthHandler, OnPostAuthToolkit } from './lifecycle/on_post_auth';
export { OnPreAuthHandler, OnPreAuthToolkit } from './lifecycle/on_pre_auth';
export {
OnPreResponseHandler,
OnPreResponseToolkit,

View file

@ -337,7 +337,7 @@ describe('http service', () => {
it('basePath information for an incoming request is available in legacy server', async () => {
const reqBasePath = '/requests-specific-base-path';
const { http } = await root.setup();
http.registerOnPreAuth((req, res, toolkit) => {
http.registerOnPreRouting((req, res, toolkit) => {
http.basePath.set(req, reqBasePath);
return toolkit.next();
});

View file

@ -57,6 +57,188 @@ interface StorageData {
expires: number;
}
describe('OnPreRouting', () => {
it('supports registering a request interceptor', async () => {
const { registerOnPreRouting, server: innerServer, createRouter } = await server.setup(
setupDeps
);
const router = createRouter('/');
router.get({ path: '/', validate: false }, (context, req, res) => res.ok({ body: 'ok' }));
const callingOrder: string[] = [];
registerOnPreRouting((req, res, t) => {
callingOrder.push('first');
return t.next();
});
registerOnPreRouting((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 { registerOnPreRouting, server: innerServer, createRouter } = await server.setup(
setupDeps
);
const router = createRouter('/');
router.get({ path: '/initial', validate: false }, (context, req, res) =>
res.ok({ body: 'initial' })
);
router.get({ path: '/redirectUrl', validate: false }, (context, req, res) =>
res.ok({ body: 'redirected' })
);
let urlBeforeForwarding;
registerOnPreRouting((req, res, t) => {
urlBeforeForwarding = ensureRawRequest(req).raw.req.url;
return t.rewriteUrl('/redirectUrl');
});
let urlAfterForwarding;
registerOnPreRouting((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 { registerOnPreRouting, server: innerServer, createRouter } = await server.setup(
setupDeps
);
const router = createRouter('/');
const redirectUrl = '/redirectUrl';
router.get({ path: '/initial', validate: false }, (context, req, res) => res.ok());
registerOnPreRouting((req, res, t) =>
res.redirected({
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 { registerOnPreRouting, server: innerServer, createRouter } = await server.setup(
setupDeps
);
const router = createRouter('/');
router.get({ path: '/', validate: false }, (context, req, res) => res.ok());
registerOnPreRouting((req, res, t) =>
res.unauthorized({
headers: {
'www-authenticate': 'challenge',
},
})
);
await server.start();
const result = await supertest(innerServer.listener).get('/').expect(401);
expect(result.header['www-authenticate']).toBe('challenge');
});
it('does not expose error details if interceptor throws', async () => {
const { registerOnPreRouting, server: innerServer, createRouter } = await server.setup(
setupDeps
);
const router = createRouter('/');
router.get({ path: '/', validate: false }, (context, req, res) => res.ok());
registerOnPreRouting((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(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: reason],
],
]
`);
});
it('returns internal error if interceptor returns unexpected result', async () => {
const { registerOnPreRouting, server: innerServer, createRouter } = await server.setup(
setupDeps
);
const router = createRouter('/');
router.get({ path: '/', validate: false }, (context, req, res) => res.ok());
registerOnPreRouting((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(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: Unexpected result from OnPreRouting. Expected OnPreRoutingResult or KibanaResponse, but given: [object Object].],
],
]
`);
});
it(`doesn't share request object between interceptors`, async () => {
const { registerOnPreRouting, server: innerServer, createRouter } = await server.setup(
setupDeps
);
const router = createRouter('/');
registerOnPreRouting((req, res, t) => {
// don't complain customField is not defined on Request type
(req as any).customField = { value: 42 };
return t.next();
});
registerOnPreRouting((req, res, t) => {
// don't complain customField is not defined on Request type
if (typeof (req as any).customField !== 'undefined') {
throw new Error('Request object was mutated');
}
return t.next();
});
router.get({ path: '/', validate: false }, (context, req, res) =>
// don't complain customField is not defined on Request type
res.ok({ body: { customField: String((req as any).customField) } })
);
await server.start();
await supertest(innerServer.listener).get('/').expect(200, { customField: 'undefined' });
});
});
describe('OnPreAuth', () => {
it('supports registering a request interceptor', async () => {
const { registerOnPreAuth, server: innerServer, createRouter } = await server.setup(setupDeps);
@ -81,38 +263,6 @@ describe('OnPreAuth', () => {
expect(callingOrder).toEqual(['first', 'second']);
});
it('supports request forwarding to specified url', async () => {
const { registerOnPreAuth, server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
router.get({ path: '/initial', validate: false }, (context, req, res) =>
res.ok({ body: 'initial' })
);
router.get({ path: '/redirectUrl', validate: false }, (context, req, res) =>
res.ok({ body: 'redirected' })
);
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 { registerOnPreAuth, server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
@ -203,20 +353,20 @@ describe('OnPreAuth', () => {
const router = createRouter('/');
registerOnPreAuth((req, res, t) => {
// don't complain customField is not defined on Request type
(req as any).customField = { value: 42 };
// @ts-expect-error customField property is not defined on request object
req.customField = { value: 42 };
return t.next();
});
registerOnPreAuth((req, res, t) => {
// don't complain customField is not defined on Request type
if (typeof (req as any).customField !== 'undefined') {
// @ts-expect-error customField property is not defined on request object
if (typeof req.customField !== 'undefined') {
throw new Error('Request object was mutated');
}
return t.next();
});
router.get({ path: '/', validate: false }, (context, req, res) =>
// don't complain customField is not defined on Request type
res.ok({ body: { customField: String((req as any).customField) } })
// @ts-expect-error customField property is not defined on request object
res.ok({ body: { customField: String(req.customField) } })
);
await server.start();
@ -664,7 +814,7 @@ describe('Auth', () => {
it.skip('is the only place with access to the authorization header', async () => {
const {
registerOnPreAuth,
registerOnPreRouting,
registerAuth,
registerOnPostAuth,
server: innerServer,
@ -672,9 +822,9 @@ describe('Auth', () => {
} = await server.setup(setupDeps);
const router = createRouter('/');
let fromRegisterOnPreAuth;
await registerOnPreAuth((req, res, toolkit) => {
fromRegisterOnPreAuth = req.headers.authorization;
let fromregisterOnPreRouting;
await registerOnPreRouting((req, res, toolkit) => {
fromregisterOnPreRouting = req.headers.authorization;
return toolkit.next();
});
@ -701,7 +851,7 @@ describe('Auth', () => {
const token = 'Basic: user:password';
await supertest(innerServer.listener).get('/').set('Authorization', token).expect(200);
expect(fromRegisterOnPreAuth).toEqual({});
expect(fromregisterOnPreRouting).toEqual({});
expect(fromRegisterAuth).toEqual({ authorization: token });
expect(fromRegisterOnPostAuth).toEqual({});
expect(fromRouteHandler).toEqual({});
@ -1137,3 +1287,135 @@ describe('OnPreResponse', () => {
expect(requestBody).toStrictEqual({});
});
});
describe('run interceptors in the right order', () => {
it('with Auth registered', async () => {
const {
registerOnPreRouting,
registerOnPreAuth,
registerAuth,
registerOnPostAuth,
registerOnPreResponse,
server: innerServer,
createRouter,
} = await server.setup(setupDeps);
const router = createRouter('/');
const executionOrder: string[] = [];
registerOnPreRouting((req, res, t) => {
executionOrder.push('onPreRouting');
return t.next();
});
registerOnPreAuth((req, res, t) => {
executionOrder.push('onPreAuth');
return t.next();
});
registerAuth((req, res, t) => {
executionOrder.push('auth');
return t.authenticated({});
});
registerOnPostAuth((req, res, t) => {
executionOrder.push('onPostAuth');
return t.next();
});
registerOnPreResponse((req, res, t) => {
executionOrder.push('onPreResponse');
return t.next();
});
router.get({ path: '/', validate: false }, (context, req, res) => res.ok({ body: 'ok' }));
await server.start();
await supertest(innerServer.listener).get('/').expect(200);
expect(executionOrder).toEqual([
'onPreRouting',
'onPreAuth',
'auth',
'onPostAuth',
'onPreResponse',
]);
});
it('with no Auth registered', async () => {
const {
registerOnPreRouting,
registerOnPreAuth,
registerOnPostAuth,
registerOnPreResponse,
server: innerServer,
createRouter,
} = await server.setup(setupDeps);
const router = createRouter('/');
const executionOrder: string[] = [];
registerOnPreRouting((req, res, t) => {
executionOrder.push('onPreRouting');
return t.next();
});
registerOnPreAuth((req, res, t) => {
executionOrder.push('onPreAuth');
return t.next();
});
registerOnPostAuth((req, res, t) => {
executionOrder.push('onPostAuth');
return t.next();
});
registerOnPreResponse((req, res, t) => {
executionOrder.push('onPreResponse');
return t.next();
});
router.get({ path: '/', validate: false }, (context, req, res) => res.ok({ body: 'ok' }));
await server.start();
await supertest(innerServer.listener).get('/').expect(200);
expect(executionOrder).toEqual(['onPreRouting', 'onPreAuth', 'onPostAuth', 'onPreResponse']);
});
it('when a user failed auth', async () => {
const {
registerOnPreRouting,
registerOnPreAuth,
registerOnPostAuth,
registerAuth,
registerOnPreResponse,
server: innerServer,
createRouter,
} = await server.setup(setupDeps);
const router = createRouter('/');
const executionOrder: string[] = [];
registerOnPreRouting((req, res, t) => {
executionOrder.push('onPreRouting');
return t.next();
});
registerOnPreAuth((req, res, t) => {
executionOrder.push('onPreAuth');
return t.next();
});
registerAuth((req, res, t) => {
executionOrder.push('auth');
return res.forbidden();
});
registerOnPostAuth((req, res, t) => {
executionOrder.push('onPostAuth');
return t.next();
});
registerOnPreResponse((req, res, t) => {
executionOrder.push('onPreResponse');
return t.next();
});
router.get({ path: '/', validate: false }, (context, req, res) => res.ok({ body: 'ok' }));
await server.start();
await supertest(innerServer.listener).get('/').expect(403);
expect(executionOrder).toEqual(['onPreRouting', 'onPreAuth', 'auth', 'onPreResponse']);
});
});

View file

@ -29,33 +29,21 @@ import {
enum ResultType {
next = 'next',
rewriteUrl = 'rewriteUrl',
}
interface Next {
type: ResultType.next;
}
interface RewriteUrl {
type: ResultType.rewriteUrl;
url: string;
}
type OnPreAuthResult = Next | RewriteUrl;
type OnPreAuthResult = Next;
const preAuthResult = {
next(): OnPreAuthResult {
return { type: ResultType.next };
},
rewriteUrl(url: string): OnPreAuthResult {
return { type: ResultType.rewriteUrl, url };
},
isNext(result: OnPreAuthResult): result is Next {
return result && result.type === ResultType.next;
},
isRewriteUrl(result: OnPreAuthResult): result is RewriteUrl {
return result && result.type === ResultType.rewriteUrl;
},
};
/**
@ -65,13 +53,10 @@ const preAuthResult = {
export interface OnPreAuthToolkit {
/** To pass request to the next handler */
next: () => OnPreAuthResult;
/** Rewrite requested resources url before is was authenticated and routed to a handler */
rewriteUrl: (url: string) => OnPreAuthResult;
}
const toolkit: OnPreAuthToolkit = {
next: preAuthResult.next,
rewriteUrl: preAuthResult.rewriteUrl,
};
/**
@ -88,9 +73,9 @@ export type OnPreAuthHandler = (
* @public
* Adopt custom request interceptor to Hapi lifecycle system.
* @param fn - an extension point allowing to perform custom logic for
* incoming HTTP requests.
* incoming HTTP requests before a user has been authenticated.
*/
export function adoptToHapiOnPreAuthFormat(fn: OnPreAuthHandler, log: Logger) {
export function adoptToHapiOnPreAuth(fn: OnPreAuthHandler, log: Logger) {
return async function interceptPreAuthRequest(
request: Request,
responseToolkit: HapiResponseToolkit
@ -107,13 +92,6 @@ export function adoptToHapiOnPreAuthFormat(fn: OnPreAuthHandler, log: Logger) {
return responseToolkit.continue;
}
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;
}
throw new Error(
`Unexpected result from OnPreAuth. Expected OnPreAuthResult or KibanaResponse, but given: ${result}.`
);

View file

@ -64,7 +64,7 @@ const preResponseResult = {
};
/**
* A tool set defining an outcome of OnPreAuth interceptor for incoming request.
* A tool set defining an outcome of OnPreResponse interceptor for incoming request.
* @public
*/
export interface OnPreResponseToolkit {
@ -77,7 +77,7 @@ const toolkit: OnPreResponseToolkit = {
};
/**
* See {@link OnPreAuthToolkit}.
* See {@link OnPreRoutingToolkit}.
* @public
*/
export type OnPreResponseHandler = (

View file

@ -0,0 +1,125 @@
/*
* 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 { Lifecycle, Request, ResponseToolkit as HapiResponseToolkit } from 'hapi';
import { Logger } from '../../logging';
import {
HapiResponseAdapter,
KibanaRequest,
KibanaResponse,
lifecycleResponseFactory,
LifecycleResponseFactory,
} from '../router';
enum ResultType {
next = 'next',
rewriteUrl = 'rewriteUrl',
}
interface Next {
type: ResultType.next;
}
interface RewriteUrl {
type: ResultType.rewriteUrl;
url: string;
}
type OnPreRoutingResult = Next | RewriteUrl;
const preRoutingResult = {
next(): OnPreRoutingResult {
return { type: ResultType.next };
},
rewriteUrl(url: string): OnPreRoutingResult {
return { type: ResultType.rewriteUrl, url };
},
isNext(result: OnPreRoutingResult): result is Next {
return result && result.type === ResultType.next;
},
isRewriteUrl(result: OnPreRoutingResult): result is RewriteUrl {
return result && result.type === ResultType.rewriteUrl;
},
};
/**
* @public
* A tool set defining an outcome of OnPreRouting interceptor for incoming request.
*/
export interface OnPreRoutingToolkit {
/** To pass request to the next handler */
next: () => OnPreRoutingResult;
/** Rewrite requested resources url before is was authenticated and routed to a handler */
rewriteUrl: (url: string) => OnPreRoutingResult;
}
const toolkit: OnPreRoutingToolkit = {
next: preRoutingResult.next,
rewriteUrl: preRoutingResult.rewriteUrl,
};
/**
* See {@link OnPreRoutingToolkit}.
* @public
*/
export type OnPreRoutingHandler = (
request: KibanaRequest,
response: LifecycleResponseFactory,
toolkit: OnPreRoutingToolkit
) => OnPreRoutingResult | KibanaResponse | Promise<OnPreRoutingResult | KibanaResponse>;
/**
* @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 adoptToHapiOnRequest(fn: OnPreRoutingHandler, log: Logger) {
return async function interceptPreRoutingRequest(
request: Request,
responseToolkit: HapiResponseToolkit
): Promise<Lifecycle.ReturnValue> {
const hapiResponseAdapter = new HapiResponseAdapter(responseToolkit);
try {
const result = await fn(KibanaRequest.from(request), lifecycleResponseFactory, toolkit);
if (result instanceof KibanaResponse) {
return hapiResponseAdapter.handle(result);
}
if (preRoutingResult.isNext(result)) {
return responseToolkit.continue;
}
if (preRoutingResult.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;
}
throw new Error(
`Unexpected result from OnPreRouting. Expected OnPreRoutingResult or KibanaResponse, but given: ${result}.`
);
} catch (error) {
log.error(error);
return hapiResponseAdapter.toInternalError();
}
};
}

View file

@ -25,6 +25,7 @@ import { HttpServerSetup } from './http_server';
import { SessionStorageCookieOptions } from './cookie_session_storage';
import { SessionStorageFactory } from './session_storage';
import { AuthenticationHandler } from './lifecycle/auth';
import { OnPreRoutingHandler } from './lifecycle/on_pre_routing';
import { OnPreAuthHandler } from './lifecycle/on_pre_auth';
import { OnPostAuthHandler } from './lifecycle/on_post_auth';
import { OnPreResponseHandler } from './lifecycle/on_pre_response';
@ -145,15 +146,26 @@ export interface HttpServiceSetup {
) => Promise<SessionStorageFactory<T>>;
/**
* To define custom logic to perform for incoming requests.
* To define custom logic to perform for incoming requests before server performs a route lookup.
*
* @remarks
* Runs the handler before Auth interceptor performs a check that user has access to requested resources, so it's the
* only place when you can forward a request to another URL right on the server.
* Can register any number of registerOnPostAuth, which are called in sequence
* It's the only place when you can forward a request to another URL right on the server.
* Can register any number of registerOnPreRouting, which are called in sequence
* (from the first registered to the last). See {@link OnPreRoutingHandler}.
*
* @param handler {@link OnPreRoutingHandler} - function to call.
*/
registerOnPreRouting: (handler: OnPreRoutingHandler) => void;
/**
* To define custom logic to perform for incoming requests before
* the Auth interceptor performs a check that user has access to requested resources.
*
* @remarks
* Can register any number of registerOnPreAuth, which are called in sequence
* (from the first registered to the last). See {@link OnPreAuthHandler}.
*
* @param handler {@link OnPreAuthHandler} - function to call.
* @param handler {@link OnPreRoutingHandler} - function to call.
*/
registerOnPreAuth: (handler: OnPreAuthHandler) => void;
@ -170,13 +182,11 @@ export interface HttpServiceSetup {
registerAuth: (handler: AuthenticationHandler) => void;
/**
* To define custom logic to perform for incoming requests.
* To define custom logic after Auth interceptor did make sure a user has access to the requested resource.
*
* @remarks
* Runs the handler after Auth interceptor
* did make sure a user has access to the requested resource.
* The auth state is available at stage via http.auth.get(..)
* Can register any number of registerOnPreAuth, which are called in sequence
* Can register any number of registerOnPostAuth, which are called in sequence
* (from the first registered to the last). See {@link OnPostAuthHandler}.
*
* @param handler {@link OnPostAuthHandler} - function to call.

View file

@ -148,6 +148,8 @@ export {
LegacyRequest,
OnPreAuthHandler,
OnPreAuthToolkit,
OnPreRoutingHandler,
OnPreRoutingToolkit,
OnPostAuthHandler,
OnPostAuthToolkit,
OnPreResponseHandler,

View file

@ -301,6 +301,7 @@ export class LegacyService implements CoreService {
),
createRouter: () => router,
resources: setupDeps.core.httpResources.createRegistrar(router),
registerOnPreRouting: setupDeps.core.http.registerOnPreRouting,
registerOnPreAuth: setupDeps.core.http.registerOnPreAuth,
registerAuth: setupDeps.core.http.registerAuth,
registerOnPostAuth: setupDeps.core.http.registerOnPostAuth,

View file

@ -157,6 +157,7 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>(
),
createRouter: () => router,
resources: deps.httpResources.createRegistrar(router),
registerOnPreRouting: deps.http.registerOnPreRouting,
registerOnPreAuth: deps.http.registerOnPreAuth,
registerAuth: deps.http.registerAuth,
registerOnPostAuth: deps.http.registerOnPostAuth,

View file

@ -811,6 +811,7 @@ export interface HttpServiceSetup {
registerOnPostAuth: (handler: OnPostAuthHandler) => void;
registerOnPreAuth: (handler: OnPreAuthHandler) => void;
registerOnPreResponse: (handler: OnPreResponseHandler) => void;
registerOnPreRouting: (handler: OnPreRoutingHandler) => void;
registerRouteHandlerContext: <T extends keyof RequestHandlerContext>(contextName: T, provider: RequestHandlerContextProvider<T>) => RequestHandlerContextContainer;
}
@ -1536,7 +1537,6 @@ export type OnPreAuthHandler = (request: KibanaRequest, response: LifecycleRespo
// @public
export interface OnPreAuthToolkit {
next: () => OnPreAuthResult;
rewriteUrl: (url: string) => OnPreAuthResult;
}
// @public
@ -1560,6 +1560,17 @@ export interface OnPreResponseToolkit {
next: (responseExtensions?: OnPreResponseExtensions) => OnPreResponseResult;
}
// Warning: (ae-forgotten-export) The symbol "OnPreRoutingResult" needs to be exported by the entry point index.d.ts
//
// @public
export type OnPreRoutingHandler = (request: KibanaRequest, response: LifecycleResponseFactory, toolkit: OnPreRoutingToolkit) => OnPreRoutingResult | KibanaResponse | Promise<OnPreRoutingResult | KibanaResponse>;
// @public
export interface OnPreRoutingToolkit {
next: () => OnPreRoutingResult;
rewriteUrl: (url: string) => OnPreRoutingResult;
}
// @public
export interface OpsMetrics {
concurrent_connections: OpsServerMetrics['concurrent_connections'];

View file

@ -5,7 +5,7 @@
*/
import {
KibanaRequest,
OnPreAuthToolkit,
OnPreRoutingToolkit,
LifecycleResponseFactory,
CoreSetup,
} from 'src/core/server';
@ -18,10 +18,10 @@ export interface OnRequestInterceptorDeps {
http: CoreSetup['http'];
}
export function initSpacesOnRequestInterceptor({ http }: OnRequestInterceptorDeps) {
http.registerOnPreAuth(async function spacesOnPreAuthHandler(
http.registerOnPreRouting(async function spacesOnPreRoutingHandler(
request: KibanaRequest,
response: LifecycleResponseFactory,
toolkit: OnPreAuthToolkit
toolkit: OnPreRoutingToolkit
) {
const serverBasePath = http.basePath.serverBasePath;
const path = request.url.pathname;