Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NP licensing plugin improvements #51818

Merged
merged 26 commits into from
Dec 9, 2019
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
6eb8572
add onPreResponse interceptor
mshustov Nov 27, 2019
c861c8b
use onPreResponse interceptor to add license sign
mshustov Nov 27, 2019
58a31d2
expose registerPreResponse to plugins
mshustov Nov 27, 2019
d536a92
refresh for license update get the most fresh license
mshustov Nov 27, 2019
c194e37
license plugin injects own header for signature: 'kbn-license-sig'
mshustov Nov 27, 2019
674062e
add integration tests for license type and license header
mshustov Nov 27, 2019
5c30f9f
Merge branch 'master' into np-licensing
mshustov Nov 28, 2019
b5e409a
Merge branch 'master' into np-licensing
mshustov Nov 28, 2019
58c92b6
switch config to duration
mshustov Nov 29, 2019
61d8a72
don't run interceptor on anon paths. add tests
mshustov Nov 29, 2019
2166b3d
add functional tests for licensing plugin
mshustov Nov 29, 2019
268cc45
regen docs
mshustov Nov 29, 2019
a3a890a
fix test in security due to updated mocks;
mshustov Nov 29, 2019
b1b79a8
Merge branch 'master' into np-licensing
mshustov Dec 2, 2019
6f0397d
update snapshots accoring to new mock implementation
mshustov Dec 2, 2019
d4cb636
Merge branch 'master' into np-licensing
mshustov Dec 3, 2019
5992878
migrate license expired banner to NP
mshustov Dec 3, 2019
10a91eb
Merge branch 'master' into np-licensing
mshustov Dec 4, 2019
4b45708
add readme for the licensing plugin
mshustov Dec 4, 2019
6b9c7bf
remove outdated import. licensing has separate functional tests
mshustov Dec 4, 2019
b9fbe71
add tag for test to run on CI
mshustov Dec 4, 2019
c9d6259
Merge branch 'master' into np-licensing
mshustov Dec 6, 2019
7eec72a
regen docs
mshustov Dec 6, 2019
404582d
Update x-pack/plugins/licensing/README.md
mshustov Dec 7, 2019
cf83cb9
Merge branch 'master' into np-licensing
mshustov Dec 9, 2019
5168054
update tests
mshustov Dec 9, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/core/public/http/http_service.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ import { HttpService } from './http_service';
import { HttpSetup } from './types';
import { BehaviorSubject } from 'rxjs';
import { BasePath } from './base_path_service';
import { AnonymousPaths } from './anonymous_paths';

type ServiceSetupMockType = jest.Mocked<HttpSetup> & {
basePath: BasePath;
anonymousPaths: jest.Mocked<HttpSetup['anonymousPaths']>;
};

const createServiceMock = ({ basePath = '' } = {}): ServiceSetupMockType => ({
Expand All @@ -37,7 +37,10 @@ const createServiceMock = ({ basePath = '' } = {}): ServiceSetupMockType => ({
delete: jest.fn(),
options: jest.fn(),
basePath: new BasePath(basePath),
anonymousPaths: new AnonymousPaths(new BasePath(basePath)),
anonymousPaths: {
register: jest.fn(),
isAnonymous: jest.fn(),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

plugin authors doesn't have to read AnonymousPaths implementation to be able to use mocks. WDYT?

},
addLoadingCount: jest.fn(),
getLoadingCount$: jest.fn().mockReturnValue(new BehaviorSubject(0)),
stop: jest.fn(),
Expand Down
6 changes: 3 additions & 3 deletions src/core/public/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*/
import { applicationServiceMock } from './application/application_service.mock';
import { chromeServiceMock } from './chrome/chrome_service.mock';
import { CoreContext, CoreSetup, CoreStart, PluginInitializerContext, NotificationsSetup } from '.';
import { CoreContext, PluginInitializerContext } from '.';
import { docLinksServiceMock } from './doc_links/doc_links_service.mock';
import { fatalErrorsServiceMock } from './fatal_errors/fatal_errors_service.mock';
import { httpServiceMock } from './http/http_service.mock';
Expand All @@ -42,7 +42,7 @@ export { overlayServiceMock } from './overlays/overlay_service.mock';
export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock';

function createCoreSetupMock({ basePath = '' } = {}) {
const mock: MockedKeys<CoreSetup> & { notifications: MockedKeys<NotificationsSetup> } = {
const mock = {
application: applicationServiceMock.createSetupContract(),
context: contextServiceMock.createSetupContract(),
fatalErrors: fatalErrorsServiceMock.createSetupContract(),
Expand All @@ -58,7 +58,7 @@ function createCoreSetupMock({ basePath = '' } = {}) {
}

function createCoreStartMock({ basePath = '' } = {}) {
const mock: MockedKeys<CoreStart> & { notifications: MockedKeys<NotificationsSetup> } = {
const mock = {
application: applicationServiceMock.createStartContract(),
chrome: chromeServiceMock.createStartContract(),
docLinks: docLinksServiceMock.createStartContract(),
Expand Down
49 changes: 15 additions & 34 deletions src/core/server/http/http_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,17 @@
* under the License.
*/

import { Request, Server } from 'hapi';
import { Server } from 'hapi';

import { Logger, LoggerFactory } from '../logging';
import { HttpConfig } from './http_config';
import { createServer, getListenerOptions, getServerOptions } from './http_tools';
import { adoptToHapiAuthFormat, AuthenticationHandler } from './lifecycle/auth';
import { adoptToHapiOnPostAuthFormat, OnPostAuthHandler } from './lifecycle/on_post_auth';
import { adoptToHapiOnPreAuthFormat, OnPreAuthHandler } from './lifecycle/on_pre_auth';
import { adoptToHapiOnPreResponseFormat, OnPreResponseHandler } from './lifecycle/on_pre_response';

import { ResponseHeaders, IRouter } from './router';
import { IRouter } from './router';
import {
SessionStorageCookieOptions,
createCookieSessionStorageFactory,
Expand All @@ -49,6 +50,7 @@ export interface HttpServerSetup {
registerAuth: HttpServiceSetup['registerAuth'];
registerOnPreAuth: HttpServiceSetup['registerOnPreAuth'];
registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];
registerOnPreResponse: HttpServiceSetup['registerOnPreResponse'];
isTlsEnabled: HttpServiceSetup['isTlsEnabled'];
auth: {
get: GetAuthState;
Expand Down Expand Up @@ -101,6 +103,7 @@ export class HttpServer {
registerRouter: this.registerRouter.bind(this),
registerOnPreAuth: this.registerOnPreAuth.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),
Expand Down Expand Up @@ -203,6 +206,14 @@ export class HttpServer {
this.server.ext('onRequest', adoptToHapiOnPreAuthFormat(fn, this.log));
}

private registerOnPreResponse(fn: OnPreResponseHandler) {
if (this.server === undefined) {
throw new Error('Server is not created yet');
}

this.server.ext('onPreResponse', adoptToHapiOnPreResponseFormat(fn, this.log));
}

private async createCookieSessionStorageFactory<T>(
cookieOptions: SessionStorageCookieOptions<T>,
basePath?: string
Expand Down Expand Up @@ -260,39 +271,9 @@ export class HttpServer {
// https://github.com/hapijs/hapi/blob/master/API.md#-serverauthdefaultoptions
this.server.auth.default('session');

this.server.ext('onPreResponse', (request, t) => {
this.registerOnPreResponse((request, responseInfo, t) => {
const authResponseHeaders = this.authResponseHeaders.get(request);
this.extendResponseWithHeaders(request, authResponseHeaders);
return t.continue;
});
}

private extendResponseWithHeaders(request: Request, headers?: ResponseHeaders) {
const response = request.response;
if (!headers || !response) return;

if (response instanceof Error) {
this.findHeadersIntersection(response.output.headers, headers);
// hapi wraps all error response in Boom object internally
response.output.headers = {
...response.output.headers,
...(headers as any), // hapi types don't specify string[] as valid value
};
} else {
for (const [headerName, headerValue] of Object.entries(headers)) {
this.findHeadersIntersection(response.headers, 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. do not present here.
private findHeadersIntersection(responseHeaders: ResponseHeaders, headers: ResponseHeaders) {
Object.keys(headers).forEach(headerName => {
if (responseHeaders[headerName] !== undefined) {
this.log.warn(`Server rewrites a response header [${headerName}].`);
}
return t.next({ headers: authResponseHeaders });
});
}
}
7 changes: 7 additions & 0 deletions src/core/server/http/http_service.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { AuthToolkit } from './lifecycle/auth';
import { sessionStorageMock } from './cookie_session_storage.mocks';
import { IRouter } from './router';
import { OnPostAuthToolkit } from './lifecycle/on_post_auth';
import { OnPreResponseToolkit } from './lifecycle/on_pre_response';

type ServiceSetupMockType = jest.Mocked<InternalHttpServiceSetup> & {
basePath: jest.Mocked<InternalHttpServiceSetup['basePath']>;
Expand Down Expand Up @@ -62,6 +63,7 @@ const createSetupContractMock = () => {
registerAuth: jest.fn(),
registerOnPostAuth: jest.fn(),
registerRouteHandlerContext: jest.fn(),
registerOnPreResponse: jest.fn(),
createRouter: jest.fn(),
basePath: createBasePathMock(),
auth: {
Expand Down Expand Up @@ -103,12 +105,17 @@ const createAuthToolkitMock = (): jest.Mocked<AuthToolkit> => ({
authenticated: jest.fn(),
});

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

export const httpServiceMock = {
create: createHttpServiceMock,
createBasePath: createBasePathMock,
createSetupContract: createSetupContractMock,
createOnPreAuthToolkit: createOnPreAuthToolkitMock,
createOnPostAuthToolkit: createOnPostAuthToolkitMock,
createOnPreResponseToolkit: createOnPreResponseToolkitMock,
createAuthToolkit: createAuthToolkitMock,
createRouter: createRouterMock,
};
1 change: 1 addition & 0 deletions src/core/server/http/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export {
AuthResultType,
} from './lifecycle/auth';
export { OnPostAuthHandler, OnPostAuthToolkit } from './lifecycle/on_post_auth';
export { OnPreResponseHandler, OnPreResponseToolkit } from './lifecycle/on_pre_response';
export { SessionStorageFactory, SessionStorage } from './session_storage';
export {
SessionStorageCookieOptions,
Expand Down
154 changes: 154 additions & 0 deletions src/core/server/http/integration_tests/lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -913,3 +913,157 @@ describe('Auth', () => {
.expect(200, { customField: 'undefined' });
});
});

describe('OnPreResponse', () => {
it('supports registering response inceptors', async () => {
const { registerOnPreResponse, 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[] = [];
registerOnPreResponse((req, res, t) => {
callingOrder.push('first');
return t.next();
});

registerOnPreResponse((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 additional headers attachments', async () => {
const { registerOnPreResponse, server: innerServer, createRouter } = await server.setup(
setupDeps
);
const router = createRouter('/');

router.get({ path: '/', validate: false }, (context, req, res) => res.ok());
mshustov marked this conversation as resolved.
Show resolved Hide resolved

registerOnPreResponse((req, res, t) =>
t.next({
headers: {
'x-kibana-header': 'value',
},
})
);
await server.start();

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

expect(result.header['x-kibana-header']).toBe('value');
});

it('logs a warning if interceptor rewrites response header', async () => {
const { registerOnPreResponse, server: innerServer, createRouter } = await server.setup(
setupDeps
);
const router = createRouter('/');

router.get({ path: '/', validate: false }, (context, req, res) =>
res.ok({
headers: { 'x-kibana-header': 'value' },
})
);
registerOnPreResponse((req, res, t) =>
t.next({
headers: { 'x-kibana-header': 'value' },
})
);
await server.start();

await supertest(innerServer.listener)
.get('/')
.expect(200);

expect(loggingServiceMock.collect(logger).warn).toMatchInlineSnapshot(`
Array [
Array [
"Server rewrites a response header [x-kibana-header].",
],
]
`);
});

it("doesn't expose error details if interceptor throws", async () => {
mshustov marked this conversation as resolved.
Show resolved Hide resolved
const { registerOnPreResponse, server: innerServer, createRouter } = await server.setup(
setupDeps
);
const router = createRouter('/');

router.get({ path: '/', validate: false }, (context, req, res) => res.ok(undefined));
registerOnPreResponse((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 { registerOnPreResponse, server: innerServer, createRouter } = await server.setup(
setupDeps
);
const router = createRouter('/');

router.get({ path: '/', validate: false }, (context, req, res) => res.ok());
registerOnPreResponse((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 OnPreResponse. Expected OnPreResponseResult, but given: [object Object].],
],
]
`);
});

it('cannot change response statusCode', async () => {
const { registerOnPreResponse, server: innerServer, createRouter } = await server.setup(
setupDeps
);
const router = createRouter('/');

registerOnPreResponse((req, res, t) => {
res.statusCode = 500;
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);
});
});
Loading