From bda8e2ef306fcf5f1c634ec1ed1b38c6f385e8b2 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 29 May 2020 14:00:19 -0400 Subject: [PATCH 01/17] Hide management sections based on ES privileges --- .github/CODEOWNERS | 1 + examples/alerting_example/server/plugin.ts | 2 +- src/core/MIGRATION.md | 2 +- .../server/capabilities/merge_capabilities.ts | 2 +- src/plugins/management/public/plugin.ts | 20 + test/common/services/security/test_user.ts | 4 +- test/functional/services/index.ts | 2 + test/functional/services/management/index.ts | 20 + .../services/management/management_menu.ts | 51 + .../actions_authorization.test.ts | 14 +- .../authorization/actions_authorization.ts | 8 +- x-pack/plugins/actions/server/plugin.ts | 2 +- .../alerting_builtins/server/plugin.test.ts | 2 +- .../alerting_builtins/server/plugin.ts | 2 +- x-pack/plugins/alerts/README.md | 4 +- .../alerts_authorization.test.ts | 396 +-- .../authorization/alerts_authorization.ts | 41 +- x-pack/plugins/alerts/server/plugin.test.ts | 2 +- x-pack/plugins/apm/server/plugin.ts | 2 +- x-pack/plugins/beats_management/kibana.json | 3 +- .../plugins/beats_management/server/plugin.ts | 16 + x-pack/plugins/canvas/server/plugin.ts | 2 +- .../cross_cluster_replication/kibana.json | 3 +- .../server/plugin.ts | 15 +- .../cross_cluster_replication/server/types.ts | 2 + .../server/lib/check_access.ts | 2 +- .../enterprise_search/server/plugin.ts | 2 +- x-pack/plugins/features/common/es_feature.ts | 85 + x-pack/plugins/features/common/feature.ts | 14 +- .../feature_elasticsearch_privileges.ts | 69 + x-pack/plugins/features/common/index.ts | 4 +- .../feature_registry.test.ts.snap | 26 +- .../features/server/feature_registry.test.ts | 2176 +++++++++-------- .../features/server/feature_registry.ts | 50 +- .../plugins/features/server/feature_schema.ts | 39 +- x-pack/plugins/features/server/index.ts | 9 +- x-pack/plugins/features/server/mocks.ts | 9 +- .../plugins/features/server/oss_features.ts | 6 +- x-pack/plugins/features/server/plugin.test.ts | 10 +- x-pack/plugins/features/server/plugin.ts | 40 +- .../features/server/routes/index.test.ts | 8 +- .../plugins/features/server/routes/index.ts | 2 +- .../ui_capabilities_for_features.test.ts | 528 ++-- .../server/ui_capabilities_for_features.ts | 93 +- x-pack/plugins/graph/server/plugin.ts | 2 +- .../index_lifecycle_management/kibana.json | 3 +- .../server/plugin.ts | 18 +- .../server/types.ts | 2 + x-pack/plugins/index_management/kibana.json | 3 +- .../plugins/index_management/server/plugin.ts | 15 +- .../plugins/index_management/server/types.ts | 2 + x-pack/plugins/infra/server/plugin.ts | 4 +- .../plugins/ingest_manager/server/plugin.ts | 2 +- x-pack/plugins/ingest_pipelines/kibana.json | 2 +- .../plugins/ingest_pipelines/server/plugin.ts | 15 +- .../plugins/ingest_pipelines/server/types.ts | 2 + x-pack/plugins/license_management/kibana.json | 2 +- .../license_management/server/plugin.ts | 15 +- .../license_management/server/types.ts | 2 + x-pack/plugins/logstash/kibana.json | 3 +- x-pack/plugins/logstash/server/plugin.ts | 18 + x-pack/plugins/maps/server/plugin.ts | 2 +- x-pack/plugins/ml/server/plugin.ts | 2 +- x-pack/plugins/monitoring/server/plugin.ts | 2 +- x-pack/plugins/remote_clusters/kibana.json | 3 +- .../plugins/remote_clusters/server/plugin.ts | 15 +- .../plugins/remote_clusters/server/types.ts | 2 + x-pack/plugins/reporting/kibana.json | 3 +- x-pack/plugins/reporting/server/core.ts | 22 + .../plugins/reporting/server/plugin.test.ts | 2 + x-pack/plugins/reporting/server/plugin.ts | 5 +- .../create_mock_reportingplugin.ts | 2 + x-pack/plugins/reporting/server/types.ts | 2 + x-pack/plugins/rollup/kibana.json | 3 +- x-pack/plugins/rollup/server/plugin.ts | 16 +- x-pack/plugins/rollup/server/types.ts | 2 + .../management/management_service.test.ts | 44 +- .../public/management/management_service.ts | 13 +- .../roles/__fixtures__/kibana_privileges.ts | 2 +- .../plugins/security/public/plugin.test.tsx | 3 +- x-pack/plugins/security/public/plugin.tsx | 2 +- .../authorization/api_authorization.test.ts | 8 +- .../server/authorization/api_authorization.ts | 2 +- .../authorization/app_authorization.test.ts | 6 +- .../server/authorization/app_authorization.ts | 4 +- .../authorization_service.test.ts | 7 +- .../authorization/authorization_service.ts | 11 +- .../authorization/check_privileges.test.ts | 448 ++-- .../server/authorization/check_privileges.ts | 108 +- .../check_privileges_dynamically.test.ts | 10 +- .../check_privileges_dynamically.ts | 9 +- .../check_saved_objects_privileges.test.ts | 6 +- .../check_saved_objects_privileges.ts | 6 +- .../disable_ui_capabilities.test.ts | 142 +- .../authorization/disable_ui_capabilities.ts | 115 +- .../server/authorization/index.mock.ts | 1 + .../privileges/privileges.test.ts | 108 +- .../authorization/privileges/privileges.ts | 3 +- .../security/server/authorization/types.ts | 16 + .../plugins/security/server/features/index.ts | 7 + .../server/features/security_features.ts | 74 + x-pack/plugins/security/server/plugin.test.ts | 2 + x-pack/plugins/security/server/plugin.ts | 14 +- ...ecure_saved_objects_client_wrapper.test.ts | 40 +- .../secure_saved_objects_client_wrapper.ts | 8 +- .../security_solution/server/plugin.ts | 2 +- x-pack/plugins/snapshot_restore/kibana.json | 3 +- .../plugins/snapshot_restore/server/plugin.ts | 17 +- .../plugins/snapshot_restore/server/types.ts | 2 + .../capabilities_provider.test.ts | 3 + .../capabilities/capabilities_provider.ts | 3 + .../capabilities_switcher.test.ts | 2 +- .../capabilities/capabilities_switcher.ts | 2 +- .../on_post_auth_interceptor.test.ts | 2 +- .../on_post_auth_interceptor.ts | 2 +- .../lib/spaces_client/spaces_client.test.ts | 92 +- .../server/lib/spaces_client/spaces_client.ts | 18 +- x-pack/plugins/spaces/server/plugin.test.ts | 10 +- .../spaces_usage_collector.test.ts | 2 +- .../spaces_usage_collector.ts | 2 +- x-pack/plugins/transform/kibana.json | 3 +- x-pack/plugins/transform/server/plugin.ts | 16 +- x-pack/plugins/transform/server/types.ts | 2 + x-pack/plugins/upgrade_assistant/kibana.json | 2 +- .../upgrade_assistant/server/plugin.ts | 17 +- x-pack/plugins/uptime/server/kibana.index.ts | 2 +- x-pack/plugins/watcher/kibana.json | 3 +- x-pack/plugins/watcher/server/plugin.ts | 30 +- x-pack/plugins/watcher/server/types.ts | 2 + .../actions_simulators/server/plugin.ts | 2 +- .../fixtures/plugins/alerts/server/plugin.ts | 2 +- .../alerts_restricted/server/plugin.ts | 2 +- .../advanced_settings_security.ts | 11 +- .../feature_controls/api_keys_security.ts | 69 + .../apps/api_keys/feature_controls/index.ts | 15 + .../functional/apps/api_keys/home_page.ts | 7 +- x-pack/test/functional/apps/api_keys/index.ts | 1 + .../feature_controls/canvas_security.ts | 4 +- .../feature_controls/ccr_security.ts | 77 + .../feature_controls/index.ts | 15 + .../apps/cross_cluster_replication/index.ts | 1 + .../feature_controls/dashboard_security.ts | 8 +- .../feature_controls/dev_tools_security.ts | 4 +- .../feature_controls/discover_security.ts | 6 +- .../graph/feature_controls/graph_security.ts | 4 +- .../feature_controls/ilm_security.ts | 69 + .../feature_controls/index.ts | 15 + .../apps/index_lifecycle_management/index.ts | 1 + .../feature_controls/index.ts | 15 + .../index_management_security.ts | 69 + .../functional/apps/index_management/index.ts | 1 + .../index_patterns_security.ts | 20 +- .../infra/feature_controls/logs_security.ts | 4 +- .../feature_controls/index.ts | 15 + .../ingest_pipelines_security.ts | 69 + .../functional/apps/ingest_pipelines/index.ts | 1 + .../feature_controls/index.ts | 15 + .../license_management_security.ts | 69 + .../apps/license_management/index.ts | 1 + .../apps/logstash/feature_controls/index.ts | 15 + .../feature_controls/logstash_security.ts | 69 + x-pack/test/functional/apps/logstash/index.js | 1 + .../apps/management/feature_controls/index.ts | 15 + .../feature_controls/management_security.ts | 74 + .../apps/management/{index.js => index.ts} | 5 +- .../maps/feature_controls/maps_security.ts | 4 +- .../remote_clusters/feature_controls/index.ts | 15 + .../remote_clusters_security.ts | 76 + .../functional/apps/remote_clusters/index.ts | 1 + .../saved_objects_management_security.ts | 20 +- .../apps/security/secure_roles_perm.js | 8 +- .../feature_controls/timelion_security.ts | 4 +- .../apps/transform/feature_controls/index.ts | 15 + .../feature_controls/transform_security.ts | 70 + .../test/functional/apps/transform/index.ts | 1 + .../feature_controls/index.ts | 15 + .../upgrade_assistant_security.ts | 72 + .../apps/upgrade_assistant/index.ts | 1 + .../feature_controls/visualize_security.ts | 6 +- x-pack/test/functional/config.js | 69 + .../fixtures/plugins/alerts/server/plugin.ts | 2 +- .../plugins/foo_plugin/server/index.ts | 2 +- .../security_and_spaces/tests/catalogue.ts | 58 +- .../security_only/tests/catalogue.ts | 19 +- .../spaces_only/tests/catalogue.ts | 10 +- 185 files changed, 4685 insertions(+), 1995 deletions(-) create mode 100644 test/functional/services/management/index.ts create mode 100644 test/functional/services/management/management_menu.ts create mode 100644 x-pack/plugins/features/common/es_feature.ts create mode 100644 x-pack/plugins/features/common/feature_elasticsearch_privileges.ts create mode 100644 x-pack/plugins/security/server/features/index.ts create mode 100644 x-pack/plugins/security/server/features/security_features.ts create mode 100644 x-pack/test/functional/apps/api_keys/feature_controls/api_keys_security.ts create mode 100644 x-pack/test/functional/apps/api_keys/feature_controls/index.ts create mode 100644 x-pack/test/functional/apps/cross_cluster_replication/feature_controls/ccr_security.ts create mode 100644 x-pack/test/functional/apps/cross_cluster_replication/feature_controls/index.ts create mode 100644 x-pack/test/functional/apps/index_lifecycle_management/feature_controls/ilm_security.ts create mode 100644 x-pack/test/functional/apps/index_lifecycle_management/feature_controls/index.ts create mode 100644 x-pack/test/functional/apps/index_management/feature_controls/index.ts create mode 100644 x-pack/test/functional/apps/index_management/feature_controls/index_management_security.ts create mode 100644 x-pack/test/functional/apps/ingest_pipelines/feature_controls/index.ts create mode 100644 x-pack/test/functional/apps/ingest_pipelines/feature_controls/ingest_pipelines_security.ts create mode 100644 x-pack/test/functional/apps/license_management/feature_controls/index.ts create mode 100644 x-pack/test/functional/apps/license_management/feature_controls/license_management_security.ts create mode 100644 x-pack/test/functional/apps/logstash/feature_controls/index.ts create mode 100644 x-pack/test/functional/apps/logstash/feature_controls/logstash_security.ts create mode 100644 x-pack/test/functional/apps/management/feature_controls/index.ts create mode 100644 x-pack/test/functional/apps/management/feature_controls/management_security.ts rename x-pack/test/functional/apps/management/{index.js => index.ts} (67%) create mode 100644 x-pack/test/functional/apps/remote_clusters/feature_controls/index.ts create mode 100644 x-pack/test/functional/apps/remote_clusters/feature_controls/remote_clusters_security.ts create mode 100644 x-pack/test/functional/apps/transform/feature_controls/index.ts create mode 100644 x-pack/test/functional/apps/transform/feature_controls/transform_security.ts create mode 100644 x-pack/test/functional/apps/upgrade_assistant/feature_controls/index.ts create mode 100644 x-pack/test/functional/apps/upgrade_assistant/feature_controls/upgrade_assistant_security.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f1a374445657f2..7a5df985fdb776 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -165,6 +165,7 @@ /x-pack/plugins/encrypted_saved_objects/ @elastic/kibana-security /x-pack/plugins/security/ @elastic/kibana-security /x-pack/test/api_integration/apis/security/ @elastic/kibana-security +/x-pack/test/ui_capabilities/ @elastic/kibana-security # Kibana Localization /src/dev/i18n/ @elastic/kibana-localization diff --git a/examples/alerting_example/server/plugin.ts b/examples/alerting_example/server/plugin.ts index e74cad28f77f4a..8e246960937ec3 100644 --- a/examples/alerting_example/server/plugin.ts +++ b/examples/alerting_example/server/plugin.ts @@ -38,7 +38,7 @@ export class AlertingExamplePlugin implements Plugin>): Capabilities => - mergeWith({}, ...sources, (a: any, b: any) => { + mergeWith({}, ...sources, (a: any, b: any, key: any) => { if ( (typeof a === 'boolean' && typeof b === 'object') || (typeof a === 'object' && typeof b === 'boolean') diff --git a/src/plugins/management/public/plugin.ts b/src/plugins/management/public/plugin.ts index 8be130b4028c6b..b8d5ba6495190e 100644 --- a/src/plugins/management/public/plugin.ts +++ b/src/plugins/management/public/plugin.ts @@ -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 { private readonly managementSections = new ManagementSectionsService(); + private readonly appUpdater = new BehaviorSubject(() => ({})); + constructor(private initializerContext: PluginInitializerContext) {} public setup(core: CoreSetup, { home }: ManagementSetupDependencies) { @@ -68,6 +74,7 @@ export class ManagementPlugin implements Plugin section.getAppsEnabled().length > 0); + + if (!hasAnyEnabledApps) { + this.appUpdater.next(() => { + return { + status: AppStatus.inaccessible, + navLinkStatus: AppNavLinkStatus.hidden, + }; + }); + } + return {}; } } diff --git a/test/common/services/security/test_user.ts b/test/common/services/security/test_user.ts index 104094f5b6fb5d..83eac78621a536 100644 --- a/test/common/services/security/test_user.ts +++ b/test/common/services/security/test_user.ts @@ -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); } } diff --git a/test/functional/services/index.ts b/test/functional/services/index.ts index 7891a6b00f7296..ce092ed0c65e86 100644 --- a/test/functional/services/index.ts +++ b/test/functional/services/index.ts @@ -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'; @@ -84,4 +85,5 @@ export const services = { savedQueryManagementComponent: SavedQueryManagementComponentProvider, elasticChart: ElasticChartProvider, supertest: KibanaSupertestProvider, + managementMenu: ManagementMenuProvider, }; diff --git a/test/functional/services/management/index.ts b/test/functional/services/management/index.ts new file mode 100644 index 00000000000000..54cd229a8e8580 --- /dev/null +++ b/test/functional/services/management/index.ts @@ -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'; diff --git a/test/functional/services/management/management_menu.ts b/test/functional/services/management/management_menu.ts new file mode 100644 index 00000000000000..9aed490bc69984 --- /dev/null +++ b/test/functional/services/management/management_menu.ts @@ -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(); +} diff --git a/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts b/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts index a48124cdbcb6ad..14573161b8d5d3 100644 --- a/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts +++ b/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts @@ -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(); diff --git a/x-pack/plugins/actions/server/authorization/actions_authorization.ts b/x-pack/plugins/actions/server/authorization/actions_authorization.ts index da5a5a1cdc3eb7..3ba798ddf1715f 100644 --- a/x-pack/plugins/actions/server/authorization/actions_authorization.ts +++ b/x-pack/plugins/actions/server/authorization/actions_authorization.ts @@ -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 { diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 5b8b25d02658ba..0d82552fd675f5 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -144,7 +144,7 @@ export class ActionsPlugin implements Plugin, Plugi ); } - plugins.features.registerFeature(ACTIONS_FEATURE); + plugins.features.registerKibanaFeature(ACTIONS_FEATURE); setupSavedObjects(core.savedObjects, plugins.encryptedSavedObjects); plugins.eventLog.registerProviderActions(EVENT_LOG_PROVIDER, Object.values(EVENT_LOG_ACTIONS)); diff --git a/x-pack/plugins/alerting_builtins/server/plugin.test.ts b/x-pack/plugins/alerting_builtins/server/plugin.test.ts index 15ad066523502f..629c02d923071e 100644 --- a/x-pack/plugins/alerting_builtins/server/plugin.test.ts +++ b/x-pack/plugins/alerting_builtins/server/plugin.test.ts @@ -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 () => { diff --git a/x-pack/plugins/alerting_builtins/server/plugin.ts b/x-pack/plugins/alerting_builtins/server/plugin.ts index 41871c01bfb505..48e5c41cbe6372 100644 --- a/x-pack/plugins/alerting_builtins/server/plugin.ts +++ b/x-pack/plugins/alerting_builtins/server/plugin.ts @@ -27,7 +27,7 @@ export class AlertingBuiltinsPlugin implements Plugin { core: CoreSetup, { alerts, features }: AlertingBuiltinsDeps ): Promise { - features.registerFeature(BUILT_IN_ALERTS_FEATURE); + features.registerKibanaFeature(BUILT_IN_ALERTS_FEATURE); registerBuiltInAlertTypes({ service: this.service, diff --git a/x-pack/plugins/alerts/README.md b/x-pack/plugins/alerts/README.md index 10568abbe3c72c..8fa939101a6909 100644 --- a/x-pack/plugins/alerts/README.md +++ b/x-pack/plugins/alerts/README.md @@ -305,7 +305,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: [], @@ -347,7 +347,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: [], diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts index b164d27ded6486..c8e653f1af16ba 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts @@ -174,7 +174,7 @@ beforeEach(() => { async executor() {}, producer: 'myApp', })); - features.getFeatures.mockReturnValue([ + features.getKibanaFeatures.mockReturnValue([ myAppFeature, myOtherAppFeature, myAppWithSubFeature, @@ -255,7 +255,7 @@ describe('AlertsAuthorization', () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', hasAllRequested: true, - privileges: [], + privileges: { kibana: [] }, }); await alertAuthorization.ensureAuthorized('myType', 'myApp', WriteOperations.Create); @@ -263,9 +263,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 +298,7 @@ describe('AlertsAuthorization', () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', hasAllRequested: true, - privileges: [], + privileges: { kibana: [] }, }); await alertAuthorization.ensureAuthorized('myType', 'alerts', WriteOperations.Create); @@ -306,9 +306,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 +332,7 @@ describe('AlertsAuthorization', () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', hasAllRequested: true, - privileges: [], + privileges: { kibana: [] }, }); const alertAuthorization = new AlertsAuthorization({ @@ -354,10 +354,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 +392,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 +443,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 +494,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 +600,7 @@ describe('AlertsAuthorization', () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', hasAllRequested: true, - privileges: [], + privileges: { kibana: [] }, }); const alertAuthorization = new AlertsAuthorization({ @@ -621,24 +629,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 +690,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 +740,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 +917,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 +1005,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 +1066,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 +1178,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({ diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts index 33a9a0bf0396ea..e78765d998810e 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts @@ -81,7 +81,7 @@ export class AlertsAuthorization { (disabledFeatures) => new Set( features - .getFeatures() + .getKibanaFeatures() .filter( ({ id, alerting }) => // ignore features which are disabled in the user's space @@ -132,20 +132,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) { /** @@ -176,7 +177,7 @@ export class AlertsAuthorization { ); } else { const authorizedPrivileges = map( - privileges.filter((privilege) => privilege.authorized), + privileges.kibana.filter((privilege) => privilege.authorized), 'privilege' ); const unauthorizedScopes = mapValues( @@ -340,9 +341,9 @@ export class AlertsAuthorization { } } - const { username, hasAllRequested, privileges } = await checkPrivileges([ - ...privilegeToAlertType.keys(), - ]); + const { username, hasAllRequested, privileges } = await checkPrivileges({ + kibana: [...privilegeToAlertType.keys()], + }); return { username, @@ -351,7 +352,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, diff --git a/x-pack/plugins/alerts/server/plugin.test.ts b/x-pack/plugins/alerts/server/plugin.test.ts index e65d195290259e..910a808702d998 100644 --- a/x-pack/plugins/alerts/server/plugin.test.ts +++ b/x-pack/plugins/alerts/server/plugin.test.ts @@ -159,7 +159,7 @@ describe('Alerting Plugin', () => { function mockFeatures() { const features = featuresPluginMock.createSetup(); - features.getFeatures.mockReturnValue([ + features.getKibanaFeatures.mockReturnValue([ new Feature({ id: 'appName', name: 'appName', diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index deafda67b806d7..63c8598463d90d 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -125,7 +125,7 @@ export class APMPlugin implements Plugin { }; }); - plugins.features.registerFeature(APM_FEATURE); + plugins.features.registerKibanaFeature(APM_FEATURE); plugins.licensing.featureUsage.register( APM_SERVICE_MAPS_FEATURE_NAME, APM_SERVICE_MAPS_LICENSE_TYPE diff --git a/x-pack/plugins/beats_management/kibana.json b/x-pack/plugins/beats_management/kibana.json index 1b431216ef9923..33091e312e86ca 100644 --- a/x-pack/plugins/beats_management/kibana.json +++ b/x-pack/plugins/beats_management/kibana.json @@ -7,7 +7,8 @@ "requiredPlugins": [ "data", "licensing", - "management" + "management", + "features" ], "optionalPlugins": [ "security" diff --git a/x-pack/plugins/beats_management/server/plugin.ts b/x-pack/plugins/beats_management/server/plugin.ts index a82dbcb4a3a6ed..d3661233e1e956 100644 --- a/x-pack/plugins/beats_management/server/plugin.ts +++ b/x-pack/plugins/beats_management/server/plugin.ts @@ -10,12 +10,14 @@ 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'; interface SetupDeps { security?: SecurityPluginSetup; + features: FeaturesPluginSetup; } interface StartDeps { @@ -30,6 +32,20 @@ export class BeatsManagementPlugin implements Plugin<{}, {}, SetupDeps, StartDep public async setup(core: CoreSetup, plugins: SetupDeps) { this.initializerContext.config.create(); + plugins.features.registerElasticsearchFeature({ + id: 'beats_management', + management: { + ingest: ['beats_management'], + }, + privileges: [ + { + ui: [], + requiredClusterPrivileges: [], + requiredRoles: ['beats_admin'], + }, + ], + }); + return {}; } diff --git a/x-pack/plugins/canvas/server/plugin.ts b/x-pack/plugins/canvas/server/plugin.ts index 4fa7e2d934647d..bc20e13532a9cc 100644 --- a/x-pack/plugins/canvas/server/plugin.ts +++ b/x-pack/plugins/canvas/server/plugin.ts @@ -35,7 +35,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, diff --git a/x-pack/plugins/cross_cluster_replication/kibana.json b/x-pack/plugins/cross_cluster_replication/kibana.json index 13746bb0e34c3a..292820f81adbee 100644 --- a/x-pack/plugins/cross_cluster_replication/kibana.json +++ b/x-pack/plugins/cross_cluster_replication/kibana.json @@ -8,7 +8,8 @@ "licensing", "management", "remoteClusters", - "indexManagement" + "indexManagement", + "features" ], "optionalPlugins": [ "usageCollection" diff --git a/x-pack/plugins/cross_cluster_replication/server/plugin.ts b/x-pack/plugins/cross_cluster_replication/server/plugin.ts index e39b4dfd471a82..d40a53f2898732 100644 --- a/x-pack/plugins/cross_cluster_replication/server/plugin.ts +++ b/x-pack/plugins/cross_cluster_replication/server/plugin.ts @@ -87,7 +87,7 @@ export class CrossClusterReplicationServerPlugin implements Plugin { this.ccrEsClient = this.ccrEsClient ?? (await getCustomEsClient(getStartServices)); return { diff --git a/x-pack/plugins/cross_cluster_replication/server/types.ts b/x-pack/plugins/cross_cluster_replication/server/types.ts index c287acf86eb2b6..62c96b48c43732 100644 --- a/x-pack/plugins/cross_cluster_replication/server/types.ts +++ b/x-pack/plugins/cross_cluster_replication/server/types.ts @@ -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 { diff --git a/x-pack/plugins/enterprise_search/server/lib/check_access.ts b/x-pack/plugins/enterprise_search/server/lib/check_access.ts index 0239cb6422d032..497747f9532894 100644 --- a/x-pack/plugins/enterprise_search/server/lib/check_access.ts +++ b/x-pack/plugins/enterprise_search/server/lib/check_access.ts @@ -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) { diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts index a7bd68f92f78b9..a47b7fedffc3f5 100644 --- a/x-pack/plugins/enterprise_search/server/plugin.ts +++ b/x-pack/plugins/enterprise_search/server/plugin.ts @@ -63,7 +63,7 @@ export class EnterpriseSearchPlugin implements Plugin { /** * Register space/feature control */ - features.registerFeature({ + features.registerKibanaFeature({ id: 'enterpriseSearch', name: 'Enterprise Search', order: 0, diff --git a/x-pack/plugins/features/common/es_feature.ts b/x-pack/plugins/features/common/es_feature.ts new file mode 100644 index 00000000000000..b40c67430bcd05 --- /dev/null +++ b/x-pack/plugins/features/common/es_feature.ts @@ -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 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) {} + + 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; + } +} diff --git a/x-pack/plugins/features/common/feature.ts b/x-pack/plugins/features/common/feature.ts index 1b700fb1a6ad09..a600ada554afd4 100644 --- a/x-pack/plugins/features/common/feature.ts +++ b/x-pack/plugins/features/common/feature.ts @@ -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) { + constructor(protected readonly config: RecursiveReadonly) { 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; } } diff --git a/x-pack/plugins/features/common/feature_elasticsearch_privileges.ts b/x-pack/plugins/features/common/feature_elasticsearch_privileges.ts new file mode 100644 index 00000000000000..6112399f492574 --- /dev/null +++ b/x-pack/plugins/features/common/feature_elasticsearch_privileges.ts @@ -0,0 +1,69 @@ +/* + * 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 + */ + 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[]; +} diff --git a/x-pack/plugins/features/common/index.ts b/x-pack/plugins/features/common/index.ts index e359efbda20d29..b5e15074b96441 100644 --- a/x-pack/plugins/features/common/index.ts +++ b/x-pack/plugins/features/common/index.ts @@ -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 './es_feature'; +export { KibanaFeature as Feature, KibanaFeatureConfig as FeatureConfig } from './feature'; export { SubFeature, SubFeatureConfig, diff --git a/x-pack/plugins/features/server/__snapshots__/feature_registry.test.ts.snap b/x-pack/plugins/features/server/__snapshots__/feature_registry.test.ts.snap index e033b241f9e25d..fdeb53dd2fa125 100644 --- a/x-pack/plugins/features/server/__snapshots__/feature_registry.test.ts.snap +++ b/x-pack/plugins/features/server/__snapshots__/feature_registry.test.ts.snap @@ -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]"`; diff --git a/x-pack/plugins/features/server/feature_registry.test.ts b/x-pack/plugins/features/server/feature_registry.test.ts index f123068e417584..af44a0872b87db 100644 --- a/x-pack/plugins/features/server/feature_registry.test.ts +++ b/x-pack/plugins/features/server/feature_registry.test.ts @@ -5,1192 +5,1390 @@ */ import { FeatureRegistry } from './feature_registry'; -import { FeatureConfig } from '../common/feature'; +import { KibanaFeatureConfig } from '../common/feature'; +import { ElasticsearchFeatureConfig } from '../common'; describe('FeatureRegistry', () => { - it('allows a minimal feature to be registered', () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - privileges: null, - }; + describe('Kibana Features', () => { + it('allows a minimal feature to be registered', () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + privileges: null, + }; - const featureRegistry = new FeatureRegistry(); - featureRegistry.register(feature); - const result = featureRegistry.getAll(); - expect(result).toHaveLength(1); + const featureRegistry = new FeatureRegistry(); + featureRegistry.registerKibanaFeature(feature); + const result = featureRegistry.getAllKibanaFeatures(); + expect(result).toHaveLength(1); - // Should be the equal, but not the same instance (i.e., a defensive copy) - expect(result[0].toRaw()).not.toBe(feature); - expect(result[0].toRaw()).toEqual(feature); - }); + // Should be the equal, but not the same instance (i.e., a defensive copy) + expect(result[0].toRaw()).not.toBe(feature); + expect(result[0].toRaw()).toEqual(feature); + }); - it('allows a complex feature to be registered', () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - excludeFromBasePrivileges: true, - icon: 'addDataApp', - navLinkId: 'someNavLink', - app: ['app1'], - validLicenses: ['standard', 'basic', 'gold', 'platinum'], - catalogue: ['foo'], - management: { - foo: ['bar'], - }, - privileges: { - all: { - catalogue: ['foo'], - management: { - foo: ['bar'], - }, - app: ['app1'], - savedObject: { - all: ['space', 'etc', 'telemetry'], - read: ['canvas', 'config', 'url'], - }, - api: ['someApiEndpointTag', 'anotherEndpointTag'], - ui: ['allowsFoo', 'showBar', 'showBaz'], + it('allows a complex feature to be registered', () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + excludeFromBasePrivileges: true, + icon: 'addDataApp', + navLinkId: 'someNavLink', + app: ['app1'], + validLicenses: ['standard', 'basic', 'gold', 'platinum'], + catalogue: ['foo'], + management: { + foo: ['bar'], }, - read: { - savedObject: { - all: [], - read: ['config', 'url'], + privileges: { + all: { + catalogue: ['foo'], + management: { + foo: ['bar'], + }, + app: ['app1'], + savedObject: { + all: ['space', 'etc', 'telemetry'], + read: ['canvas', 'config', 'url'], + }, + api: ['someApiEndpointTag', 'anotherEndpointTag'], + ui: ['allowsFoo', 'showBar', 'showBaz'], + }, + read: { + savedObject: { + all: [], + read: ['config', 'url'], + }, + ui: [], }, - ui: [], }, - }, - subFeatures: [ - { - name: 'sub-feature-1', - privilegeGroups: [ - { - groupType: 'independent', - privileges: [ - { - id: 'foo', - name: 'foo', - includeIn: 'read', - savedObject: { - all: [], - read: [], + subFeatures: [ + { + name: 'sub-feature-1', + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'foo', + name: 'foo', + includeIn: 'read', + savedObject: { + all: [], + read: [], + }, + ui: [], }, - ui: [], - }, - ], - }, - { - groupType: 'mutually_exclusive', - privileges: [ - { - id: 'bar', - name: 'bar', - includeIn: 'all', - savedObject: { - all: [], - read: [], + ], + }, + { + groupType: 'mutually_exclusive', + privileges: [ + { + id: 'bar', + name: 'bar', + includeIn: 'all', + savedObject: { + all: [], + read: [], + }, + ui: [], }, - ui: [], - }, - { - id: 'baz', - name: 'baz', - includeIn: 'none', - savedObject: { - all: [], - read: [], + { + id: 'baz', + name: 'baz', + includeIn: 'none', + savedObject: { + all: [], + read: [], + }, + ui: [], }, - ui: [], + ], + }, + ], + }, + ], + privilegesTooltip: 'some fancy tooltip', + reserved: { + privileges: [ + { + id: 'reserved', + privilege: { + catalogue: ['foo'], + management: { + foo: ['bar'], }, - ], + app: ['app1'], + savedObject: { + all: ['space', 'etc', 'telemetry'], + read: ['canvas', 'config', 'url'], + }, + api: ['someApiEndpointTag', 'anotherEndpointTag'], + ui: ['allowsFoo', 'showBar', 'showBaz'], + }, }, ], + description: 'some completely adequate description', }, - ], - privilegesTooltip: 'some fancy tooltip', - reserved: { - privileges: [ - { - id: 'reserved', - privilege: { - catalogue: ['foo'], - management: { - foo: ['bar'], - }, - app: ['app1'], - savedObject: { - all: ['space', 'etc', 'telemetry'], - read: ['canvas', 'config', 'url'], - }, - api: ['someApiEndpointTag', 'anotherEndpointTag'], - ui: ['allowsFoo', 'showBar', 'showBaz'], - }, - }, - ], - description: 'some completely adequate description', - }, - }; + }; - const featureRegistry = new FeatureRegistry(); - featureRegistry.register(feature); - const result = featureRegistry.getAll(); - expect(result).toHaveLength(1); + const featureRegistry = new FeatureRegistry(); + featureRegistry.registerKibanaFeature(feature); + const result = featureRegistry.getAllKibanaFeatures(); + expect(result).toHaveLength(1); - // Should be the equal, but not the same instance (i.e., a defensive copy) - expect(result[0].toRaw()).not.toBe(feature); - expect(result[0].toRaw()).toEqual(feature); - }); + // Should be the equal, but not the same instance (i.e., a defensive copy) + expect(result[0].toRaw()).not.toBe(feature); + expect(result[0].toRaw()).toEqual(feature); + }); - it(`requires a value for privileges`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - } as any; + it(`requires a value for privileges`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + } as any; - const featureRegistry = new FeatureRegistry(); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"child \\"privileges\\" fails because [\\"privileges\\" is required]"` - ); - }); + const featureRegistry = new FeatureRegistry(); + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"child \\"privileges\\" fails because [\\"privileges\\" is required]"` + ); + }); - it(`does not allow sub-features to be registered when no primary privileges are not registered`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - privileges: null, - subFeatures: [ - { - name: 'my sub feature', - privilegeGroups: [ - { - groupType: 'independent', - privileges: [ - { - id: 'my-sub-priv', - name: 'my sub priv', - includeIn: 'none', - savedObject: { - all: [], - read: [], + it(`does not allow sub-features to be registered when no primary privileges are not registered`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + privileges: null, + subFeatures: [ + { + name: 'my sub feature', + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'my-sub-priv', + name: 'my sub priv', + includeIn: 'none', + savedObject: { + all: [], + read: [], + }, + ui: [], }, - ui: [], - }, - ], - }, - ], - }, - ], - }; + ], + }, + ], + }, + ], + }; - const featureRegistry = new FeatureRegistry(); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"child \\"subFeatures\\" fails because [\\"subFeatures\\" must contain less than or equal to 0 items]"` - ); - }); + const featureRegistry = new FeatureRegistry(); + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"child \\"subFeatures\\" fails because [\\"subFeatures\\" must contain less than or equal to 0 items]"` + ); + }); - it(`automatically grants 'all' access to telemetry saved objects for the 'all' privilege`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - privileges: { - all: { - ui: [], - savedObject: { - all: [], - read: [], + it(`automatically grants 'all' access to telemetry saved objects for the 'all' privilege`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + privileges: { + all: { + ui: [], + savedObject: { + all: [], + read: [], + }, }, - }, - read: { - ui: [], - savedObject: { - all: [], - read: [], + read: { + ui: [], + savedObject: { + all: [], + read: [], + }, }, }, - }, - }; + }; - const featureRegistry = new FeatureRegistry(); - featureRegistry.register(feature); - const result = featureRegistry.getAll(); + const featureRegistry = new FeatureRegistry(); + featureRegistry.registerKibanaFeature(feature); + const result = featureRegistry.getAllKibanaFeatures(); - expect(result[0].privileges).toHaveProperty('all'); - expect(result[0].privileges).toHaveProperty('read'); + expect(result[0].privileges).toHaveProperty('all'); + expect(result[0].privileges).toHaveProperty('read'); - const allPrivilege = result[0].privileges?.all; - expect(allPrivilege?.savedObject.all).toEqual(['telemetry']); - }); + const allPrivilege = result[0].privileges?.all; + expect(allPrivilege?.savedObject.all).toEqual(['telemetry']); + }); - it(`automatically grants 'read' access to config and url saved objects for both privileges`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - privileges: { - all: { - ui: [], - savedObject: { - all: [], - read: [], + it(`automatically grants 'read' access to config and url saved objects for both privileges`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + privileges: { + all: { + ui: [], + savedObject: { + all: [], + read: [], + }, }, - }, - read: { - ui: [], - savedObject: { - all: [], - read: [], + read: { + ui: [], + savedObject: { + all: [], + read: [], + }, }, }, - }, - }; + }; - const featureRegistry = new FeatureRegistry(); - featureRegistry.register(feature); - const result = featureRegistry.getAll(); + const featureRegistry = new FeatureRegistry(); + featureRegistry.registerKibanaFeature(feature); + const result = featureRegistry.getAllKibanaFeatures(); - expect(result[0].privileges).toHaveProperty('all'); - expect(result[0].privileges).toHaveProperty('read'); + expect(result[0].privileges).toHaveProperty('all'); + expect(result[0].privileges).toHaveProperty('read'); - const allPrivilege = result[0].privileges?.all; - const readPrivilege = result[0].privileges?.read; - expect(allPrivilege?.savedObject.read).toEqual(['config', 'url']); - expect(readPrivilege?.savedObject.read).toEqual(['config', 'url']); - }); + const allPrivilege = result[0].privileges?.all; + const readPrivilege = result[0].privileges?.read; + expect(allPrivilege?.savedObject.read).toEqual(['config', 'url']); + expect(readPrivilege?.savedObject.read).toEqual(['config', 'url']); + }); - it(`automatically grants 'all' access to telemetry and 'read' to [config, url] saved objects for the reserved privilege`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - privileges: null, - reserved: { - description: 'foo', - privileges: [ - { - id: 'reserved', - privilege: { - ui: [], - savedObject: { - all: [], - read: [], + it(`automatically grants 'all' access to telemetry and 'read' to [config, url] saved objects for the reserved privilege`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + privileges: null, + reserved: { + description: 'foo', + privileges: [ + { + id: 'reserved', + privilege: { + ui: [], + savedObject: { + all: [], + read: [], + }, }, }, - }, - ], - }, - }; + ], + }, + }; - const featureRegistry = new FeatureRegistry(); - featureRegistry.register(feature); - const result = featureRegistry.getAll(); + const featureRegistry = new FeatureRegistry(); + featureRegistry.registerKibanaFeature(feature); + const result = featureRegistry.getAllKibanaFeatures(); - const reservedPrivilege = result[0]!.reserved!.privileges[0].privilege; - expect(reservedPrivilege.savedObject.all).toEqual(['telemetry']); - expect(reservedPrivilege.savedObject.read).toEqual(['config', 'url']); - }); + const reservedPrivilege = result[0]!.reserved!.privileges[0].privilege; + expect(reservedPrivilege.savedObject.all).toEqual(['telemetry']); + expect(reservedPrivilege.savedObject.read).toEqual(['config', 'url']); + }); - it(`does not duplicate the automatic grants if specified on the incoming feature`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - privileges: { - all: { - ui: [], - savedObject: { - all: ['telemetry'], - read: ['config', 'url'], + it(`does not duplicate the automatic grants if specified on the incoming feature`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + privileges: { + all: { + ui: [], + savedObject: { + all: ['telemetry'], + read: ['config', 'url'], + }, }, - }, - read: { - ui: [], - savedObject: { - all: [], - read: ['config', 'url'], + read: { + ui: [], + savedObject: { + all: [], + read: ['config', 'url'], + }, }, }, - }, - }; - - const featureRegistry = new FeatureRegistry(); - featureRegistry.register(feature); - const result = featureRegistry.getAll(); + }; - expect(result[0].privileges).toHaveProperty('all'); - expect(result[0].privileges).toHaveProperty('read'); - - const allPrivilege = result[0].privileges!.all; - const readPrivilege = result[0].privileges!.read; - expect(allPrivilege?.savedObject.all).toEqual(['telemetry']); - expect(allPrivilege?.savedObject.read).toEqual(['config', 'url']); - expect(readPrivilege?.savedObject.read).toEqual(['config', 'url']); - }); - - it(`does not allow duplicate features to be registered`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - privileges: null, - }; + const featureRegistry = new FeatureRegistry(); + featureRegistry.registerKibanaFeature(feature); + const result = featureRegistry.getAllKibanaFeatures(); - const duplicateFeature: FeatureConfig = { - id: 'test-feature', - name: 'Duplicate Test Feature', - app: [], - privileges: null, - }; + expect(result[0].privileges).toHaveProperty('all'); + expect(result[0].privileges).toHaveProperty('read'); - const featureRegistry = new FeatureRegistry(); - featureRegistry.register(feature); + const allPrivilege = result[0].privileges!.all; + const readPrivilege = result[0].privileges!.read; + expect(allPrivilege?.savedObject.all).toEqual(['telemetry']); + expect(allPrivilege?.savedObject.read).toEqual(['config', 'url']); + expect(readPrivilege?.savedObject.read).toEqual(['config', 'url']); + }); - expect(() => featureRegistry.register(duplicateFeature)).toThrowErrorMatchingInlineSnapshot( - `"Feature with id test-feature is already registered."` - ); - }); + it(`does not allow duplicate features to be registered`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + privileges: null, + }; + + const duplicateFeature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Duplicate Test Feature', + app: [], + privileges: null, + }; - ['contains space', 'contains_invalid()_chars', ''].forEach((prohibitedChars) => { - it(`prevents features from being registered with a navLinkId of "${prohibitedChars}"`, () => { const featureRegistry = new FeatureRegistry(); + featureRegistry.registerKibanaFeature(feature); + expect(() => - featureRegistry.register({ - id: 'foo', - name: 'some feature', - navLinkId: prohibitedChars, - app: [], - privileges: null, - }) - ).toThrowErrorMatchingSnapshot(); + featureRegistry.registerKibanaFeature(duplicateFeature) + ).toThrowErrorMatchingInlineSnapshot(`"Feature with id test-feature is already registered."`); }); - it(`prevents features from being registered with a management id of "${prohibitedChars}"`, () => { - const featureRegistry = new FeatureRegistry(); - expect(() => - featureRegistry.register({ - id: 'foo', - name: 'some feature', - management: { - kibana: [prohibitedChars], - }, - app: [], - privileges: null, - }) - ).toThrowErrorMatchingSnapshot(); + ['contains space', 'contains_invalid()_chars', ''].forEach((prohibitedChars) => { + it(`prevents features from being registered with a navLinkId of "${prohibitedChars}"`, () => { + const featureRegistry = new FeatureRegistry(); + expect(() => + featureRegistry.registerKibanaFeature({ + id: 'foo', + name: 'some feature', + navLinkId: prohibitedChars, + app: [], + privileges: null, + }) + ).toThrowErrorMatchingSnapshot(); + }); + + it(`prevents features from being registered with a management id of "${prohibitedChars}"`, () => { + const featureRegistry = new FeatureRegistry(); + expect(() => + featureRegistry.registerKibanaFeature({ + id: 'foo', + name: 'some feature', + management: { + kibana: [prohibitedChars], + }, + app: [], + privileges: null, + }) + ).toThrowErrorMatchingSnapshot(); + }); + + it(`prevents features from being registered with a catalogue entry of "${prohibitedChars}"`, () => { + const featureRegistry = new FeatureRegistry(); + expect(() => + featureRegistry.registerKibanaFeature({ + id: 'foo', + name: 'some feature', + catalogue: [prohibitedChars], + app: [], + privileges: null, + }) + ).toThrowErrorMatchingSnapshot(); + }); }); - it(`prevents features from being registered with a catalogue entry of "${prohibitedChars}"`, () => { - const featureRegistry = new FeatureRegistry(); - expect(() => - featureRegistry.register({ - id: 'foo', - name: 'some feature', - catalogue: [prohibitedChars], - app: [], - privileges: null, - }) - ).toThrowErrorMatchingSnapshot(); + ['catalogue', 'management', 'navLinks', `doesn't match valid regex`].forEach((prohibitedId) => { + it(`prevents features from being registered with an ID of "${prohibitedId}"`, () => { + const featureRegistry = new FeatureRegistry(); + expect(() => + featureRegistry.registerKibanaFeature({ + id: prohibitedId, + name: 'some feature', + app: [], + privileges: null, + }) + ).toThrowErrorMatchingSnapshot(); + }); }); - }); - ['catalogue', 'management', 'navLinks', `doesn't match valid regex`].forEach((prohibitedId) => { - it(`prevents features from being registered with an ID of "${prohibitedId}"`, () => { + it('prevents features from being registered with invalid privilege names', () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: ['app1', 'app2'], + privileges: { + foo: { + name: 'Foo', + app: ['app1', 'app2'], + savedObject: { + all: ['config', 'space', 'etc'], + read: ['canvas'], + }, + api: ['someApiEndpointTag', 'anotherEndpointTag'], + ui: ['allowsFoo', 'showBar', 'showBaz'], + }, + } as any, + }; + const featureRegistry = new FeatureRegistry(); expect(() => - featureRegistry.register({ - id: prohibitedId, - name: 'some feature', - app: [], - privileges: null, - }) - ).toThrowErrorMatchingSnapshot(); + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"child \\"privileges\\" fails because [\\"foo\\" is not allowed]"` + ); }); - }); - it('prevents features from being registered with invalid privilege names', () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: ['app1', 'app2'], - privileges: { - foo: { - name: 'Foo', - app: ['app1', 'app2'], - savedObject: { - all: ['config', 'space', 'etc'], - read: ['canvas'], + it(`prevents privileges from specifying app entries that don't exist at the root level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: ['bar'], + privileges: { + all: { + savedObject: { + all: [], + read: [], + }, + ui: [], + app: ['foo', 'bar', 'baz'], + }, + read: { + savedObject: { + all: [], + read: [], + }, + ui: [], + app: ['foo', 'bar', 'baz'], }, - api: ['someApiEndpointTag', 'anotherEndpointTag'], - ui: ['allowsFoo', 'showBar', 'showBaz'], }, - } as any, - }; + }; - const featureRegistry = new FeatureRegistry(); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"child \\"privileges\\" fails because [\\"foo\\" is not allowed]"` - ); - }); + const featureRegistry = new FeatureRegistry(); - it(`prevents privileges from specifying app entries that don't exist at the root level`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: ['bar'], - privileges: { - all: { - savedObject: { - all: [], - read: [], + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.all has unknown app entries: foo, baz"` + ); + }); + + it(`prevents features from specifying app entries that don't exist at the privilege level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: ['foo', 'bar', 'baz'], + privileges: { + all: { + savedObject: { + all: [], + read: [], + }, + ui: [], + app: ['bar'], }, - ui: [], - app: ['foo', 'bar', 'baz'], - }, - read: { - savedObject: { - all: [], - read: [], + read: { + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], }, - ui: [], - app: ['foo', 'bar', 'baz'], }, - }, - }; + subFeatures: [ + { + name: 'my sub feature', + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'cool-sub-feature-privilege', + name: 'cool privilege', + includeIn: 'none', + savedObject: { + all: [], + read: [], + }, + ui: [], + app: ['foo'], + }, + ], + }, + ], + }, + ], + }; - const featureRegistry = new FeatureRegistry(); + const featureRegistry = new FeatureRegistry(); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"Feature privilege test-feature.all has unknown app entries: foo, baz"` - ); - }); + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature test-feature specifies app entries which are not granted to any privileges: baz"` + ); + }); - it(`prevents features from specifying app entries that don't exist at the privilege level`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: ['foo', 'bar', 'baz'], - privileges: { - all: { - savedObject: { - all: [], - read: [], - }, - ui: [], - app: ['bar'], - }, - read: { - savedObject: { - all: [], - read: [], - }, - ui: [], - app: [], - }, - }, - subFeatures: [ - { - name: 'my sub feature', - privilegeGroups: [ + it(`prevents reserved privileges from specifying app entries that don't exist at the root level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: ['bar'], + privileges: null, + reserved: { + description: 'something', + privileges: [ { - groupType: 'independent', - privileges: [ - { - id: 'cool-sub-feature-privilege', - name: 'cool privilege', - includeIn: 'none', - savedObject: { - all: [], - read: [], - }, - ui: [], - app: ['foo'], + id: 'reserved', + privilege: { + savedObject: { + all: [], + read: [], }, - ], + ui: [], + app: ['foo', 'bar', 'baz'], + }, }, ], }, - ], - }; + }; - const featureRegistry = new FeatureRegistry(); + const featureRegistry = new FeatureRegistry(); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"Feature test-feature specifies app entries which are not granted to any privileges: baz"` - ); - }); + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.reserved has unknown app entries: foo, baz"` + ); + }); - it(`prevents reserved privileges from specifying app entries that don't exist at the root level`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: ['bar'], - privileges: null, - reserved: { - description: 'something', - privileges: [ - { - id: 'reserved', - privilege: { - savedObject: { - all: [], - read: [], + it(`prevents features from specifying app entries that don't exist at the reserved privilege level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: ['foo', 'bar', 'baz'], + privileges: null, + reserved: { + description: 'something', + privileges: [ + { + id: 'reserved', + privilege: { + savedObject: { + all: [], + read: [], + }, + ui: [], + app: ['foo', 'bar'], }, - ui: [], - app: ['foo', 'bar', 'baz'], }, - }, - ], - }, - }; + ], + }, + }; - const featureRegistry = new FeatureRegistry(); + const featureRegistry = new FeatureRegistry(); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"Feature privilege test-feature.reserved has unknown app entries: foo, baz"` - ); - }); + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature test-feature specifies app entries which are not granted to any privileges: baz"` + ); + }); - it(`prevents features from specifying app entries that don't exist at the reserved privilege level`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: ['foo', 'bar', 'baz'], - privileges: null, - reserved: { - description: 'something', - privileges: [ - { - id: 'reserved', - privilege: { - savedObject: { - all: [], - read: [], - }, - ui: [], - app: ['foo', 'bar'], + it(`prevents privileges from specifying catalogue entries that don't exist at the root level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + catalogue: ['bar'], + privileges: { + all: { + catalogue: ['foo', 'bar', 'baz'], + savedObject: { + all: [], + read: [], }, + ui: [], + app: [], }, - ], - }, - }; + read: { + catalogue: ['foo', 'bar', 'baz'], + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + }; - const featureRegistry = new FeatureRegistry(); + const featureRegistry = new FeatureRegistry(); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"Feature test-feature specifies app entries which are not granted to any privileges: baz"` - ); - }); + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.all has unknown catalogue entries: foo, baz"` + ); + }); - it(`prevents privileges from specifying catalogue entries that don't exist at the root level`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - catalogue: ['bar'], - privileges: { - all: { - catalogue: ['foo', 'bar', 'baz'], - savedObject: { - all: [], - read: [], + it(`prevents features from specifying catalogue entries that don't exist at the privilege level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + catalogue: ['foo', 'bar', 'baz'], + privileges: { + all: { + catalogue: ['foo'], + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], }, - ui: [], - app: [], - }, - read: { - catalogue: ['foo', 'bar', 'baz'], - savedObject: { - all: [], - read: [], + read: { + catalogue: ['foo'], + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], }, - ui: [], - app: [], }, - }, - }; + subFeatures: [ + { + name: 'my sub feature', + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'cool-sub-feature-privilege', + name: 'cool privilege', + includeIn: 'none', + savedObject: { + all: [], + read: [], + }, + ui: [], + catalogue: ['bar'], + }, + ], + }, + ], + }, + ], + }; - const featureRegistry = new FeatureRegistry(); + const featureRegistry = new FeatureRegistry(); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"Feature privilege test-feature.all has unknown catalogue entries: foo, baz"` - ); - }); + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature test-feature specifies catalogue entries which are not granted to any privileges: baz"` + ); + }); - it(`prevents features from specifying catalogue entries that don't exist at the privilege level`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - catalogue: ['foo', 'bar', 'baz'], - privileges: { - all: { - catalogue: ['foo'], - savedObject: { - all: [], - read: [], - }, - ui: [], - app: [], - }, - read: { - catalogue: ['foo'], - savedObject: { - all: [], - read: [], - }, - ui: [], - app: [], - }, - }, - subFeatures: [ - { - name: 'my sub feature', - privilegeGroups: [ + it(`prevents reserved privileges from specifying catalogue entries that don't exist at the root level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + catalogue: ['bar'], + privileges: null, + reserved: { + description: 'something', + privileges: [ { - groupType: 'independent', - privileges: [ - { - id: 'cool-sub-feature-privilege', - name: 'cool privilege', - includeIn: 'none', - savedObject: { - all: [], - read: [], - }, - ui: [], - catalogue: ['bar'], + id: 'reserved', + privilege: { + catalogue: ['foo', 'bar', 'baz'], + savedObject: { + all: [], + read: [], }, - ], + ui: [], + app: [], + }, }, ], }, - ], - }; + }; - const featureRegistry = new FeatureRegistry(); + const featureRegistry = new FeatureRegistry(); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"Feature test-feature specifies catalogue entries which are not granted to any privileges: baz"` - ); - }); + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.reserved has unknown catalogue entries: foo, baz"` + ); + }); - it(`prevents reserved privileges from specifying catalogue entries that don't exist at the root level`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - catalogue: ['bar'], - privileges: null, - reserved: { - description: 'something', - privileges: [ - { - id: 'reserved', - privilege: { - catalogue: ['foo', 'bar', 'baz'], - savedObject: { - all: [], - read: [], + it(`prevents features from specifying catalogue entries that don't exist at the reserved privilege level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + catalogue: ['foo', 'bar', 'baz'], + privileges: null, + reserved: { + description: 'something', + privileges: [ + { + id: 'reserved', + privilege: { + catalogue: ['foo', 'bar'], + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], }, - ui: [], - app: [], }, - }, - ], - }, - }; + ], + }, + }; - const featureRegistry = new FeatureRegistry(); + const featureRegistry = new FeatureRegistry(); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"Feature privilege test-feature.reserved has unknown catalogue entries: foo, baz"` - ); - }); + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature test-feature specifies catalogue entries which are not granted to any privileges: baz"` + ); + }); - it(`prevents features from specifying catalogue entries that don't exist at the reserved privilege level`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - catalogue: ['foo', 'bar', 'baz'], - privileges: null, - reserved: { - description: 'something', - privileges: [ - { - id: 'reserved', - privilege: { - catalogue: ['foo', 'bar'], - savedObject: { - all: [], - read: [], - }, - ui: [], - app: [], + it(`prevents privileges from specifying alerting entries that don't exist at the root level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + alerting: ['bar'], + privileges: { + all: { + alerting: { + all: ['foo', 'bar'], + read: ['baz'], + }, + savedObject: { + all: [], + read: [], }, + ui: [], + app: [], }, - ], - }, - }; + read: { + alerting: { read: ['foo', 'bar', 'baz'] }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + }; - const featureRegistry = new FeatureRegistry(); + const featureRegistry = new FeatureRegistry(); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"Feature test-feature specifies catalogue entries which are not granted to any privileges: baz"` - ); - }); + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.all has unknown alerting entries: foo, baz"` + ); + }); - it(`prevents privileges from specifying alerting entries that don't exist at the root level`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - alerting: ['bar'], - privileges: { - all: { - alerting: { - all: ['foo', 'bar'], - read: ['baz'], + it(`prevents features from specifying alerting entries that don't exist at the privilege level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + alerting: ['foo', 'bar', 'baz'], + privileges: { + all: { + alerting: { all: ['foo'] }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], }, - savedObject: { - all: [], - read: [], + read: { + alerting: { all: ['foo'] }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], }, - ui: [], - app: [], }, - read: { - alerting: { read: ['foo', 'bar', 'baz'] }, - savedObject: { - all: [], - read: [], + subFeatures: [ + { + name: 'my sub feature', + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'cool-sub-feature-privilege', + name: 'cool privilege', + includeIn: 'none', + savedObject: { + all: [], + read: [], + }, + ui: [], + alerting: { all: ['bar'] }, + }, + ], + }, + ], }, - ui: [], - app: [], - }, - }, - }; + ], + }; - const featureRegistry = new FeatureRegistry(); + const featureRegistry = new FeatureRegistry(); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"Feature privilege test-feature.all has unknown alerting entries: foo, baz"` - ); - }); + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature test-feature specifies alerting entries which are not granted to any privileges: baz"` + ); + }); - it(`prevents features from specifying alerting entries that don't exist at the privilege level`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - alerting: ['foo', 'bar', 'baz'], - privileges: { - all: { - alerting: { all: ['foo'] }, - savedObject: { - all: [], - read: [], - }, - ui: [], - app: [], - }, - read: { - alerting: { all: ['foo'] }, - savedObject: { - all: [], - read: [], - }, - ui: [], - app: [], - }, - }, - subFeatures: [ - { - name: 'my sub feature', - privilegeGroups: [ + it(`prevents reserved privileges from specifying alerting entries that don't exist at the root level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + alerting: ['bar'], + privileges: null, + reserved: { + description: 'something', + privileges: [ { - groupType: 'independent', - privileges: [ - { - id: 'cool-sub-feature-privilege', - name: 'cool privilege', - includeIn: 'none', - savedObject: { - all: [], - read: [], - }, - ui: [], - alerting: { all: ['bar'] }, + id: 'reserved', + privilege: { + alerting: { all: ['foo', 'bar', 'baz'] }, + savedObject: { + all: [], + read: [], }, - ], + ui: [], + app: [], + }, }, ], }, - ], - }; + }; - const featureRegistry = new FeatureRegistry(); + const featureRegistry = new FeatureRegistry(); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"Feature test-feature specifies alerting entries which are not granted to any privileges: baz"` - ); - }); + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.reserved has unknown alerting entries: foo, baz"` + ); + }); - it(`prevents reserved privileges from specifying alerting entries that don't exist at the root level`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - alerting: ['bar'], - privileges: null, - reserved: { - description: 'something', - privileges: [ - { - id: 'reserved', - privilege: { - alerting: { all: ['foo', 'bar', 'baz'] }, - savedObject: { - all: [], - read: [], + it(`prevents features from specifying alerting entries that don't exist at the reserved privilege level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + alerting: ['foo', 'bar', 'baz'], + privileges: null, + reserved: { + description: 'something', + privileges: [ + { + id: 'reserved', + privilege: { + alerting: { all: ['foo', 'bar'] }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], }, - ui: [], - app: [], }, - }, - ], - }, - }; + ], + }, + }; - const featureRegistry = new FeatureRegistry(); + const featureRegistry = new FeatureRegistry(); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"Feature privilege test-feature.reserved has unknown alerting entries: foo, baz"` - ); - }); + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature test-feature specifies alerting entries which are not granted to any privileges: baz"` + ); + }); - it(`prevents features from specifying alerting entries that don't exist at the reserved privilege level`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - alerting: ['foo', 'bar', 'baz'], - privileges: null, - reserved: { - description: 'something', - privileges: [ - { - id: 'reserved', - privilege: { - alerting: { all: ['foo', 'bar'] }, - savedObject: { - all: [], - read: [], - }, - ui: [], - app: [], + it(`prevents privileges from specifying management sections that don't exist at the root level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + catalogue: ['bar'], + management: { + kibana: ['hey'], + }, + privileges: { + all: { + catalogue: ['bar'], + management: { + elasticsearch: ['hey'], + }, + savedObject: { + all: [], + read: [], }, + ui: [], + app: [], }, - ], - }, - }; + read: { + catalogue: ['bar'], + management: { + elasticsearch: ['hey'], + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + }; - const featureRegistry = new FeatureRegistry(); + const featureRegistry = new FeatureRegistry(); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"Feature test-feature specifies alerting entries which are not granted to any privileges: baz"` - ); - }); + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.all has unknown management section: elasticsearch"` + ); + }); - it(`prevents privileges from specifying management sections that don't exist at the root level`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - catalogue: ['bar'], - management: { - kibana: ['hey'], - }, - privileges: { - all: { - catalogue: ['bar'], - management: { - elasticsearch: ['hey'], + it(`prevents features from specifying management sections that don't exist at the privilege level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + catalogue: ['bar'], + management: { + kibana: ['hey'], + elasticsearch: ['hey', 'there'], + }, + privileges: { + all: { + catalogue: ['bar'], + management: { + elasticsearch: ['hey'], + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], }, - savedObject: { - all: [], - read: [], + read: { + catalogue: ['bar'], + management: { + elasticsearch: ['hey'], + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], }, - ui: [], - app: [], }, - read: { - catalogue: ['bar'], - management: { - elasticsearch: ['hey'], - }, - savedObject: { - all: [], - read: [], + subFeatures: [ + { + name: 'my sub feature', + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'cool-sub-feature-privilege', + name: 'cool privilege', + includeIn: 'none', + savedObject: { + all: [], + read: [], + }, + ui: [], + management: { + kibana: ['hey'], + elasticsearch: ['hey'], + }, + }, + ], + }, + ], }, - ui: [], - app: [], + ], + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature test-feature specifies management entries which are not granted to any privileges: elasticsearch.there"` + ); + }); + + it(`prevents reserved privileges from specifying management entries that don't exist at the root level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + catalogue: ['bar'], + management: { + kibana: ['hey'], }, - }, - }; + privileges: null, + reserved: { + description: 'something', + privileges: [ + { + id: 'reserved', + privilege: { + catalogue: ['bar'], + management: { + kibana: ['hey-there'], + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + ], + }, + }; - const featureRegistry = new FeatureRegistry(); + const featureRegistry = new FeatureRegistry(); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"Feature privilege test-feature.all has unknown management section: elasticsearch"` - ); - }); + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.reserved has unknown management entries for section kibana: hey-there"` + ); + }); - it(`prevents features from specifying management sections that don't exist at the privilege level`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - catalogue: ['bar'], - management: { - kibana: ['hey'], - elasticsearch: ['hey', 'there'], - }, - privileges: { - all: { - catalogue: ['bar'], - management: { - elasticsearch: ['hey'], - }, - savedObject: { - all: [], - read: [], - }, - ui: [], - app: [], + it(`prevents features from specifying management entries that don't exist at the reserved privilege level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + catalogue: ['bar'], + management: { + kibana: ['hey', 'hey-there'], }, - read: { - catalogue: ['bar'], - management: { - elasticsearch: ['hey'], - }, - savedObject: { - all: [], - read: [], - }, - ui: [], - app: [], + privileges: null, + reserved: { + description: 'something', + privileges: [ + { + id: 'reserved', + privilege: { + catalogue: ['bar'], + management: { + kibana: ['hey-there'], + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + ], }, - }, - subFeatures: [ - { - name: 'my sub feature', - privilegeGroups: [ + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature test-feature specifies management entries which are not granted to any privileges: kibana.hey"` + ); + }); + + it('allows multiple reserved feature privileges to be registered', () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + privileges: null, + reserved: { + description: 'my reserved privileges', + privileges: [ { - groupType: 'independent', - privileges: [ - { - id: 'cool-sub-feature-privilege', - name: 'cool privilege', - includeIn: 'none', - savedObject: { - all: [], - read: [], - }, - ui: [], - management: { - kibana: ['hey'], - elasticsearch: ['hey'], - }, + id: 'a_reserved_1', + privilege: { + savedObject: { + all: [], + read: [], }, - ], + ui: [], + app: [], + }, + }, + { + id: 'a_reserved_2', + privilege: { + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, }, ], }, - ], - }; + }; - const featureRegistry = new FeatureRegistry(); + const featureRegistry = new FeatureRegistry(); + featureRegistry.registerKibanaFeature(feature); + const result = featureRegistry.getAllKibanaFeatures(); + expect(result).toHaveLength(1); + expect(result[0].reserved?.privileges).toHaveLength(2); + }); + + it('does not allow reserved privilege ids to start with "reserved_"', () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + privileges: null, + reserved: { + description: 'my reserved privileges', + privileges: [ + { + id: 'reserved_1', + privilege: { + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + ], + }, + }; - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"Feature test-feature specifies management entries which are not granted to any privileges: elasticsearch.there"` - ); + const featureRegistry = new FeatureRegistry(); + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"child \\"reserved\\" fails because [child \\"privileges\\" fails because [\\"privileges\\" at position 0 fails because [child \\"id\\" fails because [\\"id\\" with value \\"reserved_1\\" fails to match the required pattern: /^(?!reserved_)[a-zA-Z0-9_-]+$/]]]]"` + ); + }); + + it('cannot register feature after getAll has been called', () => { + const feature1: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + privileges: null, + }; + const feature2: KibanaFeatureConfig = { + id: 'test-feature-2', + name: 'Test Feature 2', + app: [], + privileges: null, + }; + + const featureRegistry = new FeatureRegistry(); + featureRegistry.registerKibanaFeature(feature1); + featureRegistry.getAllKibanaFeatures(); + expect(() => { + featureRegistry.registerKibanaFeature(feature2); + }).toThrowErrorMatchingInlineSnapshot( + `"Features are locked, can't register new features. Attempt to register test-feature-2 failed."` + ); + }); }); - it(`prevents reserved privileges from specifying management entries that don't exist at the root level`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - catalogue: ['bar'], - management: { - kibana: ['hey'], - }, - privileges: null, - reserved: { - description: 'something', + describe('Elasticsearch Features', () => { + it('allows a minimal feature to be registered', () => { + const feature: ElasticsearchFeatureConfig = { + id: 'test-feature', privileges: [ { - id: 'reserved', - privilege: { - catalogue: ['bar'], - management: { - kibana: ['hey-there'], - }, - savedObject: { - all: [], - read: [], - }, - ui: [], - app: [], - }, + requiredClusterPrivileges: [], + ui: [], }, ], - }, - }; + }; - const featureRegistry = new FeatureRegistry(); + const featureRegistry = new FeatureRegistry(); + featureRegistry.registerElasticsearchFeature(feature); + const result = featureRegistry.getAllElasticsearchFeatures(); + expect(result).toHaveLength(1); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"Feature privilege test-feature.reserved has unknown management entries for section kibana: hey-there"` - ); - }); + // Should be the equal, but not the same instance (i.e., a defensive copy) + expect(result[0].toRaw()).not.toBe(feature); + expect(result[0].toRaw()).toEqual(feature); + }); - it(`prevents features from specifying management entries that don't exist at the reserved privilege level`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - catalogue: ['bar'], - management: { - kibana: ['hey', 'hey-there'], - }, - privileges: null, - reserved: { - description: 'something', + it('allows a complex feature to ge registered', () => { + const feature: ElasticsearchFeatureConfig = { + id: 'test-feature', + management: { + kibana: ['foo'], + data: ['bar'], + }, + catalogue: ['foo', 'bar'], privileges: [ { - id: 'reserved', - privilege: { - catalogue: ['bar'], - management: { - kibana: ['hey-there'], - }, - savedObject: { - all: [], - read: [], - }, - ui: [], - app: [], + requiredClusterPrivileges: ['monitor', 'manage'], + requiredIndexPrivileges: { + foo: ['read'], + bar: ['all'], + baz: ['view_index_metadata'], }, + ui: ['ui_a'], + }, + { + requiredClusterPrivileges: [], + requiredRoles: ['some_role'], + ui: ['ui_b'], }, ], - }, - }; + }; - const featureRegistry = new FeatureRegistry(); + const featureRegistry = new FeatureRegistry(); + featureRegistry.registerElasticsearchFeature(feature); + const result = featureRegistry.getAllElasticsearchFeatures(); + expect(result).toHaveLength(1); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"Feature test-feature specifies management entries which are not granted to any privileges: kibana.hey"` - ); - }); + // Should be the equal, but not the same instance (i.e., a defensive copy) + expect(result[0].toRaw()).not.toBe(feature); + expect(result[0].toRaw()).toEqual(feature); + }); - it('allows multiple reserved feature privileges to be registered', () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - privileges: null, - reserved: { - description: 'my reserved privileges', + it('requires a value for privileges', () => { + const feature: ElasticsearchFeatureConfig = { + id: 'test-feature', + } as any; + const featureRegistry = new FeatureRegistry(); + expect(() => + featureRegistry.registerElasticsearchFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"child \\"privileges\\" fails because [\\"privileges\\" is required]"` + ); + }); + + it('requires privileges to declare requiredClusterPrivileges', () => { + const feature: ElasticsearchFeatureConfig = { + id: 'test-feature', privileges: [ { - id: 'a_reserved_1', - privilege: { - savedObject: { - all: [], - read: [], - }, - ui: [], - app: [], - }, + ui: [], }, + ], + } as any; + const featureRegistry = new FeatureRegistry(); + expect(() => + featureRegistry.registerElasticsearchFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"child \\"privileges\\" fails because [\\"privileges\\" at position 0 fails because [child \\"requiredClusterPrivileges\\" fails because [\\"requiredClusterPrivileges\\" is required]]]"` + ); + }); + + it('does not allow duplicate privilege ids', () => { + const feature: ElasticsearchFeatureConfig = { + id: 'test-feature', + privileges: [ { - id: 'a_reserved_2', - privilege: { - savedObject: { - all: [], - read: [], - }, - ui: [], - app: [], - }, + requiredClusterPrivileges: [], + ui: [], }, ], - }, - }; - - const featureRegistry = new FeatureRegistry(); - featureRegistry.register(feature); - const result = featureRegistry.getAll(); - expect(result).toHaveLength(1); - expect(result[0].reserved?.privileges).toHaveLength(2); + }; + const featureRegistry = new FeatureRegistry(); + featureRegistry.registerElasticsearchFeature(feature); + expect(() => + featureRegistry.registerElasticsearchFeature(feature) + ).toThrowErrorMatchingInlineSnapshot(`"Feature with id test-feature is already registered."`); + }); }); - it('does not allow reserved privilege ids to start with "reserved_"', () => { - const feature: FeatureConfig = { + it('does not allow a Kibana feature to share an id with an Elasticsearch feature', () => { + const kibanaFeature: KibanaFeatureConfig = { id: 'test-feature', name: 'Test Feature', app: [], privileges: null, - reserved: { - description: 'my reserved privileges', - privileges: [ - { - id: 'reserved_1', - privilege: { - savedObject: { - all: [], - read: [], - }, - ui: [], - app: [], - }, - }, - ], - }, + }; + + const elasticsearchFeature: ElasticsearchFeatureConfig = { + id: 'test-feature', + privileges: [ + { + requiredClusterPrivileges: [], + ui: [], + }, + ], }; const featureRegistry = new FeatureRegistry(); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"child \\"reserved\\" fails because [child \\"privileges\\" fails because [\\"privileges\\" at position 0 fails because [child \\"id\\" fails because [\\"id\\" with value \\"reserved_1\\" fails to match the required pattern: /^(?!reserved_)[a-zA-Z0-9_-]+$/]]]]"` - ); + featureRegistry.registerElasticsearchFeature(elasticsearchFeature); + expect(() => + featureRegistry.registerKibanaFeature(kibanaFeature) + ).toThrowErrorMatchingInlineSnapshot(`"Feature with id test-feature is already registered."`); }); - it('cannot register feature after getAll has been called', () => { - const feature1: FeatureConfig = { + it('does not allow an Elasticsearch feature to share an id with a Kibana feature', () => { + const kibanaFeature: KibanaFeatureConfig = { id: 'test-feature', name: 'Test Feature', app: [], privileges: null, }; - const feature2: FeatureConfig = { - id: 'test-feature-2', - name: 'Test Feature 2', - app: [], - privileges: null, + + const elasticsearchFeature: ElasticsearchFeatureConfig = { + id: 'test-feature', + privileges: [ + { + requiredClusterPrivileges: [], + ui: [], + }, + ], }; const featureRegistry = new FeatureRegistry(); - featureRegistry.register(feature1); - featureRegistry.getAll(); - expect(() => { - featureRegistry.register(feature2); - }).toThrowErrorMatchingInlineSnapshot( - `"Features are locked, can't register new features. Attempt to register test-feature-2 failed."` - ); + featureRegistry.registerKibanaFeature(kibanaFeature); + expect(() => + featureRegistry.registerElasticsearchFeature(elasticsearchFeature) + ).toThrowErrorMatchingInlineSnapshot(`"Feature with id test-feature is already registered."`); }); }); diff --git a/x-pack/plugins/features/server/feature_registry.ts b/x-pack/plugins/features/server/feature_registry.ts index 12aafd226f754c..31f2873e8c4722 100644 --- a/x-pack/plugins/features/server/feature_registry.ts +++ b/x-pack/plugins/features/server/feature_registry.ts @@ -5,34 +5,66 @@ */ import { cloneDeep, uniq } from 'lodash'; -import { FeatureConfig, Feature, FeatureKibanaPrivileges } from '../common'; -import { validateFeature } from './feature_schema'; +import { + FeatureConfig, + Feature, + FeatureKibanaPrivileges, + ElasticsearchFeatureConfig, + ElasticsearchFeature, +} from '../common'; +import { validateKibanaFeature, validateElasticsearchFeature } from './feature_schema'; export class FeatureRegistry { private locked = false; - private features: Record = {}; + private kibanaFeatures: Record = {}; + private esFeatures: Record = {}; - public register(feature: FeatureConfig) { + public registerKibanaFeature(feature: FeatureConfig) { 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(): Feature[] { + this.locked = true; + return Object.values(this.kibanaFeatures).map((featureConfig) => new Feature(featureConfig)); + } + + public getAllElasticsearchFeatures(): ElasticsearchFeature[] { this.locked = true; - return Object.values(this.features).map((featureConfig) => new Feature(featureConfig)); + return Object.values(this.esFeatures).map( + (featureConfig) => new ElasticsearchFeature(featureConfig) + ); } } diff --git a/x-pack/plugins/features/server/feature_schema.ts b/x-pack/plugins/features/server/feature_schema.ts index 95298603d706ad..92dfe32ef9722f 100644 --- a/x-pack/plugins/features/server/feature_schema.ts +++ b/x-pack/plugins/features/server/feature_schema.ts @@ -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/feature'; +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. @@ -74,7 +74,7 @@ const subFeatureSchema = Joi.object({ ), }); -const schema = Joi.object({ +const kibanaFeatureSchema = Joi.object({ id: Joi.string() .regex(featurePrivilegePartRegex) .invalid(...prohibitedFeatureIds) @@ -117,8 +117,30 @@ const schema = Joi.object({ }), }); -export function validateFeature(feature: FeatureConfig) { - const validateResult = Joi.validate(feature, schema); +const elasticsearchFeatureSchema = Joi.object({ + id: Joi.string() + .regex(featurePrivilegePartRegex) + .invalid(...prohibitedFeatureIds) + .required(), + management: managementSchema, + catalogue: catalogueSchema, + privileges: Joi.array() + .items( + Joi.object({ + ui: Joi.array().items(Joi.string()).required(), + requiredClusterPrivileges: Joi.array().items(Joi.string()).required(), + requiredIndexPrivileges: Joi.object().pattern( + Joi.string(), + Joi.array().items(Joi.string()) + ), + requiredRoles: Joi.array().items(Joi.string()), + }) + ) + .required(), +}); + +export function validateKibanaFeature(feature: KibanaFeatureConfig) { + const validateResult = Joi.validate(feature, kibanaFeatureSchema); if (validateResult.error) { throw validateResult.error; } @@ -303,3 +325,10 @@ export function validateFeature(feature: FeatureConfig) { ); } } + +export function validateElasticsearchFeature(feature: ElasticsearchFeatureConfig) { + const validateResult = Joi.validate(feature, elasticsearchFeatureSchema); + if (validateResult.error) { + throw validateResult.error; + } +} diff --git a/x-pack/plugins/features/server/index.ts b/x-pack/plugins/features/server/index.ts index 48a350ae8f8fde..13d5f9047a322b 100644 --- a/x-pack/plugins/features/server/index.ts +++ b/x-pack/plugins/features/server/index.ts @@ -13,7 +13,14 @@ import { Plugin } from './plugin'; // run-time contracts. export { uiCapabilitiesRegex } from './feature_schema'; -export { Feature, FeatureConfig, FeatureKibanaPrivileges } from '../common'; +export { + Feature, + FeatureConfig, + FeatureKibanaPrivileges, + ElasticsearchFeature, + ElasticsearchFeatureConfig, + FeatureElasticsearchPrivileges, +} from '../common'; export { PluginSetupContract, PluginStartContract } from './plugin'; export const plugin = (initializerContext: PluginInitializerContext) => diff --git a/x-pack/plugins/features/server/mocks.ts b/x-pack/plugins/features/server/mocks.ts index d9437169a7453b..91c297c50e4622 100644 --- a/x-pack/plugins/features/server/mocks.ts +++ b/x-pack/plugins/features/server/mocks.ts @@ -8,15 +8,18 @@ import { PluginSetupContract, PluginStartContract } from './plugin'; const createSetup = (): jest.Mocked => { 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 => { return { - getFeatures: jest.fn(), + getKibanaFeatures: jest.fn(), + getElasticsearchFeatures: jest.fn(), }; }; diff --git a/x-pack/plugins/features/server/oss_features.ts b/x-pack/plugins/features/server/oss_features.ts index 9df042b45a32e3..bbd6529fa5d8b4 100644 --- a/x-pack/plugins/features/server/oss_features.ts +++ b/x-pack/plugins/features/server/oss_features.ts @@ -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/feature'; export interface BuildOSSFeaturesParams { savedObjectTypes: string[]; @@ -367,10 +367,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, diff --git a/x-pack/plugins/features/server/plugin.test.ts b/x-pack/plugins/features/server/plugin.test.ts index 3d85c2e9eb751c..28cb531d4b15ed 100644 --- a/x-pack/plugins/features/server/plugin.test.ts +++ b/x-pack/plugins/features/server/plugin.test.ts @@ -23,7 +23,7 @@ coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); describe('Features Plugin', () => { it('returns OSS + registered features', async () => { const plugin = new Plugin(initContext); - const { registerFeature } = await plugin.setup(coreSetup, {}); + const { registerKibanaFeature: registerFeature } = await plugin.setup(coreSetup, {}); registerFeature({ id: 'baz', name: 'baz', @@ -31,7 +31,7 @@ describe('Features Plugin', () => { privileges: null, }); - const { getFeatures } = await plugin.start(coreStart); + const { getKibanaFeatures: getFeatures } = await plugin.start(coreStart); expect(getFeatures().map((f) => f.id)).toMatchInlineSnapshot(` Array [ @@ -49,7 +49,7 @@ describe('Features Plugin', () => { it('returns OSS + registered 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({ @@ -59,7 +59,7 @@ describe('Features Plugin', () => { privileges: null, }); - const { getFeatures } = await plugin.start(coreStart); + const { getKibanaFeatures: getFeatures } = await plugin.start(coreStart); expect(getFeatures().map((f) => f.id)).toMatchInlineSnapshot(` Array [ @@ -79,7 +79,7 @@ describe('Features Plugin', () => { it('registers not hidden saved objects types', async () => { const plugin = new Plugin(initContext); await plugin.setup(coreSetup, {}); - const { getFeatures } = await plugin.start(coreStart); + const { getKibanaFeatures: getFeatures } = await plugin.start(coreStart); const soTypes = getFeatures().find((f) => f.id === 'savedObjectsManagement')?.privileges?.all.savedObject diff --git a/x-pack/plugins/features/server/plugin.ts b/x-pack/plugins/features/server/plugin.ts index 5783b20eae6484..3e85970c5c691c 100644 --- a/x-pack/plugins/features/server/plugin.ts +++ b/x-pack/plugins/features/server/plugin.ts @@ -15,27 +15,36 @@ 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 { KibanaFeature, KibanaFeatureConfig } from '../common/feature'; import { uiCapabilitiesForFeatures } from './ui_capabilities_for_features'; import { buildOSSFeatures } from './oss_features'; import { defineRoutes } from './routes'; +import { ElasticsearchFeatureConfig, ElasticsearchFeature } 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,9 +71,19 @@ export class Plugin { }); return deepFreeze({ - registerFeature: this.featureRegistry.register.bind(this.featureRegistry), - getFeatures: this.featureRegistry.getAll.bind(this.featureRegistry), - getFeaturesUICapabilities: () => uiCapabilitiesForFeatures(this.featureRegistry.getAll()), + 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: () => + uiCapabilitiesForFeatures( + this.featureRegistry.getAllKibanaFeatures(), + this.featureRegistry.getAllElasticsearchFeatures() + ), }); } @@ -72,7 +91,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), }); } @@ -93,7 +115,7 @@ export class Plugin { }); for (const feature of features) { - this.featureRegistry.register(feature); + this.featureRegistry.registerKibanaFeature(feature); } } } diff --git a/x-pack/plugins/features/server/routes/index.test.ts b/x-pack/plugins/features/server/routes/index.test.ts index 3d1efc8a479b26..e1a406b74a49d9 100644 --- a/x-pack/plugins/features/server/routes/index.test.ts +++ b/x-pack/plugins/features/server/routes/index.test.ts @@ -24,14 +24,14 @@ describe('GET /api/features', () => { let routeHandler: RequestHandler; 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'], diff --git a/x-pack/plugins/features/server/routes/index.ts b/x-pack/plugins/features/server/routes/index.ts index 147d34d124fca0..b5a4203d7a7685 100644 --- a/x-pack/plugins/features/server/routes/index.ts +++ b/x-pack/plugins/features/server/routes/index.ts @@ -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 diff --git a/x-pack/plugins/features/server/ui_capabilities_for_features.test.ts b/x-pack/plugins/features/server/ui_capabilities_for_features.test.ts index 35dcc4cf42b378..e53ab88714eb35 100644 --- a/x-pack/plugins/features/server/ui_capabilities_for_features.test.ts +++ b/x-pack/plugins/features/server/ui_capabilities_for_features.test.ts @@ -6,9 +6,9 @@ import { uiCapabilitiesForFeatures } from './ui_capabilities_for_features'; import { Feature } from '.'; -import { SubFeaturePrivilegeGroupConfig } from '../common'; +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,101 @@ 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 Feature({ + id: 'newFeature', + name: 'my new feature', + app: ['bar-app'], + privileges: { + all: createKibanaFeaturePrivilege(), + read: createKibanaFeaturePrivilege(), + }, + }), + ], + [] + ) + ).toEqual({ + catalogue: {}, + management: {}, + newFeature: {}, + }); + }); + + it('handles elasticsearch features with no registered capabilities', () => { + expect( + uiCapabilitiesForFeatures( + [], + [ + new ElasticsearchFeature({ + id: 'newFeature', + privileges: [ + { + requiredClusterPrivileges: [], + ui: [], + }, + ], + }), + ] + ) ).toEqual({ catalogue: {}, + management: {}, newFeature: {}, }); }); - it('augments the original uiCapabilities with registered feature capabilities', () => { + it('augments the original uiCapabilities with registered kibana feature capabilities', () => { + expect( + uiCapabilitiesForFeatures( + [ + new Feature({ + 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, + }, + }); + }); + + 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'], - privileges: { - all: createFeaturePrivilege(['capability1', 'capability2']), - read: createFeaturePrivilege(), - }, - }), - ]) + uiCapabilitiesForFeatures( + [], + [ + new ElasticsearchFeature({ + id: 'newFeature', + privileges: [ + { + requiredClusterPrivileges: [], + ui: ['capability1', 'capability2'], + }, + ], + }), + ] + ) ).toEqual({ catalogue: {}, + management: {}, newFeature: { capability1: true, capability2: true, @@ -80,26 +137,66 @@ describe('populateUICapabilities', () => { }); }); - it('combines catalogue entries from multiple features', () => { + it('combines catalogue entries from multiple kibana features', () => { 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 Feature({ + 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 Feature({ + 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 Feature({ + 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 Feature({ + 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,132 @@ 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 Feature({ + id: 'newFeature', + name: 'my new feature', + navLinkId: 'newFeatureNavLink', + app: ['bar-app'], + privileges: { + all: createKibanaFeaturePrivilege(['capability1', 'capability2']), + read: createKibanaFeaturePrivilege(['capability3', 'capability4']), + }, + }), + new Feature({ + id: 'anotherNewFeature', + name: 'another new feature', + app: ['bar-app'], + privileges: { + all: createKibanaFeaturePrivilege(['capability1', 'capability2']), + read: createKibanaFeaturePrivilege(['capability3', 'capability4']), + }, + }), + new Feature({ + 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, + capability2: true, + capability3: true, + 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, @@ -290,6 +485,7 @@ describe('populateUICapabilities', () => { capability4: true, }, catalogue: {}, + management: {}, newFeature: { capability1: true, capability2: true, diff --git a/x-pack/plugins/features/server/ui_capabilities_for_features.ts b/x-pack/plugins/features/server/ui_capabilities_for_features.ts index 2570d4540b6a6a..ad470a3e5cbe46 100644 --- a/x-pack/plugins/features/server/ui_capabilities_for_features.ts +++ b/x-pack/plugins/features/server/ui_capabilities_for_features.ts @@ -6,21 +6,27 @@ import _ from 'lodash'; import { Capabilities as UICapabilities } from '../../../../src/core/server'; -import { Feature } from '../common/feature'; +import { KibanaFeature } from '../common/feature'; +import { ElasticsearchFeature } from '../common'; const ELIGIBLE_FLAT_MERGE_KEYS = ['catalogue'] as const; +const ELIGIBLE_DEEP_MERGE_KEYS = ['management'] as const; interface FeatureCapabilities { [featureId: string]: Record; } -export function uiCapabilitiesForFeatures(features: Feature[]): UICapabilities { - const featureCapabilities: FeatureCapabilities[] = features.map(getCapabilitiesFromFeature); +export function uiCapabilitiesForFeatures( + features: KibanaFeature[], + elasticsearchFeatures: ElasticsearchFeature[] +): UICapabilities { + const featureCapabilities = features.map(getCapabilitiesFromFeature); + const esFeatureCapabilities = elasticsearchFeatures.map(getCapabilitiesFromElasticsearchFeature); - return buildCapabilities(...featureCapabilities); + return buildCapabilities(...featureCapabilities, ...esFeatureCapabilities); } -function getCapabilitiesFromFeature(feature: Feature): FeatureCapabilities { +function getCapabilitiesFromFeature(feature: KibanaFeature): FeatureCapabilities { const UIFeatureCapabilities: FeatureCapabilities = { catalogue: {}, [feature.id]: {}, @@ -39,6 +45,21 @@ function getCapabilitiesFromFeature(feature: Feature): FeatureCapabilities { }; } + 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, + }; + }, {}), + }; + }, {}); + } + const featurePrivileges = Object.values(feature.privileges ?? {}); if (feature.subFeatures) { featurePrivileges.push( @@ -65,6 +86,60 @@ function getCapabilitiesFromFeature(feature: Feature): FeatureCapabilities { return UIFeatureCapabilities; } +function getCapabilitiesFromElasticsearchFeature( + feature: ElasticsearchFeature +): FeatureCapabilities { + const UIFeatureCapabilities: FeatureCapabilities = { + catalogue: {}, + [feature.id]: {}, + }; + + if (feature.catalogue) { + UIFeatureCapabilities.catalogue = { + ...UIFeatureCapabilities.catalogue, + ...feature.catalogue.reduce( + (acc, capability) => ({ + ...acc, + [capability]: true, + }), + {} + ), + }; + } + + 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, + }; + }, {}), + }; + }, {}); + } + + const featurePrivileges = Object.values(feature.privileges ?? {}); + + featurePrivileges.forEach((privilege) => { + UIFeatureCapabilities[feature.id] = { + ...UIFeatureCapabilities[feature.id], + ...privilege.ui.reduce( + (privilegeAcc, capability) => ({ + ...privilegeAcc, + [capability]: true, + }), + {} + ), + }; + }); + + return UIFeatureCapabilities; +} + function buildCapabilities(...allFeatureCapabilities: FeatureCapabilities[]): UICapabilities { return allFeatureCapabilities.reduce((acc, capabilities) => { const mergableCapabilities = _.omit(capabilities, ...ELIGIBLE_FLAT_MERGE_KEYS); @@ -81,6 +156,14 @@ function buildCapabilities(...allFeatureCapabilities: FeatureCapabilities[]): UI }; }); + ELIGIBLE_DEEP_MERGE_KEYS.forEach((key) => { + mergedFeatureCapabilities[key] = _.merge( + {}, + mergedFeatureCapabilities[key], + capabilities[key] + ); + }); + return mergedFeatureCapabilities; }, {} as UICapabilities); } diff --git a/x-pack/plugins/graph/server/plugin.ts b/x-pack/plugins/graph/server/plugin.ts index b2b825fa4683be..d69c592655fb5a 100644 --- a/x-pack/plugins/graph/server/plugin.ts +++ b/x-pack/plugins/graph/server/plugin.ts @@ -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', diff --git a/x-pack/plugins/index_lifecycle_management/kibana.json b/x-pack/plugins/index_lifecycle_management/kibana.json index 1a9f133b846fb7..6bd4d407dc322d 100644 --- a/x-pack/plugins/index_lifecycle_management/kibana.json +++ b/x-pack/plugins/index_lifecycle_management/kibana.json @@ -6,7 +6,8 @@ "requiredPlugins": [ "home", "licensing", - "management" + "management", + "features" ], "optionalPlugins": [ "usageCollection", diff --git a/x-pack/plugins/index_lifecycle_management/server/plugin.ts b/x-pack/plugins/index_lifecycle_management/server/plugin.ts index ed179255226100..e1b8fd8ba80c6a 100644 --- a/x-pack/plugins/index_lifecycle_management/server/plugin.ts +++ b/x-pack/plugins/index_lifecycle_management/server/plugin.ts @@ -53,7 +53,10 @@ export class IndexLifecycleManagementServerPlugin implements Plugin { + async setup( + { http }: CoreSetup, + { licensing, indexManagement, features }: Dependencies + ): Promise { const router = http.createRouter(); const config = await this.config$.pipe(first()).toPromise(); @@ -71,6 +74,19 @@ export class IndexLifecycleManagementServerPlugin implements Plugin { this.dataManagementESClient = this.dataManagementESClient ?? (await getCustomEsClient(getStartServices)); diff --git a/x-pack/plugins/index_management/server/types.ts b/x-pack/plugins/index_management/server/types.ts index cd3eb5dfecd401..bd09c5359a24d5 100644 --- a/x-pack/plugins/index_management/server/types.ts +++ b/x-pack/plugins/index_management/server/types.ts @@ -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 { diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index 7cd6383a9b2e5e..c70cf413350ce9 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -135,8 +135,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', [ { diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts index e5e1194d59ecb9..74f5df8f2cbfec 100644 --- a/x-pack/plugins/ingest_manager/server/plugin.ts +++ b/x-pack/plugins/ingest_manager/server/plugin.ts @@ -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', diff --git a/x-pack/plugins/ingest_pipelines/kibana.json b/x-pack/plugins/ingest_pipelines/kibana.json index 75e5e9b5d6c514..38d28fbba20b44 100644 --- a/x-pack/plugins/ingest_pipelines/kibana.json +++ b/x-pack/plugins/ingest_pipelines/kibana.json @@ -3,7 +3,7 @@ "version": "8.0.0", "server": true, "ui": true, - "requiredPlugins": ["licensing", "management"], + "requiredPlugins": ["licensing", "management", "features"], "optionalPlugins": ["security", "usageCollection"], "configPath": ["xpack", "ingest_pipelines"], "requiredBundles": ["esUiShared", "kibanaReact"] diff --git a/x-pack/plugins/ingest_pipelines/server/plugin.ts b/x-pack/plugins/ingest_pipelines/server/plugin.ts index 7a78bf608b8e1a..12668e7c4eadb5 100644 --- a/x-pack/plugins/ingest_pipelines/server/plugin.ts +++ b/x-pack/plugins/ingest_pipelines/server/plugin.ts @@ -25,7 +25,7 @@ export class IngestPipelinesPlugin implements Plugin { 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 { } ); + 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, diff --git a/x-pack/plugins/ingest_pipelines/server/types.ts b/x-pack/plugins/ingest_pipelines/server/types.ts index 261317daa26d98..c5d9158caa569d 100644 --- a/x-pack/plugins/ingest_pipelines/server/types.ts +++ b/x-pack/plugins/ingest_pipelines/server/types.ts @@ -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; } diff --git a/x-pack/plugins/license_management/kibana.json b/x-pack/plugins/license_management/kibana.json index 3dbf99fced0b07..1f925a453898e2 100644 --- a/x-pack/plugins/license_management/kibana.json +++ b/x-pack/plugins/license_management/kibana.json @@ -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"], diff --git a/x-pack/plugins/license_management/server/plugin.ts b/x-pack/plugins/license_management/server/plugin.ts index 7b1887e4380245..cb973fd9154acf 100644 --- a/x-pack/plugins/license_management/server/plugin.ts +++ b/x-pack/plugins/license_management/server/plugin.ts @@ -13,9 +13,22 @@ import { Dependencies } from './types'; export class LicenseManagementServerPlugin implements Plugin { 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: { diff --git a/x-pack/plugins/license_management/server/types.ts b/x-pack/plugins/license_management/server/types.ts index 5b432b7ff05798..911e47b5130fc0 100644 --- a/x-pack/plugins/license_management/server/types.ts +++ b/x-pack/plugins/license_management/server/types.ts @@ -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; } diff --git a/x-pack/plugins/logstash/kibana.json b/x-pack/plugins/logstash/kibana.json index 5949d5db041f2e..0d14312a154e03 100644 --- a/x-pack/plugins/logstash/kibana.json +++ b/x-pack/plugins/logstash/kibana.json @@ -5,7 +5,8 @@ "configPath": ["xpack", "logstash"], "requiredPlugins": [ "licensing", - "management" + "management", + "features" ], "optionalPlugins": [ "home", diff --git a/x-pack/plugins/logstash/server/plugin.ts b/x-pack/plugins/logstash/server/plugin.ts index eb79e1d2a8d8bc..0347a606a80441 100644 --- a/x-pack/plugins/logstash/server/plugin.ts +++ b/x-pack/plugins/logstash/server/plugin.ts @@ -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) { diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index 7d091099c1aaa8..6db4707feabdab 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -162,7 +162,7 @@ export class MapsPlugin implements Plugin { this._initHomeData(home, core.http.basePath.prepend, currentConfig); - features.registerFeature({ + features.registerKibanaFeature({ id: APP_ID, name: i18n.translate('xpack.maps.featureRegistry.mapsFeatureName', { defaultMessage: 'Maps', diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index 3c3824a785032d..5498d3d190b699 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -77,7 +77,7 @@ export class MlServerPlugin implements Plugin ({ + requiredClusterPrivileges: [], + requiredRoles: [role], + ui: [], + })), + }); + } + /* * Gives synchronous access to the config */ diff --git a/x-pack/plugins/reporting/server/plugin.test.ts b/x-pack/plugins/reporting/server/plugin.test.ts index 420fa8347cdebb..4e7438e857107e 100644 --- a/x-pack/plugins/reporting/server/plugin.test.ts +++ b/x-pack/plugins/reporting/server/plugin.test.ts @@ -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(), diff --git a/x-pack/plugins/reporting/server/plugin.ts b/x-pack/plugins/reporting/server/plugin.ts index cedc9dc14a2376..f52539102aa17a 100644 --- a/x-pack/plugins/reporting/server/plugin.ts +++ b/x-pack/plugins/reporting/server/plugin.ts @@ -49,13 +49,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, @@ -70,6 +71,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`); diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts index 95b06aa39f07e4..2574ae4faf215c 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts @@ -12,6 +12,7 @@ jest.mock('../lib/enqueue_job'); jest.mock('../lib/validate'); import * as Rx from 'rxjs'; +import { featuresPluginMock } from '../../../features/server/mocks'; import { ReportingConfig, ReportingCore } from '../'; import { chromium, @@ -32,6 +33,7 @@ import { Report } from '../lib/store'; const createMockPluginSetup = (setupMock?: any): ReportingInternalSetup => { return { + features: featuresPluginMock.createSetup(), elasticsearch: setupMock.elasticsearch || { legacy: { client: {} } }, basePath: setupMock.basePath, router: setupMock.router, diff --git a/x-pack/plugins/reporting/server/types.ts b/x-pack/plugins/reporting/server/types.ts index ff597b53ea0b0b..19093f274dad03 100644 --- a/x-pack/plugins/reporting/server/types.ts +++ b/x-pack/plugins/reporting/server/types.ts @@ -10,6 +10,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 { SecurityPluginSetup } from '../../security/server'; import { JobStatus } from '../common/types'; @@ -159,6 +160,7 @@ export type ScreenshotsObservableFn = ({ export interface ReportingSetupDeps { licensing: LicensingPluginSetup; + features: FeaturesPluginSetup; security?: SecurityPluginSetup; usageCollection?: UsageCollectionSetup; } diff --git a/x-pack/plugins/rollup/kibana.json b/x-pack/plugins/rollup/kibana.json index e6915f65599ccf..725b563c3674f3 100644 --- a/x-pack/plugins/rollup/kibana.json +++ b/x-pack/plugins/rollup/kibana.json @@ -7,7 +7,8 @@ "requiredPlugins": [ "indexPatternManagement", "management", - "licensing" + "licensing", + "features" ], "optionalPlugins": [ "home", diff --git a/x-pack/plugins/rollup/server/plugin.ts b/x-pack/plugins/rollup/server/plugin.ts index 713852b4d73984..8b3a6355f950d3 100644 --- a/x-pack/plugins/rollup/server/plugin.ts +++ b/x-pack/plugins/rollup/server/plugin.ts @@ -64,7 +64,7 @@ export class RollupPlugin implements Plugin { 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 { } ); + 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 { diff --git a/x-pack/plugins/rollup/server/types.ts b/x-pack/plugins/rollup/server/types.ts index 2a7644de764b2f..290d2df050099c 100644 --- a/x-pack/plugins/rollup/server/types.ts +++ b/x-pack/plugins/rollup/server/types.ts @@ -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 { diff --git a/x-pack/plugins/security/public/management/management_service.test.ts b/x-pack/plugins/security/public/management/management_service.test.ts index ce93fb7c98f416..cd06693a43bf98 100644 --- a/x-pack/plugins/security/public/management/management_service.test.ts +++ b/x-pack/plugins/security/public/management/management_service.test.ts @@ -78,7 +78,10 @@ describe('ManagementService', () => { }); describe('start()', () => { - function startService(initialFeatures: Partial) { + function startService( + initialFeatures: Partial, + canManageSecurity: boolean = true + ) { const { fatalErrors, getStartServices } = coreMock.createSetup(); const licenseSubject = new BehaviorSubject( @@ -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>([ - [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]>); - 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, diff --git a/x-pack/plugins/security/public/management/management_service.ts b/x-pack/plugins/security/public/management/management_service.ts index 199fd917da0714..1fc648c12f80da 100644 --- a/x-pack/plugins/security/public/management/management_service.ts +++ b/x-pack/plugins/security/public/management/management_service.ts @@ -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; } +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; } diff --git a/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_privileges.ts b/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_privileges.ts index 6821c163d817dd..fe497b27bf20a4 100644 --- a/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_privileges.ts +++ b/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_privileges.ts @@ -19,7 +19,7 @@ export const createRawKibanaPrivileges = ( { allowSubFeaturePrivileges = true } = {} ) => { const featuresService = featuresPluginMock.createSetup(); - featuresService.getFeatures.mockReturnValue(features); + featuresService.getKibanaFeatures.mockReturnValue(features); const licensingService = { getFeatures: () => ({ allowSubFeaturePrivileges } as SecurityLicenseFeatures), diff --git a/x-pack/plugins/security/public/plugin.test.tsx b/x-pack/plugins/security/public/plugin.test.tsx index 8cec4fbc2f5a2c..2dc04c59e3b78c 100644 --- a/x-pack/plugins/security/public/plugin.test.tsx +++ b/x-pack/plugins/security/public/plugin.test.tsx @@ -112,7 +112,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, diff --git a/x-pack/plugins/security/public/plugin.tsx b/x-pack/plugins/security/public/plugin.tsx index bef183bd97e8c7..43a9e7dd89cf40 100644 --- a/x-pack/plugins/security/public/plugin.tsx +++ b/x-pack/plugins/security/public/plugin.tsx @@ -140,7 +140,7 @@ export class SecurityPlugin this.sessionTimeout.start(); this.navControlService.start({ core }); if (management) { - this.managementService.start(); + this.managementService.start({ capabilities: core.application.capabilities }); } } diff --git a/x-pack/plugins/security/server/authorization/api_authorization.test.ts b/x-pack/plugins/security/server/authorization/api_authorization.test.ts index 75aa27c3c88c6a..d4ec9a0e0db51a 100644 --- a/x-pack/plugins/security/server/authorization/api_authorization.test.ts +++ b/x-pack/plugins/security/server/authorization/api_authorization.test.ts @@ -94,7 +94,9 @@ describe('initAPIAuthorization', () => { expect(mockResponse.notFound).not.toHaveBeenCalled(); expect(mockPostAuthToolkit.next).toHaveBeenCalledTimes(1); - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockAuthz.actions.api.get('foo')]); + expect(mockCheckPrivileges).toHaveBeenCalledWith({ + kibana: [mockAuthz.actions.api.get('foo')], + }); expect(mockAuthz.mode.useRbacForRequest).toHaveBeenCalledWith(mockRequest); }); @@ -129,7 +131,9 @@ describe('initAPIAuthorization', () => { expect(mockResponse.notFound).toHaveBeenCalledTimes(1); expect(mockPostAuthToolkit.next).not.toHaveBeenCalled(); - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockAuthz.actions.api.get('foo')]); + expect(mockCheckPrivileges).toHaveBeenCalledWith({ + kibana: [mockAuthz.actions.api.get('foo')], + }); expect(mockAuthz.mode.useRbacForRequest).toHaveBeenCalledWith(mockRequest); }); }); diff --git a/x-pack/plugins/security/server/authorization/api_authorization.ts b/x-pack/plugins/security/server/authorization/api_authorization.ts index 0ffd3ba7ba8239..9129330ec947ae 100644 --- a/x-pack/plugins/security/server/authorization/api_authorization.ts +++ b/x-pack/plugins/security/server/authorization/api_authorization.ts @@ -29,7 +29,7 @@ export function initAPIAuthorization( const apiActions = actionTags.map((tag) => actions.api.get(tag.substring(tagPrefix.length))); const checkPrivileges = checkPrivilegesDynamicallyWithRequest(request); - const checkPrivilegesResponse = await checkPrivileges(apiActions); + const checkPrivilegesResponse = await checkPrivileges({ kibana: apiActions }); // we've actually authorized the request if (checkPrivilegesResponse.hasAllRequested) { diff --git a/x-pack/plugins/security/server/authorization/app_authorization.test.ts b/x-pack/plugins/security/server/authorization/app_authorization.test.ts index 1dc072ab2e6e9e..f40d502a9cd7c7 100644 --- a/x-pack/plugins/security/server/authorization/app_authorization.test.ts +++ b/x-pack/plugins/security/server/authorization/app_authorization.test.ts @@ -18,7 +18,7 @@ import { authorizationMock } from './index.mock'; const createFeaturesSetupContractMock = (): FeaturesSetupContract => { const mock = featuresPluginMock.createSetup(); - mock.getFeatures.mockReturnValue([ + mock.getKibanaFeatures.mockReturnValue([ { id: 'foo', name: 'Foo', app: ['foo'], privileges: {} } as any, ]); return mock; @@ -132,7 +132,7 @@ describe('initAppAuthorization', () => { expect(mockResponse.notFound).not.toHaveBeenCalled(); expect(mockPostAuthToolkit.next).toHaveBeenCalledTimes(1); - expect(mockCheckPrivileges).toHaveBeenCalledWith(mockAuthz.actions.app.get('foo')); + expect(mockCheckPrivileges).toHaveBeenCalledWith({ kibana: mockAuthz.actions.app.get('foo') }); expect(mockAuthz.mode.useRbacForRequest).toHaveBeenCalledWith(mockRequest); }); @@ -172,7 +172,7 @@ describe('initAppAuthorization', () => { expect(mockResponse.notFound).toHaveBeenCalledTimes(1); expect(mockPostAuthToolkit.next).not.toHaveBeenCalled(); - expect(mockCheckPrivileges).toHaveBeenCalledWith(mockAuthz.actions.app.get('foo')); + expect(mockCheckPrivileges).toHaveBeenCalledWith({ kibana: mockAuthz.actions.app.get('foo') }); expect(mockAuthz.mode.useRbacForRequest).toHaveBeenCalledWith(mockRequest); }); }); diff --git a/x-pack/plugins/security/server/authorization/app_authorization.ts b/x-pack/plugins/security/server/authorization/app_authorization.ts index 1036997ca821d1..4170fd2cdb38a4 100644 --- a/x-pack/plugins/security/server/authorization/app_authorization.ts +++ b/x-pack/plugins/security/server/authorization/app_authorization.ts @@ -19,7 +19,7 @@ class ProtectedApplications { if (this.applications == null) { this.applications = new Set( this.featuresService - .getFeatures() + .getKibanaFeatures() .map((feature) => feature.app) .flat() ); @@ -63,7 +63,7 @@ export function initAppAuthorization( const checkPrivileges = checkPrivilegesDynamicallyWithRequest(request); const appAction = actions.app.get(appId); - const checkPrivilegesResponse = await checkPrivileges(appAction); + const checkPrivilegesResponse = await checkPrivileges({ kibana: appAction }); logger.debug(`authorizing access to "${appId}"`); // we've actually authorized the request diff --git a/x-pack/plugins/security/server/authorization/authorization_service.test.ts b/x-pack/plugins/security/server/authorization/authorization_service.test.ts index f67e0863086bb2..97e2de7941130d 100644 --- a/x-pack/plugins/security/server/authorization/authorization_service.test.ts +++ b/x-pack/plugins/security/server/authorization/authorization_service.test.ts @@ -76,6 +76,7 @@ it(`#setup returns exposed services`, () => { packageVersion: 'some-version', features: mockFeaturesSetup, getSpacesService: mockGetSpacesService, + getCurrentUser: jest.fn(), }); expect(authz.actions.version).toBe('version:some-version'); @@ -147,10 +148,11 @@ describe('#start', () => { getSpacesService: jest .fn() .mockReturnValue({ getSpaceId: jest.fn(), namespaceToSpaceId: jest.fn() }), + getCurrentUser: jest.fn(), }); const featuresStart = featuresPluginMock.createStart(); - featuresStart.getFeatures.mockReturnValue([]); + featuresStart.getKibanaFeatures.mockReturnValue([]); authorizationService.start({ clusterClient: mockClusterClient, features: featuresStart }); @@ -248,10 +250,11 @@ it('#stop unsubscribes from license and ES updates.', () => { getSpacesService: jest .fn() .mockReturnValue({ getSpaceId: jest.fn(), namespaceToSpaceId: jest.fn() }), + getCurrentUser: jest.fn(), }); const featuresStart = featuresPluginMock.createStart(); - featuresStart.getFeatures.mockReturnValue([]); + featuresStart.getKibanaFeatures.mockReturnValue([]); authorizationService.start({ clusterClient: mockClusterClient, features: featuresStart }); authorizationService.stop(); diff --git a/x-pack/plugins/security/server/authorization/authorization_service.ts b/x-pack/plugins/security/server/authorization/authorization_service.ts index cae273ecac3383..eb4f7724ddb1e3 100644 --- a/x-pack/plugins/security/server/authorization/authorization_service.ts +++ b/x-pack/plugins/security/server/authorization/authorization_service.ts @@ -44,6 +44,7 @@ import { validateReservedPrivileges } from './validate_reserved_privileges'; import { registerPrivilegesWithCluster } from './register_privileges_with_cluster'; import { APPLICATION_PREFIX } from '../../common/constants'; import { SecurityLicense } from '../../common/licensing'; +import { AuthenticatedUser } from '..'; export { Actions } from './actions'; export { CheckSavedObjectsPrivileges } from './check_saved_objects_privileges'; @@ -60,6 +61,7 @@ interface AuthorizationServiceSetupParams { features: FeaturesPluginSetup; kibanaIndexName: string; getSpacesService(): SpacesService | undefined; + getCurrentUser(request: KibanaRequest): AuthenticatedUser | null; } interface AuthorizationServiceStartParams { @@ -97,6 +99,7 @@ export class AuthorizationService { features, kibanaIndexName, getSpacesService, + getCurrentUser, }: AuthorizationServiceSetupParams): AuthorizationServiceSetup { this.logger = loggers.get('authorization'); this.license = license; @@ -139,9 +142,11 @@ export class AuthorizationService { const disableUICapabilities = disableUICapabilitiesFactory( request, - features.getFeatures(), + features.getKibanaFeatures(), + features.getElasticsearchFeatures(), this.logger, - authz + authz, + getCurrentUser(request) ); if (!request.auth.isAuthenticated) { @@ -159,7 +164,7 @@ export class AuthorizationService { } start({ clusterClient, features }: AuthorizationServiceStartParams) { - const allFeatures = features.getFeatures(); + const allFeatures = features.getKibanaFeatures(); validateFeaturePrivileges(allFeatures); validateReservedPrivileges(allFeatures); diff --git a/x-pack/plugins/security/server/authorization/check_privileges.test.ts b/x-pack/plugins/security/server/authorization/check_privileges.test.ts index b380f45a12d814..d236e7dcf7ff74 100644 --- a/x-pack/plugins/security/server/authorization/check_privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges.test.ts @@ -50,7 +50,9 @@ describe('#atSpace', () => { let actualResult; let errorThrown = null; try { - actualResult = await checkPrivileges.atSpace(options.spaceId, options.privilegeOrPrivileges); + actualResult = await checkPrivileges.atSpace(options.spaceId, { + kibana: options.privilegeOrPrivileges, + }); } catch (err) { errorThrown = err; } @@ -58,6 +60,7 @@ describe('#atSpace', () => { expect(mockClusterClient.asScoped).toHaveBeenCalledWith(request); expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.hasPrivileges', { body: { + index: [], applications: [ { application, @@ -100,13 +103,19 @@ describe('#atSpace', () => { expect(result).toMatchInlineSnapshot(` Object { "hasAllRequested": true, - "privileges": Array [ - Object { - "authorized": true, - "privilege": "mock-action:login", - "resource": "space_1", + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object {}, }, - ], + "kibana": Array [ + Object { + "authorized": true, + "privilege": "mock-action:login", + "resource": "space_1", + }, + ], + }, "username": "foo-username", } `); @@ -132,13 +141,19 @@ describe('#atSpace', () => { expect(result).toMatchInlineSnapshot(` Object { "hasAllRequested": false, - "privileges": Array [ - Object { - "authorized": false, - "privilege": "mock-action:login", - "resource": "space_1", + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object {}, }, - ], + "kibana": Array [ + Object { + "authorized": false, + "privilege": "mock-action:login", + "resource": "space_1", + }, + ], + }, "username": "foo-username", } `); @@ -191,18 +206,24 @@ describe('#atSpace', () => { expect(result).toMatchInlineSnapshot(` Object { "hasAllRequested": true, - "privileges": Array [ - Object { - "authorized": true, - "privilege": "saved_object:foo-type/get", - "resource": "space_1", - }, - Object { - "authorized": true, - "privilege": "saved_object:bar-type/get", - "resource": "space_1", + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object {}, }, - ], + "kibana": Array [ + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", + }, + ], + }, "username": "foo-username", } `); @@ -233,18 +254,24 @@ describe('#atSpace', () => { expect(result).toMatchInlineSnapshot(` Object { "hasAllRequested": false, - "privileges": Array [ - Object { - "authorized": false, - "privilege": "saved_object:foo-type/get", - "resource": "space_1", - }, - Object { - "authorized": true, - "privilege": "saved_object:bar-type/get", - "resource": "space_1", + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object {}, }, - ], + "kibana": Array [ + Object { + "authorized": false, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", + }, + ], + }, "username": "foo-username", } `); @@ -319,10 +346,9 @@ describe('#atSpaces', () => { let actualResult; let errorThrown = null; try { - actualResult = await checkPrivileges.atSpaces( - options.spaceIds, - options.privilegeOrPrivileges - ); + actualResult = await checkPrivileges.atSpaces(options.spaceIds, { + kibana: options.privilegeOrPrivileges, + }); } catch (err) { errorThrown = err; } @@ -330,6 +356,7 @@ describe('#atSpaces', () => { expect(mockClusterClient.asScoped).toHaveBeenCalledWith(request); expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.hasPrivileges', { body: { + index: [], applications: [ { application, @@ -376,18 +403,24 @@ describe('#atSpaces', () => { expect(result).toMatchInlineSnapshot(` Object { "hasAllRequested": true, - "privileges": Array [ - Object { - "authorized": true, - "privilege": "mock-action:login", - "resource": "space_1", + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object {}, }, - Object { - "authorized": true, - "privilege": "mock-action:login", - "resource": "space_2", - }, - ], + "kibana": Array [ + Object { + "authorized": true, + "privilege": "mock-action:login", + "resource": "space_1", + }, + Object { + "authorized": true, + "privilege": "mock-action:login", + "resource": "space_2", + }, + ], + }, "username": "foo-username", } `); @@ -417,18 +450,24 @@ describe('#atSpaces', () => { expect(result).toMatchInlineSnapshot(` Object { "hasAllRequested": false, - "privileges": Array [ - Object { - "authorized": true, - "privilege": "mock-action:login", - "resource": "space_1", + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object {}, }, - Object { - "authorized": false, - "privilege": "mock-action:login", - "resource": "space_2", - }, - ], + "kibana": Array [ + Object { + "authorized": true, + "privilege": "mock-action:login", + "resource": "space_1", + }, + Object { + "authorized": false, + "privilege": "mock-action:login", + "resource": "space_2", + }, + ], + }, "username": "foo-username", } `); @@ -520,28 +559,34 @@ describe('#atSpaces', () => { expect(result).toMatchInlineSnapshot(` Object { "hasAllRequested": true, - "privileges": Array [ - Object { - "authorized": true, - "privilege": "saved_object:foo-type/get", - "resource": "space_1", - }, - Object { - "authorized": true, - "privilege": "saved_object:bar-type/get", - "resource": "space_1", - }, - Object { - "authorized": true, - "privilege": "saved_object:foo-type/get", - "resource": "space_2", + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object {}, }, - Object { - "authorized": true, - "privilege": "saved_object:bar-type/get", - "resource": "space_2", - }, - ], + "kibana": Array [ + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", + }, + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_2", + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": "space_2", + }, + ], + }, "username": "foo-username", } `); @@ -578,28 +623,34 @@ describe('#atSpaces', () => { expect(result).toMatchInlineSnapshot(` Object { "hasAllRequested": false, - "privileges": Array [ - Object { - "authorized": true, - "privilege": "saved_object:foo-type/get", - "resource": "space_1", - }, - Object { - "authorized": false, - "privilege": "saved_object:bar-type/get", - "resource": "space_1", - }, - Object { - "authorized": false, - "privilege": "saved_object:foo-type/get", - "resource": "space_2", - }, - Object { - "authorized": false, - "privilege": "saved_object:bar-type/get", - "resource": "space_2", + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object {}, }, - ], + "kibana": Array [ + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", + }, + Object { + "authorized": false, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", + }, + Object { + "authorized": false, + "privilege": "saved_object:foo-type/get", + "resource": "space_2", + }, + Object { + "authorized": false, + "privilege": "saved_object:bar-type/get", + "resource": "space_2", + }, + ], + }, "username": "foo-username", } `); @@ -636,28 +687,34 @@ describe('#atSpaces', () => { expect(result).toMatchInlineSnapshot(` Object { "hasAllRequested": false, - "privileges": Array [ - Object { - "authorized": true, - "privilege": "saved_object:foo-type/get", - "resource": "space_1", - }, - Object { - "authorized": true, - "privilege": "saved_object:bar-type/get", - "resource": "space_1", - }, - Object { - "authorized": false, - "privilege": "saved_object:foo-type/get", - "resource": "space_2", + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object {}, }, - Object { - "authorized": false, - "privilege": "saved_object:bar-type/get", - "resource": "space_2", - }, - ], + "kibana": Array [ + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", + }, + Object { + "authorized": false, + "privilege": "saved_object:foo-type/get", + "resource": "space_2", + }, + Object { + "authorized": false, + "privilege": "saved_object:bar-type/get", + "resource": "space_2", + }, + ], + }, "username": "foo-username", } `); @@ -694,28 +751,34 @@ describe('#atSpaces', () => { expect(result).toMatchInlineSnapshot(` Object { "hasAllRequested": false, - "privileges": Array [ - Object { - "authorized": true, - "privilege": "saved_object:foo-type/get", - "resource": "space_1", + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object {}, }, - Object { - "authorized": true, - "privilege": "saved_object:bar-type/get", - "resource": "space_1", - }, - Object { - "authorized": true, - "privilege": "saved_object:foo-type/get", - "resource": "space_2", - }, - Object { - "authorized": false, - "privilege": "saved_object:bar-type/get", - "resource": "space_2", - }, - ], + "kibana": Array [ + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", + }, + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_2", + }, + Object { + "authorized": false, + "privilege": "saved_object:bar-type/get", + "resource": "space_2", + }, + ], + }, "username": "foo-username", } `); @@ -857,7 +920,7 @@ describe('#globally', () => { let actualResult; let errorThrown = null; try { - actualResult = await checkPrivileges.globally(options.privilegeOrPrivileges); + actualResult = await checkPrivileges.globally({ kibana: options.privilegeOrPrivileges }); } catch (err) { errorThrown = err; } @@ -865,6 +928,7 @@ describe('#globally', () => { expect(mockClusterClient.asScoped).toHaveBeenCalledWith(request); expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.hasPrivileges', { body: { + index: [], applications: [ { application, @@ -906,13 +970,19 @@ describe('#globally', () => { expect(result).toMatchInlineSnapshot(` Object { "hasAllRequested": true, - "privileges": Array [ - Object { - "authorized": true, - "privilege": "mock-action:login", - "resource": undefined, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object {}, }, - ], + "kibana": Array [ + Object { + "authorized": true, + "privilege": "mock-action:login", + "resource": undefined, + }, + ], + }, "username": "foo-username", } `); @@ -937,13 +1007,19 @@ describe('#globally', () => { expect(result).toMatchInlineSnapshot(` Object { "hasAllRequested": false, - "privileges": Array [ - Object { - "authorized": false, - "privilege": "mock-action:login", - "resource": undefined, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object {}, }, - ], + "kibana": Array [ + Object { + "authorized": false, + "privilege": "mock-action:login", + "resource": undefined, + }, + ], + }, "username": "foo-username", } `); @@ -1018,18 +1094,24 @@ describe('#globally', () => { expect(result).toMatchInlineSnapshot(` Object { "hasAllRequested": true, - "privileges": Array [ - Object { - "authorized": true, - "privilege": "saved_object:foo-type/get", - "resource": undefined, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object {}, }, - Object { - "authorized": true, - "privilege": "saved_object:bar-type/get", - "resource": undefined, - }, - ], + "kibana": Array [ + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": undefined, + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": undefined, + }, + ], + }, "username": "foo-username", } `); @@ -1059,18 +1141,24 @@ describe('#globally', () => { expect(result).toMatchInlineSnapshot(` Object { "hasAllRequested": false, - "privileges": Array [ - Object { - "authorized": false, - "privilege": "saved_object:foo-type/get", - "resource": undefined, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object {}, }, - Object { - "authorized": true, - "privilege": "saved_object:bar-type/get", - "resource": undefined, - }, - ], + "kibana": Array [ + Object { + "authorized": false, + "privilege": "saved_object:foo-type/get", + "resource": undefined, + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": undefined, + }, + ], + }, "username": "foo-username", } `); diff --git a/x-pack/plugins/security/server/authorization/check_privileges.ts b/x-pack/plugins/security/server/authorization/check_privileges.ts index 3129777a7881f6..ccba95ef845569 100644 --- a/x-pack/plugins/security/server/authorization/check_privileges.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges.ts @@ -8,7 +8,11 @@ import { pick, transform, uniq } from 'lodash'; import { ILegacyClusterClient, KibanaRequest } from '../../../../../src/core/server'; import { GLOBAL_RESOURCE } from '../../common/constants'; import { ResourceSerializer } from './resource_serializer'; -import { HasPrivilegesResponse, HasPrivilegesResponseApplication } from './types'; +import { + HasPrivilegesResponse, + HasPrivilegesResponseApplication, + CheckPrivilegesPayload, +} from './types'; import { validateEsPrivilegeResponse } from './validate_es_response'; interface CheckPrivilegesActions { @@ -19,28 +23,39 @@ interface CheckPrivilegesActions { export interface CheckPrivilegesResponse { hasAllRequested: boolean; username: string; - privileges: Array<{ - /** - * If this attribute is undefined, this element is a privilege for the global resource. - */ - resource?: string; - privilege: string; - authorized: boolean; - }>; + privileges: { + kibana: Array<{ + /** + * If this attribute is undefined, this element is a privilege for the global resource. + */ + resource?: string; + privilege: string; + authorized: boolean; + }>; + elasticsearch: { + cluster: Array<{ + privilege: string; + authorized: boolean; + }>; + index: { + [indexName: string]: Array<{ + privilege: string; + authorized: boolean; + }>; + }; + }; + }; } export type CheckPrivilegesWithRequest = (request: KibanaRequest) => CheckPrivileges; export interface CheckPrivileges { - atSpace( - spaceId: string, - privilegeOrPrivileges: string | string[] - ): Promise; + atSpace(spaceId: string, privileges: CheckPrivilegesPayload): Promise; atSpaces( spaceIds: string[], - privilegeOrPrivileges: string | string[] + privileges: CheckPrivilegesPayload ): Promise; - globally(privilegeOrPrivileges: string | string[]): Promise; + globally(privileges: CheckPrivilegesPayload): Promise; } export function checkPrivilegesWithRequestFactory( @@ -59,17 +74,26 @@ export function checkPrivilegesWithRequestFactory( return function checkPrivilegesWithRequest(request: KibanaRequest): CheckPrivileges { const checkPrivilegesAtResources = async ( resources: string[], - privilegeOrPrivileges: string | string[] + privileges: CheckPrivilegesPayload ): Promise => { - const privileges = Array.isArray(privilegeOrPrivileges) - ? privilegeOrPrivileges - : [privilegeOrPrivileges]; - const allApplicationPrivileges = uniq([actions.version, actions.login, ...privileges]); + const kibanaPrivileges = Array.isArray(privileges.kibana) + ? privileges.kibana + : privileges.kibana + ? [privileges.kibana] + : []; + const allApplicationPrivileges = uniq([actions.version, actions.login, ...kibanaPrivileges]); const hasPrivilegesResponse = (await clusterClient .asScoped(request) .callAsCurrentUser('shield.hasPrivileges', { body: { + cluster: privileges.elasticsearch?.cluster, + index: Object.entries(privileges.elasticsearch?.index ?? {}).map( + ([names, indexPrivileges]) => ({ + names, + privileges: indexPrivileges, + }) + ), applications: [ { application: applicationName, resources, privileges: allApplicationPrivileges }, ], @@ -85,6 +109,28 @@ export function checkPrivilegesWithRequestFactory( const applicationPrivilegesResponse = hasPrivilegesResponse.application[applicationName]; + const clusterPrivilegesResponse = hasPrivilegesResponse.cluster ?? {}; + + const clusterPrivileges = Object.entries(clusterPrivilegesResponse).map( + ([privilege, authorized]) => ({ + privilege, + authorized, + }) + ); + + const indexPrivileges = Object.entries(hasPrivilegesResponse.index ?? {}).reduce( + (acc, [index, indexResponse]) => { + return { + ...acc, + [index]: Object.entries(indexResponse).map(([privilege, authorized]) => ({ + privilege, + authorized, + })), + }; + }, + {} as CheckPrivilegesResponse['privileges']['elasticsearch']['index'] + ); + if (hasIncompatibleVersion(applicationPrivilegesResponse)) { throw new Error( 'Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.' @@ -93,7 +139,7 @@ export function checkPrivilegesWithRequestFactory( // we need to filter out the non requested privileges from the response const resourcePrivileges = transform(applicationPrivilegesResponse, (result, value, key) => { - result[key!] = pick(value, privileges); + result[key!] = pick(value, privileges.kibana ?? []); }) as HasPrivilegesResponseApplication; const privilegeArray = Object.entries(resourcePrivileges) .map(([key, val]) => { @@ -111,23 +157,29 @@ export function checkPrivilegesWithRequestFactory( return { hasAllRequested: hasPrivilegesResponse.has_all_requested, username: hasPrivilegesResponse.username, - privileges: privilegeArray, + privileges: { + kibana: privilegeArray, + elasticsearch: { + cluster: clusterPrivileges, + index: indexPrivileges, + }, + }, }; }; return { - async atSpace(spaceId: string, privilegeOrPrivileges: string | string[]) { + async atSpace(spaceId: string, privileges: CheckPrivilegesPayload) { const spaceResource = ResourceSerializer.serializeSpaceResource(spaceId); - return await checkPrivilegesAtResources([spaceResource], privilegeOrPrivileges); + return await checkPrivilegesAtResources([spaceResource], privileges); }, - async atSpaces(spaceIds: string[], privilegeOrPrivileges: string | string[]) { + async atSpaces(spaceIds: string[], privileges: CheckPrivilegesPayload) { const spaceResources = spaceIds.map((spaceId) => ResourceSerializer.serializeSpaceResource(spaceId) ); - return await checkPrivilegesAtResources(spaceResources, privilegeOrPrivileges); + return await checkPrivilegesAtResources(spaceResources, privileges); }, - async globally(privilegeOrPrivileges: string | string[]) { - return await checkPrivilegesAtResources([GLOBAL_RESOURCE], privilegeOrPrivileges); + async globally(privileges: CheckPrivilegesPayload) { + return await checkPrivilegesAtResources([GLOBAL_RESOURCE], privileges); }, }; }; diff --git a/x-pack/plugins/security/server/authorization/check_privileges_dynamically.test.ts b/x-pack/plugins/security/server/authorization/check_privileges_dynamically.test.ts index 2206748597635b..093b308f59391e 100644 --- a/x-pack/plugins/security/server/authorization/check_privileges_dynamically.test.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges_dynamically.test.ts @@ -24,11 +24,13 @@ test(`checkPrivileges.atSpace when spaces is enabled`, async () => { namespaceToSpaceId: jest.fn(), }) )(request); - const result = await checkPrivilegesDynamically(privilegeOrPrivileges); + const result = await checkPrivilegesDynamically({ kibana: privilegeOrPrivileges }); expect(result).toBe(expectedResult); expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, privilegeOrPrivileges); + expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, { + kibana: privilegeOrPrivileges, + }); }); test(`checkPrivileges.globally when spaces is disabled`, async () => { @@ -43,9 +45,9 @@ test(`checkPrivileges.globally when spaces is disabled`, async () => { mockCheckPrivilegesWithRequest, () => undefined )(request); - const result = await checkPrivilegesDynamically(privilegeOrPrivileges); + const result = await checkPrivilegesDynamically({ kibana: privilegeOrPrivileges }); expect(result).toBe(expectedResult); expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivileges.globally).toHaveBeenCalledWith(privilegeOrPrivileges); + expect(mockCheckPrivileges.globally).toHaveBeenCalledWith({ kibana: privilegeOrPrivileges }); }); diff --git a/x-pack/plugins/security/server/authorization/check_privileges_dynamically.ts b/x-pack/plugins/security/server/authorization/check_privileges_dynamically.ts index 6014bad739e776..579208becd517f 100644 --- a/x-pack/plugins/security/server/authorization/check_privileges_dynamically.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges_dynamically.ts @@ -7,9 +7,10 @@ import { KibanaRequest } from '../../../../../src/core/server'; import { SpacesService } from '../plugin'; import { CheckPrivilegesResponse, CheckPrivilegesWithRequest } from './check_privileges'; +import { CheckPrivilegesPayload } from './types'; export type CheckPrivilegesDynamically = ( - privilegeOrPrivileges: string | string[] + privileges: CheckPrivilegesPayload ) => Promise; export type CheckPrivilegesDynamicallyWithRequest = ( @@ -22,11 +23,11 @@ export function checkPrivilegesDynamicallyWithRequestFactory( ): CheckPrivilegesDynamicallyWithRequest { return function checkPrivilegesDynamicallyWithRequest(request: KibanaRequest) { const checkPrivileges = checkPrivilegesWithRequest(request); - return async function checkPrivilegesDynamically(privilegeOrPrivileges: string | string[]) { + return async function checkPrivilegesDynamically(privileges: CheckPrivilegesPayload) { const spacesService = getSpacesService(); return spacesService - ? await checkPrivileges.atSpace(spacesService.getSpaceId(request), privilegeOrPrivileges) - : await checkPrivileges.globally(privilegeOrPrivileges); + ? await checkPrivileges.atSpace(spacesService.getSpaceId(request), privileges) + : await checkPrivileges.globally(privileges); }; }; } diff --git a/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.test.ts b/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.test.ts index 5e38045b88c748..5a00fa8825eebb 100644 --- a/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.test.ts @@ -68,7 +68,7 @@ describe('#checkSavedObjectsPrivileges', () => { expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(request); expect(mockCheckPrivileges.atSpaces).toHaveBeenCalledTimes(1); const spaceIds = mockSpacesService!.namespaceToSpaceId.mock.results.map((x) => x.value); - expect(mockCheckPrivileges.atSpaces).toHaveBeenCalledWith(spaceIds, actions); + expect(mockCheckPrivileges.atSpaces).toHaveBeenCalledWith(spaceIds, { kibana: actions }); }); }); @@ -87,7 +87,7 @@ describe('#checkSavedObjectsPrivileges', () => { expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(request); expect(mockCheckPrivileges.atSpace).toHaveBeenCalledTimes(1); const spaceId = mockSpacesService!.namespaceToSpaceId.mock.results[0].value; - expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, actions); + expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, { kibana: actions }); }); test(`uses checkPrivileges.globally when Spaces is disabled`, async () => { @@ -102,7 +102,7 @@ describe('#checkSavedObjectsPrivileges', () => { expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledTimes(1); expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(request); expect(mockCheckPrivileges.globally).toHaveBeenCalledTimes(1); - expect(mockCheckPrivileges.globally).toHaveBeenCalledWith(actions); + expect(mockCheckPrivileges.globally).toHaveBeenCalledWith({ kibana: actions }); }); }); }); diff --git a/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.ts b/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.ts index 0c2260542bf728..808c285a689ad5 100644 --- a/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.ts +++ b/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.ts @@ -31,18 +31,18 @@ export const checkSavedObjectsPrivilegesWithRequestFactory = ( const spacesService = getSpacesService(); if (!spacesService) { // Spaces disabled, authorizing globally - return await checkPrivilegesWithRequest(request).globally(actions); + return await checkPrivilegesWithRequest(request).globally({ kibana: actions }); } else if (Array.isArray(namespaceOrNamespaces)) { // Spaces enabled, authorizing against multiple spaces if (!namespaceOrNamespaces.length) { throw new Error(`Can't check saved object privileges for 0 namespaces`); } const spaceIds = namespaceOrNamespaces.map((x) => spacesService.namespaceToSpaceId(x)); - return await checkPrivilegesWithRequest(request).atSpaces(spaceIds, actions); + return await checkPrivilegesWithRequest(request).atSpaces(spaceIds, { kibana: actions }); } else { // Spaces enabled, authorizing against a single space const spaceId = spacesService.namespaceToSpaceId(namespaceOrNamespaces); - return await checkPrivilegesWithRequest(request).atSpace(spaceId, actions); + return await checkPrivilegesWithRequest(request).atSpace(spaceId, { kibana: actions }); } }; }; diff --git a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts index f9405214aac5a1..689f7f25d80e31 100644 --- a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts +++ b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts @@ -9,11 +9,16 @@ import { disableUICapabilitiesFactory } from './disable_ui_capabilities'; import { httpServerMock, loggingSystemMock } from '../../../../../src/core/server/mocks'; import { authorizationMock } from './index.mock'; -import { Feature } from '../../../features/server'; +import { Feature, ElasticsearchFeature } from '../../../features/server'; +import { AuthenticatedUser } from '..'; type MockAuthzOptions = | { rejectCheckPrivileges: any } - | { resolveCheckPrivileges: { privileges: Array<{ privilege: string; authorized: boolean }> } }; + | { + resolveCheckPrivileges: { + privileges: { kibana: Array<{ privilege: string; authorized: boolean }> }; + }; + }; const actions = new Actions('1.0.0-zeta1'); const mockRequest = httpServerMock.createKibanaRequest(); @@ -31,14 +36,25 @@ const createMockAuthz = (options: MockAuthzOptions) => { throw options.rejectCheckPrivileges; } - const expected = options.resolveCheckPrivileges.privileges.map((x) => x.privilege); - expect(checkActions).toEqual(expected); + const expected = options.resolveCheckPrivileges.privileges.kibana.map((x) => x.privilege); + expect(checkActions).toEqual({ kibana: expected, elasticsearch: { cluster: [], index: {} } }); return options.resolveCheckPrivileges; }); }); + mock.checkElasticsearchPrivilegesWithRequest.mockImplementation((request) => { + expect(request).toBe(mockRequest); + return jest.fn().mockImplementation((privileges) => {}); + }); return mock; }; +const createMockUser = (user: Partial = {}) => + ({ + username: 'mock_user', + roles: [], + ...user, + } as AuthenticatedUser); + describe('usingPrivileges', () => { describe('checkPrivileges errors', () => { test(`disables uiCapabilities when a 401 is thrown`, async () => { @@ -58,8 +74,20 @@ describe('usingPrivileges', () => { privileges: null, }), ], + [ + new ElasticsearchFeature({ + id: 'esFeature', + privileges: [ + { + requiredClusterPrivileges: [], + ui: [], + }, + ], + }), + ], mockLoggers.get(), - mockAuthz + mockAuthz, + createMockUser() ); const result = await usingPrivileges( @@ -134,8 +162,20 @@ describe('usingPrivileges', () => { privileges: null, }), ], + [ + new ElasticsearchFeature({ + id: 'esFeature', + privileges: [ + { + requiredClusterPrivileges: [], + ui: [], + }, + ], + }), + ], mockLoggers.get(), - mockAuthz + mockAuthz, + createMockUser() ); const result = await usingPrivileges( @@ -199,8 +239,10 @@ describe('usingPrivileges', () => { const { usingPrivileges } = disableUICapabilitiesFactory( mockRequest, [], + [], mockLoggers.get(), - mockAuthz + mockAuthz, + createMockUser() ); await expect( @@ -234,17 +276,19 @@ describe('usingPrivileges', () => { test(`disables ui capabilities when they don't have privileges`, async () => { const mockAuthz = createMockAuthz({ resolveCheckPrivileges: { - privileges: [ - { privilege: actions.ui.get('navLinks', 'foo'), authorized: true }, - { privilege: actions.ui.get('navLinks', 'bar'), authorized: false }, - { privilege: actions.ui.get('navLinks', 'quz'), authorized: false }, - { privilege: actions.ui.get('management', 'kibana', 'indices'), authorized: true }, - { privilege: actions.ui.get('management', 'kibana', 'settings'), authorized: false }, - { privilege: actions.ui.get('fooFeature', 'foo'), authorized: true }, - { privilege: actions.ui.get('fooFeature', 'bar'), authorized: false }, - { privilege: actions.ui.get('barFeature', 'foo'), authorized: true }, - { privilege: actions.ui.get('barFeature', 'bar'), authorized: false }, - ], + privileges: { + kibana: [ + { privilege: actions.ui.get('navLinks', 'foo'), authorized: true }, + { privilege: actions.ui.get('navLinks', 'bar'), authorized: false }, + { privilege: actions.ui.get('navLinks', 'quz'), authorized: false }, + { privilege: actions.ui.get('management', 'kibana', 'indices'), authorized: true }, + { privilege: actions.ui.get('management', 'kibana', 'settings'), authorized: false }, + { privilege: actions.ui.get('fooFeature', 'foo'), authorized: true }, + { privilege: actions.ui.get('fooFeature', 'bar'), authorized: false }, + { privilege: actions.ui.get('barFeature', 'foo'), authorized: true }, + { privilege: actions.ui.get('barFeature', 'bar'), authorized: false }, + ], + }, }, }); @@ -266,8 +310,20 @@ describe('usingPrivileges', () => { privileges: null, }), ], + [ + new ElasticsearchFeature({ + id: 'esFeature', + privileges: [ + { + requiredClusterPrivileges: [], + ui: [], + }, + ], + }), + ], loggingSystemMock.create().get(), - mockAuthz + mockAuthz, + createMockUser() ); const result = await usingPrivileges( @@ -322,15 +378,17 @@ describe('usingPrivileges', () => { test(`doesn't re-enable disabled uiCapabilities`, async () => { const mockAuthz = createMockAuthz({ resolveCheckPrivileges: { - privileges: [ - { privilege: actions.ui.get('navLinks', 'foo'), authorized: true }, - { privilege: actions.ui.get('navLinks', 'bar'), authorized: true }, - { privilege: actions.ui.get('management', 'kibana', 'indices'), authorized: true }, - { privilege: actions.ui.get('fooFeature', 'foo'), authorized: true }, - { privilege: actions.ui.get('fooFeature', 'bar'), authorized: true }, - { privilege: actions.ui.get('barFeature', 'foo'), authorized: true }, - { privilege: actions.ui.get('barFeature', 'bar'), authorized: true }, - ], + privileges: { + kibana: [ + { privilege: actions.ui.get('navLinks', 'foo'), authorized: true }, + { privilege: actions.ui.get('navLinks', 'bar'), authorized: true }, + { privilege: actions.ui.get('management', 'kibana', 'indices'), authorized: true }, + { privilege: actions.ui.get('fooFeature', 'foo'), authorized: true }, + { privilege: actions.ui.get('fooFeature', 'bar'), authorized: true }, + { privilege: actions.ui.get('barFeature', 'foo'), authorized: true }, + { privilege: actions.ui.get('barFeature', 'bar'), authorized: true }, + ], + }, }, }); @@ -352,8 +410,20 @@ describe('usingPrivileges', () => { privileges: null, }), ], + [ + new ElasticsearchFeature({ + id: 'esFeature', + privileges: [ + { + requiredClusterPrivileges: [], + ui: [], + }, + ], + }), + ], loggingSystemMock.create().get(), - mockAuthz + mockAuthz, + createMockUser() ); const result = await usingPrivileges( @@ -417,8 +487,20 @@ describe('all', () => { privileges: null, }), ], + [ + new ElasticsearchFeature({ + id: 'esFeature', + privileges: [ + { + requiredClusterPrivileges: [], + ui: [], + }, + ], + }), + ], loggingSystemMock.create().get(), - mockAuthz + mockAuthz, + createMockUser() ); const result = all( diff --git a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts index c126be1b07f6e3..31ee0ab93a9902 100644 --- a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts +++ b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts @@ -6,17 +6,25 @@ import { flatten, isObject, mapValues } from 'lodash'; import { UICapabilities } from 'ui/capabilities'; +import { RecursiveReadonlyArray } from '@kbn/utility-types'; import { KibanaRequest, Logger } from '../../../../../src/core/server'; -import { Feature } from '../../../features/server'; +import { + Feature, + ElasticsearchFeature, + FeatureElasticsearchPrivileges, +} from '../../../features/server'; import { CheckPrivilegesResponse } from './check_privileges'; import { AuthorizationServiceSetup } from '.'; +import { AuthenticatedUser } from '..'; export function disableUICapabilitiesFactory( request: KibanaRequest, features: Feature[], + elasticsearchFeatures: ElasticsearchFeature[], logger: Logger, - authz: AuthorizationServiceSetup + authz: AuthorizationServiceSetup, + user: AuthenticatedUser | null ) { // nav links are sourced from the apps property. // The Kibana Platform associates nav links to the app which registers it, in a 1:1 relationship. @@ -25,6 +33,37 @@ export function disableUICapabilitiesFactory( .flatMap((feature) => feature.app) .filter((navLinkId) => navLinkId != null); + const elasticsearchFeatureMap = elasticsearchFeatures.reduce((acc, esFeature) => { + return { + ...acc, + [esFeature.id]: esFeature.privileges, + }; + }, {} as Record>); + + const allRequiredClusterPrivileges = Array.from( + new Set( + Object.values(elasticsearchFeatureMap) + .flat() + .map((p) => p.requiredClusterPrivileges) + .flat() + ) + ); + + const allRequiredIndexPrivileges = Object.values(elasticsearchFeatureMap) + .flat() + .filter((p) => !!p.requiredIndexPrivileges) + .reduce((acc, p) => { + return { + ...acc, + ...Object.entries(p.requiredIndexPrivileges!).reduce((acc2, [indexName, privileges]) => { + return { + ...acc2, + [indexName]: [...(acc[indexName] ?? []), ...privileges], + }; + }, {} as Record), + }; + }, {} as Record); + const shouldDisableFeatureUICapability = ( featureId: keyof UICapabilities, uiCapability: string @@ -85,7 +124,13 @@ export function disableUICapabilitiesFactory( let checkPrivilegesResponse: CheckPrivilegesResponse; try { const checkPrivilegesDynamically = authz.checkPrivilegesDynamicallyWithRequest(request); - checkPrivilegesResponse = await checkPrivilegesDynamically(uiActions); + checkPrivilegesResponse = await checkPrivilegesDynamically({ + kibana: uiActions, + elasticsearch: { + cluster: allRequiredClusterPrivileges, + index: allRequiredIndexPrivileges, + }, + }); } catch (err) { // if we get a 401/403, then we want to disable all uiCapabilities, as this // is generally when the user hasn't authenticated yet and we're displaying the @@ -110,9 +155,42 @@ export function disableUICapabilitiesFactory( } const action = authz.actions.ui.get(featureId, ...uiCapabilityParts); - return checkPrivilegesResponse.privileges.some( + + const hasRequiredKibanaPrivileges = checkPrivilegesResponse.privileges.kibana.some( (x) => x.privilege === action && x.authorized === true ); + + if (hasRequiredKibanaPrivileges) { + return true; + } + + const isCatalogueFeature = featureId === 'catalogue'; + const isManagementFeature = featureId === 'management'; + + if (isCatalogueFeature) { + const [catalogueEntry] = uiCapabilityParts; + return elasticsearchFeatures.some((esFeature) => { + const featureGrantsCatalogueEntry = (esFeature.catalogue ?? []).includes(catalogueEntry); + return ( + featureGrantsCatalogueEntry && + hasRequiredElasticsearchPrivilegesForFeature(esFeature, checkPrivilegesResponse, user) + ); + }); + } else if (isManagementFeature) { + const [managementSectionId, managementEntryId] = uiCapabilityParts; + return elasticsearchFeatures.some((esFeature) => { + const featureGrantsManagementEntry = + (esFeature.management ?? {}).hasOwnProperty(managementSectionId) && + esFeature.management![managementSectionId].includes(managementEntryId); + + return ( + featureGrantsManagementEntry && + hasRequiredElasticsearchPrivilegesForFeature(esFeature, checkPrivilegesResponse, user) + ); + }); + } + + return hasRequiredKibanaPrivileges; }; return mapValues(uiCapabilities, (featureUICapabilities, featureId) => { @@ -151,3 +229,32 @@ export function disableUICapabilitiesFactory( usingPrivileges, }; } + +function hasRequiredElasticsearchPrivilegesForFeature( + esFeature: ElasticsearchFeature, + checkPrivilegesResponse: CheckPrivilegesResponse, + user: AuthenticatedUser | null +) { + return esFeature.privileges.some((privilege) => { + const hasRequiredClusterPrivileges = privilege.requiredClusterPrivileges.every( + (expectedClusterPriv) => + checkPrivilegesResponse.privileges.elasticsearch.cluster.some( + (x) => x.privilege === expectedClusterPriv && x.authorized === true + ) + ); + + const hasRequiredIndexPrivileges = Object.entries( + privilege.requiredIndexPrivileges ?? {} + ).every(([indexName, requiredIndexPrivileges]) => { + return checkPrivilegesResponse.privileges.elasticsearch.index[indexName] + .filter((indexResponse) => requiredIndexPrivileges.includes(indexResponse.privilege)) + .every((indexResponse) => indexResponse.authorized); + }); + + const hasRequiredRoles = (privilege.requiredRoles ?? []).every( + (requiredRole) => user?.roles.includes(requiredRole) ?? false + ); + + return hasRequiredClusterPrivileges && hasRequiredIndexPrivileges && hasRequiredRoles; + }); +} diff --git a/x-pack/plugins/security/server/authorization/index.mock.ts b/x-pack/plugins/security/server/authorization/index.mock.ts index 62b254d132d9ed..6cb78a3001a9b3 100644 --- a/x-pack/plugins/security/server/authorization/index.mock.ts +++ b/x-pack/plugins/security/server/authorization/index.mock.ts @@ -13,6 +13,7 @@ export const authorizationMock = { }: { version?: string; applicationName?: string } = {}) => ({ actions: actionsMock.create(version), checkPrivilegesWithRequest: jest.fn(), + checkElasticsearchPrivilegesWithRequest: jest.fn(), checkPrivilegesDynamicallyWithRequest: jest.fn(), checkSavedObjectsPrivilegesWithRequest: jest.fn(), applicationName, diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts index 89ac73c2207562..4154c5c702f86a 100644 --- a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts @@ -45,7 +45,7 @@ describe('features', () => { ]; const mockFeaturesService = featuresPluginMock.createSetup(); - mockFeaturesService.getFeatures.mockReturnValue(features); + mockFeaturesService.getKibanaFeatures.mockReturnValue(features); const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), @@ -85,13 +85,13 @@ describe('features', () => { }), ]; - const mockXPackMainPlugin = { - getFeatures: jest.fn().mockReturnValue(features), + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), }; - const privileges = privilegesFactory(actions, mockXPackMainPlugin as any, mockLicenseService); + const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); const expectedAllPrivileges = [ actions.login, @@ -169,13 +169,13 @@ describe('features', () => { }), ]; - const mockXPackMainPlugin = { - getFeatures: jest.fn().mockReturnValue(features), + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), }; - const privileges = privilegesFactory(actions, mockXPackMainPlugin as any, mockLicenseService); + const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); const actual = privileges.get(); expect(actual).not.toHaveProperty('features.foo'); @@ -238,13 +238,13 @@ describe('features', () => { }), ]; - const mockXPackMainPlugin = { - getFeatures: jest.fn().mockReturnValue(features), + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), }; - const privileges = privilegesFactory(actions, mockXPackMainPlugin as any, mockLicenseService); + const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); const actual = privileges.get(); expect(actual).toHaveProperty(`${group}.all`, [ @@ -256,6 +256,7 @@ describe('features', () => { actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), + actions.ui.get('catalogue', 'spaces'), ] : []), ...(expectEnterpriseSearch ? [actions.ui.get('enterpriseSearch', 'all')] : []), @@ -357,13 +358,13 @@ describe('features', () => { }), ]; - const mockXPackMainPlugin = { - getFeatures: jest.fn().mockReturnValue(features), + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), }; - const privileges = privilegesFactory(actions, mockXPackMainPlugin as any, mockLicenseService); + const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); const actual = privileges.get(); expect(actual).toHaveProperty(`${group}.read`, [ @@ -431,13 +432,13 @@ describe('features', () => { }), ]; - const mockXPackMainPlugin = { - getFeatures: jest.fn().mockReturnValue(features), + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), }; - const privileges = privilegesFactory(actions, mockXPackMainPlugin as any, mockLicenseService); + const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); const actual = privileges.get(); expect(actual).toHaveProperty(`${group}.all`, [ @@ -449,6 +450,7 @@ describe('features', () => { actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), + actions.ui.get('catalogue', 'spaces'), ] : []), ...(expectEnterpriseSearch ? [actions.ui.get('enterpriseSearch', 'all')] : []), @@ -496,13 +498,13 @@ describe('features', () => { }), ]; - const mockXPackMainPlugin = { - getFeatures: jest.fn().mockReturnValue(features), + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), }; - const privileges = privilegesFactory(actions, mockXPackMainPlugin as any, mockLicenseService); + const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); const actual = privileges.get(); expect(actual).toHaveProperty(`${group}.all`, [ @@ -514,6 +516,7 @@ describe('features', () => { actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), + actions.ui.get('catalogue', 'spaces'), ] : []), ...(expectEnterpriseSearch ? [actions.ui.get('enterpriseSearch', 'all')] : []), @@ -562,13 +565,13 @@ describe('features', () => { }), ]; - const mockXPackMainPlugin = { - getFeatures: jest.fn().mockReturnValue(features), + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), }; - const privileges = privilegesFactory(actions, mockXPackMainPlugin as any, mockLicenseService); + const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); const actual = privileges.get(); expect(actual).toHaveProperty(`${group}.all`, [ @@ -580,6 +583,7 @@ describe('features', () => { actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), + actions.ui.get('catalogue', 'spaces'), ] : []), ...(expectEnterpriseSearch ? [actions.ui.get('enterpriseSearch', 'all')] : []), @@ -621,13 +625,13 @@ describe('reserved', () => { }), ]; - const mockXPackMainPlugin = { - getFeatures: jest.fn().mockReturnValue(features), + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), }; - const privileges = privilegesFactory(actions, mockXPackMainPlugin as any, mockLicenseService); + const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); const actual = privileges.get(); expect(actual).toHaveProperty('reserved.foo', [actions.version]); @@ -659,13 +663,13 @@ describe('reserved', () => { }), ]; - const mockXPackMainPlugin = { - getFeatures: jest.fn().mockReturnValue(features), + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), }; - const privileges = privilegesFactory(actions, mockXPackMainPlugin as any, mockLicenseService); + const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); const actual = privileges.get(); expect(actual).toHaveProperty('reserved.foo', [ @@ -723,13 +727,13 @@ describe('reserved', () => { }), ]; - const mockXPackMainPlugin = { - getFeatures: jest.fn().mockReturnValue(features), + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), }; - const privileges = privilegesFactory(actions, mockXPackMainPlugin as any, mockLicenseService); + const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); const actual = privileges.get(); expect(actual).not.toHaveProperty('reserved.foo'); @@ -786,13 +790,13 @@ describe('subFeatures', () => { }), ]; - const mockXPackMainPlugin = { - getFeatures: jest.fn().mockReturnValue(features), + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), }; - const privileges = privilegesFactory(actions, mockXPackMainPlugin as any, mockLicenseService); + const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); const actual = privileges.get(); expect(actual.features).toHaveProperty(`foo.subFeaturePriv1`, [ @@ -841,6 +845,7 @@ describe('subFeatures', () => { actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), + actions.ui.get('catalogue', 'spaces'), actions.ui.get('enterpriseSearch', 'all'), actions.ui.get('foo', 'foo'), ]); @@ -912,13 +917,13 @@ describe('subFeatures', () => { }), ]; - const mockXPackMainPlugin = { - getFeatures: jest.fn().mockReturnValue(features), + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), }; - const privileges = privilegesFactory(actions, mockXPackMainPlugin as any, mockLicenseService); + const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); const actual = privileges.get(); expect(actual.features).toHaveProperty(`foo.subFeaturePriv1`, [ @@ -993,6 +998,7 @@ describe('subFeatures', () => { actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), + actions.ui.get('catalogue', 'spaces'), actions.ui.get('enterpriseSearch', 'all'), actions.savedObject.get('all-sub-feature-type', 'bulk_get'), actions.savedObject.get('all-sub-feature-type', 'get'), @@ -1111,13 +1117,13 @@ describe('subFeatures', () => { }), ]; - const mockXPackMainPlugin = { - getFeatures: jest.fn().mockReturnValue(features), + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), }; - const privileges = privilegesFactory(actions, mockXPackMainPlugin as any, mockLicenseService); + const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); const actual = privileges.get(); expect(actual.features).toHaveProperty(`foo.subFeaturePriv1`, [ @@ -1192,6 +1198,7 @@ describe('subFeatures', () => { actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), + actions.ui.get('catalogue', 'spaces'), actions.ui.get('enterpriseSearch', 'all'), ]); expect(actual).toHaveProperty('global.read', [actions.login, actions.version]); @@ -1250,13 +1257,13 @@ describe('subFeatures', () => { }), ]; - const mockXPackMainPlugin = { - getFeatures: jest.fn().mockReturnValue(features), + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), }; - const privileges = privilegesFactory(actions, mockXPackMainPlugin as any, mockLicenseService); + const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); const actual = privileges.get(); expect(actual.features).toHaveProperty(`foo.subFeaturePriv1`, [ @@ -1319,6 +1326,7 @@ describe('subFeatures', () => { actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), + actions.ui.get('catalogue', 'spaces'), actions.ui.get('enterpriseSearch', 'all'), actions.savedObject.get('all-sub-feature-type', 'bulk_get'), actions.savedObject.get('all-sub-feature-type', 'get'), @@ -1413,13 +1421,13 @@ describe('subFeatures', () => { }), ]; - const mockXPackMainPlugin = { - getFeatures: jest.fn().mockReturnValue(features), + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), }; - const privileges = privilegesFactory(actions, mockXPackMainPlugin as any, mockLicenseService); + const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); const actual = privileges.get(); expect(actual.features).toHaveProperty(`foo.subFeaturePriv1`, [ @@ -1482,6 +1490,7 @@ describe('subFeatures', () => { actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), + actions.ui.get('catalogue', 'spaces'), actions.ui.get('enterpriseSearch', 'all'), ]); expect(actual).toHaveProperty('global.read', [actions.login, actions.version]); @@ -1540,13 +1549,13 @@ describe('subFeatures', () => { }), ]; - const mockXPackMainPlugin = { - getFeatures: jest.fn().mockReturnValue(features), + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: false }), }; - const privileges = privilegesFactory(actions, mockXPackMainPlugin as any, mockLicenseService); + const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); const actual = privileges.get(); expect(actual.features).not.toHaveProperty(`foo.subFeaturePriv1`); @@ -1598,6 +1607,7 @@ describe('subFeatures', () => { actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), + actions.ui.get('catalogue', 'spaces'), actions.ui.get('enterpriseSearch', 'all'), actions.savedObject.get('all-sub-feature-type', 'bulk_get'), actions.savedObject.get('all-sub-feature-type', 'get'), diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.ts index 5d8ef3f376cac9..b279f54adca758 100644 --- a/x-pack/plugins/security/server/authorization/privileges/privileges.ts +++ b/x-pack/plugins/security/server/authorization/privileges/privileges.ts @@ -28,7 +28,7 @@ export function privilegesFactory( return { get() { - const features = featuresService.getFeatures(); + const features = featuresService.getKibanaFeatures(); const { allowSubFeaturePrivileges } = licenseService.getFeatures(); const basePrivilegeFeatures = features.filter( (feature) => !feature.excludeFromBasePrivileges @@ -100,6 +100,7 @@ export function privilegesFactory( actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), + actions.ui.get('catalogue', 'spaces'), actions.ui.get('enterpriseSearch', 'all'), ...allActions, ], diff --git a/x-pack/plugins/security/server/authorization/types.ts b/x-pack/plugins/security/server/authorization/types.ts index 75188d1191b1a3..9616ee0c8c844f 100644 --- a/x-pack/plugins/security/server/authorization/types.ts +++ b/x-pack/plugins/security/server/authorization/types.ts @@ -16,4 +16,20 @@ export interface HasPrivilegesResponse { application: { [applicationName: string]: HasPrivilegesResponseApplication; }; + cluster?: { + [privilegeName: string]: boolean; + }; + index?: { + [indexName: string]: { + [privilegeName: string]: boolean; + }; + }; +} + +export interface CheckPrivilegesPayload { + kibana?: string | string[]; + elasticsearch?: { + cluster: string[]; + index: Record; + }; } diff --git a/x-pack/plugins/security/server/features/index.ts b/x-pack/plugins/security/server/features/index.ts new file mode 100644 index 00000000000000..3fe097c2bec121 --- /dev/null +++ b/x-pack/plugins/security/server/features/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { securityFeatures } from './security_features'; diff --git a/x-pack/plugins/security/server/features/security_features.ts b/x-pack/plugins/security/server/features/security_features.ts new file mode 100644 index 00000000000000..d80314c077aa2c --- /dev/null +++ b/x-pack/plugins/security/server/features/security_features.ts @@ -0,0 +1,74 @@ +/* + * 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 { ElasticsearchFeatureConfig } from '../../../features/server'; + +const userManagementFeature: ElasticsearchFeatureConfig = { + id: 'users', + management: { + security: ['users'], + }, + catalogue: ['security'], + privileges: [ + { + requiredClusterPrivileges: ['manage_security'], + ui: [], + }, + ], +}; + +const rolesManagementFeature: ElasticsearchFeatureConfig = { + id: 'roles', + management: { + security: ['roles'], + }, + catalogue: ['security'], + privileges: [ + { + requiredClusterPrivileges: ['manage_security'], + ui: [], + }, + ], +}; + +const apiKeysManagementFeature: ElasticsearchFeatureConfig = { + id: 'api_keys', + management: { + security: ['api_keys'], + }, + catalogue: ['security'], + privileges: [ + { + requiredClusterPrivileges: ['manage_api_key'], + ui: [], + }, + { + requiredClusterPrivileges: ['manage_own_api_key'], + ui: [], + }, + ], +}; + +const roleMappingsManagementFeature: ElasticsearchFeatureConfig = { + id: 'role_mappings', + management: { + security: ['role_mappings'], + }, + catalogue: ['security'], + privileges: [ + { + requiredClusterPrivileges: ['manage_security'], + ui: [], + }, + ], +}; + +export const securityFeatures = [ + userManagementFeature, + rolesManagementFeature, + apiKeysManagementFeature, + roleMappingsManagementFeature, +]; diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index db015d246f5917..1b6d705a4015ea 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -11,6 +11,7 @@ import { elasticsearchClientPlugin } from './elasticsearch_client_plugin'; import { Plugin, PluginSetupDependencies } from './plugin'; import { coreMock, elasticsearchServiceMock } from '../../../../src/core/server/mocks'; +import { featuresPluginMock } from '../../features/server/mocks'; describe('Security Plugin', () => { let plugin: Plugin; @@ -48,6 +49,7 @@ describe('Security Plugin', () => { mockDependencies = ({ licensing: { license$: of({}), featureUsage: { register: jest.fn() } }, + features: featuresPluginMock.createSetup(), } as unknown) as PluginSetupDependencies; }); diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 1753eb7b62ed12..1f801cd4449907 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -16,6 +16,7 @@ import { PluginInitializerContext, } from '../../../../src/core/server'; import { SpacesPluginSetup } from '../../spaces/server'; +import { PluginSetupContract as FeaturesSetupContract } from '../../features/server'; import { PluginSetupContract as FeaturesPluginSetup, PluginStartContract as FeaturesPluginStart, @@ -31,12 +32,18 @@ import { setupSavedObjects } from './saved_objects'; import { AuditService, SecurityAuditLogger, AuditServiceSetup } from './audit'; import { elasticsearchClientPlugin } from './elasticsearch_client_plugin'; import { SecurityFeatureUsageService, SecurityFeatureUsageServiceStart } from './feature_usage'; +import { securityFeatures } from './features'; export type SpacesService = Pick< SpacesPluginSetup['spacesService'], 'getSpaceId' | 'namespaceToSpaceId' >; +export type FeaturesService = Pick< + FeaturesSetupContract, + 'getKibanaFeatures' | 'getElasticsearchFeatures' +>; + /** * Describes public Security plugin contract returned at the `setup` stage. */ @@ -139,6 +146,10 @@ export class Plugin { license$: licensing.license$, }); + securityFeatures.forEach((securityFeature) => + features.registerElasticsearchFeature(securityFeature) + ); + this.featureUsageService.setup({ featureUsage: licensing.featureUsage }); const audit = this.auditService.setup({ license, config: config.audit }); @@ -165,6 +176,7 @@ export class Plugin { packageVersion: this.initializerContext.env.packageInfo.version, getSpacesService: this.getSpacesService, features, + getCurrentUser: authc.getCurrentUser, }); setupSavedObjects({ @@ -187,7 +199,7 @@ export class Plugin { getFeatures: () => core .getStartServices() - .then(([, { features: featuresStart }]) => featuresStart.getFeatures()), + .then(([, { features: featuresStart }]) => featuresStart.getKibanaFeatures()), getFeatureUsageService: this.getFeatureUsageService, }); diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts index 1cf879adc54154..0dd81b97d68b76 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts @@ -208,15 +208,17 @@ function getMockCheckPrivilegesSuccess(actions: string | string[], namespaces?: return { hasAllRequested: true, username: USERNAME, - privileges: _namespaces - .map((resource) => - _actions.map((action) => ({ - resource, - privilege: action, - authorized: true, - })) - ) - .flat(), + privileges: { + kibana: _namespaces + .map((resource) => + _actions.map((action) => ({ + resource, + privilege: action, + authorized: true, + })) + ) + .flat(), + }, }; } @@ -232,15 +234,17 @@ function getMockCheckPrivilegesFailure(actions: string | string[], namespaces?: return { hasAllRequested: false, username: USERNAME, - privileges: _namespaces - .map((resource, idxa) => - _actions.map((action, idxb) => ({ - resource, - privilege: action, - authorized: idxa > 0 || idxb > 0, - })) - ) - .flat(), + privileges: { + kibana: _namespaces + .map((resource, idxa) => + _actions.map((action, idxb) => ({ + resource, + privilege: action, + authorized: idxa > 0 || idxb > 0, + })) + ) + .flat(), + }, }; } diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts index 621299a0f025e1..bd5d824c860cfc 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts @@ -223,12 +223,12 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra const { hasAllRequested, username, privileges } = result; const spaceIds = uniq( - privileges.map(({ resource }) => resource).filter((x) => x !== undefined) + privileges.kibana.map(({ resource }) => resource).filter((x) => x !== undefined) ).sort() as string[]; const isAuthorized = (requiresAll && hasAllRequested) || - (!requiresAll && privileges.some(({ authorized }) => authorized)); + (!requiresAll && privileges.kibana.some(({ authorized }) => authorized)); if (isAuthorized) { this.auditLogger.savedObjectsAuthorizationSuccess( username, @@ -256,7 +256,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra } private getMissingPrivileges(privileges: CheckPrivilegesResponse['privileges']) { - return privileges + return privileges.kibana .filter(({ authorized }) => !authorized) .map(({ resource, privilege }) => ({ spaceId: resource, privilege })); } @@ -269,7 +269,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra const action = this.actions.login; const checkPrivilegesResult = await this.checkPrivileges(action, namespaces); // check if the user can log into each namespace - const map = checkPrivilegesResult.privileges.reduce( + const map = checkPrivilegesResult.privileges.kibana.reduce( (acc: Record, { resource, authorized }) => { // there should never be a case where more than one privilege is returned for a given space // if there is, fail-safe (authorized + unauthorized = unauthorized) diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index f2fad16d80414c..dc3627ed4c5124 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -164,7 +164,7 @@ export class Plugin implements IPlugin public async setup( { http, getStartServices }: CoreSetup, - { licensing, security, cloud }: Dependencies + { licensing, features, security, cloud }: Dependencies ): Promise { const pluginConfig = await this.context.config .create() @@ -81,6 +81,19 @@ export class SnapshotRestoreServerPlugin implements Plugin } ); + features.registerElasticsearchFeature({ + id: PLUGIN.id, + management: { + data: [PLUGIN.id], + }, + privileges: [ + { + requiredClusterPrivileges: [...APP_REQUIRED_CLUSTER_PRIVILEGES], + ui: [], + }, + ], + }); + http.registerRouteHandlerContext('snapshotRestore', async (ctx, request) => { this.snapshotRestoreESClient = this.snapshotRestoreESClient ?? (await getCustomEsClient(getStartServices)); diff --git a/x-pack/plugins/snapshot_restore/server/types.ts b/x-pack/plugins/snapshot_restore/server/types.ts index 8cfcaec1a2cd1b..eb51f086deacc3 100644 --- a/x-pack/plugins/snapshot_restore/server/types.ts +++ b/x-pack/plugins/snapshot_restore/server/types.ts @@ -7,12 +7,14 @@ import { LegacyScopedClusterClient, IRouter } from 'src/core/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { SecurityPluginSetup } from '../../security/server'; import { CloudSetup } from '../../cloud/server'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { License } from './services'; import { wrapEsError } from './lib'; import { isEsError } from './shared_imports'; export interface Dependencies { licensing: LicensingPluginSetup; + features: FeaturesPluginSetup; security?: SecurityPluginSetup; cloud?: CloudSetup; } diff --git a/x-pack/plugins/spaces/server/capabilities/capabilities_provider.test.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_provider.test.ts index 8678bdceb70f9b..b0b89afa79d5d4 100644 --- a/x-pack/plugins/spaces/server/capabilities/capabilities_provider.test.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_provider.test.ts @@ -10,6 +10,9 @@ describe('Capabilities provider', () => { it('provides the expected capabilities', () => { expect(capabilitiesProvider()).toMatchInlineSnapshot(` Object { + "catalogue": Object { + "spaces": true, + }, "management": Object { "kibana": Object { "spaces": true, diff --git a/x-pack/plugins/spaces/server/capabilities/capabilities_provider.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_provider.ts index 5976aabfa66e8a..1aaf2ad1df9256 100644 --- a/x-pack/plugins/spaces/server/capabilities/capabilities_provider.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_provider.ts @@ -8,6 +8,9 @@ export const capabilitiesProvider = () => ({ spaces: { manage: true, }, + catalogue: { + spaces: true, + }, management: { kibana: { spaces: true, diff --git a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts index c9ea1b44e723da..c4bf340577f97d 100644 --- a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts @@ -121,7 +121,7 @@ const setup = (space: Space) => { const coreSetup = coreMock.createSetup(); const featuresStart = featuresPluginMock.createStart(); - featuresStart.getFeatures.mockReturnValue(features); + featuresStart.getKibanaFeatures.mockReturnValue(features); coreSetup.getStartServices.mockResolvedValue([ coreMock.createStart(), diff --git a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts index e8d964b22010c6..7d286039ed748c 100644 --- a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts @@ -28,7 +28,7 @@ export function setupCapabilitiesSwitcher( core.getStartServices(), ]); - const registeredFeatures = features.getFeatures(); + const registeredFeatures = features.getKibanaFeatures(); // try to retrieve capabilities for authenticated or "maybe authenticated" users return toggleCapabilities(registeredFeatures, capabilities, activeSpace); diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts index 8375296d869e6d..6db49f2d2992b4 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts @@ -124,7 +124,7 @@ describe.skip('onPostAuthInterceptor', () => { const loggingMock = loggingSystemMock.create().asLoggerFactory().get('xpack', 'spaces'); const featuresPlugin = featuresPluginMock.createSetup(); - featuresPlugin.getFeatures.mockReturnValue(([ + featuresPlugin.getKibanaFeatures.mockReturnValue(([ { id: 'feature-1', name: 'feature 1', diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts index 772914bb532112..ef48af2b3a7f4e 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts @@ -108,7 +108,7 @@ export function initSpacesOnPostAuthRequestInterceptor({ if (appId !== 'kibana' && space && space.disabledFeatures.length > 0) { log.debug(`Verifying application is available: "${appId}"`); - const allFeatures = features.getFeatures(); + const allFeatures = features.getKibanaFeatures(); const isRegisteredApp = allFeatures.some((feature) => feature.app.includes(appId)); if (isRegisteredApp) { diff --git a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts index 61b1985c5a0b9f..9f6826b030fb85 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts @@ -255,10 +255,12 @@ describe('#getAll', () => { mockAuthorization.mode.useRbacForRequest.mockReturnValue(true); mockCheckPrivilegesAtSpaces.mockReturnValue({ username, - privileges: [ - { resource: savedObjects[0].id, privilege, authorized: false }, - { resource: savedObjects[1].id, privilege, authorized: false }, - ], + privileges: { + kibana: [ + { resource: savedObjects[0].id, privilege, authorized: false }, + { resource: savedObjects[1].id, privilege, authorized: false }, + ], + }, }); const maxSpaces = 1234; const mockConfig = createMockConfig({ @@ -293,7 +295,7 @@ describe('#getAll', () => { expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); expect(mockCheckPrivilegesAtSpaces).toHaveBeenCalledWith( savedObjects.map((savedObject) => savedObject.id), - [privilege] + { kibana: [privilege] } ); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledWith( username, @@ -313,10 +315,12 @@ describe('#getAll', () => { mockAuthorization.mode.useRbacForRequest.mockReturnValue(true); mockCheckPrivilegesAtSpaces.mockReturnValue({ username, - privileges: [ - { resource: savedObjects[0].id, privilege, authorized: true }, - { resource: savedObjects[1].id, privilege, authorized: false }, - ], + privileges: { + kibana: [ + { resource: savedObjects[0].id, privilege, authorized: true }, + { resource: savedObjects[1].id, privilege, authorized: false }, + ], + }, }); const mockInternalRepository = { find: jest.fn().mockReturnValue({ @@ -352,7 +356,7 @@ describe('#getAll', () => { expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); expect(mockCheckPrivilegesAtSpaces).toHaveBeenCalledWith( savedObjects.map((savedObject) => savedObject.id), - [privilege] + { kibana: [privilege] } ); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledWith( @@ -446,9 +450,9 @@ describe('#canEnumerateSpaces', () => { expect(canEnumerateSpaces).toEqual(false); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.space.manage - ); + expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith({ + kibana: mockAuthorization.actions.space.manage, + }); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0); @@ -481,9 +485,9 @@ describe('#canEnumerateSpaces', () => { expect(canEnumerateSpaces).toEqual(true); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.space.manage - ); + expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith({ + kibana: mockAuthorization.actions.space.manage, + }); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0); @@ -598,7 +602,9 @@ describe('#get', () => { await expect(client.get(id)).rejects.toThrowErrorMatchingSnapshot(); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesAtSpace).toHaveBeenCalledWith(id, mockAuthorization.actions.login); + expect(mockCheckPrivilegesAtSpace).toHaveBeenCalledWith(id, { + kibana: mockAuthorization.actions.login, + }); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledWith(username, 'get', [ id, ]); @@ -636,7 +642,9 @@ describe('#get', () => { expect(space).toEqual(expectedSpace); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesAtSpace).toHaveBeenCalledWith(id, mockAuthorization.actions.login); + expect(mockCheckPrivilegesAtSpace).toHaveBeenCalledWith(id, { + kibana: mockAuthorization.actions.login, + }); expect(mockInternalRepository.get).toHaveBeenCalledWith('space', id); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledWith(username, 'get', [ @@ -881,9 +889,9 @@ describe('#create', () => { expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.space.manage - ); + expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith({ + kibana: mockAuthorization.actions.space.manage, + }); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledWith(username, 'create'); expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0); }); @@ -934,9 +942,9 @@ describe('#create', () => { }); expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.space.manage - ); + expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith({ + kibana: mockAuthorization.actions.space.manage, + }); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledWith(username, 'create'); }); @@ -984,9 +992,9 @@ describe('#create', () => { expect(mockInternalRepository.create).not.toHaveBeenCalled(); expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.space.manage - ); + expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith({ + kibana: mockAuthorization.actions.space.manage, + }); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledWith(username, 'create'); }); @@ -1123,9 +1131,9 @@ describe('#update', () => { expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.space.manage - ); + expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith({ + kibana: mockAuthorization.actions.space.manage, + }); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledWith(username, 'update'); expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0); }); @@ -1162,9 +1170,9 @@ describe('#update', () => { expect(actualSpace).toEqual(expectedReturnedSpace); expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.space.manage - ); + expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith({ + kibana: mockAuthorization.actions.space.manage, + }); expect(mockInternalRepository.update).toHaveBeenCalledWith('space', id, attributes); expect(mockInternalRepository.get).toHaveBeenCalledWith('space', id); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); @@ -1348,9 +1356,9 @@ describe('#delete', () => { expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.space.manage - ); + expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith({ + kibana: mockAuthorization.actions.space.manage, + }); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledWith(username, 'delete'); expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0); }); @@ -1384,9 +1392,9 @@ describe('#delete', () => { expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.space.manage - ); + expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith({ + kibana: mockAuthorization.actions.space.manage, + }); expect(mockInternalRepository.get).toHaveBeenCalledWith('space', id); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledWith(username, 'delete'); @@ -1424,9 +1432,9 @@ describe('#delete', () => { expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.space.manage - ); + expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith({ + kibana: mockAuthorization.actions.space.manage, + }); expect(mockInternalRepository.get).toHaveBeenCalledWith('space', id); expect(mockInternalRepository.delete).toHaveBeenCalledWith('space', id); expect(mockInternalRepository.deleteByNamespace).toHaveBeenCalledWith(id); diff --git a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts index b4b0057a2f5a5c..bc07df25500f35 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts @@ -46,9 +46,9 @@ export class SpacesClient { public async canEnumerateSpaces(): Promise { if (this.useRbac()) { const checkPrivileges = this.authorization!.checkPrivilegesWithRequest(this.request); - const { hasAllRequested } = await checkPrivileges.globally( - this.authorization!.actions.space.manage - ); + const { hasAllRequested } = await checkPrivileges.globally({ + kibana: this.authorization!.actions.space.manage, + }); this.debugLogger(`SpacesClient.canEnumerateSpaces, using RBAC. Result: ${hasAllRequested}`); return hasAllRequested; } @@ -82,9 +82,11 @@ export class SpacesClient { const privilege = privilegeFactory(this.authorization!); - const { username, privileges } = await checkPrivileges.atSpaces(spaceIds, privilege); + const { username, privileges } = await checkPrivileges.atSpaces(spaceIds, { + kibana: privilege, + }); - const authorized = privileges.filter((x) => x.authorized).map((x) => x.resource); + const authorized = privileges.kibana.filter((x) => x.authorized).map((x) => x.resource); this.debugLogger( `SpacesClient.getAll(), authorized for ${ @@ -228,7 +230,7 @@ export class SpacesClient { private async ensureAuthorizedGlobally(action: string, method: string, forbiddenMessage: string) { const checkPrivileges = this.authorization!.checkPrivilegesWithRequest(this.request); - const { username, hasAllRequested } = await checkPrivileges.globally(action); + const { username, hasAllRequested } = await checkPrivileges.globally({ kibana: action }); if (hasAllRequested) { this.auditLogger.spacesAuthorizationSuccess(username, method); @@ -246,7 +248,9 @@ export class SpacesClient { forbiddenMessage: string ) { const checkPrivileges = this.authorization!.checkPrivilegesWithRequest(this.request); - const { username, hasAllRequested } = await checkPrivileges.atSpace(spaceId, action); + const { username, hasAllRequested } = await checkPrivileges.atSpace(spaceId, { + kibana: action, + }); if (hasAllRequested) { this.auditLogger.spacesAuthorizationSuccess(username, method, [spaceId]); diff --git a/x-pack/plugins/spaces/server/plugin.test.ts b/x-pack/plugins/spaces/server/plugin.test.ts index a82f2370cc124e..b650a114ed978f 100644 --- a/x-pack/plugins/spaces/server/plugin.test.ts +++ b/x-pack/plugins/spaces/server/plugin.test.ts @@ -8,14 +8,14 @@ import { CoreSetup } from 'src/core/server'; import { coreMock } from 'src/core/server/mocks'; import { featuresPluginMock } from '../../features/server/mocks'; import { licensingMock } from '../../licensing/server/mocks'; -import { Plugin, PluginsSetup } from './plugin'; +import { Plugin, PluginsStart } from './plugin'; import { usageCollectionPluginMock } from '../../../../src/plugins/usage_collection/server/mocks'; describe('Spaces Plugin', () => { describe('#setup', () => { it('can setup with all optional plugins disabled, exposing the expected contract', async () => { const initializerContext = coreMock.createPluginInitializerContext({}); - const core = coreMock.createSetup() as CoreSetup; + const core = coreMock.createSetup() as CoreSetup; const features = featuresPluginMock.createSetup(); const licensing = licensingMock.createSetup(); @@ -38,7 +38,7 @@ describe('Spaces Plugin', () => { it('registers the capabilities provider and switcher', async () => { const initializerContext = coreMock.createPluginInitializerContext({}); - const core = coreMock.createSetup() as CoreSetup; + const core = coreMock.createSetup() as CoreSetup; const features = featuresPluginMock.createSetup(); const licensing = licensingMock.createSetup(); @@ -52,7 +52,7 @@ describe('Spaces Plugin', () => { it('registers the usage collector', async () => { const initializerContext = coreMock.createPluginInitializerContext({}); - const core = coreMock.createSetup() as CoreSetup; + const core = coreMock.createSetup() as CoreSetup; const features = featuresPluginMock.createSetup(); const licensing = licensingMock.createSetup(); @@ -67,7 +67,7 @@ describe('Spaces Plugin', () => { it('registers the "space" saved object type and client wrapper', async () => { const initializerContext = coreMock.createPluginInitializerContext({}); - const core = coreMock.createSetup() as CoreSetup; + const core = coreMock.createSetup() as CoreSetup; const features = featuresPluginMock.createSetup(); const licensing = licensingMock.createSetup(); diff --git a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts index 57ec688ab70e83..2104c1c8c37b64 100644 --- a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts +++ b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts @@ -37,7 +37,7 @@ function setup({ } as LicensingPluginSetup; const featuresSetup = ({ - getFeatures: jest.fn().mockReturnValue(features), + getKibanaFeatures: jest.fn().mockReturnValue(features), } as unknown) as PluginsSetup['features']; return { diff --git a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts index 4ab309cc6015cc..49f37e59ec5da0 100644 --- a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts +++ b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts @@ -46,7 +46,7 @@ async function getSpacesUsage( return null; } - const knownFeatureIds = features.getFeatures().map((feature) => feature.id); + const knownFeatureIds = features.getKibanaFeatures().map((feature) => feature.id); let resp: SpacesAggregationResponse | undefined; try { diff --git a/x-pack/plugins/transform/kibana.json b/x-pack/plugins/transform/kibana.json index d7e7a7fabba4fd..2efe0bb25bc686 100644 --- a/x-pack/plugins/transform/kibana.json +++ b/x-pack/plugins/transform/kibana.json @@ -7,7 +7,8 @@ "data", "home", "licensing", - "management" + "management", + "features" ], "optionalPlugins": [ "security", diff --git a/x-pack/plugins/transform/server/plugin.ts b/x-pack/plugins/transform/server/plugin.ts index 79e9be239c798f..988750f70efe00 100644 --- a/x-pack/plugins/transform/server/plugin.ts +++ b/x-pack/plugins/transform/server/plugin.ts @@ -58,7 +58,7 @@ export class TransformServerPlugin implements Plugin<{}, void, any, any> { this.license = new License(); } - setup({ http, getStartServices }: CoreSetup, { licensing }: Dependencies): {} { + setup({ http, getStartServices }: CoreSetup, { licensing, features }: Dependencies): {} { const router = http.createRouter(); this.license.setup( @@ -75,6 +75,20 @@ export class TransformServerPlugin implements Plugin<{}, void, any, any> { } ); + features.registerElasticsearchFeature({ + id: PLUGIN.id, + management: { + data: [PLUGIN.id], + }, + catalogue: [PLUGIN.id], + privileges: [ + { + requiredClusterPrivileges: ['monitor_transform'], + ui: [], + }, + ], + }); + this.apiRoutes.setup({ router, license: this.license, diff --git a/x-pack/plugins/transform/server/types.ts b/x-pack/plugins/transform/server/types.ts index 5fcc23a6d9f484..c3d7434f14f455 100644 --- a/x-pack/plugins/transform/server/types.ts +++ b/x-pack/plugins/transform/server/types.ts @@ -6,10 +6,12 @@ import { IRouter } from 'src/core/server'; import { LicensingPluginSetup } from '../../licensing/server'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { License } from './services'; export interface Dependencies { licensing: LicensingPluginSetup; + features: FeaturesPluginSetup; } export interface RouteDependencies { diff --git a/x-pack/plugins/upgrade_assistant/kibana.json b/x-pack/plugins/upgrade_assistant/kibana.json index 273036a653aeb8..c4c6f23611f2bd 100644 --- a/x-pack/plugins/upgrade_assistant/kibana.json +++ b/x-pack/plugins/upgrade_assistant/kibana.json @@ -4,6 +4,6 @@ "server": true, "ui": true, "configPath": ["xpack", "upgrade_assistant"], - "requiredPlugins": ["management", "licensing"], + "requiredPlugins": ["management", "licensing", "features"], "optionalPlugins": ["cloud", "usageCollection"] } diff --git a/x-pack/plugins/upgrade_assistant/server/plugin.ts b/x-pack/plugins/upgrade_assistant/server/plugin.ts index 0cdf1ca05feac2..9ef0f250da8efd 100644 --- a/x-pack/plugins/upgrade_assistant/server/plugin.ts +++ b/x-pack/plugins/upgrade_assistant/server/plugin.ts @@ -16,6 +16,7 @@ import { } from '../../../../src/core/server'; import { CloudSetup } from '../../cloud/server'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { CredentialStore, credentialStoreFactory } from './lib/reindexing/credential_store'; @@ -32,6 +33,7 @@ import { RouteDependencies } from './types'; interface PluginsSetup { usageCollection: UsageCollectionSetup; licensing: LicensingPluginSetup; + features: FeaturesPluginSetup; cloud?: CloudSetup; } @@ -60,13 +62,26 @@ export class UpgradeAssistantServerPlugin implements Plugin { setup( { http, getStartServices, capabilities, savedObjects }: CoreSetup, - { usageCollection, cloud, licensing }: PluginsSetup + { usageCollection, cloud, features, licensing }: PluginsSetup ) { this.licensing = licensing; savedObjects.registerType(reindexOperationSavedObjectType); savedObjects.registerType(telemetrySavedObjectType); + features.registerElasticsearchFeature({ + id: 'upgrade_assistant', + management: { + stack: ['upgrade_assistant'], + }, + privileges: [ + { + requiredClusterPrivileges: ['manage'], + ui: [], + }, + ], + }); + const router = http.createRouter(); const dependencies: RouteDependencies = { diff --git a/x-pack/plugins/uptime/server/kibana.index.ts b/x-pack/plugins/uptime/server/kibana.index.ts index ab8d7a068b19dd..d7a4f8e76bed7e 100644 --- a/x-pack/plugins/uptime/server/kibana.index.ts +++ b/x-pack/plugins/uptime/server/kibana.index.ts @@ -27,7 +27,7 @@ export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCor const { features } = plugins; const libs = compose(server); - features.registerFeature({ + features.registerKibanaFeature({ id: PLUGIN.ID, name: PLUGIN.NAME, order: 1000, diff --git a/x-pack/plugins/watcher/kibana.json b/x-pack/plugins/watcher/kibana.json index ba6a9bfa5e1949..695686715cb6ab 100644 --- a/x-pack/plugins/watcher/kibana.json +++ b/x-pack/plugins/watcher/kibana.json @@ -7,7 +7,8 @@ "licensing", "management", "charts", - "data" + "data", + "features" ], "server": true, "ui": true, diff --git a/x-pack/plugins/watcher/server/plugin.ts b/x-pack/plugins/watcher/server/plugin.ts index 70c4f980580e8b..9ff46283a72a6b 100644 --- a/x-pack/plugins/watcher/server/plugin.ts +++ b/x-pack/plugins/watcher/server/plugin.ts @@ -18,7 +18,7 @@ import { Plugin, PluginInitializerContext, } from 'kibana/server'; -import { PLUGIN } from '../common/constants'; +import { PLUGIN, INDEX_NAMES } from '../common/constants'; import { Dependencies, LicenseStatus, RouteDependencies } from './types'; import { registerSettingsRoutes } from './routes/api/settings'; @@ -52,13 +52,39 @@ export class WatcherServerPlugin implements Plugin { this.log = ctx.logger.get(); } - async setup({ http, getStartServices }: CoreSetup, { licensing }: Dependencies) { + async setup({ http, getStartServices }: CoreSetup, { licensing, features }: Dependencies) { const router = http.createRouter(); const routeDependencies: RouteDependencies = { router, getLicenseStatus: () => this.licenseStatus, }; + features.registerElasticsearchFeature({ + id: 'watcher', + management: { + insightsAndAlerting: ['watcher'], + }, + catalogue: ['watcher'], + privileges: [ + { + requiredClusterPrivileges: ['manage_watcher'], + requiredIndexPrivileges: { + [INDEX_NAMES.WATCHES]: ['read'], + [INDEX_NAMES.WATCHER_HISTORY]: ['read'], + }, + ui: [], + }, + { + requiredClusterPrivileges: ['monitor_watcher'], + requiredIndexPrivileges: { + [INDEX_NAMES.WATCHES]: ['read'], + [INDEX_NAMES.WATCHER_HISTORY]: ['read'], + }, + ui: [], + }, + ], + }); + http.registerRouteHandlerContext('watcher', async (ctx, request) => { this.watcherESClient = this.watcherESClient ?? (await getCustomEsClient(getStartServices)); return { diff --git a/x-pack/plugins/watcher/server/types.ts b/x-pack/plugins/watcher/server/types.ts index dd941054114a84..167dcb3ab64c39 100644 --- a/x-pack/plugins/watcher/server/types.ts +++ b/x-pack/plugins/watcher/server/types.ts @@ -5,12 +5,14 @@ */ import { IRouter } from 'kibana/server'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { XPackMainPlugin } from '../../../legacy/plugins/xpack_main/server/xpack_main'; export interface Dependencies { licensing: LicensingPluginSetup; + features: FeaturesPluginSetup; } export interface ServerShim { diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts index cb1271494c294e..fbf30dfa6e4d49 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts @@ -60,7 +60,7 @@ export class FixturePlugin implements Plugin { public setup(core: CoreSetup, { features, actions, alerts }: FixtureSetupDeps) { - features.registerFeature({ + features.registerKibanaFeature({ id: 'alertsFixture', name: 'Alerts', app: ['alerts', 'kibana'], diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/plugin.ts index e297733fb47eb1..e1ef1255c6e137 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/plugin.ts @@ -23,7 +23,7 @@ export interface FixtureStartDeps { export class FixturePlugin implements Plugin { public setup(core: CoreSetup, { features, alerts }: FixtureSetupDeps) { - features.registerFeature({ + features.registerKibanaFeature({ id: 'alertsRestrictedFixture', name: 'AlertRestricted', app: ['alerts', 'kibana'], diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts index 5b0d28bf09508c..ac4a1298e28b95 100644 --- a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts @@ -10,7 +10,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const security = getService('security'); - const config = getService('config'); const PageObjects = getPageObjects(['common', 'settings', 'security', 'spaceSelector']); const appsMenu = getService('appsMenu'); const testSubjects = getService('testSubjects'); @@ -174,20 +173,18 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await security.user.delete('no_advanced_settings_privileges_user'); }); - it('shows Management navlink', async () => { + it('does not show Management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Discover', 'Stack Management']); + expect(navLinks).to.eql(['Discover']); }); - it(`does not allow navigation to advanced settings; redirects to management home`, async () => { + it(`does not allow navigation to advanced settings; shows "not found" error`, async () => { await PageObjects.common.navigateToUrl('management', 'kibana/settings', { ensureCurrentUrl: false, shouldLoginIfPrompted: false, shouldUseHashForSubUrl: false, }); - await testSubjects.existOrFail('managementHome', { - timeout: config.get('timeouts.waitFor'), - }); + await testSubjects.existOrFail('appNotFoundPageContent'); }); }); }); diff --git a/x-pack/test/functional/apps/api_keys/feature_controls/api_keys_security.ts b/x-pack/test/functional/apps/api_keys/feature_controls/api_keys_security.ts new file mode 100644 index 00000000000000..d3d2846082854e --- /dev/null +++ b/x-pack/test/functional/apps/api_keys/feature_controls/api_keys_security.ts @@ -0,0 +1,69 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'settings', 'security']); + const appsMenu = getService('appsMenu'); + const managementMenu = getService('managementMenu'); + + describe('security', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + await PageObjects.common.navigateToApp('home'); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + }); + + describe('global all privileges (aka kibana_admin)', () => { + before(async () => { + await security.testUser.setRoles(['kibana_admin'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should not render the "Security" section', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = (await managementMenu.getSections()).map((section) => section.sectionId); + expect(sections).to.eql(['insightsAndAlerting', 'kibana']); + }); + }); + + describe('global dashboard all with manage_security', () => { + before(async () => { + await security.testUser.setRoles(['global_dashboard_all', 'manage_security'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should render the "Security" section with API Keys', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = await managementMenu.getSections(); + expect(sections).to.have.length(1); + expect(sections[0]).to.eql({ + sectionId: 'security', + sectionLinks: ['users', 'roles', 'api_keys', 'role_mappings'], + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/api_keys/feature_controls/index.ts b/x-pack/test/functional/apps/api_keys/feature_controls/index.ts new file mode 100644 index 00000000000000..169b5c7fb0a736 --- /dev/null +++ b/x-pack/test/functional/apps/api_keys/feature_controls/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('feature controls', function () { + this.tags(['ciGroup2']); + + loadTestFile(require.resolve('./api_keys_security')); + }); +} diff --git a/x-pack/test/functional/apps/api_keys/home_page.ts b/x-pack/test/functional/apps/api_keys/home_page.ts index 553cceef573e14..1384592f25ae37 100644 --- a/x-pack/test/functional/apps/api_keys/home_page.ts +++ b/x-pack/test/functional/apps/api_keys/home_page.ts @@ -11,6 +11,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'apiKeys']); const log = getService('log'); const security = getService('security'); + const testSubjects = getService('testSubjects'); describe('Home page', function () { before(async () => { @@ -23,10 +24,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); // https://www.elastic.co/guide/en/kibana/7.6/api-keys.html#api-keys-security-privileges - it('Shows required privileges ', async () => { - log.debug('Checking for required privileges method section header'); - const message = await pageObjects.apiKeys.apiKeysPermissionDeniedMessage(); - expect(message).to.be('You need permission to manage API keys'); + it('Hides management link if user is not authorized', async () => { + await testSubjects.missingOrFail('apiKeys'); }); it('Loads the app', async () => { diff --git a/x-pack/test/functional/apps/api_keys/index.ts b/x-pack/test/functional/apps/api_keys/index.ts index 703aae04140f2e..7a17430dc8f6c8 100644 --- a/x-pack/test/functional/apps/api_keys/index.ts +++ b/x-pack/test/functional/apps/api_keys/index.ts @@ -10,5 +10,6 @@ export default ({ loadTestFile }: FtrProviderContext) => { describe('API Keys app', function () { this.tags(['ciGroup7']); loadTestFile(require.resolve('./home_page')); + loadTestFile(require.resolve('./feature_controls')); }); }; diff --git a/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts b/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts index e9fa4ccf8e48ba..5a8fb207d50623 100644 --- a/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts +++ b/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts @@ -66,7 +66,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows canvas navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Canvas', 'Stack Management']); + expect(navLinks).to.eql(['Canvas']); }); it(`landing page shows "Create new workpad" button`, async () => { @@ -142,7 +142,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows canvas navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Canvas', 'Stack Management']); + expect(navLinks).to.eql(['Canvas']); }); it(`landing page shows disabled "Create new workpad" button`, async () => { diff --git a/x-pack/test/functional/apps/cross_cluster_replication/feature_controls/ccr_security.ts b/x-pack/test/functional/apps/cross_cluster_replication/feature_controls/ccr_security.ts new file mode 100644 index 00000000000000..6b4b9c61151bac --- /dev/null +++ b/x-pack/test/functional/apps/cross_cluster_replication/feature_controls/ccr_security.ts @@ -0,0 +1,77 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'settings', 'security']); + const appsMenu = getService('appsMenu'); + const managementMenu = getService('managementMenu'); + + describe('security', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + await PageObjects.common.navigateToApp('home'); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + }); + + describe('global all privileges (aka kibana_admin)', () => { + before(async () => { + await security.testUser.setRoles(['kibana_admin'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should not render the "Data" section', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = (await managementMenu.getSections()).map((section) => section.sectionId); + expect(sections).to.eql(['insightsAndAlerting', 'kibana']); + }); + }); + + describe('global dashboard all with ccr_user', () => { + before(async () => { + await security.testUser.setRoles(['global_dashboard_all', 'ccr_user'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should render the "Data" section with CCR', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = await managementMenu.getSections(); + expect(sections).to.have.length(3); + expect(sections[1]).to.eql({ + sectionId: 'data', + sectionLinks: [ + 'index_management', + 'index_lifecycle_management', + 'snapshot_restore', + 'rollup_jobs', + 'transform', + 'cross_cluster_replication', + 'remote_clusters', + ], + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/cross_cluster_replication/feature_controls/index.ts b/x-pack/test/functional/apps/cross_cluster_replication/feature_controls/index.ts new file mode 100644 index 00000000000000..e7be2cb48ce3e9 --- /dev/null +++ b/x-pack/test/functional/apps/cross_cluster_replication/feature_controls/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('feature controls', function () { + this.tags(['ciGroup2']); + + loadTestFile(require.resolve('./ccr_security')); + }); +} diff --git a/x-pack/test/functional/apps/cross_cluster_replication/index.ts b/x-pack/test/functional/apps/cross_cluster_replication/index.ts index 5db6103307af9a..0e54c0d1c0d158 100644 --- a/x-pack/test/functional/apps/cross_cluster_replication/index.ts +++ b/x-pack/test/functional/apps/cross_cluster_replication/index.ts @@ -9,6 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Cross Cluster Replication app', function () { this.tags(['ciGroup4', 'skipCloud']); + loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./home_page')); }); }; diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts index 505e35907bd802..46dc0316a5d6b1 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts +++ b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts @@ -81,9 +81,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await security.user.delete('global_dashboard_all_user'); }); - it('shows dashboard navlink', async () => { + it('only shows the dashboard navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map((link) => link.text)).to.contain('Dashboard'); + expect(navLinks.map((link) => link.text)).to.eql(['Dashboard']); }); it(`landing page shows "Create new Dashboard" button`, async () => { @@ -287,7 +287,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows dashboard navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.contain('Dashboard'); + expect(navLinks).to.eql(['Dashboard']); }); it(`landing page doesn't show "Create new Dashboard" button`, async () => { @@ -415,7 +415,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows dashboard navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.contain('Dashboard'); + expect(navLinks).to.eql(['Dashboard']); }); it(`landing page doesn't show "Create new Dashboard" button`, async () => { diff --git a/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts index 803ff6399a035e..807ba6ded88a27 100644 --- a/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts +++ b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts @@ -63,7 +63,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows Dev Tools navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map((link) => link.text)).to.eql(['Dev Tools', 'Stack Management']); + expect(navLinks.map((link) => link.text)).to.eql(['Dev Tools']); }); describe('console', () => { @@ -144,7 +144,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it(`shows 'Dev Tools' navlink`, async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Dev Tools', 'Stack Management']); + expect(navLinks).to.eql(['Dev Tools']); }); describe('console', () => { diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts index 8be43497628086..d94451d023ec0d 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts @@ -82,7 +82,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows discover navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map((link) => link.text)).to.eql(['Discover', 'Stack Management']); + expect(navLinks.map((link) => link.text)).to.eql(['Discover']); }); it('shows save button', async () => { @@ -184,7 +184,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows discover navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Discover', 'Stack Management']); + expect(navLinks).to.eql(['Discover']); }); it(`doesn't show save button`, async () => { @@ -275,7 +275,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows discover navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Discover', 'Stack Management']); + expect(navLinks).to.eql(['Discover']); }); it(`doesn't show save button`, async () => { diff --git a/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts b/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts index 9121028c14404c..3b4a1fbdbe0d8f 100644 --- a/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts +++ b/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts @@ -64,7 +64,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows graph navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map((link) => link.text)).to.eql(['Graph', 'Stack Management']); + expect(navLinks.map((link) => link.text)).to.eql(['Graph']); }); it('landing page shows "Create new graph" button', async () => { @@ -127,7 +127,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows graph navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Graph', 'Stack Management']); + expect(navLinks).to.eql(['Graph']); }); it('does not show a "Create new Workspace" button', async () => { diff --git a/x-pack/test/functional/apps/index_lifecycle_management/feature_controls/ilm_security.ts b/x-pack/test/functional/apps/index_lifecycle_management/feature_controls/ilm_security.ts new file mode 100644 index 00000000000000..4cb0d3077aaa41 --- /dev/null +++ b/x-pack/test/functional/apps/index_lifecycle_management/feature_controls/ilm_security.ts @@ -0,0 +1,69 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'settings', 'security']); + const appsMenu = getService('appsMenu'); + const managementMenu = getService('managementMenu'); + + describe('security', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + await PageObjects.common.navigateToApp('home'); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + }); + + describe('global all privileges (aka kibana_admin)', () => { + before(async () => { + await security.testUser.setRoles(['kibana_admin'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should not render the "Data" section', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = (await managementMenu.getSections()).map((section) => section.sectionId); + expect(sections).to.eql(['insightsAndAlerting', 'kibana']); + }); + }); + + describe('global dashboard all with manage_ilm', () => { + before(async () => { + await security.testUser.setRoles(['global_dashboard_all', 'manage_ilm'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should render the "Data" section with ILM', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = await managementMenu.getSections(); + expect(sections).to.have.length(1); + expect(sections[0]).to.eql({ + sectionId: 'data', + sectionLinks: ['index_lifecycle_management'], + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/index_lifecycle_management/feature_controls/index.ts b/x-pack/test/functional/apps/index_lifecycle_management/feature_controls/index.ts new file mode 100644 index 00000000000000..0bb6476f366871 --- /dev/null +++ b/x-pack/test/functional/apps/index_lifecycle_management/feature_controls/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('feature controls', function () { + this.tags(['ciGroup2']); + + loadTestFile(require.resolve('./ilm_security')); + }); +} diff --git a/x-pack/test/functional/apps/index_lifecycle_management/index.ts b/x-pack/test/functional/apps/index_lifecycle_management/index.ts index f535710814ab20..157fb62b7a84d7 100644 --- a/x-pack/test/functional/apps/index_lifecycle_management/index.ts +++ b/x-pack/test/functional/apps/index_lifecycle_management/index.ts @@ -9,6 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Index Lifecycle Management app', function () { this.tags('ciGroup7'); + loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./home_page')); }); }; diff --git a/x-pack/test/functional/apps/index_management/feature_controls/index.ts b/x-pack/test/functional/apps/index_management/feature_controls/index.ts new file mode 100644 index 00000000000000..85398a73eceff4 --- /dev/null +++ b/x-pack/test/functional/apps/index_management/feature_controls/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('feature controls', function () { + this.tags(['ciGroup2']); + + loadTestFile(require.resolve('./index_management_security')); + }); +} diff --git a/x-pack/test/functional/apps/index_management/feature_controls/index_management_security.ts b/x-pack/test/functional/apps/index_management/feature_controls/index_management_security.ts new file mode 100644 index 00000000000000..2019751d9101ce --- /dev/null +++ b/x-pack/test/functional/apps/index_management/feature_controls/index_management_security.ts @@ -0,0 +1,69 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'settings', 'security']); + const appsMenu = getService('appsMenu'); + const managementMenu = getService('managementMenu'); + + describe('security', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + await PageObjects.common.navigateToApp('home'); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + }); + + describe('global all privileges (aka kibana_admin)', () => { + before(async () => { + await security.testUser.setRoles(['kibana_admin'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should not render the "Data" section', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = (await managementMenu.getSections()).map((section) => section.sectionId); + expect(sections).to.eql(['insightsAndAlerting', 'kibana']); + }); + }); + + describe('global dashboard all with index_management_user', () => { + before(async () => { + await security.testUser.setRoles(['global_dashboard_all', 'index_management_user'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should render the "Data" section with index management', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = await managementMenu.getSections(); + expect(sections).to.have.length(1); + expect(sections[0]).to.eql({ + sectionId: 'data', + sectionLinks: ['index_management', 'transform'], + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/index_management/index.ts b/x-pack/test/functional/apps/index_management/index.ts index a9bb44d002334a..97b23cbf82c311 100644 --- a/x-pack/test/functional/apps/index_management/index.ts +++ b/x-pack/test/functional/apps/index_management/index.ts @@ -9,6 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Index Management app', function () { this.tags('ciGroup3'); + loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./home_page')); }); }; diff --git a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts index bc36f70df3641d..d440271bda590d 100644 --- a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts +++ b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts @@ -10,7 +10,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const security = getService('security'); - const config = getService('config'); const PageObjects = getPageObjects(['common', 'settings', 'security']); const appsMenu = getService('appsMenu'); const testSubjects = getService('testSubjects'); @@ -175,28 +174,17 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await security.user.delete('no_index_patterns_privileges_user'); }); - it('shows Management navlink', async () => { + it('does not show Management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Discover', 'Stack Management']); + expect(navLinks).to.eql(['Discover']); }); it(`doesn't show Index Patterns in management side-nav`, async () => { - await PageObjects.settings.navigateTo(); - await testSubjects.existOrFail('managementHome', { - timeout: config.get('timeouts.waitFor'), - }); - await testSubjects.missingOrFail('indexPatterns'); - }); - - it(`does not allow navigation to Index Patterns; redirects to management home`, async () => { - await PageObjects.common.navigateToUrl('management', 'kibana/indexPatterns', { + await PageObjects.common.navigateToActualUrl('management', '', { ensureCurrentUrl: false, shouldLoginIfPrompted: false, - shouldUseHashForSubUrl: false, - }); - await testSubjects.existOrFail('managementHome', { - timeout: config.get('timeouts.waitFor'), }); + await testSubjects.existOrFail('~appNotFoundPageContent'); }); }); }); diff --git a/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts b/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts index 64154ff6cf3f79..552e948f56a9b3 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts @@ -58,7 +58,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows logs navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Overview', 'Logs', 'Stack Management']); + expect(navLinks).to.eql(['Overview', 'Logs']); }); describe('logs landing page without data', () => { @@ -121,7 +121,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows logs navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Overview', 'Logs', 'Stack Management']); + expect(navLinks).to.eql(['Overview', 'Logs']); }); describe('logs landing page without data', () => { diff --git a/x-pack/test/functional/apps/ingest_pipelines/feature_controls/index.ts b/x-pack/test/functional/apps/ingest_pipelines/feature_controls/index.ts new file mode 100644 index 00000000000000..fbaf7648646b88 --- /dev/null +++ b/x-pack/test/functional/apps/ingest_pipelines/feature_controls/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('feature controls', function () { + this.tags(['ciGroup2']); + + loadTestFile(require.resolve('./ingest_pipelines_security')); + }); +} diff --git a/x-pack/test/functional/apps/ingest_pipelines/feature_controls/ingest_pipelines_security.ts b/x-pack/test/functional/apps/ingest_pipelines/feature_controls/ingest_pipelines_security.ts new file mode 100644 index 00000000000000..bf703a8f60dc23 --- /dev/null +++ b/x-pack/test/functional/apps/ingest_pipelines/feature_controls/ingest_pipelines_security.ts @@ -0,0 +1,69 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'settings', 'security']); + const appsMenu = getService('appsMenu'); + const managementMenu = getService('managementMenu'); + + describe('security', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + await PageObjects.common.navigateToApp('home'); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + }); + + describe('global all privileges (aka kibana_admin)', () => { + before(async () => { + await security.testUser.setRoles(['kibana_admin'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should not render the "Ingest" section', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = (await managementMenu.getSections()).map((section) => section.sectionId); + expect(sections).to.eql(['insightsAndAlerting', 'kibana']); + }); + }); + + describe('global dashboard all with ingest_pipelines_user', () => { + before(async () => { + await security.testUser.setRoles(['global_dashboard_all', 'ingest_pipelines_user'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should render the "Ingest" section with ingest pipelines', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = await managementMenu.getSections(); + expect(sections).to.have.length(1); + expect(sections[0]).to.eql({ + sectionId: 'ingest', + sectionLinks: ['ingest_pipelines'], + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/ingest_pipelines/index.ts b/x-pack/test/functional/apps/ingest_pipelines/index.ts index 8d2b9ee1dcb692..2a4781c5e216d9 100644 --- a/x-pack/test/functional/apps/ingest_pipelines/index.ts +++ b/x-pack/test/functional/apps/ingest_pipelines/index.ts @@ -9,6 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Ingest pipelines app', function () { this.tags('ciGroup3'); + loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./ingest_pipelines')); }); }; diff --git a/x-pack/test/functional/apps/license_management/feature_controls/index.ts b/x-pack/test/functional/apps/license_management/feature_controls/index.ts new file mode 100644 index 00000000000000..5c7c04d4ccde1f --- /dev/null +++ b/x-pack/test/functional/apps/license_management/feature_controls/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('feature controls', function () { + this.tags(['ciGroup2']); + + loadTestFile(require.resolve('./license_management_security')); + }); +} diff --git a/x-pack/test/functional/apps/license_management/feature_controls/license_management_security.ts b/x-pack/test/functional/apps/license_management/feature_controls/license_management_security.ts new file mode 100644 index 00000000000000..59fc287c6cf2eb --- /dev/null +++ b/x-pack/test/functional/apps/license_management/feature_controls/license_management_security.ts @@ -0,0 +1,69 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'settings', 'security']); + const appsMenu = getService('appsMenu'); + const managementMenu = getService('managementMenu'); + + describe('security', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + await PageObjects.common.navigateToApp('home'); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + }); + + describe('global all privileges (aka kibana_admin)', () => { + before(async () => { + await security.testUser.setRoles(['kibana_admin'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should not render the "Stack" section', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = (await managementMenu.getSections()).map((section) => section.sectionId); + expect(sections).to.eql(['insightsAndAlerting', 'kibana']); + }); + }); + + describe('global dashboard all with license_management_user', () => { + before(async () => { + await security.testUser.setRoles(['global_dashboard_all', 'license_management_user'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should render the "Stack" section with License Management', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = await managementMenu.getSections(); + expect(sections).to.have.length(3); + expect(sections[2]).to.eql({ + sectionId: 'stack', + sectionLinks: ['license_management', 'upgrade_assistant'], + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/license_management/index.ts b/x-pack/test/functional/apps/license_management/index.ts index 6d01b1bb098f03..0b090223c18fe2 100644 --- a/x-pack/test/functional/apps/license_management/index.ts +++ b/x-pack/test/functional/apps/license_management/index.ts @@ -9,6 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('License app', function () { this.tags('ciGroup7'); + loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./home_page')); }); }; diff --git a/x-pack/test/functional/apps/logstash/feature_controls/index.ts b/x-pack/test/functional/apps/logstash/feature_controls/index.ts new file mode 100644 index 00000000000000..d3cc7fae94d98a --- /dev/null +++ b/x-pack/test/functional/apps/logstash/feature_controls/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('feature controls', function () { + this.tags(['ciGroup2']); + + loadTestFile(require.resolve('./logstash_security')); + }); +} diff --git a/x-pack/test/functional/apps/logstash/feature_controls/logstash_security.ts b/x-pack/test/functional/apps/logstash/feature_controls/logstash_security.ts new file mode 100644 index 00000000000000..8e2609e3b7e85b --- /dev/null +++ b/x-pack/test/functional/apps/logstash/feature_controls/logstash_security.ts @@ -0,0 +1,69 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'settings', 'security']); + const appsMenu = getService('appsMenu'); + const managementMenu = getService('managementMenu'); + + describe('security', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + await PageObjects.common.navigateToApp('home'); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + }); + + describe('global all privileges (aka kibana_admin)', () => { + before(async () => { + await security.testUser.setRoles(['kibana_admin'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should not render the "Ingest" section', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = (await managementMenu.getSections()).map((section) => section.sectionId); + expect(sections).to.eql(['insightsAndAlerting', 'kibana']); + }); + }); + + describe('global dashboard all with logstash_read_user', () => { + before(async () => { + await security.testUser.setRoles(['global_dashboard_all', 'logstash_read_user'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should render the "Ingest" section with Logstash Pipelines', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = await managementMenu.getSections(); + expect(sections).to.have.length(1); + expect(sections[0]).to.eql({ + sectionId: 'ingest', + sectionLinks: ['pipelines'], + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/logstash/index.js b/x-pack/test/functional/apps/logstash/index.js index 515674577fb524..3258d948cedfcf 100644 --- a/x-pack/test/functional/apps/logstash/index.js +++ b/x-pack/test/functional/apps/logstash/index.js @@ -8,6 +8,7 @@ export default function ({ loadTestFile }) { describe('logstash', function () { this.tags(['ciGroup2']); + loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./pipeline_list')); loadTestFile(require.resolve('./pipeline_create')); }); diff --git a/x-pack/test/functional/apps/management/feature_controls/index.ts b/x-pack/test/functional/apps/management/feature_controls/index.ts new file mode 100644 index 00000000000000..8b8226da7dc3c5 --- /dev/null +++ b/x-pack/test/functional/apps/management/feature_controls/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('feature controls', function () { + this.tags(['ciGroup2']); + + loadTestFile(require.resolve('./management_security')); + }); +} diff --git a/x-pack/test/functional/apps/management/feature_controls/management_security.ts b/x-pack/test/functional/apps/management/feature_controls/management_security.ts new file mode 100644 index 00000000000000..cf1a83ca49686b --- /dev/null +++ b/x-pack/test/functional/apps/management/feature_controls/management_security.ts @@ -0,0 +1,74 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'settings', 'security']); + const appsMenu = getService('appsMenu'); + const managementMenu = getService('managementMenu'); + const testSubjects = getService('testSubjects'); + + describe('security', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + await PageObjects.common.navigateToApp('home'); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + }); + + describe('no management privileges', () => { + before(async () => { + await security.testUser.setRoles(['global_dashboard_all'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('should not show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.eql(['Dashboard']); + }); + + it('should render the "application not found" view when navigating to management directly', async () => { + await PageObjects.common.navigateToApp('management'); + expect(await testSubjects.exists('appNotFoundPageContent')).to.eql(true); + }); + }); + + describe('global all privileges (aka kibana_admin)', () => { + before(async () => { + await security.testUser.setRoles(['kibana_admin'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should only render management entries controllable via Kibana privileges', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = await managementMenu.getSections(); + expect(sections).to.have.length(2); + expect(sections[0]).to.eql({ + sectionId: 'insightsAndAlerting', + sectionLinks: ['triggersActions'], + }); + expect(sections[1]).to.eql({ + sectionId: 'kibana', + sectionLinks: ['indexPatterns', 'objects', 'spaces', 'settings'], + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/management/index.js b/x-pack/test/functional/apps/management/index.ts similarity index 67% rename from x-pack/test/functional/apps/management/index.js rename to x-pack/test/functional/apps/management/index.ts index 19c68a2da9d9bd..7a461c9963be9f 100644 --- a/x-pack/test/functional/apps/management/index.js +++ b/x-pack/test/functional/apps/management/index.ts @@ -4,10 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -export default function ({ loadTestFile }) { +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { describe('management', function () { this.tags(['ciGroup2']); loadTestFile(require.resolve('./create_index_pattern_wizard')); + loadTestFile(require.resolve('./feature_controls')); }); } diff --git a/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts b/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts index f480f1f0ae24af..ac9996e7bdc62b 100644 --- a/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts +++ b/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts @@ -67,7 +67,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows maps navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Maps', 'Stack Management']); + expect(navLinks).to.eql(['Maps']); }); it(`allows a map to be created`, async () => { @@ -170,7 +170,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows Maps navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Maps', 'Stack Management']); + expect(navLinks).to.eql(['Maps']); }); it(`does not show create new button`, async () => { diff --git a/x-pack/test/functional/apps/remote_clusters/feature_controls/index.ts b/x-pack/test/functional/apps/remote_clusters/feature_controls/index.ts new file mode 100644 index 00000000000000..bfcaef629dc421 --- /dev/null +++ b/x-pack/test/functional/apps/remote_clusters/feature_controls/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('feature controls', function () { + this.tags(['ciGroup2']); + + loadTestFile(require.resolve('./remote_clusters_security')); + }); +} diff --git a/x-pack/test/functional/apps/remote_clusters/feature_controls/remote_clusters_security.ts b/x-pack/test/functional/apps/remote_clusters/feature_controls/remote_clusters_security.ts new file mode 100644 index 00000000000000..b1edc74607161e --- /dev/null +++ b/x-pack/test/functional/apps/remote_clusters/feature_controls/remote_clusters_security.ts @@ -0,0 +1,76 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'settings', 'security']); + const appsMenu = getService('appsMenu'); + const managementMenu = getService('managementMenu'); + + describe('security', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + await PageObjects.common.navigateToApp('home'); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + }); + + describe('global all privileges (aka kibana_admin)', () => { + before(async () => { + await security.testUser.setRoles(['kibana_admin'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should not render the "Stack" section', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = (await managementMenu.getSections()).map((section) => section.sectionId); + expect(sections).to.eql(['insightsAndAlerting', 'kibana']); + }); + }); + + describe('global dashboard all with license_management_user', () => { + before(async () => { + await security.testUser.setRoles(['global_dashboard_all', 'license_management_user'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should render the "Data" section with Remote Clusters', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = await managementMenu.getSections(); + expect(sections).to.have.length(3); + expect(sections[1]).to.eql({ + sectionId: 'data', + sectionLinks: [ + 'index_management', + 'index_lifecycle_management', + 'snapshot_restore', + 'rollup_jobs', + 'transform', + 'remote_clusters', + ], + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/remote_clusters/index.ts b/x-pack/test/functional/apps/remote_clusters/index.ts index d91d413e2b7af6..0839c2f22af477 100644 --- a/x-pack/test/functional/apps/remote_clusters/index.ts +++ b/x-pack/test/functional/apps/remote_clusters/index.ts @@ -9,6 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Remote Clusters app', function () { this.tags(['ciGroup4', 'skipCloud']); + loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./home_page')); }); }; diff --git a/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts b/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts index 28b8153ea4c2b2..02b2ec4d4c681c 100644 --- a/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts +++ b/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts @@ -10,14 +10,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const security = getService('security'); const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects([ - 'common', - 'settings', - 'security', - 'error', - 'header', - 'savedObjects', - ]); + const PageObjects = getPageObjects(['common', 'settings', 'security', 'error', 'savedObjects']); let version: string = ''; describe('feature controls saved objects management', () => { @@ -310,12 +303,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); describe('listing', () => { - it(`doesn't display management section`, async () => { - await PageObjects.settings.navigateTo(); - await testSubjects.existOrFail('managementHome'); // this ensures we've gotten to the management page - await testSubjects.missingOrFail('objects'); - }); - it(`can't navigate to listing page`, async () => { await PageObjects.common.navigateToUrl('management', 'kibana/objects', { ensureCurrentUrl: false, @@ -323,7 +310,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { shouldUseHashForSubUrl: false, }); - await testSubjects.existOrFail('managementHome'); + await testSubjects.existOrFail('appNotFoundPageContent'); }); }); @@ -338,8 +325,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { shouldUseHashForSubUrl: false, } ); - await PageObjects.header.waitUntilLoadingHasFinished(); - await testSubjects.existOrFail('managementHome'); + await testSubjects.existOrFail('appNotFoundPageContent'); }); }); }); diff --git a/x-pack/test/functional/apps/security/secure_roles_perm.js b/x-pack/test/functional/apps/security/secure_roles_perm.js index 2054a7b0b00380..c547657bf880ae 100644 --- a/x-pack/test/functional/apps/security/secure_roles_perm.js +++ b/x-pack/test/functional/apps/security/secure_roles_perm.js @@ -21,7 +21,6 @@ export default function ({ getService, getPageObjects }) { const browser = getService('browser'); const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); - const retry = getService('retry'); describe('secure roles and permissions', function () { before(async () => { @@ -74,12 +73,9 @@ export default function ({ getService, getPageObjects }) { await PageObjects.security.login('Rashmi', 'changeme'); }); - it('Kibana User navigating to Management gets permission denied', async function () { + it('Kibana User does not have link to user management', async function () { await PageObjects.settings.navigateTo(); - await PageObjects.security.clickElasticsearchUsers(); - await retry.tryForTime(2000, async () => { - await testSubjects.find('permissionDeniedMessage'); - }); + await testSubjects.missingOrFail('users'); }); it('Kibana User navigating to Discover and trying to generate CSV gets - Authorization Error ', async function () { diff --git a/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts b/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts index a3ade23f5c178b..d705140954de48 100644 --- a/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts +++ b/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts @@ -60,7 +60,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows timelion navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Timelion', 'Stack Management']); + expect(navLinks).to.eql(['Timelion']); }); it(`allows a timelion sheet to be created`, async () => { @@ -112,7 +112,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows timelion navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Timelion', 'Stack Management']); + expect(navLinks).to.eql(['Timelion']); }); it(`does not allow a timelion sheet to be created`, async () => { diff --git a/x-pack/test/functional/apps/transform/feature_controls/index.ts b/x-pack/test/functional/apps/transform/feature_controls/index.ts new file mode 100644 index 00000000000000..794e6f516d9823 --- /dev/null +++ b/x-pack/test/functional/apps/transform/feature_controls/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('feature controls', function () { + this.tags(['ciGroup2']); + + loadTestFile(require.resolve('./transform_security')); + }); +} diff --git a/x-pack/test/functional/apps/transform/feature_controls/transform_security.ts b/x-pack/test/functional/apps/transform/feature_controls/transform_security.ts new file mode 100644 index 00000000000000..5d7d8ec3c307e3 --- /dev/null +++ b/x-pack/test/functional/apps/transform/feature_controls/transform_security.ts @@ -0,0 +1,70 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'settings', 'security']); + const appsMenu = getService('appsMenu'); + const managementMenu = getService('managementMenu'); + + describe('security', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + await PageObjects.security.forceLogout(); + await PageObjects.common.navigateToApp('home'); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + }); + + describe('global all privileges (aka kibana_admin)', () => { + before(async () => { + await security.testUser.setRoles(['kibana_admin'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should not render the "Stack" section', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = (await managementMenu.getSections()).map((section) => section.sectionId); + expect(sections).to.eql(['insightsAndAlerting', 'kibana']); + }); + }); + + describe('global dashboard all with transform_user', () => { + before(async () => { + await security.testUser.setRoles(['global_dashboard_all', 'transform_user'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should render the "Data" section with Transform', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = await managementMenu.getSections(); + expect(sections).to.have.length(1); + expect(sections[0]).to.eql({ + sectionId: 'data', + sectionLinks: ['transform'], + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/transform/index.ts b/x-pack/test/functional/apps/transform/index.ts index 6dd22a1f4a204d..1af3f7b6c0df57 100644 --- a/x-pack/test/functional/apps/transform/index.ts +++ b/x-pack/test/functional/apps/transform/index.ts @@ -38,5 +38,6 @@ export default function ({ getService, loadTestFile, getPageObjects }: FtrProvid loadTestFile(require.resolve('./creation_saved_search')); loadTestFile(require.resolve('./cloning')); loadTestFile(require.resolve('./editing')); + loadTestFile(require.resolve('./feature_controls')); }); } diff --git a/x-pack/test/functional/apps/upgrade_assistant/feature_controls/index.ts b/x-pack/test/functional/apps/upgrade_assistant/feature_controls/index.ts new file mode 100644 index 00000000000000..f1c73e39fbc3e4 --- /dev/null +++ b/x-pack/test/functional/apps/upgrade_assistant/feature_controls/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('feature controls', function () { + this.tags(['ciGroup2']); + + loadTestFile(require.resolve('./upgrade_assistant_security')); + }); +} diff --git a/x-pack/test/functional/apps/upgrade_assistant/feature_controls/upgrade_assistant_security.ts b/x-pack/test/functional/apps/upgrade_assistant/feature_controls/upgrade_assistant_security.ts new file mode 100644 index 00000000000000..1f541dbe03537c --- /dev/null +++ b/x-pack/test/functional/apps/upgrade_assistant/feature_controls/upgrade_assistant_security.ts @@ -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. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'settings', 'security']); + const appsMenu = getService('appsMenu'); + const managementMenu = getService('managementMenu'); + + describe('security', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + await PageObjects.common.navigateToApp('home'); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + }); + + describe('global all privileges (aka kibana_admin)', () => { + before(async () => { + await security.testUser.setRoles(['kibana_admin'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should not render the "Stack" section', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = (await managementMenu.getSections()).map((section) => section.sectionId); + expect(sections).to.eql(['insightsAndAlerting', 'kibana']); + }); + }); + + describe('global dashboard all with global_upgrade_assistant_role', () => { + before(async () => { + await security.testUser.setRoles( + ['global_dashboard_all', 'global_upgrade_assistant_role'], + true + ); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should render the "Stack" section with Upgrde Assistant', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = await managementMenu.getSections(); + expect(sections).to.have.length(3); + expect(sections[2]).to.eql({ + sectionId: 'stack', + sectionLinks: ['license_management', 'upgrade_assistant'], + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/upgrade_assistant/index.ts b/x-pack/test/functional/apps/upgrade_assistant/index.ts index 0e6c52f0812ee3..131cb6a249c78d 100644 --- a/x-pack/test/functional/apps/upgrade_assistant/index.ts +++ b/x-pack/test/functional/apps/upgrade_assistant/index.ts @@ -9,6 +9,7 @@ export default function upgradeCheckup({ loadTestFile }: FtrProviderContext) { describe('Upgrade checkup ', function upgradeAssistantTestSuite() { this.tags('ciGroup4'); + loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./upgrade_assistant')); }); } diff --git a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts index 49435df4f1c2ac..ca84a8e561164d 100644 --- a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts +++ b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts @@ -79,7 +79,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows visualize navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Visualize', 'Stack Management']); + expect(navLinks).to.eql(['Visualize']); }); it(`landing page shows "Create new Visualization" button`, async () => { @@ -210,7 +210,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows visualize navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Visualize', 'Stack Management']); + expect(navLinks).to.eql(['Visualize']); }); it(`landing page shows "Create new Visualization" button`, async () => { @@ -325,7 +325,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows visualize navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Visualize', 'Stack Management']); + expect(navLinks).to.eql(['Visualize']); }); it(`landing page shows "Create new Visualization" button`, async () => { diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index fdd694e73394e3..1ade36d285e9a3 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -251,6 +251,16 @@ export default async function ({ readConfigFile }) { }, ], }, + global_dashboard_all: { + kibana: [ + { + feature: { + dashboard: ['all'], + }, + spaces: ['*'], + }, + ], + }, global_maps_all: { kibana: [ { @@ -326,6 +336,65 @@ export default async function ({ readConfigFile }) { }, ], }, + + manage_security: { + elasticsearch: { + cluster: ['manage_security'], + }, + }, + + ccr_user: { + elasticsearch: { + cluster: ['manage', 'manage_ccr'], + }, + }, + + manage_ilm: { + elasticsearch: { + cluster: ['manage_ilm'], + }, + }, + + index_management_user: { + elasticsearch: { + cluster: ['monitor'], + indices: [ + { + names: ['geo_shapes*'], + privileges: ['all'], + }, + ], + }, + }, + + ingest_pipelines_user: { + elasticsearch: { + cluster: ['manage_pipeline', 'cluster:monitor/nodes/info'], + }, + }, + + license_management_user: { + elasticsearch: { + cluster: ['manage'], + }, + }, + + logstash_read_user: { + elasticsearch: { + indices: [ + { + names: ['.logstash*'], + privileges: ['read'], + }, + ], + }, + }, + + remote_clusters_user: { + elasticsearch: { + cluster: ['manage'], + }, + }, }, defaultRoles: ['superuser'], }, diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts index dd81c860e9fa83..5c42c1978a0b5c 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts @@ -21,7 +21,7 @@ export class AlertingFixturePlugin implements Plugin { UserAtSpaceScenarios.forEach((scenario) => { it(`${scenario.id}`, async () => { @@ -35,10 +37,13 @@ export default function catalogueTests({ getService }: FtrProviderContext) { case 'dual_privileges_all at everything_space': { expect(uiCapabilities.success).to.be(true); expect(uiCapabilities.value).to.have.property('catalogue'); - // everything except ml and monitoring is enabled + // everything except ml, monitoring, and ES features are enabled const expected = mapValues( uiCapabilities.value!.catalogue, - (enabled, catalogueId) => catalogueId !== 'ml' && catalogueId !== 'monitoring' + (enabled, catalogueId) => + catalogueId !== 'ml' && + catalogueId !== 'monitoring' && + !esFeatureExceptions.includes(catalogueId) ); expect(uiCapabilities.value!.catalogue).to.eql(expected); break; @@ -49,8 +54,16 @@ export default function catalogueTests({ getService }: FtrProviderContext) { case 'everything_space_read at everything_space': { expect(uiCapabilities.success).to.be(true); expect(uiCapabilities.value).to.have.property('catalogue'); - // everything except ml and monitoring and enterprise search is enabled - const exceptions = ['ml', 'monitoring', 'appSearch', 'workplaceSearch']; + // everything except spaces, ml, monitoring, the enterprise search suite, and ES features are enabled + // (easier to say: all "proper" Kibana features are enabled) + const exceptions = [ + 'ml', + 'monitoring', + 'appSearch', + 'workplaceSearch', + 'spaces', + ...esFeatureExceptions, + ]; const expected = mapValues( uiCapabilities.value!.catalogue, (enabled, catalogueId) => !exceptions.includes(catalogueId) @@ -58,10 +71,36 @@ export default function catalogueTests({ getService }: FtrProviderContext) { expect(uiCapabilities.value!.catalogue).to.eql(expected); break; } - // the nothing_space has no features enabled, so even if we have - // privileges to perform these actions, we won't be able to - case 'superuser at nothing_space': + // the nothing_space has no Kibana features enabled, so even if we have + // privileges to perform these actions, we won't be able to. + // Note that ES features may still be enabled if the user has privileges, since + // they cannot be disabled at the space level at this time. + case 'superuser at nothing_space': { + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('catalogue'); + // everything is disabled except for the es feature exceptions and spaces management + const expected = mapValues( + uiCapabilities.value!.catalogue, + (enabled, catalogueId) => + esFeatureExceptions.includes(catalogueId) || catalogueId === 'spaces' + ); + expect(uiCapabilities.value!.catalogue).to.eql(expected); + break; + } + // the nothing_space has no Kibana features enabled, so even if we have + // privileges to perform these actions, we won't be able to. case 'global_all at nothing_space': + case 'dual_privileges_all at nothing_space': { + // everything is disabled except for spaces management + const expected = mapValues( + uiCapabilities.value!.catalogue, + (enabled, catalogueId) => catalogueId === 'spaces' + ); + expect(uiCapabilities.value!.catalogue).to.eql(expected); + break; + } + // the nothing_space has no Kibana features enabled, so even if we have + // privileges to perform these actions, we won't be able to. case 'global_read at nothing_space': case 'dual_privileges_all at nothing_space': case 'dual_privileges_read at nothing_space': @@ -78,7 +117,10 @@ export default function catalogueTests({ getService }: FtrProviderContext) { expect(uiCapabilities.success).to.be(true); expect(uiCapabilities.value).to.have.property('catalogue'); // everything is disabled - const expected = mapValues(uiCapabilities.value!.catalogue, () => false); + const expected = mapValues( + uiCapabilities.value!.catalogue, + (enabled, catalogueId) => false + ); expect(uiCapabilities.value!.catalogue).to.eql(expected); break; } diff --git a/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts b/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts index 9ed1c890bf57f4..0cfc6e617eab19 100644 --- a/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts +++ b/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts @@ -13,6 +13,8 @@ import { UserScenarios } from '../scenarios'; export default function catalogueTests({ getService }: FtrProviderContext) { const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities'); + const esFeatureExceptions = ['security', 'rollup_jobs', 'reporting', 'transform', 'watcher']; + describe('catalogue', () => { UserScenarios.forEach((scenario) => { it(`${scenario.fullName}`, async () => { @@ -35,10 +37,13 @@ export default function catalogueTests({ getService }: FtrProviderContext) { case 'dual_privileges_all': { expect(uiCapabilities.success).to.be(true); expect(uiCapabilities.value).to.have.property('catalogue'); - // everything except ml and monitoring is enabled + // everything except ml, monitoring, and ES features are enabled const expected = mapValues( uiCapabilities.value!.catalogue, - (enabled, catalogueId) => catalogueId !== 'ml' && catalogueId !== 'monitoring' + (enabled, catalogueId) => + catalogueId !== 'ml' && + catalogueId !== 'monitoring' && + !esFeatureExceptions.includes(catalogueId) ); expect(uiCapabilities.value!.catalogue).to.eql(expected); break; @@ -47,8 +52,14 @@ export default function catalogueTests({ getService }: FtrProviderContext) { case 'dual_privileges_read': { expect(uiCapabilities.success).to.be(true); expect(uiCapabilities.value).to.have.property('catalogue'); - // everything except ml and monitoring and enterprise search is enabled - const exceptions = ['ml', 'monitoring', 'appSearch', 'workplaceSearch']; + // everything except ml, monitoring, enterprise search, and es features are enabled + const exceptions = [ + 'ml', + 'monitoring', + 'appSearch', + 'workplaceSearch', + ...esFeatureExceptions, + ]; const expected = mapValues( uiCapabilities.value!.catalogue, (enabled, catalogueId) => !exceptions.includes(catalogueId) diff --git a/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts b/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts index 2ef5108403427e..baae3286ddb5d9 100644 --- a/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts +++ b/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts @@ -13,6 +13,8 @@ import { SpaceScenarios } from '../scenarios'; export default function catalogueTests({ getService }: FtrProviderContext) { const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities'); + const esFeatureExceptions = ['security', 'rollup_jobs', 'reporting', 'transform', 'watcher']; + describe('catalogue', () => { SpaceScenarios.forEach((scenario) => { it(`${scenario.name}`, async () => { @@ -29,8 +31,12 @@ export default function catalogueTests({ getService }: FtrProviderContext) { case 'nothing_space': { expect(uiCapabilities.success).to.be(true); expect(uiCapabilities.value).to.have.property('catalogue'); - // everything is disabled - const expected = mapValues(uiCapabilities.value!.catalogue, () => false); + // everything is disabled except for ES features and spaces management + const expected = mapValues( + uiCapabilities.value!.catalogue, + (enabled, catalogueId) => + esFeatureExceptions.includes(catalogueId) || catalogueId === 'spaces' + ); expect(uiCapabilities.value!.catalogue).to.eql(expected); break; } From 54af4aac379d11205b82b0abd47c5afca9fc7fd8 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Wed, 5 Aug 2020 14:26:58 -0400 Subject: [PATCH 02/17] add manage_templates as a required cluster privilege for index management --- x-pack/plugins/index_management/server/plugin.ts | 2 +- x-pack/test/functional/config.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/index_management/server/plugin.ts b/x-pack/plugins/index_management/server/plugin.ts index 21dd6a63ee00dc..22610d334accbf 100644 --- a/x-pack/plugins/index_management/server/plugin.ts +++ b/x-pack/plugins/index_management/server/plugin.ts @@ -84,7 +84,7 @@ export class IndexMgmtServerPlugin implements Plugin Date: Thu, 6 Aug 2020 10:23:40 -0400 Subject: [PATCH 03/17] fix cluster privilege name --- x-pack/plugins/index_management/server/plugin.ts | 2 +- x-pack/test/functional/config.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/index_management/server/plugin.ts b/x-pack/plugins/index_management/server/plugin.ts index 22610d334accbf..30aeeb6b453621 100644 --- a/x-pack/plugins/index_management/server/plugin.ts +++ b/x-pack/plugins/index_management/server/plugin.ts @@ -84,7 +84,7 @@ export class IndexMgmtServerPlugin implements Plugin Date: Tue, 1 Sep 2020 10:32:35 -0400 Subject: [PATCH 04/17] addressing first round of PR feedback --- .../security/feature-registration.asciidoc | 4 +- .../server/capabilities/merge_capabilities.ts | 2 +- .../plugins/xpack_main/server/xpack_main.d.ts | 4 +- .../alerts_authorization.test.ts | 9 +- x-pack/plugins/alerts/server/plugin.test.ts | 4 +- .../apm/e2e/cypress/integration/apm.feature | 2 +- ...es_feature.ts => elasticsearch_feature.ts} | 2 +- .../feature_elasticsearch_privileges.ts | 5 +- x-pack/plugins/features/common/index.ts | 4 +- .../common/{feature.ts => kibana_feature.ts} | 0 .../features/public/features_api_client.ts | 6 +- x-pack/plugins/features/public/index.ts | 4 +- .../features/server/feature_registry.test.ts | 15 ++- .../features/server/feature_registry.ts | 16 +-- .../plugins/features/server/feature_schema.ts | 60 ++++++---- x-pack/plugins/features/server/index.ts | 4 +- .../features/server/oss_features.test.ts | 4 +- .../plugins/features/server/oss_features.ts | 2 +- x-pack/plugins/features/server/plugin.test.ts | 46 ++++++-- x-pack/plugins/features/server/plugin.ts | 8 +- .../features/server/routes/index.test.ts | 10 +- .../ui_capabilities_for_features.test.ts | 20 ++-- .../server/ui_capabilities_for_features.ts | 104 +++++++----------- .../roles/__fixtures__/kibana_features.ts | 11 +- .../roles/__fixtures__/kibana_privileges.ts | 6 +- .../roles/edit_role/edit_role_page.test.tsx | 8 +- .../roles/edit_role/edit_role_page.tsx | 6 +- .../feature_table/feature_table.test.tsx | 4 +- .../privilege_space_table.test.tsx | 10 +- .../roles/model/kibana_privileges.ts | 4 +- .../management/roles/model/secured_feature.ts | 9 +- .../authorization/authorization_service.ts | 3 +- .../server/authorization/check_privileges.ts | 63 +++-------- .../check_privileges_dynamically.ts | 2 +- .../check_saved_objects_privileges.test.ts | 2 +- .../check_saved_objects_privileges.ts | 2 +- .../disable_ui_capabilities.test.ts | 30 ++--- .../authorization/disable_ui_capabilities.ts | 18 +-- .../alerting.test.ts | 10 +- .../feature_privilege_builder/alerting.ts | 7 +- .../feature_privilege_builder/api.ts | 7 +- .../feature_privilege_builder/app.ts | 7 +- .../feature_privilege_builder/catalogue.ts | 7 +- .../feature_privilege_builder.ts | 6 +- .../feature_privilege_builder/index.ts | 4 +- .../feature_privilege_builder/management.ts | 7 +- .../feature_privilege_builder/navlink.ts | 7 +- .../feature_privilege_builder/saved_object.ts | 7 +- .../feature_privilege_builder/ui.ts | 7 +- .../feature_privilege_iterator.test.ts | 22 ++-- .../feature_privilege_iterator.ts | 6 +- .../sub_feature_privilege_iterator.ts | 4 +- .../privileges/privileges.test.ts | 104 +++++++++--------- .../authorization/privileges/privileges.ts | 7 +- .../security/server/authorization/types.ts | 40 +++++++ .../validate_feature_privileges.test.ts | 20 ++-- .../validate_feature_privileges.ts | 6 +- .../validate_reserved_privileges.test.ts | 16 +-- .../validate_reserved_privileges.ts | 4 +- .../routes/authorization/roles/put.test.ts | 4 +- .../server/routes/authorization/roles/put.ts | 6 +- .../plugins/security/server/routes/index.ts | 4 +- .../secure_saved_objects_client_wrapper.ts | 2 +- .../enabled_features.test.tsx | 4 +- .../enabled_features/enabled_features.tsx | 4 +- .../enabled_features/feature_table.tsx | 8 +- .../edit_space/manage_space_page.test.tsx | 4 +- .../edit_space/manage_space_page.tsx | 6 +- .../management/lib/feature_utils.test.ts | 4 +- .../public/management/lib/feature_utils.ts | 4 +- .../spaces_grid/spaces_grid_page.tsx | 4 +- .../spaces_grid/spaces_grid_pages.test.tsx | 4 +- .../capabilities_switcher.test.ts | 4 +- .../capabilities/capabilities_switcher.ts | 12 +- .../on_post_auth_interceptor.test.ts | 4 +- .../spaces_usage_collector.test.ts | 6 +- .../apis/features/features/features.ts | 4 +- 77 files changed, 484 insertions(+), 422 deletions(-) rename x-pack/plugins/features/common/{es_feature.ts => elasticsearch_feature.ts} (94%) rename x-pack/plugins/features/common/{feature.ts => kibana_feature.ts} (100%) diff --git a/docs/developer/architecture/security/feature-registration.asciidoc b/docs/developer/architecture/security/feature-registration.asciidoc index 3724624dbb9170..b1c967a9484974 100644 --- a/docs/developer/architecture/security/feature-registration.asciidoc +++ b/docs/developer/architecture/security/feature-registration.asciidoc @@ -45,12 +45,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 <> and <> |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 <> |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. diff --git a/src/core/server/capabilities/merge_capabilities.ts b/src/core/server/capabilities/merge_capabilities.ts index 9f277d926e340a..06869089598a92 100644 --- a/src/core/server/capabilities/merge_capabilities.ts +++ b/src/core/server/capabilities/merge_capabilities.ts @@ -21,7 +21,7 @@ import { mergeWith } from 'lodash'; import { Capabilities } from './types'; export const mergeCapabilities = (...sources: Array>): Capabilities => - mergeWith({}, ...sources, (a: any, b: any, key: any) => { + mergeWith({}, ...sources, (a: any, b: any) => { if ( (typeof a === 'boolean' && typeof b === 'object') || (typeof a === 'object' && typeof b === 'boolean') diff --git a/x-pack/legacy/plugins/xpack_main/server/xpack_main.d.ts b/x-pack/legacy/plugins/xpack_main/server/xpack_main.d.ts index 2a59c2f1366d4a..5763f921bff860 100644 --- a/x-pack/legacy/plugins/xpack_main/server/xpack_main.d.ts +++ b/x-pack/legacy/plugins/xpack_main/server/xpack_main.d.ts @@ -5,11 +5,11 @@ */ 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'; export interface XPackMainPlugin { info: XPackInfo; - getFeatures(): Feature[]; + getFeatures(): KibanaFeature[]; } diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts index c8e653f1af16ba..c2506381b9df98 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts @@ -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: [], diff --git a/x-pack/plugins/alerts/server/plugin.test.ts b/x-pack/plugins/alerts/server/plugin.test.ts index 910a808702d998..026aa0c5238dc5 100644 --- a/x-pack/plugins/alerts/server/plugin.test.ts +++ b/x-pack/plugins/alerts/server/plugin.test.ts @@ -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()', () => { @@ -160,7 +160,7 @@ describe('Alerting Plugin', () => { function mockFeatures() { const features = featuresPluginMock.createSetup(); features.getKibanaFeatures.mockReturnValue([ - new Feature({ + new KibanaFeature({ id: 'appName', name: 'appName', app: [], diff --git a/x-pack/plugins/apm/e2e/cypress/integration/apm.feature b/x-pack/plugins/apm/e2e/cypress/integration/apm.feature index 285615108266b9..82d896c5ba17e3 100644 --- a/x-pack/plugins/apm/e2e/cypress/integration/apm.feature +++ b/x-pack/plugins/apm/e2e/cypress/integration/apm.feature @@ -1,4 +1,4 @@ -Feature: APM +KibanaFeature: APM Scenario: Transaction duration charts Given a user browses the APM UI application diff --git a/x-pack/plugins/features/common/es_feature.ts b/x-pack/plugins/features/common/elasticsearch_feature.ts similarity index 94% rename from x-pack/plugins/features/common/es_feature.ts rename to x-pack/plugins/features/common/elasticsearch_feature.ts index b40c67430bcd05..4566afc77bd8b3 100644 --- a/x-pack/plugins/features/common/es_feature.ts +++ b/x-pack/plugins/features/common/elasticsearch_feature.ts @@ -44,7 +44,7 @@ export interface ElasticsearchFeatureConfig { /** * Feature privilege definition. Specify one or more privileges which grant access to this feature. - * Users must satisfy at least one of the defined sets of privileges in order to be granted access. + * Users must satisfy all privileges in at least one of the defined sets of privileges in order to be granted access. * * @example * ```ts diff --git a/x-pack/plugins/features/common/feature_elasticsearch_privileges.ts b/x-pack/plugins/features/common/feature_elasticsearch_privileges.ts index 6112399f492574..1100b2cc648c98 100644 --- a/x-pack/plugins/features/common/feature_elasticsearch_privileges.ts +++ b/x-pack/plugins/features/common/feature_elasticsearch_privileges.ts @@ -41,7 +41,10 @@ export interface FeatureElasticsearchPrivileges { /** * A set of Elasticsearch roles which are required for this feature to be enabled. * - * @deprecated do not rely on hard-coded role names + * @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[]; diff --git a/x-pack/plugins/features/common/index.ts b/x-pack/plugins/features/common/index.ts index b5e15074b96441..a08de2f118712a 100644 --- a/x-pack/plugins/features/common/index.ts +++ b/x-pack/plugins/features/common/index.ts @@ -6,8 +6,8 @@ export { FeatureElasticsearchPrivileges } from './feature_elasticsearch_privileges'; export { FeatureKibanaPrivileges } from './feature_kibana_privileges'; -export { ElasticsearchFeature, ElasticsearchFeatureConfig } from './es_feature'; -export { KibanaFeature as Feature, KibanaFeatureConfig as FeatureConfig } from './feature'; +export { ElasticsearchFeature, ElasticsearchFeatureConfig } from './elasticsearch_feature'; +export { KibanaFeature, KibanaFeatureConfig } from './kibana_feature'; export { SubFeature, SubFeatureConfig, diff --git a/x-pack/plugins/features/common/feature.ts b/x-pack/plugins/features/common/kibana_feature.ts similarity index 100% rename from x-pack/plugins/features/common/feature.ts rename to x-pack/plugins/features/common/kibana_feature.ts diff --git a/x-pack/plugins/features/public/features_api_client.ts b/x-pack/plugins/features/public/features_api_client.ts index 50cc54a197f568..cacc623aa853f8 100644 --- a/x-pack/plugins/features/public/features_api_client.ts +++ b/x-pack/plugins/features/public/features_api_client.ts @@ -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('/api/features'); - return features.map((config) => new Feature(config)); + const features = await this.http.get('/api/features'); + return features.map((config) => new KibanaFeature(config)); } } diff --git a/x-pack/plugins/features/public/index.ts b/x-pack/plugins/features/public/index.ts index f19c7f947d97fa..7d86312e466ee4 100644 --- a/x-pack/plugins/features/public/index.ts +++ b/x-pack/plugins/features/public/index.ts @@ -8,8 +8,8 @@ import { PluginInitializer } from 'src/core/public'; import { FeaturesPlugin, FeaturesPluginSetup, FeaturesPluginStart } from './plugin'; export { - Feature, - FeatureConfig, + KibanaFeature, + KibanaFeatureConfig, FeatureKibanaPrivileges, SubFeatureConfig, SubFeaturePrivilegeConfig, diff --git a/x-pack/plugins/features/server/feature_registry.test.ts b/x-pack/plugins/features/server/feature_registry.test.ts index af44a0872b87db..24aae3a69ee5d9 100644 --- a/x-pack/plugins/features/server/feature_registry.test.ts +++ b/x-pack/plugins/features/server/feature_registry.test.ts @@ -5,8 +5,7 @@ */ import { FeatureRegistry } from './feature_registry'; -import { KibanaFeatureConfig } from '../common/feature'; -import { ElasticsearchFeatureConfig } from '../common'; +import { ElasticsearchFeatureConfig, KibanaFeatureConfig } from '../common'; describe('FeatureRegistry', () => { describe('Kibana Features', () => { @@ -1243,7 +1242,7 @@ describe('FeatureRegistry', () => { id: 'test-feature', privileges: [ { - requiredClusterPrivileges: [], + requiredClusterPrivileges: ['all'], ui: [], }, ], @@ -1307,7 +1306,7 @@ describe('FeatureRegistry', () => { ); }); - it('requires privileges to declare requiredClusterPrivileges', () => { + it('requires privileges to declare some form of required es privileges', () => { const feature: ElasticsearchFeatureConfig = { id: 'test-feature', privileges: [ @@ -1320,7 +1319,7 @@ describe('FeatureRegistry', () => { expect(() => featureRegistry.registerElasticsearchFeature(feature) ).toThrowErrorMatchingInlineSnapshot( - `"child \\"privileges\\" fails because [\\"privileges\\" at position 0 fails because [child \\"requiredClusterPrivileges\\" fails because [\\"requiredClusterPrivileges\\" is required]]]"` + `"Feature test-feature has a privilege definition at index 0 without any privileges defined."` ); }); @@ -1329,7 +1328,7 @@ describe('FeatureRegistry', () => { id: 'test-feature', privileges: [ { - requiredClusterPrivileges: [], + requiredClusterPrivileges: ['all'], ui: [], }, ], @@ -1354,7 +1353,7 @@ describe('FeatureRegistry', () => { id: 'test-feature', privileges: [ { - requiredClusterPrivileges: [], + requiredClusterPrivileges: ['all'], ui: [], }, ], @@ -1379,7 +1378,7 @@ describe('FeatureRegistry', () => { id: 'test-feature', privileges: [ { - requiredClusterPrivileges: [], + requiredClusterPrivileges: ['all'], ui: [], }, ], diff --git a/x-pack/plugins/features/server/feature_registry.ts b/x-pack/plugins/features/server/feature_registry.ts index 31f2873e8c4722..d357bdb782797f 100644 --- a/x-pack/plugins/features/server/feature_registry.ts +++ b/x-pack/plugins/features/server/feature_registry.ts @@ -6,8 +6,8 @@ import { cloneDeep, uniq } from 'lodash'; import { - FeatureConfig, - Feature, + KibanaFeatureConfig, + KibanaFeature, FeatureKibanaPrivileges, ElasticsearchFeatureConfig, ElasticsearchFeature, @@ -16,10 +16,10 @@ import { validateKibanaFeature, validateElasticsearchFeature } from './feature_s export class FeatureRegistry { private locked = false; - private kibanaFeatures: Record = {}; + private kibanaFeatures: Record = {}; private esFeatures: Record = {}; - public registerKibanaFeature(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.` @@ -55,9 +55,11 @@ export class FeatureRegistry { this.esFeatures[feature.id] = featureCopy; } - public getAllKibanaFeatures(): Feature[] { + public getAllKibanaFeatures(): KibanaFeature[] { this.locked = true; - return Object.values(this.kibanaFeatures).map((featureConfig) => new Feature(featureConfig)); + return Object.values(this.kibanaFeatures).map( + (featureConfig) => new KibanaFeature(featureConfig) + ); } public getAllElasticsearchFeatures(): ElasticsearchFeature[] { @@ -68,7 +70,7 @@ export class FeatureRegistry { } } -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); diff --git a/x-pack/plugins/features/server/feature_schema.ts b/x-pack/plugins/features/server/feature_schema.ts index 92dfe32ef9722f..06a3eb158d99d8 100644 --- a/x-pack/plugins/features/server/feature_schema.ts +++ b/x-pack/plugins/features/server/feature_schema.ts @@ -8,7 +8,7 @@ import Joi from 'joi'; import { difference } from 'lodash'; import { Capabilities as UICapabilities } from '../../../../src/core/server'; -import { KibanaFeatureConfig } from '../common/feature'; +import { KibanaFeatureConfig } from '../common'; import { FeatureKibanaPrivileges, ElasticsearchFeatureConfig } from '.'; // Each feature gets its own property on the UICapabilities object, @@ -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,12 +64,12 @@ 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), }) ), }); @@ -93,15 +93,15 @@ const kibanaFeatureSchema = 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,13 +110,20 @@ const kibanaFeatureSchema = Joi.object({ .items( Joi.object({ id: Joi.string().regex(reservedFeaturePrrivilegePartRegex).required(), - privilege: privilegeSchema.required(), + privilege: kibanaPrivilegeSchema.required(), }) ) .required(), }), }); +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) @@ -124,19 +131,7 @@ const elasticsearchFeatureSchema = Joi.object({ .required(), management: managementSchema, catalogue: catalogueSchema, - privileges: Joi.array() - .items( - Joi.object({ - ui: Joi.array().items(Joi.string()).required(), - requiredClusterPrivileges: Joi.array().items(Joi.string()).required(), - requiredIndexPrivileges: Joi.object().pattern( - Joi.string(), - Joi.array().items(Joi.string()) - ), - requiredRoles: Joi.array().items(Joi.string()), - }) - ) - .required(), + privileges: Joi.array().items(elasticsearchPrivilegeSchema).required(), }); export function validateKibanaFeature(feature: KibanaFeatureConfig) { @@ -331,4 +326,23 @@ export function validateElasticsearchFeature(feature: ElasticsearchFeatureConfig 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.` + ); + } + }); } diff --git a/x-pack/plugins/features/server/index.ts b/x-pack/plugins/features/server/index.ts index 13d5f9047a322b..28c0fee041594d 100644 --- a/x-pack/plugins/features/server/index.ts +++ b/x-pack/plugins/features/server/index.ts @@ -14,8 +14,8 @@ import { Plugin } from './plugin'; export { uiCapabilitiesRegex } from './feature_schema'; export { - Feature, - FeatureConfig, + KibanaFeature, + KibanaFeatureConfig, FeatureKibanaPrivileges, ElasticsearchFeature, ElasticsearchFeatureConfig, diff --git a/x-pack/plugins/features/server/oss_features.test.ts b/x-pack/plugins/features/server/oss_features.test.ts index c38f2afc88389a..961656aba8bfd7 100644 --- a/x-pack/plugins/features/server/oss_features.test.ts +++ b/x-pack/plugins/features/server/oss_features.test.ts @@ -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); diff --git a/x-pack/plugins/features/server/oss_features.ts b/x-pack/plugins/features/server/oss_features.ts index bbd6529fa5d8b4..6d464d918fbbd2 100644 --- a/x-pack/plugins/features/server/oss_features.ts +++ b/x-pack/plugins/features/server/oss_features.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; -import { KibanaFeatureConfig } from '../common/feature'; +import { KibanaFeatureConfig } from '../common'; export interface BuildOSSFeaturesParams { savedObjectTypes: string[]; diff --git a/x-pack/plugins/features/server/plugin.test.ts b/x-pack/plugins/features/server/plugin.test.ts index 28cb531d4b15ed..049027d806466e 100644 --- a/x-pack/plugins/features/server/plugin.test.ts +++ b/x-pack/plugins/features/server/plugin.test.ts @@ -21,19 +21,19 @@ typeRegistry.getVisibleTypes.mockReturnValue([ coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); describe('Features Plugin', () => { - it('returns OSS + registered features', async () => { + it('returns OSS + registered kibana features', async () => { const plugin = new Plugin(initContext); - const { registerKibanaFeature: registerFeature } = await plugin.setup(coreSetup, {}); - registerFeature({ + const { registerKibanaFeature } = await plugin.setup(coreSetup, {}); + registerKibanaFeature({ id: 'baz', name: 'baz', app: [], privileges: null, }); - const { getKibanaFeatures: 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", @@ -47,7 +47,7 @@ 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 { registerKibanaFeature: registerFeature } = await plugin.setup(coreSetup, { visTypeTimelion: { uiEnabled: true }, @@ -59,9 +59,9 @@ describe('Features Plugin', () => { privileges: null, }); - const { getKibanaFeatures: 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", @@ -76,16 +76,38 @@ 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 { getKibanaFeatures: 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", + ] + `); + }); }); diff --git a/x-pack/plugins/features/server/plugin.ts b/x-pack/plugins/features/server/plugin.ts index 3e85970c5c691c..8cec1724e6df24 100644 --- a/x-pack/plugins/features/server/plugin.ts +++ b/x-pack/plugins/features/server/plugin.ts @@ -15,11 +15,15 @@ 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 { KibanaFeature, KibanaFeatureConfig } from '../common/feature'; import { uiCapabilitiesForFeatures } from './ui_capabilities_for_features'; import { buildOSSFeatures } from './oss_features'; import { defineRoutes } from './routes'; -import { ElasticsearchFeatureConfig, ElasticsearchFeature } from '../common'; +import { + ElasticsearchFeatureConfig, + ElasticsearchFeature, + KibanaFeature, + KibanaFeatureConfig, +} from '../common'; /** * Describes public Features plugin contract returned at the `setup` stage. diff --git a/x-pack/plugins/features/server/routes/index.test.ts b/x-pack/plugins/features/server/routes/index.test.ts index e1a406b74a49d9..30aa6d07f6b5a2 100644 --- a/x-pack/plugins/features/server/routes/index.test.ts +++ b/x-pack/plugins/features/server/routes/index.test.ts @@ -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 { @@ -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 })); diff --git a/x-pack/plugins/features/server/ui_capabilities_for_features.test.ts b/x-pack/plugins/features/server/ui_capabilities_for_features.test.ts index e53ab88714eb35..7532bc0573b08d 100644 --- a/x-pack/plugins/features/server/ui_capabilities_for_features.test.ts +++ b/x-pack/plugins/features/server/ui_capabilities_for_features.test.ts @@ -5,7 +5,7 @@ */ import { uiCapabilitiesForFeatures } from './ui_capabilities_for_features'; -import { Feature } from '.'; +import { KibanaFeature } from '.'; import { SubFeaturePrivilegeGroupConfig, ElasticsearchFeature } from '../common'; function createKibanaFeaturePrivilege(capabilities: string[] = []) { @@ -42,7 +42,7 @@ describe('populateUICapabilities', () => { expect( uiCapabilitiesForFeatures( [ - new Feature({ + new KibanaFeature({ id: 'newFeature', name: 'my new feature', app: ['bar-app'], @@ -88,7 +88,7 @@ describe('populateUICapabilities', () => { expect( uiCapabilitiesForFeatures( [ - new Feature({ + new KibanaFeature({ id: 'newFeature', name: 'my new feature', navLinkId: 'newFeatureNavLink', @@ -141,7 +141,7 @@ describe('populateUICapabilities', () => { expect( uiCapabilitiesForFeatures( [ - new Feature({ + new KibanaFeature({ id: 'newFeature', name: 'my new feature', navLinkId: 'newFeatureNavLink', @@ -210,7 +210,7 @@ describe('populateUICapabilities', () => { expect( uiCapabilitiesForFeatures( [ - new Feature({ + new KibanaFeature({ id: 'newFeature', name: 'my new feature', navLinkId: 'newFeatureNavLink', @@ -240,7 +240,7 @@ describe('populateUICapabilities', () => { expect( uiCapabilitiesForFeatures( [ - new Feature({ + new KibanaFeature({ id: 'newFeature', name: 'my new feature', navLinkId: 'newFeatureNavLink', @@ -284,7 +284,7 @@ describe('populateUICapabilities', () => { expect( uiCapabilitiesForFeatures( [ - new Feature({ + new KibanaFeature({ id: 'newFeature', name: 'my new feature', navLinkId: 'newFeatureNavLink', @@ -355,7 +355,7 @@ describe('populateUICapabilities', () => { expect( uiCapabilitiesForFeatures( [ - new Feature({ + new KibanaFeature({ id: 'newFeature', name: 'my new feature', navLinkId: 'newFeatureNavLink', @@ -365,7 +365,7 @@ describe('populateUICapabilities', () => { read: createKibanaFeaturePrivilege(['capability3', 'capability4']), }, }), - new Feature({ + new KibanaFeature({ id: 'anotherNewFeature', name: 'another new feature', app: ['bar-app'], @@ -374,7 +374,7 @@ describe('populateUICapabilities', () => { read: createKibanaFeaturePrivilege(['capability3', 'capability4']), }, }), - new Feature({ + new KibanaFeature({ id: 'yetAnotherNewFeature', name: 'yet another new feature', navLinkId: 'yetAnotherNavLink', diff --git a/x-pack/plugins/features/server/ui_capabilities_for_features.ts b/x-pack/plugins/features/server/ui_capabilities_for_features.ts index ad470a3e5cbe46..8fc8fd8a038975 100644 --- a/x-pack/plugins/features/server/ui_capabilities_for_features.ts +++ b/x-pack/plugins/features/server/ui_capabilities_for_features.ts @@ -5,9 +5,9 @@ */ import _ from 'lodash'; +import { RecursiveReadonly } from '@kbn/utility-types'; import { Capabilities as UICapabilities } from '../../../../src/core/server'; -import { KibanaFeature } from '../common/feature'; -import { ElasticsearchFeature } from '../common'; +import { ElasticsearchFeature, KibanaFeature } from '../common'; const ELIGIBLE_FLAT_MERGE_KEYS = ['catalogue'] as const; const ELIGIBLE_DEEP_MERGE_KEYS = ['management'] as const; @@ -20,13 +20,20 @@ export function uiCapabilitiesForFeatures( features: KibanaFeature[], elasticsearchFeatures: ElasticsearchFeature[] ): UICapabilities { - const featureCapabilities = features.map(getCapabilitiesFromFeature); - const esFeatureCapabilities = elasticsearchFeatures.map(getCapabilitiesFromElasticsearchFeature); + const kibanaFeatureCapabilities = features.map(getCapabilitiesFromFeature); + const elasticsearchFeatureCapabilities = elasticsearchFeatures.map(getCapabilitiesFromFeature); - return buildCapabilities(...featureCapabilities, ...esFeatureCapabilities); + return buildCapabilities(...kibanaFeatureCapabilities, ...elasticsearchFeatureCapabilities); } -function getCapabilitiesFromFeature(feature: KibanaFeature): FeatureCapabilities { +function getCapabilitiesFromFeature( + feature: + | Pick< + KibanaFeature, + 'id' | 'catalogue' | 'management' | 'privileges' | 'subFeatures' | 'reserved' + > + | Pick +): FeatureCapabilities { const UIFeatureCapabilities: FeatureCapabilities = { catalogue: {}, [feature.id]: {}, @@ -60,14 +67,19 @@ function getCapabilitiesFromFeature(feature: KibanaFeature): 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.reserved?.privileges) { - featurePrivileges.push(...feature.reserved.privileges.map((rp) => rp.privilege)); + const featurePrivileges = Object.values(feature.privileges ?? {}) as Writable< + Array<{ ui: RecursiveReadonly }> + >; + + 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) => { @@ -86,58 +98,18 @@ function getCapabilitiesFromFeature(feature: KibanaFeature): FeatureCapabilities return UIFeatureCapabilities; } -function getCapabilitiesFromElasticsearchFeature( - feature: ElasticsearchFeature -): FeatureCapabilities { - const UIFeatureCapabilities: FeatureCapabilities = { - catalogue: {}, - [feature.id]: {}, - }; - - if (feature.catalogue) { - UIFeatureCapabilities.catalogue = { - ...UIFeatureCapabilities.catalogue, - ...feature.catalogue.reduce( - (acc, capability) => ({ - ...acc, - [capability]: true, - }), - {} - ), - }; - } - - 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, - }; - }, {}), - }; - }, {}); - } - - const featurePrivileges = Object.values(feature.privileges ?? {}); - - featurePrivileges.forEach((privilege) => { - UIFeatureCapabilities[feature.id] = { - ...UIFeatureCapabilities[feature.id], - ...privilege.ui.reduce( - (privilegeAcc, capability) => ({ - ...privilegeAcc, - [capability]: true, - }), - {} - ), - }; - }); - - return UIFeatureCapabilities; +function isKibanaFeature( + feature: Partial | Partial +): 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 { diff --git a/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_features.ts b/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_features.ts index 08561234fd7067..2b78355787ff23 100644 --- a/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_features.ts +++ b/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_features.ts @@ -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 & { + 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: [], diff --git a/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_privileges.ts b/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_privileges.ts index fe497b27bf20a4..02a18039cee748 100644 --- a/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_privileges.ts +++ b/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_privileges.ts @@ -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,7 +15,7 @@ import { SecurityLicenseFeatures } from '../../..'; import { featuresPluginMock } from '../../../../../features/server/mocks'; export const createRawKibanaPrivileges = ( - features: Feature[], + features: KibanaFeature[], { allowSubFeaturePrivileges = true } = {} ) => { const featuresService = featuresPluginMock.createSetup(); @@ -33,7 +33,7 @@ export const createRawKibanaPrivileges = ( }; export const createKibanaPrivileges = ( - features: Feature[], + features: KibanaFeature[], { allowSubFeaturePrivileges = true } = {} ) => { return new KibanaPrivileges( diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx index f6fe2f394fd360..bf791b37087bda 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx @@ -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 = () => { diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx index 15888733ec4247..01f8969e61f430 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx @@ -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(null); + const [features, setFeatures] = useState(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); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.test.tsx index 2a0922d614f1db..02d692bf9f5070 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.test.tsx @@ -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; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx index 5530d9964f8cd4..bc606133459107 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx @@ -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: [], diff --git a/x-pack/plugins/security/public/management/roles/model/kibana_privileges.ts b/x-pack/plugins/security/public/management/roles/model/kibana_privileges.ts index fd93aaa23194a7..4739346b2cb768 100644 --- a/x-pack/plugins/security/public/management/roles/model/kibana_privileges.ts +++ b/x-pack/plugins/security/public/management/roles/model/kibana_privileges.ts @@ -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; - constructor(rawKibanaPrivileges: RawKibanaPrivileges, features: Feature[]) { + constructor(rawKibanaPrivileges: RawKibanaPrivileges, features: KibanaFeature[]) { this.global = recordsToBasePrivilegeMap(rawKibanaPrivileges.global); this.spaces = recordsToBasePrivilegeMap(rawKibanaPrivileges.space); this.feature = new Map( diff --git a/x-pack/plugins/security/public/management/roles/model/secured_feature.ts b/x-pack/plugins/security/public/management/roles/model/secured_feature.ts index 284a85583c33c2..894e06b6e58565 100644 --- a/x-pack/plugins/security/public/management/roles/model/secured_feature.ts +++ b/x-pack/plugins/security/public/management/roles/model/secured_feature.ts @@ -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]) diff --git a/x-pack/plugins/security/server/authorization/authorization_service.ts b/x-pack/plugins/security/server/authorization/authorization_service.ts index eb4f7724ddb1e3..4b48f4a2b55235 100644 --- a/x-pack/plugins/security/server/authorization/authorization_service.ts +++ b/x-pack/plugins/security/server/authorization/authorization_service.ts @@ -25,7 +25,7 @@ import { import { SpacesService } from '../plugin'; import { Actions } from './actions'; -import { CheckPrivilegesWithRequest, checkPrivilegesWithRequestFactory } from './check_privileges'; +import { checkPrivilegesWithRequestFactory } from './check_privileges'; import { CheckPrivilegesDynamicallyWithRequest, checkPrivilegesDynamicallyWithRequestFactory, @@ -44,6 +44,7 @@ import { validateReservedPrivileges } from './validate_reserved_privileges'; import { registerPrivilegesWithCluster } from './register_privileges_with_cluster'; import { APPLICATION_PREFIX } from '../../common/constants'; import { SecurityLicense } from '../../common/licensing'; +import { CheckPrivilegesWithRequest } from './types'; import { AuthenticatedUser } from '..'; export { Actions } from './actions'; diff --git a/x-pack/plugins/security/server/authorization/check_privileges.ts b/x-pack/plugins/security/server/authorization/check_privileges.ts index ccba95ef845569..27e1802b4e5c2a 100644 --- a/x-pack/plugins/security/server/authorization/check_privileges.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges.ts @@ -12,6 +12,8 @@ import { HasPrivilegesResponse, HasPrivilegesResponseApplication, CheckPrivilegesPayload, + CheckPrivileges, + CheckPrivilegesResponse, } from './types'; import { validateEsPrivilegeResponse } from './validate_es_response'; @@ -20,44 +22,6 @@ interface CheckPrivilegesActions { version: string; } -export interface CheckPrivilegesResponse { - hasAllRequested: boolean; - username: string; - privileges: { - kibana: Array<{ - /** - * If this attribute is undefined, this element is a privilege for the global resource. - */ - resource?: string; - privilege: string; - authorized: boolean; - }>; - elasticsearch: { - cluster: Array<{ - privilege: string; - authorized: boolean; - }>; - index: { - [indexName: string]: Array<{ - privilege: string; - authorized: boolean; - }>; - }; - }; - }; -} - -export type CheckPrivilegesWithRequest = (request: KibanaRequest) => CheckPrivileges; - -export interface CheckPrivileges { - atSpace(spaceId: string, privileges: CheckPrivilegesPayload): Promise; - atSpaces( - spaceIds: string[], - privileges: CheckPrivilegesPayload - ): Promise; - globally(privileges: CheckPrivilegesPayload): Promise; -} - export function checkPrivilegesWithRequestFactory( actions: CheckPrivilegesActions, clusterClient: ILegacyClusterClient, @@ -118,18 +82,17 @@ export function checkPrivilegesWithRequestFactory( }) ); - const indexPrivileges = Object.entries(hasPrivilegesResponse.index ?? {}).reduce( - (acc, [index, indexResponse]) => { - return { - ...acc, - [index]: Object.entries(indexResponse).map(([privilege, authorized]) => ({ - privilege, - authorized, - })), - }; - }, - {} as CheckPrivilegesResponse['privileges']['elasticsearch']['index'] - ); + const indexPrivileges = Object.entries(hasPrivilegesResponse.index ?? {}).reduce< + CheckPrivilegesResponse['privileges']['elasticsearch']['index'] + >((acc, [index, indexResponse]) => { + return { + ...acc, + [index]: Object.entries(indexResponse).map(([privilege, authorized]) => ({ + privilege, + authorized, + })), + }; + }, {}); if (hasIncompatibleVersion(applicationPrivilegesResponse)) { throw new Error( diff --git a/x-pack/plugins/security/server/authorization/check_privileges_dynamically.ts b/x-pack/plugins/security/server/authorization/check_privileges_dynamically.ts index 579208becd517f..cd5961e5940ed5 100644 --- a/x-pack/plugins/security/server/authorization/check_privileges_dynamically.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges_dynamically.ts @@ -6,7 +6,7 @@ import { KibanaRequest } from '../../../../../src/core/server'; import { SpacesService } from '../plugin'; -import { CheckPrivilegesResponse, CheckPrivilegesWithRequest } from './check_privileges'; +import { CheckPrivilegesResponse, CheckPrivilegesWithRequest } from './types'; import { CheckPrivilegesPayload } from './types'; export type CheckPrivilegesDynamically = ( diff --git a/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.test.ts b/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.test.ts index 5a00fa8825eebb..5224ed5a628623 100644 --- a/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.test.ts @@ -7,7 +7,7 @@ import { checkSavedObjectsPrivilegesWithRequestFactory } from './check_saved_objects_privileges'; import { httpServerMock } from '../../../../../src/core/server/mocks'; -import { CheckPrivileges, CheckPrivilegesWithRequest } from './check_privileges'; +import { CheckPrivileges, CheckPrivilegesWithRequest } from './types'; import { SpacesService } from '../plugin'; let mockCheckPrivileges: jest.Mocked; diff --git a/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.ts b/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.ts index 808c285a689ad5..63a59c774344a8 100644 --- a/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.ts +++ b/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.ts @@ -6,7 +6,7 @@ import { KibanaRequest } from '../../../../../src/core/server'; import { SpacesService } from '../plugin'; -import { CheckPrivilegesWithRequest, CheckPrivilegesResponse } from './check_privileges'; +import { CheckPrivilegesWithRequest, CheckPrivilegesResponse } from './types'; export type CheckSavedObjectsPrivilegesWithRequest = ( request: KibanaRequest diff --git a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts index 689f7f25d80e31..9ae76d60c72aad 100644 --- a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts +++ b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts @@ -9,7 +9,7 @@ import { disableUICapabilitiesFactory } from './disable_ui_capabilities'; import { httpServerMock, loggingSystemMock } from '../../../../../src/core/server/mocks'; import { authorizationMock } from './index.mock'; -import { Feature, ElasticsearchFeature } from '../../../features/server'; +import { KibanaFeature, ElasticsearchFeature } from '../../../features/server'; import { AuthenticatedUser } from '..'; type MockAuthzOptions = @@ -66,9 +66,9 @@ describe('usingPrivileges', () => { const { usingPrivileges } = disableUICapabilitiesFactory( mockRequest, [ - new Feature({ + new KibanaFeature({ id: 'fooFeature', - name: 'Foo Feature', + name: 'Foo KibanaFeature', app: ['fooApp', 'foo'], navLinkId: 'foo', privileges: null, @@ -154,9 +154,9 @@ describe('usingPrivileges', () => { const { usingPrivileges } = disableUICapabilitiesFactory( mockRequest, [ - new Feature({ + new KibanaFeature({ id: 'fooFeature', - name: 'Foo Feature', + name: 'Foo KibanaFeature', app: ['foo'], navLinkId: 'foo', privileges: null, @@ -295,16 +295,16 @@ describe('usingPrivileges', () => { const { usingPrivileges } = disableUICapabilitiesFactory( mockRequest, [ - new Feature({ + new KibanaFeature({ id: 'fooFeature', - name: 'Foo Feature', + name: 'Foo KibanaFeature', navLinkId: 'foo', app: [], privileges: null, }), - new Feature({ + new KibanaFeature({ id: 'barFeature', - name: 'Bar Feature', + name: 'Bar KibanaFeature', navLinkId: 'bar', app: ['bar'], privileges: null, @@ -395,16 +395,16 @@ describe('usingPrivileges', () => { const { usingPrivileges } = disableUICapabilitiesFactory( mockRequest, [ - new Feature({ + new KibanaFeature({ id: 'fooFeature', - name: 'Foo Feature', + name: 'Foo KibanaFeature', navLinkId: 'foo', app: [], privileges: null, }), - new Feature({ + new KibanaFeature({ id: 'barFeature', - name: 'Bar Feature', + name: 'Bar KibanaFeature', navLinkId: 'bar', app: [], privileges: null, @@ -479,9 +479,9 @@ describe('all', () => { const { all } = disableUICapabilitiesFactory( mockRequest, [ - new Feature({ + new KibanaFeature({ id: 'fooFeature', - name: 'Foo Feature', + name: 'Foo KibanaFeature', app: ['foo'], navLinkId: 'foo', privileges: null, diff --git a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts index 31ee0ab93a9902..e632eff25e6d7d 100644 --- a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts +++ b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts @@ -9,18 +9,18 @@ import { UICapabilities } from 'ui/capabilities'; import { RecursiveReadonlyArray } from '@kbn/utility-types'; import { KibanaRequest, Logger } from '../../../../../src/core/server'; import { - Feature, + KibanaFeature, ElasticsearchFeature, FeatureElasticsearchPrivileges, } from '../../../features/server'; -import { CheckPrivilegesResponse } from './check_privileges'; +import { CheckPrivilegesResponse } from './types'; import { AuthorizationServiceSetup } from '.'; import { AuthenticatedUser } from '..'; export function disableUICapabilitiesFactory( request: KibanaRequest, - features: Feature[], + features: KibanaFeature[], elasticsearchFeatures: ElasticsearchFeature[], logger: Logger, authz: AuthorizationServiceSetup, @@ -33,12 +33,14 @@ export function disableUICapabilitiesFactory( .flatMap((feature) => feature.app) .filter((navLinkId) => navLinkId != null); - const elasticsearchFeatureMap = elasticsearchFeatures.reduce((acc, esFeature) => { + const elasticsearchFeatureMap = elasticsearchFeatures.reduce< + Record> + >((acc, esFeature) => { return { ...acc, [esFeature.id]: esFeature.privileges, }; - }, {} as Record>); + }, {}); const allRequiredClusterPrivileges = Array.from( new Set( @@ -52,7 +54,7 @@ export function disableUICapabilitiesFactory( const allRequiredIndexPrivileges = Object.values(elasticsearchFeatureMap) .flat() .filter((p) => !!p.requiredIndexPrivileges) - .reduce((acc, p) => { + .reduce>((acc, p) => { return { ...acc, ...Object.entries(p.requiredIndexPrivileges!).reduce((acc2, [indexName, privileges]) => { @@ -60,9 +62,9 @@ export function disableUICapabilitiesFactory( ...acc2, [indexName]: [...(acc[indexName] ?? []), ...privileges], }; - }, {} as Record), + }, {}), }; - }, {} as Record); + }, {}); const shouldDisableFeatureUICapability = ( featureId: keyof UICapabilities, diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts index 99d69602db1376..216e88d61c7c0b 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts @@ -6,7 +6,7 @@ import { Actions } from '../../actions'; import { FeaturePrivilegeAlertingBuilder } from './alerting'; -import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; +import { KibanaFeature, FeatureKibanaPrivileges } from '../../../../../features/server'; const version = '1.0.0-zeta1'; @@ -29,7 +29,7 @@ describe(`feature_privilege_builder`, () => { ui: [], }; - const feature = new Feature({ + const feature = new KibanaFeature({ id: 'my-feature', name: 'my-feature', app: [], @@ -60,7 +60,7 @@ describe(`feature_privilege_builder`, () => { ui: [], }; - const feature = new Feature({ + const feature = new KibanaFeature({ id: 'my-feature', name: 'my-feature', app: [], @@ -96,7 +96,7 @@ describe(`feature_privilege_builder`, () => { ui: [], }; - const feature = new Feature({ + const feature = new KibanaFeature({ id: 'my-feature', name: 'my-feature', app: [], @@ -142,7 +142,7 @@ describe(`feature_privilege_builder`, () => { ui: [], }; - const feature = new Feature({ + const feature = new KibanaFeature({ id: 'my-feature', name: 'my-feature', app: [], diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts index 42dd7794ba184f..c64cc0d46eb918 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts @@ -5,7 +5,7 @@ */ import { uniq } from 'lodash'; -import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; +import { KibanaFeature, FeatureKibanaPrivileges } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; const readOperations: string[] = ['get', 'getAlertState', 'find']; @@ -24,7 +24,10 @@ const writeOperations: string[] = [ const allOperations: string[] = [...readOperations, ...writeOperations]; export class FeaturePrivilegeAlertingBuilder extends BaseFeaturePrivilegeBuilder { - public getActions(privilegeDefinition: FeatureKibanaPrivileges, feature: Feature): string[] { + public getActions( + privilegeDefinition: FeatureKibanaPrivileges, + feature: KibanaFeature + ): string[] { const getAlertingPrivilege = ( operations: string[], privilegedTypes: readonly string[], diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/api.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/api.ts index 6b7d94bb0127ef..924150b2326041 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/api.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/api.ts @@ -4,11 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; +import { KibanaFeature, FeatureKibanaPrivileges } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; export class FeaturePrivilegeApiBuilder extends BaseFeaturePrivilegeBuilder { - public getActions(privilegeDefinition: FeatureKibanaPrivileges, feature: Feature): string[] { + public getActions( + privilegeDefinition: FeatureKibanaPrivileges, + feature: KibanaFeature + ): string[] { if (privilegeDefinition.api) { return privilegeDefinition.api.map((operation) => this.actions.api.get(operation)); } diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/app.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/app.ts index 213aa83f2d26e5..5f69a089985b5a 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/app.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/app.ts @@ -4,11 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; +import { KibanaFeature, FeatureKibanaPrivileges } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; export class FeaturePrivilegeAppBuilder extends BaseFeaturePrivilegeBuilder { - public getActions(privilegeDefinition: FeatureKibanaPrivileges, feature: Feature): string[] { + public getActions( + privilegeDefinition: FeatureKibanaPrivileges, + feature: KibanaFeature + ): string[] { const appIds = privilegeDefinition.app; if (!appIds) { diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/catalogue.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/catalogue.ts index f1ea7091b94815..abeeba2837e999 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/catalogue.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/catalogue.ts @@ -4,11 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; +import { KibanaFeature, FeatureKibanaPrivileges } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; export class FeaturePrivilegeCatalogueBuilder extends BaseFeaturePrivilegeBuilder { - public getActions(privilegeDefinition: FeatureKibanaPrivileges, feature: Feature): string[] { + public getActions( + privilegeDefinition: FeatureKibanaPrivileges, + feature: KibanaFeature + ): string[] { const catalogueEntries = privilegeDefinition.catalogue; if (!catalogueEntries) { diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/feature_privilege_builder.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/feature_privilege_builder.ts index 172ab24eb7e514..0eded66d65b063 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/feature_privilege_builder.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/feature_privilege_builder.ts @@ -4,17 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; +import { KibanaFeature, FeatureKibanaPrivileges } from '../../../../../features/server'; import { Actions } from '../../actions'; export interface FeaturePrivilegeBuilder { - getActions(privilegeDefinition: FeatureKibanaPrivileges, feature: Feature): string[]; + getActions(privilegeDefinition: FeatureKibanaPrivileges, feature: KibanaFeature): string[]; } export abstract class BaseFeaturePrivilegeBuilder implements FeaturePrivilegeBuilder { constructor(protected readonly actions: Actions) {} public abstract getActions( privilegeDefinition: FeatureKibanaPrivileges, - feature: Feature + feature: KibanaFeature ): string[]; } diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/index.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/index.ts index 76b664cbbe2a78..998fbc5cc5e243 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/index.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/index.ts @@ -5,7 +5,7 @@ */ import { flatten } from 'lodash'; -import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; +import { KibanaFeature, FeatureKibanaPrivileges } from '../../../../../features/server'; import { Actions } from '../../actions'; import { FeaturePrivilegeApiBuilder } from './api'; import { FeaturePrivilegeAppBuilder } from './app'; @@ -31,7 +31,7 @@ export const featurePrivilegeBuilderFactory = (actions: Actions): FeaturePrivile ]; return { - getActions(privilege: FeatureKibanaPrivileges, feature: Feature) { + getActions(privilege: FeatureKibanaPrivileges, feature: KibanaFeature) { return flatten(builders.map((builder) => builder.getActions(privilege, feature))); }, }; diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/management.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/management.ts index be784949dc2fa5..c92b98b0a23276 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/management.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/management.ts @@ -4,11 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; +import { KibanaFeature, FeatureKibanaPrivileges } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; export class FeaturePrivilegeManagementBuilder extends BaseFeaturePrivilegeBuilder { - public getActions(privilegeDefinition: FeatureKibanaPrivileges, feature: Feature): string[] { + public getActions( + privilegeDefinition: FeatureKibanaPrivileges, + feature: KibanaFeature + ): string[] { const managementSections = privilegeDefinition.management; if (!managementSections) { diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/navlink.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/navlink.ts index a6e5a01c7dba86..5ab5eee5fc0c44 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/navlink.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/navlink.ts @@ -4,11 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; +import { KibanaFeature, FeatureKibanaPrivileges } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; export class FeaturePrivilegeNavlinkBuilder extends BaseFeaturePrivilegeBuilder { - public getActions(privilegeDefinition: FeatureKibanaPrivileges, feature: Feature): string[] { + public getActions( + privilegeDefinition: FeatureKibanaPrivileges, + feature: KibanaFeature + ): string[] { return (privilegeDefinition.app ?? []).map((app) => this.actions.ui.get('navLinks', app)); } } diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/saved_object.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/saved_object.ts index 2c325fc8c6cb72..1d7121c17adaf3 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/saved_object.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/saved_object.ts @@ -5,7 +5,7 @@ */ import { flatten, uniq } from 'lodash'; -import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; +import { KibanaFeature, FeatureKibanaPrivileges } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; const readOperations: string[] = ['bulk_get', 'get', 'find']; @@ -13,7 +13,10 @@ const writeOperations: string[] = ['create', 'bulk_create', 'update', 'bulk_upda const allOperations: string[] = [...readOperations, ...writeOperations]; export class FeaturePrivilegeSavedObjectBuilder extends BaseFeaturePrivilegeBuilder { - public getActions(privilegeDefinition: FeatureKibanaPrivileges, feature: Feature): string[] { + public getActions( + privilegeDefinition: FeatureKibanaPrivileges, + feature: KibanaFeature + ): string[] { return uniq([ ...flatten( privilegeDefinition.savedObject.all.map((type) => [ diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/ui.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/ui.ts index 31bc351206e54f..dd167a291f11de 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/ui.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/ui.ts @@ -4,11 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; +import { KibanaFeature, FeatureKibanaPrivileges } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; export class FeaturePrivilegeUIBuilder extends BaseFeaturePrivilegeBuilder { - public getActions(privilegeDefinition: FeatureKibanaPrivileges, feature: Feature): string[] { + public getActions( + privilegeDefinition: FeatureKibanaPrivileges, + feature: KibanaFeature + ): string[] { return privilegeDefinition.ui.map((ui) => this.actions.ui.get(feature.id, ui)); } } diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.test.ts index bb1f0c33fdee9d..033040fd2f14b5 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.test.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature } from '../../../../../features/server'; +import { KibanaFeature } from '../../../../../features/server'; import { featurePrivilegeIterator } from './feature_privilege_iterator'; describe('featurePrivilegeIterator', () => { it('handles features with no privileges', () => { - const feature = new Feature({ + const feature = new KibanaFeature({ id: 'foo', name: 'foo', privileges: null, @@ -26,7 +26,7 @@ describe('featurePrivilegeIterator', () => { }); it('handles features with no sub-features', () => { - const feature = new Feature({ + const feature = new KibanaFeature({ id: 'foo', name: 'foo', privileges: { @@ -117,7 +117,7 @@ describe('featurePrivilegeIterator', () => { }); it('filters privileges using the provided predicate', () => { - const feature = new Feature({ + const feature = new KibanaFeature({ id: 'foo', name: 'foo', privileges: { @@ -190,7 +190,7 @@ describe('featurePrivilegeIterator', () => { }); it('ignores sub features when `augmentWithSubFeaturePrivileges` is false', () => { - const feature = new Feature({ + const feature = new KibanaFeature({ id: 'foo', name: 'foo', app: [], @@ -313,7 +313,7 @@ describe('featurePrivilegeIterator', () => { }); it('ignores sub features when `includeIn` is none, even if `augmentWithSubFeaturePrivileges` is true', () => { - const feature = new Feature({ + const feature = new KibanaFeature({ id: 'foo', name: 'foo', app: [], @@ -436,7 +436,7 @@ describe('featurePrivilegeIterator', () => { }); it('includes sub feature privileges into both all and read when`augmentWithSubFeaturePrivileges` is true and `includeIn: read`', () => { - const feature = new Feature({ + const feature = new KibanaFeature({ id: 'foo', name: 'foo', app: [], @@ -563,7 +563,7 @@ describe('featurePrivilegeIterator', () => { }); it('does not duplicate privileges when merging', () => { - const feature = new Feature({ + const feature = new KibanaFeature({ id: 'foo', name: 'foo', app: [], @@ -686,7 +686,7 @@ describe('featurePrivilegeIterator', () => { }); it('includes sub feature privileges into both all and read when`augmentWithSubFeaturePrivileges` is true and `includeIn: all`', () => { - const feature = new Feature({ + const feature = new KibanaFeature({ id: 'foo', name: 'foo', app: [], @@ -811,7 +811,7 @@ describe('featurePrivilegeIterator', () => { }); it(`can augment primary feature privileges even if they don't specify their own`, () => { - const feature = new Feature({ + const feature = new KibanaFeature({ id: 'foo', name: 'foo', app: [], @@ -919,7 +919,7 @@ describe('featurePrivilegeIterator', () => { }); it(`can augment primary feature privileges even if the sub-feature privileges don't specify their own`, () => { - const feature = new Feature({ + const feature = new KibanaFeature({ id: 'foo', name: 'foo', app: [], diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.ts index 17c9464b147565..dba33f7a4f3602 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.ts @@ -5,7 +5,7 @@ */ import _ from 'lodash'; -import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; +import { KibanaFeature, FeatureKibanaPrivileges } from '../../../../../features/server'; import { subFeaturePrivilegeIterator } from './sub_feature_privilege_iterator'; interface IteratorOptions { @@ -14,7 +14,7 @@ interface IteratorOptions { } export function* featurePrivilegeIterator( - feature: Feature, + feature: KibanaFeature, options: IteratorOptions ): IterableIterator<{ privilegeId: string; privilege: FeatureKibanaPrivileges }> { for (const entry of Object.entries(feature.privileges ?? {})) { @@ -35,7 +35,7 @@ export function* featurePrivilegeIterator( function mergeWithSubFeatures( privilegeId: string, privilege: FeatureKibanaPrivileges, - feature: Feature + feature: KibanaFeature ) { const mergedConfig = _.cloneDeep(privilege); for (const subFeaturePrivilege of subFeaturePrivilegeIterator(feature)) { diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/sub_feature_privilege_iterator.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/sub_feature_privilege_iterator.ts index b288262be25c67..dc8511c7f4ec43 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/sub_feature_privilege_iterator.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/sub_feature_privilege_iterator.ts @@ -5,10 +5,10 @@ */ import { SubFeaturePrivilegeConfig } from '../../../../../features/common'; -import { Feature } from '../../../../../features/server'; +import { KibanaFeature } from '../../../../../features/server'; export function* subFeaturePrivilegeIterator( - feature: Feature + feature: KibanaFeature ): IterableIterator { for (const subFeature of feature.subFeatures) { for (const group of subFeature.privilegeGroups) { diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts index 4154c5c702f86a..dd8ac44386dbd7 100644 --- a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature } from '../../../../features/server'; +import { KibanaFeature } from '../../../../features/server'; import { Actions } from '../actions'; import { privilegesFactory } from './privileges'; @@ -14,10 +14,10 @@ const actions = new Actions('1.0.0-zeta1'); describe('features', () => { test('actions defined at the feature do not cascade to the privileges', () => { - const features: Feature[] = [ - new Feature({ + const features: KibanaFeature[] = [ + new KibanaFeature({ id: 'foo-feature', - name: 'Foo Feature', + name: 'Foo KibanaFeature', icon: 'arrowDown', navLinkId: 'kibana:foo', app: ['app-1', 'app-2'], @@ -60,10 +60,10 @@ describe('features', () => { }); test(`actions only specified at the privilege are alright too`, () => { - const features: Feature[] = [ - new Feature({ + const features: KibanaFeature[] = [ + new KibanaFeature({ id: 'foo', - name: 'Foo Feature', + name: 'Foo KibanaFeature', icon: 'arrowDown', app: [], privileges: { @@ -159,10 +159,10 @@ describe('features', () => { }); test(`features with no privileges aren't listed`, () => { - const features: Feature[] = [ - new Feature({ + const features: KibanaFeature[] = [ + new KibanaFeature({ id: 'foo', - name: 'Foo Feature', + name: 'Foo KibanaFeature', icon: 'arrowDown', app: [], privileges: null, @@ -200,10 +200,10 @@ describe('features', () => { ].forEach(({ group, expectManageSpaces, expectGetFeatures, expectEnterpriseSearch }) => { describe(`${group}`, () => { test('actions defined in any feature privilege are included in `all`', () => { - const features: Feature[] = [ - new Feature({ + const features: KibanaFeature[] = [ + new KibanaFeature({ id: 'foo', - name: 'Foo Feature', + name: 'Foo KibanaFeature', icon: 'arrowDown', navLinkId: 'kibana:foo', app: [], @@ -320,10 +320,10 @@ describe('features', () => { }); test('actions defined in a feature privilege with name `read` are included in `read`', () => { - const features: Feature[] = [ - new Feature({ + const features: KibanaFeature[] = [ + new KibanaFeature({ id: 'foo', - name: 'Foo Feature', + name: 'Foo KibanaFeature', icon: 'arrowDown', navLinkId: 'kibana:foo', app: [], @@ -402,10 +402,10 @@ describe('features', () => { }); test('actions defined in a reserved privilege are not included in `all` or `read`', () => { - const features: Feature[] = [ - new Feature({ + const features: KibanaFeature[] = [ + new KibanaFeature({ id: 'foo', - name: 'Foo Feature', + name: 'Foo KibanaFeature', icon: 'arrowDown', navLinkId: 'kibana:foo', app: [], @@ -459,10 +459,10 @@ describe('features', () => { }); test('actions defined in a feature with excludeFromBasePrivileges are not included in `all` or `read', () => { - const features: Feature[] = [ - new Feature({ + const features: KibanaFeature[] = [ + new KibanaFeature({ id: 'foo', - name: 'Foo Feature', + name: 'Foo KibanaFeature', excludeFromBasePrivileges: true, icon: 'arrowDown', navLinkId: 'kibana:foo', @@ -525,10 +525,10 @@ describe('features', () => { }); test('actions defined in an individual feature privilege with excludeFromBasePrivileges are not included in `all` or `read`', () => { - const features: Feature[] = [ - new Feature({ + const features: KibanaFeature[] = [ + new KibanaFeature({ id: 'foo', - name: 'Foo Feature', + name: 'Foo KibanaFeature', icon: 'arrowDown', navLinkId: 'kibana:foo', app: [], @@ -595,10 +595,10 @@ describe('features', () => { describe('reserved', () => { test('actions defined at the feature do not cascade to the privileges', () => { - const features: Feature[] = [ - new Feature({ + const features: KibanaFeature[] = [ + new KibanaFeature({ id: 'foo', - name: 'Foo Feature', + name: 'Foo KibanaFeature', icon: 'arrowDown', navLinkId: 'kibana:foo', app: ['app-1', 'app-2'], @@ -638,10 +638,10 @@ describe('reserved', () => { }); test(`actions only specified at the privilege are alright too`, () => { - const features: Feature[] = [ - new Feature({ + const features: KibanaFeature[] = [ + new KibanaFeature({ id: 'foo', - name: 'Foo Feature', + name: 'Foo KibanaFeature', icon: 'arrowDown', app: [], privileges: null, @@ -702,10 +702,10 @@ describe('reserved', () => { }); test(`features with no reservedPrivileges aren't listed`, () => { - const features: Feature[] = [ - new Feature({ + const features: KibanaFeature[] = [ + new KibanaFeature({ id: 'foo', - name: 'Foo Feature', + name: 'Foo KibanaFeature', icon: 'arrowDown', app: [], privileges: { @@ -743,10 +743,10 @@ describe('reserved', () => { describe('subFeatures', () => { describe(`with includeIn: 'none'`, () => { test(`should not augment the primary feature privileges, base privileges, or minimal feature privileges`, () => { - const features: Feature[] = [ - new Feature({ + const features: KibanaFeature[] = [ + new KibanaFeature({ id: 'foo', - name: 'Foo Feature', + name: 'Foo KibanaFeature', icon: 'arrowDown', app: [], privileges: { @@ -870,10 +870,10 @@ describe('subFeatures', () => { describe(`with includeIn: 'read'`, () => { test(`should augment the primary feature privileges and base privileges, but never the minimal versions`, () => { - const features: Feature[] = [ - new Feature({ + const features: KibanaFeature[] = [ + new KibanaFeature({ id: 'foo', - name: 'Foo Feature', + name: 'Foo KibanaFeature', icon: 'arrowDown', app: [], privileges: { @@ -1069,10 +1069,10 @@ describe('subFeatures', () => { }); test(`should augment the primary feature privileges, but not base privileges if feature is excluded from them.`, () => { - const features: Feature[] = [ - new Feature({ + const features: KibanaFeature[] = [ + new KibanaFeature({ id: 'foo', - name: 'Foo Feature', + name: 'Foo KibanaFeature', icon: 'arrowDown', app: [], excludeFromBasePrivileges: true, @@ -1210,10 +1210,10 @@ describe('subFeatures', () => { describe(`with includeIn: 'all'`, () => { test(`should augment the primary 'all' feature privileges and base 'all' privileges, but never the minimal versions`, () => { - const features: Feature[] = [ - new Feature({ + const features: KibanaFeature[] = [ + new KibanaFeature({ id: 'foo', - name: 'Foo Feature', + name: 'Foo KibanaFeature', icon: 'arrowDown', app: [], privileges: { @@ -1373,10 +1373,10 @@ describe('subFeatures', () => { }); test(`should augment the primary 'all' feature privileges, but not the base privileges if the feature is excluded from them`, () => { - const features: Feature[] = [ - new Feature({ + const features: KibanaFeature[] = [ + new KibanaFeature({ id: 'foo', - name: 'Foo Feature', + name: 'Foo KibanaFeature', icon: 'arrowDown', app: [], excludeFromBasePrivileges: true, @@ -1502,10 +1502,10 @@ describe('subFeatures', () => { describe(`when license does not allow sub features`, () => { test(`should augment the primary feature privileges, and should not create minimal or sub-feature privileges`, () => { - const features: Feature[] = [ - new Feature({ + const features: KibanaFeature[] = [ + new KibanaFeature({ id: 'foo', - name: 'Foo Feature', + name: 'Foo KibanaFeature', icon: 'arrowDown', app: [], privileges: { diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.ts index b279f54adca758..24b46222e7f35b 100644 --- a/x-pack/plugins/security/server/authorization/privileges/privileges.ts +++ b/x-pack/plugins/security/server/authorization/privileges/privileges.ts @@ -6,7 +6,10 @@ import { uniq } from 'lodash'; import { SecurityLicense } from '../../../common/licensing'; -import { Feature, PluginSetupContract as FeaturesPluginSetup } from '../../../../features/server'; +import { + KibanaFeature, + PluginSetupContract as FeaturesPluginSetup, +} from '../../../../features/server'; import { RawKibanaPrivileges } from '../../../common/model'; import { Actions } from '../actions'; import { featurePrivilegeBuilderFactory } from './feature_privilege_builder'; @@ -110,7 +113,7 @@ export function privilegesFactory( all: [actions.login, actions.version, ...allActions], read: [actions.login, actions.version, ...readActions], }, - reserved: features.reduce((acc: Record, feature: Feature) => { + reserved: features.reduce((acc: Record, feature: KibanaFeature) => { if (feature.reserved) { feature.reserved.privileges.forEach((reservedPrivilege) => { acc[reservedPrivilege.id] = [ diff --git a/x-pack/plugins/security/server/authorization/types.ts b/x-pack/plugins/security/server/authorization/types.ts index 9616ee0c8c844f..bedf46862e4f57 100644 --- a/x-pack/plugins/security/server/authorization/types.ts +++ b/x-pack/plugins/security/server/authorization/types.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { KibanaRequest } from 'src/core/server'; + export interface HasPrivilegesResponseApplication { [resource: string]: { [privilegeName: string]: boolean; @@ -26,6 +28,44 @@ export interface HasPrivilegesResponse { }; } +export interface CheckPrivilegesResponse { + hasAllRequested: boolean; + username: string; + privileges: { + kibana: Array<{ + /** + * If this attribute is undefined, this element is a privilege for the global resource. + */ + resource?: string; + privilege: string; + authorized: boolean; + }>; + elasticsearch: { + cluster: Array<{ + privilege: string; + authorized: boolean; + }>; + index: { + [indexName: string]: Array<{ + privilege: string; + authorized: boolean; + }>; + }; + }; + }; +} + +export type CheckPrivilegesWithRequest = (request: KibanaRequest) => CheckPrivileges; + +export interface CheckPrivileges { + atSpace(spaceId: string, privileges: CheckPrivilegesPayload): Promise; + atSpaces( + spaceIds: string[], + privileges: CheckPrivilegesPayload + ): Promise; + globally(privileges: CheckPrivilegesPayload): Promise; +} + export interface CheckPrivilegesPayload { kibana?: string | string[]; elasticsearch?: { diff --git a/x-pack/plugins/security/server/authorization/validate_feature_privileges.test.ts b/x-pack/plugins/security/server/authorization/validate_feature_privileges.test.ts index cd2c7faa263c96..8e6d72670c8d99 100644 --- a/x-pack/plugins/security/server/authorization/validate_feature_privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/validate_feature_privileges.test.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature } from '../../../features/server'; +import { KibanaFeature } from '../../../features/server'; import { validateFeaturePrivileges } from './validate_feature_privileges'; it('allows features to be defined without privileges', () => { - const feature: Feature = new Feature({ + const feature: KibanaFeature = new KibanaFeature({ id: 'foo', name: 'foo', app: [], @@ -19,7 +19,7 @@ it('allows features to be defined without privileges', () => { }); it('allows features with reserved privileges to be defined', () => { - const feature: Feature = new Feature({ + const feature: KibanaFeature = new KibanaFeature({ id: 'foo', name: 'foo', app: [], @@ -45,7 +45,7 @@ it('allows features with reserved privileges to be defined', () => { }); it('allows features with sub-features to be defined', () => { - const feature: Feature = new Feature({ + const feature: KibanaFeature = new KibanaFeature({ id: 'foo', name: 'foo', app: [], @@ -108,7 +108,7 @@ it('allows features with sub-features to be defined', () => { }); it('does not allow features with sub-features which have id conflicts with the minimal privileges', () => { - const feature: Feature = new Feature({ + const feature: KibanaFeature = new KibanaFeature({ id: 'foo', name: 'foo', app: [], @@ -153,12 +153,12 @@ it('does not allow features with sub-features which have id conflicts with the m }); expect(() => validateFeaturePrivileges([feature])).toThrowErrorMatchingInlineSnapshot( - `"Feature 'foo' already has a privilege with ID 'minimal_all'. Sub feature 'sub-feature-1' cannot also specify this."` + `"KibanaFeature 'foo' already has a privilege with ID 'minimal_all'. Sub feature 'sub-feature-1' cannot also specify this."` ); }); it('does not allow features with sub-features which have id conflicts with the primary feature privileges', () => { - const feature: Feature = new Feature({ + const feature: KibanaFeature = new KibanaFeature({ id: 'foo', name: 'foo', app: [], @@ -203,12 +203,12 @@ it('does not allow features with sub-features which have id conflicts with the p }); expect(() => validateFeaturePrivileges([feature])).toThrowErrorMatchingInlineSnapshot( - `"Feature 'foo' already has a privilege with ID 'read'. Sub feature 'sub-feature-1' cannot also specify this."` + `"KibanaFeature 'foo' already has a privilege with ID 'read'. Sub feature 'sub-feature-1' cannot also specify this."` ); }); it('does not allow features with sub-features which have id conflicts each other', () => { - const feature: Feature = new Feature({ + const feature: KibanaFeature = new KibanaFeature({ id: 'foo', name: 'foo', app: [], @@ -273,6 +273,6 @@ it('does not allow features with sub-features which have id conflicts each other }); expect(() => validateFeaturePrivileges([feature])).toThrowErrorMatchingInlineSnapshot( - `"Feature 'foo' already has a privilege with ID 'some-sub-feature'. Sub feature 'sub-feature-2' cannot also specify this."` + `"KibanaFeature 'foo' already has a privilege with ID 'some-sub-feature'. Sub feature 'sub-feature-2' cannot also specify this."` ); }); diff --git a/x-pack/plugins/security/server/authorization/validate_feature_privileges.ts b/x-pack/plugins/security/server/authorization/validate_feature_privileges.ts index 79e5348b4ac645..eeb9c4cb74314b 100644 --- a/x-pack/plugins/security/server/authorization/validate_feature_privileges.ts +++ b/x-pack/plugins/security/server/authorization/validate_feature_privileges.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature } from '../../../features/server'; +import { KibanaFeature } from '../../../features/server'; -export function validateFeaturePrivileges(features: Feature[]) { +export function validateFeaturePrivileges(features: KibanaFeature[]) { for (const feature of features) { const seenPrivilegeIds = new Set(); Object.keys(feature.privileges ?? {}).forEach((privilegeId) => { @@ -20,7 +20,7 @@ export function validateFeaturePrivileges(features: Feature[]) { subFeaturePrivilegeGroup.privileges.forEach((subFeaturePrivilege) => { if (seenPrivilegeIds.has(subFeaturePrivilege.id)) { throw new Error( - `Feature '${feature.id}' already has a privilege with ID '${subFeaturePrivilege.id}'. Sub feature '${subFeature.name}' cannot also specify this.` + `KibanaFeature '${feature.id}' already has a privilege with ID '${subFeaturePrivilege.id}'. Sub feature '${subFeature.name}' cannot also specify this.` ); } seenPrivilegeIds.add(subFeaturePrivilege.id); diff --git a/x-pack/plugins/security/server/authorization/validate_reserved_privileges.test.ts b/x-pack/plugins/security/server/authorization/validate_reserved_privileges.test.ts index 26af0dadfb2886..d91a4d41513162 100644 --- a/x-pack/plugins/security/server/authorization/validate_reserved_privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/validate_reserved_privileges.test.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature } from '../../../features/server'; +import { KibanaFeature } from '../../../features/server'; import { validateReservedPrivileges } from './validate_reserved_privileges'; it('allows features to be defined without privileges', () => { - const feature: Feature = new Feature({ + const feature: KibanaFeature = new KibanaFeature({ id: 'foo', name: 'foo', app: [], @@ -19,7 +19,7 @@ it('allows features to be defined without privileges', () => { }); it('allows features with a single reserved privilege to be defined', () => { - const feature: Feature = new Feature({ + const feature: KibanaFeature = new KibanaFeature({ id: 'foo', name: 'foo', app: [], @@ -45,7 +45,7 @@ it('allows features with a single reserved privilege to be defined', () => { }); it('allows multiple features with reserved privileges to be defined', () => { - const feature1: Feature = new Feature({ + const feature1: KibanaFeature = new KibanaFeature({ id: 'foo', name: 'foo', app: [], @@ -67,7 +67,7 @@ it('allows multiple features with reserved privileges to be defined', () => { }, }); - const feature2: Feature = new Feature({ + const feature2: KibanaFeature = new KibanaFeature({ id: 'foo2', name: 'foo', app: [], @@ -93,7 +93,7 @@ it('allows multiple features with reserved privileges to be defined', () => { }); it('prevents a feature from specifying the same reserved privilege id', () => { - const feature1: Feature = new Feature({ + const feature1: KibanaFeature = new KibanaFeature({ id: 'foo', name: 'foo', app: [], @@ -131,7 +131,7 @@ it('prevents a feature from specifying the same reserved privilege id', () => { }); it('prevents features from sharing a reserved privilege id', () => { - const feature1: Feature = new Feature({ + const feature1: KibanaFeature = new KibanaFeature({ id: 'foo', name: 'foo', app: [], @@ -153,7 +153,7 @@ it('prevents features from sharing a reserved privilege id', () => { }, }); - const feature2: Feature = new Feature({ + const feature2: KibanaFeature = new KibanaFeature({ id: 'foo2', name: 'foo', app: [], diff --git a/x-pack/plugins/security/server/authorization/validate_reserved_privileges.ts b/x-pack/plugins/security/server/authorization/validate_reserved_privileges.ts index 0915308fc0f894..23e5c28a4af1b6 100644 --- a/x-pack/plugins/security/server/authorization/validate_reserved_privileges.ts +++ b/x-pack/plugins/security/server/authorization/validate_reserved_privileges.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature } from '../../../features/server'; +import { KibanaFeature } from '../../../features/server'; -export function validateReservedPrivileges(features: Feature[]) { +export function validateReservedPrivileges(features: KibanaFeature[]) { const seenPrivilegeIds = new Set(); for (const feature of features) { diff --git a/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts index 8f115f11329d3f..6e9b88f30479f7 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts @@ -15,7 +15,7 @@ import { httpServerMock, } from '../../../../../../../src/core/server/mocks'; import { routeDefinitionParamsMock } from '../../index.mock'; -import { Feature } from '../../../../../features/server'; +import { KibanaFeature } from '../../../../../features/server'; import { securityFeatureUsageServiceMock } from '../../../feature_usage/index.mock'; const application = 'kibana-.kibana'; @@ -83,7 +83,7 @@ const putRoleTest = ( ); mockRouteDefinitionParams.getFeatures.mockResolvedValue([ - new Feature({ + new KibanaFeature({ id: 'feature_1', name: 'feature 1', app: [], diff --git a/x-pack/plugins/security/server/routes/authorization/roles/put.ts b/x-pack/plugins/security/server/routes/authorization/roles/put.ts index d83cf92bcaa0d4..cdedc9ac8a5eb2 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/put.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/put.ts @@ -5,7 +5,7 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; -import { Feature } from '../../../../../features/common'; +import { KibanaFeature } from '../../../../../features/common'; import { RouteDefinitionParams } from '../../index'; import { createLicensedRouteHandler } from '../../licensed_route_handler'; import { wrapIntoCustomErrorResponse } from '../../../errors'; @@ -16,7 +16,7 @@ import { } from './model'; const roleGrantsSubFeaturePrivileges = ( - features: Feature[], + features: KibanaFeature[], role: TypeOf> ) => { if (!role.kibana) { @@ -77,7 +77,7 @@ export function definePutRolesRoutes({ rawRoles[name] ? rawRoles[name].applications : [] ); - const [features] = await Promise.all([ + const [features] = await Promise.all([ getFeatures(), clusterClient .asScoped(request) diff --git a/x-pack/plugins/security/server/routes/index.ts b/x-pack/plugins/security/server/routes/index.ts index 82c0186898d383..d7176c2505ee2e 100644 --- a/x-pack/plugins/security/server/routes/index.ts +++ b/x-pack/plugins/security/server/routes/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature } from '../../../features/server'; +import { KibanaFeature } from '../../../features/server'; import { CoreSetup, HttpResources, @@ -39,7 +39,7 @@ export interface RouteDefinitionParams { authc: Authentication; authz: AuthorizationServiceSetup; license: SecurityLicense; - getFeatures: () => Promise; + getFeatures: () => Promise; getFeatureUsageService: () => SecurityFeatureUsageServiceStart; } diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts index bd5d824c860cfc..6e48f037c8a607 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts @@ -18,7 +18,7 @@ import { } from '../../../../../src/core/server'; import { SecurityAuditLogger } from '../audit'; import { Actions, CheckSavedObjectsPrivileges } from '../authorization'; -import { CheckPrivilegesResponse } from '../authorization/check_privileges'; +import { CheckPrivilegesResponse } from '../authorization/types'; import { SpacesService } from '../plugin'; interface SecureSavedObjectsClientWrapperOptions { diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.test.tsx index ad5ebe157cfb83..0eed6793ddbe00 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.test.tsx @@ -10,9 +10,9 @@ import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { Space } from '../../../../common/model/space'; import { SectionPanel } from '../section_panel'; import { EnabledFeatures } from './enabled_features'; -import { FeatureConfig } from '../../../../../features/public'; +import { KibanaFeatureConfig } from '../../../../../features/public'; -const features: FeatureConfig[] = [ +const features: KibanaFeatureConfig[] = [ { id: 'feature-1', name: 'Feature 1', diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx index 9fe3bac73eeb18..bb42ccf250ad67 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component, Fragment, ReactNode } from 'react'; import { ApplicationStart } from 'kibana/public'; -import { FeatureConfig } from '../../../../../../plugins/features/public'; +import { KibanaFeatureConfig } from '../../../../../../plugins/features/public'; import { Space } from '../../../../common/model/space'; import { getEnabledFeatures } from '../../lib/feature_utils'; import { SectionPanel } from '../section_panel'; @@ -17,7 +17,7 @@ import { FeatureTable } from './feature_table'; interface Props { space: Partial; - features: FeatureConfig[]; + features: KibanaFeatureConfig[]; securityEnabled: boolean; onChange: (space: Partial) => void; getUrlForApp: ApplicationStart['getUrlForApp']; diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx index d52fbfb09d2353..99acda8da4c0fb 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx @@ -9,13 +9,13 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import _ from 'lodash'; import React, { ChangeEvent, Component } from 'react'; -import { FeatureConfig } from '../../../../../../plugins/features/public'; +import { KibanaFeatureConfig } from '../../../../../../plugins/features/public'; import { Space } from '../../../../common/model/space'; import { ToggleAllFeatures } from './toggle_all_features'; interface Props { space: Partial; - features: FeatureConfig[]; + features: KibanaFeatureConfig[]; onChange: (space: Partial) => void; } @@ -70,8 +70,8 @@ export class FeatureTable extends Component { defaultMessage: 'Feature', }), render: ( - feature: FeatureConfig, - _item: { feature: FeatureConfig; space: Props['space'] } + feature: KibanaFeatureConfig, + _item: { feature: KibanaFeatureConfig; space: Props['space'] } ) => { return ( diff --git a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx index b573848f0c84ae..f5807208488753 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx @@ -16,7 +16,7 @@ import { spacesManagerMock } from '../../spaces_manager/mocks'; import { SpacesManager } from '../../spaces_manager'; import { notificationServiceMock, scopedHistoryMock } from 'src/core/public/mocks'; import { featuresPluginMock } from '../../../../features/public/mocks'; -import { Feature } from '../../../../features/public'; +import { KibanaFeature } from '../../../../features/public'; // To be resolved by EUI team. // https://github.com/elastic/eui/issues/3712 @@ -34,7 +34,7 @@ const space = { const featuresStart = featuresPluginMock.createStart(); featuresStart.getFeatures.mockResolvedValue([ - new Feature({ + new KibanaFeature({ id: 'feature-1', name: 'feature 1', icon: 'spacesApp', diff --git a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx index e725310c41817a..5338710b7c8a44 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import _ from 'lodash'; import React, { Component, Fragment } from 'react'; import { ApplicationStart, Capabilities, NotificationsStart, ScopedHistory } from 'src/core/public'; -import { Feature, FeaturesPluginStart } from '../../../../features/public'; +import { KibanaFeature, FeaturesPluginStart } from '../../../../features/public'; import { isReservedSpace } from '../../../common'; import { Space } from '../../../common/model/space'; import { SpacesManager } from '../../spaces_manager'; @@ -46,7 +46,7 @@ interface Props { interface State { space: Partial; - features: Feature[]; + features: KibanaFeature[]; originalSpace?: Partial; showAlteringActiveSpaceDialog: boolean; isLoading: boolean; @@ -312,7 +312,7 @@ export class ManageSpacePage extends Component { } }; - private loadSpace = async (spaceId: string, featuresPromise: Promise) => { + private loadSpace = async (spaceId: string, featuresPromise: Promise) => { const { spacesManager, onLoadSpace } = this.props; try { diff --git a/x-pack/plugins/spaces/public/management/lib/feature_utils.test.ts b/x-pack/plugins/spaces/public/management/lib/feature_utils.test.ts index 20d419e5c90e42..212ffe96cdbf6e 100644 --- a/x-pack/plugins/spaces/public/management/lib/feature_utils.test.ts +++ b/x-pack/plugins/spaces/public/management/lib/feature_utils.test.ts @@ -5,7 +5,7 @@ */ import { getEnabledFeatures } from './feature_utils'; -import { FeatureConfig } from '../../../../features/public'; +import { KibanaFeatureConfig } from '../../../../features/public'; const buildFeatures = () => [ @@ -25,7 +25,7 @@ const buildFeatures = () => id: 'feature4', name: 'feature 4', }, - ] as FeatureConfig[]; + ] as KibanaFeatureConfig[]; const buildSpace = (disabledFeatures = [] as string[]) => ({ id: 'space', diff --git a/x-pack/plugins/spaces/public/management/lib/feature_utils.ts b/x-pack/plugins/spaces/public/management/lib/feature_utils.ts index 273ea7e60bc5ed..c6f7031976a9bd 100644 --- a/x-pack/plugins/spaces/public/management/lib/feature_utils.ts +++ b/x-pack/plugins/spaces/public/management/lib/feature_utils.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FeatureConfig } from '../../../../features/common'; +import { KibanaFeatureConfig } from '../../../../features/common'; import { Space } from '../..'; -export function getEnabledFeatures(features: FeatureConfig[], space: Partial) { +export function getEnabledFeatures(features: KibanaFeatureConfig[], space: Partial) { return features.filter((feature) => !(space.disabledFeatures || []).includes(feature.id)); } diff --git a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx index a98fae2561827e..dfca28d892eabb 100644 --- a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx @@ -21,7 +21,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { ApplicationStart, Capabilities, NotificationsStart, ScopedHistory } from 'src/core/public'; -import { Feature, FeaturesPluginStart } from '../../../../features/public'; +import { KibanaFeature, FeaturesPluginStart } from '../../../../features/public'; import { isReservedSpace } from '../../../common'; import { DEFAULT_SPACE_ID } from '../../../common/constants'; import { Space } from '../../../common/model/space'; @@ -46,7 +46,7 @@ interface Props { interface State { spaces: Space[]; - features: Feature[]; + features: KibanaFeature[]; loading: boolean; showConfirmDeleteModal: boolean; selectedSpace: Space | null; diff --git a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_pages.test.tsx b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_pages.test.tsx index 607570eedc7876..fe4bdc865094f8 100644 --- a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_pages.test.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_pages.test.tsx @@ -13,7 +13,7 @@ import { SpacesGridPage } from './spaces_grid_page'; import { httpServiceMock, scopedHistoryMock } from 'src/core/public/mocks'; import { notificationServiceMock } from 'src/core/public/mocks'; import { featuresPluginMock } from '../../../../features/public/mocks'; -import { Feature } from '../../../../features/public'; +import { KibanaFeature } from '../../../../features/public'; const spaces = [ { @@ -42,7 +42,7 @@ spacesManager.getSpaces = jest.fn().mockResolvedValue(spaces); const featuresStart = featuresPluginMock.createStart(); featuresStart.getFeatures.mockResolvedValue([ - new Feature({ + new KibanaFeature({ id: 'feature-1', name: 'feature 1', icon: 'spacesApp', diff --git a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts index c4bf340577f97d..bf0b51b7e25039 100644 --- a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature } from '../../../../plugins/features/server'; +import { KibanaFeature } from '../../../../plugins/features/server'; import { Space } from '../../common/model/space'; import { setupCapabilitiesSwitcher } from './capabilities_switcher'; import { Capabilities, CoreSetup } from 'src/core/server'; @@ -80,7 +80,7 @@ const features = ([ }, }, }, -] as unknown) as Feature[]; +] as unknown) as KibanaFeature[]; const buildCapabilities = () => Object.freeze({ diff --git a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts index 7d286039ed748c..8b0b955c40d924 100644 --- a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts @@ -5,7 +5,7 @@ */ import _ from 'lodash'; import { Capabilities, CapabilitiesSwitcher, CoreSetup, Logger } from 'src/core/server'; -import { Feature } from '../../../../plugins/features/server'; +import { KibanaFeature } from '../../../../plugins/features/server'; import { Space } from '../../common/model/space'; import { SpacesServiceSetup } from '../spaces_service'; import { PluginsStart } from '../plugin'; @@ -39,7 +39,11 @@ export function setupCapabilitiesSwitcher( }; } -function toggleCapabilities(features: Feature[], capabilities: Capabilities, activeSpace: Space) { +function toggleCapabilities( + features: KibanaFeature[], + capabilities: Capabilities, + activeSpace: Space +) { const clonedCapabilities = _.cloneDeep(capabilities); toggleDisabledFeatures(features, clonedCapabilities, activeSpace); @@ -48,7 +52,7 @@ function toggleCapabilities(features: Feature[], capabilities: Capabilities, act } function toggleDisabledFeatures( - features: Feature[], + features: KibanaFeature[], capabilities: Capabilities, activeSpace: Space ) { @@ -61,7 +65,7 @@ function toggleDisabledFeatures( } return [[...acc[0], feature], acc[1]]; }, - [[], []] as [Feature[], Feature[]] + [[], []] as [KibanaFeature[], KibanaFeature[]] ); const navLinks = capabilities.navLinks; diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts index 6db49f2d2992b4..d9ad86f5666a22 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts @@ -25,7 +25,7 @@ import { SpacesService } from '../../spaces_service'; import { SpacesAuditLogger } from '../audit_logger'; import { convertSavedObjectToSpace } from '../../routes/lib'; import { initSpacesOnPostAuthRequestInterceptor } from './on_post_auth_interceptor'; -import { Feature } from '../../../../features/server'; +import { KibanaFeature } from '../../../../features/server'; import { spacesConfig } from '../__fixtures__'; import { securityMock } from '../../../../security/server/mocks'; import { featuresPluginMock } from '../../../../features/server/mocks'; @@ -145,7 +145,7 @@ describe.skip('onPostAuthInterceptor', () => { name: 'feature 4', app: ['kibana'], }, - ] as unknown) as Feature[]); + ] as unknown) as KibanaFeature[]); const mockRepository = jest.fn().mockImplementation(() => { return { diff --git a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts index 2104c1c8c37b64..fddd7f92b7f27e 100644 --- a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts +++ b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts @@ -7,18 +7,18 @@ import { getSpacesUsageCollector, UsageStats } from './spaces_usage_collector'; import * as Rx from 'rxjs'; import { PluginsSetup } from '../plugin'; -import { Feature } from '../../../features/server'; +import { KibanaFeature } from '../../../features/server'; import { ILicense, LicensingPluginSetup } from '../../../licensing/server'; import { pluginInitializerContextConfigMock } from 'src/core/server/mocks'; interface SetupOpts { license?: Partial; - features?: Feature[]; + features?: KibanaFeature[]; } function setup({ license = { isAvailable: true }, - features = [{ id: 'feature1' } as Feature, { id: 'feature2' } as Feature], + features = [{ id: 'feature1' } as KibanaFeature, { id: 'feature2' } as KibanaFeature], }: SetupOpts = {}) { class MockUsageCollector { private fetch: any; diff --git a/x-pack/test/api_integration/apis/features/features/features.ts b/x-pack/test/api_integration/apis/features/features/features.ts index 9c44bfeb810fa8..37809a3b7aeb70 100644 --- a/x-pack/test/api_integration/apis/features/features/features.ts +++ b/x-pack/test/api_integration/apis/features/features/features.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { Feature } from '../../../../../plugins/features/server'; +import { KibanaFeature } from '../../../../../plugins/features/server'; import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { @@ -90,7 +90,7 @@ export default function ({ getService }: FtrProviderContext) { expect(body).to.be.an(Array); - const featureIds = body.map((b: Feature) => b.id); + const featureIds = body.map((b: KibanaFeature) => b.id); expect(featureIds.sort()).to.eql( [ 'discover', From 776bac2162f97f25752119230489dea0d655470d Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Tue, 1 Sep 2020 11:25:40 -0400 Subject: [PATCH 05/17] additional check_privileges tests --- .../authorization/check_privileges.test.ts | 980 +++++++++++++++++- 1 file changed, 923 insertions(+), 57 deletions(-) diff --git a/x-pack/plugins/security/server/authorization/check_privileges.test.ts b/x-pack/plugins/security/server/authorization/check_privileges.test.ts index d236e7dcf7ff74..d896f43af39d01 100644 --- a/x-pack/plugins/security/server/authorization/check_privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges.test.ts @@ -33,7 +33,11 @@ const createMockClusterClient = (response: any) => { describe('#atSpace', () => { const checkPrivilegesAtSpaceTest = async (options: { spaceId: string; - privilegeOrPrivileges: string | string[]; + kibanaPrivileges?: string | string[]; + elasticsearchPrivileges?: { + cluster: string[]; + index: Record; + }; esHasPrivilegesResponse: HasPrivilegesResponse; }) => { const { mockClusterClient, mockScopedClusterClient } = createMockClusterClient( @@ -51,27 +55,38 @@ describe('#atSpace', () => { let errorThrown = null; try { actualResult = await checkPrivileges.atSpace(options.spaceId, { - kibana: options.privilegeOrPrivileges, + kibana: options.kibanaPrivileges, + elasticsearch: options.elasticsearchPrivileges, }); } catch (err) { errorThrown = err; } + const expectedIndexPrivilegePayload = Object.entries( + options.elasticsearchPrivileges?.index ?? {} + ).map(([names, indexPrivileges]) => ({ + names, + privileges: indexPrivileges, + })); + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(request); expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.hasPrivileges', { body: { - index: [], + cluster: options.elasticsearchPrivileges?.cluster, + index: expectedIndexPrivilegePayload, applications: [ { application, resources: [`space:${options.spaceId}`], - privileges: uniq([ - mockActions.version, - mockActions.login, - ...(Array.isArray(options.privilegeOrPrivileges) - ? options.privilegeOrPrivileges - : [options.privilegeOrPrivileges]), - ]), + privileges: options.kibanaPrivileges + ? uniq([ + mockActions.version, + mockActions.login, + ...(Array.isArray(options.kibanaPrivileges) + ? options.kibanaPrivileges + : [options.kibanaPrivileges]), + ]) + : [mockActions.version, mockActions.login], }, ], }, @@ -86,7 +101,7 @@ describe('#atSpace', () => { test('successful when checking for login and user has login', async () => { const result = await checkPrivilegesAtSpaceTest({ spaceId: 'space_1', - privilegeOrPrivileges: mockActions.login, + kibanaPrivileges: mockActions.login, esHasPrivilegesResponse: { has_all_requested: true, username: 'foo-username', @@ -124,7 +139,7 @@ describe('#atSpace', () => { test(`failure when checking for login and user doesn't have login`, async () => { const result = await checkPrivilegesAtSpaceTest({ spaceId: 'space_1', - privilegeOrPrivileges: mockActions.login, + kibanaPrivileges: mockActions.login, esHasPrivilegesResponse: { has_all_requested: false, username: 'foo-username', @@ -162,7 +177,7 @@ describe('#atSpace', () => { test(`throws error when checking for login and user has login but doesn't have version`, async () => { const result = await checkPrivilegesAtSpaceTest({ spaceId: 'space_1', - privilegeOrPrivileges: mockActions.login, + kibanaPrivileges: mockActions.login, esHasPrivilegesResponse: { has_all_requested: false, username: 'foo-username', @@ -184,7 +199,7 @@ describe('#atSpace', () => { test(`successful when checking for two actions and the user has both`, async () => { const result = await checkPrivilegesAtSpaceTest({ spaceId: 'space_1', - privilegeOrPrivileges: [ + kibanaPrivileges: [ `saved_object:${savedObjectTypes[0]}/get`, `saved_object:${savedObjectTypes[1]}/get`, ], @@ -232,7 +247,7 @@ describe('#atSpace', () => { test(`failure when checking for two actions and the user has only one`, async () => { const result = await checkPrivilegesAtSpaceTest({ spaceId: 'space_1', - privilegeOrPrivileges: [ + kibanaPrivileges: [ `saved_object:${savedObjectTypes[0]}/get`, `saved_object:${savedObjectTypes[1]}/get`, ], @@ -281,7 +296,7 @@ describe('#atSpace', () => { test(`throws a validation error when an extra privilege is present in the response`, async () => { const result = await checkPrivilegesAtSpaceTest({ spaceId: 'space_1', - privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], + kibanaPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], esHasPrivilegesResponse: { has_all_requested: false, username: 'foo-username', @@ -305,7 +320,7 @@ describe('#atSpace', () => { test(`throws a validation error when privileges are missing in the response`, async () => { const result = await checkPrivilegesAtSpaceTest({ spaceId: 'space_1', - privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], + kibanaPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], esHasPrivilegesResponse: { has_all_requested: false, username: 'foo-username', @@ -324,12 +339,285 @@ describe('#atSpace', () => { ); }); }); + + describe('with Elasticsearch privileges', () => { + it('successful when checking for cluster privileges, and user has both', async () => { + const result = await checkPrivilegesAtSpaceTest({ + spaceId: 'space_1', + elasticsearchPrivileges: { + cluster: ['foo', 'bar'], + index: {}, + }, + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + }, + }, + }, + cluster: { + foo: true, + bar: true, + }, + index: {}, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": true, + "privilege": "foo", + }, + Object { + "authorized": true, + "privilege": "bar", + }, + ], + "index": Object {}, + }, + "kibana": Array [], + }, + "username": "foo-username", + } + `); + }); + + it('successful when checking for index privileges, and user has both', async () => { + const result = await checkPrivilegesAtSpaceTest({ + spaceId: 'space_1', + elasticsearchPrivileges: { + cluster: [], + index: { + foo: ['all'], + bar: ['read', 'view_index_metadata'], + }, + }, + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + }, + }, + }, + index: { + foo: { + all: true, + }, + bar: { + read: true, + view_index_metadata: true, + }, + }, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object { + "bar": Array [ + Object { + "authorized": true, + "privilege": "read", + }, + Object { + "authorized": true, + "privilege": "view_index_metadata", + }, + ], + "foo": Array [ + Object { + "authorized": true, + "privilege": "all", + }, + ], + }, + }, + "kibana": Array [], + }, + "username": "foo-username", + } + `); + }); + + it('successful when checking for a combination of index and cluster privileges', async () => { + const result = await checkPrivilegesAtSpaceTest({ + spaceId: 'space_1', + elasticsearchPrivileges: { + cluster: ['manage', 'monitor'], + index: { + foo: ['all'], + bar: ['read', 'view_index_metadata'], + }, + }, + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + }, + }, + }, + cluster: { + manage: true, + monitor: true, + }, + index: { + foo: { + all: true, + }, + bar: { + read: true, + view_index_metadata: true, + }, + }, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": true, + "privilege": "manage", + }, + Object { + "authorized": true, + "privilege": "monitor", + }, + ], + "index": Object { + "bar": Array [ + Object { + "authorized": true, + "privilege": "read", + }, + Object { + "authorized": true, + "privilege": "view_index_metadata", + }, + ], + "foo": Array [ + Object { + "authorized": true, + "privilege": "all", + }, + ], + }, + }, + "kibana": Array [], + }, + "username": "foo-username", + } + `); + }); + + it('failure when checking for a combination of index and cluster privileges, and some are missing', async () => { + const result = await checkPrivilegesAtSpaceTest({ + spaceId: 'space_1', + elasticsearchPrivileges: { + cluster: ['manage', 'monitor'], + index: { + foo: ['all'], + bar: ['read', 'view_index_metadata'], + }, + }, + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + }, + }, + }, + cluster: { + manage: true, + monitor: true, + }, + index: { + foo: { + all: true, + }, + bar: { + read: true, + view_index_metadata: false, + }, + }, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": true, + "privilege": "manage", + }, + Object { + "authorized": true, + "privilege": "monitor", + }, + ], + "index": Object { + "bar": Array [ + Object { + "authorized": true, + "privilege": "read", + }, + Object { + "authorized": false, + "privilege": "view_index_metadata", + }, + ], + "foo": Array [ + Object { + "authorized": true, + "privilege": "all", + }, + ], + }, + }, + "kibana": Array [], + }, + "username": "foo-username", + } + `); + }); + }); }); describe('#atSpaces', () => { const checkPrivilegesAtSpacesTest = async (options: { spaceIds: string[]; - privilegeOrPrivileges: string | string[]; + kibanaPrivileges?: string | string[]; + elasticsearchPrivileges?: { + cluster: string[]; + index: Record; + }; esHasPrivilegesResponse: HasPrivilegesResponse; }) => { const { mockClusterClient, mockScopedClusterClient } = createMockClusterClient( @@ -347,27 +635,38 @@ describe('#atSpaces', () => { let errorThrown = null; try { actualResult = await checkPrivileges.atSpaces(options.spaceIds, { - kibana: options.privilegeOrPrivileges, + kibana: options.kibanaPrivileges, + elasticsearch: options.elasticsearchPrivileges, }); } catch (err) { errorThrown = err; } + const expectedIndexPrivilegePayload = Object.entries( + options.elasticsearchPrivileges?.index ?? {} + ).map(([names, indexPrivileges]) => ({ + names, + privileges: indexPrivileges, + })); + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(request); expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.hasPrivileges', { body: { - index: [], + cluster: options.elasticsearchPrivileges?.cluster, + index: expectedIndexPrivilegePayload, applications: [ { application, resources: options.spaceIds.map((spaceId) => `space:${spaceId}`), - privileges: uniq([ - mockActions.version, - mockActions.login, - ...(Array.isArray(options.privilegeOrPrivileges) - ? options.privilegeOrPrivileges - : [options.privilegeOrPrivileges]), - ]), + privileges: options.kibanaPrivileges + ? uniq([ + mockActions.version, + mockActions.login, + ...(Array.isArray(options.kibanaPrivileges) + ? options.kibanaPrivileges + : [options.kibanaPrivileges]), + ]) + : [mockActions.version, mockActions.login], }, ], }, @@ -382,7 +681,7 @@ describe('#atSpaces', () => { test('successful when checking for login and user has login at both spaces', async () => { const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: mockActions.login, + kibanaPrivileges: mockActions.login, esHasPrivilegesResponse: { has_all_requested: true, username: 'foo-username', @@ -429,7 +728,7 @@ describe('#atSpaces', () => { test('failure when checking for login and user has login at only one space', async () => { const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: mockActions.login, + kibanaPrivileges: mockActions.login, esHasPrivilegesResponse: { has_all_requested: false, username: 'foo-username', @@ -476,7 +775,7 @@ describe('#atSpaces', () => { test(`throws error when checking for login and user has login but doesn't have version`, async () => { const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: mockActions.login, + kibanaPrivileges: mockActions.login, esHasPrivilegesResponse: { has_all_requested: false, username: 'foo-username', @@ -502,7 +801,7 @@ describe('#atSpaces', () => { test(`throws error when Elasticsearch returns malformed response`, async () => { const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: [ + kibanaPrivileges: [ `saved_object:${savedObjectTypes[0]}/get`, `saved_object:${savedObjectTypes[1]}/get`, ], @@ -531,7 +830,7 @@ describe('#atSpaces', () => { test(`successful when checking for two actions at two spaces and user has it all`, async () => { const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: [ + kibanaPrivileges: [ `saved_object:${savedObjectTypes[0]}/get`, `saved_object:${savedObjectTypes[1]}/get`, ], @@ -595,7 +894,7 @@ describe('#atSpaces', () => { test(`failure when checking for two actions at two spaces and user has one action at one space`, async () => { const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: [ + kibanaPrivileges: [ `saved_object:${savedObjectTypes[0]}/get`, `saved_object:${savedObjectTypes[1]}/get`, ], @@ -659,7 +958,7 @@ describe('#atSpaces', () => { test(`failure when checking for two actions at two spaces and user has two actions at one space`, async () => { const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: [ + kibanaPrivileges: [ `saved_object:${savedObjectTypes[0]}/get`, `saved_object:${savedObjectTypes[1]}/get`, ], @@ -723,7 +1022,7 @@ describe('#atSpaces', () => { test(`failure when checking for two actions at two spaces and user has two actions at one space & one action at the other`, async () => { const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: [ + kibanaPrivileges: [ `saved_object:${savedObjectTypes[0]}/get`, `saved_object:${savedObjectTypes[1]}/get`, ], @@ -788,7 +1087,7 @@ describe('#atSpaces', () => { test(`throws a validation error when an extra privilege is present in the response`, async () => { const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], + kibanaPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], esHasPrivilegesResponse: { has_all_requested: false, username: 'foo-username', @@ -818,7 +1117,7 @@ describe('#atSpaces', () => { test(`throws a validation error when privileges are missing in the response`, async () => { const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], + kibanaPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], esHasPrivilegesResponse: { has_all_requested: false, username: 'foo-username', @@ -846,7 +1145,7 @@ describe('#atSpaces', () => { test(`throws a validation error when an extra space is present in the response`, async () => { const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], + kibanaPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], esHasPrivilegesResponse: { has_all_requested: false, username: 'foo-username', @@ -879,7 +1178,7 @@ describe('#atSpaces', () => { test(`throws a validation error when an a space is missing in the response`, async () => { const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], + kibanaPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], esHasPrivilegesResponse: { has_all_requested: false, username: 'foo-username', @@ -899,11 +1198,300 @@ describe('#atSpaces', () => { ); }); }); + + describe('with Elasticsearch privileges', () => { + it('successful when checking for cluster privileges, and user has both', async () => { + const result = await checkPrivilegesAtSpacesTest({ + spaceIds: ['space_1', 'space_2'], + elasticsearchPrivileges: { + cluster: ['foo', 'bar'], + index: {}, + }, + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + }, + 'space:space_2': { + [mockActions.login]: true, + [mockActions.version]: true, + }, + }, + }, + cluster: { + foo: true, + bar: true, + }, + index: {}, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": true, + "privilege": "foo", + }, + Object { + "authorized": true, + "privilege": "bar", + }, + ], + "index": Object {}, + }, + "kibana": Array [], + }, + "username": "foo-username", + } + `); + }); + + it('successful when checking for index privileges, and user has both', async () => { + const result = await checkPrivilegesAtSpacesTest({ + spaceIds: ['space_1', 'space_2'], + elasticsearchPrivileges: { + cluster: [], + index: { + foo: ['all'], + bar: ['read', 'view_index_metadata'], + }, + }, + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + }, + 'space:space_2': { + [mockActions.login]: true, + [mockActions.version]: true, + }, + }, + }, + index: { + foo: { + all: true, + }, + bar: { + read: true, + view_index_metadata: true, + }, + }, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object { + "bar": Array [ + Object { + "authorized": true, + "privilege": "read", + }, + Object { + "authorized": true, + "privilege": "view_index_metadata", + }, + ], + "foo": Array [ + Object { + "authorized": true, + "privilege": "all", + }, + ], + }, + }, + "kibana": Array [], + }, + "username": "foo-username", + } + `); + }); + + it('successful when checking for a combination of index and cluster privileges', async () => { + const result = await checkPrivilegesAtSpacesTest({ + spaceIds: ['space_1', 'space_2'], + elasticsearchPrivileges: { + cluster: ['manage', 'monitor'], + index: { + foo: ['all'], + bar: ['read', 'view_index_metadata'], + }, + }, + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + }, + 'space:space_2': { + [mockActions.login]: true, + [mockActions.version]: true, + }, + }, + }, + cluster: { + manage: true, + monitor: true, + }, + index: { + foo: { + all: true, + }, + bar: { + read: true, + view_index_metadata: true, + }, + }, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": true, + "privilege": "manage", + }, + Object { + "authorized": true, + "privilege": "monitor", + }, + ], + "index": Object { + "bar": Array [ + Object { + "authorized": true, + "privilege": "read", + }, + Object { + "authorized": true, + "privilege": "view_index_metadata", + }, + ], + "foo": Array [ + Object { + "authorized": true, + "privilege": "all", + }, + ], + }, + }, + "kibana": Array [], + }, + "username": "foo-username", + } + `); + }); + + it('failure when checking for a combination of index and cluster privileges, and some are missing', async () => { + const result = await checkPrivilegesAtSpacesTest({ + spaceIds: ['space_1', 'space_2'], + elasticsearchPrivileges: { + cluster: ['manage', 'monitor'], + index: { + foo: ['all'], + bar: ['read', 'view_index_metadata'], + }, + }, + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + }, + 'space:space_2': { + [mockActions.login]: true, + [mockActions.version]: true, + }, + }, + }, + cluster: { + manage: true, + monitor: true, + }, + index: { + foo: { + all: true, + }, + bar: { + read: true, + view_index_metadata: false, + }, + }, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": true, + "privilege": "manage", + }, + Object { + "authorized": true, + "privilege": "monitor", + }, + ], + "index": Object { + "bar": Array [ + Object { + "authorized": true, + "privilege": "read", + }, + Object { + "authorized": false, + "privilege": "view_index_metadata", + }, + ], + "foo": Array [ + Object { + "authorized": true, + "privilege": "all", + }, + ], + }, + }, + "kibana": Array [], + }, + "username": "foo-username", + } + `); + }); + }); }); describe('#globally', () => { const checkPrivilegesGloballyTest = async (options: { - privilegeOrPrivileges: string | string[]; + kibanaPrivileges?: string | string[]; + elasticsearchPrivileges?: { + cluster: string[]; + index: Record; + }; esHasPrivilegesResponse: HasPrivilegesResponse; }) => { const { mockClusterClient, mockScopedClusterClient } = createMockClusterClient( @@ -920,26 +1508,39 @@ describe('#globally', () => { let actualResult; let errorThrown = null; try { - actualResult = await checkPrivileges.globally({ kibana: options.privilegeOrPrivileges }); + actualResult = await checkPrivileges.globally({ + kibana: options.kibanaPrivileges, + elasticsearch: options.elasticsearchPrivileges, + }); } catch (err) { errorThrown = err; } + const expectedIndexPrivilegePayload = Object.entries( + options.elasticsearchPrivileges?.index ?? {} + ).map(([names, indexPrivileges]) => ({ + names, + privileges: indexPrivileges, + })); + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(request); expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.hasPrivileges', { body: { - index: [], + cluster: options.elasticsearchPrivileges?.cluster, + index: expectedIndexPrivilegePayload, applications: [ { application, resources: [GLOBAL_RESOURCE], - privileges: uniq([ - mockActions.version, - mockActions.login, - ...(Array.isArray(options.privilegeOrPrivileges) - ? options.privilegeOrPrivileges - : [options.privilegeOrPrivileges]), - ]), + privileges: options.kibanaPrivileges + ? uniq([ + mockActions.version, + mockActions.login, + ...(Array.isArray(options.kibanaPrivileges) + ? options.kibanaPrivileges + : [options.kibanaPrivileges]), + ]) + : [mockActions.version, mockActions.login], }, ], }, @@ -953,7 +1554,7 @@ describe('#globally', () => { test('successful when checking for login and user has login', async () => { const result = await checkPrivilegesGloballyTest({ - privilegeOrPrivileges: mockActions.login, + kibanaPrivileges: mockActions.login, esHasPrivilegesResponse: { has_all_requested: true, username: 'foo-username', @@ -990,7 +1591,7 @@ describe('#globally', () => { test(`failure when checking for login and user doesn't have login`, async () => { const result = await checkPrivilegesGloballyTest({ - privilegeOrPrivileges: mockActions.login, + kibanaPrivileges: mockActions.login, esHasPrivilegesResponse: { has_all_requested: false, username: 'foo-username', @@ -1027,7 +1628,7 @@ describe('#globally', () => { test(`throws error when checking for login and user has login but doesn't have version`, async () => { const result = await checkPrivilegesGloballyTest({ - privilegeOrPrivileges: mockActions.login, + kibanaPrivileges: mockActions.login, esHasPrivilegesResponse: { has_all_requested: false, username: 'foo-username', @@ -1048,7 +1649,7 @@ describe('#globally', () => { test(`throws error when Elasticsearch returns malformed response`, async () => { const result = await checkPrivilegesGloballyTest({ - privilegeOrPrivileges: [ + kibanaPrivileges: [ `saved_object:${savedObjectTypes[0]}/get`, `saved_object:${savedObjectTypes[1]}/get`, ], @@ -1072,7 +1673,7 @@ describe('#globally', () => { test(`successful when checking for two actions and the user has both`, async () => { const result = await checkPrivilegesGloballyTest({ - privilegeOrPrivileges: [ + kibanaPrivileges: [ `saved_object:${savedObjectTypes[0]}/get`, `saved_object:${savedObjectTypes[1]}/get`, ], @@ -1119,7 +1720,7 @@ describe('#globally', () => { test(`failure when checking for two actions and the user has only one`, async () => { const result = await checkPrivilegesGloballyTest({ - privilegeOrPrivileges: [ + kibanaPrivileges: [ `saved_object:${savedObjectTypes[0]}/get`, `saved_object:${savedObjectTypes[1]}/get`, ], @@ -1167,7 +1768,7 @@ describe('#globally', () => { describe('with a malformed Elasticsearch response', () => { test(`throws a validation error when an extra privilege is present in the response`, async () => { const result = await checkPrivilegesGloballyTest({ - privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], + kibanaPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], esHasPrivilegesResponse: { has_all_requested: false, username: 'foo-username', @@ -1190,7 +1791,7 @@ describe('#globally', () => { test(`throws a validation error when privileges are missing in the response`, async () => { const result = await checkPrivilegesGloballyTest({ - privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], + kibanaPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], esHasPrivilegesResponse: { has_all_requested: false, username: 'foo-username', @@ -1209,4 +1810,269 @@ describe('#globally', () => { ); }); }); + + describe('with Elasticsearch privileges', () => { + it('successful when checking for cluster privileges, and user has both', async () => { + const result = await checkPrivilegesGloballyTest({ + elasticsearchPrivileges: { + cluster: ['foo', 'bar'], + index: {}, + }, + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [mockActions.login]: true, + [mockActions.version]: true, + }, + }, + }, + cluster: { + foo: true, + bar: true, + }, + index: {}, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": true, + "privilege": "foo", + }, + Object { + "authorized": true, + "privilege": "bar", + }, + ], + "index": Object {}, + }, + "kibana": Array [], + }, + "username": "foo-username", + } + `); + }); + + it('successful when checking for index privileges, and user has both', async () => { + const result = await checkPrivilegesGloballyTest({ + elasticsearchPrivileges: { + cluster: [], + index: { + foo: ['all'], + bar: ['read', 'view_index_metadata'], + }, + }, + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [mockActions.login]: true, + [mockActions.version]: true, + }, + }, + }, + index: { + foo: { + all: true, + }, + bar: { + read: true, + view_index_metadata: true, + }, + }, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object { + "bar": Array [ + Object { + "authorized": true, + "privilege": "read", + }, + Object { + "authorized": true, + "privilege": "view_index_metadata", + }, + ], + "foo": Array [ + Object { + "authorized": true, + "privilege": "all", + }, + ], + }, + }, + "kibana": Array [], + }, + "username": "foo-username", + } + `); + }); + + it('successful when checking for a combination of index and cluster privileges', async () => { + const result = await checkPrivilegesGloballyTest({ + elasticsearchPrivileges: { + cluster: ['manage', 'monitor'], + index: { + foo: ['all'], + bar: ['read', 'view_index_metadata'], + }, + }, + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [mockActions.login]: true, + [mockActions.version]: true, + }, + }, + }, + cluster: { + manage: true, + monitor: true, + }, + index: { + foo: { + all: true, + }, + bar: { + read: true, + view_index_metadata: true, + }, + }, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": true, + "privilege": "manage", + }, + Object { + "authorized": true, + "privilege": "monitor", + }, + ], + "index": Object { + "bar": Array [ + Object { + "authorized": true, + "privilege": "read", + }, + Object { + "authorized": true, + "privilege": "view_index_metadata", + }, + ], + "foo": Array [ + Object { + "authorized": true, + "privilege": "all", + }, + ], + }, + }, + "kibana": Array [], + }, + "username": "foo-username", + } + `); + }); + + it('failure when checking for a combination of index and cluster privileges, and some are missing', async () => { + const result = await checkPrivilegesGloballyTest({ + elasticsearchPrivileges: { + cluster: ['manage', 'monitor'], + index: { + foo: ['all'], + bar: ['read', 'view_index_metadata'], + }, + }, + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [mockActions.login]: true, + [mockActions.version]: true, + }, + }, + }, + cluster: { + manage: true, + monitor: true, + }, + index: { + foo: { + all: true, + }, + bar: { + read: true, + view_index_metadata: false, + }, + }, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": true, + "privilege": "manage", + }, + Object { + "authorized": true, + "privilege": "monitor", + }, + ], + "index": Object { + "bar": Array [ + Object { + "authorized": true, + "privilege": "read", + }, + Object { + "authorized": false, + "privilege": "view_index_metadata", + }, + ], + "foo": Array [ + Object { + "authorized": true, + "privilege": "all", + }, + ], + }, + }, + "kibana": Array [], + }, + "username": "foo-username", + } + `); + }); + }); }); From c2274e8921168b6e67a31f3bec39dd8fd26e44ce Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Tue, 1 Sep 2020 12:33:17 -0400 Subject: [PATCH 06/17] started adding additional disable_ui_capabilities tests --- .../disable_ui_capabilities.test.ts | 67 +++++++++- .../authorization/disable_ui_capabilities.ts | 119 ++++++++++++------ 2 files changed, 145 insertions(+), 41 deletions(-) diff --git a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts index 9ae76d60c72aad..53569f17868743 100644 --- a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts +++ b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts @@ -11,12 +11,13 @@ import { httpServerMock, loggingSystemMock } from '../../../../../src/core/serve import { authorizationMock } from './index.mock'; import { KibanaFeature, ElasticsearchFeature } from '../../../features/server'; import { AuthenticatedUser } from '..'; +import { CheckPrivilegesResponse } from './types'; type MockAuthzOptions = | { rejectCheckPrivileges: any } | { resolveCheckPrivileges: { - privileges: { kibana: Array<{ privilege: string; authorized: boolean }> }; + privileges: CheckPrivilegesResponse['privileges']; }; }; @@ -36,8 +37,17 @@ const createMockAuthz = (options: MockAuthzOptions) => { throw options.rejectCheckPrivileges; } - const expected = options.resolveCheckPrivileges.privileges.kibana.map((x) => x.privilege); - expect(checkActions).toEqual({ kibana: expected, elasticsearch: { cluster: [], index: {} } }); + const expectedKibana = options.resolveCheckPrivileges.privileges.kibana.map( + (x) => x.privilege + ); + const expectedCluster = ( + options.resolveCheckPrivileges.privileges.elasticsearch.cluster ?? [] + ).map((x) => x.privilege); + + expect(checkActions).toEqual({ + kibana: expectedKibana, + elasticsearch: { cluster: expectedCluster, index: {} }, + }); return options.resolveCheckPrivileges; }); }); @@ -288,6 +298,14 @@ describe('usingPrivileges', () => { { privilege: actions.ui.get('barFeature', 'foo'), authorized: true }, { privilege: actions.ui.get('barFeature', 'bar'), authorized: false }, ], + elasticsearch: { + cluster: [ + { privilege: 'manage', authorized: false }, + { privilege: 'monitor', authorized: true }, + { privilege: 'manage_security', authorized: true }, + ], + index: {}, + }, }, }, }); @@ -315,8 +333,21 @@ describe('usingPrivileges', () => { id: 'esFeature', privileges: [ { - requiredClusterPrivileges: [], - ui: [], + requiredClusterPrivileges: ['manage'], + ui: ['es_manage'], + }, + { + requiredClusterPrivileges: ['monitor'], + ui: ['es_monitor'], + }, + ], + }), + new ElasticsearchFeature({ + id: 'esSecurityFeature', + privileges: [ + { + requiredClusterPrivileges: ['manage_security'], + ui: ['es_manage_sec'], }, ], }), @@ -348,6 +379,13 @@ describe('usingPrivileges', () => { foo: true, bar: true, }, + esFeature: { + es_manage: true, + es_monitor: true, + }, + esSecurityFeature: { + es_manage_sec: true, + }, }) ); @@ -372,6 +410,13 @@ describe('usingPrivileges', () => { foo: true, bar: false, }, + esFeature: { + es_manage: false, + es_monitor: true, + }, + esSecurityFeature: { + es_manage_sec: true, + }, }); }); @@ -388,6 +433,10 @@ describe('usingPrivileges', () => { { privilege: actions.ui.get('barFeature', 'foo'), authorized: true }, { privilege: actions.ui.get('barFeature', 'bar'), authorized: true }, ], + elasticsearch: { + cluster: [], + index: {}, + }, }, }, }); @@ -493,7 +542,7 @@ describe('all', () => { privileges: [ { requiredClusterPrivileges: [], - ui: [], + ui: ['bar'], }, ], }), @@ -523,6 +572,9 @@ describe('all', () => { foo: true, bar: true, }, + esFeature: { + bar: true, + }, }) ); expect(result).toEqual({ @@ -544,6 +596,9 @@ describe('all', () => { foo: false, bar: false, }, + esFeature: { + bar: false, + }, }); }); }); diff --git a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts index 8df0affec4e3a0..5dc54e5e7ebb43 100644 --- a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts +++ b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts @@ -5,7 +5,7 @@ */ import { flatten, isObject, mapValues } from 'lodash'; -import { RecursiveReadonlyArray } from '@kbn/utility-types'; +import { RecursiveReadonly, RecursiveReadonlyArray } from '@kbn/utility-types'; import type { Capabilities as UICapabilities } from '../../../../../src/core/types'; import { KibanaRequest, Logger } from '../../../../../src/core/server'; import { @@ -100,6 +100,12 @@ export function disableUICapabilitiesFactory( uiCapability: string, value: boolean | Record ): string[] { + // Capabilities derived from Elasticsearch features should not be + // included here, as the result is used to check authorization against + // Kibana Privileges, rather than Elasticsearch Privileges. + if (elasticsearchFeatureMap.hasOwnProperty(featureId)) { + return []; + } if (typeof value === 'boolean') { return [authz.actions.ui.get(featureId, uiCapability)]; } @@ -158,41 +164,60 @@ export function disableUICapabilitiesFactory( const action = authz.actions.ui.get(featureId, ...uiCapabilityParts); - const hasRequiredKibanaPrivileges = checkPrivilegesResponse.privileges.kibana.some( - (x) => x.privilege === action && x.authorized === true - ); + const isElasticsearchFeature = elasticsearchFeatureMap.hasOwnProperty(featureId); - if (hasRequiredKibanaPrivileges) { - return true; + if (!isElasticsearchFeature) { + const hasRequiredKibanaPrivileges = checkPrivilegesResponse.privileges.kibana.some( + (x) => x.privilege === action && x.authorized === true + ); + return hasRequiredKibanaPrivileges; } const isCatalogueFeature = featureId === 'catalogue'; const isManagementFeature = featureId === 'management'; - if (isCatalogueFeature) { - const [catalogueEntry] = uiCapabilityParts; - return elasticsearchFeatures.some((esFeature) => { + return elasticsearchFeatures.some((esFeature) => { + if (isCatalogueFeature) { + const [catalogueEntry] = uiCapabilityParts; const featureGrantsCatalogueEntry = (esFeature.catalogue ?? []).includes(catalogueEntry); return ( featureGrantsCatalogueEntry && - hasRequiredElasticsearchPrivilegesForFeature(esFeature, checkPrivilegesResponse, user) + hasAnyRequiredElasticsearchPrivilegesForFeature( + esFeature, + checkPrivilegesResponse, + user + ) ); - }); - } else if (isManagementFeature) { - const [managementSectionId, managementEntryId] = uiCapabilityParts; - return elasticsearchFeatures.some((esFeature) => { + } else if (isManagementFeature) { + const [managementSectionId, managementEntryId] = uiCapabilityParts; const featureGrantsManagementEntry = (esFeature.management ?? {}).hasOwnProperty(managementSectionId) && esFeature.management![managementSectionId].includes(managementEntryId); return ( featureGrantsManagementEntry && - hasRequiredElasticsearchPrivilegesForFeature(esFeature, checkPrivilegesResponse, user) + hasAnyRequiredElasticsearchPrivilegesForFeature( + esFeature, + checkPrivilegesResponse, + user + ) ); - }); - } - - return hasRequiredKibanaPrivileges; + } else if (esFeature.id === featureId) { + if (uiCapabilityParts.length !== 1) { + // The current privilege system does not allow for this to happen. + // This is a safeguard against future changes. + throw new Error( + `Elasticsearch feature ${esFeature.id} expected a single capability, but found ${uiCapabilityParts.length}` + ); + } + return hasRequiredElasticsearchPrivilegesForCapability( + esFeature, + uiCapabilityParts[0], + checkPrivilegesResponse, + user + ); + } + }); }; return mapValues(uiCapabilities, (featureUICapabilities, featureId) => { @@ -232,31 +257,55 @@ export function disableUICapabilitiesFactory( }; } -function hasRequiredElasticsearchPrivilegesForFeature( +function hasRequiredElasticsearchPrivilegesForCapability( esFeature: ElasticsearchFeature, + uiCapability: string, checkPrivilegesResponse: CheckPrivilegesResponse, user: AuthenticatedUser | null ) { return esFeature.privileges.some((privilege) => { - const hasRequiredClusterPrivileges = privilege.requiredClusterPrivileges.every( - (expectedClusterPriv) => - checkPrivilegesResponse.privileges.elasticsearch.cluster.some( - (x) => x.privilege === expectedClusterPriv && x.authorized === true - ) - ); + const privilegeGrantsCapability = privilege.ui.includes(uiCapability); + if (!privilegeGrantsCapability) { + return false; + } + + return isGrantedElasticsearchPrivilege(privilege, checkPrivilegesResponse, user); + }); +} + +function hasAnyRequiredElasticsearchPrivilegesForFeature( + esFeature: ElasticsearchFeature, + checkPrivilegesResponse: CheckPrivilegesResponse, + user: AuthenticatedUser | null +) { + return esFeature.privileges.some((privilege) => { + return isGrantedElasticsearchPrivilege(privilege, checkPrivilegesResponse, user); + }); +} + +function isGrantedElasticsearchPrivilege( + privilege: RecursiveReadonly, + checkPrivilegesResponse: CheckPrivilegesResponse, + user: AuthenticatedUser | null +) { + const hasRequiredClusterPrivileges = privilege.requiredClusterPrivileges.every( + (expectedClusterPriv) => + checkPrivilegesResponse.privileges.elasticsearch.cluster.some( + (x) => x.privilege === expectedClusterPriv && x.authorized === true + ) + ); - const hasRequiredIndexPrivileges = Object.entries( - privilege.requiredIndexPrivileges ?? {} - ).every(([indexName, requiredIndexPrivileges]) => { + const hasRequiredIndexPrivileges = Object.entries(privilege.requiredIndexPrivileges ?? {}).every( + ([indexName, requiredIndexPrivileges]) => { return checkPrivilegesResponse.privileges.elasticsearch.index[indexName] .filter((indexResponse) => requiredIndexPrivileges.includes(indexResponse.privilege)) .every((indexResponse) => indexResponse.authorized); - }); + } + ); - const hasRequiredRoles = (privilege.requiredRoles ?? []).every( - (requiredRole) => user?.roles.includes(requiredRole) ?? false - ); + const hasRequiredRoles = (privilege.requiredRoles ?? []).every( + (requiredRole) => user?.roles.includes(requiredRole) ?? false + ); - return hasRequiredClusterPrivileges && hasRequiredIndexPrivileges && hasRequiredRoles; - }); + return hasRequiredClusterPrivileges && hasRequiredIndexPrivileges && hasRequiredRoles; } From 303d20beeb47d59aea8e874ac38f16290831fb8f Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Tue, 1 Sep 2020 13:48:43 -0400 Subject: [PATCH 07/17] fixing the build --- x-pack/plugins/beats_management/server/plugin.ts | 4 ++-- x-pack/plugins/features/server/plugin.ts | 5 ++++- .../plugins/features/server/ui_capabilities_for_features.ts | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/beats_management/server/plugin.ts b/x-pack/plugins/beats_management/server/plugin.ts index c371ddd039e478..fde0a2efecddac 100644 --- a/x-pack/plugins/beats_management/server/plugin.ts +++ b/x-pack/plugins/beats_management/server/plugin.ts @@ -44,7 +44,7 @@ export class BeatsManagementPlugin implements Plugin<{}, {}, SetupDeps, StartDep private readonly initializerContext: PluginInitializerContext ) {} - public async setup(core: CoreSetup, { security }: SetupDeps) { + public async setup(core: CoreSetup, { features, security }: SetupDeps) { this.securitySetup = security; const router = core.http.createRouter(); @@ -54,7 +54,7 @@ export class BeatsManagementPlugin implements Plugin<{}, {}, SetupDeps, StartDep return this.beatsLibs!; }); - plugins.features.registerElasticsearchFeature({ + features.registerElasticsearchFeature({ id: 'beats_management', management: { ingest: ['beats_management'], diff --git a/x-pack/plugins/features/server/plugin.ts b/x-pack/plugins/features/server/plugin.ts index 7425bbb7f5f6f5..8a799887bba096 100644 --- a/x-pack/plugins/features/server/plugin.ts +++ b/x-pack/plugins/features/server/plugin.ts @@ -75,7 +75,10 @@ export class Plugin { }); const getFeaturesUICapabilities = () => - uiCapabilitiesForFeatures(this.featureRegistry.getAll()); + uiCapabilitiesForFeatures( + this.featureRegistry.getAllKibanaFeatures(), + this.featureRegistry.getAllElasticsearchFeatures() + ); core.capabilities.registerProvider(getFeaturesUICapabilities); diff --git a/x-pack/plugins/features/server/ui_capabilities_for_features.ts b/x-pack/plugins/features/server/ui_capabilities_for_features.ts index 8fc8fd8a038975..d582dbfdab50c3 100644 --- a/x-pack/plugins/features/server/ui_capabilities_for_features.ts +++ b/x-pack/plugins/features/server/ui_capabilities_for_features.ts @@ -17,10 +17,10 @@ interface FeatureCapabilities { } export function uiCapabilitiesForFeatures( - features: KibanaFeature[], + kibanaFeatures: KibanaFeature[], elasticsearchFeatures: ElasticsearchFeature[] ): UICapabilities { - const kibanaFeatureCapabilities = features.map(getCapabilitiesFromFeature); + const kibanaFeatureCapabilities = kibanaFeatures.map(getCapabilitiesFromFeature); const elasticsearchFeatureCapabilities = elasticsearchFeatures.map(getCapabilitiesFromFeature); return buildCapabilities(...kibanaFeatureCapabilities, ...elasticsearchFeatureCapabilities); From 820ddf9b677cef9908d3eeb10c10f8ac9a28e047 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Tue, 1 Sep 2020 13:49:07 -0400 Subject: [PATCH 08/17] doc updates --- .../security/feature-registration.asciidoc | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/developer/architecture/security/feature-registration.asciidoc b/docs/developer/architecture/security/feature-registration.asciidoc index b1c967a9484974..3ff83e9db8c43f 100644 --- a/docs/developer/architecture/security/feature-registration.asciidoc +++ b/docs/developer/architecture/security/feature-registration.asciidoc @@ -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. }); } @@ -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', { From d3f30aaa98f747ac367395380ee7c153f51c3325 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Wed, 2 Sep 2020 09:51:45 -0400 Subject: [PATCH 09/17] tweak short-circuit logic for disabling capabilities --- .../disable_ui_capabilities.test.ts | 20 +++++++++++++++++++ .../authorization/disable_ui_capabilities.ts | 15 +++++++++----- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts index 53569f17868743..98faae6edab2cd 100644 --- a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts +++ b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts @@ -293,6 +293,10 @@ describe('usingPrivileges', () => { { privilege: actions.ui.get('navLinks', 'quz'), authorized: false }, { privilege: actions.ui.get('management', 'kibana', 'indices'), authorized: true }, { privilege: actions.ui.get('management', 'kibana', 'settings'), authorized: false }, + { + privilege: actions.ui.get('management', 'kibana', 'esManagement'), + authorized: false, + }, { privilege: actions.ui.get('fooFeature', 'foo'), authorized: true }, { privilege: actions.ui.get('fooFeature', 'bar'), authorized: false }, { privilege: actions.ui.get('barFeature', 'foo'), authorized: true }, @@ -351,6 +355,18 @@ describe('usingPrivileges', () => { }, ], }), + new ElasticsearchFeature({ + id: 'esManagementFeature', + management: { + kibana: ['esManagement'], + }, + privileges: [ + { + requiredClusterPrivileges: ['manage_security'], + ui: [], + }, + ], + }), ], loggingSystemMock.create().get(), mockAuthz, @@ -368,6 +384,7 @@ describe('usingPrivileges', () => { kibana: { indices: true, settings: false, + esManagement: true, }, }, catalogue: {}, @@ -386,6 +403,7 @@ describe('usingPrivileges', () => { esSecurityFeature: { es_manage_sec: true, }, + esManagementFeature: {}, }) ); @@ -399,6 +417,7 @@ describe('usingPrivileges', () => { kibana: { indices: true, settings: false, + esManagement: true, }, }, catalogue: {}, @@ -417,6 +436,7 @@ describe('usingPrivileges', () => { esSecurityFeature: { es_manage_sec: true, }, + esManagementFeature: {}, }); }); diff --git a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts index 5dc54e5e7ebb43..920ab0007032ee 100644 --- a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts +++ b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts @@ -165,16 +165,21 @@ export function disableUICapabilitiesFactory( const action = authz.actions.ui.get(featureId, ...uiCapabilityParts); const isElasticsearchFeature = elasticsearchFeatureMap.hasOwnProperty(featureId); + const isCatalogueFeature = featureId === 'catalogue'; + const isManagementFeature = featureId === 'management'; + let hasRequiredKibanaPrivileges = false; if (!isElasticsearchFeature) { - const hasRequiredKibanaPrivileges = checkPrivilegesResponse.privileges.kibana.some( + hasRequiredKibanaPrivileges = checkPrivilegesResponse.privileges.kibana.some( (x) => x.privilege === action && x.authorized === true ); - return hasRequiredKibanaPrivileges; - } - const isCatalogueFeature = featureId === 'catalogue'; - const isManagementFeature = featureId === 'management'; + // Catalogue and management capbility buckets can also be influenced by ES privileges, + // so the early return is not possible for these. + if ((!isCatalogueFeature && !isManagementFeature) || hasRequiredKibanaPrivileges) { + return hasRequiredKibanaPrivileges; + } + } return elasticsearchFeatures.some((esFeature) => { if (isCatalogueFeature) { From 2a07fbe77170d764ce0e2fe9aa520e21a08066a9 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Wed, 2 Sep 2020 12:31:11 -0400 Subject: [PATCH 10/17] dont conditionally register ml management app --- .../ml/public/application/management/index.ts | 2 +- x-pack/plugins/ml/public/plugin.ts | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/ml/public/application/management/index.ts b/x-pack/plugins/ml/public/application/management/index.ts index a1b8484f200ec0..72073dfd26a973 100644 --- a/x-pack/plugins/ml/public/application/management/index.ts +++ b/x-pack/plugins/ml/public/application/management/index.ts @@ -23,7 +23,7 @@ export function registerManagementSection( core: CoreSetup ) { 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', diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index 4f8ceb8effe982..a13c3bf4dd5730 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -106,6 +106,8 @@ export class MlPlugin implements Plugin { }, }); + const managementApp = registerManagementSection(pluginsSetup.management, core); + const licensing = pluginsSetup.licensing.license$.pipe(take(1)); licensing.subscribe(async (license) => { const [coreStart] = await core.getStartServices(); @@ -115,26 +117,35 @@ export class MlPlugin implements Plugin { 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(); + } } }); From 920406b154f902f48fe471c602c9ec53f9f5328e Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Wed, 2 Sep 2020 14:49:17 -0400 Subject: [PATCH 11/17] fixes from merge --- .../test/ui_capabilities/security_and_spaces/tests/catalogue.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts index 67da7f4a19b9ab..dde99e7409dee3 100644 --- a/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts @@ -64,6 +64,7 @@ export default function catalogueTests({ getService }: FtrProviderContext) { 'enterpriseSearch', 'appSearch', 'workplaceSearch', + 'spaces', ...esFeatureExceptions, ]; const expected = mapValues( From 688e420f92db2dbe6d43ab50e5680453eeb1cf19 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Wed, 2 Sep 2020 15:20:04 -0400 Subject: [PATCH 12/17] documenting required privileges --- .../index-lifecycle-policies/manage-policy.asciidoc | 6 ++++++ docs/management/managing-ccr.asciidoc | 7 +++++++ docs/management/managing-licenses.asciidoc | 7 +++++++ docs/management/managing-remote-clusters.asciidoc | 7 +++++++ .../rollups/create_and_manage_rollups.asciidoc | 7 +++++++ docs/management/upgrade-assistant/index.asciidoc | 8 ++++++++ docs/migration/migrate_8_0.asciidoc | 11 +++++++++++ 7 files changed, 53 insertions(+) diff --git a/docs/management/index-lifecycle-policies/manage-policy.asciidoc b/docs/management/index-lifecycle-policies/manage-policy.asciidoc index a57af8a33494b5..8e2dc96de4b99d 100644 --- a/docs/management/index-lifecycle-policies/manage-policy.asciidoc +++ b/docs/management/index-lifecycle-policies/manage-policy.asciidoc @@ -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*. diff --git a/docs/management/managing-ccr.asciidoc b/docs/management/managing-ccr.asciidoc index 67193b3b5a037c..9c06e479e28b21 100644 --- a/docs/management/managing-ccr.asciidoc +++ b/docs/management/managing-ccr.asciidoc @@ -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 diff --git a/docs/management/managing-licenses.asciidoc b/docs/management/managing-licenses.asciidoc index 25ae29036f6561..b53bda95466dc3 100644 --- a/docs/management/managing-licenses.asciidoc +++ b/docs/management/managing-licenses.asciidoc @@ -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 diff --git a/docs/management/managing-remote-clusters.asciidoc b/docs/management/managing-remote-clusters.asciidoc index 83895838efec60..92e0fa822b0567 100644 --- a/docs/management/managing-remote-clusters.asciidoc +++ b/docs/management/managing-remote-clusters.asciidoc @@ -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 diff --git a/docs/management/rollups/create_and_manage_rollups.asciidoc b/docs/management/rollups/create_and_manage_rollups.asciidoc index 831b536f8c1cb6..0c1c25bd5b83f8 100644 --- a/docs/management/rollups/create_and_manage_rollups.asciidoc +++ b/docs/management/rollups/create_and_manage_rollups.asciidoc @@ -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 diff --git a/docs/management/upgrade-assistant/index.asciidoc b/docs/management/upgrade-assistant/index.asciidoc index c5fd6a3a555a1d..2b8c2da2ef5772 100644 --- a/docs/management/upgrade-assistant/index.asciidoc +++ b/docs/management/upgrade-assistant/index.asciidoc @@ -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 diff --git a/docs/migration/migrate_8_0.asciidoc b/docs/migration/migrate_8_0.asciidoc index b80503750a26e9..b2ef15ca1015d7 100644 --- a/docs/migration/migrate_8_0.asciidoc +++ b/docs/migration/migrate_8_0.asciidoc @@ -136,6 +136,17 @@ custom roles with {kibana-ref}/kibana-privileges.html[{kib} privileges]. instead be assigned the `kibana_admin` role to maintain their current access level. +=== `kibana_dashboard_only_user` role has been removed. + +*Details:* The `kibana_dashboard_only_user` role has been removed. +If you wish to restrict access to just the Dashboard feature, create +custom roles with {kibana-ref}/kibana-privileges.html[{kib} privileges]. + +*Impact:* Any users currently assigned the `kibana_dashboard_only_user` role will need to be assigned a custom role which only grants access to the Dashboard feature. + +Granting additional cluster or index privileges may enable certain +**Stack Monitoring** features. + [float] [[breaking_80_reporting_changes]] === Reporting changes From a50ec420c59a1560b95a102690fc5b0a4f8bb466 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 3 Sep 2020 15:09:04 -0400 Subject: [PATCH 13/17] you'll float too --- docs/migration/migrate_8_0.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/migration/migrate_8_0.asciidoc b/docs/migration/migrate_8_0.asciidoc index b2ef15ca1015d7..c6d6fdc707cfc8 100644 --- a/docs/migration/migrate_8_0.asciidoc +++ b/docs/migration/migrate_8_0.asciidoc @@ -136,6 +136,7 @@ custom roles with {kibana-ref}/kibana-privileges.html[{kib} privileges]. instead be assigned the `kibana_admin` role to maintain their current access level. +[float] === `kibana_dashboard_only_user` role has been removed. *Details:* The `kibana_dashboard_only_user` role has been removed. From 19e1e00081294f15c71f8acd565a020deb8c766a Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 3 Sep 2020 15:17:00 -0400 Subject: [PATCH 14/17] fix unrelated typo in migration docs --- docs/migration/migrate_8_0.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/migration/migrate_8_0.asciidoc b/docs/migration/migrate_8_0.asciidoc index c6d6fdc707cfc8..0cb28ce0fb6e78 100644 --- a/docs/migration/migrate_8_0.asciidoc +++ b/docs/migration/migrate_8_0.asciidoc @@ -115,7 +115,7 @@ URL that it derived from the actual server address and `xpack.security.public` s *Impact:* Any workflow that involved manually clearing generated bundles will have to be updated with the new path. -[float]] +[float] === kibana.keystore has moved from the `data` folder to the `config` folder *Details:* By default, kibana.keystore has moved from the configured `path.data` folder to `/config` for archive distributions and `/etc/kibana` for package distributions. If a pre-existing keystore exists in the data directory that path will continue to be used. From 987c6f4cb163f9ea9c20c31024e8210f0244c3a9 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 3 Sep 2020 15:36:59 -0400 Subject: [PATCH 15/17] Addressing second round of feedback --- .../authorization/check_privileges.test.ts | 940 +++++++++++++++++- .../authorization/disable_ui_capabilities.ts | 3 +- .../feature_privilege_builder/api.ts | 7 +- .../feature_privilege_builder/app.ts | 7 +- .../feature_privilege_builder/catalogue.ts | 7 +- .../feature_privilege_builder/management.ts | 7 +- .../feature_privilege_builder/navlink.ts | 7 +- .../feature_privilege_builder/saved_object.ts | 7 +- .../sub_feature_privilege_iterator.ts | 3 +- 9 files changed, 913 insertions(+), 75 deletions(-) diff --git a/x-pack/plugins/security/server/authorization/check_privileges.test.ts b/x-pack/plugins/security/server/authorization/check_privileges.test.ts index d896f43af39d01..4151ff645005de 100644 --- a/x-pack/plugins/security/server/authorization/check_privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges.test.ts @@ -340,6 +340,272 @@ describe('#atSpace', () => { }); }); + describe('with both Kibana and Elasticsearch privileges', () => { + it('successful when checking for privileges, and user has all', async () => { + const result = await checkPrivilegesAtSpaceTest({ + spaceId: 'space_1', + elasticsearchPrivileges: { + cluster: ['foo', 'bar'], + index: {}, + }, + kibanaPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: true, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, + }, + }, + cluster: { + foo: true, + bar: true, + }, + index: {}, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": true, + "privilege": "foo", + }, + Object { + "authorized": true, + "privilege": "bar", + }, + ], + "index": Object {}, + }, + "kibana": Array [ + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", + }, + ], + }, + "username": "foo-username", + } + `); + }); + + it('failure when checking for privileges, and user has only es privileges', async () => { + const result = await checkPrivilegesAtSpaceTest({ + spaceId: 'space_1', + elasticsearchPrivileges: { + cluster: ['foo', 'bar'], + index: {}, + }, + kibanaPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: false, + [`saved_object:${savedObjectTypes[1]}/get`]: false, + }, + }, + }, + cluster: { + foo: true, + bar: true, + }, + index: {}, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": true, + "privilege": "foo", + }, + Object { + "authorized": true, + "privilege": "bar", + }, + ], + "index": Object {}, + }, + "kibana": Array [ + Object { + "authorized": false, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", + }, + Object { + "authorized": false, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", + }, + ], + }, + "username": "foo-username", + } + `); + }); + + it('failure when checking for privileges, and user has only kibana privileges', async () => { + const result = await checkPrivilegesAtSpaceTest({ + spaceId: 'space_1', + elasticsearchPrivileges: { + cluster: ['foo', 'bar'], + index: {}, + }, + kibanaPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: true, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, + }, + }, + cluster: { + foo: false, + bar: false, + }, + index: {}, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": false, + "privilege": "foo", + }, + Object { + "authorized": false, + "privilege": "bar", + }, + ], + "index": Object {}, + }, + "kibana": Array [ + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", + }, + ], + }, + "username": "foo-username", + } + `); + }); + + it('failure when checking for privileges, and user has none', async () => { + const result = await checkPrivilegesAtSpaceTest({ + spaceId: 'space_1', + elasticsearchPrivileges: { + cluster: ['foo', 'bar'], + index: {}, + }, + kibanaPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: false, + [`saved_object:${savedObjectTypes[1]}/get`]: false, + }, + }, + }, + cluster: { + foo: false, + bar: false, + }, + index: {}, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": false, + "privilege": "foo", + }, + Object { + "authorized": false, + "privilege": "bar", + }, + ], + "index": Object {}, + }, + "kibana": Array [ + Object { + "authorized": false, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", + }, + Object { + "authorized": false, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", + }, + ], + }, + "username": "foo-username", + } + `); + }); + }); + describe('with Elasticsearch privileges', () => { it('successful when checking for cluster privileges, and user has both', async () => { const result = await checkPrivilegesAtSpaceTest({ @@ -1199,14 +1465,18 @@ describe('#atSpaces', () => { }); }); - describe('with Elasticsearch privileges', () => { - it('successful when checking for cluster privileges, and user has both', async () => { + describe('with both Kibana and Elasticsearch privileges', () => { + it('successful when checking for privileges, and user has all', async () => { const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], elasticsearchPrivileges: { cluster: ['foo', 'bar'], index: {}, }, + kibanaPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], esHasPrivilegesResponse: { has_all_requested: true, username: 'foo-username', @@ -1215,10 +1485,14 @@ describe('#atSpaces', () => { 'space:space_1': { [mockActions.login]: true, [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: true, + [`saved_object:${savedObjectTypes[1]}/get`]: true, }, 'space:space_2': { [mockActions.login]: true, [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: true, + [`saved_object:${savedObjectTypes[1]}/get`]: true, }, }, }, @@ -1246,76 +1520,398 @@ describe('#atSpaces', () => { ], "index": Object {}, }, - "kibana": Array [], + "kibana": Array [ + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", + }, + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_2", + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": "space_2", + }, + ], }, "username": "foo-username", } `); }); - it('successful when checking for index privileges, and user has both', async () => { + it('failure when checking for privileges, and user has only es privileges', async () => { const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], elasticsearchPrivileges: { - cluster: [], - index: { - foo: ['all'], - bar: ['read', 'view_index_metadata'], - }, + cluster: ['foo', 'bar'], + index: {}, }, + kibanaPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], esHasPrivilegesResponse: { - has_all_requested: true, + has_all_requested: false, username: 'foo-username', application: { [application]: { 'space:space_1': { [mockActions.login]: true, [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: false, + [`saved_object:${savedObjectTypes[1]}/get`]: false, }, 'space:space_2': { [mockActions.login]: true, [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: false, + [`saved_object:${savedObjectTypes[1]}/get`]: false, }, }, }, - index: { - foo: { - all: true, - }, - bar: { - read: true, - view_index_metadata: true, - }, + cluster: { + foo: true, + bar: true, }, + index: {}, }, }); expect(result).toMatchInlineSnapshot(` Object { - "hasAllRequested": true, + "hasAllRequested": false, "privileges": Object { "elasticsearch": Object { - "cluster": Array [], - "index": Object { - "bar": Array [ - Object { - "authorized": true, - "privilege": "read", - }, - Object { - "authorized": true, - "privilege": "view_index_metadata", - }, - ], - "foo": Array [ - Object { - "authorized": true, - "privilege": "all", - }, - ], - }, - }, - "kibana": Array [], - }, + "cluster": Array [ + Object { + "authorized": true, + "privilege": "foo", + }, + Object { + "authorized": true, + "privilege": "bar", + }, + ], + "index": Object {}, + }, + "kibana": Array [ + Object { + "authorized": false, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", + }, + Object { + "authorized": false, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", + }, + Object { + "authorized": false, + "privilege": "saved_object:foo-type/get", + "resource": "space_2", + }, + Object { + "authorized": false, + "privilege": "saved_object:bar-type/get", + "resource": "space_2", + }, + ], + }, + "username": "foo-username", + } + `); + }); + + it('failure when checking for privileges, and user has only kibana privileges', async () => { + const result = await checkPrivilegesAtSpacesTest({ + spaceIds: ['space_1', 'space_2'], + elasticsearchPrivileges: { + cluster: ['foo', 'bar'], + index: {}, + }, + kibanaPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: true, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, + 'space:space_2': { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: true, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, + }, + }, + cluster: { + foo: false, + bar: false, + }, + index: {}, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": false, + "privilege": "foo", + }, + Object { + "authorized": false, + "privilege": "bar", + }, + ], + "index": Object {}, + }, + "kibana": Array [ + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", + }, + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_2", + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": "space_2", + }, + ], + }, + "username": "foo-username", + } + `); + }); + + it('failure when checking for privileges, and user has none', async () => { + const result = await checkPrivilegesAtSpacesTest({ + spaceIds: ['space_1', 'space_2'], + elasticsearchPrivileges: { + cluster: ['foo', 'bar'], + index: {}, + }, + kibanaPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: false, + [`saved_object:${savedObjectTypes[1]}/get`]: false, + }, + 'space:space_2': { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: false, + [`saved_object:${savedObjectTypes[1]}/get`]: false, + }, + }, + }, + cluster: { + foo: false, + bar: false, + }, + index: {}, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": false, + "privilege": "foo", + }, + Object { + "authorized": false, + "privilege": "bar", + }, + ], + "index": Object {}, + }, + "kibana": Array [ + Object { + "authorized": false, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", + }, + Object { + "authorized": false, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", + }, + Object { + "authorized": false, + "privilege": "saved_object:foo-type/get", + "resource": "space_2", + }, + Object { + "authorized": false, + "privilege": "saved_object:bar-type/get", + "resource": "space_2", + }, + ], + }, + "username": "foo-username", + } + `); + }); + }); + + describe('with Elasticsearch privileges', () => { + it('successful when checking for cluster privileges, and user has both', async () => { + const result = await checkPrivilegesAtSpacesTest({ + spaceIds: ['space_1', 'space_2'], + elasticsearchPrivileges: { + cluster: ['foo', 'bar'], + index: {}, + }, + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + }, + 'space:space_2': { + [mockActions.login]: true, + [mockActions.version]: true, + }, + }, + }, + cluster: { + foo: true, + bar: true, + }, + index: {}, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": true, + "privilege": "foo", + }, + Object { + "authorized": true, + "privilege": "bar", + }, + ], + "index": Object {}, + }, + "kibana": Array [], + }, + "username": "foo-username", + } + `); + }); + + it('successful when checking for index privileges, and user has both', async () => { + const result = await checkPrivilegesAtSpacesTest({ + spaceIds: ['space_1', 'space_2'], + elasticsearchPrivileges: { + cluster: [], + index: { + foo: ['all'], + bar: ['read', 'view_index_metadata'], + }, + }, + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + }, + 'space:space_2': { + [mockActions.login]: true, + [mockActions.version]: true, + }, + }, + }, + index: { + foo: { + all: true, + }, + bar: { + read: true, + view_index_metadata: true, + }, + }, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object { + "bar": Array [ + Object { + "authorized": true, + "privilege": "read", + }, + Object { + "authorized": true, + "privilege": "view_index_metadata", + }, + ], + "foo": Array [ + Object { + "authorized": true, + "privilege": "all", + }, + ], + }, + }, + "kibana": Array [], + }, "username": "foo-username", } `); @@ -1811,6 +2407,268 @@ describe('#globally', () => { }); }); + describe('with both Kibana and Elasticsearch privileges', () => { + it('successful when checking for privileges, and user has all', async () => { + const result = await checkPrivilegesGloballyTest({ + elasticsearchPrivileges: { + cluster: ['foo', 'bar'], + index: {}, + }, + kibanaPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: true, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, + }, + }, + cluster: { + foo: true, + bar: true, + }, + index: {}, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": true, + "privilege": "foo", + }, + Object { + "authorized": true, + "privilege": "bar", + }, + ], + "index": Object {}, + }, + "kibana": Array [ + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": undefined, + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": undefined, + }, + ], + }, + "username": "foo-username", + } + `); + }); + + it('failure when checking for privileges, and user has only es privileges', async () => { + const result = await checkPrivilegesGloballyTest({ + elasticsearchPrivileges: { + cluster: ['foo', 'bar'], + index: {}, + }, + kibanaPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: false, + [`saved_object:${savedObjectTypes[1]}/get`]: false, + }, + }, + }, + cluster: { + foo: true, + bar: true, + }, + index: {}, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": true, + "privilege": "foo", + }, + Object { + "authorized": true, + "privilege": "bar", + }, + ], + "index": Object {}, + }, + "kibana": Array [ + Object { + "authorized": false, + "privilege": "saved_object:foo-type/get", + "resource": undefined, + }, + Object { + "authorized": false, + "privilege": "saved_object:bar-type/get", + "resource": undefined, + }, + ], + }, + "username": "foo-username", + } + `); + }); + + it('failure when checking for privileges, and user has only kibana privileges', async () => { + const result = await checkPrivilegesGloballyTest({ + elasticsearchPrivileges: { + cluster: ['foo', 'bar'], + index: {}, + }, + kibanaPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: true, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, + }, + }, + cluster: { + foo: false, + bar: false, + }, + index: {}, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": false, + "privilege": "foo", + }, + Object { + "authorized": false, + "privilege": "bar", + }, + ], + "index": Object {}, + }, + "kibana": Array [ + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": undefined, + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": undefined, + }, + ], + }, + "username": "foo-username", + } + `); + }); + + it('failure when checking for privileges, and user has none', async () => { + const result = await checkPrivilegesGloballyTest({ + elasticsearchPrivileges: { + cluster: ['foo', 'bar'], + index: {}, + }, + kibanaPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: false, + [`saved_object:${savedObjectTypes[1]}/get`]: false, + }, + }, + }, + cluster: { + foo: false, + bar: false, + }, + index: {}, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": false, + "privilege": "foo", + }, + Object { + "authorized": false, + "privilege": "bar", + }, + ], + "index": Object {}, + }, + "kibana": Array [ + Object { + "authorized": false, + "privilege": "saved_object:foo-type/get", + "resource": undefined, + }, + Object { + "authorized": false, + "privilege": "saved_object:bar-type/get", + "resource": undefined, + }, + ], + }, + "username": "foo-username", + } + `); + }); + }); + describe('with Elasticsearch privileges', () => { it('successful when checking for cluster privileges, and user has both', async () => { const result = await checkPrivilegesGloballyTest({ diff --git a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts index 920ab0007032ee..89cc9065655cd6 100644 --- a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts +++ b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts @@ -168,9 +168,8 @@ export function disableUICapabilitiesFactory( const isCatalogueFeature = featureId === 'catalogue'; const isManagementFeature = featureId === 'management'; - let hasRequiredKibanaPrivileges = false; if (!isElasticsearchFeature) { - hasRequiredKibanaPrivileges = checkPrivilegesResponse.privileges.kibana.some( + const hasRequiredKibanaPrivileges = checkPrivilegesResponse.privileges.kibana.some( (x) => x.privilege === action && x.authorized === true ); diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/api.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/api.ts index 924150b2326041..0e63cdceffc57a 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/api.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/api.ts @@ -4,14 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaFeature, FeatureKibanaPrivileges } from '../../../../../features/server'; +import { FeatureKibanaPrivileges } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; export class FeaturePrivilegeApiBuilder extends BaseFeaturePrivilegeBuilder { - public getActions( - privilegeDefinition: FeatureKibanaPrivileges, - feature: KibanaFeature - ): string[] { + public getActions(privilegeDefinition: FeatureKibanaPrivileges): string[] { if (privilegeDefinition.api) { return privilegeDefinition.api.map((operation) => this.actions.api.get(operation)); } diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/app.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/app.ts index 5f69a089985b5a..bf6b0e60f10454 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/app.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/app.ts @@ -4,14 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaFeature, FeatureKibanaPrivileges } from '../../../../../features/server'; +import { FeatureKibanaPrivileges } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; export class FeaturePrivilegeAppBuilder extends BaseFeaturePrivilegeBuilder { - public getActions( - privilegeDefinition: FeatureKibanaPrivileges, - feature: KibanaFeature - ): string[] { + public getActions(privilegeDefinition: FeatureKibanaPrivileges): string[] { const appIds = privilegeDefinition.app; if (!appIds) { diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/catalogue.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/catalogue.ts index abeeba2837e999..97a3c9c1e336e5 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/catalogue.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/catalogue.ts @@ -4,14 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaFeature, FeatureKibanaPrivileges } from '../../../../../features/server'; +import { FeatureKibanaPrivileges } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; export class FeaturePrivilegeCatalogueBuilder extends BaseFeaturePrivilegeBuilder { - public getActions( - privilegeDefinition: FeatureKibanaPrivileges, - feature: KibanaFeature - ): string[] { + public getActions(privilegeDefinition: FeatureKibanaPrivileges): string[] { const catalogueEntries = privilegeDefinition.catalogue; if (!catalogueEntries) { diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/management.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/management.ts index c92b98b0a23276..67b8cdb7616d4d 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/management.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/management.ts @@ -4,14 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaFeature, FeatureKibanaPrivileges } from '../../../../../features/server'; +import { FeatureKibanaPrivileges } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; export class FeaturePrivilegeManagementBuilder extends BaseFeaturePrivilegeBuilder { - public getActions( - privilegeDefinition: FeatureKibanaPrivileges, - feature: KibanaFeature - ): string[] { + public getActions(privilegeDefinition: FeatureKibanaPrivileges): string[] { const managementSections = privilegeDefinition.management; if (!managementSections) { diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/navlink.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/navlink.ts index 5ab5eee5fc0c44..7400675ed17f3d 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/navlink.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/navlink.ts @@ -4,14 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaFeature, FeatureKibanaPrivileges } from '../../../../../features/server'; +import { FeatureKibanaPrivileges } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; export class FeaturePrivilegeNavlinkBuilder extends BaseFeaturePrivilegeBuilder { - public getActions( - privilegeDefinition: FeatureKibanaPrivileges, - feature: KibanaFeature - ): string[] { + public getActions(privilegeDefinition: FeatureKibanaPrivileges): string[] { return (privilegeDefinition.app ?? []).map((app) => this.actions.ui.get('navLinks', app)); } } diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/saved_object.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/saved_object.ts index 1d7121c17adaf3..0dd89f2c5f3c1d 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/saved_object.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/saved_object.ts @@ -5,7 +5,7 @@ */ import { flatten, uniq } from 'lodash'; -import { KibanaFeature, FeatureKibanaPrivileges } from '../../../../../features/server'; +import { FeatureKibanaPrivileges } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; const readOperations: string[] = ['bulk_get', 'get', 'find']; @@ -13,10 +13,7 @@ const writeOperations: string[] = ['create', 'bulk_create', 'update', 'bulk_upda const allOperations: string[] = [...readOperations, ...writeOperations]; export class FeaturePrivilegeSavedObjectBuilder extends BaseFeaturePrivilegeBuilder { - public getActions( - privilegeDefinition: FeatureKibanaPrivileges, - feature: KibanaFeature - ): string[] { + public getActions(privilegeDefinition: FeatureKibanaPrivileges): string[] { return uniq([ ...flatten( privilegeDefinition.savedObject.all.map((type) => [ diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/sub_feature_privilege_iterator.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/sub_feature_privilege_iterator.ts index dc8511c7f4ec43..d54b6d458d9132 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/sub_feature_privilege_iterator.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/sub_feature_privilege_iterator.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SubFeaturePrivilegeConfig } from '../../../../../features/common'; -import { KibanaFeature } from '../../../../../features/server'; +import { KibanaFeature, SubFeaturePrivilegeConfig } from '../../../../../features/common'; export function* subFeaturePrivilegeIterator( feature: KibanaFeature From f86289956274c0240ecc65572af1bbee90f10458 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Wed, 9 Sep 2020 12:47:18 -0400 Subject: [PATCH 16/17] updating ML access tests --- .../apps/ml/permissions/no_ml_access.ts | 11 ++------ .../apps/ml/permissions/read_ml_access.ts | 11 ++------ .../test/functional/services/ml/navigation.ts | 26 ++++++++----------- 3 files changed, 15 insertions(+), 33 deletions(-) diff --git a/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts index 6fd78458a6ce53..ab67e567e67acb 100644 --- a/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts @@ -55,16 +55,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('should not allow to access the Stack Management ML page', async () => { await ml.testExecution.logTestStep( - 'should load the stack management with the ML menu item being present' + 'should load the stack management with the ML menu item being absent' ); - await ml.navigation.navigateToStackManagement(); - - await ml.testExecution.logTestStep( - 'should display the access denied page in stack management' - ); - await ml.navigation.navigateToStackManagementJobsListPage({ - expectAccessDenied: true, - }); + await ml.navigation.navigateToStackManagement({ expectMlLink: false }); }); }); } diff --git a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts index a358e57f792c75..cb964995511ef3 100644 --- a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts @@ -408,16 +408,9 @@ export default function ({ getService }: FtrProviderContext) { it('should display elements on Stack Management ML page correctly', async () => { await ml.testExecution.logTestStep( - 'should load the stack management with the ML menu item being present' + 'should load the stack management with the ML menu item being absent' ); - await ml.navigation.navigateToStackManagement(); - - await ml.testExecution.logTestStep( - 'should display the access denied page in stack management' - ); - await ml.navigation.navigateToStackManagementJobsListPage({ - expectAccessDenied: true, - }); + await ml.navigation.navigateToStackManagement({ expectMlLink: false }); }); }); } diff --git a/x-pack/test/functional/services/ml/navigation.ts b/x-pack/test/functional/services/ml/navigation.ts index 9b53e5ce2f7e7e..e564c03f62d58a 100644 --- a/x-pack/test/functional/services/ml/navigation.ts +++ b/x-pack/test/functional/services/ml/navigation.ts @@ -23,10 +23,14 @@ export function MachineLearningNavigationProvider({ }); }, - async navigateToStackManagement() { + async navigateToStackManagement({ expectMlLink = true }: { expectMlLink?: boolean } = {}) { await retry.tryForTime(60 * 1000, async () => { await PageObjects.common.navigateToApp('management'); - await testSubjects.existOrFail('jobsListLink', { timeout: 2000 }); + if (expectMlLink) { + await testSubjects.existOrFail('jobsListLink', { timeout: 2000 }); + } else { + await testSubjects.missingOrFail('jobsListLink', { timeout: 2000 }); + } }); }, @@ -84,22 +88,14 @@ export function MachineLearningNavigationProvider({ await this.navigateToArea('~mlMainTab & ~settings', 'mlPageSettings'); }, - async navigateToStackManagementJobsListPage({ - expectAccessDenied = false, - }: { - expectAccessDenied?: boolean; - } = {}) { + async navigateToStackManagementJobsListPage() { // clicks the jobsListLink and loads the jobs list page await testSubjects.click('jobsListLink'); await retry.tryForTime(60 * 1000, async () => { - if (expectAccessDenied === true) { - await testSubjects.existOrFail('mlPageAccessDenied'); - } else { - // verify that the overall page is present - await testSubjects.existOrFail('mlPageStackManagementJobsList'); - // verify that the default tab with the anomaly detection jobs list got loaded - await testSubjects.existOrFail('ml-jobs-list'); - } + // verify that the overall page is present + await testSubjects.existOrFail('mlPageStackManagementJobsList'); + // verify that the default tab with the anomaly detection jobs list got loaded + await testSubjects.existOrFail('ml-jobs-list'); }); }, From d28ecd607b60d35d8573bd932e4da79e645056f4 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Wed, 9 Sep 2020 14:35:31 -0400 Subject: [PATCH 17/17] ML user should not access the management interface --- x-pack/plugins/ml/common/types/capabilities.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/ml/common/types/capabilities.ts b/x-pack/plugins/ml/common/types/capabilities.ts index 8f29365fdca1bb..42f056b8908283 100644 --- a/x-pack/plugins/ml/common/types/capabilities.ts +++ b/x-pack/plugins/ml/common/types/capabilities.ts @@ -102,6 +102,7 @@ export function getPluginPrivileges() { ...privilege, api: userMlCapabilitiesKeys.map((k) => `ml:${k}`), catalogue: [PLUGIN_ID], + management: { insightsAndAlerting: [] }, ui: userMlCapabilitiesKeys, savedObject: { all: [],