Add functional test for Kibana embedded in iframe (#68544)

* convert kbn test config into TS

* add test  for Kibana embedded in iframe

* run embedded tests in functional suite

* ignore tls errors in functional tests by default

* switch test to https

* remove env vars mutation

* allow to pass ssl config to Kibana

* pass ssl config to axios

* adopt KbnClient interfaces

* adopt KibanaServer

* use KbnRequester in security service

* set sameSiteCookies:None in test

* acceptInsecureCerts in chrome

* remove leftovers

* fix type error

* remove unnecessary field

* address comments

* refactor plugin

* refactor test

* make acceptInsecureCerts configurable

* run firefox tests on ci

* up TS version

* fix firefox.sh script

* fix path

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Mikhail Shustov 2020-06-18 13:02:56 +03:00 committed by GitHub
parent d2006ea8a0
commit c8c20e4ca8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 393 additions and 113 deletions

View file

@ -18,7 +18,7 @@
*/
import { ToolingLog } from '../tooling_log';
import { KbnClientRequester, ReqOptions } from './kbn_client_requester';
import { KibanaConfig, KbnClientRequester, ReqOptions } from './kbn_client_requester';
import { KbnClientStatus } from './kbn_client_status';
import { KbnClientPlugins } from './kbn_client_plugins';
import { KbnClientVersion } from './kbn_client_version';
@ -26,7 +26,7 @@ import { KbnClientSavedObjects } from './kbn_client_saved_objects';
import { KbnClientUiSettings, UiSettingValues } from './kbn_client_ui_settings';
export class KbnClient {
private readonly requester = new KbnClientRequester(this.log, this.kibanaUrls);
private readonly requester = new KbnClientRequester(this.log, this.kibanaConfig);
readonly status = new KbnClientStatus(this.requester);
readonly plugins = new KbnClientPlugins(this.status);
readonly version = new KbnClientVersion(this.status);
@ -43,10 +43,10 @@ export class KbnClient {
*/
constructor(
private readonly log: ToolingLog,
private readonly kibanaUrls: string[],
private readonly kibanaConfig: KibanaConfig,
private readonly uiSettingDefaults?: UiSettingValues
) {
if (!kibanaUrls.length) {
if (!kibanaConfig.url) {
throw new Error('missing Kibana urls');
}
}

View file

@ -16,10 +16,9 @@
* specific language governing permissions and limitations
* under the License.
*/
import Url from 'url';
import Axios from 'axios';
import Https from 'https';
import Axios, { AxiosResponse } from 'axios';
import { isAxiosRequestError, isAxiosResponseError } from '../axios';
import { ToolingLog } from '../tooling_log';
@ -70,20 +69,38 @@ const delay = (ms: number) =>
setTimeout(resolve, ms);
});
export interface KibanaConfig {
url: string;
ssl?: {
enabled: boolean;
key: string;
certificate: string;
certificateAuthorities: string;
};
}
export class KbnClientRequester {
constructor(private readonly log: ToolingLog, private readonly kibanaUrls: string[]) {}
private readonly httpsAgent: Https.Agent | null;
constructor(private readonly log: ToolingLog, private readonly kibanaConfig: KibanaConfig) {
this.httpsAgent =
kibanaConfig.ssl && kibanaConfig.ssl.enabled
? new Https.Agent({
cert: kibanaConfig.ssl.certificate,
key: kibanaConfig.ssl.key,
ca: kibanaConfig.ssl.certificateAuthorities,
})
: null;
}
private pickUrl() {
const url = this.kibanaUrls.shift()!;
this.kibanaUrls.push(url);
return url;
return this.kibanaConfig.url;
}
public resolveUrl(relativeUrl: string = '/') {
return Url.resolve(this.pickUrl(), relativeUrl);
}
async request<T>(options: ReqOptions): Promise<T> {
async request<T>(options: ReqOptions): Promise<AxiosResponse<T>> {
const url = Url.resolve(this.pickUrl(), options.path);
const description = options.description || `${options.method} ${url}`;
let attempt = 0;
@ -93,7 +110,7 @@ export class KbnClientRequester {
attempt += 1;
try {
const response = await Axios.request<T>({
const response = await Axios.request({
method: options.method,
url,
data: options.body,
@ -101,9 +118,10 @@ export class KbnClientRequester {
headers: {
'kbn-xsrf': 'kbn-client',
},
httpsAgent: this.httpsAgent,
});
return response.data;
return response;
} catch (error) {
const conflictOnGet = isConcliftOnGetError(error);
const requestedRetries = options.retries !== undefined;

View file

@ -71,12 +71,13 @@ export class KbnClientSavedObjects {
public async migrate() {
this.log.debug('Migrating saved objects');
return await this.requester.request<MigrateResponse>({
const { data } = await this.requester.request<MigrateResponse>({
description: 'migrate saved objects',
path: uriencode`/internal/saved_objects/_migrate`,
method: 'POST',
body: {},
});
return data;
}
/**
@ -85,11 +86,12 @@ export class KbnClientSavedObjects {
public async get<Attributes extends Record<string, any>>(options: GetOptions) {
this.log.debug('Gettings saved object: %j', options);
return await this.requester.request<SavedObjectResponse<Attributes>>({
const { data } = await this.requester.request<SavedObjectResponse<Attributes>>({
description: 'get saved object',
path: uriencode`/api/saved_objects/${options.type}/${options.id}`,
method: 'GET',
});
return data;
}
/**
@ -98,7 +100,7 @@ export class KbnClientSavedObjects {
public async create<Attributes extends Record<string, any>>(options: IndexOptions<Attributes>) {
this.log.debug('Creating saved object: %j', options);
return await this.requester.request<SavedObjectResponse<Attributes>>({
const { data } = await this.requester.request<SavedObjectResponse<Attributes>>({
description: 'update saved object',
path: options.id
? uriencode`/api/saved_objects/${options.type}/${options.id}`
@ -113,6 +115,7 @@ export class KbnClientSavedObjects {
references: options.references,
},
});
return data;
}
/**
@ -121,7 +124,7 @@ export class KbnClientSavedObjects {
public async update<Attributes extends Record<string, any>>(options: UpdateOptions<Attributes>) {
this.log.debug('Updating saved object: %j', options);
return await this.requester.request<SavedObjectResponse<Attributes>>({
const { data } = await this.requester.request<SavedObjectResponse<Attributes>>({
description: 'update saved object',
path: uriencode`/api/saved_objects/${options.type}/${options.id}`,
query: {
@ -134,6 +137,7 @@ export class KbnClientSavedObjects {
references: options.references,
},
});
return data;
}
/**
@ -142,10 +146,12 @@ export class KbnClientSavedObjects {
public async delete(options: GetOptions) {
this.log.debug('Deleting saved object %s/%s', options);
return await this.requester.request({
const { data } = await this.requester.request({
description: 'delete saved object',
path: uriencode`/api/saved_objects/${options.type}/${options.id}`,
method: 'DELETE',
});
return data;
}
}

View file

@ -52,10 +52,11 @@ export class KbnClientStatus {
* Get the full server status
*/
async get() {
return await this.requester.request<ApiResponseStatus>({
const { data } = await this.requester.request<ApiResponseStatus>({
method: 'GET',
path: 'api/status',
});
return data;
}
/**

View file

@ -57,10 +57,11 @@ export class KbnClientUiSettings {
* Unset a uiSetting
*/
async unset(setting: string) {
return await this.requester.request<any>({
const { data } = await this.requester.request<any>({
path: uriencode`/api/kibana/settings/${setting}`,
method: 'DELETE',
});
return data;
}
/**
@ -105,11 +106,11 @@ export class KbnClientUiSettings {
}
private async getAll() {
const resp = await this.requester.request<UiSettingsApiResponse>({
const { data } = await this.requester.request<UiSettingsApiResponse>({
path: '/api/kibana/settings',
method: 'GET',
});
return resp.settings;
return data.settings;
}
}

View file

@ -38,6 +38,14 @@ const urlPartsSchema = () =>
password: Joi.string(),
pathname: Joi.string().regex(/^\//, 'start with a /'),
hash: Joi.string().regex(/^\//, 'start with a /'),
ssl: Joi.object()
.keys({
enabled: Joi.boolean().default(false),
certificate: Joi.string().optional(),
certificateAuthorities: Joi.string().optional(),
key: Joi.string().optional(),
})
.default(),
})
.default();
@ -122,6 +130,7 @@ export const schema = Joi.object()
type: Joi.string().valid('chrome', 'firefox', 'ie', 'msedge').default('chrome'),
logPollingMs: Joi.number().default(100),
acceptInsecureCerts: Joi.boolean().default(false),
})
.default(),

View file

@ -16,26 +16,34 @@
* specific language governing permissions and limitations
* under the License.
*/
import { kibanaTestUser } from './users';
import url from 'url';
import { kibanaTestUser } from './users';
interface UrlParts {
protocol?: string;
hostname?: string;
port?: number;
auth?: string;
username?: string;
password?: string;
}
export const kbnTestConfig = new (class KbnTestConfig {
getPort() {
return this.getUrlParts().port;
}
getUrlParts() {
getUrlParts(): UrlParts {
// allow setting one complete TEST_KIBANA_URL for ES like https://elastic:changeme@example.com:9200
if (process.env.TEST_KIBANA_URL) {
const testKibanaUrl = url.parse(process.env.TEST_KIBANA_URL);
return {
protocol: testKibanaUrl.protocol.slice(0, -1),
protocol: testKibanaUrl.protocol?.slice(0, -1),
hostname: testKibanaUrl.hostname,
port: parseInt(testKibanaUrl.port, 10),
port: testKibanaUrl.port ? parseInt(testKibanaUrl.port, 10) : undefined,
auth: testKibanaUrl.auth,
username: testKibanaUrl.auth.split(':')[0],
password: testKibanaUrl.auth.split(':')[1],
username: testKibanaUrl.auth?.split(':')[0],
password: testKibanaUrl.auth?.split(':')[1],
};
}
@ -44,7 +52,7 @@ export const kbnTestConfig = new (class KbnTestConfig {
return {
protocol: process.env.TEST_KIBANA_PROTOCOL || 'http',
hostname: process.env.TEST_KIBANA_HOSTNAME || 'localhost',
port: parseInt(process.env.TEST_KIBANA_PORT, 10) || 5620,
port: process.env.TEST_KIBANA_PORT ? parseInt(process.env.TEST_KIBANA_PORT, 10) : 5620,
auth: `${username}:${password}`,
username,
password,

View file

@ -49,7 +49,7 @@ export class EsArchiver {
this.client = client;
this.dataDir = dataDir;
this.log = log;
this.kbnClient = new KbnClient(log, [kibanaUrl]);
this.kbnClient = new KbnClient(log, { url: kibanaUrl });
}
/**

View file

@ -27,9 +27,9 @@ export function KibanaServerProvider({ getService }: FtrProviderContext) {
const config = getService('config');
const lifecycle = getService('lifecycle');
const url = Url.format(config.get('servers.kibana'));
const ssl = config.get('servers.kibana').ssl;
const defaults = config.get('uiSettings.defaults');
const kbn = new KbnClient(log, [url], defaults);
const kbn = new KbnClient(log, { url, ssl }, defaults);
if (defaults) {
lifecycle.beforeTests.add(async () => {

View file

@ -17,27 +17,20 @@
* under the License.
*/
import axios, { AxiosInstance } from 'axios';
import util from 'util';
import { ToolingLog } from '@kbn/dev-utils';
import { KbnClient, ToolingLog } from '@kbn/dev-utils';
export class Role {
private log: ToolingLog;
private axios: AxiosInstance;
constructor(url: string, log: ToolingLog) {
this.log = log;
this.axios = axios.create({
headers: { 'kbn-xsrf': 'x-pack/ftr/services/security/role' },
baseURL: url,
maxRedirects: 0,
validateStatus: () => true, // we do our own validation below and throw better error messages
});
}
constructor(private log: ToolingLog, private kibanaServer: KbnClient) {}
public async create(name: string, role: any) {
this.log.debug(`creating role ${name}`);
const { data, status, statusText } = await this.axios.put(`/api/security/role/${name}`, role);
const { data, status, statusText } = await this.kibanaServer.request({
path: `/api/security/role/${name}`,
method: 'PUT',
body: role,
retries: 0,
});
if (status !== 204) {
throw new Error(
`Expected status code of 204, received ${status} ${statusText}: ${util.inspect(data)}`
@ -47,7 +40,10 @@ export class Role {
public async delete(name: string) {
this.log.debug(`deleting role ${name}`);
const { data, status, statusText } = await this.axios.delete(`/api/security/role/${name}`);
const { data, status, statusText } = await this.kibanaServer.request({
path: `/api/security/role/${name}`,
method: 'DELETE',
});
if (status !== 204 && status !== 404) {
throw new Error(
`Expected status code of 204 or 404, received ${status} ${statusText}: ${util.inspect(

View file

@ -17,30 +17,19 @@
* under the License.
*/
import axios, { AxiosInstance } from 'axios';
import util from 'util';
import { ToolingLog } from '@kbn/dev-utils';
import { KbnClient, ToolingLog } from '@kbn/dev-utils';
export class RoleMappings {
private log: ToolingLog;
private axios: AxiosInstance;
constructor(url: string, log: ToolingLog) {
this.log = log;
this.axios = axios.create({
headers: { 'kbn-xsrf': 'x-pack/ftr/services/security/role_mappings' },
baseURL: url,
maxRedirects: 0,
validateStatus: () => true, // we do our own validation below and throw better error messages
});
}
constructor(private log: ToolingLog, private kbnClient: KbnClient) {}
public async create(name: string, roleMapping: Record<string, any>) {
this.log.debug(`creating role mapping ${name}`);
const { data, status, statusText } = await this.axios.post(
`/internal/security/role_mapping/${name}`,
roleMapping
);
const { data, status, statusText } = await this.kbnClient.request({
path: `/internal/security/role_mapping/${name}`,
method: 'POST',
body: roleMapping,
});
if (status !== 200) {
throw new Error(
`Expected status code of 200, received ${status} ${statusText}: ${util.inspect(data)}`
@ -51,9 +40,10 @@ export class RoleMappings {
public async delete(name: string) {
this.log.debug(`deleting role mapping ${name}`);
const { data, status, statusText } = await this.axios.delete(
`/internal/security/role_mapping/${name}`
);
const { data, status, statusText } = await this.kbnClient.request({
path: `/internal/security/role_mapping/${name}`,
method: 'DELETE',
});
if (status !== 200 && status !== 404) {
throw new Error(
`Expected status code of 200 or 404, received ${status} ${statusText}: ${util.inspect(

View file

@ -17,8 +17,6 @@
* under the License.
*/
import { format as formatUrl } from 'url';
import { Role } from './role';
import { User } from './user';
import { RoleMappings } from './role_mappings';
@ -28,14 +26,14 @@ import { createTestUserService } from './test_user';
export async function SecurityServiceProvider(context: FtrProviderContext) {
const { getService } = context;
const log = getService('log');
const config = getService('config');
const url = formatUrl(config.get('servers.kibana'));
const role = new Role(url, log);
const user = new User(url, log);
const kibanaServer = getService('kibanaServer');
const role = new Role(log, kibanaServer);
const user = new User(log, kibanaServer);
const testUser = await createTestUserService(role, user, context);
return new (class SecurityService {
roleMappings = new RoleMappings(url, log);
roleMappings = new RoleMappings(log, kibanaServer);
testUser = testUser;
role = role;
user = user;

View file

@ -17,33 +17,22 @@
* under the License.
*/
import axios, { AxiosInstance } from 'axios';
import util from 'util';
import { ToolingLog } from '@kbn/dev-utils';
import { KbnClient, ToolingLog } from '@kbn/dev-utils';
export class User {
private log: ToolingLog;
private axios: AxiosInstance;
constructor(url: string, log: ToolingLog) {
this.log = log;
this.axios = axios.create({
headers: { 'kbn-xsrf': 'x-pack/ftr/services/security/user' },
baseURL: url,
maxRedirects: 0,
validateStatus: () => true, // we do our own validation below and throw better error messages
});
}
constructor(private log: ToolingLog, private kbnClient: KbnClient) {}
public async create(username: string, user: any) {
this.log.debug(`creating user ${username}`);
const { data, status, statusText } = await this.axios.post(
`/internal/security/users/${username}`,
{
const { data, status, statusText } = await this.kbnClient.request({
path: `/internal/security/users/${username}`,
method: 'POST',
body: {
username,
...user,
}
);
},
});
if (status !== 200) {
throw new Error(
`Expected status code of 200, received ${status} ${statusText}: ${util.inspect(data)}`
@ -54,9 +43,10 @@ export class User {
public async delete(username: string) {
this.log.debug(`deleting user ${username}`);
const { data, status, statusText } = await this.axios.delete(
`/internal/security/users/${username}`
);
const { data, status, statusText } = await await this.kbnClient.request({
path: `/internal/security/users/${username}`,
method: 'DELETE',
});
if (status !== 204) {
throw new Error(
`Expected status code of 204, received ${status} ${statusText}: ${util.inspect(data)}`

View file

@ -529,5 +529,10 @@ export async function BrowserProvider({ getService }: FtrProviderContext) {
await driver.executeScript('document.body.scrollLeft = ' + scrollSize);
return this.getScrollLeft();
}
public async switchToFrame(idOrElement: number | WebElementWrapper) {
const _id = idOrElement instanceof WebElementWrapper ? idOrElement._webElement : idOrElement;
await driver.switchTo().frame(_id);
}
})();
}

View file

@ -23,7 +23,7 @@ import { resolve } from 'path';
import { mergeMap } from 'rxjs/operators';
import { FtrProviderContext } from '../../ftr_provider_context';
import { initWebDriver } from './webdriver';
import { initWebDriver, BrowserConfig } from './webdriver';
import { Browsers } from './browsers';
export async function RemoteProvider({ getService }: FtrProviderContext) {
@ -58,12 +58,12 @@ export async function RemoteProvider({ getService }: FtrProviderContext) {
Fs.writeFileSync(path, JSON.stringify(JSON.parse(coverageJson), null, 2));
};
const { driver, consoleLog$ } = await initWebDriver(
log,
browserType,
lifecycle,
config.get('browser.logPollingMs')
);
const browserConfig: BrowserConfig = {
logPollingMs: config.get('browser.logPollingMs'),
acceptInsecureCerts: config.get('browser.acceptInsecureCerts'),
};
const { driver, consoleLog$ } = await initWebDriver(log, browserType, lifecycle, browserConfig);
const isW3CEnabled = (driver as any).executor_.w3c;
const caps = await driver.getCapabilities();

View file

@ -73,13 +73,18 @@ Executor.prototype.execute = preventParallelCalls(
(command: { getName: () => string }) => NO_QUEUE_COMMANDS.includes(command.getName())
);
export interface BrowserConfig {
logPollingMs: number;
acceptInsecureCerts: boolean;
}
let attemptCounter = 0;
let edgePaths: { driverPath: string | undefined; browserPath: string | undefined };
async function attemptToCreateCommand(
log: ToolingLog,
browserType: Browsers,
lifecycle: Lifecycle,
logPollingMs: number
config: BrowserConfig
) {
const attemptId = ++attemptCounter;
log.debug('[webdriver] Creating session');
@ -114,6 +119,7 @@ async function attemptToCreateCommand(
if (certValidation === '0') {
chromeOptions.push('ignore-certificate-errors');
}
if (remoteDebug === '1') {
// Visit chrome://inspect in chrome to remotely view/debug
chromeOptions.push('headless', 'disable-gpu', 'remote-debugging-port=9222');
@ -125,6 +131,7 @@ async function attemptToCreateCommand(
});
chromeCapabilities.set('unexpectedAlertBehaviour', 'accept');
chromeCapabilities.set('goog:loggingPrefs', { browser: 'ALL' });
chromeCapabilities.setAcceptInsecureCerts(config.acceptInsecureCerts);
const session = await new Builder()
.forBrowser(browserType)
@ -137,7 +144,7 @@ async function attemptToCreateCommand(
consoleLog$: pollForLogEntry$(
session,
logging.Type.BROWSER,
logPollingMs,
config.logPollingMs,
lifecycle.cleanup.after$
).pipe(
takeUntil(lifecycle.cleanup.after$),
@ -174,7 +181,7 @@ async function attemptToCreateCommand(
consoleLog$: pollForLogEntry$(
session,
logging.Type.BROWSER,
logPollingMs,
config.logPollingMs,
lifecycle.cleanup.after$
).pipe(
takeUntil(lifecycle.cleanup.after$),
@ -206,6 +213,7 @@ async function attemptToCreateCommand(
'browser.helperApps.neverAsk.saveToDisk',
'application/comma-separated-values, text/csv, text/plain'
);
firefoxOptions.setAcceptInsecureCerts(config.acceptInsecureCerts);
if (headlessBrowser === '1') {
// See: https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode
@ -317,7 +325,7 @@ export async function initWebDriver(
log: ToolingLog,
browserType: Browsers,
lifecycle: Lifecycle,
logPollingMs: number
config: BrowserConfig
) {
const logger = getLogger('webdriver.http.Executor');
logger.setLevel(logging.Level.FINEST);
@ -348,7 +356,7 @@ export async function initWebDriver(
while (true) {
const command = await Promise.race([
delay(30 * SECOND),
attemptToCreateCommand(log, browserType, lifecycle, logPollingMs),
attemptToCreateCommand(log, browserType, lifecycle, config),
]);
if (!command) {

View file

@ -7,4 +7,5 @@ checks-reporter-with-killswitch "X-Pack firefox smoke test" \
--debug --bail \
--kibana-install-dir "$KIBANA_INSTALL_DIR" \
--include-tag "includeFirefox" \
--config test/functional/config.firefox.js;
--config test/functional/config.firefox.js \
--config test/functional_embedded/config.firefox.ts;

View file

@ -51,6 +51,7 @@ const onlyNotInCoverageTests = [
require.resolve('../test/licensing_plugin/config.legacy.ts'),
require.resolve('../test/endpoint_api_integration_no_ingest/config.ts'),
require.resolve('../test/reporting_api_integration/config.js'),
require.resolve('../test/functional_embedded/config.ts'),
];
require('@kbn/plugin-helpers').babelRegister();

View file

@ -0,0 +1,27 @@
/*
* 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 { FtrConfigProviderContext } from '@kbn/test/types/ftr';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const chromeConfig = await readConfigFile(require.resolve('./config'));
return {
...chromeConfig.getAll(),
browser: {
type: 'firefox',
acceptInsecureCerts: true,
},
suiteTags: {
exclude: ['skipFirefox'],
},
junit: {
reportName: 'Firefox Kibana Embedded in iframe with X-Pack Security',
},
};
}

View file

@ -0,0 +1,67 @@
/*
* 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 Fs from 'fs';
import { resolve } from 'path';
import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils';
import { FtrConfigProviderContext } from '@kbn/test/types/ftr';
import { pageObjects } from '../functional/page_objects';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const kibanaFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js'));
const iframeEmbeddedPlugin = resolve(__dirname, './plugins/iframe_embedded');
const servers = {
...kibanaFunctionalConfig.get('servers'),
elasticsearch: {
...kibanaFunctionalConfig.get('servers.elasticsearch'),
},
kibana: {
...kibanaFunctionalConfig.get('servers.kibana'),
protocol: 'https',
ssl: {
enabled: true,
key: Fs.readFileSync(KBN_KEY_PATH).toString('utf8'),
certificate: Fs.readFileSync(KBN_CERT_PATH).toString('utf8'),
certificateAuthorities: Fs.readFileSync(CA_CERT_PATH).toString('utf8'),
},
},
};
return {
testFiles: [require.resolve('./tests')],
servers,
services: kibanaFunctionalConfig.get('services'),
pageObjects,
browser: {
acceptInsecureCerts: true,
},
junit: {
reportName: 'Kibana Embedded in iframe with X-Pack Security',
},
esTestCluster: kibanaFunctionalConfig.get('esTestCluster'),
apps: {
...kibanaFunctionalConfig.get('apps'),
},
kbnTestServer: {
...kibanaFunctionalConfig.get('kbnTestServer'),
serverArgs: [
...kibanaFunctionalConfig.get('kbnTestServer.serverArgs'),
`--plugin-path=${iframeEmbeddedPlugin}`,
'--server.ssl.enabled=true',
`--server.ssl.key=${KBN_KEY_PATH}`,
`--server.ssl.certificate=${KBN_CERT_PATH}`,
`--server.ssl.certificateAuthorities=${CA_CERT_PATH}`,
'--xpack.security.sameSiteCookies=None',
'--xpack.security.secureCookies=true',
],
},
};
}

View file

@ -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<typeof services, typeof pageObjects>;
export { pageObjects };

View file

@ -0,0 +1,7 @@
{
"id": "iframe_embedded",
"version": "1.0.0",
"kibanaVersion": "kibana",
"server": true,
"ui": false
}

View file

@ -0,0 +1,14 @@
{
"name": "iframe_embedded",
"version": "0.0.0",
"kibana": {
"version": "kibana"
},
"scripts": {
"kbn": "node ../../../../../scripts/kbn.js",
"build": "rm -rf './target' && tsc"
},
"devDependencies": {
"typescript": "3.9.5"
}
}

View file

@ -0,0 +1,11 @@
/*
* 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 { PluginInitializerContext } from 'kibana/server';
import { IframeEmbeddedPlugin } from './plugin';
export const plugin = (initContext: PluginInitializerContext) =>
new IframeEmbeddedPlugin(initContext);

View file

@ -0,0 +1,45 @@
/*
* 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 { Plugin, CoreSetup, PluginInitializerContext } from 'src/core/server';
function renderBody(iframeUrl: string) {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Kibana embedded in iframe</title>
</head>
<body>
<iframe data-test-subj="iframe_embedded" width="1000" height="1200" src="${iframeUrl}" frameborder="0"/>
</body>
</html>
`;
}
export class IframeEmbeddedPlugin implements Plugin {
constructor(initializerContext: PluginInitializerContext) {}
public setup(core: CoreSetup) {
core.http.resources.register(
{
path: '/iframe_embedded',
validate: false,
},
async (context, request, response) => {
const { protocol, port, host } = core.http.getServerInfo();
const kibanaUrl = Url.format({ protocol, hostname: host, port });
return response.renderHtml({
body: renderBody(kibanaUrl),
});
}
);
}
public start() {}
public stop() {}
}

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;
* you may not use this file except in compliance with the Elastic License.
*/
import { services as functionalServices } from '../functional/services';
export const services = functionalServices;

View file

@ -0,0 +1,42 @@
/*
* 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 expect from '@kbn/expect';
import { FtrProviderContext } from '../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const PageObjects = getPageObjects(['security', 'common']);
const browser = getService('browser');
const config = getService('config');
const testSubjects = getService('testSubjects');
describe('in iframe', () => {
it('should open Kibana for logged-in user', async () => {
const isChromeHiddenBefore = await PageObjects.common.isChromeHidden();
expect(isChromeHiddenBefore).to.be(true);
await PageObjects.security.login();
const { protocol, hostname, port } = config.get('servers.kibana');
const url = Url.format({
protocol,
hostname,
port,
pathname: 'iframe_embedded',
});
await browser.navigateTo(url);
const iframe = await testSubjects.find('iframe_embedded');
await browser.switchToFrame(iframe);
const isChromeHidden = await PageObjects.common.isChromeHidden();
expect(isChromeHidden).to.be(false);
});
});
}

View file

@ -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 embedded', function () {
this.tags('ciGroup2');
loadTestFile(require.resolve('./iframe_embedded'));
});
}