Extract License service from CCR and Watcher into license_api_guard plugin in x-pack (#95973)

* Localize error messages.
This commit is contained in:
CJ Cenizal 2021-04-15 14:54:49 -07:00 committed by GitHub
parent 6faf07d0c0
commit 35214416f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 433 additions and 340 deletions

1
.github/CODEOWNERS vendored
View file

@ -312,6 +312,7 @@
/x-pack/plugins/console_extensions/ @elastic/es-ui
/x-pack/plugins/grokdebugger/ @elastic/es-ui
/x-pack/plugins/index_management/ @elastic/es-ui
/x-pack/plugins/license_api_guard/ @elastic/es-ui
/x-pack/plugins/license_management/ @elastic/es-ui
/x-pack/plugins/painless_lab/ @elastic/es-ui
/x-pack/plugins/remote_clusters/ @elastic/es-ui

View file

@ -444,6 +444,10 @@ the infrastructure monitoring use-case within Kibana.
|Visualization editor allowing to quickly and easily configure compelling visualizations to use on dashboards and canvas workpads.
|{kib-repo}blob/{branch}/x-pack/plugins/license_api_guard[licenseApiGuard]
|WARNING: Missing README.
|{kib-repo}blob/{branch}/x-pack/plugins/license_management/README.md[licenseManagement]
|This plugin enables users to activate a trial license, downgrade to Basic, and upload a new license.

View file

@ -16,6 +16,6 @@
],
"references": [
{ "path": "../../core/tsconfig.json" },
{ "path": "../data/tsconfig.json" },
{ "path": "../data/tsconfig.json" }
]
}

View file

@ -105,6 +105,7 @@
{ "path": "./x-pack/plugins/infra/tsconfig.json" },
{ "path": "./x-pack/plugins/ingest_pipelines/tsconfig.json" },
{ "path": "./x-pack/plugins/lens/tsconfig.json" },
{ "path": "./x-pack/plugins/license_api_guard/tsconfig.json" },
{ "path": "./x-pack/plugins/license_management/tsconfig.json" },
{ "path": "./x-pack/plugins/licensing/tsconfig.json" },
{ "path": "./x-pack/plugins/logstash/tsconfig.json" },

View file

@ -31,6 +31,7 @@
"xpack.fleet": "plugins/fleet",
"xpack.ingestPipelines": "plugins/ingest_pipelines",
"xpack.lens": "plugins/lens",
"xpack.licenseApiGuard": "plugins/license_api_guard",
"xpack.licenseMgmt": "plugins/license_management",
"xpack.licensing": "plugins/licensing",
"xpack.lists": "plugins/lists",

View file

@ -6,6 +6,7 @@
"requiredPlugins": [
"home",
"licensing",
"licenseApiGuard",
"management",
"remoteClusters",
"indexManagement",

View file

@ -7,9 +7,9 @@
import { Observable } from 'rxjs';
import { first } from 'rxjs/operators';
import { i18n } from '@kbn/i18n';
import {
CoreSetup,
CoreStart,
ILegacyCustomClusterClient,
Plugin,
Logger,
@ -19,12 +19,11 @@ import {
import { Index } from '../../index_management/server';
import { PLUGIN } from '../common/constants';
import type { Dependencies, CcrRequestHandlerContext } from './types';
import { SetupDependencies, StartDependencies, CcrRequestHandlerContext } from './types';
import { registerApiRoutes } from './routes';
import { License } from './services';
import { elasticsearchJsPlugin } from './client/elasticsearch_ccr';
import { CrossClusterReplicationConfig } from './config';
import { isEsError } from './shared_imports';
import { License, isEsError } from './shared_imports';
import { formatEsError } from './lib/format_es_error';
async function getCustomEsClient(getStartServices: CoreSetup['getStartServices']) {
@ -77,7 +76,7 @@ export class CrossClusterReplicationServerPlugin implements Plugin<void, void, a
setup(
{ http, getStartServices }: CoreSetup,
{ features, licensing, indexManagement, remoteClusters }: Dependencies
{ features, licensing, indexManagement, remoteClusters }: SetupDependencies
) {
this.config$
.pipe(first())
@ -97,22 +96,10 @@ export class CrossClusterReplicationServerPlugin implements Plugin<void, void, a
}
});
this.license.setup(
{
pluginId: PLUGIN.ID,
minimumLicenseType: PLUGIN.minimumLicenseType,
defaultErrorMessage: i18n.translate(
'xpack.crossClusterReplication.licenseCheckErrorMessage',
{
defaultMessage: 'License check failed',
}
),
},
{
licensing,
logger: this.logger,
}
);
this.license.setup({
pluginName: PLUGIN.TITLE,
logger: this.logger,
});
features.registerElasticsearchFeature({
id: 'cross_cluster_replication',
@ -147,7 +134,13 @@ export class CrossClusterReplicationServerPlugin implements Plugin<void, void, a
});
}
start() {}
start(core: CoreStart, { licensing }: StartDependencies) {
this.license.start({
pluginId: PLUGIN.ID,
minimumLicenseType: PLUGIN.minimumLicenseType,
licensing,
});
}
stop() {
if (this.ccrEsClient) {

View file

@ -8,9 +8,8 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { isEsError } from '../../../shared_imports';
import { isEsError, License } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error';
import { License } from '../../../services';
import { mockRouteContext } from '../test_lib';
import { registerCreateRoute } from './register_create_route';

View file

@ -8,9 +8,8 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { isEsError } from '../../../shared_imports';
import { isEsError, License } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error';
import { License } from '../../../services';
import { mockRouteContext } from '../test_lib';
import { registerDeleteRoute } from './register_delete_route';

View file

@ -8,9 +8,8 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { isEsError } from '../../../shared_imports';
import { isEsError, License } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error';
import { License } from '../../../services';
import { mockRouteContext } from '../test_lib';
import { registerFetchRoute } from './register_fetch_route';

View file

@ -8,9 +8,8 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { isEsError } from '../../../shared_imports';
import { isEsError, License } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error';
import { License } from '../../../services';
import { mockRouteContext } from '../test_lib';
import { registerGetRoute } from './register_get_route';

View file

@ -8,9 +8,8 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { isEsError } from '../../../shared_imports';
import { isEsError, License } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error';
import { License } from '../../../services';
import { mockRouteContext } from '../test_lib';
import { registerPauseRoute } from './register_pause_route';

View file

@ -8,9 +8,8 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { isEsError } from '../../../shared_imports';
import { isEsError, License } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error';
import { License } from '../../../services';
import { mockRouteContext } from '../test_lib';
import { registerResumeRoute } from './register_resume_route';

View file

@ -8,9 +8,8 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { isEsError } from '../../../shared_imports';
import { isEsError, License } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error';
import { License } from '../../../services';
import { mockRouteContext } from '../test_lib';
import { registerUpdateRoute } from './register_update_route';

View file

@ -8,9 +8,8 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { isEsError } from '../../../shared_imports';
import { isEsError, License } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error';
import { License } from '../../../services';
import { mockRouteContext } from '../test_lib';
import { registerCreateRoute } from './register_create_route';

View file

@ -8,9 +8,8 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { isEsError } from '../../../shared_imports';
import { isEsError, License } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error';
import { License } from '../../../services';
import { mockRouteContext } from '../test_lib';
import { registerFetchRoute } from './register_fetch_route';

View file

@ -8,9 +8,8 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { isEsError } from '../../../shared_imports';
import { isEsError, License } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error';
import { License } from '../../../services';
import { mockRouteContext } from '../test_lib';
import { registerGetRoute } from './register_get_route';

View file

@ -8,9 +8,8 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { isEsError } from '../../../shared_imports';
import { isEsError, License } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error';
import { License } from '../../../services';
import { mockRouteContext } from '../test_lib';
import { registerPauseRoute } from './register_pause_route';

View file

@ -8,9 +8,8 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { isEsError } from '../../../shared_imports';
import { isEsError, License } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error';
import { License } from '../../../services';
import { mockRouteContext } from '../test_lib';
import { registerResumeRoute } from './register_resume_route';

View file

@ -8,9 +8,8 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { isEsError } from '../../../shared_imports';
import { isEsError, License } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error';
import { License } from '../../../services';
import { mockRouteContext } from '../test_lib';
import { registerUnfollowRoute } from './register_unfollow_route';

View file

@ -5,5 +5,4 @@
* 2.0.
*/
export { License } from './license';
export { addBasePath } from './add_base_path';

View file

@ -1,91 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { Logger } from 'src/core/server';
import { KibanaRequest, KibanaResponseFactory, RequestHandler } from 'src/core/server';
import type { CcrRequestHandlerContext } from '../types';
import { LicensingPluginSetup } from '../../../licensing/server';
import { LicenseType } from '../../../licensing/common/types';
export interface LicenseStatus {
isValid: boolean;
message?: string;
}
interface SetupSettings {
pluginId: string;
minimumLicenseType: LicenseType;
defaultErrorMessage: string;
}
export class License {
private licenseStatus: LicenseStatus = {
isValid: false,
message: 'Invalid License',
};
private _isEsSecurityEnabled: boolean = false;
setup(
{ pluginId, minimumLicenseType, defaultErrorMessage }: SetupSettings,
{ licensing, logger }: { licensing: LicensingPluginSetup; logger: Logger }
) {
licensing.license$.subscribe((license) => {
const { state, message } = license.check(pluginId, minimumLicenseType);
const hasRequiredLicense = state === 'valid';
// Retrieving security checks the results of GET /_xpack as well as license state,
// so we're also checking whether the security is disabled in elasticsearch.yml.
this._isEsSecurityEnabled = license.getFeature('security').isEnabled;
if (hasRequiredLicense) {
this.licenseStatus = { isValid: true };
} else {
this.licenseStatus = {
isValid: false,
message: message || defaultErrorMessage,
};
if (message) {
logger.info(message);
}
}
});
}
guardApiRoute<P, Q, B>(handler: RequestHandler<P, Q, B, CcrRequestHandlerContext>) {
const license = this;
return function licenseCheck(
ctx: CcrRequestHandlerContext,
request: KibanaRequest<P, Q, B>,
response: KibanaResponseFactory
) {
const licenseStatus = license.getStatus();
if (!licenseStatus.isValid) {
return response.customError({
body: {
message: licenseStatus.message || '',
},
statusCode: 403,
});
}
return handler(ctx, request, response);
};
}
getStatus() {
return this.licenseStatus;
}
// eslint-disable-next-line @typescript-eslint/explicit-member-accessibility
get isEsSecurityEnabled() {
return this._isEsSecurityEnabled;
}
}

View file

@ -6,3 +6,4 @@
*/
export { isEsError } from '../../../../src/plugins/es_ui_shared/server';
export { License } from '../../license_api_guard/server';

View file

@ -7,20 +7,23 @@
import { IRouter, ILegacyScopedClusterClient, RequestHandlerContext } from 'src/core/server';
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
import { LicensingPluginSetup } from '../../licensing/server';
import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/server';
import { IndexManagementPluginSetup } from '../../index_management/server';
import { RemoteClustersPluginSetup } from '../../remote_clusters/server';
import { License } from './services';
import { isEsError } from './shared_imports';
import { License, isEsError } from './shared_imports';
import { formatEsError } from './lib/format_es_error';
export interface Dependencies {
export interface SetupDependencies {
licensing: LicensingPluginSetup;
indexManagement: IndexManagementPluginSetup;
remoteClusters: RemoteClustersPluginSetup;
features: FeaturesPluginSetup;
}
export interface StartDependencies {
licensing: LicensingPluginStart;
}
export interface RouteDependencies {
router: CcrPluginRouter;
license: License;

View file

@ -27,5 +27,6 @@
{ "path": "../../../src/plugins/kibana_react/tsconfig.json" },
{ "path": "../../../src/plugins/es_ui_shared/tsconfig.json" },
{ "path": "../../../src/plugins/data/tsconfig.json" },
{ "path": "../license_api_guard/tsconfig.json" },
]
}

View file

@ -0,0 +1,3 @@
# License API guard plugin
This plugin is used by ES UI plugins to reject API requests to plugins that are unsupported by the user's license.

View file

@ -5,4 +5,8 @@
* 2.0.
*/
export { licensePreRoutingFactory } from './license_pre_routing_factory';
module.exports = {
preset: '@kbn/test',
rootDir: '../../..',
roots: ['<rootDir>/x-pack/plugins/license_api_guard'],
};

View file

@ -0,0 +1,8 @@
{
"id": "licenseApiGuard",
"version": "0.0.1",
"kibanaVersion": "kibana",
"configPath": ["xpack", "licenseApiGuard"],
"server": true,
"ui": false
}

View file

@ -0,0 +1,16 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { License } from './license';
/** dummy plugin*/
export function plugin() {
return new (class LicenseApiGuardPlugin {
setup() {}
start() {}
})();
}

View file

@ -0,0 +1,105 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { of } from 'rxjs';
import type { KibanaRequest, RequestHandlerContext } from 'src/core/server';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { httpServerMock } from 'src/core/server/http/http_server.mocks';
import { License } from './license';
import { LicenseCheckState, licensingMock } from './shared_imports';
describe('License API guard', () => {
const pluginName = 'testPlugin';
const currentLicenseType = 'basic';
const testRoute = ({ licenseState }: { licenseState: string }) => {
const license = new License();
const logger = {
warn: jest.fn(),
};
license.setup({ pluginName, logger });
const licenseMock = licensingMock.createLicenseMock();
licenseMock.type = currentLicenseType;
licenseMock.check('test', 'basic'); // Flush default mocked state
licenseMock.check.mockReturnValue({ state: licenseState as LicenseCheckState }); // Replace with new mocked state
const licensing = {
license$: of(licenseMock),
};
license.start({
pluginId: 'id',
minimumLicenseType: 'basic',
licensing,
});
const route = jest.fn();
const guardedRoute = license.guardApiRoute(route);
const forbidden = jest.fn();
const responseMock = httpServerMock.createResponseFactory();
responseMock.forbidden = forbidden;
guardedRoute({} as RequestHandlerContext, {} as KibanaRequest, responseMock);
return {
errorResponse:
forbidden.mock.calls.length > 0
? forbidden.mock.calls[forbidden.mock.calls.length - 1][0]
: undefined,
logMesssage:
logger.warn.mock.calls.length > 0
? logger.warn.mock.calls[logger.warn.mock.calls.length - 1][0]
: undefined,
route,
};
};
describe('valid license', () => {
it('the original route is called and nothing is logged', () => {
const { errorResponse, logMesssage, route } = testRoute({ licenseState: 'valid' });
expect(errorResponse).toBeUndefined();
expect(logMesssage).toBeUndefined();
expect(route).toHaveBeenCalled();
});
});
[
{
licenseState: 'invalid',
expectedMessage: `Your ${currentLicenseType} license does not support ${pluginName}. Please upgrade your license.`,
},
{
licenseState: 'expired',
expectedMessage: `You cannot use ${pluginName} because your ${currentLicenseType} license has expired.`,
},
{
licenseState: 'unavailable',
expectedMessage: `You cannot use ${pluginName} because license information is not available at this time.`,
},
].forEach(({ licenseState, expectedMessage }) => {
describe(`${licenseState} license`, () => {
it('replies with and logs the error message', () => {
const { errorResponse, logMesssage, route } = testRoute({ licenseState });
// We depend on the call to `response.forbidden()` to generate the 403 status code,
// so we can't assert for it here.
expect(errorResponse).toEqual({
body: {
message: expectedMessage,
},
});
expect(logMesssage).toBe(expectedMessage);
expect(route).not.toHaveBeenCalled();
});
});
});
});

View file

@ -0,0 +1,113 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import {
Logger,
KibanaRequest,
KibanaResponseFactory,
RequestHandler,
RequestHandlerContext,
} from 'src/core/server';
import { ILicense, LicenseType, LicenseCheckState, LicensingPluginStart } from './shared_imports';
type LicenseLogger = Pick<Logger, 'warn'>;
type LicenseDependency = Pick<LicensingPluginStart, 'license$'>;
interface SetupSettings {
pluginName: string;
logger: LicenseLogger;
}
interface StartSettings {
pluginId: string;
minimumLicenseType: LicenseType;
licensing: LicenseDependency;
}
export class License {
private pluginName?: string;
private logger?: LicenseLogger;
private licenseCheckState: LicenseCheckState = 'unavailable';
private licenseType?: LicenseType;
private _isEsSecurityEnabled: boolean = false;
setup({ pluginName, logger }: SetupSettings) {
this.pluginName = pluginName;
this.logger = logger;
}
start({ pluginId, minimumLicenseType, licensing }: StartSettings) {
licensing.license$.subscribe((license: ILicense) => {
this.licenseType = license.type;
this.licenseCheckState = license.check(pluginId, minimumLicenseType!).state;
// Retrieving security checks the results of GET /_xpack as well as license state,
// so we're also checking whether security is disabled in elasticsearch.yml.
this._isEsSecurityEnabled = license.getFeature('security').isEnabled;
});
}
private getLicenseErrorMessage(licenseCheckState: LicenseCheckState): string {
switch (licenseCheckState) {
case 'invalid':
return i18n.translate('xpack.licenseApiGuard.license.errorUnsupportedMessage', {
defaultMessage:
'Your {licenseType} license does not support {pluginName}. Please upgrade your license.',
values: { licenseType: this.licenseType!, pluginName: this.pluginName },
});
case 'expired':
return i18n.translate('xpack.licenseApiGuard.license.errorExpiredMessage', {
defaultMessage:
'You cannot use {pluginName} because your {licenseType} license has expired.',
values: { licenseType: this.licenseType!, pluginName: this.pluginName },
});
case 'unavailable':
return i18n.translate('xpack.licenseApiGuard.license.errorUnavailableMessage', {
defaultMessage:
'You cannot use {pluginName} because license information is not available at this time.',
values: { pluginName: this.pluginName },
});
}
return i18n.translate('xpack.licenseApiGuard.license.genericErrorMessage', {
defaultMessage: 'You cannot use {pluginName} because the license check failed.',
values: { pluginName: this.pluginName },
});
}
guardApiRoute<Context extends RequestHandlerContext, Params, Query, Body>(
handler: RequestHandler<Params, Query, Body, Context>
) {
return (
ctx: Context,
request: KibanaRequest<Params, Query, Body>,
response: KibanaResponseFactory
) => {
// We'll only surface license errors if users attempt disallowed access to the API.
if (this.licenseCheckState !== 'valid') {
const licenseErrorMessage = this.getLicenseErrorMessage(this.licenseCheckState);
this.logger?.warn(licenseErrorMessage);
return response.forbidden({
body: {
message: licenseErrorMessage,
},
});
}
return handler(ctx, request, response);
};
}
public get isEsSecurityEnabled() {
return this._isEsSecurityEnabled;
}
}

View file

@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export type { ILicense, LicenseType, LicenseCheckState } from '../../licensing/common/types';
export type { LicensingPluginStart } from '../../licensing/server';
export { licensingMock } from '../../licensing/server/mocks';

View file

@ -0,0 +1,17 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"outDir": "./target/types",
"emitDeclarationOnly": true,
"declaration": true,
"declarationMap": true
},
"include": [
"server/**/*"
],
"references": [
{ "path": "../licensing/tsconfig.json" },
{ "path": "../../../src/core/tsconfig.json" }
]
}

View file

@ -7241,7 +7241,6 @@
"xpack.crossClusterReplication.followerIndexList.table.statusColumnTitle": "ステータス",
"xpack.crossClusterReplication.homeBreadcrumbTitle": "クラスター横断レプリケーション",
"xpack.crossClusterReplication.indexMgmtBadge.followerLabel": "フォロワー",
"xpack.crossClusterReplication.licenseCheckErrorMessage": "ライセンス確認失敗",
"xpack.crossClusterReplication.pauseFollowerIndex.confirmModal.cancelButtonText": "キャンセル",
"xpack.crossClusterReplication.pauseFollowerIndex.confirmModal.confirmButtonText": "複製を中止",
"xpack.crossClusterReplication.pauseFollowerIndex.confirmModal.multiplePauseDescription": "これらのフォロワーインデックスの複製が一時停止されます:",

View file

@ -7303,7 +7303,6 @@
"xpack.crossClusterReplication.followerIndexList.table.statusColumnTitle": "状态",
"xpack.crossClusterReplication.homeBreadcrumbTitle": "跨集群复制",
"xpack.crossClusterReplication.indexMgmtBadge.followerLabel": "Follower",
"xpack.crossClusterReplication.licenseCheckErrorMessage": "许可证检查失败",
"xpack.crossClusterReplication.pauseAutoFollowPatternsLabel": "暂停{total, plural, other {复制}}",
"xpack.crossClusterReplication.pauseFollowerIndex.confirmModal.cancelButtonText": "取消",
"xpack.crossClusterReplication.pauseFollowerIndex.confirmModal.confirmButtonText": "暂停复制",

View file

@ -5,6 +5,7 @@
"requiredPlugins": [
"home",
"licensing",
"licenseApiGuard",
"management",
"charts",
"data",

View file

@ -1,40 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { kibanaResponseFactory } from '../../../../../../src/core/server';
import { licensePreRoutingFactory } from './license_pre_routing_factory';
describe('license_pre_routing_factory', () => {
describe('#reportingFeaturePreRoutingFactory', () => {
let mockDeps;
let licenseStatus;
beforeEach(() => {
mockDeps = { getLicenseStatus: () => licenseStatus };
});
describe('status is not valid', () => {
it('replies with 403', () => {
licenseStatus = { hasRequired: false };
const routeWithLicenseCheck = licensePreRoutingFactory(mockDeps, () => {});
const stubRequest = {};
const response = routeWithLicenseCheck({}, stubRequest, kibanaResponseFactory);
expect(response.status).toBe(403);
});
});
describe('status is valid', () => {
it('replies with nothing', () => {
licenseStatus = { hasRequired: true };
const routeWithLicenseCheck = licensePreRoutingFactory(mockDeps, () => null);
const stubRequest = {};
const response = routeWithLicenseCheck({}, stubRequest, kibanaResponseFactory);
expect(response).toBe(null);
});
});
});
});

View file

@ -1,32 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { KibanaRequest, KibanaResponseFactory, RequestHandler } from 'kibana/server';
import type { RouteDependencies, WatcherRequestHandlerContext } from '../../types';
export const licensePreRoutingFactory = <P, Q, B, Context extends WatcherRequestHandlerContext>(
{ getLicenseStatus }: RouteDependencies,
handler: RequestHandler<P, Q, B, Context>
) => {
return function licenseCheck(
ctx: Context,
request: KibanaRequest<P, Q, B>,
response: KibanaResponseFactory
) {
const licenseStatus = getLicenseStatus();
if (!licenseStatus.hasRequired) {
return response.customError({
body: {
message: licenseStatus.message || '',
},
statusCode: 403,
});
}
return handler(ctx, request, response);
};
};

View file

@ -5,17 +5,21 @@
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import {
CoreSetup,
CoreStart,
ILegacyCustomClusterClient,
Logger,
Plugin,
PluginInitializerContext,
} from 'kibana/server';
import { PLUGIN, INDEX_NAMES } from '../common/constants';
import type {
Dependencies,
LicenseStatus,
SetupDependencies,
StartDependencies,
RouteDependencies,
WatcherRequestHandlerContext,
} from './types';
@ -28,6 +32,7 @@ import { registerWatchRoutes } from './routes/api/watch';
import { registerListFieldsRoute } from './routes/api/register_list_fields_route';
import { registerLoadHistoryRoute } from './routes/api/register_load_history_route';
import { elasticsearchJsPlugin } from './lib/elasticsearch_js_plugin';
import { License, isEsError } from './shared_imports';
async function getCustomEsClient(getStartServices: CoreSetup['getStartServices']) {
const [core] = await getStartServices();
@ -36,23 +41,20 @@ async function getCustomEsClient(getStartServices: CoreSetup['getStartServices']
}
export class WatcherServerPlugin implements Plugin<void, void, any, any> {
private readonly log: Logger;
private readonly license: License;
private readonly logger: Logger;
private watcherESClient?: ILegacyCustomClusterClient;
private licenseStatus: LicenseStatus = {
hasRequired: false,
};
constructor(ctx: PluginInitializerContext) {
this.log = ctx.logger.get();
this.logger = ctx.logger.get();
this.license = new License();
}
setup({ http, getStartServices }: CoreSetup, { licensing, features }: Dependencies) {
const router = http.createRouter<WatcherRequestHandlerContext>();
const routeDependencies: RouteDependencies = {
router,
getLicenseStatus: () => this.licenseStatus,
};
setup({ http, getStartServices }: CoreSetup, { licensing, features }: SetupDependencies) {
this.license.setup({
pluginName: PLUGIN.getI18nName(i18n),
logger: this.logger,
});
features.registerElasticsearchFeature({
id: 'watcher',
@ -90,6 +92,13 @@ export class WatcherServerPlugin implements Plugin<void, void, any, any> {
}
);
const router = http.createRouter<WatcherRequestHandlerContext>();
const routeDependencies: RouteDependencies = {
router,
license: this.license,
lib: { isEsError },
};
registerListFieldsRoute(routeDependencies);
registerLoadHistoryRoute(routeDependencies);
registerIndicesRoutes(routeDependencies);
@ -97,28 +106,15 @@ export class WatcherServerPlugin implements Plugin<void, void, any, any> {
registerSettingsRoutes(routeDependencies);
registerWatchesRoutes(routeDependencies);
registerWatchRoutes(routeDependencies);
licensing.license$.subscribe(async (license) => {
const { state, message } = license.check(PLUGIN.ID, PLUGIN.MINIMUM_LICENSE_REQUIRED);
const hasMinimumLicense = state === 'valid';
if (hasMinimumLicense && license.getFeature(PLUGIN.ID)) {
this.log.info('Enabling Watcher plugin.');
this.licenseStatus = {
hasRequired: true,
};
} else {
if (message) {
this.log.info(message);
}
this.licenseStatus = {
hasRequired: false,
message,
};
}
});
}
start() {}
start(core: CoreStart, { licensing }: StartDependencies) {
this.license.start({
pluginId: PLUGIN.ID,
minimumLicenseType: PLUGIN.MINIMUM_LICENSE_REQUIRED,
licensing,
});
}
stop() {
if (this.watcherESClient) {

View file

@ -8,9 +8,7 @@
import { schema } from '@kbn/config-schema';
import { ILegacyScopedClusterClient } from 'kibana/server';
import { reduce, size } from 'lodash';
import { isEsError } from '../../../shared_imports';
import { RouteDependencies } from '../../../types';
import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory';
const bodySchema = schema.object({ pattern: schema.string() }, { unknowns: 'allow' });
@ -65,15 +63,15 @@ function getIndices(dataClient: ILegacyScopedClusterClient, pattern: string, lim
});
}
export function registerGetRoute(deps: RouteDependencies) {
deps.router.post(
export function registerGetRoute({ router, license, lib: { isEsError } }: RouteDependencies) {
router.post(
{
path: '/api/watcher/indices',
validate: {
body: bodySchema,
},
},
licensePreRoutingFactory(deps, async (ctx, request, response) => {
license.guardApiRoute(async (ctx, request, response) => {
const { pattern } = request.body;
try {

View file

@ -6,7 +6,6 @@
*/
import { RouteDependencies } from '../../../types';
import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory';
/*
In order for the client to have the most up-to-date snapshot of the current license,
it needs to make a round-trip to the kibana server. This refresh endpoint is provided
@ -14,13 +13,13 @@ for when the client needs to check the license, but doesn't need to pull data fr
server for any reason, i.e., when adding a new watch.
*/
export function registerRefreshRoute(deps: RouteDependencies) {
deps.router.get(
export function registerRefreshRoute({ router, license }: RouteDependencies) {
router.get(
{
path: '/api/watcher/license/refresh',
validate: false,
},
licensePreRoutingFactory(deps, (ctx, request, response) => {
license.guardApiRoute((ctx, request, response) => {
return response.ok({ body: { success: true } });
})
);

View file

@ -7,10 +7,8 @@
import { schema } from '@kbn/config-schema';
import { ILegacyScopedClusterClient } from 'kibana/server';
import { isEsError } from '../../shared_imports';
// @ts-ignore
import { Fields } from '../../models/fields/index';
import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory';
import { RouteDependencies } from '../../types';
const bodySchema = schema.object({
@ -29,15 +27,19 @@ function fetchFields(dataClient: ILegacyScopedClusterClient, indexes: string[])
return dataClient.callAsCurrentUser('fieldCaps', params);
}
export function registerListFieldsRoute(deps: RouteDependencies) {
deps.router.post(
export function registerListFieldsRoute({
router,
license,
lib: { isEsError },
}: RouteDependencies) {
router.post(
{
path: '/api/watcher/fields',
validate: {
body: bodySchema,
},
},
licensePreRoutingFactory(deps, async (ctx, request, response) => {
license.guardApiRoute(async (ctx, request, response) => {
const { indexes } = request.body;
try {

View file

@ -8,10 +8,8 @@
import { schema } from '@kbn/config-schema';
import { get } from 'lodash';
import { ILegacyScopedClusterClient } from 'kibana/server';
import { isEsError } from '../../shared_imports';
import { INDEX_NAMES } from '../../../common/constants';
import { RouteDependencies } from '../../types';
import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory';
// @ts-ignore
import { WatchHistoryItem } from '../../models/watch_history_item/index';
@ -32,15 +30,19 @@ function fetchHistoryItem(dataClient: ILegacyScopedClusterClient, watchHistoryIt
});
}
export function registerLoadHistoryRoute(deps: RouteDependencies) {
deps.router.get(
export function registerLoadHistoryRoute({
router,
license,
lib: { isEsError },
}: RouteDependencies) {
router.get(
{
path: '/api/watcher/history/{id}',
validate: {
params: paramsSchema,
},
},
licensePreRoutingFactory(deps, async (ctx, request, response) => {
license.guardApiRoute(async (ctx, request, response) => {
const id = request.params.id;
try {

View file

@ -6,11 +6,9 @@
*/
import { ILegacyScopedClusterClient } from 'kibana/server';
import { isEsError } from '../../../shared_imports';
// @ts-ignore
import { Settings } from '../../../models/settings/index';
import { RouteDependencies } from '../../../types';
import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory';
function fetchClusterSettings(client: ILegacyScopedClusterClient) {
return client.callAsInternalUser('cluster.getSettings', {
@ -19,13 +17,13 @@ function fetchClusterSettings(client: ILegacyScopedClusterClient) {
});
}
export function registerLoadRoute(deps: RouteDependencies) {
deps.router.get(
export function registerLoadRoute({ router, license, lib: { isEsError } }: RouteDependencies) {
router.get(
{
path: '/api/watcher/settings',
validate: false,
},
licensePreRoutingFactory(deps, async (ctx, request, response) => {
license.guardApiRoute(async (ctx, request, response) => {
try {
const settings = await fetchClusterSettings(ctx.watcher!.client);
return response.ok({ body: Settings.fromUpstreamJson(settings).downstreamJson });

View file

@ -8,11 +8,9 @@
import { schema } from '@kbn/config-schema';
import { get } from 'lodash';
import { ILegacyScopedClusterClient } from 'kibana/server';
import { isEsError } from '../../../../shared_imports';
// @ts-ignore
import { WatchStatus } from '../../../../models/watch_status/index';
import { RouteDependencies } from '../../../../types';
import { licensePreRoutingFactory } from '../../../../lib/license_pre_routing_factory';
const paramsSchema = schema.object({
watchId: schema.string(),
@ -30,15 +28,19 @@ function acknowledgeAction(
});
}
export function registerAcknowledgeRoute(deps: RouteDependencies) {
deps.router.put(
export function registerAcknowledgeRoute({
router,
license,
lib: { isEsError },
}: RouteDependencies) {
router.put(
{
path: '/api/watcher/watch/{watchId}/action/{actionId}/acknowledge',
validate: {
params: paramsSchema,
},
},
licensePreRoutingFactory(deps, async (ctx, request, response) => {
license.guardApiRoute(async (ctx, request, response) => {
const { watchId, actionId } = request.params;
try {

View file

@ -8,9 +8,7 @@
import { schema } from '@kbn/config-schema';
import { ILegacyScopedClusterClient } from 'kibana/server';
import { get } from 'lodash';
import { isEsError } from '../../../shared_imports';
import { RouteDependencies } from '../../../types';
import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory';
// @ts-ignore
import { WatchStatus } from '../../../models/watch_status/index';
@ -24,15 +22,15 @@ const paramsSchema = schema.object({
watchId: schema.string(),
});
export function registerActivateRoute(deps: RouteDependencies) {
deps.router.put(
export function registerActivateRoute({ router, license, lib: { isEsError } }: RouteDependencies) {
router.put(
{
path: '/api/watcher/watch/{watchId}/activate',
validate: {
params: paramsSchema,
},
},
licensePreRoutingFactory(deps, async (ctx, request, response) => {
license.guardApiRoute(async (ctx, request, response) => {
const { watchId } = request.params;
try {

View file

@ -8,9 +8,7 @@
import { schema } from '@kbn/config-schema';
import { ILegacyScopedClusterClient } from 'kibana/server';
import { get } from 'lodash';
import { isEsError } from '../../../shared_imports';
import { RouteDependencies } from '../../../types';
import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory';
// @ts-ignore
import { WatchStatus } from '../../../models/watch_status/index';
@ -24,15 +22,19 @@ function deactivateWatch(dataClient: ILegacyScopedClusterClient, watchId: string
});
}
export function registerDeactivateRoute(deps: RouteDependencies) {
deps.router.put(
export function registerDeactivateRoute({
router,
license,
lib: { isEsError },
}: RouteDependencies) {
router.put(
{
path: '/api/watcher/watch/{watchId}/deactivate',
validate: {
params: paramsSchema,
},
},
licensePreRoutingFactory(deps, async (ctx, request, response) => {
license.guardApiRoute(async (ctx, request, response) => {
const { watchId } = request.params;
try {

View file

@ -7,9 +7,7 @@
import { schema } from '@kbn/config-schema';
import { ILegacyScopedClusterClient } from 'kibana/server';
import { isEsError } from '../../../shared_imports';
import { RouteDependencies } from '../../../types';
import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory';
const paramsSchema = schema.object({
watchId: schema.string(),
@ -21,15 +19,15 @@ function deleteWatch(dataClient: ILegacyScopedClusterClient, watchId: string) {
});
}
export function registerDeleteRoute(deps: RouteDependencies) {
deps.router.delete(
export function registerDeleteRoute({ router, license, lib: { isEsError } }: RouteDependencies) {
router.delete(
{
path: '/api/watcher/watch/{watchId}',
validate: {
params: paramsSchema,
},
},
licensePreRoutingFactory(deps, async (ctx, request, response) => {
license.guardApiRoute(async (ctx, request, response) => {
const { watchId } = request.params;
try {

View file

@ -8,8 +8,6 @@
import { schema } from '@kbn/config-schema';
import { ILegacyScopedClusterClient } from 'kibana/server';
import { get } from 'lodash';
import { isEsError } from '../../../shared_imports';
import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory';
import { RouteDependencies } from '../../../types';
// @ts-ignore
@ -33,15 +31,15 @@ function executeWatch(dataClient: ILegacyScopedClusterClient, executeDetails: an
});
}
export function registerExecuteRoute(deps: RouteDependencies) {
deps.router.put(
export function registerExecuteRoute({ router, license, lib: { isEsError } }: RouteDependencies) {
router.put(
{
path: '/api/watcher/watch/execute',
validate: {
body: bodySchema,
},
},
licensePreRoutingFactory(deps, async (ctx, request, response) => {
license.guardApiRoute(async (ctx, request, response) => {
const executeDetails = ExecuteDetails.fromDownstreamJson(request.body.executeDetails);
const watch = Watch.fromDownstreamJson(request.body.watch);

View file

@ -10,9 +10,7 @@ import { ILegacyScopedClusterClient } from 'kibana/server';
import { get } from 'lodash';
import { fetchAllFromScroll } from '../../../lib/fetch_all_from_scroll';
import { INDEX_NAMES, ES_SCROLL_SETTINGS } from '../../../../common/constants';
import { isEsError } from '../../../shared_imports';
import { RouteDependencies } from '../../../types';
import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory';
// @ts-ignore
import { WatchHistoryItem } from '../../../models/watch_history_item/index';
@ -50,8 +48,8 @@ function fetchHistoryItems(dataClient: ILegacyScopedClusterClient, watchId: any,
.then((response: any) => fetchAllFromScroll(response, dataClient));
}
export function registerHistoryRoute(deps: RouteDependencies) {
deps.router.get(
export function registerHistoryRoute({ router, license, lib: { isEsError } }: RouteDependencies) {
router.get(
{
path: '/api/watcher/watch/{watchId}/history',
validate: {
@ -59,7 +57,7 @@ export function registerHistoryRoute(deps: RouteDependencies) {
query: querySchema,
},
},
licensePreRoutingFactory(deps, async (ctx, request, response) => {
license.guardApiRoute(async (ctx, request, response) => {
const { watchId } = request.params;
const { startTime } = request.query;

View file

@ -8,8 +8,6 @@
import { schema } from '@kbn/config-schema';
import { ILegacyScopedClusterClient } from 'kibana/server';
import { get } from 'lodash';
import { isEsError } from '../../../shared_imports';
import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory';
// @ts-ignore
import { Watch } from '../../../models/watch/index';
import { RouteDependencies } from '../../../types';
@ -24,15 +22,15 @@ function fetchWatch(dataClient: ILegacyScopedClusterClient, watchId: string) {
});
}
export function registerLoadRoute(deps: RouteDependencies) {
deps.router.get(
export function registerLoadRoute({ router, license, lib: { isEsError } }: RouteDependencies) {
router.get(
{
path: '/api/watcher/watch/{id}',
validate: {
params: paramsSchema,
},
},
licensePreRoutingFactory(deps, async (ctx, request, response) => {
license.guardApiRoute(async (ctx, request, response) => {
const id = request.params.id;
try {

View file

@ -9,9 +9,7 @@ import { schema } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n';
import { WATCH_TYPES } from '../../../../common/constants';
import { serializeJsonWatch, serializeThresholdWatch } from '../../../../common/lib/serialization';
import { isEsError } from '../../../shared_imports';
import { RouteDependencies } from '../../../types';
import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory';
const paramsSchema = schema.object({
id: schema.string(),
@ -26,8 +24,8 @@ const bodySchema = schema.object(
{ unknowns: 'allow' }
);
export function registerSaveRoute(deps: RouteDependencies) {
deps.router.put(
export function registerSaveRoute({ router, license, lib: { isEsError } }: RouteDependencies) {
router.put(
{
path: '/api/watcher/watch/{id}',
validate: {
@ -35,7 +33,7 @@ export function registerSaveRoute(deps: RouteDependencies) {
body: bodySchema,
},
},
licensePreRoutingFactory(deps, async (ctx, request, response) => {
license.guardApiRoute(async (ctx, request, response) => {
const { id } = request.params;
const { type, isNew, isActive, ...watchConfig } = request.body;

View file

@ -7,9 +7,7 @@
import { schema } from '@kbn/config-schema';
import { ILegacyScopedClusterClient } from 'kibana/server';
import { isEsError } from '../../../shared_imports';
import { RouteDependencies } from '../../../types';
import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory';
// @ts-ignore
import { Watch } from '../../../models/watch/index';
@ -33,15 +31,15 @@ function fetchVisualizeData(dataClient: ILegacyScopedClusterClient, index: any,
return dataClient.callAsCurrentUser('search', params);
}
export function registerVisualizeRoute(deps: RouteDependencies) {
deps.router.post(
export function registerVisualizeRoute({ router, license, lib: { isEsError } }: RouteDependencies) {
router.post(
{
path: '/api/watcher/watch/visualize',
validate: {
body: bodySchema,
},
},
licensePreRoutingFactory(deps, async (ctx, request, response) => {
license.guardApiRoute(async (ctx, request, response) => {
const watch = Watch.fromDownstreamJson(request.body.watch);
const options = VisualizeOptions.fromDownstreamJson(request.body.options);
const body = watch.getVisualizeQuery(options);

View file

@ -8,7 +8,6 @@
import { schema } from '@kbn/config-schema';
import { ILegacyScopedClusterClient } from 'kibana/server';
import { RouteDependencies } from '../../../types';
import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory';
const bodySchema = schema.object({
watchIds: schema.arrayOf(schema.string()),
@ -42,15 +41,15 @@ function deleteWatches(dataClient: ILegacyScopedClusterClient, watchIds: string[
});
}
export function registerDeleteRoute(deps: RouteDependencies) {
deps.router.post(
export function registerDeleteRoute({ router, license }: RouteDependencies) {
router.post(
{
path: '/api/watcher/watches/delete',
validate: {
body: bodySchema,
},
},
licensePreRoutingFactory(deps, async (ctx, request, response) => {
license.guardApiRoute(async (ctx, request, response) => {
const results = await deleteWatches(ctx.watcher!.client, request.body.watchIds);
return response.ok({ body: { results } });
})

View file

@ -9,9 +9,7 @@ import { ILegacyScopedClusterClient } from 'kibana/server';
import { get } from 'lodash';
import { fetchAllFromScroll } from '../../../lib/fetch_all_from_scroll';
import { INDEX_NAMES, ES_SCROLL_SETTINGS } from '../../../../common/constants';
import { isEsError } from '../../../shared_imports';
import { RouteDependencies } from '../../../types';
import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory';
// @ts-ignore
import { Watch } from '../../../models/watch/index';
@ -30,13 +28,13 @@ function fetchWatches(dataClient: ILegacyScopedClusterClient) {
.then((response: any) => fetchAllFromScroll(response, dataClient));
}
export function registerListRoute(deps: RouteDependencies) {
deps.router.get(
export function registerListRoute({ router, license, lib: { isEsError } }: RouteDependencies) {
router.get(
{
path: '/api/watcher/watches',
validate: false,
},
licensePreRoutingFactory(deps, async (ctx, request, response) => {
license.guardApiRoute(async (ctx, request, response) => {
try {
const hits = await fetchWatches(ctx.watcher!.client);
const watches = hits.map((hit: any) => {

View file

@ -6,3 +6,4 @@
*/
export { isEsError } from '../../../../src/plugins/es_ui_shared/server';
export { License } from '../../license_api_guard/server';

View file

@ -7,13 +7,18 @@
import type { ILegacyScopedClusterClient, IRouter, RequestHandlerContext } from 'src/core/server';
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
import { LicensingPluginSetup } from '../../licensing/server';
import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/server';
import { License, isEsError } from './shared_imports';
export interface Dependencies {
export interface SetupDependencies {
licensing: LicensingPluginSetup;
features: FeaturesPluginSetup;
}
export interface StartDependencies {
licensing: LicensingPluginStart;
}
export interface ServerShim {
route: any;
plugins: {
@ -23,12 +28,10 @@ export interface ServerShim {
export interface RouteDependencies {
router: WatcherRouter;
getLicenseStatus: () => LicenseStatus;
}
export interface LicenseStatus {
hasRequired: boolean;
message?: string;
license: License;
lib: {
isEsError: typeof isEsError;
};
}
/**

View file

@ -23,6 +23,7 @@
{ "path": "../../../src/plugins/data/tsconfig.json" },
{ "path": "../../../src/plugins/kibana_react/tsconfig.json" },
{ "path": "../../../src/plugins/es_ui_shared/tsconfig.json" },
{ "path": "../license_api_guard/tsconfig.json" },
{ "path": "../licensing/tsconfig.json" },
{ "path": "../features/tsconfig.json" },
]