From 1ea9d791bcbc16896ea96c9f0db6e129cdf62c48 Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 13 Dec 2019 13:24:14 -0700 Subject: [PATCH] Revert "Revert "Fix wrong impor (#52994)"" This reverts commit c220d770cc802fcbccf7578ddd3c23a581897b62. --- .eslintrc.js | 2 +- x-pack/legacy/common/poller.d.ts | 3 + x-pack/legacy/plugins/graph/public/index.ts | 7 +- x-pack/legacy/plugins/graph/public/plugin.ts | 2 +- .../legacy/plugins/graph/public/render_app.ts | 2 +- x-pack/legacy/plugins/xpack_main/index.js | 7 +- .../public/hacks/check_xpack_info_change.js | 53 +++ .../server/lib/__tests__/setup_xpack_main.js | 7 +- .../server/lib/__tests__/xpack_info.js | 373 ++++-------------- .../xpack_main/server/lib/setup_xpack_main.js | 12 +- .../xpack_main/server/lib/xpack_info.d.ts | 37 -- .../xpack_main/server/lib/xpack_info.js | 308 --------------- .../xpack_main/server/lib/xpack_info.ts | 240 +++++++++++ .../server/lib/xpack_info_license.d.ts | 21 - .../server/lib/xpack_info_license.test.js | 43 +- ..._info_license.js => xpack_info_license.ts} | 30 +- x-pack/plugins/graph/public/plugin.ts | 2 +- x-pack/plugins/graph/server/plugin.ts | 2 +- .../common/has_license_info_changed.test.ts | 1 + .../plugins/licensing/common/license.test.ts | 2 +- x-pack/plugins/licensing/common/license.ts | 2 + .../licensing/common/license_update.test.ts | 2 +- .../{license.mock.ts => licensing.mock.ts} | 1 + x-pack/plugins/licensing/common/types.ts | 24 +- x-pack/plugins/licensing/public/index.ts | 1 + .../licensing/public/licensing.mock.ts | 24 ++ .../plugins/licensing/public/plugin.test.ts | 16 +- x-pack/plugins/licensing/public/plugin.ts | 3 +- x-pack/plugins/licensing/public/types.ts | 20 + x-pack/plugins/licensing/server/index.ts | 1 + .../licensing/server/licensing.mock.ts | 29 ++ .../licensing/server/licensing_config.ts | 19 +- .../licensing_route_handler_context.test.ts | 2 +- x-pack/plugins/licensing/server/mocks.ts | 7 + .../server/on_pre_response_handler.test.ts | 2 +- .../plugins/licensing/server/plugin.test.ts | 89 ++++- x-pack/plugins/licensing/server/plugin.ts | 19 +- x-pack/plugins/licensing/server/types.ts | 25 ++ x-pack/test/licensing_plugin/apis/changes.ts | 45 ++- x-pack/test/licensing_plugin/config.ts | 2 +- 40 files changed, 718 insertions(+), 769 deletions(-) create mode 100644 x-pack/legacy/plugins/xpack_main/public/hacks/check_xpack_info_change.js delete mode 100644 x-pack/legacy/plugins/xpack_main/server/lib/xpack_info.d.ts delete mode 100644 x-pack/legacy/plugins/xpack_main/server/lib/xpack_info.js create mode 100644 x-pack/legacy/plugins/xpack_main/server/lib/xpack_info.ts delete mode 100644 x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.d.ts rename x-pack/legacy/plugins/xpack_main/server/lib/{xpack_info_license.js => xpack_info_license.ts} (74%) rename x-pack/plugins/licensing/common/{license.mock.ts => licensing.mock.ts} (98%) create mode 100644 x-pack/plugins/licensing/public/licensing.mock.ts create mode 100644 x-pack/plugins/licensing/public/types.ts create mode 100644 x-pack/plugins/licensing/server/licensing.mock.ts create mode 100644 x-pack/plugins/licensing/server/mocks.ts diff --git a/.eslintrc.js b/.eslintrc.js index af68f367aebb..88711c0959d6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -346,7 +346,7 @@ module.exports = { 'Server modules cannot be imported into client modules or shared modules.', }, { - target: ['src/core/**/*'], + target: ['src/**/*'], from: ['x-pack/**/*'], errorMessage: 'OSS cannot import x-pack files.', }, diff --git a/x-pack/legacy/common/poller.d.ts b/x-pack/legacy/common/poller.d.ts index c23d18dd62e8..df39d93a28a8 100644 --- a/x-pack/legacy/common/poller.d.ts +++ b/x-pack/legacy/common/poller.d.ts @@ -8,4 +8,7 @@ export declare class Poller { constructor(options: any); public start(): void; + public stop(): void; + public isRunning(): boolean; + public getPollFrequency(): number; } diff --git a/x-pack/legacy/plugins/graph/public/index.ts b/x-pack/legacy/plugins/graph/public/index.ts index e0ce21032889..600df6d30978 100644 --- a/x-pack/legacy/plugins/graph/public/index.ts +++ b/x-pack/legacy/plugins/graph/public/index.ts @@ -17,6 +17,7 @@ import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_regis import { npSetup, npStart } from 'ui/new_platform'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { start as navigation } from '../../../../../src/legacy/core_plugins/navigation/public/legacy'; +import { LicensingPluginSetup } from '../../../../plugins/licensing/public'; import { GraphPlugin } from './plugin'; // @ts-ignore @@ -39,13 +40,17 @@ async function getAngularInjectedDependencies(): Promise { const instance = new GraphPlugin(); instance.setup(npSetup.core, { __LEGACY: { Storage, }, - ...npSetup.plugins, + ...(npSetup.plugins as XpackNpSetupDeps), }); instance.start(npStart.core, { npData: npStart.plugins.data, diff --git a/x-pack/legacy/plugins/graph/public/plugin.ts b/x-pack/legacy/plugins/graph/public/plugin.ts index 431977514761..1646cf200e37 100644 --- a/x-pack/legacy/plugins/graph/public/plugin.ts +++ b/x-pack/legacy/plugins/graph/public/plugin.ts @@ -9,7 +9,7 @@ import { CoreSetup, CoreStart, Plugin, SavedObjectsClientContract } from 'src/co import { Plugin as DataPlugin } from 'src/plugins/data/public'; import { LegacyAngularInjectedDependencies } from './render_app'; import { NavigationStart } from '../../../../../src/legacy/core_plugins/navigation/public'; -import { LicensingPluginSetup } from '../../../../plugins/licensing/common/types'; +import { LicensingPluginSetup } from '../../../../plugins/licensing/public'; export interface GraphPluginStartDependencies { npData: ReturnType; diff --git a/x-pack/legacy/plugins/graph/public/render_app.ts b/x-pack/legacy/plugins/graph/public/render_app.ts index 2cad8f629f2a..b07a91e6d632 100644 --- a/x-pack/legacy/plugins/graph/public/render_app.ts +++ b/x-pack/legacy/plugins/graph/public/render_app.ts @@ -38,7 +38,7 @@ import { IndexPatternsContract, } from '../../../../../src/plugins/data/public'; import { NavigationStart } from '../../../../../src/legacy/core_plugins/navigation/public'; -import { LicensingPluginSetup } from '../../../../plugins/licensing/common/types'; +import { LicensingPluginSetup } from '../../../../plugins/licensing/public'; import { checkLicense } from '../../../../plugins/graph/common/check_license'; /** diff --git a/x-pack/legacy/plugins/xpack_main/index.js b/x-pack/legacy/plugins/xpack_main/index.js index 6828833c3f98..68fea22e4d90 100644 --- a/x-pack/legacy/plugins/xpack_main/index.js +++ b/x-pack/legacy/plugins/xpack_main/index.js @@ -6,9 +6,6 @@ import { resolve } from 'path'; import dedent from 'dedent'; -import { - XPACK_INFO_API_DEFAULT_POLL_FREQUENCY_IN_MILLIS -} from '../../server/lib/constants'; import { mirrorPluginStatus } from '../../server/lib/mirror_plugin_status'; import { replaceInjectedVars } from './server/lib/replace_injected_vars'; import { setupXPackMain } from './server/lib/setup_xpack_main'; @@ -34,7 +31,6 @@ export const xpackMain = (kibana) => { enabled: Joi.boolean().default(), url: Joi.string().default(), }).default(), // deprecated - xpack_api_polling_frequency_millis: Joi.number().default(XPACK_INFO_API_DEFAULT_POLL_FREQUENCY_IN_MILLIS), }).default(); }, @@ -47,6 +43,9 @@ export const xpackMain = (kibana) => { }, uiExports: { + hacks: [ + 'plugins/xpack_main/hacks/check_xpack_info_change', + ], replaceInjectedVars, injectDefaultVars(server) { const config = server.config(); diff --git a/x-pack/legacy/plugins/xpack_main/public/hacks/check_xpack_info_change.js b/x-pack/legacy/plugins/xpack_main/public/hacks/check_xpack_info_change.js new file mode 100644 index 000000000000..0de13da68eac --- /dev/null +++ b/x-pack/legacy/plugins/xpack_main/public/hacks/check_xpack_info_change.js @@ -0,0 +1,53 @@ +/* + * 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 { identity } from 'lodash'; +import { uiModules } from 'ui/modules'; +import { Path } from 'plugins/xpack_main/services/path'; +import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; +import { xpackInfoSignature } from 'plugins/xpack_main/services/xpack_info_signature'; + +const module = uiModules.get('xpack_main', []); + +module.factory('checkXPackInfoChange', ($q, Private, $injector) => { + /** + * Intercept each network response to look for the kbn-xpack-sig header. + * When that header is detected, compare its value with the value cached + * in the browser storage. When the value is new, call `xpackInfo.refresh()` + * so that it will pull down the latest x-pack info + * + * @param {object} response - the angular $http response object + * @param {function} handleResponse - callback, expects to receive the response + * @return + */ + function interceptor(response, handleResponse) { + if (Path.isUnauthenticated()) { + return handleResponse(response); + } + + const currentSignature = response.headers('kbn-xpack-sig'); + const cachedSignature = xpackInfoSignature.get(); + + if (currentSignature && cachedSignature !== currentSignature) { + // Signature from the server differ from the signature of our + // cached info, so we need to refresh it. + // Intentionally swallowing this error + // because nothing catches it and it's an ugly console error. + xpackInfo.refresh($injector).catch(() => {}); + } + + return handleResponse(response); + } + + return { + response: (response) => interceptor(response, identity), + responseError: (response) => interceptor(response, $q.reject) + }; +}); + +module.config(($httpProvider) => { + $httpProvider.interceptors.push('checkXPackInfoChange'); +}); diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/setup_xpack_main.js b/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/setup_xpack_main.js index 5b2c6612d2a8..bd94f951810b 100644 --- a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/setup_xpack_main.js +++ b/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/setup_xpack_main.js @@ -4,11 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import { BehaviorSubject } from 'rxjs'; import sinon from 'sinon'; import { XPackInfo } from '../xpack_info'; import { setupXPackMain } from '../setup_xpack_main'; import * as InjectXPackInfoSignatureNS from '../inject_xpack_info_signature'; + describe('setupXPackMain()', () => { const sandbox = sinon.createSandbox(); @@ -39,7 +41,7 @@ describe('setupXPackMain()', () => { elasticsearch: mockElasticsearchPlugin, xpack_main: mockXPackMainPlugin }, - newPlatform: { setup: { plugins: { features: {} } } }, + newPlatform: { setup: { plugins: { features: {}, licensing: { license$: new BehaviorSubject() } } } }, events: { on() {} }, log() {}, config() {}, @@ -47,9 +49,8 @@ describe('setupXPackMain()', () => { ext() {} }); - // Make sure we don't misspell config key. + // Make sure plugins doesn't consume config const configGetStub = sinon.stub().throws(new Error('`config.get` is called with unexpected key.')); - configGetStub.withArgs('xpack.xpack_main.xpack_api_polling_frequency_millis').returns(1234); mockServer.config.returns({ get: configGetStub }); }); 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 12426d6a4eff..52f97fc0cfc3 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 @@ -5,36 +5,32 @@ */ import { createHash } from 'crypto'; +import { BehaviorSubject } from 'rxjs'; import expect from '@kbn/expect'; import sinon from 'sinon'; import { XPackInfo } from '../xpack_info'; +import { licensingMock } from '../../../../../../plugins/licensing/server/mocks'; -const nowDate = new Date(2010, 10, 10); - -function getMockXPackInfoAPIResponse(license = {}, features = {}) { - return Promise.resolve({ - build: { - hash: '5927d85', - date: '2010-10-10T00:00:00.000Z' - }, +function createLicense(license = {}, features = {}) { + return licensingMock.createLicense({ license: { uid: 'custom-uid', type: 'gold', mode: 'gold', status: 'active', - expiry_date_in_millis: 1286575200000, + expiryDateInMillis: 1286575200000, ...license }, features: { security: { description: 'Security for the Elastic Stack', - available: true, - enabled: true + isAvailable: true, + isEnabled: true }, watcher: { description: 'Alerting, Notification and Automation for the Elastic Stack', - available: true, - enabled: false + isAvailable: true, + isEnabled: false }, ...features } @@ -48,244 +44,63 @@ function getSignature(object) { } describe('XPackInfo', () => { - const sandbox = sinon.createSandbox(); - let mockServer; - let mockElasticsearchCluster; let mockElasticsearchPlugin; beforeEach(() => { - sandbox.useFakeTimers(nowDate.getTime()); - - mockElasticsearchCluster = { - callWithInternalUser: sinon.stub() - }; - - mockElasticsearchPlugin = { - getCluster: sinon.stub().returns(mockElasticsearchCluster) - }; - mockServer = sinon.stub({ plugins: { elasticsearch: mockElasticsearchPlugin }, events: { on() {} }, - log() { } + newPlatform: { + setup: { + plugins: { + licensing: { + + } + } + } + }, }); }); - afterEach(() => sandbox.restore()); - - it('correctly initializes its own properties with defaults.', () => { - mockElasticsearchPlugin.getCluster.throws(new Error('`getCluster` is called with unexpected source.')); - mockElasticsearchPlugin.getCluster.withArgs('data').returns(mockElasticsearchCluster); - - const xPackInfo = new XPackInfo(mockServer, { pollFrequencyInMillis: 1500 }); - - expect(xPackInfo.isAvailable()).to.be(false); - expect(xPackInfo.license.isActive()).to.be(false); - expect(xPackInfo.unavailableReason()).to.be(undefined); - - // Poller is not started. - sandbox.clock.tick(10000); - sinon.assert.notCalled(mockElasticsearchCluster.callWithInternalUser); - }); - - it('correctly initializes its own properties with custom cluster type.', () => { - mockElasticsearchPlugin.getCluster.throws(new Error('`getCluster` is called with unexpected source.')); - mockElasticsearchPlugin.getCluster.withArgs('monitoring').returns(mockElasticsearchCluster); - - const xPackInfo = new XPackInfo( - mockServer, - { clusterSource: 'monitoring', pollFrequencyInMillis: 1234 } - ); - - expect(xPackInfo.isAvailable()).to.be(false); - expect(xPackInfo.license.isActive()).to.be(false); - expect(xPackInfo.unavailableReason()).to.be(undefined); - - // Poller is not started. - sandbox.clock.tick(9999); - sinon.assert.notCalled(mockElasticsearchCluster.callWithInternalUser); - }); - describe('refreshNow()', () => { - let xPackInfo; - beforeEach(async () => { - mockElasticsearchCluster.callWithInternalUser.returns(getMockXPackInfoAPIResponse()); + it('delegates to the new platform licensing plugin', async () => { + const refresh = sinon.spy(); - xPackInfo = new XPackInfo(mockServer, { pollFrequencyInMillis: 1500 }); - await xPackInfo.refreshNow(); - }); - - it('forces xpack info to be immediately updated with the data returned from Elasticsearch API.', async () => { - sinon.assert.calledOnce(mockElasticsearchCluster.callWithInternalUser); - sinon.assert.calledWithExactly(mockElasticsearchCluster.callWithInternalUser, 'transport.request', { - method: 'GET', - path: '/_xpack' + const xPackInfo = new XPackInfo(mockServer, { + licensing: { + license$: new BehaviorSubject(createLicense()), + refresh: refresh + } }); - expect(xPackInfo.isAvailable()).to.be(true); - expect(xPackInfo.license.isActive()).to.be(true); - }); - - - it('communicates X-Pack being unavailable', async () => { - const badRequestError = new Error('Bad request'); - badRequestError.status = 400; - - mockElasticsearchCluster.callWithInternalUser.returns(Promise.reject(badRequestError)); await xPackInfo.refreshNow(); - expect(xPackInfo.isAvailable()).to.be(false); - expect(xPackInfo.isXpackUnavailable()).to.be(true); - expect(xPackInfo.license.isActive()).to.be(false); - expect(xPackInfo.unavailableReason()).to.be( - 'X-Pack plugin is not installed on the [data] Elasticsearch cluster.' - ); - }); - - it('correctly updates xpack info if Elasticsearch API fails.', async () => { - expect(xPackInfo.isAvailable()).to.be(true); - expect(xPackInfo.license.isActive()).to.be(true); - - mockElasticsearchCluster.callWithInternalUser.returns(Promise.reject(new Error('Uh oh'))); - await xPackInfo.refreshNow(); - - expect(xPackInfo.isAvailable()).to.be(false); - expect(xPackInfo.license.isActive()).to.be(false); - }); - - it('correctly updates xpack info when Elasticsearch API recovers after failure.', async () => { - expect(xPackInfo.isAvailable()).to.be(true); - expect(xPackInfo.license.isActive()).to.be(true); - expect(xPackInfo.unavailableReason()).to.be(undefined); - - const randomError = new Error('Uh oh'); - mockElasticsearchCluster.callWithInternalUser.returns(Promise.reject(randomError)); - await xPackInfo.refreshNow(); - - expect(xPackInfo.isAvailable()).to.be(false); - expect(xPackInfo.license.isActive()).to.be(false); - expect(xPackInfo.unavailableReason()).to.be(randomError); - sinon.assert.calledWithExactly( - mockServer.log, - ['license', 'warning', 'xpack'], - `License information from the X-Pack plugin could not be obtained from Elasticsearch` + - ` for the [data] cluster. ${randomError}` - ); - - const badRequestError = new Error('Bad request'); - badRequestError.status = 400; - mockElasticsearchCluster.callWithInternalUser.returns(Promise.reject(badRequestError)); - await xPackInfo.refreshNow(); - - expect(xPackInfo.isAvailable()).to.be(false); - expect(xPackInfo.license.isActive()).to.be(false); - expect(xPackInfo.unavailableReason()).to.be( - 'X-Pack plugin is not installed on the [data] Elasticsearch cluster.' - ); - sinon.assert.calledWithExactly( - mockServer.log, - ['license', 'warning', 'xpack'], - `License information from the X-Pack plugin could not be obtained from Elasticsearch` + - ` for the [data] cluster. ${badRequestError}` - ); - - mockElasticsearchCluster.callWithInternalUser.returns(getMockXPackInfoAPIResponse()); - await xPackInfo.refreshNow(); - - expect(xPackInfo.isAvailable()).to.be(true); - expect(xPackInfo.license.isActive()).to.be(true); - }); - - it('logs license status changes.', async () => { - sinon.assert.calledWithExactly( - mockServer.log, - ['license', 'info', 'xpack'], - sinon.match('Imported license information from Elasticsearch for the [data] cluster: ' + - 'mode: gold | status: active | expiry date: ' - ) - ); - mockServer.log.resetHistory(); - - await xPackInfo.refreshNow(); - - // Response is still the same, so nothing should be logged. - sinon.assert.neverCalledWith(mockServer.log, ['license', 'info', 'xpack']); - - // Change mode/status of the license and the change should be logged. - mockElasticsearchCluster.callWithInternalUser.returns( - getMockXPackInfoAPIResponse({ status: 'expired', mode: 'platinum' }) - ); - - await xPackInfo.refreshNow(); - - sinon.assert.calledWithExactly( - mockServer.log, - ['license', 'info', 'xpack'], - sinon.match('Imported changed license information from Elasticsearch for the [data] cluster: ' + - 'mode: platinum | status: expired | expiry date: ' - ) - ); - }); - - it('restarts the poller.', async () => { - mockElasticsearchCluster.callWithInternalUser.resetHistory(); - - sandbox.clock.tick(1499); - sinon.assert.notCalled(mockElasticsearchCluster.callWithInternalUser); - - sandbox.clock.tick(1); - sinon.assert.calledOnce(mockElasticsearchCluster.callWithInternalUser); - // Exhaust micro-task queue, to make sure that `callWithInternalUser` is completed and - // new poller iteration is rescheduled. - await Promise.resolve(); - - sandbox.clock.tick(1500); - sinon.assert.calledTwice(mockElasticsearchCluster.callWithInternalUser); - // Exhaust micro-task queue, to make sure that `callWithInternalUser` is completed and - // new poller iteration is rescheduled. - await Promise.resolve(); - - sandbox.clock.tick(1499); - await xPackInfo.refreshNow(); - mockElasticsearchCluster.callWithInternalUser.resetHistory(); - - // Since poller has been restarted, it should not be called now. - sandbox.clock.tick(1); - sinon.assert.notCalled(mockElasticsearchCluster.callWithInternalUser); - - // Here it still shouldn't be called. - sandbox.clock.tick(1498); - sinon.assert.notCalled(mockElasticsearchCluster.callWithInternalUser); - - sandbox.clock.tick(1); - sinon.assert.calledOnce(mockElasticsearchCluster.callWithInternalUser); + sinon.assert.calledOnce(refresh); }); }); describe('license', () => { let xPackInfo; + let license$; beforeEach(async () => { - mockElasticsearchCluster.callWithInternalUser.returns(getMockXPackInfoAPIResponse()); - - xPackInfo = new XPackInfo(mockServer, { pollFrequencyInMillis: 1500 }); - await xPackInfo.refreshNow(); + license$ = new BehaviorSubject(createLicense()); + xPackInfo = new XPackInfo(mockServer, { + licensing: { + license$, + refresh: () => null + } + }); }); - it('getUid() shows license uid returned from the backend.', async () => { + it('getUid() shows license uid returned from the license$.', async () => { expect(xPackInfo.license.getUid()).to.be('custom-uid'); - mockElasticsearchCluster.callWithInternalUser.returns( - getMockXPackInfoAPIResponse({ uid: 'new-custom-uid' }) - ); - await xPackInfo.refreshNow(); + license$.next(createLicense({ uid: 'new-custom-uid' })); expect(xPackInfo.license.getUid()).to.be('new-custom-uid'); - mockElasticsearchCluster.callWithInternalUser.returns( - Promise.reject(new Error('Uh oh')) - ); - await xPackInfo.refreshNow(); + license$.next(createLicense({ uid: undefined, error: 'error-reason' })); expect(xPackInfo.license.getUid()).to.be(undefined); }); @@ -293,86 +108,46 @@ describe('XPackInfo', () => { it('isActive() is based on the status returned from the backend.', async () => { expect(xPackInfo.license.isActive()).to.be(true); - mockElasticsearchCluster.callWithInternalUser.returns( - getMockXPackInfoAPIResponse({ status: 'expired' }) - ); - await xPackInfo.refreshNow(); - + license$.next(createLicense({ status: 'expired' })); expect(xPackInfo.license.isActive()).to.be(false); - mockElasticsearchCluster.callWithInternalUser.returns( - getMockXPackInfoAPIResponse({ status: 'some other value' }) - ); - await xPackInfo.refreshNow(); - + license$.next(createLicense({ status: 'some other value' })); expect(xPackInfo.license.isActive()).to.be(false); - mockElasticsearchCluster.callWithInternalUser.returns( - getMockXPackInfoAPIResponse({ status: 'active' }) - ); - await xPackInfo.refreshNow(); - + license$.next(createLicense({ status: 'active' })); expect(xPackInfo.license.isActive()).to.be(true); - mockElasticsearchCluster.callWithInternalUser.returns( - Promise.reject(new Error('Uh oh')) - ); - await xPackInfo.refreshNow(); - + license$.next(createLicense({ status: undefined, error: 'error-reason' })); expect(xPackInfo.license.isActive()).to.be(false); }); it('getExpiryDateInMillis() is based on the value returned from the backend.', async () => { expect(xPackInfo.license.getExpiryDateInMillis()).to.be(1286575200000); - mockElasticsearchCluster.callWithInternalUser.returns( - getMockXPackInfoAPIResponse({ expiry_date_in_millis: 10203040 }) - ); - await xPackInfo.refreshNow(); - + license$.next(createLicense({ expiryDateInMillis: 10203040 })); expect(xPackInfo.license.getExpiryDateInMillis()).to.be(10203040); - mockElasticsearchCluster.callWithInternalUser.returns( - Promise.reject(new Error('Uh oh')) - ); - await xPackInfo.refreshNow(); - + license$.next(createLicense({ expiryDateInMillis: undefined, error: 'error-reason' })); expect(xPackInfo.license.getExpiryDateInMillis()).to.be(undefined); }); it('getType() is based on the value returned from the backend.', async () => { expect(xPackInfo.license.getType()).to.be('gold'); - mockElasticsearchCluster.callWithInternalUser.returns( - getMockXPackInfoAPIResponse({ type: 'basic' }) - ); - await xPackInfo.refreshNow(); - + license$.next(createLicense({ type: 'basic' })); expect(xPackInfo.license.getType()).to.be('basic'); - mockElasticsearchCluster.callWithInternalUser.returns( - Promise.reject(new Error('Uh oh')) - ); - await xPackInfo.refreshNow(); - + license$.next(createLicense({ type: undefined, error: 'error-reason' })); expect(xPackInfo.license.getType()).to.be(undefined); }); it('isOneOf() correctly determines if current license is presented in the specified list.', async () => { - mockElasticsearchCluster.callWithInternalUser.returns( - getMockXPackInfoAPIResponse({ mode: 'gold' }) - ); - await xPackInfo.refreshNow(); - expect(xPackInfo.license.isOneOf('gold')).to.be(true); expect(xPackInfo.license.isOneOf(['gold', 'basic'])).to.be(true); expect(xPackInfo.license.isOneOf(['platinum', 'basic'])).to.be(false); expect(xPackInfo.license.isOneOf('standard')).to.be(false); - mockElasticsearchCluster.callWithInternalUser.returns( - getMockXPackInfoAPIResponse({ mode: 'basic' }) - ); - await xPackInfo.refreshNow(); + license$.next(createLicense({ mode: 'basic' })); expect(xPackInfo.license.isOneOf('basic')).to.be(true); expect(xPackInfo.license.isOneOf(['gold', 'basic'])).to.be(true); @@ -383,18 +158,20 @@ describe('XPackInfo', () => { describe('feature', () => { let xPackInfo; + let license$; beforeEach(async () => { - mockElasticsearchCluster.callWithInternalUser.returns( - getMockXPackInfoAPIResponse({}, { - feature: { - available: false, - enabled: true - } - }) - ); - - xPackInfo = new XPackInfo(mockServer, { pollFrequencyInMillis: 1500 }); - await xPackInfo.refreshNow(); + license$ = new BehaviorSubject(createLicense({}, { + feature: { + isAvailable: false, + isEnabled: true + } + })); + xPackInfo = new XPackInfo(mockServer, { + licensing: { + license$, + refresh: () => null + } + }); }); it('isAvailable() checks whether particular feature is available.', async () => { @@ -462,10 +239,7 @@ describe('XPackInfo', () => { someAnotherCustomValue: 500100 }); - mockElasticsearchCluster.callWithInternalUser.returns( - getMockXPackInfoAPIResponse({ type: 'platinum' }) - ); - await xPackInfo.refreshNow(); + license$.next(createLicense({ type: 'platinum' })); expect(xPackInfo.toJSON().features.security).to.eql({ isXPackInfo: true, @@ -520,10 +294,8 @@ describe('XPackInfo', () => { someAnotherCustomValue: 500100 }); - mockElasticsearchCluster.callWithInternalUser.returns( - getMockXPackInfoAPIResponse({ type: 'platinum' }) - ); - await xPackInfo.refreshNow(); + + license$.next(createLicense({ type: 'platinum' })); expect(securityFeature.getLicenseCheckResults()).to.eql({ isXPackInfo: true, @@ -539,9 +311,13 @@ describe('XPackInfo', () => { }); it('getSignature() returns correct signature.', async () => { - mockElasticsearchCluster.callWithInternalUser.returns(getMockXPackInfoAPIResponse()); - const xPackInfo = new XPackInfo(mockServer, { pollFrequencyInMillis: 1500 }); - await xPackInfo.refreshNow(); + const license$ = new BehaviorSubject(createLicense()); + const xPackInfo = new XPackInfo(mockServer, { + licensing: { + license$, + refresh: () => null + } + }); expect(xPackInfo.getSignature()).to.be(getSignature({ license: { @@ -552,24 +328,21 @@ describe('XPackInfo', () => { features: {} })); - mockElasticsearchCluster.callWithInternalUser.returns( - getMockXPackInfoAPIResponse({ type: 'platinum', expiry_date_in_millis: nowDate.getTime() }) - ); - - await xPackInfo.refreshNow(); + license$.next(createLicense({ type: 'platinum', expiryDateInMillis: 20304050 })); const expectedSignature = getSignature({ license: { type: 'platinum', isActive: true, - expiryDateInMillis: nowDate.getTime() + expiryDateInMillis: 20304050 }, features: {} }); expect(xPackInfo.getSignature()).to.be(expectedSignature); // Should stay the same after refresh if nothing changed. - await xPackInfo.refreshNow(); + license$.next(createLicense({ type: 'platinum', expiryDateInMillis: 20304050 })); + expect(xPackInfo.getSignature()).to.be(expectedSignature); }); }); diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/setup_xpack_main.js b/x-pack/legacy/plugins/xpack_main/server/lib/setup_xpack_main.js index 0155def677c2..03e629a18e57 100644 --- a/x-pack/legacy/plugins/xpack_main/server/lib/setup_xpack_main.js +++ b/x-pack/legacy/plugins/xpack_main/server/lib/setup_xpack_main.js @@ -16,12 +16,16 @@ import { XPackInfo } from './xpack_info'; * @param server {Object} The Kibana server object. */ export function setupXPackMain(server) { - const info = new XPackInfo(server, { - pollFrequencyInMillis: server.config().get('xpack.xpack_main.xpack_api_polling_frequency_millis') - }); + const info = new XPackInfo(server, { licensing: server.newPlatform.setup.plugins.licensing }); server.expose('info', info); - server.expose('createXPackInfo', (options) => new XPackInfo(server, options)); + server.expose('createXPackInfo', (options) => { + const client = server.newPlatform.setup.core.elasticsearch.createClient(options.clusterSource); + const monitoringLicensing = server.newPlatform.setup.plugins.licensing.createLicensePoller(client, options.pollFrequencyInMillis); + + return new XPackInfo(server, { licensing: monitoringLicensing }); + }); + server.ext('onPreResponse', (request, h) => injectXPackInfoSignature(info, request, h)); const { registerFeature, getFeatures } = server.newPlatform.setup.plugins.features; diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info.d.ts b/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info.d.ts deleted file mode 100644 index ed7e5be3a8e9..000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info.d.ts +++ /dev/null @@ -1,37 +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 { Server } from 'hapi'; -import { XPackInfoLicense } from './xpack_info_license'; - -interface XPackFeature { - isAvailable(): boolean; - isEnabled(): boolean; - registerLicenseCheckResultsGenerator(generator: (xpackInfo: XPackInfo) => void): void; - getLicenseCheckResults(): any; -} - -export interface XPackInfoOptions { - clusterSource?: string; - pollFrequencyInMillis: number; -} - -export declare class XPackInfo { - public license: XPackInfoLicense; - - constructor(server: Server, options: XPackInfoOptions); - - public isAvailable(): boolean; - public isXpackUnavailable(): boolean; - public unavailableReason(): string | Error; - public onLicenseInfoChange(handler: () => void): void; - public refreshNow(): Promise; - - public feature(name: string): XPackFeature; - - public getSignature(): string; - public toJSON(): any; -} diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info.js b/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info.js deleted file mode 100644 index c0e4c779ba59..000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info.js +++ /dev/null @@ -1,308 +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 { createHash } from 'crypto'; -import moment from 'moment'; -import { get, has } from 'lodash'; -import { Poller } from '../../../../common/poller'; -import { XPackInfoLicense } from './xpack_info_license'; - -/** - * A helper that provides a convenient way to access XPack Info returned by Elasticsearch. - */ -export class XPackInfo { - /** - * XPack License object. - * @type {XPackInfoLicense} - * @private - */ - _license; - - /** - * Feature name <-> feature license check generator function mapping. - * @type {Map} - * @private - */ - _featureLicenseCheckResultsGenerators = new Map(); - - - /** - * Set of listener functions that will be called whenever the license - * info changes - * @type {Set} - */ - _licenseInfoChangedListeners = new Set(); - - - /** - * Cache that may contain last xpack info API response or error, json representation - * of xpack info and xpack info signature. - * @type {{response: Object|undefined, error: Object|undefined, json: Object|undefined, signature: string|undefined}} - * @private - */ - _cache = {}; - - /** - * XPack info poller. - * @type {Poller} - * @private - */ - _poller; - - /** - * XPack License instance. - * @returns {XPackInfoLicense} - */ - get license() { - return this._license; - } - - /** - * Constructs XPack info object. - * @param {Hapi.Server} server HapiJS server instance. - * @param {Object} options - * @property {string} [options.clusterSource] Type of the cluster that should be used - * to fetch XPack info (data, monitoring etc.). If not provided, `data` is used. - * @property {number} options.pollFrequencyInMillis Polling interval used to automatically - * refresh XPack Info by the internal poller. - */ - constructor(server, { clusterSource = 'data', pollFrequencyInMillis }) { - this._log = server.log.bind(server); - this._cluster = server.plugins.elasticsearch.getCluster(clusterSource); - this._clusterSource = clusterSource; - - // Create a poller that will be (re)started inside of the `refreshNow` call. - this._poller = new Poller({ - functionToPoll: () => this.refreshNow(), - trailing: true, - pollFrequencyInMillis, - continuePollingOnError: true - }); - - server.events.on('stop', () => { - this._poller.stop(); - }); - - this._license = new XPackInfoLicense( - () => this._cache.response && this._cache.response.license - ); - } - - /** - * Checks whether XPack info is available. - * @returns {boolean} - */ - isAvailable() { - return !!this._cache.response && !!this._cache.response.license; - } - - /** - * Checks whether ES was available - * @returns {boolean} - */ - isXpackUnavailable() { - return this._cache.error instanceof Error && this._cache.error.status === 400; - } - - /** - * If present, describes the reason why XPack info is not available. - * @returns {Error|string} - */ - unavailableReason() { - if (!this._cache.error && this._cache.response && !this._cache.response.license) { - return `[${this._clusterSource}] Elasticsearch cluster did not respond with license information.`; - } - - if (this.isXpackUnavailable()) { - return `X-Pack plugin is not installed on the [${this._clusterSource}] Elasticsearch cluster.`; - } - - return this._cache.error; - } - - onLicenseInfoChange(handler) { - this._licenseInfoChangedListeners.add(handler); - } - - /** - * Queries server to get the updated XPack info. - * @returns {Promise.} - */ - async refreshNow() { - this._log(['license', 'debug', 'xpack'], ( - `Calling [${this._clusterSource}] Elasticsearch _xpack API. Polling frequency: ${this._poller.getPollFrequency()}` - )); - - // We can reset polling timer since we force refresh here. - this._poller.stop(); - - try { - const response = await this._cluster.callWithInternalUser('transport.request', { - method: 'GET', - path: '/_xpack' - }); - - const licenseInfoChanged = this._hasLicenseInfoChanged(response); - - if (licenseInfoChanged) { - const licenseInfoParts = [ - `mode: ${get(response, 'license.mode')}`, - `status: ${get(response, 'license.status')}`, - ]; - - if (has(response, 'license.expiry_date_in_millis')) { - const expiryDate = moment(response.license.expiry_date_in_millis, 'x').format(); - licenseInfoParts.push(`expiry date: ${expiryDate}`); - } - - const licenseInfo = licenseInfoParts.join(' | '); - - this._log( - ['license', 'info', 'xpack'], - `Imported ${this._cache.response ? 'changed ' : ''}license information` + - ` from Elasticsearch for the [${this._clusterSource}] cluster: ${licenseInfo}` - ); - } - - this._cache = { response }; - - if (licenseInfoChanged) { - // call license info changed listeners - for (const listener of this._licenseInfoChangedListeners) { - listener(); - } - } - - } catch(error) { - this._log( - ['license', 'warning', 'xpack'], - `License information from the X-Pack plugin could not be obtained from Elasticsearch` + - ` for the [${this._clusterSource}] cluster. ${error}` - ); - - this._cache = { error }; - } - - this._poller.start(); - - return this; - } - - /** - * Returns a wrapper around XPack info that gives an access to the properties of - * the specific feature. - * @param {string} name Name of the feature to get a wrapper for. - * @returns {Object} - */ - feature(name) { - return { - /** - * Checks whether feature is available (permitted by the current license). - * @returns {boolean} - */ - isAvailable: () => { - return !!get(this._cache.response, `features.${name}.available`); - }, - - /** - * Checks whether feature is enabled (not disabled by the configuration specifically). - * @returns {boolean} - */ - isEnabled: () => { - return !!get(this._cache.response, `features.${name}.enabled`); - }, - - /** - * Registers a `generator` function that will be called with XPackInfo instance as - * argument whenever XPack info changes. Whatever `generator` returns will be stored - * in XPackInfo JSON representation and can be accessed with `getLicenseCheckResults`. - * @param {Function} generator Function to call whenever XPackInfo changes. - */ - registerLicenseCheckResultsGenerator: (generator) => { - this._featureLicenseCheckResultsGenerators.set(name, generator); - - // Since JSON representation and signature are cached we should invalidate them to - // include results from newly registered generator when they are requested. - this._cache.json = undefined; - this._cache.signature = undefined; - }, - - /** - * Returns license check results that were previously produced by the `generator` function. - * @returns {Object} - */ - getLicenseCheckResults: () => this.toJSON().features[name] - }; - } - - /** - * Extracts string md5 hash from the stringified version of license JSON representation. - * @returns {string} - */ - getSignature() { - if (this._cache.signature) { - return this._cache.signature; - } - - this._cache.signature = createHash('md5') - .update(JSON.stringify(this.toJSON())) - .digest('hex'); - - return this._cache.signature; - } - - /** - * Returns JSON representation of the license object that is suitable for serialization. - * @returns {Object} - */ - toJSON() { - if (this._cache.json) { - return this._cache.json; - } - - this._cache.json = { - license: { - type: this.license.getType(), - isActive: this.license.isActive(), - expiryDateInMillis: this.license.getExpiryDateInMillis() - }, - features: {} - }; - - // Set response elements specific to each feature. To do this, - // call the license check results generator for each feature, passing them - // the xpack info object - for (const [feature, licenseChecker] of this._featureLicenseCheckResultsGenerators) { - // return value expected to be a dictionary object. - this._cache.json.features[feature] = licenseChecker(this); - } - - return this._cache.json; - } - - /** - * Checks whether license within specified response differs from the current license. - * Comparison is based on license mode, status and expiration date. - * @param {Object} response xPack info response object returned from the backend. - * @returns {boolean} True if license within specified response object differs from - * the one we already have. - * @private - */ - _hasLicenseInfoChanged(response) { - const newLicense = get(response, 'license') || {}; - const cachedLicense = get(this._cache.response, 'license') || {}; - - if (newLicense.mode !== cachedLicense.mode) { - return true; - } - - if (newLicense.status !== cachedLicense.status) { - return true; - } - - return newLicense.expiry_date_in_millis !== cachedLicense.expiry_date_in_millis; - } -} 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 new file mode 100644 index 000000000000..fbb8929154c3 --- /dev/null +++ b/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info.ts @@ -0,0 +1,240 @@ +/* + * 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 { createHash } from 'crypto'; +import { Legacy } from 'kibana'; + +import { XPackInfoLicense } from './xpack_info_license'; + +import { LicensingPluginSetup, ILicense } from '../../../../../plugins/licensing/server'; + +export interface XPackInfoOptions { + clusterSource?: string; + pollFrequencyInMillis: number; +} + +type LicenseGeneratorCheck = (xpackInfo: XPackInfo) => any; + +export interface XPackFeature { + isAvailable(): boolean; + isEnabled(): boolean; + registerLicenseCheckResultsGenerator(generator: LicenseGeneratorCheck): void; + getLicenseCheckResults(): any; +} + +interface Deps { + licensing: LicensingPluginSetup; +} + +/** + * A helper that provides a convenient way to access XPack Info returned by Elasticsearch. + */ +export class XPackInfo { + /** + * XPack License object. + * @type {XPackInfoLicense} + * @private + */ + _license: XPackInfoLicense; + + /** + * Feature name <-> feature license check generator function mapping. + * @type {Map} + * @private + */ + _featureLicenseCheckResultsGenerators = new Map(); + + /** + * Set of listener functions that will be called whenever the license + * info changes + * @type {Set} + */ + _licenseInfoChangedListeners = new Set<() => void>(); + + /** + * Cache that may contain last xpack info API response or error, json representation + * of xpack info and xpack info signature. + * @type {{response: Object|undefined, error: Object|undefined, json: Object|undefined, signature: string|undefined}} + * @private + */ + private _cache: { + license?: ILicense; + error?: string; + json?: Record; + signature?: string; + }; + + /** + * XPack License instance. + * @returns {XPackInfoLicense} + */ + public get license() { + return this._license; + } + + private readonly licensingPlugin: LicensingPluginSetup; + + /** + * Constructs XPack info object. + * @param {Hapi.Server} server HapiJS server instance. + */ + constructor(server: Legacy.Server, deps: Deps) { + if (!deps.licensing) { + throw new Error('XPackInfo requires enabled Licensing plugin'); + } + this.licensingPlugin = deps.licensing; + + this._cache = {}; + + this.licensingPlugin.license$.subscribe((license: ILicense) => { + if (license.isActive) { + this._cache = { + license, + error: undefined, + }; + } else { + this._cache = { + license, + error: license.error, + }; + } + }); + + this._license = new XPackInfoLicense(() => this._cache.license); + } + + /** + * Checks whether XPack info is available. + * @returns {boolean} + */ + isAvailable() { + return Boolean(this._cache.license?.isAvailable); + } + + /** + * Checks whether ES was available + * @returns {boolean} + */ + isXpackUnavailable() { + return ( + this._cache.error && + this._cache.error === 'X-Pack plugin is not installed on the Elasticsearch cluster.' + ); + } + + /** + * If present, describes the reason why XPack info is not available. + * @returns {Error|string} + */ + unavailableReason() { + return this._cache.license?.getUnavailableReason(); + } + + onLicenseInfoChange(handler: () => void) { + this._licenseInfoChangedListeners.add(handler); + } + + /** + * Queries server to get the updated XPack info. + * @returns {Promise.} + */ + async refreshNow() { + await this.licensingPlugin.refresh(); + return this; + } + + /** + * Returns a wrapper around XPack info that gives an access to the properties of + * the specific feature. + * @param {string} name Name of the feature to get a wrapper for. + * @returns {Object} + */ + feature(name: string): XPackFeature { + return { + /** + * Checks whether feature is available (permitted by the current license). + * @returns {boolean} + */ + isAvailable: () => { + return Boolean(this._cache.license?.getFeature(name).isAvailable); + }, + + /** + * Checks whether feature is enabled (not disabled by the configuration specifically). + * @returns {boolean} + */ + isEnabled: () => { + return Boolean(this._cache.license?.getFeature(name).isEnabled); + }, + + /** + * Registers a `generator` function that will be called with XPackInfo instance as + * argument whenever XPack info changes. Whatever `generator` returns will be stored + * in XPackInfo JSON representation and can be accessed with `getLicenseCheckResults`. + * @param {Function} generator Function to call whenever XPackInfo changes. + */ + registerLicenseCheckResultsGenerator: (generator: LicenseGeneratorCheck) => { + this._featureLicenseCheckResultsGenerators.set(name, generator); + + // Since JSON representation and signature are cached we should invalidate them to + // include results from newly registered generator when they are requested. + this._cache.json = undefined; + this._cache.signature = undefined; + }, + + /** + * Returns license check results that were previously produced by the `generator` function. + * @returns {Object} + */ + getLicenseCheckResults: () => this.toJSON().features[name], + }; + } + + /** + * Extracts string md5 hash from the stringified version of license JSON representation. + * @returns {string} + */ + getSignature() { + if (this._cache.signature) { + return this._cache.signature; + } + + this._cache.signature = createHash('md5') + .update(JSON.stringify(this.toJSON())) + .digest('hex'); + + return this._cache.signature; + } + + /** + * Returns JSON representation of the license object that is suitable for serialization. + * @returns {Object} + */ + toJSON() { + if (this._cache.json) { + return this._cache.json; + } + + this._cache.json = { + license: { + type: this.license.getType(), + isActive: this.license.isActive(), + expiryDateInMillis: this.license.getExpiryDateInMillis(), + }, + features: {}, + }; + + // Set response elements specific to each feature. To do this, + // call the license check results generator for each feature, passing them + // the xpack info object + for (const [feature, licenseChecker] of this._featureLicenseCheckResultsGenerators) { + // return value expected to be a dictionary object. + this._cache.json.features[feature] = licenseChecker(this); + } + + return this._cache.json; + } +} diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.d.ts b/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.d.ts deleted file mode 100644 index ab09e0d73b80..000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.d.ts +++ /dev/null @@ -1,21 +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. - */ - -type LicenseType = 'oss' | 'basic' | 'trial' | 'standard' | 'basic' | 'gold' | 'platinum'; - -export declare class XPackInfoLicense { - constructor(getRawLicense: () => any); - - public getUid(): string | undefined; - public isActive(): boolean; - public getExpiryDateInMillis(): number | undefined; - public isOneOf(candidateLicenses: string[]): boolean; - public getType(): LicenseType | undefined; - public getMode(): string | undefined; - public isActiveLicense(typeChecker: (mode: string) => boolean): boolean; - public isBasic(): boolean; - public isNotBasic(): boolean; -} diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.test.js b/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.test.js index 300110744e97..9cf1e141e098 100644 --- a/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.test.js +++ b/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.test.js @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { licensingMock } from '../../../../../plugins/licensing/server/licensing.mock'; import { XPackInfoLicense } from './xpack_info_license'; function getXPackInfoLicense(getRawLicense) { @@ -24,7 +25,7 @@ describe('XPackInfoLicense', () => { test('getUid returns uid field', () => { const uid = 'abc123'; - getRawLicense.mockReturnValue({ uid }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { uid } })); expect(xpackInfoLicense.getUid()).toBe(uid); expect(getRawLicense).toHaveBeenCalledTimes(1); @@ -33,14 +34,14 @@ describe('XPackInfoLicense', () => { }); test('isActive returns true if status is active', () => { - getRawLicense.mockReturnValue({ status: 'active' }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { status: 'active' } })); expect(xpackInfoLicense.isActive()).toBe(true); expect(getRawLicense).toHaveBeenCalledTimes(1); }); test('isActive returns false if status is not active', () => { - getRawLicense.mockReturnValue({ status: 'aCtIvE' }); // needs to match exactly + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { status: 'aCtIvE' } })); // needs to match exactly expect(xpackInfoLicense.isActive()).toBe(false); expect(getRawLicense).toHaveBeenCalledTimes(1); @@ -49,7 +50,7 @@ describe('XPackInfoLicense', () => { }); test('getExpiryDateInMillis returns expiry_date_in_millis', () => { - getRawLicense.mockReturnValue({ expiry_date_in_millis: 123 }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { expiryDateInMillis: 123 } })); expect(xpackInfoLicense.getExpiryDateInMillis()).toBe(123); expect(getRawLicense).toHaveBeenCalledTimes(1); @@ -58,7 +59,7 @@ describe('XPackInfoLicense', () => { }); test('isOneOf returns true of the mode includes one of the types', () => { - getRawLicense.mockReturnValue({ mode: 'platinum' }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { mode: 'platinum' } })); expect(xpackInfoLicense.isOneOf('platinum')).toBe(true); expect(getRawLicense).toHaveBeenCalledTimes(1); @@ -78,12 +79,12 @@ describe('XPackInfoLicense', () => { }); test('getType returns the type', () => { - getRawLicense.mockReturnValue({ type: 'basic' }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { type: 'basic' } })); expect(xpackInfoLicense.getType()).toBe('basic'); expect(getRawLicense).toHaveBeenCalledTimes(1); - getRawLicense.mockReturnValue({ type: 'gold' }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { type: 'gold' } })); expect(xpackInfoLicense.getType()).toBe('gold'); expect(getRawLicense).toHaveBeenCalledTimes(2); @@ -92,12 +93,12 @@ describe('XPackInfoLicense', () => { }); test('getMode returns the mode', () => { - getRawLicense.mockReturnValue({ mode: 'basic' }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { mode: 'basic' } })); expect(xpackInfoLicense.getMode()).toBe('basic'); expect(getRawLicense).toHaveBeenCalledTimes(1); - getRawLicense.mockReturnValue({ mode: 'gold' }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { mode: 'gold' } })); expect(xpackInfoLicense.getMode()).toBe('gold'); expect(getRawLicense).toHaveBeenCalledTimes(2); @@ -108,22 +109,22 @@ describe('XPackInfoLicense', () => { test('isActiveLicense returns the true if active and typeChecker matches', () => { const expectAbc123 = type => type === 'abc123'; - getRawLicense.mockReturnValue({ status: 'active', mode: 'abc123' }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { status: 'active', mode: 'abc123' } })); expect(xpackInfoLicense.isActiveLicense(expectAbc123)).toBe(true); expect(getRawLicense).toHaveBeenCalledTimes(1); - getRawLicense.mockReturnValue({ status: 'NOTactive', mode: 'abc123' }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { status: 'NOTactive', mode: 'abc123' } })); expect(xpackInfoLicense.isActiveLicense(expectAbc123)).toBe(false); expect(getRawLicense).toHaveBeenCalledTimes(2); - getRawLicense.mockReturnValue({ status: 'NOTactive', mode: 'NOTabc123' }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { status: 'NOTactive', mode: 'NOTabc123' } })); expect(xpackInfoLicense.isActiveLicense(expectAbc123)).toBe(false); expect(getRawLicense).toHaveBeenCalledTimes(3); - getRawLicense.mockReturnValue({ status: 'active', mode: 'NOTabc123' }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { status: 'active', mode: 'NOTabc123' } })); expect(xpackInfoLicense.isActiveLicense(expectAbc123)).toBe(false); expect(getRawLicense).toHaveBeenCalledTimes(4); @@ -132,22 +133,22 @@ describe('XPackInfoLicense', () => { }); test('isBasic returns the true if active and basic', () => { - getRawLicense.mockReturnValue({ status: 'active', mode: 'basic' }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { status: 'active', mode: 'basic' } })); expect(xpackInfoLicense.isBasic()).toBe(true); expect(getRawLicense).toHaveBeenCalledTimes(1); - getRawLicense.mockReturnValue({ status: 'NOTactive', mode: 'gold' }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { status: 'NOTactive', mode: 'gold' } })); expect(xpackInfoLicense.isBasic()).toBe(false); expect(getRawLicense).toHaveBeenCalledTimes(2); - getRawLicense.mockReturnValue({ status: 'NOTactive', mode: 'trial' }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { status: 'NOTactive', mode: 'trial' } })); expect(xpackInfoLicense.isBasic()).toBe(false); expect(getRawLicense).toHaveBeenCalledTimes(3); - getRawLicense.mockReturnValue({ status: 'active', mode: 'platinum' }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { status: 'active', mode: 'platinum' } })); expect(xpackInfoLicense.isBasic()).toBe(false); expect(getRawLicense).toHaveBeenCalledTimes(4); @@ -157,22 +158,22 @@ describe('XPackInfoLicense', () => { test('isNotBasic returns the true if active and not basic', () => { - getRawLicense.mockReturnValue({ status: 'active', mode: 'platinum' }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { status: 'active', mode: 'platinum' } })); expect(xpackInfoLicense.isNotBasic()).toBe(true); expect(getRawLicense).toHaveBeenCalledTimes(1); - getRawLicense.mockReturnValue({ status: 'NOTactive', mode: 'gold' }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { status: 'NOTactive', mode: 'gold' } })); expect(xpackInfoLicense.isNotBasic()).toBe(false); expect(getRawLicense).toHaveBeenCalledTimes(2); - getRawLicense.mockReturnValue({ status: 'NOTactive', mode: 'trial' }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { status: 'NOTactive', mode: 'trial' } })); expect(xpackInfoLicense.isNotBasic()).toBe(false); expect(getRawLicense).toHaveBeenCalledTimes(3); - getRawLicense.mockReturnValue({ status: 'active', mode: 'basic' }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { status: 'active', mode: 'basic' } })); expect(xpackInfoLicense.isNotBasic()).toBe(false); expect(getRawLicense).toHaveBeenCalledTimes(4); diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.js b/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.ts similarity index 74% rename from x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.js rename to x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.ts index b87bae9e403d..e1951a4bca04 100644 --- a/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.js +++ b/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get } from 'lodash'; +import { ILicense } from '../../../../../plugins/licensing/server'; /** * "View" for XPack Info license information. @@ -15,9 +15,9 @@ export class XPackInfoLicense { * @type {Function} * @private */ - _getRawLicense = null; + _getRawLicense: () => ILicense | undefined; - constructor(getRawLicense) { + constructor(getRawLicense: () => ILicense | undefined) { this._getRawLicense = getRawLicense; } @@ -26,7 +26,7 @@ export class XPackInfoLicense { * @returns {string|undefined} */ getUid() { - return get(this._getRawLicense(), 'uid'); + return this._getRawLicense()?.uid; } /** @@ -34,7 +34,7 @@ export class XPackInfoLicense { * @returns {boolean} */ isActive() { - return get(this._getRawLicense(), 'status') === 'active'; + return Boolean(this._getRawLicense()?.isActive); } /** @@ -45,7 +45,7 @@ export class XPackInfoLicense { * @returns {number|undefined} */ getExpiryDateInMillis() { - return get(this._getRawLicense(), 'expiry_date_in_millis'); + return this._getRawLicense()?.expiryDateInMillis; } /** @@ -53,12 +53,10 @@ export class XPackInfoLicense { * @param {String} candidateLicenses List of the licenses to check against. * @returns {boolean} */ - isOneOf(candidateLicenses) { - if (!Array.isArray(candidateLicenses)) { - candidateLicenses = [candidateLicenses]; - } - - return candidateLicenses.includes(get(this._getRawLicense(), 'mode')); + isOneOf(candidateLicenses: string | string[]) { + const candidates = Array.isArray(candidateLicenses) ? candidateLicenses : [candidateLicenses]; + const mode = this._getRawLicense()?.mode; + return Boolean(mode && candidates.includes(mode)); } /** @@ -66,7 +64,7 @@ export class XPackInfoLicense { * @returns {string|undefined} */ getType() { - return get(this._getRawLicense(), 'type'); + return this._getRawLicense()?.type; } /** @@ -74,7 +72,7 @@ export class XPackInfoLicense { * @returns {string|undefined} */ getMode() { - return get(this._getRawLicense(), 'mode'); + return this._getRawLicense()?.mode; } /** @@ -83,10 +81,10 @@ export class XPackInfoLicense { * @param {Function} typeChecker The license type checker. * @returns {boolean} */ - isActiveLicense(typeChecker) { + isActiveLicense(typeChecker: (mode: string) => boolean) { const license = this._getRawLicense(); - return get(license, 'status') === 'active' && typeChecker(get(license, 'mode')); + return Boolean(license?.isActive && typeChecker(license.mode as any)); } /** diff --git a/x-pack/plugins/graph/public/plugin.ts b/x-pack/plugins/graph/public/plugin.ts index cb997ff42754..c0cec14e04d6 100644 --- a/x-pack/plugins/graph/public/plugin.ts +++ b/x-pack/plugins/graph/public/plugin.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, CoreStart } from 'kibana/public'; import { Plugin } from 'src/core/public'; import { toggleNavLink } from './services/toggle_nav_link'; -import { LicensingPluginSetup } from '../../licensing/common/types'; +import { LicensingPluginSetup } from '../../licensing/public'; import { checkLicense } from '../common/check_license'; import { FeatureCatalogueCategory, diff --git a/x-pack/plugins/graph/server/plugin.ts b/x-pack/plugins/graph/server/plugin.ts index 4b5a17707641..c7ada3af31b7 100644 --- a/x-pack/plugins/graph/server/plugin.ts +++ b/x-pack/plugins/graph/server/plugin.ts @@ -5,7 +5,7 @@ */ import { Plugin, CoreSetup } from 'src/core/server'; -import { LicensingPluginSetup } from '../../licensing/common/types'; +import { LicensingPluginSetup } from '../../licensing/server'; import { LicenseState } from './lib/license_state'; import { registerSearchRoute } from './routes/search'; import { registerExploreRoute } from './routes/explore'; diff --git a/x-pack/plugins/licensing/common/has_license_info_changed.test.ts b/x-pack/plugins/licensing/common/has_license_info_changed.test.ts index 08657826a556..18c23a41530e 100644 --- a/x-pack/plugins/licensing/common/has_license_info_changed.test.ts +++ b/x-pack/plugins/licensing/common/has_license_info_changed.test.ts @@ -13,6 +13,7 @@ function license({ error, ...customLicense }: { error?: string; [key: string]: a uid: 'uid-000000001234', status: 'active', type: 'basic', + mode: 'basic', expiryDateInMillis: 1000, }; diff --git a/x-pack/plugins/licensing/common/license.test.ts b/x-pack/plugins/licensing/common/license.test.ts index 6dbf009deabb..884327acd778 100644 --- a/x-pack/plugins/licensing/common/license.test.ts +++ b/x-pack/plugins/licensing/common/license.test.ts @@ -6,7 +6,7 @@ import { License } from './license'; import { LICENSE_CHECK_STATE } from './types'; -import { licenseMock } from './license.mock'; +import { licenseMock } from './licensing.mock'; describe('License', () => { const basicLicense = licenseMock.create(); diff --git a/x-pack/plugins/licensing/common/license.ts b/x-pack/plugins/licensing/common/license.ts index b8327ac55410..8423fed1d6a4 100644 --- a/x-pack/plugins/licensing/common/license.ts +++ b/x-pack/plugins/licensing/common/license.ts @@ -33,6 +33,7 @@ export class License implements ILicense { public readonly status?: LicenseStatus; public readonly expiryDateInMillis?: number; public readonly type?: LicenseType; + public readonly mode?: LicenseType; public readonly signature: string; /** @@ -65,6 +66,7 @@ export class License implements ILicense { this.status = license.status; this.expiryDateInMillis = license.expiryDateInMillis; this.type = license.type; + this.mode = license.mode; } this.isActive = this.status === 'active'; diff --git a/x-pack/plugins/licensing/common/license_update.test.ts b/x-pack/plugins/licensing/common/license_update.test.ts index 68660eaf2d71..e714edfbdd88 100644 --- a/x-pack/plugins/licensing/common/license_update.test.ts +++ b/x-pack/plugins/licensing/common/license_update.test.ts @@ -9,7 +9,7 @@ import { take, toArray } from 'rxjs/operators'; import { ILicense, LicenseType } from './types'; import { createLicenseUpdate } from './license_update'; -import { licenseMock } from './license.mock'; +import { licenseMock } from './licensing.mock'; const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); const stop$ = new Subject(); diff --git a/x-pack/plugins/licensing/common/license.mock.ts b/x-pack/plugins/licensing/common/licensing.mock.ts similarity index 98% rename from x-pack/plugins/licensing/common/license.mock.ts rename to x-pack/plugins/licensing/common/licensing.mock.ts index f04ebeec81bd..52721703fcb7 100644 --- a/x-pack/plugins/licensing/common/license.mock.ts +++ b/x-pack/plugins/licensing/common/licensing.mock.ts @@ -19,6 +19,7 @@ function createLicense({ uid: 'uid-000000001234', status: 'active', type: 'basic', + mode: 'basic', expiryDateInMillis: 5000, }; diff --git a/x-pack/plugins/licensing/common/types.ts b/x-pack/plugins/licensing/common/types.ts index c5d838d23d8c..840f90e083d5 100644 --- a/x-pack/plugins/licensing/common/types.ts +++ b/x-pack/plugins/licensing/common/types.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Observable } from 'rxjs'; export enum LICENSE_CHECK_STATE { Unavailable = 'UNAVAILABLE', @@ -57,6 +56,11 @@ export interface PublicLicense { * The license type, being usually one of basic, standard, gold, platinum, or trial. */ type: LicenseType; + /** + * The license type, being usually one of basic, standard, gold, platinum, or trial. + * @deprecated use 'type' instead + */ + mode: LicenseType; } /** @@ -119,6 +123,12 @@ export interface ILicense { */ type?: LicenseType; + /** + * The license type, being usually one of basic, standard, gold, platinum, or trial. + * @deprecated use 'type' instead. + */ + mode?: LicenseType; + /** * Signature of the license content. */ @@ -173,15 +183,3 @@ export interface ILicense { */ getFeature(name: string): LicenseFeature; } - -/** @public */ -export interface LicensingPluginSetup { - /** - * Steam of licensing information {@link ILicense}. - */ - license$: Observable; - /** - * Triggers licensing information re-fetch. - */ - refresh(): Promise; -} diff --git a/x-pack/plugins/licensing/public/index.ts b/x-pack/plugins/licensing/public/index.ts index 32e911bb2cdd..e19ebe7a6841 100644 --- a/x-pack/plugins/licensing/public/index.ts +++ b/x-pack/plugins/licensing/public/index.ts @@ -8,4 +8,5 @@ import { PluginInitializerContext } from 'src/core/public'; import { LicensingPlugin } from './plugin'; export * from '../common/types'; +export * from './types'; export const plugin = (context: PluginInitializerContext) => new LicensingPlugin(context); diff --git a/x-pack/plugins/licensing/public/licensing.mock.ts b/x-pack/plugins/licensing/public/licensing.mock.ts new file mode 100644 index 000000000000..e2ed07001784 --- /dev/null +++ b/x-pack/plugins/licensing/public/licensing.mock.ts @@ -0,0 +1,24 @@ +/* + * 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 { BehaviorSubject } from 'rxjs'; +import { LicensingPluginSetup } from './types'; +import { licenseMock } from '../common/licensing.mock'; + +const createSetupMock = () => { + const license = licenseMock.create(); + const mock: jest.Mocked = { + license$: new BehaviorSubject(license), + refresh: jest.fn(), + }; + mock.refresh.mockResolvedValue(license); + + return mock; +}; + +export const licensingMock = { + createSetup: createSetupMock, + createLicense: licenseMock.create, +}; diff --git a/x-pack/plugins/licensing/public/plugin.test.ts b/x-pack/plugins/licensing/public/plugin.test.ts index c356f7f5df18..4469f26836b1 100644 --- a/x-pack/plugins/licensing/public/plugin.test.ts +++ b/x-pack/plugins/licensing/public/plugin.test.ts @@ -11,12 +11,10 @@ import { LicenseType } from '../common/types'; import { LicensingPlugin, licensingSessionStorageKey } from './plugin'; import { License } from '../common/license'; -import { licenseMock } from '../common/license.mock'; +import { licenseMock } from '../common/licensing.mock'; import { coreMock } from '../../../../src/core/public/mocks'; import { HttpInterceptor } from 'src/core/public'; -const delay = (ms: number) => new Promise(res => setTimeout(res, ms)); - describe('licensing plugin', () => { let plugin: LicensingPlugin; @@ -34,15 +32,7 @@ describe('licensing plugin', () => { const coreSetup = coreMock.createSetup(); const firstLicense = licenseMock.create({ license: { uid: 'first', type: 'basic' } }); const secondLicense = licenseMock.create({ license: { uid: 'second', type: 'gold' } }); - coreSetup.http.get - .mockImplementationOnce(async () => { - await delay(100); - return firstLicense; - }) - .mockImplementationOnce(async () => { - await delay(100); - return secondLicense; - }); + coreSetup.http.get.mockResolvedValueOnce(firstLicense).mockResolvedValueOnce(secondLicense); const { license$, refresh } = await plugin.setup(coreSetup); @@ -147,7 +137,7 @@ describe('licensing plugin', () => { expect(sessionStorage.setItem.mock.calls[0][0]).toBe(licensingSessionStorageKey); expect(sessionStorage.setItem.mock.calls[0][1]).toMatchInlineSnapshot( - `"{\\"license\\":{\\"uid\\":\\"fresh\\",\\"status\\":\\"active\\",\\"type\\":\\"basic\\",\\"expiryDateInMillis\\":5000},\\"features\\":{\\"ccr\\":{\\"isEnabled\\":true,\\"isAvailable\\":true},\\"ml\\":{\\"isEnabled\\":false,\\"isAvailable\\":true}},\\"signature\\":\\"xxxxxxxxx\\"}"` + `"{\\"license\\":{\\"uid\\":\\"fresh\\",\\"status\\":\\"active\\",\\"type\\":\\"basic\\",\\"mode\\":\\"basic\\",\\"expiryDateInMillis\\":5000},\\"features\\":{\\"ccr\\":{\\"isEnabled\\":true,\\"isAvailable\\":true},\\"ml\\":{\\"isEnabled\\":false,\\"isAvailable\\":true}},\\"signature\\":\\"xxxxxxxxx\\"}"` ); const saved = JSON.parse(sessionStorage.setItem.mock.calls[0][1]); diff --git a/x-pack/plugins/licensing/public/plugin.ts b/x-pack/plugins/licensing/public/plugin.ts index c0dc0f21b90b..7d2498b0f7ff 100644 --- a/x-pack/plugins/licensing/public/plugin.ts +++ b/x-pack/plugins/licensing/public/plugin.ts @@ -7,7 +7,8 @@ import { Subject, Subscription } from 'rxjs'; import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; -import { ILicense, LicensingPluginSetup } from '../common/types'; +import { ILicense } from '../common/types'; +import { LicensingPluginSetup } from './types'; import { createLicenseUpdate } from '../common/license_update'; import { License } from '../common/license'; import { mountExpiredBanner } from './expired_banner'; diff --git a/x-pack/plugins/licensing/public/types.ts b/x-pack/plugins/licensing/public/types.ts new file mode 100644 index 000000000000..df8e50be5d15 --- /dev/null +++ b/x-pack/plugins/licensing/public/types.ts @@ -0,0 +1,20 @@ +/* + * 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 { Observable } from 'rxjs'; + +import { ILicense } from '../common/types'; + +/** @public */ +export interface LicensingPluginSetup { + /** + * Steam of licensing information {@link ILicense}. + */ + license$: Observable; + /** + * Triggers licensing information re-fetch. + */ + refresh(): Promise; +} diff --git a/x-pack/plugins/licensing/server/index.ts b/x-pack/plugins/licensing/server/index.ts index fff9ccc296ce..0e14ead7c6c5 100644 --- a/x-pack/plugins/licensing/server/index.ts +++ b/x-pack/plugins/licensing/server/index.ts @@ -10,4 +10,5 @@ import { LicensingPlugin } from './plugin'; export const plugin = (context: PluginInitializerContext) => new LicensingPlugin(context); export * from '../common/types'; +export * from './types'; export { config } from './licensing_config'; diff --git a/x-pack/plugins/licensing/server/licensing.mock.ts b/x-pack/plugins/licensing/server/licensing.mock.ts new file mode 100644 index 000000000000..b2059e36fd0c --- /dev/null +++ b/x-pack/plugins/licensing/server/licensing.mock.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 { BehaviorSubject } from 'rxjs'; +import { LicensingPluginSetup } from './types'; +import { licenseMock } from '../common/licensing.mock'; + +const createSetupMock = () => { + const license = licenseMock.create(); + const mock: jest.Mocked = { + license$: new BehaviorSubject(license), + refresh: jest.fn(), + createLicensePoller: jest.fn(), + }; + mock.refresh.mockResolvedValue(license); + mock.createLicensePoller.mockReturnValue({ + license$: mock.license$, + refresh: mock.refresh, + }); + + return mock; +}; + +export const licensingMock = { + createSetup: createSetupMock, + createLicense: licenseMock.create, +}; diff --git a/x-pack/plugins/licensing/server/licensing_config.ts b/x-pack/plugins/licensing/server/licensing_config.ts index 6cb3e8d9ef3a..d218b6438127 100644 --- a/x-pack/plugins/licensing/server/licensing_config.ts +++ b/x-pack/plugins/licensing/server/licensing_config.ts @@ -5,11 +5,22 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; +import { PluginConfigDescriptor } from 'kibana/server'; -export const config = { +const configSchema = schema.object({ + api_polling_frequency: schema.duration({ defaultValue: '30s' }), +}); + +export type LicenseConfigType = TypeOf; + +export const config: PluginConfigDescriptor = { schema: schema.object({ - pollingFrequency: schema.duration({ defaultValue: '30s' }), + api_polling_frequency: schema.duration({ defaultValue: '30s' }), }), + deprecations: ({ renameFromRoot }) => [ + renameFromRoot( + 'xpack.xpack_main.xpack_api_polling_frequency_millis', + 'xpack.licensing.api_polling_frequency' + ), + ], }; - -export type LicenseConfigType = TypeOf; diff --git a/x-pack/plugins/licensing/server/licensing_route_handler_context.test.ts b/x-pack/plugins/licensing/server/licensing_route_handler_context.test.ts index 82af786482d5..20e7f34c3ce3 100644 --- a/x-pack/plugins/licensing/server/licensing_route_handler_context.test.ts +++ b/x-pack/plugins/licensing/server/licensing_route_handler_context.test.ts @@ -5,7 +5,7 @@ */ import { BehaviorSubject } from 'rxjs'; -import { licenseMock } from '../common/license.mock'; +import { licenseMock } from '../common/licensing.mock'; import { createRouteHandlerContext } from './licensing_route_handler_context'; diff --git a/x-pack/plugins/licensing/server/mocks.ts b/x-pack/plugins/licensing/server/mocks.ts new file mode 100644 index 000000000000..237636d16301 --- /dev/null +++ b/x-pack/plugins/licensing/server/mocks.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export * from './licensing.mock'; diff --git a/x-pack/plugins/licensing/server/on_pre_response_handler.test.ts b/x-pack/plugins/licensing/server/on_pre_response_handler.test.ts index 4251e72accc9..9acfcef0ac8d 100644 --- a/x-pack/plugins/licensing/server/on_pre_response_handler.test.ts +++ b/x-pack/plugins/licensing/server/on_pre_response_handler.test.ts @@ -6,7 +6,7 @@ import { BehaviorSubject } from 'rxjs'; import { createOnPreResponseHandler } from './on_pre_response_handler'; import { httpServiceMock, httpServerMock } from '../../../../src/core/server/mocks'; -import { licenseMock } from '../common/license.mock'; +import { licenseMock } from '../common/licensing.mock'; describe('createOnPreResponseHandler', () => { it('sets license.signature header immediately for non-error responses', async () => { diff --git a/x-pack/plugins/licensing/server/plugin.test.ts b/x-pack/plugins/licensing/server/plugin.test.ts index 62b6ec6a106b..0b5a3533bd3b 100644 --- a/x-pack/plugins/licensing/server/plugin.test.ts +++ b/x-pack/plugins/licensing/server/plugin.test.ts @@ -21,11 +21,11 @@ function buildRawLicense(options: Partial = {}): RawLicense { uid: 'uid-000000001234', status: 'active', type: 'basic', + mode: 'basic', expiry_date_in_millis: 1000, }; return Object.assign(defaultRawLicense, options); } -const pollingFrequency = moment.duration(100); const flushPromises = (ms = 50) => new Promise(res => setTimeout(res, ms)); @@ -37,7 +37,7 @@ describe('licensing plugin', () => { beforeEach(() => { pluginInitContextMock = coreMock.createPluginInitializerContext({ - pollingFrequency, + api_polling_frequency: moment.duration(100), }); plugin = new LicensingPlugin(pluginInitContextMock); }); @@ -200,7 +200,7 @@ describe('licensing plugin', () => { plugin = new LicensingPlugin( coreMock.createPluginInitializerContext({ // disable polling mechanism - pollingFrequency: moment.duration(50000), + api_polling_frequency: moment.duration(50000), }) ); const dataClient = elasticsearchServiceMock.createClusterClient(); @@ -222,13 +222,88 @@ describe('licensing plugin', () => { }); }); + describe('#createLicensePoller', () => { + let plugin: LicensingPlugin; + + afterEach(async () => { + await plugin.stop(); + }); + + it(`creates a poller fetching license from passed 'clusterClient' every 'api_polling_frequency' ms`, async () => { + plugin = new LicensingPlugin( + coreMock.createPluginInitializerContext({ + api_polling_frequency: moment.duration(50000), + }) + ); + + const dataClient = elasticsearchServiceMock.createClusterClient(); + dataClient.callAsInternalUser.mockResolvedValue({ + license: buildRawLicense(), + features: {}, + }); + const coreSetup = coreMock.createSetup(); + coreSetup.elasticsearch.dataClient$ = new BehaviorSubject(dataClient); + + const { createLicensePoller, license$ } = await plugin.setup(coreSetup); + const customClient = elasticsearchServiceMock.createClusterClient(); + customClient.callAsInternalUser.mockResolvedValue({ + license: buildRawLicense({ type: 'gold' }), + features: {}, + }); + + const customPollingFrequency = 100; + const { license$: customLicense$ } = createLicensePoller( + customClient, + customPollingFrequency + ); + expect(customClient.callAsInternalUser).toHaveBeenCalledTimes(0); + + const customLicense = await customLicense$.pipe(take(1)).toPromise(); + expect(customClient.callAsInternalUser).toHaveBeenCalledTimes(1); + + await flushPromises(customPollingFrequency * 1.5); + expect(customClient.callAsInternalUser).toHaveBeenCalledTimes(2); + + expect(customLicense.isAvailable).toBe(true); + expect(customLicense.type).toBe('gold'); + + expect(await license$.pipe(take(1)).toPromise()).not.toBe(customLicense); + }); + + it('creates a poller with a manual refresh control', async () => { + plugin = new LicensingPlugin( + coreMock.createPluginInitializerContext({ + api_polling_frequency: moment.duration(100), + }) + ); + + const coreSetup = coreMock.createSetup(); + const { createLicensePoller } = await plugin.setup(coreSetup); + + const customClient = elasticsearchServiceMock.createClusterClient(); + customClient.callAsInternalUser.mockResolvedValue({ + license: buildRawLicense({ type: 'gold' }), + features: {}, + }); + + const { license$, refresh } = createLicensePoller(customClient, 10000); + expect(customClient.callAsInternalUser).toHaveBeenCalledTimes(0); + + await refresh(); + + expect(customClient.callAsInternalUser).toHaveBeenCalledTimes(1); + const license = await license$.pipe(take(1)).toPromise(); + expect(license.type).toBe('gold'); + }); + }); + describe('extends core contexts', () => { let plugin: LicensingPlugin; beforeEach(() => { plugin = new LicensingPlugin( coreMock.createPluginInitializerContext({ - pollingFrequency, + api_polling_frequency: moment.duration(100), }) ); }); @@ -257,7 +332,9 @@ describe('licensing plugin', () => { let plugin: LicensingPlugin; beforeEach(() => { - plugin = new LicensingPlugin(coreMock.createPluginInitializerContext({ pollingFrequency })); + plugin = new LicensingPlugin( + coreMock.createPluginInitializerContext({ api_polling_frequency: moment.duration(100) }) + ); }); afterEach(async () => { @@ -278,7 +355,7 @@ describe('licensing plugin', () => { it('stops polling', async () => { const plugin = new LicensingPlugin( coreMock.createPluginInitializerContext({ - pollingFrequency, + api_polling_frequency: moment.duration(100), }) ); const coreSetup = coreMock.createSetup(); diff --git a/x-pack/plugins/licensing/server/plugin.ts b/x-pack/plugins/licensing/server/plugin.ts index 64f7cc56948f..2eabd534a997 100644 --- a/x-pack/plugins/licensing/server/plugin.ts +++ b/x-pack/plugins/licensing/server/plugin.ts @@ -6,7 +6,7 @@ import { Observable, Subject, Subscription, timer } from 'rxjs'; import { take } from 'rxjs/operators'; -import moment, { Duration } from 'moment'; +import moment from 'moment'; import { createHash } from 'crypto'; import stringify from 'json-stable-stringify'; @@ -19,7 +19,8 @@ import { IClusterClient, } from 'src/core/server'; -import { ILicense, LicensingPluginSetup, PublicLicense, PublicFeatures } from '../common/types'; +import { ILicense, PublicLicense, PublicFeatures } from '../common/types'; +import { LicensingPluginSetup } from './types'; import { License } from '../common/license'; import { createLicenseUpdate } from '../common/license_update'; @@ -34,6 +35,7 @@ function normalizeServerLicense(license: RawLicense): PublicLicense { return { uid: license.uid, type: license.type, + mode: license.mode, expiryDateInMillis: license.expiry_date_in_millis, status: license.status, }; @@ -89,9 +91,13 @@ export class LicensingPlugin implements Plugin { public async setup(core: CoreSetup) { this.logger.debug('Setting up Licensing plugin'); const config = await this.config$.pipe(take(1)).toPromise(); + const pollingFrequency = config.api_polling_frequency; const dataClient = await core.elasticsearch.dataClient$.pipe(take(1)).toPromise(); - const { refresh, license$ } = this.createLicensePoller(dataClient, config.pollingFrequency); + const { refresh, license$ } = this.createLicensePoller( + dataClient, + pollingFrequency.asMilliseconds() + ); core.http.registerRouteHandlerContext('licensing', createRouteHandlerContext(license$)); @@ -101,11 +107,14 @@ export class LicensingPlugin implements Plugin { return { refresh, license$, + createLicensePoller: this.createLicensePoller.bind(this), }; } - private createLicensePoller(clusterClient: IClusterClient, pollingFrequency: Duration) { - const intervalRefresh$ = timer(0, pollingFrequency.asMilliseconds()); + private createLicensePoller(clusterClient: IClusterClient, pollingFrequency: number) { + this.logger.debug(`Polling Elasticsearch License API with frequency ${pollingFrequency}ms.`); + + const intervalRefresh$ = timer(0, pollingFrequency); const { license$, refreshManually } = createLicenseUpdate(intervalRefresh$, this.stop$, () => this.fetchLicense(clusterClient) diff --git a/x-pack/plugins/licensing/server/types.ts b/x-pack/plugins/licensing/server/types.ts index d553f090fb64..f46167a0d0a4 100644 --- a/x-pack/plugins/licensing/server/types.ts +++ b/x-pack/plugins/licensing/server/types.ts @@ -3,6 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { Observable } from 'rxjs'; +import { IClusterClient } from 'src/core/server'; import { ILicense, LicenseStatus, LicenseType } from '../common/types'; export interface ElasticsearchError extends Error { @@ -34,6 +36,7 @@ export interface RawLicense { status: LicenseStatus; expiry_date_in_millis: number; type: LicenseType; + mode: LicenseType; } declare module 'src/core/server' { @@ -43,3 +46,25 @@ declare module 'src/core/server' { }; } } + +/** @public */ +export interface LicensingPluginSetup { + /** + * Steam of licensing information {@link ILicense}. + */ + license$: Observable; + /** + * Triggers licensing information re-fetch. + */ + refresh(): Promise; + + /** + * Creates a license poller to retrieve a license data with. + * Allows a plugin to configure a cluster to retrieve data from at + * given polling frequency. + */ + createLicensePoller: ( + clusterClient: IClusterClient, + pollingFrequency: number + ) => { license$: Observable; refresh(): Promise }; +} diff --git a/x-pack/test/licensing_plugin/apis/changes.ts b/x-pack/test/licensing_plugin/apis/changes.ts index 72a8dc08ca81..cf4fecfa32d9 100644 --- a/x-pack/test/licensing_plugin/apis/changes.ts +++ b/x-pack/test/licensing_plugin/apis/changes.ts @@ -73,7 +73,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { }, async getLicense(): Promise { - // > --xpack.licensing.pollingFrequency set in test config + // > --xpack.licensing.api_polling_frequency set in test config // to wait for Kibana server to re-fetch the license from Elasticsearch await delay(1000); @@ -97,30 +97,71 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { 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(trialLicense.features?.security).to.eql({ + + 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); diff --git a/x-pack/test/licensing_plugin/config.ts b/x-pack/test/licensing_plugin/config.ts index 810dd3edc76b..9a83a6f6b5a0 100644 --- a/x-pack/test/licensing_plugin/config.ts +++ b/x-pack/test/licensing_plugin/config.ts @@ -43,7 +43,7 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { ...functionalTestsConfig.get('kbnTestServer'), serverArgs: [ ...functionalTestsConfig.get('kbnTestServer.serverArgs'), - '--xpack.licensing.pollingFrequency=300', + '--xpack.licensing.api_polling_frequency=300', ], },