diff --git a/packages/kbn-config-schema/src/index.ts b/packages/kbn-config-schema/src/index.ts index 8635421beb0a..46ee8e9e3d99 100644 --- a/packages/kbn-config-schema/src/index.ts +++ b/packages/kbn-config-schema/src/index.ts @@ -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'; diff --git a/packages/kbn-config-schema/src/types/index.ts b/packages/kbn-config-schema/src/types/index.ts index 5152137985ff..23b114fd824d 100644 --- a/packages/kbn-config-schema/src/types/index.ts +++ b/packages/kbn-config-schema/src/types/index.ts @@ -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'; diff --git a/packages/kbn-config-schema/src/types/object_type.test.ts b/packages/kbn-config-schema/src/types/object_type.test.ts index 67f0963fefdd..6548e808395b 100644 --- a/packages/kbn-config-schema/src/types/object_type.test.ts +++ b/packages/kbn-config-schema/src/types/object_type.test.ts @@ -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' }, + ]); +}); diff --git a/packages/kbn-config-schema/src/types/string_type.ts b/packages/kbn-config-schema/src/types/string_type.ts index 1442c5b9b4ef..f2792e4031e0 100644 --- a/packages/kbn-config-schema/src/types/string_type.ts +++ b/packages/kbn-config-schema/src/types/string_type.ts @@ -53,6 +53,7 @@ export class StringType extends Type { ); } + schema.type = 'string'; super(schema, options); } diff --git a/packages/kbn-config-schema/src/types/type.ts b/packages/kbn-config-schema/src/types/type.ts index 696101fb2c22..d3bab0106e5c 100644 --- a/packages/kbn-config-schema/src/types/type.ts +++ b/packages/kbn-config-schema/src/types/type.ts @@ -15,6 +15,11 @@ export interface TypeOptions { validate?: (value: T) => string | void; } +export interface SchemaStructureEntry { + path: string[]; + type: string; +} + export const convertValidationFunction = ( validate: (value: T) => string | void ): CustomValidator => { @@ -98,6 +103,10 @@ export abstract class Type { return this.internalSchema; } + public getSchemaStructure() { + return recursiveGetSchemaStructure(this.internalSchema); + } + protected handleError( type: string, context: Record, @@ -141,3 +150,17 @@ export abstract class Type { 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; +} diff --git a/src/core/server/http_resources/http_resources_service.ts b/src/core/server/http_resources/http_resources_service.ts index 25467152ce4f..95610d36d423 100644 --- a/src/core/server/http_resources/http_resources_service.ts +++ b/src/core/server/http_resources/http_resources_service.ts @@ -98,6 +98,7 @@ export class HttpResourcesService implements CoreService { 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 = {}; - - const browserConfig = createBrowserConfig(config, descriptor); + const descriptor: PluginConfigDescriptor> = { + schema: configSchema, + }; - expect(browserConfig).toEqual({}); + const result = createBrowserConfig(config, descriptor); + expect(result).toEqual({ browserConfig: {}, exposedConfigKeys: {} }); }); it('picks all the nested properties when using `true`', () => { - const config = { - foo: 'bar', - nested: { - str: 'string', - num: 42, - }, - }; - - const descriptor: ExposedToBrowserDescriptor = { - foo: true, - nested: true, - }; - - const browserConfig = createBrowserConfig(config, descriptor); - - expect(browserConfig).toEqual({ - foo: 'bar', - nested: { - str: 'string', - num: 42, - }, + const configSchema = schema.object({ + exposed1: schema.string(), + nested: schema.object({ + exposed2: schema.boolean(), + exposed3: schema.maybe(schema.number()), + }), + notExposed4: schema.string(), }); - }); - - it('picks specific nested properties when using a nested declaration', () => { const config = { - foo: 'bar', + exposed1: '1', nested: { - str: 'string', - num: 42, + exposed2: true, + exposed3: 3, }, + notExposed4: '4', }; - - const descriptor: ExposedToBrowserDescriptor = { - foo: true, - nested: { - str: true, - num: false, + const descriptor: PluginConfigDescriptor> = { + schema: configSchema, + exposeToBrowser: { + exposed1: true, + nested: true, }, }; - const browserConfig = createBrowserConfig(config, descriptor); - - expect(browserConfig).toEqual({ - foo: 'bar', - nested: { - str: 'string', + 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('accepts deeply nested structures', () => { + 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', - deeply: { - str: 'string', - nested: { - hello: 'dolly', - structure: { - propA: 'propA', - propB: 'propB', - }, - }, + exposed1: '1', + nested: { + exposed2: true, + notExposed3: 3, }, + notExposed4: '4', }; - - const descriptor: ExposedToBrowserDescriptor = { - foo: false, - deeply: { - str: false, - nested: { - hello: true, - structure: { - propA: true, - propB: false, - }, - }, + const descriptor: PluginConfigDescriptor> = { + schema: configSchema, + exposeToBrowser: { + exposed1: true, + nested: { exposed2: true }, }, }; - const browserConfig = createBrowserConfig(config, descriptor); - - expect(browserConfig).toEqual({ - deeply: { - nested: { - hello: 'dolly', - structure: { - propA: 'propA', - }, - }, + const result = createBrowserConfig(config, descriptor); + expect(result).toEqual({ + browserConfig: { + exposed1: '1', + nested: { exposed2: true }, + // notExposed3 and notExposed4 are not present + }, + exposedConfigKeys: { + exposed1: 'string', + 'nested.exposed2': 'boolean', + // notExposed3 and notExposed4 are not present }, }); }); - it('only includes leaf properties that are `true` when in nested structures', () => { + 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(), + }); 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: ExposedToBrowserDescriptor = { - deeply: { - nested: { - hello: true, - structure: { - propA: true, + const descriptor: PluginConfigDescriptor> = { + schema: configSchema, + exposeToBrowser: { + exposed1: true, + deeply: { + exposed2: true, + nested: { + exposed3: true, + structure: { + exposed4: true, + }, }, }, }, }; - const browserConfig = createBrowserConfig(config, descriptor); - - expect(browserConfig).toEqual({ - deeply: { - nested: { - hello: 'dolly', - structure: { - propA: 'propA', + 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 + }, + exposedConfigKeys: { + exposed1: 'string', + 'deeply.exposed2': 'boolean', + 'deeply.nested.exposed3': 'number', + 'deeply.nested.structure.exposed4': 'string', + // notExposed5, notExposed6, notExposed7, and notExposed8 are not present }, }); }); diff --git a/src/core/server/plugins/create_browser_config.ts b/src/core/server/plugins/create_browser_config.ts index 95c8de7f4c8c..0bf812d2e5cc 100644 --- a/src/core/server/plugins/create_browser_config.ts +++ b/src/core/server/plugins/create_browser_config.ts @@ -6,19 +6,25 @@ * Side Public License, v 1. */ -import { ExposedToBrowserDescriptor } from './types'; +import { ExposedToBrowserDescriptor, PluginConfigDescriptor } from './types'; export const createBrowserConfig = ( config: T, - descriptor: ExposedToBrowserDescriptor -): unknown => { - return recursiveCreateConfig(config, descriptor); + descriptor: PluginConfigDescriptor +) => { + if (!descriptor.exposeToBrowser) { + return { browserConfig: {}, exposedConfigKeys: {} }; + } + return { + browserConfig: recursiveCreateConfig(config, descriptor.exposeToBrowser), + exposedConfigKeys: getExposedConfigKeys(descriptor), + }; }; const recursiveCreateConfig = ( config: T, descriptor: ExposedToBrowserDescriptor = {} -): unknown => { +) => { return Object.entries(config || {}).reduce((browserConfig, [key, value]) => { const exposedConfig = descriptor[key as keyof ExposedToBrowserDescriptor]; if (exposedConfig && typeof exposedConfig === 'object') { @@ -30,3 +36,39 @@ const recursiveCreateConfig = ( return browserConfig; }, {} as Record); }; + +/** + * 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(descriptor: PluginConfigDescriptor) { + const schemaStructure = descriptor.schema.getSchemaStructure(); + const flattenedConfigSchema: Record = {}; + for (const { path, type } of schemaStructure) { + if (checkIsPathExposed(path, descriptor.exposeToBrowser!)) { + flattenedConfigSchema[path.join('.')] = type; + } + } + return flattenedConfigSchema; +} + +function checkIsPathExposed( + path: string[], + descriptor: ExposedToBrowserDescriptor +) { + 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]; + if (exposedConfig && typeof exposedConfig === 'object') { + // @ts-expect-error Type 'undefined' is not assignable to type 'ExposedToBrowserDescriptor' + descriptor = exposedConfig; + continue; + } + if (exposedConfig === true) { + isExposed = true; + } + break; + } + return isExposed; +} diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index d45e7f9bf0bd..4d7922ae1dc4 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -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' }, }); }); diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index 85bb5acc5bf8..022be8e19431 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -231,9 +231,7 @@ export class PluginsService implements CoreService createBrowserConfig(config, configDescriptor.exposeToBrowser!)) - ), + .pipe(map((config: any) => createBrowserConfig(config, configDescriptor))), ]; }) ); diff --git a/src/core/server/rendering/rendering_service.tsx b/src/core/server/rendering/rendering_service.tsx index ea204d120655..5cfb66562bd2 100644 --- a/src/core/server/rendering/rendering_service.tsx +++ b/src/core/server/rendering/rendering_service.tsx @@ -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; + return ((await browserConfig?.pipe(take(1)).toPromise()) ?? { + browserConfig: {}, + exposedConfigKeys: {}, + }) as { browserConfig: Record; exposedConfigKeys: Record }; }; diff --git a/src/core/server/rendering/types.ts b/src/core/server/rendering/types.ts index 5d2ba371217e..0d417b93b5bb 100644 --- a/src/core/server/rendering/types.ts +++ b/src/core/server/rendering/types.ts @@ -85,6 +85,12 @@ export interface IRenderOptions { * @internal */ vars?: Record; + + /** + * @internal + * This is only used for integration tests that allow us to verify which config keys are exposed to the browser. + */ + includeExposedConfigKeys?: boolean; } /** @internal */ diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index b7039cb42b43..a8a3d14671a8 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -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; diff --git a/test/api_integration/apis/custom_integration/integrations.ts b/test/api_integration/apis/custom_integration/integrations.ts index 08217119bf53..036eb2ef33c7 100644 --- a/test/api_integration/apis/custom_integration/integrations.ts +++ b/test/api_integration/apis/custom_integration/integrations.ts @@ -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( diff --git a/test/common/services/security/test_user.ts b/test/common/services/security/test_user.ts index 695294f08b02..1161e7b493f4 100644 --- a/test/common/services/security/test_user.ts +++ b/test/common/services/security/test_user.ts @@ -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==============='); diff --git a/test/functional/config.js b/test/functional/config.js index 9bad5babd05e..b8e3cb697663 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -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', ], }, diff --git a/test/plugin_functional/plugins/rendering_plugin/server/plugin.ts b/test/plugin_functional/plugins/rendering_plugin/server/plugin.ts index c1c929b0e2ce..d2e4d4386cbf 100644 --- a/test/plugin_functional/plugins/rendering_plugin/server/plugin.ts +++ b/test/plugin_functional/plugins/rendering_plugin/server/plugin.ts @@ -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 }); } ); } diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index 4752f77f6cda..94c20a6aa417 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -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()); diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 3fe5ecb6076e..7bcec7b11c10 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -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)}` diff --git a/x-pack/test/case_api_integration/common/config.ts b/x-pack/test/case_api_integration/common/config.ts index 514b54982ee4..32bd089a39e7 100644 --- a/x-pack/test/case_api_integration/common/config.ts +++ b/x-pack/test/case_api_integration/common/config.ts @@ -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) => diff --git a/x-pack/test/lists_api_integration/common/config.ts b/x-pack/test/lists_api_integration/common/config.ts index 4983f00cce04..214f03b63265 100644 --- a/x-pack/test/lists_api_integration/common/config.ts +++ b/x-pack/test/lists_api_integration/common/config.ts @@ -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')}`, diff --git a/x-pack/test/reporting_api_integration/reporting_without_security.config.ts b/x-pack/test/reporting_api_integration/reporting_without_security.config.ts index dfd79916b5ce..0779b3b871e3 100644 --- a/x-pack/test/reporting_api_integration/reporting_without_security.config.ts +++ b/x-pack/test/reporting_api_integration/reporting_without_security.config.ts @@ -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')], }, }; } diff --git a/x-pack/test/reporting_functional/reporting_without_security.config.ts b/x-pack/test/reporting_functional/reporting_without_security.config.ts index 0269f57bf08c..7ca7f89a0b70 100644 --- a/x-pack/test/reporting_functional/reporting_without_security.config.ts +++ b/x-pack/test/reporting_functional/reporting_without_security.config.ts @@ -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'), diff --git a/x-pack/test/rule_registry/common/config.ts b/x-pack/test/rule_registry/common/config.ts index 487af84141d2..8e672a596bf7 100644 --- a/x-pack/test/rule_registry/common/config.ts +++ b/x-pack/test/rule_registry/common/config.ts @@ -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', diff --git a/x-pack/test/saved_object_api_integration/common/config.ts b/x-pack/test/saved_object_api_integration/common/config.ts index 9b9e46efa6d5..8ca74c7fcea4 100644 --- a/x-pack/test/saved_object_api_integration/common/config.ts +++ b/x-pack/test/saved_object_api_integration/common/config.ts @@ -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`), ], }, }; diff --git a/x-pack/test/spaces_api_integration/common/config.ts b/x-pack/test/spaces_api_integration/common/config.ts index 7cceb945790d..5d135cd05605 100644 --- a/x-pack/test/spaces_api_integration/common/config.ts +++ b/x-pack/test/spaces_api_integration/common/config.ts @@ -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`), ], }, }; diff --git a/x-pack/test/timeline/common/config.ts b/x-pack/test/timeline/common/config.ts index 011b3044022b..75f0eb24a6cd 100644 --- a/x-pack/test/timeline/common/config.ts +++ b/x-pack/test/timeline/common/config.ts @@ -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', diff --git a/x-pack/test/ui_capabilities/common/config.ts b/x-pack/test/ui_capabilities/common/config.ts index 1f695e562da0..f676a5eeccee 100644 --- a/x-pack/test/ui_capabilities/common/config.ts +++ b/x-pack/test/ui_capabilities/common/config.ts @@ -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')}`, ], }, diff --git a/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts b/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts index b50f11553747..8d334574d23f 100644 --- a/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts +++ b/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts @@ -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;