diff --git a/docs/development/core/server/kibana-plugin-server.logger.get.md b/docs/development/core/server/kibana-plugin-server.logger.get.md new file mode 100644 index 0000000000000..b4a2d8a124260 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.logger.get.md @@ -0,0 +1,33 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Logger](./kibana-plugin-server.logger.md) > [get](./kibana-plugin-server.logger.get.md) + +## Logger.get() method + +Returns a new [Logger](./kibana-plugin-server.logger.md) instance extending the current logger context. + +Signature: + +```typescript +get(...childContextPaths: string[]): Logger; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| childContextPaths | string[] | | + +Returns: + +`Logger` + +## Example + + +```typescript +const logger = loggerFactory.get('plugin', 'service'); // 'plugin.service' context +const subLogger = logger.get('feature'); // 'plugin.service.feature' context + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index ea5ca6502b076..cdd709375aa51 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -90,7 +90,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PluginManifest](./kibana-plugin-server.pluginmanifest.md) | Describes the set of required and optional properties plugin can define in its mandatory JSON manifest file. | | [PluginsServiceSetup](./kibana-plugin-server.pluginsservicesetup.md) | | | [PluginsServiceStart](./kibana-plugin-server.pluginsservicestart.md) | | -| [RequestHandlerContext](./kibana-plugin-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.Provides the following clients: - [savedObjects.client](./kibana-plugin-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [elasticsearch.dataClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request | +| [RequestHandlerContext](./kibana-plugin-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.Provides the following clients: - [savedObjects.client](./kibana-plugin-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [elasticsearch.dataClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request | | [RouteConfig](./kibana-plugin-server.routeconfig.md) | Route specific configuration. | | [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md) | Additional route options. | | [RouteConfigOptionsBody](./kibana-plugin-server.routeconfigoptionsbody.md) | Additional body options for a route | diff --git a/docs/development/core/server/kibana-plugin-server.requesthandlercontext.md b/docs/development/core/server/kibana-plugin-server.requesthandlercontext.md index c9fc80596efa9..d9b781e1e550e 100644 --- a/docs/development/core/server/kibana-plugin-server.requesthandlercontext.md +++ b/docs/development/core/server/kibana-plugin-server.requesthandlercontext.md @@ -6,7 +6,7 @@ Plugin specific context passed to a route handler. -Provides the following clients: - [savedObjects.client](./kibana-plugin-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [elasticsearch.dataClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request +Provides the following clients: - [savedObjects.client](./kibana-plugin-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [elasticsearch.dataClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request Signature: diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 5bb22579d123e..1c78de966c46f 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -46,6 +46,8 @@ - [How to](#how-to) - [Configure plugin](#configure-plugin) - [Handle plugin configuration deprecations](#handle-plugin-config-deprecations) + - [Use scoped services](#use-scoped-services) + - [Declare a custom scoped service](#declare-a-custom-scoped-service) - [Mock new platform services in tests](#mock-new-platform-services-in-tests) - [Writing mocks for your plugin](#writing-mocks-for-your-plugin) - [Using mocks in your tests](#using-mocks-in-your-tests) @@ -1190,22 +1192,23 @@ In server code, `core` can be accessed from either `server.newPlatform` or `kbnS | `server.config()` | [`initializerContext.config.create()`](/docs/development/core/server/kibana-plugin-server.plugininitializercontext.config.md) | Must also define schema. See _[how to configure plugin](#configure-plugin)_ | | `server.route` | [`core.http.createRouter`](/docs/development/core/server/kibana-plugin-server.httpservicesetup.createrouter.md) | [Examples](./MIGRATION_EXAMPLES.md#route-registration) | | `request.getBasePath()` | [`core.http.basePath.get`](/docs/development/core/server/kibana-plugin-server.httpservicesetup.basepath.md) | | -| `server.plugins.elasticsearch.getCluster('data')` | [`core.elasticsearch.dataClient$`](/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.dataclient_.md) | Handlers will also include a pre-configured client | -| `server.plugins.elasticsearch.getCluster('admin')` | [`core.elasticsearch.adminClient$`](/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.adminclient_.md) | Handlers will also include a pre-configured client | -| `xpackMainPlugin.info.feature(pluginID).registerLicenseCheckResultsGenerator` | [`x-pack licensing plugin`](/x-pack/plugins/licensing/README.md) | | +| `server.plugins.elasticsearch.getCluster('data')` | [`context.elasticsearch.dataClient`](/docs/development/core/server/kibana-plugin-server.iscopedclusterclient.md) | | +| `server.plugins.elasticsearch.getCluster('admin')` | [`context.elasticsearch.adminClient`](/docs/development/core/server/kibana-plugin-server.iscopedclusterclient.md) | | | `server.savedObjects.setScopedSavedObjectsClientFactory` | [`core.savedObjects.setClientFactory`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.setclientfactory.md) | | | `server.savedObjects.addScopedSavedObjectsClientWrapperFactory` | [`core.savedObjects.addClientWrapper`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.addclientwrapper.md) | | | `server.savedObjects.getSavedObjectsRepository` | [`core.savedObjects.createInternalRepository`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.createinternalrepository.md) [`core.savedObjects.createScopedRepository`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.createscopedrepository.md) | | | `server.savedObjects.getScopedSavedObjectsClient` | [`core.savedObjects.getScopedClient`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicestart.getscopedclient.md) | | | `request.getSavedObjectsClient` | [`context.core.savedObjects.client`](/docs/development/core/server/kibana-plugin-server.requesthandlercontext.core.md) | | +| `request.getUiSettingsService` | [`context.uiSettings.client`](/docs/development/core/server/kibana-plugin-server.iuisettingsclient.md) | | | `kibana.Plugin.deprecations` | [Handle plugin configuration deprecations](#handle-plugin-config-deprecations) and [`PluginConfigDescriptor.deprecations`](docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md) | Deprecations from New Platform are not applied to legacy configuration | _See also: [Server's CoreSetup API Docs](/docs/development/core/server/kibana-plugin-server.coresetup.md)_ ##### Plugin services -| Legacy Platform | New Platform | Notes | -| ------------------------------------------- | ------------------------------------------------------------------------------ | ----- | -| `server.plugins.xpack_main.registerFeature` | [`plugins.features.registerFeature`](x-pack/plugins/features/server/plugin.ts) | | +| Legacy Platform | New Platform | Notes | +| ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | ----- | +| `server.plugins.xpack_main.registerFeature` | [`plugins.features.registerFeature`](x-pack/plugins/features/server/plugin.ts) | | +| `server.plugins.xpack_main.feature(pluginID).registerLicenseCheckResultsGenerator` | [`x-pack licensing plugin`](/x-pack/plugins/licensing/README.md) | | #### UI Exports @@ -1399,7 +1402,7 @@ export const config: PluginConfigDescriptor = { deprecations: ({ rename, unused }) => [ rename('oldProperty', 'newProperty'), unused('someUnusedProperty'), - ] + ] }; ``` @@ -1413,7 +1416,7 @@ export const config: PluginConfigDescriptor = { deprecations: ({ renameFromRoot, unusedFromRoot }) => [ renameFromRoot('oldplugin.property', 'myplugin.property'), unusedFromRoot('oldplugin.deprecated'), - ] + ] }; ``` @@ -1421,6 +1424,68 @@ Note that deprecations registered in new platform's plugins are not applied to t During migration, if you still need the deprecations to be effective in the legacy plugin, you need to declare them in both plugin definitions. +### Use scoped services +Whenever Kibana needs to get access to data saved in elasticsearch, it should perform a check whether an end-user has access to the data. +In the legacy platform, Kibana requires to bind elasticsearch related API with an incoming request to access elasticsearch service on behalf of a user. +```js + async function handler(req, res) { + const dataCluster = server.plugins.elasticsearch.getCluster('data'); + const data = await dataCluster.callWithRequest(req, 'ping'); + } +``` + +The new platform introduced [a handler interface](/rfcs/text/0003_handler_interface.md) on the server-side to perform that association internally. Core services, that require impersonation with an incoming request, are +exposed via `context` argument of [the request handler interface.](/docs/development/core/server/kibana-plugin-server.requesthandler.md) +The above example looks in the new platform as +```js + async function handler(context, req, res) { + const data = await context.core.elasticsearch.adminClient.callAsInternalUser('ping') + } +``` + +The [request handler context](/docs/development/core/server/kibana-plugin-server.requesthandlercontext.md) exposed the next scoped **core** services: +| Legacy Platform | New Platform | +| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------| +| `request.getSavedObjectsClient` | [`context.savedObjects.client`](/docs/development/core/server/kibana-plugin-server.savedobjectsclient.md) | +| `server.plugins.elasticsearch.getCluster('admin')` | [`context.elasticsearch.adminClient`](/docs/development/core/server/kibana-plugin-server.iscopedclusterclient.md) | +| `server.plugins.elasticsearch.getCluster('data')` | [`context.elasticsearch.dataClient`](/docs/development/core/server/kibana-plugin-server.iscopedclusterclient.md) | +| `request.getUiSettingsService` | [`context.uiSettings.client`](/docs/development/core/server/kibana-plugin-server.iuisettingsclient.md) | + +#### Declare a custom scoped service +Plugins can extend the handler context with custom API that will be available to the plugin itself and all dependent plugins. +For example, the plugin creates a custom elasticsearch client and want to use it via the request handler context: + +```ts +import { CoreSetup, IScopedClusterClient } from 'kibana/server'; + +export interface MyPluginContext { + client: IScopedClusterClient; +} + +// extend RequestHandlerContext when a dependent plugin imports MyPluginContext from the file +declare module 'src/core/server' { + interface RequestHandlerContext { + myPlugin?: MyPluginContext; + } +} + +class Plugin { + setup(core: CoreSetup) { + const client = core.elasticsearch.createClient('myClient'); + core.http.registerRouteHandlerContext('myPlugin', (context, req, res) => { + return { client: client.asScoped(req) }; + }); + + router.get( + { path: '/api/my-plugin/', validate }, + async (context, req, res) => { + const data = await context.myPlugin.client.callAsCurrentUser('endpoint'); + ... + } + ); + } +``` + ### Mock new platform services in tests #### Writing mocks for your plugin diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 2aaa8306e871f..ba930d46e0865 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -234,6 +234,8 @@ export { LegacyServiceSetupDeps, LegacyServiceStartDeps } from './legacy'; * data client which uses the credentials of the incoming request * - {@link ScopedClusterClient | elasticsearch.adminClient} - Elasticsearch * admin client which uses the credentials of the incoming request + * - {@link IUiSettingsClient | uiSettings.client} - uiSettings client + * which uses the credentials of the incoming request * * @public */ diff --git a/test/plugin_functional/config.js b/test/plugin_functional/config.js index 172dec9a1d111..87026ce25d9aa 100644 --- a/test/plugin_functional/config.js +++ b/test/plugin_functional/config.js @@ -57,11 +57,12 @@ export default async function({ readConfigFile }) { ...functionalConfig.get('kbnTestServer'), serverArgs: [ ...functionalConfig.get('kbnTestServer.serverArgs'), + + // Required to load new platform plugins via `--plugin-path` flag. + '--env.name=development', ...plugins.map( pluginDir => `--plugin-path=${path.resolve(__dirname, 'plugins', pluginDir)}` ), - // Required to load new platform plugins via `--plugin-path` flag. - '--env.name=development', ], }, }; diff --git a/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx b/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx index 5c8e1d03d5a4a..bda1557bdaf91 100644 --- a/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx +++ b/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx @@ -22,9 +22,6 @@ import { CorePluginAPluginSetup } from '../../core_plugin_a/public/plugin'; declare global { interface Window { - corePluginB?: string; - hasAccessToInjectedMetadata?: boolean; - receivedStartServices?: boolean; env?: PluginInitializerContext['env']; } } @@ -39,12 +36,6 @@ export class CorePluginBPlugin window.env = pluginContext.env; } public setup(core: CoreSetup, deps: CorePluginBDeps) { - window.corePluginB = `Plugin A said: ${deps.core_plugin_a.getGreeting()}`; - window.hasAccessToInjectedMetadata = 'getInjectedVar' in core.injectedMetadata; - core.getStartServices().then(([coreStart, plugins]) => { - window.receivedStartServices = 'overlays' in coreStart; - }); - core.application.register({ id: 'bar', title: 'Bar', @@ -53,6 +44,12 @@ export class CorePluginBPlugin return renderApp(context, params); }, }); + + return { + sayHi() { + return `Plugin A said: ${deps.core_plugin_a.getGreeting()}`; + }, + }; } public start() {} diff --git a/test/plugin_functional/plugins/core_provider_plugin/index.ts b/test/plugin_functional/plugins/core_provider_plugin/index.ts new file mode 100644 index 0000000000000..01f3a67c6b554 --- /dev/null +++ b/test/plugin_functional/plugins/core_provider_plugin/index.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { resolve } from 'path'; +import { Legacy } from '../../../../kibana'; + +// eslint-disable-next-line import/no-default-export +export default function CoreProviderPlugin(kibana: any) { + const config: Legacy.PluginSpecOptions = { + id: 'core-provider', + require: [], + publicDir: resolve(__dirname, 'public'), + init: (server: Legacy.Server) => ({}), + uiExports: { + hacks: [resolve(__dirname, 'public/index')], + }, + }; + + return new kibana.Plugin(config); +} diff --git a/test/plugin_functional/plugins/core_provider_plugin/package.json b/test/plugin_functional/plugins/core_provider_plugin/package.json new file mode 100644 index 0000000000000..941503b934cbb --- /dev/null +++ b/test/plugin_functional/plugins/core_provider_plugin/package.json @@ -0,0 +1,17 @@ +{ + "name": "core_provider_plugin", + "version": "1.0.0", + "main": "target/test/plugin_functional/plugins/core_provider_plugin", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "scripts": { + "kbn": "node ../../../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "3.5.3" + } +} diff --git a/test/plugin_functional/plugins/ui_settings_plugin/public/index.ts b/test/plugin_functional/plugins/core_provider_plugin/public/index.ts similarity index 78% rename from test/plugin_functional/plugins/ui_settings_plugin/public/index.ts rename to test/plugin_functional/plugins/core_provider_plugin/public/index.ts index 3c5997132d460..c74928203db56 100644 --- a/test/plugin_functional/plugins/ui_settings_plugin/public/index.ts +++ b/test/plugin_functional/plugins/core_provider_plugin/public/index.ts @@ -16,6 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -import { UiSettingsPlugin } from './plugin'; +import { npSetup, npStart } from 'ui/new_platform'; +import '../types'; -export const plugin = () => new UiSettingsPlugin(); +window.__coreProvider = { + setup: npSetup, + start: npStart, + testUtils: { + delay: (ms: number) => new Promise(res => setTimeout(res, ms)), + }, +}; diff --git a/test/plugin_functional/plugins/core_provider_plugin/tsconfig.json b/test/plugin_functional/plugins/core_provider_plugin/tsconfig.json new file mode 100644 index 0000000000000..c29959197958d --- /dev/null +++ b/test/plugin_functional/plugins/core_provider_plugin/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "types.ts", + "public/**/*.ts", + "../../../../typings/**/*", + ], + "exclude": [] +} diff --git a/test/plugin_functional/plugins/ui_settings_plugin/public/plugin.tsx b/test/plugin_functional/plugins/core_provider_plugin/types.ts similarity index 66% rename from test/plugin_functional/plugins/ui_settings_plugin/public/plugin.tsx rename to test/plugin_functional/plugins/core_provider_plugin/types.ts index 883d203b4c37a..bf19578c37baa 100644 --- a/test/plugin_functional/plugins/ui_settings_plugin/public/plugin.tsx +++ b/test/plugin_functional/plugins/core_provider_plugin/types.ts @@ -16,22 +16,22 @@ * specific language governing permissions and limitations * under the License. */ - -import { CoreSetup, Plugin } from 'kibana/public'; +import { LegacyCoreSetup, LegacyCoreStart } from 'kibana/public'; declare global { interface Window { - uiSettingsPlugin?: Record; - uiSettingsPluginValue?: string; + __coreProvider: { + setup: { + core: LegacyCoreSetup; + plugins: Record; + }; + start: { + core: LegacyCoreStart; + plugins: Record; + }; + testUtils: { + delay: (ms: number) => Promise; + }; + }; } } - -export class UiSettingsPlugin implements Plugin { - public setup(core: CoreSetup) { - window.uiSettingsPlugin = core.uiSettings.getAll().ui_settings_plugin; - window.uiSettingsPluginValue = core.uiSettings.get('ui_settings_plugin'); - } - - public start() {} - public stop() {} -} diff --git a/test/plugin_functional/plugins/ui_settings_plugin/kibana.json b/test/plugin_functional/plugins/ui_settings_plugin/kibana.json index 05d2dca0af937..35e4c35490e2f 100644 --- a/test/plugin_functional/plugins/ui_settings_plugin/kibana.json +++ b/test/plugin_functional/plugins/ui_settings_plugin/kibana.json @@ -4,5 +4,5 @@ "kibanaVersion": "kibana", "configPath": ["ui_settings_plugin"], "server": true, - "ui": true + "ui": false } diff --git a/test/plugin_functional/plugins/ui_settings_plugin/tsconfig.json b/test/plugin_functional/plugins/ui_settings_plugin/tsconfig.json index 1ba21f11b7de2..7c170405bbfc7 100644 --- a/test/plugin_functional/plugins/ui_settings_plugin/tsconfig.json +++ b/test/plugin_functional/plugins/ui_settings_plugin/tsconfig.json @@ -5,9 +5,6 @@ "skipLibCheck": true }, "include": [ - "index.ts", - "public/**/*.ts", - "public/**/*.tsx", "server/**/*.ts", "../../../../typings/**/*", ], diff --git a/test/plugin_functional/test_suites/core_plugins/ui_plugins.ts b/test/plugin_functional/test_suites/core_plugins/ui_plugins.ts index ff53583546487..b76463ee76739 100644 --- a/test/plugin_functional/test_suites/core_plugins/ui_plugins.ts +++ b/test/plugin_functional/test_suites/core_plugins/ui_plugins.ts @@ -19,6 +19,7 @@ import expect from '@kbn/expect'; import { PluginFunctionalProviderContext } from '../../services'; +import '../../../../test/plugin_functional/plugins/core_provider_plugin/types'; // eslint-disable-next-line import/no-default-export export default function({ getService, getPageObjects }: PluginFunctionalProviderContext) { @@ -31,22 +32,35 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider await PageObjects.common.navigateToApp('settings'); }); - it('should attach string to window.corePluginB', async () => { - const corePluginB = await browser.execute('return window.corePluginB'); - expect(corePluginB).to.equal(`Plugin A said: Hello from Plugin A!`); + it('should run the new platform plugins', async () => { + expect( + await browser.execute(() => { + return window.__coreProvider.setup.plugins.core_plugin_b.sayHi(); + }) + ).to.be('Plugin A said: Hello from Plugin A!'); }); }); - describe('have injectedMetadata service provided', function describeIndexTests() { + describe('should have access to the core services', function describeIndexTests() { before(async () => { - await PageObjects.common.navigateToApp('bar'); + await PageObjects.common.navigateToApp('settings'); + }); + + it('to injectedMetadata service', async () => { + expect( + await browser.execute(() => { + return window.__coreProvider.setup.core.injectedMetadata.getKibanaBuildNumber(); + }) + ).to.be.a('number'); }); - it('should attach boolean to window.hasAccessToInjectedMetadata', async () => { - const hasAccessToInjectedMetadata = await browser.execute( - 'return window.hasAccessToInjectedMetadata' - ); - expect(hasAccessToInjectedMetadata).to.equal(true); + it('to start services via coreSetup.getStartServices', async () => { + expect( + await browser.executeAsync(async cb => { + const [coreStart] = await window.__coreProvider.setup.core.getStartServices(); + cb(Boolean(coreStart.overlays)); + }) + ).to.be(true); }); }); @@ -61,16 +75,5 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider expect(envData.packageInfo.version).to.be.a('string'); }); }); - - describe('have access to start services via coreSetup.getStartServices', function describeIndexTests() { - before(async () => { - await PageObjects.common.navigateToApp('bar'); - }); - - it('should attach boolean to window.receivedStartServices', async () => { - const receivedStartServices = await browser.execute('return window.receivedStartServices'); - expect(receivedStartServices).to.equal(true); - }); - }); }); } diff --git a/test/plugin_functional/test_suites/core_plugins/ui_settings.ts b/test/plugin_functional/test_suites/core_plugins/ui_settings.ts index 2b4227ee798e3..dec79fd15f4dd 100644 --- a/test/plugin_functional/test_suites/core_plugins/ui_settings.ts +++ b/test/plugin_functional/test_suites/core_plugins/ui_settings.ts @@ -18,6 +18,7 @@ */ import expect from '@kbn/expect'; import { PluginFunctionalProviderContext } from '../../services'; +import '../../plugins/core_provider_plugin/types'; // eslint-disable-next-line import/no-default-export export default function({ getService, getPageObjects }: PluginFunctionalProviderContext) { @@ -31,15 +32,30 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider }); it('client plugins have access to registered settings', async () => { - const settings = await browser.execute('return window.uiSettingsPlugin'); + const settings = await browser.execute(() => { + return window.__coreProvider.setup.core.uiSettings.getAll().ui_settings_plugin; + }); + expect(settings).to.eql({ category: ['any'], description: 'just for testing', name: 'from_ui_settings_plugin', value: '2', }); - const settingsValue = await browser.execute('return window.uiSettingsPluginValue'); + + const settingsValue = await browser.execute(() => { + return window.__coreProvider.setup.core.uiSettings.get('ui_settings_plugin'); + }); + expect(settingsValue).to.be('2'); + + const settingsValueViaObservables = await browser.executeAsync(async (callback: Function) => { + window.__coreProvider.setup.core.uiSettings + .get$('ui_settings_plugin') + .subscribe(v => callback(v)); + }); + + expect(settingsValueViaObservables).to.be('2'); }); it('server plugins have access to registered settings', async () => { diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/xpack_info.js b/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/xpack_info.js index 2edfbbc27fc45..2e0d608e522d7 100644 --- a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/xpack_info.js +++ b/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/xpack_info.js @@ -312,6 +312,54 @@ describe('XPackInfo', () => { }); }); + it('onLicenseInfoChange() allows to subscribe to license update', async () => { + const license$ = new BehaviorSubject(createLicense()); + + const xPackInfo = new XPackInfo(mockServer, { + licensing: { + license$, + refresh: () => null, + }, + }); + + const watcherFeature = xPackInfo.feature('watcher'); + watcherFeature.registerLicenseCheckResultsGenerator(info => ({ + type: info.license.getType(), + })); + + const statuses = []; + xPackInfo.onLicenseInfoChange(() => statuses.push(watcherFeature.getLicenseCheckResults())); + + license$.next(createLicense({ type: 'basic' })); + expect(statuses).to.eql([{ type: 'basic' }]); + + license$.next(createLicense({ type: 'trial' })); + expect(statuses).to.eql([{ type: 'basic' }, { type: 'trial' }]); + }); + + it('refreshNow() leads to onLicenseInfoChange()', async () => { + const license$ = new BehaviorSubject(createLicense()); + + const xPackInfo = new XPackInfo(mockServer, { + licensing: { + license$, + refresh: () => license$.next({ type: 'basic' }), + }, + }); + + const watcherFeature = xPackInfo.feature('watcher'); + + watcherFeature.registerLicenseCheckResultsGenerator(info => ({ + type: info.license.getType(), + })); + + const statuses = []; + xPackInfo.onLicenseInfoChange(() => statuses.push(watcherFeature.getLicenseCheckResults())); + + await xPackInfo.refreshNow(); + expect(statuses).to.eql([{ type: 'basic' }]); + }); + it('getSignature() returns correct signature.', async () => { const license$ = new BehaviorSubject(createLicense()); const xPackInfo = new XPackInfo(mockServer, { diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info.ts b/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info.ts index fbb8929154c36..9d5a8e64645ec 100644 --- a/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info.ts +++ b/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info.ts @@ -101,6 +101,8 @@ export class XPackInfo { error: license.error, }; } + + this._licenseInfoChangedListeners.forEach(fn => fn()); }); this._license = new XPackInfoLicense(() => this._cache.license); diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index f3e9db0053ad6..2b92e70fb30af 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -36,4 +36,6 @@ require('@kbn/test').runTestsCli([ require.resolve('../test/ui_capabilities/spaces_only/config'), require.resolve('../test/upgrade_assistant_integration/config'), require.resolve('../test/licensing_plugin/config'), + require.resolve('../test/licensing_plugin/config.public'), + require.resolve('../test/licensing_plugin/config.legacy'), ]); diff --git a/x-pack/test/licensing_plugin/apis/changes.ts b/x-pack/test/licensing_plugin/apis/changes.ts deleted file mode 100644 index cf4fecfa32d94..0000000000000 --- a/x-pack/test/licensing_plugin/apis/changes.ts +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../services'; -import { PublicLicenseJSON } from '../../../plugins/licensing/server'; - -const delay = (ms: number) => new Promise(res => setTimeout(res, ms)); - -export default function({ getService, getPageObjects }: FtrProviderContext) { - const supertest = getService('supertest'); - const esSupertestWithoutAuth = getService('esSupertestWithoutAuth'); - const security = getService('security'); - const PageObjects = getPageObjects(['common', 'security']); - const testSubjects = getService('testSubjects'); - - const scenario = { - async setup() { - await security.role.create('license_manager-role', { - elasticsearch: { - cluster: ['all'], - }, - kibana: [ - { - base: ['all'], - spaces: ['*'], - }, - ], - }); - - await security.user.create('license_manager_user', { - password: 'license_manager_user-password', - roles: ['license_manager-role'], - full_name: 'license_manager user', - }); - - // ensure we're logged out so we can login as the appropriate users - await PageObjects.security.forceLogout(); - await PageObjects.security.login('license_manager_user', 'license_manager_user-password'); - }, - - async teardown() { - await security.role.delete('license_manager-role'); - }, - - async startBasic() { - const response = await esSupertestWithoutAuth - .post('/_license/start_basic?acknowledge=true') - .auth('license_manager_user', 'license_manager_user-password') - .expect(200); - - expect(response.body.basic_was_started).to.be(true); - }, - - async startTrial() { - const response = await esSupertestWithoutAuth - .post('/_license/start_trial?acknowledge=true') - .auth('license_manager_user', 'license_manager_user-password') - .expect(200); - - expect(response.body.trial_was_started).to.be(true); - }, - - async deleteLicense() { - const response = await esSupertestWithoutAuth - .delete('/_license') - .auth('license_manager_user', 'license_manager_user-password') - .expect(200); - - expect(response.body.acknowledged).to.be(true); - }, - - async getLicense(): Promise { - // > --xpack.licensing.api_polling_frequency set in test config - // to wait for Kibana server to re-fetch the license from Elasticsearch - await delay(1000); - - const { body } = await supertest.get('/api/licensing/info').expect(200); - return body; - }, - }; - - describe('changes in license types', () => { - after(async () => { - await scenario.startBasic(); - }); - - it('provides changes in license types', async () => { - await scenario.setup(); - const initialLicense = await scenario.getLicense(); - expect(initialLicense.license?.type).to.be('basic'); - // security enabled explicitly in test config - expect(initialLicense.features?.security).to.eql({ - isAvailable: true, - isEnabled: true, - }); - - const { - body: legacyInitialLicense, - headers: legacyInitialLicenseHeaders, - } = await supertest.get('/api/xpack/v1/info').expect(200); - - expect(legacyInitialLicense.license?.type).to.be('basic'); - expect(legacyInitialLicense.features).to.have.property('security'); - expect(legacyInitialLicenseHeaders['kbn-xpack-sig']).to.be.a('string'); - - // license hasn't changed - const refetchedLicense = await scenario.getLicense(); - expect(refetchedLicense.license?.type).to.be('basic'); - expect(refetchedLicense.signature).to.be(initialLicense.signature); - - const { - body: legacyRefetchedLicense, - headers: legacyRefetchedLicenseHeaders, - } = await supertest.get('/api/xpack/v1/info').expect(200); - - expect(legacyRefetchedLicense.license?.type).to.be('basic'); - expect(legacyRefetchedLicenseHeaders['kbn-xpack-sig']).to.be( - legacyInitialLicenseHeaders['kbn-xpack-sig'] - ); - - // server allows to request trial only once. - // other attempts will throw 403 - await scenario.startTrial(); - const trialLicense = await scenario.getLicense(); - expect(trialLicense.license?.type).to.be('trial'); - expect(trialLicense.signature).to.not.be(initialLicense.signature); - - expect(trialLicense.features?.security).to.eql({ - isAvailable: true, - isEnabled: true, - }); - - const { body: legacyTrialLicense, headers: legacyTrialLicenseHeaders } = await supertest - .get('/api/xpack/v1/info') - .expect(200); - - expect(legacyTrialLicense.license?.type).to.be('trial'); - expect(legacyTrialLicense.features).to.have.property('security'); - expect(legacyTrialLicenseHeaders['kbn-xpack-sig']).to.not.be( - legacyInitialLicenseHeaders['kbn-xpack-sig'] - ); - - await scenario.startBasic(); - const basicLicense = await scenario.getLicense(); - expect(basicLicense.license?.type).to.be('basic'); - expect(basicLicense.signature).not.to.be(initialLicense.signature); - - expect(basicLicense.features?.security).to.eql({ - isAvailable: true, - isEnabled: true, - }); - - const { body: legacyBasicLicense, headers: legacyBasicLicenseHeaders } = await supertest - .get('/api/xpack/v1/info') - .expect(200); - expect(legacyBasicLicense.license?.type).to.be('basic'); - expect(legacyBasicLicense.features).to.have.property('security'); - expect(legacyBasicLicenseHeaders['kbn-xpack-sig']).to.not.be( - legacyInitialLicenseHeaders['kbn-xpack-sig'] - ); - - await scenario.deleteLicense(); - const inactiveLicense = await scenario.getLicense(); - expect(inactiveLicense.signature).to.not.be(initialLicense.signature); - expect(inactiveLicense).to.not.have.property('license'); - expect(inactiveLicense.features?.security).to.eql({ - isAvailable: false, - isEnabled: true, - }); - // banner shown only when license expired not just deleted - await testSubjects.missingOrFail('licenseExpiredBanner'); - }); - }); -} diff --git a/x-pack/test/licensing_plugin/config.legacy.ts b/x-pack/test/licensing_plugin/config.legacy.ts new file mode 100644 index 0000000000000..27dc3df9944ad --- /dev/null +++ b/x-pack/test/licensing_plugin/config.legacy.ts @@ -0,0 +1,15 @@ +/* + * 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 commonConfig = await readConfigFile(require.resolve('./config')); + + return { + ...commonConfig.getAll(), + testFiles: [require.resolve('./legacy')], + }; +} diff --git a/x-pack/test/licensing_plugin/config.public.ts b/x-pack/test/licensing_plugin/config.public.ts new file mode 100644 index 0000000000000..42209aa49bcb4 --- /dev/null +++ b/x-pack/test/licensing_plugin/config.public.ts @@ -0,0 +1,29 @@ +/* + * 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 path from 'path'; +import { KIBANA_ROOT } from '@kbn/test'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; + +export default async function({ readConfigFile }: FtrConfigProviderContext) { + const commonConfig = await readConfigFile(require.resolve('./config')); + + return { + ...commonConfig.getAll(), + testFiles: [require.resolve('./public')], + kbnTestServer: { + serverArgs: [ + ...commonConfig.get('kbnTestServer.serverArgs'), + + // Required to load new platform plugin provider via `--plugin-path` flag. + '--env.name=development', + `--plugin-path=${path.resolve( + KIBANA_ROOT, + 'test/plugin_functional/plugins/core_provider_plugin' + )}`, + ], + }, + }; +} diff --git a/x-pack/test/licensing_plugin/config.ts b/x-pack/test/licensing_plugin/config.ts index 9a83a6f6b5a0b..60d44cbd4c47f 100644 --- a/x-pack/test/licensing_plugin/config.ts +++ b/x-pack/test/licensing_plugin/config.ts @@ -22,7 +22,7 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { }; return { - testFiles: [require.resolve('./apis')], + testFiles: [require.resolve('./server')], servers, services, pageObjects, @@ -43,7 +43,7 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { ...functionalTestsConfig.get('kbnTestServer'), serverArgs: [ ...functionalTestsConfig.get('kbnTestServer.serverArgs'), - '--xpack.licensing.api_polling_frequency=300', + '--xpack.licensing.api_polling_frequency=100', ], }, diff --git a/x-pack/test/licensing_plugin/legacy/index.ts b/x-pack/test/licensing_plugin/legacy/index.ts new file mode 100644 index 0000000000000..5c45b8f097baf --- /dev/null +++ b/x-pack/test/licensing_plugin/legacy/index.ts @@ -0,0 +1,16 @@ +/* + * 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 '../services'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: FtrProviderContext) { + describe('Legacy licensing plugin', function() { + this.tags('ciGroup2'); + // MUST BE LAST! CHANGES LICENSE TYPE! + loadTestFile(require.resolve('./updates')); + }); +} diff --git a/x-pack/test/licensing_plugin/legacy/updates.ts b/x-pack/test/licensing_plugin/legacy/updates.ts new file mode 100644 index 0000000000000..14657368c78ae --- /dev/null +++ b/x-pack/test/licensing_plugin/legacy/updates.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../services'; +import { createScenario } from '../scenario'; +import '../../../../test/plugin_functional/plugins/core_provider_plugin/types'; + +// eslint-disable-next-line import/no-default-export +export default function(ftrContext: FtrProviderContext) { + const { getService } = ftrContext; + const supertest = getService('supertest'); + const testSubjects = getService('testSubjects'); + + const scenario = createScenario(ftrContext); + + describe('changes in license types', () => { + after(async () => { + await scenario.startBasic(); + await scenario.waitForPluginToDetectLicenseUpdate(); + await scenario.teardown(); + }); + + it('provides changes in license types', async () => { + await scenario.setup(); + await scenario.waitForPluginToDetectLicenseUpdate(); + + const { + body: legacyInitialLicense, + headers: legacyInitialLicenseHeaders, + } = await supertest.get('/api/xpack/v1/info').expect(200); + + expect(legacyInitialLicense.license?.type).to.be('basic'); + expect(legacyInitialLicense.features).to.have.property('security'); + expect(legacyInitialLicenseHeaders['kbn-xpack-sig']).to.be.a('string'); + + await scenario.startTrial(); + await scenario.waitForPluginToDetectLicenseUpdate(); + + const { body: legacyTrialLicense, headers: legacyTrialLicenseHeaders } = await supertest + .get('/api/xpack/v1/info') + .expect(200); + + expect(legacyTrialLicense.license?.type).to.be('trial'); + expect(legacyTrialLicense.features).to.have.property('security'); + expect(legacyTrialLicenseHeaders['kbn-xpack-sig']).to.not.be( + legacyInitialLicenseHeaders['kbn-xpack-sig'] + ); + + await scenario.startBasic(); + await scenario.waitForPluginToDetectLicenseUpdate(); + + const { body: legacyBasicLicense, headers: legacyBasicLicenseHeaders } = await supertest + .get('/api/xpack/v1/info') + .expect(200); + expect(legacyBasicLicense.license?.type).to.be('basic'); + expect(legacyBasicLicense.features).to.have.property('security'); + expect(legacyBasicLicenseHeaders['kbn-xpack-sig']).to.not.be( + legacyInitialLicenseHeaders['kbn-xpack-sig'] + ); + + await scenario.deleteLicense(); + await scenario.waitForPluginToDetectLicenseUpdate(); + + // banner shown only when license expired not just deleted + await testSubjects.missingOrFail('licenseExpiredBanner'); + }); + }); +} diff --git a/x-pack/test/licensing_plugin/public/index.ts b/x-pack/test/licensing_plugin/public/index.ts new file mode 100644 index 0000000000000..3e1445d9a4aab --- /dev/null +++ b/x-pack/test/licensing_plugin/public/index.ts @@ -0,0 +1,16 @@ +/* + * 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 '../services'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: FtrProviderContext) { + describe('Licensing plugin public client', function() { + this.tags('ciGroup2'); + // MUST BE LAST! CHANGES LICENSE TYPE! + loadTestFile(require.resolve('./updates')); + }); +} diff --git a/x-pack/test/licensing_plugin/public/updates.ts b/x-pack/test/licensing_plugin/public/updates.ts new file mode 100644 index 0000000000000..80822f6fb2505 --- /dev/null +++ b/x-pack/test/licensing_plugin/public/updates.ts @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../services'; +import { LicensingPluginSetup } from '../../../plugins/licensing/public'; +import { createScenario } from '../scenario'; +import '../../../../test/plugin_functional/plugins/core_provider_plugin/types'; + +// eslint-disable-next-line import/no-default-export +export default function(ftrContext: FtrProviderContext) { + const { getService } = ftrContext; + const testSubjects = getService('testSubjects'); + const browser = getService('browser'); + + const scenario = createScenario(ftrContext); + + describe('changes in license types', () => { + after(async () => { + await scenario.startBasic(); + await scenario.waitForPluginToDetectLicenseUpdate(); + await scenario.teardown(); + }); + + it('provides changes in license types', async () => { + await scenario.setup(); + await scenario.waitForPluginToDetectLicenseUpdate(); + + expect( + await browser.executeAsync(async (cb: Function) => { + const { setup, testUtils } = window.__coreProvider; + // this call enforces signature check to detect license update + // and causes license re-fetch + await setup.core.http.get('/'); + await testUtils.delay(100); + + const licensing: LicensingPluginSetup = setup.plugins.licensing; + licensing.license$.subscribe(license => cb(license.type)); + }) + ).to.be('basic'); + + // license hasn't changed + await scenario.waitForPluginToDetectLicenseUpdate(); + + expect( + await browser.executeAsync(async (cb: Function) => { + const { setup, testUtils } = window.__coreProvider; + // this call enforces signature check to detect license update + // and causes license re-fetch + await setup.core.http.get('/'); + await testUtils.delay(100); + + const licensing: LicensingPluginSetup = setup.plugins.licensing; + licensing.license$.subscribe(license => cb(license.type)); + }) + ).to.be('basic'); + + await scenario.startTrial(); + await scenario.waitForPluginToDetectLicenseUpdate(); + + expect( + await browser.executeAsync(async (cb: Function) => { + const { setup, testUtils } = window.__coreProvider; + // this call enforces signature check to detect license update + // and causes license re-fetch + await setup.core.http.get('/'); + await testUtils.delay(100); + + const licensing: LicensingPluginSetup = setup.plugins.licensing; + licensing.license$.subscribe(license => cb(license.type)); + }) + ).to.be('trial'); + + await scenario.startBasic(); + await scenario.waitForPluginToDetectLicenseUpdate(); + + expect( + await browser.executeAsync(async (cb: Function) => { + const { setup, testUtils } = window.__coreProvider; + // this call enforces signature check to detect license update + // and causes license re-fetch + await setup.core.http.get('/'); + await testUtils.delay(100); + + const licensing: LicensingPluginSetup = setup.plugins.licensing; + licensing.license$.subscribe(license => cb(license.type)); + }) + ).to.be('basic'); + + await scenario.deleteLicense(); + await scenario.waitForPluginToDetectLicenseUpdate(); + + expect( + await browser.executeAsync(async (cb: Function) => { + const { setup, testUtils } = window.__coreProvider; + // this call enforces signature check to detect license update + // and causes license re-fetch + await setup.core.http.get('/'); + await testUtils.delay(100); + + const licensing: LicensingPluginSetup = setup.plugins.licensing; + licensing.license$.subscribe(license => cb(license.type)); + }) + ).to.be(null); + + // banner shown only when license expired not just deleted + await testSubjects.missingOrFail('licenseExpiredBanner'); + }); + }); +} diff --git a/x-pack/test/licensing_plugin/scenario.ts b/x-pack/test/licensing_plugin/scenario.ts new file mode 100644 index 0000000000000..46837dfc1be91 --- /dev/null +++ b/x-pack/test/licensing_plugin/scenario.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from './services'; +import { PublicLicenseJSON } from '../../plugins/licensing/server'; +import '../../../test/plugin_functional/plugins/core_provider_plugin/types'; + +const delay = (ms: number) => new Promise(res => setTimeout(res, ms)); + +export function createScenario({ getService, getPageObjects }: FtrProviderContext) { + const supertest = getService('supertest'); + const esSupertestWithoutAuth = getService('esSupertestWithoutAuth'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'security']); + + const scenario = { + async setup() { + await security.role.create('license_manager-role', { + elasticsearch: { + cluster: ['all'], + }, + kibana: [ + { + base: ['all'], + spaces: ['*'], + }, + ], + }); + + await security.user.create('license_manager_user', { + password: 'license_manager_user-password', + roles: ['license_manager-role'], + full_name: 'license_manager user', + }); + + // ensure we're logged out so we can login as the appropriate users + await PageObjects.security.logout(); + await PageObjects.security.login('license_manager_user', 'license_manager_user-password'); + }, + + // make sure a license is present, otherwise the security is not available anymore. + async teardown() { + await security.role.delete('license_manager-role'); + await security.user.delete('license_manager_user'); + }, + + // elasticsearch allows to downgrade a license only once. other attempts will throw 403. + async startBasic() { + const response = await esSupertestWithoutAuth + .post('/_license/start_basic?acknowledge=true') + .auth('license_manager_user', 'license_manager_user-password') + .expect(200); + + expect(response.body.basic_was_started).to.be(true); + }, + + // elasticsearch allows to request trial only once. other attempts will throw 403. + async startTrial() { + const response = await esSupertestWithoutAuth + .post('/_license/start_trial?acknowledge=true') + .auth('license_manager_user', 'license_manager_user-password') + .expect(200); + + expect(response.body.trial_was_started).to.be(true); + }, + + async deleteLicense() { + const response = await esSupertestWithoutAuth + .delete('/_license') + .auth('license_manager_user', 'license_manager_user-password') + .expect(200); + + expect(response.body.acknowledged).to.be(true); + }, + + async getLicense(): Promise { + const { body } = await supertest.get('/api/licensing/info').expect(200); + return body; + }, + + async waitForPluginToDetectLicenseUpdate() { + // > --xpack.licensing.api_polling_frequency set in test config + // to wait for Kibana server to re-fetch the license from Elasticsearch + await delay(500); + }, + }; + return scenario; +} diff --git a/x-pack/test/licensing_plugin/apis/header.ts b/x-pack/test/licensing_plugin/server/header.ts similarity index 93% rename from x-pack/test/licensing_plugin/apis/header.ts rename to x-pack/test/licensing_plugin/server/header.ts index 8d95054feaaf2..d2073e8773f18 100644 --- a/x-pack/test/licensing_plugin/apis/header.ts +++ b/x-pack/test/licensing_plugin/server/header.ts @@ -7,6 +7,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../services'; +// eslint-disable-next-line import/no-default-export export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/licensing_plugin/apis/index.ts b/x-pack/test/licensing_plugin/server/index.ts similarity index 76% rename from x-pack/test/licensing_plugin/apis/index.ts rename to x-pack/test/licensing_plugin/server/index.ts index fbc0449dcd8fc..374bfcc0aa6b4 100644 --- a/x-pack/test/licensing_plugin/apis/index.ts +++ b/x-pack/test/licensing_plugin/server/index.ts @@ -6,13 +6,14 @@ import { FtrProviderContext } from '../services'; +// eslint-disable-next-line import/no-default-export export default function({ loadTestFile }: FtrProviderContext) { - describe('Licensing plugin', function() { + describe('Licensing plugin server client', function() { this.tags('ciGroup2'); loadTestFile(require.resolve('./info')); loadTestFile(require.resolve('./header')); // MUST BE LAST! CHANGES LICENSE TYPE! - loadTestFile(require.resolve('./changes')); + loadTestFile(require.resolve('./updates')); }); } diff --git a/x-pack/test/licensing_plugin/apis/info.ts b/x-pack/test/licensing_plugin/server/info.ts similarity index 95% rename from x-pack/test/licensing_plugin/apis/info.ts rename to x-pack/test/licensing_plugin/server/info.ts index 7ec009d85cd09..cce042c718b7b 100644 --- a/x-pack/test/licensing_plugin/apis/info.ts +++ b/x-pack/test/licensing_plugin/server/info.ts @@ -7,6 +7,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../services'; +// eslint-disable-next-line import/no-default-export export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/licensing_plugin/server/updates.ts b/x-pack/test/licensing_plugin/server/updates.ts new file mode 100644 index 0000000000000..ca0fb37069b3f --- /dev/null +++ b/x-pack/test/licensing_plugin/server/updates.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../services'; +import { createScenario } from '../scenario'; +import '../../../../test/plugin_functional/plugins/core_provider_plugin/types'; + +// eslint-disable-next-line import/no-default-export +export default function(ftrContext: FtrProviderContext) { + const { getService } = ftrContext; + const testSubjects = getService('testSubjects'); + + const scenario = createScenario(ftrContext); + + describe('changes in license types', () => { + after(async () => { + await scenario.startBasic(); + await scenario.waitForPluginToDetectLicenseUpdate(); + await scenario.teardown(); + }); + + it('provides changes in license types', async () => { + await scenario.setup(); + await scenario.waitForPluginToDetectLicenseUpdate(); + const initialLicense = await scenario.getLicense(); + expect(initialLicense.license?.type).to.be('basic'); + // security enabled explicitly in test config + expect(initialLicense.features?.security).to.eql({ + isAvailable: true, + isEnabled: true, + }); + + // license hasn't changed + await scenario.waitForPluginToDetectLicenseUpdate(); + const refetchedLicense = await scenario.getLicense(); + expect(refetchedLicense.license?.type).to.be('basic'); + expect(refetchedLicense.signature).to.be(initialLicense.signature); + + await scenario.startTrial(); + await scenario.waitForPluginToDetectLicenseUpdate(); + const trialLicense = await scenario.getLicense(); + expect(trialLicense.license?.type).to.be('trial'); + expect(trialLicense.signature).to.not.be(initialLicense.signature); + + expect(trialLicense.features?.security).to.eql({ + isAvailable: true, + isEnabled: true, + }); + + await scenario.startBasic(); + await scenario.waitForPluginToDetectLicenseUpdate(); + const basicLicense = await scenario.getLicense(); + expect(basicLicense.license?.type).to.be('basic'); + expect(basicLicense.signature).not.to.be(initialLicense.signature); + + expect(basicLicense.features?.security).to.eql({ + isAvailable: true, + isEnabled: true, + }); + + await scenario.deleteLicense(); + await scenario.waitForPluginToDetectLicenseUpdate(); + const inactiveLicense = await scenario.getLicense(); + expect(inactiveLicense.signature).to.not.be(initialLicense.signature); + expect(inactiveLicense).to.not.have.property('license'); + expect(inactiveLicense.features?.security).to.eql({ + isAvailable: false, + isEnabled: true, + }); + + // banner shown only when license expired not just deleted + await testSubjects.missingOrFail('licenseExpiredBanner'); + }); + }); +}