From 047152f890c13e613ef777904514320a924baa27 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Mon, 14 Sep 2020 14:33:17 -0400 Subject: [PATCH] [7.x] Hide management sections based on cluster/index privileges (#67791) (#77345) Co-authored-by: Elastic Machine --- .../security/feature-registration.asciidoc | 48 +- .../manage-policy.asciidoc | 6 + docs/management/managing-ccr.asciidoc | 7 + docs/management/managing-licenses.asciidoc | 7 + .../managing-remote-clusters.asciidoc | 7 + .../create_and_manage_rollups.asciidoc | 7 + .../upgrade-assistant/index.asciidoc | 8 + examples/alerting_example/server/plugin.ts | 2 +- src/core/MIGRATION.md | 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 + .../plugins/xpack_main/server/xpack_main.d.ts | 2 +- .../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 | 405 +-- .../authorization/alerts_authorization.ts | 41 +- x-pack/plugins/alerts/server/plugin.test.ts | 6 +- x-pack/plugins/apm/server/plugin.ts | 2 +- x-pack/plugins/beats_management/kibana.json | 3 +- .../plugins/beats_management/server/plugin.ts | 18 +- 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 +- .../features/common/elasticsearch_feature.ts | 85 + .../feature_elasticsearch_privileges.ts | 72 + x-pack/plugins/features/common/index.ts | 4 +- .../common/{feature.ts => kibana_feature.ts} | 14 +- .../features/public/features_api_client.ts | 6 +- x-pack/plugins/features/public/index.ts | 4 +- .../feature_registry.test.ts.snap | 26 +- .../features/server/feature_registry.test.ts | 2175 +++++++++------- .../features/server/feature_registry.ts | 54 +- .../plugins/features/server/feature_schema.ts | 71 +- x-pack/plugins/features/server/index.ts | 9 +- x-pack/plugins/features/server/mocks.ts | 9 +- .../features/server/oss_features.test.ts | 4 +- .../plugins/features/server/oss_features.ts | 6 +- x-pack/plugins/features/server/plugin.test.ts | 48 +- x-pack/plugins/features/server/plugin.ts | 43 +- .../features/server/routes/index.test.ts | 18 +- .../plugins/features/server/routes/index.ts | 2 +- .../ui_capabilities_for_features.test.ts | 530 ++-- .../server/ui_capabilities_for_features.ts | 79 +- 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 +- .../plugins/ml/common/types/capabilities.ts | 1 + .../ml/public/application/management/index.ts | 2 +- x-pack/plugins/ml/public/plugin.ts | 19 +- 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_features.ts | 11 +- .../roles/__fixtures__/kibana_privileges.ts | 8 +- .../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 +- .../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 | 8 +- .../authorization/authorization_service.ts | 14 +- .../authorization/check_privileges.test.ts | 2284 +++++++++++++++-- .../server/authorization/check_privileges.ts | 97 +- .../check_privileges_dynamically.test.ts | 10 +- .../check_privileges_dynamically.ts | 11 +- .../check_saved_objects_privileges.test.ts | 10 +- .../check_saved_objects_privileges.ts | 8 +- .../disable_ui_capabilities.test.ts | 245 +- .../authorization/disable_ui_capabilities.ts | 178 +- .../server/authorization/index.mock.ts | 1 + .../alerting.test.ts | 10 +- .../feature_privilege_builder/alerting.ts | 7 +- .../feature_privilege_builder/api.ts | 4 +- .../feature_privilege_builder/app.ts | 4 +- .../feature_privilege_builder/catalogue.ts | 4 +- .../feature_privilege_builder.ts | 6 +- .../feature_privilege_builder/index.ts | 4 +- .../feature_privilege_builder/management.ts | 4 +- .../feature_privilege_builder/navlink.ts | 4 +- .../feature_privilege_builder/saved_object.ts | 4 +- .../feature_privilege_builder/ui.ts | 7 +- .../feature_privilege_iterator.test.ts | 22 +- .../feature_privilege_iterator.ts | 6 +- .../sub_feature_privilege_iterator.ts | 5 +- .../privileges/privileges.test.ts | 212 +- .../authorization/privileges/privileges.ts | 10 +- .../security/server/authorization/types.ts | 56 + .../validate_feature_privileges.test.ts | 20 +- .../validate_feature_privileges.ts | 6 +- .../validate_reserved_privileges.test.ts | 16 +- .../validate_reserved_privileges.ts | 4 +- .../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 +- .../routes/authorization/roles/put.test.ts | 4 +- .../server/routes/authorization/roles/put.ts | 6 +- .../plugins/security/server/routes/index.ts | 4 +- ...ecure_saved_objects_client_wrapper.test.ts | 40 +- .../secure_saved_objects_client_wrapper.ts | 10 +- .../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 + .../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_provider.test.ts | 3 + .../capabilities/capabilities_provider.ts | 3 + .../capabilities_switcher.test.ts | 6 +- .../capabilities/capabilities_switcher.ts | 14 +- .../on_post_auth_interceptor.test.ts | 6 +- .../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 | 8 +- .../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 +- .../apis/features/features/features.ts | 4 +- .../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 | 6 +- 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 +- .../apps/ml/permissions/no_ml_access.ts | 11 +- .../apps/ml/permissions/read_ml_access.ts | 11 +- .../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 | 83 + .../test/functional/services/ml/navigation.ts | 26 +- .../fixtures/plugins/alerts/server/plugin.ts | 2 +- .../plugins/foo_plugin/server/index.ts | 2 +- .../security_and_spaces/tests/catalogue.ts | 49 +- .../security_only/tests/catalogue.ts | 8 +- .../spaces_only/tests/catalogue.ts | 10 +- 238 files changed, 6997 insertions(+), 2378 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/elasticsearch_feature.ts create mode 100644 x-pack/plugins/features/common/feature_elasticsearch_privileges.ts rename x-pack/plugins/features/common/{feature.ts => kibana_feature.ts} (92%) 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/docs/developer/architecture/security/feature-registration.asciidoc b/docs/developer/architecture/security/feature-registration.asciidoc index 3724624dbb917..3ff83e9db8c43 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. }); } @@ -45,12 +44,12 @@ Registering a feature consists of the following fields. For more information, co |An array of applications this feature enables. Typically, all of your plugin's apps (from `uiExports`) will be included here. |`privileges` (required) -|{kib-repo}blob/{branch}/x-pack/plugins/features/common/feature.ts[`FeatureConfig`]. +|{kib-repo}blob/{branch}/x-pack/plugins/features/common/feature.ts[`KibanaFeatureConfig`]. |See <> 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. @@ -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', { diff --git a/docs/management/index-lifecycle-policies/manage-policy.asciidoc b/docs/management/index-lifecycle-policies/manage-policy.asciidoc index a57af8a33494b..8e2dc96de4b99 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 67193b3b5a037..9c06e479e28b2 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 25ae29036f656..b53bda95466dc 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 83895838efec6..92e0fa822b056 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 8aa57f50fe94b..e20f384b5ed18 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 9f8a0f00dbf33..994780c559334 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/examples/alerting_example/server/plugin.ts b/examples/alerting_example/server/plugin.ts index e74cad28f77f4..8e246960937ec 100644 --- a/examples/alerting_example/server/plugin.ts +++ b/examples/alerting_example/server/plugin.ts @@ -38,7 +38,7 @@ export class AlertingExamplePlugin implements Plugin { private readonly managementSections = new ManagementSectionsService(); + private readonly appUpdater = new BehaviorSubject(() => ({})); + constructor(private initializerContext: PluginInitializerContext) {} public setup(core: CoreSetup, { home }: ManagementSetupDependencies) { @@ -70,6 +76,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 104094f5b6fb5..83eac78621a53 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 4c97d672bae2e..057ae0bd13b6e 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'; @@ -91,4 +92,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 0000000000000..54cd229a8e858 --- /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 0000000000000..9aed490bc6998 --- /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/legacy/plugins/xpack_main/server/xpack_main.d.ts b/x-pack/legacy/plugins/xpack_main/server/xpack_main.d.ts index f4363a8e57b37..c2ec5662ad12e 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,7 +5,7 @@ */ import KbnServer from 'src/legacy/server/kbn_server'; -import { Feature, FeatureConfig } from '../../../../plugins/features/server'; +import { KibanaFeature } from '../../../../plugins/features/server'; import { XPackInfo, XPackInfoOptions } from './lib/xpack_info'; export { XPackFeature } from './lib/xpack_info'; 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 a48124cdbcb6a..14573161b8d5d 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 da5a5a1cdc3eb..3ba798ddf1715 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 a6c5899281658..592ca93ef5a16 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -159,7 +159,7 @@ export class ActionsPlugin implements Plugin, Plugi ); } - plugins.features.registerFeature(ACTIONS_FEATURE); + plugins.features.registerKibanaFeature(ACTIONS_FEATURE); setupSavedObjects(core.savedObjects, plugins.encryptedSavedObjects); this.eventLogService = plugins.eventLog; diff --git a/x-pack/plugins/alerting_builtins/server/plugin.test.ts b/x-pack/plugins/alerting_builtins/server/plugin.test.ts index 15ad066523502..629c02d923071 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 41871c01bfb50..48e5c41cbe637 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 6307e463af853..62058d47cbd44 100644 --- a/x-pack/plugins/alerts/README.md +++ b/x-pack/plugins/alerts/README.md @@ -306,7 +306,7 @@ In addition, when users are inside your feature you might want to grant them acc You can control all of these abilities by assigning privileges to the Alerting Framework from within your own feature, for example: ```typescript -features.registerFeature({ +features.registerKibanaFeature({ id: 'my-application-id', name: 'My Application', app: [], @@ -348,7 +348,7 @@ In this example we can see the following: It's important to note that any role can be granted a mix of `all` and `read` privileges accross multiple type, for example: ```typescript -features.registerFeature({ +features.registerKibanaFeature({ id: 'my-application-id', name: 'My Application', app: [], 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 b164d27ded648..c2506381b9df9 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: [], @@ -174,7 +177,7 @@ beforeEach(() => { async executor() {}, producer: 'myApp', })); - features.getFeatures.mockReturnValue([ + features.getKibanaFeatures.mockReturnValue([ myAppFeature, myOtherAppFeature, myAppWithSubFeature, @@ -255,7 +258,7 @@ describe('AlertsAuthorization', () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', hasAllRequested: true, - privileges: [], + privileges: { kibana: [] }, }); await alertAuthorization.ensureAuthorized('myType', 'myApp', WriteOperations.Create); @@ -263,9 +266,9 @@ describe('AlertsAuthorization', () => { expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType'); expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'create'); - expect(checkPrivileges).toHaveBeenCalledWith([ - mockAuthorizationAction('myType', 'myApp', 'create'), - ]); + expect(checkPrivileges).toHaveBeenCalledWith({ + kibana: [mockAuthorizationAction('myType', 'myApp', 'create')], + }); expect(auditLogger.alertsAuthorizationSuccess).toHaveBeenCalledTimes(1); expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); @@ -298,7 +301,7 @@ describe('AlertsAuthorization', () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', hasAllRequested: true, - privileges: [], + privileges: { kibana: [] }, }); await alertAuthorization.ensureAuthorized('myType', 'alerts', WriteOperations.Create); @@ -306,9 +309,9 @@ describe('AlertsAuthorization', () => { expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType'); expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'create'); - expect(checkPrivileges).toHaveBeenCalledWith([ - mockAuthorizationAction('myType', 'myApp', 'create'), - ]); + expect(checkPrivileges).toHaveBeenCalledWith({ + kibana: [mockAuthorizationAction('myType', 'myApp', 'create')], + }); expect(auditLogger.alertsAuthorizationSuccess).toHaveBeenCalledTimes(1); expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); @@ -332,7 +335,7 @@ describe('AlertsAuthorization', () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', hasAllRequested: true, - privileges: [], + privileges: { kibana: [] }, }); const alertAuthorization = new AlertsAuthorization({ @@ -354,10 +357,12 @@ describe('AlertsAuthorization', () => { 'myOtherApp', 'create' ); - expect(checkPrivileges).toHaveBeenCalledWith([ - mockAuthorizationAction('myType', 'myOtherApp', 'create'), - mockAuthorizationAction('myType', 'myApp', 'create'), - ]); + expect(checkPrivileges).toHaveBeenCalledWith({ + kibana: [ + mockAuthorizationAction('myType', 'myOtherApp', 'create'), + mockAuthorizationAction('myType', 'myApp', 'create'), + ], + }); expect(auditLogger.alertsAuthorizationSuccess).toHaveBeenCalledTimes(1); expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); @@ -390,16 +395,18 @@ describe('AlertsAuthorization', () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', hasAllRequested: false, - privileges: [ - { - privilege: mockAuthorizationAction('myType', 'myOtherApp', 'create'), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myType', 'myApp', 'create'), - authorized: true, - }, - ], + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('myType', 'myOtherApp', 'create'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myType', 'myApp', 'create'), + authorized: true, + }, + ], + }, }); await expect( @@ -439,16 +446,18 @@ describe('AlertsAuthorization', () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', hasAllRequested: false, - privileges: [ - { - privilege: mockAuthorizationAction('myType', 'myOtherApp', 'create'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myType', 'myApp', 'create'), - authorized: false, - }, - ], + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('myType', 'myOtherApp', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myType', 'myApp', 'create'), + authorized: false, + }, + ], + }, }); await expect( @@ -488,16 +497,18 @@ describe('AlertsAuthorization', () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', hasAllRequested: false, - privileges: [ - { - privilege: mockAuthorizationAction('myType', 'myOtherApp', 'create'), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myType', 'myApp', 'create'), - authorized: false, - }, - ], + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('myType', 'myOtherApp', 'create'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myType', 'myApp', 'create'), + authorized: false, + }, + ], + }, }); await expect( @@ -592,7 +603,7 @@ describe('AlertsAuthorization', () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', hasAllRequested: true, - privileges: [], + privileges: { kibana: [] }, }); const alertAuthorization = new AlertsAuthorization({ @@ -621,24 +632,26 @@ describe('AlertsAuthorization', () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', hasAllRequested: false, - privileges: [ - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'find'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'find'), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'find'), - authorized: false, - }, - ], + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'find'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'find'), + authorized: false, + }, + ], + }, }); const alertAuthorization = new AlertsAuthorization({ @@ -680,24 +693,26 @@ describe('AlertsAuthorization', () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', hasAllRequested: false, - privileges: [ - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'find'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'find'), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'find'), - authorized: true, - }, - ], + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'find'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'find'), + authorized: true, + }, + ], + }, }); const alertAuthorization = new AlertsAuthorization({ @@ -728,32 +743,34 @@ describe('AlertsAuthorization', () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', hasAllRequested: false, - privileges: [ - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'find'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'find'), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'find'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('mySecondAppAlertType', 'myApp', 'find'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('mySecondAppAlertType', 'myOtherApp', 'find'), - authorized: true, - }, - ], + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'find'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('mySecondAppAlertType', 'myApp', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('mySecondAppAlertType', 'myOtherApp', 'find'), + authorized: true, + }, + ], + }, }); const alertAuthorization = new AlertsAuthorization({ @@ -903,24 +920,26 @@ describe('AlertsAuthorization', () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', hasAllRequested: false, - privileges: [ - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'create'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'create'), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), - authorized: true, - }, - ], + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'create'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), + authorized: true, + }, + ], + }, }); const alertAuthorization = new AlertsAuthorization({ @@ -989,16 +1008,18 @@ describe('AlertsAuthorization', () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', hasAllRequested: false, - privileges: [ - { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), - authorized: false, - }, - ], + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), + authorized: false, + }, + ], + }, }); const alertAuthorization = new AlertsAuthorization({ @@ -1048,40 +1069,42 @@ describe('AlertsAuthorization', () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', hasAllRequested: false, - privileges: [ - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'create'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'create'), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'get'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'get'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'get'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'get'), - authorized: true, - }, - ], + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'create'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'get'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'get'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'get'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'get'), + authorized: true, + }, + ], + }, }); const alertAuthorization = new AlertsAuthorization({ @@ -1158,24 +1181,26 @@ describe('AlertsAuthorization', () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', hasAllRequested: false, - privileges: [ - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'create'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'create'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), - authorized: false, - }, - ], + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), + authorized: false, + }, + ], + }, }); const alertAuthorization = new AlertsAuthorization({ diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts index b362a50c9f10b..9dda006c1eb8e 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts @@ -82,7 +82,7 @@ export class AlertsAuthorization { (disabledFeatures) => new Set( features - .getFeatures() + .getKibanaFeatures() .filter( ({ id, alerting }) => // ignore features which are disabled in the user's space @@ -133,20 +133,21 @@ export class AlertsAuthorization { const shouldAuthorizeConsumer = consumer !== ALERTS_FEATURE_ID; const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request); - const { hasAllRequested, username, privileges } = await checkPrivileges( - shouldAuthorizeConsumer && consumer !== alertType.producer - ? [ - // check for access at consumer level - requiredPrivilegesByScope.consumer, - // check for access at producer level - requiredPrivilegesByScope.producer, - ] - : [ - // skip consumer privilege checks under `alerts` as all alert types can - // be created under `alerts` if you have producer level privileges - requiredPrivilegesByScope.producer, - ] - ); + const { hasAllRequested, username, privileges } = await checkPrivileges({ + kibana: + shouldAuthorizeConsumer && consumer !== alertType.producer + ? [ + // check for access at consumer level + requiredPrivilegesByScope.consumer, + // check for access at producer level + requiredPrivilegesByScope.producer, + ] + : [ + // skip consumer privilege checks under `alerts` as all alert types can + // be created under `alerts` if you have producer level privileges + requiredPrivilegesByScope.producer, + ], + }); if (!isAvailableConsumer) { /** @@ -177,7 +178,7 @@ export class AlertsAuthorization { ); } else { const authorizedPrivileges = map( - privileges.filter((privilege) => privilege.authorized), + privileges.kibana.filter((privilege) => privilege.authorized), 'privilege' ); const unauthorizedScopes = mapValues( @@ -341,9 +342,9 @@ export class AlertsAuthorization { } } - const { username, hasAllRequested, privileges } = await checkPrivileges([ - ...privilegeToAlertType.keys(), - ]); + const { username, hasAllRequested, privileges } = await checkPrivileges({ + kibana: [...privilegeToAlertType.keys()], + }); return { username, @@ -352,7 +353,7 @@ export class AlertsAuthorization { ? // has access to all features this.augmentWithAuthorizedConsumers(alertTypes, await this.allPossibleConsumers) : // only has some of the required privileges - privileges.reduce((authorizedAlertTypes, { authorized, privilege }) => { + privileges.kibana.reduce((authorizedAlertTypes, { authorized, privilege }) => { if (authorized && privilegeToAlertType.has(privilege)) { const [ alertType, diff --git a/x-pack/plugins/alerts/server/plugin.test.ts b/x-pack/plugins/alerts/server/plugin.test.ts index e65d195290259..026aa0c5238dc 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()', () => { @@ -159,8 +159,8 @@ describe('Alerting Plugin', () => { function mockFeatures() { const features = featuresPluginMock.createSetup(); - features.getFeatures.mockReturnValue([ - new Feature({ + features.getKibanaFeatures.mockReturnValue([ + new KibanaFeature({ id: 'appName', name: 'appName', app: [], diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index f7e3977ae7d31..f25e37927f094 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -127,7 +127,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 3fd1ab6fd8701..c1070eedf07a6 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 92c2278148bc1..fde0a2efecdda 100644 --- a/x-pack/plugins/beats_management/server/plugin.ts +++ b/x-pack/plugins/beats_management/server/plugin.ts @@ -11,6 +11,7 @@ import { Plugin, PluginInitializerContext, } from '../../../../src/core/server'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { SecurityPluginSetup } from '../../security/server'; import { LicensingPluginStart } from '../../licensing/server'; import { BeatsManagementConfigType } from '../common'; @@ -22,6 +23,7 @@ import { beatsIndexTemplate } from './index_templates'; interface SetupDeps { security?: SecurityPluginSetup; + features: FeaturesPluginSetup; } interface StartDeps { @@ -42,7 +44,7 @@ export class BeatsManagementPlugin implements Plugin<{}, {}, SetupDeps, StartDep private readonly initializerContext: PluginInitializerContext ) {} - public async setup(core: CoreSetup, { security }: SetupDeps) { + public async setup(core: CoreSetup, { features, security }: SetupDeps) { this.securitySetup = security; const router = core.http.createRouter(); @@ -52,6 +54,20 @@ export class BeatsManagementPlugin implements Plugin<{}, {}, SetupDeps, StartDep return this.beatsLibs!; }); + features.registerElasticsearchFeature({ + id: 'beats_management', + management: { + ingest: ['beats_management'], + }, + privileges: [ + { + ui: [], + requiredClusterPrivileges: [], + requiredRoles: ['beats_admin'], + }, + ], + }); + return {}; } diff --git a/x-pack/plugins/canvas/server/plugin.ts b/x-pack/plugins/canvas/server/plugin.ts index c822ed86cb01c..9a41a00883c13 100644 --- a/x-pack/plugins/canvas/server/plugin.ts +++ b/x-pack/plugins/canvas/server/plugin.ts @@ -37,7 +37,7 @@ export class CanvasPlugin implements Plugin { coreSetup.savedObjects.registerType(workpadType); coreSetup.savedObjects.registerType(workpadTemplateType); - plugins.features.registerFeature({ + plugins.features.registerKibanaFeature({ id: 'canvas', name: 'Canvas', order: 400, diff --git a/x-pack/plugins/cross_cluster_replication/kibana.json b/x-pack/plugins/cross_cluster_replication/kibana.json index 13746bb0e34c3..292820f81adbe 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 e39b4dfd471a8..d40a53f289873 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 c287acf86eb2b..62c96b48c4373 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 0239cb6422d03..497747f953289 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 729a03d24065e..3d28a05a4b7b4 100644 --- a/x-pack/plugins/enterprise_search/server/plugin.ts +++ b/x-pack/plugins/enterprise_search/server/plugin.ts @@ -78,7 +78,7 @@ export class EnterpriseSearchPlugin implements Plugin { /** * Register space/feature control */ - features.registerFeature({ + features.registerKibanaFeature({ id: ENTERPRISE_SEARCH_PLUGIN.ID, name: ENTERPRISE_SEARCH_PLUGIN.NAME, order: 0, diff --git a/x-pack/plugins/features/common/elasticsearch_feature.ts b/x-pack/plugins/features/common/elasticsearch_feature.ts new file mode 100644 index 0000000000000..4566afc77bd8b --- /dev/null +++ b/x-pack/plugins/features/common/elasticsearch_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 all privileges in at least one of the defined sets of privileges in order to be granted access. + * + * @example + * ```ts + * [{ + * requiredClusterPrivileges: ['monitor'], + * requiredIndexPrivileges: { + * ['metricbeat-*']: ['read', 'view_index_metadata'] + * } + * }] + * ``` + * @see FeatureElasticsearchPrivileges + */ + privileges: FeatureElasticsearchPrivileges[]; +} + +export class ElasticsearchFeature { + constructor(protected readonly config: RecursiveReadonly) {} + + 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_elasticsearch_privileges.ts b/x-pack/plugins/features/common/feature_elasticsearch_privileges.ts new file mode 100644 index 0000000000000..1100b2cc648c9 --- /dev/null +++ b/x-pack/plugins/features/common/feature_elasticsearch_privileges.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. + */ + +/** + * Elasticsearch Feature privilege definition + */ +export interface FeatureElasticsearchPrivileges { + /** + * A set of Elasticsearch cluster privileges which are required for this feature to be enabled. + * See https://www.elastic.co/guide/en/elasticsearch/reference/current/security-privileges.html + * + */ + requiredClusterPrivileges: string[]; + + /** + * A set of Elasticsearch index privileges which are required for this feature to be enabled, keyed on index name or pattern. + * See https://www.elastic.co/guide/en/elasticsearch/reference/current/security-privileges.html#privileges-list-indices + * + * @example + * + * Requiring `read` access to `logstash-*` and `all` access to `foo-*` + * ```ts + * feature.registerElasticsearchPrivilege({ + * privileges: [{ + * requiredIndexPrivileges: { + * ['logstash-*']: ['read'], + * ['foo-*]: ['all'] + * } + * }] + * }) + * ``` + * + */ + requiredIndexPrivileges?: { + [indexName: string]: string[]; + }; + + /** + * A set of Elasticsearch roles which are required for this feature to be enabled. + * + * @deprecated do not rely on hard-coded role names. + * + * This is relied on by the reporting feature, and should be removed once reporting + * migrates to using the Kibana Privilege model: https://github.com/elastic/kibana/issues/19914 + */ + requiredRoles?: string[]; + + /** + * A list of UI Capabilities that should be granted to users with this privilege. + * These capabilities will automatically be namespaces within your feature id. + * + * @example + * ```ts + * { + * ui: ['show', 'save'] + * } + * + * This translates in the UI to the following (assuming a feature id of "foo"): + * import { uiCapabilities } from 'ui/capabilities'; + * + * const canShowApp = uiCapabilities.foo.show; + * const canSave = uiCapabilities.foo.save; + * ``` + * Note: Since these are automatically namespaced, you are free to use generic names like "show" and "save". + * + * @see UICapabilities + */ + ui: string[]; +} diff --git a/x-pack/plugins/features/common/index.ts b/x-pack/plugins/features/common/index.ts index e359efbda20d2..a08de2f118712 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 './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 92% rename from x-pack/plugins/features/common/feature.ts rename to x-pack/plugins/features/common/kibana_feature.ts index 1b700fb1a6ad0..a600ada554afd 100644 --- a/x-pack/plugins/features/common/feature.ts +++ b/x-pack/plugins/features/common/kibana_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/public/features_api_client.ts b/x-pack/plugins/features/public/features_api_client.ts index 50cc54a197f56..cacc623aa853f 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 f19c7f947d97f..7d86312e466ee 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/__snapshots__/feature_registry.test.ts.snap b/x-pack/plugins/features/server/__snapshots__/feature_registry.test.ts.snap index e033b241f9e25..fdeb53dd2fa12 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 f123068e41758..24aae3a69ee5d 100644 --- a/x-pack/plugins/features/server/feature_registry.test.ts +++ b/x-pack/plugins/features/server/feature_registry.test.ts @@ -5,1192 +5,1389 @@ */ import { FeatureRegistry } from './feature_registry'; -import { FeatureConfig } from '../common/feature'; +import { ElasticsearchFeatureConfig, KibanaFeatureConfig } 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: ['all'], + 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 some form of required es privileges', () => { + 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( + `"Feature test-feature has a privilege definition at index 0 without any privileges defined."` + ); + }); + + 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: ['all'], + 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: ['all'], + 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: ['all'], + 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 12aafd226f754..d357bdb782797 100644 --- a/x-pack/plugins/features/server/feature_registry.ts +++ b/x-pack/plugins/features/server/feature_registry.ts @@ -5,38 +5,72 @@ */ import { cloneDeep, uniq } from 'lodash'; -import { FeatureConfig, Feature, FeatureKibanaPrivileges } from '../common'; -import { validateFeature } from './feature_schema'; +import { + KibanaFeatureConfig, + KibanaFeature, + FeatureKibanaPrivileges, + ElasticsearchFeatureConfig, + ElasticsearchFeature, +} from '../common'; +import { validateKibanaFeature, validateElasticsearchFeature } from './feature_schema'; export class FeatureRegistry { private locked = false; - private features: Record = {}; + private kibanaFeatures: Record = {}; + private esFeatures: Record = {}; - public register(feature: FeatureConfig) { + public registerKibanaFeature(feature: KibanaFeatureConfig) { if (this.locked) { throw new Error( `Features are locked, can't register new features. Attempt to register ${feature.id} failed.` ); } - validateFeature(feature); + validateKibanaFeature(feature); - if (feature.id in this.features) { + if (feature.id in this.kibanaFeatures || feature.id in this.esFeatures) { throw new Error(`Feature with id ${feature.id} is already registered.`); } const featureCopy = cloneDeep(feature); - this.features[feature.id] = applyAutomaticPrivilegeGrants(featureCopy); + this.kibanaFeatures[feature.id] = applyAutomaticPrivilegeGrants(featureCopy); } - public getAll(): Feature[] { + public registerElasticsearchFeature(feature: ElasticsearchFeatureConfig) { + if (this.locked) { + throw new Error( + `Features are locked, can't register new features. Attempt to register ${feature.id} failed.` + ); + } + + if (feature.id in this.kibanaFeatures || feature.id in this.esFeatures) { + throw new Error(`Feature with id ${feature.id} is already registered.`); + } + + validateElasticsearchFeature(feature); + + const featureCopy = cloneDeep(feature); + + this.esFeatures[feature.id] = featureCopy; + } + + public getAllKibanaFeatures(): KibanaFeature[] { + this.locked = true; + return Object.values(this.kibanaFeatures).map( + (featureConfig) => new KibanaFeature(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) + ); } } -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 95298603d706a..06a3eb158d99d 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'; +import { FeatureKibanaPrivileges, ElasticsearchFeatureConfig } from '.'; // Each feature gets its own property on the UICapabilities object, // but that object has a few built-in properties which should not be overwritten. @@ -28,7 +28,7 @@ const managementSchema = Joi.object().pattern( const catalogueSchema = Joi.array().items(Joi.string().regex(uiCapabilitiesRegex)); const alertingSchema = Joi.array().items(Joi.string()); -const privilegeSchema = Joi.object({ +const kibanaPrivilegeSchema = Joi.object({ excludeFromBasePrivileges: Joi.boolean(), management: managementSchema, catalogue: catalogueSchema, @@ -45,7 +45,7 @@ const privilegeSchema = Joi.object({ ui: Joi.array().items(Joi.string().regex(uiCapabilitiesRegex)).required(), }); -const subFeaturePrivilegeSchema = Joi.object({ +const kibanaSubFeaturePrivilegeSchema = Joi.object({ id: Joi.string().regex(subFeaturePrivilegePartRegex).required(), name: Joi.string().required(), includeIn: Joi.string().allow('all', 'read', 'none').required(), @@ -64,17 +64,17 @@ const subFeaturePrivilegeSchema = Joi.object({ ui: Joi.array().items(Joi.string().regex(uiCapabilitiesRegex)).required(), }); -const subFeatureSchema = Joi.object({ +const kibanaSubFeatureSchema = Joi.object({ name: Joi.string().required(), privilegeGroups: Joi.array().items( Joi.object({ groupType: Joi.string().valid('mutually_exclusive', 'independent').required(), - privileges: Joi.array().items(subFeaturePrivilegeSchema).min(1), + privileges: Joi.array().items(kibanaSubFeaturePrivilegeSchema).min(1), }) ), }); -const schema = Joi.object({ +const kibanaFeatureSchema = Joi.object({ id: Joi.string() .regex(featurePrivilegePartRegex) .invalid(...prohibitedFeatureIds) @@ -93,15 +93,15 @@ const schema = Joi.object({ catalogue: catalogueSchema, alerting: alertingSchema, privileges: Joi.object({ - all: privilegeSchema, - read: privilegeSchema, + all: kibanaPrivilegeSchema, + read: kibanaPrivilegeSchema, }) .allow(null) .required(), subFeatures: Joi.when('privileges', { is: null, - then: Joi.array().items(subFeatureSchema).max(0), - otherwise: Joi.array().items(subFeatureSchema), + then: Joi.array().items(kibanaSubFeatureSchema).max(0), + otherwise: Joi.array().items(kibanaSubFeatureSchema), }), privilegesTooltip: Joi.string(), reserved: Joi.object({ @@ -110,15 +110,32 @@ const schema = Joi.object({ .items( Joi.object({ id: Joi.string().regex(reservedFeaturePrrivilegePartRegex).required(), - privilege: privilegeSchema.required(), + privilege: kibanaPrivilegeSchema.required(), }) ) .required(), }), }); -export function validateFeature(feature: FeatureConfig) { - const validateResult = Joi.validate(feature, schema); +const elasticsearchPrivilegeSchema = Joi.object({ + ui: Joi.array().items(Joi.string()).required(), + requiredClusterPrivileges: Joi.array().items(Joi.string()), + requiredIndexPrivileges: Joi.object().pattern(Joi.string(), Joi.array().items(Joi.string())), + requiredRoles: Joi.array().items(Joi.string()), +}); + +const elasticsearchFeatureSchema = Joi.object({ + id: Joi.string() + .regex(featurePrivilegePartRegex) + .invalid(...prohibitedFeatureIds) + .required(), + management: managementSchema, + catalogue: catalogueSchema, + privileges: Joi.array().items(elasticsearchPrivilegeSchema).required(), +}); + +export function validateKibanaFeature(feature: KibanaFeatureConfig) { + const validateResult = Joi.validate(feature, kibanaFeatureSchema); if (validateResult.error) { throw validateResult.error; } @@ -303,3 +320,29 @@ export function validateFeature(feature: FeatureConfig) { ); } } + +export function validateElasticsearchFeature(feature: ElasticsearchFeatureConfig) { + const validateResult = Joi.validate(feature, elasticsearchFeatureSchema); + if (validateResult.error) { + throw validateResult.error; + } + // the following validation can't be enforced by the Joi schema without a very convoluted and verbose definition + const { privileges } = feature; + privileges.forEach((privilege, index) => { + const { + requiredClusterPrivileges = [], + requiredIndexPrivileges = [], + requiredRoles = [], + } = privilege; + + if ( + requiredClusterPrivileges.length === 0 && + requiredIndexPrivileges.length === 0 && + requiredRoles.length === 0 + ) { + throw new Error( + `Feature ${feature.id} has a privilege definition at index ${index} without any privileges defined.` + ); + } + }); +} diff --git a/x-pack/plugins/features/server/index.ts b/x-pack/plugins/features/server/index.ts index 48a350ae8f8fd..28c0fee041594 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 { + KibanaFeature, + KibanaFeatureConfig, + 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 d9437169a7453..91c297c50e462 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.test.ts b/x-pack/plugins/features/server/oss_features.test.ts index c38f2afc88389..961656aba8bfd 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 4122c590e74b1..3ff6b1b7bf44f 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'; export interface BuildOSSFeaturesParams { savedObjectTypes: string[]; @@ -368,10 +368,10 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS }, }, ...(includeTimelion ? [timelionFeature] : []), - ] as FeatureConfig[]; + ] as KibanaFeatureConfig[]; }; -const timelionFeature: FeatureConfig = { +const timelionFeature: KibanaFeatureConfig = { id: 'timelion', name: 'Timelion', order: 350, diff --git a/x-pack/plugins/features/server/plugin.test.ts b/x-pack/plugins/features/server/plugin.test.ts index 00d578f5ca866..ee11e0e2bbe2e 100644 --- a/x-pack/plugins/features/server/plugin.test.ts +++ b/x-pack/plugins/features/server/plugin.test.ts @@ -28,19 +28,19 @@ describe('Features Plugin', () => { coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); }); - it('returns OSS + registered features', async () => { + it('returns OSS + registered kibana features', async () => { const plugin = new Plugin(initContext); - const { registerFeature } = await plugin.setup(coreSetup, {}); - registerFeature({ + const { registerKibanaFeature } = await plugin.setup(coreSetup, {}); + registerKibanaFeature({ id: 'baz', name: 'baz', app: [], privileges: null, }); - const { getFeatures } = await plugin.start(coreStart); + const { getKibanaFeatures } = plugin.start(coreStart); - expect(getFeatures().map((f) => f.id)).toMatchInlineSnapshot(` + expect(getKibanaFeatures().map((f) => f.id)).toMatchInlineSnapshot(` Array [ "baz", "discover", @@ -54,9 +54,9 @@ describe('Features Plugin', () => { `); }); - it('returns OSS + registered features with timelion when available', async () => { + it('returns OSS + registered kibana features with timelion when available', async () => { const plugin = new Plugin(initContext); - const { registerFeature } = await plugin.setup(coreSetup, { + const { registerKibanaFeature: registerFeature } = await plugin.setup(coreSetup, { visTypeTimelion: { uiEnabled: true }, }); registerFeature({ @@ -66,9 +66,9 @@ describe('Features Plugin', () => { privileges: null, }); - const { getFeatures } = await plugin.start(coreStart); + const { getKibanaFeatures } = plugin.start(coreStart); - expect(getFeatures().map((f) => f.id)).toMatchInlineSnapshot(` + expect(getKibanaFeatures().map((f) => f.id)).toMatchInlineSnapshot(` Array [ "baz", "discover", @@ -83,19 +83,41 @@ describe('Features Plugin', () => { `); }); - it('registers not hidden saved objects types', async () => { + it('registers kibana features with not hidden saved objects types', async () => { const plugin = new Plugin(initContext); await plugin.setup(coreSetup, {}); - const { getFeatures } = await plugin.start(coreStart); + const { getKibanaFeatures } = plugin.start(coreStart); const soTypes = - getFeatures().find((f) => f.id === 'savedObjectsManagement')?.privileges?.all.savedObject - .all || []; + getKibanaFeatures().find((f) => f.id === 'savedObjectsManagement')?.privileges?.all + .savedObject.all || []; expect(soTypes.includes('foo')).toBe(true); expect(soTypes.includes('bar')).toBe(false); }); + it('returns registered elasticsearch features', async () => { + const plugin = new Plugin(initContext); + const { registerElasticsearchFeature } = await plugin.setup(coreSetup, {}); + registerElasticsearchFeature({ + id: 'baz', + privileges: [ + { + requiredClusterPrivileges: ['all'], + ui: ['baz-ui'], + }, + ], + }); + + const { getElasticsearchFeatures } = plugin.start(coreStart); + + expect(getElasticsearchFeatures().map((f) => f.id)).toMatchInlineSnapshot(` + Array [ + "baz", + ] + `); + }); + it('registers a capabilities provider', async () => { const plugin = new Plugin(initContext); await plugin.setup(coreSetup, {}); diff --git a/x-pack/plugins/features/server/plugin.ts b/x-pack/plugins/features/server/plugin.ts index 61b66d95ca44f..8a799887bba09 100644 --- a/x-pack/plugins/features/server/plugin.ts +++ b/x-pack/plugins/features/server/plugin.ts @@ -15,27 +15,40 @@ import { Capabilities as UICapabilities } from '../../../../src/core/server'; import { deepFreeze } from '../../../../src/core/server'; import { PluginSetupContract as TimelionSetupContract } from '../../../../src/plugins/vis_type_timelion/server'; import { FeatureRegistry } from './feature_registry'; -import { Feature, FeatureConfig } from '../common/feature'; import { uiCapabilitiesForFeatures } from './ui_capabilities_for_features'; import { buildOSSFeatures } from './oss_features'; import { defineRoutes } from './routes'; +import { + ElasticsearchFeatureConfig, + ElasticsearchFeature, + KibanaFeature, + KibanaFeatureConfig, +} from '../common'; /** * Describes public Features plugin contract returned at the `setup` stage. */ export interface PluginSetupContract { - registerFeature(feature: FeatureConfig): void; + registerKibanaFeature(feature: KibanaFeatureConfig): void; + registerElasticsearchFeature(feature: ElasticsearchFeatureConfig): void; + /* + * Calling this function during setup will crash Kibana. + * Use start contract instead. + * @deprecated + * */ + getKibanaFeatures(): KibanaFeature[]; /* * Calling this function during setup will crash Kibana. * Use start contract instead. * @deprecated * */ - getFeatures(): Feature[]; + getElasticsearchFeatures(): ElasticsearchFeature[]; getFeaturesUICapabilities(): UICapabilities; } export interface PluginStartContract { - getFeatures(): Feature[]; + getElasticsearchFeatures(): ElasticsearchFeature[]; + getKibanaFeatures(): KibanaFeature[]; } /** @@ -62,13 +75,22 @@ export class Plugin { }); const getFeaturesUICapabilities = () => - uiCapabilitiesForFeatures(this.featureRegistry.getAll()); + uiCapabilitiesForFeatures( + this.featureRegistry.getAllKibanaFeatures(), + this.featureRegistry.getAllElasticsearchFeatures() + ); core.capabilities.registerProvider(getFeaturesUICapabilities); return deepFreeze({ - registerFeature: this.featureRegistry.register.bind(this.featureRegistry), - getFeatures: this.featureRegistry.getAll.bind(this.featureRegistry), + registerKibanaFeature: this.featureRegistry.registerKibanaFeature.bind(this.featureRegistry), + registerElasticsearchFeature: this.featureRegistry.registerElasticsearchFeature.bind( + this.featureRegistry + ), + getKibanaFeatures: this.featureRegistry.getAllKibanaFeatures.bind(this.featureRegistry), + getElasticsearchFeatures: this.featureRegistry.getAllElasticsearchFeatures.bind( + this.featureRegistry + ), getFeaturesUICapabilities, }); } @@ -77,7 +99,10 @@ export class Plugin { this.registerOssFeatures(core.savedObjects); return deepFreeze({ - getFeatures: this.featureRegistry.getAll.bind(this.featureRegistry), + getElasticsearchFeatures: this.featureRegistry.getAllElasticsearchFeatures.bind( + this.featureRegistry + ), + getKibanaFeatures: this.featureRegistry.getAllKibanaFeatures.bind(this.featureRegistry), }); } @@ -98,7 +123,7 @@ export class Plugin { }); for (const feature of features) { - this.featureRegistry.register(feature); + this.featureRegistry.registerKibanaFeature(feature); } } } diff --git a/x-pack/plugins/features/server/routes/index.test.ts b/x-pack/plugins/features/server/routes/index.test.ts index 3d1efc8a479b2..30aa6d07f6b5a 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 { @@ -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'], @@ -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/routes/index.ts b/x-pack/plugins/features/server/routes/index.ts index 147d34d124fca..b5a4203d7a768 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 35dcc4cf42b37..7532bc0573b08 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,10 +5,10 @@ */ import { uiCapabilitiesForFeatures } from './ui_capabilities_for_features'; -import { Feature } from '.'; -import { SubFeaturePrivilegeGroupConfig } from '../common'; +import { KibanaFeature } from '.'; +import { SubFeaturePrivilegeGroupConfig, ElasticsearchFeature } from '../common'; -function createFeaturePrivilege(capabilities: string[] = []) { +function createKibanaFeaturePrivilege(capabilities: string[] = []) { return { savedObject: { all: [], @@ -19,7 +19,7 @@ function createFeaturePrivilege(capabilities: string[] = []) { }; } -function createSubFeaturePrivilege(privilegeId: string, capabilities: string[] = []) { +function createKibanaSubFeaturePrivilege(privilegeId: string, capabilities: string[] = []) { return { id: privilegeId, name: `sub-feature privilege ${privilegeId}`, @@ -35,44 +35,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 KibanaFeature({ + 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 KibanaFeature({ + id: 'newFeature', + name: 'my new feature', + navLinkId: 'newFeatureNavLink', + app: ['bar-app'], + privileges: { + all: createKibanaFeaturePrivilege(['capability1', 'capability2']), + read: createKibanaFeaturePrivilege(), + }, + }), + ], + [] + ) + ).toEqual({ + catalogue: {}, + management: {}, + newFeature: { + capability1: true, + capability2: true, + }, + }); + }); + + 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 KibanaFeature({ + id: 'newFeature', + name: 'my new feature', + navLinkId: 'newFeatureNavLink', + app: ['bar-app'], + catalogue: ['anotherFooEntry', 'anotherBarEntry'], + privileges: { + all: createKibanaFeaturePrivilege(['capability1', 'capability2']), + read: createKibanaFeaturePrivilege(['capability3', 'capability4']), + }, + }), + ], + [] + ) ).toEqual({ catalogue: { anotherFooEntry: true, anotherBarEntry: true, }, + management: {}, + newFeature: { + capability1: true, + capability2: true, + capability3: true, + capability4: true, + }, + }); + }); + + it('combines catalogue entries from multiple elasticsearch privileges', () => { + expect( + uiCapabilitiesForFeatures( + [], + [ + new ElasticsearchFeature({ + id: 'newFeature', + catalogue: ['anotherFooEntry', 'anotherBarEntry'], + privileges: [ + { + requiredClusterPrivileges: [], + ui: ['capability1', 'capability2'], + }, + { + requiredClusterPrivileges: [], + ui: ['capability3', 'capability4'], + }, + ], + }), + ] + ) + ).toEqual({ + catalogue: { + anotherFooEntry: true, + anotherBarEntry: true, + }, + management: {}, newFeature: { capability1: true, capability2: true, @@ -111,20 +208,24 @@ describe('populateUICapabilities', () => { it(`merges capabilities from all feature privileges`, () => { expect( - uiCapabilitiesForFeatures([ - new Feature({ - id: 'newFeature', - name: 'my new feature', - navLinkId: 'newFeatureNavLink', - app: ['bar-app'], - privileges: { - all: createFeaturePrivilege(['capability1', 'capability2']), - read: createFeaturePrivilege(['capability3', 'capability4', 'capability5']), - }, - }), - ]) + uiCapabilitiesForFeatures( + [ + new KibanaFeature({ + id: 'newFeature', + name: 'my new feature', + navLinkId: 'newFeatureNavLink', + app: ['bar-app'], + privileges: { + all: createKibanaFeaturePrivilege(['capability1', 'capability2']), + read: createKibanaFeaturePrivilege(['capability3', 'capability4', 'capability5']), + }, + }), + ], + [] + ) ).toEqual({ catalogue: {}, + management: {}, newFeature: { capability1: true, capability2: true, @@ -137,30 +238,38 @@ describe('populateUICapabilities', () => { it(`supports capabilities from reserved privileges`, () => { expect( - uiCapabilitiesForFeatures([ - new Feature({ - id: 'newFeature', - name: 'my new feature', - navLinkId: 'newFeatureNavLink', - app: ['bar-app'], - privileges: null, - reserved: { - description: '', - privileges: [ - { - id: 'rp_1', - privilege: createFeaturePrivilege(['capability1', 'capability2']), - }, - { - id: 'rp_2', - privilege: createFeaturePrivilege(['capability3', 'capability4', 'capability5']), - }, - ], - }, - }), - ]) + uiCapabilitiesForFeatures( + [ + new KibanaFeature({ + id: 'newFeature', + name: 'my new feature', + navLinkId: 'newFeatureNavLink', + app: ['bar-app'], + privileges: null, + reserved: { + description: '', + privileges: [ + { + id: 'rp_1', + privilege: createKibanaFeaturePrivilege(['capability1', 'capability2']), + }, + { + id: 'rp_2', + privilege: createKibanaFeaturePrivilege([ + 'capability3', + 'capability4', + 'capability5', + ]), + }, + ], + }, + }), + ], + [] + ) ).toEqual({ catalogue: {}, + management: {}, newFeature: { capability1: true, capability2: true, @@ -173,53 +282,60 @@ describe('populateUICapabilities', () => { it(`supports merging features with sub privileges`, () => { expect( - uiCapabilitiesForFeatures([ - new Feature({ - id: 'newFeature', - name: 'my new feature', - navLinkId: 'newFeatureNavLink', - app: ['bar-app'], - privileges: { - all: createFeaturePrivilege(['capability1', 'capability2']), - read: createFeaturePrivilege(['capability3', 'capability4']), - }, - subFeatures: [ - { - name: 'sub-feature-1', - privilegeGroups: [ - { - groupType: 'independent', - privileges: [ - createSubFeaturePrivilege('privilege-1', ['capability5']), - createSubFeaturePrivilege('privilege-2', ['capability6']), - ], - } as SubFeaturePrivilegeGroupConfig, - { - groupType: 'mutually_exclusive', - privileges: [ - createSubFeaturePrivilege('privilege-3', ['capability7']), - createSubFeaturePrivilege('privilege-4', ['capability8']), - ], - } as SubFeaturePrivilegeGroupConfig, - ], + uiCapabilitiesForFeatures( + [ + new KibanaFeature({ + id: 'newFeature', + name: 'my new feature', + navLinkId: 'newFeatureNavLink', + app: ['bar-app'], + privileges: { + all: createKibanaFeaturePrivilege(['capability1', 'capability2']), + read: createKibanaFeaturePrivilege(['capability3', 'capability4']), }, - { - name: 'sub-feature-2', - privilegeGroups: [ - { - name: 'Group Name', - groupType: 'independent', - privileges: [ - createSubFeaturePrivilege('privilege-5', ['capability9', 'capability10']), - ], - } as SubFeaturePrivilegeGroupConfig, - ], - }, - ], - }), - ]) + subFeatures: [ + { + name: 'sub-feature-1', + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + createKibanaSubFeaturePrivilege('privilege-1', ['capability5']), + createKibanaSubFeaturePrivilege('privilege-2', ['capability6']), + ], + } as SubFeaturePrivilegeGroupConfig, + { + groupType: 'mutually_exclusive', + privileges: [ + createKibanaSubFeaturePrivilege('privilege-3', ['capability7']), + createKibanaSubFeaturePrivilege('privilege-4', ['capability8']), + ], + } as SubFeaturePrivilegeGroupConfig, + ], + }, + { + name: 'sub-feature-2', + privilegeGroups: [ + { + name: 'Group Name', + groupType: 'independent', + privileges: [ + createKibanaSubFeaturePrivilege('privilege-5', [ + 'capability9', + 'capability10', + ]), + ], + } as SubFeaturePrivilegeGroupConfig, + ], + }, + ], + }), + ], + [] + ) ).toEqual({ catalogue: {}, + management: {}, newFeature: { capability1: true, capability2: true, @@ -235,53 +351,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 KibanaFeature({ + id: 'newFeature', + name: 'my new feature', + navLinkId: 'newFeatureNavLink', + app: ['bar-app'], + privileges: { + all: createKibanaFeaturePrivilege(['capability1', 'capability2']), + read: createKibanaFeaturePrivilege(['capability3', 'capability4']), + }, + }), + new KibanaFeature({ + id: 'anotherNewFeature', + name: 'another new feature', + app: ['bar-app'], + privileges: { + all: createKibanaFeaturePrivilege(['capability1', 'capability2']), + read: createKibanaFeaturePrivilege(['capability3', 'capability4']), + }, + }), + new KibanaFeature({ + id: 'yetAnotherNewFeature', + name: 'yet another new feature', + navLinkId: 'yetAnotherNavLink', + app: ['bar-app'], + privileges: { + all: createKibanaFeaturePrivilege(['capability1', 'capability2']), + read: createKibanaFeaturePrivilege(['something1', 'something2', 'something3']), }, - ], - }), - ]) + subFeatures: [ + { + name: 'sub-feature-1', + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + createKibanaSubFeaturePrivilege('privilege-1', ['capability3']), + createKibanaSubFeaturePrivilege('privilege-2', ['capability4']), + ], + } as SubFeaturePrivilegeGroupConfig, + ], + }, + ], + }), + ], + [] + ) + ).toEqual({ + anotherNewFeature: { + capability1: true, + 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 2570d4540b6a6..d582dbfdab50c 100644 --- a/x-pack/plugins/features/server/ui_capabilities_for_features.ts +++ b/x-pack/plugins/features/server/ui_capabilities_for_features.ts @@ -5,22 +5,35 @@ */ import _ from 'lodash'; +import { RecursiveReadonly } from '@kbn/utility-types'; import { Capabilities as UICapabilities } from '../../../../src/core/server'; -import { Feature } from '../common/feature'; +import { ElasticsearchFeature, KibanaFeature } from '../common'; const ELIGIBLE_FLAT_MERGE_KEYS = ['catalogue'] as const; +const ELIGIBLE_DEEP_MERGE_KEYS = ['management'] as const; interface FeatureCapabilities { [featureId: string]: Record; } -export function uiCapabilitiesForFeatures(features: Feature[]): UICapabilities { - const featureCapabilities: FeatureCapabilities[] = features.map(getCapabilitiesFromFeature); +export function uiCapabilitiesForFeatures( + kibanaFeatures: KibanaFeature[], + elasticsearchFeatures: ElasticsearchFeature[] +): UICapabilities { + const kibanaFeatureCapabilities = kibanaFeatures.map(getCapabilitiesFromFeature); + const elasticsearchFeatureCapabilities = elasticsearchFeatures.map(getCapabilitiesFromFeature); - return buildCapabilities(...featureCapabilities); + return buildCapabilities(...kibanaFeatureCapabilities, ...elasticsearchFeatureCapabilities); } -function getCapabilitiesFromFeature(feature: Feature): FeatureCapabilities { +function getCapabilitiesFromFeature( + feature: + | Pick< + KibanaFeature, + 'id' | 'catalogue' | 'management' | 'privileges' | 'subFeatures' | 'reserved' + > + | Pick +): FeatureCapabilities { const UIFeatureCapabilities: FeatureCapabilities = { catalogue: {}, [feature.id]: {}, @@ -39,14 +52,34 @@ function getCapabilitiesFromFeature(feature: Feature): FeatureCapabilities { }; } - const featurePrivileges = Object.values(feature.privileges ?? {}); - if (feature.subFeatures) { - featurePrivileges.push( - ...feature.subFeatures.map((sf) => sf.privilegeGroups.map((pg) => pg.privileges)).flat(2) - ); + if (feature.management) { + const sectionEntries = Object.entries(feature.management); + UIFeatureCapabilities.management = sectionEntries.reduce((acc, [sectionId, sectionItems]) => { + return { + ...acc, + [sectionId]: sectionItems.reduce((acc2, item) => { + return { + ...acc2, + [item]: true, + }; + }, {}), + }; + }, {}); } - if (feature.reserved?.privileges) { - featurePrivileges.push(...feature.reserved.privileges.map((rp) => rp.privilege)); + + const featurePrivileges = Object.values(feature.privileges ?? {}) as Writable< + Array<{ ui: RecursiveReadonly }> + >; + + if (isKibanaFeature(feature)) { + if (feature.subFeatures) { + featurePrivileges.push( + ...feature.subFeatures.map((sf) => sf.privilegeGroups.map((pg) => pg.privileges)).flat(2) + ); + } + if (feature.reserved?.privileges) { + featurePrivileges.push(...feature.reserved.privileges.map((rp) => rp.privilege)); + } } featurePrivileges.forEach((privilege) => { @@ -65,6 +98,20 @@ function getCapabilitiesFromFeature(feature: Feature): FeatureCapabilities { return UIFeatureCapabilities; } +function isKibanaFeature( + feature: Partial | 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 { return allFeatureCapabilities.reduce((acc, capabilities) => { const mergableCapabilities = _.omit(capabilities, ...ELIGIBLE_FLAT_MERGE_KEYS); @@ -81,6 +128,14 @@ function buildCapabilities(...allFeatureCapabilities: FeatureCapabilities[]): UI }; }); + ELIGIBLE_DEEP_MERGE_KEYS.forEach((key) => { + mergedFeatureCapabilities[key] = _.merge( + {}, + mergedFeatureCapabilities[key], + capabilities[key] + ); + }); + return mergedFeatureCapabilities; }, {} as UICapabilities); } diff --git a/x-pack/plugins/graph/server/plugin.ts b/x-pack/plugins/graph/server/plugin.ts index b2b825fa4683b..d69c592655fb5 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 f899287642786..479d651fc6698 100644 --- a/x-pack/plugins/index_lifecycle_management/kibana.json +++ b/x-pack/plugins/index_lifecycle_management/kibana.json @@ -5,7 +5,8 @@ "ui": true, "requiredPlugins": [ "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 76d8539eb4a07..3075f9c89eb8d 100644 --- a/x-pack/plugins/index_lifecycle_management/server/plugin.ts +++ b/x-pack/plugins/index_lifecycle_management/server/plugin.ts @@ -60,7 +60,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(); @@ -78,6 +81,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 fce0414dee936..7aa91629f0a47 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 90b73b9a7585a..737f7ed1b6e4f 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -132,8 +132,8 @@ export class InfraServerPlugin { ...domainLibs, }; - plugins.features.registerFeature(METRICS_FEATURE); - plugins.features.registerFeature(LOGS_FEATURE); + plugins.features.registerKibanaFeature(METRICS_FEATURE); + plugins.features.registerKibanaFeature(LOGS_FEATURE); plugins.home.sampleData.addAppLinksToSampleDataset('logs', [ { diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts index 4a7677d69d6e7..b10f3527a0459 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 16da98796c08c..1eec8cf87bf47 100644 --- a/x-pack/plugins/ingest_pipelines/kibana.json +++ b/x-pack/plugins/ingest_pipelines/kibana.json @@ -3,7 +3,7 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["licensing", "management"], + "requiredPlugins": ["licensing", "management", "features"], "optionalPlugins": ["security", "usageCollection"], "configPath": ["xpack", "ingest_pipelines"], "requiredBundles": ["esUiShared", "kibanaReact"] diff --git a/x-pack/plugins/ingest_pipelines/server/plugin.ts b/x-pack/plugins/ingest_pipelines/server/plugin.ts index 7a78bf608b8e1..12668e7c4eadb 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 261317daa26d9..c5d9158caa569 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 3dbf99fced0b0..1f925a453898e 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 7b1887e438024..cb973fd9154ac 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 5b432b7ff0579..911e47b5130fc 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 5949d5db041f2..0d14312a154e0 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 eb79e1d2a8d8b..0347a606a8044 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 6862e7536b07f..5eb0482905e36 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -163,7 +163,7 @@ export class MapsPlugin implements Plugin { this._initHomeData(home, core.http.basePath.prepend, mapsLegacyConfig); - features.registerFeature({ + features.registerKibanaFeature({ id: APP_ID, name: i18n.translate('xpack.maps.featureRegistry.mapsFeatureName', { defaultMessage: 'Maps', diff --git a/x-pack/plugins/ml/common/types/capabilities.ts b/x-pack/plugins/ml/common/types/capabilities.ts index 8f29365fdca1b..42f056b890828 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: [], diff --git a/x-pack/plugins/ml/public/application/management/index.ts b/x-pack/plugins/ml/public/application/management/index.ts index a1b8484f200ec..72073dfd26a97 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 3e8ab99e341ad..fc0d21e9353cf 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -101,6 +101,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(); @@ -110,26 +112,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(); + } } }); diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index 39672f5b188bc..cf248fcc60896 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -67,7 +67,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 e0d018869cef1..d323a281c06ff 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 af1ccfd592b96..adb89abe20280 100644 --- a/x-pack/plugins/reporting/server/plugin.ts +++ b/x-pack/plugins/reporting/server/plugin.ts @@ -70,13 +70,14 @@ export class ReportingPlugin }); const { elasticsearch, http } = core; - const { licensing, security } = plugins; + const { features, licensing, security } = plugins; const { initializerContext: initContext, reportingCore } = this; const router = http.createRouter(); const basePath = http.basePath.get; reportingCore.pluginSetup({ + features, elasticsearch, licensing, basePath, @@ -91,6 +92,8 @@ export class ReportingPlugin (async () => { const config = await buildConfig(initContext, core, this.logger); reportingCore.setConfig(config); + // Feature registration relies on config, so it cannot be setup before here. + reportingCore.registerFeature(); this.logger.debug('Setup complete'); })().catch((e) => { this.logger.error(`Error in Reporting setup, reporting may not function properly`); 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 d1ebb4d59e631..559726e0b8a99 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 @@ -10,6 +10,7 @@ jest.mock('../browsers'); jest.mock('../lib/create_queue'); import * as Rx from 'rxjs'; +import { featuresPluginMock } from '../../../features/server/mocks'; import { ReportingConfig, ReportingCore } from '../'; import { chromium, @@ -32,6 +33,7 @@ const createMockPluginSetup = ( setupMock?: any ): ReportingInternalSetup => { return { + features: featuresPluginMock.createSetup(), elasticsearch: setupMock.elasticsearch || { legacy: { client: {} } }, basePath: setupMock.basePath || '/all-about-that-basepath', router: setupMock.router, diff --git a/x-pack/plugins/reporting/server/types.ts b/x-pack/plugins/reporting/server/types.ts index 71c8da0ee36f6..063f9e1bf9b3b 100644 --- a/x-pack/plugins/reporting/server/types.ts +++ b/x-pack/plugins/reporting/server/types.ts @@ -9,6 +9,7 @@ import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; import { DataPluginStart } from 'src/plugins/data/server/plugin'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { CancellationToken } from '../../../plugins/reporting/common'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { AuthenticatedUser, SecurityPluginSetup } from '../../security/server'; import { JobStatus } from '../common/types'; @@ -92,6 +93,7 @@ export interface ConditionalHeaders { export interface ReportingSetupDeps { licensing: LicensingPluginSetup; + features: FeaturesPluginSetup; security?: SecurityPluginSetup; usageCollection?: UsageCollectionSetup; } diff --git a/x-pack/plugins/rollup/kibana.json b/x-pack/plugins/rollup/kibana.json index e6915f65599cc..725b563c3674f 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 713852b4d7398..8b3a6355f950d 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 2a7644de764b2..290d2df050099 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 ce93fb7c98f41..cd06693a43bf9 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 199fd917da071..1fc648c12f80d 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_features.ts b/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_features.ts index 08561234fd706..2b78355787ff2 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 6821c163d817d..02a18039cee74 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,11 +15,11 @@ import { SecurityLicenseFeatures } from '../../..'; import { featuresPluginMock } from '../../../../../features/server/mocks'; export const createRawKibanaPrivileges = ( - features: Feature[], + features: KibanaFeature[], { allowSubFeaturePrivileges = true } = {} ) => { const featuresService = featuresPluginMock.createSetup(); - featuresService.getFeatures.mockReturnValue(features); + featuresService.getKibanaFeatures.mockReturnValue(features); const licensingService = { getFeatures: () => ({ allowSubFeaturePrivileges } as SecurityLicenseFeatures), @@ -33,7 +33,7 @@ export const createRawKibanaPrivileges = ( }; export const createKibanaPrivileges = ( - features: Feature[], + features: KibanaFeature[], { allowSubFeaturePrivileges = true } = {} ) => { return new KibanaPrivileges( 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 f6fe2f394fd36..bf791b37087bd 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 15888733ec424..01f8969e61f43 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 2a0922d614f1d..02d692bf9f507 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 5530d9964f8cd..bc60613345910 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 fd93aaa23194a..4739346b2cb76 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 284a85583c33c..894e06b6e5856 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/public/plugin.test.tsx b/x-pack/plugins/security/public/plugin.test.tsx index 8fe7d2805e18e..fb8034da11731 100644 --- a/x-pack/plugins/security/public/plugin.test.tsx +++ b/x-pack/plugins/security/public/plugin.test.tsx @@ -114,7 +114,8 @@ describe('Security Plugin', () => { } ); - plugin.start(coreMock.createStart({ basePath: '/some-base-path' }), { + const coreStart = coreMock.createStart({ basePath: '/some-base-path' }); + plugin.start(coreStart, { data: {} as DataPublicPluginStart, features: {} as FeaturesPluginStart, management: managementStartMock, diff --git a/x-pack/plugins/security/public/plugin.tsx b/x-pack/plugins/security/public/plugin.tsx index e3905dc2acf45..f5770ae2bc35c 100644 --- a/x-pack/plugins/security/public/plugin.tsx +++ b/x-pack/plugins/security/public/plugin.tsx @@ -141,7 +141,7 @@ export class SecurityPlugin this.sessionTimeout.start(); this.navControlService.start({ core }); if (management) { - this.managementService.start(); + this.managementService.start({ capabilities: core.application.capabilities }); } } 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 75aa27c3c88c6..d4ec9a0e0db51 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 0ffd3ba7ba823..9129330ec947a 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 1dc072ab2e6e9..f40d502a9cd7c 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 1036997ca821d..4170fd2cdb38a 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 2fdc2d169e972..c00127f7d1229 100644 --- a/x-pack/plugins/security/server/authorization/authorization_service.test.ts +++ b/x-pack/plugins/security/server/authorization/authorization_service.test.ts @@ -74,6 +74,7 @@ it(`#setup returns exposed services`, () => { packageVersion: 'some-version', features: mockFeaturesSetup, getSpacesService: mockGetSpacesService, + getCurrentUser: jest.fn(), }); expect(authz.actions.version).toBe('version:some-version'); @@ -133,10 +134,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, @@ -203,10 +205,12 @@ it('#stop unsubscribes from license and ES updates.', async () => { 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, diff --git a/x-pack/plugins/security/server/authorization/authorization_service.ts b/x-pack/plugins/security/server/authorization/authorization_service.ts index 2dead301b298a..fd3a60fb4d900 100644 --- a/x-pack/plugins/security/server/authorization/authorization_service.ts +++ b/x-pack/plugins/security/server/authorization/authorization_service.ts @@ -22,7 +22,7 @@ import { import { SpacesService } from '../plugin'; import { Actions } from './actions'; -import { CheckPrivilegesWithRequest, checkPrivilegesWithRequestFactory } from './check_privileges'; +import { checkPrivilegesWithRequestFactory } from './check_privileges'; import { CheckPrivilegesDynamicallyWithRequest, checkPrivilegesDynamicallyWithRequestFactory, @@ -41,7 +41,9 @@ 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 { OnlineStatusRetryScheduler } from '../elasticsearch'; +import { AuthenticatedUser } from '..'; export { Actions } from './actions'; export { CheckSavedObjectsPrivileges } from './check_saved_objects_privileges'; @@ -57,6 +59,7 @@ interface AuthorizationServiceSetupParams { features: FeaturesPluginSetup; kibanaIndexName: string; getSpacesService(): SpacesService | undefined; + getCurrentUser(request: KibanaRequest): AuthenticatedUser | null; } interface AuthorizationServiceStartParams { @@ -92,6 +95,7 @@ export class AuthorizationService { features, kibanaIndexName, getSpacesService, + getCurrentUser, }: AuthorizationServiceSetupParams): AuthorizationServiceSetup { this.logger = loggers.get('authorization'); this.applicationName = `${APPLICATION_PREFIX}${kibanaIndexName}`; @@ -132,9 +136,11 @@ export class AuthorizationService { const disableUICapabilities = disableUICapabilitiesFactory( request, - features.getFeatures(), + features.getKibanaFeatures(), + features.getElasticsearchFeatures(), this.logger, - authz + authz, + getCurrentUser(request) ); if (!request.auth.isAuthenticated) { @@ -152,7 +158,7 @@ export class AuthorizationService { } start({ clusterClient, features, online$ }: 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 b380f45a12d81..4151ff645005d 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( @@ -50,25 +54,39 @@ describe('#atSpace', () => { let actualResult; let errorThrown = null; try { - actualResult = await checkPrivileges.atSpace(options.spaceId, options.privilegeOrPrivileges); + actualResult = await checkPrivileges.atSpace(options.spaceId, { + 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: { + 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], }, ], }, @@ -83,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', @@ -100,13 +118,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", } `); @@ -115,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', @@ -132,13 +156,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", } `); @@ -147,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', @@ -169,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`, ], @@ -191,18 +221,24 @@ describe('#atSpace', () => { expect(result).toMatchInlineSnapshot(` Object { "hasAllRequested": true, - "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", - }, - ], + "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", } `); @@ -211,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`, ], @@ -233,18 +269,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", } `); @@ -254,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', @@ -278,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', @@ -297,12 +339,551 @@ 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({ + 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( @@ -319,28 +900,39 @@ describe('#atSpaces', () => { let actualResult; let errorThrown = null; try { - actualResult = await checkPrivileges.atSpaces( - options.spaceIds, - options.privilegeOrPrivileges - ); + actualResult = await checkPrivileges.atSpaces(options.spaceIds, { + 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: { + 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], }, ], }, @@ -355,7 +947,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', @@ -376,18 +968,24 @@ describe('#atSpaces', () => { expect(result).toMatchInlineSnapshot(` Object { "hasAllRequested": true, - "privileges": Array [ - Object { - "authorized": true, - "privilege": "mock-action:login", - "resource": "space_1", - }, - Object { - "authorized": true, - "privilege": "mock-action:login", - "resource": "space_2", + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object {}, }, - ], + "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", } `); @@ -396,7 +994,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', @@ -417,18 +1015,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", } `); @@ -437,7 +1041,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', @@ -463,7 +1067,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`, ], @@ -492,7 +1096,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`, ], @@ -520,28 +1124,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", - }, - Object { - "authorized": true, - "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": 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", } `); @@ -550,7 +1160,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`, ], @@ -578,28 +1188,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", } `); @@ -608,7 +1224,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`, ], @@ -636,28 +1252,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", - }, - 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": 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", } `); @@ -666,7 +1288,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`, ], @@ -694,28 +1316,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": true, - "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": 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", } `); @@ -725,7 +1353,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', @@ -755,7 +1383,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', @@ -783,7 +1411,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', @@ -816,7 +1444,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', @@ -836,13 +1464,632 @@ describe('#atSpaces', () => { ); }); }); -}); -describe('#globally', () => { - const checkPrivilegesGloballyTest = async (options: { - privilegeOrPrivileges: string | string[]; - esHasPrivilegesResponse: HasPrivilegesResponse; - }) => { + 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', + 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: 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", + }, + 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 only es 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`]: 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: 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", + }, + 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", + } + `); + }); + + 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: { + kibanaPrivileges?: string | string[]; + elasticsearchPrivileges?: { + cluster: string[]; + index: Record; + }; + esHasPrivilegesResponse: HasPrivilegesResponse; + }) => { const { mockClusterClient, mockScopedClusterClient } = createMockClusterClient( options.esHasPrivilegesResponse ); @@ -857,25 +2104,39 @@ describe('#globally', () => { let actualResult; let errorThrown = null; try { - actualResult = await checkPrivileges.globally(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: { + 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], }, ], }, @@ -889,7 +2150,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', @@ -906,13 +2167,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", } `); @@ -920,7 +2187,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', @@ -937,13 +2204,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", } `); @@ -951,7 +2224,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', @@ -972,7 +2245,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`, ], @@ -996,7 +2269,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`, ], @@ -1018,18 +2291,24 @@ describe('#globally', () => { expect(result).toMatchInlineSnapshot(` Object { "hasAllRequested": true, - "privileges": Array [ - Object { - "authorized": true, - "privilege": "saved_object:foo-type/get", - "resource": undefined, - }, - Object { - "authorized": true, - "privilege": "saved_object:bar-type/get", - "resource": undefined, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "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", } `); @@ -1037,7 +2316,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`, ], @@ -1059,18 +2338,24 @@ describe('#globally', () => { expect(result).toMatchInlineSnapshot(` Object { "hasAllRequested": false, - "privileges": Array [ - Object { - "authorized": false, - "privilege": "saved_object:foo-type/get", - "resource": undefined, - }, - Object { - "authorized": true, - "privilege": "saved_object:bar-type/get", - "resource": undefined, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object {}, }, - ], + "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", } `); @@ -1079,7 +2364,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', @@ -1102,7 +2387,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', @@ -1121,4 +2406,531 @@ 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({ + 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", + } + `); + }); + }); }); diff --git a/x-pack/plugins/security/server/authorization/check_privileges.ts b/x-pack/plugins/security/server/authorization/check_privileges.ts index 3129777a7881f..27e1802b4e5c2 100644 --- a/x-pack/plugins/security/server/authorization/check_privileges.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges.ts @@ -8,7 +8,13 @@ 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, + CheckPrivileges, + CheckPrivilegesResponse, +} from './types'; import { validateEsPrivilegeResponse } from './validate_es_response'; interface CheckPrivilegesActions { @@ -16,33 +22,6 @@ interface CheckPrivilegesActions { version: string; } -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; - }>; -} - -export type CheckPrivilegesWithRequest = (request: KibanaRequest) => CheckPrivileges; - -export interface CheckPrivileges { - atSpace( - spaceId: string, - privilegeOrPrivileges: string | string[] - ): Promise; - atSpaces( - spaceIds: string[], - privilegeOrPrivileges: string | string[] - ): Promise; - globally(privilegeOrPrivileges: string | string[]): Promise; -} - export function checkPrivilegesWithRequestFactory( actions: CheckPrivilegesActions, clusterClient: ILegacyClusterClient, @@ -59,17 +38,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 +73,27 @@ 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< + CheckPrivilegesResponse['privileges']['elasticsearch']['index'] + >((acc, [index, indexResponse]) => { + return { + ...acc, + [index]: Object.entries(indexResponse).map(([privilege, authorized]) => ({ + privilege, + authorized, + })), + }; + }, {}); + if (hasIncompatibleVersion(applicationPrivilegesResponse)) { throw new Error( 'Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.' @@ -93,7 +102,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 +120,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 2206748597635..093b308f59391 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 6014bad739e77..cd5961e5940ed 100644 --- a/x-pack/plugins/security/server/authorization/check_privileges_dynamically.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges_dynamically.ts @@ -6,10 +6,11 @@ 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 = ( - 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 b393c4a9e1a04..f287cc04280ac 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; @@ -69,7 +69,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 }); }); test(`de-duplicates namespaces`, async () => { @@ -93,7 +93,7 @@ describe('#checkSavedObjectsPrivileges', () => { mockSpacesService!.namespaceToSpaceId(undefined), // deduplicated with 'default' mockSpacesService!.namespaceToSpaceId(namespace1), // deduplicated with namespace1 ]; - expect(mockCheckPrivileges.atSpaces).toHaveBeenCalledWith(spaceIds, actions); + expect(mockCheckPrivileges.atSpaces).toHaveBeenCalledWith(spaceIds, { kibana: actions }); }); }); @@ -112,7 +112,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 () => { @@ -127,7 +127,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 6d2f724dae948..7c0ca7dcaa392 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 @@ -35,7 +35,7 @@ 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) { @@ -45,11 +45,11 @@ export const checkSavedObjectsPrivilegesWithRequestFactory = ( 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 f9405214aac5a..98faae6edab2c 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,17 @@ 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 { KibanaFeature, ElasticsearchFeature } from '../../../features/server'; +import { AuthenticatedUser } from '..'; +import { CheckPrivilegesResponse } from './types'; type MockAuthzOptions = | { rejectCheckPrivileges: any } - | { resolveCheckPrivileges: { privileges: Array<{ privilege: string; authorized: boolean }> } }; + | { + resolveCheckPrivileges: { + privileges: CheckPrivilegesResponse['privileges']; + }; + }; const actions = new Actions('1.0.0-zeta1'); const mockRequest = httpServerMock.createKibanaRequest(); @@ -31,14 +37,34 @@ const createMockAuthz = (options: MockAuthzOptions) => { throw options.rejectCheckPrivileges; } - const expected = options.resolveCheckPrivileges.privileges.map((x) => x.privilege); - expect(checkActions).toEqual(expected); + 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; }); }); + 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 () => { @@ -50,16 +76,28 @@ 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, }), ], + [ + new ElasticsearchFeature({ + id: 'esFeature', + privileges: [ + { + requiredClusterPrivileges: [], + ui: [], + }, + ], + }), + ], mockLoggers.get(), - mockAuthz + mockAuthz, + createMockUser() ); const result = await usingPrivileges( @@ -126,16 +164,28 @@ describe('usingPrivileges', () => { const { usingPrivileges } = disableUICapabilitiesFactory( mockRequest, [ - new Feature({ + new KibanaFeature({ id: 'fooFeature', - name: 'Foo Feature', + name: 'Foo KibanaFeature', app: ['foo'], navLinkId: 'foo', privileges: null, }), ], + [ + new ElasticsearchFeature({ + id: 'esFeature', + privileges: [ + { + requiredClusterPrivileges: [], + ui: [], + }, + ], + }), + ], mockLoggers.get(), - mockAuthz + mockAuthz, + createMockUser() ); const result = await usingPrivileges( @@ -199,8 +249,10 @@ describe('usingPrivileges', () => { const { usingPrivileges } = disableUICapabilitiesFactory( mockRequest, [], + [], mockLoggers.get(), - mockAuthz + mockAuthz, + createMockUser() ); await expect( @@ -234,40 +286,91 @@ 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('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 }, + { privilege: actions.ui.get('barFeature', 'bar'), authorized: false }, + ], + elasticsearch: { + cluster: [ + { privilege: 'manage', authorized: false }, + { privilege: 'monitor', authorized: true }, + { privilege: 'manage_security', authorized: true }, + ], + index: {}, + }, + }, }, }); 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, }), ], + [ + new ElasticsearchFeature({ + id: 'esFeature', + privileges: [ + { + requiredClusterPrivileges: ['manage'], + ui: ['es_manage'], + }, + { + requiredClusterPrivileges: ['monitor'], + ui: ['es_monitor'], + }, + ], + }), + new ElasticsearchFeature({ + id: 'esSecurityFeature', + privileges: [ + { + requiredClusterPrivileges: ['manage_security'], + ui: ['es_manage_sec'], + }, + ], + }), + new ElasticsearchFeature({ + id: 'esManagementFeature', + management: { + kibana: ['esManagement'], + }, + privileges: [ + { + requiredClusterPrivileges: ['manage_security'], + ui: [], + }, + ], + }), + ], loggingSystemMock.create().get(), - mockAuthz + mockAuthz, + createMockUser() ); const result = await usingPrivileges( @@ -281,6 +384,7 @@ describe('usingPrivileges', () => { kibana: { indices: true, settings: false, + esManagement: true, }, }, catalogue: {}, @@ -292,6 +396,14 @@ describe('usingPrivileges', () => { foo: true, bar: true, }, + esFeature: { + es_manage: true, + es_monitor: true, + }, + esSecurityFeature: { + es_manage_sec: true, + }, + esManagementFeature: {}, }) ); @@ -305,6 +417,7 @@ describe('usingPrivileges', () => { kibana: { indices: true, settings: false, + esManagement: true, }, }, catalogue: {}, @@ -316,44 +429,70 @@ describe('usingPrivileges', () => { foo: true, bar: false, }, + esFeature: { + es_manage: false, + es_monitor: true, + }, + esSecurityFeature: { + es_manage_sec: true, + }, + esManagementFeature: {}, }); }); 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 }, + ], + elasticsearch: { + cluster: [], + index: {}, + }, + }, }, }); 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, }), ], + [ + new ElasticsearchFeature({ + id: 'esFeature', + privileges: [ + { + requiredClusterPrivileges: [], + ui: [], + }, + ], + }), + ], loggingSystemMock.create().get(), - mockAuthz + mockAuthz, + createMockUser() ); const result = await usingPrivileges( @@ -409,16 +548,28 @@ describe('all', () => { const { all } = disableUICapabilitiesFactory( mockRequest, [ - new Feature({ + new KibanaFeature({ id: 'fooFeature', - name: 'Foo Feature', + name: 'Foo KibanaFeature', app: ['foo'], navLinkId: 'foo', privileges: null, }), ], + [ + new ElasticsearchFeature({ + id: 'esFeature', + privileges: [ + { + requiredClusterPrivileges: [], + ui: ['bar'], + }, + ], + }), + ], loggingSystemMock.create().get(), - mockAuthz + mockAuthz, + createMockUser() ); const result = all( @@ -441,6 +592,9 @@ describe('all', () => { foo: true, bar: true, }, + esFeature: { + bar: true, + }, }) ); expect(result).toEqual({ @@ -462,6 +616,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 41d596d570fb9..89cc9065655cd 100644 --- a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts +++ b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts @@ -5,18 +5,26 @@ */ import { flatten, isObject, mapValues } from 'lodash'; +import { RecursiveReadonly, RecursiveReadonlyArray } from '@kbn/utility-types'; import type { Capabilities as UICapabilities } from '../../../../../src/core/types'; import { KibanaRequest, Logger } from '../../../../../src/core/server'; -import { Feature } from '../../../features/server'; +import { + 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 + 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,39 @@ export function disableUICapabilitiesFactory( .flatMap((feature) => feature.app) .filter((navLinkId) => navLinkId != null); + const elasticsearchFeatureMap = elasticsearchFeatures.reduce< + Record> + >((acc, esFeature) => { + return { + ...acc, + [esFeature.id]: esFeature.privileges, + }; + }, {}); + + 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], + }; + }, {}), + }; + }, {}); + const shouldDisableFeatureUICapability = ( featureId: keyof UICapabilities, uiCapability: string @@ -59,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)]; } @@ -85,7 +132,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 +163,65 @@ export function disableUICapabilitiesFactory( } const action = authz.actions.ui.get(featureId, ...uiCapabilityParts); - return checkPrivilegesResponse.privileges.some( - (x) => x.privilege === action && x.authorized === true - ); + + const isElasticsearchFeature = elasticsearchFeatureMap.hasOwnProperty(featureId); + const isCatalogueFeature = featureId === 'catalogue'; + const isManagementFeature = featureId === 'management'; + + if (!isElasticsearchFeature) { + const hasRequiredKibanaPrivileges = checkPrivilegesResponse.privileges.kibana.some( + (x) => x.privilege === action && x.authorized === true + ); + + // 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) { + const [catalogueEntry] = uiCapabilityParts; + const featureGrantsCatalogueEntry = (esFeature.catalogue ?? []).includes(catalogueEntry); + return ( + featureGrantsCatalogueEntry && + hasAnyRequiredElasticsearchPrivilegesForFeature( + esFeature, + checkPrivilegesResponse, + user + ) + ); + } else if (isManagementFeature) { + const [managementSectionId, managementEntryId] = uiCapabilityParts; + const featureGrantsManagementEntry = + (esFeature.management ?? {}).hasOwnProperty(managementSectionId) && + esFeature.management![managementSectionId].includes(managementEntryId); + + return ( + featureGrantsManagementEntry && + hasAnyRequiredElasticsearchPrivilegesForFeature( + esFeature, + checkPrivilegesResponse, + user + ) + ); + } 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) => { @@ -151,3 +260,56 @@ export function disableUICapabilitiesFactory( usingPrivileges, }; } + +function hasRequiredElasticsearchPrivilegesForCapability( + esFeature: ElasticsearchFeature, + uiCapability: string, + checkPrivilegesResponse: CheckPrivilegesResponse, + user: AuthenticatedUser | null +) { + return esFeature.privileges.some((privilege) => { + 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]) => { + 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 62b254d132d9e..6cb78a3001a9b 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/feature_privilege_builder/alerting.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts index 5e9c1818cad2b..dc261e2eec982 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: [], @@ -97,7 +97,7 @@ describe(`feature_privilege_builder`, () => { ui: [], }; - const feature = new Feature({ + const feature = new KibanaFeature({ id: 'my-feature', name: 'my-feature', app: [], @@ -144,7 +144,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 eb278a5755204..fa9cadf2aea62 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', 'getAlertInstanceSummary', '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 6b7d94bb0127e..0e63cdceffc57 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,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature, 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: Feature): 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 213aa83f2d26e..bf6b0e60f1045 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,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature, 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: Feature): 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 f1ea7091b9481..97a3c9c1e336e 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,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature, 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: Feature): 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/feature_privilege_builder.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/feature_privilege_builder.ts index 172ab24eb7e51..0eded66d65b06 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 76b664cbbe2a7..998fbc5cc5e24 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 be784949dc2fa..67b8cdb7616d4 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,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature, 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: Feature): 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 a6e5a01c7dba8..7400675ed17f3 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,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature, 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: Feature): 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 2c325fc8c6cb7..0dd89f2c5f3c1 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 { FeatureKibanaPrivileges } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; const readOperations: string[] = ['bulk_get', 'get', 'find']; @@ -13,7 +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: Feature): 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_builder/ui.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/ui.ts index 31bc351206e54..dd167a291f11d 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 bb1f0c33fdee9..033040fd2f14b 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 17c9464b14756..dba33f7a4f360 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 b288262be25c6..d54b6d458d913 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,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SubFeaturePrivilegeConfig } from '../../../../../features/common'; -import { Feature } from '../../../../../features/server'; +import { KibanaFeature, SubFeaturePrivilegeConfig } from '../../../../../features/common'; 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 89ac73c220756..dd8ac44386dbd 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'], @@ -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 }), @@ -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: { @@ -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, @@ -159,23 +159,23 @@ 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, }), ]; - 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'); @@ -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: [], @@ -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')] : []), @@ -319,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: [], @@ -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`, [ @@ -401,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: [], @@ -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')] : []), @@ -457,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', @@ -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')] : []), @@ -522,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: [], @@ -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')] : []), @@ -591,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'], @@ -621,23 +625,23 @@ 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]); }); 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, @@ -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', [ @@ -698,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: { @@ -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'); @@ -739,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: { @@ -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'), ]); @@ -865,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: { @@ -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'), @@ -1063,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, @@ -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]); @@ -1203,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: { @@ -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'), @@ -1365,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, @@ -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]); @@ -1493,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: { @@ -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 5d8ef3f376cac..24b46222e7f35 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'; @@ -28,7 +31,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 +103,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, ], @@ -109,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 75188d1191b1a..bedf46862e4f5 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; @@ -16,4 +18,58 @@ export interface HasPrivilegesResponse { application: { [applicationName: string]: HasPrivilegesResponseApplication; }; + cluster?: { + [privilegeName: string]: boolean; + }; + index?: { + [indexName: string]: { + [privilegeName: string]: boolean; + }; + }; +} + +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?: { + cluster: string[]; + index: Record; + }; } 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 cd2c7faa263c9..8e6d72670c8d9 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 79e5348b4ac64..eeb9c4cb74314 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 26af0dadfb288..d91a4d4151316 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 0915308fc0f89..23e5c28a4af1b 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/features/index.ts b/x-pack/plugins/security/server/features/index.ts new file mode 100644 index 0000000000000..3fe097c2bec12 --- /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 0000000000000..d80314c077aa2 --- /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 9825e77b164c8..9088d4f08d0ef 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -11,6 +11,7 @@ import { ConfigSchema } from './config'; import { Plugin, PluginSetupDependencies } from './plugin'; import { coreMock, elasticsearchServiceMock } from '../../../../src/core/server/mocks'; +import { featuresPluginMock } from '../../features/server/mocks'; import { taskManagerMock } from '../../task_manager/server/mocks'; describe('Security Plugin', () => { @@ -44,6 +45,7 @@ describe('Security Plugin', () => { mockDependencies = ({ licensing: { license$: of({}), featureUsage: { register: jest.fn() } }, + features: featuresPluginMock.createSetup(), taskManager: taskManagerMock.createSetup(), } as unknown) as PluginSetupDependencies; }); diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 1eb406dd2061b..dc9139473004b 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,6 +32,7 @@ import { SecurityLicenseService, SecurityLicense } from '../common/licensing'; import { setupSavedObjects } from './saved_objects'; import { AuditService, SecurityAuditLogger, AuditServiceSetup } from './audit'; import { SecurityFeatureUsageService, SecurityFeatureUsageServiceStart } from './feature_usage'; +import { securityFeatures } from './features'; import { ElasticsearchService } from './elasticsearch'; import { SessionManagementService } from './session_management'; import { registerSecurityUsageCollector } from './usage_collector'; @@ -40,6 +42,11 @@ export type SpacesService = Pick< 'getSpaceId' | 'namespaceToSpaceId' >; +export type FeaturesService = Pick< + FeaturesSetupContract, + 'getKibanaFeatures' | 'getElasticsearchFeatures' +>; + /** * Describes public Security plugin contract returned at the `setup` stage. */ @@ -146,6 +153,10 @@ export class Plugin { license$: licensing.license$, }); + securityFeatures.forEach((securityFeature) => + features.registerElasticsearchFeature(securityFeature) + ); + const { clusterClient } = this.elasticsearchService.setup({ elasticsearch: core.elasticsearch, license, @@ -188,6 +199,7 @@ export class Plugin { packageVersion: this.initializerContext.env.packageInfo.version, getSpacesService: this.getSpacesService, features, + getCurrentUser: authc.getCurrentUser, }); setupSavedObjects({ @@ -211,7 +223,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/routes/authorization/roles/put.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts index 8f115f11329d3..6e9b88f30479f 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 d83cf92bcaa0d..cdedc9ac8a5eb 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 a3f046ae4f9e6..7880e95240ff0 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 { HttpResources, IBasePath, @@ -42,7 +42,7 @@ export interface RouteDefinitionParams { authz: AuthorizationServiceSetup; session: PublicMethodsOf; license: SecurityLicense; - getFeatures: () => Promise; + getFeatures: () => Promise; getFeatureUsageService: () => SecurityFeatureUsageServiceStart; } 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 2cf072453a3c7..7ada34ff5ccac 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 @@ -222,15 +222,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(), + }, }; } @@ -246,15 +248,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 bfa08a0116644..16e52c69f274f 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 @@ -19,7 +19,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 { @@ -242,12 +242,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, @@ -275,7 +275,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 })); } @@ -288,7 +288,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 1f4790a8981c9..d203c6dcc48c4 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -171,7 +171,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 8cfcaec1a2cd1..eb51f086deacc 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/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 ad5ebe157cfb8..0eed6793ddbe0 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 373e0b42aebe5..689bb610d5f38 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 df07d128e497b..9265ca46e3a3a 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 b573848f0c84a..f580720848875 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 e725310c41817..5338710b7c8a4 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 20d419e5c90e4..212ffe96cdbf6 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 273ea7e60bc5e..c6f7031976a9b 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 36efc68749783..b40f34273d99f 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 607570eedc787..fe4bdc865094f 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_provider.test.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_provider.test.ts index 8678bdceb70f9..b0b89afa79d5d 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 5976aabfa66e8..1aaf2ad1df925 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 c9ea1b44e723d..bf0b51b7e2503 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({ @@ -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 e8d964b22010c..8b0b955c40d92 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'; @@ -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); @@ -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 dabdcf553edb4..fe1acd93570f6 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'; @@ -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', @@ -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/lib/request_interceptors/on_post_auth_interceptor.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts index 3d6084d37a384..e4ca0f8072f96 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 90ce2b01bfd20..1090b029069d2 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 @@ -260,10 +260,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({ @@ -298,7 +300,7 @@ describe('#getAll', () => { expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); expect(mockCheckPrivilegesAtSpaces).toHaveBeenCalledWith( savedObjects.map((savedObject) => savedObject.id), - [privilege] + { kibana: [privilege] } ); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledWith( username, @@ -318,10 +320,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({ @@ -357,7 +361,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( @@ -451,9 +455,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); @@ -486,9 +490,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); @@ -603,7 +607,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, ]); @@ -641,7 +647,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', [ @@ -886,9 +894,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); }); @@ -939,9 +947,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'); }); @@ -989,9 +997,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'); }); @@ -1128,9 +1136,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); }); @@ -1167,9 +1175,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); @@ -1353,9 +1361,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); }); @@ -1389,9 +1397,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'); @@ -1429,9 +1437,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 b1d6e3200ab3a..acb00a87bf7d9 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 @@ -50,9 +50,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; } @@ -87,9 +87,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 ${ @@ -234,7 +236,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); @@ -252,7 +254,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 a82f2370cc124..b650a114ed978 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 57ec688ab70e8..fddd7f92b7f27 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; @@ -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 3ea4693d9e9d7..36d46c3d01baf 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 d7e7a7fabba4f..2efe0bb25bc68 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 79e9be239c798..988750f70efe0 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 5fcc23a6d9f48..c3d7434f14f45 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 5a07f68586bd7..0a3da4c055b1c 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", "apmOss"], + "requiredPlugins": ["management", "licensing", "apmOss", "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 46d553a7ea2bb..3608c8e9a0c03 100644 --- a/x-pack/plugins/upgrade_assistant/server/plugin.ts +++ b/x-pack/plugins/upgrade_assistant/server/plugin.ts @@ -18,6 +18,7 @@ import { } from '../../../../src/core/server'; import { CloudSetup } from '../../cloud/server'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { APMOSSPluginSetup } from '../../../../src/plugins/apm_oss/server'; @@ -39,6 +40,7 @@ interface PluginsSetup { usageCollection: UsageCollectionSetup; licensing: LicensingPluginSetup; apmOss: APMOSSPluginSetup; + features: FeaturesPluginSetup; cloud?: CloudSetup; } @@ -68,7 +70,7 @@ export class UpgradeAssistantServerPlugin implements Plugin { setup( { http, getStartServices, capabilities, savedObjects }: CoreSetup, - { usageCollection, cloud, licensing, apmOss: apmOSS }: PluginsSetup + { usageCollection, cloud, features, licensing, apmOss: apmOSS }: PluginsSetup ) { this.licensing = licensing; this.apmOSS = apmOSS; @@ -76,6 +78,19 @@ export class UpgradeAssistantServerPlugin implements Plugin { 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 76359a3b60a6a..5c3211eff3b4e 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 ba6a9bfa5e194..695686715cb6a 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 70c4f980580e8..9ff46283a72a6 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 dd941054114a8..167dcb3ab64c3 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 88f0f02794c9b..68ff3dad9ae86 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 @@ -72,7 +72,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 e297733fb47eb..e1ef1255c6e13 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/api_integration/apis/features/features/features.ts b/x-pack/test/api_integration/apis/features/features/features.ts index 9c44bfeb810fa..37809a3b7aeb7 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', 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 5b0d28bf09508..ac4a1298e28b9 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 0000000000000..d3d2846082854 --- /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 0000000000000..169b5c7fb0a73 --- /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 0c4097a1d5c4e..39d8449218ffa 100644 --- a/x-pack/test/functional/apps/api_keys/home_page.ts +++ b/x-pack/test/functional/apps/api_keys/home_page.ts @@ -24,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 703aae04140f2..7a17430dc8f6c 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 e9fa4ccf8e48b..5a8fb207d5062 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 0000000000000..6b4b9c61151ba --- /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 0000000000000..e7be2cb48ce3e --- /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 5db6103307af9..0e54c0d1c0d15 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 505e35907bd80..46dc0316a5d6b 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 803ff6399a035..807ba6ded88a2 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 8be4349762808..d94451d023ec0 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 9121028c14404..3b4a1fbdbe0d8 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 0000000000000..4cb0d3077aaa4 --- /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 0000000000000..0bb6476f36687 --- /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 f535710814ab2..157fb62b7a84d 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 0000000000000..85398a73eceff --- /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 0000000000000..2019751d9101c --- /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 a9bb44d002334..97b23cbf82c31 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 cedd96f147c2b..4873a11d75eaa 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 6daa530261456..31cd39aca1f9e 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 0000000000000..fbaf7648646b8 --- /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 0000000000000..bf703a8f60dc2 --- /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 8d2b9ee1dcb69..2a4781c5e216d 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 0000000000000..5c7c04d4ccde1 --- /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 0000000000000..59fc287c6cf2e --- /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 6d01b1bb098f0..0b090223c18fe 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 0000000000000..d3cc7fae94d98 --- /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 0000000000000..8e2609e3b7e85 --- /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 515674577fb52..3258d948cedfc 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 0000000000000..8b8226da7dc3c --- /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 0000000000000..cf1a83ca49686 --- /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 19c68a2da9d9b..7a461c9963be9 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 ae9b0f095fc44..e32f14200ad80 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/ml/permissions/no_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts index 6fd78458a6ce5..ab67e567e67ac 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 a358e57f792c7..cb964995511ef 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/apps/remote_clusters/feature_controls/index.ts b/x-pack/test/functional/apps/remote_clusters/feature_controls/index.ts new file mode 100644 index 0000000000000..bfcaef629dc42 --- /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 0000000000000..b1edc74607161 --- /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 d91d413e2b7af..0839c2f22af47 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 28b8153ea4c2b..02b2ec4d4c681 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 2054a7b0b0038..c547657bf880a 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 a3ade23f5c178..d705140954de4 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 0000000000000..794e6f516d982 --- /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 0000000000000..5d7d8ec3c307e --- /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 a01f3fa5d53a5..2837ddb7333e6 100644 --- a/x-pack/test/functional/apps/transform/index.ts +++ b/x-pack/test/functional/apps/transform/index.ts @@ -37,5 +37,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { 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 0000000000000..f1c73e39fbc3e --- /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 0000000000000..1f541dbe03537 --- /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 0e6c52f0812ee..131cb6a249c78 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 49435df4f1c2a..ca84a8e561164 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 adfe780a937ea..a4d0bbc3e1e9d 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -266,6 +266,16 @@ export default async function ({ readConfigFile }) { }, ], }, + global_dashboard_all: { + kibana: [ + { + feature: { + dashboard: ['all'], + }, + spaces: ['*'], + }, + ], + }, global_maps_all: { kibana: [ { @@ -324,6 +334,20 @@ export default async function ({ readConfigFile }) { ], }, + global_upgrade_assistant_role: { + elasticsearch: { + cluster: ['manage'], + }, + kibana: [ + { + feature: { + discover: ['read'], + }, + spaces: ['*'], + }, + ], + }, + //Kibana feature privilege isn't specific to advancedSetting. It can be anything. https://github.com/elastic/kibana/issues/35965 test_api_keys: { elasticsearch: { @@ -338,6 +362,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', 'manage_index_templates'], + 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/services/ml/navigation.ts b/x-pack/test/functional/services/ml/navigation.ts index 9b53e5ce2f7e7..e564c03f62d58 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'); }); }, 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 dd81c860e9fa8..5c42c1978a0b5 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,13 +37,14 @@ 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 !== 'ml_file_data_visualizer' && - catalogueId !== 'monitoring' + catalogueId !== 'monitoring' && + !esFeatureExceptions.includes(catalogueId) ); expect(uiCapabilities.value!.catalogue).to.eql(expected); break; @@ -52,7 +55,8 @@ 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 + // 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', 'ml_file_data_visualizer', @@ -60,6 +64,8 @@ export default function catalogueTests({ getService }: FtrProviderContext) { 'enterpriseSearch', 'appSearch', 'workplaceSearch', + 'spaces', + ...esFeatureExceptions, ]; const expected = mapValues( uiCapabilities.value!.catalogue, @@ -68,10 +74,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': @@ -88,7 +120,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 7852167fcc1cb..1f19228b2d958 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,13 +37,14 @@ 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' && catalogueId !== 'ml_file_data_visualizer' && - catalogueId !== 'monitoring' + !esFeatureExceptions.includes(catalogueId) ); expect(uiCapabilities.value!.catalogue).to.eql(expected); break; @@ -58,6 +61,7 @@ export default function catalogueTests({ getService }: FtrProviderContext) { 'enterpriseSearch', 'appSearch', 'workplaceSearch', + ...esFeatureExceptions, ]; const expected = mapValues( uiCapabilities.value!.catalogue, 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 2ef5108403427..baae3286ddb5d 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; }