From ca97dcd4cdf9ec01d649f89ada77ad0ac1ecb809 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Thu, 10 Dec 2020 21:15:58 +0300 Subject: [PATCH] Add ability to specify CORS accepted origins (#84316) (#85570) * add settings * update abab package to version with types * add test case for CORS * add tests for cors config * fix jest tests * add deprecation message * tweak deprecation * make test runable on Cloud * add docs * fix type error * add test to throw on invalid URL * address comments * Update src/core/server/http/http_config.test.ts Co-authored-by: Larry Gregory * Update docs/setup/settings.asciidoc Co-authored-by: Brandon Kobel * allow kbn-xsrf headers to be set on CORS request Co-authored-by: Larry Gregory Co-authored-by: Brandon Kobel # Conflicts: # src/core/server/config/deprecation/core_deprecations.ts # x-pack/scripts/functional_tests.js --- docs/setup/settings.asciidoc | 9 ++ package.json | 2 +- .../deprecation/core_deprecations.test.ts | 26 ++++++ .../config/deprecation/core_deprecations.ts | 12 +++ .../__snapshots__/http_config.test.ts.snap | 6 +- .../http/cookie_session_storage.test.ts | 3 + src/core/server/http/http_config.test.ts | 53 ++++++++++++ src/core/server/http/http_config.ts | 28 +++++- src/core/server/http/http_server.test.ts | 3 + src/core/server/http/http_tools.test.ts | 23 +++++ src/core/server/http/http_tools.ts | 25 ++++-- .../server/http/https_redirect_server.test.ts | 3 + .../lifecycle_handlers.test.ts | 3 + src/core/server/http/test_utils.ts | 3 + x-pack/scripts/functional_tests.js | 3 + x-pack/test/functional_cors/config.ts | 63 ++++++++++++++ .../functional_cors/ftr_provider_context.d.ts | 12 +++ .../plugins/kibana_cors_test/kibana.json | 8 ++ .../plugins/kibana_cors_test/package.json | 11 +++ .../plugins/kibana_cors_test/server/config.ts | 12 +++ .../plugins/kibana_cors_test/server/index.ts | 14 +++ .../plugins/kibana_cors_test/server/plugin.ts | 86 +++++++++++++++++++ x-pack/test/functional_cors/services.ts | 9 ++ x-pack/test/functional_cors/tests/cors.ts | 30 +++++++ x-pack/test/functional_cors/tests/index.ts | 14 +++ yarn.lock | 7 +- 26 files changed, 452 insertions(+), 16 deletions(-) create mode 100644 x-pack/test/functional_cors/config.ts create mode 100644 x-pack/test/functional_cors/ftr_provider_context.d.ts create mode 100644 x-pack/test/functional_cors/plugins/kibana_cors_test/kibana.json create mode 100644 x-pack/test/functional_cors/plugins/kibana_cors_test/package.json create mode 100644 x-pack/test/functional_cors/plugins/kibana_cors_test/server/config.ts create mode 100644 x-pack/test/functional_cors/plugins/kibana_cors_test/server/index.ts create mode 100644 x-pack/test/functional_cors/plugins/kibana_cors_test/server/plugin.ts create mode 100644 x-pack/test/functional_cors/services.ts create mode 100644 x-pack/test/functional_cors/tests/cors.ts create mode 100644 x-pack/test/functional_cors/tests/index.ts diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index ed6bc9b1f55b6..6cd848e963431 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -450,6 +450,15 @@ deprecation warning at startup. This setting cannot end in a slash (`/`). | [[server-compression]] `server.compression.enabled:` | Set to `false` to disable HTTP compression for all responses. *Default: `true`* +| `server.cors.enabled:` + | experimental[] Set to `true` to allow cross-origin API calls. *Default:* `false` + +| `server.cors.credentials:` + | experimental[] Set to `true` to allow browser code to access response body whenever request performed with user credentials. *Default:* `false` + +| `server.cors.origin:` + | experimental[] List of origins permitted to access resources. You must specify explicit hostnames and not use `*` for `server.cors.origin` when `server.cors.credentials: true`. *Default:* "*" + | `server.compression.referrerWhitelist:` | Specifies an array of trusted hostnames, such as the {kib} host, or a reverse proxy sitting in front of it. This determines whether HTTP compression may be used for responses, based on the request `Referer` header. diff --git a/package.json b/package.json index 136571418d30d..dd06a31be7556 100644 --- a/package.json +++ b/package.json @@ -570,7 +570,7 @@ "@typescript-eslint/parser": "^4.8.1", "@welldone-software/why-did-you-render": "^5.0.0", "@yarnpkg/lockfile": "^1.1.0", - "abab": "^1.0.4", + "abab": "^2.0.4", "angular-aria": "^1.8.0", "angular-mocks": "^1.7.9", "angular-recursion": "^1.0.5", diff --git a/src/core/server/config/deprecation/core_deprecations.test.ts b/src/core/server/config/deprecation/core_deprecations.test.ts index c645629fa5653..ca9b8675a2f3a 100644 --- a/src/core/server/config/deprecation/core_deprecations.test.ts +++ b/src/core/server/config/deprecation/core_deprecations.test.ts @@ -94,6 +94,32 @@ describe('core deprecations', () => { }); }); + describe('server.cors', () => { + it('renames server.cors to server.cors.enabled', () => { + const { migrated } = applyCoreDeprecations({ + server: { cors: true }, + }); + expect(migrated.server.cors).toEqual({ enabled: true }); + }); + it('logs a warning message about server.cors renaming', () => { + const { messages } = applyCoreDeprecations({ + server: { cors: true }, + }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"server.cors\\" is deprecated and has been replaced by \\"server.cors.enabled\\"", + ] + `); + }); + it('does not log deprecation message when server.cors.enabled set', () => { + const { migrated, messages } = applyCoreDeprecations({ + server: { cors: { enabled: true } }, + }); + expect(migrated.server.cors).toEqual({ enabled: true }); + expect(messages.length).toBe(0); + }); + }); + describe('rewriteBasePath', () => { it('logs a warning is server.basePath is set and server.rewriteBasePath is not', () => { const { messages } = applyCoreDeprecations({ diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts index bbdf12f374558..466546d855cea 100644 --- a/src/core/server/config/deprecation/core_deprecations.ts +++ b/src/core/server/config/deprecation/core_deprecations.ts @@ -50,6 +50,17 @@ const rewriteBasePathDeprecation: ConfigDeprecation = (settings, fromPath, log) return settings; }; +const rewriteCorsSettings: ConfigDeprecation = (settings, fromPath, log) => { + const corsSettings = get(settings, 'server.cors'); + if (typeof get(settings, 'server.cors') === 'boolean') { + log('"server.cors" is deprecated and has been replaced by "server.cors.enabled"'); + settings.server.cors = { + enabled: corsSettings, + }; + } + return settings; +}; + const cspRulesDeprecation: ConfigDeprecation = (settings, fromPath, log) => { const NONCE_STRING = `{nonce}`; // Policies that should include the 'self' source @@ -142,6 +153,7 @@ export const coreDeprecationProvider: ConfigDeprecationProvider = ({ renameFromRoot('server.xsrf.whitelist', 'server.xsrf.allowlist'), unusedFromRoot('elasticsearch.preserveHost'), unusedFromRoot('elasticsearch.startupTimeout'), + rewriteCorsSettings, configPathDeprecation, dataPathDeprecation, rewriteBasePathDeprecation, diff --git a/src/core/server/http/__snapshots__/http_config.test.ts.snap b/src/core/server/http/__snapshots__/http_config.test.ts.snap index 7020c5eee6501..a440c67944fab 100644 --- a/src/core/server/http/__snapshots__/http_config.test.ts.snap +++ b/src/core/server/http/__snapshots__/http_config.test.ts.snap @@ -38,7 +38,11 @@ Object { "compression": Object { "enabled": true, }, - "cors": false, + "cors": Object { + "credentials": false, + "enabled": false, + "origin": "*", + }, "customResponseHeaders": Object {}, "host": "localhost", "keepaliveTimeout": 120000, diff --git a/src/core/server/http/cookie_session_storage.test.ts b/src/core/server/http/cookie_session_storage.test.ts index 0e7b55b7d35ab..c7146da7a899a 100644 --- a/src/core/server/http/cookie_session_storage.test.ts +++ b/src/core/server/http/cookie_session_storage.test.ts @@ -68,6 +68,9 @@ configService.atPath.mockImplementation((path) => { allowFromAnyIp: true, ipAllowlist: [], }, + cors: { + enabled: false, + }, } as any); } if (path === 'externalUrl') { diff --git a/src/core/server/http/http_config.test.ts b/src/core/server/http/http_config.test.ts index c82e7c3796e4b..f893e7783ac8f 100644 --- a/src/core/server/http/http_config.test.ts +++ b/src/core/server/http/http_config.test.ts @@ -330,6 +330,59 @@ describe('with compression', () => { }); }); +describe('cors', () => { + describe('origin', () => { + it('list cannot be empty', () => { + expect(() => + config.schema.validate({ + cors: { + origin: [], + }, + }) + ).toThrowErrorMatchingInlineSnapshot(` + "[cors.origin]: types that failed validation: + - [cors.origin.0]: expected value to equal [*] + - [cors.origin.1]: array size is [0], but cannot be smaller than [1]" + `); + }); + + it('list of valid URLs', () => { + const origin = ['http://127.0.0.1:3000', 'https://elastic.co']; + expect( + config.schema.validate({ + cors: { origin }, + }).cors.origin + ).toStrictEqual(origin); + + expect(() => + config.schema.validate({ + cors: { + origin: ['*://elastic.co/*'], + }, + }) + ).toThrow(); + }); + + it('can be configured as "*" wildcard', () => { + expect(config.schema.validate({ cors: { origin: '*' } }).cors.origin).toBe('*'); + }); + }); + describe('credentials', () => { + it('cannot use wildcard origin if "credentials: true"', () => { + expect( + () => config.schema.validate({ cors: { credentials: true, origin: '*' } }).cors.origin + ).toThrowErrorMatchingInlineSnapshot( + `"[cors]: Cannot specify wildcard origin \\"*\\" with \\"credentials: true\\". Please provide a list of allowed origins."` + ); + expect( + () => config.schema.validate({ cors: { credentials: true } }).cors.origin + ).toThrowErrorMatchingInlineSnapshot( + `"[cors]: Cannot specify wildcard origin \\"*\\" with \\"credentials: true\\". Please provide a list of allowed origins."` + ); + }); + }); +}); + describe('HttpConfig', () => { it('converts customResponseHeaders to strings or arrays of strings', () => { const httpSchema = config.schema; diff --git a/src/core/server/http/http_config.ts b/src/core/server/http/http_config.ts index d26f077723ce3..74cdbfbedeea9 100644 --- a/src/core/server/http/http_config.ts +++ b/src/core/server/http/http_config.ts @@ -27,7 +27,7 @@ import { SslConfig, sslSchema } from './ssl_config'; const validBasePathRegex = /^\/.*[^\/]$/; const uuidRegexp = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i; - +const hostURISchema = schema.uri({ scheme: ['http', 'https'] }); const match = (regex: RegExp, errorMsg: string) => (str: string) => regex.test(str) ? undefined : errorMsg; @@ -45,7 +45,25 @@ export const config = { validate: match(validBasePathRegex, "must start with a slash, don't end with one"), }) ), - cors: schema.boolean({ defaultValue: false }), + cors: schema.object( + { + enabled: schema.boolean({ defaultValue: false }), + credentials: schema.boolean({ defaultValue: false }), + origin: schema.oneOf( + [schema.literal('*'), schema.arrayOf(hostURISchema, { minSize: 1 })], + { + defaultValue: '*', + } + ), + }, + { + validate(value) { + if (value.credentials === true && value.origin === '*') { + return 'Cannot specify wildcard origin "*" with "credentials: true". Please provide a list of allowed origins.'; + } + }, + } + ), customResponseHeaders: schema.recordOf(schema.string(), schema.any(), { defaultValue: {}, }), @@ -148,7 +166,11 @@ export class HttpConfig { public keepaliveTimeout: number; public socketTimeout: number; public port: number; - public cors: boolean | { origin: string[] }; + public cors: { + enabled: boolean; + credentials: boolean; + origin: '*' | string[]; + }; public customResponseHeaders: Record; public maxPayload: ByteSizeValue; public basePath?: string; diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index d4c400f53f84a..355803a74b8bb 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -72,6 +72,9 @@ beforeEach(() => { allowFromAnyIp: true, ipAllowlist: [], }, + cors: { + enabled: false, + }, } as any; configWithSSL = { diff --git a/src/core/server/http/http_tools.test.ts b/src/core/server/http/http_tools.test.ts index a409a7485a0ef..4098b631b19d8 100644 --- a/src/core/server/http/http_tools.test.ts +++ b/src/core/server/http/http_tools.test.ts @@ -102,6 +102,9 @@ describe('timeouts', () => { host: '127.0.0.1', maxPayload: new ByteSizeValue(1024), ssl: {}, + cors: { + enabled: false, + }, compression: { enabled: true }, requestId: { allowFromAnyIp: true, @@ -187,6 +190,26 @@ describe('getServerOptions', () => { } `); }); + + it('properly configures CORS when cors enabled', () => { + const httpConfig = new HttpConfig( + config.schema.validate({ + cors: { + enabled: true, + credentials: false, + origin: '*', + }, + }), + {} as any, + {} as any + ); + + expect(getServerOptions(httpConfig).routes?.cors).toEqual({ + credentials: false, + origin: '*', + headers: ['Accept', 'Authorization', 'Content-Type', 'If-None-Match', 'kbn-xsrf'], + }); + }); }); describe('getRequestId', () => { diff --git a/src/core/server/http/http_tools.ts b/src/core/server/http/http_tools.ts index 1e69669e080ec..61688a51345b5 100644 --- a/src/core/server/http/http_tools.ts +++ b/src/core/server/http/http_tools.ts @@ -16,19 +16,34 @@ * specific language governing permissions and limitations * under the License. */ - -import { Lifecycle, Request, ResponseToolkit, Server, ServerOptions, Util } from '@hapi/hapi'; +import { Server } from '@hapi/hapi'; +import type { + Lifecycle, + Request, + ResponseToolkit, + RouteOptionsCors, + ServerOptions, + Util, +} from '@hapi/hapi'; import Hoek from '@hapi/hoek'; -import { ServerOptions as TLSOptions } from 'https'; -import { ValidationError } from 'joi'; +import type { ServerOptions as TLSOptions } from 'https'; +import type { ValidationError } from 'joi'; import uuid from 'uuid'; import { HttpConfig } from './http_config'; import { validateObject } from './prototype_pollution'; +const corsAllowedHeaders = ['Accept', 'Authorization', 'Content-Type', 'If-None-Match', 'kbn-xsrf']; /** * Converts Kibana `HttpConfig` into `ServerOptions` that are accepted by the Hapi server. */ export function getServerOptions(config: HttpConfig, { configureTLS = true } = {}) { + const cors: RouteOptionsCors | false = config.cors.enabled + ? { + credentials: config.cors.credentials, + origin: config.cors.origin, + headers: corsAllowedHeaders, + } + : false; // Note that all connection options configured here should be exactly the same // as in the legacy platform server (see `src/legacy/server/http/index`). Any change // SHOULD BE applied in both places. The only exception is TLS-specific options, @@ -41,7 +56,7 @@ export function getServerOptions(config: HttpConfig, { configureTLS = true } = { privacy: 'private', otherwise: 'private, no-cache, no-store, must-revalidate', }, - cors: config.cors, + cors, payload: { maxBytes: config.maxPayload.getValueInBytes(), }, diff --git a/src/core/server/http/https_redirect_server.test.ts b/src/core/server/http/https_redirect_server.test.ts index f35456f01c19b..1771968702761 100644 --- a/src/core/server/http/https_redirect_server.test.ts +++ b/src/core/server/http/https_redirect_server.test.ts @@ -48,6 +48,9 @@ beforeEach(() => { enabled: true, redirectHttpFromPort: chance.integer({ min: 20000, max: 30000 }), }, + cors: { + enabled: false, + }, } as HttpConfig; server = new HttpsRedirectServer(loggingSystemMock.create().get()); diff --git a/src/core/server/http/integration_tests/lifecycle_handlers.test.ts b/src/core/server/http/integration_tests/lifecycle_handlers.test.ts index ba7f55caeba22..882e289ff2dca 100644 --- a/src/core/server/http/integration_tests/lifecycle_handlers.test.ts +++ b/src/core/server/http/integration_tests/lifecycle_handlers.test.ts @@ -59,6 +59,9 @@ describe('core lifecycle handlers', () => { ssl: { enabled: false, }, + cors: { + enabled: false, + }, compression: { enabled: true }, name: kibanaName, customResponseHeaders: { diff --git a/src/core/server/http/test_utils.ts b/src/core/server/http/test_utils.ts index 0a5cee5505ef1..c91e7a83cfef3 100644 --- a/src/core/server/http/test_utils.ts +++ b/src/core/server/http/test_utils.ts @@ -41,6 +41,9 @@ configService.atPath.mockImplementation((path) => { ssl: { enabled: false, }, + cors: { + enabled: false, + }, compression: { enabled: true }, xsrf: { disableProtection: true, diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 8b5c49c6ece56..848921ca30083 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -16,6 +16,9 @@ require('@kbn/test').runTestsCli([ require.resolve('../test/security_functional/login_selector.config.ts'), require.resolve('../test/security_functional/oidc.config.ts'), require.resolve('../test/security_functional/saml.config.ts'), + require.resolve('../test/functional_embedded/config.ts'), + require.resolve('../test/functional_cors/config.ts'), + require.resolve('../test/functional_enterprise_search/without_host_configured.config.ts'), require.resolve('../test/api_integration/config_security_basic.ts'), require.resolve('../test/api_integration/config_security_trial.ts'), require.resolve('../test/api_integration/config.ts'), diff --git a/x-pack/test/functional_cors/config.ts b/x-pack/test/functional_cors/config.ts new file mode 100644 index 0000000000000..da03fee476f13 --- /dev/null +++ b/x-pack/test/functional_cors/config.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Url from 'url'; +import Path from 'path'; +import getPort from 'get-port'; +import type { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { kbnTestConfig } from '@kbn/test'; +import { pageObjects } from '../functional/page_objects'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const kibanaFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); + + const corsTestPlugin = Path.resolve(__dirname, './plugins/kibana_cors_test'); + + const servers = { + ...kibanaFunctionalConfig.get('servers'), + elasticsearch: { + ...kibanaFunctionalConfig.get('servers.elasticsearch'), + }, + kibana: { + ...kibanaFunctionalConfig.get('servers.kibana'), + }, + }; + + const { protocol, hostname } = kbnTestConfig.getUrlParts(); + const pluginPort = await getPort({ port: 9000 }); + const originUrl = Url.format({ + protocol, + hostname, + port: pluginPort, + }); + + return { + testFiles: [require.resolve('./tests')], + servers, + services: kibanaFunctionalConfig.get('services'), + pageObjects, + junit: { + reportName: 'Kibana CORS with X-Pack Security', + }, + + esTestCluster: kibanaFunctionalConfig.get('esTestCluster'), + apps: { + ...kibanaFunctionalConfig.get('apps'), + }, + + kbnTestServer: { + ...kibanaFunctionalConfig.get('kbnTestServer'), + serverArgs: [ + ...kibanaFunctionalConfig.get('kbnTestServer.serverArgs'), + `--plugin-path=${corsTestPlugin}`, + `--test.cors.port=${pluginPort}`, + '--server.cors.enabled=true', + '--server.cors.credentials=true', + `--server.cors.origin=["${originUrl}"]`, + ], + }, + }; +} diff --git a/x-pack/test/functional_cors/ftr_provider_context.d.ts b/x-pack/test/functional_cors/ftr_provider_context.d.ts new file mode 100644 index 0000000000000..5646c06a3cd30 --- /dev/null +++ b/x-pack/test/functional_cors/ftr_provider_context.d.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { pageObjects } from '../functional/page_objects'; +import { services } from './services'; + +export type FtrProviderContext = GenericFtrProviderContext; +export { pageObjects }; diff --git a/x-pack/test/functional_cors/plugins/kibana_cors_test/kibana.json b/x-pack/test/functional_cors/plugins/kibana_cors_test/kibana.json new file mode 100644 index 0000000000000..9c94f2006b7f8 --- /dev/null +++ b/x-pack/test/functional_cors/plugins/kibana_cors_test/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "kibana_cors_test", + "version": "1.0.0", + "kibanaVersion": "kibana", + "configPath": ["test", "cors"], + "server": true, + "ui": false +} diff --git a/x-pack/test/functional_cors/plugins/kibana_cors_test/package.json b/x-pack/test/functional_cors/plugins/kibana_cors_test/package.json new file mode 100644 index 0000000000000..a0959f4a409d1 --- /dev/null +++ b/x-pack/test/functional_cors/plugins/kibana_cors_test/package.json @@ -0,0 +1,11 @@ +{ + "name": "kiban_cors_test", + "version": "0.0.0", + "kibana": { + "version": "kibana" + }, + "scripts": { + "kbn": "node ../../../../../scripts/kbn.js", + "build": "rm -rf './target' && ../../../../../node_modules/.bin/tsc" + } +} \ No newline at end of file diff --git a/x-pack/test/functional_cors/plugins/kibana_cors_test/server/config.ts b/x-pack/test/functional_cors/plugins/kibana_cors_test/server/config.ts new file mode 100644 index 0000000000000..8e94f3b452bb3 --- /dev/null +++ b/x-pack/test/functional_cors/plugins/kibana_cors_test/server/config.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + port: schema.number(), +}); + +export type ConfigSchema = TypeOf; diff --git a/x-pack/test/functional_cors/plugins/kibana_cors_test/server/index.ts b/x-pack/test/functional_cors/plugins/kibana_cors_test/server/index.ts new file mode 100644 index 0000000000000..9b50f6811ffcd --- /dev/null +++ b/x-pack/test/functional_cors/plugins/kibana_cors_test/server/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import type { PluginConfigDescriptor, PluginInitializerContext } from 'kibana/server'; +import { CorsTestPlugin } from './plugin'; +import { configSchema, ConfigSchema } from './config'; + +export const plugin = (initContext: PluginInitializerContext) => new CorsTestPlugin(initContext); + +export const config: PluginConfigDescriptor = { + schema: configSchema, +}; diff --git a/x-pack/test/functional_cors/plugins/kibana_cors_test/server/plugin.ts b/x-pack/test/functional_cors/plugins/kibana_cors_test/server/plugin.ts new file mode 100644 index 0000000000000..4f545f8907cb7 --- /dev/null +++ b/x-pack/test/functional_cors/plugins/kibana_cors_test/server/plugin.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import Hapi from '@hapi/hapi'; +import { kbnTestConfig } from '@kbn/test'; +import { take } from 'rxjs/operators'; +import Url from 'url'; +import abab from 'abab'; + +import type { Plugin, CoreSetup, CoreStart, PluginInitializerContext } from 'src/core/server'; +import type { ConfigSchema } from './config'; + +const apiToken = abab.btoa(kbnTestConfig.getUrlParts().auth!); + +function renderBody(kibanaUrl: string) { + const url = Url.resolve(kibanaUrl, '/cors-test'); + return ` + + + + + Request to CORS Kibana + + + +`; +} + +export class CorsTestPlugin implements Plugin { + private server?: Hapi.Server; + constructor(private readonly initializerContext: PluginInitializerContext) {} + + async setup(core: CoreSetup) { + const router = core.http.createRouter(); + router.post({ path: '/cors-test', validate: false }, (context, req, res) => + res.ok({ body: 'content from kibana' }) + ); + } + + async start(core: CoreStart) { + const config = await this.initializerContext.config + .create() + .pipe(take(1)) + .toPromise(); + + const server = new Hapi.Server({ + port: config.port, + }); + this.server = server; + + const { protocol, port, hostname } = core.http.getServerInfo(); + + const kibanaUrl = Url.format({ protocol, hostname, port }); + + server.route({ + path: '/', + method: 'GET', + handler(_, h) { + return h.response(renderBody(kibanaUrl)); + }, + }); + await server.start(); + } + public stop() { + if (this.server) { + this.server.stop(); + } + } +} diff --git a/x-pack/test/functional_cors/services.ts b/x-pack/test/functional_cors/services.ts new file mode 100644 index 0000000000000..1bdf67abd89d0 --- /dev/null +++ b/x-pack/test/functional_cors/services.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { services as functionalServices } from '../functional/services'; + +export const services = functionalServices; diff --git a/x-pack/test/functional_cors/tests/cors.ts b/x-pack/test/functional_cors/tests/cors.ts new file mode 100644 index 0000000000000..ff5da26b4e275 --- /dev/null +++ b/x-pack/test/functional_cors/tests/cors.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const browser = getService('browser'); + const config = getService('config'); + const find = getService('find'); + + describe('CORS', () => { + it('Communicates to Kibana with configured CORS', async () => { + const args: string[] = config.get('kbnTestServer.serverArgs'); + const originSetting = args.find((str) => str.includes('server.cors.origin')); + if (!originSetting) { + throw new Error('Cannot find "server.cors.origin" argument'); + } + const [, value] = originSetting.split('='); + const url = JSON.parse(value); + + await browser.navigateTo(url[0]); + const element = await find.byCssSelector('p'); + expect(await element.getVisibleText()).to.be('content from kibana'); + }); + }); +} diff --git a/x-pack/test/functional_cors/tests/index.ts b/x-pack/test/functional_cors/tests/index.ts new file mode 100644 index 0000000000000..7e16e1339b1e7 --- /dev/null +++ b/x-pack/test/functional_cors/tests/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Kibana cors', function () { + this.tags('ciGroup2'); + loadTestFile(require.resolve('./cors')); + }); +} diff --git a/yarn.lock b/yarn.lock index 1ad90ce334baa..84a513a9dfeff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6405,12 +6405,7 @@ JSONStream@1.3.5, JSONStream@^1.0.3: resolved "https://registry.yarnpkg.com/JSV/-/JSV-4.0.2.tgz#d077f6825571f82132f9dffaed587b4029feff57" integrity sha1-0Hf2glVx+CEy+d/67Vh7QCn+/1c= -abab@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e" - integrity sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4= - -abab@^2.0.0, abab@^2.0.3: +abab@^2.0.0, abab@^2.0.3, abab@^2.0.4: version "2.0.5" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==