mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[http] Adds route config option access flag to indicate if an API is public or internal (#152404)
This commit is contained in:
parent
0c5fb37670
commit
41f7e633a9
10 changed files with 96 additions and 5 deletions
|
@ -201,6 +201,7 @@ export class CoreKibanaRequest<
|
|||
xsrfRequired:
|
||||
((request.route?.settings as RouteOptions)?.app as KibanaRouteOptions)?.xsrfRequired ??
|
||||
true, // some places in LP call KibanaRequest.from(request) manually. remove fallback to true before v8
|
||||
access: this.getAccess(request),
|
||||
tags: request.route?.settings?.tags || [],
|
||||
timeout: {
|
||||
payload: payloadTimeout,
|
||||
|
@ -222,6 +223,13 @@ export class CoreKibanaRequest<
|
|||
options,
|
||||
};
|
||||
}
|
||||
/** infer route access from path if not declared */
|
||||
private getAccess(request: RawRequest): 'internal' | 'public' {
|
||||
return (
|
||||
((request.route?.settings as RouteOptions)?.app as KibanaRouteOptions)?.access ??
|
||||
(request.path.startsWith('/internal') ? 'internal' : 'public')
|
||||
);
|
||||
}
|
||||
|
||||
private getAuthRequired(request: RawRequest): boolean | 'optional' {
|
||||
if (isFakeRawRequest(request)) {
|
||||
|
|
|
@ -71,7 +71,7 @@ function createKibanaRequestMock<P = any, Q = any, B = any>({
|
|||
routeTags,
|
||||
routeAuthRequired,
|
||||
validation = {},
|
||||
kibanaRouteOptions = { xsrfRequired: true },
|
||||
kibanaRouteOptions = { xsrfRequired: true, access: 'public' },
|
||||
kibanaRequestState = {
|
||||
requestId: '123',
|
||||
requestUuid: '123e4567-e89b-12d3-a456-426614174000',
|
||||
|
|
|
@ -817,6 +817,56 @@ test('allows attaching metadata to attach meta-data tag strings to a route', asy
|
|||
await supertest(innerServer.listener).get('/without-tags').expect(200, { tags: [] });
|
||||
});
|
||||
|
||||
test('allows declaring route access to flag a route as public or internal', async () => {
|
||||
const access = 'internal';
|
||||
const { registerRouter, server: innerServer } = await server.setup(config);
|
||||
|
||||
const router = new Router('', logger, enhanceWithContext);
|
||||
router.get({ path: '/with-access', validate: false, options: { access } }, (context, req, res) =>
|
||||
res.ok({ body: { access: req.route.options.access } })
|
||||
);
|
||||
router.get({ path: '/without-access', validate: false }, (context, req, res) =>
|
||||
res.ok({ body: { access: req.route.options.access } })
|
||||
);
|
||||
registerRouter(router);
|
||||
|
||||
await server.start();
|
||||
await supertest(innerServer.listener).get('/with-access').expect(200, { access });
|
||||
|
||||
await supertest(innerServer.listener).get('/without-access').expect(200, { access: 'public' });
|
||||
});
|
||||
|
||||
test('infers access flag from path if not defined', async () => {
|
||||
const { registerRouter, server: innerServer } = await server.setup(config);
|
||||
|
||||
const router = new Router('', logger, enhanceWithContext);
|
||||
router.get({ path: '/internal/foo', validate: false }, (context, req, res) =>
|
||||
res.ok({ body: { access: req.route.options.access } })
|
||||
);
|
||||
router.get({ path: '/random/foo', validate: false }, (context, req, res) =>
|
||||
res.ok({ body: { access: req.route.options.access } })
|
||||
);
|
||||
router.get({ path: '/random/internal/foo', validate: false }, (context, req, res) =>
|
||||
res.ok({ body: { access: req.route.options.access } })
|
||||
);
|
||||
|
||||
router.get({ path: '/api/foo/internal/my-foo', validate: false }, (context, req, res) =>
|
||||
res.ok({ body: { access: req.route.options.access } })
|
||||
);
|
||||
registerRouter(router);
|
||||
|
||||
await server.start();
|
||||
await supertest(innerServer.listener).get('/internal/foo').expect(200, { access: 'internal' });
|
||||
|
||||
await supertest(innerServer.listener).get('/random/foo').expect(200, { access: 'public' });
|
||||
await supertest(innerServer.listener)
|
||||
.get('/random/internal/foo')
|
||||
.expect(200, { access: 'public' });
|
||||
await supertest(innerServer.listener)
|
||||
.get('/api/foo/internal/my-foo')
|
||||
.expect(200, { access: 'public' });
|
||||
});
|
||||
|
||||
test('exposes route details of incoming request to a route handler', async () => {
|
||||
const { registerRouter, server: innerServer } = await server.setup(config);
|
||||
|
||||
|
@ -833,6 +883,7 @@ test('exposes route details of incoming request to a route handler', async () =>
|
|||
options: {
|
||||
authRequired: true,
|
||||
xsrfRequired: false,
|
||||
access: 'public',
|
||||
tags: [],
|
||||
timeout: {},
|
||||
},
|
||||
|
@ -1010,6 +1061,7 @@ test('exposes route details of incoming request to a route handler (POST + paylo
|
|||
options: {
|
||||
authRequired: true,
|
||||
xsrfRequired: true,
|
||||
access: 'public',
|
||||
tags: [],
|
||||
timeout: {
|
||||
payload: 10000,
|
||||
|
|
|
@ -524,6 +524,7 @@ export class HttpServer {
|
|||
|
||||
const kibanaRouteOptions: KibanaRouteOptions = {
|
||||
xsrfRequired: route.options.xsrfRequired ?? !isSafeMethod(route.method),
|
||||
access: route.options.access ?? (route.path.startsWith('/internal') ? 'internal' : 'public'),
|
||||
};
|
||||
|
||||
this.server!.route({
|
||||
|
|
|
@ -167,6 +167,7 @@ describe('xsrf post-auth handler', () => {
|
|||
path: '/some-path',
|
||||
kibanaRouteOptions: {
|
||||
xsrfRequired: false,
|
||||
access: 'public',
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -60,6 +60,7 @@ export const createVersionCheckPostAuthHandler = (kibanaVersion: string): OnPost
|
|||
};
|
||||
};
|
||||
|
||||
// TODO: implement header required for accessing internal routes. See https://github.com/elastic/kibana/issues/151940
|
||||
export const createCustomHeadersPreResponseHandler = (config: HttpConfig): OnPreResponseHandler => {
|
||||
const {
|
||||
name: serverName,
|
||||
|
|
|
@ -19,6 +19,7 @@ import type { Headers } from './headers';
|
|||
*/
|
||||
export interface KibanaRouteOptions extends RouteOptionsApp {
|
||||
xsrfRequired: boolean;
|
||||
access: 'internal' | 'public';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -120,6 +120,18 @@ export interface RouteConfigOptions<Method extends RouteMethod> {
|
|||
*/
|
||||
xsrfRequired?: Method extends 'get' ? never : boolean;
|
||||
|
||||
/**
|
||||
* Defines intended request origin of the route:
|
||||
* - public. The route is public, declared stable and intended for external access.
|
||||
* In the future, may require an incomming request to contain a specified header.
|
||||
* - internal. The route is internal and intended for internal access only.
|
||||
*
|
||||
* If not declared, infers access from route path:
|
||||
* - access =`internal` for '/internal' route path prefix
|
||||
* - access = `public` for everything else
|
||||
*/
|
||||
access?: 'public' | 'internal';
|
||||
|
||||
/**
|
||||
* Additional metadata tag strings to attach to the route.
|
||||
*/
|
||||
|
|
|
@ -22,7 +22,7 @@ const versionedRouter = vtk.createVersionedRouter({ router });
|
|||
const versionedRoute = versionedRouter
|
||||
.post({
|
||||
path: '/api/my-app/foo/{id?}',
|
||||
options: { timeout: { payload: 60000 } },
|
||||
options: { timeout: { payload: 60000 }, access: 'public' },
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
|
|
|
@ -13,6 +13,7 @@ import type {
|
|||
RequestHandler,
|
||||
RouteValidatorFullConfig,
|
||||
RequestHandlerContextBase,
|
||||
RouteConfigOptions,
|
||||
} from '@kbn/core-http-server';
|
||||
|
||||
type RqCtx = RequestHandlerContextBase;
|
||||
|
@ -45,7 +46,7 @@ export interface CreateVersionedRouterArgs<Ctx extends RqCtx = RqCtx> {
|
|||
* const versionedRoute = versionedRouter
|
||||
* .post({
|
||||
* path: '/api/my-app/foo/{id?}',
|
||||
* options: { timeout: { payload: 60000 } },
|
||||
* options: { timeout: { payload: 60000 }, access: 'public' },
|
||||
* })
|
||||
* .addVersion(
|
||||
* {
|
||||
|
@ -99,14 +100,28 @@ export interface VersionHTTPToolkit {
|
|||
): VersionedRouter<Ctx>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an input property from optional to required. Needed for making RouteConfigOptions['access'] required.
|
||||
*/
|
||||
type WithRequiredProperty<Type, Key extends keyof Type> = Type & {
|
||||
[Property in Key]-?: Type[Property];
|
||||
};
|
||||
|
||||
/**
|
||||
* Versioned route access flag, required
|
||||
* - '/api/foo' is 'public'
|
||||
* - '/internal/my-foo' is 'internal'
|
||||
* Required
|
||||
*/
|
||||
type VersionedRouteConfigOptions = WithRequiredProperty<RouteConfigOptions<RouteMethod>, 'access'>;
|
||||
/**
|
||||
* Configuration for a versioned route
|
||||
* @experimental
|
||||
*/
|
||||
export type VersionedRouteConfig<Method extends RouteMethod> = Omit<
|
||||
RouteConfig<unknown, unknown, unknown, Method>,
|
||||
'validate'
|
||||
>;
|
||||
'validate' | 'options'
|
||||
> & { options: VersionedRouteConfigOptions };
|
||||
|
||||
/**
|
||||
* Create an {@link VersionedRoute | versioned route}.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue