[Console] Move out of legacy + migrate server side to New Platform (#55690) (#56360)

* Initial move of public and setup of server skeleton

* Fix public paths and types

* Use new usage stats dependency directly in tracker also mark as an optional dependency

* WiP on getting server side working

* Restore proxy route behaviour for base case, still need to test custom proxy and SSL

* Add new type and lib files

* Clean up legacy start up code and add comment about issue in kibana.yml config for console

* Move console_extensions to new platform and introduce ConsoleSetup API for extending autocomplete
Add TODO regarding exposing legacy ES config

* Re-introduce injected elasticsearch variable and use it in public

* Don't pass stateSetter prop through to checkbox

* Refactor of proxy route (split into separate files). Easier testing for now.
Refactor file name of request.ts -> proxy_request.ts. This is consistent with the exported function now
Started fixing server side tests for the proxy route
  - Migrated away from sinon
  - Completed the body.js -> body.test.ts. Still have to do the rest

* headers.js test -> headers.test.ts and moved some of the proxy route mocking logic to a common space

* Finish migration of rest of proxy route test away from hapi
Add test for custom route validation

* Bring console application in line with https://github.com/elastic/kibana/blob/master/src/core/CONVENTIONS.md#applications
Change log from info level to debug level for console_extensions plugin

* Update i18nrc file for console

* Add setHeaders when passing back error response

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>

# Conflicts:
#	.github/CODEOWNERS
#	src/legacy/core_plugins/console/server/__tests__/proxy_route/params.js
#	src/legacy/core_plugins/console/server/__tests__/proxy_route/query_string.js
#	src/plugins/console/server/lib/spec_definitions/spec/generated/get_script_context.json
#	x-pack/plugins/console_extensions/server/spec/overrides/security.delete_privileges.json
#	x-pack/plugins/console_extensions/server/spec/overrides/security.put_privileges.json
This commit is contained in:
Jean-Louis Leysens 2020-01-30 16:16:24 +01:00 committed by GitHub
parent 66066c3bab
commit 077a37bc19
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
612 changed files with 1360 additions and 1080 deletions

View file

@ -1,7 +1,7 @@
{
"paths": {
"common.ui": "src/legacy/ui",
"console": "src/legacy/core_plugins/console",
"console": "src/plugins/console",
"core": "src/core",
"dashboardEmbeddableContainer": "src/plugins/dashboard_embeddable_container",
"data": [

View file

@ -19,6 +19,7 @@
import { ResponseObject as HapiResponseObject, ResponseToolkit as HapiResponseToolkit } from 'hapi';
import typeDetect from 'type-detect';
import Boom from 'boom';
import * as stream from 'stream';
import {
HttpResponsePayload,
@ -112,8 +113,18 @@ export class HapiResponseAdapter {
return response;
}
private toError(kibanaResponse: KibanaResponse<ResponseError>) {
private toError(kibanaResponse: KibanaResponse<ResponseError | Buffer | stream.Readable>) {
const { payload } = kibanaResponse;
// Special case for when we are proxying requests and want to enable streaming back error responses opaquely.
if (Buffer.isBuffer(payload) || payload instanceof stream.Readable) {
const response = this.responseToolkit
.response(kibanaResponse.payload)
.code(kibanaResponse.status);
setHeaders(response, kibanaResponse.options.headers);
return response;
}
// we use for BWC with Boom payload for error responses - {error: string, message: string, statusCode: string}
const error = new Boom('', {
statusCode: kibanaResponse.status,

View file

@ -1,75 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Deprecations } from '../../../deprecation';
import expect from '@kbn/expect';
import index from '../index';
import { noop } from 'lodash';
import sinon from 'sinon';
describe('plugins/console', function() {
describe('#deprecate()', function() {
let transformDeprecations;
before(function() {
const Plugin = function(options) {
this.deprecations = options.deprecations;
};
const plugin = index({ Plugin });
const deprecations = plugin.deprecations(Deprecations);
transformDeprecations = (settings, log = noop) => {
deprecations.forEach(deprecation => deprecation(settings, log));
};
});
describe('proxyConfig', function() {
it('leaves the proxyConfig settings', function() {
const proxyConfigOne = {};
const proxyConfigTwo = {};
const settings = {
proxyConfig: [proxyConfigOne, proxyConfigTwo],
};
transformDeprecations(settings);
expect(settings.proxyConfig[0]).to.be(proxyConfigOne);
expect(settings.proxyConfig[1]).to.be(proxyConfigTwo);
});
it('logs a warning when proxyConfig is specified', function() {
const settings = {
proxyConfig: [],
};
const log = sinon.spy();
transformDeprecations(settings, log);
expect(log.calledOnce).to.be(true);
});
it(`doesn't log a warning when proxyConfig isn't specified`, function() {
const settings = {};
const log = sinon.spy();
transformDeprecations(settings, log);
expect(log.called).to.be(false);
});
});
});
});

View file

@ -1,187 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import Boom from 'boom';
import { first } from 'rxjs/operators';
import { resolve, join } from 'path';
import url from 'url';
import { has, isEmpty, head, pick } from 'lodash';
// @ts-ignore
import { addProcessorDefinition } from './server/api_server/es_6_0/ingest';
// @ts-ignore
import { resolveApi } from './server/api_server/server';
// @ts-ignore
import { addExtensionSpecFilePath } from './server/api_server/spec';
// @ts-ignore
import { setHeaders } from './server/set_headers';
// @ts-ignore
import { ProxyConfigCollection, getElasticsearchProxyConfig, createProxyRoute } from './server';
function filterHeaders(originalHeaders: any, headersToKeep: any) {
const normalizeHeader = function(header: any) {
if (!header) {
return '';
}
header = header.toString();
return header.trim().toLowerCase();
};
// Normalize list of headers we want to allow in upstream request
const headersToKeepNormalized = headersToKeep.map(normalizeHeader);
return pick(originalHeaders, headersToKeepNormalized);
}
// eslint-disable-next-line
export default function(kibana: any) {
const npSrc = resolve(__dirname, 'public/np_ready');
let defaultVars: any;
return new kibana.Plugin({
id: 'console',
require: ['elasticsearch'],
config(Joi: any) {
return Joi.object({
enabled: Joi.boolean().default(true),
proxyFilter: Joi.array()
.items(Joi.string())
.single()
.default(['.*']),
ssl: Joi.object({
verify: Joi.boolean(),
}).default(),
proxyConfig: Joi.array()
.items(
Joi.object().keys({
match: Joi.object().keys({
protocol: Joi.string().default('*'),
host: Joi.string().default('*'),
port: Joi.string().default('*'),
path: Joi.string().default('*'),
}),
timeout: Joi.number(),
ssl: Joi.object()
.keys({
verify: Joi.boolean(),
ca: Joi.array()
.single()
.items(Joi.string()),
cert: Joi.string(),
key: Joi.string(),
})
.default(),
})
)
.default(),
}).default();
},
deprecations() {
return [
(settings: any, log: any) => {
if (has(settings, 'proxyConfig')) {
log(
'Config key "proxyConfig" is deprecated. Configuration can be inferred from the "elasticsearch" settings'
);
}
},
];
},
uiCapabilities() {
return {
dev_tools: {
show: true,
save: true,
},
};
},
async init(server: any, options: any) {
server.expose('addExtensionSpecFilePath', addExtensionSpecFilePath);
server.expose('addProcessorDefinition', addProcessorDefinition);
if (options.ssl && options.ssl.verify) {
throw new Error('sense.ssl.verify is no longer supported.');
}
const config = server.config();
const legacyEsConfig = await server.newPlatform.__internals.elasticsearch.legacy.config$
.pipe(first())
.toPromise();
const proxyConfigCollection = new ProxyConfigCollection(options.proxyConfig);
const proxyPathFilters = options.proxyFilter.map((str: string) => new RegExp(str));
defaultVars = {
elasticsearchUrl: url.format(
Object.assign(url.parse(head(legacyEsConfig.hosts)), { auth: false })
),
};
server.route(
createProxyRoute({
hosts: legacyEsConfig.hosts,
pathFilters: proxyPathFilters,
getConfigForReq(req: any, uri: any) {
const filteredHeaders = filterHeaders(
req.headers,
legacyEsConfig.requestHeadersWhitelist
);
const headers = setHeaders(filteredHeaders, legacyEsConfig.customHeaders);
if (!isEmpty(config.get('console.proxyConfig'))) {
return {
...proxyConfigCollection.configForUri(uri),
headers,
};
}
return {
...getElasticsearchProxyConfig(legacyEsConfig),
headers,
};
},
})
);
server.route({
path: '/api/console/api_server',
method: ['GET', 'POST'],
handler(req: any, h: any) {
const { sense_version: version, apis } = req.query;
if (!apis) {
throw Boom.badRequest('"apis" is a required param.');
}
return resolveApi(version, apis.split(','), h);
},
});
},
uiExports: {
devTools: [resolve(__dirname, 'public/legacy')],
styleSheetPaths: resolve(npSrc, 'application/styles/index.scss'),
injectDefaultVars: () => defaultVars,
noParse: [join(npSrc, 'application/models/legacy_core_editor/mode/worker/worker.js')],
},
} as any);
}

View file

@ -1,8 +0,0 @@
{
"author": "Boaz Leskes <boaz@elastic.co>",
"contributors": [
"Spencer Alger <spencer.alger@elastic.co>"
],
"name": "console",
"version": "kibana"
}

View file

@ -1,53 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { npSetup, npStart } from 'ui/new_platform';
import { I18nContext } from 'ui/i18n';
import chrome from 'ui/chrome';
import { FeatureCatalogueCategory } from 'ui/registry/feature_catalogue';
import { plugin } from './np_ready';
import { DevToolsSetup } from '../../../../plugins/dev_tools/public';
import { HomePublicPluginSetup } from '../../../../plugins/home/public';
import { UsageCollectionSetup } from '../../../../plugins/usage_collection/public';
export interface XPluginSet {
usageCollection: UsageCollectionSetup;
dev_tools: DevToolsSetup;
home: HomePublicPluginSetup;
__LEGACY: {
I18nContext: any;
elasticsearchUrl: string;
category: FeatureCatalogueCategory;
};
}
const pluginInstance = plugin({} as any);
(async () => {
await pluginInstance.setup(npSetup.core, {
...npSetup.plugins,
__LEGACY: {
elasticsearchUrl: chrome.getInjected('elasticsearchUrl'),
I18nContext,
category: FeatureCatalogueCategory.ADMIN,
},
});
await pluginInstance.start(npStart.core);
})();

View file

@ -1,96 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { request } from 'http';
import sinon from 'sinon';
import expect from '@kbn/expect';
import { Server } from 'hapi';
import * as requestModule from '../../request';
import { createProxyRoute } from '../../';
import { createResponseStub } from './stubs';
describe('Console Proxy Route', () => {
const sandbox = sinon.createSandbox();
const teardowns = [];
let setup;
beforeEach(() => {
sandbox.stub(requestModule, 'sendRequest').callsFake(createResponseStub());
setup = () => {
const server = new Server();
server.route(
createProxyRoute({
hosts: ['http://localhost:9200'],
})
);
teardowns.push(() => server.stop());
return { server };
};
});
afterEach(async () => {
sandbox.restore();
await Promise.all(teardowns.splice(0).map(fn => fn()));
});
describe('headers', function() {
this.timeout(Infinity);
it('forwards the remote header info', async () => {
const { server } = setup();
await server.start();
const resp = await new Promise(resolve => {
request(
{
protocol: server.info.protocol + ':',
host: server.info.address,
port: server.info.port,
method: 'POST',
path: '/api/console/proxy?method=GET&path=/',
},
resolve
).end();
});
resp.destroy();
sinon.assert.calledOnce(requestModule.sendRequest);
const { headers } = requestModule.sendRequest.getCall(0).args[0];
expect(headers)
.to.have.property('x-forwarded-for')
.and.not.be('');
expect(headers)
.to.have.property('x-forwarded-port')
.and.not.be('');
expect(headers)
.to.have.property('x-forwarded-proto')
.and.not.be('');
expect(headers)
.to.have.property('x-forwarded-host')
.and.not.be('');
});
});
});

View file

@ -1,170 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Agent } from 'http';
import sinon from 'sinon';
import * as requestModule from '../../request';
import expect from '@kbn/expect';
import { Server } from 'hapi';
import { createProxyRoute } from '../../';
import { createResponseStub } from './stubs';
describe('Console Proxy Route', () => {
const sandbox = sinon.createSandbox();
const teardowns = [];
let setup;
beforeEach(() => {
sandbox.stub(requestModule, 'sendRequest').callsFake(createResponseStub());
setup = () => {
const server = new Server();
teardowns.push(() => server.stop());
return { server };
};
});
afterEach(async () => {
sandbox.restore();
await Promise.all(teardowns.splice(0).map(fn => fn()));
});
describe('params', () => {
describe('pathFilters', () => {
describe('no matches', () => {
it('rejects with 403', async () => {
const { server } = setup();
server.route(
createProxyRoute({
pathFilters: [/^\/foo\//, /^\/bar\//],
})
);
const { statusCode } = await server.inject({
method: 'POST',
url: '/api/console/proxy?method=GET&path=/baz/type/id',
});
expect(statusCode).to.be(403);
});
});
describe('one match', () => {
it('allows the request', async () => {
const { server } = setup();
server.route(
createProxyRoute({
hosts: ['http://localhost:9200'],
pathFilters: [/^\/foo\//, /^\/bar\//],
})
);
const { statusCode } = await server.inject({
method: 'POST',
url: '/api/console/proxy?method=GET&path=/foo/type/id',
});
expect(statusCode).to.be(200);
sinon.assert.calledOnce(requestModule.sendRequest);
});
});
describe('all match', () => {
it('allows the request', async () => {
const { server } = setup();
server.route(
createProxyRoute({
hosts: ['http://localhost:9200'],
pathFilters: [/^\/foo\//, /^\/bar\//],
})
);
const { statusCode } = await server.inject({
method: 'POST',
url: '/api/console/proxy?method=GET&path=/foo/type/id',
});
expect(statusCode).to.be(200);
sinon.assert.calledOnce(requestModule.sendRequest);
});
});
});
describe('getConfigForReq()', () => {
it('passes the request and targeted uri', async () => {
const { server } = setup();
const getConfigForReq = sinon.stub().returns({});
server.route(createProxyRoute({ hosts: ['http://localhost:9200'], getConfigForReq }));
await server.inject({
method: 'POST',
url: '/api/console/proxy?method=HEAD&path=/index/type/id',
});
sinon.assert.calledOnce(getConfigForReq);
const args = getConfigForReq.getCall(0).args;
expect(args[0]).to.have.property('path', '/api/console/proxy');
expect(args[0]).to.have.property('method', 'post');
expect(args[0])
.to.have.property('query')
.eql({ method: 'HEAD', path: '/index/type/id' });
expect(args[1]).to.be('http://localhost:9200/index/type/id?pretty=true');
});
it('sends the returned timeout, agent, and base headers to request', async () => {
const { server } = setup();
const timeout = Math.round(Math.random() * 10000);
const agent = new Agent();
const rejectUnauthorized = !!Math.round(Math.random());
const headers = {
foo: 'bar',
baz: 'bop',
};
server.route(
createProxyRoute({
hosts: ['http://localhost:9200'],
getConfigForReq: () => ({
timeout,
agent,
headers,
rejectUnauthorized,
}),
})
);
await server.inject({
method: 'POST',
url: '/api/console/proxy?method=HEAD&path=/index/type/id',
});
sinon.assert.calledOnce(requestModule.sendRequest);
const opts = requestModule.sendRequest.getCall(0).args[0];
expect(opts).to.have.property('timeout', timeout);
expect(opts).to.have.property('agent', agent);
expect(opts).to.have.property('rejectUnauthorized', rejectUnauthorized);
expect(opts.headers).to.have.property('foo', 'bar');
expect(opts.headers).to.have.property('baz', 'bop');
});
});
});
});

View file

@ -1,137 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import sinon from 'sinon';
import * as requestModule from '../../request';
import expect from '@kbn/expect';
import { Server } from 'hapi';
import { createProxyRoute } from '../../';
import { createResponseStub } from './stubs';
describe('Console Proxy Route', () => {
const sandbox = sinon.createSandbox();
const teardowns = [];
let request;
beforeEach(() => {
sandbox.stub(requestModule, 'sendRequest').callsFake(createResponseStub());
request = async (method, path) => {
const server = new Server();
server.route(
createProxyRoute({
hosts: ['http://localhost:9200'],
})
);
teardowns.push(() => server.stop());
const params = [];
if (path != null) params.push(`path=${path}`);
if (method != null) params.push(`method=${method}`);
return await server.inject({
method: 'POST',
url: `/api/console/proxy${params.length ? `?${params.join('&')}` : ''}`,
});
};
});
afterEach(async () => {
sandbox.restore();
await Promise.all(teardowns.splice(0).map(fn => fn()));
});
describe('query string', () => {
describe('path', () => {
describe('contains full url', () => {
it('treats the url as a path', async () => {
await request('GET', 'http://evil.com/test');
sinon.assert.calledOnce(requestModule.sendRequest);
const args = requestModule.sendRequest.getCall(0).args;
expect(args[0].uri.href).to.be('http://localhost:9200/http://evil.com/test?pretty=true');
});
});
describe('is missing', () => {
it('returns a 400 error', async () => {
const { statusCode } = await request('GET', undefined);
expect(statusCode).to.be(400);
sinon.assert.notCalled(requestModule.sendRequest);
});
});
describe('is empty', () => {
it('returns a 400 error', async () => {
const { statusCode } = await request('GET', '');
expect(statusCode).to.be(400);
sinon.assert.notCalled(requestModule.sendRequest);
});
});
describe('starts with a slash', () => {
it('combines well with the base url', async () => {
await request('GET', '/index/type/id');
sinon.assert.calledOnce(requestModule.sendRequest);
expect(requestModule.sendRequest.getCall(0).args[0].uri.href).to.be(
'http://localhost:9200/index/type/id?pretty=true'
);
});
});
describe(`doesn't start with a slash`, () => {
it('combines well with the base url', async () => {
await request('GET', 'index/type/id');
sinon.assert.calledOnce(requestModule.sendRequest);
expect(requestModule.sendRequest.getCall(0).args[0].uri.href).to.be(
'http://localhost:9200/index/type/id?pretty=true'
);
});
});
});
describe('method', () => {
describe('is missing', () => {
it('returns a 400 error', async () => {
const { statusCode } = await request(null, '/');
expect(statusCode).to.be(400);
sinon.assert.notCalled(requestModule.sendRequest);
});
});
describe('is empty', () => {
it('returns a 400 error', async () => {
const { statusCode } = await request('', '/');
expect(statusCode).to.be(400);
sinon.assert.notCalled(requestModule.sendRequest);
});
});
describe('is an invalid http method', () => {
it('returns a 400 error', async () => {
const { statusCode } = await request('foo', '/');
expect(statusCode).to.be(400);
sinon.assert.notCalled(requestModule.sendRequest);
});
});
describe('is mixed case', () => {
it('sends a request with the exact method', async () => {
const { statusCode } = await request('HeAd', '/');
expect(statusCode).to.be(200);
sinon.assert.calledOnce(requestModule.sendRequest);
expect(requestModule.sendRequest.getCall(0).args[0].method).to.be('HeAd');
});
});
});
});
});

View file

@ -1,171 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import Joi from 'joi';
import * as url from 'url';
import { IncomingMessage } from 'http';
import Boom from 'boom';
import { trimLeft, trimRight } from 'lodash';
import { sendRequest } from './request';
function toURL(base: string, path: string) {
const urlResult = new url.URL(`${trimRight(base, '/')}/${trimLeft(path, '/')}`);
// Appending pretty here to have Elasticsearch do the JSON formatting, as doing
// in JS can lead to data loss (7.0 will get munged into 7, thus losing indication of
// measurement precision)
if (!urlResult.searchParams.get('pretty')) {
urlResult.searchParams.append('pretty', 'true');
}
return urlResult;
}
function getProxyHeaders(req: any) {
const headers = Object.create(null);
// Scope this proto-unsafe functionality to where it is being used.
function extendCommaList(obj: Record<string, any>, property: string, value: any) {
obj[property] = (obj[property] ? obj[property] + ',' : '') + value;
}
if (req.info.remotePort && req.info.remoteAddress) {
// see https://git.io/vytQ7
extendCommaList(headers, 'x-forwarded-for', req.info.remoteAddress);
extendCommaList(headers, 'x-forwarded-port', req.info.remotePort);
extendCommaList(headers, 'x-forwarded-proto', req.server.info.protocol);
extendCommaList(headers, 'x-forwarded-host', req.info.host);
}
const contentType = req.headers['content-type'];
if (contentType) {
headers['content-type'] = contentType;
}
return headers;
}
export const createProxyRoute = ({
hosts,
pathFilters = [/.*/],
getConfigForReq = () => ({}),
}: {
hosts: string[];
pathFilters: RegExp[];
getConfigForReq: (...args: any[]) => any;
}) => ({
path: '/api/console/proxy',
method: 'POST',
config: {
tags: ['access:console'],
payload: {
output: 'stream',
parse: false,
},
validate: {
payload: true,
query: Joi.object()
.keys({
method: Joi.string()
.valid('HEAD', 'GET', 'POST', 'PUT', 'DELETE')
.insensitive()
.required(),
path: Joi.string().required(),
})
.unknown(true),
},
pre: [
function filterPath(req: any) {
const { path } = req.query;
if (pathFilters.some(re => re.test(path))) {
return null;
}
const err = Boom.forbidden();
err.output.payload = `Error connecting to '${path}':\n\nUnable to send requests to that path.` as any;
err.output.headers['content-type'] = 'text/plain';
throw err;
},
],
handler: async (req: any, h: any) => {
const { payload, query } = req;
const { path, method } = query;
let esIncomingMessage: IncomingMessage;
for (let idx = 0; idx < hosts.length; ++idx) {
const host = hosts[idx];
try {
const uri = toURL(host, path);
// Because this can technically be provided by a settings-defined proxy config, we need to
// preserve these property names to maintain BWC.
const { timeout, agent, headers, rejectUnauthorized } = getConfigForReq(
req,
uri.toString()
);
const requestHeaders = {
...headers,
...getProxyHeaders(req),
};
esIncomingMessage = await sendRequest({
method,
headers: requestHeaders,
uri,
timeout,
payload,
rejectUnauthorized,
agent,
});
break;
} catch (e) {
if (e.code !== 'ECONNREFUSED') {
throw Boom.boomify(e);
}
if (idx === hosts.length - 1) {
throw Boom.badGateway('Could not reach any configured nodes.');
}
// Otherwise, try the next host...
}
}
const {
statusCode,
statusMessage,
headers: { warning },
} = esIncomingMessage!;
if (method.toUpperCase() !== 'HEAD') {
return h
.response(esIncomingMessage!)
.code(statusCode)
.header('warning', warning!);
} else {
return h
.response(`${statusCode} - ${statusMessage}`)
.code(statusCode)
.type('text/plain')
.header('warning', warning!);
}
},
},
});

View file

@ -0,0 +1,51 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { first } from 'rxjs/operators';
import { head } from 'lodash';
import { resolve } from 'path';
import url from 'url';
// TODO: Remove this hack once we can get the ES config we need for Console proxy a better way.
let _legacyEsConfig: any;
export const readLegacyEsConfig = () => {
return _legacyEsConfig;
};
// eslint-disable-next-line import/no-default-export
export default function(kibana: any) {
return new kibana.Plugin({
id: 'console_legacy',
async init(server: any) {
_legacyEsConfig = await server.newPlatform.__internals.elasticsearch.legacy.config$
.pipe(first())
.toPromise();
},
uiExports: {
styleSheetPaths: resolve(__dirname, 'public/styles/index.scss'),
injectDefaultVars: () => ({
elasticsearchUrl: url.format(
Object.assign(url.parse(head(_legacyEsConfig.hosts)), { auth: false })
),
}),
},
} as any);
}

View file

@ -0,0 +1,4 @@
{
"name": "console_legacy",
"version": "kibana"
}

View file

@ -0,0 +1,21 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export * from './models';
export * from './plugin_config';

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { TextObject } from './text_object';
import { TextObject } from '../text_object';
export interface IdObject {
id: string;

View file

@ -0,0 +1,22 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export interface PluginServerConfig {
elasticsearchUrl: string;
}

View file

@ -3,6 +3,6 @@
"version": "kibana",
"server": true,
"ui": true,
"requiredPlugins": ["home"],
"requiredPlugins": ["dev_tools", "home"],
"optionalPlugins": ["usageCollection"]
}

View file

@ -248,7 +248,10 @@ export function DevToolsSettingsModal(props: Props) {
}
>
<EuiCheckboxGroup
options={autoCompleteCheckboxes}
options={autoCompleteCheckboxes.map(opts => {
const { stateSetter, ...rest } = opts;
return rest;
})}
idToSelectedMap={checkboxIdToSelectedMap}
onChange={(e: any) => {
onAutocompleteChange(e as AutocompleteOptions);

View file

@ -21,7 +21,7 @@ import React, { useCallback } from 'react';
import { debounce } from 'lodash';
import { EditorContentSpinner } from '../../components';
import { Panel, PanelsContainer } from '../../../../../../../../plugins/kibana_react/public';
import { Panel, PanelsContainer } from '../../../../../kibana_react/public';
import { Editor as EditorUI, EditorOutput } from './legacy/console_editor';
import { StorageKeys } from '../../../services';
import { useEditorReadContext, useServicesContext } from '../../contexts';

View file

@ -25,7 +25,7 @@ import { I18nProvider } from '@kbn/i18n/react';
import { act } from 'react-dom/test-utils';
import * as sinon from 'sinon';
import { notificationServiceMock } from '../../../../../../../../../../../src/core/public/mocks';
import { notificationServiceMock } from '../../../../../../../../core/public/mocks';
import { nextTick } from 'test_utils/enzyme_helpers';
import {

View file

@ -16,7 +16,8 @@
* specific language governing permissions and limitations
* under the License.
*/
import { ResizeChecker } from '../../../../../../../../../plugins/kibana_utils/public';
import { ResizeChecker } from '../../../../../../kibana_utils/public';
export function subscribeResizeChecker(el: HTMLElement, ...editors: any[]) {
const checker = new ResizeChecker(el);

View file

@ -20,7 +20,7 @@
import React, { createContext, useContext } from 'react';
import { NotificationsSetup } from 'kibana/public';
import { History, Storage, Settings } from '../../services';
import { ObjectStorageClient } from '../../../../common/types';
import { ObjectStorageClient } from '../../../common/types';
import { MetricsTracker } from '../../types';
export interface ContextValue {

View file

@ -18,7 +18,7 @@
*/
import { History } from '../../../services';
import { ObjectStorageClient } from '../../../../../common/types';
import { ObjectStorageClient } from '../../../../common/types';
export interface Dependencies {
history: History;

View file

@ -18,27 +18,38 @@
*/
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { NotificationsSetup } from 'src/core/public';
import { ServicesContextProvider, EditorContextProvider, RequestContextProvider } from './contexts';
import { Main } from './containers';
import { createStorage, createHistory, createSettings, Settings } from '../services';
import * as localStorageObjectClient from '../lib/local_storage_object_client';
import { createUsageTracker } from '../services/tracker';
import { UsageCollectionSetup } from '../../../usage_collection/public';
let settingsRef: Settings;
export function legacyBackDoorToSettings() {
return settingsRef;
}
export function boot(deps: {
export interface BootDependencies {
docLinkVersion: string;
I18nContext: any;
notifications: NotificationsSetup;
elasticsearchUrl: string;
}) {
const { I18nContext, notifications, docLinkVersion, elasticsearchUrl } = deps;
usageCollection?: UsageCollectionSetup;
element: HTMLElement;
}
const trackUiMetric = createUsageTracker();
export function renderApp({
I18nContext,
notifications,
docLinkVersion,
elasticsearchUrl,
usageCollection,
element,
}: BootDependencies) {
const trackUiMetric = createUsageTracker(usageCollection);
trackUiMetric.load('opened_app');
const storage = createStorage({
@ -50,7 +61,7 @@ export function boot(deps: {
const objectStorageClient = localStorageObjectClient.create(storage);
settingsRef = settings;
return (
render(
<I18nContext>
<ServicesContextProvider
value={{
@ -72,6 +83,9 @@ export function boot(deps: {
</EditorContextProvider>
</RequestContextProvider>
</ServicesContextProvider>
</I18nContext>
</I18nContext>,
element
);
return () => unmountComponentAtNode(element);
}

View file

Before

Width:  |  Height:  |  Size: 217 B

After

Width:  |  Height:  |  Size: 217 B

Some files were not shown because too many files have changed in this diff Show more