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

Route tags #37344

Merged
merged 16 commits into from
Jun 6, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
11 changes: 7 additions & 4 deletions src/core/server/http/http_server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,7 @@ test('#setBasePathFor() cannot be set twice for one request', async () => {
raw: {
req: incomingMessage,
},
route: { settings: {} },
} as any,
undefined
);
Expand Down Expand Up @@ -739,7 +740,9 @@ test('Should support disabling auth for a route', async () => {
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);

const router = new Router('');
router.get({ path: '/', validate: false, authRequired: false }, async (req, res) => res.ok({}));
router.get({ path: '/', validate: false, options: { authRequired: false } }, async (req, res) =>
mshustov marked this conversation as resolved.
Show resolved Hide resolved
res.ok({})
);
registerRouter(router);
const authenticate = jest.fn();
await registerAuth(authenticate, cookieOptions);
Expand Down Expand Up @@ -774,7 +777,7 @@ describe('#auth.isAuthenticated()', () => {
const { registerAuth, registerRouter, server: innerServer, auth } = await server.setup(config);

const router = new Router('');
router.get({ path: '/', validate: false, authRequired: false }, async (req, res) =>
router.get({ path: '/', validate: false, options: { authRequired: false } }, async (req, res) =>
mshustov marked this conversation as resolved.
Show resolved Hide resolved
res.ok({ isAuthenticated: auth.isAuthenticated(req) })
);
registerRouter(router);
Expand All @@ -791,7 +794,7 @@ describe('#auth.isAuthenticated()', () => {
const { registerRouter, server: innerServer, auth } = await server.setup(config);

const router = new Router('');
router.get({ path: '/', validate: false, authRequired: false }, async (req, res) =>
router.get({ path: '/', validate: false, options: { authRequired: false } }, async (req, res) =>
mshustov marked this conversation as resolved.
Show resolved Hide resolved
res.ok({ isAuthenticated: auth.isAuthenticated(req) })
);
registerRouter(router);
Expand Down Expand Up @@ -840,7 +843,7 @@ describe('#auth.get()', () => {
const { registerRouter, registerAuth, server: innerServer, auth } = await server.setup(config);
await registerAuth(authenticate, cookieOptions);
const router = new Router('');
router.get({ path: '/', validate: false, authRequired: false }, async (req, res) =>
router.get({ path: '/', validate: false, options: { authRequired: false } }, async (req, res) =>
mshustov marked this conversation as resolved.
Show resolved Hide resolved
res.ok(auth.get(req))
);

Expand Down
5 changes: 3 additions & 2 deletions src/core/server/http/http_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,13 +157,14 @@ export class HttpServer {

for (const router of this.registeredRouters) {
for (const route of router.getRoutes()) {
const isAuthRequired = Boolean(this.authRegistered && route.authRequired);
const { authRequired = true, tags } = route.options;
this.server.route({
handler: route.handler,
method: route.method,
path: this.getRouteFullPath(router.path, route.path),
options: {
auth: isAuthRequired ? undefined : false,
auth: this.authRegistered && authRequired ? undefined : false,
tags,
},
});
}
Expand Down
2 changes: 1 addition & 1 deletion src/core/server/http/lifecycle/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { adoptToHapiAuthFormat } from './auth';
const SessionStorageMock = {
asScoped: () => null as any,
};
const requestMock = {} as any;
const requestMock = { route: { settings: {} } } as any;
mshustov marked this conversation as resolved.
Show resolved Hide resolved
const createResponseToolkit = (customization = {}): any => ({ ...customization });

describe('adoptToHapiAuthFormat', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/core/server/http/lifecycle/on_post_auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import Boom from 'boom';
import { adoptToHapiOnPostAuthFormat } from './on_post_auth';

const requestMock = {} as any;
const requestMock = { route: { settings: {} } } as any;
const createResponseToolkit = (customization = {}): any => ({ ...customization });

describe('adoptToHapiOnPostAuthFormat', () => {
Expand Down
23 changes: 14 additions & 9 deletions src/core/server/http/lifecycle/on_pre_auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,20 @@
import Boom from 'boom';
import { adoptToHapiOnPreAuthFormat } from './on_pre_auth';

const requestMock = {} as any;
const createRequestMock = (customization: object = {}) =>
({
route: { settings: {} },
raw: { req: {} },
...customization,
} as any);
const createResponseToolkit = (customization = {}): any => ({ ...customization });

describe('adoptToHapiOnPreAuthFormat', () => {
it('Should allow passing request to the next handler', async () => {
const continueSymbol = {};
const onPreAuth = adoptToHapiOnPreAuthFormat((req, t) => t.next());
const result = await onPreAuth(
requestMock,
createRequestMock(),
createResponseToolkit({
['continue']: continueSymbol,
})
Expand All @@ -43,7 +48,7 @@ describe('adoptToHapiOnPreAuthFormat', () => {
const takeoverSymbol = {};
const redirectMock = jest.fn(() => ({ takeover: () => takeoverSymbol }));
const result = await onPreAuth(
requestMock,
createRequestMock(),
createResponseToolkit({
redirect: redirectMock,
})
Expand All @@ -60,24 +65,24 @@ describe('adoptToHapiOnPreAuthFormat', () => {
);
const continueSymbol = {};
const setUrl = jest.fn();
const reqMock = { setUrl, raw: { req: {} } } as any;
const requestMock = createRequestMock({ setUrl });
const result = await onPreAuth(
reqMock as any,
requestMock,
createResponseToolkit({
['continue']: continueSymbol,
})
);

expect(setUrl).toBeCalledWith(redirectUrl);
expect(reqMock.raw.req.url).toBe(redirectUrl);
expect(requestMock.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(requestMock, createResponseToolkit())) as Boom;
const result = (await onPreAuth(createRequestMock(), createResponseToolkit())) as Boom;

expect(result).toBeInstanceOf(Boom);
expect(result.message).toBe('unexpected result');
Expand All @@ -88,7 +93,7 @@ describe('adoptToHapiOnPreAuthFormat', () => {
const onPreAuth = adoptToHapiOnPreAuthFormat((req, t) => {
throw new Error('unknown error');
});
const result = (await onPreAuth(requestMock, createResponseToolkit())) as Boom;
const result = (await onPreAuth(createRequestMock(), createResponseToolkit())) as Boom;

expect(result).toBeInstanceOf(Boom);
expect(result.message).toBe('unknown error');
Expand All @@ -97,7 +102,7 @@ describe('adoptToHapiOnPreAuthFormat', () => {

it('Should return Boom.internal error if interceptor returns unexpected result', async () => {
const onPreAuth = adoptToHapiOnPreAuthFormat((req, toolkit) => undefined as any);
const result = (await onPreAuth(requestMock, createResponseToolkit())) as Boom;
const result = (await onPreAuth(createRequestMock(), createResponseToolkit())) as Boom;

expect(result).toBeInstanceOf(Boom);
expect(result.message).toMatchInlineSnapshot(
Expand Down
32 changes: 27 additions & 5 deletions src/core/server/http/router/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,26 @@ import { ObjectType, TypeOf } from '@kbn/config-schema';
import { Request } from 'hapi';

import { filterHeaders, Headers } from './headers';
import { RouteSchemas } from './route';
import { RouteSchemas, RouteConfigOptions } from './route';

interface KibanaRequestRoute {
path: string;
method: 'get' | 'post' | 'put' | 'patch' | 'delete' | 'options';
mshustov marked this conversation as resolved.
Show resolved Hide resolved
options: RouteConfigOptions;
mshustov marked this conversation as resolved.
Show resolved Hide resolved
}

/** @public */
export class KibanaRequest<Params = unknown, Query = unknown, Body = unknown> {
public static getRoute(request: Request): KibanaRequestRoute {
mshustov marked this conversation as resolved.
Show resolved Hide resolved
return {
mshustov marked this conversation as resolved.
Show resolved Hide resolved
path: request.path,
method: request.method,
options: {
authRequired: request.route.settings.auth === false ? false : true,
mshustov marked this conversation as resolved.
Show resolved Hide resolved
tags: request.route.settings.tags || [],
},
};
}
/**
* Factory for creating requests. Validates the request before creating an
* instance of a KibanaRequest.
Expand All @@ -35,7 +51,14 @@ export class KibanaRequest<Params = unknown, Query = unknown, Body = unknown> {
routeSchemas: RouteSchemas<P, Q, B> | undefined
) {
const requestParts = KibanaRequest.validate(req, routeSchemas);
return new KibanaRequest(req, requestParts.params, requestParts.query, requestParts.body);
const route = KibanaRequest.getRoute(req);
return new KibanaRequest(
req,
requestParts.params,
requestParts.query,
requestParts.body,
route
);
}

/**
Expand Down Expand Up @@ -70,17 +93,16 @@ export class KibanaRequest<Params = unknown, Query = unknown, Body = unknown> {
}

public readonly headers: Headers;
public readonly path: string;
public readonly url: Url;

constructor(
private readonly request: Request,
readonly params: Params,
readonly query: Query,
readonly body: Body
readonly body: Body,
readonly route: KibanaRequestRoute
) {
this.headers = request.headers;
this.path = request.path;
this.url = request.url;
}

Expand Down
27 changes: 18 additions & 9 deletions src/core/server/http/router/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,23 @@
*/

import { ObjectType, Schema } from '@kbn/config-schema';
export type RouteMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
export type RouteMethod = 'get' | 'post' | 'put' | 'delete';
Copy link
Contributor

Choose a reason for hiding this comment

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

A couple questions:

  • Why should these now be lower case? It looks like it would lead to less diff changes in this patch.
  • Why not add 'patch' | 'options' to this list since they are also used in: method: RouteMethod | 'patch' | 'options';

Copy link
Contributor Author

@mshustov mshustov Jun 4, 2019

Choose a reason for hiding this comment

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

  • current codebase relays on method names in lower case, because it's a default in hapi. changing to the upper case would lead to bugs during migration.
  • the router doesn't support PATCH and OPTIONS methods as of now and typing should reflect this fact


export interface RouteConfigOptions {
/**
* A flag shows that authentication for a route:
* enabled when true
* disabled when false
*
* Enabled by default.
*/
authRequired?: boolean;

/**
* Additional meta-data information to attach fot the route.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

typo: fot --> to

*/
tags?: string[];
Copy link
Member

Choose a reason for hiding this comment

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

question: do you think using ReadonlySet<string> would introduce too much unnecessary friction? I usually prefer Map and Set where uniqueness is implied unless it really reduces API ergonomics.

Copy link
Contributor Author

@mshustov mshustov Jun 3, 2019

Choose a reason for hiding this comment

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

I think using Set could be error-prone because strings are iterable in js

new Set('my:tag')   // wrong declaration, don't throw
new Set(['my:tag']) // correct declaration, but not intuitive

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, it's arguable and may not be the best choice in some cases, don't have a strong opinion on that, feel free to pick whatever you think is better.

P.S. We decided to move with Set for a similar thing in EcnrytpedSavedObjects to signify uniqueness of the setting. It's not super important, just slowly transforming my coding habits towards string[] ---> Set<string> and Record<string, string> ---> Map<string, string> where appropriate 🙂

}

export interface RouteConfig<P extends ObjectType, Q extends ObjectType, B extends ObjectType> {
/**
Expand All @@ -36,14 +52,7 @@ export interface RouteConfig<P extends ObjectType, Q extends ObjectType, B exten
*/
validate: RouteValidateFactory<P, Q, B> | false;

/**
* A flag shows that authentication for a route:
* enabled when true
* disabled when false
*
* Enabled by default.
*/
authRequired?: boolean;
options?: RouteConfigOptions;
}

export type RouteValidateFactory<
Expand Down
38 changes: 19 additions & 19 deletions src/core/server/http/router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ import { Request, ResponseObject, ResponseToolkit } from 'hapi';

import { KibanaRequest } from './request';
import { KibanaResponse, ResponseFactory, responseFactory } from './response';
import { RouteConfig, RouteMethod, RouteSchemas } from './route';
import { RouteConfig, RouteConfigOptions, RouteMethod, RouteSchemas } from './route';

export interface RouterRoute {
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
method: RouteMethod;
path: string;
authRequired: boolean;
options: RouteConfigOptions;
handler: (req: Request, responseToolkit: ResponseToolkit) => Promise<ResponseObject>;
}

Expand All @@ -44,14 +44,14 @@ export class Router {
route: RouteConfig<P, Q, B>,
handler: RequestHandler<P, Q, B>
) {
const { path, authRequired = true } = route;
const routeSchemas = this.routeSchemasFromRouteConfig(route, 'GET');
const { path, options = {} } = route;
const routeSchemas = this.routeSchemasFromRouteConfig(route, 'get');
this.routes.push({
handler: async (req, responseToolkit) =>
await this.handle(routeSchemas, req, responseToolkit, handler),
method: 'GET',
method: 'get',
path,
authRequired,
options,
});
}

Expand All @@ -62,14 +62,14 @@ export class Router {
route: RouteConfig<P, Q, B>,
handler: RequestHandler<P, Q, B>
) {
const { path, authRequired = true } = route;
const routeSchemas = this.routeSchemasFromRouteConfig(route, 'POST');
const { path, options = {} } = route;
const routeSchemas = this.routeSchemasFromRouteConfig(route, 'post');
this.routes.push({
handler: async (req, responseToolkit) =>
await this.handle(routeSchemas, req, responseToolkit, handler),
method: 'POST',
method: 'post',
path,
authRequired,
options,
});
}

Expand All @@ -80,14 +80,14 @@ export class Router {
route: RouteConfig<P, Q, B>,
handler: RequestHandler<P, Q, B>
) {
const { path, authRequired = true } = route;
const routeSchemas = this.routeSchemasFromRouteConfig(route, 'POST');
const { path, options = {} } = route;
const routeSchemas = this.routeSchemasFromRouteConfig(route, 'put');
this.routes.push({
handler: async (req, responseToolkit) =>
await this.handle(routeSchemas, req, responseToolkit, handler),
method: 'PUT',
method: 'put',
path,
authRequired,
options,
});
}

Expand All @@ -98,14 +98,14 @@ export class Router {
route: RouteConfig<P, Q, B>,
handler: RequestHandler<P, Q, B>
) {
const { path, authRequired = true } = route;
const routeSchemas = this.routeSchemasFromRouteConfig(route, 'DELETE');
const { path, options = {} } = route;
const routeSchemas = this.routeSchemasFromRouteConfig(route, 'delete');
this.routes.push({
handler: async (req, responseToolkit) =>
await this.handle(routeSchemas, req, responseToolkit, handler),
method: 'DELETE',
method: 'delete',
path,
authRequired,
options,
});
}

Expand Down