mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
# Backport This will backport the following commits from `main` to `8.x`: - [[Http] Make HTTP resource routes respond without the versioned header (#195940)](https://github.com/elastic/kibana/pull/195940) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Jean-Louis Leysens","email":"jeanlouis.leysens@elastic.co"},"sourceCommit":{"committedDate":"2024-10-15T14:09:42Z","message":"[Http] Make HTTP resource routes respond without the versioned header (#195940)\n\n## Summary\r\n\r\nFollow up on https://github.com/elastic/kibana/pull/195464\r\n\r\nAdds public route registrar option `httpResource` to distinguish API\r\nroutes from routes intended to be used for loading resources, [for\r\nex](bd22f1370f/x-pack/plugins/security/server/routes/authentication/oidc.ts (L36)
).\r\n\r\nThis enables us to avoid returning the version header\r\n`elastic-api-version` for HTTP resource routes.\r\n\r\nIt's still possible for API authors to use the versioned router for\r\nthings that should be HTTP resources, but it's assumed that all routes\r\nregistered through HTTP resources services are:\r\n\r\n1. Public\r\n2. Not versioned (focus of this PR)\r\n3. Not documented (done in\r\nhttps://github.com/elastic/kibana/pull/192675)\r\n\r\n### Checklist\r\n\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"72f3d2d3491f6da4b0c9147e766635e9dbb9cbe8","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Feature:http","Team:Core","release_note:skip","v9.0.0","v8.16.0","backport:version"],"title":"[Http] Make HTTP resource routes respond without the versioned header","number":195940,"url":"https://github.com/elastic/kibana/pull/195940","mergeCommit":{"message":"[Http] Make HTTP resource routes respond without the versioned header (#195940)\n\n## Summary\r\n\r\nFollow up on https://github.com/elastic/kibana/pull/195464\r\n\r\nAdds public route registrar option `httpResource` to distinguish API\r\nroutes from routes intended to be used for loading resources, [for\r\nex](bd22f1370f/x-pack/plugins/security/server/routes/authentication/oidc.ts (L36)
).\r\n\r\nThis enables us to avoid returning the version header\r\n`elastic-api-version` for HTTP resource routes.\r\n\r\nIt's still possible for API authors to use the versioned router for\r\nthings that should be HTTP resources, but it's assumed that all routes\r\nregistered through HTTP resources services are:\r\n\r\n1. Public\r\n2. Not versioned (focus of this PR)\r\n3. Not documented (done in\r\nhttps://github.com/elastic/kibana/pull/192675)\r\n\r\n### Checklist\r\n\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"72f3d2d3491f6da4b0c9147e766635e9dbb9cbe8"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/195940","number":195940,"mergeCommit":{"message":"[Http] Make HTTP resource routes respond without the versioned header (#195940)\n\n## Summary\r\n\r\nFollow up on https://github.com/elastic/kibana/pull/195464\r\n\r\nAdds public route registrar option `httpResource` to distinguish API\r\nroutes from routes intended to be used for loading resources, [for\r\nex](bd22f1370f/x-pack/plugins/security/server/routes/authentication/oidc.ts (L36)
).\r\n\r\nThis enables us to avoid returning the version header\r\n`elastic-api-version` for HTTP resource routes.\r\n\r\nIt's still possible for API authors to use the versioned router for\r\nthings that should be HTTP resources, but it's assumed that all routes\r\nregistered through HTTP resources services are:\r\n\r\n1. Public\r\n2. Not versioned (focus of this PR)\r\n3. Not documented (done in\r\nhttps://github.com/elastic/kibana/pull/192675)\r\n\r\n### Checklist\r\n\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"72f3d2d3491f6da4b0c9147e766635e9dbb9cbe8"}},{"branch":"8.x","label":"v8.16.0","branchLabelMappingKey":"^v8.16.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> Co-authored-by: Jean-Louis Leysens <jeanlouis.leysens@elastic.co>
This commit is contained in:
parent
c156cb3816
commit
0158fec36f
12 changed files with 153 additions and 40 deletions
|
@ -45,6 +45,7 @@ describe('registerRouteForBundle', () => {
|
|||
options: {
|
||||
access: 'public',
|
||||
authRequired: false,
|
||||
httpResource: true,
|
||||
},
|
||||
validate: expect.any(Object),
|
||||
},
|
||||
|
|
|
@ -32,6 +32,7 @@ export function registerRouteForBundle(
|
|||
{
|
||||
path: `${routePath}{path*}`,
|
||||
options: {
|
||||
httpResource: true,
|
||||
authRequired: false,
|
||||
access: 'public',
|
||||
},
|
||||
|
|
|
@ -61,6 +61,25 @@ describe('HttpResources service', () => {
|
|||
expect(registeredRouteConfig.options?.access).toBe('public');
|
||||
});
|
||||
|
||||
it('registration does not allow changing "httpResource"', () => {
|
||||
register(
|
||||
{ ...routeConfig, options: { ...routeConfig.options, httpResource: undefined } },
|
||||
async (ctx, req, res) => res.ok()
|
||||
);
|
||||
register(
|
||||
{ ...routeConfig, options: { ...routeConfig.options, httpResource: true } },
|
||||
async (ctx, req, res) => res.ok()
|
||||
);
|
||||
register(
|
||||
{ ...routeConfig, options: { ...routeConfig.options, httpResource: false } },
|
||||
async (ctx, req, res) => res.ok()
|
||||
);
|
||||
const [[first], [second], [third]] = router.get.mock.calls;
|
||||
expect(first.options?.httpResource).toBe(true);
|
||||
expect(second.options?.httpResource).toBe(true);
|
||||
expect(third.options?.httpResource).toBe(true);
|
||||
});
|
||||
|
||||
it('registration can set access to "internal"', () => {
|
||||
register({ ...routeConfig, options: { access: 'internal' } }, async (ctx, req, res) =>
|
||||
res.ok()
|
||||
|
|
|
@ -91,6 +91,7 @@ export class HttpResourcesService implements CoreService<InternalHttpResourcesSe
|
|||
access: 'public',
|
||||
excludeFromOAS: true,
|
||||
...route.options,
|
||||
httpResource: true,
|
||||
},
|
||||
},
|
||||
(context, request, response) => {
|
||||
|
|
|
@ -134,40 +134,76 @@ describe('Router', () => {
|
|||
}
|
||||
);
|
||||
|
||||
it('adds versioned header v2023-10-31 to public, unversioned routes', async () => {
|
||||
const router = new Router('', logger, enhanceWithContext, routerOptions);
|
||||
router.post(
|
||||
{
|
||||
path: '/public',
|
||||
options: {
|
||||
access: 'public',
|
||||
describe('elastic-api-version header', () => {
|
||||
it('adds the header to public, unversioned routes', async () => {
|
||||
const router = new Router('', logger, enhanceWithContext, routerOptions);
|
||||
router.post(
|
||||
{
|
||||
path: '/public',
|
||||
options: {
|
||||
access: 'public',
|
||||
},
|
||||
validate: false,
|
||||
},
|
||||
validate: false,
|
||||
},
|
||||
(context, req, res) => res.ok({ headers: { AAAA: 'test' } }) // with some fake headers
|
||||
);
|
||||
router.post(
|
||||
{
|
||||
path: '/internal',
|
||||
options: {
|
||||
access: 'internal',
|
||||
(context, req, res) => res.ok({ headers: { AAAA: 'test' } }) // with some fake headers
|
||||
);
|
||||
router.post(
|
||||
{
|
||||
path: '/internal',
|
||||
options: {
|
||||
access: 'internal',
|
||||
},
|
||||
validate: false,
|
||||
},
|
||||
validate: false,
|
||||
},
|
||||
(context, req, res) => res.ok()
|
||||
);
|
||||
const [{ handler: publicHandler }, { handler: internalHandler }] = router.getRoutes();
|
||||
(context, req, res) => res.ok()
|
||||
);
|
||||
const [{ handler: publicHandler }, { handler: internalHandler }] = router.getRoutes();
|
||||
|
||||
await publicHandler(createRequestMock(), mockResponseToolkit);
|
||||
expect(mockResponse.header).toHaveBeenCalledTimes(2);
|
||||
const [first, second] = mockResponse.header.mock.calls
|
||||
.concat()
|
||||
.sort(([k1], [k2]) => k1.localeCompare(k2));
|
||||
expect(first).toEqual(['AAAA', 'test']);
|
||||
expect(second).toEqual(['elastic-api-version', '2023-10-31']);
|
||||
await publicHandler(createRequestMock(), mockResponseToolkit);
|
||||
expect(mockResponse.header).toHaveBeenCalledTimes(2);
|
||||
const [first, second] = mockResponse.header.mock.calls
|
||||
.concat()
|
||||
.sort(([k1], [k2]) => k1.localeCompare(k2));
|
||||
expect(first).toEqual(['AAAA', 'test']);
|
||||
expect(second).toEqual(['elastic-api-version', '2023-10-31']);
|
||||
|
||||
await internalHandler(createRequestMock(), mockResponseToolkit);
|
||||
expect(mockResponse.header).toHaveBeenCalledTimes(2); // no additional calls
|
||||
await internalHandler(createRequestMock(), mockResponseToolkit);
|
||||
expect(mockResponse.header).toHaveBeenCalledTimes(2); // no additional calls
|
||||
});
|
||||
|
||||
it('does not add the header to public http resource routes', async () => {
|
||||
const router = new Router('', logger, enhanceWithContext, routerOptions);
|
||||
router.post(
|
||||
{
|
||||
path: '/public',
|
||||
options: {
|
||||
access: 'public',
|
||||
},
|
||||
validate: false,
|
||||
},
|
||||
(context, req, res) => res.ok()
|
||||
);
|
||||
router.post(
|
||||
{
|
||||
path: '/public-resource',
|
||||
options: {
|
||||
access: 'public',
|
||||
httpResource: true,
|
||||
},
|
||||
validate: false,
|
||||
},
|
||||
(context, req, res) => res.ok()
|
||||
);
|
||||
const [{ handler: publicHandler }, { handler: resourceHandler }] = router.getRoutes();
|
||||
|
||||
await publicHandler(createRequestMock(), mockResponseToolkit);
|
||||
expect(mockResponse.header).toHaveBeenCalledTimes(1);
|
||||
const [headersTuple] = mockResponse.header.mock.calls;
|
||||
expect(headersTuple).toEqual(['elastic-api-version', '2023-10-31']);
|
||||
|
||||
await resourceHandler(createRequestMock(), mockResponseToolkit);
|
||||
expect(mockResponse.header).toHaveBeenCalledTimes(1); // no additional calls
|
||||
});
|
||||
});
|
||||
|
||||
it('constructs lazily provided validations once (idempotency)', async () => {
|
||||
|
|
|
@ -149,6 +149,7 @@ export interface RouterOptions {
|
|||
export interface InternalRegistrarOptions {
|
||||
isVersioned: boolean;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export type VersionedRouteConfig<P, Q, B, M extends RouteMethod> = Omit<
|
||||
RouteConfig<P, Q, B, M>,
|
||||
|
@ -201,11 +202,15 @@ export class Router<Context extends RequestHandlerContextBase = RequestHandlerCo
|
|||
<P, Q, B>(
|
||||
route: InternalRouteConfig<P, Q, B, Method>,
|
||||
handler: RequestHandler<P, Q, B, Context, Method>,
|
||||
{ isVersioned }: { isVersioned: boolean } = { isVersioned: false }
|
||||
{ isVersioned }: InternalRegistrarOptions = { isVersioned: false }
|
||||
) => {
|
||||
route = prepareRouteConfigValidation(route);
|
||||
const routeSchemas = routeSchemasFromRouteConfig(route, method);
|
||||
const isPublicUnversionedRoute = route.options?.access === 'public' && !isVersioned;
|
||||
const isPublicUnversionedApi =
|
||||
!isVersioned &&
|
||||
route.options?.access === 'public' &&
|
||||
// We do not consider HTTP resource routes as APIs
|
||||
route.options?.httpResource !== true;
|
||||
|
||||
this.routes.push({
|
||||
handler: async (req, responseToolkit) =>
|
||||
|
@ -213,7 +218,7 @@ export class Router<Context extends RequestHandlerContextBase = RequestHandlerCo
|
|||
routeSchemas,
|
||||
request: req,
|
||||
responseToolkit,
|
||||
isPublicUnversionedRoute,
|
||||
isPublicUnversionedApi,
|
||||
handler: this.enhanceWithContext(handler),
|
||||
}),
|
||||
method,
|
||||
|
@ -223,7 +228,6 @@ export class Router<Context extends RequestHandlerContextBase = RequestHandlerCo
|
|||
security: isVersioned
|
||||
? route.security
|
||||
: validRouteSecurity(route.security as DeepPartial<RouteSecurity>, route.options),
|
||||
/** Below is added for introspection */
|
||||
validationSchemas: route.validate,
|
||||
isVersioned,
|
||||
});
|
||||
|
@ -269,12 +273,12 @@ export class Router<Context extends RequestHandlerContextBase = RequestHandlerCo
|
|||
routeSchemas,
|
||||
request,
|
||||
responseToolkit,
|
||||
isPublicUnversionedRoute,
|
||||
isPublicUnversionedApi,
|
||||
handler,
|
||||
}: {
|
||||
request: Request;
|
||||
responseToolkit: ResponseToolkit;
|
||||
isPublicUnversionedRoute: boolean;
|
||||
isPublicUnversionedApi: boolean;
|
||||
handler: RequestHandlerEnhanced<
|
||||
P,
|
||||
Q,
|
||||
|
@ -301,7 +305,7 @@ export class Router<Context extends RequestHandlerContextBase = RequestHandlerCo
|
|||
} catch (error) {
|
||||
this.logError('400 Bad Request', 400, { request, error });
|
||||
const response = hapiResponseAdapter.toBadRequest(error.message);
|
||||
if (isPublicUnversionedRoute) {
|
||||
if (isPublicUnversionedApi) {
|
||||
response.output.headers = {
|
||||
...response.output.headers,
|
||||
...getVersionHeader(ALLOWED_PUBLIC_VERSION),
|
||||
|
@ -312,7 +316,7 @@ export class Router<Context extends RequestHandlerContextBase = RequestHandlerCo
|
|||
|
||||
try {
|
||||
const kibanaResponse = await handler(kibanaRequest, kibanaResponseFactory);
|
||||
if (isPublicUnversionedRoute) {
|
||||
if (isPublicUnversionedApi) {
|
||||
injectVersionHeader(ALLOWED_PUBLIC_VERSION, kibanaResponse);
|
||||
}
|
||||
if (kibanaRequest.protocol === 'http2' && kibanaResponse.options.headers) {
|
||||
|
|
|
@ -138,6 +138,9 @@ describe('Versioned route', () => {
|
|||
tags: ['access:test'],
|
||||
timeout: { payload: 60_000, idleSocket: 10_000 },
|
||||
xsrfRequired: false,
|
||||
excludeFromOAS: true,
|
||||
httpResource: true,
|
||||
summary: `test`,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -307,6 +307,20 @@ export interface RouteConfigOptions<Method extends RouteMethod> {
|
|||
* @remarks This will be surfaced in OAS documentation.
|
||||
*/
|
||||
security?: RouteSecurity;
|
||||
|
||||
/**
|
||||
* Whether this endpoint is being used to serve generated or static HTTP resources
|
||||
* like JS, CSS or HTML. _Do not set to `true` for HTTP APIs._
|
||||
*
|
||||
* @note Unless you need this setting for a special case, rather use the
|
||||
* {@link HttpResources} service exposed to plugins directly.
|
||||
*
|
||||
* @note This is not a security feature. It may affect aspects of the HTTP
|
||||
* response like headers.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
httpResource?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -24,7 +24,7 @@ describe('registerTranslationsRoute', () => {
|
|||
1,
|
||||
expect.objectContaining({
|
||||
path: '/translations/{locale}.json',
|
||||
options: { access: 'public', authRequired: false },
|
||||
options: { access: 'public', authRequired: false, httpResource: true },
|
||||
}),
|
||||
expect.any(Function)
|
||||
);
|
||||
|
@ -32,7 +32,7 @@ describe('registerTranslationsRoute', () => {
|
|||
2,
|
||||
expect.objectContaining({
|
||||
path: '/translations/XXXX/{locale}.json',
|
||||
options: { access: 'public', authRequired: false },
|
||||
options: { access: 'public', authRequired: false, httpResource: true },
|
||||
}),
|
||||
expect.any(Function)
|
||||
);
|
||||
|
|
|
@ -45,6 +45,7 @@ export const registerTranslationsRoute = ({
|
|||
},
|
||||
options: {
|
||||
access: 'public',
|
||||
httpResource: true,
|
||||
authRequired: false,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -188,6 +188,24 @@ describe('Routing versioned requests', () => {
|
|||
).resolves.toMatchObject({ 'elastic-api-version': '2023-10-31' });
|
||||
});
|
||||
|
||||
it('returns the version in response headers, even for HTTP resources', async () => {
|
||||
router.versioned
|
||||
.get({ path: '/my-path', access: 'public', options: { httpResource: true } })
|
||||
.addVersion({ validate: false, version: '2023-10-31' }, async (ctx, req, res) => {
|
||||
return res.ok({ body: { foo: 'bar' } });
|
||||
});
|
||||
|
||||
await server.start();
|
||||
|
||||
await expect(
|
||||
supertest
|
||||
.get('/my-path')
|
||||
.set('Elastic-Api-Version', '2023-10-31')
|
||||
.expect(200)
|
||||
.then(({ header }) => header)
|
||||
).resolves.toMatchObject({ 'elastic-api-version': '2023-10-31' });
|
||||
});
|
||||
|
||||
it('runs response validation when in dev', async () => {
|
||||
router.versioned
|
||||
.get({ path: '/my-path', access: 'internal' })
|
||||
|
|
|
@ -198,5 +198,20 @@ function applyTestsWithDisableUnsafeEvalSetTo(disableUnsafeEval: boolean) {
|
|||
expect(response.text).toBe('window.alert(42);');
|
||||
});
|
||||
});
|
||||
|
||||
it('responses do not contain the elastic-api-version header', async () => {
|
||||
const { http, httpResources } = await root.setup();
|
||||
|
||||
const router = http.createRouter('');
|
||||
const resources = httpResources.createRegistrar(router);
|
||||
const htmlBody = `<p>HtMlr00lz</p>`;
|
||||
resources.register({ path: '/render-html', validate: false }, (context, req, res) =>
|
||||
res.renderHtml({ body: htmlBody })
|
||||
);
|
||||
|
||||
await root.start();
|
||||
const { header } = await request.get(root, '/render-html').expect(200);
|
||||
expect(header).not.toMatchObject({ 'elastic-api-version': expect.any(String) });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue