[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
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"]
-----------
init(server) {
const xpackMainPlugin = server.plugins.xpack_main;
xpackMainPlugin.registerFeature({
setup(core, { features }) {
features.registerKibanaFeature({
// 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.
|`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>>
|The set of privileges this feature requires to function.
|`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>>
|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
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"]
-----------
import { uiCapabilities } from 'ui/capabilities';
public start(core) {
const { capabilities } = core.application;
const canUserSave = uiCapabilities.foo.save;
if (canUserSave) {
// show save button
const canUserSave = capabilities.foo.save;
if (canUserSave) {
// show save button
}
}
-----------
@ -89,9 +90,8 @@ if (canUserSave) {
=== Example 1: Canvas Application
["source","javascript"]
-----------
init(server) {
const xpackMainPlugin = server.plugins.xpack_main;
xpackMainPlugin.registerFeature({
public setup(core, { features }) {
features.registerKibanaFeature({
id: 'canvas',
name: 'Canvas',
icon: 'canvasApp',
@ -130,11 +130,13 @@ The `all` privilege defines a single “save” UI Capability. To access this in
["source","javascript"]
-----------
import { uiCapabilities } from 'ui/capabilities';
public start(core) {
const { capabilities } = core.application;
const canUserSave = uiCapabilities.canvas.save;
if (canUserSave) {
// show save button
const canUserSave = capabilities.canvas.save;
if (canUserSave) {
// show save button
}
}
-----------
@ -145,9 +147,8 @@ Because the `read` privilege does not define the `save` capability, users with r
["source","javascript"]
-----------
init(server) {
const xpackMainPlugin = server.plugins.xpack_main;
xpackMainPlugin.registerFeature({
public setup(core, { features }) {
features.registerKibanaFeature({
id: 'dev_tools',
name: i18n.translate('xpack.features.devToolsFeatureName', {
defaultMessage: 'Dev Tools',
@ -206,9 +207,8 @@ a single "Create Short URLs" subfeature privilege is defined, which allows users
["source","javascript"]
-----------
init(server) {
const xpackMainPlugin = server.plugins.xpack_main;
xpackMainPlugin.registerFeature({
public setup(core, { features }) {
features.registerKibanaFeature({
{
id: 'discover',
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
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.
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]
[[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
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]
[[update-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"]
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]
[[managing-remote-clusters]]
=== 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.
{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]
[[create-and-manage-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.
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]
=== Reindexing

View file

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

View file

@ -1283,7 +1283,7 @@ _See also: [Server's CoreSetup API Docs](/docs/development/core/server/kibana-pl
##### Plugin services
| 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) | |
#### UI Exports

View file

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

View file

@ -65,9 +65,9 @@ export async function createTestUserService(
}
return new (class TestUser {
async restoreDefaults() {
async restoreDefaults(shouldRefreshBrowser: boolean = true) {
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 { GlobalNavProvider } from './global_nav';
import { InspectorProvider } from './inspector';
import { ManagementMenuProvider } from './management';
import { QueryBarProvider } from './query_bar';
import { RemoteProvider } from './remote';
import { RenderableProvider } from './renderable';
@ -91,4 +92,5 @@ export const services = {
savedQueryManagementComponent: SavedQueryManagementComponentProvider,
elasticChart: ElasticChartProvider,
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 { Feature, FeatureConfig } from '../../../../plugins/features/server';
import { KibanaFeature } from '../../../../plugins/features/server';
import { XPackInfo, XPackInfoOptions } from './lib/xpack_info';
export { XPackFeature } from './lib/xpack_info';

View file

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

View file

@ -42,11 +42,11 @@ export class ActionsAuthorization {
const { authorization } = this;
if (authorization?.mode?.useRbacForRequest(this.request)) {
const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request);
const { hasAllRequested, username } = await checkPrivileges(
operationAlias[operation]
const { hasAllRequested, username } = await checkPrivileges({
kibana: operationAlias[operation]
? operationAlias[operation](authorization)
: authorization.actions.savedObject.get(ACTION_SAVED_OBJECT_TYPE, operation)
);
: authorization.actions.savedObject.get(ACTION_SAVED_OBJECT_TYPE, operation),
});
if (hasAllRequested) {
this.auditLogger.actionsAuthorizationSuccess(username, operation, actionTypeId);
} 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);
this.eventLogService = plugins.eventLog;

View file

@ -43,7 +43,7 @@ describe('AlertingBuiltins Plugin', () => {
"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 () => {

View file

@ -27,7 +27,7 @@ export class AlertingBuiltinsPlugin implements Plugin<IService, IService> {
core: CoreSetup,
{ alerts, features }: AlertingBuiltinsDeps
): Promise<IService> {
features.registerFeature(BUILT_IN_ALERTS_FEATURE);
features.registerKibanaFeature(BUILT_IN_ALERTS_FEATURE);
registerBuiltInAlertTypes({
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:
```typescript
features.registerFeature({
features.registerKibanaFeature({
id: 'my-application-id',
name: 'My Application',
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:
```typescript
features.registerFeature({
features.registerKibanaFeature({
id: 'my-application-id',
name: 'My Application',
app: [],

View file

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

View file

@ -82,7 +82,7 @@ export class AlertsAuthorization {
(disabledFeatures) =>
new Set(
features
.getFeatures()
.getKibanaFeatures()
.filter(
({ id, alerting }) =>
// ignore features which are disabled in the user's space
@ -133,20 +133,21 @@ export class AlertsAuthorization {
const shouldAuthorizeConsumer = consumer !== ALERTS_FEATURE_ID;
const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request);
const { hasAllRequested, username, privileges } = await checkPrivileges(
shouldAuthorizeConsumer && consumer !== alertType.producer
? [
// check for access at consumer level
requiredPrivilegesByScope.consumer,
// 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
requiredPrivilegesByScope.producer,
]
);
const { hasAllRequested, username, privileges } = await checkPrivileges({
kibana:
shouldAuthorizeConsumer && consumer !== alertType.producer
? [
// check for access at consumer level
requiredPrivilegesByScope.consumer,
// 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
requiredPrivilegesByScope.producer,
],
});
if (!isAvailableConsumer) {
/**
@ -177,7 +178,7 @@ export class AlertsAuthorization {
);
} else {
const authorizedPrivileges = map(
privileges.filter((privilege) => privilege.authorized),
privileges.kibana.filter((privilege) => privilege.authorized),
'privilege'
);
const unauthorizedScopes = mapValues(
@ -341,9 +342,9 @@ export class AlertsAuthorization {
}
}
const { username, hasAllRequested, privileges } = await checkPrivileges([
...privilegeToAlertType.keys(),
]);
const { username, hasAllRequested, privileges } = await checkPrivileges({
kibana: [...privilegeToAlertType.keys()],
});
return {
username,
@ -352,7 +353,7 @@ export class AlertsAuthorization {
? // has access to all features
this.augmentWithAuthorizedConsumers(alertTypes, await this.allPossibleConsumers)
: // only has some of the required privileges
privileges.reduce((authorizedAlertTypes, { authorized, privilege }) => {
privileges.kibana.reduce((authorizedAlertTypes, { authorized, privilege }) => {
if (authorized && privilegeToAlertType.has(privilege)) {
const [
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 { KibanaRequest, CoreSetup } from 'kibana/server';
import { featuresPluginMock } from '../../features/server/mocks';
import { Feature } from '../../features/server';
import { KibanaFeature } from '../../features/server';
describe('Alerting Plugin', () => {
describe('setup()', () => {
@ -159,8 +159,8 @@ describe('Alerting Plugin', () => {
function mockFeatures() {
const features = featuresPluginMock.createSetup();
features.getFeatures.mockReturnValue([
new Feature({
features.getKibanaFeatures.mockReturnValue([
new KibanaFeature({
id: 'appName',
name: 'appName',
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(
APM_SERVICE_MAPS_FEATURE_NAME,
APM_SERVICE_MAPS_LICENSE_TYPE

View file

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

View file

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

View file

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

View file

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

View file

@ -87,7 +87,7 @@ export class CrossClusterReplicationServerPlugin implements Plugin<void, void, a
setup(
{ http, getStartServices }: CoreSetup,
{ licensing, indexManagement, remoteClusters }: Dependencies
{ features, licensing, indexManagement, remoteClusters }: Dependencies
) {
this.config$
.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) => {
this.ccrEsClient = this.ccrEsClient ?? (await getCustomEsClient(getStartServices));
return {

View file

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

View file

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

View file

@ -78,7 +78,7 @@ export class EnterpriseSearchPlugin implements Plugin {
/**
* Register space/feature control
*/
features.registerFeature({
features.registerKibanaFeature({
id: ENTERPRISE_SEARCH_PLUGIN.ID,
name: ENTERPRISE_SEARCH_PLUGIN.NAME,
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.
*/
export { FeatureElasticsearchPrivileges } from './feature_elasticsearch_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 {
SubFeature,
SubFeatureConfig,

View file

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

View file

@ -1,27 +1,27 @@
// 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 { FeatureConfig, Feature, FeatureKibanaPrivileges } from '../common';
import { validateFeature } from './feature_schema';
import {
KibanaFeatureConfig,
KibanaFeature,
FeatureKibanaPrivileges,
ElasticsearchFeatureConfig,
ElasticsearchFeature,
} from '../common';
import { validateKibanaFeature, validateElasticsearchFeature } from './feature_schema';
export class FeatureRegistry {
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) {
throw new Error(
`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.`);
}
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;
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 readPrivilege = feature.privileges?.read;
const reservedPrivileges = (feature.reserved?.privileges ?? []).map((rp) => rp.privilege);

View file

@ -8,8 +8,8 @@ import Joi from 'joi';
import { difference } from 'lodash';
import { Capabilities as UICapabilities } from '../../../../src/core/server';
import { FeatureConfig } from '../common/feature';
import { FeatureKibanaPrivileges } from '.';
import { KibanaFeatureConfig } from '../common';
import { FeatureKibanaPrivileges, ElasticsearchFeatureConfig } from '.';
// Each feature gets its own property on the UICapabilities object,
// 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 alertingSchema = Joi.array().items(Joi.string());
const privilegeSchema = Joi.object({
const kibanaPrivilegeSchema = Joi.object({
excludeFromBasePrivileges: Joi.boolean(),
management: managementSchema,
catalogue: catalogueSchema,
@ -45,7 +45,7 @@ const privilegeSchema = Joi.object({
ui: Joi.array().items(Joi.string().regex(uiCapabilitiesRegex)).required(),
});
const subFeaturePrivilegeSchema = Joi.object({
const kibanaSubFeaturePrivilegeSchema = Joi.object({
id: Joi.string().regex(subFeaturePrivilegePartRegex).required(),
name: Joi.string().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(),
});
const subFeatureSchema = Joi.object({
const kibanaSubFeatureSchema = Joi.object({
name: Joi.string().required(),
privilegeGroups: Joi.array().items(
Joi.object({
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()
.regex(featurePrivilegePartRegex)
.invalid(...prohibitedFeatureIds)
@ -93,15 +93,15 @@ const schema = Joi.object({
catalogue: catalogueSchema,
alerting: alertingSchema,
privileges: Joi.object({
all: privilegeSchema,
read: privilegeSchema,
all: kibanaPrivilegeSchema,
read: kibanaPrivilegeSchema,
})
.allow(null)
.required(),
subFeatures: Joi.when('privileges', {
is: null,
then: Joi.array().items(subFeatureSchema).max(0),
otherwise: Joi.array().items(subFeatureSchema),
then: Joi.array().items(kibanaSubFeatureSchema).max(0),
otherwise: Joi.array().items(kibanaSubFeatureSchema),
}),
privilegesTooltip: Joi.string(),
reserved: Joi.object({
@ -110,15 +110,32 @@ const schema = Joi.object({
.items(
Joi.object({
id: Joi.string().regex(reservedFeaturePrrivilegePartRegex).required(),
privilege: privilegeSchema.required(),
privilege: kibanaPrivilegeSchema.required(),
})
)
.required(),
}),
});
export function validateFeature(feature: FeatureConfig) {
const validateResult = Joi.validate(feature, schema);
const elasticsearchPrivilegeSchema = Joi.object({
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) {
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.
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 const plugin = (initializerContext: PluginInitializerContext) =>

View file

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

View file

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

View file

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

View file

@ -28,19 +28,19 @@ describe('Features Plugin', () => {
coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry);
});
it('returns OSS + registered features', async () => {
it('returns OSS + registered kibana features', async () => {
const plugin = new Plugin(initContext);
const { registerFeature } = await plugin.setup(coreSetup, {});
registerFeature({
const { registerKibanaFeature } = await plugin.setup(coreSetup, {});
registerKibanaFeature({
id: 'baz',
name: 'baz',
app: [],
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 [
"baz",
"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 { registerFeature } = await plugin.setup(coreSetup, {
const { registerKibanaFeature: registerFeature } = await plugin.setup(coreSetup, {
visTypeTimelion: { uiEnabled: true },
});
registerFeature({
@ -66,9 +66,9 @@ describe('Features Plugin', () => {
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 [
"baz",
"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);
await plugin.setup(coreSetup, {});
const { getFeatures } = await plugin.start(coreStart);
const { getKibanaFeatures } = plugin.start(coreStart);
const soTypes =
getFeatures().find((f) => f.id === 'savedObjectsManagement')?.privileges?.all.savedObject
.all || [];
getKibanaFeatures().find((f) => f.id === 'savedObjectsManagement')?.privileges?.all
.savedObject.all || [];
expect(soTypes.includes('foo')).toBe(true);
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 () => {
const plugin = new Plugin(initContext);
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 { PluginSetupContract as TimelionSetupContract } from '../../../../src/plugins/vis_type_timelion/server';
import { FeatureRegistry } from './feature_registry';
import { Feature, FeatureConfig } from '../common/feature';
import { uiCapabilitiesForFeatures } from './ui_capabilities_for_features';
import { buildOSSFeatures } from './oss_features';
import { defineRoutes } from './routes';
import {
ElasticsearchFeatureConfig,
ElasticsearchFeature,
KibanaFeature,
KibanaFeatureConfig,
} from '../common';
/**
* Describes public Features plugin contract returned at the `setup` stage.
*/
export interface PluginSetupContract {
registerFeature(feature: FeatureConfig): void;
registerKibanaFeature(feature: KibanaFeatureConfig): void;
registerElasticsearchFeature(feature: ElasticsearchFeatureConfig): void;
/*
* Calling this function during setup will crash Kibana.
* Use start contract instead.
* @deprecated
* */
getFeatures(): Feature[];
getKibanaFeatures(): KibanaFeature[];
/*
* Calling this function during setup will crash Kibana.
* Use start contract instead.
* @deprecated
* */
getElasticsearchFeatures(): ElasticsearchFeature[];
getFeaturesUICapabilities(): UICapabilities;
}
export interface PluginStartContract {
getFeatures(): Feature[];
getElasticsearchFeatures(): ElasticsearchFeature[];
getKibanaFeatures(): KibanaFeature[];
}
/**
@ -62,13 +75,22 @@ export class Plugin {
});
const getFeaturesUICapabilities = () =>
uiCapabilitiesForFeatures(this.featureRegistry.getAll());
uiCapabilitiesForFeatures(
this.featureRegistry.getAllKibanaFeatures(),
this.featureRegistry.getAllElasticsearchFeatures()
);
core.capabilities.registerProvider(getFeaturesUICapabilities);
return deepFreeze({
registerFeature: this.featureRegistry.register.bind(this.featureRegistry),
getFeatures: this.featureRegistry.getAll.bind(this.featureRegistry),
registerKibanaFeature: this.featureRegistry.registerKibanaFeature.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,
});
}
@ -77,7 +99,10 @@ export class Plugin {
this.registerOssFeatures(core.savedObjects);
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) {
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 { licensingMock } from '../../../licensing/server/mocks';
import { RequestHandler } from '../../../../../src/core/server';
import { FeatureConfig } from '../../common';
import { KibanaFeatureConfig } from '../../common';
function createContextMock(licenseType: LicenseType = 'gold') {
return {
@ -24,14 +24,14 @@ describe('GET /api/features', () => {
let routeHandler: RequestHandler<any, any, any>;
beforeEach(() => {
const featureRegistry = new FeatureRegistry();
featureRegistry.register({
featureRegistry.registerKibanaFeature({
id: 'feature_1',
name: 'Feature 1',
app: [],
privileges: null,
});
featureRegistry.register({
featureRegistry.registerKibanaFeature({
id: 'feature_2',
name: 'Feature 2',
order: 2,
@ -39,7 +39,7 @@ describe('GET /api/features', () => {
privileges: null,
});
featureRegistry.register({
featureRegistry.registerKibanaFeature({
id: 'feature_3',
name: 'Feature 2',
order: 1,
@ -47,7 +47,7 @@ describe('GET /api/features', () => {
privileges: null,
});
featureRegistry.register({
featureRegistry.registerKibanaFeature({
id: 'licensed_feature',
name: 'Licensed Feature',
app: ['bar-app'],
@ -70,7 +70,7 @@ describe('GET /api/features', () => {
expect(mockResponse.ok).toHaveBeenCalledTimes(1);
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 }));
expect(features).toEqual([
@ -99,7 +99,7 @@ describe('GET /api/features', () => {
expect(mockResponse.ok).toHaveBeenCalledTimes(1);
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 }));
@ -129,7 +129,7 @@ describe('GET /api/features', () => {
expect(mockResponse.ok).toHaveBeenCalledTimes(1);
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 }));
@ -159,7 +159,7 @@ describe('GET /api/features', () => {
expect(mockResponse.ok).toHaveBeenCalledTimes(1);
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 }));

View file

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

View file

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

View file

@ -5,22 +5,35 @@
*/
import _ from 'lodash';
import { RecursiveReadonly } from '@kbn/utility-types';
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_DEEP_MERGE_KEYS = ['management'] as const;
interface FeatureCapabilities {
[featureId: string]: Record<string, boolean>;
}
export function uiCapabilitiesForFeatures(features: Feature[]): UICapabilities {
const featureCapabilities: FeatureCapabilities[] = features.map(getCapabilitiesFromFeature);
export function uiCapabilitiesForFeatures(
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 = {
catalogue: {},
[feature.id]: {},
@ -39,14 +52,34 @@ function getCapabilitiesFromFeature(feature: Feature): FeatureCapabilities {
};
}
const featurePrivileges = Object.values(feature.privileges ?? {});
if (feature.subFeatures) {
featurePrivileges.push(
...feature.subFeatures.map((sf) => sf.privilegeGroups.map((pg) => pg.privileges)).flat(2)
);
if (feature.management) {
const sectionEntries = Object.entries(feature.management);
UIFeatureCapabilities.management = sectionEntries.reduce((acc, [sectionId, sectionItems]) => {
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) => {
@ -65,6 +98,20 @@ function getCapabilitiesFromFeature(feature: Feature): FeatureCapabilities {
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 {
return allFeatureCapabilities.reduce<UICapabilities>((acc, capabilities) => {
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;
}, {} as UICapabilities);
}

View file

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

View file

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

View file

@ -60,7 +60,10 @@ export class IndexLifecycleManagementServerPlugin implements Plugin<void, void,
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 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({
router,
config,

View file

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

View file

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

View file

@ -59,7 +59,7 @@ export class IndexMgmtServerPlugin implements Plugin<IndexManagementPluginSetup,
setup(
{ http, getStartServices }: CoreSetup,
{ licensing, security }: Dependencies
{ features, licensing, security }: Dependencies
): IndexManagementPluginSetup {
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) => {
this.dataManagementESClient =
this.dataManagementESClient ?? (await getCustomEsClient(getStartServices));

View file

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

View file

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

View file

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

View file

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

View file

@ -25,7 +25,7 @@ export class IngestPipelinesPlugin implements Plugin<void, void, any, any> {
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');
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({
router,
license: this.license,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -23,7 +23,7 @@ export function registerManagementSection(
core: CoreSetup<MlStartDependencies>
) {
if (management !== undefined) {
management.sections.section.insightsAndAlerting.registerApp({
return management.sections.section.insightsAndAlerting.registerApp({
id: 'jobsListLink',
title: i18n.translate('xpack.ml.management.jobsListTitle', {
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));
licensing.subscribe(async (license) => {
const [coreStart] = await core.getStartServices();
@ -110,26 +112,35 @@ export class MlPlugin implements Plugin<MlPluginSetup, MlPluginStart> {
registerFeature(pluginsSetup.home);
}
const { capabilities } = coreStart.application;
// register ML for the index pattern management no data screen.
pluginsSetup.indexPatternManagement.environment.update({
ml: () =>
coreStart.application.capabilities.ml.canFindFileStructure
? MlCardState.ENABLED
: MlCardState.HIDDEN,
capabilities.ml.canFindFileStructure ? MlCardState.ENABLED : MlCardState.HIDDEN,
});
const canManageMLJobs = capabilities.management?.insightsAndAlerting?.jobsListLink ?? false;
// register various ML plugin features which require a full license
if (isFullLicense(license)) {
registerManagementSection(pluginsSetup.management, core);
if (canManageMLJobs && managementApp) {
managementApp.enable();
}
registerEmbeddables(pluginsSetup.embeddable, core);
registerMlUiActions(pluginsSetup.uiActions, core);
registerUrlGenerator(pluginsSetup.share, core);
} else if (managementApp) {
managementApp.disable();
}
} else {
// if ml is disabled in elasticsearch, disable ML in kibana
this.appUpdater.next(() => ({
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 {
const { admin, user, apmUser } = getPluginPrivileges();
plugins.features.registerFeature({
plugins.features.registerKibanaFeature({
id: PLUGIN_ID,
name: i18n.translate('xpack.ml.featureRegistry.mlFeatureName', {
defaultMessage: 'Machine Learning',

View file

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

View file

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

View file

@ -35,7 +35,7 @@ export class RemoteClustersServerPlugin
this.licenseStatus = { valid: false };
}
async setup({ http }: CoreSetup, { licensing, cloud }: Dependencies) {
async setup({ http }: CoreSetup, { features, licensing, cloud }: Dependencies) {
const router = http.createRouter();
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
registerGetRoute(routeDependencies);
registerAddRoute(routeDependencies);

View file

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

View file

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

View file

@ -15,6 +15,7 @@ import {
SavedObjectsServiceStart,
UiSettingsServiceStart,
} from 'src/core/server';
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
import { LicensingPluginSetup } from '../../licensing/server';
import { SecurityPluginSetup } from '../../security/server';
import { ReportingConfig } from './';
@ -25,6 +26,7 @@ import { screenshotsObservableFactory, ScreenshotsObservableFn } from './lib/scr
import { ReportingStore } from './lib/store';
export interface ReportingInternalSetup {
features: FeaturesPluginSetup;
elasticsearch: ElasticsearchServiceSetup;
licensing: LicensingPluginSetup;
basePath: BasePath['get'];
@ -99,6 +101,26 @@ export class ReportingCore {
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
*/

View file

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

View file

@ -70,13 +70,14 @@ export class ReportingPlugin
});
const { elasticsearch, http } = core;
const { licensing, security } = plugins;
const { features, licensing, security } = plugins;
const { initializerContext: initContext, reportingCore } = this;
const router = http.createRouter();
const basePath = http.basePath.get;
reportingCore.pluginSetup({
features,
elasticsearch,
licensing,
basePath,
@ -91,6 +92,8 @@ export class ReportingPlugin
(async () => {
const config = await buildConfig(initContext, core, this.logger);
reportingCore.setConfig(config);
// Feature registration relies on config, so it cannot be setup before here.
reportingCore.registerFeature();
this.logger.debug('Setup complete');
})().catch((e) => {
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');
import * as Rx from 'rxjs';
import { featuresPluginMock } from '../../../features/server/mocks';
import { ReportingConfig, ReportingCore } from '../';
import {
chromium,
@ -32,6 +33,7 @@ const createMockPluginSetup = (
setupMock?: any
): ReportingInternalSetup => {
return {
features: featuresPluginMock.createSetup(),
elasticsearch: setupMock.elasticsearch || { legacy: { client: {} } },
basePath: setupMock.basePath || '/all-about-that-basepath',
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 { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { CancellationToken } from '../../../plugins/reporting/common';
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
import { LicensingPluginSetup } from '../../licensing/server';
import { AuthenticatedUser, SecurityPluginSetup } from '../../security/server';
import { JobStatus } from '../common/types';
@ -92,6 +93,7 @@ export interface ConditionalHeaders {
export interface ReportingSetupDeps {
licensing: LicensingPluginSetup;
features: FeaturesPluginSetup;
security?: SecurityPluginSetup;
usageCollection?: UsageCollectionSetup;
}

View file

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

View file

@ -64,7 +64,7 @@ export class RollupPlugin implements Plugin<void, void, any, any> {
public setup(
{ http, uiSettings, getStartServices }: CoreSetup,
{ licensing, indexManagement, visTypeTimeseries, usageCollection }: Dependencies
{ features, licensing, indexManagement, visTypeTimeseries, usageCollection }: Dependencies
) {
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) => {
this.rollupEsClient = this.rollupEsClient ?? (await getCustomEsClient(getStartServices));
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 { IndexManagementPluginSetup } from '../../index_management/server';
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
import { LicensingPluginSetup } from '../../licensing/server';
import { License } from './services';
import { IndexPatternsFetcher } from './shared_imports';
@ -22,6 +23,7 @@ export interface Dependencies {
visTypeTimeseries?: VisTypeTimeseriesSetup;
usageCollection?: UsageCollectionSetup;
licensing: LicensingPluginSetup;
features: FeaturesPluginSetup;
}
export interface RouteDependencies {

View file

@ -78,7 +78,10 @@ describe('ManagementService', () => {
});
describe('start()', () => {
function startService(initialFeatures: Partial<SecurityLicenseFeatures>) {
function startService(
initialFeatures: Partial<SecurityLicenseFeatures>,
canManageSecurity: boolean = true
) {
const { fatalErrors, getStartServices } = coreMock.createSetup();
const licenseSubject = new BehaviorSubject<SecurityLicenseFeatures>(
@ -106,10 +109,11 @@ describe('ManagementService', () => {
management: managementSetup,
});
const getMockedApp = () => {
const getMockedApp = (id: string) => {
// All apps are enabled by default.
let enabled = true;
return ({
id,
get enabled() {
return enabled;
},
@ -123,13 +127,26 @@ describe('ManagementService', () => {
};
mockSection.getApp = jest.fn().mockImplementation((id) => mockApps.get(id));
const mockApps = new Map<string, jest.Mocked<ManagementApp>>([
[usersManagementApp.id, getMockedApp()],
[rolesManagementApp.id, getMockedApp()],
[apiKeysManagementApp.id, getMockedApp()],
[roleMappingsManagementApp.id, getMockedApp()],
[usersManagementApp.id, getMockedApp(usersManagementApp.id)],
[rolesManagementApp.id, getMockedApp(rolesManagementApp.id)],
[apiKeysManagementApp.id, getMockedApp(apiKeysManagementApp.id)],
[roleMappingsManagementApp.id, getMockedApp(roleMappingsManagementApp.id)],
] 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 {
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`', () => {
const { mockApps, updateFeatures } = startService({
showLinks: true,

View file

@ -5,7 +5,7 @@
*/
import { Subscription } from 'rxjs';
import { StartServicesAccessor, FatalErrorsSetup } from 'src/core/public';
import { StartServicesAccessor, FatalErrorsSetup, Capabilities } from 'src/core/public';
import {
ManagementApp,
ManagementSetup,
@ -27,6 +27,10 @@ interface SetupParams {
getStartServices: StartServicesAccessor<PluginStartDependencies>;
}
interface StartParams {
capabilities: Capabilities;
}
export class ManagementService {
private license!: SecurityLicense;
private licenseFeaturesSubscription?: Subscription;
@ -44,7 +48,7 @@ export class ManagementService {
this.securitySection.registerApp(roleMappingsManagementApp.create({ getStartServices }));
}
start() {
start({ capabilities }: StartParams) {
this.licenseFeaturesSubscription = this.license.features$.subscribe(async (features) => {
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
// license features.
for (const [app, enableStatus] of securityManagementAppsStatuses) {
if (capabilities.management.security[app.id] !== true) {
app.disable();
continue;
}
if (app.enabled === enableStatus) {
continue;
}

View file

@ -4,17 +4,20 @@
* 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 = (
config: Pick<FeatureConfig, 'id' | 'name' | 'subFeatures' | 'reserved' | 'privilegesTooltip'> & {
config: Pick<
KibanaFeatureConfig,
'id' | 'name' | 'subFeatures' | 'reserved' | 'privilegesTooltip'
> & {
excludeFromBaseAll?: boolean;
excludeFromBaseRead?: boolean;
privileges?: FeatureConfig['privileges'];
privileges?: KibanaFeatureConfig['privileges'];
}
) => {
const { excludeFromBaseAll, excludeFromBaseRead, privileges, ...rest } = config;
return new Feature({
return new KibanaFeature({
icon: 'discoverApp',
navLinkId: 'discover',
app: [],

View file

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

View file

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

View file

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

View file

@ -7,7 +7,7 @@ import React from 'react';
import { FeatureTable } from './feature_table';
import { Role } from '../../../../../../../common/model';
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 { createKibanaPrivileges } from '../../../../__fixtures__/kibana_privileges';
import { PrivilegeFormCalculator } from '../privilege_form_calculator';
@ -24,7 +24,7 @@ const createRole = (kibana: Role['kibana'] = []): Role => {
};
interface TestConfig {
features: Feature[];
features: KibanaFeature[];
role: Role;
privilegeIndex: number;
calculateDisplayedPrivileges: boolean;

View file

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

View file

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

View file

@ -4,12 +4,12 @@
* 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 { SecuredSubFeature } from './secured_sub_feature';
import { SubFeaturePrivilege } from './sub_feature_privilege';
export class SecuredFeature extends Feature {
export class SecuredFeature extends KibanaFeature {
private readonly primaryFeaturePrivileges: PrimaryFeaturePrivilege[];
private readonly minimalPrimaryFeaturePrivileges: PrimaryFeaturePrivilege[];
@ -18,7 +18,10 @@ export class SecuredFeature extends Feature {
private readonly securedSubFeatures: SecuredSubFeature[];
constructor(config: FeatureConfig, actionMapping: { [privilegeId: string]: string[] } = {}) {
constructor(
config: KibanaFeatureConfig,
actionMapping: { [privilegeId: string]: string[] } = {}
) {
super(config);
this.primaryFeaturePrivileges = Object.entries(this.config.privileges || {}).map(
([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,
features: {} as FeaturesPluginStart,
management: managementStartMock,

View file

@ -141,7 +141,7 @@ export class SecurityPlugin
this.sessionTimeout.start();
this.navControlService.start({ core });
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