diff --git a/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts index 497f47a48d57d..d17c3b1577d7f 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts @@ -23,13 +23,28 @@ export class KibanaFrameworkAdapter implements FrameworkAdapter { private rootComponent: React.ReactElement | null = null; private uiModule: IModule; private routes: any; - - constructor(uiModule: IModule, management: any, routes: any) { + private XPackInfoProvider: any; + private xpackInfo: null | any; + private notifier: any; + private kbnUrlService: any; + private chrome: any; + + constructor( + uiModule: IModule, + management: any, + routes: any, + chrome: any, + XPackInfoProvider: any, + Notifier: any + ) { this.adapterService = new KibanaAdapterServiceProvider(); this.management = management; this.uiModule = uiModule; this.routes = routes; + this.chrome = chrome; + this.XPackInfoProvider = XPackInfoProvider; this.appState = {}; + this.notifier = new Notifier({ location: 'Beats' }); } public setUISettings = (key: string, value: any) => { @@ -42,24 +57,48 @@ export class KibanaFrameworkAdapter implements FrameworkAdapter { this.rootComponent = component; }; - public registerManagementSection(pluginId: string, displayName: string, basePath: string) { - const registerSection = () => - this.management.register(pluginId, { - display: displayName, - order: 30, - }); - const getSection = () => this.management.getSection(pluginId); + public hadValidLicense() { + if (!this.xpackInfo) { + return false; + } + return this.xpackInfo.get('features.beats_management.licenseValid', false); + } - const section = this.management.hasItem(pluginId) ? getSection() : registerSection(); + public securityEnabled() { + if (!this.xpackInfo) { + return false; + } - section.register(pluginId, { - visible: true, - display: displayName, - order: 30, - url: `#${basePath}`, - }); + return this.xpackInfo.get('features.beats_management.securityEnabled', false); + } + public registerManagementSection(pluginId: string, displayName: string, basePath: string) { this.register(this.uiModule); + + this.hookAngular(() => { + if (this.hadValidLicense() && this.securityEnabled()) { + const registerSection = () => + this.management.register(pluginId, { + display: displayName, + order: 30, + }); + const getSection = () => this.management.getSection(pluginId); + + const section = this.management.hasItem(pluginId) ? getSection() : registerSection(); + + section.register(pluginId, { + visible: true, + display: displayName, + order: 30, + url: `#${basePath}`, + }); + } + + if (!this.securityEnabled()) { + this.notifier.error(this.xpackInfo.get(`features.beats_management.message`)); + this.kbnUrlService.redirect('/management'); + } + }); } private manageAngularLifecycle($scope: any, $route: any, elem: any) { @@ -83,6 +122,18 @@ export class KibanaFrameworkAdapter implements FrameworkAdapter { }); } + private hookAngular(done: () => any) { + this.chrome.dangerouslyGetActiveInjector().then(($injector: any) => { + const Private = $injector.get('Private'); + const xpackInfo = Private(this.XPackInfoProvider); + const kbnUrlService = $injector.get('kbnUrl'); + + this.xpackInfo = xpackInfo; + this.kbnUrlService = kbnUrlService; + done(); + }); + } + private register = (adapterModule: IModule) => { const adapter = this; this.routes.when(`/management/beats_management/:view?/:id?/:other?/:other2?`, { diff --git a/x-pack/plugins/beats_management/public/lib/compose/kibana.ts b/x-pack/plugins/beats_management/public/lib/compose/kibana.ts index 0d313c3184356..3488a5d23a1ef 100644 --- a/x-pack/plugins/beats_management/public/lib/compose/kibana.ts +++ b/x-pack/plugins/beats_management/public/lib/compose/kibana.ts @@ -4,6 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +// @ts-ignore +import { XPackInfoProvider } from 'plugins/xpack_main/services/xpack_info'; +// @ts-ignore import 'ui/autoload/all'; // @ts-ignore: path dynamic for kibana import chrome from 'ui/chrome'; @@ -11,8 +14,11 @@ import chrome from 'ui/chrome'; import { management } from 'ui/management'; // @ts-ignore: path dynamic for kibana import { uiModules } from 'ui/modules'; +// @ts-ignore +import { Notifier } from 'ui/notify'; // @ts-ignore: path dynamic for kibana import routes from 'ui/routes'; + import { supportedConfigs } from '../../config_schemas'; import { RestBeatsAdapter } from '../adapters/beats/rest_beats_adapter'; import { KibanaFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter'; @@ -39,7 +45,14 @@ export function compose(): FrontendLibs { }; const pluginUIModule = uiModules.get('app/beats_management'); - const framework = new KibanaFrameworkAdapter(pluginUIModule, management, routes); + const framework = new KibanaFrameworkAdapter( + pluginUIModule, + management, + routes, + chrome, + XPackInfoProvider, + Notifier + ); const libs: FrontendLibs = { framework, diff --git a/x-pack/plugins/beats_management/public/lib/compose/memory.ts b/x-pack/plugins/beats_management/public/lib/compose/memory.ts index e5f515014652c..ab56f6708123d 100644 --- a/x-pack/plugins/beats_management/public/lib/compose/memory.ts +++ b/x-pack/plugins/beats_management/public/lib/compose/memory.ts @@ -35,7 +35,14 @@ export function compose(): FrontendLibs { }; const pluginUIModule = uiModules.get('app/beats_management'); - const framework = new KibanaFrameworkAdapter(pluginUIModule, management, routes); + const framework = new KibanaFrameworkAdapter( + pluginUIModule, + management, + routes, + null, + null, + null + ); const libs: FrontendLibs = { ...domainLibs, framework, diff --git a/x-pack/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts index 014c041b11f0a..0e690b53fe8c2 100644 --- a/x-pack/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts @@ -50,6 +50,7 @@ export interface FrameworkRouteOptions< path: string; method: string | string[]; vhost?: string; + licenseRequired?: boolean; handler: FrameworkRouteHandler; config?: {}; } diff --git a/x-pack/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts index d07170acb7050..d2efd7e49bb39 100644 --- a/x-pack/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -4,6 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import Boom from 'boom'; +// @ts-ignore +import { mirrorPluginStatus } from '../../../../../../server/lib/mirror_plugin_status'; +import { PLUGIN } from '../../../../common/constants/plugin'; import { wrapRequest } from '../../../utils/wrap_request'; import { BackendFrameworkAdapter, @@ -29,6 +33,18 @@ export class KibanaBackendFrameworkAdapter implements BackendFrameworkAdapter { } this.cryptoHash = null; this.validateConfig(); + + const xpackMainPlugin = hapiServer.plugins.xpack_main; + const thisPlugin = hapiServer.plugins.beats_management; + + mirrorPluginStatus(xpackMainPlugin, thisPlugin); + xpackMainPlugin.status.once('green', () => { + // Register a function that is called whenever the xpack info changes, + // to re-compute the license check results for this plugin + xpackMainPlugin.info + .feature(PLUGIN.ID) + .registerLicenseCheckResultsGenerator(this.checkLicense); + }); } public getSetting(settingPath: string) { @@ -56,10 +72,17 @@ export class KibanaBackendFrameworkAdapter implements BackendFrameworkAdapter { public registerRoute( route: FrameworkRouteOptions ) { - const wrappedHandler = (request: any, reply: any) => route.handler(wrapRequest(request), reply); + const wrappedHandler = (licenseRequired: boolean) => (request: any, reply: any) => { + const xpackMainPlugin = this.server.plugins.xpack_main; + const licenseCheckResults = xpackMainPlugin.info.feature(PLUGIN.ID).getLicenseCheckResults(); + if (licenseRequired && !licenseCheckResults.licenseValid) { + reply(Boom.forbidden(licenseCheckResults.message)); + } + return route.handler(wrapRequest(request), reply); + }; this.server.route({ - handler: wrappedHandler, + handler: wrappedHandler(route.licenseRequired), method: route.method, path: route.path, config: route.config, @@ -78,4 +101,60 @@ export class KibanaBackendFrameworkAdapter implements BackendFrameworkAdapter { this.cryptoHash = 'xpack_beats_default_encryptionKey'; } } + + private checkLicense(xPackInfo: any) { + // If, for some reason, we cannot get the license information + // from Elasticsearch, assume worst case and disable the Logstash pipeline UI + if (!xPackInfo || !xPackInfo.isAvailable()) { + return { + securityEnabled: false, + licenseValid: false, + message: + 'You cannot manage Beats centeral management because license information is not available at this time.', + }; + } + + const VALID_LICENSE_MODES = ['trial', 'gold', 'platinum']; + + const isLicenseValid = xPackInfo.license.isOneOf(VALID_LICENSE_MODES); + const isLicenseActive = xPackInfo.license.isActive(); + const licenseType = xPackInfo.license.getType(); + const isSecurityEnabled = xPackInfo.feature('security').isEnabled(); + + // Security is not enabled in ES + if (!isSecurityEnabled) { + const message = + 'Security must be enabled in order to use Beats centeral management features.' + + ' Please set xpack.security.enabled: true in your elasticsearch.yml.'; + return { + securityEnabled: false, + licenseValid: true, + message, + }; + } + + // License is not valid + if (!isLicenseValid) { + return { + securityEnabled: true, + licenseValid: false, + message: `Your ${licenseType} license does not support Beats centeral management features. Please upgrade your license.`, + }; + } + + // License is valid but not active, we go into a read-only mode. + if (!isLicenseActive) { + return { + securityEnabled: true, + licenseValid: false, + message: `You cannot edit, create, or delete your Beats centeral management configurations because your ${licenseType} license has expired.`, + }; + } + + // License is valid and active + return { + securityEnabled: true, + licenseValid: true, + }; + } } diff --git a/x-pack/plugins/beats_management/server/rest_api/beats/enroll.ts b/x-pack/plugins/beats_management/server/rest_api/beats/enroll.ts index c1c23537218bd..8e7d2aee956ee 100644 --- a/x-pack/plugins/beats_management/server/rest_api/beats/enroll.ts +++ b/x-pack/plugins/beats_management/server/rest_api/beats/enroll.ts @@ -10,11 +10,11 @@ import { CMServerLibs } from '../../lib/lib'; import { BeatEnrollmentStatus } from '../../lib/lib'; import { wrapEsError } from '../../utils/error_wrappers'; -// TODO: add license check pre-hook // TODO: write to Kibana audit log file export const createBeatEnrollmentRoute = (libs: CMServerLibs) => ({ method: 'POST', path: '/api/beats/agent/{beatId}', + licenseRequired: true, config: { auth: false, validate: { diff --git a/x-pack/plugins/beats_management/server/rest_api/beats/list.ts b/x-pack/plugins/beats_management/server/rest_api/beats/list.ts index 3bfb249b39b6f..21f5a9d799b2d 100644 --- a/x-pack/plugins/beats_management/server/rest_api/beats/list.ts +++ b/x-pack/plugins/beats_management/server/rest_api/beats/list.ts @@ -9,10 +9,10 @@ import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; import { CMServerLibs } from '../../lib/lib'; import { wrapEsError } from '../../utils/error_wrappers'; -// TODO: add license check pre-hook export const createListAgentsRoute = (libs: CMServerLibs) => ({ method: 'GET', path: '/api/beats/agents/{listByAndValue*}', + licenseRequired: true, handler: async (request: FrameworkRequest, reply: any) => { const listByAndValueParts = request.params.listByAndValue.split('/'); let listBy: 'tag' | null = null; diff --git a/x-pack/plugins/beats_management/server/rest_api/beats/tag_assignment.ts b/x-pack/plugins/beats_management/server/rest_api/beats/tag_assignment.ts index 857b68a921597..b0a73f1706571 100644 --- a/x-pack/plugins/beats_management/server/rest_api/beats/tag_assignment.ts +++ b/x-pack/plugins/beats_management/server/rest_api/beats/tag_assignment.ts @@ -11,11 +11,11 @@ import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; import { CMServerLibs } from '../../lib/lib'; import { wrapEsError } from '../../utils/error_wrappers'; -// TODO: add license check pre-hook // TODO: write to Kibana audit log file export const createTagAssignmentsRoute = (libs: CMServerLibs) => ({ method: 'POST', path: '/api/beats/agents_tags/assignments', + licenseRequired: true, config: { validate: { payload: Joi.object({ diff --git a/x-pack/plugins/beats_management/server/rest_api/beats/tag_removal.ts b/x-pack/plugins/beats_management/server/rest_api/beats/tag_removal.ts index 0cdd9f2f28c2b..e8d395b27eaa6 100644 --- a/x-pack/plugins/beats_management/server/rest_api/beats/tag_removal.ts +++ b/x-pack/plugins/beats_management/server/rest_api/beats/tag_removal.ts @@ -9,11 +9,11 @@ import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; import { CMServerLibs } from '../../lib/lib'; import { wrapEsError } from '../../utils/error_wrappers'; -// TODO: add license check pre-hook // TODO: write to Kibana audit log file export const createTagRemovalsRoute = (libs: CMServerLibs) => ({ method: 'POST', path: '/api/beats/agents_tags/removals', + licenseRequired: true, config: { validate: { payload: Joi.object({ diff --git a/x-pack/plugins/beats_management/server/rest_api/beats/update.ts b/x-pack/plugins/beats_management/server/rest_api/beats/update.ts index bf94f83ec22d7..e33c5b5d11e53 100644 --- a/x-pack/plugins/beats_management/server/rest_api/beats/update.ts +++ b/x-pack/plugins/beats_management/server/rest_api/beats/update.ts @@ -9,11 +9,11 @@ import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; import { CMServerLibs } from '../../lib/lib'; import { wrapEsError } from '../../utils/error_wrappers'; -// TODO: add license check pre-hook // TODO: write to Kibana audit log file (include who did the verification as well) export const createBeatUpdateRoute = (libs: CMServerLibs) => ({ method: 'PUT', path: '/api/beats/agent/{beatId}', + licenseRequired: true, config: { auth: { mode: 'optional', diff --git a/x-pack/plugins/beats_management/server/rest_api/tags/delete.ts b/x-pack/plugins/beats_management/server/rest_api/tags/delete.ts index 451d800122a04..34dde929b4586 100644 --- a/x-pack/plugins/beats_management/server/rest_api/tags/delete.ts +++ b/x-pack/plugins/beats_management/server/rest_api/tags/delete.ts @@ -10,6 +10,7 @@ import { wrapEsError } from '../../utils/error_wrappers'; export const createDeleteTagsWithIdsRoute = (libs: CMServerLibs) => ({ method: 'DELETE', path: '/api/beats/tags/{tagIds}', + licenseRequired: true, handler: async (request: any, reply: any) => { const tagIdString: string = request.params.tagIds; const tagIds = tagIdString.split(',').filter((id: string) => id.length > 0); diff --git a/x-pack/plugins/beats_management/server/rest_api/tags/get.ts b/x-pack/plugins/beats_management/server/rest_api/tags/get.ts index 97aa6b413aeb3..c65640ee3eb2f 100644 --- a/x-pack/plugins/beats_management/server/rest_api/tags/get.ts +++ b/x-pack/plugins/beats_management/server/rest_api/tags/get.ts @@ -11,6 +11,7 @@ import { wrapEsError } from '../../utils/error_wrappers'; export const createGetTagsWithIdsRoute = (libs: CMServerLibs) => ({ method: 'GET', path: '/api/beats/tags/{tagIds}', + licenseRequired: true, handler: async (request: any, reply: any) => { const tagIdString: string = request.params.tagIds; const tagIds = tagIdString.split(',').filter((id: string) => id.length > 0); diff --git a/x-pack/plugins/beats_management/server/rest_api/tags/list.ts b/x-pack/plugins/beats_management/server/rest_api/tags/list.ts index 0569e29bb60db..63ab0d5b52e7e 100644 --- a/x-pack/plugins/beats_management/server/rest_api/tags/list.ts +++ b/x-pack/plugins/beats_management/server/rest_api/tags/list.ts @@ -11,6 +11,7 @@ import { wrapEsError } from '../../utils/error_wrappers'; export const createListTagsRoute = (libs: CMServerLibs) => ({ method: 'GET', path: '/api/beats/tags', + licenseRequired: true, handler: async (request: any, reply: any) => { let tags: BeatTag[]; try { diff --git a/x-pack/plugins/beats_management/server/rest_api/tags/set.ts b/x-pack/plugins/beats_management/server/rest_api/tags/set.ts index 4ec082ba99de6..a69f102b127c8 100644 --- a/x-pack/plugins/beats_management/server/rest_api/tags/set.ts +++ b/x-pack/plugins/beats_management/server/rest_api/tags/set.ts @@ -11,11 +11,11 @@ import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; import { CMServerLibs } from '../../lib/lib'; import { wrapEsError } from '../../utils/error_wrappers'; -// TODO: add license check pre-hook // TODO: write to Kibana audit log file export const createSetTagRoute = (libs: CMServerLibs) => ({ method: 'PUT', path: '/api/beats/tag/{tag}', + licenseRequired: true, config: { validate: { params: Joi.object({ diff --git a/x-pack/plugins/beats_management/server/rest_api/tokens/create.ts b/x-pack/plugins/beats_management/server/rest_api/tokens/create.ts index 15ab7b872df8a..08205bcba43dd 100644 --- a/x-pack/plugins/beats_management/server/rest_api/tokens/create.ts +++ b/x-pack/plugins/beats_management/server/rest_api/tokens/create.ts @@ -9,12 +9,13 @@ import { get } from 'lodash'; import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; import { CMServerLibs } from '../../lib/lib'; import { wrapEsError } from '../../utils/error_wrappers'; -// TODO: add license check pre-hook + // TODO: write to Kibana audit log file const DEFAULT_NUM_TOKENS = 1; export const createTokensRoute = (libs: CMServerLibs) => ({ method: 'POST', path: '/api/beats/enrollment_tokens', + licenseRequired: true, config: { validate: { payload: Joi.object({