Skip to content

Commit

Permalink
Give user the option to log out if they encounter a 403 (#75538)
Browse files Browse the repository at this point in the history
  • Loading branch information
watson authored Oct 6, 2020
1 parent 4c65b6d commit e31ec7e
Show file tree
Hide file tree
Showing 113 changed files with 862 additions and 444 deletions.
1 change: 1 addition & 0 deletions docs/development/core/server/kibana-plugin-core-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ 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. |
| [OnPreResponseRender](./kibana-plugin-core-server.onpreresponserender.md) | Additional data to extend a response when rendering a new body |
| [OnPreResponseToolkit](./kibana-plugin-core-server.onpreresponsetoolkit.md) | A tool set defining an outcome of OnPreResponse 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. |
Expand Down
Original file line number Diff line number Diff line change
@@ -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; [OnPreResponseRender](./kibana-plugin-core-server.onpreresponserender.md) &gt; [body](./kibana-plugin-core-server.onpreresponserender.body.md)

## OnPreResponseRender.body property

the body to use in the response

<b>Signature:</b>

```typescript
body: string;
```
Original file line number Diff line number Diff line change
@@ -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; [OnPreResponseRender](./kibana-plugin-core-server.onpreresponserender.md) &gt; [headers](./kibana-plugin-core-server.onpreresponserender.headers.md)

## OnPreResponseRender.headers property

additional headers to attach to the response

<b>Signature:</b>

```typescript
headers?: ResponseHeaders;
```
Original file line number Diff line number Diff line change
@@ -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; [OnPreResponseRender](./kibana-plugin-core-server.onpreresponserender.md)

## OnPreResponseRender interface

Additional data to extend a response when rendering a new body

<b>Signature:</b>

```typescript
export interface OnPreResponseRender
```

## Properties

| Property | Type | Description |
| --- | --- | --- |
| [body](./kibana-plugin-core-server.onpreresponserender.body.md) | <code>string</code> | the body to use in the response |
| [headers](./kibana-plugin-core-server.onpreresponserender.headers.md) | <code>ResponseHeaders</code> | additional headers to attach to the response |

Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ export interface OnPreResponseToolkit
| Property | Type | Description |
| --- | --- | --- |
| [next](./kibana-plugin-core-server.onpreresponsetoolkit.next.md) | <code>(responseExtensions?: OnPreResponseExtensions) =&gt; OnPreResponseResult</code> | To pass request to the next handler |
| [render](./kibana-plugin-core-server.onpreresponsetoolkit.render.md) | <code>(responseRender: OnPreResponseRender) =&gt; OnPreResponseResult</code> | To override the response with a different body |

Original file line number Diff line number Diff line change
@@ -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; [OnPreResponseToolkit](./kibana-plugin-core-server.onpreresponsetoolkit.md) &gt; [render](./kibana-plugin-core-server.onpreresponsetoolkit.render.md)

## OnPreResponseToolkit.render property

To override the response with a different body

<b>Signature:</b>

```typescript
render: (responseRender: OnPreResponseRender) => OnPreResponseResult;
```
1 change: 1 addition & 0 deletions src/core/server/http/http_server.mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ type ToolkitMock = jest.Mocked<OnPreResponseToolkit & OnPostAuthToolkit & OnPreR

const createToolkitMock = (): ToolkitMock => {
return {
render: jest.fn(),
next: jest.fn(),
rewriteUrl: jest.fn(),
};
Expand Down
1 change: 1 addition & 0 deletions src/core/server/http/http_service.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ const createAuthToolkitMock = (): jest.Mocked<AuthToolkit> => ({
});

const createOnPreResponseToolkitMock = (): jest.Mocked<OnPreResponseToolkit> => ({
render: jest.fn(),
next: jest.fn(),
});

Expand Down
1 change: 1 addition & 0 deletions src/core/server/http/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export { OnPreAuthHandler, OnPreAuthToolkit } from './lifecycle/on_pre_auth';
export {
OnPreResponseHandler,
OnPreResponseToolkit,
OnPreResponseRender,
OnPreResponseExtensions,
OnPreResponseInfo,
} from './lifecycle/on_pre_response';
Expand Down
61 changes: 61 additions & 0 deletions src/core/server/http/integration_tests/lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1286,6 +1286,67 @@ describe('OnPreResponse', () => {

expect(requestBody).toStrictEqual({});
});

it('supports rendering a different response body', async () => {
const { registerOnPreResponse, server: innerServer, createRouter } = await server.setup(
setupDeps
);
const router = createRouter('/');

router.get({ path: '/', validate: false }, (context, req, res) => {
return res.ok({
headers: {
'Original-Header-A': 'A',
},
body: 'original',
});
});

registerOnPreResponse((req, res, t) => {
return t.render({ body: 'overridden' });
});

await server.start();

const result = await supertest(innerServer.listener).get('/').expect(200, 'overridden');

expect(result.header['original-header-a']).toBe('A');
});

it('supports rendering a different response body + headers', async () => {
const { registerOnPreResponse, server: innerServer, createRouter } = await server.setup(
setupDeps
);
const router = createRouter('/');

router.get({ path: '/', validate: false }, (context, req, res) => {
return res.ok({
headers: {
'Original-Header-A': 'A',
'Original-Header-B': 'B',
},
body: 'original',
});
});

registerOnPreResponse((req, res, t) => {
return t.render({
headers: {
'Original-Header-A': 'AA',
'New-Header-C': 'C',
},
body: 'overridden',
});
});

await server.start();

const result = await supertest(innerServer.listener).get('/').expect(200, 'overridden');

expect(result.header['original-header-a']).toBe('AA');
expect(result.header['original-header-b']).toBe('B');
expect(result.header['new-header-c']).toBe('C');
});
});

describe('run interceptors in the right order', () => {
Expand Down
79 changes: 61 additions & 18 deletions src/core/server/http/lifecycle/on_pre_response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,23 @@
* under the License.
*/

import { Lifecycle, Request, ResponseToolkit as HapiResponseToolkit } from 'hapi';
import { Lifecycle, Request, ResponseObject, ResponseToolkit as HapiResponseToolkit } from 'hapi';
import Boom from 'boom';
import { Logger } from '../../logging';

import { HapiResponseAdapter, KibanaRequest, ResponseHeaders } from '../router';

enum ResultType {
render = 'render',
next = 'next',
}

interface Render {
type: ResultType.render;
body: string;
headers?: ResponseHeaders;
}

interface Next {
type: ResultType.next;
headers?: ResponseHeaders;
Expand All @@ -35,7 +42,18 @@ interface Next {
/**
* @internal
*/
type OnPreResponseResult = Next;
type OnPreResponseResult = Render | Next;

/**
* Additional data to extend a response when rendering a new body
* @public
*/
export interface OnPreResponseRender {
/** additional headers to attach to the response */
headers?: ResponseHeaders;
/** the body to use in the response */
body: string;
}

/**
* Additional data to extend a response.
Expand All @@ -55,6 +73,12 @@ export interface OnPreResponseInfo {
}

const preResponseResult = {
render(responseRender: OnPreResponseRender): OnPreResponseResult {
return { type: ResultType.render, body: responseRender.body, headers: responseRender?.headers };
},
isRender(result: OnPreResponseResult): result is Render {
return result && result.type === ResultType.render;
},
next(responseExtensions?: OnPreResponseExtensions): OnPreResponseResult {
return { type: ResultType.next, headers: responseExtensions?.headers };
},
Expand All @@ -68,11 +92,14 @@ const preResponseResult = {
* @public
*/
export interface OnPreResponseToolkit {
/** To override the response with a different body */
render: (responseRender: OnPreResponseRender) => OnPreResponseResult;
/** To pass request to the next handler */
next: (responseExtensions?: OnPreResponseExtensions) => OnPreResponseResult;
}

const toolkit: OnPreResponseToolkit = {
render: preResponseResult.render,
next: preResponseResult.next,
};

Expand Down Expand Up @@ -106,26 +133,36 @@ export function adoptToHapiOnPreResponseFormat(fn: OnPreResponseHandler, log: Lo
: response.statusCode;

const result = await fn(KibanaRequest.from(request), { statusCode }, toolkit);
if (!preResponseResult.isNext(result)) {

if (preResponseResult.isNext(result)) {
if (result.headers) {
if (isBoom(response)) {
findHeadersIntersection(response.output.headers, result.headers, log);
// hapi wraps all error response in Boom object internally
response.output.headers = {
...response.output.headers,
...(result.headers as any), // hapi types don't specify string[] as valid value
};
} else {
findHeadersIntersection(response.headers, result.headers, log);
setHeaders(response, result.headers);
}
}
} else if (preResponseResult.isRender(result)) {
const overriddenResponse = responseToolkit.response(result.body).code(statusCode);

const originalHeaders = isBoom(response) ? response.output.headers : response.headers;
setHeaders(overriddenResponse, originalHeaders);
if (result.headers) {
setHeaders(overriddenResponse, result.headers);
}

return overriddenResponse;
} else {
throw new Error(
`Unexpected result from OnPreResponse. Expected OnPreResponseResult, but given: ${result}.`
);
}
if (result.headers) {
if (isBoom(response)) {
findHeadersIntersection(response.output.headers, result.headers, log);
// hapi wraps all error response in Boom object internally
response.output.headers = {
...response.output.headers,
...(result.headers as any), // hapi types don't specify string[] as valid value
};
} else {
findHeadersIntersection(response.headers, result.headers, log);
for (const [headerName, headerValue] of Object.entries(result.headers)) {
response.header(headerName, headerValue as any); // hapi types don't specify string[] as valid value
}
}
}
}
} catch (error) {
log.error(error);
Expand All @@ -140,6 +177,12 @@ function isBoom(response: any): response is Boom {
return response instanceof Boom;
}

function setHeaders(response: ResponseObject, headers: ResponseHeaders) {
for (const [headerName, headerValue] of Object.entries(headers)) {
response.header(headerName, headerValue as any); // hapi types don't specify string[] as valid value
}
}

// NOTE: responseHeaders contains not a full list of response headers, but only explicitly set on a response object.
// any headers added by hapi internally, like `content-type`, `content-length`, etc. are not present here.
function findHeadersIntersection(
Expand Down
1 change: 1 addition & 0 deletions src/core/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ export {
OnPostAuthToolkit,
OnPreResponseHandler,
OnPreResponseToolkit,
OnPreResponseRender,
OnPreResponseExtensions,
OnPreResponseInfo,
RedirectResponseOptions,
Expand Down
7 changes: 7 additions & 0 deletions src/core/server/server.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1530,9 +1530,16 @@ export interface OnPreResponseInfo {
statusCode: number;
}

// @public
export interface OnPreResponseRender {
body: string;
headers?: ResponseHeaders;
}

// @public
export interface OnPreResponseToolkit {
next: (responseExtensions?: OnPreResponseExtensions) => OnPreResponseResult;
render: (responseRender: OnPreResponseRender) => OnPreResponseResult;
}

// Warning: (ae-forgotten-export) The symbol "OnPreRoutingResult" needs to be exported by the entry point index.d.ts
Expand Down
7 changes: 6 additions & 1 deletion test/functional/page_objects/common_page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo
}
}

async getBodyText() {
async getJsonBodyText() {
if (await find.existsByCssSelector('a[id=rawdata-tab]', defaultFindTimeout)) {
// Firefox has 3 tabs and requires navigation to see Raw output
await find.clickByCssSelector('a[id=rawdata-tab]');
Expand All @@ -449,6 +449,11 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo
}
}

async getBodyText() {
const body = await find.byCssSelector('body');
return await body.getVisibleText();
}

/**
* Helper to detect an OSS licensed Kibana
* Useful for functional testing in cloud environment
Expand Down
10 changes: 2 additions & 8 deletions test/functional/page_objects/error_page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,11 @@ export function ErrorPageProvider({ getPageObjects }: FtrProviderContext) {
class ErrorPage {
public async expectForbidden() {
const messageText = await common.getBodyText();
expect(messageText).to.eql(
JSON.stringify({
statusCode: 403,
error: 'Forbidden',
message: 'Forbidden',
})
);
expect(messageText).to.contain('You do not have permission to access the requested page');
}

public async expectNotFound() {
const messageText = await common.getBodyText();
const messageText = await common.getJsonBodyText();
expect(messageText).to.eql(
JSON.stringify({
statusCode: 404,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,14 @@ const KIBANA_VERSION_HEADER = 'kbn-version';
*/
export function canRedirectRequest(request: KibanaRequest) {
const headers = request.headers;
const route = request.route;
const hasVersionHeader = headers.hasOwnProperty(KIBANA_VERSION_HEADER);
const hasXsrfHeader = headers.hasOwnProperty(KIBANA_XSRF_HEADER);

const isApiRoute = request.route.options.tags.includes(ROUTE_TAG_API);
const isApiRoute =
route.options.tags.includes(ROUTE_TAG_API) ||
(route.path.startsWith('/api/') && route.path !== '/api/security/logout') ||
route.path.startsWith('/internal/');
const isAjaxRequest = hasVersionHeader || hasXsrfHeader;

return !isApiRoute && !isAjaxRequest;
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit e31ec7e

Please sign in to comment.