[7.x] Hide management sections based on cluster/index privileges (#67791) (#77345)

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Larry Gregory 2020-09-14 14:33:17 -04:00 committed by GitHub
parent c3a2451833
commit 047152f890
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
238 changed files with 7114 additions and 2495 deletions

View file

@ -9,13 +9,12 @@ Registering features also gives your plugin access to “UI Capabilities”. The
=== Registering a feature === Registering a feature
Feature registration is controlled via the built-in `xpack_main` plugin. To register a feature, call `xpack_main`'s `registerFeature` function from your plugin's `init` function, and provide the appropriate details: Feature registration is controlled via the built-in `features` plugin. To register a feature, call `features`'s `registerKibanaFeature` function from your plugin's `setup` lifecycle function, and provide the appropriate details:
["source","javascript"] ["source","javascript"]
----------- -----------
init(server) { setup(core, { features }) {
const xpackMainPlugin = server.plugins.xpack_main; features.registerKibanaFeature({
xpackMainPlugin.registerFeature({
// feature details here. // feature details here.
}); });
} }
@ -45,12 +44,12 @@ 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. |An array of applications this feature enables. Typically, all of your plugin's apps (from `uiExports`) will be included here.
|`privileges` (required) |`privileges` (required)
|{kib-repo}blob/{branch}/x-pack/plugins/features/common/feature.ts[`FeatureConfig`]. |{kib-repo}blob/{branch}/x-pack/plugins/features/common/feature.ts[`KibanaFeatureConfig`].
|See <<example-1-canvas,Example 1>> and <<example-2-dev-tools,Example 2>> |See <<example-1-canvas,Example 1>> and <<example-2-dev-tools,Example 2>>
|The set of privileges this feature requires to function. |The set of privileges this feature requires to function.
|`subFeatures` (optional) |`subFeatures` (optional)
|{kib-repo}blob/{branch}/x-pack/plugins/features/common/feature.ts[`FeatureConfig`]. |{kib-repo}blob/{branch}/x-pack/plugins/features/common/feature.ts[`KibanaFeatureConfig`].
|See <<example-3-discover,Example 3>> |See <<example-3-discover,Example 3>>
|The set of subfeatures that enables finer access control than the `all` and `read` feature privileges. These options are only available in the Gold subscription level and higher. |The set of subfeatures that enables finer access control than the `all` and `read` feature privileges. These options are only available in the Gold subscription level and higher.
@ -73,15 +72,17 @@ For a full explanation of fields and options, consult the {kib-repo}blob/{branch
=== Using UI Capabilities === Using UI Capabilities
UI Capabilities are available to your public (client) plugin code. These capabilities are read-only, and are used to inform the UI. This object is namespaced by feature id. For example, if your feature id is “foo”, then your UI Capabilities are stored at `uiCapabilities.foo`. UI Capabilities are available to your public (client) plugin code. These capabilities are read-only, and are used to inform the UI. This object is namespaced by feature id. For example, if your feature id is “foo”, then your UI Capabilities are stored at `uiCapabilities.foo`.
To access capabilities, import them from `ui/capabilities`: Capabilities can be accessed from your plugin's `start` lifecycle from the `core.application` service:
["source","javascript"] ["source","javascript"]
----------- -----------
import { uiCapabilities } from 'ui/capabilities'; public start(core) {
const { capabilities } = core.application;
const canUserSave = uiCapabilities.foo.save; const canUserSave = capabilities.foo.save;
if (canUserSave) { if (canUserSave) {
// show save button // show save button
}
} }
----------- -----------
@ -89,9 +90,8 @@ if (canUserSave) {
=== Example 1: Canvas Application === Example 1: Canvas Application
["source","javascript"] ["source","javascript"]
----------- -----------
init(server) { public setup(core, { features }) {
const xpackMainPlugin = server.plugins.xpack_main; features.registerKibanaFeature({
xpackMainPlugin.registerFeature({
id: 'canvas', id: 'canvas',
name: 'Canvas', name: 'Canvas',
icon: 'canvasApp', icon: 'canvasApp',
@ -130,11 +130,13 @@ The `all` privilege defines a single “save” UI Capability. To access this in
["source","javascript"] ["source","javascript"]
----------- -----------
import { uiCapabilities } from 'ui/capabilities'; public start(core) {
const { capabilities } = core.application;
const canUserSave = uiCapabilities.canvas.save; const canUserSave = capabilities.canvas.save;
if (canUserSave) { if (canUserSave) {
// show save button // show save button
}
} }
----------- -----------
@ -145,9 +147,8 @@ Because the `read` privilege does not define the `save` capability, users with r
["source","javascript"] ["source","javascript"]
----------- -----------
init(server) { public setup(core, { features }) {
const xpackMainPlugin = server.plugins.xpack_main; features.registerKibanaFeature({
xpackMainPlugin.registerFeature({
id: 'dev_tools', id: 'dev_tools',
name: i18n.translate('xpack.features.devToolsFeatureName', { name: i18n.translate('xpack.features.devToolsFeatureName', {
defaultMessage: 'Dev Tools', defaultMessage: 'Dev Tools',
@ -206,9 +207,8 @@ a single "Create Short URLs" subfeature privilege is defined, which allows users
["source","javascript"] ["source","javascript"]
----------- -----------
init(server) { public setup(core, { features }) {
const xpackMainPlugin = server.plugins.xpack_main; features.registerKibanaFeature({
xpackMainPlugin.registerFeature({
{ {
id: 'discover', id: 'discover',
name: i18n.translate('xpack.features.discoverFeatureName', { name: i18n.translate('xpack.features.discoverFeatureName', {

View file

@ -25,4 +25,10 @@ created index. For more information, see {ref}/indices-templates.html[Index temp
* *Delete a policy.* You cant delete a policy that is currently in use or * *Delete a policy.* You cant delete a policy that is currently in use or
recover a deleted index. recover a deleted index.
[float]
=== Required permissions
The `manage_ilm` cluster privilege is required to access *Index lifecycle policies*.
You can add these privileges in *Stack Management > Security > Roles*.

View file

@ -20,6 +20,13 @@ image::images/cross-cluster-replication-list-view.png[][Cross-cluster replicatio
* The Elasticsearch version of the local cluster must be the same as or newer than the remote cluster. * The Elasticsearch version of the local cluster must be the same as or newer than the remote cluster.
Refer to {ref}/ccr-overview.html[this document] for more information. Refer to {ref}/ccr-overview.html[this document] for more information.
[float]
=== Required permissions
The `manage` and `manage_ccr` cluster privileges are required to access *Cross-Cluster Replication*.
You can add these privileges in *Stack Management > Security > Roles*.
[float] [float]
[[configure-replication]] [[configure-replication]]
=== Configure replication === Configure replication

View file

@ -29,6 +29,13 @@ See {ref}/encrypting-communications.html[Encrypting communications].
{kib} and the {ref}/start-basic.html[start basic API] provide a list of all of {kib} and the {ref}/start-basic.html[start basic API] provide a list of all of
the features that will no longer be supported if you revert to a basic license. the features that will no longer be supported if you revert to a basic license.
[float]
=== Required permissions
The `manage` cluster privilege is required to access *License Management*.
You can add this privilege in *Stack Management > Security > Roles*.
[discrete] [discrete]
[[update-license]] [[update-license]]
=== Update your license === Update your license

View file

@ -11,6 +11,13 @@ To get started, open the menu, then go to *Stack Management > Data > Remote Clus
[role="screenshot"] [role="screenshot"]
image::images/remote-clusters-list-view.png[Remote Clusters list view, including Add a remote cluster button] image::images/remote-clusters-list-view.png[Remote Clusters list view, including Add a remote cluster button]
[float]
=== Required permissions
The `manage` cluster privilege is required to access *Remote Clusters*.
You can add this privilege in *Stack Management > Security > Roles*.
[float] [float]
[[managing-remote-clusters]] [[managing-remote-clusters]]
=== Add a remote cluster === Add a remote cluster

View file

@ -20,6 +20,13 @@ image::images/management_rollup_list.png[][List of currently active rollup jobs]
Before using this feature, you should be familiar with how rollups work. Before using this feature, you should be familiar with how rollups work.
{ref}/xpack-rollup.html[Rolling up historical data] is a good source for more detailed information. {ref}/xpack-rollup.html[Rolling up historical data] is a good source for more detailed information.
[float]
=== Required permissions
The `manage_rollup` cluster privilege is required to access *Rollup jobs*.
You can add this privilege in *Stack Management > Security > Roles*.
[float] [float]
[[create-and-manage-rollup-job]] [[create-and-manage-rollup-job]]
=== Create a rollup job === Create a rollup job

View file

@ -13,6 +13,14 @@ Before you upgrade, make sure that you are using the latest released minor
version of {es} to see the most up-to-date deprecation issues. version of {es} to see the most up-to-date deprecation issues.
For example, if you want to upgrade to to 7.0, make sure that you are using 6.8. For example, if you want to upgrade to to 7.0, make sure that you are using 6.8.
[float]
=== Required permissions
The `manage` cluster privilege is required to access the *Upgrade assistant*.
Additional privileges may be needed to perform certain actions.
You can add this privilege in *Stack Management > Security > Roles*.
[float] [float]
=== Reindexing === Reindexing

View file

@ -38,7 +38,7 @@ export class AlertingExamplePlugin implements Plugin<void, void, AlertingExample
alerts.registerType(alwaysFiringAlert); alerts.registerType(alwaysFiringAlert);
alerts.registerType(peopleInSpaceAlert); alerts.registerType(peopleInSpaceAlert);
features.registerFeature({ features.registerKibanaFeature({
id: ALERTING_EXAMPLE_APP_ID, id: ALERTING_EXAMPLE_APP_ID,
name: i18n.translate('alertsExample.featureRegistry.alertsExampleFeatureName', { name: i18n.translate('alertsExample.featureRegistry.alertsExampleFeatureName', {
defaultMessage: 'Alerts Example', defaultMessage: 'Alerts Example',

View file

@ -1283,7 +1283,7 @@ _See also: [Server's CoreSetup API Docs](/docs/development/core/server/kibana-pl
##### Plugin services ##### Plugin services
| Legacy Platform | New Platform | Notes | | Legacy Platform | New Platform | Notes |
| ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | ----- | | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | ----- |
| `server.plugins.xpack_main.registerFeature` | [`plugins.features.registerFeature`](x-pack/plugins/features/server/plugin.ts) | | | `server.plugins.xpack_main.registerFeature` | [`plugins.features.registerKibanaFeature`](x-pack/plugins/features/server/plugin.ts) | |
| `server.plugins.xpack_main.feature(pluginID).registerLicenseCheckResultsGenerator` | [`x-pack licensing plugin`](/x-pack/plugins/licensing/README.md) | | | `server.plugins.xpack_main.feature(pluginID).registerLicenseCheckResultsGenerator` | [`x-pack licensing plugin`](/x-pack/plugins/licensing/README.md) | |
#### UI Exports #### UI Exports

View file

@ -18,6 +18,7 @@
*/ */
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { BehaviorSubject } from 'rxjs';
import { ManagementSetup, ManagementStart } from './types'; import { ManagementSetup, ManagementStart } from './types';
import { FeatureCatalogueCategory, HomePublicPluginSetup } from '../../home/public'; import { FeatureCatalogueCategory, HomePublicPluginSetup } from '../../home/public';
import { import {
@ -27,6 +28,9 @@ import {
DEFAULT_APP_CATEGORIES, DEFAULT_APP_CATEGORIES,
PluginInitializerContext, PluginInitializerContext,
AppMountParameters, AppMountParameters,
AppUpdater,
AppStatus,
AppNavLinkStatus,
} from '../../../core/public'; } from '../../../core/public';
import { import {
@ -41,6 +45,8 @@ interface ManagementSetupDependencies {
export class ManagementPlugin implements Plugin<ManagementSetup, ManagementStart> { export class ManagementPlugin implements Plugin<ManagementSetup, ManagementStart> {
private readonly managementSections = new ManagementSectionsService(); private readonly managementSections = new ManagementSectionsService();
private readonly appUpdater = new BehaviorSubject<AppUpdater>(() => ({}));
constructor(private initializerContext: PluginInitializerContext) {} constructor(private initializerContext: PluginInitializerContext) {}
public setup(core: CoreSetup, { home }: ManagementSetupDependencies) { public setup(core: CoreSetup, { home }: ManagementSetupDependencies) {
@ -70,6 +76,7 @@ export class ManagementPlugin implements Plugin<ManagementSetup, ManagementStart
order: 9040, order: 9040,
euiIconType: 'managementApp', euiIconType: 'managementApp',
category: DEFAULT_APP_CATEGORIES.management, category: DEFAULT_APP_CATEGORIES.management,
updater$: this.appUpdater,
async mount(params: AppMountParameters) { async mount(params: AppMountParameters) {
const { renderApp } = await import('./application'); const { renderApp } = await import('./application');
const [coreStart] = await core.getStartServices(); const [coreStart] = await core.getStartServices();
@ -89,6 +96,19 @@ export class ManagementPlugin implements Plugin<ManagementSetup, ManagementStart
public start(core: CoreStart) { public start(core: CoreStart) {
this.managementSections.start({ capabilities: core.application.capabilities }); this.managementSections.start({ capabilities: core.application.capabilities });
const hasAnyEnabledApps = getSectionsServiceStartPrivate()
.getSectionsEnabled()
.some((section) => section.getAppsEnabled().length > 0);
if (!hasAnyEnabledApps) {
this.appUpdater.next(() => {
return {
status: AppStatus.inaccessible,
navLinkStatus: AppNavLinkStatus.hidden,
};
});
}
return {}; return {};
} }
} }

View file

@ -65,9 +65,9 @@ export async function createTestUserService(
} }
return new (class TestUser { return new (class TestUser {
async restoreDefaults() { async restoreDefaults(shouldRefreshBrowser: boolean = true) {
if (isEnabled()) { if (isEnabled()) {
await this.setRoles(config.get('security.defaultRoles')); await this.setRoles(config.get('security.defaultRoles'), shouldRefreshBrowser);
} }
} }

View file

@ -42,6 +42,7 @@ import { FilterBarProvider } from './filter_bar';
import { FlyoutProvider } from './flyout'; import { FlyoutProvider } from './flyout';
import { GlobalNavProvider } from './global_nav'; import { GlobalNavProvider } from './global_nav';
import { InspectorProvider } from './inspector'; import { InspectorProvider } from './inspector';
import { ManagementMenuProvider } from './management';
import { QueryBarProvider } from './query_bar'; import { QueryBarProvider } from './query_bar';
import { RemoteProvider } from './remote'; import { RemoteProvider } from './remote';
import { RenderableProvider } from './renderable'; import { RenderableProvider } from './renderable';
@ -91,4 +92,5 @@ export const services = {
savedQueryManagementComponent: SavedQueryManagementComponentProvider, savedQueryManagementComponent: SavedQueryManagementComponentProvider,
elasticChart: ElasticChartProvider, elasticChart: ElasticChartProvider,
supertest: KibanaSupertestProvider, supertest: KibanaSupertestProvider,
managementMenu: ManagementMenuProvider,
}; };

View file

@ -0,0 +1,20 @@
/*
* 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.
*/
export { ManagementMenuProvider } from './management_menu';

View file

@ -0,0 +1,51 @@
/*
* 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 { FtrProviderContext } from 'test/functional/ftr_provider_context';
export function ManagementMenuProvider({ getService }: FtrProviderContext) {
const find = getService('find');
class ManagementMenu {
public async getSections() {
const sectionsElements = await find.allByCssSelector(
'.mgtSideBarNav > .euiSideNav__content > .euiSideNavItem'
);
const sections = [];
for (const el of sectionsElements) {
const sectionId = await (await el.findByClassName('euiSideNavItemButton')).getAttribute(
'data-test-subj'
);
const sectionLinks = await Promise.all(
(await el.findAllByCssSelector('.euiSideNavItem > a.euiSideNavItemButton')).map((item) =>
item.getAttribute('data-test-subj')
)
);
sections.push({ sectionId, sectionLinks });
}
return sections;
}
}
return new ManagementMenu();
}

View file

@ -5,7 +5,7 @@
*/ */
import KbnServer from 'src/legacy/server/kbn_server'; import KbnServer from 'src/legacy/server/kbn_server';
import { Feature, FeatureConfig } from '../../../../plugins/features/server'; import { KibanaFeature } from '../../../../plugins/features/server';
import { XPackInfo, XPackInfoOptions } from './lib/xpack_info'; import { XPackInfo, XPackInfoOptions } from './lib/xpack_info';
export { XPackFeature } from './lib/xpack_info'; export { XPackFeature } from './lib/xpack_info';

View file

@ -85,7 +85,9 @@ describe('ensureAuthorized', () => {
await actionsAuthorization.ensureAuthorized('create', 'myType'); await actionsAuthorization.ensureAuthorized('create', 'myType');
expect(authorization.actions.savedObject.get).toHaveBeenCalledWith('action', 'create'); expect(authorization.actions.savedObject.get).toHaveBeenCalledWith('action', 'create');
expect(checkPrivileges).toHaveBeenCalledWith(mockAuthorizationAction('action', 'create')); expect(checkPrivileges).toHaveBeenCalledWith({
kibana: mockAuthorizationAction('action', 'create'),
});
expect(auditLogger.actionsAuthorizationSuccess).toHaveBeenCalledTimes(1); expect(auditLogger.actionsAuthorizationSuccess).toHaveBeenCalledTimes(1);
expect(auditLogger.actionsAuthorizationFailure).not.toHaveBeenCalled(); expect(auditLogger.actionsAuthorizationFailure).not.toHaveBeenCalled();
@ -131,10 +133,12 @@ describe('ensureAuthorized', () => {
ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE,
'create' 'create'
); );
expect(checkPrivileges).toHaveBeenCalledWith([ expect(checkPrivileges).toHaveBeenCalledWith({
mockAuthorizationAction(ACTION_SAVED_OBJECT_TYPE, 'get'), kibana: [
mockAuthorizationAction(ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, 'create'), mockAuthorizationAction(ACTION_SAVED_OBJECT_TYPE, 'get'),
]); mockAuthorizationAction(ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, 'create'),
],
});
expect(auditLogger.actionsAuthorizationSuccess).toHaveBeenCalledTimes(1); expect(auditLogger.actionsAuthorizationSuccess).toHaveBeenCalledTimes(1);
expect(auditLogger.actionsAuthorizationFailure).not.toHaveBeenCalled(); expect(auditLogger.actionsAuthorizationFailure).not.toHaveBeenCalled();

View file

@ -42,11 +42,11 @@ export class ActionsAuthorization {
const { authorization } = this; const { authorization } = this;
if (authorization?.mode?.useRbacForRequest(this.request)) { if (authorization?.mode?.useRbacForRequest(this.request)) {
const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request); const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request);
const { hasAllRequested, username } = await checkPrivileges( const { hasAllRequested, username } = await checkPrivileges({
operationAlias[operation] kibana: operationAlias[operation]
? operationAlias[operation](authorization) ? operationAlias[operation](authorization)
: authorization.actions.savedObject.get(ACTION_SAVED_OBJECT_TYPE, operation) : authorization.actions.savedObject.get(ACTION_SAVED_OBJECT_TYPE, operation),
); });
if (hasAllRequested) { if (hasAllRequested) {
this.auditLogger.actionsAuthorizationSuccess(username, operation, actionTypeId); this.auditLogger.actionsAuthorizationSuccess(username, operation, actionTypeId);
} else { } else {

View file

@ -159,7 +159,7 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
); );
} }
plugins.features.registerFeature(ACTIONS_FEATURE); plugins.features.registerKibanaFeature(ACTIONS_FEATURE);
setupSavedObjects(core.savedObjects, plugins.encryptedSavedObjects); setupSavedObjects(core.savedObjects, plugins.encryptedSavedObjects);
this.eventLogService = plugins.eventLog; this.eventLogService = plugins.eventLog;

View file

@ -43,7 +43,7 @@ describe('AlertingBuiltins Plugin', () => {
"name": "Index threshold", "name": "Index threshold",
} }
`); `);
expect(featuresSetup.registerFeature).toHaveBeenCalledWith(BUILT_IN_ALERTS_FEATURE); expect(featuresSetup.registerKibanaFeature).toHaveBeenCalledWith(BUILT_IN_ALERTS_FEATURE);
}); });
it('should return a service in the expected shape', async () => { it('should return a service in the expected shape', async () => {

View file

@ -27,7 +27,7 @@ export class AlertingBuiltinsPlugin implements Plugin<IService, IService> {
core: CoreSetup, core: CoreSetup,
{ alerts, features }: AlertingBuiltinsDeps { alerts, features }: AlertingBuiltinsDeps
): Promise<IService> { ): Promise<IService> {
features.registerFeature(BUILT_IN_ALERTS_FEATURE); features.registerKibanaFeature(BUILT_IN_ALERTS_FEATURE);
registerBuiltInAlertTypes({ registerBuiltInAlertTypes({
service: this.service, service: this.service,

View file

@ -306,7 +306,7 @@ In addition, when users are inside your feature you might want to grant them acc
You can control all of these abilities by assigning privileges to the Alerting Framework from within your own feature, for example: You can control all of these abilities by assigning privileges to the Alerting Framework from within your own feature, for example:
```typescript ```typescript
features.registerFeature({ features.registerKibanaFeature({
id: 'my-application-id', id: 'my-application-id',
name: 'My Application', name: 'My Application',
app: [], app: [],
@ -348,7 +348,7 @@ In this example we can see the following:
It's important to note that any role can be granted a mix of `all` and `read` privileges accross multiple type, for example: It's important to note that any role can be granted a mix of `all` and `read` privileges accross multiple type, for example:
```typescript ```typescript
features.registerFeature({ features.registerKibanaFeature({
id: 'my-application-id', id: 'my-application-id',
name: 'My Application', name: 'My Application',
app: [], app: [],

View file

@ -6,7 +6,10 @@
import { KibanaRequest } from 'kibana/server'; import { KibanaRequest } from 'kibana/server';
import { alertTypeRegistryMock } from '../alert_type_registry.mock'; import { alertTypeRegistryMock } from '../alert_type_registry.mock';
import { securityMock } from '../../../../plugins/security/server/mocks'; import { securityMock } from '../../../../plugins/security/server/mocks';
import { PluginStartContract as FeaturesStartContract, Feature } from '../../../features/server'; import {
PluginStartContract as FeaturesStartContract,
KibanaFeature,
} from '../../../features/server';
import { featuresPluginMock } from '../../../features/server/mocks'; import { featuresPluginMock } from '../../../features/server/mocks';
import { import {
AlertsAuthorization, AlertsAuthorization,
@ -41,7 +44,7 @@ function mockSecurity() {
} }
function mockFeature(appName: string, typeName?: string) { function mockFeature(appName: string, typeName?: string) {
return new Feature({ return new KibanaFeature({
id: appName, id: appName,
name: appName, name: appName,
app: [], app: [],
@ -84,7 +87,7 @@ function mockFeature(appName: string, typeName?: string) {
} }
function mockFeatureWithSubFeature(appName: string, typeName: string) { function mockFeatureWithSubFeature(appName: string, typeName: string) {
return new Feature({ return new KibanaFeature({
id: appName, id: appName,
name: appName, name: appName,
app: [], app: [],
@ -174,7 +177,7 @@ beforeEach(() => {
async executor() {}, async executor() {},
producer: 'myApp', producer: 'myApp',
})); }));
features.getFeatures.mockReturnValue([ features.getKibanaFeatures.mockReturnValue([
myAppFeature, myAppFeature,
myOtherAppFeature, myOtherAppFeature,
myAppWithSubFeature, myAppWithSubFeature,
@ -255,7 +258,7 @@ describe('AlertsAuthorization', () => {
checkPrivileges.mockResolvedValueOnce({ checkPrivileges.mockResolvedValueOnce({
username: 'some-user', username: 'some-user',
hasAllRequested: true, hasAllRequested: true,
privileges: [], privileges: { kibana: [] },
}); });
await alertAuthorization.ensureAuthorized('myType', 'myApp', WriteOperations.Create); await alertAuthorization.ensureAuthorized('myType', 'myApp', WriteOperations.Create);
@ -263,9 +266,9 @@ describe('AlertsAuthorization', () => {
expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType'); expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType');
expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'create'); expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'create');
expect(checkPrivileges).toHaveBeenCalledWith([ expect(checkPrivileges).toHaveBeenCalledWith({
mockAuthorizationAction('myType', 'myApp', 'create'), kibana: [mockAuthorizationAction('myType', 'myApp', 'create')],
]); });
expect(auditLogger.alertsAuthorizationSuccess).toHaveBeenCalledTimes(1); expect(auditLogger.alertsAuthorizationSuccess).toHaveBeenCalledTimes(1);
expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled();
@ -298,7 +301,7 @@ describe('AlertsAuthorization', () => {
checkPrivileges.mockResolvedValueOnce({ checkPrivileges.mockResolvedValueOnce({
username: 'some-user', username: 'some-user',
hasAllRequested: true, hasAllRequested: true,
privileges: [], privileges: { kibana: [] },
}); });
await alertAuthorization.ensureAuthorized('myType', 'alerts', WriteOperations.Create); await alertAuthorization.ensureAuthorized('myType', 'alerts', WriteOperations.Create);
@ -306,9 +309,9 @@ describe('AlertsAuthorization', () => {
expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType'); expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType');
expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'create'); expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'create');
expect(checkPrivileges).toHaveBeenCalledWith([ expect(checkPrivileges).toHaveBeenCalledWith({
mockAuthorizationAction('myType', 'myApp', 'create'), kibana: [mockAuthorizationAction('myType', 'myApp', 'create')],
]); });
expect(auditLogger.alertsAuthorizationSuccess).toHaveBeenCalledTimes(1); expect(auditLogger.alertsAuthorizationSuccess).toHaveBeenCalledTimes(1);
expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled();
@ -332,7 +335,7 @@ describe('AlertsAuthorization', () => {
checkPrivileges.mockResolvedValueOnce({ checkPrivileges.mockResolvedValueOnce({
username: 'some-user', username: 'some-user',
hasAllRequested: true, hasAllRequested: true,
privileges: [], privileges: { kibana: [] },
}); });
const alertAuthorization = new AlertsAuthorization({ const alertAuthorization = new AlertsAuthorization({
@ -354,10 +357,12 @@ describe('AlertsAuthorization', () => {
'myOtherApp', 'myOtherApp',
'create' 'create'
); );
expect(checkPrivileges).toHaveBeenCalledWith([ expect(checkPrivileges).toHaveBeenCalledWith({
mockAuthorizationAction('myType', 'myOtherApp', 'create'), kibana: [
mockAuthorizationAction('myType', 'myApp', 'create'), mockAuthorizationAction('myType', 'myOtherApp', 'create'),
]); mockAuthorizationAction('myType', 'myApp', 'create'),
],
});
expect(auditLogger.alertsAuthorizationSuccess).toHaveBeenCalledTimes(1); expect(auditLogger.alertsAuthorizationSuccess).toHaveBeenCalledTimes(1);
expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled();
@ -390,16 +395,18 @@ describe('AlertsAuthorization', () => {
checkPrivileges.mockResolvedValueOnce({ checkPrivileges.mockResolvedValueOnce({
username: 'some-user', username: 'some-user',
hasAllRequested: false, hasAllRequested: false,
privileges: [ privileges: {
{ kibana: [
privilege: mockAuthorizationAction('myType', 'myOtherApp', 'create'), {
authorized: false, privilege: mockAuthorizationAction('myType', 'myOtherApp', 'create'),
}, authorized: false,
{ },
privilege: mockAuthorizationAction('myType', 'myApp', 'create'), {
authorized: true, privilege: mockAuthorizationAction('myType', 'myApp', 'create'),
}, authorized: true,
], },
],
},
}); });
await expect( await expect(
@ -439,16 +446,18 @@ describe('AlertsAuthorization', () => {
checkPrivileges.mockResolvedValueOnce({ checkPrivileges.mockResolvedValueOnce({
username: 'some-user', username: 'some-user',
hasAllRequested: false, hasAllRequested: false,
privileges: [ privileges: {
{ kibana: [
privilege: mockAuthorizationAction('myType', 'myOtherApp', 'create'), {
authorized: true, privilege: mockAuthorizationAction('myType', 'myOtherApp', 'create'),
}, authorized: true,
{ },
privilege: mockAuthorizationAction('myType', 'myApp', 'create'), {
authorized: false, privilege: mockAuthorizationAction('myType', 'myApp', 'create'),
}, authorized: false,
], },
],
},
}); });
await expect( await expect(
@ -488,16 +497,18 @@ describe('AlertsAuthorization', () => {
checkPrivileges.mockResolvedValueOnce({ checkPrivileges.mockResolvedValueOnce({
username: 'some-user', username: 'some-user',
hasAllRequested: false, hasAllRequested: false,
privileges: [ privileges: {
{ kibana: [
privilege: mockAuthorizationAction('myType', 'myOtherApp', 'create'), {
authorized: false, privilege: mockAuthorizationAction('myType', 'myOtherApp', 'create'),
}, authorized: false,
{ },
privilege: mockAuthorizationAction('myType', 'myApp', 'create'), {
authorized: false, privilege: mockAuthorizationAction('myType', 'myApp', 'create'),
}, authorized: false,
], },
],
},
}); });
await expect( await expect(
@ -592,7 +603,7 @@ describe('AlertsAuthorization', () => {
checkPrivileges.mockResolvedValueOnce({ checkPrivileges.mockResolvedValueOnce({
username: 'some-user', username: 'some-user',
hasAllRequested: true, hasAllRequested: true,
privileges: [], privileges: { kibana: [] },
}); });
const alertAuthorization = new AlertsAuthorization({ const alertAuthorization = new AlertsAuthorization({
@ -621,24 +632,26 @@ describe('AlertsAuthorization', () => {
checkPrivileges.mockResolvedValueOnce({ checkPrivileges.mockResolvedValueOnce({
username: 'some-user', username: 'some-user',
hasAllRequested: false, hasAllRequested: false,
privileges: [ privileges: {
{ kibana: [
privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'find'), {
authorized: true, privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'find'),
}, authorized: true,
{ },
privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'find'), {
authorized: false, privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'find'),
}, authorized: false,
{ },
privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'), {
authorized: true, privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'),
}, authorized: true,
{ },
privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'find'), {
authorized: false, privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'find'),
}, authorized: false,
], },
],
},
}); });
const alertAuthorization = new AlertsAuthorization({ const alertAuthorization = new AlertsAuthorization({
@ -680,24 +693,26 @@ describe('AlertsAuthorization', () => {
checkPrivileges.mockResolvedValueOnce({ checkPrivileges.mockResolvedValueOnce({
username: 'some-user', username: 'some-user',
hasAllRequested: false, hasAllRequested: false,
privileges: [ privileges: {
{ kibana: [
privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'find'), {
authorized: true, privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'find'),
}, authorized: true,
{ },
privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'find'), {
authorized: false, privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'find'),
}, authorized: false,
{ },
privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'), {
authorized: true, privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'),
}, authorized: true,
{ },
privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'find'), {
authorized: true, privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'find'),
}, authorized: true,
], },
],
},
}); });
const alertAuthorization = new AlertsAuthorization({ const alertAuthorization = new AlertsAuthorization({
@ -728,32 +743,34 @@ describe('AlertsAuthorization', () => {
checkPrivileges.mockResolvedValueOnce({ checkPrivileges.mockResolvedValueOnce({
username: 'some-user', username: 'some-user',
hasAllRequested: false, hasAllRequested: false,
privileges: [ privileges: {
{ kibana: [
privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'find'), {
authorized: true, privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'find'),
}, authorized: true,
{ },
privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'find'), {
authorized: false, privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'find'),
}, authorized: false,
{ },
privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'), {
authorized: true, privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'),
}, authorized: true,
{ },
privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'find'), {
authorized: true, privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'find'),
}, authorized: true,
{ },
privilege: mockAuthorizationAction('mySecondAppAlertType', 'myApp', 'find'), {
authorized: true, privilege: mockAuthorizationAction('mySecondAppAlertType', 'myApp', 'find'),
}, authorized: true,
{ },
privilege: mockAuthorizationAction('mySecondAppAlertType', 'myOtherApp', 'find'), {
authorized: true, privilege: mockAuthorizationAction('mySecondAppAlertType', 'myOtherApp', 'find'),
}, authorized: true,
], },
],
},
}); });
const alertAuthorization = new AlertsAuthorization({ const alertAuthorization = new AlertsAuthorization({
@ -903,24 +920,26 @@ describe('AlertsAuthorization', () => {
checkPrivileges.mockResolvedValueOnce({ checkPrivileges.mockResolvedValueOnce({
username: 'some-user', username: 'some-user',
hasAllRequested: false, hasAllRequested: false,
privileges: [ privileges: {
{ kibana: [
privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'create'), {
authorized: true, privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'create'),
}, authorized: true,
{ },
privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'create'), {
authorized: false, privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'create'),
}, authorized: false,
{ },
privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), {
authorized: true, privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'),
}, authorized: true,
{ },
privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), {
authorized: true, privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'),
}, authorized: true,
], },
],
},
}); });
const alertAuthorization = new AlertsAuthorization({ const alertAuthorization = new AlertsAuthorization({
@ -989,16 +1008,18 @@ describe('AlertsAuthorization', () => {
checkPrivileges.mockResolvedValueOnce({ checkPrivileges.mockResolvedValueOnce({
username: 'some-user', username: 'some-user',
hasAllRequested: false, hasAllRequested: false,
privileges: [ privileges: {
{ kibana: [
privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), {
authorized: true, privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'),
}, authorized: true,
{ },
privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), {
authorized: false, privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'),
}, authorized: false,
], },
],
},
}); });
const alertAuthorization = new AlertsAuthorization({ const alertAuthorization = new AlertsAuthorization({
@ -1048,40 +1069,42 @@ describe('AlertsAuthorization', () => {
checkPrivileges.mockResolvedValueOnce({ checkPrivileges.mockResolvedValueOnce({
username: 'some-user', username: 'some-user',
hasAllRequested: false, hasAllRequested: false,
privileges: [ privileges: {
{ kibana: [
privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'create'), {
authorized: true, privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'create'),
}, authorized: true,
{ },
privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'create'), {
authorized: false, privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'create'),
}, authorized: false,
{ },
privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), {
authorized: false, privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'),
}, authorized: false,
{ },
privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), {
authorized: false, privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'),
}, authorized: false,
{ },
privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'get'), {
authorized: true, privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'get'),
}, authorized: true,
{ },
privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'get'), {
authorized: true, privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'get'),
}, authorized: true,
{ },
privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'get'), {
authorized: true, privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'get'),
}, authorized: true,
{ },
privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'get'), {
authorized: true, privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'get'),
}, authorized: true,
], },
],
},
}); });
const alertAuthorization = new AlertsAuthorization({ const alertAuthorization = new AlertsAuthorization({
@ -1158,24 +1181,26 @@ describe('AlertsAuthorization', () => {
checkPrivileges.mockResolvedValueOnce({ checkPrivileges.mockResolvedValueOnce({
username: 'some-user', username: 'some-user',
hasAllRequested: false, hasAllRequested: false,
privileges: [ privileges: {
{ kibana: [
privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'create'), {
authorized: true, privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'create'),
}, authorized: true,
{ },
privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'create'), {
authorized: true, privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'create'),
}, authorized: true,
{ },
privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), {
authorized: false, privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'),
}, authorized: false,
{ },
privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), {
authorized: false, privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'),
}, authorized: false,
], },
],
},
}); });
const alertAuthorization = new AlertsAuthorization({ const alertAuthorization = new AlertsAuthorization({

View file

@ -82,7 +82,7 @@ export class AlertsAuthorization {
(disabledFeatures) => (disabledFeatures) =>
new Set( new Set(
features features
.getFeatures() .getKibanaFeatures()
.filter( .filter(
({ id, alerting }) => ({ id, alerting }) =>
// ignore features which are disabled in the user's space // ignore features which are disabled in the user's space
@ -133,20 +133,21 @@ export class AlertsAuthorization {
const shouldAuthorizeConsumer = consumer !== ALERTS_FEATURE_ID; const shouldAuthorizeConsumer = consumer !== ALERTS_FEATURE_ID;
const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request); const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request);
const { hasAllRequested, username, privileges } = await checkPrivileges( const { hasAllRequested, username, privileges } = await checkPrivileges({
shouldAuthorizeConsumer && consumer !== alertType.producer kibana:
? [ shouldAuthorizeConsumer && consumer !== alertType.producer
// check for access at consumer level ? [
requiredPrivilegesByScope.consumer, // check for access at consumer level
// check for access at producer level requiredPrivilegesByScope.consumer,
requiredPrivilegesByScope.producer, // check for access at producer level
] requiredPrivilegesByScope.producer,
: [ ]
// skip consumer privilege checks under `alerts` as all alert types can : [
// be created under `alerts` if you have producer level privileges // skip consumer privilege checks under `alerts` as all alert types can
requiredPrivilegesByScope.producer, // be created under `alerts` if you have producer level privileges
] requiredPrivilegesByScope.producer,
); ],
});
if (!isAvailableConsumer) { if (!isAvailableConsumer) {
/** /**
@ -177,7 +178,7 @@ export class AlertsAuthorization {
); );
} else { } else {
const authorizedPrivileges = map( const authorizedPrivileges = map(
privileges.filter((privilege) => privilege.authorized), privileges.kibana.filter((privilege) => privilege.authorized),
'privilege' 'privilege'
); );
const unauthorizedScopes = mapValues( const unauthorizedScopes = mapValues(
@ -341,9 +342,9 @@ export class AlertsAuthorization {
} }
} }
const { username, hasAllRequested, privileges } = await checkPrivileges([ const { username, hasAllRequested, privileges } = await checkPrivileges({
...privilegeToAlertType.keys(), kibana: [...privilegeToAlertType.keys()],
]); });
return { return {
username, username,
@ -352,7 +353,7 @@ export class AlertsAuthorization {
? // has access to all features ? // has access to all features
this.augmentWithAuthorizedConsumers(alertTypes, await this.allPossibleConsumers) this.augmentWithAuthorizedConsumers(alertTypes, await this.allPossibleConsumers)
: // only has some of the required privileges : // only has some of the required privileges
privileges.reduce((authorizedAlertTypes, { authorized, privilege }) => { privileges.kibana.reduce((authorizedAlertTypes, { authorized, privilege }) => {
if (authorized && privilegeToAlertType.has(privilege)) { if (authorized && privilegeToAlertType.has(privilege)) {
const [ const [
alertType, alertType,

View file

@ -12,7 +12,7 @@ import { taskManagerMock } from '../../task_manager/server/mocks';
import { eventLogServiceMock } from '../../event_log/server/event_log_service.mock'; import { eventLogServiceMock } from '../../event_log/server/event_log_service.mock';
import { KibanaRequest, CoreSetup } from 'kibana/server'; import { KibanaRequest, CoreSetup } from 'kibana/server';
import { featuresPluginMock } from '../../features/server/mocks'; import { featuresPluginMock } from '../../features/server/mocks';
import { Feature } from '../../features/server'; import { KibanaFeature } from '../../features/server';
describe('Alerting Plugin', () => { describe('Alerting Plugin', () => {
describe('setup()', () => { describe('setup()', () => {
@ -159,8 +159,8 @@ describe('Alerting Plugin', () => {
function mockFeatures() { function mockFeatures() {
const features = featuresPluginMock.createSetup(); const features = featuresPluginMock.createSetup();
features.getFeatures.mockReturnValue([ features.getKibanaFeatures.mockReturnValue([
new Feature({ new KibanaFeature({
id: 'appName', id: 'appName',
name: 'appName', name: 'appName',
app: [], app: [],

View file

@ -127,7 +127,7 @@ export class APMPlugin implements Plugin<APMPluginSetup> {
}; };
}); });
plugins.features.registerFeature(APM_FEATURE); plugins.features.registerKibanaFeature(APM_FEATURE);
plugins.licensing.featureUsage.register( plugins.licensing.featureUsage.register(
APM_SERVICE_MAPS_FEATURE_NAME, APM_SERVICE_MAPS_FEATURE_NAME,
APM_SERVICE_MAPS_LICENSE_TYPE APM_SERVICE_MAPS_LICENSE_TYPE

View file

@ -7,7 +7,8 @@
"requiredPlugins": [ "requiredPlugins": [
"data", "data",
"licensing", "licensing",
"management" "management",
"features"
], ],
"optionalPlugins": [ "optionalPlugins": [
"security" "security"

View file

@ -11,6 +11,7 @@ import {
Plugin, Plugin,
PluginInitializerContext, PluginInitializerContext,
} from '../../../../src/core/server'; } from '../../../../src/core/server';
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
import { SecurityPluginSetup } from '../../security/server'; import { SecurityPluginSetup } from '../../security/server';
import { LicensingPluginStart } from '../../licensing/server'; import { LicensingPluginStart } from '../../licensing/server';
import { BeatsManagementConfigType } from '../common'; import { BeatsManagementConfigType } from '../common';
@ -22,6 +23,7 @@ import { beatsIndexTemplate } from './index_templates';
interface SetupDeps { interface SetupDeps {
security?: SecurityPluginSetup; security?: SecurityPluginSetup;
features: FeaturesPluginSetup;
} }
interface StartDeps { interface StartDeps {
@ -42,7 +44,7 @@ export class BeatsManagementPlugin implements Plugin<{}, {}, SetupDeps, StartDep
private readonly initializerContext: PluginInitializerContext<BeatsManagementConfigType> private readonly initializerContext: PluginInitializerContext<BeatsManagementConfigType>
) {} ) {}
public async setup(core: CoreSetup<StartDeps>, { security }: SetupDeps) { public async setup(core: CoreSetup<StartDeps>, { features, security }: SetupDeps) {
this.securitySetup = security; this.securitySetup = security;
const router = core.http.createRouter(); const router = core.http.createRouter();
@ -52,6 +54,20 @@ export class BeatsManagementPlugin implements Plugin<{}, {}, SetupDeps, StartDep
return this.beatsLibs!; return this.beatsLibs!;
}); });
features.registerElasticsearchFeature({
id: 'beats_management',
management: {
ingest: ['beats_management'],
},
privileges: [
{
ui: [],
requiredClusterPrivileges: [],
requiredRoles: ['beats_admin'],
},
],
});
return {}; return {};
} }

View file

@ -37,7 +37,7 @@ export class CanvasPlugin implements Plugin {
coreSetup.savedObjects.registerType(workpadType); coreSetup.savedObjects.registerType(workpadType);
coreSetup.savedObjects.registerType(workpadTemplateType); coreSetup.savedObjects.registerType(workpadTemplateType);
plugins.features.registerFeature({ plugins.features.registerKibanaFeature({
id: 'canvas', id: 'canvas',
name: 'Canvas', name: 'Canvas',
order: 400, order: 400,

View file

@ -8,7 +8,8 @@
"licensing", "licensing",
"management", "management",
"remoteClusters", "remoteClusters",
"indexManagement" "indexManagement",
"features"
], ],
"optionalPlugins": [ "optionalPlugins": [
"usageCollection" "usageCollection"

View file

@ -87,7 +87,7 @@ export class CrossClusterReplicationServerPlugin implements Plugin<void, void, a
setup( setup(
{ http, getStartServices }: CoreSetup, { http, getStartServices }: CoreSetup,
{ licensing, indexManagement, remoteClusters }: Dependencies { features, licensing, indexManagement, remoteClusters }: Dependencies
) { ) {
this.config$ this.config$
.pipe(first()) .pipe(first())
@ -124,6 +124,19 @@ export class CrossClusterReplicationServerPlugin implements Plugin<void, void, a
} }
); );
features.registerElasticsearchFeature({
id: 'cross_cluster_replication',
management: {
data: ['cross_cluster_replication'],
},
privileges: [
{
requiredClusterPrivileges: ['manage', 'manage_ccr'],
ui: [],
},
],
});
http.registerRouteHandlerContext('crossClusterReplication', async (ctx, request) => { http.registerRouteHandlerContext('crossClusterReplication', async (ctx, request) => {
this.ccrEsClient = this.ccrEsClient ?? (await getCustomEsClient(getStartServices)); this.ccrEsClient = this.ccrEsClient ?? (await getCustomEsClient(getStartServices));
return { return {

View file

@ -5,6 +5,7 @@
*/ */
import { IRouter } from 'src/core/server'; import { IRouter } from 'src/core/server';
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
import { LicensingPluginSetup } from '../../licensing/server'; import { LicensingPluginSetup } from '../../licensing/server';
import { IndexManagementPluginSetup } from '../../index_management/server'; import { IndexManagementPluginSetup } from '../../index_management/server';
import { RemoteClustersPluginSetup } from '../../remote_clusters/server'; import { RemoteClustersPluginSetup } from '../../remote_clusters/server';
@ -16,6 +17,7 @@ export interface Dependencies {
licensing: LicensingPluginSetup; licensing: LicensingPluginSetup;
indexManagement: IndexManagementPluginSetup; indexManagement: IndexManagementPluginSetup;
remoteClusters: RemoteClustersPluginSetup; remoteClusters: RemoteClustersPluginSetup;
features: FeaturesPluginSetup;
} }
export interface RouteDependencies { export interface RouteDependencies {

View file

@ -51,7 +51,7 @@ export const checkAccess = async ({
try { try {
const { hasAllRequested } = await security.authz const { hasAllRequested } = await security.authz
.checkPrivilegesWithRequest(request) .checkPrivilegesWithRequest(request)
.globally(security.authz.actions.ui.get('enterpriseSearch', 'all')); .globally({ kibana: security.authz.actions.ui.get('enterpriseSearch', 'all') });
return hasAllRequested; return hasAllRequested;
} catch (err) { } catch (err) {
if (err.statusCode === 401 || err.statusCode === 403) { if (err.statusCode === 401 || err.statusCode === 403) {

View file

@ -78,7 +78,7 @@ export class EnterpriseSearchPlugin implements Plugin {
/** /**
* Register space/feature control * Register space/feature control
*/ */
features.registerFeature({ features.registerKibanaFeature({
id: ENTERPRISE_SEARCH_PLUGIN.ID, id: ENTERPRISE_SEARCH_PLUGIN.ID,
name: ENTERPRISE_SEARCH_PLUGIN.NAME, name: ENTERPRISE_SEARCH_PLUGIN.NAME,
order: 0, order: 0,

View file

@ -0,0 +1,85 @@
/*
* 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 { RecursiveReadonly } from '@kbn/utility-types';
import { FeatureElasticsearchPrivileges } from './feature_elasticsearch_privileges';
/**
* Interface for registering an Elasticsearch feature.
* Feature registration allows plugins to hide their applications based
* on configured cluster or index privileges.
*/
export interface ElasticsearchFeatureConfig {
/**
* Unique identifier for this feature.
* This identifier is also used when generating UI Capabilities.
*
* @see UICapabilities
*/
id: string;
/**
* Management sections associated with this feature.
*
* @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.
*
*/
catalogue?: string[];
/**
* Feature privilege definition. Specify one or more privileges which grant access to this feature.
* Users must satisfy all privileges in at least one of the defined sets of privileges in order to be granted access.
*
* @example
* ```ts
* [{
* requiredClusterPrivileges: ['monitor'],
* requiredIndexPrivileges: {
* ['metricbeat-*']: ['read', 'view_index_metadata']
* }
* }]
* ```
* @see FeatureElasticsearchPrivileges
*/
privileges: FeatureElasticsearchPrivileges[];
}
export class ElasticsearchFeature {
constructor(protected readonly config: RecursiveReadonly<ElasticsearchFeatureConfig>) {}
public get id() {
return this.config.id;
}
public get catalogue() {
return this.config.catalogue;
}
public get management() {
return this.config.management;
}
public get privileges() {
return this.config.privileges;
}
public toRaw() {
return { ...this.config } as ElasticsearchFeatureConfig;
}
}

View file

@ -0,0 +1,72 @@
/*
* 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.
*/
/**
* Elasticsearch Feature privilege definition
*/
export interface FeatureElasticsearchPrivileges {
/**
* A set of Elasticsearch cluster privileges which are required for this feature to be enabled.
* See https://www.elastic.co/guide/en/elasticsearch/reference/current/security-privileges.html
*
*/
requiredClusterPrivileges: string[];
/**
* A set of Elasticsearch index privileges which are required for this feature to be enabled, keyed on index name or pattern.
* See https://www.elastic.co/guide/en/elasticsearch/reference/current/security-privileges.html#privileges-list-indices
*
* @example
*
* Requiring `read` access to `logstash-*` and `all` access to `foo-*`
* ```ts
* feature.registerElasticsearchPrivilege({
* privileges: [{
* requiredIndexPrivileges: {
* ['logstash-*']: ['read'],
* ['foo-*]: ['all']
* }
* }]
* })
* ```
*
*/
requiredIndexPrivileges?: {
[indexName: string]: string[];
};
/**
* A set of Elasticsearch roles which are required for this feature to be enabled.
*
* @deprecated do not rely on hard-coded role names.
*
* This is relied on by the reporting feature, and should be removed once reporting
* migrates to using the Kibana Privilege model: https://github.com/elastic/kibana/issues/19914
*/
requiredRoles?: 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[];
}

View file

@ -4,8 +4,10 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
export { FeatureElasticsearchPrivileges } from './feature_elasticsearch_privileges';
export { FeatureKibanaPrivileges } from './feature_kibana_privileges'; export { FeatureKibanaPrivileges } from './feature_kibana_privileges';
export { Feature, FeatureConfig } from './feature'; export { ElasticsearchFeature, ElasticsearchFeatureConfig } from './elasticsearch_feature';
export { KibanaFeature, KibanaFeatureConfig } from './kibana_feature';
export { export {
SubFeature, SubFeature,
SubFeatureConfig, SubFeatureConfig,

View file

@ -6,7 +6,7 @@
import { RecursiveReadonly } from '@kbn/utility-types'; import { RecursiveReadonly } from '@kbn/utility-types';
import { FeatureKibanaPrivileges } from './feature_kibana_privileges'; import { FeatureKibanaPrivileges } from './feature_kibana_privileges';
import { SubFeatureConfig, SubFeature } from './sub_feature'; import { SubFeatureConfig, SubFeature as KibanaSubFeature } from './sub_feature';
import { ReservedKibanaPrivilege } from './reserved_kibana_privilege'; import { ReservedKibanaPrivilege } from './reserved_kibana_privilege';
/** /**
@ -14,7 +14,7 @@ import { ReservedKibanaPrivilege } from './reserved_kibana_privilege';
* Feature registration allows plugins to hide their applications with spaces, * Feature registration allows plugins to hide their applications with spaces,
* and secure access when configured for security. * and secure access when configured for security.
*/ */
export interface FeatureConfig { export interface KibanaFeatureConfig {
/** /**
* Unique identifier for this feature. * Unique identifier for this feature.
* This identifier is also used when generating UI Capabilities. * This identifier is also used when generating UI Capabilities.
@ -137,12 +137,12 @@ export interface FeatureConfig {
}; };
} }
export class Feature { export class KibanaFeature {
public readonly subFeatures: SubFeature[]; public readonly subFeatures: KibanaSubFeature[];
constructor(protected readonly config: RecursiveReadonly<FeatureConfig>) { constructor(protected readonly config: RecursiveReadonly<KibanaFeatureConfig>) {
this.subFeatures = (config.subFeatures ?? []).map( this.subFeatures = (config.subFeatures ?? []).map(
(subFeatureConfig) => new SubFeature(subFeatureConfig) (subFeatureConfig) => new KibanaSubFeature(subFeatureConfig)
); );
} }
@ -199,6 +199,6 @@ export class Feature {
} }
public toRaw() { public toRaw() {
return { ...this.config } as FeatureConfig; return { ...this.config } as KibanaFeatureConfig;
} }
} }

View file

@ -5,13 +5,13 @@
*/ */
import { HttpSetup } from 'src/core/public'; import { HttpSetup } from 'src/core/public';
import { FeatureConfig, Feature } from '.'; import { KibanaFeatureConfig, KibanaFeature } from '.';
export class FeaturesAPIClient { export class FeaturesAPIClient {
constructor(private readonly http: HttpSetup) {} constructor(private readonly http: HttpSetup) {}
public async getFeatures() { public async getFeatures() {
const features = await this.http.get<FeatureConfig[]>('/api/features'); const features = await this.http.get<KibanaFeatureConfig[]>('/api/features');
return features.map((config) => new Feature(config)); return features.map((config) => new KibanaFeature(config));
} }
} }

View file

@ -8,8 +8,8 @@ import { PluginInitializer } from 'src/core/public';
import { FeaturesPlugin, FeaturesPluginSetup, FeaturesPluginStart } from './plugin'; import { FeaturesPlugin, FeaturesPluginSetup, FeaturesPluginStart } from './plugin';
export { export {
Feature, KibanaFeature,
FeatureConfig, KibanaFeatureConfig,
FeatureKibanaPrivileges, FeatureKibanaPrivileges,
SubFeatureConfig, SubFeatureConfig,
SubFeaturePrivilegeConfig, SubFeaturePrivilegeConfig,

View file

@ -1,27 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FeatureRegistry prevents features from being registered with a catalogue entry of "" 1`] = `"child \\"catalogue\\" fails because [\\"catalogue\\" at position 0 fails because [\\"0\\" is not allowed to be empty]]"`; exports[`FeatureRegistry Kibana Features prevents features from being registered with a catalogue entry of "" 1`] = `"child \\"catalogue\\" fails because [\\"catalogue\\" at position 0 fails because [\\"0\\" is not allowed to be empty]]"`;
exports[`FeatureRegistry prevents features from being registered with a catalogue entry of "contains space" 1`] = `"child \\"catalogue\\" fails because [\\"catalogue\\" at position 0 fails because [\\"0\\" with value \\"contains space\\" fails to match the required pattern: /^[a-zA-Z0-9:_-]+$/]]"`; exports[`FeatureRegistry Kibana Features prevents features from being registered with a catalogue entry of "contains space" 1`] = `"child \\"catalogue\\" fails because [\\"catalogue\\" at position 0 fails because [\\"0\\" with value \\"contains space\\" fails to match the required pattern: /^[a-zA-Z0-9:_-]+$/]]"`;
exports[`FeatureRegistry prevents features from being registered with a catalogue entry of "contains_invalid()_chars" 1`] = `"child \\"catalogue\\" fails because [\\"catalogue\\" at position 0 fails because [\\"0\\" with value \\"contains_invalid()_chars\\" fails to match the required pattern: /^[a-zA-Z0-9:_-]+$/]]"`; exports[`FeatureRegistry Kibana Features prevents features from being registered with a catalogue entry of "contains_invalid()_chars" 1`] = `"child \\"catalogue\\" fails because [\\"catalogue\\" at position 0 fails because [\\"0\\" with value \\"contains_invalid()_chars\\" fails to match the required pattern: /^[a-zA-Z0-9:_-]+$/]]"`;
exports[`FeatureRegistry prevents features from being registered with a management id of "" 1`] = `"child \\"management\\" fails because [child \\"kibana\\" fails because [\\"kibana\\" at position 0 fails because [\\"0\\" is not allowed to be empty]]]"`; exports[`FeatureRegistry Kibana Features prevents features from being registered with a management id of "" 1`] = `"child \\"management\\" fails because [child \\"kibana\\" fails because [\\"kibana\\" at position 0 fails because [\\"0\\" is not allowed to be empty]]]"`;
exports[`FeatureRegistry prevents features from being registered with a management id of "contains space" 1`] = `"child \\"management\\" fails because [child \\"kibana\\" fails because [\\"kibana\\" at position 0 fails because [\\"0\\" with value \\"contains space\\" fails to match the required pattern: /^[a-zA-Z0-9:_-]+$/]]]"`; exports[`FeatureRegistry Kibana Features prevents features from being registered with a management id of "contains space" 1`] = `"child \\"management\\" fails because [child \\"kibana\\" fails because [\\"kibana\\" at position 0 fails because [\\"0\\" with value \\"contains space\\" fails to match the required pattern: /^[a-zA-Z0-9:_-]+$/]]]"`;
exports[`FeatureRegistry prevents features from being registered with a management id of "contains_invalid()_chars" 1`] = `"child \\"management\\" fails because [child \\"kibana\\" fails because [\\"kibana\\" at position 0 fails because [\\"0\\" with value \\"contains_invalid()_chars\\" fails to match the required pattern: /^[a-zA-Z0-9:_-]+$/]]]"`; exports[`FeatureRegistry Kibana Features prevents features from being registered with a management id of "contains_invalid()_chars" 1`] = `"child \\"management\\" fails because [child \\"kibana\\" fails because [\\"kibana\\" at position 0 fails because [\\"0\\" with value \\"contains_invalid()_chars\\" fails to match the required pattern: /^[a-zA-Z0-9:_-]+$/]]]"`;
exports[`FeatureRegistry prevents features from being registered with a navLinkId of "" 1`] = `"child \\"navLinkId\\" fails because [\\"navLinkId\\" is not allowed to be empty]"`; exports[`FeatureRegistry Kibana Features prevents features from being registered with a navLinkId of "" 1`] = `"child \\"navLinkId\\" fails because [\\"navLinkId\\" is not allowed to be empty]"`;
exports[`FeatureRegistry prevents features from being registered with a navLinkId of "contains space" 1`] = `"child \\"navLinkId\\" fails because [\\"navLinkId\\" with value \\"contains space\\" fails to match the required pattern: /^[a-zA-Z0-9:_-]+$/]"`; exports[`FeatureRegistry Kibana Features prevents features from being registered with a navLinkId of "contains space" 1`] = `"child \\"navLinkId\\" fails because [\\"navLinkId\\" with value \\"contains space\\" fails to match the required pattern: /^[a-zA-Z0-9:_-]+$/]"`;
exports[`FeatureRegistry prevents features from being registered with a navLinkId of "contains_invalid()_chars" 1`] = `"child \\"navLinkId\\" fails because [\\"navLinkId\\" with value \\"contains_invalid()_chars\\" fails to match the required pattern: /^[a-zA-Z0-9:_-]+$/]"`; exports[`FeatureRegistry Kibana Features prevents features from being registered with a navLinkId of "contains_invalid()_chars" 1`] = `"child \\"navLinkId\\" fails because [\\"navLinkId\\" with value \\"contains_invalid()_chars\\" fails to match the required pattern: /^[a-zA-Z0-9:_-]+$/]"`;
exports[`FeatureRegistry prevents features from being registered with an ID of "catalogue" 1`] = `"child \\"id\\" fails because [\\"id\\" contains an invalid value]"`; exports[`FeatureRegistry Kibana Features prevents features from being registered with an ID of "catalogue" 1`] = `"child \\"id\\" fails because [\\"id\\" contains an invalid value]"`;
exports[`FeatureRegistry prevents features from being registered with an ID of "doesn't match valid regex" 1`] = `"child \\"id\\" fails because [\\"id\\" with value \\"doesn't match valid regex\\" fails to match the required pattern: /^[a-zA-Z0-9_-]+$/]"`; exports[`FeatureRegistry Kibana Features prevents features from being registered with an ID of "doesn't match valid regex" 1`] = `"child \\"id\\" fails because [\\"id\\" with value \\"doesn't match valid regex\\" fails to match the required pattern: /^[a-zA-Z0-9_-]+$/]"`;
exports[`FeatureRegistry prevents features from being registered with an ID of "management" 1`] = `"child \\"id\\" fails because [\\"id\\" contains an invalid value]"`; exports[`FeatureRegistry Kibana Features prevents features from being registered with an ID of "management" 1`] = `"child \\"id\\" fails because [\\"id\\" contains an invalid value]"`;
exports[`FeatureRegistry prevents features from being registered with an ID of "navLinks" 1`] = `"child \\"id\\" fails because [\\"id\\" contains an invalid value]"`; exports[`FeatureRegistry Kibana Features prevents features from being registered with an ID of "navLinks" 1`] = `"child \\"id\\" fails because [\\"id\\" contains an invalid value]"`;

File diff suppressed because it is too large Load diff

View file

@ -5,38 +5,72 @@
*/ */
import { cloneDeep, uniq } from 'lodash'; import { cloneDeep, uniq } from 'lodash';
import { FeatureConfig, Feature, FeatureKibanaPrivileges } from '../common'; import {
import { validateFeature } from './feature_schema'; KibanaFeatureConfig,
KibanaFeature,
FeatureKibanaPrivileges,
ElasticsearchFeatureConfig,
ElasticsearchFeature,
} from '../common';
import { validateKibanaFeature, validateElasticsearchFeature } from './feature_schema';
export class FeatureRegistry { export class FeatureRegistry {
private locked = false; private locked = false;
private features: Record<string, FeatureConfig> = {}; private kibanaFeatures: Record<string, KibanaFeatureConfig> = {};
private esFeatures: Record<string, ElasticsearchFeatureConfig> = {};
public register(feature: FeatureConfig) { public registerKibanaFeature(feature: KibanaFeatureConfig) {
if (this.locked) { if (this.locked) {
throw new Error( throw new Error(
`Features are locked, can't register new features. Attempt to register ${feature.id} failed.` `Features are locked, can't register new features. Attempt to register ${feature.id} failed.`
); );
} }
validateFeature(feature); validateKibanaFeature(feature);
if (feature.id in this.features) { if (feature.id in this.kibanaFeatures || feature.id in this.esFeatures) {
throw new Error(`Feature with id ${feature.id} is already registered.`); throw new Error(`Feature with id ${feature.id} is already registered.`);
} }
const featureCopy = cloneDeep(feature); const featureCopy = cloneDeep(feature);
this.features[feature.id] = applyAutomaticPrivilegeGrants(featureCopy); this.kibanaFeatures[feature.id] = applyAutomaticPrivilegeGrants(featureCopy);
} }
public getAll(): Feature[] { public registerElasticsearchFeature(feature: ElasticsearchFeatureConfig) {
if (this.locked) {
throw new Error(
`Features are locked, can't register new features. Attempt to register ${feature.id} failed.`
);
}
if (feature.id in this.kibanaFeatures || feature.id in this.esFeatures) {
throw new Error(`Feature with id ${feature.id} is already registered.`);
}
validateElasticsearchFeature(feature);
const featureCopy = cloneDeep(feature);
this.esFeatures[feature.id] = featureCopy;
}
public getAllKibanaFeatures(): KibanaFeature[] {
this.locked = true; this.locked = true;
return Object.values(this.features).map((featureConfig) => new Feature(featureConfig)); return Object.values(this.kibanaFeatures).map(
(featureConfig) => new KibanaFeature(featureConfig)
);
}
public getAllElasticsearchFeatures(): ElasticsearchFeature[] {
this.locked = true;
return Object.values(this.esFeatures).map(
(featureConfig) => new ElasticsearchFeature(featureConfig)
);
} }
} }
function applyAutomaticPrivilegeGrants(feature: FeatureConfig): FeatureConfig { function applyAutomaticPrivilegeGrants(feature: KibanaFeatureConfig): KibanaFeatureConfig {
const allPrivilege = feature.privileges?.all; const allPrivilege = feature.privileges?.all;
const readPrivilege = feature.privileges?.read; const readPrivilege = feature.privileges?.read;
const reservedPrivileges = (feature.reserved?.privileges ?? []).map((rp) => rp.privilege); const reservedPrivileges = (feature.reserved?.privileges ?? []).map((rp) => rp.privilege);

View file

@ -8,8 +8,8 @@ import Joi from 'joi';
import { difference } from 'lodash'; import { difference } from 'lodash';
import { Capabilities as UICapabilities } from '../../../../src/core/server'; import { Capabilities as UICapabilities } from '../../../../src/core/server';
import { FeatureConfig } from '../common/feature'; import { KibanaFeatureConfig } from '../common';
import { FeatureKibanaPrivileges } from '.'; import { FeatureKibanaPrivileges, ElasticsearchFeatureConfig } from '.';
// Each feature gets its own property on the UICapabilities object, // Each feature gets its own property on the UICapabilities object,
// but that object has a few built-in properties which should not be overwritten. // but that object has a few built-in properties which should not be overwritten.
@ -28,7 +28,7 @@ const managementSchema = Joi.object().pattern(
const catalogueSchema = Joi.array().items(Joi.string().regex(uiCapabilitiesRegex)); const catalogueSchema = Joi.array().items(Joi.string().regex(uiCapabilitiesRegex));
const alertingSchema = Joi.array().items(Joi.string()); const alertingSchema = Joi.array().items(Joi.string());
const privilegeSchema = Joi.object({ const kibanaPrivilegeSchema = Joi.object({
excludeFromBasePrivileges: Joi.boolean(), excludeFromBasePrivileges: Joi.boolean(),
management: managementSchema, management: managementSchema,
catalogue: catalogueSchema, catalogue: catalogueSchema,
@ -45,7 +45,7 @@ const privilegeSchema = Joi.object({
ui: Joi.array().items(Joi.string().regex(uiCapabilitiesRegex)).required(), ui: Joi.array().items(Joi.string().regex(uiCapabilitiesRegex)).required(),
}); });
const subFeaturePrivilegeSchema = Joi.object({ const kibanaSubFeaturePrivilegeSchema = Joi.object({
id: Joi.string().regex(subFeaturePrivilegePartRegex).required(), id: Joi.string().regex(subFeaturePrivilegePartRegex).required(),
name: Joi.string().required(), name: Joi.string().required(),
includeIn: Joi.string().allow('all', 'read', 'none').required(), includeIn: Joi.string().allow('all', 'read', 'none').required(),
@ -64,17 +64,17 @@ const subFeaturePrivilegeSchema = Joi.object({
ui: Joi.array().items(Joi.string().regex(uiCapabilitiesRegex)).required(), ui: Joi.array().items(Joi.string().regex(uiCapabilitiesRegex)).required(),
}); });
const subFeatureSchema = Joi.object({ const kibanaSubFeatureSchema = Joi.object({
name: Joi.string().required(), name: Joi.string().required(),
privilegeGroups: Joi.array().items( privilegeGroups: Joi.array().items(
Joi.object({ Joi.object({
groupType: Joi.string().valid('mutually_exclusive', 'independent').required(), groupType: Joi.string().valid('mutually_exclusive', 'independent').required(),
privileges: Joi.array().items(subFeaturePrivilegeSchema).min(1), privileges: Joi.array().items(kibanaSubFeaturePrivilegeSchema).min(1),
}) })
), ),
}); });
const schema = Joi.object({ const kibanaFeatureSchema = Joi.object({
id: Joi.string() id: Joi.string()
.regex(featurePrivilegePartRegex) .regex(featurePrivilegePartRegex)
.invalid(...prohibitedFeatureIds) .invalid(...prohibitedFeatureIds)
@ -93,15 +93,15 @@ const schema = Joi.object({
catalogue: catalogueSchema, catalogue: catalogueSchema,
alerting: alertingSchema, alerting: alertingSchema,
privileges: Joi.object({ privileges: Joi.object({
all: privilegeSchema, all: kibanaPrivilegeSchema,
read: privilegeSchema, read: kibanaPrivilegeSchema,
}) })
.allow(null) .allow(null)
.required(), .required(),
subFeatures: Joi.when('privileges', { subFeatures: Joi.when('privileges', {
is: null, is: null,
then: Joi.array().items(subFeatureSchema).max(0), then: Joi.array().items(kibanaSubFeatureSchema).max(0),
otherwise: Joi.array().items(subFeatureSchema), otherwise: Joi.array().items(kibanaSubFeatureSchema),
}), }),
privilegesTooltip: Joi.string(), privilegesTooltip: Joi.string(),
reserved: Joi.object({ reserved: Joi.object({
@ -110,15 +110,32 @@ const schema = Joi.object({
.items( .items(
Joi.object({ Joi.object({
id: Joi.string().regex(reservedFeaturePrrivilegePartRegex).required(), id: Joi.string().regex(reservedFeaturePrrivilegePartRegex).required(),
privilege: privilegeSchema.required(), privilege: kibanaPrivilegeSchema.required(),
}) })
) )
.required(), .required(),
}), }),
}); });
export function validateFeature(feature: FeatureConfig) { const elasticsearchPrivilegeSchema = Joi.object({
const validateResult = Joi.validate(feature, schema); ui: Joi.array().items(Joi.string()).required(),
requiredClusterPrivileges: Joi.array().items(Joi.string()),
requiredIndexPrivileges: Joi.object().pattern(Joi.string(), Joi.array().items(Joi.string())),
requiredRoles: Joi.array().items(Joi.string()),
});
const elasticsearchFeatureSchema = Joi.object({
id: Joi.string()
.regex(featurePrivilegePartRegex)
.invalid(...prohibitedFeatureIds)
.required(),
management: managementSchema,
catalogue: catalogueSchema,
privileges: Joi.array().items(elasticsearchPrivilegeSchema).required(),
});
export function validateKibanaFeature(feature: KibanaFeatureConfig) {
const validateResult = Joi.validate(feature, kibanaFeatureSchema);
if (validateResult.error) { if (validateResult.error) {
throw validateResult.error; throw validateResult.error;
} }
@ -303,3 +320,29 @@ export function validateFeature(feature: FeatureConfig) {
); );
} }
} }
export function validateElasticsearchFeature(feature: ElasticsearchFeatureConfig) {
const validateResult = Joi.validate(feature, elasticsearchFeatureSchema);
if (validateResult.error) {
throw validateResult.error;
}
// the following validation can't be enforced by the Joi schema without a very convoluted and verbose definition
const { privileges } = feature;
privileges.forEach((privilege, index) => {
const {
requiredClusterPrivileges = [],
requiredIndexPrivileges = [],
requiredRoles = [],
} = privilege;
if (
requiredClusterPrivileges.length === 0 &&
requiredIndexPrivileges.length === 0 &&
requiredRoles.length === 0
) {
throw new Error(
`Feature ${feature.id} has a privilege definition at index ${index} without any privileges defined.`
);
}
});
}

View file

@ -13,7 +13,14 @@ import { Plugin } from './plugin';
// run-time contracts. // run-time contracts.
export { uiCapabilitiesRegex } from './feature_schema'; export { uiCapabilitiesRegex } from './feature_schema';
export { Feature, FeatureConfig, FeatureKibanaPrivileges } from '../common'; export {
KibanaFeature,
KibanaFeatureConfig,
FeatureKibanaPrivileges,
ElasticsearchFeature,
ElasticsearchFeatureConfig,
FeatureElasticsearchPrivileges,
} from '../common';
export { PluginSetupContract, PluginStartContract } from './plugin'; export { PluginSetupContract, PluginStartContract } from './plugin';
export const plugin = (initializerContext: PluginInitializerContext) => export const plugin = (initializerContext: PluginInitializerContext) =>

View file

@ -8,15 +8,18 @@ import { PluginSetupContract, PluginStartContract } from './plugin';
const createSetup = (): jest.Mocked<PluginSetupContract> => { const createSetup = (): jest.Mocked<PluginSetupContract> => {
return { return {
getFeatures: jest.fn(), getKibanaFeatures: jest.fn(),
getElasticsearchFeatures: jest.fn(),
getFeaturesUICapabilities: jest.fn(), getFeaturesUICapabilities: jest.fn(),
registerFeature: jest.fn(), registerKibanaFeature: jest.fn(),
registerElasticsearchFeature: jest.fn(),
}; };
}; };
const createStart = (): jest.Mocked<PluginStartContract> => { const createStart = (): jest.Mocked<PluginStartContract> => {
return { return {
getFeatures: jest.fn(), getKibanaFeatures: jest.fn(),
getElasticsearchFeatures: jest.fn(),
}; };
}; };

View file

@ -6,7 +6,7 @@
import { buildOSSFeatures } from './oss_features'; import { buildOSSFeatures } from './oss_features';
import { featurePrivilegeIterator } from '../../security/server/authorization'; import { featurePrivilegeIterator } from '../../security/server/authorization';
import { Feature } from '.'; import { KibanaFeature } from '.';
describe('buildOSSFeatures', () => { describe('buildOSSFeatures', () => {
it('returns features including timelion', () => { it('returns features including timelion', () => {
@ -48,7 +48,7 @@ Array [
features.forEach((featureConfig) => { features.forEach((featureConfig) => {
it(`returns the ${featureConfig.id} feature augmented with appropriate sub feature privileges`, () => { it(`returns the ${featureConfig.id} feature augmented with appropriate sub feature privileges`, () => {
const privileges = []; const privileges = [];
for (const featurePrivilege of featurePrivilegeIterator(new Feature(featureConfig), { for (const featurePrivilege of featurePrivilegeIterator(new KibanaFeature(featureConfig), {
augmentWithSubFeaturePrivileges: true, augmentWithSubFeaturePrivileges: true,
})) { })) {
privileges.push(featurePrivilege); privileges.push(featurePrivilege);

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { FeatureConfig } from '../common/feature'; import { KibanaFeatureConfig } from '../common';
export interface BuildOSSFeaturesParams { export interface BuildOSSFeaturesParams {
savedObjectTypes: string[]; savedObjectTypes: string[];
@ -368,10 +368,10 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
}, },
}, },
...(includeTimelion ? [timelionFeature] : []), ...(includeTimelion ? [timelionFeature] : []),
] as FeatureConfig[]; ] as KibanaFeatureConfig[];
}; };
const timelionFeature: FeatureConfig = { const timelionFeature: KibanaFeatureConfig = {
id: 'timelion', id: 'timelion',
name: 'Timelion', name: 'Timelion',
order: 350, order: 350,

View file

@ -28,19 +28,19 @@ describe('Features Plugin', () => {
coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry);
}); });
it('returns OSS + registered features', async () => { it('returns OSS + registered kibana features', async () => {
const plugin = new Plugin(initContext); const plugin = new Plugin(initContext);
const { registerFeature } = await plugin.setup(coreSetup, {}); const { registerKibanaFeature } = await plugin.setup(coreSetup, {});
registerFeature({ registerKibanaFeature({
id: 'baz', id: 'baz',
name: 'baz', name: 'baz',
app: [], app: [],
privileges: null, privileges: null,
}); });
const { getFeatures } = await plugin.start(coreStart); const { getKibanaFeatures } = plugin.start(coreStart);
expect(getFeatures().map((f) => f.id)).toMatchInlineSnapshot(` expect(getKibanaFeatures().map((f) => f.id)).toMatchInlineSnapshot(`
Array [ Array [
"baz", "baz",
"discover", "discover",
@ -54,9 +54,9 @@ describe('Features Plugin', () => {
`); `);
}); });
it('returns OSS + registered features with timelion when available', async () => { it('returns OSS + registered kibana features with timelion when available', async () => {
const plugin = new Plugin(initContext); const plugin = new Plugin(initContext);
const { registerFeature } = await plugin.setup(coreSetup, { const { registerKibanaFeature: registerFeature } = await plugin.setup(coreSetup, {
visTypeTimelion: { uiEnabled: true }, visTypeTimelion: { uiEnabled: true },
}); });
registerFeature({ registerFeature({
@ -66,9 +66,9 @@ describe('Features Plugin', () => {
privileges: null, privileges: null,
}); });
const { getFeatures } = await plugin.start(coreStart); const { getKibanaFeatures } = plugin.start(coreStart);
expect(getFeatures().map((f) => f.id)).toMatchInlineSnapshot(` expect(getKibanaFeatures().map((f) => f.id)).toMatchInlineSnapshot(`
Array [ Array [
"baz", "baz",
"discover", "discover",
@ -83,19 +83,41 @@ describe('Features Plugin', () => {
`); `);
}); });
it('registers not hidden saved objects types', async () => { it('registers kibana features with not hidden saved objects types', async () => {
const plugin = new Plugin(initContext); const plugin = new Plugin(initContext);
await plugin.setup(coreSetup, {}); await plugin.setup(coreSetup, {});
const { getFeatures } = await plugin.start(coreStart); const { getKibanaFeatures } = plugin.start(coreStart);
const soTypes = const soTypes =
getFeatures().find((f) => f.id === 'savedObjectsManagement')?.privileges?.all.savedObject getKibanaFeatures().find((f) => f.id === 'savedObjectsManagement')?.privileges?.all
.all || []; .savedObject.all || [];
expect(soTypes.includes('foo')).toBe(true); expect(soTypes.includes('foo')).toBe(true);
expect(soTypes.includes('bar')).toBe(false); expect(soTypes.includes('bar')).toBe(false);
}); });
it('returns registered elasticsearch features', async () => {
const plugin = new Plugin(initContext);
const { registerElasticsearchFeature } = await plugin.setup(coreSetup, {});
registerElasticsearchFeature({
id: 'baz',
privileges: [
{
requiredClusterPrivileges: ['all'],
ui: ['baz-ui'],
},
],
});
const { getElasticsearchFeatures } = plugin.start(coreStart);
expect(getElasticsearchFeatures().map((f) => f.id)).toMatchInlineSnapshot(`
Array [
"baz",
]
`);
});
it('registers a capabilities provider', async () => { it('registers a capabilities provider', async () => {
const plugin = new Plugin(initContext); const plugin = new Plugin(initContext);
await plugin.setup(coreSetup, {}); await plugin.setup(coreSetup, {});

View file

@ -15,27 +15,40 @@ import { Capabilities as UICapabilities } from '../../../../src/core/server';
import { deepFreeze } from '../../../../src/core/server'; import { deepFreeze } from '../../../../src/core/server';
import { PluginSetupContract as TimelionSetupContract } from '../../../../src/plugins/vis_type_timelion/server'; import { PluginSetupContract as TimelionSetupContract } from '../../../../src/plugins/vis_type_timelion/server';
import { FeatureRegistry } from './feature_registry'; import { FeatureRegistry } from './feature_registry';
import { Feature, FeatureConfig } from '../common/feature';
import { uiCapabilitiesForFeatures } from './ui_capabilities_for_features'; import { uiCapabilitiesForFeatures } from './ui_capabilities_for_features';
import { buildOSSFeatures } from './oss_features'; import { buildOSSFeatures } from './oss_features';
import { defineRoutes } from './routes'; import { defineRoutes } from './routes';
import {
ElasticsearchFeatureConfig,
ElasticsearchFeature,
KibanaFeature,
KibanaFeatureConfig,
} from '../common';
/** /**
* Describes public Features plugin contract returned at the `setup` stage. * Describes public Features plugin contract returned at the `setup` stage.
*/ */
export interface PluginSetupContract { export interface PluginSetupContract {
registerFeature(feature: FeatureConfig): void; registerKibanaFeature(feature: KibanaFeatureConfig): void;
registerElasticsearchFeature(feature: ElasticsearchFeatureConfig): void;
/* /*
* Calling this function during setup will crash Kibana. * Calling this function during setup will crash Kibana.
* Use start contract instead. * Use start contract instead.
* @deprecated * @deprecated
* */ * */
getFeatures(): Feature[]; getKibanaFeatures(): KibanaFeature[];
/*
* Calling this function during setup will crash Kibana.
* Use start contract instead.
* @deprecated
* */
getElasticsearchFeatures(): ElasticsearchFeature[];
getFeaturesUICapabilities(): UICapabilities; getFeaturesUICapabilities(): UICapabilities;
} }
export interface PluginStartContract { export interface PluginStartContract {
getFeatures(): Feature[]; getElasticsearchFeatures(): ElasticsearchFeature[];
getKibanaFeatures(): KibanaFeature[];
} }
/** /**
@ -62,13 +75,22 @@ export class Plugin {
}); });
const getFeaturesUICapabilities = () => const getFeaturesUICapabilities = () =>
uiCapabilitiesForFeatures(this.featureRegistry.getAll()); uiCapabilitiesForFeatures(
this.featureRegistry.getAllKibanaFeatures(),
this.featureRegistry.getAllElasticsearchFeatures()
);
core.capabilities.registerProvider(getFeaturesUICapabilities); core.capabilities.registerProvider(getFeaturesUICapabilities);
return deepFreeze({ return deepFreeze({
registerFeature: this.featureRegistry.register.bind(this.featureRegistry), registerKibanaFeature: this.featureRegistry.registerKibanaFeature.bind(this.featureRegistry),
getFeatures: this.featureRegistry.getAll.bind(this.featureRegistry), registerElasticsearchFeature: this.featureRegistry.registerElasticsearchFeature.bind(
this.featureRegistry
),
getKibanaFeatures: this.featureRegistry.getAllKibanaFeatures.bind(this.featureRegistry),
getElasticsearchFeatures: this.featureRegistry.getAllElasticsearchFeatures.bind(
this.featureRegistry
),
getFeaturesUICapabilities, getFeaturesUICapabilities,
}); });
} }
@ -77,7 +99,10 @@ export class Plugin {
this.registerOssFeatures(core.savedObjects); this.registerOssFeatures(core.savedObjects);
return deepFreeze({ return deepFreeze({
getFeatures: this.featureRegistry.getAll.bind(this.featureRegistry), getElasticsearchFeatures: this.featureRegistry.getAllElasticsearchFeatures.bind(
this.featureRegistry
),
getKibanaFeatures: this.featureRegistry.getAllKibanaFeatures.bind(this.featureRegistry),
}); });
} }
@ -98,7 +123,7 @@ export class Plugin {
}); });
for (const feature of features) { for (const feature of features) {
this.featureRegistry.register(feature); this.featureRegistry.registerKibanaFeature(feature);
} }
} }
} }

View file

@ -11,7 +11,7 @@ import { httpServerMock, httpServiceMock, coreMock } from '../../../../../src/co
import { LicenseType } from '../../../licensing/server/'; import { LicenseType } from '../../../licensing/server/';
import { licensingMock } from '../../../licensing/server/mocks'; import { licensingMock } from '../../../licensing/server/mocks';
import { RequestHandler } from '../../../../../src/core/server'; import { RequestHandler } from '../../../../../src/core/server';
import { FeatureConfig } from '../../common'; import { KibanaFeatureConfig } from '../../common';
function createContextMock(licenseType: LicenseType = 'gold') { function createContextMock(licenseType: LicenseType = 'gold') {
return { return {
@ -24,14 +24,14 @@ describe('GET /api/features', () => {
let routeHandler: RequestHandler<any, any, any>; let routeHandler: RequestHandler<any, any, any>;
beforeEach(() => { beforeEach(() => {
const featureRegistry = new FeatureRegistry(); const featureRegistry = new FeatureRegistry();
featureRegistry.register({ featureRegistry.registerKibanaFeature({
id: 'feature_1', id: 'feature_1',
name: 'Feature 1', name: 'Feature 1',
app: [], app: [],
privileges: null, privileges: null,
}); });
featureRegistry.register({ featureRegistry.registerKibanaFeature({
id: 'feature_2', id: 'feature_2',
name: 'Feature 2', name: 'Feature 2',
order: 2, order: 2,
@ -39,7 +39,7 @@ describe('GET /api/features', () => {
privileges: null, privileges: null,
}); });
featureRegistry.register({ featureRegistry.registerKibanaFeature({
id: 'feature_3', id: 'feature_3',
name: 'Feature 2', name: 'Feature 2',
order: 1, order: 1,
@ -47,7 +47,7 @@ describe('GET /api/features', () => {
privileges: null, privileges: null,
}); });
featureRegistry.register({ featureRegistry.registerKibanaFeature({
id: 'licensed_feature', id: 'licensed_feature',
name: 'Licensed Feature', name: 'Licensed Feature',
app: ['bar-app'], app: ['bar-app'],
@ -70,7 +70,7 @@ describe('GET /api/features', () => {
expect(mockResponse.ok).toHaveBeenCalledTimes(1); expect(mockResponse.ok).toHaveBeenCalledTimes(1);
const [call] = mockResponse.ok.mock.calls; const [call] = mockResponse.ok.mock.calls;
const body = call[0]!.body as FeatureConfig[]; const body = call[0]!.body as KibanaFeatureConfig[];
const features = body.map((feature) => ({ id: feature.id, order: feature.order })); const features = body.map((feature) => ({ id: feature.id, order: feature.order }));
expect(features).toEqual([ expect(features).toEqual([
@ -99,7 +99,7 @@ describe('GET /api/features', () => {
expect(mockResponse.ok).toHaveBeenCalledTimes(1); expect(mockResponse.ok).toHaveBeenCalledTimes(1);
const [call] = mockResponse.ok.mock.calls; const [call] = mockResponse.ok.mock.calls;
const body = call[0]!.body as FeatureConfig[]; const body = call[0]!.body as KibanaFeatureConfig[];
const features = body.map((feature) => ({ id: feature.id, order: feature.order })); const features = body.map((feature) => ({ id: feature.id, order: feature.order }));
@ -129,7 +129,7 @@ describe('GET /api/features', () => {
expect(mockResponse.ok).toHaveBeenCalledTimes(1); expect(mockResponse.ok).toHaveBeenCalledTimes(1);
const [call] = mockResponse.ok.mock.calls; const [call] = mockResponse.ok.mock.calls;
const body = call[0]!.body as FeatureConfig[]; const body = call[0]!.body as KibanaFeatureConfig[];
const features = body.map((feature) => ({ id: feature.id, order: feature.order })); const features = body.map((feature) => ({ id: feature.id, order: feature.order }));
@ -159,7 +159,7 @@ describe('GET /api/features', () => {
expect(mockResponse.ok).toHaveBeenCalledTimes(1); expect(mockResponse.ok).toHaveBeenCalledTimes(1);
const [call] = mockResponse.ok.mock.calls; const [call] = mockResponse.ok.mock.calls;
const body = call[0]!.body as FeatureConfig[]; const body = call[0]!.body as KibanaFeatureConfig[];
const features = body.map((feature) => ({ id: feature.id, order: feature.order })); const features = body.map((feature) => ({ id: feature.id, order: feature.order }));

View file

@ -26,7 +26,7 @@ export function defineRoutes({ router, featureRegistry }: RouteDefinitionParams)
}, },
}, },
(context, request, response) => { (context, request, response) => {
const allFeatures = featureRegistry.getAll(); const allFeatures = featureRegistry.getAllKibanaFeatures();
return response.ok({ return response.ok({
body: allFeatures body: allFeatures

View file

@ -5,10 +5,10 @@
*/ */
import { uiCapabilitiesForFeatures } from './ui_capabilities_for_features'; import { uiCapabilitiesForFeatures } from './ui_capabilities_for_features';
import { Feature } from '.'; import { KibanaFeature } from '.';
import { SubFeaturePrivilegeGroupConfig } from '../common'; import { SubFeaturePrivilegeGroupConfig, ElasticsearchFeature } from '../common';
function createFeaturePrivilege(capabilities: string[] = []) { function createKibanaFeaturePrivilege(capabilities: string[] = []) {
return { return {
savedObject: { savedObject: {
all: [], all: [],
@ -19,7 +19,7 @@ function createFeaturePrivilege(capabilities: string[] = []) {
}; };
} }
function createSubFeaturePrivilege(privilegeId: string, capabilities: string[] = []) { function createKibanaSubFeaturePrivilege(privilegeId: string, capabilities: string[] = []) {
return { return {
id: privilegeId, id: privilegeId,
name: `sub-feature privilege ${privilegeId}`, name: `sub-feature privilege ${privilegeId}`,
@ -35,44 +35,75 @@ function createSubFeaturePrivilege(privilegeId: string, capabilities: string[] =
describe('populateUICapabilities', () => { describe('populateUICapabilities', () => {
it('handles no original uiCapabilities and no registered features gracefully', () => { it('handles no original uiCapabilities and no registered features gracefully', () => {
expect(uiCapabilitiesForFeatures([])).toEqual({}); expect(uiCapabilitiesForFeatures([], [])).toEqual({});
}); });
it('handles features with no registered capabilities', () => { it('handles kibana features with no registered capabilities', () => {
expect( expect(
uiCapabilitiesForFeatures([ uiCapabilitiesForFeatures(
new Feature({ [
id: 'newFeature', new KibanaFeature({
name: 'my new feature', id: 'newFeature',
app: ['bar-app'], name: 'my new feature',
privileges: { app: ['bar-app'],
all: createFeaturePrivilege(), privileges: {
read: createFeaturePrivilege(), all: createKibanaFeaturePrivilege(),
}, read: createKibanaFeaturePrivilege(),
}), },
]) }),
],
[]
)
).toEqual({ ).toEqual({
catalogue: {}, catalogue: {},
management: {},
newFeature: {}, newFeature: {},
}); });
}); });
it('augments the original uiCapabilities with registered feature capabilities', () => { it('handles elasticsearch features with no registered capabilities', () => {
expect( expect(
uiCapabilitiesForFeatures([ uiCapabilitiesForFeatures(
new Feature({ [],
id: 'newFeature', [
name: 'my new feature', new ElasticsearchFeature({
navLinkId: 'newFeatureNavLink', id: 'newFeature',
app: ['bar-app'], privileges: [
privileges: { {
all: createFeaturePrivilege(['capability1', 'capability2']), requiredClusterPrivileges: [],
read: createFeaturePrivilege(), ui: [],
}, },
}), ],
]) }),
]
)
).toEqual({ ).toEqual({
catalogue: {}, catalogue: {},
management: {},
newFeature: {},
});
});
it('augments the original uiCapabilities with registered kibana feature capabilities', () => {
expect(
uiCapabilitiesForFeatures(
[
new KibanaFeature({
id: 'newFeature',
name: 'my new feature',
navLinkId: 'newFeatureNavLink',
app: ['bar-app'],
privileges: {
all: createKibanaFeaturePrivilege(['capability1', 'capability2']),
read: createKibanaFeaturePrivilege(),
},
}),
],
[]
)
).toEqual({
catalogue: {},
management: {},
newFeature: { newFeature: {
capability1: true, capability1: true,
capability2: true, capability2: true,
@ -80,26 +111,92 @@ describe('populateUICapabilities', () => {
}); });
}); });
it('combines catalogue entries from multiple features', () => { it('augments the original uiCapabilities with registered elasticsearch feature capabilities', () => {
expect( expect(
uiCapabilitiesForFeatures([ uiCapabilitiesForFeatures(
new Feature({ [],
id: 'newFeature', [
name: 'my new feature', new ElasticsearchFeature({
navLinkId: 'newFeatureNavLink', id: 'newFeature',
app: ['bar-app'], privileges: [
catalogue: ['anotherFooEntry', 'anotherBarEntry'], {
privileges: { requiredClusterPrivileges: [],
all: createFeaturePrivilege(['capability1', 'capability2']), ui: ['capability1', 'capability2'],
read: createFeaturePrivilege(['capability3', 'capability4']), },
}, ],
}), }),
]) ]
)
).toEqual({
catalogue: {},
management: {},
newFeature: {
capability1: true,
capability2: true,
},
});
});
it('combines catalogue entries from multiple kibana features', () => {
expect(
uiCapabilitiesForFeatures(
[
new KibanaFeature({
id: 'newFeature',
name: 'my new feature',
navLinkId: 'newFeatureNavLink',
app: ['bar-app'],
catalogue: ['anotherFooEntry', 'anotherBarEntry'],
privileges: {
all: createKibanaFeaturePrivilege(['capability1', 'capability2']),
read: createKibanaFeaturePrivilege(['capability3', 'capability4']),
},
}),
],
[]
)
).toEqual({ ).toEqual({
catalogue: { catalogue: {
anotherFooEntry: true, anotherFooEntry: true,
anotherBarEntry: true, anotherBarEntry: true,
}, },
management: {},
newFeature: {
capability1: true,
capability2: true,
capability3: true,
capability4: true,
},
});
});
it('combines catalogue entries from multiple elasticsearch privileges', () => {
expect(
uiCapabilitiesForFeatures(
[],
[
new ElasticsearchFeature({
id: 'newFeature',
catalogue: ['anotherFooEntry', 'anotherBarEntry'],
privileges: [
{
requiredClusterPrivileges: [],
ui: ['capability1', 'capability2'],
},
{
requiredClusterPrivileges: [],
ui: ['capability3', 'capability4'],
},
],
}),
]
)
).toEqual({
catalogue: {
anotherFooEntry: true,
anotherBarEntry: true,
},
management: {},
newFeature: { newFeature: {
capability1: true, capability1: true,
capability2: true, capability2: true,
@ -111,20 +208,24 @@ describe('populateUICapabilities', () => {
it(`merges capabilities from all feature privileges`, () => { it(`merges capabilities from all feature privileges`, () => {
expect( expect(
uiCapabilitiesForFeatures([ uiCapabilitiesForFeatures(
new Feature({ [
id: 'newFeature', new KibanaFeature({
name: 'my new feature', id: 'newFeature',
navLinkId: 'newFeatureNavLink', name: 'my new feature',
app: ['bar-app'], navLinkId: 'newFeatureNavLink',
privileges: { app: ['bar-app'],
all: createFeaturePrivilege(['capability1', 'capability2']), privileges: {
read: createFeaturePrivilege(['capability3', 'capability4', 'capability5']), all: createKibanaFeaturePrivilege(['capability1', 'capability2']),
}, read: createKibanaFeaturePrivilege(['capability3', 'capability4', 'capability5']),
}), },
]) }),
],
[]
)
).toEqual({ ).toEqual({
catalogue: {}, catalogue: {},
management: {},
newFeature: { newFeature: {
capability1: true, capability1: true,
capability2: true, capability2: true,
@ -137,30 +238,38 @@ describe('populateUICapabilities', () => {
it(`supports capabilities from reserved privileges`, () => { it(`supports capabilities from reserved privileges`, () => {
expect( expect(
uiCapabilitiesForFeatures([ uiCapabilitiesForFeatures(
new Feature({ [
id: 'newFeature', new KibanaFeature({
name: 'my new feature', id: 'newFeature',
navLinkId: 'newFeatureNavLink', name: 'my new feature',
app: ['bar-app'], navLinkId: 'newFeatureNavLink',
privileges: null, app: ['bar-app'],
reserved: { privileges: null,
description: '', reserved: {
privileges: [ description: '',
{ privileges: [
id: 'rp_1', {
privilege: createFeaturePrivilege(['capability1', 'capability2']), id: 'rp_1',
}, privilege: createKibanaFeaturePrivilege(['capability1', 'capability2']),
{ },
id: 'rp_2', {
privilege: createFeaturePrivilege(['capability3', 'capability4', 'capability5']), id: 'rp_2',
}, privilege: createKibanaFeaturePrivilege([
], 'capability3',
}, 'capability4',
}), 'capability5',
]) ]),
},
],
},
}),
],
[]
)
).toEqual({ ).toEqual({
catalogue: {}, catalogue: {},
management: {},
newFeature: { newFeature: {
capability1: true, capability1: true,
capability2: true, capability2: true,
@ -173,53 +282,60 @@ describe('populateUICapabilities', () => {
it(`supports merging features with sub privileges`, () => { it(`supports merging features with sub privileges`, () => {
expect( expect(
uiCapabilitiesForFeatures([ uiCapabilitiesForFeatures(
new Feature({ [
id: 'newFeature', new KibanaFeature({
name: 'my new feature', id: 'newFeature',
navLinkId: 'newFeatureNavLink', name: 'my new feature',
app: ['bar-app'], navLinkId: 'newFeatureNavLink',
privileges: { app: ['bar-app'],
all: createFeaturePrivilege(['capability1', 'capability2']), privileges: {
read: createFeaturePrivilege(['capability3', 'capability4']), all: createKibanaFeaturePrivilege(['capability1', 'capability2']),
}, read: createKibanaFeaturePrivilege(['capability3', 'capability4']),
subFeatures: [
{
name: 'sub-feature-1',
privilegeGroups: [
{
groupType: 'independent',
privileges: [
createSubFeaturePrivilege('privilege-1', ['capability5']),
createSubFeaturePrivilege('privilege-2', ['capability6']),
],
} as SubFeaturePrivilegeGroupConfig,
{
groupType: 'mutually_exclusive',
privileges: [
createSubFeaturePrivilege('privilege-3', ['capability7']),
createSubFeaturePrivilege('privilege-4', ['capability8']),
],
} as SubFeaturePrivilegeGroupConfig,
],
}, },
{ subFeatures: [
name: 'sub-feature-2', {
privilegeGroups: [ name: 'sub-feature-1',
{ privilegeGroups: [
name: 'Group Name', {
groupType: 'independent', groupType: 'independent',
privileges: [ privileges: [
createSubFeaturePrivilege('privilege-5', ['capability9', 'capability10']), createKibanaSubFeaturePrivilege('privilege-1', ['capability5']),
], createKibanaSubFeaturePrivilege('privilege-2', ['capability6']),
} as SubFeaturePrivilegeGroupConfig, ],
], } as SubFeaturePrivilegeGroupConfig,
}, {
], groupType: 'mutually_exclusive',
}), privileges: [
]) createKibanaSubFeaturePrivilege('privilege-3', ['capability7']),
createKibanaSubFeaturePrivilege('privilege-4', ['capability8']),
],
} as SubFeaturePrivilegeGroupConfig,
],
},
{
name: 'sub-feature-2',
privilegeGroups: [
{
name: 'Group Name',
groupType: 'independent',
privileges: [
createKibanaSubFeaturePrivilege('privilege-5', [
'capability9',
'capability10',
]),
],
} as SubFeaturePrivilegeGroupConfig,
],
},
],
}),
],
[]
)
).toEqual({ ).toEqual({
catalogue: {}, catalogue: {},
management: {},
newFeature: { newFeature: {
capability1: true, capability1: true,
capability2: true, capability2: true,
@ -235,53 +351,56 @@ describe('populateUICapabilities', () => {
}); });
}); });
it('supports merging multiple features with multiple privileges each', () => { it('supports merging multiple kibana features with multiple privileges each', () => {
expect( expect(
uiCapabilitiesForFeatures([ uiCapabilitiesForFeatures(
new Feature({ [
id: 'newFeature', new KibanaFeature({
name: 'my new feature', id: 'newFeature',
navLinkId: 'newFeatureNavLink', name: 'my new feature',
app: ['bar-app'], navLinkId: 'newFeatureNavLink',
privileges: { app: ['bar-app'],
all: createFeaturePrivilege(['capability1', 'capability2']), privileges: {
read: createFeaturePrivilege(['capability3', 'capability4']), all: createKibanaFeaturePrivilege(['capability1', 'capability2']),
}, read: createKibanaFeaturePrivilege(['capability3', 'capability4']),
}),
new Feature({
id: 'anotherNewFeature',
name: 'another new feature',
app: ['bar-app'],
privileges: {
all: createFeaturePrivilege(['capability1', 'capability2']),
read: createFeaturePrivilege(['capability3', 'capability4']),
},
}),
new Feature({
id: 'yetAnotherNewFeature',
name: 'yet another new feature',
navLinkId: 'yetAnotherNavLink',
app: ['bar-app'],
privileges: {
all: createFeaturePrivilege(['capability1', 'capability2']),
read: createFeaturePrivilege(['something1', 'something2', 'something3']),
},
subFeatures: [
{
name: 'sub-feature-1',
privilegeGroups: [
{
groupType: 'independent',
privileges: [
createSubFeaturePrivilege('privilege-1', ['capability3']),
createSubFeaturePrivilege('privilege-2', ['capability4']),
],
} as SubFeaturePrivilegeGroupConfig,
],
}, },
], }),
}), new KibanaFeature({
]) id: 'anotherNewFeature',
name: 'another new feature',
app: ['bar-app'],
privileges: {
all: createKibanaFeaturePrivilege(['capability1', 'capability2']),
read: createKibanaFeaturePrivilege(['capability3', 'capability4']),
},
}),
new KibanaFeature({
id: 'yetAnotherNewFeature',
name: 'yet another new feature',
navLinkId: 'yetAnotherNavLink',
app: ['bar-app'],
privileges: {
all: createKibanaFeaturePrivilege(['capability1', 'capability2']),
read: createKibanaFeaturePrivilege(['something1', 'something2', 'something3']),
},
subFeatures: [
{
name: 'sub-feature-1',
privilegeGroups: [
{
groupType: 'independent',
privileges: [
createKibanaSubFeaturePrivilege('privilege-1', ['capability3']),
createKibanaSubFeaturePrivilege('privilege-2', ['capability4']),
],
} as SubFeaturePrivilegeGroupConfig,
],
},
],
}),
],
[]
)
).toEqual({ ).toEqual({
anotherNewFeature: { anotherNewFeature: {
capability1: true, capability1: true,
@ -290,6 +409,83 @@ describe('populateUICapabilities', () => {
capability4: true, capability4: true,
}, },
catalogue: {}, catalogue: {},
management: {},
newFeature: {
capability1: true,
capability2: true,
capability3: true,
capability4: true,
},
yetAnotherNewFeature: {
capability1: true,
capability2: true,
capability3: true,
capability4: true,
something1: true,
something2: true,
something3: true,
},
});
});
it('supports merging multiple elasticsearch features with multiple privileges each', () => {
expect(
uiCapabilitiesForFeatures(
[],
[
new ElasticsearchFeature({
id: 'newFeature',
privileges: [
{
requiredClusterPrivileges: [],
ui: ['capability1', 'capability2'],
},
{
requiredClusterPrivileges: [],
ui: ['capability3', 'capability4'],
},
],
}),
new ElasticsearchFeature({
id: 'anotherNewFeature',
privileges: [
{
requiredClusterPrivileges: [],
ui: ['capability1', 'capability2'],
},
{
requiredClusterPrivileges: [],
ui: ['capability3', 'capability4'],
},
],
}),
new ElasticsearchFeature({
id: 'yetAnotherNewFeature',
privileges: [
{
requiredClusterPrivileges: [],
ui: ['capability1', 'capability2', 'capability3', 'capability4'],
},
{
requiredClusterPrivileges: [],
ui: ['something1', 'something2', 'something3'],
},
],
}),
]
)
).toEqual({
anotherNewFeature: {
capability1: true,
capability2: true,
capability3: true,
capability4: true,
},
catalogue: {},
management: {},
newFeature: { newFeature: {
capability1: true, capability1: true,
capability2: true, capability2: true,

View file

@ -5,22 +5,35 @@
*/ */
import _ from 'lodash'; import _ from 'lodash';
import { RecursiveReadonly } from '@kbn/utility-types';
import { Capabilities as UICapabilities } from '../../../../src/core/server'; import { Capabilities as UICapabilities } from '../../../../src/core/server';
import { Feature } from '../common/feature'; import { ElasticsearchFeature, KibanaFeature } from '../common';
const ELIGIBLE_FLAT_MERGE_KEYS = ['catalogue'] as const; const ELIGIBLE_FLAT_MERGE_KEYS = ['catalogue'] as const;
const ELIGIBLE_DEEP_MERGE_KEYS = ['management'] as const;
interface FeatureCapabilities { interface FeatureCapabilities {
[featureId: string]: Record<string, boolean>; [featureId: string]: Record<string, boolean>;
} }
export function uiCapabilitiesForFeatures(features: Feature[]): UICapabilities { export function uiCapabilitiesForFeatures(
const featureCapabilities: FeatureCapabilities[] = features.map(getCapabilitiesFromFeature); kibanaFeatures: KibanaFeature[],
elasticsearchFeatures: ElasticsearchFeature[]
): UICapabilities {
const kibanaFeatureCapabilities = kibanaFeatures.map(getCapabilitiesFromFeature);
const elasticsearchFeatureCapabilities = elasticsearchFeatures.map(getCapabilitiesFromFeature);
return buildCapabilities(...featureCapabilities); return buildCapabilities(...kibanaFeatureCapabilities, ...elasticsearchFeatureCapabilities);
} }
function getCapabilitiesFromFeature(feature: Feature): FeatureCapabilities { function getCapabilitiesFromFeature(
feature:
| Pick<
KibanaFeature,
'id' | 'catalogue' | 'management' | 'privileges' | 'subFeatures' | 'reserved'
>
| Pick<ElasticsearchFeature, 'id' | 'catalogue' | 'management' | 'privileges'>
): FeatureCapabilities {
const UIFeatureCapabilities: FeatureCapabilities = { const UIFeatureCapabilities: FeatureCapabilities = {
catalogue: {}, catalogue: {},
[feature.id]: {}, [feature.id]: {},
@ -39,14 +52,34 @@ function getCapabilitiesFromFeature(feature: Feature): FeatureCapabilities {
}; };
} }
const featurePrivileges = Object.values(feature.privileges ?? {}); if (feature.management) {
if (feature.subFeatures) { const sectionEntries = Object.entries(feature.management);
featurePrivileges.push( UIFeatureCapabilities.management = sectionEntries.reduce((acc, [sectionId, sectionItems]) => {
...feature.subFeatures.map((sf) => sf.privilegeGroups.map((pg) => pg.privileges)).flat(2) return {
); ...acc,
[sectionId]: sectionItems.reduce((acc2, item) => {
return {
...acc2,
[item]: true,
};
}, {}),
};
}, {});
} }
if (feature.reserved?.privileges) {
featurePrivileges.push(...feature.reserved.privileges.map((rp) => rp.privilege)); const featurePrivileges = Object.values(feature.privileges ?? {}) as Writable<
Array<{ ui: RecursiveReadonly<string[]> }>
>;
if (isKibanaFeature(feature)) {
if (feature.subFeatures) {
featurePrivileges.push(
...feature.subFeatures.map((sf) => sf.privilegeGroups.map((pg) => pg.privileges)).flat(2)
);
}
if (feature.reserved?.privileges) {
featurePrivileges.push(...feature.reserved.privileges.map((rp) => rp.privilege));
}
} }
featurePrivileges.forEach((privilege) => { featurePrivileges.forEach((privilege) => {
@ -65,6 +98,20 @@ function getCapabilitiesFromFeature(feature: Feature): FeatureCapabilities {
return UIFeatureCapabilities; return UIFeatureCapabilities;
} }
function isKibanaFeature(
feature: Partial<KibanaFeature> | Partial<ElasticsearchFeature>
): feature is KibanaFeature {
// Elasticsearch features define privileges as an array,
// whereas Kibana features define privileges as an object,
// or they define reserved privileges, or they don't define either.
// Elasticsearch features are required to defined privileges.
return (
(feature as any).reserved != null ||
(feature.privileges && !Array.isArray(feature.privileges)) ||
feature.privileges === null
);
}
function buildCapabilities(...allFeatureCapabilities: FeatureCapabilities[]): UICapabilities { function buildCapabilities(...allFeatureCapabilities: FeatureCapabilities[]): UICapabilities {
return allFeatureCapabilities.reduce<UICapabilities>((acc, capabilities) => { return allFeatureCapabilities.reduce<UICapabilities>((acc, capabilities) => {
const mergableCapabilities = _.omit(capabilities, ...ELIGIBLE_FLAT_MERGE_KEYS); const mergableCapabilities = _.omit(capabilities, ...ELIGIBLE_FLAT_MERGE_KEYS);
@ -81,6 +128,14 @@ function buildCapabilities(...allFeatureCapabilities: FeatureCapabilities[]): UI
}; };
}); });
ELIGIBLE_DEEP_MERGE_KEYS.forEach((key) => {
mergedFeatureCapabilities[key] = _.merge(
{},
mergedFeatureCapabilities[key],
capabilities[key]
);
});
return mergedFeatureCapabilities; return mergedFeatureCapabilities;
}, {} as UICapabilities); }, {} as UICapabilities);
} }

View file

@ -41,7 +41,7 @@ export class GraphPlugin implements Plugin {
} }
if (features) { if (features) {
features.registerFeature({ features.registerKibanaFeature({
id: 'graph', id: 'graph',
name: i18n.translate('xpack.graph.featureRegistry.graphFeatureName', { name: i18n.translate('xpack.graph.featureRegistry.graphFeatureName', {
defaultMessage: 'Graph', defaultMessage: 'Graph',

View file

@ -5,7 +5,8 @@
"ui": true, "ui": true,
"requiredPlugins": [ "requiredPlugins": [
"licensing", "licensing",
"management" "management",
"features"
], ],
"optionalPlugins": [ "optionalPlugins": [
"usageCollection", "usageCollection",

View file

@ -60,7 +60,10 @@ export class IndexLifecycleManagementServerPlugin implements Plugin<void, void,
this.license = new License(); this.license = new License();
} }
async setup({ http }: CoreSetup, { licensing, indexManagement }: Dependencies): Promise<void> { async setup(
{ http }: CoreSetup,
{ licensing, indexManagement, features }: Dependencies
): Promise<void> {
const router = http.createRouter(); const router = http.createRouter();
const config = await this.config$.pipe(first()).toPromise(); const config = await this.config$.pipe(first()).toPromise();
@ -78,6 +81,19 @@ export class IndexLifecycleManagementServerPlugin implements Plugin<void, void,
} }
); );
features.registerElasticsearchFeature({
id: 'index_lifecycle_management',
management: {
data: ['index_lifecycle_management'],
},
privileges: [
{
requiredClusterPrivileges: ['manage_ilm'],
ui: [],
},
],
});
registerApiRoutes({ registerApiRoutes({
router, router,
config, config,

View file

@ -6,6 +6,7 @@
import { IRouter } from 'src/core/server'; import { IRouter } from 'src/core/server';
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
import { LicensingPluginSetup } from '../../licensing/server'; import { LicensingPluginSetup } from '../../licensing/server';
import { IndexManagementPluginSetup } from '../../index_management/server'; import { IndexManagementPluginSetup } from '../../index_management/server';
import { License } from './services'; import { License } from './services';
@ -14,6 +15,7 @@ import { isEsError } from './shared_imports';
export interface Dependencies { export interface Dependencies {
licensing: LicensingPluginSetup; licensing: LicensingPluginSetup;
features: FeaturesPluginSetup;
indexManagement?: IndexManagementPluginSetup; indexManagement?: IndexManagementPluginSetup;
} }

View file

@ -6,7 +6,8 @@
"requiredPlugins": [ "requiredPlugins": [
"home", "home",
"licensing", "licensing",
"management" "management",
"features"
], ],
"optionalPlugins": [ "optionalPlugins": [
"security", "security",

View file

@ -59,7 +59,7 @@ export class IndexMgmtServerPlugin implements Plugin<IndexManagementPluginSetup,
setup( setup(
{ http, getStartServices }: CoreSetup, { http, getStartServices }: CoreSetup,
{ licensing, security }: Dependencies { features, licensing, security }: Dependencies
): IndexManagementPluginSetup { ): IndexManagementPluginSetup {
const router = http.createRouter(); const router = http.createRouter();
@ -77,6 +77,19 @@ export class IndexMgmtServerPlugin implements Plugin<IndexManagementPluginSetup,
} }
); );
features.registerElasticsearchFeature({
id: PLUGIN.id,
management: {
data: ['index_management'],
},
privileges: [
{
requiredClusterPrivileges: ['monitor', 'manage_index_templates'],
ui: [],
},
],
});
http.registerRouteHandlerContext('dataManagement', async (ctx, request) => { http.registerRouteHandlerContext('dataManagement', async (ctx, request) => {
this.dataManagementESClient = this.dataManagementESClient =
this.dataManagementESClient ?? (await getCustomEsClient(getStartServices)); this.dataManagementESClient ?? (await getCustomEsClient(getStartServices));

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import { LegacyScopedClusterClient, IRouter } from 'src/core/server'; import { LegacyScopedClusterClient, IRouter } from 'src/core/server';
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
import { LicensingPluginSetup } from '../../licensing/server'; import { LicensingPluginSetup } from '../../licensing/server';
import { SecurityPluginSetup } from '../../security/server'; import { SecurityPluginSetup } from '../../security/server';
import { License, IndexDataEnricher } from './services'; import { License, IndexDataEnricher } from './services';
@ -12,6 +13,7 @@ import { isEsError } from './shared_imports';
export interface Dependencies { export interface Dependencies {
security: SecurityPluginSetup; security: SecurityPluginSetup;
licensing: LicensingPluginSetup; licensing: LicensingPluginSetup;
features: FeaturesPluginSetup;
} }
export interface RouteDependencies { export interface RouteDependencies {

View file

@ -132,8 +132,8 @@ export class InfraServerPlugin {
...domainLibs, ...domainLibs,
}; };
plugins.features.registerFeature(METRICS_FEATURE); plugins.features.registerKibanaFeature(METRICS_FEATURE);
plugins.features.registerFeature(LOGS_FEATURE); plugins.features.registerKibanaFeature(LOGS_FEATURE);
plugins.home.sampleData.addAppLinksToSampleDataset('logs', [ plugins.home.sampleData.addAppLinksToSampleDataset('logs', [
{ {

View file

@ -173,7 +173,7 @@ export class IngestManagerPlugin
// Register feature // Register feature
// TODO: Flesh out privileges // TODO: Flesh out privileges
if (deps.features) { if (deps.features) {
deps.features.registerFeature({ deps.features.registerKibanaFeature({
id: PLUGIN_ID, id: PLUGIN_ID,
name: 'Ingest Manager', name: 'Ingest Manager',
icon: 'savedObjectsApp', icon: 'savedObjectsApp',

View file

@ -3,7 +3,7 @@
"version": "kibana", "version": "kibana",
"server": true, "server": true,
"ui": true, "ui": true,
"requiredPlugins": ["licensing", "management"], "requiredPlugins": ["licensing", "management", "features"],
"optionalPlugins": ["security", "usageCollection"], "optionalPlugins": ["security", "usageCollection"],
"configPath": ["xpack", "ingest_pipelines"], "configPath": ["xpack", "ingest_pipelines"],
"requiredBundles": ["esUiShared", "kibanaReact"] "requiredBundles": ["esUiShared", "kibanaReact"]

View file

@ -25,7 +25,7 @@ export class IngestPipelinesPlugin implements Plugin<void, void, any, any> {
this.apiRoutes = new ApiRoutes(); this.apiRoutes = new ApiRoutes();
} }
public setup({ http }: CoreSetup, { licensing, security }: Dependencies) { public setup({ http }: CoreSetup, { licensing, security, features }: Dependencies) {
this.logger.debug('ingest_pipelines: setup'); this.logger.debug('ingest_pipelines: setup');
const router = http.createRouter(); const router = http.createRouter();
@ -44,6 +44,19 @@ export class IngestPipelinesPlugin implements Plugin<void, void, any, any> {
} }
); );
features.registerElasticsearchFeature({
id: 'ingest_pipelines',
management: {
ingest: ['ingest_pipelines'],
},
privileges: [
{
ui: [],
requiredClusterPrivileges: ['manage_pipeline', 'cluster:monitor/nodes/info'],
},
],
});
this.apiRoutes.setup({ this.apiRoutes.setup({
router, router,
license: this.license, license: this.license,

View file

@ -7,11 +7,13 @@
import { IRouter } from 'src/core/server'; import { IRouter } from 'src/core/server';
import { LicensingPluginSetup } from '../../licensing/server'; import { LicensingPluginSetup } from '../../licensing/server';
import { SecurityPluginSetup } from '../../security/server'; import { SecurityPluginSetup } from '../../security/server';
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
import { License } from './services'; import { License } from './services';
import { isEsError } from './shared_imports'; import { isEsError } from './shared_imports';
export interface Dependencies { export interface Dependencies {
security: SecurityPluginSetup; security: SecurityPluginSetup;
features: FeaturesPluginSetup;
licensing: LicensingPluginSetup; licensing: LicensingPluginSetup;
} }

View file

@ -3,7 +3,7 @@
"version": "kibana", "version": "kibana",
"server": true, "server": true,
"ui": true, "ui": true,
"requiredPlugins": ["home", "licensing", "management"], "requiredPlugins": ["home", "licensing", "management", "features"],
"optionalPlugins": ["telemetry"], "optionalPlugins": ["telemetry"],
"configPath": ["xpack", "license_management"], "configPath": ["xpack", "license_management"],
"extraPublicDirs": ["common/constants"], "extraPublicDirs": ["common/constants"],

View file

@ -13,9 +13,22 @@ import { Dependencies } from './types';
export class LicenseManagementServerPlugin implements Plugin<void, void, any, any> { export class LicenseManagementServerPlugin implements Plugin<void, void, any, any> {
private readonly apiRoutes = new ApiRoutes(); private readonly apiRoutes = new ApiRoutes();
setup({ http }: CoreSetup, { licensing, security }: Dependencies) { setup({ http }: CoreSetup, { licensing, features, security }: Dependencies) {
const router = http.createRouter(); const router = http.createRouter();
features.registerElasticsearchFeature({
id: 'license_management',
management: {
stack: ['license_management'],
},
privileges: [
{
requiredClusterPrivileges: ['manage'],
ui: [],
},
],
});
this.apiRoutes.setup({ this.apiRoutes.setup({
router, router,
plugins: { plugins: {

View file

@ -5,12 +5,14 @@
*/ */
import { LegacyScopedClusterClient, IRouter } from 'kibana/server'; import { LegacyScopedClusterClient, IRouter } from 'kibana/server';
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
import { LicensingPluginSetup } from '../../licensing/server'; import { LicensingPluginSetup } from '../../licensing/server';
import { SecurityPluginSetup } from '../../security/server'; import { SecurityPluginSetup } from '../../security/server';
import { isEsError } from './shared_imports'; import { isEsError } from './shared_imports';
export interface Dependencies { export interface Dependencies {
licensing: LicensingPluginSetup; licensing: LicensingPluginSetup;
features: FeaturesPluginSetup;
security?: SecurityPluginSetup; security?: SecurityPluginSetup;
} }

View file

@ -5,7 +5,8 @@
"configPath": ["xpack", "logstash"], "configPath": ["xpack", "logstash"],
"requiredPlugins": [ "requiredPlugins": [
"licensing", "licensing",
"management" "management",
"features"
], ],
"optionalPlugins": [ "optionalPlugins": [
"home", "home",

View file

@ -12,6 +12,7 @@ import {
PluginInitializerContext, PluginInitializerContext,
} from 'src/core/server'; } from 'src/core/server';
import { LicensingPluginSetup } from '../../licensing/server'; import { LicensingPluginSetup } from '../../licensing/server';
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
import { SecurityPluginSetup } from '../../security/server'; import { SecurityPluginSetup } from '../../security/server';
import { registerRoutes } from './routes'; import { registerRoutes } from './routes';
@ -19,6 +20,7 @@ import { registerRoutes } from './routes';
interface SetupDeps { interface SetupDeps {
licensing: LicensingPluginSetup; licensing: LicensingPluginSetup;
security?: SecurityPluginSetup; security?: SecurityPluginSetup;
features: FeaturesPluginSetup;
} }
export class LogstashPlugin implements Plugin { export class LogstashPlugin implements Plugin {
@ -34,6 +36,22 @@ export class LogstashPlugin implements Plugin {
this.coreSetup = core; this.coreSetup = core;
registerRoutes(core.http.createRouter(), deps.security); registerRoutes(core.http.createRouter(), deps.security);
deps.features.registerElasticsearchFeature({
id: 'pipelines',
management: {
ingest: ['pipelines'],
},
privileges: [
{
requiredClusterPrivileges: [],
requiredIndexPrivileges: {
['.logstash']: ['read'],
},
ui: [],
},
],
});
} }
start(core: CoreStart) { start(core: CoreStart) {

View file

@ -163,7 +163,7 @@ export class MapsPlugin implements Plugin {
this._initHomeData(home, core.http.basePath.prepend, mapsLegacyConfig); this._initHomeData(home, core.http.basePath.prepend, mapsLegacyConfig);
features.registerFeature({ features.registerKibanaFeature({
id: APP_ID, id: APP_ID,
name: i18n.translate('xpack.maps.featureRegistry.mapsFeatureName', { name: i18n.translate('xpack.maps.featureRegistry.mapsFeatureName', {
defaultMessage: 'Maps', defaultMessage: 'Maps',

View file

@ -102,6 +102,7 @@ export function getPluginPrivileges() {
...privilege, ...privilege,
api: userMlCapabilitiesKeys.map((k) => `ml:${k}`), api: userMlCapabilitiesKeys.map((k) => `ml:${k}`),
catalogue: [PLUGIN_ID], catalogue: [PLUGIN_ID],
management: { insightsAndAlerting: [] },
ui: userMlCapabilitiesKeys, ui: userMlCapabilitiesKeys,
savedObject: { savedObject: {
all: [], all: [],

View file

@ -23,7 +23,7 @@ export function registerManagementSection(
core: CoreSetup<MlStartDependencies> core: CoreSetup<MlStartDependencies>
) { ) {
if (management !== undefined) { if (management !== undefined) {
management.sections.section.insightsAndAlerting.registerApp({ return management.sections.section.insightsAndAlerting.registerApp({
id: 'jobsListLink', id: 'jobsListLink',
title: i18n.translate('xpack.ml.management.jobsListTitle', { title: i18n.translate('xpack.ml.management.jobsListTitle', {
defaultMessage: 'Machine Learning Jobs', defaultMessage: 'Machine Learning Jobs',

View file

@ -101,6 +101,8 @@ export class MlPlugin implements Plugin<MlPluginSetup, MlPluginStart> {
}, },
}); });
const managementApp = registerManagementSection(pluginsSetup.management, core);
const licensing = pluginsSetup.licensing.license$.pipe(take(1)); const licensing = pluginsSetup.licensing.license$.pipe(take(1));
licensing.subscribe(async (license) => { licensing.subscribe(async (license) => {
const [coreStart] = await core.getStartServices(); const [coreStart] = await core.getStartServices();
@ -110,26 +112,35 @@ export class MlPlugin implements Plugin<MlPluginSetup, MlPluginStart> {
registerFeature(pluginsSetup.home); registerFeature(pluginsSetup.home);
} }
const { capabilities } = coreStart.application;
// register ML for the index pattern management no data screen. // register ML for the index pattern management no data screen.
pluginsSetup.indexPatternManagement.environment.update({ pluginsSetup.indexPatternManagement.environment.update({
ml: () => ml: () =>
coreStart.application.capabilities.ml.canFindFileStructure capabilities.ml.canFindFileStructure ? MlCardState.ENABLED : MlCardState.HIDDEN,
? MlCardState.ENABLED
: MlCardState.HIDDEN,
}); });
const canManageMLJobs = capabilities.management?.insightsAndAlerting?.jobsListLink ?? false;
// register various ML plugin features which require a full license // register various ML plugin features which require a full license
if (isFullLicense(license)) { if (isFullLicense(license)) {
registerManagementSection(pluginsSetup.management, core); if (canManageMLJobs && managementApp) {
managementApp.enable();
}
registerEmbeddables(pluginsSetup.embeddable, core); registerEmbeddables(pluginsSetup.embeddable, core);
registerMlUiActions(pluginsSetup.uiActions, core); registerMlUiActions(pluginsSetup.uiActions, core);
registerUrlGenerator(pluginsSetup.share, core); registerUrlGenerator(pluginsSetup.share, core);
} else if (managementApp) {
managementApp.disable();
} }
} else { } else {
// if ml is disabled in elasticsearch, disable ML in kibana // if ml is disabled in elasticsearch, disable ML in kibana
this.appUpdater.next(() => ({ this.appUpdater.next(() => ({
status: AppStatus.inaccessible, status: AppStatus.inaccessible,
})); }));
if (managementApp) {
managementApp.disable();
}
} }
}); });

View file

@ -67,7 +67,7 @@ export class MlServerPlugin implements Plugin<MlPluginSetup, MlPluginStart, Plug
public setup(coreSetup: CoreSetup, plugins: PluginsSetup): MlPluginSetup { public setup(coreSetup: CoreSetup, plugins: PluginsSetup): MlPluginSetup {
const { admin, user, apmUser } = getPluginPrivileges(); const { admin, user, apmUser } = getPluginPrivileges();
plugins.features.registerFeature({ plugins.features.registerKibanaFeature({
id: PLUGIN_ID, id: PLUGIN_ID,
name: i18n.translate('xpack.ml.featureRegistry.mlFeatureName', { name: i18n.translate('xpack.ml.featureRegistry.mlFeatureName', {
defaultMessage: 'Machine Learning', defaultMessage: 'Machine Learning',

View file

@ -242,7 +242,7 @@ export class Plugin {
} }
registerPluginInUI(plugins: PluginsSetup) { registerPluginInUI(plugins: PluginsSetup) {
plugins.features.registerFeature({ plugins.features.registerKibanaFeature({
id: 'monitoring', id: 'monitoring',
name: i18n.translate('xpack.monitoring.featureRegistry.monitoringFeatureName', { name: i18n.translate('xpack.monitoring.featureRegistry.monitoringFeatureName', {
defaultMessage: 'Stack Monitoring', defaultMessage: 'Stack Monitoring',

View file

@ -8,7 +8,8 @@
"requiredPlugins": [ "requiredPlugins": [
"licensing", "licensing",
"management", "management",
"indexManagement" "indexManagement",
"features"
], ],
"optionalPlugins": [ "optionalPlugins": [
"usageCollection", "usageCollection",

View file

@ -35,7 +35,7 @@ export class RemoteClustersServerPlugin
this.licenseStatus = { valid: false }; this.licenseStatus = { valid: false };
} }
async setup({ http }: CoreSetup, { licensing, cloud }: Dependencies) { async setup({ http }: CoreSetup, { features, licensing, cloud }: Dependencies) {
const router = http.createRouter(); const router = http.createRouter();
const config = await this.config$.pipe(first()).toPromise(); const config = await this.config$.pipe(first()).toPromise();
@ -47,6 +47,19 @@ export class RemoteClustersServerPlugin
}, },
}; };
features.registerElasticsearchFeature({
id: 'remote_clusters',
management: {
data: ['remote_clusters'],
},
privileges: [
{
requiredClusterPrivileges: ['manage'],
ui: [],
},
],
});
// Register routes // Register routes
registerGetRoute(routeDependencies); registerGetRoute(routeDependencies);
registerAddRoute(routeDependencies); registerAddRoute(routeDependencies);

View file

@ -5,12 +5,14 @@
*/ */
import { IRouter } from 'kibana/server'; import { IRouter } from 'kibana/server';
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
import { LicensingPluginSetup } from '../../licensing/server'; import { LicensingPluginSetup } from '../../licensing/server';
import { CloudSetup } from '../../cloud/server'; import { CloudSetup } from '../../cloud/server';
export interface Dependencies { export interface Dependencies {
licensing: LicensingPluginSetup; licensing: LicensingPluginSetup;
cloud: CloudSetup; cloud: CloudSetup;
features: FeaturesPluginSetup;
} }
export interface RouteDependencies { export interface RouteDependencies {

View file

@ -14,7 +14,8 @@
"licensing", "licensing",
"uiActions", "uiActions",
"embeddable", "embeddable",
"share" "share",
"features"
], ],
"server": true, "server": true,
"ui": true, "ui": true,

View file

@ -15,6 +15,7 @@ import {
SavedObjectsServiceStart, SavedObjectsServiceStart,
UiSettingsServiceStart, UiSettingsServiceStart,
} from 'src/core/server'; } from 'src/core/server';
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
import { LicensingPluginSetup } from '../../licensing/server'; import { LicensingPluginSetup } from '../../licensing/server';
import { SecurityPluginSetup } from '../../security/server'; import { SecurityPluginSetup } from '../../security/server';
import { ReportingConfig } from './'; import { ReportingConfig } from './';
@ -25,6 +26,7 @@ import { screenshotsObservableFactory, ScreenshotsObservableFn } from './lib/scr
import { ReportingStore } from './lib/store'; import { ReportingStore } from './lib/store';
export interface ReportingInternalSetup { export interface ReportingInternalSetup {
features: FeaturesPluginSetup;
elasticsearch: ElasticsearchServiceSetup; elasticsearch: ElasticsearchServiceSetup;
licensing: LicensingPluginSetup; licensing: LicensingPluginSetup;
basePath: BasePath['get']; basePath: BasePath['get'];
@ -99,6 +101,26 @@ export class ReportingCore {
this.pluginSetup$.next(true); this.pluginSetup$.next(true);
} }
/**
* Registers reporting as an Elasticsearch feature for the purpose of toggling visibility based on roles.
*/
public registerFeature() {
const config = this.getConfig();
const allowedRoles = ['superuser', ...(config.get('roles')?.allow ?? [])];
this.getPluginSetupDeps().features.registerElasticsearchFeature({
id: 'reporting',
catalogue: ['reporting'],
management: {
insightsAndAlerting: ['reporting'],
},
privileges: allowedRoles.map((role) => ({
requiredClusterPrivileges: [],
requiredRoles: [role],
ui: [],
})),
});
}
/* /*
* Gives synchronous access to the config * Gives synchronous access to the config
*/ */

View file

@ -17,6 +17,7 @@ jest.mock('./browsers/install', () => ({
import { coreMock } from 'src/core/server/mocks'; import { coreMock } from 'src/core/server/mocks';
import { ReportingPlugin } from './plugin'; import { ReportingPlugin } from './plugin';
import { createMockConfigSchema } from './test_helpers'; import { createMockConfigSchema } from './test_helpers';
import { featuresPluginMock } from '../../features/server/mocks';
const sleep = (time: number) => new Promise((r) => setTimeout(r, time)); const sleep = (time: number) => new Promise((r) => setTimeout(r, time));
@ -35,6 +36,7 @@ describe('Reporting Plugin', () => {
coreStart = await coreMock.createStart(); coreStart = await coreMock.createStart();
pluginSetup = ({ pluginSetup = ({
licensing: {}, licensing: {},
features: featuresPluginMock.createSetup(),
usageCollection: { usageCollection: {
makeUsageCollector: jest.fn(), makeUsageCollector: jest.fn(),
registerCollector: jest.fn(), registerCollector: jest.fn(),

View file

@ -70,13 +70,14 @@ export class ReportingPlugin
}); });
const { elasticsearch, http } = core; const { elasticsearch, http } = core;
const { licensing, security } = plugins; const { features, licensing, security } = plugins;
const { initializerContext: initContext, reportingCore } = this; const { initializerContext: initContext, reportingCore } = this;
const router = http.createRouter(); const router = http.createRouter();
const basePath = http.basePath.get; const basePath = http.basePath.get;
reportingCore.pluginSetup({ reportingCore.pluginSetup({
features,
elasticsearch, elasticsearch,
licensing, licensing,
basePath, basePath,
@ -91,6 +92,8 @@ export class ReportingPlugin
(async () => { (async () => {
const config = await buildConfig(initContext, core, this.logger); const config = await buildConfig(initContext, core, this.logger);
reportingCore.setConfig(config); reportingCore.setConfig(config);
// Feature registration relies on config, so it cannot be setup before here.
reportingCore.registerFeature();
this.logger.debug('Setup complete'); this.logger.debug('Setup complete');
})().catch((e) => { })().catch((e) => {
this.logger.error(`Error in Reporting setup, reporting may not function properly`); this.logger.error(`Error in Reporting setup, reporting may not function properly`);

View file

@ -10,6 +10,7 @@ jest.mock('../browsers');
jest.mock('../lib/create_queue'); jest.mock('../lib/create_queue');
import * as Rx from 'rxjs'; import * as Rx from 'rxjs';
import { featuresPluginMock } from '../../../features/server/mocks';
import { ReportingConfig, ReportingCore } from '../'; import { ReportingConfig, ReportingCore } from '../';
import { import {
chromium, chromium,
@ -32,6 +33,7 @@ const createMockPluginSetup = (
setupMock?: any setupMock?: any
): ReportingInternalSetup => { ): ReportingInternalSetup => {
return { return {
features: featuresPluginMock.createSetup(),
elasticsearch: setupMock.elasticsearch || { legacy: { client: {} } }, elasticsearch: setupMock.elasticsearch || { legacy: { client: {} } },
basePath: setupMock.basePath || '/all-about-that-basepath', basePath: setupMock.basePath || '/all-about-that-basepath',
router: setupMock.router, router: setupMock.router,

View file

@ -9,6 +9,7 @@ import { KibanaRequest, RequestHandlerContext } from 'src/core/server';
import { DataPluginStart } from 'src/plugins/data/server/plugin'; import { DataPluginStart } from 'src/plugins/data/server/plugin';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { CancellationToken } from '../../../plugins/reporting/common'; import { CancellationToken } from '../../../plugins/reporting/common';
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
import { LicensingPluginSetup } from '../../licensing/server'; import { LicensingPluginSetup } from '../../licensing/server';
import { AuthenticatedUser, SecurityPluginSetup } from '../../security/server'; import { AuthenticatedUser, SecurityPluginSetup } from '../../security/server';
import { JobStatus } from '../common/types'; import { JobStatus } from '../common/types';
@ -92,6 +93,7 @@ export interface ConditionalHeaders {
export interface ReportingSetupDeps { export interface ReportingSetupDeps {
licensing: LicensingPluginSetup; licensing: LicensingPluginSetup;
features: FeaturesPluginSetup;
security?: SecurityPluginSetup; security?: SecurityPluginSetup;
usageCollection?: UsageCollectionSetup; usageCollection?: UsageCollectionSetup;
} }

View file

@ -7,7 +7,8 @@
"requiredPlugins": [ "requiredPlugins": [
"indexPatternManagement", "indexPatternManagement",
"management", "management",
"licensing" "licensing",
"features"
], ],
"optionalPlugins": [ "optionalPlugins": [
"home", "home",

View file

@ -64,7 +64,7 @@ export class RollupPlugin implements Plugin<void, void, any, any> {
public setup( public setup(
{ http, uiSettings, getStartServices }: CoreSetup, { http, uiSettings, getStartServices }: CoreSetup,
{ licensing, indexManagement, visTypeTimeseries, usageCollection }: Dependencies { features, licensing, indexManagement, visTypeTimeseries, usageCollection }: Dependencies
) { ) {
this.license.setup( this.license.setup(
{ {
@ -80,6 +80,20 @@ export class RollupPlugin implements Plugin<void, void, any, any> {
} }
); );
features.registerElasticsearchFeature({
id: 'rollup_jobs',
management: {
data: ['rollup_jobs'],
},
catalogue: ['rollup_jobs'],
privileges: [
{
requiredClusterPrivileges: ['manage_rollup'],
ui: [],
},
],
});
http.registerRouteHandlerContext('rollup', async (context, request) => { http.registerRouteHandlerContext('rollup', async (context, request) => {
this.rollupEsClient = this.rollupEsClient ?? (await getCustomEsClient(getStartServices)); this.rollupEsClient = this.rollupEsClient ?? (await getCustomEsClient(getStartServices));
return { return {

View file

@ -9,6 +9,7 @@ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { VisTypeTimeseriesSetup } from 'src/plugins/vis_type_timeseries/server'; import { VisTypeTimeseriesSetup } from 'src/plugins/vis_type_timeseries/server';
import { IndexManagementPluginSetup } from '../../index_management/server'; import { IndexManagementPluginSetup } from '../../index_management/server';
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
import { LicensingPluginSetup } from '../../licensing/server'; import { LicensingPluginSetup } from '../../licensing/server';
import { License } from './services'; import { License } from './services';
import { IndexPatternsFetcher } from './shared_imports'; import { IndexPatternsFetcher } from './shared_imports';
@ -22,6 +23,7 @@ export interface Dependencies {
visTypeTimeseries?: VisTypeTimeseriesSetup; visTypeTimeseries?: VisTypeTimeseriesSetup;
usageCollection?: UsageCollectionSetup; usageCollection?: UsageCollectionSetup;
licensing: LicensingPluginSetup; licensing: LicensingPluginSetup;
features: FeaturesPluginSetup;
} }
export interface RouteDependencies { export interface RouteDependencies {

View file

@ -78,7 +78,10 @@ describe('ManagementService', () => {
}); });
describe('start()', () => { describe('start()', () => {
function startService(initialFeatures: Partial<SecurityLicenseFeatures>) { function startService(
initialFeatures: Partial<SecurityLicenseFeatures>,
canManageSecurity: boolean = true
) {
const { fatalErrors, getStartServices } = coreMock.createSetup(); const { fatalErrors, getStartServices } = coreMock.createSetup();
const licenseSubject = new BehaviorSubject<SecurityLicenseFeatures>( const licenseSubject = new BehaviorSubject<SecurityLicenseFeatures>(
@ -106,10 +109,11 @@ describe('ManagementService', () => {
management: managementSetup, management: managementSetup,
}); });
const getMockedApp = () => { const getMockedApp = (id: string) => {
// All apps are enabled by default. // All apps are enabled by default.
let enabled = true; let enabled = true;
return ({ return ({
id,
get enabled() { get enabled() {
return enabled; return enabled;
}, },
@ -123,13 +127,26 @@ describe('ManagementService', () => {
}; };
mockSection.getApp = jest.fn().mockImplementation((id) => mockApps.get(id)); mockSection.getApp = jest.fn().mockImplementation((id) => mockApps.get(id));
const mockApps = new Map<string, jest.Mocked<ManagementApp>>([ const mockApps = new Map<string, jest.Mocked<ManagementApp>>([
[usersManagementApp.id, getMockedApp()], [usersManagementApp.id, getMockedApp(usersManagementApp.id)],
[rolesManagementApp.id, getMockedApp()], [rolesManagementApp.id, getMockedApp(rolesManagementApp.id)],
[apiKeysManagementApp.id, getMockedApp()], [apiKeysManagementApp.id, getMockedApp(apiKeysManagementApp.id)],
[roleMappingsManagementApp.id, getMockedApp()], [roleMappingsManagementApp.id, getMockedApp(roleMappingsManagementApp.id)],
] as Array<[string, jest.Mocked<ManagementApp>]>); ] as Array<[string, jest.Mocked<ManagementApp>]>);
service.start(); service.start({
capabilities: {
management: {
security: {
users: canManageSecurity,
roles: canManageSecurity,
role_mappings: canManageSecurity,
api_keys: canManageSecurity,
},
},
navLinks: {},
catalogue: {},
},
});
return { return {
mockApps, mockApps,
@ -178,6 +195,19 @@ describe('ManagementService', () => {
} }
}); });
it('apps are disabled if capabilities are false', () => {
const { mockApps } = startService(
{
showLinks: true,
showRoleMappingsManagement: true,
},
false
);
for (const [, mockApp] of mockApps) {
expect(mockApp.enabled).toBe(false);
}
});
it('role mappings app is disabled if `showRoleMappingsManagement` changes after `start`', () => { it('role mappings app is disabled if `showRoleMappingsManagement` changes after `start`', () => {
const { mockApps, updateFeatures } = startService({ const { mockApps, updateFeatures } = startService({
showLinks: true, showLinks: true,

View file

@ -5,7 +5,7 @@
*/ */
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { StartServicesAccessor, FatalErrorsSetup } from 'src/core/public'; import { StartServicesAccessor, FatalErrorsSetup, Capabilities } from 'src/core/public';
import { import {
ManagementApp, ManagementApp,
ManagementSetup, ManagementSetup,
@ -27,6 +27,10 @@ interface SetupParams {
getStartServices: StartServicesAccessor<PluginStartDependencies>; getStartServices: StartServicesAccessor<PluginStartDependencies>;
} }
interface StartParams {
capabilities: Capabilities;
}
export class ManagementService { export class ManagementService {
private license!: SecurityLicense; private license!: SecurityLicense;
private licenseFeaturesSubscription?: Subscription; private licenseFeaturesSubscription?: Subscription;
@ -44,7 +48,7 @@ export class ManagementService {
this.securitySection.registerApp(roleMappingsManagementApp.create({ getStartServices })); this.securitySection.registerApp(roleMappingsManagementApp.create({ getStartServices }));
} }
start() { start({ capabilities }: StartParams) {
this.licenseFeaturesSubscription = this.license.features$.subscribe(async (features) => { this.licenseFeaturesSubscription = this.license.features$.subscribe(async (features) => {
const securitySection = this.securitySection!; const securitySection = this.securitySection!;
@ -61,6 +65,11 @@ export class ManagementService {
// Iterate over all registered apps and update their enable status depending on the available // Iterate over all registered apps and update their enable status depending on the available
// license features. // license features.
for (const [app, enableStatus] of securityManagementAppsStatuses) { for (const [app, enableStatus] of securityManagementAppsStatuses) {
if (capabilities.management.security[app.id] !== true) {
app.disable();
continue;
}
if (app.enabled === enableStatus) { if (app.enabled === enableStatus) {
continue; continue;
} }

View file

@ -4,17 +4,20 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import { Feature, FeatureConfig } from '../../../../../features/public'; import { KibanaFeature, KibanaFeatureConfig } from '../../../../../features/public';
export const createFeature = ( export const createFeature = (
config: Pick<FeatureConfig, 'id' | 'name' | 'subFeatures' | 'reserved' | 'privilegesTooltip'> & { config: Pick<
KibanaFeatureConfig,
'id' | 'name' | 'subFeatures' | 'reserved' | 'privilegesTooltip'
> & {
excludeFromBaseAll?: boolean; excludeFromBaseAll?: boolean;
excludeFromBaseRead?: boolean; excludeFromBaseRead?: boolean;
privileges?: FeatureConfig['privileges']; privileges?: KibanaFeatureConfig['privileges'];
} }
) => { ) => {
const { excludeFromBaseAll, excludeFromBaseRead, privileges, ...rest } = config; const { excludeFromBaseAll, excludeFromBaseRead, privileges, ...rest } = config;
return new Feature({ return new KibanaFeature({
icon: 'discoverApp', icon: 'discoverApp',
navLinkId: 'discover', navLinkId: 'discover',
app: [], app: [],

View file

@ -7,7 +7,7 @@
import { Actions } from '../../../../server/authorization'; import { Actions } from '../../../../server/authorization';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths // eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { privilegesFactory } from '../../../../server/authorization/privileges'; import { privilegesFactory } from '../../../../server/authorization/privileges';
import { Feature } from '../../../../../features/public'; import { KibanaFeature } from '../../../../../features/public';
import { KibanaPrivileges } from '../model'; import { KibanaPrivileges } from '../model';
import { SecurityLicenseFeatures } from '../../..'; import { SecurityLicenseFeatures } from '../../..';
@ -15,11 +15,11 @@ import { SecurityLicenseFeatures } from '../../..';
import { featuresPluginMock } from '../../../../../features/server/mocks'; import { featuresPluginMock } from '../../../../../features/server/mocks';
export const createRawKibanaPrivileges = ( export const createRawKibanaPrivileges = (
features: Feature[], features: KibanaFeature[],
{ allowSubFeaturePrivileges = true } = {} { allowSubFeaturePrivileges = true } = {}
) => { ) => {
const featuresService = featuresPluginMock.createSetup(); const featuresService = featuresPluginMock.createSetup();
featuresService.getFeatures.mockReturnValue(features); featuresService.getKibanaFeatures.mockReturnValue(features);
const licensingService = { const licensingService = {
getFeatures: () => ({ allowSubFeaturePrivileges } as SecurityLicenseFeatures), getFeatures: () => ({ allowSubFeaturePrivileges } as SecurityLicenseFeatures),
@ -33,7 +33,7 @@ export const createRawKibanaPrivileges = (
}; };
export const createKibanaPrivileges = ( export const createKibanaPrivileges = (
features: Feature[], features: KibanaFeature[],
{ allowSubFeaturePrivileges = true } = {} { allowSubFeaturePrivileges = true } = {}
) => { ) => {
return new KibanaPrivileges( return new KibanaPrivileges(

View file

@ -9,7 +9,7 @@ import React from 'react';
import { act } from '@testing-library/react'; import { act } from '@testing-library/react';
import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers';
import { Capabilities } from 'src/core/public'; import { Capabilities } from 'src/core/public';
import { Feature } from '../../../../../features/public'; import { KibanaFeature } from '../../../../../features/public';
import { Role } from '../../../../common/model'; import { Role } from '../../../../common/model';
import { DocumentationLinksService } from '../documentation_links'; import { DocumentationLinksService } from '../documentation_links';
import { EditRolePage } from './edit_role_page'; import { EditRolePage } from './edit_role_page';
@ -27,7 +27,7 @@ import { createRawKibanaPrivileges } from '../__fixtures__/kibana_privileges';
const buildFeatures = () => { const buildFeatures = () => {
return [ return [
new Feature({ new KibanaFeature({
id: 'feature1', id: 'feature1',
name: 'Feature 1', name: 'Feature 1',
icon: 'addDataApp', icon: 'addDataApp',
@ -51,7 +51,7 @@ const buildFeatures = () => {
}, },
}, },
}), }),
new Feature({ new KibanaFeature({
id: 'feature2', id: 'feature2',
name: 'Feature 2', name: 'Feature 2',
icon: 'addDataApp', icon: 'addDataApp',
@ -75,7 +75,7 @@ const buildFeatures = () => {
}, },
}, },
}), }),
] as Feature[]; ] as KibanaFeature[];
}; };
const buildBuiltinESPrivileges = () => { const buildBuiltinESPrivileges = () => {

View file

@ -40,7 +40,7 @@ import {
} from 'src/core/public'; } from 'src/core/public';
import { ScopedHistory } from 'kibana/public'; import { ScopedHistory } from 'kibana/public';
import { FeaturesPluginStart } from '../../../../../features/public'; import { FeaturesPluginStart } from '../../../../../features/public';
import { Feature } from '../../../../../features/common'; import { KibanaFeature } from '../../../../../features/common';
import { IndexPatternsContract } from '../../../../../../../src/plugins/data/public'; import { IndexPatternsContract } from '../../../../../../../src/plugins/data/public';
import { Space } from '../../../../../spaces/public'; import { Space } from '../../../../../spaces/public';
import { import {
@ -247,7 +247,7 @@ function useFeatures(
getFeatures: FeaturesPluginStart['getFeatures'], getFeatures: FeaturesPluginStart['getFeatures'],
fatalErrors: FatalErrorsSetup fatalErrors: FatalErrorsSetup
) { ) {
const [features, setFeatures] = useState<Feature[] | null>(null); const [features, setFeatures] = useState<KibanaFeature[] | null>(null);
useEffect(() => { useEffect(() => {
getFeatures() getFeatures()
.catch((err: IHttpFetchError) => { .catch((err: IHttpFetchError) => {
@ -260,7 +260,7 @@ function useFeatures(
// 404 here, and respond in a way that still allows the UI to render itself. // 404 here, and respond in a way that still allows the UI to render itself.
const unauthorizedForFeatures = err.response?.status === 404; const unauthorizedForFeatures = err.response?.status === 404;
if (unauthorizedForFeatures) { if (unauthorizedForFeatures) {
return [] as Feature[]; return [] as KibanaFeature[];
} }
fatalErrors.add(err); fatalErrors.add(err);

View file

@ -7,7 +7,7 @@ import React from 'react';
import { FeatureTable } from './feature_table'; import { FeatureTable } from './feature_table';
import { Role } from '../../../../../../../common/model'; import { Role } from '../../../../../../../common/model';
import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { mountWithIntl } from 'test_utils/enzyme_helpers';
import { Feature, SubFeatureConfig } from '../../../../../../../../features/public'; import { KibanaFeature, SubFeatureConfig } from '../../../../../../../../features/public';
import { kibanaFeatures, createFeature } from '../../../../__fixtures__/kibana_features'; import { kibanaFeatures, createFeature } from '../../../../__fixtures__/kibana_features';
import { createKibanaPrivileges } from '../../../../__fixtures__/kibana_privileges'; import { createKibanaPrivileges } from '../../../../__fixtures__/kibana_privileges';
import { PrivilegeFormCalculator } from '../privilege_form_calculator'; import { PrivilegeFormCalculator } from '../privilege_form_calculator';
@ -24,7 +24,7 @@ const createRole = (kibana: Role['kibana'] = []): Role => {
}; };
interface TestConfig { interface TestConfig {
features: Feature[]; features: KibanaFeature[];
role: Role; role: Role;
privilegeIndex: number; privilegeIndex: number;
calculateDisplayedPrivileges: boolean; calculateDisplayedPrivileges: boolean;

View file

@ -13,7 +13,7 @@ import { PrivilegeDisplay } from './privilege_display';
import { Role, RoleKibanaPrivilege } from '../../../../../../../common/model'; import { Role, RoleKibanaPrivilege } from '../../../../../../../common/model';
import { createKibanaPrivileges } from '../../../../__fixtures__/kibana_privileges'; import { createKibanaPrivileges } from '../../../../__fixtures__/kibana_privileges';
import { PrivilegeFormCalculator } from '../privilege_form_calculator'; import { PrivilegeFormCalculator } from '../privilege_form_calculator';
import { Feature } from '../../../../../../../../features/public'; import { KibanaFeature } from '../../../../../../../../features/public';
import { findTestSubject } from 'test_utils/find_test_subject'; import { findTestSubject } from 'test_utils/find_test_subject';
interface TableRow { interface TableRow {
@ -24,7 +24,7 @@ interface TableRow {
} }
const features = [ const features = [
new Feature({ new KibanaFeature({
id: 'normal', id: 'normal',
name: 'normal feature', name: 'normal feature',
app: [], app: [],
@ -39,7 +39,7 @@ const features = [
}, },
}, },
}), }),
new Feature({ new KibanaFeature({
id: 'normal_with_sub', id: 'normal_with_sub',
name: 'normal feature with sub features', name: 'normal feature with sub features',
app: [], app: [],
@ -92,7 +92,7 @@ const features = [
}, },
], ],
}), }),
new Feature({ new KibanaFeature({
id: 'bothPrivilegesExcludedFromBase', id: 'bothPrivilegesExcludedFromBase',
name: 'bothPrivilegesExcludedFromBase', name: 'bothPrivilegesExcludedFromBase',
app: [], app: [],
@ -109,7 +109,7 @@ const features = [
}, },
}, },
}), }),
new Feature({ new KibanaFeature({
id: 'allPrivilegeExcludedFromBase', id: 'allPrivilegeExcludedFromBase',
name: 'allPrivilegeExcludedFromBase', name: 'allPrivilegeExcludedFromBase',
app: [], app: [],

View file

@ -8,7 +8,7 @@ import { RawKibanaPrivileges, RoleKibanaPrivilege } from '../../../../common/mod
import { KibanaPrivilege } from './kibana_privilege'; import { KibanaPrivilege } from './kibana_privilege';
import { PrivilegeCollection } from './privilege_collection'; import { PrivilegeCollection } from './privilege_collection';
import { SecuredFeature } from './secured_feature'; import { SecuredFeature } from './secured_feature';
import { Feature } from '../../../../../features/common'; import { KibanaFeature } from '../../../../../features/common';
import { isGlobalPrivilegeDefinition } from '../edit_role/privilege_utils'; import { isGlobalPrivilegeDefinition } from '../edit_role/privilege_utils';
function toBasePrivilege(entry: [string, string[]]): [string, KibanaPrivilege] { function toBasePrivilege(entry: [string, string[]]): [string, KibanaPrivilege] {
@ -29,7 +29,7 @@ export class KibanaPrivileges {
private feature: ReadonlyMap<string, SecuredFeature>; private feature: ReadonlyMap<string, SecuredFeature>;
constructor(rawKibanaPrivileges: RawKibanaPrivileges, features: Feature[]) { constructor(rawKibanaPrivileges: RawKibanaPrivileges, features: KibanaFeature[]) {
this.global = recordsToBasePrivilegeMap(rawKibanaPrivileges.global); this.global = recordsToBasePrivilegeMap(rawKibanaPrivileges.global);
this.spaces = recordsToBasePrivilegeMap(rawKibanaPrivileges.space); this.spaces = recordsToBasePrivilegeMap(rawKibanaPrivileges.space);
this.feature = new Map( this.feature = new Map(

View file

@ -4,12 +4,12 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import { Feature, FeatureConfig } from '../../../../../features/common'; import { KibanaFeature, KibanaFeatureConfig } from '../../../../../features/common';
import { PrimaryFeaturePrivilege } from './primary_feature_privilege'; import { PrimaryFeaturePrivilege } from './primary_feature_privilege';
import { SecuredSubFeature } from './secured_sub_feature'; import { SecuredSubFeature } from './secured_sub_feature';
import { SubFeaturePrivilege } from './sub_feature_privilege'; import { SubFeaturePrivilege } from './sub_feature_privilege';
export class SecuredFeature extends Feature { export class SecuredFeature extends KibanaFeature {
private readonly primaryFeaturePrivileges: PrimaryFeaturePrivilege[]; private readonly primaryFeaturePrivileges: PrimaryFeaturePrivilege[];
private readonly minimalPrimaryFeaturePrivileges: PrimaryFeaturePrivilege[]; private readonly minimalPrimaryFeaturePrivileges: PrimaryFeaturePrivilege[];
@ -18,7 +18,10 @@ export class SecuredFeature extends Feature {
private readonly securedSubFeatures: SecuredSubFeature[]; private readonly securedSubFeatures: SecuredSubFeature[];
constructor(config: FeatureConfig, actionMapping: { [privilegeId: string]: string[] } = {}) { constructor(
config: KibanaFeatureConfig,
actionMapping: { [privilegeId: string]: string[] } = {}
) {
super(config); super(config);
this.primaryFeaturePrivileges = Object.entries(this.config.privileges || {}).map( this.primaryFeaturePrivileges = Object.entries(this.config.privileges || {}).map(
([id, privilege]) => new PrimaryFeaturePrivilege(id, privilege, actionMapping[id]) ([id, privilege]) => new PrimaryFeaturePrivilege(id, privilege, actionMapping[id])

View file

@ -114,7 +114,8 @@ describe('Security Plugin', () => {
} }
); );
plugin.start(coreMock.createStart({ basePath: '/some-base-path' }), { const coreStart = coreMock.createStart({ basePath: '/some-base-path' });
plugin.start(coreStart, {
data: {} as DataPublicPluginStart, data: {} as DataPublicPluginStart,
features: {} as FeaturesPluginStart, features: {} as FeaturesPluginStart,
management: managementStartMock, management: managementStartMock,

View file

@ -141,7 +141,7 @@ export class SecurityPlugin
this.sessionTimeout.start(); this.sessionTimeout.start();
this.navControlService.start({ core }); this.navControlService.start({ core });
if (management) { if (management) {
this.managementService.start(); this.managementService.start({ capabilities: core.application.capabilities });
} }
} }

Some files were not shown because too many files have changed in this diff Show more