mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
c3a2451833
commit
047152f890
238 changed files with 7114 additions and 2495 deletions
|
@ -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', {
|
||||
|
|
|
@ -25,4 +25,10 @@ created index. For more information, see {ref}/indices-templates.html[Index temp
|
|||
* *Delete a policy.* You can’t 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*.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
20
test/functional/services/management/index.ts
Normal file
20
test/functional/services/management/index.ts
Normal 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';
|
51
test/functional/services/management/management_menu.ts
Normal file
51
test/functional/services/management/management_menu.ts
Normal 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();
|
||||
}
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: [],
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: [],
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
"requiredPlugins": [
|
||||
"data",
|
||||
"licensing",
|
||||
"management"
|
||||
"management",
|
||||
"features"
|
||||
],
|
||||
"optionalPlugins": [
|
||||
"security"
|
||||
|
|
|
@ -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 {};
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -8,7 +8,8 @@
|
|||
"licensing",
|
||||
"management",
|
||||
"remoteClusters",
|
||||
"indexManagement"
|
||||
"indexManagement",
|
||||
"features"
|
||||
],
|
||||
"optionalPlugins": [
|
||||
"usageCollection"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
85
x-pack/plugins/features/common/elasticsearch_feature.ts
Normal file
85
x-pack/plugins/features/common/elasticsearch_feature.ts
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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[];
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,8 +8,8 @@ import { PluginInitializer } from 'src/core/public';
|
|||
import { FeaturesPlugin, FeaturesPluginSetup, FeaturesPluginStart } from './plugin';
|
||||
|
||||
export {
|
||||
Feature,
|
||||
FeatureConfig,
|
||||
KibanaFeature,
|
||||
KibanaFeatureConfig,
|
||||
FeatureKibanaPrivileges,
|
||||
SubFeatureConfig,
|
||||
SubFeaturePrivilegeConfig,
|
||||
|
|
|
@ -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
|
@ -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);
|
||||
|
|
|
@ -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.`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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) =>
|
||||
|
|
|
@ -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(),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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, {});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }));
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
"ui": true,
|
||||
"requiredPlugins": [
|
||||
"licensing",
|
||||
"management"
|
||||
"management",
|
||||
"features"
|
||||
],
|
||||
"optionalPlugins": [
|
||||
"usageCollection",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
"requiredPlugins": [
|
||||
"home",
|
||||
"licensing",
|
||||
"management"
|
||||
"management",
|
||||
"features"
|
||||
],
|
||||
"optionalPlugins": [
|
||||
"security",
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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', [
|
||||
{
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
"configPath": ["xpack", "logstash"],
|
||||
"requiredPlugins": [
|
||||
"licensing",
|
||||
"management"
|
||||
"management",
|
||||
"features"
|
||||
],
|
||||
"optionalPlugins": [
|
||||
"home",
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -102,6 +102,7 @@ export function getPluginPrivileges() {
|
|||
...privilege,
|
||||
api: userMlCapabilitiesKeys.map((k) => `ml:${k}`),
|
||||
catalogue: [PLUGIN_ID],
|
||||
management: { insightsAndAlerting: [] },
|
||||
ui: userMlCapabilitiesKeys,
|
||||
savedObject: {
|
||||
all: [],
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -8,7 +8,8 @@
|
|||
"requiredPlugins": [
|
||||
"licensing",
|
||||
"management",
|
||||
"indexManagement"
|
||||
"indexManagement",
|
||||
"features"
|
||||
],
|
||||
"optionalPlugins": [
|
||||
"usageCollection",
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -14,7 +14,8 @@
|
|||
"licensing",
|
||||
"uiActions",
|
||||
"embeddable",
|
||||
"share"
|
||||
"share",
|
||||
"features"
|
||||
],
|
||||
"server": true,
|
||||
"ui": true,
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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`);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
"requiredPlugins": [
|
||||
"indexPatternManagement",
|
||||
"management",
|
||||
"licensing"
|
||||
"licensing",
|
||||
"features"
|
||||
],
|
||||
"optionalPlugins": [
|
||||
"home",
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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: [],
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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: [],
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue