[7.17] Test config settings that are exposed to the browser (#129438) (#129857)

* Test config settings that are exposed to the browser (#129438)

(cherry picked from commit 27ff7d3424)

# Conflicts:
#	.github/CODEOWNERS
#	packages/kbn-config-schema/src/index.ts

* Enable security plugin in OSS tests

This is a partial backport of #111681, so the Kibana security
plugin is enabled but Elasticsearch security is still disabled.

* Fix exposed config key tests

The exposed config keys are slightly different in the 7.17 branch.

* Fix UI Capabilities tests

The enterpriseSearch plugin does not have a required dependency on
the security plugin in the 7.17 branch, so our bacported
assertions for these tests needed to change accordingly.
This commit is contained in:
Joe Portner 2022-04-11 13:31:45 -04:00 committed by GitHub
parent 923cd73b3b
commit 6ba02ec7d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 562 additions and 156 deletions

View file

@ -38,6 +38,7 @@ import {
NullableProps,
RecordOfOptions,
RecordOfType,
SchemaStructureEntry,
StringOptions,
StringType,
Type,
@ -49,7 +50,7 @@ import {
StreamType,
} from './types';
export type { TypeOf, Props, NullableProps };
export type { TypeOf, Props, SchemaStructureEntry, NullableProps };
export { ObjectType, Type };
export { ByteSizeValue } from './byte_size_value';
export { SchemaTypeError, ValidationError } from './errors';

View file

@ -7,6 +7,7 @@
*/
export type { TypeOptions } from './type';
export type { SchemaStructureEntry } from './type';
export { Type } from './type';
export { AnyType } from './any_type';
export type { ArrayOptions } from './array_type';

View file

@ -490,3 +490,76 @@ describe('#extends', () => {
expect(extended.validate(undefined)).toEqual({ initial: 'bar', added: 42 });
});
});
test('returns schema structure', () => {
// This test covers different schema types that may or may not be nested
const objSchema = schema.object({
any: schema.any(),
array: schema.arrayOf(schema.string()),
boolean: schema.boolean(),
buffer: schema.buffer(),
byteSize: schema.byteSize(),
conditional: schema.conditional(
schema.contextRef('context_value_1'),
schema.contextRef('context_value_2'),
schema.string(),
schema.string()
),
duration: schema.duration(),
ip: schema.ip(),
literal: schema.literal('foo'),
map: schema.mapOf(schema.string(), schema.string()),
maybe: schema.maybe(schema.string()),
never: schema.never(),
nullable: schema.nullable(schema.string()),
number: schema.number(),
record: schema.recordOf(schema.string(), schema.string()),
stream: schema.stream(),
string: schema.string(),
union: schema.oneOf([schema.string()]),
uri: schema.uri(),
});
const type = objSchema.extends({
nested: objSchema,
});
expect(type.getSchemaStructure()).toEqual([
{ path: ['any'], type: 'any' },
{ path: ['array'], type: 'array' },
{ path: ['boolean'], type: 'boolean' },
{ path: ['buffer'], type: 'binary' },
{ path: ['byteSize'], type: 'bytes' },
{ path: ['conditional'], type: 'any' },
{ path: ['duration'], type: 'duration' },
{ path: ['ip'], type: 'string' },
{ path: ['literal'], type: 'any' },
{ path: ['map'], type: 'map' },
{ path: ['maybe'], type: 'string' },
{ path: ['never'], type: 'any' },
{ path: ['nullable'], type: 'alternatives' },
{ path: ['number'], type: 'number' },
{ path: ['record'], type: 'record' },
{ path: ['stream'], type: 'stream' },
{ path: ['string'], type: 'string' },
{ path: ['union'], type: 'alternatives' },
{ path: ['uri'], type: 'string' },
{ path: ['nested', 'any'], type: 'any' },
{ path: ['nested', 'array'], type: 'array' },
{ path: ['nested', 'boolean'], type: 'boolean' },
{ path: ['nested', 'buffer'], type: 'binary' },
{ path: ['nested', 'byteSize'], type: 'bytes' },
{ path: ['nested', 'conditional'], type: 'any' },
{ path: ['nested', 'duration'], type: 'duration' },
{ path: ['nested', 'ip'], type: 'string' },
{ path: ['nested', 'literal'], type: 'any' },
{ path: ['nested', 'map'], type: 'map' },
{ path: ['nested', 'maybe'], type: 'string' },
{ path: ['nested', 'never'], type: 'any' },
{ path: ['nested', 'nullable'], type: 'alternatives' },
{ path: ['nested', 'number'], type: 'number' },
{ path: ['nested', 'record'], type: 'record' },
{ path: ['nested', 'stream'], type: 'stream' },
{ path: ['nested', 'string'], type: 'string' },
{ path: ['nested', 'union'], type: 'alternatives' },
{ path: ['nested', 'uri'], type: 'string' },
]);
});

View file

@ -53,6 +53,7 @@ export class StringType extends Type<string> {
);
}
schema.type = 'string';
super(schema, options);
}

View file

@ -15,6 +15,11 @@ export interface TypeOptions<T> {
validate?: (value: T) => string | void;
}
export interface SchemaStructureEntry {
path: string[];
type: string;
}
export const convertValidationFunction = <T = unknown>(
validate: (value: T) => string | void
): CustomValidator<T> => {
@ -98,6 +103,10 @@ export abstract class Type<V> {
return this.internalSchema;
}
public getSchemaStructure() {
return recursiveGetSchemaStructure(this.internalSchema);
}
protected handleError(
type: string,
context: Record<string, any>,
@ -141,3 +150,17 @@ export abstract class Type<V> {
return new SchemaTypeError(message || code, convertedPath);
}
}
function recursiveGetSchemaStructure(internalSchema: AnySchema, path: string[] = []) {
const array: SchemaStructureEntry[] = [];
// Note: we are relying on Joi internals to obtain the schema structure (recursive keys).
// This is not ideal, but it works for now and we only need it for some integration test assertions.
// If it breaks in the future, we'll need to update our tests.
for (const [key, val] of (internalSchema as any)._ids._byKey.entries()) {
array.push(...recursiveGetSchemaStructure(val.schema, [...path, key]));
}
if (!array.length) {
array.push({ path, type: internalSchema.type ?? 'unknown' });
}
return array;
}

View file

@ -98,6 +98,7 @@ export class HttpResourcesService implements CoreService<InternalHttpResourcesSe
vars: {
apmConfig,
},
includeExposedConfigKeys: options.includeExposedConfigKeys,
});
return response.ok({
@ -112,6 +113,7 @@ export class HttpResourcesService implements CoreService<InternalHttpResourcesSe
vars: {
apmConfig,
},
includeExposedConfigKeys: options.includeExposedConfigKeys,
});
return response.ok({

View file

@ -28,6 +28,11 @@ export interface HttpResourcesRenderOptions {
* All HTML pages are already pre-configured with `content-security-policy` header that cannot be overridden.
* */
headers?: ResponseHeaders;
/**
* @internal
* This is only used for integration tests that allow us to verify which config keys are exposed to the browser.
*/
includeExposedConfigKeys?: boolean;
}
/**

View file

@ -6,156 +6,185 @@
* Side Public License, v 1.
*/
import { ExposedToBrowserDescriptor } from './types';
import type { PluginConfigDescriptor } from './types';
import { createBrowserConfig } from './create_browser_config';
import { schema, TypeOf } from '@kbn/config-schema';
describe('createBrowserConfig', () => {
it('picks nothing by default', () => {
const configSchema = schema.object({
notExposed1: schema.string(),
nested: schema.object({
notExposed2: schema.boolean(),
notExposed3: schema.maybe(schema.number()),
}),
});
const config = {
foo: 'bar',
notExposed1: '1',
nested: {
str: 'string',
num: 42,
notExposed2: true,
notExposed3: 3,
},
};
const descriptor: ExposedToBrowserDescriptor<typeof config> = {};
const descriptor: PluginConfigDescriptor<TypeOf<typeof configSchema>> = {
schema: configSchema,
};
const browserConfig = createBrowserConfig(config, descriptor);
expect(browserConfig).toEqual({});
const result = createBrowserConfig(config, descriptor);
expect(result).toEqual({ browserConfig: {}, exposedConfigKeys: {} });
});
it('picks all the nested properties when using `true`', () => {
const configSchema = schema.object({
exposed1: schema.string(),
nested: schema.object({
exposed2: schema.boolean(),
exposed3: schema.maybe(schema.number()),
}),
notExposed4: schema.string(),
});
const config = {
foo: 'bar',
exposed1: '1',
nested: {
str: 'string',
num: 42,
exposed2: true,
exposed3: 3,
},
notExposed4: '4',
};
const descriptor: PluginConfigDescriptor<TypeOf<typeof configSchema>> = {
schema: configSchema,
exposeToBrowser: {
exposed1: true,
nested: true,
},
};
const descriptor: ExposedToBrowserDescriptor<typeof config> = {
foo: true,
nested: true,
};
const browserConfig = createBrowserConfig(config, descriptor);
expect(browserConfig).toEqual({
foo: 'bar',
nested: {
str: 'string',
num: 42,
const result = createBrowserConfig(config, descriptor);
expect(result).toEqual({
browserConfig: {
exposed1: '1',
nested: { exposed2: true, exposed3: 3 },
// notExposed4 is not present
},
exposedConfigKeys: {
exposed1: 'string',
'nested.exposed2': 'boolean',
'nested.exposed3': 'number',
// notExposed4 is not present
},
});
});
it('picks specific nested properties when using a nested declaration', () => {
it('picks specific nested properties, omitting those which are not specified', () => {
const configSchema = schema.object({
exposed1: schema.string(),
nested: schema.object({
exposed2: schema.boolean(),
notExposed3: schema.maybe(schema.number()),
}),
notExposed4: schema.string(),
});
const config = {
foo: 'bar',
exposed1: '1',
nested: {
str: 'string',
num: 42,
exposed2: true,
notExposed3: 3,
},
notExposed4: '4',
};
const descriptor: PluginConfigDescriptor<TypeOf<typeof configSchema>> = {
schema: configSchema,
exposeToBrowser: {
exposed1: true,
nested: { exposed2: true },
},
};
const descriptor: ExposedToBrowserDescriptor<typeof config> = {
foo: true,
nested: {
str: true,
num: false,
const result = createBrowserConfig(config, descriptor);
expect(result).toEqual({
browserConfig: {
exposed1: '1',
nested: { exposed2: true },
// notExposed3 and notExposed4 are not present
},
};
const browserConfig = createBrowserConfig(config, descriptor);
expect(browserConfig).toEqual({
foo: 'bar',
nested: {
str: 'string',
exposedConfigKeys: {
exposed1: 'string',
'nested.exposed2': 'boolean',
// notExposed3 and notExposed4 are not present
},
});
});
it('accepts deeply nested structures', () => {
const config = {
foo: 'bar',
deeply: {
str: 'string',
nested: {
hello: 'dolly',
structure: {
propA: 'propA',
propB: 'propB',
},
},
},
};
const descriptor: ExposedToBrowserDescriptor<typeof config> = {
foo: false,
deeply: {
str: false,
nested: {
hello: true,
structure: {
propA: true,
propB: false,
},
},
},
};
const browserConfig = createBrowserConfig(config, descriptor);
expect(browserConfig).toEqual({
deeply: {
nested: {
hello: 'dolly',
structure: {
propA: 'propA',
},
},
},
it('picks specific deeply nested properties, omitting those which are not specified', () => {
const configSchema = schema.object({
exposed1: schema.string(),
deeply: schema.object({
exposed2: schema.boolean(),
nested: schema.object({
exposed3: schema.maybe(schema.number()),
structure: schema.object({
exposed4: schema.string(),
notExposed5: schema.string(),
}),
notExposed6: schema.string(),
}),
notExposed7: schema.string(),
}),
notExposed8: schema.string(),
});
});
it('only includes leaf properties that are `true` when in nested structures', () => {
const config = {
foo: 'bar',
exposed1: '1',
deeply: {
str: 'string',
exposed2: true,
nested: {
hello: 'dolly',
exposed3: 3,
structure: {
propA: 'propA',
propB: 'propB',
exposed4: '4',
notExposed5: '5',
},
notExposed6: '6',
},
notExposed7: '7',
},
notExposed8: '8',
};
const descriptor: PluginConfigDescriptor<TypeOf<typeof configSchema>> = {
schema: configSchema,
exposeToBrowser: {
exposed1: true,
deeply: {
exposed2: true,
nested: {
exposed3: true,
structure: {
exposed4: true,
},
},
},
},
};
const descriptor: ExposedToBrowserDescriptor<typeof config> = {
deeply: {
nested: {
hello: true,
structure: {
propA: true,
const result = createBrowserConfig(config, descriptor);
expect(result).toEqual({
browserConfig: {
exposed1: '1',
deeply: {
exposed2: true,
nested: {
exposed3: 3,
structure: {
exposed4: '4',
},
},
},
// notExposed5, notExposed6, notExposed7, and notExposed8 are not present
},
};
const browserConfig = createBrowserConfig(config, descriptor);
expect(browserConfig).toEqual({
deeply: {
nested: {
hello: 'dolly',
structure: {
propA: 'propA',
},
},
exposedConfigKeys: {
exposed1: 'string',
'deeply.exposed2': 'boolean',
'deeply.nested.exposed3': 'number',
'deeply.nested.structure.exposed4': 'string',
// notExposed5, notExposed6, notExposed7, and notExposed8 are not present
},
});
});

View file

@ -6,19 +6,25 @@
* Side Public License, v 1.
*/
import { ExposedToBrowserDescriptor } from './types';
import { ExposedToBrowserDescriptor, PluginConfigDescriptor } from './types';
export const createBrowserConfig = <T = unknown>(
config: T,
descriptor: ExposedToBrowserDescriptor<T>
): unknown => {
return recursiveCreateConfig(config, descriptor);
descriptor: PluginConfigDescriptor<T>
) => {
if (!descriptor.exposeToBrowser) {
return { browserConfig: {}, exposedConfigKeys: {} };
}
return {
browserConfig: recursiveCreateConfig(config, descriptor.exposeToBrowser),
exposedConfigKeys: getExposedConfigKeys(descriptor),
};
};
const recursiveCreateConfig = <T = unknown>(
config: T,
descriptor: ExposedToBrowserDescriptor<T> = {}
): unknown => {
) => {
return Object.entries(config || {}).reduce((browserConfig, [key, value]) => {
const exposedConfig = descriptor[key as keyof ExposedToBrowserDescriptor<T>];
if (exposedConfig && typeof exposedConfig === 'object') {
@ -30,3 +36,39 @@ const recursiveCreateConfig = <T = unknown>(
return browserConfig;
}, {} as Record<string, unknown>);
};
/**
* Given a plugin descriptor, this function returns an object that contains a flattened list of exposed config keys. This is used for a CI
* check to ensure that consumers don't accidentally expose more config settings to the browser than intended.
*/
function getExposedConfigKeys<T = unknown>(descriptor: PluginConfigDescriptor<T>) {
const schemaStructure = descriptor.schema.getSchemaStructure();
const flattenedConfigSchema: Record<string, string> = {};
for (const { path, type } of schemaStructure) {
if (checkIsPathExposed(path, descriptor.exposeToBrowser!)) {
flattenedConfigSchema[path.join('.')] = type;
}
}
return flattenedConfigSchema;
}
function checkIsPathExposed<T = unknown>(
path: string[],
descriptor: ExposedToBrowserDescriptor<T>
) {
let isExposed = false;
for (const key of path) {
// Traverse the path to see if it is exposed or not
const exposedConfig = descriptor[key as keyof ExposedToBrowserDescriptor<T>];
if (exposedConfig && typeof exposedConfig === 'object') {
// @ts-expect-error Type 'undefined' is not assignable to type 'ExposedToBrowserDescriptor<T>'
descriptor = exposedConfig;
continue;
}
if (exposedConfig === true) {
isExposed = true;
}
break;
}
return isExposed;
}

View file

@ -1010,14 +1010,16 @@ describe('PluginsService', () => {
const prebootUIConfig$ = preboot.uiPlugins.browserConfigs.get('plugin-with-expose-preboot')!;
await expect(prebootUIConfig$.pipe(take(1)).toPromise()).resolves.toEqual({
sharedProp: 'sharedProp default value plugin-with-expose-preboot',
browserConfig: { sharedProp: 'sharedProp default value plugin-with-expose-preboot' },
exposedConfigKeys: { sharedProp: 'string' },
});
const standardUIConfig$ = standard.uiPlugins.browserConfigs.get(
'plugin-with-expose-standard'
)!;
await expect(standardUIConfig$.pipe(take(1)).toPromise()).resolves.toEqual({
sharedProp: 'sharedProp default value plugin-with-expose-standard',
browserConfig: { sharedProp: 'sharedProp default value plugin-with-expose-standard' },
exposedConfigKeys: { sharedProp: 'string' },
});
});

View file

@ -231,9 +231,7 @@ export class PluginsService implements CoreService<PluginsServiceSetup, PluginsS
pluginId,
this.configService
.atPath(plugin.configPath)
.pipe(
map((config: any) => createBrowserConfig(config, configDescriptor.exposeToBrowser!))
),
.pipe(map((config: any) => createBrowserConfig(config, configDescriptor))),
];
})
);

View file

@ -79,7 +79,7 @@ export class RenderingService {
{ http, uiPlugins, status }: RenderOptions,
request: KibanaRequest,
uiSettings: IUiSettingsClient,
{ isAnonymousPage = false, vars }: IRenderOptions = {}
{ isAnonymousPage = false, vars, includeExposedConfigKeys }: IRenderOptions = {}
) {
const env = {
mode: this.coreContext.env.mode,
@ -130,11 +130,15 @@ export class RenderingService {
externalUrl: http.externalUrl,
vars: vars ?? {},
uiPlugins: await Promise.all(
filteredPlugins.map(async ([id, plugin]) => ({
id,
plugin,
config: await getUiConfig(uiPlugins, id),
}))
filteredPlugins.map(async ([id, plugin]) => {
const { browserConfig, exposedConfigKeys } = await getUiConfig(uiPlugins, id);
return {
id,
plugin,
config: browserConfig,
...(includeExposedConfigKeys && { exposedConfigKeys }),
};
})
),
legacyMetadata: {
uiSettings: settings,
@ -150,5 +154,8 @@ export class RenderingService {
const getUiConfig = async (uiPlugins: UiPlugins, pluginId: string) => {
const browserConfig = uiPlugins.browserConfigs.get(pluginId);
return ((await browserConfig?.pipe(take(1)).toPromise()) ?? {}) as Record<string, any>;
return ((await browserConfig?.pipe(take(1)).toPromise()) ?? {
browserConfig: {},
exposedConfigKeys: {},
}) as { browserConfig: Record<string, unknown>; exposedConfigKeys: Record<string, string> };
};

View file

@ -85,6 +85,12 @@ export interface IRenderOptions {
* @internal
*/
vars?: Record<string, any>;
/**
* @internal
* This is only used for integration tests that allow us to verify which config keys are exposed to the browser.
*/
includeExposedConfigKeys?: boolean;
}
/** @internal */

View file

@ -1051,6 +1051,8 @@ export interface HttpResources {
// @public
export interface HttpResourcesRenderOptions {
headers?: ResponseHeaders;
// @internal
includeExposedConfigKeys?: boolean;
}
// @public
@ -1227,6 +1229,8 @@ export interface IntervalHistogram {
// @public (undocumented)
export interface IRenderOptions {
// @internal
includeExposedConfigKeys?: boolean;
isAnonymousPage?: boolean;
// @internal @deprecated
vars?: Record<string, any>;

View file

@ -22,7 +22,7 @@ export default function ({ getService }: FtrProviderContext) {
expect(resp.body).to.be.an('array');
expect(resp.body.length).to.be(13);
expect(resp.body.length).to.be(34);
// Test for sample data card
expect(resp.body.findIndex((c: { id: string }) => c.id === 'sample_data_all')).to.be.above(

View file

@ -71,13 +71,12 @@ export class TestUser extends FtrService {
export async function createTestUserService(ctx: FtrProviderContext, role: Role, user: User) {
const log = ctx.getService('log');
const config = ctx.getService('config');
const kibanaServer = ctx.getService('kibanaServer');
const enabledPlugins = config.get('security.disableTestUser')
? []
: await kibanaServer.plugins.getEnabledIds();
const enabled = enabledPlugins.includes('security') && !config.get('security.disableTestUser');
const enabled =
!config
.get('esTestCluster.serverArgs')
.some((arg: string) => arg === 'xpack.security.enabled=false') &&
!config.get('security.disableTestUser');
if (enabled) {
log.debug('===============creating roles and users===============');

View file

@ -46,10 +46,7 @@ export default async function ({ readConfigFile }) {
'--xpack.maps.showMapVisualizationTypes=true',
// to be re-enabled once kibana/issues/102552 is completed
'--xpack.security.enabled=false',
'--monitoring.enabled=false',
'--xpack.reporting.enabled=false',
'--enterpriseSearch.enabled=false',
],
},

View file

@ -31,9 +31,9 @@ export class RenderingPlugin implements Plugin {
const { isAnonymousPage } = req.query;
if (isAnonymousPage) {
return res.renderAnonymousCoreApp();
return res.renderAnonymousCoreApp({ includeExposedConfigKeys: true });
}
return res.renderCoreApp();
return res.renderCoreApp({ includeExposedConfigKeys: true });
}
);
}

View file

@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
import _ from 'lodash';
import expect from '@kbn/expect';
import '../../plugins/core_provider_plugin/types';
@ -21,6 +22,9 @@ declare global {
}
}
const EXPOSED_CONFIG_SETTINGS_ERROR =
'Actual config settings exposed to the browser do not match what is expected; this assertion fails if extra settings are present and/or expected settings are missing';
export default function ({ getService }: PluginFunctionalProviderContext) {
const appsMenu = getService('appsMenu');
const browser = getService('browser');
@ -41,6 +45,10 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
});
};
const getInjectedMetadata = () =>
browser.execute(() => {
return JSON.parse(document.querySelector('kbn-injected-metadata')!.getAttribute('data')!);
});
const getUserSettings = () =>
browser.execute(() => {
return JSON.parse(document.querySelector('kbn-injected-metadata')!.getAttribute('data')!)
@ -53,9 +61,201 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
return window.__RENDERING_SESSION__;
});
// Talked to @dover, he aggreed we can skip these tests that are unexpectedly flaky
describe.skip('rendering service', () => {
it('renders "core" application', async () => {
describe('rendering service', () => {
it('exposes plugin config settings to authenticated users', async () => {
await navigateTo('/render/core');
const injectedMetadata = await getInjectedMetadata();
expect(injectedMetadata).to.not.be.empty();
expect(injectedMetadata.uiPlugins).to.not.be.empty();
const actualExposedConfigKeys = [];
for (const { plugin, exposedConfigKeys } of injectedMetadata.uiPlugins) {
const configPath = Array.isArray(plugin.configPath)
? plugin.configPath.join('.')
: plugin.configPath;
for (const [exposedConfigKey, type] of Object.entries(exposedConfigKeys)) {
actualExposedConfigKeys.push(`${configPath}.${exposedConfigKey} (${type})`);
}
}
const expectedExposedConfigKeys = [
// NOTE: each exposed config key has its schema type at the end in "(parentheses)". The schema type comes from Joi; in particular,
// "(any)" can mean a few other data types. This is only intended to be a hint to make it easier for future reviewers to understand
// what types of config settings can be exposed to the browser.
// When plugin owners make a change that exposes additional config values, the changes will be reflected in this test assertion.
// Ensure that your change does not unintentionally expose any sensitive values!
'console.ui.enabled (boolean)',
'dashboard.allowByValueEmbeddables (boolean)',
'data.autocomplete.querySuggestions.enabled (boolean)',
'data.autocomplete.valueSuggestions.enabled (boolean)',
'data.autocomplete.valueSuggestions.terminateAfter (duration)',
'data.autocomplete.valueSuggestions.tiers (array)',
'data.autocomplete.valueSuggestions.timeout (duration)',
'data.search.aggs.shardDelay.enabled (boolean)',
'enterpriseSearch.host (string)',
'home.disableWelcomeScreen (boolean)',
'kibana_legacy.defaultAppId (string)',
'map.emsFileApiUrl (string)',
'map.emsFontLibraryUrl (string)',
'map.emsLandingPageUrl (string)',
'map.emsTileApiUrl (string)',
'map.emsTileLayerId.bright (string)',
'map.emsTileLayerId.dark (string)',
'map.emsTileLayerId.desaturated (string)',
'map.emsUrl (any)',
'map.manifestServiceUrl (string)',
'map.proxyElasticMapsServiceInMaps (boolean)',
'map.regionmap.includeElasticMapsService (boolean)',
'map.regionmap.layers (array)',
'map.includeElasticMapsService (boolean)',
'map.tilemap.options.attribution (string)',
'map.tilemap.options.bounds (array)',
'map.tilemap.options.default (boolean)',
'map.tilemap.options.errorTileUrl (string)',
'map.tilemap.options.maxZoom (number)',
'map.tilemap.options.minZoom (number)',
'map.tilemap.options.reuseTiles (boolean)',
'map.tilemap.options.subdomains (array)',
'map.tilemap.options.tileSize (number)',
'map.tilemap.options.tms (boolean)',
'map.tilemap.url (string)',
'monitoring.enabled (boolean)',
'monitoring.kibana.collection.enabled (boolean)',
'monitoring.kibana.collection.interval (number)',
'monitoring.ui.ccs.enabled (boolean)',
'monitoring.ui.container.apm.enabled (boolean)',
'monitoring.ui.container.elasticsearch.enabled (boolean)',
'monitoring.ui.container.logstash.enabled (boolean)',
'monitoring.ui.enabled (boolean)',
'monitoring.ui.min_interval_seconds (number)',
'monitoring.ui.show_license_expiration (boolean)',
'newsfeed.fetchInterval (duration)',
'newsfeed.mainInterval (duration)',
'newsfeed.service.pathTemplate (string)',
'newsfeed.service.urlRoot (any)',
'telemetry.allowChangingOptInStatus (boolean)',
'telemetry.banner (boolean)',
'telemetry.enabled (boolean)',
'telemetry.optIn (any)',
'telemetry.sendUsageFrom (alternatives)',
'telemetry.sendUsageTo (any)',
'usageCollection.uiCounters.debug (boolean)',
'usageCollection.uiCounters.enabled (boolean)',
'vis_type_vega.enableExternalUrls (boolean)',
'xpack.apm.profilingEnabled (boolean)',
'xpack.apm.serviceMapEnabled (boolean)',
'xpack.apm.ui.enabled (boolean)',
'xpack.apm.ui.maxTraceItems (number)',
'xpack.apm.ui.transactionGroupBucketSize (number)',
'xpack.cases.markdownPlugins.lens (boolean)',
'xpack.ccr.ui.enabled (boolean)',
'xpack.cloud.base_url (string)',
'xpack.cloud.cname (string)',
'xpack.cloud.deployment_url (string)',
'xpack.cloud.full_story.enabled (boolean)',
'xpack.cloud.full_story.org_id (any)',
'xpack.cloud.id (string)',
'xpack.cloud.organization_url (string)',
'xpack.cloud.profile_url (string)',
'xpack.data_enhanced.search.sessions.cleanupInterval (duration)',
'xpack.data_enhanced.search.sessions.defaultExpiration (duration)',
'xpack.data_enhanced.search.sessions.enabled (boolean)',
'xpack.data_enhanced.search.sessions.expireInterval (duration)',
'xpack.data_enhanced.search.sessions.management.expiresSoonWarning (duration)',
'xpack.data_enhanced.search.sessions.management.maxSessions (number)',
'xpack.data_enhanced.search.sessions.management.refreshInterval (duration)',
'xpack.data_enhanced.search.sessions.management.refreshTimeout (duration)',
'xpack.data_enhanced.search.sessions.maxUpdateRetries (number)',
'xpack.data_enhanced.search.sessions.monitoringTaskTimeout (duration)',
'xpack.data_enhanced.search.sessions.notTouchedInProgressTimeout (duration)',
'xpack.data_enhanced.search.sessions.notTouchedTimeout (duration)',
'xpack.data_enhanced.search.sessions.pageSize (number)',
'xpack.data_enhanced.search.sessions.trackingInterval (duration)',
'xpack.discoverEnhanced.actions.exploreDataInChart.enabled (boolean)',
'xpack.discoverEnhanced.actions.exploreDataInContextMenu.enabled (boolean)',
'xpack.fleet.agents.enabled (boolean)',
'xpack.global_search.search_timeout (duration)',
'xpack.graph.canEditDrillDownUrls (boolean)',
'xpack.graph.savePolicy (alternatives)',
'xpack.ilm.ui.enabled (boolean)',
'xpack.index_management.ui.enabled (boolean)',
'xpack.license_management.ui.enabled (boolean)',
'xpack.maps.enabled (boolean)',
'xpack.maps.preserveDrawingBuffer (boolean)',
'xpack.maps.showMapsInspectorAdapter (boolean)',
'xpack.maps.showMapVisualizationTypes (boolean)',
'xpack.observability.unsafe.alertingExperience.enabled (boolean)',
'xpack.observability.unsafe.cases.enabled (boolean)',
'xpack.osquery.actionEnabled (boolean)',
'xpack.osquery.enabled (boolean)',
'xpack.osquery.packs (boolean)',
'xpack.osquery.savedQueries (boolean)',
'xpack.remote_clusters.ui.enabled (boolean)',
/**
* NOTE: The Reporting plugin is currently disabled in functional tests (see test/functional/config.js).
* It will be re-enabled once #102552 is completed.
*/
// 'xpack.reporting.roles.allow (array)',
// 'xpack.reporting.roles.enabled (boolean)',
// 'xpack.reporting.poll.jobCompletionNotifier.interval (number)',
// 'xpack.reporting.poll.jobCompletionNotifier.intervalErrorMultiplier (number)',
// 'xpack.reporting.poll.jobsRefresh.interval (number)',
// 'xpack.reporting.poll.jobsRefresh.intervalErrorMultiplier (number)',
'xpack.rollup.ui.enabled (boolean)',
'xpack.saved_object_tagging.cache_refresh_interval (duration)',
'xpack.security.loginAssistanceMessage (string)',
'xpack.security.sameSiteCookies (alternatives)',
'xpack.security.showInsecureClusterWarning (boolean)',
'xpack.securitySolution.enableExperimental (array)',
'xpack.snapshot_restore.slm_ui.enabled (boolean)',
'xpack.snapshot_restore.ui.enabled (boolean)',
'xpack.timelines.enabled (boolean)',
'xpack.trigger_actions_ui.enableGeoTrackingThresholdAlert (boolean)',
'xpack.upgrade_assistant.readonly (boolean)',
'xpack.upgrade_assistant.ui.enabled (boolean)',
];
// We don't assert that actualExposedConfigKeys and expectedExposedConfigKeys are equal, because test failure messages with large
// arrays are hard to grok. Instead, we take the difference between the two arrays and assert them separately, that way it's
// abundantly clear when the test fails that (A) Kibana is exposing a new key, or (B) Kibana is no longer exposing a key.
const extra = _.difference(actualExposedConfigKeys, expectedExposedConfigKeys).sort();
const missing = _.difference(expectedExposedConfigKeys, actualExposedConfigKeys).sort();
expect({ extra, missing }).to.eql({ extra: [], missing: [] }, EXPOSED_CONFIG_SETTINGS_ERROR);
});
it('exposes plugin config settings to unauthenticated users', async () => {
await navigateTo('/render/core?isAnonymousPage=true');
const injectedMetadata = await getInjectedMetadata();
expect(injectedMetadata).to.not.be.empty();
expect(injectedMetadata.uiPlugins).to.not.be.empty();
const actualExposedConfigKeys = [];
for (const { plugin, exposedConfigKeys } of injectedMetadata.uiPlugins) {
const configPath = Array.isArray(plugin.configPath)
? plugin.configPath.join('.')
: plugin.configPath;
for (const [exposedConfigKey, type] of Object.entries(exposedConfigKeys)) {
actualExposedConfigKeys.push(`${configPath}.${exposedConfigKey} (${type})`);
}
}
const expectedExposedConfigKeys = [
// NOTE: each exposed config key has its schema type at the end in "(parentheses)". The schema type comes from Joi; in particular,
// "(any)" can mean a few other data types. This is only intended to be a hint to make it easier for future reviewers to understand
// what types of config settings can be exposed to the browser.
// When plugin owners make a change that exposes additional config values, the changes will be reflected in this test assertion.
// Ensure that your change does not unintentionally expose any sensitive values!
'xpack.security.loginAssistanceMessage (string)',
'xpack.security.sameSiteCookies (alternatives)',
'xpack.security.showInsecureClusterWarning (boolean)',
];
// We don't assert that actualExposedConfigKeys and expectedExposedConfigKeys are equal, because test failure messages with large
// arrays are hard to grok. Instead, we take the difference between the two arrays and assert them separately, that way it's
// abundantly clear when the test fails that (A) Kibana is exposing a new key, or (B) Kibana is no longer exposing a key.
const extra = _.difference(actualExposedConfigKeys, expectedExposedConfigKeys).sort();
const missing = _.difference(expectedExposedConfigKeys, actualExposedConfigKeys).sort();
expect({ extra, missing }).to.eql({ extra: [], missing: [] }, EXPOSED_CONFIG_SETTINGS_ERROR);
});
// FLAKY
it.skip('renders "core" application', async () => {
await navigateTo('/render/core');
const [loadingMessage, userSettings] = await Promise.all([
@ -70,7 +270,8 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
expect(await exists('renderingHeader')).to.be(true);
});
it('renders "core" application without user settings', async () => {
// FLAKY
it.skip('renders "core" application without user settings', async () => {
await navigateTo('/render/core?isAnonymousPage=true');
const [loadingMessage, userSettings] = await Promise.all([
@ -85,7 +286,8 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
expect(await exists('renderingHeader')).to.be(true);
});
it('navigates between standard application and one with custom appRoute', async () => {
// FLAKY
it.skip('navigates between standard application and one with custom appRoute', async () => {
await navigateTo('/');
await find.waitForElementStale(await findLoadingMessage());
@ -108,7 +310,8 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
]);
});
it('navigates between applications with custom appRoutes', async () => {
// FLAKY
it.skip('navigates between applications with custom appRoutes', async () => {
await navigateTo('/');
await find.waitForElementStale(await findLoadingMessage());

View file

@ -242,7 +242,9 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
},
},
})}`,
...disabledPlugins.map((key) => `--xpack.${key}.enabled=false`),
...disabledPlugins
.filter((k) => k !== 'security')
.map((key) => `--xpack.${key}.enabled=false`),
...plugins.map(
(pluginDir) =>
`--plugin-path=${path.resolve(__dirname, 'fixtures', 'plugins', pluginDir)}`

View file

@ -121,7 +121,9 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
`--xpack.actions.allowedHosts=${JSON.stringify(['localhost', 'some.non.existent.com'])}`,
`--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`,
'--xpack.eventLog.logEntries=true',
...disabledPlugins.map((key) => `--xpack.${key}.enabled=false`),
...disabledPlugins
.filter((k) => k !== 'security')
.map((key) => `--xpack.${key}.enabled=false`),
// Actions simulators plugin. Needed for testing push to external services.
...alertingPlugins.map(
(pluginDir) =>

View file

@ -51,7 +51,9 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
...xPackApiIntegrationTestsConfig.get('kbnTestServer'),
serverArgs: [
...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'),
...disabledPlugins.map((key) => `--xpack.${key}.enabled=false`),
...disabledPlugins
.filter((k) => k !== 'security')
.map((key) => `--xpack.${key}.enabled=false`),
`--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'alerts')}`,
`--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'actions')}`,
`--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'task_manager')}`,

View file

@ -24,7 +24,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
},
kbnTestServer: {
...apiConfig.get('kbnTestServer'),
serverArgs: [...apiConfig.get('kbnTestServer.serverArgs'), `--xpack.security.enabled=false`],
serverArgs: [...apiConfig.get('kbnTestServer.serverArgs')],
},
};
}

View file

@ -17,10 +17,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
testFiles: [resolve(__dirname, './reporting_without_security')],
kbnTestServer: {
...reportingConfig.get('kbnTestServer'),
serverArgs: [
...reportingConfig.get('kbnTestServer.serverArgs'),
`--xpack.security.enabled=false`,
],
serverArgs: [...reportingConfig.get('kbnTestServer.serverArgs')],
},
esTestCluster: {
...reportingConfig.get('esTestCluster'),

View file

@ -79,7 +79,9 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
`--xpack.actions.allowedHosts=${JSON.stringify(['localhost', 'some.non.existent.com'])}`,
`--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`,
'--xpack.eventLog.logEntries=true',
...disabledPlugins.map((key) => `--xpack.${key}.enabled=false`),
...disabledPlugins
.filter((k) => k !== 'security')
.map((key) => `--xpack.${key}.enabled=false`),
// TO DO: Remove feature flags once we're good to go
'--xpack.securitySolution.enableExperimental=["ruleRegistryEnabled"]',
'--xpack.ruleRegistry.write.enabled=true',

View file

@ -54,7 +54,9 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
...config.xpack.api.get('kbnTestServer.serverArgs'),
'--server.xsrf.disableProtection=true',
`--plugin-path=${path.join(__dirname, 'fixtures', 'saved_object_test_plugin')}`,
...disabledPlugins.map((key) => `--xpack.${key}.enabled=false`),
...disabledPlugins
.filter((k) => k !== 'security')
.map((key) => `--xpack.${key}.enabled=false`),
],
},
};

View file

@ -61,7 +61,9 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
'--status.allowAnonymous=false',
'--server.xsrf.disableProtection=true',
`--plugin-path=${path.join(__dirname, 'fixtures', 'spaces_test_plugin')}`,
...disabledPlugins.map((key) => `--xpack.${key}.enabled=false`),
...disabledPlugins
.filter((k) => k !== 'security')
.map((key) => `--xpack.${key}.enabled=false`),
],
},
};

View file

@ -81,7 +81,9 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
`--xpack.actions.allowedHosts=${JSON.stringify(['localhost', 'some.non.existent.com'])}`,
`--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`,
'--xpack.eventLog.logEntries=true',
...disabledPlugins.map((key) => `--xpack.${key}.enabled=false`),
...disabledPlugins
.filter((k) => k !== 'security')
.map((key) => `--xpack.${key}.enabled=false`),
// TO DO: Remove feature flags once we're good to go
'--xpack.securitySolution.enableExperimental=["ruleRegistryEnabled"]',
'--xpack.ruleRegistry.write.enabled=true',

View file

@ -42,7 +42,9 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
...xPackFunctionalTestsConfig.get('kbnTestServer'),
serverArgs: [
...xPackFunctionalTestsConfig.get('kbnTestServer.serverArgs'),
...disabledPlugins.map((key) => `--xpack.${key}.enabled=false`),
...disabledPlugins
.filter((k) => k !== 'security')
.map((key) => `--xpack.${key}.enabled=false`),
`--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'foo_plugin')}`,
],
},

View file

@ -55,7 +55,7 @@ export default function catalogueTests({ getService }: FtrProviderContext) {
// only foo is disabled
const expected = mapValues(
uiCapabilities.value!.catalogue,
(value, catalogueId) => catalogueId !== 'foo'
(enabled, catalogueId) => catalogueId !== 'foo'
);
expect(uiCapabilities.value!.catalogue).to.eql(expected);
break;