[7.x] [console] Deprecate "proxyFilter" and "proxyConfig" on 8.x (#113555) (#113781)

* [console] Deprecate "proxyFilter" and "proxyConfig" on 8.x (#113555)

* Change MAJOR_VERSION to 7.16.0
This commit is contained in:
Sébastien Loix 2021-10-04 18:43:22 +01:00 committed by GitHub
parent b47f88afb4
commit cef53a981a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 220 additions and 118 deletions

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { MAJOR_VERSION } from './plugin';

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export const MAJOR_VERSION = '7.16.0';

View file

@ -1,12 +1,14 @@
{
"id": "console",
"version": "kibana",
"version": "8.0.0",
"kibanaVersion": "kibana",
"server": true,
"ui": true,
"owner": {
"name": "Stack Management",
"githubTeam": "kibana-stack-management"
},
"configPath": ["console"],
"requiredPlugins": ["devTools", "share"],
"optionalPlugins": ["usageCollection", "home"],
"requiredBundles": ["esUiShared", "kibanaReact", "kibanaUtils", "home"]

View file

@ -6,37 +6,70 @@
* Side Public License, v 1.
*/
import { SemVer } from 'semver';
import { schema, TypeOf } from '@kbn/config-schema';
import { PluginConfigDescriptor } from 'kibana/server';
export type ConfigType = TypeOf<typeof config>;
import { MAJOR_VERSION } from '../common/constants';
export const config = schema.object(
{
enabled: schema.boolean({ defaultValue: true }),
proxyFilter: schema.arrayOf(schema.string(), { defaultValue: ['.*'] }),
ssl: schema.object({ verify: schema.boolean({ defaultValue: false }) }, {}),
proxyConfig: schema.arrayOf(
schema.object({
match: schema.object({
protocol: schema.string({ defaultValue: '*' }),
host: schema.string({ defaultValue: '*' }),
port: schema.string({ defaultValue: '*' }),
path: schema.string({ defaultValue: '*' }),
}),
const kibanaVersion = new SemVer(MAJOR_VERSION);
timeout: schema.number(),
ssl: schema.object(
{
verify: schema.boolean(),
ca: schema.arrayOf(schema.string()),
cert: schema.string(),
key: schema.string(),
},
{ defaultValue: undefined }
),
const baseSettings = {
enabled: schema.boolean({ defaultValue: true }),
ssl: schema.object({ verify: schema.boolean({ defaultValue: false }) }, {}),
};
// Settings only available in 7.x
const deprecatedSettings = {
proxyFilter: schema.arrayOf(schema.string(), { defaultValue: ['.*'] }),
proxyConfig: schema.arrayOf(
schema.object({
match: schema.object({
protocol: schema.string({ defaultValue: '*' }),
host: schema.string({ defaultValue: '*' }),
port: schema.string({ defaultValue: '*' }),
path: schema.string({ defaultValue: '*' }),
}),
{ defaultValue: [] }
),
timeout: schema.number(),
ssl: schema.object(
{
verify: schema.boolean(),
ca: schema.arrayOf(schema.string()),
cert: schema.string(),
key: schema.string(),
},
{ defaultValue: undefined }
),
}),
{ defaultValue: [] }
),
};
const configSchema = schema.object(
{
...baseSettings,
},
{ defaultValue: undefined }
);
const configSchema7x = schema.object(
{
...baseSettings,
...deprecatedSettings,
},
{ defaultValue: undefined }
);
export type ConfigType = TypeOf<typeof configSchema>;
export type ConfigType7x = TypeOf<typeof configSchema7x>;
export const config: PluginConfigDescriptor<ConfigType | ConfigType7x> = {
schema: kibanaVersion.major < 8 ? configSchema7x : configSchema,
deprecations: ({ deprecate, unused }) => [
deprecate('enabled', '8.0.0'),
deprecate('proxyFilter', '8.0.0'),
deprecate('proxyConfig', '8.0.0'),
unused('ssl'),
],
};

View file

@ -6,16 +6,11 @@
* Side Public License, v 1.
*/
import { PluginConfigDescriptor, PluginInitializerContext } from 'kibana/server';
import { PluginInitializerContext } from 'kibana/server';
import { ConfigType, config as configSchema } from './config';
import { ConsoleServerPlugin } from './plugin';
export { ConsoleSetup, ConsoleStart } from './types';
export { config } from './config';
export const plugin = (ctx: PluginInitializerContext) => new ConsoleServerPlugin(ctx);
export const config: PluginConfigDescriptor<ConfigType> = {
deprecations: ({ deprecate, unused, rename }) => [deprecate('enabled', '8.0.0'), unused('ssl')],
schema: configSchema,
};

View file

@ -7,10 +7,11 @@
*/
import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'kibana/server';
import { SemVer } from 'semver';
import { ProxyConfigCollection } from './lib';
import { SpecDefinitionsService, EsLegacyConfigService } from './services';
import { ConfigType } from './config';
import { ConfigType, ConfigType7x } from './config';
import { registerRoutes } from './routes';
@ -23,7 +24,7 @@ export class ConsoleServerPlugin implements Plugin<ConsoleSetup, ConsoleStart> {
esLegacyConfigService = new EsLegacyConfigService();
constructor(private readonly ctx: PluginInitializerContext<ConfigType>) {
constructor(private readonly ctx: PluginInitializerContext<ConfigType | ConfigType7x>) {
this.log = this.ctx.logger.get();
}
@ -34,10 +35,17 @@ export class ConsoleServerPlugin implements Plugin<ConsoleSetup, ConsoleStart> {
save: true,
},
}));
const kibanaVersion = new SemVer(this.ctx.env.packageInfo.version);
const config = this.ctx.config.get();
const globalConfig = this.ctx.config.legacy.get();
const proxyPathFilters = config.proxyFilter.map((str: string) => new RegExp(str));
let pathFilters: RegExp[] | undefined;
let proxyConfigCollection: ProxyConfigCollection | undefined;
if (kibanaVersion.major < 8) {
// "pathFilters" and "proxyConfig" are only used in 7.x
pathFilters = (config as ConfigType7x).proxyFilter.map((str: string) => new RegExp(str));
proxyConfigCollection = new ProxyConfigCollection((config as ConfigType7x).proxyConfig);
}
this.esLegacyConfigService.setup(elasticsearch.legacy.config$);
@ -51,7 +59,6 @@ export class ConsoleServerPlugin implements Plugin<ConsoleSetup, ConsoleStart> {
specDefinitionService: this.specDefinitionsService,
},
proxy: {
proxyConfigCollection: new ProxyConfigCollection(config.proxyConfig),
readLegacyESConfig: async (): Promise<ESConfigForProxy> => {
const legacyConfig = await this.esLegacyConfigService.readConfig();
return {
@ -59,8 +66,11 @@ export class ConsoleServerPlugin implements Plugin<ConsoleSetup, ConsoleStart> {
...legacyConfig,
};
},
pathFilters: proxyPathFilters,
// Deprecated settings (only used in 7.x):
proxyConfigCollection,
pathFilters,
},
kibanaVersion,
});
return {

View file

@ -9,6 +9,7 @@
import { Agent, IncomingMessage } from 'http';
import * as url from 'url';
import { pick, trimStart, trimEnd } from 'lodash';
import { SemVer } from 'semver';
import { KibanaRequest, RequestHandler } from 'kibana/server';
@ -58,17 +59,22 @@ function filterHeaders(originalHeaders: object, headersToKeep: string[]): object
function getRequestConfig(
headers: object,
esConfig: ESConfigForProxy,
proxyConfigCollection: ProxyConfigCollection,
uri: string
uri: string,
kibanaVersion: SemVer,
proxyConfigCollection?: ProxyConfigCollection
): { agent: Agent; timeout: number; headers: object; rejectUnauthorized?: boolean } {
const filteredHeaders = filterHeaders(headers, esConfig.requestHeadersWhitelist);
const newHeaders = setHeaders(filteredHeaders, esConfig.customHeaders);
if (proxyConfigCollection.hasConfig()) {
return {
...proxyConfigCollection.configForUri(uri),
headers: newHeaders,
};
if (kibanaVersion.major < 8) {
// In 7.x we still support the proxyConfig setting defined in kibana.yml
// From 8.x we don't support it anymore so we don't try to read it here.
if (proxyConfigCollection!.hasConfig()) {
return {
...proxyConfigCollection!.configForUri(uri),
headers: newHeaders,
};
}
}
return {
@ -106,18 +112,23 @@ export const createHandler =
({
log,
proxy: { readLegacyESConfig, pathFilters, proxyConfigCollection },
kibanaVersion,
}: RouteDependencies): RequestHandler<unknown, Query, Body> =>
async (ctx, request, response) => {
const { body, query } = request;
const { path, method } = query;
if (!pathFilters.some((re) => re.test(path))) {
return response.forbidden({
body: `Error connecting to '${path}':\n\nUnable to send requests to that path.`,
headers: {
'Content-Type': 'text/plain',
},
});
if (kibanaVersion.major < 8) {
// The "console.proxyFilter" setting in kibana.yaml has been deprecated in 8.x
// We only read it on the 7.x branch
if (!pathFilters!.some((re) => re.test(path))) {
return response.forbidden({
body: `Error connecting to '${path}':\n\nUnable to send requests to that path.`,
headers: {
'Content-Type': 'text/plain',
},
});
}
}
const legacyConfig = await readLegacyESConfig();
@ -134,8 +145,9 @@ export const createHandler =
const { timeout, agent, headers, rejectUnauthorized } = getRequestConfig(
request.headers,
legacyConfig,
proxyConfigCollection,
uri.toString()
uri.toString(),
kibanaVersion,
proxyConfigCollection
);
const requestHeaders = {

View file

@ -5,28 +5,41 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { SemVer } from 'semver';
jest.mock('../../../../lib/proxy_request', () => ({
proxyRequest: jest.fn(),
}));
import { duration } from 'moment';
import { MAJOR_VERSION } from '../../../../../common/constants';
import { ProxyConfigCollection } from '../../../../lib';
import { RouteDependencies, ProxyDependencies } from '../../../../routes';
import { EsLegacyConfigService, SpecDefinitionsService } from '../../../../services';
import { coreMock, httpServiceMock } from '../../../../../../../core/server/mocks';
const defaultProxyValue = Object.freeze({
readLegacyESConfig: async () => ({
requestTimeout: duration(30000),
customHeaders: {},
requestHeadersWhitelist: [],
hosts: ['http://localhost:9200'],
}),
pathFilters: [/.*/],
proxyConfigCollection: new ProxyConfigCollection([]),
const kibanaVersion = new SemVer(MAJOR_VERSION);
const readLegacyESConfig = async () => ({
requestTimeout: duration(30000),
customHeaders: {},
requestHeadersWhitelist: [],
hosts: ['http://localhost:9200'],
});
let defaultProxyValue = Object.freeze({
readLegacyESConfig,
});
if (kibanaVersion.major < 8) {
// In 7.x we still support the "pathFilter" and "proxyConfig" kibana.yml settings
defaultProxyValue = Object.freeze({
readLegacyESConfig,
pathFilters: [/.*/],
proxyConfigCollection: new ProxyConfigCollection([]),
});
}
interface MockDepsArgument extends Partial<Omit<RouteDependencies, 'proxy'>> {
proxy?: Partial<ProxyDependencies>;
}
@ -51,5 +64,6 @@ export const getProxyRouteHandlerDeps = ({
}
: defaultProxyValue,
log,
kibanaVersion,
};
};

View file

@ -5,14 +5,17 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { SemVer } from 'semver';
import { kibanaResponseFactory } from '../../../../../../../core/server';
import { getProxyRouteHandlerDeps } from './mocks';
import { createResponseStub } from './stubs';
import { getProxyRouteHandlerDeps } from './mocks'; // import need to come first
import { createResponseStub } from './stubs'; // import needs to come first
import { MAJOR_VERSION } from '../../../../../common/constants';
import * as requestModule from '../../../../lib/proxy_request';
import { createHandler } from './create_handler';
const kibanaVersion = new SemVer(MAJOR_VERSION);
describe('Console Proxy Route', () => {
let handler: ReturnType<typeof createHandler>;
@ -21,58 +24,71 @@ describe('Console Proxy Route', () => {
});
describe('params', () => {
describe('pathFilters', () => {
describe('no matches', () => {
it('rejects with 403', async () => {
handler = createHandler(
getProxyRouteHandlerDeps({ proxy: { pathFilters: [/^\/foo\//, /^\/bar\//] } })
);
if (kibanaVersion.major < 8) {
describe('pathFilters', () => {
describe('no matches', () => {
it('rejects with 403', async () => {
handler = createHandler(
getProxyRouteHandlerDeps({
proxy: { pathFilters: [/^\/foo\//, /^\/bar\//] },
})
);
const { status } = await handler(
{} as any,
{ query: { method: 'POST', path: '/baz/id' } } as any,
kibanaResponseFactory
);
const { status } = await handler(
{} as any,
{ query: { method: 'POST', path: '/baz/id' } } as any,
kibanaResponseFactory
);
expect(status).toBe(403);
expect(status).toBe(403);
});
});
describe('one match', () => {
it('allows the request', async () => {
handler = createHandler(
getProxyRouteHandlerDeps({
proxy: { pathFilters: [/^\/foo\//, /^\/bar\//] },
})
);
(requestModule.proxyRequest as jest.Mock).mockResolvedValue(createResponseStub('foo'));
const { status } = await handler(
{} as any,
{ headers: {}, query: { method: 'POST', path: '/foo/id' } } as any,
kibanaResponseFactory
);
expect(status).toBe(200);
expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).toBe(1);
});
});
describe('all match', () => {
it('allows the request', async () => {
handler = createHandler(
getProxyRouteHandlerDeps({ proxy: { pathFilters: [/^\/foo\//] } })
);
(requestModule.proxyRequest as jest.Mock).mockResolvedValue(createResponseStub('foo'));
const { status } = await handler(
{} as any,
{ headers: {}, query: { method: 'GET', path: '/foo/id' } } as any,
kibanaResponseFactory
);
expect(status).toBe(200);
expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).toBe(1);
});
});
});
describe('one match', () => {
it('allows the request', async () => {
handler = createHandler(
getProxyRouteHandlerDeps({ proxy: { pathFilters: [/^\/foo\//, /^\/bar\//] } })
);
(requestModule.proxyRequest as jest.Mock).mockResolvedValue(createResponseStub('foo'));
const { status } = await handler(
{} as any,
{ headers: {}, query: { method: 'POST', path: '/foo/id' } } as any,
kibanaResponseFactory
);
expect(status).toBe(200);
expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).toBe(1);
});
} else {
// jest requires to have at least one test in the file
test('dummy required test', () => {
expect(true).toBe(true);
});
describe('all match', () => {
it('allows the request', async () => {
handler = createHandler(
getProxyRouteHandlerDeps({ proxy: { pathFilters: [/^\/foo\//] } })
);
(requestModule.proxyRequest as jest.Mock).mockResolvedValue(createResponseStub('foo'));
const { status } = await handler(
{} as any,
{ headers: {}, query: { method: 'GET', path: '/foo/id' } } as any,
kibanaResponseFactory
);
expect(status).toBe(200);
expect((requestModule.proxyRequest as jest.Mock).mock.calls.length).toBe(1);
});
});
});
}
});
});

View file

@ -7,6 +7,7 @@
*/
import { IRouter, Logger } from 'kibana/server';
import { SemVer } from 'semver';
import { EsLegacyConfigService, SpecDefinitionsService } from '../services';
import { ESConfigForProxy } from '../types';
@ -18,8 +19,8 @@ import { registerSpecDefinitionsRoute } from './api/console/spec_definitions';
export interface ProxyDependencies {
readLegacyESConfig: () => Promise<ESConfigForProxy>;
pathFilters: RegExp[];
proxyConfigCollection: ProxyConfigCollection;
pathFilters?: RegExp[]; // Only present in 7.x
proxyConfigCollection?: ProxyConfigCollection; // Only present in 7.x
}
export interface RouteDependencies {
@ -30,6 +31,7 @@ export interface RouteDependencies {
esLegacyConfigService: EsLegacyConfigService;
specDefinitionService: SpecDefinitionsService;
};
kibanaVersion: SemVer;
}
export const registerRoutes = (dependencies: RouteDependencies) => {