Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Beats/security #22500

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,28 @@ export class KibanaFrameworkAdapter implements FrameworkAdapter {
private rootComponent: React.ReactElement<any> | 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) => {
Expand All @@ -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) {
Expand All @@ -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?`, {
Expand Down
15 changes: 14 additions & 1 deletion x-pack/plugins/beats_management/public/lib/compose/kibana.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,21 @@
* 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';
// @ts-ignore: path dynamic for kibana
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';
Expand All @@ -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,
Expand Down
9 changes: 8 additions & 1 deletion x-pack/plugins/beats_management/public/lib/compose/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export interface FrameworkRouteOptions<
path: string;
method: string | string[];
vhost?: string;
licenseRequired?: boolean;
handler: FrameworkRouteHandler<RouteRequest, RouteResponse>;
config?: {};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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) {
Expand Down Expand Up @@ -56,10 +72,17 @@ export class KibanaBackendFrameworkAdapter implements BackendFrameworkAdapter {
public registerRoute<RouteRequest extends FrameworkWrappableRequest, RouteResponse>(
route: FrameworkRouteOptions<RouteRequest, RouteResponse>
) {
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,
Expand All @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logstash pipeline UI

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

heh, yup

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.',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we update this language to say

You cannot use Beats Central 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,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Loading