From fd14b087bc1d5fbe297e09f4b3aa021dab6a28b2 Mon Sep 17 00:00:00 2001 From: Brandon Kobel Date: Mon, 3 Dec 2018 15:21:15 -0800 Subject: [PATCH] GAP: Security disables UI capabilities (#25809) * Restructure user profile for granular app privs (#23750) merging to feature branch for further development * Fixing saved object capability checking * Beginning to restructure actions to be used for all action building * Using actions to build ui capabilities * dropping /read from client-side userprovide ui capabilities * Adding some actions * Using different syntax which will hopefully help with allowing apps to specify the privileges themselves * Exposing all saved object operations in the capabilities * Using actions in security's onPostAuth * Only loading the default index pattern when it's required * Only using the navlinks for the "ui capabilities" * Redirecting from the discover application if the user can't access kibana:discover * Redirecting from dashboard if they're hidden * Features register their privileges now * Introducing a FeaturesPrivilegesBuilder * REmoving app from the feature definition * Adding navlink specific ations * Beginning to break out the serializer * Exposing privileges from the authorization service * Restructuring the privilege/resource serialization to support features * Adding actions unit tests * Adding features privileges builders tests * Adding PrivilegeSerializer tests * Renaming missed usages * Adding tests for the privileges serializer * Adding privileges tests * Adding registerPrivilegesWithCluster tests * Better tests * Restructure user profile for granular app privs (#23750) merging to feature branch for further development * Fixing authorization service tests * Adding ResourceSerializer tests * Fixing Privileges tests * Some PUT role tests * Fixing read ui/api actions * Introducing uiCapabilities, removing config providers & user profile (#25387) ## Summary Introduces the concept of "UI Capabilities", which allows Kibana applications to declare capabilities via the `uiCapabilities` injected var, and then use them client-side via the `ui/capabilities` module to inform their rendering decisions. * Exposing features from xpackMainPlugin * Adding navlink:* to the "reserved privileges" * navlink -> navLink | nav_linknavlink -> navLink | nav_linknavlink -> navLink | nav_linknavlink -> navLink | nav_linknavlink -> navLink | nav_linknavlink -> navLink | nav_linknavlink -> navLink | nav_linknavlink -> navLink | nav_linknavlink -> navLink | nav_link * Automatically determining navlink based ui capabilities * Backing out changes that got left behind * Using ui actions for navlinks * Adding TODOs * Ui -> UI * Deleting unused file * Removing api: [] as it's not necessary anymore * Fixing graph saved object privileges * Privileges are now async * Pushing the asycnchronicity to the privileges "service" * Adding TODO * Providing initial value for reduce * adds uiCapabilities to test_entry_template * Adding config to APM/ML feature privileges * Commenting out obviously failing test so we can get CI greeenn * Fixing browser tests * First, very crappy implementation * Adding tests for disabling ui capabilities * All being set to false no longer requires a clone * Using _.mapValues makes this a lot more readable * Checking those privileges dynamically * Fixing some broken stuff when i introduced checkPrivilegesDynamically * Adding conditional plugin tests * Renaming conditional plugin to optional plugin * Fixing type errors * GAP - Actions Restructured and Extensible (#25347) * Restructure user profile for granular app privs (#23750) merging to feature branch for further development * Fixing saved object capability checking * Beginning to restructure actions to be used for all action building * Using actions to build ui capabilities * dropping /read from client-side userprovide ui capabilities * Adding some actions * Using different syntax which will hopefully help with allowing apps to specify the privileges themselves * Exposing all saved object operations in the capabilities * Using actions in security's onPostAuth * Only loading the default index pattern when it's required * Only using the navlinks for the "ui capabilities" * Redirecting from the discover application if the user can't access kibana:discover * Redirecting from dashboard if they're hidden * Features register their privileges now * Introducing a FeaturesPrivilegesBuilder * REmoving app from the feature definition * Adding navlink specific ations * Beginning to break out the serializer * Exposing privileges from the authorization service * Restructuring the privilege/resource serialization to support features * Adding actions unit tests * Adding features privileges builders tests * Adding PrivilegeSerializer tests * Renaming missed usages * Adding tests for the privileges serializer * Adding privileges tests * Adding registerPrivilegesWithCluster tests * Better tests * Fixing authorization service tests * Adding ResourceSerializer tests * Fixing Privileges tests * Some PUT role tests * Fixing read ui/api actions * Exposing features from xpackMainPlugin * Adding navlink:* to the "reserved privileges" * navlink -> navLink | nav_linknavlink -> navLink | nav_linknavlink -> navLink | nav_linknavlink -> navLink | nav_linknavlink -> navLink | nav_linknavlink -> navLink | nav_linknavlink -> navLink | nav_linknavlink -> navLink | nav_linknavlink -> navLink | nav_link * Automatically determining navlink based ui capabilities * Backing out changes that got left behind * Using ui actions for navlinks * Adding TODOs * Ui -> UI * Deleting unused file * Removing api: [] as it's not necessary anymore * Fixing graph saved object privileges * Privileges are now async * Pushing the asycnchronicity to the privileges "service" * Adding TODO * Providing initial value for reduce * adds uiCapabilities to test_entry_template * Adding config to APM/ML feature privileges * Commenting out obviously failing test so we can get CI greeenn * Fixing browser tests * Goodbyyeee * Adding app actions to the reserved privileges * Restructure user profile for granular app privs (#23750) merging to feature branch for further development * Introducing uiCapabilities, removing config providers & user profile (#25387) ## Summary Introduces the concept of "UI Capabilities", which allows Kibana applications to declare capabilities via the `uiCapabilities` injected var, and then use them client-side via the `ui/capabilities` module to inform their rendering decisions. * GAP - Actions Restructured and Extensible (#25347) * Restructure user profile for granular app privs (#23750) merging to feature branch for further development * Fixing saved object capability checking * Beginning to restructure actions to be used for all action building * Using actions to build ui capabilities * dropping /read from client-side userprovide ui capabilities * Adding some actions * Using different syntax which will hopefully help with allowing apps to specify the privileges themselves * Exposing all saved object operations in the capabilities * Using actions in security's onPostAuth * Only loading the default index pattern when it's required * Only using the navlinks for the "ui capabilities" * Redirecting from the discover application if the user can't access kibana:discover * Redirecting from dashboard if they're hidden * Features register their privileges now * Introducing a FeaturesPrivilegesBuilder * REmoving app from the feature definition * Adding navlink specific ations * Beginning to break out the serializer * Exposing privileges from the authorization service * Restructuring the privilege/resource serialization to support features * Adding actions unit tests * Adding features privileges builders tests * Adding PrivilegeSerializer tests * Renaming missed usages * Adding tests for the privileges serializer * Adding privileges tests * Adding registerPrivilegesWithCluster tests * Better tests * Fixing authorization service tests * Adding ResourceSerializer tests * Fixing Privileges tests * Some PUT role tests * Fixing read ui/api actions * Exposing features from xpackMainPlugin * Adding navlink:* to the "reserved privileges" * navlink -> navLink | nav_linknavlink -> navLink | nav_linknavlink -> navLink | nav_linknavlink -> navLink | nav_linknavlink -> navLink | nav_linknavlink -> navLink | nav_linknavlink -> navLink | nav_linknavlink -> navLink | nav_linknavlink -> navLink | nav_link * Automatically determining navlink based ui capabilities * Backing out changes that got left behind * Using ui actions for navlinks * Adding TODOs * Ui -> UI * Deleting unused file * Removing api: [] as it's not necessary anymore * Fixing graph saved object privileges * Privileges are now async * Pushing the asycnchronicity to the privileges "service" * Adding TODO * Providing initial value for reduce * adds uiCapabilities to test_entry_template * Adding config to APM/ML feature privileges * Commenting out obviously failing test so we can get CI greeenn * Fixing browser tests * Goodbyyeee * Adding app actions to the reserved privileges * Update x-pack/plugins/security/server/lib/authorization/disable_ui_capabilities.ts Co-Authored-By: kobelb * Update x-pack/plugins/security/server/lib/authorization/check_privileges_dynamically.ts Co-Authored-By: kobelb * Disabling all ui capabilities if route is anonymous * More typescript * Even more typescript * Updating snapshot * Less any * More safer * Another one * Restructure user profile for granular app privs (#23750) merging to feature branch for further development * Introducing uiCapabilities, removing config providers & user profile (#25387) ## Summary Introduces the concept of "UI Capabilities", which allows Kibana applications to declare capabilities via the `uiCapabilities` injected var, and then use them client-side via the `ui/capabilities` module to inform their rendering decisions. * GAP - Actions Restructured and Extensible (#25347) * Restructure user profile for granular app privs (#23750) merging to feature branch for further development * Fixing saved object capability checking * Beginning to restructure actions to be used for all action building * Using actions to build ui capabilities * dropping /read from client-side userprovide ui capabilities * Adding some actions * Using different syntax which will hopefully help with allowing apps to specify the privileges themselves * Exposing all saved object operations in the capabilities * Using actions in security's onPostAuth * Only loading the default index pattern when it's required * Only using the navlinks for the "ui capabilities" * Redirecting from the discover application if the user can't access kibana:discover * Redirecting from dashboard if they're hidden * Features register their privileges now * Introducing a FeaturesPrivilegesBuilder * REmoving app from the feature definition * Adding navlink specific ations * Beginning to break out the serializer * Exposing privileges from the authorization service * Restructuring the privilege/resource serialization to support features * Adding actions unit tests * Adding features privileges builders tests * Adding PrivilegeSerializer tests * Renaming missed usages * Adding tests for the privileges serializer * Adding privileges tests * Adding registerPrivilegesWithCluster tests * Better tests * Fixing authorization service tests * Adding ResourceSerializer tests * Fixing Privileges tests * Some PUT role tests * Fixing read ui/api actions * Exposing features from xpackMainPlugin * Adding navlink:* to the "reserved privileges" * navlink -> navLink | nav_linknavlink -> navLink | nav_linknavlink -> navLink | nav_linknavlink -> navLink | nav_linknavlink -> navLink | nav_linknavlink -> navLink | nav_linknavlink -> navLink | nav_linknavlink -> navLink | nav_linknavlink -> navLink | nav_link * Automatically determining navlink based ui capabilities * Backing out changes that got left behind * Using ui actions for navlinks * Adding TODOs * Ui -> UI * Deleting unused file * Removing api: [] as it's not necessary anymore * Fixing graph saved object privileges * Privileges are now async * Pushing the asycnchronicity to the privileges "service" * Adding TODO * Providing initial value for reduce * adds uiCapabilities to test_entry_template * Adding config to APM/ML feature privileges * Commenting out obviously failing test so we can get CI greeenn * Fixing browser tests * Goodbyyeee * Adding app actions to the reserved privileges * update snapshot * Update x-pack/plugins/security/server/lib/authorization/check_privileges.ts Co-Authored-By: kobelb * Update x-pack/plugins/security/server/lib/authorization/check_privileges.ts Co-Authored-By: kobelb * Fixing type errors * Only disabling navLinks if a feature is registered for them * Adding non i18n'ed tooltip * Making metadata and tooltip optional * i18n'ing tooltips * Responding to peer review comments --- x-pack/plugins/ml/index.js | 6 + x-pack/plugins/monitoring/init.js | 6 + x-pack/plugins/security/index.js | 36 +- .../optional_plugin.test.ts.snap | 13 + .../check_privileges.test.js.snap | 27 - .../check_privileges.test.ts.snap | 27 + .../disable_ui_capabilities.test.ts.snap | 3 + ...snap => validate_es_response.test.ts.snap} | 0 .../lib/authorization/check_privileges.js | 85 - .../authorization/check_privileges.test.js | 867 --------- .../authorization/check_privileges.test.ts | 1000 +++++++++++ .../lib/authorization/check_privileges.ts | 168 ++ .../check_privileges_dynamically.test.ts | 53 + .../check_privileges_dynamically.ts | 38 + .../disable_ui_capabilities.test.ts | 400 +++++ .../authorization/disable_ui_capabilities.ts | 102 ++ .../lib/authorization/{index.js => index.ts} | 6 +- .../server/lib/authorization/service.js | 5 +- .../server/lib/authorization/service.test.js | 12 +- .../server/lib/authorization/types.ts | 19 + ...e.test.js => validate_es_response.test.ts} | 131 +- ...es_response.js => validate_es_response.ts} | 29 +- .../server/lib/capability_decorator.ts | 61 + .../server/lib/optional_plugin.test.ts | 85 + .../security/server/lib/optional_plugin.ts | 46 + .../secure_saved_objects_client_wrapper.js | 14 +- ...ecure_saved_objects_client_wrapper.test.js | 1580 +++-------------- .../public/views/nav_control/nav_control.tsx | 2 +- .../lib/feature_registry/feature_registry.ts | 3 + .../xpack_main/server/lib/setup_xpack_main.js | 1 - 30 files changed, 2408 insertions(+), 2417 deletions(-) create mode 100644 x-pack/plugins/security/server/lib/__snapshots__/optional_plugin.test.ts.snap delete mode 100644 x-pack/plugins/security/server/lib/authorization/__snapshots__/check_privileges.test.js.snap create mode 100644 x-pack/plugins/security/server/lib/authorization/__snapshots__/check_privileges.test.ts.snap create mode 100644 x-pack/plugins/security/server/lib/authorization/__snapshots__/disable_ui_capabilities.test.ts.snap rename x-pack/plugins/security/server/lib/authorization/__snapshots__/{validate_es_response.test.js.snap => validate_es_response.test.ts.snap} (100%) delete mode 100644 x-pack/plugins/security/server/lib/authorization/check_privileges.js delete mode 100644 x-pack/plugins/security/server/lib/authorization/check_privileges.test.js create mode 100644 x-pack/plugins/security/server/lib/authorization/check_privileges.test.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/check_privileges.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/check_privileges_dynamically.test.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/check_privileges_dynamically.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/disable_ui_capabilities.test.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/disable_ui_capabilities.ts rename x-pack/plugins/security/server/lib/authorization/{index.js => index.ts} (78%) create mode 100644 x-pack/plugins/security/server/lib/authorization/types.ts rename x-pack/plugins/security/server/lib/authorization/{validate_es_response.test.js => validate_es_response.test.ts} (69%) rename x-pack/plugins/security/server/lib/authorization/{validate_es_response.js => validate_es_response.ts} (61%) create mode 100644 x-pack/plugins/security/server/lib/capability_decorator.ts create mode 100644 x-pack/plugins/security/server/lib/optional_plugin.test.ts create mode 100644 x-pack/plugins/security/server/lib/optional_plugin.ts diff --git a/x-pack/plugins/ml/index.js b/x-pack/plugins/ml/index.js index c7a39ef0ec987..6a1c68be753cc 100644 --- a/x-pack/plugins/ml/index.js +++ b/x-pack/plugins/ml/index.js @@ -8,6 +8,7 @@ import { resolve } from 'path'; import Boom from 'boom'; +import { i18n } from '@kbn/i18n'; import { checkLicense } from './server/lib/check_license'; import { mirrorPluginStatus } from '../../server/lib/mirror_plugin_status'; import { jobRoutes } from './server/routes/anomaly_detectors'; @@ -70,6 +71,11 @@ export const ml = (kibana) => { navLinkId: 'ml', privileges: { all: { + metadata: { + tooltip: i18n.translate('xpack.ml.privileges.tooltip', { + defaultMessage: 'The machine_learning_user or machine_learning_admin role should be assigned to grant access' + }) + }, app: ['ml'], savedObject: { all: [], diff --git a/x-pack/plugins/monitoring/init.js b/x-pack/plugins/monitoring/init.js index 4027ec3e804b6..fc914d280a03d 100644 --- a/x-pack/plugins/monitoring/init.js +++ b/x-pack/plugins/monitoring/init.js @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { LOGGING_TAG, KIBANA_MONITORING_LOGGING_TAG } from './common/constants'; import { requireUIRoutes } from './server/routes'; import { instantiateClient } from './server/es_client/instantiate_client'; @@ -61,6 +62,11 @@ export const init = (monitoringPlugin, server) => { navLinkId: 'monitoring', privileges: { all: { + metadata: { + tooltip: i18n.translate('xpack.monitoring.privileges.tooltip', { + defaultMessage: 'The monitoring_user role should be assigned to grant access' + }) + }, app: ['monitoring'], savedObject: { all: [], diff --git a/x-pack/plugins/security/index.js b/x-pack/plugins/security/index.js index c4a7b9080fc10..89f1c8ecc844a 100644 --- a/x-pack/plugins/security/index.js +++ b/x-pack/plugins/security/index.js @@ -20,10 +20,11 @@ import { checkLicense } from './server/lib/check_license'; import { initAuthenticator } from './server/lib/authentication/authenticator'; import { SecurityAuditLogger } from './server/lib/audit_logger'; import { AuditLogger } from '../../server/lib/audit_logger'; -import { createAuthorizationService, registerPrivilegesWithCluster } from './server/lib/authorization'; +import { createAuthorizationService, disableUICapabilitesFactory, registerPrivilegesWithCluster } from './server/lib/authorization'; import { watchStatusAndLicenseToInitialize } from '../../server/lib/watch_status_and_license_to_initialize'; import { SecureSavedObjectsClientWrapper } from './server/lib/saved_objects_client/secure_saved_objects_client_wrapper'; import { deepFreeze } from './server/lib/deep_freeze'; +import { createOptionalPlugin } from './server/lib/optional_plugin'; export const security = (kibana) => new kibana.Plugin({ id: 'security', @@ -88,6 +89,21 @@ export const security = (kibana) => new kibana.Plugin({ sessionTimeout: config.get('xpack.security.sessionTimeout'), enableSpaceAwarePrivileges: config.get('xpack.spaces.enabled'), }; + }, + replaceInjectedVars: async function (originalInjectedVars, request, server) { + const disableUICapabilites = disableUICapabilitesFactory(server, request); + // if we're an anonymous route, we disable all ui capabilities + if (request.route.settings.auth === false) { + return { + ...originalInjectedVars, + uiCapabilities: disableUICapabilites.all(originalInjectedVars.uiCapabilities) + }; + } + + return { + ...originalInjectedVars, + uiCapabilities: await disableUICapabilites.usingPrivileges(originalInjectedVars.uiCapabilities) + }; } }, @@ -117,8 +133,10 @@ export const security = (kibana) => new kibana.Plugin({ const { savedObjects } = server; + const spaces = createOptionalPlugin(config, 'xpack.spaces', server.plugins, 'spaces'); + // exposes server.plugins.security.authorization - const authorization = createAuthorizationService(server, xpackInfoFeature, savedObjects.types, xpackMainPlugin); + const authorization = createAuthorizationService(server, xpackInfoFeature, savedObjects.types, xpackMainPlugin, spaces); server.expose('authorization', deepFreeze(authorization)); watchStatusAndLicenseToInitialize(xpackMainPlugin, plugin, async (license) => { @@ -147,17 +165,15 @@ export const security = (kibana) => new kibana.Plugin({ savedObjects.addScopedSavedObjectsClientWrapperFactory(Number.MIN_VALUE, ({ client, request }) => { if (authorization.mode.useRbacForRequest(request)) { - const { spaces } = server.plugins; return new SecureSavedObjectsClientWrapper({ actions: authorization.actions, auditLogger, baseClient: client, - checkPrivilegesWithRequest: authorization.checkPrivilegesWithRequest, + checkPrivilegesDynamicallyWithRequest: authorization.checkPrivilegesDynamicallyWithRequest, errors: savedObjects.SavedObjectsClient.errors, request, savedObjectTypes: savedObjects.types, - spaces, }); } @@ -193,16 +209,15 @@ export const security = (kibana) => new kibana.Plugin({ server.ext('onPostAuth', async function (req, h) { const path = req.path; - const { actions, checkPrivilegesWithRequest } = server.plugins.security.authorization; - const checkPrivileges = checkPrivilegesWithRequest(req); + const { actions, checkPrivilegesDynamicallyWithRequest } = server.plugins.security.authorization; + const checkPrivileges = checkPrivilegesDynamicallyWithRequest(req); // Enforce app restrictions if (path.startsWith('/app/')) { const appId = path.split('/', 3)[2]; const appAction = actions.app.get(appId); - // TODO: Check this at the specific space - const checkPrivilegesResponse = await checkPrivileges.globally(appAction); + const checkPrivilegesResponse = await checkPrivileges(appAction); if (!checkPrivilegesResponse.hasAllRequested) { return Boom.notFound(); } @@ -218,8 +233,7 @@ export const security = (kibana) => new kibana.Plugin({ const feature = path.split('/', 3)[2]; const apiActions = actionTags.map(tag => actions.api.get(`${feature}/${tag.split(':', 2)[1]}`)); - // TODO: Check this at the specific space - const checkPrivilegesResponse = await checkPrivileges.globally(apiActions); + const checkPrivilegesResponse = await checkPrivileges(apiActions); if (!checkPrivilegesResponse.hasAllRequested) { return Boom.notFound(); } diff --git a/x-pack/plugins/security/server/lib/__snapshots__/optional_plugin.test.ts.snap b/x-pack/plugins/security/server/lib/__snapshots__/optional_plugin.test.ts.snap new file mode 100644 index 0000000000000..a57512e7e8dc6 --- /dev/null +++ b/x-pack/plugins/security/server/lib/__snapshots__/optional_plugin.test.ts.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`throws error when invoked before it's available 1`] = `"Plugin accessed before it's available"`; + +exports[`throws error when invoked before it's available 2`] = `"Plugin accessed before it's available"`; + +exports[`throws error when invoked before it's available 3`] = `"Plugin accessed before it's available"`; + +exports[`throws error when invoked when it's not enabled 1`] = `"Plugin isn't enabled, check isEnabled before using"`; + +exports[`throws error when invoked when it's not enabled 2`] = `"Plugin isn't enabled, check isEnabled before using"`; + +exports[`throws error when invoked when it's not enabled 3`] = `"Plugin isn't enabled, check isEnabled before using"`; diff --git a/x-pack/plugins/security/server/lib/authorization/__snapshots__/check_privileges.test.js.snap b/x-pack/plugins/security/server/lib/authorization/__snapshots__/check_privileges.test.js.snap deleted file mode 100644 index be5794d393be7..0000000000000 --- a/x-pack/plugins/security/server/lib/authorization/__snapshots__/check_privileges.test.js.snap +++ /dev/null @@ -1,27 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`#checkPrivilegesAtSpace throws error when checking for login and user has login but doesn't have version 1`] = `[Error: Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.]`; - -exports[`#checkPrivilegesAtSpace with a malformed Elasticsearch response throws a validation error when an extra privilege is present in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_1" fails because ["saved_object:bar-type/get" is not allowed]]]]`; - -exports[`#checkPrivilegesAtSpace with a malformed Elasticsearch response throws a validation error when privileges are missing in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_1" fails because [child "saved_object:foo-type/get" fails because ["saved_object:foo-type/get" is required]]]]]`; - -exports[`#checkPrivilegesAtSpaces throws error when Elasticsearch returns malformed response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_1" fails because [child "mock-action:version" fails because ["mock-action:version" is required]]]]]`; - -exports[`#checkPrivilegesAtSpaces throws error when checking for login and user has login but doesn't have version 1`] = `[Error: Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.]`; - -exports[`#checkPrivilegesAtSpaces with a malformed Elasticsearch response throws a validation error when an a space is missing in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_2" fails because ["space:space_2" is required]]]]`; - -exports[`#checkPrivilegesAtSpaces with a malformed Elasticsearch response throws a validation error when an extra privilege is present in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_2" fails because ["space:space_2" is required]]]]`; - -exports[`#checkPrivilegesAtSpaces with a malformed Elasticsearch response throws a validation error when an extra space is present in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because ["space:space_3" is not allowed]]]`; - -exports[`#checkPrivilegesAtSpaces with a malformed Elasticsearch response throws a validation error when privileges are missing in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_2" fails because ["space:space_2" is required]]]]`; - -exports[`#checkPrivilegesGlobally throws error when Elasticsearch returns malformed response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "*" fails because [child "mock-action:version" fails because ["mock-action:version" is required]]]]]`; - -exports[`#checkPrivilegesGlobally throws error when checking for login and user has login but doesn't have version 1`] = `[Error: Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.]`; - -exports[`#checkPrivilegesGlobally with a malformed Elasticsearch response throws a validation error when an extra privilege is present in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "*" fails because ["saved_object:bar-type/get" is not allowed]]]]`; - -exports[`#checkPrivilegesGlobally with a malformed Elasticsearch response throws a validation error when privileges are missing in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "*" fails because [child "saved_object:foo-type/get" fails because ["saved_object:foo-type/get" is required]]]]]`; diff --git a/x-pack/plugins/security/server/lib/authorization/__snapshots__/check_privileges.test.ts.snap b/x-pack/plugins/security/server/lib/authorization/__snapshots__/check_privileges.test.ts.snap new file mode 100644 index 0000000000000..1212c2cd6a5cb --- /dev/null +++ b/x-pack/plugins/security/server/lib/authorization/__snapshots__/check_privileges.test.ts.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`#atSpace throws error when checking for login and user has login but doesn't have version 1`] = `[Error: Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.]`; + +exports[`#atSpace with a malformed Elasticsearch response throws a validation error when an extra privilege is present in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_1" fails because ["saved_object:bar-type/get" is not allowed]]]]`; + +exports[`#atSpace with a malformed Elasticsearch response throws a validation error when privileges are missing in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_1" fails because [child "saved_object:foo-type/get" fails because ["saved_object:foo-type/get" is required]]]]]`; + +exports[`#atSpaces throws error when Elasticsearch returns malformed response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_1" fails because [child "mock-action:version" fails because ["mock-action:version" is required]]]]]`; + +exports[`#atSpaces throws error when checking for login and user has login but doesn't have version 1`] = `[Error: Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.]`; + +exports[`#atSpaces with a malformed Elasticsearch response throws a validation error when an a space is missing in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_2" fails because ["space:space_2" is required]]]]`; + +exports[`#atSpaces with a malformed Elasticsearch response throws a validation error when an extra privilege is present in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_2" fails because ["space:space_2" is required]]]]`; + +exports[`#atSpaces with a malformed Elasticsearch response throws a validation error when an extra space is present in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because ["space:space_3" is not allowed]]]`; + +exports[`#atSpaces with a malformed Elasticsearch response throws a validation error when privileges are missing in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "space:space_2" fails because ["space:space_2" is required]]]]`; + +exports[`#globally throws error when Elasticsearch returns malformed response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "*" fails because [child "mock-action:version" fails because ["mock-action:version" is required]]]]]`; + +exports[`#globally throws error when checking for login and user has login but doesn't have version 1`] = `[Error: Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.]`; + +exports[`#globally with a malformed Elasticsearch response throws a validation error when an extra privilege is present in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "*" fails because ["saved_object:bar-type/get" is not allowed]]]]`; + +exports[`#globally with a malformed Elasticsearch response throws a validation error when privileges are missing in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "*" fails because [child "saved_object:foo-type/get" fails because ["saved_object:foo-type/get" is required]]]]]`; diff --git a/x-pack/plugins/security/server/lib/authorization/__snapshots__/disable_ui_capabilities.test.ts.snap b/x-pack/plugins/security/server/lib/authorization/__snapshots__/disable_ui_capabilities.test.ts.snap new file mode 100644 index 0000000000000..53c567726332c --- /dev/null +++ b/x-pack/plugins/security/server/lib/authorization/__snapshots__/disable_ui_capabilities.test.ts.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`usingPrivileges checkPrivileges errors otherwise it throws the error 1`] = `"something else entirely"`; diff --git a/x-pack/plugins/security/server/lib/authorization/__snapshots__/validate_es_response.test.js.snap b/x-pack/plugins/security/server/lib/authorization/__snapshots__/validate_es_response.test.ts.snap similarity index 100% rename from x-pack/plugins/security/server/lib/authorization/__snapshots__/validate_es_response.test.js.snap rename to x-pack/plugins/security/server/lib/authorization/__snapshots__/validate_es_response.test.ts.snap diff --git a/x-pack/plugins/security/server/lib/authorization/check_privileges.js b/x-pack/plugins/security/server/lib/authorization/check_privileges.js deleted file mode 100644 index d7038ac1c3edf..0000000000000 --- a/x-pack/plugins/security/server/lib/authorization/check_privileges.js +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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 { pick, transform, uniq } from 'lodash'; -import { GLOBAL_RESOURCE } from '../../../common/constants'; -import { ResourceSerializer } from './resource_serializer'; -import { validateEsPrivilegeResponse } from './validate_es_response'; - -export function checkPrivilegesWithRequestFactory(actions, application, shieldClient) { - const { callWithRequest } = shieldClient; - - const hasIncompatibileVersion = (applicationPrivilegesResponse) => { - return Object.values(applicationPrivilegesResponse).some(resource => !resource[actions.version] && resource[actions.login]); - }; - - return function checkPrivilegesWithRequest(request) { - - const checkPrivilegesAtResources = async (resources, privilegeOrPrivileges) => { - const privileges = Array.isArray(privilegeOrPrivileges) ? privilegeOrPrivileges : [privilegeOrPrivileges]; - const allApplicationPrivileges = uniq([actions.version, actions.login, ...privileges]); - - const hasPrivilegesResponse = await callWithRequest(request, 'shield.hasPrivileges', { - body: { - applications: [{ - application, - resources, - privileges: allApplicationPrivileges - }], - } - }); - - validateEsPrivilegeResponse(hasPrivilegesResponse, application, allApplicationPrivileges, resources); - - const applicationPrivilegesResponse = hasPrivilegesResponse.application[application]; - - if (hasIncompatibileVersion(applicationPrivilegesResponse)) { - throw new Error('Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.'); - } - - return { - hasAllRequested: hasPrivilegesResponse.has_all_requested, - username: hasPrivilegesResponse.username, - // we need to filter out the non requested privileges from the response - resourcePrivileges: transform(applicationPrivilegesResponse, (result, value, key) => { - result[key] = pick(value, privileges); - }), - }; - }; - - const checkPrivilegesAtResource = async (resource, privilegeOrPrivileges) => { - const { hasAllRequested, username, resourcePrivileges } = await checkPrivilegesAtResources([resource], privilegeOrPrivileges); - return { - hasAllRequested, - username, - privileges: resourcePrivileges[resource], - }; - }; - - return { - async atSpace(spaceId, privilegeOrPrivileges) { - const spaceResource = ResourceSerializer.serializeSpaceResource(spaceId); - return await checkPrivilegesAtResource(spaceResource, privilegeOrPrivileges); - }, - async atSpaces(spaceIds, privilegeOrPrivileges) { - const spaceResources = spaceIds.map(spaceId => ResourceSerializer.serializeSpaceResource(spaceId)); - const { hasAllRequested, username, resourcePrivileges } = await checkPrivilegesAtResources(spaceResources, privilegeOrPrivileges); - return { - hasAllRequested, - username, - // we need to turn the resource responses back into the space ids - spacePrivileges: transform(resourcePrivileges, (result, value, key) => { - result[ResourceSerializer.deserializeSpaceResource(key)] = value; - }), - }; - - }, - async globally(privilegeOrPrivileges) { - return await checkPrivilegesAtResource(GLOBAL_RESOURCE, privilegeOrPrivileges); - }, - }; - }; -} diff --git a/x-pack/plugins/security/server/lib/authorization/check_privileges.test.js b/x-pack/plugins/security/server/lib/authorization/check_privileges.test.js deleted file mode 100644 index 49b8e104c54d1..0000000000000 --- a/x-pack/plugins/security/server/lib/authorization/check_privileges.test.js +++ /dev/null @@ -1,867 +0,0 @@ -/* - * 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 { uniq } from 'lodash'; -import { checkPrivilegesWithRequestFactory } from './check_privileges'; -import { GLOBAL_RESOURCE } from '../../../common/constants'; - -const application = 'kibana-our_application'; - -const mockActions = { - login: 'mock-action:login', - version: 'mock-action:version', -}; - -const savedObjectTypes = ['foo-type', 'bar-type']; - -const createMockShieldClient = (response) => { - const mockCallWithRequest = jest.fn(); - - mockCallWithRequest.mockImplementationOnce(async () => response); - - return { - callWithRequest: mockCallWithRequest, - }; -}; - -describe('#checkPrivilegesAtSpace', () => { - const checkPrivilegesAtSpaceTest = (description, { - spaceId, - privilegeOrPrivileges, - esHasPrivilegesResponse, - expectedResult, - expectErrorThrown - }) => { - test(description, async () => { - const mockShieldClient = createMockShieldClient(esHasPrivilegesResponse); - const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory(mockActions, application, mockShieldClient); - const request = Symbol(); - const checkPrivileges = checkPrivilegesWithRequest(request); - - let actualResult; - let errorThrown = null; - try { - actualResult = await checkPrivileges.atSpace(spaceId, privilegeOrPrivileges); - } catch (err) { - errorThrown = err; - } - - expect(mockShieldClient.callWithRequest).toHaveBeenCalledWith(request, 'shield.hasPrivileges', { - body: { - applications: [{ - application, - resources: [`space:${spaceId}`], - privileges: uniq([ - mockActions.version, - mockActions.login, - ...Array.isArray(privilegeOrPrivileges) ? privilegeOrPrivileges : [privilegeOrPrivileges], - ]) - }] - } - }); - - if (expectedResult) { - expect(errorThrown).toBeNull(); - expect(actualResult).toEqual(expectedResult); - } - - if (expectErrorThrown) { - expect(errorThrown).toMatchSnapshot(); - } - }); - }; - - checkPrivilegesAtSpaceTest('successful when checking for login and user has login', { - spaceId: 'space_1', - privilegeOrPrivileges: mockActions.login, - esHasPrivilegesResponse: { - has_all_requested: true, - username: 'foo-username', - application: { - [application]: { - 'space:space_1': { - [mockActions.login]: true, - [mockActions.version]: true, - } - } - } - }, - expectedResult: { - hasAllRequested: true, - username: 'foo-username', - privileges: { - [mockActions.login]: true - } - }, - }); - - checkPrivilegesAtSpaceTest(`failure when checking for login and user doesn't have login`, { - spaceId: 'space_1', - privilegeOrPrivileges: mockActions.login, - esHasPrivilegesResponse: { - has_all_requested: false, - username: 'foo-username', - application: { - [application]: { - 'space:space_1': { - [mockActions.login]: false, - [mockActions.version]: true, - } - } - } - }, - expectedResult: { - hasAllRequested: false, - username: 'foo-username', - privileges: { - [mockActions.login]: false - } - }, - }); - - checkPrivilegesAtSpaceTest(`throws error when checking for login and user has login but doesn't have version`, { - spaceId: 'space_1', - privilegeOrPrivileges: mockActions.login, - esHasPrivilegesResponse: { - has_all_requested: false, - username: 'foo-username', - application: { - [application]: { - 'space:space_1': { - [mockActions.login]: true, - [mockActions.version]: false, - } - } - } - }, - expectErrorThrown: true, - }); - - checkPrivilegesAtSpaceTest(`successful when checking for two actions and the user has both`, { - spaceId: 'space_1', - privilegeOrPrivileges: [`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, - } - } - } - }, - expectedResult: { - hasAllRequested: true, - username: 'foo-username', - privileges: { - [`saved_object:${savedObjectTypes[0]}/get`]: true, - [`saved_object:${savedObjectTypes[1]}/get`]: true, - } - }, - }); - - checkPrivilegesAtSpaceTest(`failure when checking for two actions and the user has only one`, { - spaceId: 'space_1', - privilegeOrPrivileges: [`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`]: true, - } - } - } - }, - expectedResult: { - hasAllRequested: false, - username: 'foo-username', - privileges: { - [`saved_object:${savedObjectTypes[0]}/get`]: false, - [`saved_object:${savedObjectTypes[1]}/get`]: true, - } - }, - }); - - describe('with a malformed Elasticsearch response', () => { - checkPrivilegesAtSpaceTest(`throws a validation error when an extra privilege is present in the response`, { - spaceId: 'space_1', - privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/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`]: true, - } - } - } - }, - expectErrorThrown: true, - }); - - checkPrivilegesAtSpaceTest(`throws a validation error when privileges are missing in the response`, { - spaceId: 'space_1', - privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], - esHasPrivilegesResponse: { - has_all_requested: false, - username: 'foo-username', - application: { - [application]: { - 'space:space_1': { - [mockActions.login]: true, - [mockActions.version]: true, - } - } - } - }, - expectErrorThrown: true, - }); - }); -}); - -describe('#checkPrivilegesAtSpaces', () => { - const checkPrivilegesAtSpacesTest = (description, { - spaceIds, - privilegeOrPrivileges, - esHasPrivilegesResponse, - expectedResult, - expectErrorThrown - }) => { - test(description, async () => { - const mockShieldClient = createMockShieldClient(esHasPrivilegesResponse); - const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory(mockActions, application, mockShieldClient); - const request = Symbol(); - const checkPrivileges = checkPrivilegesWithRequest(request); - - let actualResult; - let errorThrown = null; - try { - actualResult = await checkPrivileges.atSpaces(spaceIds, privilegeOrPrivileges); - } catch (err) { - errorThrown = err; - } - - expect(mockShieldClient.callWithRequest).toHaveBeenCalledWith(request, 'shield.hasPrivileges', { - body: { - applications: [{ - application, - resources: spaceIds.map(spaceId => `space:${spaceId}`), - privileges: uniq([ - mockActions.version, - mockActions.login, - ...Array.isArray(privilegeOrPrivileges) ? privilegeOrPrivileges : [privilegeOrPrivileges], - ]) - }] - } - }); - - if (expectedResult) { - expect(errorThrown).toBeNull(); - expect(actualResult).toEqual(expectedResult); - } - - if (expectErrorThrown) { - expect(errorThrown).toMatchSnapshot(); - } - }); - }; - - checkPrivilegesAtSpacesTest('successful when checking for login and user has login at both spaces', { - spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: mockActions.login, - 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, - } - } - } - }, - expectedResult: { - hasAllRequested: true, - username: 'foo-username', - spacePrivileges: { - space_1: { - [mockActions.login]: true - }, - space_2: { - [mockActions.login]: true - }, - } - }, - }); - - checkPrivilegesAtSpacesTest('failure when checking for login and user has login at only one space', { - spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: mockActions.login, - esHasPrivilegesResponse: { - has_all_requested: false, - username: 'foo-username', - application: { - [application]: { - 'space:space_1': { - [mockActions.login]: true, - [mockActions.version]: true, - }, - 'space:space_2': { - [mockActions.login]: false, - [mockActions.version]: true, - } - } - } - }, - expectedResult: { - hasAllRequested: false, - username: 'foo-username', - spacePrivileges: { - space_1: { - [mockActions.login]: true - }, - space_2: { - [mockActions.login]: false - }, - } - }, - }); - - checkPrivilegesAtSpacesTest(`throws error when checking for login and user has login but doesn't have version`, { - spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: mockActions.login, - esHasPrivilegesResponse: { - has_all_requested: false, - username: 'foo-username', - application: { - [application]: { - 'space:space_1': { - [mockActions.login]: true, - [mockActions.version]: false, - }, - 'space:space_2': { - [mockActions.login]: true, - [mockActions.version]: false, - } - } - } - }, - expectErrorThrown: true, - }); - - checkPrivilegesAtSpacesTest(`throws error when Elasticsearch returns malformed response`, { - spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`, `saved_object:${savedObjectTypes[1]}/get`], - esHasPrivilegesResponse: { - has_all_requested: true, - username: 'foo-username', - application: { - [application]: { - 'space:space_1': { - [`saved_object:${savedObjectTypes[0]}/get`]: true, - [`saved_object:${savedObjectTypes[1]}/get`]: true, - }, - 'space:space_2': { - [`saved_object:${savedObjectTypes[0]}/get`]: true, - [`saved_object:${savedObjectTypes[1]}/get`]: true, - } - } - } - }, - expectErrorThrown: true, - }); - - checkPrivilegesAtSpacesTest(`successful when checking for two actions at two spaces and user has it all`, { - spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: [`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, - } - } - } - }, - expectedResult: { - hasAllRequested: true, - username: 'foo-username', - spacePrivileges: { - space_1: { - [`saved_object:${savedObjectTypes[0]}/get`]: true, - [`saved_object:${savedObjectTypes[1]}/get`]: true, - }, - space_2: { - [`saved_object:${savedObjectTypes[0]}/get`]: true, - [`saved_object:${savedObjectTypes[1]}/get`]: true, - } - } - }, - }); - - checkPrivilegesAtSpacesTest(`failure when checking for two actions at two spaces and user has one action at one space`, { - spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: [`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`]: false, - }, - 'space:space_2': { - [mockActions.login]: true, - [mockActions.version]: true, - [`saved_object:${savedObjectTypes[0]}/get`]: false, - [`saved_object:${savedObjectTypes[1]}/get`]: false, - } - } - } - }, - expectedResult: { - hasAllRequested: false, - username: 'foo-username', - spacePrivileges: { - space_1: { - [`saved_object:${savedObjectTypes[0]}/get`]: true, - [`saved_object:${savedObjectTypes[1]}/get`]: false, - }, - space_2: { - [`saved_object:${savedObjectTypes[0]}/get`]: false, - [`saved_object:${savedObjectTypes[1]}/get`]: false, - } - } - }, - }); - - checkPrivilegesAtSpacesTest(`failure when checking for two actions at two spaces and user has two actions at one space`, { - spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: [`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`]: false, - [`saved_object:${savedObjectTypes[1]}/get`]: false, - } - } - } - }, - expectedResult: { - hasAllRequested: false, - username: 'foo-username', - spacePrivileges: { - space_1: { - [`saved_object:${savedObjectTypes[0]}/get`]: true, - [`saved_object:${savedObjectTypes[1]}/get`]: true, - }, - space_2: { - [`saved_object:${savedObjectTypes[0]}/get`]: false, - [`saved_object:${savedObjectTypes[1]}/get`]: false, - } - } - }, - }); - - checkPrivilegesAtSpacesTest( - `failure when checking for two actions at two spaces and user has two actions at one space & one action at the other`, { - spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: [`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`]: false, - } - } - } - }, - expectedResult: { - hasAllRequested: false, - username: 'foo-username', - spacePrivileges: { - space_1: { - [`saved_object:${savedObjectTypes[0]}/get`]: true, - [`saved_object:${savedObjectTypes[1]}/get`]: true, - }, - space_2: { - [`saved_object:${savedObjectTypes[0]}/get`]: true, - [`saved_object:${savedObjectTypes[1]}/get`]: false, - } - } - }, - }); - - describe('with a malformed Elasticsearch response', () => { - checkPrivilegesAtSpacesTest(`throws a validation error when an extra privilege is present in the response`, { - spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/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`]: true, - }, - 'space:space_1': { - [mockActions.login]: true, - [mockActions.version]: true, - [`saved_object:${savedObjectTypes[0]}/get`]: false, - } - } - } - }, - expectErrorThrown: true, - }); - - checkPrivilegesAtSpacesTest(`throws a validation error when privileges are missing in the response`, { - spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], - esHasPrivilegesResponse: { - has_all_requested: false, - username: 'foo-username', - application: { - [application]: { - 'space:space_1': { - [mockActions.login]: true, - [mockActions.version]: true, - }, - 'space:space_1': { - [mockActions.login]: true, - [mockActions.version]: true, - [`saved_object:${savedObjectTypes[0]}/get`]: false, - } - } - } - }, - expectErrorThrown: true, - }); - - checkPrivilegesAtSpacesTest(`throws a validation error when an extra space is present in the response`, { - spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/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, - }, - 'space:space_2': { - [mockActions.login]: true, - [mockActions.version]: true, - [`saved_object:${savedObjectTypes[0]}/get`]: false, - }, - 'space:space_3': { - [mockActions.login]: true, - [mockActions.version]: true, - [`saved_object:${savedObjectTypes[0]}/get`]: false, - }, - } - } - }, - expectErrorThrown: true, - }); - - checkPrivilegesAtSpacesTest(`throws a validation error when an a space is missing in the response`, { - spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/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, - } - } - } - }, - expectErrorThrown: true, - }); - }); -}); - -describe('#checkPrivilegesGlobally', () => { - const checkPrivilegesGloballyTest = (description, { - privilegeOrPrivileges, - esHasPrivilegesResponse, - expectedResult, - expectErrorThrown - }) => { - test(description, async () => { - const mockShieldClient = createMockShieldClient(esHasPrivilegesResponse); - const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory(mockActions, application, mockShieldClient); - const request = Symbol(); - const checkPrivileges = checkPrivilegesWithRequest(request); - - let actualResult; - let errorThrown = null; - try { - actualResult = await checkPrivileges.globally(privilegeOrPrivileges); - } catch (err) { - errorThrown = err; - } - - expect(mockShieldClient.callWithRequest).toHaveBeenCalledWith(request, 'shield.hasPrivileges', { - body: { - applications: [{ - application, - resources: [GLOBAL_RESOURCE], - privileges: uniq([ - mockActions.version, - mockActions.login, - ...Array.isArray(privilegeOrPrivileges) ? privilegeOrPrivileges : [privilegeOrPrivileges], - ]) - }] - } - }); - - if (expectedResult) { - expect(errorThrown).toBeNull(); - expect(actualResult).toEqual(expectedResult); - } - - if (expectErrorThrown) { - expect(errorThrown).toMatchSnapshot(); - } - }); - }; - - checkPrivilegesGloballyTest('successful when checking for login and user has login', { - spaceId: 'space_1', - privilegeOrPrivileges: mockActions.login, - esHasPrivilegesResponse: { - has_all_requested: true, - username: 'foo-username', - application: { - [application]: { - [GLOBAL_RESOURCE]: { - [mockActions.login]: true, - [mockActions.version]: true, - } - } - } - }, - expectedResult: { - hasAllRequested: true, - username: 'foo-username', - privileges: { - [mockActions.login]: true - } - }, - }); - - checkPrivilegesGloballyTest(`failure when checking for login and user doesn't have login`, { - privilegeOrPrivileges: mockActions.login, - esHasPrivilegesResponse: { - has_all_requested: false, - username: 'foo-username', - application: { - [application]: { - [GLOBAL_RESOURCE]: { - [mockActions.login]: false, - [mockActions.version]: true, - } - } - } - }, - expectedResult: { - hasAllRequested: false, - username: 'foo-username', - privileges: { - [mockActions.login]: false - } - }, - }); - - checkPrivilegesGloballyTest(`throws error when checking for login and user has login but doesn't have version`, { - privilegeOrPrivileges: mockActions.login, - esHasPrivilegesResponse: { - has_all_requested: false, - username: 'foo-username', - application: { - [application]: { - [GLOBAL_RESOURCE]: { - [mockActions.login]: true, - [mockActions.version]: false, - } - } - } - }, - expectErrorThrown: true, - }); - - checkPrivilegesGloballyTest(`throws error when Elasticsearch returns malformed response`, { - privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`, `saved_object:${savedObjectTypes[1]}/get`], - esHasPrivilegesResponse: { - has_all_requested: false, - username: 'foo-username', - application: { - [application]: { - [GLOBAL_RESOURCE]: { - [`saved_object:${savedObjectTypes[0]}/get`]: false, - [`saved_object:${savedObjectTypes[1]}/get`]: true, - } - } - } - }, - expectErrorThrown: true, - }); - - checkPrivilegesGloballyTest(`successful when checking for two actions and the user has both`, { - privilegeOrPrivileges: [`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, - } - } - } - }, - expectedResult: { - hasAllRequested: true, - username: 'foo-username', - privileges: { - [`saved_object:${savedObjectTypes[0]}/get`]: true, - [`saved_object:${savedObjectTypes[1]}/get`]: true, - } - }, - }); - - checkPrivilegesGloballyTest(`failure when checking for two actions and the user has only one`, { - privilegeOrPrivileges: [`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`]: true, - } - } - } - }, - expectedResult: { - hasAllRequested: false, - username: 'foo-username', - privileges: { - [`saved_object:${savedObjectTypes[0]}/get`]: false, - [`saved_object:${savedObjectTypes[1]}/get`]: true, - } - }, - }); - - describe('with a malformed Elasticsearch response', () => { - checkPrivilegesGloballyTest(`throws a validation error when an extra privilege is present in the response`, { - privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/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`]: true, - } - } - } - }, - expectErrorThrown: true, - }); - - checkPrivilegesGloballyTest(`throws a validation error when privileges are missing in the response`, { - privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], - esHasPrivilegesResponse: { - has_all_requested: false, - username: 'foo-username', - application: { - [application]: { - [GLOBAL_RESOURCE]: { - [mockActions.login]: true, - [mockActions.version]: true, - } - } - } - }, - expectErrorThrown: true, - }); - }); -}); diff --git a/x-pack/plugins/security/server/lib/authorization/check_privileges.test.ts b/x-pack/plugins/security/server/lib/authorization/check_privileges.test.ts new file mode 100644 index 0000000000000..b418e02474f4a --- /dev/null +++ b/x-pack/plugins/security/server/lib/authorization/check_privileges.test.ts @@ -0,0 +1,1000 @@ +/* + * 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 { uniq } from 'lodash'; +import { GLOBAL_RESOURCE } from '../../../common/constants'; +import { checkPrivilegesWithRequestFactory } from './check_privileges'; +import { HasPrivilegesResponse } from './types'; + +const application = 'kibana-our_application'; + +const mockActions = { + login: 'mock-action:login', + version: 'mock-action:version', +}; + +const savedObjectTypes = ['foo-type', 'bar-type']; + +const createMockShieldClient = (response: any) => { + const mockCallWithRequest = jest.fn(); + + mockCallWithRequest.mockImplementationOnce(async () => response); + + return { + callWithRequest: mockCallWithRequest, + }; +}; + +describe('#atSpace', () => { + const checkPrivilegesAtSpaceTest = ( + description: string, + options: { + spaceId: string; + privilegeOrPrivileges: string | string[]; + esHasPrivilegesResponse: HasPrivilegesResponse; + expectedResult?: any; + expectErrorThrown?: any; + } + ) => { + test(description, async () => { + const mockShieldClient = createMockShieldClient(options.esHasPrivilegesResponse); + const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory( + mockActions, + application, + mockShieldClient + ); + const request = { foo: Symbol() }; + const checkPrivileges = checkPrivilegesWithRequest(request); + + let actualResult; + let errorThrown = null; + try { + actualResult = await checkPrivileges.atSpace( + options.spaceId, + options.privilegeOrPrivileges + ); + } catch (err) { + errorThrown = err; + } + + expect(mockShieldClient.callWithRequest).toHaveBeenCalledWith( + request, + 'shield.hasPrivileges', + { + body: { + applications: [ + { + application, + resources: [`space:${options.spaceId}`], + privileges: uniq([ + mockActions.version, + mockActions.login, + ...(Array.isArray(options.privilegeOrPrivileges) + ? options.privilegeOrPrivileges + : [options.privilegeOrPrivileges]), + ]), + }, + ], + }, + } + ); + + if (options.expectedResult) { + expect(errorThrown).toBeNull(); + expect(actualResult).toEqual(options.expectedResult); + } + + if (options.expectErrorThrown) { + expect(errorThrown).toMatchSnapshot(); + } + }); + }; + + checkPrivilegesAtSpaceTest('successful when checking for login and user has login', { + spaceId: 'space_1', + privilegeOrPrivileges: mockActions.login, + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + }, + }, + }, + }, + expectedResult: { + hasAllRequested: true, + username: 'foo-username', + privileges: { + [mockActions.login]: true, + }, + }, + }); + + checkPrivilegesAtSpaceTest(`failure when checking for login and user doesn't have login`, { + spaceId: 'space_1', + privilegeOrPrivileges: mockActions.login, + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: false, + [mockActions.version]: true, + }, + }, + }, + }, + expectedResult: { + hasAllRequested: false, + username: 'foo-username', + privileges: { + [mockActions.login]: false, + }, + }, + }); + + checkPrivilegesAtSpaceTest( + `throws error when checking for login and user has login but doesn't have version`, + { + spaceId: 'space_1', + privilegeOrPrivileges: mockActions.login, + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: false, + }, + }, + }, + }, + expectErrorThrown: true, + } + ); + + checkPrivilegesAtSpaceTest(`successful when checking for two actions and the user has both`, { + spaceId: 'space_1', + privilegeOrPrivileges: [ + `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, + }, + }, + }, + }, + expectedResult: { + hasAllRequested: true, + username: 'foo-username', + privileges: { + [`saved_object:${savedObjectTypes[0]}/get`]: true, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, + }, + }); + + checkPrivilegesAtSpaceTest(`failure when checking for two actions and the user has only one`, { + spaceId: 'space_1', + privilegeOrPrivileges: [ + `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`]: true, + }, + }, + }, + }, + expectedResult: { + hasAllRequested: false, + username: 'foo-username', + privileges: { + [`saved_object:${savedObjectTypes[0]}/get`]: false, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, + }, + }); + + describe('with a malformed Elasticsearch response', () => { + checkPrivilegesAtSpaceTest( + `throws a validation error when an extra privilege is present in the response`, + { + spaceId: 'space_1', + privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/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`]: true, + }, + }, + }, + }, + expectErrorThrown: true, + } + ); + + checkPrivilegesAtSpaceTest( + `throws a validation error when privileges are missing in the response`, + { + spaceId: 'space_1', + privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + }, + }, + }, + }, + expectErrorThrown: true, + } + ); + }); +}); + +describe('#atSpaces', () => { + const checkPrivilegesAtSpacesTest = ( + description: string, + options: { + spaceIds: string[]; + privilegeOrPrivileges: string | string[]; + esHasPrivilegesResponse: HasPrivilegesResponse; + expectedResult?: any; + expectErrorThrown?: any; + } + ) => { + test(description, async () => { + const mockShieldClient = createMockShieldClient(options.esHasPrivilegesResponse); + const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory( + mockActions, + application, + mockShieldClient + ); + const request = { foo: Symbol() }; + const checkPrivileges = checkPrivilegesWithRequest(request); + + let actualResult; + let errorThrown = null; + try { + actualResult = await checkPrivileges.atSpaces( + options.spaceIds, + options.privilegeOrPrivileges + ); + } catch (err) { + errorThrown = err; + } + + expect(mockShieldClient.callWithRequest).toHaveBeenCalledWith( + request, + 'shield.hasPrivileges', + { + body: { + applications: [ + { + application, + resources: options.spaceIds.map(spaceId => `space:${spaceId}`), + privileges: uniq([ + mockActions.version, + mockActions.login, + ...(Array.isArray(options.privilegeOrPrivileges) + ? options.privilegeOrPrivileges + : [options.privilegeOrPrivileges]), + ]), + }, + ], + }, + } + ); + + if (options.expectedResult) { + expect(errorThrown).toBeNull(); + expect(actualResult).toEqual(options.expectedResult); + } + + if (options.expectErrorThrown) { + expect(errorThrown).toMatchSnapshot(); + } + }); + }; + + checkPrivilegesAtSpacesTest( + 'successful when checking for login and user has login at both spaces', + { + spaceIds: ['space_1', 'space_2'], + privilegeOrPrivileges: mockActions.login, + 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, + }, + }, + }, + }, + expectedResult: { + hasAllRequested: true, + username: 'foo-username', + spacePrivileges: { + space_1: { + [mockActions.login]: true, + }, + space_2: { + [mockActions.login]: true, + }, + }, + }, + } + ); + + checkPrivilegesAtSpacesTest( + 'failure when checking for login and user has login at only one space', + { + spaceIds: ['space_1', 'space_2'], + privilegeOrPrivileges: mockActions.login, + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + }, + 'space:space_2': { + [mockActions.login]: false, + [mockActions.version]: true, + }, + }, + }, + }, + expectedResult: { + hasAllRequested: false, + username: 'foo-username', + spacePrivileges: { + space_1: { + [mockActions.login]: true, + }, + space_2: { + [mockActions.login]: false, + }, + }, + }, + } + ); + + checkPrivilegesAtSpacesTest( + `throws error when checking for login and user has login but doesn't have version`, + { + spaceIds: ['space_1', 'space_2'], + privilegeOrPrivileges: mockActions.login, + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: false, + }, + 'space:space_2': { + [mockActions.login]: true, + [mockActions.version]: false, + }, + }, + }, + }, + expectErrorThrown: true, + } + ); + + checkPrivilegesAtSpacesTest(`throws error when Elasticsearch returns malformed response`, { + spaceIds: ['space_1', 'space_2'], + privilegeOrPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [`saved_object:${savedObjectTypes[0]}/get`]: true, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, + 'space:space_2': { + [`saved_object:${savedObjectTypes[0]}/get`]: true, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, + }, + }, + }, + expectErrorThrown: true, + }); + + checkPrivilegesAtSpacesTest( + `successful when checking for two actions at two spaces and user has it all`, + { + spaceIds: ['space_1', 'space_2'], + privilegeOrPrivileges: [ + `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, + }, + }, + }, + }, + expectedResult: { + hasAllRequested: true, + username: 'foo-username', + spacePrivileges: { + space_1: { + [`saved_object:${savedObjectTypes[0]}/get`]: true, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, + space_2: { + [`saved_object:${savedObjectTypes[0]}/get`]: true, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, + }, + }, + } + ); + + checkPrivilegesAtSpacesTest( + `failure when checking for two actions at two spaces and user has one action at one space`, + { + spaceIds: ['space_1', 'space_2'], + privilegeOrPrivileges: [ + `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`]: false, + }, + 'space:space_2': { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: false, + [`saved_object:${savedObjectTypes[1]}/get`]: false, + }, + }, + }, + }, + expectedResult: { + hasAllRequested: false, + username: 'foo-username', + spacePrivileges: { + space_1: { + [`saved_object:${savedObjectTypes[0]}/get`]: true, + [`saved_object:${savedObjectTypes[1]}/get`]: false, + }, + space_2: { + [`saved_object:${savedObjectTypes[0]}/get`]: false, + [`saved_object:${savedObjectTypes[1]}/get`]: false, + }, + }, + }, + } + ); + + checkPrivilegesAtSpacesTest( + `failure when checking for two actions at two spaces and user has two actions at one space`, + { + spaceIds: ['space_1', 'space_2'], + privilegeOrPrivileges: [ + `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`]: false, + [`saved_object:${savedObjectTypes[1]}/get`]: false, + }, + }, + }, + }, + expectedResult: { + hasAllRequested: false, + username: 'foo-username', + spacePrivileges: { + space_1: { + [`saved_object:${savedObjectTypes[0]}/get`]: true, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, + space_2: { + [`saved_object:${savedObjectTypes[0]}/get`]: false, + [`saved_object:${savedObjectTypes[1]}/get`]: false, + }, + }, + }, + } + ); + + checkPrivilegesAtSpacesTest( + `failure when checking for two actions at two spaces and user has two actions at one space & one action at the other`, + { + spaceIds: ['space_1', 'space_2'], + privilegeOrPrivileges: [ + `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`]: false, + }, + }, + }, + }, + expectedResult: { + hasAllRequested: false, + username: 'foo-username', + spacePrivileges: { + space_1: { + [`saved_object:${savedObjectTypes[0]}/get`]: true, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, + space_2: { + [`saved_object:${savedObjectTypes[0]}/get`]: true, + [`saved_object:${savedObjectTypes[1]}/get`]: false, + }, + }, + }, + } + ); + + describe('with a malformed Elasticsearch response', () => { + checkPrivilegesAtSpacesTest( + `throws a validation error when an extra privilege is present in the response`, + { + spaceIds: ['space_1', 'space_2'], + privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/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`]: true, + }, + // @ts-ignore this is wrong on purpose + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: false, + }, + }, + }, + }, + expectErrorThrown: true, + } + ); + + checkPrivilegesAtSpacesTest( + `throws a validation error when privileges are missing in the response`, + { + spaceIds: ['space_1', 'space_2'], + privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + }, + // @ts-ignore this is wrong on purpose + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: false, + }, + }, + }, + }, + expectErrorThrown: true, + } + ); + + checkPrivilegesAtSpacesTest( + `throws a validation error when an extra space is present in the response`, + { + spaceIds: ['space_1', 'space_2'], + privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/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, + }, + 'space:space_2': { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: false, + }, + 'space:space_3': { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: false, + }, + }, + }, + }, + expectErrorThrown: true, + } + ); + + checkPrivilegesAtSpacesTest( + `throws a validation error when an a space is missing in the response`, + { + spaceIds: ['space_1', 'space_2'], + privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/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, + }, + }, + }, + }, + expectErrorThrown: true, + } + ); + }); +}); + +describe('#globally', () => { + const checkPrivilegesGloballyTest = ( + description: string, + options: { + privilegeOrPrivileges: string | string[]; + esHasPrivilegesResponse: HasPrivilegesResponse; + expectedResult?: any; + expectErrorThrown?: any; + } + ) => { + test(description, async () => { + const mockShieldClient = createMockShieldClient(options.esHasPrivilegesResponse); + const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory( + mockActions, + application, + mockShieldClient + ); + const request = { foo: Symbol() }; + const checkPrivileges = checkPrivilegesWithRequest(request); + + let actualResult; + let errorThrown = null; + try { + actualResult = await checkPrivileges.globally(options.privilegeOrPrivileges); + } catch (err) { + errorThrown = err; + } + + expect(mockShieldClient.callWithRequest).toHaveBeenCalledWith( + request, + 'shield.hasPrivileges', + { + body: { + applications: [ + { + application, + resources: [GLOBAL_RESOURCE], + privileges: uniq([ + mockActions.version, + mockActions.login, + ...(Array.isArray(options.privilegeOrPrivileges) + ? options.privilegeOrPrivileges + : [options.privilegeOrPrivileges]), + ]), + }, + ], + }, + } + ); + + if (options.expectedResult) { + expect(errorThrown).toBeNull(); + expect(actualResult).toEqual(options.expectedResult); + } + + if (options.expectErrorThrown) { + expect(errorThrown).toMatchSnapshot(); + } + }); + }; + + checkPrivilegesGloballyTest('successful when checking for login and user has login', { + privilegeOrPrivileges: mockActions.login, + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [mockActions.login]: true, + [mockActions.version]: true, + }, + }, + }, + }, + expectedResult: { + hasAllRequested: true, + username: 'foo-username', + privileges: { + [mockActions.login]: true, + }, + }, + }); + + checkPrivilegesGloballyTest(`failure when checking for login and user doesn't have login`, { + privilegeOrPrivileges: mockActions.login, + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [mockActions.login]: false, + [mockActions.version]: true, + }, + }, + }, + }, + expectedResult: { + hasAllRequested: false, + username: 'foo-username', + privileges: { + [mockActions.login]: false, + }, + }, + }); + + checkPrivilegesGloballyTest( + `throws error when checking for login and user has login but doesn't have version`, + { + privilegeOrPrivileges: mockActions.login, + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [mockActions.login]: true, + [mockActions.version]: false, + }, + }, + }, + }, + expectErrorThrown: true, + } + ); + + checkPrivilegesGloballyTest(`throws error when Elasticsearch returns malformed response`, { + privilegeOrPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [`saved_object:${savedObjectTypes[0]}/get`]: false, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, + }, + }, + }, + expectErrorThrown: true, + }); + + checkPrivilegesGloballyTest(`successful when checking for two actions and the user has both`, { + privilegeOrPrivileges: [ + `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, + }, + }, + }, + }, + expectedResult: { + hasAllRequested: true, + username: 'foo-username', + privileges: { + [`saved_object:${savedObjectTypes[0]}/get`]: true, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, + }, + }); + + checkPrivilegesGloballyTest(`failure when checking for two actions and the user has only one`, { + privilegeOrPrivileges: [ + `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`]: true, + }, + }, + }, + }, + expectedResult: { + hasAllRequested: false, + username: 'foo-username', + privileges: { + [`saved_object:${savedObjectTypes[0]}/get`]: false, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, + }, + }); + + describe('with a malformed Elasticsearch response', () => { + checkPrivilegesGloballyTest( + `throws a validation error when an extra privilege is present in the response`, + { + privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/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`]: true, + }, + }, + }, + }, + expectErrorThrown: true, + } + ); + + checkPrivilegesGloballyTest( + `throws a validation error when privileges are missing in the response`, + { + privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [mockActions.login]: true, + [mockActions.version]: true, + }, + }, + }, + }, + expectErrorThrown: true, + } + ); + }); +}); diff --git a/x-pack/plugins/security/server/lib/authorization/check_privileges.ts b/x-pack/plugins/security/server/lib/authorization/check_privileges.ts new file mode 100644 index 0000000000000..a23f89a4bd7a5 --- /dev/null +++ b/x-pack/plugins/security/server/lib/authorization/check_privileges.ts @@ -0,0 +1,168 @@ +/* + * 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 { pick, transform, uniq } from 'lodash'; +import { GLOBAL_RESOURCE } from '../../../common/constants'; +import { ResourceSerializer } from './resource_serializer'; +import { HasPrivilegesResponse, HasPrivilegesResponseApplication } from './types'; +import { validateEsPrivilegeResponse } from './validate_es_response'; + +interface CheckPrivilegesActions { + login: string; + version: string; +} + +interface CheckPrivilegesAtResourcesResponse { + hasAllRequested: boolean; + username: string; + resourcePrivileges: { + [resource: string]: { + [privilege: string]: boolean; + }; + }; +} + +export interface CheckPrivilegesAtResourceResponse { + hasAllRequested: boolean; + username: string; + privileges: { + [privilege: string]: boolean; + }; +} + +export interface CheckPrivilegesAtSpacesResponse { + hasAllRequested: boolean; + username: string; + spacePrivileges: { + [spaceId: string]: { + [privilege: string]: boolean; + }; + }; +} + +export type CheckPrivilegesWithRequest = (request: Record) => 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, + application: string, + shieldClient: any +) { + const { callWithRequest } = shieldClient; + + const hasIncompatibileVersion = ( + applicationPrivilegesResponse: HasPrivilegesResponseApplication + ) => { + return Object.values(applicationPrivilegesResponse).some( + resource => !resource[actions.version] && resource[actions.login] + ); + }; + + return function checkPrivilegesWithRequest(request: Record): CheckPrivileges { + const checkPrivilegesAtResources = async ( + resources: string[], + privilegeOrPrivileges: string | string[] + ): Promise => { + const privileges = Array.isArray(privilegeOrPrivileges) + ? privilegeOrPrivileges + : [privilegeOrPrivileges]; + const allApplicationPrivileges = uniq([actions.version, actions.login, ...privileges]); + + const hasPrivilegesResponse: HasPrivilegesResponse = await callWithRequest( + request, + 'shield.hasPrivileges', + { + body: { + applications: [ + { + application, + resources, + privileges: allApplicationPrivileges, + }, + ], + }, + } + ); + + validateEsPrivilegeResponse( + hasPrivilegesResponse, + application, + allApplicationPrivileges, + resources + ); + + const applicationPrivilegesResponse = hasPrivilegesResponse.application[application]; + + if (hasIncompatibileVersion(applicationPrivilegesResponse)) { + throw new Error( + 'Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.' + ); + } + + return { + hasAllRequested: hasPrivilegesResponse.has_all_requested, + username: hasPrivilegesResponse.username, + // we need to filter out the non requested privileges from the response + resourcePrivileges: transform(applicationPrivilegesResponse, (result, value, key) => { + result[key!] = pick(value, privileges); + }), + }; + }; + + const checkPrivilegesAtResource = async ( + resource: string, + privilegeOrPrivileges: string | string[] + ) => { + const { hasAllRequested, username, resourcePrivileges } = await checkPrivilegesAtResources( + [resource], + privilegeOrPrivileges + ); + return { + hasAllRequested, + username, + privileges: resourcePrivileges[resource], + }; + }; + + return { + async atSpace(spaceId: string, privilegeOrPrivileges: string | string[]) { + const spaceResource = ResourceSerializer.serializeSpaceResource(spaceId); + return await checkPrivilegesAtResource(spaceResource, privilegeOrPrivileges); + }, + async atSpaces(spaceIds: string[], privilegeOrPrivileges: string | string[]) { + const spaceResources = spaceIds.map(spaceId => + ResourceSerializer.serializeSpaceResource(spaceId) + ); + const { hasAllRequested, username, resourcePrivileges } = await checkPrivilegesAtResources( + spaceResources, + privilegeOrPrivileges + ); + return { + hasAllRequested, + username, + // we need to turn the resource responses back into the space ids + spacePrivileges: transform(resourcePrivileges, (result, value, key) => { + result[ResourceSerializer.deserializeSpaceResource(key!)] = value; + }), + }; + }, + async globally(privilegeOrPrivileges: string | string[]) { + return await checkPrivilegesAtResource(GLOBAL_RESOURCE, privilegeOrPrivileges); + }, + }; + }; +} diff --git a/x-pack/plugins/security/server/lib/authorization/check_privileges_dynamically.test.ts b/x-pack/plugins/security/server/lib/authorization/check_privileges_dynamically.test.ts new file mode 100644 index 0000000000000..9b76f554b7ee8 --- /dev/null +++ b/x-pack/plugins/security/server/lib/authorization/check_privileges_dynamically.test.ts @@ -0,0 +1,53 @@ +/* + * 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 { checkPrivilegesDynamicallyWithRequestFactory } from './check_privileges_dynamically'; + +test(`checkPrivileges.atSpace when spaces is enabled`, async () => { + const expectedResult = Symbol(); + const spaceId = 'foo-space'; + const mockCheckPrivileges = { + atSpace: jest.fn().mockReturnValue(expectedResult), + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockSpaces = { + isEnabled: true, + getSpaceId: jest.fn().mockReturnValue(spaceId), + }; + const request = Symbol(); + const privilegeOrPrivileges = ['foo', 'bar']; + const checkPrivilegesDynamically = checkPrivilegesDynamicallyWithRequestFactory( + mockCheckPrivilegesWithRequest, + mockSpaces + )(request as any); + const result = await checkPrivilegesDynamically(privilegeOrPrivileges); + + expect(result).toBe(expectedResult); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(request); + expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, privilegeOrPrivileges); +}); + +test(`checkPrivileges.globally when spaces is disabled`, async () => { + const expectedResult = Symbol(); + const mockCheckPrivileges = { + globally: jest.fn().mockReturnValue(expectedResult), + }; + const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockSpaces = { + isEnabled: false, + }; + const request = Symbol(); + const privilegeOrPrivileges = ['foo', 'bar']; + const checkPrivilegesDynamically = checkPrivilegesDynamicallyWithRequestFactory( + mockCheckPrivilegesWithRequest, + mockSpaces + )(request as any); + const result = await checkPrivilegesDynamically(privilegeOrPrivileges); + + expect(result).toBe(expectedResult); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(request); + expect(mockCheckPrivileges.globally).toHaveBeenCalledWith(privilegeOrPrivileges); +}); diff --git a/x-pack/plugins/security/server/lib/authorization/check_privileges_dynamically.ts b/x-pack/plugins/security/server/lib/authorization/check_privileges_dynamically.ts new file mode 100644 index 0000000000000..02e25dc3e6862 --- /dev/null +++ b/x-pack/plugins/security/server/lib/authorization/check_privileges_dynamically.ts @@ -0,0 +1,38 @@ +/* + * 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 { CheckPrivilegesAtResourceResponse, CheckPrivilegesWithRequest } from './check_privileges'; + +/* + * 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 type CheckPrivilegesDynamically = ( + privilegeOrPrivileges: string | string[] +) => Promise; + +export type CheckPrivilegesDynamicallyWithRequest = ( + request: Record +) => CheckPrivilegesDynamically; + +export function checkPrivilegesDynamicallyWithRequestFactory( + checkPrivilegesWithRequest: CheckPrivilegesWithRequest, + spaces: any +): CheckPrivilegesDynamicallyWithRequest { + return function checkPrivilegesDynamicallyWithRequest(request: Record) { + const checkPrivileges = checkPrivilegesWithRequest(request); + return async function checkPrivilegesDynamically(privilegeOrPrivileges: string | string[]) { + if (spaces.isEnabled) { + const spaceId = spaces.getSpaceId(request); + return await checkPrivileges.atSpace(spaceId, privilegeOrPrivileges); + } else { + return await checkPrivileges.globally(privilegeOrPrivileges); + } + }; + }; +} diff --git a/x-pack/plugins/security/server/lib/authorization/disable_ui_capabilities.test.ts b/x-pack/plugins/security/server/lib/authorization/disable_ui_capabilities.test.ts new file mode 100644 index 0000000000000..72aafa034f423 --- /dev/null +++ b/x-pack/plugins/security/server/lib/authorization/disable_ui_capabilities.test.ts @@ -0,0 +1,400 @@ +/* + * 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 { Actions } from '.'; +import { Feature } from '../../../../xpack_main/types'; +import { disableUICapabilitesFactory } from './disable_ui_capabilities'; + +interface MockServerOptions { + checkPrivileges: { + reject?: any; + resolve?: any; + }; + features: Feature[]; +} + +const actions = new Actions('1.0.0-zeta1'); +const mockRequest = { + foo: Symbol(), +}; + +const createMockServer = (options: MockServerOptions) => { + const mockAuthorizationService = { + actions, + checkPrivilegesDynamicallyWithRequest(request: any) { + expect(request).toBe(mockRequest); + + return jest.fn().mockImplementation(checkActions => { + if (options.checkPrivileges.reject) { + throw options.checkPrivileges.reject; + } + + if (options.checkPrivileges.resolve) { + expect(checkActions).toEqual(Object.keys(options.checkPrivileges.resolve.privileges)); + return options.checkPrivileges.resolve; + } + + throw new Error('resolve or reject should have been provided'); + }); + }, + }; + + const mockXPackMainPlugin = { + getFeatures: jest.fn().mockReturnValue(options.features), + }; + + return { + log: jest.fn(), + plugins: { + security: { + authorization: mockAuthorizationService, + }, + xpack_main: mockXPackMainPlugin, + }, + }; +}; + +describe('usingPrivileges', () => { + describe('checkPrivileges errors', () => { + test(`disables uiCapabilities when a 401 is thrown`, async () => { + const mockServer = createMockServer({ + checkPrivileges: { + reject: { + statusCode: 401, + message: 'super informative message', + }, + }, + features: [ + { + id: 'fooFeature', + name: 'Foo Feature', + navLinkId: 'foo', + privileges: {}, + }, + ], + }); + const { usingPrivileges } = disableUICapabilitesFactory(mockServer, mockRequest); + const result = await usingPrivileges( + Object.freeze({ + navLinks: { + foo: true, + bar: true, + }, + fooFeature: { + foo: true, + bar: true, + }, + barFeature: { + foo: true, + bar: true, + }, + }) + ); + + expect(result).toEqual({ + navLinks: { + foo: false, + bar: true, + }, + fooFeature: { + foo: false, + bar: false, + }, + barFeature: { + foo: false, + bar: false, + }, + }); + + expect(mockServer.log).toMatchInlineSnapshot(` +[MockFunction] { + "calls": Array [ + Array [ + Array [ + "security", + "debug", + ], + "Disabling all uiCapabilities because we received a 401: super informative message", + ], + ], + "results": Array [ + Object { + "isThrow": false, + "value": undefined, + }, + ], +} +`); + }); + + test(`disables uiCapabilities when a 403 is thrown`, async () => { + const mockServer = createMockServer({ + checkPrivileges: { + reject: { + statusCode: 403, + message: 'even more super informative message', + }, + }, + features: [ + { + id: 'fooFeature', + name: 'Foo Feature', + navLinkId: 'foo', + privileges: {}, + }, + ], + }); + const { usingPrivileges } = disableUICapabilitesFactory(mockServer, mockRequest); + const result = await usingPrivileges( + Object.freeze({ + navLinks: { + foo: true, + bar: true, + }, + fooFeature: { + foo: true, + bar: true, + }, + barFeature: { + foo: true, + bar: true, + }, + }) + ); + + expect(result).toEqual({ + navLinks: { + foo: false, + bar: true, + }, + fooFeature: { + foo: false, + bar: false, + }, + barFeature: { + foo: false, + bar: false, + }, + }); + expect(mockServer.log).toMatchInlineSnapshot(` +[MockFunction] { + "calls": Array [ + Array [ + Array [ + "security", + "debug", + ], + "Disabling all uiCapabilities because we received a 403: even more super informative message", + ], + ], + "results": Array [ + Object { + "isThrow": false, + "value": undefined, + }, + ], +} +`); + }); + + test(`otherwise it throws the error`, async () => { + const mockServer = createMockServer({ + checkPrivileges: { + reject: new Error('something else entirely'), + }, + features: [], + }); + const { usingPrivileges } = disableUICapabilitesFactory(mockServer, mockRequest); + await expect( + usingPrivileges({ + navLinks: { + foo: true, + bar: false, + }, + }) + ).rejects.toThrowErrorMatchingSnapshot(); + expect(mockServer.log).not.toHaveBeenCalled(); + }); + }); + + test(`disables ui capabilities when they don't have privileges`, async () => { + const mockServer = createMockServer({ + checkPrivileges: { + resolve: { + privileges: { + [actions.ui.get('navLinks', 'foo')]: true, + [actions.ui.get('navLinks', 'bar')]: false, + [actions.ui.get('navLinks', 'quz')]: false, + [actions.ui.get('fooFeature', 'foo')]: true, + [actions.ui.get('fooFeature', 'bar')]: false, + [actions.ui.get('barFeature', 'foo')]: true, + [actions.ui.get('barFeature', 'bar')]: false, + }, + }, + }, + features: [ + { + id: 'fooFeature', + name: 'Foo Feature', + navLinkId: 'foo', + privileges: {}, + }, + { + id: 'barFeature', + name: 'Bar Feature', + navLinkId: 'bar', + privileges: {}, + }, + ], + }); + const { usingPrivileges } = disableUICapabilitesFactory(mockServer, mockRequest); + const result = await usingPrivileges( + Object.freeze({ + navLinks: { + foo: true, + bar: true, + quz: true, + }, + fooFeature: { + foo: true, + bar: true, + }, + barFeature: { + foo: true, + bar: true, + }, + }) + ); + + expect(result).toEqual({ + navLinks: { + foo: true, + bar: false, + quz: true, + }, + fooFeature: { + foo: true, + bar: false, + }, + barFeature: { + foo: true, + bar: false, + }, + }); + }); + + test(`doesn't re-enable disabled uiCapabilities`, async () => { + const mockServer = createMockServer({ + checkPrivileges: { + resolve: { + privileges: { + [actions.ui.get('navLinks', 'foo')]: true, + [actions.ui.get('navLinks', 'bar')]: true, + [actions.ui.get('fooFeature', 'foo')]: true, + [actions.ui.get('fooFeature', 'bar')]: true, + [actions.ui.get('barFeature', 'foo')]: true, + [actions.ui.get('barFeature', 'bar')]: true, + }, + }, + }, + features: [ + { + id: 'fooFeature', + name: 'Foo Feature', + navLinkId: 'foo', + privileges: {}, + }, + { + id: 'barFeature', + name: 'Bar Feature', + navLinkId: 'bar', + privileges: {}, + }, + ], + }); + const { usingPrivileges } = disableUICapabilitesFactory(mockServer, mockRequest); + const result = await usingPrivileges( + Object.freeze({ + navLinks: { + foo: false, + bar: false, + }, + fooFeature: { + foo: false, + bar: false, + }, + barFeature: { + foo: false, + bar: false, + }, + }) + ); + + expect(result).toEqual({ + navLinks: { + foo: false, + bar: false, + }, + fooFeature: { + foo: false, + bar: false, + }, + barFeature: { + foo: false, + bar: false, + }, + }); + }); +}); + +describe('all', () => { + test(`disables uiCapabilities`, () => { + const mockServer = createMockServer({ + checkPrivileges: { + reject: new Error(`Don't use me`), + }, + features: [ + { + id: 'fooFeature', + name: 'Foo Feature', + navLinkId: 'foo', + privileges: {}, + }, + ], + }); + const { all } = disableUICapabilitesFactory(mockServer, mockRequest); + const result = all( + Object.freeze({ + navLinks: { + foo: true, + bar: true, + }, + fooFeature: { + foo: true, + bar: true, + }, + barFeature: { + foo: true, + bar: true, + }, + }) + ); + expect(result).toEqual({ + navLinks: { + foo: false, + bar: true, + }, + fooFeature: { + foo: false, + bar: false, + }, + barFeature: { + foo: false, + bar: false, + }, + }); + }); +}); diff --git a/x-pack/plugins/security/server/lib/authorization/disable_ui_capabilities.ts b/x-pack/plugins/security/server/lib/authorization/disable_ui_capabilities.ts new file mode 100644 index 0000000000000..d1c704fedf6eb --- /dev/null +++ b/x-pack/plugins/security/server/lib/authorization/disable_ui_capabilities.ts @@ -0,0 +1,102 @@ +/* + * 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 { mapValues } from 'lodash'; +import { UICapabilities } from 'ui/capabilities'; +import { Feature } from '../../../../xpack_main/types'; +import { Actions } from './actions'; +import { CheckPrivilegesAtResourceResponse } from './check_privileges'; +import { CheckPrivilegesDynamically } from './check_privileges_dynamically'; + +export function disableUICapabilitesFactory( + server: Record, + request: Record +) { + const { + security: { authorization }, + xpack_main: xpackMainPlugin, + } = server.plugins; + + const features: Feature[] = xpackMainPlugin.getFeatures(); + const featureNavLinkIds = features + .map(feature => feature.navLinkId) + .filter(navLinkId => navLinkId != null); + + const actions: Actions = authorization.actions; + const shouldDisableFeatureUICapability = ( + featureId: keyof UICapabilities, + uiCapability: string + ) => { + // if the navLink isn't for a feature that we have registered, we don't wish to + // disable it based on privileges + return featureId !== 'navLinks' || featureNavLinkIds.includes(uiCapability); + }; + + const disableAll = (uiCapabilities: UICapabilities) => { + return mapValues(uiCapabilities, (featureUICapabilities, featureId) => + mapValues(featureUICapabilities, (enabled, uiCapability) => { + if (shouldDisableFeatureUICapability(featureId!, uiCapability!)) { + return false; + } + + return enabled; + }) + ); + }; + + const usingPrivileges = async (uiCapabilities: UICapabilities) => { + const uiActions = Object.entries(uiCapabilities).reduce( + (acc, [featureId, featureUICapabilities]) => [ + ...acc, + ...Object.keys(featureUICapabilities).map(uiCapability => + actions.ui.get(featureId, uiCapability) + ), + ], + [] + ); + + let checkPrivilegesResponse: CheckPrivilegesAtResourceResponse; + try { + const checkPrivilegesDynamically: CheckPrivilegesDynamically = authorization.checkPrivilegesDynamicallyWithRequest( + request + ); + checkPrivilegesResponse = await checkPrivilegesDynamically(uiActions); + } 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 + // login screen, which isn't driven any uiCapabilities + if (err.statusCode === 401 || err.statusCode === 403) { + server.log( + ['security', 'debug'], + `Disabling all uiCapabilities because we received a ${err.statusCode}: ${err.message}` + ); + return disableAll(uiCapabilities); + } + throw err; + } + + return mapValues(uiCapabilities, (featureUICapabilities, featureId) => { + return mapValues(featureUICapabilities, (enabled, uiCapability) => { + if (!shouldDisableFeatureUICapability(featureId!, uiCapability!)) { + return enabled; + } + + // if the uiCapability has already been disabled, we don't want to re-enable it + if (!enabled) { + return false; + } + + const action = actions.ui.get(featureId!, uiCapability!); + return checkPrivilegesResponse.privileges[action] === true; + }); + }); + }; + + return { + all: disableAll, + usingPrivileges, + }; +} diff --git a/x-pack/plugins/security/server/lib/authorization/index.js b/x-pack/plugins/security/server/lib/authorization/index.ts similarity index 78% rename from x-pack/plugins/security/server/lib/authorization/index.js rename to x-pack/plugins/security/server/lib/authorization/index.ts index 8bf109547f188..69e31b8d48445 100644 --- a/x-pack/plugins/security/server/lib/authorization/index.js +++ b/x-pack/plugins/security/server/lib/authorization/index.ts @@ -4,7 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -export { registerPrivilegesWithCluster } from './register_privileges_with_cluster'; +export { Actions } from './actions'; +// @ts-ignore export { createAuthorizationService } from './service'; +export { disableUICapabilitesFactory } from './disable_ui_capabilities'; export { PrivilegeSerializer } from './privilege_serializer'; +// @ts-ignore +export { registerPrivilegesWithCluster } from './register_privileges_with_cluster'; export { ResourceSerializer } from './resource_serializer'; diff --git a/x-pack/plugins/security/server/lib/authorization/service.js b/x-pack/plugins/security/server/lib/authorization/service.js index a081e17a9a96f..4d0e6e8fc8290 100644 --- a/x-pack/plugins/security/server/lib/authorization/service.js +++ b/x-pack/plugins/security/server/lib/authorization/service.js @@ -8,15 +8,17 @@ import { actionsFactory } from './actions'; import { authorizationModeFactory } from './mode'; import { privilegesFactory } from './privileges'; import { checkPrivilegesWithRequestFactory } from './check_privileges'; +import { checkPrivilegesDynamicallyWithRequestFactory } from './check_privileges_dynamically'; import { getClient } from '../../../../../server/lib/get_client_shield'; -export function createAuthorizationService(server, xpackInfoFeature, savedObjectTypes, xpackMainPlugin) { +export function createAuthorizationService(server, xpackInfoFeature, savedObjectTypes, xpackMainPlugin, spaces) { const shieldClient = getClient(server); const config = server.config(); const actions = actionsFactory(config); const application = `kibana-${config.get('kibana.index')}`; const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory(actions, application, shieldClient); + const checkPrivilegesDynamicallyWithRequest = checkPrivilegesDynamicallyWithRequestFactory(checkPrivilegesWithRequest, spaces); const mode = authorizationModeFactory( application, config, @@ -30,6 +32,7 @@ export function createAuthorizationService(server, xpackInfoFeature, savedObject actions, application, checkPrivilegesWithRequest, + checkPrivilegesDynamicallyWithRequest, mode, privileges, }; diff --git a/x-pack/plugins/security/server/lib/authorization/service.test.js b/x-pack/plugins/security/server/lib/authorization/service.test.js index 7fdccacc41362..f31ba1d5d0b38 100644 --- a/x-pack/plugins/security/server/lib/authorization/service.test.js +++ b/x-pack/plugins/security/server/lib/authorization/service.test.js @@ -8,6 +8,7 @@ import { createAuthorizationService } from './service'; import { actionsFactory } from './actions'; import { privilegesFactory } from './privileges'; import { checkPrivilegesWithRequestFactory } from './check_privileges'; +import { checkPrivilegesDynamicallyWithRequestFactory } from './check_privileges_dynamically'; import { getClient } from '../../../../../server/lib/get_client_shield'; import { authorizationModeFactory } from './mode'; @@ -15,6 +16,10 @@ jest.mock('./check_privileges', () => ({ checkPrivilegesWithRequestFactory: jest.fn(), })); +jest.mock('./check_privileges_dynamically', () => ({ + checkPrivilegesDynamicallyWithRequestFactory: jest.fn(), +})); + jest.mock('../../../../../server/lib/get_client_shield', () => ({ getClient: jest.fn(), })); @@ -57,6 +62,8 @@ test(`returns exposed services`, () => { getClient.mockReturnValue(mockShieldClient); const mockCheckPrivilegesWithRequest = Symbol(); checkPrivilegesWithRequestFactory.mockReturnValue(mockCheckPrivilegesWithRequest); + const mockCheckPrivilegesDynamicallyWithRequest = Symbol(); + checkPrivilegesDynamicallyWithRequestFactory.mockReturnValue(mockCheckPrivilegesDynamicallyWithRequest); const mockActions = Symbol(); actionsFactory.mockReturnValue(mockActions); mockConfig.get.mock; @@ -70,13 +77,15 @@ test(`returns exposed services`, () => { privilegesFactory.mockReturnValue(mockPrivilegesService); const mockAuthorizationMode = Symbol(); authorizationModeFactory.mockReturnValue(mockAuthorizationMode); + const mockSpaces = Symbol(); - const authorization = createAuthorizationService(mockServer, mockXpackInfoFeature, mockSavedObjectTypes, mockXpackMainPlugin); + const authorization = createAuthorizationService(mockServer, mockXpackInfoFeature, mockSavedObjectTypes, mockXpackMainPlugin, mockSpaces); const application = `kibana-${kibanaIndex}`; expect(getClient).toHaveBeenCalledWith(mockServer); expect(actionsFactory).toHaveBeenCalledWith(mockConfig); expect(checkPrivilegesWithRequestFactory).toHaveBeenCalledWith(mockActions, application, mockShieldClient); + expect(checkPrivilegesDynamicallyWithRequestFactory).toHaveBeenCalledWith(mockCheckPrivilegesWithRequest, mockSpaces); expect(privilegesFactory).toHaveBeenCalledWith(mockSavedObjectTypes, mockActions, mockXpackMainPlugin); expect(authorizationModeFactory).toHaveBeenCalledWith( application, @@ -90,6 +99,7 @@ test(`returns exposed services`, () => { actions: mockActions, application, checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + checkPrivilegesDynamicallyWithRequest: mockCheckPrivilegesDynamicallyWithRequest, mode: mockAuthorizationMode, privileges: mockPrivilegesService, }); diff --git a/x-pack/plugins/security/server/lib/authorization/types.ts b/x-pack/plugins/security/server/lib/authorization/types.ts new file mode 100644 index 0000000000000..75188d1191b1a --- /dev/null +++ b/x-pack/plugins/security/server/lib/authorization/types.ts @@ -0,0 +1,19 @@ +/* + * 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 interface HasPrivilegesResponseApplication { + [resource: string]: { + [privilegeName: string]: boolean; + }; +} + +export interface HasPrivilegesResponse { + has_all_requested: boolean; + username: string; + application: { + [applicationName: string]: HasPrivilegesResponseApplication; + }; +} diff --git a/x-pack/plugins/security/server/lib/authorization/validate_es_response.test.js b/x-pack/plugins/security/server/lib/authorization/validate_es_response.test.ts similarity index 69% rename from x-pack/plugins/security/server/lib/authorization/validate_es_response.test.js rename to x-pack/plugins/security/server/lib/authorization/validate_es_response.test.ts index a7fba14229de2..2e0d1b8501e65 100644 --- a/x-pack/plugins/security/server/lib/authorization/validate_es_response.test.js +++ b/x-pack/plugins/security/server/lib/authorization/validate_es_response.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { validateEsPrivilegeResponse } from "./validate_es_response"; +import { validateEsPrivilegeResponse } from './validate_es_response'; const resource1 = 'foo-resource'; const resource2 = 'bar-resource'; @@ -16,7 +16,6 @@ const commonResponse = { }; describe('validateEsPrivilegeResponse', () => { - it('should validate a proper response', () => { const response = { ...commonResponse, @@ -25,18 +24,23 @@ describe('validateEsPrivilegeResponse', () => { [resource1]: { action1: true, action2: true, - action3: true + action3: true, }, [resource2]: { action1: true, action2: true, - action3: true - } - } - } + action3: true, + }, + }, + }, }; - const result = validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource1, resource2]); + const result = validateEsPrivilegeResponse( + response, + application, + ['action1', 'action2', 'action3'], + [resource1, resource2] + ); expect(result).toEqual(response); }); @@ -47,19 +51,24 @@ describe('validateEsPrivilegeResponse', () => { [application]: { [resource1]: { action1: true, - action3: true + action3: true, }, [resource2]: { action1: true, action2: true, - action3: true - } - } - } + action3: true, + }, + }, + }, }; expect(() => - validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource1, resource2]) + validateEsPrivilegeResponse( + response, + application, + ['action1', 'action2', 'action3'], + [resource1, resource2] + ) ).toThrowErrorMatchingSnapshot(); }); @@ -77,14 +86,19 @@ describe('validateEsPrivilegeResponse', () => { [resource2]: { action1: true, action2: true, - action3: true - } - } - } + action3: true, + }, + }, + }, }; expect(() => - validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource1, resource2]) + validateEsPrivilegeResponse( + response, + application, + ['action1', 'action2', 'action3'], + [resource1, resource2] + ) ).toThrowErrorMatchingSnapshot(); }); @@ -102,13 +116,18 @@ describe('validateEsPrivilegeResponse', () => { action1: true, action2: true, action3: true, - } - } + }, + }, }, }; expect(() => - validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource1, resource2]) + validateEsPrivilegeResponse( + response as any, + application, + ['action1', 'action2', 'action3'], + [resource1, resource2] + ) ).toThrowErrorMatchingSnapshot(); }); @@ -126,7 +145,7 @@ describe('validateEsPrivilegeResponse', () => { action1: true, action2: true, action3: true, - } + }, }, otherApplication: { [resource1]: { @@ -138,13 +157,18 @@ describe('validateEsPrivilegeResponse', () => { action1: true, action2: true, action3: true, - } - } + }, + }, }, }; expect(() => - validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource1, resource2]) + validateEsPrivilegeResponse( + response, + application, + ['action1', 'action2', 'action3'], + [resource1, resource2] + ) ).toThrowErrorMatchingSnapshot(); }); @@ -155,18 +179,28 @@ describe('validateEsPrivilegeResponse', () => { }; expect(() => - validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource1, resource2]) + validateEsPrivilegeResponse( + response, + application, + ['action1', 'action2', 'action3'], + [resource1, resource2] + ) ).toThrowErrorMatchingSnapshot(); }); it('fails validation when the "application" property is missing from the response', () => { const response = { ...commonResponse, - index: {} + index: {}, }; expect(() => - validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource1, resource2]) + validateEsPrivilegeResponse( + response as any, + application, + ['action1', 'action2', 'action3'], + [resource1, resource2] + ) ).toThrowErrorMatchingSnapshot(); }); @@ -180,12 +214,17 @@ describe('validateEsPrivilegeResponse', () => { action2: true, action3: true, }, - } + }, }, }; expect(() => - validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource1, resource2]) + validateEsPrivilegeResponse( + response, + application, + ['action1', 'action2', 'action3'], + [resource1, resource2] + ) ).toThrowErrorMatchingSnapshot(); }); @@ -193,13 +232,17 @@ describe('validateEsPrivilegeResponse', () => { const response = { ...commonResponse, application: { - [application]: { - } + [application]: {}, }, }; expect(() => - validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource1, resource2]) + validateEsPrivilegeResponse( + response, + application, + ['action1', 'action2', 'action3'], + [resource1, resource2] + ) ).toThrowErrorMatchingSnapshot(); }); @@ -217,13 +260,18 @@ describe('validateEsPrivilegeResponse', () => { action1: true, action2: true, action3: true, - } - } + }, + }, }, }; expect(() => - validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource1, resource2]) + validateEsPrivilegeResponse( + response, + application, + ['action1', 'action2', 'action3'], + [resource1, resource2] + ) ).toThrowErrorMatchingSnapshot(); }); @@ -238,12 +286,17 @@ describe('validateEsPrivilegeResponse', () => { action2: true, action3: true, }, - } + }, }, }; expect(() => - validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource1, resource2]) + validateEsPrivilegeResponse( + response as any, + application, + ['action1', 'action2', 'action3'], + [resource1, resource2] + ) ).toThrowErrorMatchingSnapshot(); }); }); diff --git a/x-pack/plugins/security/server/lib/authorization/validate_es_response.js b/x-pack/plugins/security/server/lib/authorization/validate_es_response.ts similarity index 61% rename from x-pack/plugins/security/server/lib/authorization/validate_es_response.js rename to x-pack/plugins/security/server/lib/authorization/validate_es_response.ts index 2819983cf43c8..8d8e3125266a2 100644 --- a/x-pack/plugins/security/server/lib/authorization/validate_es_response.js +++ b/x-pack/plugins/security/server/lib/authorization/validate_es_response.ts @@ -5,40 +5,47 @@ */ import Joi from 'joi'; - -export function validateEsPrivilegeResponse(response, application, actions, resources) { +import { HasPrivilegesResponse } from './types'; + +export function validateEsPrivilegeResponse( + response: HasPrivilegesResponse, + application: string, + actions: string[], + resources: string[] +) { const schema = buildValidationSchema(application, actions, resources); const { error, value } = schema.validate(response); if (error) { - throw new Error(`Invalid response received from Elasticsearch has_privilege endpoint. ${error}`); + throw new Error( + `Invalid response received from Elasticsearch has_privilege endpoint. ${error}` + ); } return value; } -function buildActionsValidationSchema(actions) { +function buildActionsValidationSchema(actions: string[]) { return Joi.object({ - ...actions.reduce((acc, action) => { + ...actions.reduce>((acc, action) => { return { ...acc, - [action]: Joi.bool().required() + [action]: Joi.bool().required(), }; - }, {}) + }, {}), }).required(); } -function buildValidationSchema(application, actions, resources) { - +function buildValidationSchema(application: string, actions: string[], resources: string[]) { const actionValidationSchema = buildActionsValidationSchema(actions); const resourceValidationSchema = Joi.object({ ...resources.reduce((acc, resource) => { return { ...acc, - [resource]: actionValidationSchema + [resource]: actionValidationSchema, }; - }, {}) + }, {}), }).required(); return Joi.object({ diff --git a/x-pack/plugins/security/server/lib/capability_decorator.ts b/x-pack/plugins/security/server/lib/capability_decorator.ts new file mode 100644 index 0000000000000..f98852f7090a3 --- /dev/null +++ b/x-pack/plugins/security/server/lib/capability_decorator.ts @@ -0,0 +1,61 @@ +/* + * 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 _ from 'lodash'; +import { UICapabilities } from 'ui/capabilities'; + +export async function capabilityDecorator( + server: Record, + request: Record, + capabilities: UICapabilities +) { + if (!isAuthenticatedRoute(request)) { + return capabilities; + } + + const { checkPrivilegesWithRequest, actions } = server.plugins.security.authorization; + + const checkPrivileges = checkPrivilegesWithRequest(request); + + const privilegedActions = getPrivilegedActions(server, actions); + + const { spaces } = server.plugins; + + let result; + if (spaces) { + result = await checkPrivileges.atSpace(spaces.getSpaceId(request), privilegedActions); + } else { + result = await checkPrivileges.globally(privilegedActions); + } + + return { + ...capabilities, + ...result.privileges, + }; +} + +function isAuthenticatedRoute(request: Record) { + const { settings } = request.route; + return settings.auth !== false; +} + +function getPrivilegedActions(server: Record, actions: Record) { + const uiApps = server.getAllUiApps(); + + const navLinkSpecs = server.getUiNavLinks(); + + const uiCapabilityActions = [...uiApps, ...navLinkSpecs].map(entry => `ui:${entry._id}/read`); + + const { types } = server.savedObjects; + + const savedObjectsActions = _.flatten( + types.map((type: string) => [ + actions.getSavedObjectAction(type, 'read'), + actions.getSavedObjectAction(type, 'create'), + ]) + ); + + return [...uiCapabilityActions, ...savedObjectsActions]; +} diff --git a/x-pack/plugins/security/server/lib/optional_plugin.test.ts b/x-pack/plugins/security/server/lib/optional_plugin.test.ts new file mode 100644 index 0000000000000..3f3ca1fa3926c --- /dev/null +++ b/x-pack/plugins/security/server/lib/optional_plugin.test.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 { createOptionalPlugin } from './optional_plugin'; + +class FooPlugin { + get aProp() { + return 'a prop'; + } + + public aField = 'a field'; + + public aMethod() { + return 'a method'; + } +} + +const createMockConfig = (settings: Record) => { + return { + get: (key: string) => { + if (!Object.keys(settings).includes(key)) { + throw new Error('Unknown config key'); + } + + return settings[key]; + }, + }; +}; + +describe('isEnabled', () => { + test('returns true when config.get(`${configPrefix}.enabled`) is true', () => { + const config = createMockConfig({ 'xpack.fooPlugin.enabled': true }); + const conditionalFooPlugin = createOptionalPlugin(config, 'xpack.fooPlugin', {}, 'fooPlugin'); + expect(conditionalFooPlugin.isEnabled).toBe(true); + }); + + test('returns false when config.get(`${configPrefix}.enabled`) is false', () => { + const config = createMockConfig({ 'xpack.fooPlugin.enabled': false }); + const conditionalFooPlugin = createOptionalPlugin(config, 'xpack.fooPlugin', {}, 'fooPlugin'); + expect(conditionalFooPlugin.isEnabled).toBe(false); + }); +}); + +test(`throws error when invoked before it's available`, () => { + const config = createMockConfig({ 'xpack.fooPlugin.enabled': true }); + const conditionalFooPlugin = createOptionalPlugin( + config, + 'xpack.fooPlugin', + {}, + 'fooPlugin' + ); + expect(() => conditionalFooPlugin.aProp).toThrowErrorMatchingSnapshot(); + expect(() => conditionalFooPlugin.aMethod()).toThrowErrorMatchingSnapshot(); + expect(() => conditionalFooPlugin.aField).toThrowErrorMatchingSnapshot(); +}); + +test(`throws error when invoked when it's not enabled`, () => { + const config = createMockConfig({ 'xpack.fooPlugin.enabled': false }); + const conditionalFooPlugin = createOptionalPlugin( + config, + 'xpack.fooPlugin', + {}, + 'fooPlugin' + ); + expect(() => conditionalFooPlugin.aProp).toThrowErrorMatchingSnapshot(); + expect(() => conditionalFooPlugin.aMethod()).toThrowErrorMatchingSnapshot(); + expect(() => conditionalFooPlugin.aField).toThrowErrorMatchingSnapshot(); +}); + +test(`behaves normally when it's enabled and available`, () => { + const config = createMockConfig({ 'xpack.fooPlugin.enabled': false }); + const conditionalFooPlugin = createOptionalPlugin( + config, + 'xpack.fooPlugin', + { + fooPlugin: new FooPlugin(), + }, + 'fooPlugin' + ); + expect(conditionalFooPlugin.aProp).toBe('a prop'); + expect(conditionalFooPlugin.aMethod()).toBe('a method'); + expect(conditionalFooPlugin.aField).toBe('a field'); +}); diff --git a/x-pack/plugins/security/server/lib/optional_plugin.ts b/x-pack/plugins/security/server/lib/optional_plugin.ts new file mode 100644 index 0000000000000..841d99f809222 --- /dev/null +++ b/x-pack/plugins/security/server/lib/optional_plugin.ts @@ -0,0 +1,46 @@ +/* + * 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. + */ + +interface Config { + get(key: string): any; +} + +interface Plugins { + [key: string]: any; +} + +export interface OptionalPlugin { + isEnabled: boolean; +} + +export function createOptionalPlugin( + config: Config, + configPrefix: string, + plugins: Plugins, + pluginId: string +): OptionalPlugin & T { + return new Proxy( + {}, + { + get(obj, prop) { + const isEnabled = config.get(`${configPrefix}.enabled`); + if (prop === 'isEnabled') { + return isEnabled; + } + + if (!plugins[pluginId] && isEnabled) { + throw new Error(`Plugin accessed before it's available`); + } + + if (!plugins[pluginId] && !isEnabled) { + throw new Error(`Plugin isn't enabled, check isEnabled before using`); + } + + return plugins[pluginId][prop]; + }, + } + ) as OptionalPlugin & T; +} diff --git a/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client_wrapper.js b/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client_wrapper.js index 90e2305298901..56ae43178e4f8 100644 --- a/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client_wrapper.js +++ b/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client_wrapper.js @@ -12,21 +12,19 @@ export class SecureSavedObjectsClientWrapper { actions, auditLogger, baseClient, - checkPrivilegesWithRequest, + checkPrivilegesDynamicallyWithRequest, errors, request, savedObjectTypes, - spaces, } = options; this.errors = errors; this._actions = actions; this._auditLogger = auditLogger; this._baseClient = baseClient; - this._checkPrivileges = checkPrivilegesWithRequest(request); + this._checkPrivileges = checkPrivilegesDynamicallyWithRequest(request); this._request = request; this._savedObjectTypes = savedObjectTypes; - this._spaces = spaces; } async create(type, attributes = {}, options = {}) { @@ -103,13 +101,7 @@ export class SecureSavedObjectsClientWrapper { async _checkSavedObjectPrivileges(actions) { try { - if (this._spaces) { - const spaceId = this._spaces.getSpaceId(this._request); - return await this._checkPrivileges.atSpace(spaceId, actions); - } - else { - return await this._checkPrivileges.globally(actions); - } + return await this._checkPrivileges(actions); } catch(error) { const { reason } = get(error, 'body.error', {}); throw this.errors.decorateGeneralError(error, reason); diff --git a/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client_wrapper.test.js b/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client_wrapper.test.js index bb53fc20d80eb..7700306d8f09c 100644 --- a/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client_wrapper.test.js +++ b/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client_wrapper.test.js @@ -40,7 +40,7 @@ describe('#errors', () => { const errors = Symbol(); const client = new SecureSavedObjectsClientWrapper({ - checkPrivilegesWithRequest: () => {}, + checkPrivilegesDynamicallyWithRequest: () => {}, errors }); @@ -53,12 +53,10 @@ describe(`spaces disabled`, () => { test(`throws decorated GeneralError when checkPrivileges.globally rejects promise`, async () => { const type = 'foo'; const mockErrors = createMockErrors(); - const mockCheckPrivileges = { - globally: jest.fn(async () => { - throw new Error('An actual error would happen here'); - }) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockCheckPrivilegesDynamically = jest.fn(async () => { + throw new Error('An actual error would happen here'); + }); + const mockCheckPrivilegesDynamicallyWithRequest = jest.fn().mockReturnValue(mockCheckPrivilegesDynamically); const mockRequest = Symbol(); const mockAuditLogger = createMockAuditLogger(); const mockActions = createMockActions(); @@ -66,7 +64,7 @@ describe(`spaces disabled`, () => { actions: mockActions, auditLogger: mockAuditLogger, baseClient: null, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + checkPrivilegesDynamicallyWithRequest: mockCheckPrivilegesDynamicallyWithRequest, errors: mockErrors, request: mockRequest, savedObjectTypes: [], @@ -74,8 +72,8 @@ describe(`spaces disabled`, () => { }); await expect(client.create(type)).rejects.toThrowError(mockErrors.generalError); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'create')]); + expect(mockCheckPrivilegesDynamicallyWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivilegesDynamically).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'create')]); expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); @@ -86,23 +84,21 @@ describe(`spaces disabled`, () => { const username = Symbol(); const mockActions = createMockActions(); const mockErrors = createMockErrors(); - const mockCheckPrivileges = { - globally: jest.fn(async () => ({ - hasAllRequested: false, - username, - privileges: { - [mockActions.savedObject.get(type, 'create')]: false, - } - })) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockCheckPrivileges = jest.fn(async () => ({ + hasAllRequested: false, + username, + privileges: { + [mockActions.savedObject.get(type, 'create')]: false, + } + })); + const mockCheckPrivilegesDynamicallyWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); const mockRequest = Symbol(); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClientWrapper({ actions: mockActions, auditLogger: mockAuditLogger, baseClient: null, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + checkPrivilegesDynamicallyWithRequest: mockCheckPrivilegesDynamicallyWithRequest, errors: mockErrors, request: mockRequest, savedObjectTypes: [], @@ -113,8 +109,8 @@ describe(`spaces disabled`, () => { await expect(client.create(type, attributes, options)).rejects.toThrowError(mockErrors.forbiddenError); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'create')]); + expect(mockCheckPrivilegesDynamicallyWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'create')]); expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( username, @@ -138,23 +134,21 @@ describe(`spaces disabled`, () => { const mockBaseClient = { create: jest.fn().mockReturnValue(returnValue) }; - const mockCheckPrivileges = { - globally: jest.fn(async () => ({ - hasAllRequested: true, - username, - privileges: { - [mockActions.savedObject.get(type, 'create')]: true, - } - })) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockCheckPrivileges = jest.fn(async () => ({ + hasAllRequested: true, + username, + privileges: { + [mockActions.savedObject.get(type, 'create')]: true, + } + })); + const mockCheckPrivilegesDynamicallyWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); const mockRequest = Symbol(); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClientWrapper({ actions: mockActions, auditLogger: mockAuditLogger, baseClient: mockBaseClient, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + checkPrivilegesDynamicallyWithRequest: mockCheckPrivilegesDynamicallyWithRequest, errors: null, request: mockRequest, savedObjectTypes: [], @@ -166,8 +160,8 @@ describe(`spaces disabled`, () => { const result = await client.create(type, attributes, options); expect(result).toBe(returnValue); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'create')]); + expect(mockCheckPrivilegesDynamicallyWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'create')]); expect(mockBaseClient.create).toHaveBeenCalledWith(type, attributes, options); expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'create', [type], { @@ -182,12 +176,10 @@ describe(`spaces disabled`, () => { test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { const type = 'foo'; const mockErrors = createMockErrors(); - const mockCheckPrivileges = { - globally: jest.fn(async () => { - throw new Error('An actual error would happen here'); - }) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockCheckPrivileges = jest.fn(async () => { + throw new Error('An actual error would happen here'); + }); + const mockCheckPrivilegesDynamicallyWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); const mockRequest = Symbol(); const mockAuditLogger = createMockAuditLogger(); const mockActions = createMockActions(); @@ -195,7 +187,7 @@ describe(`spaces disabled`, () => { actions: mockActions, auditLogger: mockAuditLogger, baseClient: null, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + checkPrivilegesDynamicallyWithRequest: mockCheckPrivilegesDynamicallyWithRequest, errors: mockErrors, request: mockRequest, savedObjectTypes: [], @@ -204,8 +196,8 @@ describe(`spaces disabled`, () => { await expect(client.bulkCreate([{ type }])).rejects.toThrowError(mockErrors.generalError); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'bulk_create')]); + expect(mockCheckPrivilegesDynamicallyWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'bulk_create')]); expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); @@ -217,24 +209,22 @@ describe(`spaces disabled`, () => { const username = Symbol(); const mockActions = createMockActions(); const mockErrors = createMockErrors(); - const mockCheckPrivileges = { - globally: jest.fn(async () => ({ - hasAllRequested: false, - username, - privileges: { - [mockActions.savedObject.get(type1, 'bulk_create')]: false, - [mockActions.savedObject.get(type2, 'bulk_create')]: true, - } - })) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockCheckPrivileges = jest.fn(async () => ({ + hasAllRequested: false, + username, + privileges: { + [mockActions.savedObject.get(type1, 'bulk_create')]: false, + [mockActions.savedObject.get(type2, 'bulk_create')]: true, + } + })); + const mockCheckPrivilegesDynamicallyWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); const mockRequest = Symbol(); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClientWrapper({ actions: mockActions, auditLogger: mockAuditLogger, baseClient: null, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + checkPrivilegesDynamicallyWithRequest: mockCheckPrivilegesDynamicallyWithRequest, errors: mockErrors, request: mockRequest, savedObjectTypes: [], @@ -249,8 +239,8 @@ describe(`spaces disabled`, () => { await expect(client.bulkCreate(objects, options)).rejects.toThrowError(mockErrors.forbiddenError); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([ + expect(mockCheckPrivilegesDynamicallyWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges).toHaveBeenCalledWith([ mockActions.savedObject.get(type1, 'bulk_create'), mockActions.savedObject.get(type2, 'bulk_create'), ]); @@ -276,24 +266,22 @@ describe(`spaces disabled`, () => { bulkCreate: jest.fn().mockReturnValue(returnValue) }; const mockActions = createMockActions(); - const mockCheckPrivileges = { - globally: jest.fn(async () => ({ - hasAllRequested: true, - username, - privileges: { - [mockActions.savedObject.get(type1, 'bulk_create')]: true, - [mockActions.savedObject.get(type2, 'bulk_create')]: true, - } - })) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockCheckPrivileges = jest.fn(async () => ({ + hasAllRequested: true, + username, + privileges: { + [mockActions.savedObject.get(type1, 'bulk_create')]: true, + [mockActions.savedObject.get(type2, 'bulk_create')]: true, + } + })); + const mockCheckPrivilegesDynamicallyWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); const mockRequest = Symbol(); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClientWrapper({ actions: mockActions, auditLogger: mockAuditLogger, baseClient: mockBaseClient, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + checkPrivilegesDynamicallyWithRequest: mockCheckPrivilegesDynamicallyWithRequest, errors: null, request: mockRequest, savedObjectTypes: [], @@ -308,8 +296,8 @@ describe(`spaces disabled`, () => { const result = await client.bulkCreate(objects, options); expect(result).toBe(returnValue); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([ + expect(mockCheckPrivilegesDynamicallyWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges).toHaveBeenCalledWith([ mockActions.savedObject.get(type1, 'bulk_create'), mockActions.savedObject.get(type2, 'bulk_create'), ]); @@ -326,12 +314,10 @@ describe(`spaces disabled`, () => { test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { const type = 'foo'; const mockErrors = createMockErrors(); - const mockCheckPrivileges = { - globally: jest.fn(async () => { - throw new Error('An actual error would happen here'); - }) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockCheckPrivileges = jest.fn(async () => { + throw new Error('An actual error would happen here'); + }); + const mockCheckPrivilegesDynamicallyWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); const mockRequest = Symbol(); const mockAuditLogger = createMockAuditLogger(); const mockActions = createMockActions(); @@ -339,7 +325,7 @@ describe(`spaces disabled`, () => { actions: mockActions, auditLogger: mockAuditLogger, baseClient: null, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + checkPrivilegesDynamicallyWithRequest: mockCheckPrivilegesDynamicallyWithRequest, errors: mockErrors, request: mockRequest, savedObjectTypes: [], @@ -348,8 +334,8 @@ describe(`spaces disabled`, () => { await expect(client.delete(type)).rejects.toThrowError(mockErrors.generalError); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'delete')]); + expect(mockCheckPrivilegesDynamicallyWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'delete')]); expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); @@ -360,23 +346,21 @@ describe(`spaces disabled`, () => { const username = Symbol(); const mockActions = createMockActions(); const mockErrors = createMockErrors(); - const mockCheckPrivileges = { - globally: jest.fn(async () => ({ - hasAllRequested: false, - username, - privileges: { - [mockActions.savedObject.get(type, 'delete')]: false, - } - })) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockCheckPrivileges = jest.fn(async () => ({ + hasAllRequested: false, + username, + privileges: { + [mockActions.savedObject.get(type, 'delete')]: false, + } + })); + const mockCheckPrivilegesDynamicallyWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); const mockRequest = Symbol(); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClientWrapper({ actions: mockActions, auditLogger: mockAuditLogger, baseClient: null, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + checkPrivilegesDynamicallyWithRequest: mockCheckPrivilegesDynamicallyWithRequest, errors: mockErrors, request: mockRequest, savedObjectTypes: [], @@ -386,8 +370,8 @@ describe(`spaces disabled`, () => { await expect(client.delete(type, id)).rejects.toThrowError(mockErrors.forbiddenError); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'delete')]); + expect(mockCheckPrivilegesDynamicallyWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'delete')]); expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( username, @@ -410,23 +394,21 @@ describe(`spaces disabled`, () => { const mockBaseClient = { delete: jest.fn().mockReturnValue(returnValue) }; - const mockCheckPrivileges = { - globally: jest.fn(async () => ({ - hasAllRequested: true, - username, - privileges: { - [mockActions.savedObject.get(type, 'delete')]: true, - } - })) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockCheckPrivileges = jest.fn(async () => ({ + hasAllRequested: true, + username, + privileges: { + [mockActions.savedObject.get(type, 'delete')]: true, + } + })); + const mockCheckPrivilegesDynamicallyWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); const mockRequest = Symbol(); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClientWrapper({ actions: mockActions, auditLogger: mockAuditLogger, baseClient: mockBaseClient, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + checkPrivilegesDynamicallyWithRequest: mockCheckPrivilegesDynamicallyWithRequest, errors: null, request: mockRequest, savedObjectTypes: [], @@ -438,8 +420,8 @@ describe(`spaces disabled`, () => { const result = await client.delete(type, id, options); expect(result).toBe(returnValue); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'delete')]); + expect(mockCheckPrivilegesDynamicallyWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'delete')]); expect(mockBaseClient.delete).toHaveBeenCalledWith(type, id, options); expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'delete', [type], { @@ -454,12 +436,10 @@ describe(`spaces disabled`, () => { test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { const type = 'foo'; const mockErrors = createMockErrors(); - const mockCheckPrivileges = { - globally: jest.fn(async () => { - throw new Error('An actual error would happen here'); - }) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockCheckPrivileges = jest.fn(async () => { + throw new Error('An actual error would happen here'); + }); + const mockCheckPrivilegesDynamicallyWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); const mockRequest = Symbol(); const mockAuditLogger = createMockAuditLogger(); const mockActions = createMockActions(); @@ -467,7 +447,7 @@ describe(`spaces disabled`, () => { actions: mockActions, auditLogger: mockAuditLogger, baseClient: null, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + checkPrivilegesDynamicallyWithRequest: mockCheckPrivilegesDynamicallyWithRequest, errors: mockErrors, request: mockRequest, savedObjectTypes: [], @@ -476,8 +456,8 @@ describe(`spaces disabled`, () => { await expect(client.find({ type })).rejects.toThrowError(mockErrors.generalError); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'find')]); + expect(mockCheckPrivilegesDynamicallyWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'find')]); expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); @@ -488,23 +468,21 @@ describe(`spaces disabled`, () => { const username = Symbol(); const mockActions = createMockActions(); const mockErrors = createMockErrors(); - const mockCheckPrivileges = { - globally: jest.fn(async () => ({ - hasAllRequested: false, - username, - privileges: { - [mockActions.savedObject.get(type, 'find')]: false, - } - })) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockCheckPrivileges = jest.fn(async () => ({ + hasAllRequested: false, + username, + privileges: { + [mockActions.savedObject.get(type, 'find')]: false, + } + })); + const mockCheckPrivilegesDynamicallyWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); const mockRequest = Symbol(); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClientWrapper({ actions: mockActions, auditLogger: mockAuditLogger, baseClient: null, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + checkPrivilegesDynamicallyWithRequest: mockCheckPrivilegesDynamicallyWithRequest, errors: mockErrors, request: mockRequest, savedObjectTypes: [], @@ -514,8 +492,8 @@ describe(`spaces disabled`, () => { await expect(client.find(options)).rejects.toThrowError(mockErrors.forbiddenError); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'find')]); + expect(mockCheckPrivilegesDynamicallyWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'find')]); expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( username, @@ -535,17 +513,15 @@ describe(`spaces disabled`, () => { const username = Symbol(); const mockActions = createMockActions(); const mockErrors = createMockErrors(); - const mockCheckPrivileges = { - globally: jest.fn(async () => ({ - hasAllRequested: false, - username, - privileges: { - [mockActions.savedObject.get(type1, 'find')]: false, - [mockActions.savedObject.get(type2, 'find')]: true, - } - })) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockCheckPrivileges = jest.fn(async () => ({ + hasAllRequested: false, + username, + privileges: { + [mockActions.savedObject.get(type1, 'find')]: false, + [mockActions.savedObject.get(type2, 'find')]: true, + } + })); + const mockCheckPrivilegesDynamicallyWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); const mockRequest = Symbol(); const mockAuditLogger = createMockAuditLogger(); @@ -553,7 +529,7 @@ describe(`spaces disabled`, () => { actions: mockActions, auditLogger: mockAuditLogger, baseClient: null, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + checkPrivilegesDynamicallyWithRequest: mockCheckPrivilegesDynamicallyWithRequest, errors: mockErrors, request: mockRequest, savedObjectTypes: [], @@ -563,8 +539,8 @@ describe(`spaces disabled`, () => { await expect(client.find(options)).rejects.toThrowError(mockErrors.forbiddenError); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([ + expect(mockCheckPrivilegesDynamicallyWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges).toHaveBeenCalledWith([ mockActions.savedObject.get(type1, 'find'), mockActions.savedObject.get(type2, 'find') ]); @@ -589,23 +565,21 @@ describe(`spaces disabled`, () => { const mockBaseClient = { find: jest.fn().mockReturnValue(returnValue) }; - const mockCheckPrivileges = { - globally: jest.fn(async () => ({ - hasAllRequested: true, - username, - privileges: { - [mockActions.savedObject.get(type, 'find')]: true, - } - })) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockCheckPrivileges = jest.fn(async () => ({ + hasAllRequested: true, + username, + privileges: { + [mockActions.savedObject.get(type, 'find')]: true, + } + })); + const mockCheckPrivilegesDynamicallyWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); const mockRequest = Symbol(); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClientWrapper({ actions: mockActions, auditLogger: mockAuditLogger, baseClient: mockBaseClient, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + checkPrivilegesDynamicallyWithRequest: mockCheckPrivilegesDynamicallyWithRequest, errors: null, request: mockRequest, savedObjectTypes: [], @@ -616,8 +590,8 @@ describe(`spaces disabled`, () => { const result = await client.find(options); expect(result).toBe(returnValue); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'find')]); + expect(mockCheckPrivilegesDynamicallyWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'find')]); expect(mockBaseClient.find).toHaveBeenCalledWith({ type }); expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'find', [type], { @@ -630,12 +604,10 @@ describe(`spaces disabled`, () => { test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { const type = 'foo'; const mockErrors = createMockErrors(); - const mockCheckPrivileges = { - globally: jest.fn(async () => { - throw new Error('An actual error would happen here'); - }) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockCheckPrivileges = jest.fn(async () => { + throw new Error('An actual error would happen here'); + }); + const mockCheckPrivilegesDynamicallyWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); const mockRequest = Symbol(); const mockAuditLogger = createMockAuditLogger(); const mockActions = createMockActions(); @@ -643,7 +615,7 @@ describe(`spaces disabled`, () => { actions: mockActions, auditLogger: mockAuditLogger, baseClient: null, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + checkPrivilegesDynamicallyWithRequest: mockCheckPrivilegesDynamicallyWithRequest, errors: mockErrors, request: mockRequest, savedObjectTypes: [], @@ -652,8 +624,8 @@ describe(`spaces disabled`, () => { await expect(client.bulkGet([{ type }])).rejects.toThrowError(mockErrors.generalError); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'bulk_get')]); + expect(mockCheckPrivilegesDynamicallyWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'bulk_get')]); expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); @@ -665,24 +637,22 @@ describe(`spaces disabled`, () => { const username = Symbol(); const mockActions = createMockActions(); const mockErrors = createMockErrors(); - const mockCheckPrivileges = { - globally: jest.fn(async () => ({ - hasAllRequested: false, - username, - privileges: { - [mockActions.savedObject.get(type1, 'bulk_get')]: false, - [mockActions.savedObject.get(type2, 'bulk_get')]: true, - } - })) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockCheckPrivileges = jest.fn(async () => ({ + hasAllRequested: false, + username, + privileges: { + [mockActions.savedObject.get(type1, 'bulk_get')]: false, + [mockActions.savedObject.get(type2, 'bulk_get')]: true, + } + })); + const mockCheckPrivilegesDynamicallyWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); const mockRequest = Symbol(); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClientWrapper({ actions: mockActions, auditLogger: mockAuditLogger, baseClient: null, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + checkPrivilegesDynamicallyWithRequest: mockCheckPrivilegesDynamicallyWithRequest, errors: mockErrors, request: mockRequest, savedObjectTypes: [], @@ -697,8 +667,8 @@ describe(`spaces disabled`, () => { await expect(client.bulkGet(objects, options)).rejects.toThrowError(mockErrors.forbiddenError); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([ + expect(mockCheckPrivilegesDynamicallyWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges).toHaveBeenCalledWith([ mockActions.savedObject.get(type1, 'bulk_get'), mockActions.savedObject.get(type2, 'bulk_get'), ]); @@ -725,24 +695,22 @@ describe(`spaces disabled`, () => { const mockBaseClient = { bulkGet: jest.fn().mockReturnValue(returnValue) }; - const mockCheckPrivileges = { - globally: jest.fn(async () => ({ - hasAllRequested: true, - username, - privileges: { - [mockActions.savedObject.get(type1, 'bulk_get')]: true, - [mockActions.savedObject.get(type2, 'bulk_get')]: true, - } - })) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockCheckPrivileges = jest.fn(async () => ({ + hasAllRequested: true, + username, + privileges: { + [mockActions.savedObject.get(type1, 'bulk_get')]: true, + [mockActions.savedObject.get(type2, 'bulk_get')]: true, + } + })); + const mockCheckPrivilegesDynamicallyWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); const mockRequest = Symbol(); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClientWrapper({ actions: mockActions, auditLogger: mockAuditLogger, baseClient: mockBaseClient, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + checkPrivilegesDynamicallyWithRequest: mockCheckPrivilegesDynamicallyWithRequest, errors: null, request: mockRequest, savedObjectTypes: [], @@ -757,8 +725,8 @@ describe(`spaces disabled`, () => { const result = await client.bulkGet(objects, options); expect(result).toBe(returnValue); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([ + expect(mockCheckPrivilegesDynamicallyWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges).toHaveBeenCalledWith([ mockActions.savedObject.get(type1, 'bulk_get'), mockActions.savedObject.get(type2, 'bulk_get'), ]); @@ -775,12 +743,10 @@ describe(`spaces disabled`, () => { test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { const type = 'foo'; const mockErrors = createMockErrors(); - const mockCheckPrivileges = { - globally: jest.fn(async () => { - throw new Error('An actual error would happen here'); - }) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockCheckPrivileges = jest.fn(async () => { + throw new Error('An actual error would happen here'); + }); + const mockCheckPrivilegesDynamicallyWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); const mockRequest = Symbol(); const mockAuditLogger = createMockAuditLogger(); const mockActions = createMockActions(); @@ -788,7 +754,7 @@ describe(`spaces disabled`, () => { actions: mockActions, auditLogger: mockAuditLogger, baseClient: null, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + checkPrivilegesDynamicallyWithRequest: mockCheckPrivilegesDynamicallyWithRequest, errors: mockErrors, request: mockRequest, savedObjectTypes: [], @@ -797,8 +763,8 @@ describe(`spaces disabled`, () => { await expect(client.get(type)).rejects.toThrowError(mockErrors.generalError); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'get')]); + expect(mockCheckPrivilegesDynamicallyWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'get')]); expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); @@ -809,23 +775,21 @@ describe(`spaces disabled`, () => { const username = Symbol(); const mockActions = createMockActions(); const mockErrors = createMockErrors(); - const mockCheckPrivileges = { - globally: jest.fn(async () => ({ - hasAllRequested: false, - username, - privileges: { - [mockActions.savedObject.get(type, 'get')]: false, - } - })) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockCheckPrivileges = jest.fn(async () => ({ + hasAllRequested: false, + username, + privileges: { + [mockActions.savedObject.get(type, 'get')]: false, + } + })); + const mockCheckPrivilegesDynamicallyWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); const mockRequest = Symbol(); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClientWrapper({ actions: mockActions, auditLogger: mockAuditLogger, baseClient: null, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + checkPrivilegesDynamicallyWithRequest: mockCheckPrivilegesDynamicallyWithRequest, errors: mockErrors, request: mockRequest, savedObjectTypes: [], @@ -836,8 +800,8 @@ describe(`spaces disabled`, () => { await expect(client.get(type, id, options)).rejects.toThrowError(mockErrors.forbiddenError); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'get')]); + expect(mockCheckPrivilegesDynamicallyWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'get')]); expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( username, @@ -861,23 +825,21 @@ describe(`spaces disabled`, () => { const mockBaseClient = { get: jest.fn().mockReturnValue(returnValue) }; - const mockCheckPrivileges = { - globally: jest.fn(async () => ({ - hasAllRequested: true, - username, - privileges: { - [mockActions.savedObject.get(type, 'get')]: true, - } - })) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockCheckPrivileges = jest.fn(async () => ({ + hasAllRequested: true, + username, + privileges: { + [mockActions.savedObject.get(type, 'get')]: true, + } + })); + const mockCheckPrivilegesDynamicallyWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); const mockRequest = Symbol(); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClientWrapper({ actions: mockActions, auditLogger: mockAuditLogger, baseClient: mockBaseClient, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + checkPrivilegesDynamicallyWithRequest: mockCheckPrivilegesDynamicallyWithRequest, errors: null, request: mockRequest, savedObjectTypes: [], @@ -889,8 +851,8 @@ describe(`spaces disabled`, () => { const result = await client.get(type, id, options); expect(result).toBe(returnValue); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'get')]); + expect(mockCheckPrivilegesDynamicallyWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'get')]); expect(mockBaseClient.get).toHaveBeenCalledWith(type, id, options); expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'get', [type], { @@ -905,12 +867,10 @@ describe(`spaces disabled`, () => { test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { const type = 'foo'; const mockErrors = createMockErrors(); - const mockCheckPrivileges = { - globally: jest.fn(async () => { - throw new Error('An actual error would happen here'); - }) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockCheckPrivileges = jest.fn(async () => { + throw new Error('An actual error would happen here'); + }); + const mockCheckPrivilegesDynamicallyWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); const mockRequest = Symbol(); const mockAuditLogger = createMockAuditLogger(); const mockActions = createMockActions(); @@ -918,7 +878,7 @@ describe(`spaces disabled`, () => { actions: mockActions, auditLogger: mockAuditLogger, baseClient: null, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + checkPrivilegesDynamicallyWithRequest: mockCheckPrivilegesDynamicallyWithRequest, errors: mockErrors, request: mockRequest, savedObjectTypes: [], @@ -927,8 +887,8 @@ describe(`spaces disabled`, () => { await expect(client.update(type)).rejects.toThrowError(mockErrors.generalError); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'update')]); + expect(mockCheckPrivilegesDynamicallyWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'update')]); expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); @@ -939,23 +899,21 @@ describe(`spaces disabled`, () => { const username = Symbol(); const mockActions = createMockActions(); const mockErrors = createMockErrors(); - const mockCheckPrivileges = { - globally: jest.fn(async () => ({ - hasAllRequested: false, - username, - privileges: { - [mockActions.savedObject.get(type, 'update')]: false, - } - })) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + const mockCheckPrivileges = jest.fn(async () => ({ + hasAllRequested: false, + username, + privileges: { + [mockActions.savedObject.get(type, 'update')]: false, + } + })); + const mockCheckPrivilegesDynamicallyWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); const mockRequest = Symbol(); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClientWrapper({ actions: mockActions, auditLogger: mockAuditLogger, baseClient: null, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + checkPrivilegesDynamicallyWithRequest: mockCheckPrivilegesDynamicallyWithRequest, errors: mockErrors, request: mockRequest, savedObjectTypes: [], @@ -967,8 +925,8 @@ describe(`spaces disabled`, () => { await expect(client.update(type, id, attributes, options)).rejects.toThrowError(mockErrors.forbiddenError); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'update')]); + expect(mockCheckPrivilegesDynamicallyWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'update')]); expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( username, @@ -993,1124 +951,25 @@ describe(`spaces disabled`, () => { const mockBaseClient = { update: jest.fn().mockReturnValue(returnValue) }; - const mockCheckPrivileges = { - globally: jest.fn(async () => ({ - hasAllRequested: true, - username, - privileges: { - [mockActions.savedObject.get(type, 'update')]: true, - } - })) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: mockBaseClient, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, - errors: null, - request: mockRequest, - savedObjectTypes: [], - spaces: null, - }); - const id = Symbol(); - const attributes = Symbol(); - const options = Symbol(); - - const result = await client.update(type, id, attributes, options); - - expect(result).toBe(returnValue); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.globally).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'update')]); - expect(mockBaseClient.update).toHaveBeenCalledWith(type, id, attributes, options); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'update', [type], { - type, - id, - attributes, - options, - }); - }); - }); -}); - -describe(`spaces enabled`, () => { - describe('#create', () => { - test(`throws decorated GeneralError when checkPrivileges.globally rejects promise`, async () => { - const spaceId = 'space_1'; - const type = 'foo'; - const mockErrors = createMockErrors(); - const mockCheckPrivileges = { - atSpace: jest.fn(async () => { - throw new Error('An actual error would happen here'); - }) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const mockActions = createMockActions(); - const mockSpaces = { - getSpaceId: jest.fn().mockReturnValue(spaceId) - }; - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: null, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, - errors: mockErrors, - request: mockRequest, - savedObjectTypes: [], - spaces: mockSpaces, - }); - - await expect(client.create(type)).rejects.toThrowError(mockErrors.generalError); - expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [mockActions.savedObject.get(type, 'create')]); - expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when unauthorized`, async () => { - const spaceId = 'space_1'; - const type = 'foo'; - const username = Symbol(); - const mockActions = createMockActions(); - const mockErrors = createMockErrors(); - const mockCheckPrivileges = { - atSpace: jest.fn(async () => ({ - hasAllRequested: false, - username, - privileges: { - [mockActions.savedObject.get(type, 'create')]: false, - } - })) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const mockSpaces = { - getSpaceId: jest.fn().mockReturnValue(spaceId) - }; - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: null, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, - errors: mockErrors, - request: mockRequest, - savedObjectTypes: [], - spaces: mockSpaces, - }); - const attributes = Symbol(); - const options = Symbol(); - - await expect(client.create(type, attributes, options)).rejects.toThrowError(mockErrors.forbiddenError); - - expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [mockActions.savedObject.get(type, 'create')]); - expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + const mockCheckPrivileges = jest.fn(async () => ({ + hasAllRequested: true, username, - 'create', - [type], - [mockActions.savedObject.get(type, 'create')], - { - type, - attributes, - options, + privileges: { + [mockActions.savedObject.get(type, 'update')]: true, } - ); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of baseClient.create when authorized`, async () => { - const spaceId = 'space_1'; - const type = 'foo'; - const username = Symbol(); - const returnValue = Symbol(); - const mockActions = createMockActions(); - const mockBaseClient = { - create: jest.fn().mockReturnValue(returnValue) - }; - const mockCheckPrivileges = { - atSpace: jest.fn(async () => ({ - hasAllRequested: true, - username, - privileges: { - [mockActions.savedObject.get(type, 'create')]: true, - } - })) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); + })); + const mockCheckPrivilegesDynamicallyWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); const mockRequest = Symbol(); const mockAuditLogger = createMockAuditLogger(); - const mockSpaces = { - getSpaceId: jest.fn().mockReturnValue(spaceId) - }; const client = new SecureSavedObjectsClientWrapper({ actions: mockActions, auditLogger: mockAuditLogger, baseClient: mockBaseClient, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, + checkPrivilegesDynamicallyWithRequest: mockCheckPrivilegesDynamicallyWithRequest, errors: null, request: mockRequest, savedObjectTypes: [], - spaces: mockSpaces, - }); - const attributes = Symbol(); - const options = Symbol(); - - const result = await client.create(type, attributes, options); - - expect(result).toBe(returnValue); - expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [mockActions.savedObject.get(type, 'create')]); - expect(mockBaseClient.create).toHaveBeenCalledWith(type, attributes, options); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'create', [type], { - type, - attributes, - options, - }); - }); - }); - - describe('#bulkCreate', () => { - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const spaceId = 'space_1'; - const type = 'foo'; - const mockErrors = createMockErrors(); - const mockCheckPrivileges = { - atSpace: jest.fn(async () => { - throw new Error('An actual error would happen here'); - }) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const mockActions = createMockActions(); - const mockSpaces = { - getSpaceId: jest.fn().mockReturnValue(spaceId) - }; - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: null, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, - errors: mockErrors, - request: mockRequest, - savedObjectTypes: [], - spaces: mockSpaces, - }); - - await expect(client.bulkCreate([{ type }])).rejects.toThrowError(mockErrors.generalError); - - expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [mockActions.savedObject.get(type, 'bulk_create')]); - expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when unauthorized`, async () => { - const spaceId = 'space_1'; - const type1 = 'foo'; - const type2 = 'bar'; - const username = Symbol(); - const mockActions = createMockActions(); - const mockErrors = createMockErrors(); - const mockCheckPrivileges = { - atSpace: jest.fn(async () => ({ - hasAllRequested: false, - username, - privileges: { - [mockActions.savedObject.get(type1, 'bulk_create')]: false, - [mockActions.savedObject.get(type2, 'bulk_create')]: true, - } - })) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const mockSpaces = { - getSpaceId: jest.fn().mockReturnValue(spaceId) - }; - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: null, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, - errors: mockErrors, - request: mockRequest, - savedObjectTypes: [], - spaces: mockSpaces, - }); - const objects = [ - { type: type1 }, - { type: type1 }, - { type: type2 }, - ]; - const options = Symbol(); - - await expect(client.bulkCreate(objects, options)).rejects.toThrowError(mockErrors.forbiddenError); - - expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [ - mockActions.savedObject.get(type1, 'bulk_create'), - mockActions.savedObject.get(type2, 'bulk_create'), - ]); - expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'bulk_create', - [type1, type2], - [mockActions.savedObject.get(type1, 'bulk_create')], - { - objects, - options, - } - ); - }); - - test(`returns result of baseClient.bulkCreate when authorized`, async () => { - const spaceId = 'space_1'; - const username = Symbol(); - const type1 = 'foo'; - const type2 = 'bar'; - const returnValue = Symbol(); - const mockBaseClient = { - bulkCreate: jest.fn().mockReturnValue(returnValue) - }; - const mockActions = createMockActions(); - const mockCheckPrivileges = { - atSpace: jest.fn(async () => ({ - hasAllRequested: true, - username, - privileges: { - [mockActions.savedObject.get(type1, 'bulk_create')]: true, - [mockActions.savedObject.get(type2, 'bulk_create')]: true, - } - })) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const mockSpaces = { - getSpaceId: jest.fn().mockReturnValue(spaceId) - }; - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: mockBaseClient, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, - errors: null, - request: mockRequest, - savedObjectTypes: [], - spaces: mockSpaces, - }); - const objects = [ - { type: type1, otherThing: 'sup' }, - { type: type2, otherThing: 'everyone' }, - ]; - const options = Symbol(); - - const result = await client.bulkCreate(objects, options); - - expect(result).toBe(returnValue); - expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [ - mockActions.savedObject.get(type1, 'bulk_create'), - mockActions.savedObject.get(type2, 'bulk_create'), - ]); - expect(mockBaseClient.bulkCreate).toHaveBeenCalledWith(objects, options); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'bulk_create', [type1, type2], { - objects, - options, - }); - }); - }); - - describe('#delete', () => { - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const spaceId = 'space_1'; - const type = 'foo'; - const mockErrors = createMockErrors(); - const mockCheckPrivileges = { - atSpace: jest.fn(async () => { - throw new Error('An actual error would happen here'); - }) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const mockActions = createMockActions(); - const mockSpaces = { - getSpaceId: jest.fn().mockReturnValue(spaceId) - }; - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: null, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, - errors: mockErrors, - request: mockRequest, - savedObjectTypes: [], - spaces: mockSpaces, - }); - - await expect(client.delete(type)).rejects.toThrowError(mockErrors.generalError); - - expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [mockActions.savedObject.get(type, 'delete')]); - expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when unauthorized`, async () => { - const spaceId = 'space_1'; - const type = 'foo'; - const username = Symbol(); - const mockActions = createMockActions(); - const mockErrors = createMockErrors(); - const mockCheckPrivileges = { - atSpace: jest.fn(async () => ({ - hasAllRequested: false, - username, - privileges: { - [mockActions.savedObject.get(type, 'delete')]: false, - } - })) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const mockSpaces = { - getSpaceId: jest.fn().mockReturnValue(spaceId) - }; - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: null, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, - errors: mockErrors, - request: mockRequest, - savedObjectTypes: [], - spaces: mockSpaces, - }); - const id = Symbol(); - - await expect(client.delete(type, id)).rejects.toThrowError(mockErrors.forbiddenError); - - expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [mockActions.savedObject.get(type, 'delete')]); - expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'delete', - [type], - [mockActions.savedObject.get(type, 'delete')], - { - type, - id, - } - ); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of internalRepository.delete when authorized`, async () => { - const spaceId = 'space_1'; - const type = 'foo'; - const username = Symbol(); - const returnValue = Symbol(); - const mockActions = createMockActions(); - const mockBaseClient = { - delete: jest.fn().mockReturnValue(returnValue) - }; - const mockCheckPrivileges = { - atSpace: jest.fn(async () => ({ - hasAllRequested: true, - username, - privileges: { - [mockActions.savedObject.get(type, 'delete')]: true, - } - })) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const mockSpaces = { - getSpaceId: jest.fn().mockReturnValue(spaceId) - }; - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: mockBaseClient, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, - errors: null, - request: mockRequest, - savedObjectTypes: [], - spaces: mockSpaces, - }); - const id = Symbol(); - const options = Symbol(); - - const result = await client.delete(type, id, options); - - expect(result).toBe(returnValue); - expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [mockActions.savedObject.get(type, 'delete')]); - expect(mockBaseClient.delete).toHaveBeenCalledWith(type, id, options); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'delete', [type], { - type, - id, - options, - }); - }); - }); - - describe('#find', () => { - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const spaceId = 'space_1'; - const type = 'foo'; - const mockErrors = createMockErrors(); - const mockCheckPrivileges = { - atSpace: jest.fn(async () => { - throw new Error('An actual error would happen here'); - }) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const mockActions = createMockActions(); - const mockSpaces = { - getSpaceId: jest.fn().mockReturnValue(spaceId) - }; - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: null, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, - errors: mockErrors, - request: mockRequest, - savedObjectTypes: [], - spaces: mockSpaces, - }); - - await expect(client.find({ type })).rejects.toThrowError(mockErrors.generalError); - - expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [mockActions.savedObject.get(type, 'find')]); - expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when type's singular and unauthorized`, async () => { - const spaceId = 'space_1'; - const type = 'foo'; - const username = Symbol(); - const mockActions = createMockActions(); - const mockErrors = createMockErrors(); - const mockCheckPrivileges = { - atSpace: jest.fn(async () => ({ - hasAllRequested: false, - username, - privileges: { - [mockActions.savedObject.get(type, 'find')]: false, - } - })) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const mockSpaces = { - getSpaceId: jest.fn().mockReturnValue(spaceId) - }; - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: null, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, - errors: mockErrors, - request: mockRequest, - savedObjectTypes: [], - spaces: mockSpaces, - }); - const options = { type }; - - await expect(client.find(options)).rejects.toThrowError(mockErrors.forbiddenError); - - expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [mockActions.savedObject.get(type, 'find')]); - expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'find', - [type], - [mockActions.savedObject.get(type, 'find')], - { - options - } - ); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when type's an array and unauthorized`, async () => { - const spaceId = 'space_1'; - const type1 = 'foo'; - const type2 = 'bar'; - const username = Symbol(); - const mockActions = createMockActions(); - const mockErrors = createMockErrors(); - const mockCheckPrivileges = { - atSpace: jest.fn(async () => ({ - hasAllRequested: false, - username, - privileges: { - [mockActions.savedObject.get(type1, 'find')]: false, - [mockActions.savedObject.get(type2, 'find')]: true, - } - })) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - - const mockAuditLogger = createMockAuditLogger(); - const mockSpaces = { - getSpaceId: jest.fn().mockReturnValue(spaceId) - }; - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: null, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, - errors: mockErrors, - request: mockRequest, - savedObjectTypes: [], - spaces: mockSpaces, - }); - const options = { type: [type1, type2] }; - - await expect(client.find(options)).rejects.toThrowError(mockErrors.forbiddenError); - - expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [ - mockActions.savedObject.get(type1, 'find'), - mockActions.savedObject.get(type2, 'find') - ]); - expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'find', - [type1, type2], - [mockActions.savedObject.get(type1, 'find')], - { - options - } - ); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of baseClient.find when authorized`, async () => { - const spaceId = 'space_1'; - const type = 'foo'; - const username = Symbol(); - const returnValue = Symbol(); - const mockActions = createMockActions(); - const mockBaseClient = { - find: jest.fn().mockReturnValue(returnValue) - }; - const mockCheckPrivileges = { - atSpace: jest.fn(async () => ({ - hasAllRequested: true, - username, - privileges: { - [mockActions.savedObject.get(type, 'find')]: true, - } - })) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const mockSpaces = { - getSpaceId: jest.fn().mockReturnValue(spaceId) - }; - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: mockBaseClient, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, - errors: null, - request: mockRequest, - savedObjectTypes: [], - spaces: mockSpaces, - }); - const options = { type }; - - const result = await client.find(options); - - expect(result).toBe(returnValue); - expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [mockActions.savedObject.get(type, 'find')]); - expect(mockBaseClient.find).toHaveBeenCalledWith({ type }); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'find', [type], { - options, - }); - }); - }); - - describe('#bulkGet', () => { - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const spaceId = 'space_1'; - const type = 'foo'; - const mockErrors = createMockErrors(); - const mockCheckPrivileges = { - atSpace: jest.fn(async () => { - throw new Error('An actual error would happen here'); - }) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const mockActions = createMockActions(); - const mockSpaces = { - getSpaceId: jest.fn().mockReturnValue(spaceId) - }; - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: null, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, - errors: mockErrors, - request: mockRequest, - savedObjectTypes: [], - spaces: mockSpaces, - }); - - await expect(client.bulkGet([{ type }])).rejects.toThrowError(mockErrors.generalError); - - expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [mockActions.savedObject.get(type, 'bulk_get')]); - expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when unauthorized`, async () => { - const spaceId = 'space_1'; - const type1 = 'foo'; - const type2 = 'bar'; - const username = Symbol(); - const mockActions = createMockActions(); - const mockErrors = createMockErrors(); - const mockCheckPrivileges = { - atSpace: jest.fn(async () => ({ - hasAllRequested: false, - username, - privileges: { - [mockActions.savedObject.get(type1, 'bulk_get')]: false, - [mockActions.savedObject.get(type2, 'bulk_get')]: true, - } - })) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const mockSpaces = { - getSpaceId: jest.fn().mockReturnValue(spaceId) - }; - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: null, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, - errors: mockErrors, - request: mockRequest, - savedObjectTypes: [], - spaces: mockSpaces, - }); - const objects = [ - { type: type1 }, - { type: type1 }, - { type: type2 }, - ]; - const options = Symbol(); - - await expect(client.bulkGet(objects, options)).rejects.toThrowError(mockErrors.forbiddenError); - - expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [ - mockActions.savedObject.get(type1, 'bulk_get'), - mockActions.savedObject.get(type2, 'bulk_get'), - ]); - expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'bulk_get', - [type1, type2], - [mockActions.savedObject.get(type1, 'bulk_get')], - { - objects, - options, - } - ); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of baseClient.bulkGet when authorized`, async () => { - const spaceId = 'space_1'; - const type1 = 'foo'; - const type2 = 'bar'; - const username = Symbol(); - const returnValue = Symbol(); - const mockActions = createMockActions(); - const mockBaseClient = { - bulkGet: jest.fn().mockReturnValue(returnValue) - }; - const mockCheckPrivileges = { - atSpace: jest.fn(async () => ({ - hasAllRequested: true, - username, - privileges: { - [mockActions.savedObject.get(type1, 'bulk_get')]: true, - [mockActions.savedObject.get(type2, 'bulk_get')]: true, - } - })) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const mockSpaces = { - getSpaceId: jest.fn().mockReturnValue(spaceId) - }; - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: mockBaseClient, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, - errors: null, - request: mockRequest, - savedObjectTypes: [], - spaces: mockSpaces, - }); - const objects = [ - { type: type1, id: 'foo-id' }, - { type: type2, id: 'bar-id' }, - ]; - const options = Symbol(); - - const result = await client.bulkGet(objects, options); - - expect(result).toBe(returnValue); - expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [ - mockActions.savedObject.get(type1, 'bulk_get'), - mockActions.savedObject.get(type2, 'bulk_get'), - ]); - expect(mockBaseClient.bulkGet).toHaveBeenCalledWith(objects, options); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'bulk_get', [type1, type2], { - objects, - options, - }); - }); - }); - - describe('#get', () => { - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const spaceId = 'space_1'; - const type = 'foo'; - const mockErrors = createMockErrors(); - const mockCheckPrivileges = { - atSpace: jest.fn(async () => { - throw new Error('An actual error would happen here'); - }) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const mockActions = createMockActions(); - const mockSpaces = { - getSpaceId: jest.fn().mockReturnValue(spaceId) - }; - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: null, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, - errors: mockErrors, - request: mockRequest, - savedObjectTypes: [], - spaces: mockSpaces, - }); - - await expect(client.get(type)).rejects.toThrowError(mockErrors.generalError); - - expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [mockActions.savedObject.get(type, 'get')]); - expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when unauthorized`, async () => { - const spaceId = 'space_1'; - const type = 'foo'; - const username = Symbol(); - const mockActions = createMockActions(); - const mockErrors = createMockErrors(); - const mockCheckPrivileges = { - atSpace: jest.fn(async () => ({ - hasAllRequested: false, - username, - privileges: { - [mockActions.savedObject.get(type, 'get')]: false, - } - })) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const mockSpaces = { - getSpaceId: jest.fn().mockReturnValue(spaceId) - }; - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: null, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, - errors: mockErrors, - request: mockRequest, - savedObjectTypes: [], - spaces: mockSpaces, - }); - const id = Symbol(); - const options = Symbol(); - - await expect(client.get(type, id, options)).rejects.toThrowError(mockErrors.forbiddenError); - - expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [mockActions.savedObject.get(type, 'get')]); - expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'get', - [type], - [mockActions.savedObject.get(type, 'get')], - { - type, - id, - options, - } - ); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of baseClient.get when authorized`, async () => { - const spaceId = 'space_1'; - const type = 'foo'; - const username = Symbol(); - const returnValue = Symbol(); - const mockActions = createMockActions(); - const mockBaseClient = { - get: jest.fn().mockReturnValue(returnValue) - }; - const mockCheckPrivileges = { - atSpace: jest.fn(async () => ({ - hasAllRequested: true, - username, - privileges: { - [mockActions.savedObject.get(type, 'get')]: true, - } - })) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const mockSpaces = { - getSpaceId: jest.fn().mockReturnValue(spaceId) - }; - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: mockBaseClient, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, - errors: null, - request: mockRequest, - savedObjectTypes: [], - spaces: mockSpaces, - }); - const id = Symbol(); - const options = Symbol(); - - const result = await client.get(type, id, options); - - expect(result).toBe(returnValue); - expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [mockActions.savedObject.get(type, 'get')]); - expect(mockBaseClient.get).toHaveBeenCalledWith(type, id, options); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'get', [type], { - type, - id, - options - }); - }); - }); - - describe('#update', () => { - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const spaceId = 'space_1'; - const type = 'foo'; - const mockErrors = createMockErrors(); - const mockCheckPrivileges = { - atSpace: jest.fn(async () => { - throw new Error('An actual error would happen here'); - }) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const mockActions = createMockActions(); - const mockSpaces = { - getSpaceId: jest.fn().mockReturnValue(spaceId) - }; - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: null, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, - errors: mockErrors, - request: mockRequest, - savedObjectTypes: [], - spaces: mockSpaces, - }); - - await expect(client.update(type)).rejects.toThrowError(mockErrors.generalError); - - expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [mockActions.savedObject.get(type, 'update')]); - expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when unauthorized`, async () => { - const spaceId = 'space_1'; - const type = 'foo'; - const username = Symbol(); - const mockActions = createMockActions(); - const mockErrors = createMockErrors(); - const mockCheckPrivileges = { - atSpace: jest.fn(async () => ({ - hasAllRequested: false, - username, - privileges: { - [mockActions.savedObject.get(type, 'update')]: false, - } - })) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const mockSpaces = { - getSpaceId: jest.fn().mockReturnValue(spaceId) - }; - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: null, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, - errors: mockErrors, - request: mockRequest, - savedObjectTypes: [], - spaces: mockSpaces, - }); - const id = Symbol(); - const attributes = Symbol(); - const options = Symbol(); - - await expect(client.update(type, id, attributes, options)).rejects.toThrowError(mockErrors.forbiddenError); - - expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [mockActions.savedObject.get(type, 'update')]); - expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - username, - 'update', - [type], - [mockActions.savedObject.get(type, 'update')], - { - type, - id, - attributes, - options, - } - ); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of baseClient.update when authorized`, async () => { - const spaceId = 'space_1'; - const type = 'foo'; - const username = Symbol(); - const returnValue = Symbol(); - const mockActions = createMockActions(); - const mockBaseClient = { - update: jest.fn().mockReturnValue(returnValue) - }; - const mockCheckPrivileges = { - atSpace: jest.fn(async () => ({ - hasAllRequested: true, - username, - privileges: { - [mockActions.savedObject.get(type, 'update')]: true, - } - })) - }; - const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges); - const mockRequest = Symbol(); - const mockAuditLogger = createMockAuditLogger(); - const mockSpaces = { - getSpaceId: jest.fn().mockReturnValue(spaceId) - }; - const client = new SecureSavedObjectsClientWrapper({ - actions: mockActions, - auditLogger: mockAuditLogger, - baseClient: mockBaseClient, - checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest, - errors: null, - request: mockRequest, - savedObjectTypes: [], - spaces: mockSpaces, + spaces: null, }); const id = Symbol(); const attributes = Symbol(); @@ -2119,9 +978,8 @@ describe(`spaces enabled`, () => { const result = await client.update(type, id, attributes, options); expect(result).toBe(returnValue); - expect(mockSpaces.getSpaceId).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(mockRequest); - expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, [mockActions.savedObject.get(type, 'update')]); + expect(mockCheckPrivilegesDynamicallyWithRequest).toHaveBeenCalledWith(mockRequest); + expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.savedObject.get(type, 'update')]); expect(mockBaseClient.update).toHaveBeenCalledWith(type, id, attributes, options); expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'update', [type], { diff --git a/x-pack/plugins/spaces/public/views/nav_control/nav_control.tsx b/x-pack/plugins/spaces/public/views/nav_control/nav_control.tsx index 2110addfdd793..180d9114ee40b 100644 --- a/x-pack/plugins/spaces/public/views/nav_control/nav_control.tsx +++ b/x-pack/plugins/spaces/public/views/nav_control/nav_control.tsx @@ -12,8 +12,8 @@ import { NavControlPopover } from 'plugins/spaces/views/nav_control/nav_control_ // @ts-ignore import { PathProvider } from 'plugins/xpack_main/services/path'; import React from 'react'; -import ReactDOM from 'react-dom'; import { render, unmountComponentAtNode } from 'react-dom'; +import ReactDOM from 'react-dom'; import { NavControlSide } from 'ui/chrome/directives/header_global_nav'; // @ts-ignore import { uiModules } from 'ui/modules'; diff --git a/x-pack/plugins/xpack_main/server/lib/feature_registry/feature_registry.ts b/x-pack/plugins/xpack_main/server/lib/feature_registry/feature_registry.ts index 25f7e7249a031..fcf9babd3bbd6 100644 --- a/x-pack/plugins/xpack_main/server/lib/feature_registry/feature_registry.ts +++ b/x-pack/plugins/xpack_main/server/lib/feature_registry/feature_registry.ts @@ -8,6 +8,9 @@ import { IconType } from '@elastic/eui'; import _ from 'lodash'; export interface FeaturePrivilegeDefinition { + metadata?: { + tooltip?: string; + }; api?: string[]; app: string[]; savedObject: { diff --git a/x-pack/plugins/xpack_main/server/lib/setup_xpack_main.js b/x-pack/plugins/xpack_main/server/lib/setup_xpack_main.js index 927ec185b432f..51f31505545b7 100644 --- a/x-pack/plugins/xpack_main/server/lib/setup_xpack_main.js +++ b/x-pack/plugins/xpack_main/server/lib/setup_xpack_main.js @@ -23,7 +23,6 @@ export function setupXPackMain(server) { server.expose('info', info); server.expose('createXPackInfo', (options) => new XPackInfo(server, options)); - server.expose('registerFeature', registerFeature); server.expose('getFeatures', getFeatures); server.ext('onPreResponse', (request, h) => injectXPackInfoSignature(info, request, h));