mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
Move base feature controls functionality from XPack Main plugin to a dedicated XPack Features plugin (#44664)
This commit is contained in:
parent
b7aeaf5ad3
commit
9d69b72a5f
83 changed files with 1190 additions and 976 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -38,6 +38,7 @@
|
|||
/src/legacy/server/saved_objects/ @elastic/kibana-platform
|
||||
/src/legacy/ui/public/saved_objects @elastic/kibana-platform
|
||||
/config/kibana.yml @elastic/kibana-platform
|
||||
/x-pack/plugins/features/ @elastic/kibana-platform
|
||||
|
||||
# Security
|
||||
/x-pack/legacy/plugins/security/ @elastic/kibana-security
|
||||
|
|
|
@ -11,7 +11,7 @@ experimental[This API is *experimental* and may be changed or removed completely
|
|||
[[features-api-get-request]]
|
||||
==== Request
|
||||
|
||||
`GET /api/features/v1`
|
||||
`GET /api/features`
|
||||
|
||||
[[features-api-get-codes]]
|
||||
==== Response code
|
||||
|
|
|
@ -22,7 +22,7 @@ init(server) {
|
|||
-----------
|
||||
|
||||
===== Feature details
|
||||
Registering a feature consists of the following fields. For more information, consult the {repo}blob/{branch}/x-pack/legacy/plugins/xpack_main/server/lib/feature_registry/feature_registry.ts[feature registry interface].
|
||||
Registering a feature consists of the following fields. For more information, consult the {repo}blob/{branch}/x-pack/plugins/features/server/feature_registry.ts[feature registry interface].
|
||||
|
||||
|
||||
[cols="1a, 1a, 1a, 1a"]
|
||||
|
@ -45,7 +45,7 @@ Registering a feature consists of the following fields. For more information, co
|
|||
|An array of applications this feature enables. Typically, all of your plugin's apps (from `uiExports`) will be included here.
|
||||
|
||||
|`privileges` (required)
|
||||
|{repo}blob/{branch}/x-pack/legacy/plugins/xpack_main/server/lib/feature_registry/feature_registry.ts[`FeatureWithAllOrReadPrivileges`].
|
||||
|{repo}blob/{branch}/x-pack/plugins/features/server/feature.ts[`FeatureWithAllOrReadPrivileges`].
|
||||
|see examples below
|
||||
|The set of privileges this feature requires to function.
|
||||
|
||||
|
@ -63,7 +63,7 @@ Registering a feature consists of the following fields. For more information, co
|
|||
===== Privilege definition
|
||||
The `privileges` section of feature registration allows plugins to implement read/write and read-only modes for their applications.
|
||||
|
||||
For a full explanation of fields and options, consult the {repo}blob/{branch}/x-pack/legacy/plugins/xpack_main/server/lib/feature_registry/feature_registry.ts[feature registry interface].
|
||||
For a full explanation of fields and options, consult the {repo}blob/{branch}/x-pack/plugins/features/server/feature_registry.ts[feature registry interface].
|
||||
|
||||
==== Using UI Capabilities
|
||||
|
||||
|
@ -142,7 +142,7 @@ init(server) {
|
|||
const xpackMainPlugin = server.plugins.xpack_main;
|
||||
xpackMainPlugin.registerFeature({
|
||||
id: 'dev_tools',
|
||||
name: i18n.translate('xpack.main.featureRegistry.devToolsFeatureName', {
|
||||
name: i18n.translate('xpack.features.devToolsFeatureName', {
|
||||
defaultMessage: 'Dev Tools',
|
||||
}),
|
||||
icon: 'devToolsApp',
|
||||
|
@ -167,7 +167,7 @@ init(server) {
|
|||
ui: ['show'],
|
||||
},
|
||||
},
|
||||
privilegesTooltip: i18n.translate('xpack.main.featureRegistry.devToolsPrivilegesTooltip', {
|
||||
privilegesTooltip: i18n.translate('xpack.features.devToolsPrivilegesTooltip', {
|
||||
defaultMessage:
|
||||
'User should also be granted the appropriate Elasticsearch cluster and index privileges',
|
||||
}),
|
||||
|
|
|
@ -99,4 +99,5 @@ export const httpServiceMock = {
|
|||
createSetupContract: createSetupContractMock,
|
||||
createOnPreAuthToolkit: createOnPreAuthToolkitMock,
|
||||
createAuthToolkit: createAuthToolkitMock,
|
||||
createRouter: createRouterMock,
|
||||
};
|
||||
|
|
8
src/plugins/timelion/kibana.json
Normal file
8
src/plugins/timelion/kibana.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"id": "timelion",
|
||||
"version": "8.0.0",
|
||||
"kibanaVersion": "kibana",
|
||||
"configPath": ["timelion"],
|
||||
"server": true,
|
||||
"ui": false
|
||||
}
|
28
src/plugins/timelion/server/config.ts
Normal file
28
src/plugins/timelion/server/config.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
export const ConfigSchema = schema.object(
|
||||
{
|
||||
ui: schema.object({ enabled: schema.boolean({ defaultValue: false }) }),
|
||||
},
|
||||
// This option should be removed as soon as we entirely migrate config from legacy Timelion plugin.
|
||||
{ allowUnknowns: true }
|
||||
);
|
28
src/plugins/timelion/server/index.ts
Normal file
28
src/plugins/timelion/server/index.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { PluginInitializerContext } from '../../../../src/core/server';
|
||||
import { ConfigSchema } from './config';
|
||||
import { Plugin } from './plugin';
|
||||
|
||||
export { PluginSetupContract } from './plugin';
|
||||
|
||||
export const config = { schema: ConfigSchema };
|
||||
export const plugin = (initializerContext: PluginInitializerContext) =>
|
||||
new Plugin(initializerContext);
|
55
src/plugins/timelion/server/plugin.ts
Normal file
55
src/plugins/timelion/server/plugin.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { first } from 'rxjs/operators';
|
||||
import { TypeOf } from '@kbn/config-schema';
|
||||
import { PluginInitializerContext, RecursiveReadonly } from '../../../../src/core/server';
|
||||
import { deepFreeze } from '../../../../src/core/utils';
|
||||
import { ConfigSchema } from './config';
|
||||
|
||||
/**
|
||||
* Describes public Timelion plugin contract returned at the `setup` stage.
|
||||
*/
|
||||
export interface PluginSetupContract {
|
||||
uiEnabled: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents Timelion Plugin instance that will be managed by the Kibana plugin system.
|
||||
*/
|
||||
export class Plugin {
|
||||
constructor(private readonly initializerContext: PluginInitializerContext) {}
|
||||
|
||||
public async setup(): Promise<RecursiveReadonly<PluginSetupContract>> {
|
||||
const config = await this.initializerContext.config
|
||||
.create<TypeOf<typeof ConfigSchema>>()
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
|
||||
return deepFreeze({ uiEnabled: config.ui.enabled });
|
||||
}
|
||||
|
||||
public start() {
|
||||
this.initializerContext.logger.get().debug('Starting plugin');
|
||||
}
|
||||
|
||||
public stop() {
|
||||
this.initializerContext.logger.get().debug('Stopping plugin');
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@
|
|||
"xpack.code": "legacy/plugins/code",
|
||||
"xpack.crossClusterReplication": "legacy/plugins/cross_cluster_replication",
|
||||
"xpack.dashboardMode": "legacy/plugins/dashboard_mode",
|
||||
"xpack.features": "plugins/features",
|
||||
"xpack.fileUpload": "legacy/plugins/file_upload",
|
||||
"xpack.graph": "legacy/plugins/graph",
|
||||
"xpack.grokDebugger": "legacy/plugins/grokdebugger",
|
||||
|
|
|
@ -9,7 +9,7 @@ import React from 'react';
|
|||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { UICapabilities } from 'ui/capabilities';
|
||||
import { Space } from '../../../../../../spaces/common/model/space';
|
||||
import { Feature } from '../../../../../../xpack_main/types';
|
||||
import { Feature } from '../../../../../../../../plugins/features/server';
|
||||
import { RawKibanaPrivileges, Role } from '../../../../../common/model';
|
||||
import { actionsFactory } from '../../../../../server/lib/authorization/actions';
|
||||
import { privilegesFactory } from '../../../../../server/lib/authorization/privileges';
|
||||
|
|
|
@ -23,7 +23,7 @@ import React, { ChangeEvent, Component, Fragment, HTMLProps } from 'react';
|
|||
import { UICapabilities } from 'ui/capabilities';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { Space } from '../../../../../../spaces/common/model/space';
|
||||
import { Feature } from '../../../../../../xpack_main/types';
|
||||
import { Feature } from '../../../../../../../../plugins/features/server';
|
||||
import {
|
||||
KibanaPrivileges,
|
||||
RawKibanaPrivileges,
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react';
|
||||
import _ from 'lodash';
|
||||
import React, { Component } from 'react';
|
||||
import { Feature } from '../../../../../../../../../xpack_main/types';
|
||||
import { Feature } from '../../../../../../../../../../../plugins/features/server';
|
||||
import { FeaturesPrivileges, KibanaPrivileges, Role } from '../../../../../../../../common/model';
|
||||
import {
|
||||
AllowedPrivilege,
|
||||
|
|
|
@ -8,7 +8,7 @@ import { InjectedIntl } from '@kbn/i18n/react';
|
|||
import React, { Component } from 'react';
|
||||
import { UICapabilities } from 'ui/capabilities';
|
||||
import { Space } from '../../../../../../../../spaces/common/model/space';
|
||||
import { Feature } from '../../../../../../../../xpack_main/types';
|
||||
import { Feature } from '../../../../../../../../../../plugins/features/server';
|
||||
import { KibanaPrivileges, Role } from '../../../../../../../common/model';
|
||||
import { KibanaPrivilegeCalculatorFactory } from '../../../../../../lib/kibana_privilege_calculator';
|
||||
import { RoleValidator } from '../../../lib/validate_role';
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { EuiButtonGroup, EuiButtonGroupProps, EuiComboBox, EuiSuperSelect } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { Feature } from '../../../../../../../../../xpack_main/types';
|
||||
import { Feature } from '../../../../../../../../../../../plugins/features/server';
|
||||
import { KibanaPrivileges, Role } from '../../../../../../../../common/model';
|
||||
import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator';
|
||||
import { SimplePrivilegeSection } from './simple_privilege_section';
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
|
||||
import { Feature } from '../../../../../../../../../xpack_main/types';
|
||||
import { Feature } from '../../../../../../../../../../../plugins/features/server';
|
||||
import { KibanaPrivileges, Role, RoleKibanaPrivilege } from '../../../../../../../../common/model';
|
||||
import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator';
|
||||
import { isGlobalPrivilegeDefinition } from '../../../../../../../lib/privilege_utils';
|
||||
|
|
|
@ -8,7 +8,7 @@ import { EuiButtonEmpty, EuiInMemoryTable } from '@elastic/eui';
|
|||
import React from 'react';
|
||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { Space } from '../../../../../../../../../spaces/common/model/space';
|
||||
import { Feature } from '../../../../../../../../../xpack_main/types';
|
||||
import { Feature } from '../../../../../../../../../../../plugins/features/server';
|
||||
import { KibanaPrivileges, Role } from '../../../../../../../../common/model';
|
||||
import { KibanaPrivilegeCalculatorFactory } from '../../../../../../..//lib/kibana_privilege_calculator';
|
||||
import { PrivilegeMatrix } from './privilege_matrix';
|
||||
|
|
|
@ -24,7 +24,7 @@ import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react';
|
|||
import React, { Component, Fragment } from 'react';
|
||||
import { Space } from '../../../../../../../../../spaces/common/model/space';
|
||||
import { SpaceAvatar } from '../../../../../../../../../spaces/public/components';
|
||||
import { Feature } from '../../../../../../../../../xpack_main/types';
|
||||
import { Feature } from '../../../../../../../../../../../plugins/features/server';
|
||||
import { FeaturesPrivileges, Role } from '../../../../../../../../common/model';
|
||||
import { CalculatedPrivilege } from '../../../../../../../lib/kibana_privilege_calculator';
|
||||
import { isGlobalPrivilegeDefinition } from '../../../../../../../lib/privilege_utils';
|
||||
|
|
|
@ -25,7 +25,7 @@ import {
|
|||
import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { Space } from '../../../../../../../../../spaces/common/model/space';
|
||||
import { Feature } from '../../../../../../../../../xpack_main/types';
|
||||
import { Feature } from '../../../../../../../../../../../plugins/features/server';
|
||||
import { KibanaPrivileges, Role } from '../../../../../../../../common/model';
|
||||
import {
|
||||
AllowedPrivilege,
|
||||
|
|
|
@ -16,7 +16,7 @@ import _ from 'lodash';
|
|||
import React, { Component, Fragment } from 'react';
|
||||
import { UICapabilities } from 'ui/capabilities';
|
||||
import { Space } from '../../../../../../../../../spaces/common/model/space';
|
||||
import { Feature } from '../../../../../../../../../xpack_main/types';
|
||||
import { Feature } from '../../../../../../../../../../../plugins/features/server';
|
||||
import { KibanaPrivileges, Role } from '../../../../../../../../common/model';
|
||||
import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator';
|
||||
import { isReservedRole } from '../../../../../../../lib/role_utils';
|
||||
|
|
|
@ -92,7 +92,7 @@ const routeDefinition = (action) => ({
|
|||
return kfetch({ method: 'get', pathname: '/api/security/v1/esPrivileges/builtin' });
|
||||
},
|
||||
features() {
|
||||
return kfetch({ method: 'get', pathname: '/api/features/v1' }).catch(e => {
|
||||
return kfetch({ method: 'get', pathname: '/api/features' }).catch(e => {
|
||||
// TODO: This check can be removed once all of these `resolve` entries are moved out of Angular and into the React app.
|
||||
const unauthorizedForFeatures = _.get(e, 'body.statusCode') === 404;
|
||||
if (unauthorizedForFeatures) {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
import { isString } from 'lodash';
|
||||
import { UICapabilities } from 'ui/capabilities';
|
||||
import { uiCapabilitiesRegex } from '../../../../../xpack_main/types';
|
||||
import { uiCapabilitiesRegex } from '../../../../../../../plugins/features/server';
|
||||
|
||||
export class UIActions {
|
||||
private readonly prefix: string;
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { Server } from 'hapi';
|
||||
import { AuthorizationService } from './service';
|
||||
|
||||
import { Feature } from '../../../../xpack_main/types';
|
||||
import { Feature } from '../../../../../../plugins/features/server';
|
||||
import { XPackMainPlugin } from '../../../../xpack_main/xpack_main';
|
||||
import { actionsFactory } from './actions';
|
||||
import { initAppAuthorization } from './app_authorization';
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { Actions } from '.';
|
||||
import { Feature } from '../../../../xpack_main/types';
|
||||
import { Feature } from '../../../../../../plugins/features/server';
|
||||
import { disableUICapabilitesFactory } from './disable_ui_capabilities';
|
||||
|
||||
interface MockServerOptions {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { flatten, isObject, mapValues } from 'lodash';
|
||||
import { UICapabilities } from 'ui/capabilities';
|
||||
import { Feature } from '../../../../xpack_main/types';
|
||||
import { Feature } from '../../../../../../plugins/features/server';
|
||||
import { Actions } from './actions';
|
||||
import { CheckPrivilegesAtResourceResponse } from './check_privileges';
|
||||
import { CheckPrivilegesDynamically } from './check_privileges_dynamically';
|
||||
|
|
|
@ -4,10 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
Feature,
|
||||
FeatureKibanaPrivileges,
|
||||
} from '../../../../../../xpack_main/server/lib/feature_registry/feature_registry';
|
||||
import { Feature, FeatureKibanaPrivileges } from '../../../../../../../../plugins/features/server';
|
||||
import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder';
|
||||
|
||||
export class FeaturePrivilegeApiBuilder extends BaseFeaturePrivilegeBuilder {
|
||||
|
|
|
@ -4,10 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
Feature,
|
||||
FeatureKibanaPrivileges,
|
||||
} from '../../../../../../xpack_main/server/lib/feature_registry/feature_registry';
|
||||
import { Feature, FeatureKibanaPrivileges } from '../../../../../../../../plugins/features/server';
|
||||
import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder';
|
||||
|
||||
export class FeaturePrivilegeAppBuilder extends BaseFeaturePrivilegeBuilder {
|
||||
|
|
|
@ -4,10 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
Feature,
|
||||
FeatureKibanaPrivileges,
|
||||
} from '../../../../../../xpack_main/server/lib/feature_registry/feature_registry';
|
||||
import { Feature, FeatureKibanaPrivileges } from '../../../../../../../../plugins/features/server';
|
||||
import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder';
|
||||
|
||||
export class FeaturePrivilegeCatalogueBuilder extends BaseFeaturePrivilegeBuilder {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Feature, FeatureKibanaPrivileges } from '../../../../../../xpack_main/types';
|
||||
import { Feature, FeatureKibanaPrivileges } from '../../../../../../../../plugins/features/server';
|
||||
import { Actions } from '../../actions';
|
||||
|
||||
export interface FeaturePrivilegeBuilder {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { flatten } from 'lodash';
|
||||
import { Feature, FeatureKibanaPrivileges } from '../../../../../../xpack_main/types';
|
||||
import { Feature, FeatureKibanaPrivileges } from '../../../../../../../../plugins/features/server';
|
||||
import { Actions } from '../../actions';
|
||||
import { FeaturePrivilegeApiBuilder } from './api';
|
||||
import { FeaturePrivilegeAppBuilder } from './app';
|
||||
|
|
|
@ -4,10 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
Feature,
|
||||
FeatureKibanaPrivileges,
|
||||
} from '../../../../../../xpack_main/server/lib/feature_registry/feature_registry';
|
||||
import { Feature, FeatureKibanaPrivileges } from '../../../../../../../../plugins/features/server';
|
||||
import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder';
|
||||
|
||||
export class FeaturePrivilegeManagementBuilder extends BaseFeaturePrivilegeBuilder {
|
||||
|
|
|
@ -4,10 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
Feature,
|
||||
FeatureKibanaPrivileges,
|
||||
} from '../../../../../../xpack_main/server/lib/feature_registry/feature_registry';
|
||||
import { Feature, FeatureKibanaPrivileges } from '../../../../../../../../plugins/features/server';
|
||||
import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder';
|
||||
|
||||
export class FeaturePrivilegeNavlinkBuilder extends BaseFeaturePrivilegeBuilder {
|
||||
|
|
|
@ -5,10 +5,7 @@
|
|||
*/
|
||||
|
||||
import { flatten, uniq } from 'lodash';
|
||||
import {
|
||||
Feature,
|
||||
FeatureKibanaPrivileges,
|
||||
} from '../../../../../../xpack_main/server/lib/feature_registry/feature_registry';
|
||||
import { Feature, FeatureKibanaPrivileges } from '../../../../../../../../plugins/features/server';
|
||||
import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder';
|
||||
|
||||
const readOperations: string[] = ['bulk_get', 'get', 'find'];
|
||||
|
|
|
@ -4,10 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
Feature,
|
||||
FeatureKibanaPrivileges,
|
||||
} from '../../../../../../xpack_main/server/lib/feature_registry/feature_registry';
|
||||
import { Feature, FeatureKibanaPrivileges } from '../../../../../../../../plugins/features/server';
|
||||
import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder';
|
||||
|
||||
export class FeaturePrivilegeUIBuilder extends BaseFeaturePrivilegeBuilder {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Feature } from '../../../../../xpack_main/types';
|
||||
import { Feature } from '../../../../../../../plugins/features/server';
|
||||
import { Actions } from '../actions';
|
||||
import { privilegesFactory } from './privileges';
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { flatten, mapValues, uniq } from 'lodash';
|
||||
import { Feature } from '../../../../../xpack_main/types';
|
||||
import { Feature } from '../../../../../../../plugins/features/server';
|
||||
import { XPackMainPlugin } from '../../../../../xpack_main/xpack_main';
|
||||
import { RawKibanaFeaturePrivileges, RawKibanaPrivileges } from '../../../../common/model';
|
||||
import { Actions } from '../actions';
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Feature } from '../../../../xpack_main/types';
|
||||
import { Feature } from '../../../../../../plugins/features/server';
|
||||
import { actionsFactory } from './actions';
|
||||
import { validateFeaturePrivileges } from './validate_feature_privileges';
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Feature } from '../../../../xpack_main/types';
|
||||
import { Feature } from '../../../../../../plugins/features/server';
|
||||
import { areActionsFullyCovered } from '../../../common/privilege_calculator_utils';
|
||||
import { Actions } from './actions';
|
||||
import { featurePrivilegeBuilderFactory } from './privileges/feature_privilege_builder';
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { EuiLink } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { Feature } from '../../../../../../xpack_main/types';
|
||||
import { Feature } from '../../../../../../../../plugins/features/server';
|
||||
import { Space } from '../../../../../common/model/space';
|
||||
import { SectionPanel } from '../section_panel';
|
||||
import { EnabledFeatures } from './enabled_features';
|
||||
|
|
|
@ -8,7 +8,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiText, EuiTitle } from
|
|||
import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react';
|
||||
import React, { Component, Fragment, ReactNode } from 'react';
|
||||
import { UICapabilities } from 'ui/capabilities';
|
||||
import { Feature } from '../../../../../../xpack_main/types';
|
||||
import { Feature } from '../../../../../../../../plugins/features/server';
|
||||
import { Space } from '../../../../../common/model/space';
|
||||
import { getEnabledFeatures } from '../../lib/feature_utils';
|
||||
import { SectionPanel } from '../section_panel';
|
||||
|
|
|
@ -8,7 +8,7 @@ import { EuiCheckbox, EuiIcon, EuiInMemoryTable, EuiSwitch, EuiText, IconType }
|
|||
import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react';
|
||||
import _ from 'lodash';
|
||||
import React, { ChangeEvent, Component } from 'react';
|
||||
import { Feature } from '../../../../../../xpack_main/types';
|
||||
import { Feature } from '../../../../../../../../plugins/features/server';
|
||||
import { Space } from '../../../../../common/model/space';
|
||||
import { ToggleAllFeatures } from './toggle_all_features';
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ import { capabilities } from 'ui/capabilities';
|
|||
import { Breadcrumb } from 'ui/chrome';
|
||||
import { kfetch } from 'ui/kfetch';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { Feature } from '../../../../../xpack_main/types';
|
||||
import { Feature } from '../../../../../../../plugins/features/server';
|
||||
import { isReservedSpace } from '../../../../common';
|
||||
import { Space } from '../../../../common/model/space';
|
||||
import { SpacesManager } from '../../../lib';
|
||||
|
@ -79,7 +79,7 @@ class ManageSpacePageUI extends Component<Props, State> {
|
|||
|
||||
const { spaceId, spacesManager, intl, setBreadcrumbs } = this.props;
|
||||
|
||||
const getFeatures = kfetch({ method: 'get', pathname: '/api/features/v1' });
|
||||
const getFeatures = kfetch({ method: 'get', pathname: '/api/features' });
|
||||
|
||||
if (spaceId) {
|
||||
try {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Feature } from '../../../../../xpack_main/types';
|
||||
import { Feature } from '../../../../../../../plugins/features/server';
|
||||
import { getEnabledFeatures } from './feature_utils';
|
||||
|
||||
const buildFeatures = () =>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Feature } from '../../../../../xpack_main/types';
|
||||
import { Feature } from '../../../../../../../plugins/features/server';
|
||||
|
||||
import { Space } from '../../../../common/model/space';
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ import { capabilities } from 'ui/capabilities';
|
|||
import { kfetch } from 'ui/kfetch';
|
||||
// @ts-ignore
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { Feature } from '../../../../../xpack_main/types';
|
||||
import { Feature } from '../../../../../../../plugins/features/server';
|
||||
import { isReservedSpace } from '../../../../common';
|
||||
import { DEFAULT_SPACE_ID } from '../../../../common/constants';
|
||||
import { Space } from '../../../../common/model/space';
|
||||
|
@ -234,7 +234,7 @@ class SpacesGridPageUI extends Component<Props, State> {
|
|||
});
|
||||
|
||||
const getSpaces = spacesManager.getSpaces();
|
||||
const getFeatures = kfetch({ method: 'get', pathname: '/api/features/v1' });
|
||||
const getFeatures = kfetch({ method: 'get', pathname: '/api/features' });
|
||||
|
||||
try {
|
||||
const [spaces, features] = await Promise.all([getSpaces, getFeatures]);
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import * as Rx from 'rxjs';
|
||||
import { SavedObject, SavedObjectsService } from 'src/core/server';
|
||||
import { Feature } from '../../../../xpack_main/types';
|
||||
import { Feature } from '../../../../../../plugins/features/server';
|
||||
import { convertSavedObjectToSpace } from '../../routes/lib';
|
||||
import { initSpacesOnPostAuthRequestInterceptor } from './on_post_auth_interceptor';
|
||||
import { initSpacesOnRequestInterceptor } from './on_request_interceptor';
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { UICapabilities } from 'ui/capabilities';
|
||||
import { Feature } from '../../../xpack_main/types';
|
||||
import { Feature } from '../../../../../plugins/features/server';
|
||||
import { Space } from '../../common/model/space';
|
||||
import { toggleUICapabilities } from './toggle_ui_capabilities';
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
import _ from 'lodash';
|
||||
import { UICapabilities } from 'ui/capabilities';
|
||||
import { Feature } from '../../../xpack_main/types';
|
||||
import { Feature } from '../../../../../plugins/features/server';
|
||||
import { Space } from '../../common/model/space';
|
||||
|
||||
export function toggleUICapabilities(
|
||||
|
|
|
@ -13,14 +13,8 @@ import { getXpackConfigWithDeprecated } from '../telemetry/common/get_xpack_conf
|
|||
import { mirrorPluginStatus } from '../../server/lib/mirror_plugin_status';
|
||||
import { replaceInjectedVars } from './server/lib/replace_injected_vars';
|
||||
import { setupXPackMain } from './server/lib/setup_xpack_main';
|
||||
import {
|
||||
xpackInfoRoute,
|
||||
featuresRoute,
|
||||
settingsRoute,
|
||||
} from './server/routes/api/v1';
|
||||
import { xpackInfoRoute, settingsRoute } from './server/routes/api/v1';
|
||||
|
||||
import { registerOssFeatures } from './server/lib/register_oss_features';
|
||||
import { uiCapabilitiesForFeatures } from './server/lib/ui_capabilities_for_features';
|
||||
import { has } from 'lodash';
|
||||
|
||||
function movedToTelemetry(configPath) {
|
||||
|
@ -52,7 +46,11 @@ export const xpackMain = (kibana) => {
|
|||
},
|
||||
|
||||
uiCapabilities(server) {
|
||||
return uiCapabilitiesForFeatures(server.plugins.xpack_main);
|
||||
const featuresPlugin = server.newPlatform.setup.plugins.features;
|
||||
if (!featuresPlugin) {
|
||||
throw new Error('New Platform XPack Features plugin is not available.');
|
||||
}
|
||||
return featuresPlugin.getFeaturesUICapabilities();
|
||||
},
|
||||
|
||||
uiExports: {
|
||||
|
@ -81,18 +79,21 @@ export const xpackMain = (kibana) => {
|
|||
},
|
||||
|
||||
init(server) {
|
||||
const featuresPlugin = server.newPlatform.setup.plugins.features;
|
||||
if (!featuresPlugin) {
|
||||
throw new Error('New Platform XPack Features plugin is not available.');
|
||||
}
|
||||
|
||||
mirrorPluginStatus(server.plugins.elasticsearch, this, 'yellow', 'red');
|
||||
|
||||
setupXPackMain(server);
|
||||
const { types: savedObjectTypes } = server.savedObjects;
|
||||
const config = server.config();
|
||||
const isTimelionUiEnabled = config.get('timelion.enabled') && config.get('timelion.ui.enabled');
|
||||
registerOssFeatures(server.plugins.xpack_main.registerFeature, savedObjectTypes, isTimelionUiEnabled);
|
||||
featuresPlugin.registerLegacyAPI({
|
||||
xpackInfo: setupXPackMain(server),
|
||||
savedObjectTypes: server.savedObjects.types
|
||||
});
|
||||
|
||||
// register routes
|
||||
xpackInfoRoute(server);
|
||||
settingsRoute(server, this.kbnServer);
|
||||
featuresRoute(server);
|
||||
},
|
||||
deprecations: () => [
|
||||
movedToTelemetry('telemetry.config'),
|
||||
|
|
|
@ -39,6 +39,7 @@ describe('setupXPackMain()', () => {
|
|||
elasticsearch: mockElasticsearchPlugin,
|
||||
xpack_main: mockXPackMainPlugin
|
||||
},
|
||||
newPlatform: { setup: { plugins: { features: {} } } },
|
||||
events: { on() {} },
|
||||
log() {},
|
||||
config() {},
|
||||
|
|
|
@ -1,401 +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 Joi from 'joi';
|
||||
import { cloneDeep, difference, uniq } from 'lodash';
|
||||
import { UICapabilities } from 'ui/capabilities';
|
||||
/**
|
||||
* Feature privilege definition
|
||||
*/
|
||||
export interface FeatureKibanaPrivileges {
|
||||
/**
|
||||
* Whether or not this specific privilege should be excluded from the base privileges.
|
||||
*/
|
||||
excludeFromBasePrivileges?: boolean;
|
||||
|
||||
/**
|
||||
* If this feature includes management sections, you can specify them here to control visibility of those
|
||||
* pages based on user privileges.
|
||||
*
|
||||
* Example:
|
||||
* // Enables access to the "Advanced Settings" management page within the Kibana section
|
||||
* management: {
|
||||
* kibana: ['settings']
|
||||
* }
|
||||
*/
|
||||
management?: {
|
||||
[sectionId: string]: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* If this feature includes a catalogue entry, you can specify them here to control visibility based on user permissions.
|
||||
*/
|
||||
catalogue?: string[];
|
||||
|
||||
/**
|
||||
* If your feature includes server-side APIs, you can tag those routes to secure access based on user permissions.
|
||||
*
|
||||
* Example:
|
||||
* // Configure your routes with a tag starting with the 'access:' prefix
|
||||
* server.route({
|
||||
* path: '/api/my-route',
|
||||
* method: 'GET',
|
||||
* handler: () => { ...},
|
||||
* options: {
|
||||
* tags: ['access:my_feature-admin']
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* Then, specify the tags here (without the 'access:' prefix) which should be secured:
|
||||
*
|
||||
* {
|
||||
* api: ['my_feature-admin']
|
||||
* }
|
||||
*
|
||||
* NOTE: It is important to name your tags in a way that will not collide with other plugins/features.
|
||||
* A generic tag name like "access:read" could be used elsewhere, and access to that API endpoint would also
|
||||
* extend to any routes you have also tagged with that name.
|
||||
*/
|
||||
api?: string[];
|
||||
|
||||
/**
|
||||
* If your feature exposes a client-side application (most of them do!), then you can control access to them here.
|
||||
*
|
||||
* Example:
|
||||
* {
|
||||
* app: ['my-app', 'kibana']
|
||||
* }
|
||||
*
|
||||
*/
|
||||
app?: string[];
|
||||
|
||||
/**
|
||||
* If your feature requires access to specific saved objects, then specify your access needs here.
|
||||
*/
|
||||
savedObject: {
|
||||
/**
|
||||
* List of saved object types which users should have full read/write access to when granted this privilege.
|
||||
* Example:
|
||||
* {
|
||||
* all: ['my-saved-object-type']
|
||||
* }
|
||||
*/
|
||||
all: string[];
|
||||
|
||||
/**
|
||||
* List of saved object types which users should have read-only access to when granted this privilege.
|
||||
* Example:
|
||||
* {
|
||||
* read: ['config']
|
||||
* }
|
||||
*/
|
||||
read: string[];
|
||||
};
|
||||
/**
|
||||
* A list of UI Capabilities that should be granted to users with this privilege.
|
||||
* These capabilities will automatically be namespaces within your feature id.
|
||||
*
|
||||
* Example:
|
||||
* {
|
||||
* ui: ['show', 'save']
|
||||
* }
|
||||
*
|
||||
* This translates in the UI to the following (assuming a feature id of "foo"):
|
||||
* import { uiCapabilities } from 'ui/capabilities';
|
||||
*
|
||||
* const canShowApp = uiCapabilities.foo.show;
|
||||
* const canSave = uiCapabilities.foo.save;
|
||||
*
|
||||
* Note: Since these are automatically namespaced, you are free to use generic names like "show" and "save".
|
||||
*
|
||||
* @see UICapabilities
|
||||
*/
|
||||
ui: string[];
|
||||
}
|
||||
|
||||
type PrivilegesSet = Record<string, FeatureKibanaPrivileges>;
|
||||
|
||||
export type FeatureWithAllOrReadPrivileges = Feature<{
|
||||
all?: FeatureKibanaPrivileges;
|
||||
read?: FeatureKibanaPrivileges;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Interface for registering a feature.
|
||||
* Feature registration allows plugins to hide their applications with spaces,
|
||||
* and secure access when configured for security.
|
||||
*/
|
||||
export interface Feature<TPrivileges extends Partial<PrivilegesSet> = PrivilegesSet> {
|
||||
/**
|
||||
* Unique identifier for this feature.
|
||||
* This identifier is also used when generating UI Capabilities.
|
||||
*
|
||||
* @see UICapabilities
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* Display name for this feature.
|
||||
* This will be displayed to end-users, so a translatable string is advised for i18n.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Whether or not this feature should be excluded from the base privileges.
|
||||
* This is primarily helpful when migrating applications with a "legacy" privileges model
|
||||
* to use Kibana privileges. We don't want these features to be considered part of the `all`
|
||||
* or `read` base privileges in a minor release if the user was previously granted access
|
||||
* using an additional reserved role.
|
||||
*/
|
||||
excludeFromBasePrivileges?: boolean;
|
||||
|
||||
/**
|
||||
* Optional array of supported licenses.
|
||||
* If omitted, all licenses are allowed.
|
||||
* This does not restrict access to your feature based on license.
|
||||
* Its only purpose is to inform the space and roles UIs on which features to display.
|
||||
*/
|
||||
validLicenses?: Array<'basic' | 'standard' | 'gold' | 'platinum'>;
|
||||
|
||||
/**
|
||||
* An optional EUI Icon to be used when displaying your feature.
|
||||
*/
|
||||
icon?: string;
|
||||
|
||||
/**
|
||||
* The optional Nav Link ID for feature.
|
||||
* If specified, your link will be automatically hidden if needed based on the current space and user permissions.
|
||||
*/
|
||||
navLinkId?: string;
|
||||
|
||||
/**
|
||||
* An array of app ids that are enabled when this feature is enabled.
|
||||
* Apps specified here will automatically cascade to the privileges defined below, unless specified differently there.
|
||||
*/
|
||||
app: string[];
|
||||
|
||||
/**
|
||||
* If this feature includes management sections, you can specify them here to control visibility of those
|
||||
* pages based on the current space.
|
||||
*
|
||||
* Items specified here will automatically cascade to the privileges defined below, unless specified differently there.
|
||||
*
|
||||
* Example:
|
||||
* // Enables access to the "Advanced Settings" management page within the Kibana section
|
||||
* management: {
|
||||
* kibana: ['settings']
|
||||
* }
|
||||
*/
|
||||
management?: {
|
||||
[sectionId: string]: string[];
|
||||
};
|
||||
/**
|
||||
* If this feature includes a catalogue entry, you can specify them here to control visibility based on the current space.
|
||||
*
|
||||
* Items specified here will automatically cascade to the privileges defined below, unless specified differently there.
|
||||
*/
|
||||
catalogue?: string[];
|
||||
|
||||
/**
|
||||
* Feature privilege definition.
|
||||
*
|
||||
* Example:
|
||||
* {
|
||||
* all: {...},
|
||||
* read: {...}
|
||||
* }
|
||||
* @see FeatureKibanaPrivileges
|
||||
*/
|
||||
privileges: TPrivileges;
|
||||
|
||||
/**
|
||||
* Optional message to display on the Role Management screen when configuring permissions for this feature.
|
||||
*/
|
||||
privilegesTooltip?: string;
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
reserved?: {
|
||||
privilege: FeatureKibanaPrivileges;
|
||||
description: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Each feature gets its own property on the UICapabilities object,
|
||||
// but that object has a few built-in properties which should not be overwritten.
|
||||
const prohibitedFeatureIds: Array<keyof UICapabilities> = ['catalogue', 'management', 'navLinks'];
|
||||
|
||||
const featurePrivilegePartRegex = /^[a-zA-Z0-9_-]+$/;
|
||||
const managementSectionIdRegex = /^[a-zA-Z0-9_-]+$/;
|
||||
export const uiCapabilitiesRegex = /^[a-zA-Z0-9:_-]+$/;
|
||||
|
||||
const managementSchema = Joi.object().pattern(
|
||||
managementSectionIdRegex,
|
||||
Joi.array().items(Joi.string().regex(uiCapabilitiesRegex))
|
||||
);
|
||||
const catalogueSchema = Joi.array().items(Joi.string().regex(uiCapabilitiesRegex));
|
||||
|
||||
const privilegeSchema = Joi.object({
|
||||
excludeFromBasePrivileges: Joi.boolean(),
|
||||
management: managementSchema,
|
||||
catalogue: catalogueSchema,
|
||||
api: Joi.array().items(Joi.string()),
|
||||
app: Joi.array().items(Joi.string()),
|
||||
savedObject: Joi.object({
|
||||
all: Joi.array()
|
||||
.items(Joi.string())
|
||||
.required(),
|
||||
read: Joi.array()
|
||||
.items(Joi.string())
|
||||
.required(),
|
||||
}).required(),
|
||||
ui: Joi.array()
|
||||
.items(Joi.string().regex(uiCapabilitiesRegex))
|
||||
.required(),
|
||||
});
|
||||
|
||||
const schema = Joi.object({
|
||||
id: Joi.string()
|
||||
.regex(featurePrivilegePartRegex)
|
||||
.invalid(...prohibitedFeatureIds)
|
||||
.required(),
|
||||
name: Joi.string().required(),
|
||||
excludeFromBasePrivileges: Joi.boolean(),
|
||||
validLicenses: Joi.array().items(Joi.string().valid('basic', 'standard', 'gold', 'platinum')),
|
||||
icon: Joi.string(),
|
||||
description: Joi.string(),
|
||||
navLinkId: Joi.string().regex(uiCapabilitiesRegex),
|
||||
app: Joi.array()
|
||||
.items(Joi.string())
|
||||
.required(),
|
||||
management: managementSchema,
|
||||
catalogue: catalogueSchema,
|
||||
privileges: Joi.object({
|
||||
all: privilegeSchema,
|
||||
read: privilegeSchema,
|
||||
}).required(),
|
||||
privilegesTooltip: Joi.string(),
|
||||
reserved: Joi.object({
|
||||
privilege: privilegeSchema.required(),
|
||||
description: Joi.string().required(),
|
||||
}),
|
||||
});
|
||||
|
||||
export class FeatureRegistry {
|
||||
private locked = false;
|
||||
private features: Record<string, Feature> = {};
|
||||
|
||||
public register(feature: FeatureWithAllOrReadPrivileges) {
|
||||
if (this.locked) {
|
||||
throw new Error(`Features are locked, can't register new features`);
|
||||
}
|
||||
|
||||
validateFeature(feature);
|
||||
|
||||
if (feature.id in this.features) {
|
||||
throw new Error(`Feature with id ${feature.id} is already registered.`);
|
||||
}
|
||||
|
||||
const featureCopy: Feature = cloneDeep(feature as Feature);
|
||||
|
||||
this.features[feature.id] = applyAutomaticPrivilegeGrants(featureCopy as Feature);
|
||||
}
|
||||
|
||||
public getAll(): Feature[] {
|
||||
this.locked = true;
|
||||
return cloneDeep(Object.values(this.features));
|
||||
}
|
||||
}
|
||||
|
||||
function validateFeature(feature: FeatureWithAllOrReadPrivileges) {
|
||||
const validateResult = Joi.validate(feature, schema);
|
||||
if (validateResult.error) {
|
||||
throw validateResult.error;
|
||||
}
|
||||
// the following validation can't be enforced by the Joi schema, since it'd require us looking "up" the object graph for the list of valid value, which they explicitly forbid.
|
||||
const { app = [], management = {}, catalogue = [] } = feature;
|
||||
|
||||
const privilegeEntries = [...Object.entries(feature.privileges)];
|
||||
if (feature.reserved) {
|
||||
privilegeEntries.push(['reserved', feature.reserved.privilege]);
|
||||
}
|
||||
|
||||
privilegeEntries.forEach(([privilegeId, privilegeDefinition]) => {
|
||||
if (!privilegeDefinition) {
|
||||
throw new Error('Privilege definition may not be null or undefined');
|
||||
}
|
||||
|
||||
const unknownAppEntries = difference(privilegeDefinition.app || [], app);
|
||||
if (unknownAppEntries.length > 0) {
|
||||
throw new Error(
|
||||
`Feature privilege ${
|
||||
feature.id
|
||||
}.${privilegeId} has unknown app entries: ${unknownAppEntries.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
const unknownCatalogueEntries = difference(privilegeDefinition.catalogue || [], catalogue);
|
||||
if (unknownCatalogueEntries.length > 0) {
|
||||
throw new Error(
|
||||
`Feature privilege ${
|
||||
feature.id
|
||||
}.${privilegeId} has unknown catalogue entries: ${unknownCatalogueEntries.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
Object.entries(privilegeDefinition.management || {}).forEach(
|
||||
([managementSectionId, managementEntry]) => {
|
||||
if (!management[managementSectionId]) {
|
||||
throw new Error(
|
||||
`Feature privilege ${feature.id}.${privilegeId} has unknown management section: ${managementSectionId}`
|
||||
);
|
||||
}
|
||||
|
||||
const unknownSectionEntries = difference(managementEntry, management[managementSectionId]);
|
||||
|
||||
if (unknownSectionEntries.length > 0) {
|
||||
throw new Error(
|
||||
`Feature privilege ${
|
||||
feature.id
|
||||
}.${privilegeId} has unknown management entries for section ${managementSectionId}: ${unknownSectionEntries.join(
|
||||
', '
|
||||
)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function applyAutomaticPrivilegeGrants(feature: Feature): Feature {
|
||||
const { all: allPrivilege, read: readPrivilege } = feature.privileges;
|
||||
const reservedPrivilege = feature.reserved ? feature.reserved.privilege : null;
|
||||
|
||||
applyAutomaticAllPrivilegeGrants(allPrivilege, reservedPrivilege);
|
||||
applyAutomaticReadPrivilegeGrants(readPrivilege);
|
||||
|
||||
return feature;
|
||||
}
|
||||
|
||||
function applyAutomaticAllPrivilegeGrants(...allPrivileges: Array<FeatureKibanaPrivileges | null>) {
|
||||
allPrivileges.forEach(allPrivilege => {
|
||||
if (allPrivilege) {
|
||||
allPrivilege.savedObject.all = uniq([...allPrivilege.savedObject.all, 'telemetry']);
|
||||
allPrivilege.savedObject.read = uniq([...allPrivilege.savedObject.read, 'config', 'url']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function applyAutomaticReadPrivilegeGrants(
|
||||
...readPrivileges: Array<FeatureKibanaPrivileges | null>
|
||||
) {
|
||||
readPrivileges.forEach(readPrivilege => {
|
||||
if (readPrivilege) {
|
||||
readPrivilege.savedObject.read = uniq([...readPrivilege.savedObject.read, 'config', 'url']);
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,49 +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 { FeatureRegistry } from './feature_registry';
|
||||
import { registerOssFeatures } from './register_oss_features';
|
||||
|
||||
describe('registerOssFeatures', () => {
|
||||
it('registers features including timelion', () => {
|
||||
const registry = new FeatureRegistry();
|
||||
const savedObjectTypes = ['foo', 'bar'];
|
||||
registerOssFeatures(feature => registry.register(feature), savedObjectTypes, true);
|
||||
|
||||
const features = registry.getAll();
|
||||
expect(features.map(f => f.id)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"discover",
|
||||
"visualize",
|
||||
"dashboard",
|
||||
"dev_tools",
|
||||
"advancedSettings",
|
||||
"indexPatterns",
|
||||
"savedObjectsManagement",
|
||||
"timelion",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('registers features excluding timelion', () => {
|
||||
const registry = new FeatureRegistry();
|
||||
const savedObjectTypes = ['foo', 'bar'];
|
||||
registerOssFeatures(feature => registry.register(feature), savedObjectTypes, false);
|
||||
|
||||
const features = registry.getAll();
|
||||
expect(features.map(f => f.id)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"discover",
|
||||
"visualize",
|
||||
"dashboard",
|
||||
"dev_tools",
|
||||
"advancedSettings",
|
||||
"indexPatterns",
|
||||
"savedObjectsManagement",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
import { injectXPackInfoSignature } from './inject_xpack_info_signature';
|
||||
import { XPackInfo } from './xpack_info';
|
||||
import { FeatureRegistry } from './feature_registry';
|
||||
|
||||
/**
|
||||
* Setup the X-Pack Main plugin. This is fired every time that the Elasticsearch plugin becomes Green.
|
||||
|
@ -25,9 +24,9 @@ export function setupXPackMain(server) {
|
|||
server.expose('createXPackInfo', (options) => new XPackInfo(server, options));
|
||||
server.ext('onPreResponse', (request, h) => injectXPackInfoSignature(info, request, h));
|
||||
|
||||
const featureRegistry = new FeatureRegistry();
|
||||
server.expose('registerFeature', (feature) => featureRegistry.register(feature));
|
||||
server.expose('getFeatures', () => featureRegistry.getAll());
|
||||
const { registerFeature, getFeatures } = server.newPlatform.setup.plugins.features;
|
||||
server.expose('registerFeature', registerFeature);
|
||||
server.expose('getFeatures', getFeatures);
|
||||
|
||||
const setPluginStatus = () => {
|
||||
if (info.isAvailable()) {
|
||||
|
@ -46,4 +45,6 @@ export function setupXPackMain(server) {
|
|||
// whenever the license info is updated, regardless of the elasticsearch plugin status
|
||||
// changes, reflect the change in our plugin status. See https://github.com/elastic/kibana/issues/20017
|
||||
info.onLicenseInfoChange(setPluginStatus);
|
||||
|
||||
return info;
|
||||
}
|
||||
|
|
|
@ -1,196 +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 { Feature } from './feature_registry';
|
||||
import { uiCapabilitiesForFeatures } from './ui_capabilities_for_features';
|
||||
|
||||
function getMockXpackMainPlugin(features: Feature[]) {
|
||||
return {
|
||||
getFeatures: () => features,
|
||||
};
|
||||
}
|
||||
|
||||
function createFeaturePrivilege(key: string, capabilities: string[] = []) {
|
||||
return {
|
||||
[key]: {
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
app: [],
|
||||
ui: [...capabilities],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('populateUICapabilities', () => {
|
||||
it('handles no original uiCapabilites and no registered features gracefully', () => {
|
||||
const xpackMainPlugin = getMockXpackMainPlugin([]);
|
||||
|
||||
expect(uiCapabilitiesForFeatures(xpackMainPlugin)).toEqual({});
|
||||
});
|
||||
|
||||
it('handles features with no registered capabilities', () => {
|
||||
const xpackMainPlugin = getMockXpackMainPlugin([
|
||||
{
|
||||
id: 'newFeature',
|
||||
name: 'my new feature',
|
||||
app: ['bar-app'],
|
||||
privileges: {
|
||||
...createFeaturePrivilege('all'),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
expect(uiCapabilitiesForFeatures(xpackMainPlugin)).toEqual({
|
||||
catalogue: {},
|
||||
newFeature: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('augments the original uiCapabilities with registered feature capabilities', () => {
|
||||
const xpackMainPlugin = getMockXpackMainPlugin([
|
||||
{
|
||||
id: 'newFeature',
|
||||
name: 'my new feature',
|
||||
navLinkId: 'newFeatureNavLink',
|
||||
app: ['bar-app'],
|
||||
privileges: {
|
||||
...createFeaturePrivilege('all', ['capability1', 'capability2']),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
expect(uiCapabilitiesForFeatures(xpackMainPlugin)).toEqual({
|
||||
catalogue: {},
|
||||
newFeature: {
|
||||
capability1: true,
|
||||
capability2: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('combines catalogue entries from multiple features', () => {
|
||||
const xpackMainPlugin = getMockXpackMainPlugin([
|
||||
{
|
||||
id: 'newFeature',
|
||||
name: 'my new feature',
|
||||
navLinkId: 'newFeatureNavLink',
|
||||
app: ['bar-app'],
|
||||
catalogue: ['anotherFooEntry', 'anotherBarEntry'],
|
||||
privileges: {
|
||||
...createFeaturePrivilege('foo', ['capability1', 'capability2']),
|
||||
...createFeaturePrivilege('bar', ['capability3', 'capability4']),
|
||||
...createFeaturePrivilege('baz'),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
expect(uiCapabilitiesForFeatures(xpackMainPlugin)).toEqual({
|
||||
catalogue: {
|
||||
anotherFooEntry: true,
|
||||
anotherBarEntry: true,
|
||||
},
|
||||
newFeature: {
|
||||
capability1: true,
|
||||
capability2: true,
|
||||
capability3: true,
|
||||
capability4: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it(`merges capabilities from all feature privileges`, () => {
|
||||
const xpackMainPlugin = getMockXpackMainPlugin([
|
||||
{
|
||||
id: 'newFeature',
|
||||
name: 'my new feature',
|
||||
navLinkId: 'newFeatureNavLink',
|
||||
app: ['bar-app'],
|
||||
privileges: {
|
||||
...createFeaturePrivilege('foo', ['capability1', 'capability2']),
|
||||
...createFeaturePrivilege('bar', ['capability3', 'capability4']),
|
||||
...createFeaturePrivilege('baz', ['capability1', 'capability5']),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
expect(uiCapabilitiesForFeatures(xpackMainPlugin)).toEqual({
|
||||
catalogue: {},
|
||||
newFeature: {
|
||||
capability1: true,
|
||||
capability2: true,
|
||||
capability3: true,
|
||||
capability4: true,
|
||||
capability5: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('supports merging multiple features with multiple privileges each', () => {
|
||||
const xpackMainPlugin = getMockXpackMainPlugin([
|
||||
{
|
||||
id: 'newFeature',
|
||||
name: 'my new feature',
|
||||
navLinkId: 'newFeatureNavLink',
|
||||
app: ['bar-app'],
|
||||
privileges: {
|
||||
...createFeaturePrivilege('foo', ['capability1', 'capability2']),
|
||||
...createFeaturePrivilege('bar', ['capability3', 'capability4']),
|
||||
...createFeaturePrivilege('baz', ['capability1', 'capability5']),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'anotherNewFeature',
|
||||
name: 'another new feature',
|
||||
app: ['bar-app'],
|
||||
privileges: {
|
||||
...createFeaturePrivilege('foo', ['capability1', 'capability2']),
|
||||
...createFeaturePrivilege('bar', ['capability3', 'capability4']),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'yetAnotherNewFeature',
|
||||
name: 'yet another new feature',
|
||||
navLinkId: 'yetAnotherNavLink',
|
||||
app: ['bar-app'],
|
||||
privileges: {
|
||||
...createFeaturePrivilege('all', ['capability1', 'capability2']),
|
||||
...createFeaturePrivilege('read', []),
|
||||
...createFeaturePrivilege('somethingInBetween', [
|
||||
'something1',
|
||||
'something2',
|
||||
'something3',
|
||||
]),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
expect(uiCapabilitiesForFeatures(xpackMainPlugin)).toEqual({
|
||||
anotherNewFeature: {
|
||||
capability1: true,
|
||||
capability2: true,
|
||||
capability3: true,
|
||||
capability4: true,
|
||||
},
|
||||
catalogue: {},
|
||||
newFeature: {
|
||||
capability1: true,
|
||||
capability2: true,
|
||||
capability3: true,
|
||||
capability4: true,
|
||||
capability5: true,
|
||||
},
|
||||
yetAnotherNewFeature: {
|
||||
capability1: true,
|
||||
capability2: true,
|
||||
something1: true,
|
||||
something2: true,
|
||||
something3: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,34 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`GET /api/features/v1 does not return features that arent allowed by current license 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"app": Array [],
|
||||
"id": "feature_1",
|
||||
"name": "Feature 1",
|
||||
"privileges": Object {},
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`GET /api/features/v1 returns a list of available features 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"app": Array [],
|
||||
"id": "feature_1",
|
||||
"name": "Feature 1",
|
||||
"privileges": Object {},
|
||||
},
|
||||
Object {
|
||||
"app": Array [
|
||||
"bar-app",
|
||||
],
|
||||
"id": "licensed_feature",
|
||||
"name": "Licensed Feature",
|
||||
"privileges": Object {},
|
||||
"validLicenses": Array [
|
||||
"gold",
|
||||
],
|
||||
},
|
||||
]
|
||||
`;
|
|
@ -1,80 +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 { KibanaConfig } from 'src/legacy/server/kbn_server';
|
||||
import { FeatureRegistry } from '../../../../lib/feature_registry';
|
||||
// @ts-ignore
|
||||
import { setupXPackMain } from '../../../../lib/setup_xpack_main';
|
||||
import { featuresRoute } from './features';
|
||||
|
||||
let server: Server;
|
||||
let currentLicenseLevel: string = 'gold';
|
||||
|
||||
describe('GET /api/features/v1', () => {
|
||||
beforeAll(() => {
|
||||
server = new Server();
|
||||
|
||||
const config: Record<string, any> = {};
|
||||
server.config = () => {
|
||||
return {
|
||||
get: (key: string) => {
|
||||
return config[key];
|
||||
},
|
||||
} as KibanaConfig;
|
||||
};
|
||||
const featureRegistry = new FeatureRegistry();
|
||||
// @ts-ignore
|
||||
server.plugins.xpack_main = {
|
||||
getFeatures: () => featureRegistry.getAll(),
|
||||
info: {
|
||||
// @ts-ignore
|
||||
license: {
|
||||
isOneOf: (candidateLicenses: string[]) => {
|
||||
return candidateLicenses.includes(currentLicenseLevel);
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
featuresRoute(server);
|
||||
|
||||
featureRegistry.register({
|
||||
id: 'feature_1',
|
||||
name: 'Feature 1',
|
||||
app: [],
|
||||
privileges: {},
|
||||
});
|
||||
|
||||
featureRegistry.register({
|
||||
id: 'licensed_feature',
|
||||
name: 'Licensed Feature',
|
||||
app: ['bar-app'],
|
||||
validLicenses: ['gold'],
|
||||
privileges: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a list of available features', async () => {
|
||||
const response = await server.inject({
|
||||
url: '/api/features/v1',
|
||||
});
|
||||
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(JSON.parse(response.payload)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it(`does not return features that arent allowed by current license`, async () => {
|
||||
currentLicenseLevel = 'basic';
|
||||
|
||||
const response = await server.inject({
|
||||
url: '/api/features/v1',
|
||||
});
|
||||
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(JSON.parse(response.payload)).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -1,29 +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 { Feature } from '../../../../../types';
|
||||
|
||||
export function featuresRoute(server: Record<string, any>) {
|
||||
server.route({
|
||||
path: '/api/features/v1',
|
||||
method: 'GET',
|
||||
options: {
|
||||
tags: ['access:features'],
|
||||
},
|
||||
async handler(request: Record<string, any>) {
|
||||
const xpackInfo = server.plugins.xpack_main.info;
|
||||
|
||||
const allFeatures: Feature[] = server.plugins.xpack_main.getFeatures();
|
||||
|
||||
return allFeatures.filter(
|
||||
feature =>
|
||||
!feature.validLicenses ||
|
||||
!feature.validLicenses.length ||
|
||||
xpackInfo.license.isOneOf(feature.validLicenses)
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
|
@ -1,7 +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.
|
||||
*/
|
||||
|
||||
export { featuresRoute } from './features';
|
|
@ -5,5 +5,4 @@
|
|||
*/
|
||||
|
||||
export { xpackInfoRoute } from './xpack_info';
|
||||
export { featuresRoute } from './features';
|
||||
export { settingsRoute } from './settings';
|
||||
|
|
|
@ -1,11 +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.
|
||||
*/
|
||||
|
||||
export {
|
||||
Feature,
|
||||
FeatureKibanaPrivileges,
|
||||
uiCapabilitiesRegex,
|
||||
} from './server/lib/feature_registry';
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Feature, FeatureWithAllOrReadPrivileges } from './server/lib/feature_registry';
|
||||
import { Feature, FeatureWithAllOrReadPrivileges } from '../../../plugins/features/server';
|
||||
import { XPackInfo, XPackInfoOptions } from './server/lib/xpack_info';
|
||||
export { XPackFeature } from './server/lib/xpack_info';
|
||||
|
||||
|
|
8
x-pack/plugins/features/kibana.json
Normal file
8
x-pack/plugins/features/kibana.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"id": "features",
|
||||
"version": "8.0.0",
|
||||
"kibanaVersion": "kibana",
|
||||
"optionalPlugins": ["timelion"],
|
||||
"server": true,
|
||||
"ui": false
|
||||
}
|
120
x-pack/plugins/features/server/feature.ts
Normal file
120
x-pack/plugins/features/server/feature.ts
Normal file
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* 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 { FeatureKibanaPrivileges, FeatureKibanaPrivilegesSet } from './feature_kibana_privileges';
|
||||
|
||||
/**
|
||||
* Interface for registering a feature.
|
||||
* Feature registration allows plugins to hide their applications with spaces,
|
||||
* and secure access when configured for security.
|
||||
*/
|
||||
export interface Feature<
|
||||
TPrivileges extends Partial<FeatureKibanaPrivilegesSet> = FeatureKibanaPrivilegesSet
|
||||
> {
|
||||
/**
|
||||
* Unique identifier for this feature.
|
||||
* This identifier is also used when generating UI Capabilities.
|
||||
*
|
||||
* @see UICapabilities
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* Display name for this feature.
|
||||
* This will be displayed to end-users, so a translatable string is advised for i18n.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Whether or not this feature should be excluded from the base privileges.
|
||||
* This is primarily helpful when migrating applications with a "legacy" privileges model
|
||||
* to use Kibana privileges. We don't want these features to be considered part of the `all`
|
||||
* or `read` base privileges in a minor release if the user was previously granted access
|
||||
* using an additional reserved role.
|
||||
*/
|
||||
excludeFromBasePrivileges?: boolean;
|
||||
|
||||
/**
|
||||
* Optional array of supported licenses.
|
||||
* If omitted, all licenses are allowed.
|
||||
* This does not restrict access to your feature based on license.
|
||||
* Its only purpose is to inform the space and roles UIs on which features to display.
|
||||
*/
|
||||
validLicenses?: Array<'basic' | 'standard' | 'gold' | 'platinum'>;
|
||||
|
||||
/**
|
||||
* An optional EUI Icon to be used when displaying your feature.
|
||||
*/
|
||||
icon?: string;
|
||||
|
||||
/**
|
||||
* The optional Nav Link ID for feature.
|
||||
* If specified, your link will be automatically hidden if needed based on the current space and user permissions.
|
||||
*/
|
||||
navLinkId?: string;
|
||||
|
||||
/**
|
||||
* An array of app ids that are enabled when this feature is enabled.
|
||||
* Apps specified here will automatically cascade to the privileges defined below, unless specified differently there.
|
||||
*/
|
||||
app: string[];
|
||||
|
||||
/**
|
||||
* If this feature includes management sections, you can specify them here to control visibility of those
|
||||
* pages based on the current space.
|
||||
*
|
||||
* Items specified here will automatically cascade to the privileges defined below, unless specified differently there.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Enables access to the "Advanced Settings" management page within the Kibana section
|
||||
* management: {
|
||||
* kibana: ['settings']
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
management?: {
|
||||
[sectionId: string]: string[];
|
||||
};
|
||||
/**
|
||||
* If this feature includes a catalogue entry, you can specify them here to control visibility based on the current space.
|
||||
*
|
||||
* Items specified here will automatically cascade to the privileges defined below, unless specified differently there.
|
||||
*/
|
||||
catalogue?: string[];
|
||||
|
||||
/**
|
||||
* Feature privilege definition.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* {
|
||||
* all: {...},
|
||||
* read: {...}
|
||||
* }
|
||||
* ```
|
||||
* @see FeatureKibanaPrivileges
|
||||
*/
|
||||
privileges: TPrivileges;
|
||||
|
||||
/**
|
||||
* Optional message to display on the Role Management screen when configuring permissions for this feature.
|
||||
*/
|
||||
privilegesTooltip?: string;
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
reserved?: {
|
||||
privilege: FeatureKibanaPrivileges;
|
||||
description: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type FeatureWithAllOrReadPrivileges = Feature<{
|
||||
all?: FeatureKibanaPrivileges;
|
||||
read?: FeatureKibanaPrivileges;
|
||||
}>;
|
127
x-pack/plugins/features/server/feature_kibana_privileges.ts
Normal file
127
x-pack/plugins/features/server/feature_kibana_privileges.ts
Normal file
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Feature privilege definition
|
||||
*/
|
||||
export interface FeatureKibanaPrivileges {
|
||||
/**
|
||||
* Whether or not this specific privilege should be excluded from the base privileges.
|
||||
*/
|
||||
excludeFromBasePrivileges?: boolean;
|
||||
|
||||
/**
|
||||
* If this feature includes management sections, you can specify them here to control visibility of those
|
||||
* pages based on user privileges.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Enables access to the "Advanced Settings" management page within the Kibana section
|
||||
* management: {
|
||||
* kibana: ['settings']
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
management?: {
|
||||
[sectionId: string]: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* If this feature includes a catalogue entry, you can specify them here to control visibility based on user permissions.
|
||||
*/
|
||||
catalogue?: string[];
|
||||
|
||||
/**
|
||||
* If your feature includes server-side APIs, you can tag those routes to secure access based on user permissions.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Configure your routes with a tag starting with the 'access:' prefix
|
||||
* server.route({
|
||||
* path: '/api/my-route',
|
||||
* method: 'GET',
|
||||
* handler: () => { ...},
|
||||
* options: {
|
||||
* tags: ['access:my_feature-admin']
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* Then, specify the tags here (without the 'access:' prefix) which should be secured:
|
||||
*
|
||||
* {
|
||||
* api: ['my_feature-admin']
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* NOTE: It is important to name your tags in a way that will not collide with other plugins/features.
|
||||
* A generic tag name like "access:read" could be used elsewhere, and access to that API endpoint would also
|
||||
* extend to any routes you have also tagged with that name.
|
||||
*/
|
||||
api?: string[];
|
||||
|
||||
/**
|
||||
* If your feature exposes a client-side application (most of them do!), then you can control access to them here.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* {
|
||||
* app: ['my-app', 'kibana']
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
app?: string[];
|
||||
|
||||
/**
|
||||
* If your feature requires access to specific saved objects, then specify your access needs here.
|
||||
*/
|
||||
savedObject: {
|
||||
/**
|
||||
* List of saved object types which users should have full read/write access to when granted this privilege.
|
||||
* @example
|
||||
* ```ts
|
||||
* {
|
||||
* all: ['my-saved-object-type']
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
all: string[];
|
||||
|
||||
/**
|
||||
* List of saved object types which users should have read-only access to when granted this privilege.
|
||||
* @example
|
||||
* ```ts
|
||||
* {
|
||||
* read: ['config']
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
read: string[];
|
||||
};
|
||||
/**
|
||||
* A list of UI Capabilities that should be granted to users with this privilege.
|
||||
* These capabilities will automatically be namespaces within your feature id.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* {
|
||||
* ui: ['show', 'save']
|
||||
* }
|
||||
*
|
||||
* This translates in the UI to the following (assuming a feature id of "foo"):
|
||||
* import { uiCapabilities } from 'ui/capabilities';
|
||||
*
|
||||
* const canShowApp = uiCapabilities.foo.show;
|
||||
* const canSave = uiCapabilities.foo.save;
|
||||
* ```
|
||||
* Note: Since these are automatically namespaced, you are free to use generic names like "show" and "save".
|
||||
*
|
||||
* @see UICapabilities
|
||||
*/
|
||||
ui: string[];
|
||||
}
|
||||
|
||||
export type FeatureKibanaPrivilegesSet = Record<string, FeatureKibanaPrivileges>;
|
|
@ -4,7 +4,8 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Feature, FeatureRegistry } from './feature_registry';
|
||||
import { FeatureRegistry } from './feature_registry';
|
||||
import { Feature } from './feature';
|
||||
|
||||
describe('FeatureRegistry', () => {
|
||||
it('allows a minimal feature to be registered', () => {
|
65
x-pack/plugins/features/server/feature_registry.ts
Normal file
65
x-pack/plugins/features/server/feature_registry.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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 { cloneDeep, uniq } from 'lodash';
|
||||
import { FeatureKibanaPrivileges } from './feature_kibana_privileges';
|
||||
import { Feature, FeatureWithAllOrReadPrivileges } from './feature';
|
||||
import { validateFeature } from './feature_schema';
|
||||
|
||||
export class FeatureRegistry {
|
||||
private locked = false;
|
||||
private features: Record<string, Feature> = {};
|
||||
|
||||
public register(feature: FeatureWithAllOrReadPrivileges) {
|
||||
if (this.locked) {
|
||||
throw new Error(`Features are locked, can't register new features`);
|
||||
}
|
||||
|
||||
validateFeature(feature);
|
||||
|
||||
if (feature.id in this.features) {
|
||||
throw new Error(`Feature with id ${feature.id} is already registered.`);
|
||||
}
|
||||
|
||||
const featureCopy: Feature = cloneDeep(feature as Feature);
|
||||
|
||||
this.features[feature.id] = applyAutomaticPrivilegeGrants(featureCopy as Feature);
|
||||
}
|
||||
|
||||
public getAll(): Feature[] {
|
||||
this.locked = true;
|
||||
return cloneDeep(Object.values(this.features));
|
||||
}
|
||||
}
|
||||
|
||||
function applyAutomaticPrivilegeGrants(feature: Feature): Feature {
|
||||
const { all: allPrivilege, read: readPrivilege } = feature.privileges;
|
||||
const reservedPrivilege = feature.reserved ? feature.reserved.privilege : null;
|
||||
|
||||
applyAutomaticAllPrivilegeGrants(allPrivilege, reservedPrivilege);
|
||||
applyAutomaticReadPrivilegeGrants(readPrivilege);
|
||||
|
||||
return feature;
|
||||
}
|
||||
|
||||
function applyAutomaticAllPrivilegeGrants(...allPrivileges: Array<FeatureKibanaPrivileges | null>) {
|
||||
allPrivileges.forEach(allPrivilege => {
|
||||
if (allPrivilege) {
|
||||
allPrivilege.savedObject.all = uniq([...allPrivilege.savedObject.all, 'telemetry']);
|
||||
allPrivilege.savedObject.read = uniq([...allPrivilege.savedObject.read, 'config', 'url']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function applyAutomaticReadPrivilegeGrants(
|
||||
...readPrivileges: Array<FeatureKibanaPrivileges | null>
|
||||
) {
|
||||
readPrivileges.forEach(readPrivilege => {
|
||||
if (readPrivilege) {
|
||||
readPrivilege.savedObject.read = uniq([...readPrivilege.savedObject.read, 'config', 'url']);
|
||||
}
|
||||
});
|
||||
}
|
131
x-pack/plugins/features/server/feature_schema.ts
Normal file
131
x-pack/plugins/features/server/feature_schema.ts
Normal file
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* 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 Joi from 'joi';
|
||||
|
||||
import { difference } from 'lodash';
|
||||
import { Capabilities as UICapabilities } from '../../../../src/core/public';
|
||||
import { FeatureWithAllOrReadPrivileges } from './feature';
|
||||
|
||||
// Each feature gets its own property on the UICapabilities object,
|
||||
// but that object has a few built-in properties which should not be overwritten.
|
||||
const prohibitedFeatureIds: Array<keyof UICapabilities> = ['catalogue', 'management', 'navLinks'];
|
||||
|
||||
const featurePrivilegePartRegex = /^[a-zA-Z0-9_-]+$/;
|
||||
const managementSectionIdRegex = /^[a-zA-Z0-9_-]+$/;
|
||||
export const uiCapabilitiesRegex = /^[a-zA-Z0-9:_-]+$/;
|
||||
|
||||
const managementSchema = Joi.object().pattern(
|
||||
managementSectionIdRegex,
|
||||
Joi.array().items(Joi.string().regex(uiCapabilitiesRegex))
|
||||
);
|
||||
const catalogueSchema = Joi.array().items(Joi.string().regex(uiCapabilitiesRegex));
|
||||
|
||||
const privilegeSchema = Joi.object({
|
||||
excludeFromBasePrivileges: Joi.boolean(),
|
||||
management: managementSchema,
|
||||
catalogue: catalogueSchema,
|
||||
api: Joi.array().items(Joi.string()),
|
||||
app: Joi.array().items(Joi.string()),
|
||||
savedObject: Joi.object({
|
||||
all: Joi.array()
|
||||
.items(Joi.string())
|
||||
.required(),
|
||||
read: Joi.array()
|
||||
.items(Joi.string())
|
||||
.required(),
|
||||
}).required(),
|
||||
ui: Joi.array()
|
||||
.items(Joi.string().regex(uiCapabilitiesRegex))
|
||||
.required(),
|
||||
});
|
||||
|
||||
const schema = Joi.object({
|
||||
id: Joi.string()
|
||||
.regex(featurePrivilegePartRegex)
|
||||
.invalid(...prohibitedFeatureIds)
|
||||
.required(),
|
||||
name: Joi.string().required(),
|
||||
excludeFromBasePrivileges: Joi.boolean(),
|
||||
validLicenses: Joi.array().items(Joi.string().valid('basic', 'standard', 'gold', 'platinum')),
|
||||
icon: Joi.string(),
|
||||
description: Joi.string(),
|
||||
navLinkId: Joi.string().regex(uiCapabilitiesRegex),
|
||||
app: Joi.array()
|
||||
.items(Joi.string())
|
||||
.required(),
|
||||
management: managementSchema,
|
||||
catalogue: catalogueSchema,
|
||||
privileges: Joi.object({
|
||||
all: privilegeSchema,
|
||||
read: privilegeSchema,
|
||||
}).required(),
|
||||
privilegesTooltip: Joi.string(),
|
||||
reserved: Joi.object({
|
||||
privilege: privilegeSchema.required(),
|
||||
description: Joi.string().required(),
|
||||
}),
|
||||
});
|
||||
|
||||
export function validateFeature(feature: FeatureWithAllOrReadPrivileges) {
|
||||
const validateResult = Joi.validate(feature, schema);
|
||||
if (validateResult.error) {
|
||||
throw validateResult.error;
|
||||
}
|
||||
// the following validation can't be enforced by the Joi schema, since it'd require us looking "up" the object graph for the list of valid value, which they explicitly forbid.
|
||||
const { app = [], management = {}, catalogue = [] } = feature;
|
||||
|
||||
const privilegeEntries = [...Object.entries(feature.privileges)];
|
||||
if (feature.reserved) {
|
||||
privilegeEntries.push(['reserved', feature.reserved.privilege]);
|
||||
}
|
||||
|
||||
privilegeEntries.forEach(([privilegeId, privilegeDefinition]) => {
|
||||
if (!privilegeDefinition) {
|
||||
throw new Error('Privilege definition may not be null or undefined');
|
||||
}
|
||||
|
||||
const unknownAppEntries = difference(privilegeDefinition.app || [], app);
|
||||
if (unknownAppEntries.length > 0) {
|
||||
throw new Error(
|
||||
`Feature privilege ${
|
||||
feature.id
|
||||
}.${privilegeId} has unknown app entries: ${unknownAppEntries.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
const unknownCatalogueEntries = difference(privilegeDefinition.catalogue || [], catalogue);
|
||||
if (unknownCatalogueEntries.length > 0) {
|
||||
throw new Error(
|
||||
`Feature privilege ${
|
||||
feature.id
|
||||
}.${privilegeId} has unknown catalogue entries: ${unknownCatalogueEntries.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
Object.entries(privilegeDefinition.management || {}).forEach(
|
||||
([managementSectionId, managementEntry]) => {
|
||||
if (!management[managementSectionId]) {
|
||||
throw new Error(
|
||||
`Feature privilege ${feature.id}.${privilegeId} has unknown management section: ${managementSectionId}`
|
||||
);
|
||||
}
|
||||
|
||||
const unknownSectionEntries = difference(managementEntry, management[managementSectionId]);
|
||||
|
||||
if (unknownSectionEntries.length > 0) {
|
||||
throw new Error(
|
||||
`Feature privilege ${
|
||||
feature.id
|
||||
}.${privilegeId} has unknown management entries for section ${managementSectionId}: ${unknownSectionEntries.join(
|
||||
', '
|
||||
)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
21
x-pack/plugins/features/server/index.ts
Normal file
21
x-pack/plugins/features/server/index.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 { PluginInitializerContext } from '../../../../src/core/server';
|
||||
import { Plugin } from './plugin';
|
||||
|
||||
// These exports are part of public Features plugin contract, any change in signature of exported
|
||||
// functions or removal of exports should be considered as a breaking change. Ideally we should
|
||||
// reduce number of such exports to zero and provide everything we want to expose via Setup/Start
|
||||
// run-time contracts.
|
||||
export { uiCapabilitiesRegex } from './feature_schema';
|
||||
|
||||
export { Feature, FeatureWithAllOrReadPrivileges } from './feature';
|
||||
export { FeatureKibanaPrivileges } from './feature_kibana_privileges';
|
||||
export { PluginSetupContract } from './plugin';
|
||||
|
||||
export const plugin = (initializerContext: PluginInitializerContext) =>
|
||||
new Plugin(initializerContext);
|
42
x-pack/plugins/features/server/oss_features.test.ts
Normal file
42
x-pack/plugins/features/server/oss_features.test.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 { buildOSSFeatures } from './oss_features';
|
||||
|
||||
describe('buildOSSFeatures', () => {
|
||||
it('returns features including timelion', () => {
|
||||
expect(
|
||||
buildOSSFeatures({ savedObjectTypes: ['foo', 'bar'], includeTimelion: true }).map(f => f.id)
|
||||
).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"discover",
|
||||
"visualize",
|
||||
"dashboard",
|
||||
"dev_tools",
|
||||
"advancedSettings",
|
||||
"indexPatterns",
|
||||
"savedObjectsManagement",
|
||||
"timelion",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns features excluding timelion', () => {
|
||||
expect(
|
||||
buildOSSFeatures({ savedObjectTypes: ['foo', 'bar'], includeTimelion: false }).map(f => f.id)
|
||||
).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"discover",
|
||||
"visualize",
|
||||
"dashboard",
|
||||
"dev_tools",
|
||||
"advancedSettings",
|
||||
"indexPatterns",
|
||||
"savedObjectsManagement",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -4,13 +4,18 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Feature } from './feature_registry';
|
||||
import { Feature } from './feature';
|
||||
|
||||
const buildKibanaFeatures = (savedObjectTypes: string[]) => {
|
||||
export interface BuildOSSFeaturesParams {
|
||||
savedObjectTypes: string[];
|
||||
includeTimelion: boolean;
|
||||
}
|
||||
|
||||
export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSSFeaturesParams) => {
|
||||
return [
|
||||
{
|
||||
id: 'discover',
|
||||
name: i18n.translate('xpack.main.featureRegistry.discoverFeatureName', {
|
||||
name: i18n.translate('xpack.features.discoverFeatureName', {
|
||||
defaultMessage: 'Discover',
|
||||
}),
|
||||
icon: 'discoverApp',
|
||||
|
@ -36,7 +41,7 @@ const buildKibanaFeatures = (savedObjectTypes: string[]) => {
|
|||
},
|
||||
{
|
||||
id: 'visualize',
|
||||
name: i18n.translate('xpack.main.featureRegistry.visualizeFeatureName', {
|
||||
name: i18n.translate('xpack.features.visualizeFeatureName', {
|
||||
defaultMessage: 'Visualize',
|
||||
}),
|
||||
icon: 'visualizeApp',
|
||||
|
@ -62,7 +67,7 @@ const buildKibanaFeatures = (savedObjectTypes: string[]) => {
|
|||
},
|
||||
{
|
||||
id: 'dashboard',
|
||||
name: i18n.translate('xpack.main.featureRegistry.dashboardFeatureName', {
|
||||
name: i18n.translate('xpack.features.dashboardFeatureName', {
|
||||
defaultMessage: 'Dashboard',
|
||||
}),
|
||||
icon: 'dashboardApp',
|
||||
|
@ -104,7 +109,7 @@ const buildKibanaFeatures = (savedObjectTypes: string[]) => {
|
|||
},
|
||||
{
|
||||
id: 'dev_tools',
|
||||
name: i18n.translate('xpack.main.featureRegistry.devToolsFeatureName', {
|
||||
name: i18n.translate('xpack.features.devToolsFeatureName', {
|
||||
defaultMessage: 'Dev Tools',
|
||||
}),
|
||||
icon: 'devToolsApp',
|
||||
|
@ -129,14 +134,14 @@ const buildKibanaFeatures = (savedObjectTypes: string[]) => {
|
|||
ui: ['show'],
|
||||
},
|
||||
},
|
||||
privilegesTooltip: i18n.translate('xpack.main.featureRegistry.devToolsPrivilegesTooltip', {
|
||||
privilegesTooltip: i18n.translate('xpack.features.devToolsPrivilegesTooltip', {
|
||||
defaultMessage:
|
||||
'User should also be granted the appropriate Elasticsearch cluster and index privileges',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'advancedSettings',
|
||||
name: i18n.translate('xpack.main.featureRegistry.advancedSettingsFeatureName', {
|
||||
name: i18n.translate('xpack.features.advancedSettingsFeatureName', {
|
||||
defaultMessage: 'Advanced Settings',
|
||||
}),
|
||||
icon: 'advancedSettingsApp',
|
||||
|
@ -164,7 +169,7 @@ const buildKibanaFeatures = (savedObjectTypes: string[]) => {
|
|||
},
|
||||
{
|
||||
id: 'indexPatterns',
|
||||
name: i18n.translate('xpack.main.featureRegistry.indexPatternFeatureName', {
|
||||
name: i18n.translate('xpack.features.indexPatternFeatureName', {
|
||||
defaultMessage: 'Index Pattern Management',
|
||||
}),
|
||||
icon: 'indexPatternApp',
|
||||
|
@ -192,7 +197,7 @@ const buildKibanaFeatures = (savedObjectTypes: string[]) => {
|
|||
},
|
||||
{
|
||||
id: 'savedObjectsManagement',
|
||||
name: i18n.translate('xpack.main.featureRegistry.savedObjectsManagementFeatureName', {
|
||||
name: i18n.translate('xpack.features.savedObjectsManagementFeatureName', {
|
||||
defaultMessage: 'Saved Objects Management',
|
||||
}),
|
||||
icon: 'savedObjectsApp',
|
||||
|
@ -220,6 +225,7 @@ const buildKibanaFeatures = (savedObjectTypes: string[]) => {
|
|||
},
|
||||
},
|
||||
},
|
||||
...(includeTimelion ? [timelionFeature] : []),
|
||||
];
|
||||
};
|
||||
|
||||
|
@ -247,17 +253,3 @@ const timelionFeature: Feature = {
|
|||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function registerOssFeatures(
|
||||
registerFeature: (feature: Feature) => void,
|
||||
savedObjectTypes: string[],
|
||||
includeTimelion: boolean
|
||||
) {
|
||||
for (const feature of buildKibanaFeatures(savedObjectTypes)) {
|
||||
registerFeature(feature);
|
||||
}
|
||||
|
||||
if (includeTimelion) {
|
||||
registerFeature(timelionFeature);
|
||||
}
|
||||
}
|
98
x-pack/plugins/features/server/plugin.ts
Normal file
98
x-pack/plugins/features/server/plugin.ts
Normal file
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* 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 {
|
||||
CoreSetup,
|
||||
Logger,
|
||||
PluginInitializerContext,
|
||||
RecursiveReadonly,
|
||||
} from '../../../../src/core/server';
|
||||
import { Capabilities as UICapabilities } from '../../../../src/core/public';
|
||||
import { deepFreeze } from '../../../../src/core/utils';
|
||||
import { XPackInfo } from '../../../legacy/plugins/xpack_main/server/lib/xpack_info';
|
||||
import { PluginSetupContract as TimelionSetupContract } from '../../../../src/plugins/timelion/server';
|
||||
import { FeatureRegistry } from './feature_registry';
|
||||
import { Feature, FeatureWithAllOrReadPrivileges } from './feature';
|
||||
import { uiCapabilitiesForFeatures } from './ui_capabilities_for_features';
|
||||
import { buildOSSFeatures } from './oss_features';
|
||||
import { defineRoutes } from './routes';
|
||||
|
||||
/**
|
||||
* Describes public Features plugin contract returned at the `setup` stage.
|
||||
*/
|
||||
export interface PluginSetupContract {
|
||||
registerFeature(feature: FeatureWithAllOrReadPrivileges): void;
|
||||
getFeatures(): Feature[];
|
||||
getFeaturesUICapabilities(): UICapabilities;
|
||||
registerLegacyAPI: (legacyAPI: LegacyAPI) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes a set of APIs that are available in the legacy platform only and required by this plugin
|
||||
* to function properly.
|
||||
*/
|
||||
export interface LegacyAPI {
|
||||
xpackInfo: Pick<XPackInfo, 'license'>;
|
||||
savedObjectTypes: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents Features Plugin instance that will be managed by the Kibana plugin system.
|
||||
*/
|
||||
export class Plugin {
|
||||
private readonly logger: Logger;
|
||||
|
||||
private legacyAPI?: LegacyAPI;
|
||||
private readonly getLegacyAPI = () => {
|
||||
if (!this.legacyAPI) {
|
||||
throw new Error('Legacy API is not registered!');
|
||||
}
|
||||
return this.legacyAPI;
|
||||
};
|
||||
|
||||
constructor(private readonly initializerContext: PluginInitializerContext) {
|
||||
this.logger = this.initializerContext.logger.get();
|
||||
}
|
||||
|
||||
public async setup(
|
||||
core: CoreSetup,
|
||||
{ timelion }: { timelion?: TimelionSetupContract }
|
||||
): Promise<RecursiveReadonly<PluginSetupContract>> {
|
||||
const featureRegistry = new FeatureRegistry();
|
||||
|
||||
defineRoutes({
|
||||
router: core.http.createRouter(),
|
||||
featureRegistry,
|
||||
getLegacyAPI: this.getLegacyAPI,
|
||||
});
|
||||
|
||||
return deepFreeze({
|
||||
registerFeature: featureRegistry.register.bind(featureRegistry),
|
||||
getFeatures: featureRegistry.getAll.bind(featureRegistry),
|
||||
getFeaturesUICapabilities: () => uiCapabilitiesForFeatures(featureRegistry.getAll()),
|
||||
|
||||
registerLegacyAPI: (legacyAPI: LegacyAPI) => {
|
||||
this.legacyAPI = legacyAPI;
|
||||
|
||||
// Register OSS features.
|
||||
for (const feature of buildOSSFeatures({
|
||||
savedObjectTypes: this.legacyAPI.savedObjectTypes,
|
||||
includeTimelion: timelion !== undefined && timelion.uiEnabled,
|
||||
})) {
|
||||
featureRegistry.register(feature);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public start() {
|
||||
this.logger.debug('Starting plugin');
|
||||
}
|
||||
|
||||
public stop() {
|
||||
this.logger.debug('Stopping plugin');
|
||||
}
|
||||
}
|
110
x-pack/plugins/features/server/routes/index.test.ts
Normal file
110
x-pack/plugins/features/server/routes/index.test.ts
Normal file
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* 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 { FeatureRegistry } from '../feature_registry';
|
||||
import { defineRoutes } from './index';
|
||||
|
||||
import { httpServerMock, httpServiceMock } from '../../../../../src/core/server/mocks';
|
||||
import { XPackInfoLicense } from '../../../../legacy/plugins/xpack_main/server/lib/xpack_info_license';
|
||||
import { RequestHandler } from '../../../../../src/core/server';
|
||||
|
||||
let currentLicenseLevel: string = 'gold';
|
||||
|
||||
describe('GET /api/features', () => {
|
||||
let routeHandler: RequestHandler<any, any, any>;
|
||||
beforeEach(() => {
|
||||
const featureRegistry = new FeatureRegistry();
|
||||
featureRegistry.register({
|
||||
id: 'feature_1',
|
||||
name: 'Feature 1',
|
||||
app: [],
|
||||
privileges: {},
|
||||
});
|
||||
|
||||
featureRegistry.register({
|
||||
id: 'licensed_feature',
|
||||
name: 'Licensed Feature',
|
||||
app: ['bar-app'],
|
||||
validLicenses: ['gold'],
|
||||
privileges: {},
|
||||
});
|
||||
|
||||
const routerMock = httpServiceMock.createRouter();
|
||||
defineRoutes({
|
||||
router: routerMock,
|
||||
featureRegistry,
|
||||
getLegacyAPI: () => ({
|
||||
xpackInfo: {
|
||||
license: {
|
||||
isOneOf(candidateLicenses: string[]) {
|
||||
return candidateLicenses.includes(currentLicenseLevel);
|
||||
},
|
||||
} as XPackInfoLicense,
|
||||
},
|
||||
savedObjectTypes: [],
|
||||
}),
|
||||
});
|
||||
|
||||
routeHandler = routerMock.get.mock.calls[0][1];
|
||||
});
|
||||
|
||||
it('returns a list of available features', async () => {
|
||||
const mockResponse = httpServerMock.createResponseFactory();
|
||||
routeHandler(undefined as any, undefined as any, mockResponse);
|
||||
|
||||
expect(mockResponse.ok.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"body": Array [
|
||||
Object {
|
||||
"app": Array [],
|
||||
"id": "feature_1",
|
||||
"name": "Feature 1",
|
||||
"privileges": Object {},
|
||||
},
|
||||
Object {
|
||||
"app": Array [
|
||||
"bar-app",
|
||||
],
|
||||
"id": "licensed_feature",
|
||||
"name": "Licensed Feature",
|
||||
"privileges": Object {},
|
||||
"validLicenses": Array [
|
||||
"gold",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it(`does not return features that arent allowed by current license`, async () => {
|
||||
currentLicenseLevel = 'basic';
|
||||
|
||||
const mockResponse = httpServerMock.createResponseFactory();
|
||||
routeHandler(undefined as any, undefined as any, mockResponse);
|
||||
|
||||
expect(mockResponse.ok.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"body": Array [
|
||||
Object {
|
||||
"app": Array [],
|
||||
"id": "feature_1",
|
||||
"name": "Feature 1",
|
||||
"privileges": Object {},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
36
x-pack/plugins/features/server/routes/index.ts
Normal file
36
x-pack/plugins/features/server/routes/index.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 { IRouter } from '../../../../../src/core/server';
|
||||
import { LegacyAPI } from '../plugin';
|
||||
import { FeatureRegistry } from '../feature_registry';
|
||||
|
||||
/**
|
||||
* Describes parameters used to define HTTP routes.
|
||||
*/
|
||||
export interface RouteDefinitionParams {
|
||||
router: IRouter;
|
||||
featureRegistry: FeatureRegistry;
|
||||
getLegacyAPI: () => LegacyAPI;
|
||||
}
|
||||
|
||||
export function defineRoutes({ router, featureRegistry, getLegacyAPI }: RouteDefinitionParams) {
|
||||
router.get(
|
||||
{ path: '/api/features', options: { tags: ['access:features'] }, validate: false },
|
||||
(context, request, response) => {
|
||||
const allFeatures = featureRegistry.getAll();
|
||||
|
||||
return response.ok({
|
||||
body: allFeatures.filter(
|
||||
feature =>
|
||||
!feature.validLicenses ||
|
||||
!feature.validLicenses.length ||
|
||||
getLegacyAPI().xpackInfo.license.isOneOf(feature.validLicenses)
|
||||
),
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
/*
|
||||
* 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 { uiCapabilitiesForFeatures } from './ui_capabilities_for_features';
|
||||
|
||||
function createFeaturePrivilege(key: string, capabilities: string[] = []) {
|
||||
return {
|
||||
[key]: {
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
app: [],
|
||||
ui: [...capabilities],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('populateUICapabilities', () => {
|
||||
it('handles no original uiCapabilities and no registered features gracefully', () => {
|
||||
expect(uiCapabilitiesForFeatures([])).toEqual({});
|
||||
});
|
||||
|
||||
it('handles features with no registered capabilities', () => {
|
||||
expect(
|
||||
uiCapabilitiesForFeatures([
|
||||
{
|
||||
id: 'newFeature',
|
||||
name: 'my new feature',
|
||||
app: ['bar-app'],
|
||||
privileges: {
|
||||
...createFeaturePrivilege('all'),
|
||||
},
|
||||
},
|
||||
])
|
||||
).toEqual({
|
||||
catalogue: {},
|
||||
newFeature: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('augments the original uiCapabilities with registered feature capabilities', () => {
|
||||
expect(
|
||||
uiCapabilitiesForFeatures([
|
||||
{
|
||||
id: 'newFeature',
|
||||
name: 'my new feature',
|
||||
navLinkId: 'newFeatureNavLink',
|
||||
app: ['bar-app'],
|
||||
privileges: {
|
||||
...createFeaturePrivilege('all', ['capability1', 'capability2']),
|
||||
},
|
||||
},
|
||||
])
|
||||
).toEqual({
|
||||
catalogue: {},
|
||||
newFeature: {
|
||||
capability1: true,
|
||||
capability2: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('combines catalogue entries from multiple features', () => {
|
||||
expect(
|
||||
uiCapabilitiesForFeatures([
|
||||
{
|
||||
id: 'newFeature',
|
||||
name: 'my new feature',
|
||||
navLinkId: 'newFeatureNavLink',
|
||||
app: ['bar-app'],
|
||||
catalogue: ['anotherFooEntry', 'anotherBarEntry'],
|
||||
privileges: {
|
||||
...createFeaturePrivilege('foo', ['capability1', 'capability2']),
|
||||
...createFeaturePrivilege('bar', ['capability3', 'capability4']),
|
||||
...createFeaturePrivilege('baz'),
|
||||
},
|
||||
},
|
||||
])
|
||||
).toEqual({
|
||||
catalogue: {
|
||||
anotherFooEntry: true,
|
||||
anotherBarEntry: true,
|
||||
},
|
||||
newFeature: {
|
||||
capability1: true,
|
||||
capability2: true,
|
||||
capability3: true,
|
||||
capability4: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it(`merges capabilities from all feature privileges`, () => {
|
||||
expect(
|
||||
uiCapabilitiesForFeatures([
|
||||
{
|
||||
id: 'newFeature',
|
||||
name: 'my new feature',
|
||||
navLinkId: 'newFeatureNavLink',
|
||||
app: ['bar-app'],
|
||||
privileges: {
|
||||
...createFeaturePrivilege('foo', ['capability1', 'capability2']),
|
||||
...createFeaturePrivilege('bar', ['capability3', 'capability4']),
|
||||
...createFeaturePrivilege('baz', ['capability1', 'capability5']),
|
||||
},
|
||||
},
|
||||
])
|
||||
).toEqual({
|
||||
catalogue: {},
|
||||
newFeature: {
|
||||
capability1: true,
|
||||
capability2: true,
|
||||
capability3: true,
|
||||
capability4: true,
|
||||
capability5: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('supports merging multiple features with multiple privileges each', () => {
|
||||
expect(
|
||||
uiCapabilitiesForFeatures([
|
||||
{
|
||||
id: 'newFeature',
|
||||
name: 'my new feature',
|
||||
navLinkId: 'newFeatureNavLink',
|
||||
app: ['bar-app'],
|
||||
privileges: {
|
||||
...createFeaturePrivilege('foo', ['capability1', 'capability2']),
|
||||
...createFeaturePrivilege('bar', ['capability3', 'capability4']),
|
||||
...createFeaturePrivilege('baz', ['capability1', 'capability5']),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'anotherNewFeature',
|
||||
name: 'another new feature',
|
||||
app: ['bar-app'],
|
||||
privileges: {
|
||||
...createFeaturePrivilege('foo', ['capability1', 'capability2']),
|
||||
...createFeaturePrivilege('bar', ['capability3', 'capability4']),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'yetAnotherNewFeature',
|
||||
name: 'yet another new feature',
|
||||
navLinkId: 'yetAnotherNavLink',
|
||||
app: ['bar-app'],
|
||||
privileges: {
|
||||
...createFeaturePrivilege('all', ['capability1', 'capability2']),
|
||||
...createFeaturePrivilege('read', []),
|
||||
...createFeaturePrivilege('somethingInBetween', [
|
||||
'something1',
|
||||
'something2',
|
||||
'something3',
|
||||
]),
|
||||
},
|
||||
},
|
||||
])
|
||||
).toEqual({
|
||||
anotherNewFeature: {
|
||||
capability1: true,
|
||||
capability2: true,
|
||||
capability3: true,
|
||||
capability4: true,
|
||||
},
|
||||
catalogue: {},
|
||||
newFeature: {
|
||||
capability1: true,
|
||||
capability2: true,
|
||||
capability3: true,
|
||||
capability4: true,
|
||||
capability5: true,
|
||||
},
|
||||
yetAnotherNewFeature: {
|
||||
capability1: true,
|
||||
capability2: true,
|
||||
something1: true,
|
||||
something2: true,
|
||||
something3: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -5,8 +5,8 @@
|
|||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { UICapabilities } from 'ui/capabilities';
|
||||
import { Feature } from '../../types';
|
||||
import { Capabilities as UICapabilities } from '../../../../src/core/public';
|
||||
import { Feature } from './feature';
|
||||
|
||||
const ELIGIBLE_FLAT_MERGE_KEYS = ['catalogue'];
|
||||
|
||||
|
@ -14,9 +14,7 @@ interface FeatureCapabilities {
|
|||
[featureId: string]: Record<string, boolean>;
|
||||
}
|
||||
|
||||
export function uiCapabilitiesForFeatures(xpackMainPlugin: Record<string, any>): UICapabilities {
|
||||
const features: Feature[] = xpackMainPlugin.getFeatures();
|
||||
|
||||
export function uiCapabilitiesForFeatures(features: Feature[]): UICapabilities {
|
||||
const featureCapabilities: FeatureCapabilities[] = features.map(getCapabilitiesFromFeature);
|
||||
|
||||
return buildCapabilities(...featureCapabilities);
|
|
@ -4501,6 +4501,14 @@
|
|||
"xpack.dashboardMode.dashboardViewerTitle": "ダッシュボードビューアー",
|
||||
"xpack.dashboardMode.uiSettings.dashboardsOnlyRolesDescription": "ダッシュボード表示専用モードのロールです",
|
||||
"xpack.dashboardMode.uiSettings.dashboardsOnlyRolesTitle": "ダッシュボード専用ロール",
|
||||
"xpack.features.advancedSettingsFeatureName": "高度な設定",
|
||||
"xpack.features.dashboardFeatureName": "ダッシュボード",
|
||||
"xpack.features.devToolsFeatureName": "開発ツール",
|
||||
"xpack.features.devToolsPrivilegesTooltip": "また、ユーザーに適切な Elasticsearch クラスターとインデックスの権限が与えられている必要があります。",
|
||||
"xpack.features.discoverFeatureName": "ディスカバー",
|
||||
"xpack.features.indexPatternFeatureName": "インデックスパターン管理",
|
||||
"xpack.features.savedObjectsManagementFeatureName": "保存されたオブジェクトの管理",
|
||||
"xpack.features.visualizeFeatureName": "可視化",
|
||||
"xpack.graph.badge.readOnly.text": "読み込み専用",
|
||||
"xpack.graph.badge.readOnly.tooltip": "Graph ワークスペースを保存できません",
|
||||
"xpack.graph.clearWorkspace.confirmButtonLabel": "ワークスペースを消去",
|
||||
|
@ -5371,14 +5379,6 @@
|
|||
"xpack.logstash.upgradeFailureActions.goBackButtonLabel": "戻る",
|
||||
"xpack.logstash.upstreamPipelineArgumentMustContainAnIdPropertyErrorMessage": "upstreamPipeline 引数には id プロパティを含める必要があります",
|
||||
"xpack.logstash.workersTooltip": "パイプラインのフィルターとアウトプットステージを同時に実行するワーカーの数です。イベントが詰まってしまう場合や CPU が飽和状態ではない場合は、マシンの処理能力をより有効に活用するため、この数字を上げてみてください。\n\nデフォルト値:ホストの CPU コア数です",
|
||||
"xpack.main.featureRegistry.advancedSettingsFeatureName": "高度な設定",
|
||||
"xpack.main.featureRegistry.dashboardFeatureName": "ダッシュボード",
|
||||
"xpack.main.featureRegistry.devToolsFeatureName": "開発ツール",
|
||||
"xpack.main.featureRegistry.devToolsPrivilegesTooltip": "また、ユーザーに適切な Elasticsearch クラスターとインデックスの権限が与えられている必要があります。",
|
||||
"xpack.main.featureRegistry.discoverFeatureName": "ディスカバー",
|
||||
"xpack.main.featureRegistry.indexPatternFeatureName": "インデックスパターン管理",
|
||||
"xpack.main.featureRegistry.savedObjectsManagementFeatureName": "保存されたオブジェクトの管理",
|
||||
"xpack.main.featureRegistry.visualizeFeatureName": "可視化",
|
||||
"xpack.main.welcomeBanner.licenseIsExpiredDescription": "管理者または {updateYourLicenseLinkText} に直接お問い合わせください。",
|
||||
"xpack.main.welcomeBanner.licenseIsExpiredDescription.updateYourLicenseLinkText": "ライセンスを更新",
|
||||
"xpack.main.welcomeBanner.licenseIsExpiredTitle": "ご使用の {licenseType} ライセンスは期限切れです",
|
||||
|
|
|
@ -4644,6 +4644,14 @@
|
|||
"xpack.dashboardMode.dashboardViewerTitle": "仪表板查看器",
|
||||
"xpack.dashboardMode.uiSettings.dashboardsOnlyRolesDescription": "属于“仅查看仪表板”模式的角色",
|
||||
"xpack.dashboardMode.uiSettings.dashboardsOnlyRolesTitle": "仅限仪表板的角色",
|
||||
"xpack.features.advancedSettingsFeatureName": "高级设置",
|
||||
"xpack.features.dashboardFeatureName": "仪表板",
|
||||
"xpack.features.devToolsFeatureName": "开发工具",
|
||||
"xpack.features.devToolsPrivilegesTooltip": "还应向用户授予适当的 Elasticsearch 集群和索引权限",
|
||||
"xpack.features.discoverFeatureName": "Discover",
|
||||
"xpack.features.indexPatternFeatureName": "索引模式管理",
|
||||
"xpack.features.savedObjectsManagementFeatureName": "已保存对象管理",
|
||||
"xpack.features.visualizeFeatureName": "可视化",
|
||||
"xpack.graph.badge.readOnly.text": "只读",
|
||||
"xpack.graph.badge.readOnly.tooltip": "无法保存 Graph 工作空间",
|
||||
"xpack.graph.clearWorkspace.confirmButtonLabel": "清除工作空间",
|
||||
|
@ -5514,14 +5522,6 @@
|
|||
"xpack.logstash.upgradeFailureActions.goBackButtonLabel": "返回",
|
||||
"xpack.logstash.upstreamPipelineArgumentMustContainAnIdPropertyErrorMessage": "upstreamPipeline 参数必须包含 id 属性",
|
||||
"xpack.logstash.workersTooltip": "并行执行管道的筛选和输出阶段的工作线程数目。如果您发现事件出现积压或 CPU 未饱和,请考虑增大此数值,以更好地利用机器处理能力。\n\n默认值:主机的 CPU 核心数",
|
||||
"xpack.main.featureRegistry.advancedSettingsFeatureName": "高级设置",
|
||||
"xpack.main.featureRegistry.dashboardFeatureName": "仪表板",
|
||||
"xpack.main.featureRegistry.devToolsFeatureName": "开发工具",
|
||||
"xpack.main.featureRegistry.devToolsPrivilegesTooltip": "还应向用户授予适当的 Elasticsearch 集群和索引权限",
|
||||
"xpack.main.featureRegistry.discoverFeatureName": "Discover",
|
||||
"xpack.main.featureRegistry.indexPatternFeatureName": "索引模式管理",
|
||||
"xpack.main.featureRegistry.savedObjectsManagementFeatureName": "已保存对象管理",
|
||||
"xpack.main.featureRegistry.visualizeFeatureName": "可视化",
|
||||
"xpack.main.welcomeBanner.licenseIsExpiredDescription": "联系您的管理员或直接{updateYourLicenseLinkText}。",
|
||||
"xpack.main.welcomeBanner.licenseIsExpiredDescription.updateYourLicenseLinkText": "更新您的许可",
|
||||
"xpack.main.welcomeBanner.licenseIsExpiredTitle": "您的{licenseType}许可已过期",
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import expect from '@kbn/expect';
|
||||
import { SecurityService } from '../../../../common/services';
|
||||
import { Feature } from '../../../../../legacy/plugins/xpack_main/types';
|
||||
import { Feature } from '../../../../../plugins/features/server';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default function({ getService }: FtrProviderContext) {
|
||||
|
@ -15,18 +15,6 @@ export default function({ getService }: FtrProviderContext) {
|
|||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const security: SecurityService = getService('security');
|
||||
|
||||
const expect404 = (result: any) => {
|
||||
expect(result.error).to.be(undefined);
|
||||
expect(result.response).not.to.be(undefined);
|
||||
expect(result.response).to.have.property('statusCode', 404);
|
||||
};
|
||||
|
||||
const expect200 = (result: any) => {
|
||||
expect(result.error).to.be(undefined);
|
||||
expect(result.response).not.to.be(undefined);
|
||||
expect(result.response).to.have.property('statusCode', 200);
|
||||
};
|
||||
|
||||
describe('/api/features', () => {
|
||||
describe('with the "global all" privilege', () => {
|
||||
it('should return a 200', async () => {
|
||||
|
@ -50,14 +38,11 @@ export default function({ getService }: FtrProviderContext) {
|
|||
full_name: 'a kibana user',
|
||||
});
|
||||
|
||||
const result = await supertestWithoutAuth
|
||||
.get(`/api/features/v1`)
|
||||
await supertestWithoutAuth
|
||||
.get('/api/features')
|
||||
.auth(username, password)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.then((response: any) => ({ error: undefined, response }))
|
||||
.catch((error: any) => ({ error, response: undefined }));
|
||||
|
||||
expect200(result);
|
||||
.expect(200);
|
||||
} finally {
|
||||
await security.role.delete(roleName);
|
||||
await security.user.delete(username);
|
||||
|
@ -88,14 +73,11 @@ export default function({ getService }: FtrProviderContext) {
|
|||
full_name: 'a kibana user',
|
||||
});
|
||||
|
||||
const result = await supertestWithoutAuth
|
||||
.get(`/api/features/v1`)
|
||||
await supertestWithoutAuth
|
||||
.get('/api/features')
|
||||
.auth(username, password)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.then((response: any) => ({ error: undefined, response }))
|
||||
.catch((error: any) => ({ error, response: undefined }));
|
||||
|
||||
expect404(result);
|
||||
.expect(404);
|
||||
} finally {
|
||||
await security.role.delete(roleName);
|
||||
await security.user.delete(username);
|
||||
|
@ -106,7 +88,7 @@ export default function({ getService }: FtrProviderContext) {
|
|||
describe('with trial license', () => {
|
||||
it('should return a full feature set', async () => {
|
||||
const { body } = await supertest
|
||||
.get('/api/features/v1')
|
||||
.get('/api/features')
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.expect(200);
|
||||
|
|
@ -4,10 +4,10 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export {
|
||||
Feature,
|
||||
FeatureKibanaPrivileges,
|
||||
FeatureRegistry,
|
||||
FeatureWithAllOrReadPrivileges,
|
||||
uiCapabilitiesRegex,
|
||||
} from './feature_registry';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function({ loadTestFile }: FtrProviderContext) {
|
||||
describe('features', () => {
|
||||
loadTestFile(require.resolve('./features'));
|
||||
});
|
||||
}
|
|
@ -13,6 +13,7 @@ export default function ({ loadTestFile }) {
|
|||
loadTestFile(require.resolve('./spaces'));
|
||||
loadTestFile(require.resolve('./monitoring'));
|
||||
loadTestFile(require.resolve('./xpack_main'));
|
||||
loadTestFile(require.resolve('./features'));
|
||||
loadTestFile(require.resolve('./telemetry'));
|
||||
loadTestFile(require.resolve('./logstash'));
|
||||
loadTestFile(require.resolve('./kibana'));
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
export default function ({ loadTestFile }) {
|
||||
describe('xpack_main', () => {
|
||||
loadTestFile(require.resolve('./features'));
|
||||
loadTestFile(require.resolve('./settings'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -23,8 +23,8 @@ export class FeaturesService {
|
|||
}
|
||||
|
||||
public async get(): Promise<Features> {
|
||||
this.log.debug(`requesting /api/features/v1 to get the features`);
|
||||
const response = await this.axios.get('/api/features/v1');
|
||||
this.log.debug('requesting /api/features to get the features');
|
||||
const response = await this.axios.get('/api/features');
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue